mancala/packages/core/tests/clock/Clock.test.ts

137 lines
5.8 KiB
TypeScript
Raw Normal View History

import { CommonMancalaGame } from "mancala.js";
import { ClockSync, formatClock, getClockAtMove, getClockTimes } from "../../src/clock";
import { Game, GameClock, GameRecord, MoveRecord } from "../../src/models";
const P1 = "player-1";
const P2 = "player-2";
const MINUTE = 60_000;
const clock = (overrides: Partial<GameClock> = {}): GameClock =>
({ initialMs: 5 * MINUTE, player1Ms: 5 * MINUTE, player2Ms: 5 * MINUTE, turnStartedAt: 1_000_000, ...overrides });
const liveGame = (turnPlayerId: string, gameClock: GameClock | undefined, serverTime?: number): Game => {
const mancalaGame = new CommonMancalaGame("game-1", P1, P2);
mancalaGame.turnPlayerId = turnPlayerId;
return {
id: "game-1",
mancalaGame,
gameUsersConnectionInfo: {
user1ConnectionInfo: { userId: P1, isOnline: true },
user2ConnectionInfo: { userId: P2, isOnline: true },
},
clock: gameClock,
serverTime,
};
};
const move = (playerId: string, remainingMs?: number): MoveRecord =>
({ playerId, moveIndex: 0, boardSnapshot: [], gameSteps: [], at: 0, remainingMs });
const record = (overrides: Partial<GameRecord>): GameRecord => ({
id: "game-1",
player1: { userId: P1, isGuest: true },
player2: { userId: P2, isGuest: true },
board: [],
result: "player1",
endReason: "completed",
startedAt: 0,
endedAt: 0,
moveCount: 0,
peakSpectators: 0,
totalSpectators: 0,
moves: [],
clockInitialMs: 5 * MINUTE,
...overrides,
});
describe("getClockTimes", () => {
it("deducts the time since the turn started from the running seat only", () => {
expect(getClockTimes(clock(), "player1", 1_000_000 + 20_000)).toEqual({ player1Ms: 280_000, player2Ms: 300_000 });
expect(getClockTimes(clock(), "player2", 1_000_000 + 20_000)).toEqual({ player1Ms: 300_000, player2Ms: 280_000 });
});
it("does not count down when no clock runs", () => {
expect(getClockTimes(clock({ turnStartedAt: undefined }), "player1", 5_000_000)).toEqual({ player1Ms: 300_000, player2Ms: 300_000 });
expect(getClockTimes(clock(), undefined, 5_000_000)).toEqual({ player1Ms: 300_000, player2Ms: 300_000 });
});
it("never goes below zero", () => {
expect(getClockTimes(clock({ player1Ms: 1_000 }), "player1", 1_000_000 + 60_000).player1Ms).toBe(0);
});
});
describe("formatClock", () => {
it("shows minutes and seconds, rounding up", () => {
expect(formatClock(300_000)).toBe("5:00");
expect(formatClock(299_001)).toBe("5:00");
expect(formatClock(299_000)).toBe("4:59");
expect(formatClock(9_500)).toBe("0:10");
expect(formatClock(1)).toBe("0:01");
expect(formatClock(0)).toBe("0:00");
expect(formatClock(-5)).toBe("0:00");
});
});
describe("ClockSync", () => {
it("counts down the running clock in server time, whatever the local clock says", () => {
const sync = new ClockSync();
const localTimeAtReceive = 1_000_000 + 10 * MINUTE; // the device clock is 10 minutes fast
sync.update(liveGame(P2, clock({ turnStartedAt: 1_000_000 }), 1_000_000 + 5_000), localTimeAtReceive);
expect(sync.isRunning).toBe(true);
expect(sync.getTimes(localTimeAtReceive)).toEqual({ player1Ms: 300_000, player2Ms: 295_000, runningSeat: "player2" });
expect(sync.getTimes(localTimeAtReceive + 3_000)).toEqual({ player1Ms: 300_000, player2Ms: 292_000, runningSeat: "player2" });
});
it("stops with the clocks the server sends when the game ends", () => {
const sync = new ClockSync();
sync.update(liveGame(P1, clock(), 1_000_000), 1_000_000);
sync.stop(clock({ player1Ms: 0, turnStartedAt: undefined }));
expect(sync.isRunning).toBe(false);
expect(sync.getTimes(9_000_000)).toEqual({ player1Ms: 0, player2Ms: 300_000, runningSeat: undefined });
});
it("does not run once the game has ended", () => {
const game = liveGame(P1, clock(), 1_000_000);
game.mancalaGame.state = "ended";
const sync = new ClockSync();
sync.update(game, 1_000_000);
expect(sync.isRunning).toBe(false);
expect(sync.getTimes(2_000_000)?.player1Ms).toBe(300_000);
});
it("has no times when the server sends no clock", () => {
const sync = new ClockSync();
sync.update(liveGame(P1, undefined), 0);
expect(sync.getTimes()).toBeUndefined();
expect(sync.isRunning).toBe(false);
});
});
describe("getClockAtMove", () => {
const moves = [move(P1, 290_000), move(P2, 280_000), move(P2, 270_000), move(P1, 250_000)];
it("starts with the initial time", () => {
expect(getClockAtMove(record({ moves }), 0)).toEqual({ player1Ms: 300_000, player2Ms: 300_000 });
});
it("shows each player's time after their last move so far", () => {
expect(getClockAtMove(record({ moves }), 1)).toEqual({ player1Ms: 290_000, player2Ms: 300_000 });
expect(getClockAtMove(record({ moves }), 3)).toEqual({ player1Ms: 290_000, player2Ms: 270_000 });
expect(getClockAtMove(record({ moves }), 4)).toEqual({ player1Ms: 250_000, player2Ms: 270_000 });
expect(getClockAtMove(record({ moves }), 99)).toEqual({ player1Ms: 250_000, player2Ms: 270_000 });
});
it("shows the player who ran out of time at zero in the final position", () => {
const timedOut = record({ moves, endReason: "timeout", result: "player1" });
expect(getClockAtMove(timedOut, 4)).toEqual({ player1Ms: 250_000, player2Ms: 0 });
expect(getClockAtMove(timedOut, 3)).toEqual({ player1Ms: 290_000, player2Ms: 270_000 });
});
it("is undefined for games saved before the game clock", () => {
expect(getClockAtMove(record({ moves: [move(P1)], clockInitialMs: undefined }), 1)).toBeUndefined();
});
});