Add spectator watch page, forfeit-on-new-game, CPU self-play, and threefold repetition

- /watch live feed of active games with click-to-fullscreen and per-game back button
- Starting a new game forfeits the in-progress game; opponent is flagged the winner
- Watch CPU vs CPU self-play games; results shown for ~30s before cleanup
- Detect threefold repetition in the C# rules layer and end the game as a draw

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Josh-Heaps
2026-06-08 16:21:49 -06:00
co-authored by Claude Opus 4.8
parent fc2a2e122b
commit 2db95cec34
15 changed files with 642 additions and 5 deletions
@@ -35,6 +35,9 @@ public class ChessService : IChessService
gameState.CurrentPlayer = PieceColor.White;
UpdateCheckStatus(gameState);
gameState.PositionHistory.Clear();
gameState.PositionHistory.Add(PositionKey(gameState));
}
private static void SetupBlackPieces(GameState gs)
@@ -144,16 +147,33 @@ public class ChessService : IChessService
var notation = $"{piece.Id}:{piece.Position}->{targetPos}";
gameState.MoveHistory.Add(notation);
var positionKey = PositionKey(gameState);
gameState.PositionHistory.Add(positionKey);
if (gameState.PositionHistory.Count(k => k == positionKey) >= 3)
gameState.IsThreefoldRepetition = true;
return new MoveResultDto
{
Success = true,
Message = "Move successful.",
IsCheck = gameState.IsCheck,
IsCheckmate = gameState.IsCheckmate,
IsStalemate = gameState.IsStalemate
IsStalemate = gameState.IsStalemate,
IsThreefoldRepetition = gameState.IsThreefoldRepetition
};
}
/// <summary>
/// The repetition signature of a position: the first four FEN fields — piece placement,
/// side to move, castling rights, and en-passant target. Move counters are excluded.
/// </summary>
private static string PositionKey(GameState gs)
{
var fields = gs.ToFen().Split(' ');
return string.Join(' ', fields.Take(4));
}
private static void PerformMove(GameState gs, ChessPiece piece, Position targetPos, MoveDto moveDto)
{
var oldPos = piece.Position;