Save finished games and add history views
This commit is contained in:
+150
-22
@@ -2,6 +2,7 @@ import random
|
||||
import string
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
from typing import Optional
|
||||
|
||||
@@ -10,6 +11,7 @@ from flask_login import current_user
|
||||
from flask import request
|
||||
from flask_socketio import emit, join_room, leave_room
|
||||
from app import sIO
|
||||
from app.models.game import save_finished_game
|
||||
from .types import validate_client_event
|
||||
|
||||
|
||||
@@ -17,10 +19,12 @@ from .types import validate_client_event
|
||||
class GameRoom:
|
||||
code: str
|
||||
p1_sid: str
|
||||
p1_user_id: int
|
||||
p1_name: str
|
||||
p1_pref: str
|
||||
time_mode: str
|
||||
p2_sid: Optional[str] = None
|
||||
p2_user_id: Optional[int] = None
|
||||
p2_name: Optional[str] = None
|
||||
ready: dict[str, bool] = field(default_factory=dict)
|
||||
board: chess.Board = field(default_factory=chess.Board)
|
||||
@@ -32,6 +36,10 @@ class GameRoom:
|
||||
active_since: Optional[float] = None
|
||||
game_active: bool = False
|
||||
completed: bool = False
|
||||
move_history: list[str] = field(default_factory=list)
|
||||
started_at: Optional[str] = None
|
||||
ended_at: Optional[str] = None
|
||||
saved_game_id: Optional[int] = None
|
||||
|
||||
|
||||
games_by_code: dict[str, GameRoom] = {}
|
||||
@@ -66,6 +74,10 @@ def _parse_time_mode(time_mode: str) -> tuple[int, int]:
|
||||
return minutes * 60 * 1000, increment_seconds * 1000
|
||||
|
||||
|
||||
def _timestamp_now() -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _room_for_sid(sid: str) -> Optional[GameRoom]:
|
||||
code = code_by_sid.get(sid)
|
||||
if not code:
|
||||
@@ -139,26 +151,82 @@ def _game_over_reason(board: chess.Board) -> str:
|
||||
return "game over"
|
||||
|
||||
|
||||
def _termination_key(reason: str) -> str:
|
||||
if reason == "checkmate":
|
||||
return "checkmate"
|
||||
if reason == "resignation":
|
||||
return "resignation"
|
||||
if reason == "timeout":
|
||||
return "timeout"
|
||||
if reason in {
|
||||
"stalemate",
|
||||
"insufficient material",
|
||||
"75-move rule",
|
||||
"fivefold repetition",
|
||||
"draw agreed",
|
||||
}:
|
||||
return "draw"
|
||||
return "other"
|
||||
|
||||
|
||||
def _winner_color_value(room: GameRoom, winner_sid: Optional[str]) -> Optional[str]:
|
||||
if winner_sid is None:
|
||||
return "draw"
|
||||
|
||||
winner = room.color_by_sid.get(winner_sid)
|
||||
if winner == "w":
|
||||
return "white"
|
||||
if winner == "b":
|
||||
return "black"
|
||||
return None
|
||||
|
||||
|
||||
def _result_for_sid(room: GameRoom, sid: str, winner_color: Optional[str]) -> str:
|
||||
if winner_color == "draw" or winner_color is None:
|
||||
return "draw"
|
||||
|
||||
player_color = room.color_by_sid.get(sid)
|
||||
return "win" if player_color == winner_color else "loss"
|
||||
|
||||
|
||||
def _save_completed_game(room: GameRoom, reason: str, winner_color: Optional[str]) -> Optional[int]:
|
||||
if room.saved_game_id is not None:
|
||||
return room.saved_game_id
|
||||
if not room.started_at or room.p2_user_id is None:
|
||||
return None
|
||||
|
||||
room.ended_at = room.ended_at or _timestamp_now()
|
||||
room.saved_game_id = save_finished_game(
|
||||
white_player_id=room.p1_user_id if room.color_by_sid.get(room.p1_sid) == "w" else room.p2_user_id,
|
||||
black_player_id=room.p1_user_id if room.color_by_sid.get(room.p1_sid) == "b" else room.p2_user_id,
|
||||
final_fen=room.board.fen(),
|
||||
termination=_termination_key(reason),
|
||||
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,
|
||||
)
|
||||
return room.saved_game_id
|
||||
|
||||
|
||||
def _emit_game_over(room: GameRoom, reason: str) -> None:
|
||||
room.completed = True
|
||||
room.game_active = False
|
||||
|
||||
outcome = room.board.outcome(claim_draw=True)
|
||||
p1_result = "draw"
|
||||
p2_result = "draw"
|
||||
|
||||
winner_color = "draw"
|
||||
if outcome and outcome.winner is not None:
|
||||
winner_color = "w" if outcome.winner == chess.WHITE else "b"
|
||||
if room.color_by_sid.get(room.p1_sid) == winner_color:
|
||||
p1_result = "win"
|
||||
p2_result = "loss"
|
||||
else:
|
||||
p1_result = "loss"
|
||||
p2_result = "win"
|
||||
|
||||
emit("game_over", {"result": p1_result, "reason": reason}, to=room.p1_sid)
|
||||
game_id = _save_completed_game(room, reason, "white" if winner_color == "w" else "black" if winner_color == "b" else "draw")
|
||||
p1_result = _result_for_sid(room, room.p1_sid, winner_color)
|
||||
|
||||
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)
|
||||
p2_result = _result_for_sid(room, room.p2_sid, winner_color)
|
||||
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:
|
||||
@@ -166,13 +234,18 @@ def _emit_timeout(room: GameRoom, timed_out_color: str) -> None:
|
||||
room.game_active = False
|
||||
reason = "timeout"
|
||||
|
||||
p1_color = room.color_by_sid.get(room.p1_sid)
|
||||
p1_result = "loss" if p1_color == timed_out_color else "win"
|
||||
p2_result = "win" if p1_result == "loss" else "loss"
|
||||
winner_color = "b" if timed_out_color == "w" else "w"
|
||||
game_id = _save_completed_game(
|
||||
room,
|
||||
reason,
|
||||
"white" if winner_color == "w" else "black",
|
||||
)
|
||||
p1_result = _result_for_sid(room, room.p1_sid, winner_color)
|
||||
|
||||
sIO.emit("game_over", {"result": p1_result, "reason": reason}, to=room.p1_sid)
|
||||
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)
|
||||
p2_result = _result_for_sid(room, room.p2_sid, winner_color)
|
||||
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:
|
||||
@@ -198,6 +271,10 @@ def _start_game_if_ready(room: GameRoom) -> None:
|
||||
room.active_since = time.monotonic()
|
||||
room.game_active = True
|
||||
room.completed = False
|
||||
room.move_history = []
|
||||
room.started_at = _timestamp_now()
|
||||
room.ended_at = None
|
||||
room.saved_game_id = None
|
||||
|
||||
p1_payload = {
|
||||
"play_as": p1_color,
|
||||
@@ -272,13 +349,54 @@ def on_disconnect():
|
||||
leave_room(code)
|
||||
code_by_sid.pop(sid, None)
|
||||
|
||||
if sid == room.p1_sid:
|
||||
if room.p2_sid:
|
||||
emit("game_over", {"result": "win", "reason": "opponent disconnected"}, to=room.p2_sid)
|
||||
if room.completed:
|
||||
_cleanup_room(code)
|
||||
return
|
||||
|
||||
if sid == room.p1_sid:
|
||||
if room.p2_sid:
|
||||
if room.started_at and room.color_by_sid and not room.completed:
|
||||
room.completed = True
|
||||
room.game_active = False
|
||||
game_id = _save_completed_game(
|
||||
room,
|
||||
"opponent disconnected",
|
||||
_winner_color_value(room, room.p2_sid),
|
||||
)
|
||||
emit(
|
||||
"game_over",
|
||||
{
|
||||
"result": "win",
|
||||
"reason": "opponent disconnected",
|
||||
"game_id": game_id,
|
||||
},
|
||||
to=room.p2_sid,
|
||||
)
|
||||
_cleanup_room(code)
|
||||
return
|
||||
|
||||
if room.started_at and room.color_by_sid and not room.completed:
|
||||
room.completed = True
|
||||
room.game_active = False
|
||||
game_id = _save_completed_game(
|
||||
room,
|
||||
"opponent disconnected",
|
||||
_winner_color_value(room, room.p1_sid),
|
||||
)
|
||||
_cleanup_room(code)
|
||||
emit(
|
||||
"game_over",
|
||||
{
|
||||
"result": "win",
|
||||
"reason": "opponent disconnected",
|
||||
"game_id": game_id,
|
||||
},
|
||||
to=room.p1_sid,
|
||||
)
|
||||
return
|
||||
|
||||
room.p2_sid = None
|
||||
room.p2_user_id = None
|
||||
room.p2_name = None
|
||||
room.ready.pop(sid, None)
|
||||
room.color_by_sid.pop(sid, None)
|
||||
@@ -287,6 +405,10 @@ def on_disconnect():
|
||||
room.game_active = False
|
||||
room.completed = False
|
||||
room.active_since = None
|
||||
room.move_history = []
|
||||
room.started_at = None
|
||||
room.ended_at = None
|
||||
room.saved_game_id = None
|
||||
|
||||
if other_sid:
|
||||
emit("p2_connected", {"p2_name": None, "ready": False}, to=other_sid)
|
||||
@@ -312,6 +434,7 @@ def on_create_code_game(payload):
|
||||
room = GameRoom(
|
||||
code=code,
|
||||
p1_sid=sid,
|
||||
p1_user_id=current_user.id,
|
||||
p1_name=current_user.username,
|
||||
p1_pref=payload["play_as"],
|
||||
time_mode=payload["time_mode"],
|
||||
@@ -355,6 +478,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
|
||||
@@ -442,6 +566,7 @@ def on_move_request(payload):
|
||||
|
||||
mover_color = room.color_by_sid.get(sid)
|
||||
san = room.board.san(move)
|
||||
room.move_history.append(san)
|
||||
room.board.push(move)
|
||||
|
||||
if mover_color == "w":
|
||||
@@ -491,9 +616,11 @@ 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)
|
||||
winner_sid = other_sid if other_sid else None
|
||||
game_id = _save_completed_game(room, reason, _winner_color_value(room, winner_sid))
|
||||
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")
|
||||
@@ -515,7 +642,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_completed_game(room, "draw agreed", "draw")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user