Merge branch 'main' into HEAD

This commit is contained in:
simoncreates
2026-03-03 20:17:54 +01:00
6 changed files with 251 additions and 174 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")
+36 -160
View File
@@ -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
+31
View File
@@ -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
View File
@@ -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;
+16
View File
@@ -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 %}