almost ready for production

This commit is contained in:
ReneCareenium 2021-08-30 10:10:21 +02:00
parent 746aabc010
commit 74567e1135
6 changed files with 371 additions and 1 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
token.txt
*.sgf
*.png

View File

@ -1,3 +1,8 @@
A discord bot for playing rengo games!
WIP!
# Dependencies
- sgf-render
- python-discord
- python-sgfmill
Make sure to run the bot in an environment with read/write permissions

Binary file not shown.

283
rengobot.py Normal file
View File

@ -0,0 +1,283 @@
import os
import ast
import time
from datetime import datetime, timedelta
import asyncio
import sgfengine
import discord
from discord.ext import commands
# We don't use fancy slash commands here. It seems there is this library for python but it looks a bit more involved.
# https://pypi.org/project/discord-py-slash-command/
bot = commands.Bot(command_prefix='$', help_command=None)
# People who can start and resign games :O
# Later we might replace this with checking for a role.
admins=[ 756220448118669463, # Young Sun
732403731810877450, # Yeonwoo
145294584077877249, # Mrchance
477895596141707264 # René
]
with open("token.txt") as f:
token = f.readlines()[0] # Get your own token and put it in token.txt
f.close()
#client= discord.Client()
#@bot.command()
#async def test(ctx, arg1, arg2):
# await ctx.send('You passed {} and {}'.format(arg1, arg2))
@bot.command()
async def help(ctx):
print(ctx.message.content)
await ctx.send(
'$help : shows this help\n\n'+
'$join : join the game in this channel\n'+
'$leave: leave the game in this channel\n'+
'$play <move>: play a move. For example, `$play Q16`\n\n'+
'$sgf: get the sgf file of the game\n'+
'$queue: get the queue of players\n\n'+
'$newgame <queue/random>: starts a game in this channel (admin only!)\n'+
'$resign <B/W>: resigns the game in this channel (admin only!)'
)
# ctx has guild, message, author, send, and channel (?)
@bot.command()
async def play(ctx, arg):
channel_id= ctx.channel.id
user = ctx.author
# lowest effort serialization
with open("state.txt") as f: state = ast.literal_eval(f.read())
f.close()
filter_state= [i for i in range(len(state)) if state[i][0] == channel_id]
if not filter_state:
await ctx.send("No active game in this channel!")
return
i= filter_state[0]
if state[i][1] == queue and user.id not in state[i][4]:
await ctx.send("Player hasn't joined yet! Join us with `$join`")
return
if state[i][1] == queue and user.id!= state[i][4][0]:
await ctx.send("It is not your turn yet!")
return
legal_moves=[chr(col+ord('A')-1)+str(row) for col in range(1,21) if col!=9 for row in range(1,20)]
if arg not in legal_moves:
await ctx.send("I don't understand the move! Please input it in the format `$play Q16`")
return
# TODO test for last player to move and time in free games
# perhaps we could switch it to max one move per player every 5 turns, put the last few players in state[i][4]
try:
sgfengine.play_move(str(channel_id), arg, user.name)
except ValueError as e:
await ctx.send(str(e))
return
state[i][4].pop(0)
state[i][4].append(user.id)
file = discord.File(str(ctx.channel.id)+".png")
if state[i][1]=="queue":
next_player=(await bot.fetch_user(state[i][4][0]))
await ctx.send(file=file, content="{}'s turn! ⭐".format(next_player.mention))
else:
await ctx.send(file=file)
with open("state.txt", "w") as f: f.write(repr(state))
f.close()
@bot.command()
async def join(ctx):
channel_id= ctx.channel.id
user = ctx.author
# lowest effort serialization
with open("state.txt") as f: state = ast.literal_eval(f.read())
f.close()
filter_state= [i for i in range(len(state)) if state[i][0] == channel_id]
if not filter_state:
await ctx.send("No active game in this channel!")
return
i= filter_state[0]
if user.id in state[i][4]:
await ctx.send("Player already in this game!")
return
if state[i][1] != "queue":
await ctx.send("This game has no queue! Play whenever you want :P")
return
state[i][4].append(user.id)
await ctx.send("User joined!")
with open("state.txt", "w") as f: f.write(repr(state))
f.close()
@bot.command()
async def queue(ctx):
channel_id= ctx.channel.id
# lowest effort serialization
with open("state.txt") as f: state = ast.literal_eval(f.read())
f.close()
filter_state= [i for i in range(len(state)) if state[i][0] == channel_id]
if not filter_state:
await ctx.send("No active game in this channel!")
return
i= filter_state[0]
if state[i][1] != "queue":
await ctx.send("This game has no queue! Play whenever you want :P")
return
output= "Player list:\n"
for i, player_id in enumerate(state[i][4]):
player_name=(await bot.fetch_user(player_id)).name
output+=str(i+1)+". "+ player_name+"\n"
if not state[i][4]:
output+="Nobody yet! Join us with `$join`"
await ctx.send(output)
@bot.command()
async def sgf(ctx):
file = discord.File(str(ctx.channel.id)+".sgf")
await ctx.send(file=file)
@bot.command()
async def leave(ctx):
channel_id= ctx.channel.id
user = ctx.author
# lowest effort serialization
with open("state.txt") as f: state = ast.literal_eval(f.read())
f.close()
filter_state= [i for i in range(len(state)) if state[i][0] == channel_id]
if not filter_state:
await ctx.send("No active game in this channel!")
return
i= filter_state[0]
if user.id not in state[i][4]:
await ctx.send("Player not in this game!")
return
if state[i][1] != "queue":
await ctx.send("This game has no queue! No need to leave!")
return
state[i][4].remove(user.id)
await ctx.send("User left :(")
with open("state.txt", "w") as f: f.write(repr(state))
f.close()
@bot.command()
async def newgame(ctx, arg):
channel_id= ctx.channel.id
user = ctx.author
if user.id not in admins:
await ctx.send("You don't have permissions for this!")
return
if arg not in ["queue", "random"]:
await ctx.send("Unrecognized game type! Please try `$newgame <queue/random>")
return
# lowest effort serialization
with open("state.txt") as f: state = ast.literal_eval(f.read())
f.close()
if ctx.channel.id in [ ch for (ch,_,_,_,_) in state]:
await ctx.send("A game is already active in this channel!")
return
sgfengine.new_game(str(ctx.channel.id))
state.append((ctx.channel.id, arg, None, None, []))
file = discord.File(str(ctx.channel.id)+".png")
if arg=="queue":
await ctx.send(file=file, content="A new game has started! Join with `$join`")
else:
await ctx.send(file=file, content="A new game has started! Play with `$play <move>`")
with open("state.txt", "w") as f: f.write(repr(state))
f.close()
@bot.command()
async def resign(ctx, arg):
channel_id= ctx.channel.id
user = ctx.author
if user.id not in admins:
await ctx.send("You don't have permissions for this!")
return
if arg not in ["W","B"]:
await ctx.send("Unrecognized colour! Please try `$resign <B/W>` to resign as Black/White")
return
with open("state.txt") as f: state = ast.literal_eval(f.read())
f.close()
now=datetime.now()
file_name= "rengo_"+now.strftime("%Y_%m_%d_%H_%M_%S_")+ctx.channel.name+".sgf" #remove the hour minute and second later
sgfengine.resign(str(channel_id), arg, file_name)
file = discord.File(file_name)
await ctx.send(file=file, content=("Black" if arg=="W" else "White")+" wins!")
state = [s for s in state if s[0]!=channel_id]
with open("state.txt", "w") as f: f.write(repr(state))
f.close()
async def background_task():
await bot.wait_until_ready()
print("bot ready!")
guild=discord.utils.get(bot.guilds, name="Awesome Baduk")
while not bot.is_closed():
try:
print("ping")
await asyncio.sleep(1000)
# lowest effort serialization
with open("state.txt") as f: state = ast.literal_eval(f.read())
f.close()
#TODO find who has to move, skip players accordingly, notify if any has to move
with open("state.txt") as f: f.write(repr(state))
f.close()
except ConnectionResetError:
print("Connection error")
bot.loop.create_task(background_task())
bot.run(token)

78
sgfengine.py Normal file
View File

@ -0,0 +1,78 @@
# Needs sgf-render and sgfmill
# https://mjw.woodcraft.me.uk/sgfmill/doc/1.1.1/properties.html?highlight=list%20properties
import os
from sgfmill import sgf, boards, sgf_moves, ascii_boards
# This file only deals with the png and sgf side of things. To manage users etc go to the main file.
def new_game(channel_id):
game= sgf.Sgf_game(19)
with open (channel_id+".sgf", "wb") as f:
f.write(game.serialise())
f.close()
os.system("sgf-render --style fancy -o "+channel_id+".png -n last "+channel_id+".sgf")
# Could be an illegal move, or maybe I don't understand the message
# outputs to <channel_id>.png
def play_move(channel_id, messagestr, player):
thecol= ord(messagestr[0].lower()) - ord('a')
if thecol>8: thecol-=1 # Go boards don't have an I column!!
therow= int(messagestr[1:]) - 1
with open(channel_id+".sgf","rb") as f:
game = sgf.Sgf_game.from_bytes(f.read())
f.close()
koban=None
node= game.get_last_node()
board, moves= sgf_moves.get_setup_and_moves(game)
for (colour, (row, col)) in moves:
koban=board.play(row,col,colour)
if (therow, thecol)==koban:
raise ValueError("Ko banned move!")
colour = "w" if ("B" in node.properties()) else "b"
board2= board.copy()
try:
board2.play(therow, thecol, colour)
except ValueError as e:
raise ValueError("Illegal move1!")
if ascii_boards.render_board(board)== ascii_boards.render_board(board2):
raise ValueError("Illegal move2!")
#print(moves)
node2= node.new_child()
node2.set(("B" if colour =='b' else "W"), (therow,thecol))
if koban is not None: node2.set("SQ", [koban])
node2.set("CR", [(therow, thecol)])
node2.set("C", player) # I think this would be fun for the review
if node.has_property("CR"): node.unset("CR")
if node.has_property("SQ"): node.unset("SQ")
with open (channel_id+".sgf", "wb") as f:
f.write(game.serialise())
f.close()
os.system("sgf-render --style fancy -o "+channel_id+".png -n last "+channel_id+".sgf")
# colour is "B" if black resigns, "W" if white resigns
def resign(channel_id, colour, file_name):
with open(channel_id+".sgf","rb") as f:
game = sgf.Sgf_game.from_bytes(f.read())
f.close()
node= game.root
node.set("RE", ("B" if colour=="W" else "W")+"+R")
with open (file_name, "wb") as f:
f.write(game.serialise())
f.close()
os.remove(channel_id+".sgf")

1
state.txt Normal file
View File

@ -0,0 +1 @@
[(881537306073387091, 'queue', None, None, [477895596141707264])]