kripke: refactor
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
import type { SeoProps } from "$components/seo";
|
import type { SeoProps } from "$components/seo";
|
||||||
|
import { dailySeed } from "./lib/system";
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
|
const seed = dailySeed();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
seed,
|
||||||
seo: {
|
seo: {
|
||||||
title: "KRIPKE - cannorin.net",
|
title: "KRIPKE - cannorin.net",
|
||||||
description: "KRIPKE - WORDLE, but for Kripke frames!",
|
description: "KRIPKE - WORDLE, but for Kripke frames!",
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ import LuRotateCw from "lucide-svelte/icons/rotate-cw";
|
|||||||
import LuX from "lucide-svelte/icons/x";
|
import LuX from "lucide-svelte/icons/x";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import FrameInput from "./frame-input.svelte";
|
import FrameInput from "./components/frame-input.svelte";
|
||||||
import Game, { type GameStatus, type Move } from "./game.svelte";
|
import Game, { type GameStatus } from "./components/game.svelte";
|
||||||
import Rules from "./rules.svelte";
|
import Rules from "./components/rules.svelte";
|
||||||
import Share from "./share.svelte";
|
import Share from "./components/share.svelte";
|
||||||
|
|
||||||
|
import { getFrameBySeed, getTimeUntilNextGame } from "./lib/system";
|
||||||
import { daily } from "./store";
|
import { daily } from "./store";
|
||||||
import { getDailyFrame, getTimeUntilNextGame } from "./system";
|
|
||||||
|
|
||||||
const { id, frame } = getDailyFrame();
|
let { data } = $props();
|
||||||
|
const seed = data.seed;
|
||||||
|
const { id, frame } = getFrameBySeed(seed);
|
||||||
const guess = (frameId: number) => isomorphic[frameId] === id;
|
const guess = (frameId: number) => isomorphic[frameId] === id;
|
||||||
const check = (formula: Formula) => validWorlds(frame, formula).length;
|
const check = (formula: Formula) => validWorlds(frame, formula).length;
|
||||||
const getAnswer = () => id;
|
const getAnswer = () => id;
|
||||||
@@ -46,7 +48,10 @@ onMount(() => {
|
|||||||
<span class="font-bold">Daily Challenge: </span>
|
<span class="font-bold">Daily Challenge: </span>
|
||||||
{timeUntilNextGame.hours}:{timeUntilNextGame.minutes}:{timeUntilNextGame.seconds} until the next game.
|
{timeUntilNextGame.hours}:{timeUntilNextGame.minutes}:{timeUntilNextGame.seconds} until the next game.
|
||||||
</h2>
|
</h2>
|
||||||
<Game bind:moves={$daily.moves} bind:status relationSize={relationSize} guess={guess} check={check} getAnswer={getAnswer} />
|
<Game
|
||||||
|
bind:moves={$daily.moves} bind:status relationSize={relationSize}
|
||||||
|
guess={guess} check={check} getAnswer={getAnswer}
|
||||||
|
onShare={() => { dialogOpen = true; }} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="w-[300px] prose prose-sm">
|
<section class="w-[300px] prose prose-sm">
|
||||||
@@ -80,7 +85,7 @@ onMount(() => {
|
|||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<div class="flex flex-col items-center w-fit rounded bg-background mx-auto">
|
<div class="flex flex-col items-center w-fit rounded bg-background mx-auto">
|
||||||
<span class="text-xs text-muted self-start px-2 py-1">id: {answerId}</span>
|
<span class="text-xs text-muted self-start px-2 py-1">id: {answerId}, seed: <a class="text-primary underline font-medium" href="/kripke/random/{seed}">{seed}</a></span>
|
||||||
<FrameInput class="pb-6" disabled width={250} height={250} frame={frame} />
|
<FrameInput class="pb-6" disabled width={250} height={250} frame={frame} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,8 +93,7 @@ onMount(() => {
|
|||||||
<div class="flex flex-col md:flex-row gap-2 w-full justify-end">
|
<div class="flex flex-col md:flex-row gap-2 w-full justify-end">
|
||||||
<Share date={$daily.date} moves={$daily.moves} status={status} />
|
<Share date={$daily.date} moves={$daily.moves} status={status} />
|
||||||
<Button
|
<Button
|
||||||
href="/kripke/random"
|
href="/kripke/random">
|
||||||
data-sveltekit-reload>
|
|
||||||
<LuRotateCw class="w-4 h-4 mt-[2px]" /> Play Random Challenge
|
<LuRotateCw class="w-4 h-4 mt-[2px]" /> Play Random Challenge
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
worlds,
|
worlds,
|
||||||
} from "@cannorin/kripke";
|
} from "@cannorin/kripke";
|
||||||
import type { SVGAttributes } from "svelte/elements";
|
import type { SVGAttributes } from "svelte/elements";
|
||||||
import { type Vector, add, degree, rotate, sub, theta } from "./vector";
|
import { type Vector, add, degree, rotate, sub, theta } from "../lib/vector";
|
||||||
|
|
||||||
export interface FrameInputProps extends SVGAttributes<SVGElement> {
|
export interface FrameInputProps extends SVGAttributes<SVGElement> {
|
||||||
frame?: Frame | undefined;
|
frame?: Frame | undefined;
|
||||||
@@ -116,28 +116,30 @@ function getPath(rel: Relation) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.node {
|
|
||||||
fill: rgb(var(--background));
|
|
||||||
stroke: rgb(var(--foreground));
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.node.selected {
|
|
||||||
stroke: rgb(var(--primary));
|
|
||||||
stroke-width: 3;
|
|
||||||
}
|
|
||||||
.edge {
|
|
||||||
stroke: rgb(var(--foreground));
|
|
||||||
stroke-width: 1;
|
|
||||||
fill: none;
|
|
||||||
marker-end: url(#arrowhead);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<svg width={width ?? 250} height={height ?? 250} {...rest} viewBox="0,0,250,250" onclick={handleSvgClick}>
|
<svg width={width ?? 250} height={height ?? 250} {...rest} viewBox="0,0,250,250" onclick={handleSvgClick} xmlns="http://www.w3.org/2000/svg">
|
||||||
<defs>
|
<style>
|
||||||
|
.node {
|
||||||
|
fill: rgb(var(--background));
|
||||||
|
stroke: rgb(var(--foreground));
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
.node.selected {
|
||||||
|
stroke: rgb(var(--primary));
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
.edge {
|
||||||
|
stroke: rgb(var(--foreground));
|
||||||
|
stroke-width: 1;
|
||||||
|
fill: none;
|
||||||
|
marker-end: url(#arrowhead);
|
||||||
|
}
|
||||||
|
.arrowhead {
|
||||||
|
fill: rgb(var(--foreground));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<defs>
|
||||||
<marker
|
<marker
|
||||||
id="arrowhead"
|
id="arrowhead"
|
||||||
viewBox="0 0 10 10"
|
viewBox="0 0 10 10"
|
||||||
@@ -148,7 +150,7 @@ function getPath(rel: Relation) {
|
|||||||
orient="auto"
|
orient="auto"
|
||||||
markerUnits="strokeWidth"
|
markerUnits="strokeWidth"
|
||||||
>
|
>
|
||||||
<path d="M0,0 L0,10 L10,5 z" fill="rgb(var(--foreground))" />
|
<path class="arrowhead" d="M0,0 L0,10 L10,5 z" />
|
||||||
</marker>
|
</marker>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ export type Props = {
|
|||||||
guess: (frameId: number) => boolean | Promise<boolean>;
|
guess: (frameId: number) => boolean | Promise<boolean>;
|
||||||
check: (formula: Formula) => number | Promise<number>;
|
check: (formula: Formula) => number | Promise<number>;
|
||||||
getAnswer: () => number | Promise<number>;
|
getAnswer: () => number | Promise<number>;
|
||||||
|
onShare?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -40,6 +41,7 @@ let {
|
|||||||
guess: guessImpl,
|
guess: guessImpl,
|
||||||
check: checkImpl,
|
check: checkImpl,
|
||||||
getAnswer,
|
getAnswer,
|
||||||
|
onShare,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let formula: Formula | undefined = $state(undefined);
|
let formula: Formula | undefined = $state(undefined);
|
||||||
@@ -200,20 +202,30 @@ const colors: Record<number, string> = {
|
|||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
{#if status === "win"}
|
{#if status === "win"}
|
||||||
<li class="flex flex-col items-center gap-2 rounded bg-green-700 text-background p-5 animate-fade-in">
|
<li>
|
||||||
<p class="text-xl font-bold">YOU WIN!</p>
|
<button
|
||||||
|
class="flex flex-col items-center gap-2 rounded bg-green-700 text-background p-5 animate-fade-in w-full"
|
||||||
|
onclick={() => onShare?.()}>
|
||||||
|
<p class="text-xl font-bold">YOU WIN!</p>
|
||||||
|
<p class="sr-only">Open result screen</p>
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{:else if status === "lose"}
|
{:else if status === "lose"}
|
||||||
{#await getAnswer() then answerId}
|
{#await getAnswer() then answerId}
|
||||||
<li class="flex flex-col gap-2 rounded bg-foreground text-background p-5 animate-fade-in">
|
<li>
|
||||||
<div>
|
<button
|
||||||
<p class="text-xl font-bold">YOU LOSE!</p>
|
class="flex flex-col gap-2 rounded bg-foreground text-background p-5 animate-fade-in w-full"
|
||||||
<p class="text-sm">The answer was:</p>
|
onclick={() => onShare?.()}>
|
||||||
</div>
|
<div>
|
||||||
<div class="flex flex-col items-center rounded bg-background w-full">
|
<p class="text-xl font-bold">YOU LOSE!</p>
|
||||||
<span class="text-xs text-muted self-start px-2 py-1">id: {answerId}</span>
|
<p class="text-sm">The answer was:</p>
|
||||||
<FrameInput class="pb-6" disabled width={250} height={250} frame={getFrame(answerId)} />
|
</div>
|
||||||
</div>
|
<div class="flex flex-col items-center rounded bg-background w-full">
|
||||||
|
<span class="text-xs text-muted self-start px-2 py-1">id: {answerId}</span>
|
||||||
|
<FrameInput class="pb-6" disabled width={250} height={250} frame={getFrame(answerId)} />
|
||||||
|
</div>
|
||||||
|
<p class="sr-only">Open result screen</p>
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -35,7 +35,7 @@ let shareText = $derived.by(() => {
|
|||||||
|
|
||||||
${history} ${status === "win" ? moves.length : "X"}/10
|
${history} ${status === "win" ? moves.length : "X"}/10
|
||||||
|
|
||||||
https://www.cannorin.net/kripke${seed ? `/random?seed=${seed}` : ""}`;
|
https://www.cannorin.net/kripke${seed ? `/random/${seed}` : ""}`;
|
||||||
|
|
||||||
return encodeURIComponent(text);
|
return encodeURIComponent(text);
|
||||||
});
|
});
|
||||||
@@ -20,26 +20,17 @@ function cyrb53(str: string, seed = 0): number {
|
|||||||
|
|
||||||
export const date = () => new Date().toISOString().split("T")[0];
|
export const date = () => new Date().toISOString().split("T")[0];
|
||||||
|
|
||||||
export function getDailyFrame() {
|
export const dailySeed = () => {
|
||||||
// Use UTC ISO string so it's consistent across time zones
|
|
||||||
const dateStr = date();
|
const dateStr = date();
|
||||||
const hash = cyrb53(dateStr);
|
return cyrb53(dateStr) % 0x100000000;
|
||||||
const index = hash % nontrivials.length;
|
};
|
||||||
return {
|
|
||||||
date: dateStr,
|
|
||||||
id: nontrivials[index],
|
|
||||||
frame: getFrame(nontrivials[index]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const randomSeed = () => Math.floor(Math.random() * 0x100000000);
|
export const randomSeed = () => Math.floor(Math.random() * 0x100000000);
|
||||||
|
|
||||||
export function getRandomFrame(seed?: number) {
|
export function getFrameBySeed(seed: number) {
|
||||||
const actualSeed = seed ?? randomSeed();
|
|
||||||
const hash = cyrb53("random", seed);
|
const hash = cyrb53("random", seed);
|
||||||
const index = hash % nontrivials.length;
|
const index = hash % nontrivials.length;
|
||||||
return {
|
return {
|
||||||
seed: actualSeed,
|
|
||||||
id: nontrivials[index],
|
id: nontrivials[index],
|
||||||
frame: getFrame(nontrivials[index]),
|
frame: getFrame(nontrivials[index]),
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SeoProps } from "$components/seo";
|
import { redirect } from "@sveltejs/kit";
|
||||||
import { randomSeed } from "../system.js";
|
import { randomSeed } from "../lib/system";
|
||||||
|
|
||||||
export async function load({ url }) {
|
export async function load({ url }) {
|
||||||
const seedStr = url.searchParams.get("seed");
|
const seedStr = url.searchParams.get("seed");
|
||||||
@@ -13,20 +13,5 @@ export async function load({ url }) {
|
|||||||
return randomSeed();
|
return randomSeed();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
redirect(302, `/kripke/random/${seed}`);
|
||||||
return {
|
|
||||||
seed,
|
|
||||||
seo: {
|
|
||||||
title: "KRIPKE (random challenge) - cannorin.net",
|
|
||||||
description: "KRIPKE - WORDLE, but for Kripke frames!",
|
|
||||||
openGraph: {
|
|
||||||
title: "KRIPKE (random challenge) - cannorin.net",
|
|
||||||
description: "KRIPKE - WORDLE, but for Kripke frames!",
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
title: "KRIPKE (random challenge) - cannorin.net",
|
|
||||||
description: "KRIPKE - WORDLE, but for Kripke frames!",
|
|
||||||
},
|
|
||||||
} as SeoProps,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
31
apps/web/src/routes/kripke/random/[seed]/+page.server.ts
Normal file
31
apps/web/src/routes/kripke/random/[seed]/+page.server.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { SeoProps } from "$components/seo";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export async function load({ params }) {
|
||||||
|
const seedStr = params.seed;
|
||||||
|
const seed = (() => {
|
||||||
|
try {
|
||||||
|
const seed = Number.parseInt(seedStr);
|
||||||
|
if (!Number.isSafeInteger(seed)) throw error(400);
|
||||||
|
return seed;
|
||||||
|
} catch {
|
||||||
|
throw error(400);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
seed,
|
||||||
|
seo: {
|
||||||
|
title: `KRIPKE (seed ${seed}) - cannorin.net`,
|
||||||
|
description: "KRIPKE - WORDLE, but for Kripke frames!",
|
||||||
|
openGraph: {
|
||||||
|
title: `KRIPKE (seed ${seed}) - cannorin.net`,
|
||||||
|
description: "KRIPKE - WORDLE, but for Kripke frames!",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
title: `KRIPKE (seed ${seed}) - cannorin.net`,
|
||||||
|
description: "KRIPKE - WORDLE, but for Kripke frames!",
|
||||||
|
},
|
||||||
|
} as SeoProps,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,15 +5,15 @@ import { type Formula, isomorphic, validWorlds } from "@cannorin/kripke";
|
|||||||
import LuRotateCw from "lucide-svelte/icons/rotate-cw";
|
import LuRotateCw from "lucide-svelte/icons/rotate-cw";
|
||||||
import LuX from "lucide-svelte/icons/x";
|
import LuX from "lucide-svelte/icons/x";
|
||||||
|
|
||||||
import FrameInput from "../frame-input.svelte";
|
import FrameInput from "../../components/frame-input.svelte";
|
||||||
import Game, { type GameStatus, type Move } from "../game.svelte";
|
import Game, { type GameStatus, type Move } from "../../components/game.svelte";
|
||||||
import Rules from "../rules.svelte";
|
import Rules from "../../components/rules.svelte";
|
||||||
import Share from "../share.svelte";
|
import Share from "../../components/share.svelte";
|
||||||
import { getRandomFrame } from "../system";
|
import { getFrameBySeed } from "../../lib/system";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
const seed = data.seed;
|
const seed = data.seed;
|
||||||
const { id, frame } = getRandomFrame(seed);
|
const { id, frame } = getFrameBySeed(seed);
|
||||||
const guess = (frameId: number) => isomorphic[frameId] === id;
|
const guess = (frameId: number) => isomorphic[frameId] === id;
|
||||||
const check = (formula: Formula) => validWorlds(frame, formula).length;
|
const check = (formula: Formula) => validWorlds(frame, formula).length;
|
||||||
const getAnswer = () => id;
|
const getAnswer = () => id;
|
||||||
@@ -29,7 +29,7 @@ $effect(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet seedNumber()}
|
{#snippet seedNumber()}
|
||||||
<a class="text-primary font-medium underline" href={`/kripke/random?seed=${seed}`}>{seed}</a>
|
<a class="text-primary font-medium underline" href={`/kripke/random/${seed}`}>{seed}</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<main class="flex flex-col min-h-screen max-w-full items-center gap-12 lg:gap-16 py-8">
|
<main class="flex flex-col min-h-screen max-w-full items-center gap-12 lg:gap-16 py-8">
|
||||||
@@ -41,7 +41,10 @@ $effect(() => {
|
|||||||
<span class="font-bold">Random Challenge</span>
|
<span class="font-bold">Random Challenge</span>
|
||||||
<span>(seed: {@render seedNumber()})</span>
|
<span>(seed: {@render seedNumber()})</span>
|
||||||
</h2>
|
</h2>
|
||||||
<Game bind:moves bind:status relationSize={relationSize} guess={guess} check={check} getAnswer={getAnswer} />
|
<Game
|
||||||
|
bind:moves bind:status relationSize={relationSize}
|
||||||
|
guess={guess} check={check} getAnswer={getAnswer}
|
||||||
|
onShare={() => { dialogOpen = true; }}/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="w-[300px] prose prose-sm">
|
<section class="w-[300px] prose prose-sm">
|
||||||
@@ -76,7 +79,7 @@ $effect(() => {
|
|||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<div class="flex flex-col items-center w-fit rounded bg-background mx-auto">
|
<div class="flex flex-col items-center w-fit rounded bg-background mx-auto">
|
||||||
<span class="text-xs text-muted self-start px-2 py-1">id: {answerId}</span>
|
<span class="text-xs text-muted self-start px-2 py-1">id: {answerId}, seed: <a class="text-primary underline font-medium" href="/kripke/random/{seed}">{seed}</a></span>
|
||||||
<FrameInput class="pb-6" disabled width={250} height={250} frame={frame} />
|
<FrameInput class="pb-6" disabled width={250} height={250} frame={frame} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -84,8 +87,7 @@ $effect(() => {
|
|||||||
<div class="flex flex-col md:flex-row gap-2 w-full justify-end">
|
<div class="flex flex-col md:flex-row gap-2 w-full justify-end">
|
||||||
<Share seed={seed} moves={moves} status={status} />
|
<Share seed={seed} moves={moves} status={status} />
|
||||||
<Button
|
<Button
|
||||||
href="/kripke/random"
|
href="/kripke/random">
|
||||||
data-sveltekit-reload>
|
|
||||||
<LuRotateCw class="w-4 h-4 mt-[2px]" /> Play New Game
|
<LuRotateCw class="w-4 h-4 mt-[2px]" /> Play New Game
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { persisted } from "svelte-persisted-store";
|
import { persisted } from "svelte-persisted-store";
|
||||||
import type { Move } from "./game.svelte";
|
import type { Move } from "./components/game.svelte";
|
||||||
import { date } from "./system";
|
import { date } from "./lib/system";
|
||||||
|
|
||||||
export type Daily = {
|
export type Daily = {
|
||||||
date: string;
|
date: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user