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
+51 -5
View File
@@ -153,12 +153,17 @@ class ChessBoard:
return len(self.move_history) >= 75 return len(self.move_history) >= 75
def is_fivefold_repetition(self) -> bool: def is_fivefold_repetition(self) -> bool:
return self.highest_repetiton_amount() >= 5 return self.highest_repetiton_amount() >= 5
def outcome(self) -> Outcome: def outcome(self) -> Outcome:
if self.is_checkmate(): if self.is_checkmate():
return Outcome.from_color(self.turn) return Outcome.from_color(self.turn)
if self.is_stalemate() or self.is_seventyfive_moves or self.is_fivefold_repetition: if self.is_stalemate():
return Outcome.DRAW
if self.is_seventyfive_moves():
return Outcome.DRAW
if self.is_fivefold_repetition():
return Outcome.DRAW return Outcome.DRAW
return Outcome.NOT_FINISHED return Outcome.NOT_FINISHED
@@ -204,7 +209,7 @@ class ChessBoard:
def init_default(cls) -> "ChessBoard": def init_default(cls) -> "ChessBoard":
empty_board = [[BoardField() for _ in range(8)] for _ in range(8)] empty_board = [[BoardField() for _ in range(8)] for _ in range(8)]
brd = cls( brd = cls(
empty_board, 0, [], empty_board empty_board, 0, [], copy.deepcopy(empty_board)
) )
# place pawns # place pawns
@@ -232,8 +237,8 @@ class ChessBoard:
for col, piece_type in enumerate(back_rank): for col, piece_type in enumerate(back_rank):
brd.place(7, col, piece_type, Color.WHITE) brd.place(7, col, piece_type, Color.WHITE)
#set the inital board for later ref # set the initial board for later ref (must be a copy, not a reference)
brd.initial_board = brd.fields brd.initial_board = copy.deepcopy(brd.fields)
return brd return brd
@@ -427,10 +432,15 @@ class ChessBoard:
else: else:
# occupied # occupied
if target_field.piece.color == piece.color: if target_field.piece.color == piece.color:
if non_sliding:
continue
# for sliding pieces a blocker stops further squares
break break
else: else:
# opponent piece # opponent piece: capture is allowed
moves.append(BoardMove(pos, BoardPos((tr, tc)))) moves.append(BoardMove(pos, BoardPos((tr, tc))))
if non_sliding:
continue
break break
@@ -739,3 +749,39 @@ class ChessBoard:
bm = BoardMove(src, dst, move_type, promotion_piece=promo_piece) bm = BoardMove(src, dst, move_type, promotion_piece=promo_piece)
return self.make_move(bm, color) return self.make_move(bm, color)
def print_legal_moves(self, color: Color) -> None:
moves = self.generate_moves(color)
if not moves:
print(f"{color.name}: no legal moves")
return
move_strings = []
for move in moves:
from_sq = self.pos_to_algebraic(move.m_from)
to_sq = self.pos_to_algebraic(move.m_to)
move_str = f"{from_sq}-{to_sq}"
# Add move type notation
if move.move_type.name == "CASTLING_KINGSIDE":
move_str += " (O-O)"
elif move.move_type.name == "CASTLING_QUEENSIDE":
move_str += " (O-O-O)"
elif move.move_type.name == "EN_PASSANT":
move_str += " (e.p.)"
elif move.promotion_piece:
move_str += f" (={move.promotion_piece.name[0]})"
move_strings.append(move_str)
print(f"\n{color.name} Legal Moves ({len(moves)} total):")
print("-" * 50)
# Print in columns (6 moves per row)
moves_per_row = 6
for i in range(0, len(move_strings), moves_per_row):
row = move_strings[i:i + moves_per_row]
print(" " + " | ".join(f"{m:12}" for m in row))
print("-" * 50 + "\n")
+77 -31
View File
@@ -6,7 +6,13 @@ from datetime import datetime
from threading import Lock from threading import Lock
from typing import Optional 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_login import current_user
from flask import request from flask import request
from flask_socketio import emit, join_room, leave_room from flask_socketio import emit, join_room, leave_room
@@ -27,7 +33,7 @@ class GameRoom:
p2_user_id: Optional[int] = None p2_user_id: Optional[int] = None
p2_name: Optional[str] = None p2_name: Optional[str] = None
ready: dict[str, bool] = field(default_factory=dict) 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) color_by_sid: dict[str, str] = field(default_factory=dict)
initial_ms: int = 600000 initial_ms: int = 600000
increment_ms: int = 0 increment_ms: int = 0
@@ -160,15 +166,18 @@ def _emit_game_over(room: GameRoom, reason: str) -> None:
p1_result = "draw" p1_result = "draw"
p2_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) emit("game_over", {"result": p1_result, "reason": reason}, to=room.p1_sid)
if room.p2_sid: if room.p2_sid:
@@ -296,8 +305,7 @@ def on_disconnect():
room.p2_name = None room.p2_name = None
room.ready.pop(sid, None) room.ready.pop(sid, None)
room.color_by_sid.pop(sid, None) room.color_by_sid.pop(sid, None)
# initialize a new chessboard??? room.board = ChessBoard.init_default()
room.board = chess.Board()
room.ready[room.p1_sid] = False room.ready[room.p1_sid] = False
room.game_active = False room.game_active = False
room.completed = False room.completed = False
@@ -307,6 +315,18 @@ def on_disconnect():
emit("p2_connected", {"p2_name": None, "ready": False}, to=other_sid) 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") @sIO.on("create_code_game")
def on_create_code_game(payload): def on_create_code_game(payload):
ok, err = validate_client_event("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) _cleanup_room(existing.code)
code = _new_code() code = _new_code()
room = GameRoom( room = _make_room(sid, payload["play_as"], payload["time_mode"])
code=code, room.code = code # assign after generation so constructor stays pure
p1_sid=sid,
p1_name=current_user.username,
p1_pref=payload["play_as"],
time_mode=payload["time_mode"],
ready={sid: False},
)
games_by_code[code] = room games_by_code[code] = room
code_by_sid[sid] = code code_by_sid[sid] = code
@@ -410,6 +425,31 @@ def on_user_ready(payload):
_start_game_if_ready(room) _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") @sIO.on("move_request")
def on_move_request(payload): def on_move_request(payload):
ok, err = validate_client_event("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"}) emit("move_reject", {"reason": "not your turn"})
return return
uci = (
f"{payload['from_square'].lower()}{payload['to_square'].lower()}"
f"{payload.get('promotion', '')}"
)
try: try:
move = chess.Move.from_uci(uci) move = _payload_to_move(payload)
except ValueError: except ValueError:
print("rejecting move due to invalid move format")
emit("move_reject", {"reason": "invalid move format"}, to=sid) emit("move_reject", {"reason": "invalid move format"}, to=sid)
return 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) emit("move_reject", {"reason": "illegal move"}, to=sid)
return return
mover_color = room.color_by_sid.get(sid) mover_color = room.color_by_sid.get(sid)
san = room.board.san(move) san = _move_to_san(move)
room.board.push(move) # perform the move on our own engine
room.board.make_move(move, current_color)
room.move_history.append(san)
if mover_color == "w": if mover_color == "w":
room.white_ms += room.increment_ms room.white_ms += room.increment_ms
@@ -472,7 +516,7 @@ def on_move_request(payload):
"promotion": payload.get("promotion"), "promotion": payload.get("promotion"),
"san": san, "san": san,
"time_left_ms": room.white_ms if mover_color == "w" else room.black_ms, "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), **_clock_payload(room),
} }
@@ -481,7 +525,9 @@ def on_move_request(payload):
if other_sid: if other_sid:
emit("user_move", move_payload, to=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)) _emit_game_over(room, _game_over_reason(room.board))