diff options
| author | Anson Bridges <bridges.anson@gmail.com> | 2025-08-11 22:42:00 -0700 |
|---|---|---|
| committer | Anson Bridges <bridges.anson@gmail.com> | 2025-08-11 22:42:00 -0700 |
| commit | d558a9add0e183219a7a9ff482807bdcd677e21a (patch) | |
| tree | 49e454649a4b45ce02c419894109de55f7f2e465 /resources/external/game_coordinator.py | |
Initialize repo from local files
Diffstat (limited to 'resources/external/game_coordinator.py')
| -rw-r--r-- | resources/external/game_coordinator.py | 175 |
1 files changed, 175 insertions, 0 deletions
diff --git a/resources/external/game_coordinator.py b/resources/external/game_coordinator.py new file mode 100644 index 0000000..6904cb0 --- /dev/null +++ b/resources/external/game_coordinator.py @@ -0,0 +1,175 @@ +#!/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 <IP String>] [-port <valid 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))
\ No newline at end of file |
