#!/usr/bin/env python """ WebSocket game coordinator for ATC. Provides list of currently open games for server-browser purposes, and provides connection management and game state forwarding between players. syntax: python game_coordinator.py [-ip ] [-port ] e.g.: python game_coordinator.py -ip 127.0.0.1 -port 25565 """ import asyncio import json import secrets import sys import random from websockets.asyncio.server import serve DEFAULT_PORT = 8181 DEFAULT_IP = "127.0.0.1" LOBBY_STATES = [ "LOBBY", "UNDERWAY", "FINISHED" ] GAME_STATES = [ "SETUP", "ACTION", "PLACEMENT" ] GAMES = {} # placeholder #GAMES = {"1b32b1" : {"game_name" : "Game 1", "lobby_state" : "LOBBY", "current_players" : 1, "max_players" : 4, "private" : False}, # "ab1b27" : {"game_name" : "Patrick's Game", "lobby_state" : "UNDERWAY", "current_players" : 3, "max_players" : 3, "private" : True}, # "7a7df8" : {"game_name" : "New Jersey ATC", "lobby_state" : "FINISHED", "current_players" : 2, "max_players" : 2, "private" : False}} async def select_valid_color(game, color_id, color_id_alt): taken_ids = set(GAMES[game_id]["players"].keys()) color_okay = True for player_color in taken_ids: if color_id == player_color: color_okay = False break if color_okay: return color_id color_okay = True for player_color in taken_ids: if color_id_alt == player_color: color_okay = False break if color_okay: return color_id_alt available_colors = list(set(range(10)) ^ taken_ids) return random.choice(available_colors) # Send active games for server browser purposes async def send_game_list(websocket): games = {"type" : "game_list", "games" : []} for game_id, game_info in GAMES.items(): current_game_info = {"id" : game_id, "game_name" : game_info["game_name"], "state" : game_info["lobby_state"], "current_players" : len(game_info["players"]), "max_players" : game_info["max_players"], "private" : game_info["private"]} games["games"].append( current_game_info ) await websocket.send(json.dumps(games)) async for message in websocket: # wait for acknowledgment print(json.loads(message)) return # end of connection (closes automatically) async def lobby(websocket, game, player_id): async for message in websocket: event = json.loads(message) if event["type"] == "game_control": # message forwarding elif event["type"] == "request": response = {"type" : "request", "data" : event["request_fields"] } await game["players"][ int(event["source"]) ]["socket"].send( json.dumps(response) ) elif event["type"] == "request_response": response = {"type" : "request_response", "data" : event["requested_data"] } await game["players"][ int(event["destination"]) ]["socket"].send( json.dumps(response) ) elif event["type"] == "broadcast": message = {"type" : "broadcast", "payload" : event["payload"]} for player in game["players"]: if player != player_id: await player["socket"].send(json.dumps(message)) async def join_game(websocket, event): for required_field in ["game_id", "username", "color_id", "color_id_alt"]: if required_field not in event: response = {"type" : "error", "message" : f"Missing field: '{required_field}'."} await websocket.send(json.dumps(response)) return # close connection game_id = event["game_id"] if game_id not in GAMES: response = {"type" : "error", "message" : f"Game '{game_id}' does not exist."} await websocket.send(json.dumps(response)) return # close connection if len(GAMES[game_id]["players"]) >= GAMES[game_id]["max_players"]: response = {"type" : "error", "message" : f"Server full."} await websocket.send(json.dumps(response)) return # close connection player_color = select_valid_color(GAMES[game_id], int(event["color_id"]), int(event["color_id_alt"])) response = {"type" : "JOIN_OK", "game_id" : game_id, "color_id" : player_color} await websocket.send(json.dumps(response)) try: await lobby(websocket, GAMES[game_id], player_color) finally: del GAMES[game_id] async def create_game(websocket, event): for required_field in ["game_name", "username", "color_id", "private", "password", "max_players"]: if required_field not in event: response = {"type" : "error", "message" : f"Missing field: '{required_field}'."} await websocket.send(json.dumps(response)) return # close connection new_game = { "game_name" : event["game_name"], "lobby_state" : "LOBBY", "is_board_generated" : False, "max_players" : event["max_players"], "players" : {} } event["color_id"] = int(event["color_id"]) host_player = {"socket" : websocket, "is_host" : True, "username" : event["username"] } new_game["host_id"] = host_ new_game["players"][event["color_id"]] = host_player game_id = secrets.token_urlsafe(12) GAMES[game_id] = new_game response = {"type" : "HOST_OK", "game_id" : game_id} await websocket.send(json.dumps(response)) try: await lobby(websocket, GAMES[game_id], event["color_id"]) finally: del GAMES[game_id] async def new_connection_handler(websocket): print("Client connected.") message = await websocket.recv() event = json.loads(message) if event["type"] == "list_open_games": print("List of games requested.") await send_game_list(websocket) elif event["type"] == "create_game": await create_game(websocket) elif event["type"] == "join_game": await join_game(websocket, event) elif event["type"] == "watch_game": await watch_game(websocket, event) async def main(ip, port): async with serve(new_connection_handler, ip, port) as server: print("serving...") await server.serve_forever() if __name__ == "__main__": IP = DEFAULT_IP PORT = DEFAULT_PORT args = sys.argv[1:] for arg_i in range(len(args)): if args[arg_i] == "-ip" and arg_i < (len(args)-1): arg_i += 1 IP = args[arg_i] if args[arg_i] == "-port" and arg_i < (len(args)-1): arg_i += 1 try: PORT = int(args[arg_i]) except: print("Port must be an integer.") quit() asyncio.run(main(IP, PORT))