1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
import { atom } from "jotai";
import type { components } from "../api/schema";
const gameStartedAtAtom = atom<number | null>(null);
export const setGameStartedAtAtom = atom(null, (_, set, value: number | null) =>
set(gameStartedAtAtom, value),
);
export type GameStateKind =
| "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 currentTimestampAtom = atom<number | null>(null);
export const setCurrentTimestampAtom = atom(null, (_, set) =>
set(currentTimestampAtom, Math.floor(Date.now() / 1000)),
);
const durationSecondsAtom = atom<number>(0);
export const setDurationSecondsAtom = atom(null, (_, set, value: number) =>
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);
});
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));
});
export const rankingAtom = atom<RankingEntry[]>([]);
const rawLatestGameStatesAtom = atom<{
[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);
},
);
export function calcCodeSize(code: string): number {
const trimmed = code
.replace(/\s+/g, "")
.replace(/^<\?php/, "")
.replace(/^<\?/, "")
.replace(/\?>$/, "");
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,
): GameResultKind | 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";
}
}
|