ASHu2's picture
Scaffold Rune Goblin: RuneLang engine, fine-tuning, game UIs
7b007ed unverified
Raw
History Blame Contribute Delete
4.24 kB
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 (
<div className="wrap">
<h1>🪄 Rune Goblin</h1>
<p className="muted">{error ?? "Summoning the dungeon…"}</p>
</div>
);
}
const { enemy, player, room, runes, log } = snap;
return (
<div className="wrap">
<header>
<h1>🪄 Rune Goblin</h1>
<p className="tag">Draw forbidden spells. Regret efficiently.</p>
</header>
<div className="cards">
<section className="card enemy">
<h2>👹 {enemy.name}</h2>
<Bar value={enemy.hp} max={enemy.max_hp} kind="hp" />
<p className="muted">
weak: <b>{enemy.weakness.join(", ")}</b> · resists: {enemy.resistance.join(", ")}
</p>
<p className="mood">mood: {enemy.mood}</p>
</section>
<section className="card player">
<h2>
🧙 You — Room {room.index + 1}/{room.total}
</h2>
<p className="room">{room.name}</p>
<Bar value={player.hp} max={player.max_hp} kind="health" />
<p className="muted">score: {snap.score} · 🎒 {player.inventory.join(", ") || "empty"}</p>
</section>
</div>
<div className="board">
{runes.map((r) => (
<button
key={r.key}
className={`rune ${selected.includes(r.key) ? "on" : ""}`}
onClick={() => toggleRune(r.key)}
title={r.label}
>
<span className="sym">{r.symbol}</span>
<span className="lbl">{r.label}</span>
</button>
))}
</div>
<div className="selected">{selected.join(" + ") || "pick 2–4 runes"}</div>
<div className="actions">
<button className="cast" disabled={busy || snap.over} onClick={cast}>
{busy ? "casting…" : "🔮 CAST SPELL"}
</button>
<button onClick={() => setSelected([])}>clear</button>
<button onClick={start}>new run</button>
</div>
{spell && (
<div className="result">
<h3>✨ {spell.spell_name}</h3>
<p className="flavor">{spell.flavor}</p>
<p>{spell.effect}</p>
{spell.side_effect && <p className="side">⚠ {spell.side_effect}</p>}
<p className="muted">
enemy {spell.enemy_hp_delta} · player {spell.player_hp_delta} · chaos {spell.chaos}/10
</p>
</div>
)}
{snap.over && (
<div className={`banner ${snap.won ? "win" : "lose"}`}>
{snap.won ? "🏆 You survived the dungeon!" : "💀 You collapsed."} Score: {snap.score}
</div>
)}
<pre className="log">{log.join("\n")}</pre>
{error && <p className="error">{error}</p>}
</div>
);
}
function Bar({ value, max, kind }) {
const pct = Math.max(0, Math.round((value / max) * 100));
return (
<div className="bar">
<div className={`fill ${kind}`} style={{ width: `${pct}%` }} />
<span>
{value}/{max}
</span>
</div>
);
}