Merge branch 'main' into HEAD
This commit is contained in:
+104
-4
@@ -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))))
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+36
-160
@@ -2,16 +2,15 @@ import random
|
|||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
|
||||||
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
|
||||||
from app import sIO
|
from app import sIO
|
||||||
from app.models.game import save_finished_game
|
|
||||||
from .types import validate_client_event
|
from .types import validate_client_event
|
||||||
|
|
||||||
|
|
||||||
@@ -19,15 +18,13 @@ from .types import validate_client_event
|
|||||||
class GameRoom:
|
class GameRoom:
|
||||||
code: str
|
code: str
|
||||||
p1_sid: str
|
p1_sid: str
|
||||||
p1_user_id: int
|
|
||||||
p1_name: str
|
p1_name: str
|
||||||
p1_pref: str
|
p1_pref: str
|
||||||
time_mode: str
|
time_mode: str
|
||||||
p2_sid: Optional[str] = None
|
p2_sid: Optional[str] = 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: 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
|
||||||
@@ -36,10 +33,6 @@ class GameRoom:
|
|||||||
active_since: Optional[float] = None
|
active_since: Optional[float] = None
|
||||||
game_active: bool = False
|
game_active: bool = False
|
||||||
completed: bool = False
|
completed: bool = False
|
||||||
move_history: list[str] = field(default_factory=list)
|
|
||||||
started_at: Optional[str] = None
|
|
||||||
ended_at: Optional[str] = None
|
|
||||||
saved_game_id: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
games_by_code: dict[str, GameRoom] = {}
|
games_by_code: dict[str, GameRoom] = {}
|
||||||
@@ -74,10 +67,6 @@ def _parse_time_mode(time_mode: str) -> tuple[int, int]:
|
|||||||
return minutes * 60 * 1000, increment_seconds * 1000
|
return minutes * 60 * 1000, increment_seconds * 1000
|
||||||
|
|
||||||
|
|
||||||
def _timestamp_now() -> str:
|
|
||||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
|
|
||||||
def _room_for_sid(sid: str) -> Optional[GameRoom]:
|
def _room_for_sid(sid: str) -> Optional[GameRoom]:
|
||||||
code = code_by_sid.get(sid)
|
code = code_by_sid.get(sid)
|
||||||
if not code:
|
if not code:
|
||||||
@@ -93,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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -113,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:
|
||||||
@@ -137,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():
|
||||||
@@ -151,82 +141,27 @@ def _game_over_reason(board: chess.Board) -> str:
|
|||||||
return "game over"
|
return "game over"
|
||||||
|
|
||||||
|
|
||||||
def _termination_key(reason: str) -> str:
|
|
||||||
if reason == "checkmate":
|
|
||||||
return "checkmate"
|
|
||||||
if reason == "resignation":
|
|
||||||
return "resignation"
|
|
||||||
if reason == "timeout":
|
|
||||||
return "timeout"
|
|
||||||
if reason in {
|
|
||||||
"stalemate",
|
|
||||||
"insufficient material",
|
|
||||||
"75-move rule",
|
|
||||||
"fivefold repetition",
|
|
||||||
"draw agreed",
|
|
||||||
}:
|
|
||||||
return "draw"
|
|
||||||
return "other"
|
|
||||||
|
|
||||||
|
|
||||||
def _winner_color_value(room: GameRoom, winner_sid: Optional[str]) -> Optional[str]:
|
|
||||||
if winner_sid is None:
|
|
||||||
return "draw"
|
|
||||||
|
|
||||||
winner = room.color_by_sid.get(winner_sid)
|
|
||||||
if winner == "w":
|
|
||||||
return "white"
|
|
||||||
if winner == "b":
|
|
||||||
return "black"
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _result_for_sid(room: GameRoom, sid: str, winner_color: Optional[str]) -> str:
|
|
||||||
if winner_color == "draw" or winner_color is None:
|
|
||||||
return "draw"
|
|
||||||
|
|
||||||
player_color = room.color_by_sid.get(sid)
|
|
||||||
return "win" if player_color == winner_color else "loss"
|
|
||||||
|
|
||||||
|
|
||||||
def _save_completed_game(room: GameRoom, reason: str, winner_color: Optional[str]) -> Optional[int]:
|
|
||||||
if room.saved_game_id is not None:
|
|
||||||
return room.saved_game_id
|
|
||||||
if not room.started_at or room.p2_user_id is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
room.ended_at = room.ended_at or _timestamp_now()
|
|
||||||
room.saved_game_id = save_finished_game(
|
|
||||||
white_player_id=room.p1_user_id if room.color_by_sid.get(room.p1_sid) == "w" else room.p2_user_id,
|
|
||||||
black_player_id=room.p1_user_id if room.color_by_sid.get(room.p1_sid) == "b" else room.p2_user_id,
|
|
||||||
final_fen=room.board.fen(),
|
|
||||||
termination=_termination_key(reason),
|
|
||||||
termination_detail=reason,
|
|
||||||
winner_color=winner_color,
|
|
||||||
move_history=room.move_history,
|
|
||||||
time_mode=room.time_mode,
|
|
||||||
started_at=room.started_at,
|
|
||||||
ended_at=room.ended_at,
|
|
||||||
)
|
|
||||||
return room.saved_game_id
|
|
||||||
|
|
||||||
|
|
||||||
def _emit_game_over(room: GameRoom, reason: str) -> None:
|
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()
|
||||||
winner_color = "draw"
|
p1_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:
|
||||||
|
p1_result = "win"
|
||||||
|
p2_result = "loss"
|
||||||
|
else:
|
||||||
|
p1_result = "loss"
|
||||||
|
p2_result = "win"
|
||||||
|
|
||||||
game_id = _save_completed_game(room, reason, "white" if winner_color == "w" else "black" if winner_color == "b" else "draw")
|
emit("game_over", {"result": p1_result, "reason": reason}, to=room.p1_sid)
|
||||||
p1_result = _result_for_sid(room, room.p1_sid, winner_color)
|
|
||||||
|
|
||||||
emit("game_over", {"result": p1_result, "reason": reason, "game_id": game_id}, to=room.p1_sid)
|
|
||||||
if room.p2_sid:
|
if room.p2_sid:
|
||||||
p2_result = _result_for_sid(room, room.p2_sid, winner_color)
|
emit("game_over", {"result": p2_result, "reason": reason}, to=room.p2_sid)
|
||||||
emit("game_over", {"result": p2_result, "reason": reason, "game_id": game_id}, to=room.p2_sid)
|
|
||||||
|
|
||||||
|
|
||||||
def _emit_timeout(room: GameRoom, timed_out_color: str) -> None:
|
def _emit_timeout(room: GameRoom, timed_out_color: str) -> None:
|
||||||
@@ -234,18 +169,13 @@ def _emit_timeout(room: GameRoom, timed_out_color: str) -> None:
|
|||||||
room.game_active = False
|
room.game_active = False
|
||||||
reason = "timeout"
|
reason = "timeout"
|
||||||
|
|
||||||
winner_color = "b" if timed_out_color == "w" else "w"
|
p1_color = room.color_by_sid.get(room.p1_sid)
|
||||||
game_id = _save_completed_game(
|
p1_result = "loss" if p1_color == timed_out_color else "win"
|
||||||
room,
|
p2_result = "win" if p1_result == "loss" else "loss"
|
||||||
reason,
|
|
||||||
"white" if winner_color == "w" else "black",
|
|
||||||
)
|
|
||||||
p1_result = _result_for_sid(room, room.p1_sid, winner_color)
|
|
||||||
|
|
||||||
sIO.emit("game_over", {"result": p1_result, "reason": reason, "game_id": game_id}, to=room.p1_sid)
|
sIO.emit("game_over", {"result": p1_result, "reason": reason}, to=room.p1_sid)
|
||||||
if room.p2_sid:
|
if room.p2_sid:
|
||||||
p2_result = _result_for_sid(room, room.p2_sid, winner_color)
|
sIO.emit("game_over", {"result": p2_result, "reason": reason}, to=room.p2_sid)
|
||||||
sIO.emit("game_over", {"result": p2_result, "reason": reason, "game_id": game_id}, to=room.p2_sid)
|
|
||||||
|
|
||||||
|
|
||||||
def _start_game_if_ready(room: GameRoom) -> None:
|
def _start_game_if_ready(room: GameRoom) -> None:
|
||||||
@@ -254,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":
|
||||||
@@ -271,22 +201,18 @@ def _start_game_if_ready(room: GameRoom) -> None:
|
|||||||
room.active_since = time.monotonic()
|
room.active_since = time.monotonic()
|
||||||
room.game_active = True
|
room.game_active = True
|
||||||
room.completed = False
|
room.completed = False
|
||||||
room.move_history = []
|
|
||||||
room.started_at = _timestamp_now()
|
|
||||||
room.ended_at = None
|
|
||||||
room.saved_game_id = 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),
|
||||||
}
|
}
|
||||||
@@ -349,66 +275,22 @@ def on_disconnect():
|
|||||||
leave_room(code)
|
leave_room(code)
|
||||||
code_by_sid.pop(sid, None)
|
code_by_sid.pop(sid, None)
|
||||||
|
|
||||||
if room.completed:
|
|
||||||
_cleanup_room(code)
|
|
||||||
return
|
|
||||||
|
|
||||||
if sid == room.p1_sid:
|
if sid == room.p1_sid:
|
||||||
if room.p2_sid:
|
if room.p2_sid:
|
||||||
if room.started_at and room.color_by_sid and not room.completed:
|
emit("game_over", {"result": "win", "reason": "opponent disconnected"}, to=room.p2_sid)
|
||||||
room.completed = True
|
|
||||||
room.game_active = False
|
|
||||||
game_id = _save_completed_game(
|
|
||||||
room,
|
|
||||||
"opponent disconnected",
|
|
||||||
_winner_color_value(room, room.p2_sid),
|
|
||||||
)
|
|
||||||
emit(
|
|
||||||
"game_over",
|
|
||||||
{
|
|
||||||
"result": "win",
|
|
||||||
"reason": "opponent disconnected",
|
|
||||||
"game_id": game_id,
|
|
||||||
},
|
|
||||||
to=room.p2_sid,
|
|
||||||
)
|
|
||||||
_cleanup_room(code)
|
_cleanup_room(code)
|
||||||
return
|
return
|
||||||
|
|
||||||
if room.started_at and room.color_by_sid and not room.completed:
|
|
||||||
room.completed = True
|
|
||||||
room.game_active = False
|
|
||||||
game_id = _save_completed_game(
|
|
||||||
room,
|
|
||||||
"opponent disconnected",
|
|
||||||
_winner_color_value(room, room.p1_sid),
|
|
||||||
)
|
|
||||||
_cleanup_room(code)
|
|
||||||
emit(
|
|
||||||
"game_over",
|
|
||||||
{
|
|
||||||
"result": "win",
|
|
||||||
"reason": "opponent disconnected",
|
|
||||||
"game_id": game_id,
|
|
||||||
},
|
|
||||||
to=room.p1_sid,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
room.p2_sid = None
|
room.p2_sid = None
|
||||||
room.p2_user_id = None
|
|
||||||
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 = chess.Board()
|
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
|
||||||
room.active_since = None
|
room.active_since = None
|
||||||
room.move_history = []
|
|
||||||
room.started_at = None
|
|
||||||
room.ended_at = None
|
|
||||||
room.saved_game_id = None
|
|
||||||
|
|
||||||
if other_sid:
|
if other_sid:
|
||||||
emit("p2_connected", {"p2_name": None, "ready": False}, to=other_sid)
|
emit("p2_connected", {"p2_name": None, "ready": False}, to=other_sid)
|
||||||
@@ -434,7 +316,6 @@ def on_create_code_game(payload):
|
|||||||
room = GameRoom(
|
room = GameRoom(
|
||||||
code=code,
|
code=code,
|
||||||
p1_sid=sid,
|
p1_sid=sid,
|
||||||
p1_user_id=current_user.id,
|
|
||||||
p1_name=current_user.username,
|
p1_name=current_user.username,
|
||||||
p1_pref=payload["play_as"],
|
p1_pref=payload["play_as"],
|
||||||
time_mode=payload["time_mode"],
|
time_mode=payload["time_mode"],
|
||||||
@@ -478,7 +359,6 @@ def on_join_code_game(payload):
|
|||||||
_cleanup_room(previous.code)
|
_cleanup_room(previous.code)
|
||||||
|
|
||||||
room.p2_sid = sid
|
room.p2_sid = sid
|
||||||
room.p2_user_id = current_user.id
|
|
||||||
room.p2_name = current_user.username
|
room.p2_name = current_user.username
|
||||||
room.ready.setdefault(room.p1_sid, False)
|
room.ready.setdefault(room.p1_sid, False)
|
||||||
room.ready[sid] = False
|
room.ready[sid] = False
|
||||||
@@ -566,7 +446,6 @@ def on_move_request(payload):
|
|||||||
|
|
||||||
mover_color = room.color_by_sid.get(sid)
|
mover_color = room.color_by_sid.get(sid)
|
||||||
san = room.board.san(move)
|
san = room.board.san(move)
|
||||||
room.move_history.append(san)
|
|
||||||
room.board.push(move)
|
room.board.push(move)
|
||||||
|
|
||||||
if mover_color == "w":
|
if mover_color == "w":
|
||||||
@@ -616,11 +495,9 @@ def on_request_resign(payload):
|
|||||||
reason = "resignation"
|
reason = "resignation"
|
||||||
|
|
||||||
other_sid = room.p2_sid if sid == room.p1_sid else room.p1_sid
|
other_sid = room.p2_sid if sid == room.p1_sid else room.p1_sid
|
||||||
winner_sid = other_sid if other_sid else None
|
emit("game_over", {"result": "loss", "reason": reason}, to=sid)
|
||||||
game_id = _save_completed_game(room, reason, _winner_color_value(room, winner_sid))
|
|
||||||
emit("game_over", {"result": "loss", "reason": reason, "game_id": game_id}, to=sid)
|
|
||||||
if other_sid:
|
if other_sid:
|
||||||
emit("game_over", {"result": "win", "reason": reason, "game_id": game_id}, to=other_sid)
|
emit("game_over", {"result": "win", "reason": reason}, to=other_sid)
|
||||||
|
|
||||||
|
|
||||||
@sIO.on("request_draw")
|
@sIO.on("request_draw")
|
||||||
@@ -642,8 +519,7 @@ def on_request_draw(payload):
|
|||||||
if payload.get("accepted") is True:
|
if payload.get("accepted") is True:
|
||||||
room.completed = True
|
room.completed = True
|
||||||
room.game_active = False
|
room.game_active = False
|
||||||
game_id = _save_completed_game(room, "draw agreed", "draw")
|
emit("game_over", {"result": "draw", "reason": "draw agreed"}, to=room.code)
|
||||||
emit("game_over", {"result": "draw", "reason": "draw agreed", "game_id": game_id}, to=room.code)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
other_sid = room.p2_sid if sid == room.p1_sid else room.p1_sid
|
other_sid = room.p2_sid if sid == room.p1_sid else room.p1_sid
|
||||||
|
|||||||
@@ -79,6 +79,26 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-join-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-join-form input {
|
||||||
|
width: 116px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
font: inherit;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-join {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.page-wrap {
|
.page-wrap {
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -329,6 +349,17 @@ h3 {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-join-form {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-join-form input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.page-wrap {
|
.page-wrap {
|
||||||
padding: 16px 14px 24px;
|
padding: 16px 14px 24px;
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-10
@@ -87,6 +87,7 @@ export class PlayApp {
|
|||||||
this.displayBoardSize = 640;
|
this.displayBoardSize = 640;
|
||||||
this.squareSize = 80;
|
this.squareSize = 80;
|
||||||
this.dpr = Math.max(1, window.devicePixelRatio || 1);
|
this.dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
|
this.urlJoinConsumed = false;
|
||||||
|
|
||||||
this.loadPieceImages();
|
this.loadPieceImages();
|
||||||
this.installControls();
|
this.installControls();
|
||||||
@@ -127,6 +128,7 @@ export class PlayApp {
|
|||||||
|
|
||||||
installSocket() {
|
installSocket() {
|
||||||
this.socket = createWSClient({
|
this.socket = createWSClient({
|
||||||
|
onServerReady: () => this.consumeJoinCodeFromUrl(),
|
||||||
onGameStarted: handleGameStarted,
|
onGameStarted: handleGameStarted,
|
||||||
onP2Connected: handleP2Connected,
|
onP2Connected: handleP2Connected,
|
||||||
onGameCreated: handleCodeGameCreated,
|
onGameCreated: handleCodeGameCreated,
|
||||||
@@ -170,16 +172,7 @@ export class PlayApp {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.joinBtn.addEventListener("click", () => {
|
this.joinBtn.addEventListener("click", () => {
|
||||||
const code = this.joinInput.value.trim().toUpperCase();
|
this.joinCode(this.joinInput.value);
|
||||||
if (!code) {
|
|
||||||
this.showModal("Join game", "Enter a game code first.", [
|
|
||||||
{ label: "OK" },
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.gameCode = code;
|
|
||||||
this.socket.emit("join_code_game", { code });
|
|
||||||
this.setLobbyStatus("Joining game...");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.copyCodeBtn.addEventListener("click", async () => {
|
this.copyCodeBtn.addEventListener("click", async () => {
|
||||||
@@ -358,6 +351,40 @@ export class PlayApp {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
joinCode(rawCode) {
|
||||||
|
const code = String(rawCode || "").trim().toUpperCase();
|
||||||
|
if (!code) {
|
||||||
|
this.showModal("Join game", "Enter a game code first.", [
|
||||||
|
{ label: "OK" },
|
||||||
|
]);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gameCode = code;
|
||||||
|
this.joinInput.value = code;
|
||||||
|
this.socket.emit("join_code_game", { code });
|
||||||
|
this.setLobbyStatus("Joining game...");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeJoinCodeFromUrl() {
|
||||||
|
if (this.urlJoinConsumed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const code = url.searchParams.get("code");
|
||||||
|
if (!code) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.urlJoinConsumed = true;
|
||||||
|
url.searchParams.delete("code");
|
||||||
|
const nextUrl = `${url.pathname}${url.search}${url.hash}`;
|
||||||
|
window.history.replaceState({}, "", nextUrl);
|
||||||
|
this.joinCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
onCodeCreated(data) {
|
onCodeCreated(data) {
|
||||||
this.gameCode = data.code;
|
this.gameCode = data.code;
|
||||||
this.codeEl.textContent = data.code;
|
this.codeEl.textContent = data.code;
|
||||||
|
|||||||
@@ -10,4 +10,20 @@
|
|||||||
>Past games</a
|
>Past games</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="home-join">
|
||||||
|
<form
|
||||||
|
class="quick-join-form"
|
||||||
|
action="{{ url_for('main.play') }}"
|
||||||
|
method="get"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="code"
|
||||||
|
maxlength="6"
|
||||||
|
placeholder="Game code"
|
||||||
|
aria-label="Join by code"
|
||||||
|
/>
|
||||||
|
<button class="btn btn-secondary" type="submit">Join</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user