introduced internal chess engine, fixed bugs
This commit is contained in:
@@ -153,12 +153,17 @@ class ChessBoard:
|
||||
return len(self.move_history) >= 75
|
||||
|
||||
def is_fivefold_repetition(self) -> bool:
|
||||
|
||||
return self.highest_repetiton_amount() >= 5
|
||||
|
||||
def outcome(self) -> Outcome:
|
||||
if self.is_checkmate():
|
||||
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.NOT_FINISHED
|
||||
|
||||
@@ -204,7 +209,7 @@ class ChessBoard:
|
||||
def init_default(cls) -> "ChessBoard":
|
||||
empty_board = [[BoardField() for _ in range(8)] for _ in range(8)]
|
||||
brd = cls(
|
||||
empty_board, 0, [], empty_board
|
||||
empty_board, 0, [], copy.deepcopy(empty_board)
|
||||
)
|
||||
|
||||
# place pawns
|
||||
@@ -232,8 +237,8 @@ class ChessBoard:
|
||||
for col, piece_type in enumerate(back_rank):
|
||||
brd.place(7, col, piece_type, Color.WHITE)
|
||||
|
||||
#set the inital board for later ref
|
||||
brd.initial_board = brd.fields
|
||||
# set the initial board for later ref (must be a copy, not a reference)
|
||||
brd.initial_board = copy.deepcopy(brd.fields)
|
||||
|
||||
return brd
|
||||
|
||||
@@ -427,10 +432,15 @@ class ChessBoard:
|
||||
else:
|
||||
# occupied
|
||||
if target_field.piece.color == piece.color:
|
||||
if non_sliding:
|
||||
continue
|
||||
# for sliding pieces a blocker stops further squares
|
||||
break
|
||||
else:
|
||||
# opponent piece
|
||||
# opponent piece: capture is allowed
|
||||
moves.append(BoardMove(pos, BoardPos((tr, tc))))
|
||||
if non_sliding:
|
||||
continue
|
||||
break
|
||||
|
||||
|
||||
@@ -739,3 +749,39 @@ class ChessBoard:
|
||||
bm = BoardMove(src, dst, move_type, promotion_piece=promo_piece)
|
||||
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")
|
||||
|
||||
|
||||
|
||||
+72
-26
@@ -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,9 +166,10 @@ def _emit_game_over(room: GameRoom, reason: str) -> None:
|
||||
p1_result = "draw"
|
||||
p2_result = "draw"
|
||||
|
||||
|
||||
if outcome and outcome.winner is not None:
|
||||
winner_color = "w" if outcome.winner == chess.WHITE else "b"
|
||||
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"
|
||||
@@ -170,6 +177,8 @@ def _emit_game_over(room: GameRoom, reason: str) -> None:
|
||||
p1_result = "loss"
|
||||
p2_result = "win"
|
||||
|
||||
|
||||
|
||||
emit("game_over", {"result": p1_result, "reason": reason}, to=room.p1_sid)
|
||||
if room.p2_sid:
|
||||
emit("game_over", {"result": p2_result, "reason": reason}, to=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))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user