summaryrefslogtreecommitdiff
path: root/resources/external/game_coordinator.py
diff options
context:
space:
mode:
authorAnson Bridges <bridges.anson@gmail.com>2025-08-11 22:42:00 -0700
committerAnson Bridges <bridges.anson@gmail.com>2025-08-11 22:42:00 -0700
commitd558a9add0e183219a7a9ff482807bdcd677e21a (patch)
tree49e454649a4b45ce02c419894109de55f7f2e465 /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.py175
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