GameClock and optional clock fields on live games, end messages, move records and finished games; getClockTimes, ClockSync, getClockAtMove and formatClock with tests.
137 lines
5.8 KiB
TypeScript
137 lines
5.8 KiB
TypeScript
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();
|
|
});
|
|
});
|