aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/states
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app/states')
-rw-r--r--frontend/app/states/play.test.ts318
-rw-r--r--frontend/app/states/play.ts146
-rw-r--r--frontend/app/states/watch.test.ts350
-rw-r--r--frontend/app/states/watch.ts178
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";
+ }
}