diff --git a/apps/web/package.json b/apps/web/package.json
index 0b1bf51..bda8e81 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -13,6 +13,7 @@
"fix": "biome check --fix"
},
"devDependencies": {
+ "@biomejs/biome": "1.9.4",
"@poppanator/sveltekit-svg": "5.0.0",
"@sveltejs/adapter-auto": "3.3.1",
"@sveltejs/enhanced-img": "0.4.4",
diff --git a/apps/web/src/routes/kripke/+page.svelte b/apps/web/src/routes/kripke/+page.svelte
index 19fc97b..3c11208 100644
--- a/apps/web/src/routes/kripke/+page.svelte
+++ b/apps/web/src/routes/kripke/+page.svelte
@@ -1,66 +1,95 @@
KRiPkE
-
+
+
+ Daily Challenge:
+ {timeUntilNextGame.hours}:{timeUntilNextGame.minutes}:{timeUntilNextGame.seconds} until the next game.
+
+
+
-
-
Rules
+
+ Daily Challenge!
- -
- A Kripke frame with 4 worlds is generated.
-
- -
- The game tells you how many accessibility relations are in the frame, but not the exact shape of it.
-
- -
- You have a total of 10 moves ().
- In each move you can do one of the following:
-
- -
- Enter a modal formula.
- The game tells you in how many worlds the formula is valid (for every valuation).
-
- -
- Guess the Kripke frame.
- If your frame is equal or isomorphic to the secret frame, you win.
-
-
-
- -
- You lose when you run out of moves.
-
+ - The answer of the game changes every day.
+ - The progress of the game persists until the next day ({timeUntilNextGame.hours}:{timeUntilNextGame.minutes}:{timeUntilNextGame.seconds} from now).
+ - You can also play Random Challenge.
- Syntax
- You may use the following symbols:
-
- - propositional variables:
p, q, r, s
- - verum:
⊤, T, 1, \top
- - falsum:
⊥, F, 0, \bot
- - negation:
¬, ~, \neg, \lnot
- - box modality:
□, [], !, L, \Box
- - diamond modality:
⋄, <>, ?, M, \Diamond
- - conjunction:
∧, ^, &, \wedge, \land
- - disjunction:
∨, v, |, \vee, \lor
- - implication:
→, ->, >, \rightarrow, \to
- - equivalence:
↔, <->, =, \leftrightarrow, \equiv
- - parentheses:
(, )
-
-
+
+
+
+{#if status !== "playing"}
+ {#await getAnswer() then answerId}
+
+
+
+
+ {#if status === "win"}
+ YOU WIN!
+ {:else if status === "lose"}
+ YOU LOSE!
+ {/if}
+
+
+ The answer was:
+
+
+
+
+ id: {answerId}
+
+
+
+
+
+
+
+
+ {/await}
+{/if}
diff --git a/apps/web/src/routes/kripke/game.svelte b/apps/web/src/routes/kripke/game.svelte
index 85bf4c5..3b11de6 100644
--- a/apps/web/src/routes/kripke/game.svelte
+++ b/apps/web/src/routes/kripke/game.svelte
@@ -1,7 +1,6 @@
+
+{#snippet seedNumber()}
+ {seed}
+{/snippet}
+
+
+ KRiPkE
+
+
+
+
+ Random Challenge
+ (seed: {@render seedNumber()})
+
+
+
+
+
+ Random Challenge!
+
+ - The answer of the game is determined by a seed number {@render seedNumber()}, which changes on every reload.
+ - You can right-click on the seed number to obtain a permalink to this exact game.
+ - Unlike Daily Challenge, the progress of the game does not persist.
+ - You can also play Daily Challenge, if you have not yet.
+
+
+
+
+
+
+
+{#if status !== "playing"}
+ {#await getAnswer() then answerId}
+
+
+
+
+ {#if status === "win"}
+ YOU WIN!
+ {:else if status === "lose"}
+ YOU LOSE!
+ {/if}
+
+
+ The answer was:
+
+
+
+
+ id: {answerId}
+
+
+
+
+
+
+
+
+ {/await}
+{/if}
diff --git a/apps/web/src/routes/kripke/rules.svelte b/apps/web/src/routes/kripke/rules.svelte
new file mode 100644
index 0000000..5f6a430
--- /dev/null
+++ b/apps/web/src/routes/kripke/rules.svelte
@@ -0,0 +1,46 @@
+
+
+
Rules
+
+ -
+ A Kripke frame with 4 worlds is generated.
+
+ -
+ The game tells you how many accessibility relations are in the frame, but not the exact shape of it.
+
+ -
+ You have a total of 10 moves ().
+ In each move you can do one of the following:
+
+ -
+ Enter a modal formula.
+ The game tells you in how many worlds the formula is valid (for every valuation).
+
+ -
+ Guess the Kripke frame.
+ If your frame is equal or isomorphic to the secret frame, you win.
+
+
+
+ -
+ You lose when you run out of moves.
+
+
+
+Syntax
+You may use the following symbols:
+
+ - propositional variables:
p, q, r, s
+ - verum:
⊤, T, 1, \top
+ - falsum:
⊥, F, 0, \bot
+ - negation:
¬, ~, \neg, \lnot
+ - box modality:
□, [], !, L, \Box
+ - diamond modality:
⋄, <>, ?, M, \Diamond
+ - conjunction:
∧, ^, &, \wedge, \land
+ - disjunction:
∨, v, |, \vee, \lor
+ - implication:
→, ->, >, \rightarrow, \to
+ - equivalence:
↔, <->, =, \leftrightarrow, \equiv
+ - parentheses:
(, )
+
diff --git a/apps/web/src/routes/kripke/store.ts b/apps/web/src/routes/kripke/store.ts
new file mode 100644
index 0000000..0fb1b30
--- /dev/null
+++ b/apps/web/src/routes/kripke/store.ts
@@ -0,0 +1,24 @@
+import { persisted } from "svelte-persisted-store";
+import type { Move } from "./game.svelte";
+import { date } from "./system";
+
+export type Daily = {
+ date: string;
+ moves: Move[];
+};
+
+export const daily = persisted(
+ "daily",
+ {
+ date: date(),
+ moves: [],
+ },
+ {
+ syncTabs: true,
+ storage: "local",
+ beforeRead: (value) => {
+ if (value.date !== date()) return { date: date(), moves: [] };
+ return value;
+ },
+ },
+);
diff --git a/apps/web/src/routes/kripke/system.ts b/apps/web/src/routes/kripke/system.ts
index 6aa31ac..044fb3d 100644
--- a/apps/web/src/routes/kripke/system.ts
+++ b/apps/web/src/routes/kripke/system.ts
@@ -18,10 +18,43 @@ function cyrb53(str: string, seed = 0): number {
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
+export const date = () => new Date().toISOString().split("T")[0];
+
export function getDailyFrame() {
// Use UTC ISO string so it's consistent across time zones
- const dateStr = new Date().toISOString().split("T")[0];
+ const dateStr = date();
const hash = cyrb53(dateStr);
const index = hash % nontrivials.length;
- return getFrame(nontrivials[index]);
+ return {
+ date: dateStr,
+ id: nontrivials[index],
+ frame: getFrame(nontrivials[index]),
+ };
+}
+
+export const randomSeed = () => Math.floor(Math.random() * 0x100000000);
+
+export function getRandomFrame(seed?: number) {
+ const actualSeed = seed ?? randomSeed();
+ const hash = cyrb53("random", seed);
+ const index = hash % nontrivials.length;
+ return {
+ seed: actualSeed,
+ id: nontrivials[index],
+ frame: getFrame(nontrivials[index]),
+ };
+}
+
+export function getTimeUntilNextGame() {
+ const now = new Date();
+ const nextDayUTC = new Date(
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
+ );
+ const deltaSeconds = (nextDayUTC.getTime() - now.getTime()) / 1000;
+ const pad = (num: number) => `0${num}`.slice(-2);
+ return {
+ hours: pad(Math.floor(deltaSeconds / 3600)),
+ minutes: pad(Math.floor((deltaSeconds % 3600) / 60)),
+ seconds: pad(Math.floor(deltaSeconds % 60)),
+ };
}
diff --git a/yarn.lock b/yarn.lock
index 2aa3b5e..1159b92 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3948,6 +3948,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "web@workspace:apps/web"
dependencies:
+ "@biomejs/biome": "npm:1.9.4"
"@cannorin/kripke": "workspace:*"
"@fontsource/poiret-one": "npm:5.1.1"
"@fontsource/zen-kaku-gothic-new": "npm:5.1.1"