introduced internal chess engine, fixed bugs

This commit is contained in:
simoncreates
2026-03-03 21:07:20 +01:00
parent b9dd663113
commit b673c1382c
2 changed files with 128 additions and 36 deletions
+77 -31
View File
@@ -6,7 +6,13 @@ from datetime import datetime
from threading import Lock
from typing import Optional
from chess_sim.game_board import ChessBoard, Outcome, Color
from app.chess_sim.game_board import (
ChessBoard,
Outcome,
Color,
BoardMove,
PieceType,
)
from flask_login import current_user
from flask import request
from flask_socketio import emit, join_room, leave_room
@@ -27,7 +33,7 @@ class GameRoom:
p2_user_id: Optional[int] = None
p2_name: Optional[str] = None
ready: dict[str, bool] = field(default_factory=dict)
board: ChessBoard = ChessBoard.init_default()
board: ChessBoard = field(default_factory=ChessBoard.init_default)
color_by_sid: dict[str, str] = field(default_factory=dict)
initial_ms: int = 600000
increment_ms: int = 0
@@ -160,15 +166,18 @@ def _emit_game_over(room: GameRoom, reason: str) -> None:
p1_result = "draw"
p2_result = "draw"
if outcome == Outcome.WHITE_WIN:
winner_color = "w"
else:
winner_color = "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"
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)
if room.p2_sid:
@@ -296,8 +305,7 @@ def on_disconnect():
room.p2_name = None
room.ready.pop(sid, None)
room.color_by_sid.pop(sid, None)
# initialize a new chessboard???
room.board = chess.Board()
room.board = ChessBoard.init_default()
room.ready[room.p1_sid] = False
room.game_active = False
room.completed = False
@@ -307,6 +315,18 @@ def on_disconnect():
emit("p2_connected", {"p2_name": None, "ready": False}, to=other_sid)
def _make_room(sid: str, play_as: str, time_mode: str) -> GameRoom:
return GameRoom(
code="", # caller will fill in the actual code
p1_sid=sid,
p1_user_id=current_user.id,
p1_name=current_user.username,
p1_pref=play_as,
time_mode=time_mode,
ready={sid: False},
)
@sIO.on("create_code_game")
def on_create_code_game(payload):
ok, err = validate_client_event("create_code_game", payload)
@@ -324,14 +344,9 @@ def on_create_code_game(payload):
_cleanup_room(existing.code)
code = _new_code()
room = GameRoom(
code=code,
p1_sid=sid,
p1_name=current_user.username,
p1_pref=payload["play_as"],
time_mode=payload["time_mode"],
ready={sid: False},
)
room = _make_room(sid, payload["play_as"], payload["time_mode"])
room.code = code # assign after generation so constructor stays pure
games_by_code[code] = room
code_by_sid[sid] = code
@@ -410,6 +425,31 @@ def on_user_ready(payload):
_start_game_if_ready(room)
# helpers for converting client payloads to engine moves and back
def _payload_to_move(payload: dict) -> BoardMove:
from_sq = ChessBoard.algebraic_to_pos(payload["from_square"])
to_sq = ChessBoard.algebraic_to_pos(payload["to_square"])
promotion = payload.get("promotion")
prom_piece: Optional[PieceType] = None
if promotion:
mapping = {
"q": PieceType.QUEEN,
"r": PieceType.ROOK,
"b": PieceType.BISHOP,
"n": PieceType.KNIGHT,
}
prom_piece = mapping.get(promotion.lower())
if prom_piece is None:
raise ValueError("invalid promotion piece")
return BoardMove(from_sq, to_sq, promotion_piece=prom_piece)
def _move_to_san(move: BoardMove) -> str:
# todo can be improved, very simple
return f"{ChessBoard.pos_to_algebraic(move.m_from)}{ChessBoard.pos_to_algebraic(move.m_to)}"
@sIO.on("move_request")
def on_move_request(payload):
ok, err = validate_client_event("move_request", payload)
@@ -440,24 +480,28 @@ def on_move_request(payload):
emit("move_reject", {"reason": "not your turn"})
return
uci = (
f"{payload['from_square'].lower()}{payload['to_square'].lower()}"
f"{payload.get('promotion', '')}"
)
try:
move = chess.Move.from_uci(uci)
move = _payload_to_move(payload)
except ValueError:
print("rejecting move due to invalid move format")
emit("move_reject", {"reason": "invalid move format"}, to=sid)
return
if move not in room.board.legal_moves:
current_color = room.board.turn
legal = room.board.generate_moves(current_color)
if move not in legal:
print(f"current_color: {current_color}")
print("rejecting move due to illegal move")
print(f"move: {move}")
room.board.print_legal_moves(current_color)
emit("move_reject", {"reason": "illegal move"}, to=sid)
return
mover_color = room.color_by_sid.get(sid)
san = room.board.san(move)
room.board.push(move)
san = _move_to_san(move)
# perform the move on our own engine
room.board.make_move(move, current_color)
room.move_history.append(san)
if mover_color == "w":
room.white_ms += room.increment_ms
@@ -472,7 +516,7 @@ def on_move_request(payload):
"promotion": payload.get("promotion"),
"san": san,
"time_left_ms": room.white_ms if mover_color == "w" else room.black_ms,
"fen": room.board.fen(),
"fen": room.board.to_fen(),
**_clock_payload(room),
}
@@ -481,7 +525,9 @@ def on_move_request(payload):
if other_sid:
emit("user_move", move_payload, to=other_sid)
if room.board.is_game_over(claim_draw=True):
if room.board.outcome() != Outcome.NOT_FINISHED:
print(f"ending a game due to outcome: { room.board.outcome()}")
_emit_game_over(room, _game_over_reason(room.board))