kripke: refactor

This commit is contained in:
2025-02-20 20:46:26 +09:00
parent fb8379db71
commit 5aa6cac0c2
13 changed files with 119 additions and 88 deletions

View File

@@ -1,7 +1,11 @@
import type { SeoProps } from "$components/seo";
import { dailySeed } from "./lib/system";
export async function load() {
const seed = dailySeed();
return {
seed,
seo: {
title: "KRIPKE - cannorin.net",
description: "KRIPKE - WORDLE, but for Kripke frames!",

View File

@@ -6,15 +6,17 @@ import LuRotateCw from "lucide-svelte/icons/rotate-cw";
import LuX from "lucide-svelte/icons/x";
import { onMount } from "svelte";
import FrameInput from "./frame-input.svelte";
import Game, { type GameStatus, type Move } from "./game.svelte";
import Rules from "./rules.svelte";
import Share from "./share.svelte";
import FrameInput from "./components/frame-input.svelte";
import Game, { type GameStatus } from "./components/game.svelte";
import Rules from "./components/rules.svelte";
import Share from "./components/share.svelte";
import { getFrameBySeed, getTimeUntilNextGame } from "./lib/system";
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 check = (formula: Formula) => validWorlds(frame, formula).length;
const getAnswer = () => id;
@@ -46,7 +48,10 @@ onMount(() => {
<span class="font-bold">Daily Challenge: </span>
{timeUntilNextGame.hours}:{timeUntilNextGame.minutes}:{timeUntilNextGame.seconds} until the next game.
</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 class="w-[300px] prose prose-sm">
@@ -80,7 +85,7 @@ onMount(() => {
</Dialog.Header>
<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} />
</div>
@@ -88,8 +93,7 @@ onMount(() => {
<div class="flex flex-col md:flex-row gap-2 w-full justify-end">
<Share date={$daily.date} moves={$daily.moves} status={status} />
<Button
href="/kripke/random"
data-sveltekit-reload>
href="/kripke/random">
<LuRotateCw class="w-4 h-4 mt-[2px]" /> Play Random Challenge
</Button>
<Button

View File

@@ -9,7 +9,7 @@ import {
worlds,
} from "@cannorin/kripke";
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> {
frame?: Frame | undefined;
@@ -116,28 +116,30 @@ function getPath(rel: Relation) {
}
</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_no_static_element_interactions -->
<svg width={width ?? 250} height={height ?? 250} {...rest} viewBox="0,0,250,250" onclick={handleSvgClick}>
<defs>
<svg width={width ?? 250} height={height ?? 250} {...rest} viewBox="0,0,250,250" onclick={handleSvgClick} xmlns="http://www.w3.org/2000/svg">
<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
id="arrowhead"
viewBox="0 0 10 10"
@@ -148,7 +150,7 @@ function getPath(rel: Relation) {
orient="auto"
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>
</defs>

View File

@@ -31,6 +31,7 @@ export type Props = {
guess: (frameId: number) => boolean | Promise<boolean>;
check: (formula: Formula) => number | Promise<number>;
getAnswer: () => number | Promise<number>;
onShare?: () => void;
};
let {
@@ -40,6 +41,7 @@ let {
guess: guessImpl,
check: checkImpl,
getAnswer,
onShare,
}: Props = $props();
let formula: Formula | undefined = $state(undefined);
@@ -200,20 +202,30 @@ const colors: Record<number, string> = {
</li>
{/if}
{#if status === "win"}
<li class="flex flex-col items-center gap-2 rounded bg-green-700 text-background p-5 animate-fade-in">
<p class="text-xl font-bold">YOU WIN!</p>
<li>
<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>
{:else if status === "lose"}
{#await getAnswer() then answerId}
<li class="flex flex-col gap-2 rounded bg-foreground text-background p-5 animate-fade-in">
<div>
<p class="text-xl font-bold">YOU LOSE!</p>
<p class="text-sm">The answer was:</p>
</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>
<li>
<button
class="flex flex-col gap-2 rounded bg-foreground text-background p-5 animate-fade-in w-full"
onclick={() => onShare?.()}>
<div>
<p class="text-xl font-bold">YOU LOSE!</p>
<p class="text-sm">The answer was:</p>
</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>
{/await}
{/if}

View File

@@ -35,7 +35,7 @@ let shareText = $derived.by(() => {
${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);
});

View File

@@ -20,26 +20,17 @@ function cyrb53(str: string, seed = 0): number {
export const date = () => new Date().toISOString().split("T")[0];
export function getDailyFrame() {
// Use UTC ISO string so it's consistent across time zones
export const dailySeed = () => {
const dateStr = date();
const hash = cyrb53(dateStr);
const index = hash % nontrivials.length;
return {
date: dateStr,
id: nontrivials[index],
frame: getFrame(nontrivials[index]),
};
}
return cyrb53(dateStr) % 0x100000000;
};
export const randomSeed = () => Math.floor(Math.random() * 0x100000000);
export function getRandomFrame(seed?: number) {
const actualSeed = seed ?? randomSeed();
export function getFrameBySeed(seed: number) {
const hash = cyrb53("random", seed);
const index = hash % nontrivials.length;
return {
seed: actualSeed,
id: nontrivials[index],
frame: getFrame(nontrivials[index]),
};

View File

@@ -1,5 +1,5 @@
import type { SeoProps } from "$components/seo";
import { randomSeed } from "../system.js";
import { redirect } from "@sveltejs/kit";
import { randomSeed } from "../lib/system";
export async function load({ url }) {
const seedStr = url.searchParams.get("seed");
@@ -13,20 +13,5 @@ export async function load({ url }) {
return randomSeed();
}
})();
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,
};
redirect(302, `/kripke/random/${seed}`);
}

View 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,
};
}

View File

@@ -5,15 +5,15 @@ import { type Formula, isomorphic, validWorlds } from "@cannorin/kripke";
import LuRotateCw from "lucide-svelte/icons/rotate-cw";
import LuX from "lucide-svelte/icons/x";
import FrameInput from "../frame-input.svelte";
import Game, { type GameStatus, type Move } from "../game.svelte";
import Rules from "../rules.svelte";
import Share from "../share.svelte";
import { getRandomFrame } from "../system";
import FrameInput from "../../components/frame-input.svelte";
import Game, { type GameStatus, type Move } from "../../components/game.svelte";
import Rules from "../../components/rules.svelte";
import Share from "../../components/share.svelte";
import { getFrameBySeed } from "../../lib/system";
let { data } = $props();
const seed = data.seed;
const { id, frame } = getRandomFrame(seed);
const { id, frame } = getFrameBySeed(seed);
const guess = (frameId: number) => isomorphic[frameId] === id;
const check = (formula: Formula) => validWorlds(frame, formula).length;
const getAnswer = () => id;
@@ -29,7 +29,7 @@ $effect(() => {
</script>
{#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}
<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>(seed: {@render seedNumber()})</span>
</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 class="w-[300px] prose prose-sm">
@@ -76,7 +79,7 @@ $effect(() => {
</Dialog.Header>
<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} />
</div>
@@ -84,8 +87,7 @@ $effect(() => {
<div class="flex flex-col md:flex-row gap-2 w-full justify-end">
<Share seed={seed} moves={moves} status={status} />
<Button
href="/kripke/random"
data-sveltekit-reload>
href="/kripke/random">
<LuRotateCw class="w-4 h-4 mt-[2px]" /> Play New Game
</Button>
<Button

View File

@@ -1,6 +1,6 @@
import { persisted } from "svelte-persisted-store";
import type { Move } from "./game.svelte";
import { date } from "./system";
import type { Move } from "./components/game.svelte";
import { date } from "./lib/system";
export type Daily = {
date: string;