import React, { useEffect, useState } from "react"; import { newGame, castSpell } from "./api.js"; export default function App() { const [snap, setSnap] = useState(null); const [selected, setSelected] = useState([]); const [spell, setSpell] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const start = async () => { setError(null); setSpell(null); setSelected([]); try { setSnap(await newGame()); } catch (e) { setError(`Backend unreachable. Start it with: uv run uvicorn api.server:app --port 8000`); } }; useEffect(() => { start(); }, []); const toggleRune = (key) => { setSelected((cur) => { if (cur.includes(key)) return cur.filter((k) => k !== key); if (cur.length >= 4) return cur; return [...cur, key]; }); }; const cast = async () => { if (!snap || selected.length === 0 || busy) return; setBusy(true); setError(null); try { const res = await castSpell(snap.session_id, selected); setSpell(res.spell); setSnap(res); setSelected([]); } catch (e) { setError(String(e)); } finally { setBusy(false); } }; if (!snap) { return (

🪄 Rune Goblin

{error ?? "Summoning the dungeon…"}

); } const { enemy, player, room, runes, log } = snap; return (

🪄 Rune Goblin

Draw forbidden spells. Regret efficiently.

👹 {enemy.name}

weak: {enemy.weakness.join(", ")} · resists: {enemy.resistance.join(", ")}

mood: {enemy.mood}

🧙 You — Room {room.index + 1}/{room.total}

{room.name}

score: {snap.score} · 🎒 {player.inventory.join(", ") || "empty"}

{runes.map((r) => ( ))}
{selected.join(" + ") || "pick 2–4 runes"}
{spell && (

✨ {spell.spell_name}

{spell.flavor}

{spell.effect}

{spell.side_effect &&

⚠ {spell.side_effect}

}

enemy {spell.enemy_hp_delta} · player {spell.player_hp_delta} · chaos {spell.chaos}/10

)} {snap.over && (
{snap.won ? "🏆 You survived the dungeon!" : "💀 You collapsed."} Score: {snap.score}
)}
{log.join("\n")}
{error &&

{error}

}
); } function Bar({ value, max, kind }) { const pct = Math.max(0, Math.round((value / max) * 100)); return (
{value}/{max}
); }