159 lines
5.1 KiB
Python
159 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import threading
|
|
import sys
|
|
from typing import Optional
|
|
|
|
from .net import ServerListener, ServerClient
|
|
from .protocol import (
|
|
parse_client_line,
|
|
ClientHello,
|
|
ClientQueue,
|
|
ClientMove,
|
|
UnknownCommand,
|
|
format_hello,
|
|
)
|
|
from .match import Matchmaker
|
|
from .session import SessionManager
|
|
|
|
|
|
class ServerApp:
|
|
def __init__(self, port: int) -> None:
|
|
self.port = port
|
|
self.clients: list[ServerClient] = []
|
|
self.listener = ServerListener(port)
|
|
self.matchmaker = Matchmaker()
|
|
self.sessions = SessionManager()
|
|
|
|
def run(self) -> None:
|
|
def on_new_client(client: ServerClient) -> None:
|
|
self.clients.append(client)
|
|
|
|
t = threading.Thread(target=self._handle_client, args=(client,), daemon=True)
|
|
t.start()
|
|
|
|
self.listener.accept_loop(on_new_client)
|
|
|
|
def _handle_client(self, client: ServerClient) -> None:
|
|
# Expect HELLO
|
|
line = client.read_line()
|
|
if line is None:
|
|
client.close()
|
|
return
|
|
cmd = parse_client_line(line)
|
|
if isinstance(cmd, ClientHello):
|
|
desired_desc = cmd.description or ""
|
|
|
|
# Enforce unique descriptions: if name is already taken, add suffix
|
|
existing_names = {c.description for c in self.clients if c.description is not None}
|
|
final_desc = desired_desc
|
|
if final_desc in existing_names:
|
|
i = 2
|
|
while f"{desired_desc}#{i}" in existing_names:
|
|
i += 1
|
|
final_desc = f"{desired_desc}#{i}"
|
|
|
|
client.description = final_desc
|
|
client.send_line(format_hello("QuartoServer"))
|
|
|
|
print(
|
|
f"[SERVER] Client #{client.id} connected from {client.addr}, "
|
|
f"description='{client.description}' (requested '{desired_desc}')",
|
|
file=sys.stderr,
|
|
)
|
|
else:
|
|
client.close()
|
|
return
|
|
try:
|
|
while True:
|
|
line = client.read_line()
|
|
if line is None:
|
|
# disconnect
|
|
self._handle_disconnect(client)
|
|
break
|
|
cmd = parse_client_line(line)
|
|
if isinstance(cmd, ClientQueue):
|
|
self._handle_queue(client)
|
|
elif isinstance(cmd, ClientMove):
|
|
self._handle_move(client, cmd)
|
|
elif isinstance(cmd, UnknownCommand):
|
|
# Silently ignore unknown commands
|
|
continue
|
|
else:
|
|
# Unknown type; ignore
|
|
continue
|
|
finally:
|
|
client.close()
|
|
|
|
def _handle_queue(self, client: ServerClient) -> None:
|
|
# If already in a game, ignore
|
|
if client.in_game:
|
|
return
|
|
self.matchmaker.enqueue(client)
|
|
pair = self.matchmaker.dequeue_pair()
|
|
if pair is None:
|
|
return
|
|
c1, c2 = pair
|
|
c1.in_game = True
|
|
c2.in_game = True
|
|
session = self.sessions.create_session(c1, c2)
|
|
session.send_newgame()
|
|
|
|
# LOG: new game created
|
|
print(
|
|
f"[SERVER] New game #{session.id} created: "
|
|
f"P1=#{c1.id} '{session.player1_name}' vs "
|
|
f"P2=#{c2.id} '{session.player2_name}'",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
def _handle_move(self, client: ServerClient, move: ClientMove) -> None:
|
|
session = self.sessions.session_for_client(client)
|
|
if session is None:
|
|
# Not in a game; ignore
|
|
return
|
|
|
|
# LOG: raw MOVE from client
|
|
print(
|
|
f"[SERVER] Game #{session.id} - Client #{client.id} "
|
|
f"('{session.player1_name if client is session.player1 else session.player2_name}') "
|
|
f"sends MOVE: square={move.square}, m_value={move.m_value}",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
session.handle_client_move(client, move)
|
|
# If session ended, clean up
|
|
if session.state.game_over:
|
|
self.sessions.end_session(session)
|
|
session.player1.in_game = False
|
|
session.player2.in_game = False
|
|
|
|
def _handle_disconnect(self, client: ServerClient) -> None:
|
|
# If in a game, inform session
|
|
session = self.sessions.session_for_client(client)
|
|
if session is not None:
|
|
session.handle_disconnect(client)
|
|
self.sessions.end_session(session)
|
|
session.player1.in_game = False
|
|
session.player2.in_game = False
|
|
# If in lobby, remove from queue
|
|
self.matchmaker.remove(client)
|
|
if client in self.clients:
|
|
self.clients.remove(client)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Quarto tournament server")
|
|
parser.add_argument("--port", type=int, default=5000, help="Listen port")
|
|
args = parser.parse_args()
|
|
|
|
app = ServerApp(args.port)
|
|
try:
|
|
app.run()
|
|
except KeyboardInterrupt:
|
|
print("Server shutting down", file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|