diff --git a/app/chess_sim/game_board.py b/app/chess_sim/game_board.py index 934f60f..9f0caa8 100644 --- a/app/chess_sim/game_board.py +++ b/app/chess_sim/game_board.py @@ -158,7 +158,7 @@ class ChessBoard: def outcome(self) -> Outcome: if self.is_checkmate(): - return Outcome.from_color(self.turn) + return Outcome.from_color(self.turn.opposite) if self.is_stalemate(): return Outcome.DRAW if self.is_seventyfive_moves(): diff --git a/app/sockets/socket.py b/app/sockets/socket.py index 608ba05..a5bf26f 100644 --- a/app/sockets/socket.py +++ b/app/sockets/socket.py @@ -158,6 +158,57 @@ def _game_over_reason(board: ChessBoard) -> str: return "game over" +_TERMINATION_MAP = { + "checkmate": "checkmate", + "resignation": "resignation", + "timeout": "timeout", + "stalemate": "draw", + "draw agreed": "draw", + "75-move rule": "draw", + "fivefold repetition": "draw", + "opponent disconnected": "other", + "game over": "other", +} + + +def _save_game(room: GameRoom, winner_color: Optional[str], reason: str = "other") -> Optional[int]: + """Persist the finished game and return the saved game_id (or None on error).""" + if room.saved_game_id is not None: + return room.saved_game_id + try: + white_id = next( + (uid for sid, uid in [(room.p1_sid, room.p1_user_id), (room.p2_sid, room.p2_user_id)] + if room.color_by_sid.get(sid) == "w"), + room.p1_user_id, + ) + black_id = next( + (uid for sid, uid in [(room.p1_sid, room.p1_user_id), (room.p2_sid, room.p2_user_id)] + if room.color_by_sid.get(sid) == "b"), + room.p2_user_id, + ) + if black_id is None: + return None + termination = _TERMINATION_MAP.get(reason, "other") + room.ended_at = _timestamp_now() + game_id = save_finished_game( + white_player_id=white_id, + black_player_id=black_id, + final_fen=room.board.to_fen(), + termination=termination, + termination_detail=reason, + winner_color=winner_color, + move_history=room.move_history, + time_mode=room.time_mode, + started_at=room.started_at, + ended_at=room.ended_at, + ) + room.saved_game_id = game_id + return game_id + except Exception as exc: + print(f"[save_game] failed: {exc}") + return None + + def _emit_game_over(room: GameRoom, reason: str) -> None: room.completed = True room.game_active = False @@ -165,23 +216,30 @@ def _emit_game_over(room: GameRoom, reason: str) -> None: outcome = room.board.outcome() p1_result = "draw" p2_result = "draw" + winner_color_str: Optional[str] = None if outcome == Outcome.WHITE_WIN: - winner_color = "w" - else: + winner_color = "w" + winner_color_str = "white" + elif outcome == Outcome.BLACK_WIN: winner_color = "b" - if room.color_by_sid.get(room.p1_sid) == winner_color: - p1_result = "win" - p2_result = "loss" + winner_color_str = "black" else: - p1_result = "loss" - p2_result = "win" + winner_color = None + if winner_color is not None: + if room.color_by_sid.get(room.p1_sid) == winner_color: + p1_result = "win" + p2_result = "loss" + else: + p1_result = "loss" + p2_result = "win" + game_id = _save_game(room, winner_color_str, reason) - emit("game_over", {"result": p1_result, "reason": reason}, to=room.p1_sid) + emit("game_over", {"result": p1_result, "reason": reason, "game_id": game_id}, to=room.p1_sid) if room.p2_sid: - emit("game_over", {"result": p2_result, "reason": reason}, to=room.p2_sid) + emit("game_over", {"result": p2_result, "reason": reason, "game_id": game_id}, to=room.p2_sid) def _emit_timeout(room: GameRoom, timed_out_color: str) -> None: @@ -193,9 +251,12 @@ def _emit_timeout(room: GameRoom, timed_out_color: str) -> None: p1_result = "loss" if p1_color == timed_out_color else "win" p2_result = "win" if p1_result == "loss" else "loss" - sIO.emit("game_over", {"result": p1_result, "reason": reason}, to=room.p1_sid) + winner_color_str = "white" if timed_out_color == "b" else "black" + game_id = _save_game(room, winner_color_str, reason) + + sIO.emit("game_over", {"result": p1_result, "reason": reason, "game_id": game_id}, to=room.p1_sid) if room.p2_sid: - sIO.emit("game_over", {"result": p2_result, "reason": reason}, to=room.p2_sid) + sIO.emit("game_over", {"result": p2_result, "reason": reason, "game_id": game_id}, to=room.p2_sid) def _start_game_if_ready(room: GameRoom) -> None: @@ -221,6 +282,7 @@ def _start_game_if_ready(room: GameRoom) -> None: room.active_since = time.monotonic() room.game_active = True room.completed = False + room.started_at = _timestamp_now() p1_payload = { "play_as": p1_color, @@ -385,6 +447,7 @@ def on_join_code_game(payload): _cleanup_room(previous.code) room.p2_sid = sid + room.p2_user_id = current_user.id room.p2_name = current_user.username room.ready.setdefault(room.p1_sid, False) room.ready[sid] = False @@ -553,9 +616,13 @@ def on_request_resign(payload): reason = "resignation" other_sid = room.p2_sid if sid == room.p1_sid else room.p1_sid - emit("game_over", {"result": "loss", "reason": reason}, to=sid) + resigning_color = room.color_by_sid.get(sid) + winner_color_str = "black" if resigning_color == "w" else "white" + game_id = _save_game(room, winner_color_str, reason) + + emit("game_over", {"result": "loss", "reason": reason, "game_id": game_id}, to=sid) if other_sid: - emit("game_over", {"result": "win", "reason": reason}, to=other_sid) + emit("game_over", {"result": "win", "reason": reason, "game_id": game_id}, to=other_sid) @sIO.on("request_draw") @@ -577,7 +644,8 @@ def on_request_draw(payload): if payload.get("accepted") is True: room.completed = True room.game_active = False - emit("game_over", {"result": "draw", "reason": "draw agreed"}, to=room.code) + game_id = _save_game(room, None, "draw agreed") + sIO.emit("game_over", {"result": "draw", "reason": "draw agreed", "game_id": game_id}, to=room.code) return other_sid = room.p2_sid if sid == room.p1_sid else room.p1_sid