Spaces:
Running
Running
Download game.js from CyberMax-tools/sumbloom: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/CyberMax-tools/sumbloom/resolve/96a4f6b282f85a53d741a4da427332e22084cbfe/game.js
- Command line
-
hf download hf://spaces/CyberMax-tools/sumbloom@96a4f6b282f85a53d741a4da427332e22084cbfe/game.js
-
curl -L -o game.js https://huggingface.co/spaces/CyberMax-tools/sumbloom/resolve/96a4f6b282f85a53d741a4da427332e22084cbfe/game.js
14.6 kB
| /* Sumbloom — daily number puzzle. © 2026 CyberMax. All rights reserved. */ | |
| (function () { | |
| "use strict"; | |
| var SITE = "https://shaw276-sumbloom.static.hf.space/"; | |
| var KEY = "sumbloom:v1:"; | |
| var C = window.SumbloomCore; | |
| var makePuzzle = C.makePuzzle, apply = C.apply, puzzleNo = C.puzzleNo, todayStr = C.todayStr, OPS = C.OPS, FLOWER = C.FLOWER; | |
| // ---------- storage (never throws) ---------- | |
| function load(k, dflt) { | |
| try { var v = localStorage.getItem(KEY + k); return v ? JSON.parse(v) : dflt; } catch (e) { return dflt; } | |
| } | |
| function save(k, v) { try { localStorage.setItem(KEY + k, JSON.stringify(v)); } catch (e) { /* ignore */ } } | |
| // ---------- state ---------- | |
| var el = function (id) { return document.getElementById(id); }; | |
| var mode = "daily"; | |
| var dateStr = todayStr(); | |
| var puzzle, state, active = 0, selTile = null, selOp = null, practiceSeed = null; | |
| function freshState() { | |
| return { | |
| rounds: puzzle.map(function (r) { | |
| return { tiles: r.nums.map(function (n, i) { return { id: i, v: n, gone: false }; }), hist: [], log: [], done: false, stars: 0, best: null }; | |
| }) | |
| }; | |
| } | |
| function startDaily() { | |
| mode = "daily"; | |
| dateStr = todayStr(); | |
| puzzle = makePuzzle("daily:" + dateStr); | |
| state = load("day:" + dateStr, null); | |
| if (!state || !state.rounds || state.rounds.length !== 5) state = freshState(); | |
| active = firstOpen(); | |
| el("puzzle-label").textContent = "Daily #" + puzzleNo(dateStr) + " · " + dateStr; | |
| el("btn-mode").textContent = "Practice mode"; | |
| render(); | |
| } | |
| function startPractice() { | |
| mode = "practice"; | |
| practiceSeed = "practice:" + Date.now() + ":" + Math.random(); | |
| puzzle = makePuzzle(practiceSeed); | |
| state = freshState(); | |
| active = 0; | |
| el("puzzle-label").textContent = "Practice puzzle"; | |
| el("btn-mode").textContent = "Back to daily"; | |
| render(); | |
| } | |
| function firstOpen() { | |
| for (var i = 0; i < 5; i++) if (!state.rounds[i].done) return i; | |
| return 4; | |
| } | |
| function persist() { if (mode === "daily") save("day:" + dateStr, state); } | |
| // ---------- rendering ---------- | |
| function render() { | |
| var R = state.rounds[active]; | |
| var rounds = el("rounds"); | |
| rounds.innerHTML = ""; | |
| puzzle.forEach(function (p, i) { | |
| var b = document.createElement("button"); | |
| var rs = state.rounds[i]; | |
| b.className = "round" + (i === active ? " active" : "") + (rs.done ? " done" : ""); | |
| b.innerHTML = p.target + "<small>" + (rs.done ? "★".repeat(rs.stars) + "☆".repeat(3 - rs.stars) : "") + "</small>"; | |
| b.setAttribute("aria-label", "Target " + p.target + (rs.done ? ", " + rs.stars + " stars" : "")); | |
| b.onclick = function () { active = i; selTile = null; selOp = null; render(); }; | |
| rounds.appendChild(b); | |
| }); | |
| el("target").textContent = puzzle[active].target; | |
| var best = closestOnBoard(R); | |
| if (R.done) { | |
| el("closest").textContent = R.stars === 3 ? "Bloomed! Exact hit." : "Submitted " + R.best + " (" + Math.abs(R.best - puzzle[active].target) + " away)"; | |
| } else { | |
| el("closest").textContent = best !== null ? "Closest so far: " + best + (best === puzzle[active].target ? "" : " (" + Math.abs(best - puzzle[active].target) + " away)") : " "; | |
| } | |
| var board = el("board"); | |
| board.innerHTML = ""; | |
| R.tiles.forEach(function (t) { | |
| var b = document.createElement("button"); | |
| b.className = "tile" + (t.gone ? " gone" : "") + (selTile === t.id ? " sel" : "") + (t.fresh ? " new" : "") + | |
| (R.done && t.v === puzzle[active].target && !t.gone ? " hit" : ""); | |
| b.textContent = t.v; | |
| b.disabled = R.done || t.gone; | |
| b.setAttribute("aria-label", "Number " + t.v); | |
| b.onclick = function () { tapTile(t.id, b); }; | |
| board.appendChild(b); | |
| t.fresh = false; | |
| }); | |
| Array.prototype.forEach.call(document.querySelectorAll(".op"), function (b) { | |
| b.classList.toggle("sel", selOp === b.dataset.op); | |
| b.disabled = R.done; | |
| }); | |
| el("btn-undo").disabled = R.done || R.hist.length === 0; | |
| el("btn-submit").disabled = R.done; | |
| el("btn-submit").textContent = R.done ? (allDone() ? "See results" : "Next target →") : "Submit closest"; | |
| if (R.done) el("btn-submit").disabled = false; | |
| var log = el("log"); | |
| log.innerHTML = ""; | |
| R.log.forEach(function (s) { var li = document.createElement("li"); li.textContent = s; log.appendChild(li); }); | |
| } | |
| function closestOnBoard(R) { | |
| var t = puzzle[active].target, best = null; | |
| R.tiles.forEach(function (x) { if (!x.gone && (best === null || Math.abs(x.v - t) < Math.abs(best - t))) best = x.v; }); | |
| return best; | |
| } | |
| function allDone() { return state.rounds.every(function (r) { return r.done; }); } | |
| function tapTile(id, node) { | |
| var R = state.rounds[active]; | |
| if (R.done) return; | |
| if (selTile === null || (selOp === null)) { | |
| selTile = selTile === id ? null : id; | |
| render(); | |
| return; | |
| } | |
| if (selTile === id) { selTile = null; selOp = null; render(); return; } | |
| var a = R.tiles.filter(function (t) { return t.id === selTile; })[0]; | |
| var b = R.tiles.filter(function (t) { return t.id === id; })[0]; | |
| var v = apply(a.v, selOp, b.v); | |
| if (!v) { | |
| node.classList.add("shake"); | |
| toast(selOp === "-" ? "Results must be positive: try the bigger number first" : "Only whole-number division"); | |
| setTimeout(function () { node.classList.remove("shake"); }, 320); | |
| return; | |
| } | |
| R.hist.push(JSON.stringify({ tiles: R.tiles, log: R.log })); | |
| R.log = R.log.concat(a.v + " " + OPS[selOp] + " " + b.v + " = " + v); | |
| a.gone = true; | |
| b.v = v; b.fresh = true; | |
| selTile = id; selOp = null; | |
| if (v === puzzle[active].target) finishRound(3, v); | |
| persist(); | |
| render(); | |
| } | |
| function tapOp(op) { | |
| var R = state.rounds[active]; | |
| if (R.done) return; | |
| if (selTile === null) { toast("Pick a number first"); return; } | |
| selOp = selOp === op ? null : op; | |
| render(); | |
| } | |
| function undo() { | |
| var R = state.rounds[active]; | |
| if (R.done || !R.hist.length) return; | |
| var prev = JSON.parse(R.hist.pop()); | |
| R.tiles = prev.tiles; R.log = prev.log; | |
| selTile = null; selOp = null; | |
| persist(); render(); | |
| } | |
| function starsFor(diff) { return diff === 0 ? 3 : diff <= 10 ? 2 : diff <= 25 ? 1 : 0; } | |
| function finishRound(stars, best) { | |
| var R = state.rounds[active]; | |
| R.done = true; R.stars = stars; R.best = best; R.hist = []; | |
| selTile = null; selOp = null; | |
| toast(stars === 3 ? "🌸 Bloomed!" : FLOWER[stars] + " " + stars + " star" + (stars === 1 ? "" : "s")); | |
| if (allDone()) { | |
| if (mode === "daily") recordStats(); | |
| persist(); | |
| setTimeout(showResults, 700); | |
| } | |
| } | |
| function submit() { | |
| var R = state.rounds[active]; | |
| if (R.done) { | |
| if (allDone()) { showResults(); return; } | |
| active = firstOpen(); render(); return; | |
| } | |
| var best = closestOnBoard(R); | |
| finishRound(starsFor(Math.abs(best - puzzle[active].target)), best); | |
| persist(); render(); | |
| } | |
| // ---------- stats ---------- | |
| function recordStats() { | |
| var st = load("stats", { played: 0, streak: 0, maxStreak: 0, last: null, total: 0, perfect: 0, dist: {} }); | |
| if (st.last === dateStr) return; | |
| var score = totalStars(); | |
| var y = new Date(); y.setDate(y.getDate() - 1); | |
| st.streak = st.last === todayStr(y) ? st.streak + 1 : 1; | |
| st.maxStreak = Math.max(st.maxStreak, st.streak); | |
| st.played += 1; st.total += score; if (score === 15) st.perfect += 1; | |
| st.dist[score] = (st.dist[score] || 0) + 1; | |
| st.last = dateStr; | |
| save("stats", st); | |
| } | |
| function totalStars() { return state.rounds.reduce(function (s, r) { return s + (r.stars || 0); }, 0); } | |
| function shareText() { | |
| var head = mode === "daily" ? "Sumbloom #" + puzzleNo(dateStr) : "Sumbloom practice"; | |
| return head + " " + totalStars() + "/15\n" + state.rounds.map(function (r) { return FLOWER[r.stars || 0]; }).join("") + "\n" + SITE; | |
| } | |
| function share() { | |
| var text = shareText(); | |
| if (navigator.share && /Mobi|Android/i.test(navigator.userAgent)) { | |
| navigator.share({ text: text }).catch(function () { copy(text); }); | |
| } else copy(text); | |
| } | |
| function copy(text) { | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(text).then(function () { toast("Copied to clipboard"); }, function () { fallbackCopy(text); }); | |
| } else fallbackCopy(text); | |
| } | |
| function fallbackCopy(text) { | |
| var ta = document.createElement("textarea"); | |
| ta.value = text; document.body.appendChild(ta); ta.select(); | |
| try { document.execCommand("copy"); toast("Copied to clipboard"); } catch (e) { toast("Copy failed"); } | |
| document.body.removeChild(ta); | |
| } | |
| // ---------- modals ---------- | |
| function openModal(html) { | |
| el("modal-body").innerHTML = html; | |
| el("modal").hidden = false; | |
| el("modal-close").focus(); | |
| } | |
| function closeModal() { el("modal").hidden = true; clearInterval(cdTimer); } | |
| var cdTimer = null; | |
| function countdown() { | |
| var now = new Date(), next = new Date(now); next.setHours(24, 0, 0, 0); | |
| var s = Math.max(0, Math.floor((next - now) / 1000)); | |
| return String(Math.floor(s / 3600)).padStart(2, "0") + ":" + String(Math.floor(s % 3600 / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0"); | |
| } | |
| function solutionHtml() { | |
| return puzzle.map(function (p, i) { | |
| return "<div class='sol'><b>" + p.target + "</b>: " + p.solution.map(function (s) { return s[0] + " " + OPS[s[1]] + " " + s[2] + " = " + s[3]; }).join(" · ") + "</div>"; | |
| }).join("<div style='height:6px'></div>"); | |
| } | |
| function showResults() { | |
| var html = "<h2>" + (mode === "daily" ? "Daily #" + puzzleNo(dateStr) : "Practice") + " complete</h2>" + | |
| "<div class='big-score'>" + totalStars() + "/15</div>" + | |
| "<div class='share-grid'>" + state.rounds.map(function (r) { return FLOWER[r.stars || 0]; }).join("") + "</div>" + | |
| "<button class='btn' id='btn-share' style='width:100%'>Share result</button>" + | |
| "<p style='font-size:14px'>One way to reach each target:</p>" + solutionHtml() + | |
| (mode === "daily" ? "<p class='countdown'>Next puzzle in <b id='cd'>" + countdown() + "</b></p>" : "") + | |
| "<button class='btn ghost' id='btn-practice' style='width:100%;margin-top:8px'>" + (mode === "daily" ? "Play a practice puzzle" : "New practice puzzle") + "</button>"; | |
| openModal(html); | |
| el("btn-share").onclick = share; | |
| el("btn-practice").onclick = function () { closeModal(); startPractice(); }; | |
| if (mode === "daily") cdTimer = setInterval(function () { var c = el("cd"); if (c) c.textContent = countdown(); }, 1000); | |
| } | |
| function showStats() { | |
| var st = load("stats", { played: 0, streak: 0, maxStreak: 0, total: 0, perfect: 0, dist: {} }); | |
| var avg = st.played ? (st.total / st.played).toFixed(1) : "–"; | |
| var max = 1; for (var k in st.dist) max = Math.max(max, st.dist[k]); | |
| var rows = ""; | |
| for (var sc = 15; sc >= 0; sc--) { | |
| var n = st.dist[sc] || 0; | |
| if (!n && sc < 10) continue; | |
| rows += "<div class='dist-row'><span style='width:22px;text-align:right'>" + sc + "</span><span class='bar-fill' style='width:" + (8 + 80 * n / max) + "%'>" + n + "</span></div>"; | |
| } | |
| openModal("<h2>Statistics</h2><div class='stats'><div><b>" + st.played + "</b><span>Played</span></div><div><b>" + avg + | |
| "</b><span>Avg stars</span></div><div><b>" + st.streak + "</b><span>Streak</span></div><div><b>" + st.maxStreak + | |
| "</b><span>Best streak</span></div></div><p style='font-size:14px;margin-bottom:4px'>Daily scores (" + st.perfect + " perfect)</p><div class='dist'>" + | |
| (rows || "<p style='font-size:14px;color:var(--muted)'>Finish today's puzzle to start your stats.</p>") + "</div>" + | |
| (allDone() && mode === "daily" ? "<button class='btn' id='btn-share2' style='width:100%'>Share today's result</button>" : "")); | |
| var b = el("btn-share2"); if (b) b.onclick = share; | |
| } | |
| function showHelp() { | |
| openModal("<h2>How to play</h2><p>Reach the target by combining numbers with + − × ÷.</p>" + | |
| "<div class='how-demo'><span>5</span><span class='o'>×</span><span>6</span><span class='o'>=</span><span style='background:var(--petal);color:#fff;border-color:var(--petal)'>30</span></div>" + | |
| "<p>Tap a number, tap an operation, then tap a second number. They merge into the result. You don't need every number, and only whole positive results count.</p>" + | |
| "<p><b>Scoring:</b> exact = 3 blossoms 🌸, within 10 = 2 🌼, within 25 = 1 🌱. Can't get closer? Press <b>Submit closest</b>.</p>" + | |
| "<p>Five targets a day, the same for everyone. A new puzzle every midnight, plus unlimited practice. Keys: + − * / and Backspace to undo.</p>" + | |
| "<button class='btn' id='btn-go' style='width:100%'>Play</button>"); | |
| el("btn-go").onclick = closeModal; | |
| } | |
| var toastTimer = null; | |
| function toast(msg) { | |
| var t = el("toast"); t.textContent = msg; t.classList.add("show"); | |
| clearTimeout(toastTimer); toastTimer = setTimeout(function () { t.classList.remove("show"); }, 1600); | |
| } | |
| // ---------- wire up ---------- | |
| Array.prototype.forEach.call(document.querySelectorAll(".op"), function (b) { b.onclick = function () { tapOp(b.dataset.op); }; }); | |
| el("btn-undo").onclick = undo; | |
| el("btn-submit").onclick = submit; | |
| el("btn-help").onclick = showHelp; | |
| el("btn-stats").onclick = showStats; | |
| el("btn-mode").onclick = function () { mode === "daily" ? startPractice() : startDaily(); }; | |
| el("modal-close").onclick = closeModal; | |
| el("modal").onclick = function (e) { if (e.target === el("modal")) closeModal(); }; | |
| document.addEventListener("keydown", function (e) { | |
| if (!el("modal").hidden) { if (e.key === "Escape") closeModal(); return; } | |
| if (OPS[e.key]) tapOp(e.key); | |
| else if (e.key === "x" || e.key === "X") tapOp("*"); | |
| else if (e.key === "Backspace") undo(); | |
| else if (e.key === "Enter") submit(); | |
| }); | |
| // expose for tests | |
| window.Sumbloom = { makePuzzle: makePuzzle, apply: apply, puzzleNo: puzzleNo, state: function () { return state; }, puzzle: function () { return puzzle; } }; | |
| document.addEventListener("visibilitychange", function () { | |
| if (!document.hidden && mode === "daily" && todayStr() !== dateStr) { closeModal(); startDaily(); } | |
| }); | |
| startDaily(); | |
| if (!load("seenHelp", false)) { save("seenHelp", true); showHelp(); } | |
| if ("serviceWorker" in navigator && location.protocol === "https:") { | |
| navigator.serviceWorker.register("sw.js").catch(function () {}); | |
| } | |
| })(); | |