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 dataclasses import dataclass
from typing import Iterator, Optional, List, Tuple
import random
import copy
FILES = "abcdefgh"
RANKS = "12345678"
class PieceType(Enum):
PAWN = auto()
KNIGHT = auto()
@@ -26,6 +28,19 @@ class Color(Enum):
else:
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
class BoardPos:
@@ -99,6 +114,87 @@ class ChessBoard:
fields: List[List[BoardField]]
num_moves: int
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):
self.fields[row][col].piece = Piece(piece_type, color)
@@ -106,8 +202,9 @@ class ChessBoard:
# initialize default starting position
@classmethod
def init_default(cls) -> "ChessBoard":
empty_board = [[BoardField() for _ in range(8)] for _ in range(8)]
brd = cls(
[[BoardField() for _ in range(8)] for _ in range(8)], 0, []
empty_board, 0, [], empty_board
)
# place pawns
@@ -135,6 +232,9 @@ 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
return brd
# attempt to make move for color
@@ -317,7 +417,7 @@ class ChessBoard:
color.opposite,
):
# 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)):
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():
board = ChessBoard.init_default()
# rewrite with boardmove from string
seq = [
(BoardMove(BoardPos((7, 6)), BoardPos((5, 5))), Color.WHITE),
(BoardMove(BoardPos((0, 1)), BoardPos((2, 2))), Color.BLACK),
@@ -139,6 +140,32 @@ def main():
test_castling_kingside_both_sides()
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")
+15 -12
View File
@@ -5,7 +5,8 @@ from dataclasses import dataclass, field
from threading import Lock
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 import request
from flask_socketio import emit, join_room, leave_room
@@ -23,7 +24,7 @@ class GameRoom:
p2_sid: Optional[str] = None
p2_name: Optional[str] = None
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)
initial_ms: int = 600000
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]:
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:
return {
"white_time_left_ms": max(0, room.white_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:
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":
room.white_ms = max(0, room.white_ms - elapsed_ms)
else:
@@ -125,13 +126,14 @@ def _cleanup_room(code: str) -> 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():
return "checkmate"
if board.is_stalemate():
return "stalemate"
if board.is_insufficient_material():
return "insufficient material"
# todo introduce game over reason insufficient material
# if board.is_insufficient_material():
# return "insufficient material"
if board.is_seventyfive_moves():
return "75-move rule"
if board.is_fivefold_repetition():
@@ -143,10 +145,11 @@ def _emit_game_over(room: GameRoom, reason: str) -> None:
room.completed = True
room.game_active = False
outcome = room.board.outcome(claim_draw=True)
outcome = room.board.outcome()
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 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):
return
room.board = chess.Board()
room.board = ChessBoard.init_default()
if room.p1_pref == "w":
p1_color = "w"
elif room.p1_pref == "b":
@@ -202,14 +205,14 @@ def _start_game_if_ready(room: GameRoom) -> None:
p1_payload = {
"play_as": p1_color,
"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,
**_clock_payload(room),
}
p2_payload = {
"play_as": p2_color,
"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,
**_clock_payload(room),
}