|
import asyncio |
|
import websockets |
|
import argparse |
|
from argparse import RawTextHelpFormatter |
|
import json |
|
from subprocess import Popen, PIPE |
|
import time |
|
import sys |
|
import logging |
|
import traceback |
|
from datetime import datetime |
|
|
|
|
|
async def create_room_and_play_online(args): |
|
logging.basicConfig(filename='agent_online.log', level=logging.INFO) |
|
games_played = 0 |
|
white_wins = 0 |
|
black_wins = 0 |
|
|
|
while True: |
|
try: |
|
if args.num_games is not None and games_played >= args.num_games: |
|
print(f"Finished playing {games_played} games. Exiting...") |
|
return |
|
print("Establishing web socket connection to:", args.websocket) |
|
async with websockets.connect(args.websocket) as websocket: |
|
|
|
#await websocket.send(json.dumps({'action': 'get_rooms'})) |
|
#response = await websocket.recv() |
|
#print("Open rooms: ", response) |
|
|
|
# Create or join a room on the server |
|
num_attempts = 10 |
|
for i in range(num_attempts): |
|
await websocket.send(json.dumps({ |
|
'action': 'join_room' if args.join_room else 'create_room', |
|
'data': args.room_name |
|
})) |
|
|
|
# Check if the room was successfully created |
|
response = await websocket.recv() |
|
print(response) |
|
if response.startswith("Successfully"): |
|
break |
|
else: |
|
if i == (num_attempts - 1): |
|
print("Reached max room creation/joining attempts. Quitting...") |
|
return |
|
print("Failed to join/create the room. Trying again...") |
|
time.sleep(1) |
|
|
|
# Wait for first game state to be sent (someone joined) |
|
recheck_rate = 1 |
|
while True: |
|
try: |
|
# double the recheck_rate until we are checking once every 10 minutes |
|
recheck_rate = min(recheck_rate * 2, 60 * 10) |
|
response = await asyncio.wait_for(websocket.recv(), recheck_rate) |
|
print("Received response", response) |
|
game_state = json.loads(response) |
|
break |
|
# periodically check if our room still exists |
|
except asyncio.exceptions.TimeoutError as e: |
|
await websocket.send(json.dumps({'action': 'get_rooms'})) |
|
response = json.loads(await websocket.recv()) |
|
# If it doesnt, raise an error and restart the connection |
|
if args.room_name not in response: |
|
raise ConnectionError(f"Our room {args.room_name} is missing!") |
|
|
|
# If we are white, wait for black to make one move so it is our turn |
|
# We do this because the agent script protocol expects to have the first turn when it loads the initial board |
|
if game_state['you'] == 'white': |
|
game_state = json.loads(await websocket.recv()) |
|
|
|
# White the initial board to a file, since the agent script expects to load it this way |
|
game_file_name = 'online_game.txt' |
|
print(f"Received initial game state\n{game_state['game']}\nPlayer: {game_state['you']}") |
|
with open(game_file_name, "w") as file: |
|
file.write(game_state['game']) |
|
|
|
# Start the agent process and get the first move |
|
formatted_agent_call = args.agent_call.format(game_file_name, 'B' if game_state['you'] == 'black' else 'W') |
|
agent_call_args = formatted_agent_call.split(' ') |
|
print("Starting process", formatted_agent_call) |
|
with Popen(agent_call_args, stdout=PIPE, stdin=PIPE, stderr=PIPE, text=True) as agent_process: |
|
|
|
# Play opening |
|
print("Process started, getting first move.") |
|
agent_process.stdout.flush() |
|
agent_move = agent_process.stdout.readline()[:-1] |
|
print("First move:", agent_move) |
|
await websocket.send(json.dumps({ |
|
'action': 'move', |
|
'data': agent_move |
|
})) |
|
|
|
# Main play loop |
|
while True: |
|
|
|
# Wait for the opponents move |
|
try: |
|
response = await asyncio.wait_for(websocket.recv(), args.wait_time) |
|
print("Received response", response) |
|
if response == "Opponent left.": |
|
break |
|
game_state = json.loads(response) |
|
# periodically check if our room still exists |
|
except asyncio.exceptions.TimeoutError as e: |
|
raise ConnectionError(f"Opponent took longer than {args.wait_time}s to make a move. " |
|
f"Qutting...") |
|
|
|
# Check and handle win/loss |
|
if game_state['winner'] != 'none': |
|
games_played += 1 |
|
if game_state['winner'] == game_state['you']: |
|
print("Agent won") |
|
if game_state['you'] == 'white': |
|
white_wins += 1 |
|
else: |
|
black_wins += 1 |
|
else: |
|
print("Agent lost") |
|
logging.info(f'{datetime.now().strftime("[%d/%m/%y %H:%M:%S]")}\n' |
|
f'agent_call=\"{args.agent_call}\" ' |
|
f'room_name=\"{args.room_name}\"\n' |
|
f'Games played: {games_played}, ' |
|
f'wins: {white_wins + black_wins}, ' |
|
f'white wins: {white_wins}, ' |
|
f'black_wins: {black_wins}') |
|
break |
|
|
|
# Check if its your turn |
|
if game_state['current_player'] != game_state['you']: |
|
continue |
|
print("Received move:", game_state['last_move']) |
|
|
|
# Make a move |
|
agent_process.stdout.flush() |
|
agent_process.stdin.write(game_state['last_move'] + "\n") |
|
agent_process.stdin.flush() |
|
print('Sent move to agent, awaiting response...') |
|
agent_move = agent_process.stdout.readline()[:-1] |
|
print("Sending agents move:", agent_move) |
|
await websocket.send(json.dumps({ |
|
'action': 'move', |
|
'data': agent_move |
|
})) |
|
except: |
|
print("Unexpected error:", sys.exc_info()[0]) |
|
traceback.print_exc() |
|
|
|
|
|
if __name__ == "__main__": |
|
parser = argparse.ArgumentParser( |
|
description="Run your Konane Agent online at coryefird.com/konane\n" |
|
"If your agent worked with drivercheck.pl, then it should work with this script as well.\n" |
|
"Win/loss results of your agent will be output to a log file \"agent_online.log\"\n" |
|
"Some examples of how you can run this script with your agent:\n" |
|
" An agent that runs through a bash script:\n" |
|
" python run_agent_online.py \"./monte_carlo_agent.sh {} {}\" \"Fight Monte Carlo!\"\n" |
|
" A python agent that accepts additional parameters:\n" |
|
" python run_agent_online.py \"python run_agent.py {} {} alpha_beta --max_time 2\" \"my_room\"\n" |
|
" An agent that joins another agents room and plays 10 games\n" |
|
" python run_agent_online.py \"python run_agent.py {} {} " |
|
"random_player \"[G5][Beginner][0] Random\" --join_room --num_games 10\n", |
|
formatter_class=RawTextHelpFormatter |
|
) |
|
parser.add_argument("agent_call", type=str, |
|
help="String to run your agent, as you would type it on the command line. " |
|
"You must leave two sets of curly braces for arguments to be substituded: '{} {}'. " |
|
"The first curly brace will become the game state file name, and the second becomes a " |
|
"character to specify which player the agent is, i.e. 'B' or 'W'." |
|
) |
|
parser.add_argument("room_name", type=str, help="Room name to use on the server") |
|
parser.set_defaults(join_room=False) |
|
parser.add_argument('--join_room', dest='join_room', action='store_true', |
|
help="Join an existing room instead of creating one." |
|
) |
|
parser.add_argument('--num_games', type=int, default=None, |
|
help="Terminate the agent after completing a number of games. Will play indefinitely otherwise." |
|
) |
|
parser.add_argument('--wait_time', type=int, default=300, |
|
help="How many seconds to wait for the opponent to make a move before exiting." |
|
) |
|
parser.add_argument("--websocket", type=str, default="ws://3.141.93.154:5555", |
|
help='Websocket address for the server. The default should connect to coryefird.com/konane.' |
|
) |
|
args = parser.parse_args() |
|
|
|
asyncio.get_event_loop().run_until_complete(create_room_and_play_online(args)) |