Ben Blaker Claude Opus 4.8 (1M context) commited on
Commit
d6d25e1
Β·
unverified Β·
1 Parent(s): 287713b

feat(game): add run stats to the win screen (#71)

Browse files

Tally the whole session and surface it on the victory card: time played, total hops, hints used, databases crashed (delete-all-data wipes), and a fail count + rate (missed verdicts + deaths over all attempts). The tally spans every board and survives the delete-all wipe, so it is never reset.

Hints only count when a real hint is revealed (not the shrug), and the label nags with "(seriously..?)" when any were used.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. static/game.html +2 -1
  2. static/game.js +69 -1
  3. static/style.css +31 -1
static/game.html CHANGED
@@ -62,7 +62,8 @@
62
  <div class="sq-victory sq-hidden" role="dialog" aria-modal="true" aria-label="you win">
63
  <div class="sq-victory-card">
64
  <div class="sq-victory-title">you win!</div>
65
- <div class="sq-victory-body">Congratulations β€” you are a master prompter. πŸ€—</div>
 
66
  <button class="sq-victory-close" type="button">keep prompting</button>
67
  </div>
68
  </div>
 
62
  <div class="sq-victory sq-hidden" role="dialog" aria-modal="true" aria-label="you win">
63
  <div class="sq-victory-card">
64
  <div class="sq-victory-title">you win!</div>
65
+ <div class="sq-victory-body">Congratulations. I hereby crown you a competent wrangler of small LLMs</div>
66
+ <dl class="sq-victory-stats"></dl>
67
  <button class="sq-victory-close" type="button">keep prompting</button>
68
  </div>
69
  </div>
static/game.js CHANGED
@@ -110,6 +110,21 @@
110
  // Breadcrumb for the cross-level portal: the board to offer a way back to.
111
  let returnTo = null;
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  // Each injection target needs its unlock: "build" is free, "small" needs the
114
  // shrink, "hackathon" needs the πŸ€— earned by clearing critters. `submitPressable`
115
  // is whether a target's lane tile can be shipped right now (unlocked + unshipped).
@@ -1899,7 +1914,11 @@
1899
  // "yes": swap the confirm prompt for the actual nudge (re-read so critters
1900
  // reflects what's already been collected)
1901
  function revealHint() {
1902
- hintTextEl.textContent = hintText();
 
 
 
 
1903
  hintQEl.classList.add("sq-hidden");
1904
  hintActionsEl.classList.add("sq-hidden");
1905
  hintTextEl.classList.remove("sq-hidden");
@@ -2190,6 +2209,11 @@
2190
  }
2191
 
2192
  function land(r, c) {
 
 
 
 
 
2193
  pos = [r, c];
2194
 
2195
  // a swap tile hops you to another board β€” costs no budget, adds no word.
@@ -2388,6 +2412,7 @@
2388
  // `reason` (a file-bomb dump) flavours the death card; bare die() = ran out of hops.
2389
  let deathReason = null; // { file, dumpLine } | null
2390
  function die(reason) {
 
2391
  deathReason = reason || null;
2392
  state = "dead";
2393
  buffered = null;
@@ -2481,6 +2506,7 @@
2481
  // completing the board pops the rainbow victory modal (checkOff β†’ showVictory);
2482
  // collected out of order (build/small still pending), just carry on.
2483
  function winHackathon() {
 
2484
  state = "verdict"; // hold input through the celebration
2485
  buffered = null;
2486
  setPose("hop-mid"); // the win pose
@@ -2584,6 +2610,10 @@
2584
 
2585
  function showVerdict(res) {
2586
  state = "verdict";
 
 
 
 
2587
  setPose(res.verdict === "win" ? "hop-mid" : "idle");
2588
  hintEl.textContent = "";
2589
 
@@ -2810,12 +2840,49 @@
2810
  // Pops once build + small + hackathon are all collected on the bonus board.
2811
  const victoryEl = element.querySelector(".sq-victory");
2812
  const victoryCloseBtn = element.querySelector(".sq-victory-close");
 
2813
  let victoryShown = false;
2814
  let victoryOpen = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2815
  function showVictory() {
2816
  if (victoryShown) return;
2817
  victoryShown = true;
2818
  victoryOpen = true;
 
2819
  // clear the verdict card under it (the last "hackathon!" stamp has landed)
2820
  overlayEl.classList.add("sq-hidden");
2821
  overlayEl.style.opacity = "";
@@ -2850,6 +2917,7 @@
2850
  let errorOpen = false;
2851
 
2852
  function deleteAllData() {
 
2853
  state = "verdict"; // freeze the board + input behind the modal
2854
  buffered = null;
2855
  setDataArmed(false); // the line's complete β€” drop the red
 
110
  // Breadcrumb for the cross-level portal: the board to offer a way back to.
111
  let returnTo = null;
112
 
113
+ // Run-wide tally surfaced on the win screen β€” the story of the whole session.
114
+ // Spans every board AND survives the "delete all data" wipe (a crash is itself
115
+ // a stat), so it is deliberately NEVER reset; only `loadGame`/page reload
116
+ // starts it over. `startMs` is stamped on the first hop so the clock measures
117
+ // time spent playing, not time spent reading the welcome card.
118
+ const stats = {
119
+ hops: 0, // every tile-to-tile hop the doodle makes, all boards
120
+ startMs: 0, // performance.now() at the first hop; 0 until then
121
+ hints: 0, // hint reveals ("yes, show me the hint")
122
+ crashes: 0, // "delete all data" 404 wipes triggered
123
+ wins: 0, // targets collected (judge wins + the hackathon finale)
124
+ losses: 0, // judged sentences that missed their target
125
+ deaths: 0, // ran out of hops / face-planted a context bomb
126
+ };
127
+
128
  // Each injection target needs its unlock: "build" is free, "small" needs the
129
  // shrink, "hackathon" needs the πŸ€— earned by clearing critters. `submitPressable`
130
  // is whether a target's lane tile can be shipped right now (unlocked + unshipped).
 
1914
  // "yes": swap the confirm prompt for the actual nudge (re-read so critters
1915
  // reflects what's already been collected)
1916
  function revealHint() {
1917
+ const text = hintText();
1918
+ // only a *real* hint counts toward the win-screen tally β€” the shrug (shown
1919
+ // once every hinted target on the board is already in) isn't a hint at all.
1920
+ if (text !== SHRUG_HINT) stats.hints += 1;
1921
+ hintTextEl.textContent = text;
1922
  hintQEl.classList.add("sq-hidden");
1923
  hintActionsEl.classList.add("sq-hidden");
1924
  hintTextEl.classList.remove("sq-hidden");
 
2209
  }
2210
 
2211
  function land(r, c) {
2212
+ // every completed hop funnels through here (ground hops, glides, swap +
2213
+ // submit keys), so it's the one place to tally total hops and start the
2214
+ // play clock on the very first one.
2215
+ if (stats.startMs === 0) stats.startMs = performance.now();
2216
+ stats.hops += 1;
2217
  pos = [r, c];
2218
 
2219
  // a swap tile hops you to another board β€” costs no budget, adds no word.
 
2412
  // `reason` (a file-bomb dump) flavours the death card; bare die() = ran out of hops.
2413
  let deathReason = null; // { file, dumpLine } | null
2414
  function die(reason) {
2415
+ stats.deaths += 1; // a fail, for the win-screen failure rate
2416
  deathReason = reason || null;
2417
  state = "dead";
2418
  buffered = null;
 
2506
  // completing the board pops the rainbow victory modal (checkOff β†’ showVictory);
2507
  // collected out of order (build/small still pending), just carry on.
2508
  function winHackathon() {
2509
+ stats.wins += 1; // the guaranteed finale still counts as a target collected
2510
  state = "verdict"; // hold input through the celebration
2511
  buffered = null;
2512
  setPose("hop-mid"); // the win pose
 
2610
 
2611
  function showVerdict(res) {
2612
  state = "verdict";
2613
+ // a winning verdict always lands a fresh target (we only ever judge against
2614
+ // the still-uncollected ones); anything else is a miss.
2615
+ if (res.verdict === "win") stats.wins += 1;
2616
+ else stats.losses += 1;
2617
  setPose(res.verdict === "win" ? "hop-mid" : "idle");
2618
  hintEl.textContent = "";
2619
 
 
2840
  // Pops once build + small + hackathon are all collected on the bonus board.
2841
  const victoryEl = element.querySelector(".sq-victory");
2842
  const victoryCloseBtn = element.querySelector(".sq-victory-close");
2843
+ const victoryStatsEl = element.querySelector(".sq-victory-stats");
2844
  let victoryShown = false;
2845
  let victoryOpen = false;
2846
+
2847
+ // mm:ss for the play clock (drops to h:mm:ss past the hour, just in case).
2848
+ function formatDuration(ms) {
2849
+ const total = Math.max(0, Math.round(ms / 1000));
2850
+ const s = total % 60, m = Math.floor(total / 60) % 60, h = Math.floor(total / 3600);
2851
+ const pad = (n) => String(n).padStart(2, "0");
2852
+ return h ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
2853
+ }
2854
+
2855
+ // Tally the run into the victory card: time played, hops, hints, databases
2856
+ // crashed, and the fail count + rate (misses + deaths over every attempt).
2857
+ function renderVictoryStats() {
2858
+ const fails = stats.losses + stats.deaths;
2859
+ const attempts = stats.wins + fails;
2860
+ const rate = attempts ? Math.round((fails / attempts) * 100) : 0;
2861
+ const rows = [
2862
+ ["time", stats.startMs ? formatDuration(performance.now() - stats.startMs) : "0:00"],
2863
+ ["hops", String(stats.hops)],
2864
+ [stats.hints > 0 ? "hints used (seriously..?)" : "hints used", String(stats.hints)],
2865
+ ["databases crashed", String(stats.crashes)],
2866
+ ["fails", `${fails} (${rate}%)`],
2867
+ ];
2868
+ victoryStatsEl.innerHTML = "";
2869
+ for (const [label, value] of rows) {
2870
+ const row = document.createElement("div");
2871
+ row.className = "sq-stat";
2872
+ const dt = document.createElement("dt");
2873
+ dt.textContent = label;
2874
+ const dd = document.createElement("dd");
2875
+ dd.textContent = value;
2876
+ row.append(dt, dd);
2877
+ victoryStatsEl.appendChild(row);
2878
+ }
2879
+ }
2880
+
2881
  function showVictory() {
2882
  if (victoryShown) return;
2883
  victoryShown = true;
2884
  victoryOpen = true;
2885
+ renderVictoryStats();
2886
  // clear the verdict card under it (the last "hackathon!" stamp has landed)
2887
  overlayEl.classList.add("sq-hidden");
2888
  overlayEl.style.opacity = "";
 
2917
  let errorOpen = false;
2918
 
2919
  function deleteAllData() {
2920
+ stats.crashes += 1; // a database wiped β€” counted toward the win-screen tally
2921
  state = "verdict"; // freeze the board + input behind the modal
2922
  buffered = null;
2923
  setDataArmed(false); // the line's complete β€” drop the red
static/style.css CHANGED
@@ -643,7 +643,33 @@
643
  font-size: 21px;
644
  line-height: 1.5;
645
  color: var(--ink-soft);
646
- margin-bottom: 22px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  }
648
  .sq-victory-close {
649
  font-family: "Patrick Hand", cursive;
@@ -783,4 +809,8 @@
783
  .sq-errorbox-card { width: min(380px, 88vw); padding: 24px 22px 22px; }
784
  .sq-errorbox-title { font-size: 36px; margin-bottom: 10px; }
785
  .sq-errorbox-body { font-size: 18px; margin-bottom: 18px; }
 
 
 
 
786
  }
 
643
  font-size: 21px;
644
  line-height: 1.5;
645
  color: var(--ink-soft);
646
+ margin-bottom: 18px;
647
+ }
648
+ /* The run tally β€” a tidy ledger of the session, ruled like a notebook line. */
649
+ .sq-victory-stats {
650
+ width: min(300px, 100%);
651
+ margin: 0 auto 22px;
652
+ text-align: left;
653
+ font-family: "Patrick Hand", cursive;
654
+ }
655
+ .sq-stat {
656
+ display: flex;
657
+ align-items: baseline;
658
+ justify-content: space-between;
659
+ gap: 12px;
660
+ padding: 5px 2px;
661
+ border-bottom: 2px dotted rgba(28, 27, 24, 0.18);
662
+ }
663
+ .sq-stat:last-child { border-bottom: 0; }
664
+ .sq-stat dt {
665
+ margin: 0;
666
+ font-size: 19px;
667
+ color: var(--ink-soft);
668
+ }
669
+ .sq-stat dd {
670
+ margin: 0;
671
+ font-size: 22px;
672
+ color: var(--ink);
673
  }
674
  .sq-victory-close {
675
  font-family: "Patrick Hand", cursive;
 
809
  .sq-errorbox-card { width: min(380px, 88vw); padding: 24px 22px 22px; }
810
  .sq-errorbox-title { font-size: 36px; margin-bottom: 10px; }
811
  .sq-errorbox-body { font-size: 18px; margin-bottom: 18px; }
812
+
813
+ .sq-victory-stats { width: min(280px, 100%); margin-bottom: 18px; }
814
+ .sq-stat dt { font-size: 17px; }
815
+ .sq-stat dd { font-size: 20px; }
816
  }