aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend/app/states
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2024-08-22 14:16:21 +0900
committernsfisis <nsfisis@gmail.com>2024-08-22 14:16:21 +0900
commit81dd1db4daa57e67b1f603c260afe6cb526e5278 (patch)
tree8e5a857769db577b034a9a0404e70cf6c2d2693a /frontend/app/states
parent483e297f4789fb1fdaa241f87686a00ef55a046d (diff)
parentdfd33e58a5e6f830d60e978afad7348f7a16068d (diff)
downloadiosdc-japan-2024-albatross-81dd1db4daa57e67b1f603c260afe6cb526e5278.tar.gz
iosdc-japan-2024-albatross-81dd1db4daa57e67b1f603c260afe6cb526e5278.tar.zst
iosdc-japan-2024-albatross-81dd1db4daa57e67b1f603c260afe6cb526e5278.zip
Merge branch 'refactor/frontend-state'
Diffstat (limited to 'frontend/app/states')
-rw-r--r--frontend/app/states/play.ts185
-rw-r--r--frontend/app/states/watch.ts250
2 files changed, 435 insertions, 0 deletions
diff --git a/frontend/app/states/play.ts b/frontend/app/states/play.ts
new file mode 100644
index 0000000..13bd39f
--- /dev/null
+++ b/frontend/app/states/play.ts
@@ -0,0 +1,185 @@
+import { atom } from "jotai";
+import type { components } from "../.server/api/schema";
+import type { SubmitResult } from "../types/SubmitResult";
+
+type RawGameState =
+ | {
+ kind: "connecting";
+ startedAtTimestamp: null;
+ }
+ | {
+ kind: "waiting";
+ startedAtTimestamp: null;
+ }
+ | {
+ kind: "starting";
+ startedAtTimestamp: number;
+ };
+
+const rawGameStateAtom = atom<RawGameState>({
+ kind: "connecting",
+ startedAtTimestamp: null,
+});
+
+export type GameStateKind =
+ | "connecting"
+ | "waiting"
+ | "starting"
+ | "gaming"
+ | "finished";
+
+export const gameStateKindAtom = atom<GameStateKind>((get) => {
+ const { kind: rawKind, startedAtTimestamp } = get(rawGameStateAtom);
+ if (rawKind === "connecting" || rawKind === "waiting") {
+ return rawKind;
+ } else {
+ const durationSeconds = get(rawDurationSecondsAtom);
+ const finishedAtTimestamp = startedAtTimestamp + durationSeconds;
+ const currentTimestamp = get(rawCurrentTimestampAtom);
+ if (currentTimestamp < startedAtTimestamp) {
+ return "starting";
+ } else if (currentTimestamp < finishedAtTimestamp) {
+ return "gaming";
+ } else {
+ return "finished";
+ }
+ }
+});
+
+export const gameStartAtom = atom(null, (get, set, value: number) => {
+ const { kind } = get(rawGameStateAtom);
+ if (kind === "starting") {
+ return;
+ }
+ set(rawGameStateAtom, {
+ kind: "starting",
+ startedAtTimestamp: value,
+ });
+});
+export const setGameStateConnectingAtom = atom(null, (_, set) =>
+ set(rawGameStateAtom, { kind: "connecting", startedAtTimestamp: null }),
+);
+export const setGameStateWaitingAtom = atom(null, (_, set) =>
+ set(rawGameStateAtom, { kind: "waiting", startedAtTimestamp: null }),
+);
+
+const rawCurrentTimestampAtom = atom(0);
+export const setCurrentTimestampAtom = atom(null, (_, set) =>
+ set(rawCurrentTimestampAtom, Math.floor(Date.now() / 1000)),
+);
+
+const rawDurationSecondsAtom = atom<number>(0);
+export const setDurationSecondsAtom = atom(null, (_, set, value: number) =>
+ set(rawDurationSecondsAtom, value),
+);
+
+export const startingLeftTimeSecondsAtom = atom<number | null>((get) => {
+ const { startedAtTimestamp } = get(rawGameStateAtom);
+ if (startedAtTimestamp === null) {
+ return null;
+ }
+ const currentTimestamp = get(rawCurrentTimestampAtom);
+ return Math.max(0, startedAtTimestamp - currentTimestamp);
+});
+
+export const gamingLeftTimeSecondsAtom = atom<number | null>((get) => {
+ const { startedAtTimestamp } = get(rawGameStateAtom);
+ if (startedAtTimestamp === null) {
+ return null;
+ }
+ const durationSeconds = get(rawDurationSecondsAtom);
+ const finishedAtTimestamp = startedAtTimestamp + durationSeconds;
+ const currentTimestamp = get(rawCurrentTimestampAtom);
+ return Math.min(
+ durationSeconds,
+ Math.max(0, finishedAtTimestamp - currentTimestamp),
+ );
+});
+
+export const handleWsConnectionClosedAtom = atom(null, (get, set) => {
+ const kind = get(gameStateKindAtom);
+ if (kind !== "finished") {
+ set(setGameStateConnectingAtom);
+ }
+});
+
+export const scoreAtom = atom<number | null>(null);
+export const submitResultAtom = atom<SubmitResult>({
+ status: "waiting_submission",
+ execResults: [],
+});
+
+export const handleSubmitCodeAtom = atom(null, (_, set) => {
+ set(submitResultAtom, (prev) => ({
+ status: "running",
+ execResults: prev.execResults.map((r) => ({
+ ...r,
+ status: "running",
+ stdout: "",
+ stderr: "",
+ })),
+ }));
+});
+
+type GamePlayerMessageS2CExecResultPayload =
+ components["schemas"]["GamePlayerMessageS2CExecResultPayload"];
+type GamePlayerMessageS2CSubmitResultPayload =
+ components["schemas"]["GamePlayerMessageS2CSubmitResultPayload"];
+
+export const handleWsExecResultMessageAtom = atom(
+ null,
+ (
+ get,
+ set,
+ data: GamePlayerMessageS2CExecResultPayload,
+ callback: (submissionResult: SubmitResult) => void,
+ ) => {
+ const { testcase_id, status, stdout, stderr } = data;
+ const prev = get(submitResultAtom);
+ const newResult = {
+ ...prev,
+ execResults: prev.execResults.map((r) =>
+ r.testcase_id === testcase_id && r.status === "running"
+ ? {
+ ...r,
+ status,
+ stdout,
+ stderr,
+ }
+ : r,
+ ),
+ };
+ set(submitResultAtom, newResult);
+ callback(newResult);
+ },
+);
+
+export const handleWsSubmitResultMessageAtom = atom(
+ null,
+ (
+ get,
+ set,
+ data: GamePlayerMessageS2CSubmitResultPayload,
+ callback: (submissionResult: SubmitResult, score: number | null) => void,
+ ) => {
+ const { status, score } = data;
+ const prev = get(submitResultAtom);
+ const newResult = {
+ ...prev,
+ status,
+ };
+ if (status !== "success") {
+ newResult.execResults = prev.execResults.map((r) =>
+ r.status === "running" ? { ...r, status: "canceled" } : r,
+ );
+ }
+ set(submitResultAtom, newResult);
+ if (status === "success" && score !== null) {
+ const currentScore = get(scoreAtom);
+ if (currentScore === null || score < currentScore) {
+ set(scoreAtom, score);
+ }
+ }
+ callback(newResult, score);
+ },
+);
diff --git a/frontend/app/states/watch.ts b/frontend/app/states/watch.ts
new file mode 100644
index 0000000..ba3dd2a
--- /dev/null
+++ b/frontend/app/states/watch.ts
@@ -0,0 +1,250 @@
+import { atom } from "jotai";
+import { AudioController } from "../.client/audio/AudioController";
+import type { components } from "../.server/api/schema";
+import type { SubmitResult } from "../types/SubmitResult";
+
+type RawGameState =
+ | {
+ kind: "connecting";
+ startedAtTimestamp: null;
+ }
+ | {
+ kind: "waiting";
+ startedAtTimestamp: null;
+ }
+ | {
+ kind: "starting";
+ startedAtTimestamp: number;
+ };
+
+const rawGameStateAtom = atom<RawGameState>({
+ kind: "connecting",
+ startedAtTimestamp: null,
+});
+
+export type GameStateKind =
+ | "connecting"
+ | "waiting"
+ | "starting"
+ | "gaming"
+ | "finished";
+
+export const gameStateKindAtom = atom<GameStateKind>((get) => {
+ const { kind: rawKind, startedAtTimestamp } = get(rawGameStateAtom);
+ if (rawKind === "connecting" || rawKind === "waiting") {
+ return rawKind;
+ } else {
+ const durationSeconds = get(rawDurationSecondsAtom);
+ const finishedAtTimestamp = startedAtTimestamp + durationSeconds;
+ const currentTimestamp = get(rawCurrentTimestampAtom);
+ if (currentTimestamp < startedAtTimestamp) {
+ return "starting";
+ } else if (currentTimestamp < finishedAtTimestamp) {
+ return "gaming";
+ } else {
+ return "finished";
+ }
+ }
+});
+
+export const gameStartAtom = atom(null, (get, set, value: number) => {
+ const { kind } = get(rawGameStateAtom);
+ if (kind === "starting") {
+ return;
+ }
+ set(rawGameStateAtom, {
+ kind: "starting",
+ startedAtTimestamp: value,
+ });
+});
+export const setGameStateConnectingAtom = atom(null, (_, set) =>
+ set(rawGameStateAtom, { kind: "connecting", startedAtTimestamp: null }),
+);
+export const setGameStateWaitingAtom = atom(null, (_, set) =>
+ set(rawGameStateAtom, { kind: "waiting", startedAtTimestamp: null }),
+);
+
+const rawCurrentTimestampAtom = atom(0);
+export const setCurrentTimestampAtom = atom(null, (_, set) =>
+ set(rawCurrentTimestampAtom, Math.floor(Date.now() / 1000)),
+);
+
+const rawDurationSecondsAtom = atom<number>(0);
+export const setDurationSecondsAtom = atom(null, (_, set, value: number) =>
+ set(rawDurationSecondsAtom, value),
+);
+
+export const startingLeftTimeSecondsAtom = atom<number | null>((get) => {
+ const { startedAtTimestamp } = get(rawGameStateAtom);
+ if (startedAtTimestamp === null) {
+ return null;
+ }
+ const currentTimestamp = get(rawCurrentTimestampAtom);
+ return Math.max(0, startedAtTimestamp - currentTimestamp);
+});
+
+export const gamingLeftTimeSecondsAtom = atom<number | null>((get) => {
+ const { startedAtTimestamp } = get(rawGameStateAtom);
+ if (startedAtTimestamp === null) {
+ return null;
+ }
+ const durationSeconds = get(rawDurationSecondsAtom);
+ const finishedAtTimestamp = startedAtTimestamp + durationSeconds;
+ const currentTimestamp = get(rawCurrentTimestampAtom);
+ return Math.min(
+ durationSeconds,
+ Math.max(0, finishedAtTimestamp - currentTimestamp),
+ );
+});
+
+export const handleWsConnectionClosedAtom = atom(null, (get, set) => {
+ const kind = get(gameStateKindAtom);
+ if (kind !== "finished") {
+ set(setGameStateConnectingAtom);
+ }
+});
+
+export const codeAAtom = atom("");
+export const codeBAtom = atom("");
+export const scoreAAtom = atom<number | null>(null);
+export const scoreBAtom = atom<number | null>(null);
+export const submitResultAAtom = atom<SubmitResult>({
+ status: "waiting_submission",
+ execResults: [],
+});
+export const submitResultBAtom = atom<SubmitResult>({
+ status: "waiting_submission",
+ execResults: [],
+});
+
+type GameWatcherMessageS2CSubmitPayload =
+ components["schemas"]["GameWatcherMessageS2CSubmitPayload"];
+type GameWatcherMessageS2CCodePayload =
+ components["schemas"]["GameWatcherMessageS2CCodePayload"];
+type GameWatcherMessageS2CExecResultPayload =
+ components["schemas"]["GameWatcherMessageS2CExecResultPayload"];
+type GameWatcherMessageS2CSubmitResultPayload =
+ components["schemas"]["GameWatcherMessageS2CSubmitResultPayload"];
+
+export const handleWsCodeMessageAtom = atom(
+ null,
+ (
+ _,
+ set,
+ data: GameWatcherMessageS2CCodePayload,
+ getTarget: <T>(player_id: number, atomA: T, atomB: T) => T,
+ callback: (player_id: number, code: string) => void,
+ ) => {
+ const { player_id, code } = data;
+ const codeAtom = getTarget(player_id, codeAAtom, codeBAtom);
+ set(codeAtom, code);
+ callback(player_id, code);
+ },
+);
+
+export const handleWsSubmitMessageAtom = atom(
+ null,
+ (
+ get,
+ set,
+ data: GameWatcherMessageS2CSubmitPayload,
+ getTarget: <T>(player_id: number, atomA: T, atomB: T) => T,
+ callback: (player_id: number, submissionResult: SubmitResult) => void,
+ ) => {
+ const { player_id } = data;
+ const submitResultAtom = getTarget(
+ player_id,
+ submitResultAAtom,
+ submitResultBAtom,
+ );
+ const prev = get(submitResultAtom);
+ const newResult = {
+ status: "running" as const,
+ execResults: prev.execResults.map((r) => ({
+ ...r,
+ status: "running" as const,
+ stdout: "",
+ stderr: "",
+ })),
+ };
+ set(submitResultAtom, newResult);
+ callback(player_id, newResult);
+ },
+);
+
+export const handleWsExecResultMessageAtom = atom(
+ null,
+ (
+ get,
+ set,
+ data: GameWatcherMessageS2CExecResultPayload,
+ getTarget: <T>(player_id: number, atomA: T, atomB: T) => T,
+ callback: (player_id: number, submissionResult: SubmitResult) => void,
+ ) => {
+ const { player_id, testcase_id, status, stdout, stderr } = data;
+ const submitResultAtom = getTarget(
+ player_id,
+ submitResultAAtom,
+ submitResultBAtom,
+ );
+ const prev = get(submitResultAtom);
+ const newResult = {
+ ...prev,
+ execResults: prev.execResults.map((r) =>
+ r.testcase_id === testcase_id && r.status === "running"
+ ? {
+ ...r,
+ status,
+ stdout,
+ stderr,
+ }
+ : r,
+ ),
+ };
+ set(submitResultAtom, newResult);
+ callback(player_id, newResult);
+ },
+);
+
+export const handleWsSubmitResultMessageAtom = atom(
+ null,
+ (
+ get,
+ set,
+ data: GameWatcherMessageS2CSubmitResultPayload,
+ getTarget: <T>(player_id: number, atomA: T, atomB: T) => T,
+ callback: (
+ player_id: number,
+ submissionResult: SubmitResult,
+ score: number | null,
+ ) => void,
+ ) => {
+ const { player_id, status, score } = data;
+ const submitResultAtom = getTarget(
+ player_id,
+ submitResultAAtom,
+ submitResultBAtom,
+ );
+ const scoreAtom = getTarget(player_id, scoreAAtom, scoreBAtom);
+ const prev = get(submitResultAtom);
+ const newResult = {
+ ...prev,
+ status,
+ };
+ if (status !== "success") {
+ newResult.execResults = prev.execResults.map((r) =>
+ r.status === "running" ? { ...r, status: "canceled" } : r,
+ );
+ }
+ set(submitResultAtom, newResult);
+ if (status === "success" && score !== null) {
+ const currentScore = get(scoreAtom);
+ if (currentScore === null || score < currentScore) {
+ set(scoreAtom, score);
+ }
+ }
+ callback(player_id, newResult, score);
+ },
+);
+
+export const audioControllerAtom = atom<AudioController | null>(null);