second iteration
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| token.txt | ||||
| state.txt | ||||
| *.sgf | ||||
| *.png | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										171
									
								
								rengobot.py
									
									
									
									
									
								
							
							
						
						
									
										171
									
								
								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 <move>: play a move. For example, `$play Q16`\n\n'+ | ||||
|             '$play <move>: play a move. For example, `$play Q16`\n'+ | ||||
|             '$edit <move>: 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 <queue/random>: starts a game in this channel (admin only!)\n'+ | ||||
|             '$resign <B/W>: 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]) <min_players: | ||||
|         await ctx.send("Waiting for {} more players to join".format(min_players-len(state[i][4]))) | ||||
|         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 state[i][1] == "random": | ||||
|         assert( len(state[i][2]) == len(state[i][3])) | ||||
|  | ||||
|         if len(state[i][2])>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(seconds=4): | ||||
|         return #silent error | ||||
|  | ||||
|     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 | ||||
|  | ||||
|     # 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) | ||||
|     # move registered, let's do the other things | ||||
|     state[i][2].append(user.id) | ||||
|     state[i][3].append(datetime.now().strftime(format)) | ||||
|  | ||||
|     if state[i][1] == "queue": | ||||
|         state[i][4].pop(0) | ||||
|         state[i][4].append(user.id) | ||||
|  | ||||
|     file = discord.File(str(ctx.channel.id)+".png") | ||||
|     if state[i][1]=="queue": | ||||
| @@ -95,7 +124,76 @@ async def play(ctx, arg): | ||||
|         await ctx.send(file=file) | ||||
|  | ||||
|     with open("state.txt", "w") as f: f.write(repr(state)) | ||||
|     f.close() | ||||
|  | ||||
| @bot.command() | ||||
| async def edit(ctx, arg): #literally play but with less things | ||||
|     # It should wait until the queue has 4 players or so | ||||
|     channel_id= ctx.channel.id | ||||
|     user = ctx.author | ||||
|  | ||||
|     # lowest effort serialization | ||||
|     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] | ||||
|  | ||||
|     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 | ||||
|  | ||||
|     if state[i][2][-1] != user.id or 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 <move>`") | ||||
|  | ||||
|     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) | ||||
|  | ||||
|   | ||||
							
								
								
									
										16
									
								
								sgfengine.py
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								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 <channel_id>.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") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 ReneCareenium
					ReneCareenium