diff --git a/.gitignore b/.gitignore index 22fae77..9964e89 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ token.txt +state.txt *.sgf *.png diff --git a/__pycache__/sgfengine.cpython-39.pyc b/__pycache__/sgfengine.cpython-39.pyc index 201d8df..b9f878f 100644 Binary files a/__pycache__/sgfengine.cpython-39.pyc and b/__pycache__/sgfengine.cpython-39.pyc differ diff --git a/rengobot.py b/rengobot.py index d1a6129..eedc7e2 100644 --- a/rengobot.py +++ b/rengobot.py @@ -13,6 +13,11 @@ from discord.ext import commands # https://pypi.org/project/discord-py-slash-command/ bot = commands.Bot(command_prefix='$', help_command=None) +format="%Y_%m_%d_%H_%M_%S_%f" +min_time_player= timedelta(minutes=1) +time_to_skip= timedelta(seconds=30) # in queue games, how much time to wait for the next move +min_players = 2 + # People who can start and resign games :O # Later we might replace this with checking for a role. admins=[ 756220448118669463, # Young Sun @@ -23,24 +28,25 @@ admins=[ 756220448118669463, # Young Sun 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() +# The state is a list of tuples (channel_id, "queue"/"random", last_players, last_times, [player_id]) -#@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 : play a move. For example, `$play Q16`\n\n'+ + '$play : play a move. For example, `$play Q16`\n'+ + '$edit : if you make a mistake in your move, you have 5 minutes to correct it with this command\n\n'+ + '$sgf: get the sgf file of the game\n'+ + '$board: shows the current board\n'+ '$queue: get the queue of players\n\n'+ + '$newgame : starts a game in this channel (admin only!)\n'+ '$resign : resigns the game in this channel (admin only!)' ) @@ -53,7 +59,6 @@ async def play(ctx, arg): # 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: @@ -62,30 +67,54 @@ async def play(ctx, arg): i= filter_state[0] - if state[i][1] == queue and user.id not in state[i][4]: + 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]: + if state[i][1] == "queue" and len(state[i][4]) 0 and state[i][2][-1] == user.id: + await ctx.send("No two consecutive moves per player!") + return + + for j in range(len(state[i][2])): + if (state[i][2][j] == user.id and + datetime.now() - datetime.strptime(state[i][3][j],format) < min_time_player): + await ctx.send("At most one move per player per day!") + return + + + if state[i][3] != [] and datetime.now()-datetime.strptime(state[i][3][-1],format) timedelta(minutes=5): + await ctx.send("You cannot edit this move!") + 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)] + 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 + + try: + sgfengine.play_move(str(channel_id), arg, user.name, True) + except ValueError as e: + await ctx.send(str(e)) + return + + 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.name)) + else: + await ctx.send(file=file) + + with open("state.txt", "w") as f: f.write(repr(state)) + +@bot.command() +async def board(ctx): + channel_id= ctx.channel.id + user = ctx.author + + with open("state.txt") as f: state = ast.literal_eval(f.read()) + + 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] + + 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.name)) + else: + await ctx.send(file=file) @bot.command() async def join(ctx): @@ -104,7 +202,6 @@ async def join(ctx): # 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: @@ -126,7 +223,6 @@ async def join(ctx): await ctx.send("User joined!") with open("state.txt", "w") as f: f.write(repr(state)) - f.close() @bot.command() async def queue(ctx): @@ -134,7 +230,6 @@ async def queue(ctx): # 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: @@ -169,7 +264,6 @@ async def leave(ctx): # 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: @@ -191,7 +285,6 @@ async def leave(ctx): 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): @@ -208,14 +301,13 @@ async def newgame(ctx, arg): # 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, [])) + state.append((ctx.channel.id, arg, [], [], [])) file = discord.File(str(ctx.channel.id)+".png") if arg=="queue": @@ -224,7 +316,6 @@ async def newgame(ctx, arg): await ctx.send(file=file, content="A new game has started! Play with `$play `") with open("state.txt", "w") as f: f.write(repr(state)) - f.close() @bot.command() async def resign(ctx, arg): @@ -240,7 +331,6 @@ async def resign(ctx, arg): 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 @@ -253,7 +343,6 @@ async def resign(ctx, arg): 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() @@ -263,21 +352,37 @@ async def background_task(): 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() + #print(state) #TODO find who has to move, skip players accordingly, notify if any has to move + for i in range(len(state)): + if state[i][3] == [] or state[i][1]!="queue": continue - with open("state.txt") as f: f.write(repr(state)) - f.close() + channel_id= state[i][0] + channel= bot.get_channel(channel_id) + + last_time= datetime.strptime(state[i][3][-1],format) + time_left= last_time + time_to_skip-datetime.now() + if time_left < time_to_skip * 2/3: # Probably remove? Depends on how passive aggressive it is + next_user = await bot.fetch_user(state[i][4][0]) + await channel.send("{}'s turn!".format(next_user.mention))#, time_left.total_seconds()/3600) ) + if time_left < timedelta(): + state[i][3][-1]= datetime.strftime(datetime.now(),format) + state[i][2][-1]= None + user_id= state[i][4][0] + state[i][4].pop(0) + state[i][4].append(user_id) + next_player=(await bot.fetch_user(state[i][4][0])) + await channel.send(content="{}'s turn! ⭐".format(next_player.mention)) + #Should I output the board too? + + with open("state.txt", "w") as f: f.write(repr(state)) + await asyncio.sleep(10) except ConnectionResetError: print("Connection error") bot.loop.create_task(background_task()) bot.run(token) - diff --git a/sgfengine.py b/sgfengine.py index 11a4165..62e1cba 100644 --- a/sgfengine.py +++ b/sgfengine.py @@ -16,7 +16,7 @@ def new_game(channel_id): # Could be an illegal move, or maybe I don't understand the message # outputs to .png -def play_move(channel_id, messagestr, player): +def play_move(channel_id, messagestr, player, overwrite=False): thecol= ord(messagestr[0].lower()) - ord('a') if thecol>8: thecol-=1 # Go boards don't have an I column!! @@ -29,6 +29,12 @@ def play_move(channel_id, messagestr, player): koban=None node= game.get_last_node() board, moves= sgf_moves.get_setup_and_moves(game) + if overwrite: + node2= node.parent + node.delete() + node= node2 + moves= moves[:-1] + for (colour, (row, col)) in moves: koban=board.play(row,col,colour) @@ -39,18 +45,16 @@ def play_move(channel_id, messagestr, player): board2= board.copy() try: - board2.play(therow, thecol, colour) + koban2=board2.play(therow, thecol, colour) except ValueError as e: raise ValueError("Illegal move1!") - if ascii_boards.render_board(board)== ascii_boards.render_board(board2): + if board2.get(therow,thecol) == None: 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]) + if koban2 is not None: node2.set("SQ", [koban2]) 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") diff --git a/state.txt b/state.txt index 1c59b50..614665e 100644 --- a/state.txt +++ b/state.txt @@ -1 +1 @@ -[(881537306073387091, 'queue', None, None, [477895596141707264])] \ No newline at end of file +[(870604751354613770, 'random', [477895596141707264], ['2021_08_30_19_03_29_444139'], [])] \ No newline at end of file