diff options
Diffstat (limited to 'frontend/app/states')
| -rw-r--r-- | frontend/app/states/play.test.ts | 318 | ||||
| -rw-r--r-- | frontend/app/states/play.ts | 146 | ||||
| -rw-r--r-- | frontend/app/states/watch.test.ts | 350 | ||||
| -rw-r--r-- | frontend/app/states/watch.ts | 178 |
4 files changed, 496 insertions, 496 deletions
diff --git a/frontend/app/states/play.test.ts b/frontend/app/states/play.test.ts index 0f73039..299cb22 100644 --- a/frontend/app/states/play.test.ts +++ b/frontend/app/states/play.test.ts @@ -1,194 +1,194 @@ import { createStore } from "jotai"; import { describe, expect, test } from "vitest"; import { - calcCodeSize, - gameStateKindAtom, - gamingLeftTimeSecondsAtom, - handleSubmitCodePostAtom, - handleSubmitCodePreAtom, - scoreAtom, - setCurrentTimestampAtom, - setDurationSecondsAtom, - setGameStartedAtAtom, - setLatestGameStateAtom, - startingLeftTimeSecondsAtom, - statusAtom, + calcCodeSize, + gameStateKindAtom, + gamingLeftTimeSecondsAtom, + handleSubmitCodePostAtom, + handleSubmitCodePreAtom, + scoreAtom, + setCurrentTimestampAtom, + setDurationSecondsAtom, + setGameStartedAtAtom, + setLatestGameStateAtom, + startingLeftTimeSecondsAtom, + statusAtom, } from "./play"; describe("calcCodeSize", () => { - test("counts UTF-8 bytes after removing whitespace (swift)", () => { - expect(calcCodeSize("print(1)", "swift")).toBe(8); - }); + test("counts UTF-8 bytes after removing whitespace (swift)", () => { + expect(calcCodeSize("print(1)", "swift")).toBe(8); + }); - test("removes all whitespace for swift", () => { - expect(calcCodeSize("print( 1 )\n", "swift")).toBe(8); - }); + test("removes all whitespace for swift", () => { + expect(calcCodeSize("print( 1 )\n", "swift")).toBe(8); + }); - test("removes <?php tag for php", () => { - expect(calcCodeSize("<?php echo 1;", "php")).toBe(6); - }); + test("removes <?php tag for php", () => { + expect(calcCodeSize("<?php echo 1;", "php")).toBe(6); + }); - test("removes <? short tag for php", () => { - expect(calcCodeSize("<? echo 1;", "php")).toBe(6); - }); + test("removes <? short tag for php", () => { + expect(calcCodeSize("<? echo 1;", "php")).toBe(6); + }); - test("removes ?> closing tag for php", () => { - expect(calcCodeSize("<?php echo 1;?>", "php")).toBe(6); - }); + test("removes ?> closing tag for php", () => { + expect(calcCodeSize("<?php echo 1;?>", "php")).toBe(6); + }); - test("removes whitespace and tags together for php", () => { - expect(calcCodeSize("<?php\n echo 1; \n?>", "php")).toBe(6); - }); + test("removes whitespace and tags together for php", () => { + expect(calcCodeSize("<?php\n echo 1; \n?>", "php")).toBe(6); + }); - test("returns 0 for empty string", () => { - expect(calcCodeSize("", "swift")).toBe(0); - }); + test("returns 0 for empty string", () => { + expect(calcCodeSize("", "swift")).toBe(0); + }); - test("counts multi-byte characters correctly", () => { - // "あ" is 3 bytes in UTF-8 - expect(calcCodeSize("あ", "swift")).toBe(3); - }); + test("counts multi-byte characters correctly", () => { + // "あ" is 3 bytes in UTF-8 + expect(calcCodeSize("あ", "swift")).toBe(3); + }); - test("php with only tags and whitespace returns 0", () => { - expect(calcCodeSize("<?php ?>", "php")).toBe(0); - }); + test("php with only tags and whitespace returns 0", () => { + expect(calcCodeSize("<?php ?>", "php")).toBe(0); + }); }); describe("Jotai atoms", () => { - test("gameStateKindAtom returns 'loading' initially", () => { - const store = createStore(); - expect(store.get(gameStateKindAtom)).toBe("loading"); - }); + test("gameStateKindAtom returns 'loading' initially", () => { + const store = createStore(); + expect(store.get(gameStateKindAtom)).toBe("loading"); + }); - test("gameStateKindAtom returns 'waiting' when timestamp set but no startedAt", () => { - const store = createStore(); - store.set(setCurrentTimestampAtom); - expect(store.get(gameStateKindAtom)).toBe("waiting"); - }); + test("gameStateKindAtom returns 'waiting' when timestamp set but no startedAt", () => { + const store = createStore(); + store.set(setCurrentTimestampAtom); + expect(store.get(gameStateKindAtom)).toBe("waiting"); + }); - test("gameStateKindAtom returns 'starting' when now < startedAt", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - store.set(setGameStartedAtAtom, now + 60); - store.set(setDurationSecondsAtom, 300); - expect(store.get(gameStateKindAtom)).toBe("starting"); - }); + test("gameStateKindAtom returns 'starting' when now < startedAt", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); + store.set(setCurrentTimestampAtom); + store.set(setGameStartedAtAtom, now + 60); + store.set(setDurationSecondsAtom, 300); + expect(store.get(gameStateKindAtom)).toBe("starting"); + }); - test("gameStateKindAtom returns 'gaming' when now >= startedAt and now < finishedAt", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - store.set(setGameStartedAtAtom, now - 10); - store.set(setDurationSecondsAtom, 300); - expect(store.get(gameStateKindAtom)).toBe("gaming"); - }); + test("gameStateKindAtom returns 'gaming' when now >= startedAt and now < finishedAt", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); + store.set(setCurrentTimestampAtom); + store.set(setGameStartedAtAtom, now - 10); + store.set(setDurationSecondsAtom, 300); + expect(store.get(gameStateKindAtom)).toBe("gaming"); + }); - test("gameStateKindAtom returns 'finished' when now >= finishedAt", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - store.set(setGameStartedAtAtom, now - 400); - store.set(setDurationSecondsAtom, 300); - expect(store.get(gameStateKindAtom)).toBe("finished"); - }); + test("gameStateKindAtom returns 'finished' when now >= finishedAt", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); + store.set(setCurrentTimestampAtom); + store.set(setGameStartedAtAtom, now - 400); + store.set(setDurationSecondsAtom, 300); + expect(store.get(gameStateKindAtom)).toBe("finished"); + }); - test("startingLeftTimeSecondsAtom returns null when startedAt is null", () => { - const store = createStore(); - expect(store.get(startingLeftTimeSecondsAtom)).toBeNull(); - }); + test("startingLeftTimeSecondsAtom returns null when startedAt is null", () => { + const store = createStore(); + expect(store.get(startingLeftTimeSecondsAtom)).toBeNull(); + }); - test("startingLeftTimeSecondsAtom returns null when currentTimestamp is null", () => { - const store = createStore(); - store.set(setGameStartedAtAtom, 1000); - expect(store.get(startingLeftTimeSecondsAtom)).toBeNull(); - }); + test("startingLeftTimeSecondsAtom returns null when currentTimestamp is null", () => { + const store = createStore(); + store.set(setGameStartedAtAtom, 1000); + expect(store.get(startingLeftTimeSecondsAtom)).toBeNull(); + }); - test("startingLeftTimeSecondsAtom returns remaining time before start", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - store.set(setGameStartedAtAtom, now + 30); - const leftTime = store.get(startingLeftTimeSecondsAtom); - expect(leftTime).toBeGreaterThanOrEqual(29); - expect(leftTime).toBeLessThanOrEqual(30); - }); + test("startingLeftTimeSecondsAtom returns remaining time before start", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); + store.set(setCurrentTimestampAtom); + store.set(setGameStartedAtAtom, now + 30); + const leftTime = store.get(startingLeftTimeSecondsAtom); + expect(leftTime).toBeGreaterThanOrEqual(29); + expect(leftTime).toBeLessThanOrEqual(30); + }); - test("startingLeftTimeSecondsAtom returns 0 when past start time", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - store.set(setGameStartedAtAtom, now - 10); - expect(store.get(startingLeftTimeSecondsAtom)).toBe(0); - }); + test("startingLeftTimeSecondsAtom returns 0 when past start time", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); + store.set(setCurrentTimestampAtom); + store.set(setGameStartedAtAtom, now - 10); + expect(store.get(startingLeftTimeSecondsAtom)).toBe(0); + }); - test("gamingLeftTimeSecondsAtom returns null when startedAt is null", () => { - const store = createStore(); - expect(store.get(gamingLeftTimeSecondsAtom)).toBeNull(); - }); + test("gamingLeftTimeSecondsAtom returns null when startedAt is null", () => { + const store = createStore(); + expect(store.get(gamingLeftTimeSecondsAtom)).toBeNull(); + }); - test("gamingLeftTimeSecondsAtom returns remaining game time", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - store.set(setGameStartedAtAtom, now - 10); - store.set(setDurationSecondsAtom, 300); - const leftTime = store.get(gamingLeftTimeSecondsAtom); - expect(leftTime).toBeGreaterThanOrEqual(289); - expect(leftTime).toBeLessThanOrEqual(290); - }); + test("gamingLeftTimeSecondsAtom returns remaining game time", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); + store.set(setCurrentTimestampAtom); + store.set(setGameStartedAtAtom, now - 10); + store.set(setDurationSecondsAtom, 300); + const leftTime = store.get(gamingLeftTimeSecondsAtom); + expect(leftTime).toBeGreaterThanOrEqual(289); + expect(leftTime).toBeLessThanOrEqual(290); + }); - test("gamingLeftTimeSecondsAtom clamps to 0 when game is over", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - store.set(setGameStartedAtAtom, now - 400); - store.set(setDurationSecondsAtom, 300); - expect(store.get(gamingLeftTimeSecondsAtom)).toBe(0); - }); + test("gamingLeftTimeSecondsAtom clamps to 0 when game is over", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); + store.set(setCurrentTimestampAtom); + store.set(setGameStartedAtAtom, now - 400); + store.set(setDurationSecondsAtom, 300); + expect(store.get(gamingLeftTimeSecondsAtom)).toBe(0); + }); - test("gamingLeftTimeSecondsAtom clamps to duration before start", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - store.set(setGameStartedAtAtom, now + 60); - store.set(setDurationSecondsAtom, 300); - expect(store.get(gamingLeftTimeSecondsAtom)).toBe(300); - }); + test("gamingLeftTimeSecondsAtom clamps to duration before start", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); + store.set(setCurrentTimestampAtom); + store.set(setGameStartedAtAtom, now + 60); + store.set(setDurationSecondsAtom, 300); + expect(store.get(gamingLeftTimeSecondsAtom)).toBe(300); + }); - test("statusAtom returns 'running' when submitting code", () => { - const store = createStore(); - store.set(handleSubmitCodePreAtom); - expect(store.get(statusAtom)).toBe("running"); - }); + test("statusAtom returns 'running' when submitting code", () => { + const store = createStore(); + store.set(handleSubmitCodePreAtom); + expect(store.get(statusAtom)).toBe("running"); + }); - test("statusAtom returns raw status when not submitting", () => { - const store = createStore(); - expect(store.get(statusAtom)).toBe("none"); - }); + test("statusAtom returns raw status when not submitting", () => { + const store = createStore(); + expect(store.get(statusAtom)).toBe("none"); + }); - test("handleSubmitCodePostAtom resets submitting state", () => { - const store = createStore(); - store.set(handleSubmitCodePreAtom); - expect(store.get(statusAtom)).toBe("running"); - store.set(handleSubmitCodePostAtom); - expect(store.get(statusAtom)).toBe("none"); - }); + test("handleSubmitCodePostAtom resets submitting state", () => { + const store = createStore(); + store.set(handleSubmitCodePreAtom); + expect(store.get(statusAtom)).toBe("running"); + store.set(handleSubmitCodePostAtom); + expect(store.get(statusAtom)).toBe("none"); + }); - test("setLatestGameStateAtom updates status and score", () => { - const store = createStore(); - store.set(setLatestGameStateAtom, { - code: "", - status: "success", - score: 42, - best_score_submitted_at: null, - }); - expect(store.get(statusAtom)).toBe("success"); - expect(store.get(scoreAtom)).toBe(42); - }); + test("setLatestGameStateAtom updates status and score", () => { + const store = createStore(); + store.set(setLatestGameStateAtom, { + code: "", + status: "success", + score: 42, + best_score_submitted_at: null, + }); + expect(store.get(statusAtom)).toBe("success"); + expect(store.get(scoreAtom)).toBe(42); + }); - test("scoreAtom returns null initially", () => { - const store = createStore(); - expect(store.get(scoreAtom)).toBeNull(); - }); + test("scoreAtom returns null initially", () => { + const store = createStore(); + expect(store.get(scoreAtom)).toBeNull(); + }); }); diff --git a/frontend/app/states/play.ts b/frontend/app/states/play.ts index 22d338c..a8a4727 100644 --- a/frontend/app/states/play.ts +++ b/frontend/app/states/play.ts @@ -4,121 +4,121 @@ import type { SupportedLanguage } from "../types/SupportedLanguage"; const gameStartedAtAtom = atom<number | null>(null); export const setGameStartedAtAtom = atom(null, (_, set, value: number | null) => - set(gameStartedAtAtom, value), + set(gameStartedAtAtom, value), ); export type GameStateKind = - | "loading" - | "waiting" - | "starting" - | "gaming" - | "finished"; + | "loading" + | "waiting" + | "starting" + | "gaming" + | "finished"; type ExecutionStatus = components["schemas"]["ExecutionStatus"]; type LatestGameState = components["schemas"]["LatestGameState"]; export const gameStateKindAtom = atom<GameStateKind>((get) => { - const now = get(currentTimestampAtom); - if (!now) { - return "loading"; - } - const startedAt = get(gameStartedAtAtom); - if (!startedAt) { - return "waiting"; - } - const durationSeconds = get(durationSecondsAtom); - const finishedAt = startedAt + durationSeconds; - if (now < startedAt) { - return "starting"; - } else if (now < finishedAt) { - return "gaming"; - } else { - return "finished"; - } + const now = get(currentTimestampAtom); + if (!now) { + return "loading"; + } + const startedAt = get(gameStartedAtAtom); + if (!startedAt) { + return "waiting"; + } + const durationSeconds = get(durationSecondsAtom); + const finishedAt = startedAt + durationSeconds; + if (now < startedAt) { + return "starting"; + } else if (now < finishedAt) { + return "gaming"; + } else { + return "finished"; + } }); const currentTimestampAtom = atom<number | null>(null); export const setCurrentTimestampAtom = atom(null, (_, set) => - set(currentTimestampAtom, Math.floor(Date.now() / 1000)), + set(currentTimestampAtom, Math.floor(Date.now() / 1000)), ); const durationSecondsAtom = atom<number>(0); export const setDurationSecondsAtom = atom(null, (_, set, value: number) => - set(durationSecondsAtom, value), + set(durationSecondsAtom, value), ); export const startingLeftTimeSecondsAtom = atom<number | null>((get) => { - const startedAt = get(gameStartedAtAtom); - if (startedAt === null) { - return null; - } - const currentTimestamp = get(currentTimestampAtom); - if (currentTimestamp === null) { - return null; - } - return Math.max(0, startedAt - currentTimestamp); + const startedAt = get(gameStartedAtAtom); + if (startedAt === null) { + return null; + } + const currentTimestamp = get(currentTimestampAtom); + if (currentTimestamp === null) { + return null; + } + return Math.max(0, startedAt - currentTimestamp); }); export const gamingLeftTimeSecondsAtom = atom<number | null>((get) => { - const startedAt = get(gameStartedAtAtom); - if (startedAt === null) { - return null; - } - const durationSeconds = get(durationSecondsAtom); - const finishedAt = startedAt + durationSeconds; - const currentTimestamp = get(currentTimestampAtom); - if (currentTimestamp === null) { - return null; - } - return Math.min(durationSeconds, Math.max(0, finishedAt - currentTimestamp)); + const startedAt = get(gameStartedAtAtom); + if (startedAt === null) { + return null; + } + const durationSeconds = get(durationSecondsAtom); + const finishedAt = startedAt + durationSeconds; + const currentTimestamp = get(currentTimestampAtom); + if (currentTimestamp === null) { + return null; + } + return Math.min(durationSeconds, Math.max(0, finishedAt - currentTimestamp)); }); const rawStatusAtom = atom<ExecutionStatus>("none"); const rawScoreAtom = atom<number | null>(null); export const statusAtom = atom<ExecutionStatus>((get) => { - const isSubmittingCode = get(isSubmittingCodeAtom); - if (isSubmittingCode) { - return "running"; - } else { - return get(rawStatusAtom); - } + const isSubmittingCode = get(isSubmittingCodeAtom); + if (isSubmittingCode) { + return "running"; + } else { + return get(rawStatusAtom); + } }); export const scoreAtom = atom<number | null>((get) => { - return get(rawScoreAtom); + return get(rawScoreAtom); }); const isSubmittingCodeAtom = atom(false); export const handleSubmitCodePreAtom = atom(null, (_, set) => { - set(isSubmittingCodeAtom, true); + set(isSubmittingCodeAtom, true); }); export const handleSubmitCodePostAtom = atom(null, (_, set) => { - set(isSubmittingCodeAtom, false); + set(isSubmittingCodeAtom, false); }); export const setLatestGameStateAtom = atom( - null, - (_, set, value: LatestGameState) => { - set(rawStatusAtom, value.status); - set(rawScoreAtom, value.score); - }, + null, + (_, set, value: LatestGameState) => { + set(rawStatusAtom, value.status); + set(rawScoreAtom, value.score); + }, ); function cleanCode(code: string, language: SupportedLanguage) { - if (language === "php") { - return code - .replace(/\s+/g, "") - .replace(/^<\?php/, "") - .replace(/^<\?/, "") - .replace(/\?>$/, ""); - } else { - return code.replace(/\s+/g, ""); - } + if (language === "php") { + return code + .replace(/\s+/g, "") + .replace(/^<\?php/, "") + .replace(/^<\?/, "") + .replace(/\?>$/, ""); + } else { + return code.replace(/\s+/g, ""); + } } export function calcCodeSize( - code: string, - language: SupportedLanguage, + code: string, + language: SupportedLanguage, ): number { - const trimmed = cleanCode(code, language); - const utf8Encoded = new TextEncoder().encode(trimmed); - return utf8Encoded.length; + const trimmed = cleanCode(code, language); + const utf8Encoded = new TextEncoder().encode(trimmed); + return utf8Encoded.length; } diff --git a/frontend/app/states/watch.test.ts b/frontend/app/states/watch.test.ts index dae1cb9..db33c87 100644 --- a/frontend/app/states/watch.test.ts +++ b/frontend/app/states/watch.test.ts @@ -1,206 +1,206 @@ import { createStore } from "jotai"; import { describe, expect, test } from "vitest"; import { - calcCodeSize, - checkGameResultKind, - gameStateKindAtom, - gamingLeftTimeSecondsAtom, - latestGameStatesAtom, - rankingAtom, - setCurrentTimestampAtom, - setDurationSecondsAtom, - setGameStartedAtAtom, - setLatestGameStatesAtom, - startingLeftTimeSecondsAtom, + calcCodeSize, + checkGameResultKind, + gameStateKindAtom, + gamingLeftTimeSecondsAtom, + latestGameStatesAtom, + rankingAtom, + setCurrentTimestampAtom, + setDurationSecondsAtom, + setGameStartedAtAtom, + setLatestGameStatesAtom, + startingLeftTimeSecondsAtom, } from "./watch"; describe("checkGameResultKind", () => { - test("returns null when game is not finished", () => { - expect(checkGameResultKind("gaming", null, null)).toBeNull(); - expect(checkGameResultKind("waiting", null, null)).toBeNull(); - expect(checkGameResultKind("starting", null, null)).toBeNull(); - expect(checkGameResultKind("loading", null, null)).toBeNull(); - }); + test("returns null when game is not finished", () => { + expect(checkGameResultKind("gaming", null, null)).toBeNull(); + expect(checkGameResultKind("waiting", null, null)).toBeNull(); + expect(checkGameResultKind("starting", null, null)).toBeNull(); + expect(checkGameResultKind("loading", null, null)).toBeNull(); + }); - test("returns draw when both scores are null", () => { - expect(checkGameResultKind("finished", null, null)).toBe("draw"); - }); + test("returns draw when both scores are null", () => { + expect(checkGameResultKind("finished", null, null)).toBe("draw"); + }); - test("returns draw when both states have null scores", () => { - const stateA = { - code: "", - status: "none" as const, - score: null, - best_score_submitted_at: null, - }; - const stateB = { - code: "", - status: "none" as const, - score: null, - best_score_submitted_at: null, - }; - expect(checkGameResultKind("finished", stateA, stateB)).toBe("draw"); - }); + test("returns draw when both states have null scores", () => { + const stateA = { + code: "", + status: "none" as const, + score: null, + best_score_submitted_at: null, + }; + const stateB = { + code: "", + status: "none" as const, + score: null, + best_score_submitted_at: null, + }; + expect(checkGameResultKind("finished", stateA, stateB)).toBe("draw"); + }); - test("returns winB when only A has null score", () => { - const stateA = { - code: "", - status: "none" as const, - score: null, - best_score_submitted_at: null, - }; - const stateB = { - code: "echo 1;", - status: "success" as const, - score: 10, - best_score_submitted_at: 1000, - }; - expect(checkGameResultKind("finished", stateA, stateB)).toBe("winB"); - }); + test("returns winB when only A has null score", () => { + const stateA = { + code: "", + status: "none" as const, + score: null, + best_score_submitted_at: null, + }; + const stateB = { + code: "echo 1;", + status: "success" as const, + score: 10, + best_score_submitted_at: 1000, + }; + expect(checkGameResultKind("finished", stateA, stateB)).toBe("winB"); + }); - test("returns winA when only B has null score", () => { - const stateA = { - code: "echo 1;", - status: "success" as const, - score: 10, - best_score_submitted_at: 1000, - }; - const stateB = { - code: "", - status: "none" as const, - score: null, - best_score_submitted_at: null, - }; - expect(checkGameResultKind("finished", stateA, stateB)).toBe("winA"); - }); + test("returns winA when only B has null score", () => { + const stateA = { + code: "echo 1;", + status: "success" as const, + score: 10, + best_score_submitted_at: 1000, + }; + const stateB = { + code: "", + status: "none" as const, + score: null, + best_score_submitted_at: null, + }; + expect(checkGameResultKind("finished", stateA, stateB)).toBe("winA"); + }); - test("returns winA when A has lower score (code golf)", () => { - const stateA = { - code: "a", - status: "success" as const, - score: 5, - best_score_submitted_at: 1000, - }; - const stateB = { - code: "abcdefghij", - status: "success" as const, - score: 10, - best_score_submitted_at: 1000, - }; - expect(checkGameResultKind("finished", stateA, stateB)).toBe("winA"); - }); + test("returns winA when A has lower score (code golf)", () => { + const stateA = { + code: "a", + status: "success" as const, + score: 5, + best_score_submitted_at: 1000, + }; + const stateB = { + code: "abcdefghij", + status: "success" as const, + score: 10, + best_score_submitted_at: 1000, + }; + expect(checkGameResultKind("finished", stateA, stateB)).toBe("winA"); + }); - test("returns winB when B has lower score (code golf)", () => { - const stateA = { - code: "abcdefghij", - status: "success" as const, - score: 10, - best_score_submitted_at: 1000, - }; - const stateB = { - code: "a", - status: "success" as const, - score: 5, - best_score_submitted_at: 1000, - }; - expect(checkGameResultKind("finished", stateA, stateB)).toBe("winB"); - }); + test("returns winB when B has lower score (code golf)", () => { + const stateA = { + code: "abcdefghij", + status: "success" as const, + score: 10, + best_score_submitted_at: 1000, + }; + const stateB = { + code: "a", + status: "success" as const, + score: 5, + best_score_submitted_at: 1000, + }; + expect(checkGameResultKind("finished", stateA, stateB)).toBe("winB"); + }); - test("breaks tie by earlier submission time - A wins", () => { - const stateA = { - code: "echo 1;", - status: "success" as const, - score: 10, - best_score_submitted_at: 1000, - }; - const stateB = { - code: "echo 1;", - status: "success" as const, - score: 10, - best_score_submitted_at: 1060, - }; - expect(checkGameResultKind("finished", stateA, stateB)).toBe("winA"); - }); + test("breaks tie by earlier submission time - A wins", () => { + const stateA = { + code: "echo 1;", + status: "success" as const, + score: 10, + best_score_submitted_at: 1000, + }; + const stateB = { + code: "echo 1;", + status: "success" as const, + score: 10, + best_score_submitted_at: 1060, + }; + expect(checkGameResultKind("finished", stateA, stateB)).toBe("winA"); + }); - test("breaks tie by earlier submission time - B wins", () => { - const stateA = { - code: "echo 1;", - status: "success" as const, - score: 10, - best_score_submitted_at: 1060, - }; - const stateB = { - code: "echo 1;", - status: "success" as const, - score: 10, - best_score_submitted_at: 1000, - }; - expect(checkGameResultKind("finished", stateA, stateB)).toBe("winB"); - }); + test("breaks tie by earlier submission time - B wins", () => { + const stateA = { + code: "echo 1;", + status: "success" as const, + score: 10, + best_score_submitted_at: 1060, + }; + const stateB = { + code: "echo 1;", + status: "success" as const, + score: 10, + best_score_submitted_at: 1000, + }; + expect(checkGameResultKind("finished", stateA, stateB)).toBe("winB"); + }); }); describe("watch calcCodeSize", () => { - test("works the same as play calcCodeSize", () => { - expect(calcCodeSize("<?php echo 1;", "php")).toBe(6); - expect(calcCodeSize("print(1)", "swift")).toBe(8); - }); + test("works the same as play calcCodeSize", () => { + expect(calcCodeSize("<?php echo 1;", "php")).toBe(6); + expect(calcCodeSize("print(1)", "swift")).toBe(8); + }); }); describe("watch Jotai atoms", () => { - test("gameStateKindAtom returns 'loading' initially", () => { - const store = createStore(); - expect(store.get(gameStateKindAtom)).toBe("loading"); - }); + test("gameStateKindAtom returns 'loading' initially", () => { + const store = createStore(); + expect(store.get(gameStateKindAtom)).toBe("loading"); + }); - test("gameStateKindAtom transitions through states correctly", () => { - const store = createStore(); - const now = Math.floor(Date.now() / 1000); + test("gameStateKindAtom transitions through states correctly", () => { + const store = createStore(); + const now = Math.floor(Date.now() / 1000); - store.set(setCurrentTimestampAtom); - expect(store.get(gameStateKindAtom)).toBe("waiting"); + store.set(setCurrentTimestampAtom); + expect(store.get(gameStateKindAtom)).toBe("waiting"); - store.set(setGameStartedAtAtom, now + 60); - store.set(setDurationSecondsAtom, 300); - expect(store.get(gameStateKindAtom)).toBe("starting"); + store.set(setGameStartedAtAtom, now + 60); + store.set(setDurationSecondsAtom, 300); + expect(store.get(gameStateKindAtom)).toBe("starting"); - store.set(setGameStartedAtAtom, now - 10); - expect(store.get(gameStateKindAtom)).toBe("gaming"); + store.set(setGameStartedAtAtom, now - 10); + expect(store.get(gameStateKindAtom)).toBe("gaming"); - store.set(setGameStartedAtAtom, now - 400); - expect(store.get(gameStateKindAtom)).toBe("finished"); - }); + store.set(setGameStartedAtAtom, now - 400); + expect(store.get(gameStateKindAtom)).toBe("finished"); + }); - test("rankingAtom is empty initially", () => { - const store = createStore(); - expect(store.get(rankingAtom)).toEqual([]); - }); + test("rankingAtom is empty initially", () => { + const store = createStore(); + expect(store.get(rankingAtom)).toEqual([]); + }); - test("latestGameStatesAtom is empty initially", () => { - const store = createStore(); - expect(store.get(latestGameStatesAtom)).toEqual({}); - }); + test("latestGameStatesAtom is empty initially", () => { + const store = createStore(); + expect(store.get(latestGameStatesAtom)).toEqual({}); + }); - test("setLatestGameStatesAtom updates states", () => { - const store = createStore(); - const states = { - player1: { - code: "echo 1;", - status: "success" as const, - score: 10, - best_score_submitted_at: 1000, - }, - }; - store.set(setLatestGameStatesAtom, states); - expect(store.get(latestGameStatesAtom)).toEqual(states); - }); + test("setLatestGameStatesAtom updates states", () => { + const store = createStore(); + const states = { + player1: { + code: "echo 1;", + status: "success" as const, + score: 10, + best_score_submitted_at: 1000, + }, + }; + store.set(setLatestGameStatesAtom, states); + expect(store.get(latestGameStatesAtom)).toEqual(states); + }); - test("startingLeftTimeSecondsAtom returns null initially", () => { - const store = createStore(); - expect(store.get(startingLeftTimeSecondsAtom)).toBeNull(); - }); + test("startingLeftTimeSecondsAtom returns null initially", () => { + const store = createStore(); + expect(store.get(startingLeftTimeSecondsAtom)).toBeNull(); + }); - test("gamingLeftTimeSecondsAtom returns null initially", () => { - const store = createStore(); - expect(store.get(gamingLeftTimeSecondsAtom)).toBeNull(); - }); + test("gamingLeftTimeSecondsAtom returns null initially", () => { + const store = createStore(); + expect(store.get(gamingLeftTimeSecondsAtom)).toBeNull(); + }); }); diff --git a/frontend/app/states/watch.ts b/frontend/app/states/watch.ts index 50fa425..3431b6d 100644 --- a/frontend/app/states/watch.ts +++ b/frontend/app/states/watch.ts @@ -4,136 +4,136 @@ import type { SupportedLanguage } from "../types/SupportedLanguage"; const gameStartedAtAtom = atom<number | null>(null); export const setGameStartedAtAtom = atom(null, (_, set, value: number | null) => - set(gameStartedAtAtom, value), + set(gameStartedAtAtom, value), ); export type GameStateKind = - | "loading" - | "waiting" - | "starting" - | "gaming" - | "finished"; + | "loading" + | "waiting" + | "starting" + | "gaming" + | "finished"; type LatestGameState = components["schemas"]["LatestGameState"]; type RankingEntry = components["schemas"]["RankingEntry"]; export const gameStateKindAtom = atom<GameStateKind>((get) => { - const now = get(currentTimestampAtom); - if (!now) { - return "loading"; - } - const startedAt = get(gameStartedAtAtom); - if (!startedAt) { - return "waiting"; - } - const durationSeconds = get(durationSecondsAtom); - const finishedAt = startedAt + durationSeconds; - if (now < startedAt) { - return "starting"; - } else if (now < finishedAt) { - return "gaming"; - } else { - return "finished"; - } + const now = get(currentTimestampAtom); + if (!now) { + return "loading"; + } + const startedAt = get(gameStartedAtAtom); + if (!startedAt) { + return "waiting"; + } + const durationSeconds = get(durationSecondsAtom); + const finishedAt = startedAt + durationSeconds; + if (now < startedAt) { + return "starting"; + } else if (now < finishedAt) { + return "gaming"; + } else { + return "finished"; + } }); const currentTimestampAtom = atom<number | null>(null); export const setCurrentTimestampAtom = atom(null, (_, set) => - set(currentTimestampAtom, Math.floor(Date.now() / 1000)), + set(currentTimestampAtom, Math.floor(Date.now() / 1000)), ); const durationSecondsAtom = atom<number>(0); export const setDurationSecondsAtom = atom(null, (_, set, value: number) => - set(durationSecondsAtom, value), + set(durationSecondsAtom, value), ); export const startingLeftTimeSecondsAtom = atom<number | null>((get) => { - const startedAt = get(gameStartedAtAtom); - if (startedAt === null) { - return null; - } - const currentTimestamp = get(currentTimestampAtom); - if (currentTimestamp === null) { - return null; - } - return Math.max(0, startedAt - currentTimestamp); + const startedAt = get(gameStartedAtAtom); + if (startedAt === null) { + return null; + } + const currentTimestamp = get(currentTimestampAtom); + if (currentTimestamp === null) { + return null; + } + return Math.max(0, startedAt - currentTimestamp); }); export const gamingLeftTimeSecondsAtom = atom<number | null>((get) => { - const startedAt = get(gameStartedAtAtom); - if (startedAt === null) { - return null; - } - const durationSeconds = get(durationSecondsAtom); - const finishedAt = startedAt + durationSeconds; - const currentTimestamp = get(currentTimestampAtom); - if (currentTimestamp === null) { - return null; - } - return Math.min(durationSeconds, Math.max(0, finishedAt - currentTimestamp)); + const startedAt = get(gameStartedAtAtom); + if (startedAt === null) { + return null; + } + const durationSeconds = get(durationSecondsAtom); + const finishedAt = startedAt + durationSeconds; + const currentTimestamp = get(currentTimestampAtom); + if (currentTimestamp === null) { + return null; + } + return Math.min(durationSeconds, Math.max(0, finishedAt - currentTimestamp)); }); export const rankingAtom = atom<RankingEntry[]>([]); const rawLatestGameStatesAtom = atom<{ - [key: string]: LatestGameState | undefined; + [key: string]: LatestGameState | undefined; }>({}); export const latestGameStatesAtom = atom((get) => get(rawLatestGameStatesAtom)); export const setLatestGameStatesAtom = atom( - null, - (_, set, value: { [key: string]: LatestGameState | undefined }) => { - set(rawLatestGameStatesAtom, value); - }, + null, + (_, set, value: { [key: string]: LatestGameState | undefined }) => { + set(rawLatestGameStatesAtom, value); + }, ); function cleanCode(code: string, language: SupportedLanguage) { - if (language === "php") { - return code - .replace(/\s+/g, "") - .replace(/^<\?php/, "") - .replace(/^<\?/, "") - .replace(/\?>$/, ""); - } else { - return code.replace(/\s+/g, ""); - } + if (language === "php") { + return code + .replace(/\s+/g, "") + .replace(/^<\?php/, "") + .replace(/^<\?/, "") + .replace(/\?>$/, ""); + } else { + return code.replace(/\s+/g, ""); + } } export function calcCodeSize( - code: string, - language: SupportedLanguage, + code: string, + language: SupportedLanguage, ): number { - const trimmed = cleanCode(code, language); - const utf8Encoded = new TextEncoder().encode(trimmed); - return utf8Encoded.length; + const trimmed = cleanCode(code, language); + const utf8Encoded = new TextEncoder().encode(trimmed); + return utf8Encoded.length; } export type GameResultKind = "winA" | "winB" | "draw"; export function checkGameResultKind( - gameStateKind: GameStateKind, - stateA: LatestGameState | null, - stateB: LatestGameState | null, + gameStateKind: GameStateKind, + stateA: LatestGameState | null, + stateB: LatestGameState | null, ): GameResultKind | null { - if (gameStateKind !== "finished") { - return null; - } + if (gameStateKind !== "finished") { + return null; + } - const scoreA = stateA?.score; - const scoreB = stateB?.score; - if (scoreA == null && scoreB == null) { - return "draw"; - } - if (scoreA == null) { - return "winB"; - } - if (scoreB == null) { - return "winA"; - } - if (scoreA === scoreB) { - // If score is non-null, state and best_score_submitted_at should also be non-null. - const submittedAtA = stateA!.best_score_submitted_at!; - const submittedAtB = stateB!.best_score_submitted_at!; - return submittedAtA < submittedAtB ? "winA" : "winB"; - } else { - return scoreA < scoreB ? "winA" : "winB"; - } + const scoreA = stateA?.score; + const scoreB = stateB?.score; + if (scoreA == null && scoreB == null) { + return "draw"; + } + if (scoreA == null) { + return "winB"; + } + if (scoreB == null) { + return "winA"; + } + if (scoreA === scoreB) { + // If score is non-null, state and best_score_submitted_at should also be non-null. + const submittedAtA = stateA!.best_score_submitted_at!; + const submittedAtB = stateB!.best_score_submitted_at!; + return submittedAtA < submittedAtB ? "winA" : "winB"; + } else { + return scoreA < scoreB ? "winA" : "winB"; + } } |
