/* Sumbloom — daily number puzzle. © 2026 CyberMax. All rights reserved. */ (function () { "use strict"; var SITE = "https://cybermax-tools-cybermax.static.hf.space/sumbloom/index.html"; 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 }; }) }; } // ?date=YYYY-MM-DD opens that day's daily (archive links from cybermaxtools.com/games/sumbloom/daily//). // Only real past dailies (#1 = 2026-09-24 up to today); anything else is ignored and today's puzzle opens. function dateParam(search) { var d = null; try { d = new URLSearchParams(search).get("date"); } catch (e) { return null; } if (!d || !/^\d{4}-\d{2}-\d{2}$/.test(d)) return null; var t = new Date(d + "T00:00:00Z"); if (isNaN(t.getTime()) || t.toISOString().slice(0, 10) !== d) return null; if (puzzleNo(d) < 1 || d >= todayStr()) return null; return d; } var pastDay = null; function startDaily(day) { mode = "daily"; pastDay = day || null; dateStr = pastDay || 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 + (pastDay ? " (archive)" : ""); 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 + "" + (rs.done ? "★".repeat(rs.stars) + "☆".repeat(3 - rs.stars) : "") + ""; 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 || pastDay) return; // an archive day never changes the streak 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 "
" + p.target + ": " + p.solution.map(function (s) { return s[0] + " " + OPS[s[1]] + " " + s[2] + " = " + s[3]; }).join(" · ") + "
"; }).join("
"); } function showResults() { var html = "

" + (mode === "daily" ? "Daily #" + puzzleNo(dateStr) : "Practice") + " complete

" + "
" + totalStars() + "/15
" + "
" + state.rounds.map(function (r) { return FLOWER[r.stars || 0]; }).join("") + "
" + "" + "

One way to reach each target:

" + solutionHtml() + (mode === "daily" ? "

Next puzzle in " + countdown() + "

" : "") + ""; 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 += "
" + sc + "" + n + "
"; } openModal("

Statistics

" + st.played + "Played
" + avg + "Avg stars
" + st.streak + "Streak
" + st.maxStreak + "Best streak

Daily scores (" + st.perfect + " perfect)

" + (rows || "

Finish today's puzzle to start your stats.

") + "
" + (allDone() && mode === "daily" ? "" : "")); var b = el("btn-share2"); if (b) b.onclick = share; } function showHelp() { openModal("

How to play

Reach the target by combining numbers with + − × ÷.

" + "
5×6=30
" + "

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.

" + "

Scoring: exact = 3 blossoms 🌸, within 10 = 2 🌼, within 25 = 1 🌱. Can't get closer? Press Submit closest.

" + "

Five targets a day, the same for everyone. A new puzzle every midnight, plus unlimited practice. Keys: + − * / and Backspace to undo.

" + ""); 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, dateParam: dateParam, state: function () { return state; }, puzzle: function () { return puzzle; } }; document.addEventListener("visibilitychange", function () { if (!document.hidden && mode === "daily" && !pastDay && todayStr() !== dateStr) { closeModal(); startDaily(); } }); startDaily(dateParam(location.search)); if (!load("seenHelp", false)) { save("seenHelp", true); showHelp(); } if ("serviceWorker" in navigator && location.protocol === "https:") { navigator.serviceWorker.register("sw.js").catch(function () {}); } })();