Skip to content

Commit efdf6f5

Browse files
committed
fix(clock): only flag the player whose turn it is
After the first-move deduction fix, both players had identical stored times (5000ms). When the clock expired, checkTimeout reported both players as timed out because it didn't verify whose turn it was. Whichever flag message arrived first determined the winner — on CI, black's flag won the race, producing "Black wins" instead of "White wins". Add a FEN turn check to checkTimeout: only the player whose clock is actively ticking (the one whose turn it is) can time out.
1 parent 693db1f commit efdf6f5

2 files changed

Lines changed: 27 additions & 3 deletions

File tree

lib/game-session.test.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,34 +1077,52 @@ describe("game-session", () => {
10771077
whiteTimeMs: "5000",
10781078
blackTimeMs: "5000",
10791079
lastMoveAt: "0",
1080+
currentFen: "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
10801081
});
10811082

10821083
const result = await checkTimeout(TEST_GAME_ID, "white", Date.now());
10831084
expect(result.timedOut).toBe(false);
10841085
});
10851086

1086-
it("returns timed out when clock has expired", async () => {
1087+
it("returns timed out when active player's clock has expired", async () => {
10871088
const lastMoveAt = Date.now() - 6000;
1089+
// FEN has "b" turn — black's clock is ticking
10881090
mockRedis.hgetall.mockResolvedValueOnce({
10891091
whiteTimeMs: "5000",
10901092
blackTimeMs: "5000",
10911093
lastMoveAt: String(lastMoveAt),
1094+
currentFen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
10921095
});
10931096

1094-
const result = await checkTimeout(TEST_GAME_ID, "white", Date.now());
1097+
const result = await checkTimeout(TEST_GAME_ID, "black", Date.now());
10951098
expect(result.timedOut).toBe(true);
10961099
expect(result.remainingMs).toBe(0);
10971100
});
10981101

1102+
it("returns not timed out when it is not the checked player's turn", async () => {
1103+
const lastMoveAt = Date.now() - 6000;
1104+
// FEN has "b" turn — black's clock is ticking, not white's
1105+
mockRedis.hgetall.mockResolvedValueOnce({
1106+
whiteTimeMs: "5000",
1107+
blackTimeMs: "5000",
1108+
lastMoveAt: String(lastMoveAt),
1109+
currentFen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
1110+
});
1111+
1112+
const result = await checkTimeout(TEST_GAME_ID, "white", Date.now());
1113+
expect(result.timedOut).toBe(false);
1114+
});
1115+
10991116
it("returns not timed out when clock has time remaining", async () => {
11001117
const lastMoveAt = Date.now() - 2000;
11011118
mockRedis.hgetall.mockResolvedValueOnce({
11021119
whiteTimeMs: "5000",
11031120
blackTimeMs: "5000",
11041121
lastMoveAt: String(lastMoveAt),
1122+
currentFen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
11051123
});
11061124

1107-
const result = await checkTimeout(TEST_GAME_ID, "white", Date.now());
1125+
const result = await checkTimeout(TEST_GAME_ID, "black", Date.now());
11081126
expect(result.timedOut).toBe(false);
11091127
expect(result.remainingMs).toBeGreaterThan(0);
11101128
});

lib/game-session.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,12 @@ export async function checkTimeout(
600600
// Clock hasn't started yet (no moves made)
601601
if (lastMoveAt === 0) return { timedOut: false, remainingMs: 0 };
602602

603+
// Only the player whose turn it is can time out
604+
const fen = data.currentFen || "";
605+
const turnChar = fen.split(" ")[1];
606+
const turnColor = turnChar === "w" ? "white" : "black";
607+
if (activeColor !== turnColor) return { timedOut: false, remainingMs: 0 };
608+
603609
const timeField =
604610
activeColor === "white" ? "whiteTimeMs" : "blackTimeMs";
605611
const remaining = parseInt(data[timeField] || "0", 10);

0 commit comments

Comments
 (0)