beginning of transition

This commit is contained in:
simoncreates
2026-03-03 20:06:50 +01:00
parent 7887986a5a
commit 813134ad68
3 changed files with 146 additions and 16 deletions
+104 -4
View File
@@ -1,11 +1,13 @@
from enum import Enum, auto from enum import Enum, auto
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator, Optional, List, Tuple from typing import Iterator, Optional, List, Tuple
import random import copy
FILES = "abcdefgh" FILES = "abcdefgh"
RANKS = "12345678" RANKS = "12345678"
class PieceType(Enum): class PieceType(Enum):
PAWN = auto() PAWN = auto()
KNIGHT = auto() KNIGHT = auto()
@@ -26,6 +28,19 @@ class Color(Enum):
else: else:
return Color.WHITE return Color.WHITE
class Outcome(Enum):
DRAW = auto()
WHITE_WIN = auto()
BLACK_WIN = auto()
NOT_FINISHED = auto()
@classmethod
def from_color(cls, color: Color) -> "Outcome":
if color == Color.WHITE:
return Outcome.WHITE_WIN
else:
return Outcome.BLACK_WIN
@dataclass @dataclass
class BoardPos: class BoardPos:
@@ -99,6 +114,87 @@ class ChessBoard:
fields: List[List[BoardField]] fields: List[List[BoardField]]
num_moves: int num_moves: int
move_history: List[Tuple['BoardMove', Optional[Piece]]] move_history: List[Tuple['BoardMove', Optional[Piece]]]
initial_board: List[List[BoardField]]
@property
def turn(self) -> "Color":
len_mv_history = len(self.move_history)
if len_mv_history > 0:
first_moved_piece_color = self.initial_move_color
if len_mv_history % 2 == 0:
return first_moved_piece_color
else:
return first_moved_piece_color.opposite
else:
return Color.WHITE
@property
def initial_move_color(self) -> Color:
first_move = self.move_history[0]
from_pos = first_move[0].m_from
# look up what color the piece had that moved first
# (odd solution but i think its quite fine)
first_moved_field = self.initial_board[from_pos.x][from_pos.y]
first_moved_piece_color = Color.WHITE
if first_moved_field.piece is not None:
first_moved_piece_color = first_moved_field.piece.color
return first_moved_piece_color
def is_checkmate(self) -> bool:
cur_turn_color = self.turn
return len(self.generate_moves(cur_turn_color)) == 0 and self.is_field_attacked(self.pos_of_king(cur_turn_color), cur_turn_color.opposite)
def is_stalemate(self) -> bool:
cur_turn_color = self.turn
return len(self.generate_moves(cur_turn_color)) == 0
def is_seventyfive_moves(self) -> bool:
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:
return Outcome.DRAW
return Outcome.NOT_FINISHED
def highest_repetiton_amount(self) -> int:
if self.move_history:
starting_color = self.initial_move_color
else:
starting_color = Color.WHITE
dummy = ChessBoard(
copy.deepcopy(self.initial_board),
0,
[],
copy.deepcopy(self.initial_board),
)
color = starting_color
states: list[str] = []
states.append(str(dummy))
for mv, _ in self.move_history:
dummy.make_move(mv, color)
color = color.opposite
states.append(str(dummy))
counts: dict[str, int] = {}
max_count = 0
for state in states:
counts[state] = counts.get(state, 0) + 1
if counts[state] > max_count:
max_count = counts[state]
return max_count
def place(self, row: int, col: int, piece_type: PieceType, color: Color): def place(self, row: int, col: int, piece_type: PieceType, color: Color):
self.fields[row][col].piece = Piece(piece_type, color) self.fields[row][col].piece = Piece(piece_type, color)
@@ -106,8 +202,9 @@ class ChessBoard:
# initialize default starting position # initialize default starting position
@classmethod @classmethod
def init_default(cls) -> "ChessBoard": def init_default(cls) -> "ChessBoard":
empty_board = [[BoardField() for _ in range(8)] for _ in range(8)]
brd = cls( brd = cls(
[[BoardField() for _ in range(8)] for _ in range(8)], 0, [] empty_board, 0, [], empty_board
) )
# place pawns # place pawns
@@ -135,6 +232,9 @@ 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
brd.initial_board = brd.fields
return brd return brd
# attempt to make move for color # attempt to make move for color
@@ -317,7 +417,7 @@ class ChessBoard:
color.opposite, color.opposite,
): ):
# check if the last move moved the pawn there from the starting square # check if the last move moved the pawn there from the starting square
last_move = self.move_history[move_history_len][0] last_move = self.move_history[-1][0] # final move, not past-the-end
if last_move.m_from == BoardPos((tr, tc + en_passant_dir)) and last_move.m_to == BoardPos((tr, tc - en_passant_dir)): if last_move.m_from == BoardPos((tr, tc + en_passant_dir)) and last_move.m_to == BoardPos((tr, tc - en_passant_dir)):
moves.append(BoardMove(pos, BoardPos((tr, tc)))) moves.append(BoardMove(pos, BoardPos((tr, tc))))
+27
View File
@@ -65,6 +65,7 @@ def run_random_games(n: int = 20, max_moves: int = 200, verbose: bool = False):
def test_castling_kingside_both_sides(): def test_castling_kingside_both_sides():
board = ChessBoard.init_default() board = ChessBoard.init_default()
# rewrite with boardmove from string
seq = [ seq = [
(BoardMove(BoardPos((7, 6)), BoardPos((5, 5))), Color.WHITE), (BoardMove(BoardPos((7, 6)), BoardPos((5, 5))), Color.WHITE),
(BoardMove(BoardPos((0, 1)), BoardPos((2, 2))), Color.BLACK), (BoardMove(BoardPos((0, 1)), BoardPos((2, 2))), Color.BLACK),
@@ -139,6 +140,32 @@ def main():
test_castling_kingside_both_sides() test_castling_kingside_both_sides()
run_random_games(verbose=False) run_random_games(verbose=False)
board = ChessBoard.init_default()
# helper to create boardmove from two-square string
def bm(s):
return BoardMove(
ChessBoard.algebraic_to_pos(s[0:2]),
ChessBoard.algebraic_to_pos(s[2:4])
)
# perform four cycles of the knight shuffle
# Nf3-Nf6-Ng1-Ng8 and returns to the starting position.
seq = []
for _ in range(4):
seq.extend([
(bm("g1f3"), Color.WHITE),
(bm("g8f6"), Color.BLACK),
(bm("f3g1"), Color.WHITE),
(bm("f6g8"), Color.BLACK),
])
for move, color in seq:
ok = board.make_move(move, color)
assert ok, "repetition test move failed"
rep = board.highest_repetiton_amount()
assert rep >= 5, f"expected at least 5 repetitions, got {rep}"
assert board.is_fivefold_repetition(), "board should report fivefold repetition"
print(f"all tests passed") print(f"all tests passed")
+15 -12
View File
@@ -5,7 +5,8 @@ from dataclasses import dataclass, field
from threading import Lock from threading import Lock
from typing import Optional from typing import Optional
import chess #todo: replace with own chess logic implementation to remove dependency import chess
from chess_sim.game_board import ChessBoard, Color, Outcome
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
@@ -23,7 +24,7 @@ class GameRoom:
p2_sid: Optional[str] = None p2_sid: Optional[str] = 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: chess.Board = field(default_factory=chess.Board) board: ChessBoard = 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
@@ -81,14 +82,14 @@ def _sid_for_color(room: GameRoom, color: str) -> Optional[str]:
def _turn_sid(room: GameRoom) -> Optional[str]: def _turn_sid(room: GameRoom) -> Optional[str]:
return _sid_for_color(room, "w" if room.board.turn == chess.WHITE else "b") return _sid_for_color(room, "w" if room.board.turn == Color.WHITE else "b")
def _clock_payload(room: GameRoom) -> dict: def _clock_payload(room: GameRoom) -> dict:
return { return {
"white_time_left_ms": max(0, room.white_ms), "white_time_left_ms": max(0, room.white_ms),
"black_time_left_ms": max(0, room.black_ms), "black_time_left_ms": max(0, room.black_ms),
"turn": "w" if room.board.turn == chess.WHITE else "b", "turn": "w" if room.board.turn == Color.WHITE else "b",
} }
@@ -101,7 +102,7 @@ def _apply_elapsed(room: GameRoom) -> Optional[str]:
if elapsed_ms <= 0: if elapsed_ms <= 0:
return None return None
active_color = "w" if room.board.turn == chess.WHITE else "b" active_color = "w" if room.board.turn == Color.WHITE else "b"
if active_color == "w": if active_color == "w":
room.white_ms = max(0, room.white_ms - elapsed_ms) room.white_ms = max(0, room.white_ms - elapsed_ms)
else: else:
@@ -125,13 +126,14 @@ def _cleanup_room(code: str) -> None:
code_by_sid.pop(room.p2_sid, None) code_by_sid.pop(room.p2_sid, None)
def _game_over_reason(board: chess.Board) -> str: def _game_over_reason(board: ChessBoard) -> str:
if board.is_checkmate(): if board.is_checkmate():
return "checkmate" return "checkmate"
if board.is_stalemate(): if board.is_stalemate():
return "stalemate" return "stalemate"
if board.is_insufficient_material(): # todo introduce game over reason insufficient material
return "insufficient material" # if board.is_insufficient_material():
# return "insufficient material"
if board.is_seventyfive_moves(): if board.is_seventyfive_moves():
return "75-move rule" return "75-move rule"
if board.is_fivefold_repetition(): if board.is_fivefold_repetition():
@@ -143,10 +145,11 @@ def _emit_game_over(room: GameRoom, reason: str) -> None:
room.completed = True room.completed = True
room.game_active = False room.game_active = False
outcome = room.board.outcome(claim_draw=True) outcome = room.board.outcome()
p1_result = "draw" p1_result = "draw"
p2_result = "draw" p2_result = "draw"
if outcome and outcome.winner is not None: if outcome and outcome.winner is not None:
winner_color = "w" if outcome.winner == chess.WHITE else "b" winner_color = "w" if outcome.winner == chess.WHITE else "b"
if room.color_by_sid.get(room.p1_sid) == winner_color: if room.color_by_sid.get(room.p1_sid) == winner_color:
@@ -181,7 +184,7 @@ def _start_game_if_ready(room: GameRoom) -> None:
if not room.ready.get(room.p1_sid) or not room.ready.get(room.p2_sid): if not room.ready.get(room.p1_sid) or not room.ready.get(room.p2_sid):
return return
room.board = chess.Board() room.board = ChessBoard.init_default()
if room.p1_pref == "w": if room.p1_pref == "w":
p1_color = "w" p1_color = "w"
elif room.p1_pref == "b": elif room.p1_pref == "b":
@@ -202,14 +205,14 @@ def _start_game_if_ready(room: GameRoom) -> None:
p1_payload = { p1_payload = {
"play_as": p1_color, "play_as": p1_color,
"time_left_ms": room.white_ms if p1_color == "w" else room.black_ms, "time_left_ms": room.white_ms if p1_color == "w" else room.black_ms,
"fen": room.board.fen(), "fen": room.board.to_fen(),
"opponent": room.p2_name, "opponent": room.p2_name,
**_clock_payload(room), **_clock_payload(room),
} }
p2_payload = { p2_payload = {
"play_as": p2_color, "play_as": p2_color,
"time_left_ms": room.white_ms if p2_color == "w" else room.black_ms, "time_left_ms": room.white_ms if p2_color == "w" else room.black_ms,
"fen": room.board.fen(), "fen": room.board.to_fen(),
"opponent": room.p1_name, "opponent": room.p1_name,
**_clock_payload(room), **_clock_payload(room),
} }