Spaces:
Running
Running
CyberMax commited on
Deploy sumbloom
Browse files- README.md +24 -4
- game.js +318 -0
- icon-192.png +0 -0
- icon-512.png +0 -0
- icon.svg +1 -0
- index.html +94 -17
- manifest.webmanifest +15 -0
- og.png +0 -0
- puzzle.js +121 -0
- robots.txt +3 -0
- sitemap.xml +2 -0
- style.css +82 -26
- sw.js +20 -0
README.md
CHANGED
|
@@ -1,10 +1,30 @@
|
|
| 1 |
---
|
| 2 |
-
title: Sumbloom
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: pink
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
|
|
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: "Sumbloom — Daily Number Puzzle"
|
| 3 |
+
emoji: 🌸
|
| 4 |
colorFrom: pink
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
license: other
|
| 9 |
+
short_description: "Free daily math puzzle: 6 numbers, 5 targets"
|
| 10 |
+
tags:
|
| 11 |
+
- game
|
| 12 |
+
- puzzle
|
| 13 |
+
- math
|
| 14 |
+
- daily-puzzle
|
| 15 |
+
- html5
|
| 16 |
+
- browser-game
|
| 17 |
+
- cybermax
|
| 18 |
---
|
| 19 |
+
**Sumbloom** is a free daily number puzzle. Combine six numbers with + − × ÷ to hit five targets. Everyone gets the same puzzle each day, and every target is guaranteed solvable. Share your blossoms 🌸🌼🌱 without spoilers, keep a streak, or play unlimited practice puzzles.
|
| 20 |
|
| 21 |
+
- A new puzzle every midnight (your local time), plus practice mode
|
| 22 |
+
- Scoring: exact = 3 stars, within 10 = 2, within 25 = 1 (15 max)
|
| 23 |
+
- Stats, streaks and a spoiler-free share grid
|
| 24 |
+
- Works on phones, offline once loaded (PWA), no sign-up, no ads
|
| 25 |
+
|
| 26 |
+
Open it full screen: https://shaw276-sumbloom.static.hf.space/
|
| 27 |
+
|
| 28 |
+
More free tools and games from CyberMax: https://huggingface.co/spaces/shaw276/cybermax
|
| 29 |
+
|
| 30 |
+
© 2026 CyberMax. All rights reserved.
|
game.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Sumbloom — daily number puzzle. © 2026 CyberMax. All rights reserved. */
|
| 2 |
+
(function () {
|
| 3 |
+
"use strict";
|
| 4 |
+
|
| 5 |
+
var SITE = "https://shaw276-sumbloom.static.hf.space/";
|
| 6 |
+
var KEY = "sumbloom:v1:";
|
| 7 |
+
var C = window.SumbloomCore;
|
| 8 |
+
var makePuzzle = C.makePuzzle, apply = C.apply, puzzleNo = C.puzzleNo, todayStr = C.todayStr, OPS = C.OPS, FLOWER = C.FLOWER;
|
| 9 |
+
|
| 10 |
+
// ---------- storage (never throws) ----------
|
| 11 |
+
function load(k, dflt) {
|
| 12 |
+
try { var v = localStorage.getItem(KEY + k); return v ? JSON.parse(v) : dflt; } catch (e) { return dflt; }
|
| 13 |
+
}
|
| 14 |
+
function save(k, v) { try { localStorage.setItem(KEY + k, JSON.stringify(v)); } catch (e) { /* ignore */ } }
|
| 15 |
+
|
| 16 |
+
// ---------- state ----------
|
| 17 |
+
var el = function (id) { return document.getElementById(id); };
|
| 18 |
+
var mode = "daily";
|
| 19 |
+
var dateStr = todayStr();
|
| 20 |
+
var puzzle, state, active = 0, selTile = null, selOp = null, practiceSeed = null;
|
| 21 |
+
|
| 22 |
+
function freshState() {
|
| 23 |
+
return {
|
| 24 |
+
rounds: puzzle.map(function (r) {
|
| 25 |
+
return { tiles: r.nums.map(function (n, i) { return { id: i, v: n, gone: false }; }), hist: [], log: [], done: false, stars: 0, best: null };
|
| 26 |
+
})
|
| 27 |
+
};
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function startDaily() {
|
| 31 |
+
mode = "daily";
|
| 32 |
+
dateStr = todayStr();
|
| 33 |
+
puzzle = makePuzzle("daily:" + dateStr);
|
| 34 |
+
state = load("day:" + dateStr, null);
|
| 35 |
+
if (!state || !state.rounds || state.rounds.length !== 5) state = freshState();
|
| 36 |
+
active = firstOpen();
|
| 37 |
+
el("puzzle-label").textContent = "Daily #" + puzzleNo(dateStr) + " · " + dateStr;
|
| 38 |
+
el("btn-mode").textContent = "Practice mode";
|
| 39 |
+
render();
|
| 40 |
+
}
|
| 41 |
+
function startPractice() {
|
| 42 |
+
mode = "practice";
|
| 43 |
+
practiceSeed = "practice:" + Date.now() + ":" + Math.random();
|
| 44 |
+
puzzle = makePuzzle(practiceSeed);
|
| 45 |
+
state = freshState();
|
| 46 |
+
active = 0;
|
| 47 |
+
el("puzzle-label").textContent = "Practice puzzle";
|
| 48 |
+
el("btn-mode").textContent = "Back to daily";
|
| 49 |
+
render();
|
| 50 |
+
}
|
| 51 |
+
function firstOpen() {
|
| 52 |
+
for (var i = 0; i < 5; i++) if (!state.rounds[i].done) return i;
|
| 53 |
+
return 4;
|
| 54 |
+
}
|
| 55 |
+
function persist() { if (mode === "daily") save("day:" + dateStr, state); }
|
| 56 |
+
|
| 57 |
+
// ---------- rendering ----------
|
| 58 |
+
function render() {
|
| 59 |
+
var R = state.rounds[active];
|
| 60 |
+
var rounds = el("rounds");
|
| 61 |
+
rounds.innerHTML = "";
|
| 62 |
+
puzzle.forEach(function (p, i) {
|
| 63 |
+
var b = document.createElement("button");
|
| 64 |
+
var rs = state.rounds[i];
|
| 65 |
+
b.className = "round" + (i === active ? " active" : "") + (rs.done ? " done" : "");
|
| 66 |
+
b.innerHTML = p.target + "<small>" + (rs.done ? "★".repeat(rs.stars) + "☆".repeat(3 - rs.stars) : "") + "</small>";
|
| 67 |
+
b.setAttribute("aria-label", "Target " + p.target + (rs.done ? ", " + rs.stars + " stars" : ""));
|
| 68 |
+
b.onclick = function () { active = i; selTile = null; selOp = null; render(); };
|
| 69 |
+
rounds.appendChild(b);
|
| 70 |
+
});
|
| 71 |
+
el("target").textContent = puzzle[active].target;
|
| 72 |
+
var best = closestOnBoard(R);
|
| 73 |
+
if (R.done) {
|
| 74 |
+
el("closest").textContent = R.stars === 3 ? "Bloomed! Exact hit." : "Submitted " + R.best + " (" + Math.abs(R.best - puzzle[active].target) + " away)";
|
| 75 |
+
} else {
|
| 76 |
+
el("closest").textContent = best !== null ? "Closest so far: " + best + (best === puzzle[active].target ? "" : " (" + Math.abs(best - puzzle[active].target) + " away)") : " ";
|
| 77 |
+
}
|
| 78 |
+
var board = el("board");
|
| 79 |
+
board.innerHTML = "";
|
| 80 |
+
R.tiles.forEach(function (t) {
|
| 81 |
+
var b = document.createElement("button");
|
| 82 |
+
b.className = "tile" + (t.gone ? " gone" : "") + (selTile === t.id ? " sel" : "") + (t.fresh ? " new" : "") +
|
| 83 |
+
(R.done && t.v === puzzle[active].target && !t.gone ? " hit" : "");
|
| 84 |
+
b.textContent = t.v;
|
| 85 |
+
b.disabled = R.done || t.gone;
|
| 86 |
+
b.setAttribute("aria-label", "Number " + t.v);
|
| 87 |
+
b.onclick = function () { tapTile(t.id, b); };
|
| 88 |
+
board.appendChild(b);
|
| 89 |
+
t.fresh = false;
|
| 90 |
+
});
|
| 91 |
+
Array.prototype.forEach.call(document.querySelectorAll(".op"), function (b) {
|
| 92 |
+
b.classList.toggle("sel", selOp === b.dataset.op);
|
| 93 |
+
b.disabled = R.done;
|
| 94 |
+
});
|
| 95 |
+
el("btn-undo").disabled = R.done || R.hist.length === 0;
|
| 96 |
+
el("btn-submit").disabled = R.done;
|
| 97 |
+
el("btn-submit").textContent = R.done ? (allDone() ? "See results" : "Next target →") : "Submit closest";
|
| 98 |
+
if (R.done) el("btn-submit").disabled = false;
|
| 99 |
+
var log = el("log");
|
| 100 |
+
log.innerHTML = "";
|
| 101 |
+
R.log.forEach(function (s) { var li = document.createElement("li"); li.textContent = s; log.appendChild(li); });
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function closestOnBoard(R) {
|
| 105 |
+
var t = puzzle[active].target, best = null;
|
| 106 |
+
R.tiles.forEach(function (x) { if (!x.gone && (best === null || Math.abs(x.v - t) < Math.abs(best - t))) best = x.v; });
|
| 107 |
+
return best;
|
| 108 |
+
}
|
| 109 |
+
function allDone() { return state.rounds.every(function (r) { return r.done; }); }
|
| 110 |
+
|
| 111 |
+
function tapTile(id, node) {
|
| 112 |
+
var R = state.rounds[active];
|
| 113 |
+
if (R.done) return;
|
| 114 |
+
if (selTile === null || (selOp === null)) {
|
| 115 |
+
selTile = selTile === id ? null : id;
|
| 116 |
+
render();
|
| 117 |
+
return;
|
| 118 |
+
}
|
| 119 |
+
if (selTile === id) { selTile = null; selOp = null; render(); return; }
|
| 120 |
+
var a = R.tiles.filter(function (t) { return t.id === selTile; })[0];
|
| 121 |
+
var b = R.tiles.filter(function (t) { return t.id === id; })[0];
|
| 122 |
+
var v = apply(a.v, selOp, b.v);
|
| 123 |
+
if (!v) {
|
| 124 |
+
node.classList.add("shake");
|
| 125 |
+
toast(selOp === "-" ? "Results must be positive: try the bigger number first" : "Only whole-number division");
|
| 126 |
+
setTimeout(function () { node.classList.remove("shake"); }, 320);
|
| 127 |
+
return;
|
| 128 |
+
}
|
| 129 |
+
R.hist.push(JSON.stringify({ tiles: R.tiles, log: R.log }));
|
| 130 |
+
R.log = R.log.concat(a.v + " " + OPS[selOp] + " " + b.v + " = " + v);
|
| 131 |
+
a.gone = true;
|
| 132 |
+
b.v = v; b.fresh = true;
|
| 133 |
+
selTile = id; selOp = null;
|
| 134 |
+
if (v === puzzle[active].target) finishRound(3, v);
|
| 135 |
+
persist();
|
| 136 |
+
render();
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
function tapOp(op) {
|
| 140 |
+
var R = state.rounds[active];
|
| 141 |
+
if (R.done) return;
|
| 142 |
+
if (selTile === null) { toast("Pick a number first"); return; }
|
| 143 |
+
selOp = selOp === op ? null : op;
|
| 144 |
+
render();
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
function undo() {
|
| 148 |
+
var R = state.rounds[active];
|
| 149 |
+
if (R.done || !R.hist.length) return;
|
| 150 |
+
var prev = JSON.parse(R.hist.pop());
|
| 151 |
+
R.tiles = prev.tiles; R.log = prev.log;
|
| 152 |
+
selTile = null; selOp = null;
|
| 153 |
+
persist(); render();
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function starsFor(diff) { return diff === 0 ? 3 : diff <= 10 ? 2 : diff <= 25 ? 1 : 0; }
|
| 157 |
+
|
| 158 |
+
function finishRound(stars, best) {
|
| 159 |
+
var R = state.rounds[active];
|
| 160 |
+
R.done = true; R.stars = stars; R.best = best; R.hist = [];
|
| 161 |
+
selTile = null; selOp = null;
|
| 162 |
+
toast(stars === 3 ? "🌸 Bloomed!" : FLOWER[stars] + " " + stars + " star" + (stars === 1 ? "" : "s"));
|
| 163 |
+
if (allDone()) {
|
| 164 |
+
if (mode === "daily") recordStats();
|
| 165 |
+
persist();
|
| 166 |
+
setTimeout(showResults, 700);
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
function submit() {
|
| 171 |
+
var R = state.rounds[active];
|
| 172 |
+
if (R.done) {
|
| 173 |
+
if (allDone()) { showResults(); return; }
|
| 174 |
+
active = firstOpen(); render(); return;
|
| 175 |
+
}
|
| 176 |
+
var best = closestOnBoard(R);
|
| 177 |
+
finishRound(starsFor(Math.abs(best - puzzle[active].target)), best);
|
| 178 |
+
persist(); render();
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// ---------- stats ----------
|
| 182 |
+
function recordStats() {
|
| 183 |
+
var st = load("stats", { played: 0, streak: 0, maxStreak: 0, last: null, total: 0, perfect: 0, dist: {} });
|
| 184 |
+
if (st.last === dateStr) return;
|
| 185 |
+
var score = totalStars();
|
| 186 |
+
var y = new Date(); y.setDate(y.getDate() - 1);
|
| 187 |
+
st.streak = st.last === todayStr(y) ? st.streak + 1 : 1;
|
| 188 |
+
st.maxStreak = Math.max(st.maxStreak, st.streak);
|
| 189 |
+
st.played += 1; st.total += score; if (score === 15) st.perfect += 1;
|
| 190 |
+
st.dist[score] = (st.dist[score] || 0) + 1;
|
| 191 |
+
st.last = dateStr;
|
| 192 |
+
save("stats", st);
|
| 193 |
+
}
|
| 194 |
+
function totalStars() { return state.rounds.reduce(function (s, r) { return s + (r.stars || 0); }, 0); }
|
| 195 |
+
|
| 196 |
+
function shareText() {
|
| 197 |
+
var head = mode === "daily" ? "Sumbloom #" + puzzleNo(dateStr) : "Sumbloom practice";
|
| 198 |
+
return head + " " + totalStars() + "/15\n" + state.rounds.map(function (r) { return FLOWER[r.stars || 0]; }).join("") + "\n" + SITE;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
function share() {
|
| 202 |
+
var text = shareText();
|
| 203 |
+
if (navigator.share && /Mobi|Android/i.test(navigator.userAgent)) {
|
| 204 |
+
navigator.share({ text: text }).catch(function () { copy(text); });
|
| 205 |
+
} else copy(text);
|
| 206 |
+
}
|
| 207 |
+
function copy(text) {
|
| 208 |
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
| 209 |
+
navigator.clipboard.writeText(text).then(function () { toast("Copied to clipboard"); }, function () { fallbackCopy(text); });
|
| 210 |
+
} else fallbackCopy(text);
|
| 211 |
+
}
|
| 212 |
+
function fallbackCopy(text) {
|
| 213 |
+
var ta = document.createElement("textarea");
|
| 214 |
+
ta.value = text; document.body.appendChild(ta); ta.select();
|
| 215 |
+
try { document.execCommand("copy"); toast("Copied to clipboard"); } catch (e) { toast("Copy failed"); }
|
| 216 |
+
document.body.removeChild(ta);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
// ---------- modals ----------
|
| 220 |
+
function openModal(html) {
|
| 221 |
+
el("modal-body").innerHTML = html;
|
| 222 |
+
el("modal").hidden = false;
|
| 223 |
+
el("modal-close").focus();
|
| 224 |
+
}
|
| 225 |
+
function closeModal() { el("modal").hidden = true; clearInterval(cdTimer); }
|
| 226 |
+
var cdTimer = null;
|
| 227 |
+
|
| 228 |
+
function countdown() {
|
| 229 |
+
var now = new Date(), next = new Date(now); next.setHours(24, 0, 0, 0);
|
| 230 |
+
var s = Math.max(0, Math.floor((next - now) / 1000));
|
| 231 |
+
return String(Math.floor(s / 3600)).padStart(2, "0") + ":" + String(Math.floor(s % 3600 / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0");
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
function solutionHtml() {
|
| 235 |
+
return puzzle.map(function (p, i) {
|
| 236 |
+
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>";
|
| 237 |
+
}).join("<div style='height:6px'></div>");
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
function showResults() {
|
| 241 |
+
var html = "<h2>" + (mode === "daily" ? "Daily #" + puzzleNo(dateStr) : "Practice") + " complete</h2>" +
|
| 242 |
+
"<div class='big-score'>" + totalStars() + "/15</div>" +
|
| 243 |
+
"<div class='share-grid'>" + state.rounds.map(function (r) { return FLOWER[r.stars || 0]; }).join("") + "</div>" +
|
| 244 |
+
"<button class='btn' id='btn-share' style='width:100%'>Share result</button>" +
|
| 245 |
+
"<p style='font-size:14px'>One way to reach each target:</p>" + solutionHtml() +
|
| 246 |
+
(mode === "daily" ? "<p class='countdown'>Next puzzle in <b id='cd'>" + countdown() + "</b></p>" : "") +
|
| 247 |
+
"<button class='btn ghost' id='btn-practice' style='width:100%;margin-top:8px'>" + (mode === "daily" ? "Play a practice puzzle" : "New practice puzzle") + "</button>";
|
| 248 |
+
openModal(html);
|
| 249 |
+
el("btn-share").onclick = share;
|
| 250 |
+
el("btn-practice").onclick = function () { closeModal(); startPractice(); };
|
| 251 |
+
if (mode === "daily") cdTimer = setInterval(function () { var c = el("cd"); if (c) c.textContent = countdown(); }, 1000);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
function showStats() {
|
| 255 |
+
var st = load("stats", { played: 0, streak: 0, maxStreak: 0, total: 0, perfect: 0, dist: {} });
|
| 256 |
+
var avg = st.played ? (st.total / st.played).toFixed(1) : "–";
|
| 257 |
+
var max = 1; for (var k in st.dist) max = Math.max(max, st.dist[k]);
|
| 258 |
+
var rows = "";
|
| 259 |
+
for (var sc = 15; sc >= 0; sc--) {
|
| 260 |
+
var n = st.dist[sc] || 0;
|
| 261 |
+
if (!n && sc < 10) continue;
|
| 262 |
+
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>";
|
| 263 |
+
}
|
| 264 |
+
openModal("<h2>Statistics</h2><div class='stats'><div><b>" + st.played + "</b><span>Played</span></div><div><b>" + avg +
|
| 265 |
+
"</b><span>Avg stars</span></div><div><b>" + st.streak + "</b><span>Streak</span></div><div><b>" + st.maxStreak +
|
| 266 |
+
"</b><span>Best streak</span></div></div><p style='font-size:14px;margin-bottom:4px'>Daily scores (" + st.perfect + " perfect)</p><div class='dist'>" +
|
| 267 |
+
(rows || "<p style='font-size:14px;color:var(--muted)'>Finish today's puzzle to start your stats.</p>") + "</div>" +
|
| 268 |
+
(allDone() && mode === "daily" ? "<button class='btn' id='btn-share2' style='width:100%'>Share today's result</button>" : ""));
|
| 269 |
+
var b = el("btn-share2"); if (b) b.onclick = share;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
function showHelp() {
|
| 273 |
+
openModal("<h2>How to play</h2><p>Reach the target by combining numbers with + − × ÷.</p>" +
|
| 274 |
+
"<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>" +
|
| 275 |
+
"<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>" +
|
| 276 |
+
"<p><b>Scoring:</b> exact = 3 blossoms 🌸, within 10 = 2 🌼, within 25 = 1 🌱. Can't get closer? Press <b>Submit closest</b>.</p>" +
|
| 277 |
+
"<p>Five targets a day, the same for everyone. A new puzzle every midnight, plus unlimited practice. Keys: + − * / and Backspace to undo.</p>" +
|
| 278 |
+
"<button class='btn' id='btn-go' style='width:100%'>Play</button>");
|
| 279 |
+
el("btn-go").onclick = closeModal;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
var toastTimer = null;
|
| 283 |
+
function toast(msg) {
|
| 284 |
+
var t = el("toast"); t.textContent = msg; t.classList.add("show");
|
| 285 |
+
clearTimeout(toastTimer); toastTimer = setTimeout(function () { t.classList.remove("show"); }, 1600);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// ---------- wire up ----------
|
| 289 |
+
Array.prototype.forEach.call(document.querySelectorAll(".op"), function (b) { b.onclick = function () { tapOp(b.dataset.op); }; });
|
| 290 |
+
el("btn-undo").onclick = undo;
|
| 291 |
+
el("btn-submit").onclick = submit;
|
| 292 |
+
el("btn-help").onclick = showHelp;
|
| 293 |
+
el("btn-stats").onclick = showStats;
|
| 294 |
+
el("btn-mode").onclick = function () { mode === "daily" ? startPractice() : startDaily(); };
|
| 295 |
+
el("modal-close").onclick = closeModal;
|
| 296 |
+
el("modal").onclick = function (e) { if (e.target === el("modal")) closeModal(); };
|
| 297 |
+
document.addEventListener("keydown", function (e) {
|
| 298 |
+
if (!el("modal").hidden) { if (e.key === "Escape") closeModal(); return; }
|
| 299 |
+
if (OPS[e.key]) tapOp(e.key);
|
| 300 |
+
else if (e.key === "x" || e.key === "X") tapOp("*");
|
| 301 |
+
else if (e.key === "Backspace") undo();
|
| 302 |
+
else if (e.key === "Enter") submit();
|
| 303 |
+
});
|
| 304 |
+
|
| 305 |
+
// expose for tests
|
| 306 |
+
window.Sumbloom = { makePuzzle: makePuzzle, apply: apply, puzzleNo: puzzleNo, state: function () { return state; }, puzzle: function () { return puzzle; } };
|
| 307 |
+
|
| 308 |
+
document.addEventListener("visibilitychange", function () {
|
| 309 |
+
if (!document.hidden && mode === "daily" && todayStr() !== dateStr) { closeModal(); startDaily(); }
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
startDaily();
|
| 313 |
+
if (!load("seenHelp", false)) { save("seenHelp", true); showHelp(); }
|
| 314 |
+
|
| 315 |
+
if ("serviceWorker" in navigator && location.protocol === "https:") {
|
| 316 |
+
navigator.serviceWorker.register("sw.js").catch(function () {});
|
| 317 |
+
}
|
| 318 |
+
})();
|
icon-192.png
ADDED
|
|
icon-512.png
ADDED
|
|
icon.svg
ADDED
|
|
index.html
CHANGED
|
@@ -1,19 +1,96 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 6 |
+
<title>Sumbloom — Free Daily Number Puzzle (Math Game Like Digits)</title>
|
| 7 |
+
<meta name="description" content="Free daily number puzzle: combine six numbers with + − × ÷ to hit five targets. New puzzle every day, shareable results, streaks and practice.">
|
| 8 |
+
<meta name="keywords" content="daily number puzzle, math puzzle game, digits game, number game, daily math game, arithmetic puzzle, countdown numbers">
|
| 9 |
+
<meta name="theme-color" content="#e8527a">
|
| 10 |
+
<link rel="canonical" href="https://shaw276-sumbloom.static.hf.space/">
|
| 11 |
+
<link rel="manifest" href="manifest.webmanifest">
|
| 12 |
+
<link rel="icon" href="icon.svg" type="image/svg+xml">
|
| 13 |
+
<link rel="apple-touch-icon" href="icon-192.png">
|
| 14 |
+
<meta property="og:type" content="website">
|
| 15 |
+
<meta property="og:title" content="Sumbloom — the daily number puzzle">
|
| 16 |
+
<meta property="og:description" content="Six numbers, four operations, five targets. A new free puzzle every day.">
|
| 17 |
+
<meta property="og:url" content="https://shaw276-sumbloom.static.hf.space/">
|
| 18 |
+
<meta property="og:image" content="https://shaw276-sumbloom.static.hf.space/og.png">
|
| 19 |
+
<meta name="twitter:card" content="summary_large_image">
|
| 20 |
+
<script type="application/ld+json">
|
| 21 |
+
{"@context":"https://schema.org","@type":"VideoGame","name":"Sumbloom","description":"A free daily number puzzle: combine six numbers with addition, subtraction, multiplication and division to reach five targets.","url":"https://shaw276-sumbloom.static.hf.space/","genre":["Puzzle","Math"],"gamePlatform":"Web browser","applicationCategory":"GameApplication","operatingSystem":"Any","offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},"author":{"@type":"Organization","name":"CyberMax"},"publisher":{"@type":"Organization","name":"CyberMax"},"copyrightYear":2026}
|
| 22 |
+
</script>
|
| 23 |
+
<link rel="stylesheet" href="style.css">
|
| 24 |
+
</head>
|
| 25 |
+
<body>
|
| 26 |
+
<header class="bar">
|
| 27 |
+
<button class="icon-btn" id="btn-help" aria-label="How to play">?</button>
|
| 28 |
+
<h1 class="logo"><span class="logo-mark" aria-hidden="true">✿</span> Sumbloom</h1>
|
| 29 |
+
<div class="bar-right">
|
| 30 |
+
<button class="icon-btn" id="btn-stats" aria-label="Statistics">▦</button>
|
| 31 |
+
</div>
|
| 32 |
+
</header>
|
| 33 |
+
|
| 34 |
+
<main id="app">
|
| 35 |
+
<div class="mode-row">
|
| 36 |
+
<span id="puzzle-label" class="puzzle-label">Daily #1</span>
|
| 37 |
+
<button class="link-btn" id="btn-mode">Practice mode</button>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<nav class="rounds" id="rounds" aria-label="Targets"></nav>
|
| 41 |
+
|
| 42 |
+
<section class="target-wrap" aria-live="polite">
|
| 43 |
+
<div class="target-caption">Make</div>
|
| 44 |
+
<div class="target" id="target">0</div>
|
| 45 |
+
<div class="closest" id="closest"> </div>
|
| 46 |
+
</section>
|
| 47 |
+
|
| 48 |
+
<section class="board" id="board" aria-label="Numbers"></section>
|
| 49 |
+
|
| 50 |
+
<section class="ops" id="ops" aria-label="Operations">
|
| 51 |
+
<button class="op" data-op="+" aria-label="add">+</button>
|
| 52 |
+
<button class="op" data-op="-" aria-label="subtract">−</button>
|
| 53 |
+
<button class="op" data-op="*" aria-label="multiply">×</button>
|
| 54 |
+
<button class="op" data-op="/" aria-label="divide">÷</button>
|
| 55 |
+
</section>
|
| 56 |
+
|
| 57 |
+
<section class="actions">
|
| 58 |
+
<button class="btn ghost" id="btn-undo">↶ Undo</button>
|
| 59 |
+
<button class="btn" id="btn-submit">Submit closest</button>
|
| 60 |
+
</section>
|
| 61 |
+
|
| 62 |
+
<ol class="log" id="log" aria-label="Your steps"></ol>
|
| 63 |
+
</main>
|
| 64 |
+
|
| 65 |
+
<section class="seo" aria-label="About Sumbloom">
|
| 66 |
+
<h2>How to play Sumbloom</h2>
|
| 67 |
+
<p>Each day brings five targets. For each one you get six numbers. Tap a number, an operation (+, −, ×, ÷), then another number: the two combine into a new number. Keep combining until you hit the target exactly. You don't have to use every number. Only whole, positive results are allowed.</p>
|
| 68 |
+
<p>Hit a target exactly for three blossoms. Within 10 earns two, within 25 earns one. The best daily score is 15. Share your result without spoilers, keep your streak going, and play unlimited practice puzzles any time.</p>
|
| 69 |
+
<h2>A daily math game for everyone</h2>
|
| 70 |
+
<p>Sumbloom is a free arithmetic puzzle in the spirit of classic "numbers round" games. Every player in the world gets the same puzzle on the same day, and every target is guaranteed solvable. It works on phones, tablets and desktops, offline too once loaded, with no account and no ads.</p>
|
| 71 |
+
</section>
|
| 72 |
+
|
| 73 |
+
<footer class="more">
|
| 74 |
+
<div class="more-title">More from CyberMax</div>
|
| 75 |
+
<div class="more-links">
|
| 76 |
+
<a href="https://huggingface.co/spaces/shaw276/honeyfold" target="_blank" rel="noopener">Honeyfold · idle bee game</a>
|
| 77 |
+
<a href="https://huggingface.co/spaces/shaw276/utm-builder" target="_blank" rel="noopener">UTM Builder</a>
|
| 78 |
+
<a href="https://huggingface.co/spaces/shaw276/profit-margin-calculator" target="_blank" rel="noopener">Profit Margin Calculator</a>
|
| 79 |
+
<a href="https://huggingface.co/spaces/shaw276/salary-to-hourly-calculator" target="_blank" rel="noopener">Salary to Hourly Calculator</a>
|
| 80 |
+
<a href="https://huggingface.co/spaces/shaw276/cybermax" target="_blank" rel="noopener">All CyberMax tools</a>
|
| 81 |
+
</div>
|
| 82 |
+
<div class="copy">© 2026 CyberMax. All rights reserved.</div>
|
| 83 |
+
</footer>
|
| 84 |
+
|
| 85 |
+
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" hidden>
|
| 86 |
+
<div class="modal-card">
|
| 87 |
+
<button class="icon-btn modal-close" id="modal-close" aria-label="Close">×</button>
|
| 88 |
+
<div id="modal-body"></div>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
| 92 |
+
|
| 93 |
+
<script src="puzzle.js"></script>
|
| 94 |
+
<script src="game.js"></script>
|
| 95 |
+
</body>
|
| 96 |
</html>
|
manifest.webmanifest
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Sumbloom — Daily Number Puzzle",
|
| 3 |
+
"short_name": "Sumbloom",
|
| 4 |
+
"description": "Combine six numbers with + − × ÷ to hit five targets. A new puzzle every day.",
|
| 5 |
+
"start_url": "./",
|
| 6 |
+
"scope": "./",
|
| 7 |
+
"display": "standalone",
|
| 8 |
+
"background_color": "#fff8f4",
|
| 9 |
+
"theme_color": "#e8527a",
|
| 10 |
+
"icons": [
|
| 11 |
+
{"src": "icon.svg", "sizes": "any", "type": "image/svg+xml"},
|
| 12 |
+
{"src": "icon-192.png", "sizes": "192x192", "type": "image/png"},
|
| 13 |
+
{"src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable"}
|
| 14 |
+
]
|
| 15 |
+
}
|
og.png
ADDED
|
puzzle.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Sumbloom puzzle core (pure, no DOM). © 2026 CyberMax. All rights reserved. */
|
| 2 |
+
(function (root) {
|
| 3 |
+
"use strict";
|
| 4 |
+
var EPOCH = Date.UTC(2026, 8, 24); // puzzle #1 = 24 Sep 2026
|
| 5 |
+
var ROUNDS = [
|
| 6 |
+
{ small: 5, large: 1, min: 20, max: 99, steps: [2] },
|
| 7 |
+
{ small: 4, large: 2, min: 50, max: 199, steps: [2, 3] },
|
| 8 |
+
{ small: 4, large: 2, min: 100, max: 299, steps: [3] },
|
| 9 |
+
{ small: 3, large: 3, min: 150, max: 399, steps: [3, 4] },
|
| 10 |
+
{ small: 3, large: 3, min: 250, max: 499, steps: [4] }
|
| 11 |
+
];
|
| 12 |
+
var LARGE = [10, 11, 12, 13, 15, 20, 25];
|
| 13 |
+
var OPS = { "+": "+", "-": "−", "*": "×", "/": "÷" };
|
| 14 |
+
var FLOWER = ["🍂", "🌱", "🌼", "🌸"];
|
| 15 |
+
|
| 16 |
+
// ---------- seeded random ----------
|
| 17 |
+
function hash(str) {
|
| 18 |
+
var h = 1779033703 ^ str.length;
|
| 19 |
+
for (var i = 0; i < str.length; i++) {
|
| 20 |
+
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
| 21 |
+
h = (h << 13) | (h >>> 19);
|
| 22 |
+
}
|
| 23 |
+
return function () {
|
| 24 |
+
h = Math.imul(h ^ (h >>> 16), 2246822507);
|
| 25 |
+
h = Math.imul(h ^ (h >>> 13), 3266489909);
|
| 26 |
+
return (h ^= h >>> 16) >>> 0;
|
| 27 |
+
};
|
| 28 |
+
}
|
| 29 |
+
function mulberry(seed) {
|
| 30 |
+
return function () {
|
| 31 |
+
seed |= 0; seed = (seed + 0x6d2b79f5) | 0;
|
| 32 |
+
var t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
| 33 |
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
| 34 |
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
| 35 |
+
};
|
| 36 |
+
}
|
| 37 |
+
function pick(rng, arr) { return arr[Math.floor(rng() * arr.length)]; }
|
| 38 |
+
|
| 39 |
+
function apply(a, op, b) {
|
| 40 |
+
if (op === "+") return a + b;
|
| 41 |
+
if (op === "-") return a - b > 0 ? a - b : null;
|
| 42 |
+
if (op === "*") return a * b;
|
| 43 |
+
if (op === "/") return b !== 0 && a % b === 0 ? a / b : null;
|
| 44 |
+
return null;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function oneStepValues(nums) {
|
| 48 |
+
var out = {};
|
| 49 |
+
for (var i = 0; i < nums.length; i++) for (var j = 0; j < nums.length; j++) {
|
| 50 |
+
if (i === j) continue;
|
| 51 |
+
for (var op in OPS) { var v = apply(nums[i], op, nums[j]); if (v) out[v] = true; }
|
| 52 |
+
}
|
| 53 |
+
return out;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Build one round: numbers + a target reached by a known sequence of steps.
|
| 57 |
+
function makeRound(rng, cfg) {
|
| 58 |
+
for (var attempt = 0; attempt < 5000; attempt++) {
|
| 59 |
+
var small = [];
|
| 60 |
+
while (small.length < cfg.small) {
|
| 61 |
+
var s = 1 + Math.floor(rng() * 9);
|
| 62 |
+
if (small.filter(function (x) { return x === s; }).length < 1 || rng() < 0.15) small.push(s);
|
| 63 |
+
}
|
| 64 |
+
var large = [];
|
| 65 |
+
while (large.length < cfg.large) {
|
| 66 |
+
var l = pick(rng, LARGE);
|
| 67 |
+
if (large.indexOf(l) < 0) large.push(l);
|
| 68 |
+
}
|
| 69 |
+
var nums = small.concat(large).sort(function (a, b) { return a - b; });
|
| 70 |
+
var pool = nums.slice();
|
| 71 |
+
var steps = pick(rng, cfg.steps);
|
| 72 |
+
var sol = [];
|
| 73 |
+
var ok = true;
|
| 74 |
+
for (var k = 0; k < steps && ok; k++) {
|
| 75 |
+
var tries = 0, done = false;
|
| 76 |
+
while (!done && tries++ < 60) {
|
| 77 |
+
var i = Math.floor(rng() * pool.length), j = Math.floor(rng() * pool.length);
|
| 78 |
+
if (i === j) continue;
|
| 79 |
+
var r = rng();
|
| 80 |
+
var op = r < 0.3 ? "+" : r < 0.5 ? "-" : r < 0.85 ? "*" : "/";
|
| 81 |
+
var a = pool[i], b = pool[j];
|
| 82 |
+
if (op === "*" && (a === 1 || b === 1)) continue;
|
| 83 |
+
if (op === "/" && b === 1) continue;
|
| 84 |
+
var v = apply(a, op, b);
|
| 85 |
+
if (!v) continue;
|
| 86 |
+
sol.push([a, op, b, v]);
|
| 87 |
+
pool.splice(Math.max(i, j), 1);
|
| 88 |
+
pool.splice(Math.min(i, j), 1);
|
| 89 |
+
pool.push(v);
|
| 90 |
+
done = true;
|
| 91 |
+
}
|
| 92 |
+
if (!done) ok = false;
|
| 93 |
+
}
|
| 94 |
+
if (!ok) continue;
|
| 95 |
+
var target = sol[sol.length - 1][3];
|
| 96 |
+
if (target < cfg.min || target > cfg.max) continue;
|
| 97 |
+
if (nums.indexOf(target) >= 0) continue;
|
| 98 |
+
if (oneStepValues(nums)[target]) continue; // no one-move targets
|
| 99 |
+
return { nums: nums, target: target, solution: sol };
|
| 100 |
+
}
|
| 101 |
+
throw new Error("could not build round");
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function makePuzzle(seedStr) {
|
| 105 |
+
var rng = mulberry(hash(seedStr)());
|
| 106 |
+
return ROUNDS.map(function (cfg) { return makeRound(rng, cfg); });
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// ---------- dates ----------
|
| 110 |
+
function todayStr(d) {
|
| 111 |
+
d = d || new Date();
|
| 112 |
+
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
|
| 113 |
+
}
|
| 114 |
+
function puzzleNo(dateStr) {
|
| 115 |
+
var p = dateStr.split("-").map(Number);
|
| 116 |
+
return Math.floor((Date.UTC(p[0], p[1] - 1, p[2]) - EPOCH) / 864e5) + 1;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
root.SumbloomCore = { makePuzzle: makePuzzle, apply: apply, puzzleNo: puzzleNo, todayStr: todayStr, OPS: OPS, FLOWER: FLOWER };
|
| 120 |
+
if (typeof module !== "undefined") module.exports = root.SumbloomCore;
|
| 121 |
+
})(typeof window !== "undefined" ? window : globalThis);
|
robots.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
User-agent: *
|
| 2 |
+
Allow: /
|
| 3 |
+
Sitemap: https://shaw276-sumbloom.static.hf.space/sitemap.xml
|
sitemap.xml
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://shaw276-sumbloom.static.hf.space/</loc><lastmod>2026-09-24</lastmod><changefreq>daily</changefreq></url></urlset>
|
style.css
CHANGED
|
@@ -1,28 +1,84 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
}
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
p {
|
| 12 |
-
color: rgb(107, 114, 128);
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root{
|
| 2 |
+
--bg:#fff8f4;--surface:#ffffff;--ink:#2b2230;--muted:#7a6d7f;--line:#f0dfe3;
|
| 3 |
+
--petal:#e8527a;--petal-soft:#fde3ea;--leaf:#3f9a6b;--leaf-soft:#dcf3e6;--sun:#f4b73f;
|
| 4 |
+
--tile:#fff;--tile-ink:#2b2230;--tile-line:#eac9d2;--shadow:0 2px 0 #eac9d2;
|
| 5 |
+
color-scheme:light;
|
| 6 |
}
|
| 7 |
+
@media (prefers-color-scheme:dark){
|
| 8 |
+
:root{--bg:#1b1620;--surface:#251e2b;--ink:#f4ecf5;--muted:#b3a5b8;--line:#3a2f41;
|
| 9 |
+
--petal:#ff7098;--petal-soft:#4a2433;--leaf:#5cc48e;--leaf-soft:#1f3a2c;--sun:#f7c55c;
|
| 10 |
+
--tile:#2f2636;--tile-ink:#f4ecf5;--tile-line:#4d3f56;--shadow:0 2px 0 #120e15;color-scheme:dark}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
}
|
| 12 |
+
*{box-sizing:border-box}
|
| 13 |
+
html,body{margin:0}
|
| 14 |
+
body{background:var(--bg);color:var(--ink);font:16px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
| 15 |
+
-webkit-tap-highlight-color:transparent;min-height:100vh;display:flex;flex-direction:column;align-items:center}
|
| 16 |
+
button{font:inherit;color:inherit;cursor:pointer}
|
| 17 |
+
.bar{width:100%;max-width:560px;display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--line)}
|
| 18 |
+
.logo{margin:0;font-size:24px;letter-spacing:.5px;font-weight:800}
|
| 19 |
+
.logo-mark{color:var(--petal)}
|
| 20 |
+
.icon-btn{width:40px;height:40px;border-radius:50%;border:1px solid var(--line);background:var(--surface);font-weight:700;font-size:18px}
|
| 21 |
+
.bar-right{display:flex;gap:8px}
|
| 22 |
+
main{width:100%;max-width:560px;padding:12px 16px 8px}
|
| 23 |
+
.mode-row{display:flex;justify-content:space-between;align-items:center;font-size:14px;color:var(--muted)}
|
| 24 |
+
.link-btn{background:none;border:0;color:var(--petal);font-weight:600;padding:6px 0;text-decoration:underline;text-underline-offset:3px}
|
| 25 |
+
.rounds{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin:10px 0 6px}
|
| 26 |
+
.round{border:2px solid var(--line);background:var(--surface);border-radius:12px;padding:6px 2px;font-weight:700;font-size:17px;position:relative;min-height:54px}
|
| 27 |
+
.round small{display:block;font-size:12px;font-weight:600;color:var(--muted);letter-spacing:1px;min-height:16px}
|
| 28 |
+
.round.active{border-color:var(--petal);box-shadow:0 0 0 3px var(--petal-soft)}
|
| 29 |
+
.round.done{background:var(--leaf-soft)}
|
| 30 |
+
.target-wrap{text-align:center;margin:10px 0 4px}
|
| 31 |
+
.target-caption{font-size:13px;text-transform:uppercase;letter-spacing:2px;color:var(--muted)}
|
| 32 |
+
.target{font-size:64px;line-height:1;font-weight:800;color:var(--petal);font-variant-numeric:tabular-nums}
|
| 33 |
+
.closest{font-size:14px;color:var(--muted);min-height:20px;margin-top:4px}
|
| 34 |
+
.board{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:14px auto 12px;max-width:320px}
|
| 35 |
+
.tile{aspect-ratio:1/1;border-radius:50%;border:2px solid var(--tile-line);background:var(--tile);color:var(--tile-ink);
|
| 36 |
+
font-size:clamp(22px,7vw,32px);font-weight:800;font-variant-numeric:tabular-nums;box-shadow:var(--shadow);
|
| 37 |
+
transition:transform .12s ease,background .15s,border-color .15s;display:flex;align-items:center;justify-content:center}
|
| 38 |
+
.tile:active{transform:scale(.95)}
|
| 39 |
+
.tile.sel{background:var(--petal);color:#fff;border-color:var(--petal)}
|
| 40 |
+
.tile.gone{visibility:hidden}
|
| 41 |
+
.tile.new{animation:bloom .35s ease}
|
| 42 |
+
.tile.hit{background:var(--leaf);border-color:var(--leaf);color:#fff}
|
| 43 |
+
@keyframes bloom{0%{transform:scale(.4)}70%{transform:scale(1.12)}100%{transform:scale(1)}}
|
| 44 |
+
.shake{animation:shake .3s}
|
| 45 |
+
@keyframes shake{25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
|
| 46 |
+
.ops{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;max-width:360px;margin:0 auto}
|
| 47 |
+
.op{height:54px;border-radius:14px;border:2px solid var(--line);background:var(--surface);font-size:28px;font-weight:700}
|
| 48 |
+
.op.sel{background:var(--sun);border-color:var(--sun);color:#2b2230}
|
| 49 |
+
.actions{display:flex;gap:10px;max-width:360px;margin:14px auto 0}
|
| 50 |
+
.btn{flex:1;height:48px;border-radius:24px;border:0;background:var(--petal);color:#fff;font-weight:700;font-size:16px}
|
| 51 |
+
.btn.ghost{background:var(--surface);color:var(--ink);border:2px solid var(--line)}
|
| 52 |
+
.btn:disabled{opacity:.45;cursor:default}
|
| 53 |
+
.log{list-style:none;padding:0;margin:14px auto 0;max-width:360px;font-size:15px;color:var(--muted);text-align:center;min-height:24px}
|
| 54 |
+
.log li{display:inline-block;margin:2px 6px;font-variant-numeric:tabular-nums}
|
| 55 |
+
.seo{width:100%;max-width:560px;padding:8px 16px 0;color:var(--muted);font-size:14px}
|
| 56 |
+
.seo h2{font-size:16px;color:var(--ink);margin:18px 0 6px}
|
| 57 |
+
.more{width:100%;max-width:560px;margin-top:20px;padding:16px;border-top:1px solid var(--line);text-align:center;font-size:14px}
|
| 58 |
+
.more-title{font-weight:700;margin-bottom:8px}
|
| 59 |
+
.more-links{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}
|
| 60 |
+
.more-links a{color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:999px;padding:6px 12px;text-decoration:none}
|
| 61 |
+
.more-links a:hover{border-color:var(--petal)}
|
| 62 |
+
.copy{margin-top:12px;color:var(--muted);font-size:12px}
|
| 63 |
+
.modal{position:fixed;inset:0;background:rgba(20,10,20,.55);display:flex;align-items:center;justify-content:center;padding:16px;z-index:10}
|
| 64 |
+
.modal[hidden]{display:none}
|
| 65 |
+
.modal-card{position:relative;background:var(--surface);border-radius:18px;max-width:420px;width:100%;max-height:90vh;overflow:auto;padding:22px 20px 20px;box-shadow:0 20px 50px rgba(0,0,0,.3)}
|
| 66 |
+
.modal-close{position:absolute;right:10px;top:10px}
|
| 67 |
+
.modal-card h2{margin:0 0 10px;font-size:22px}
|
| 68 |
+
.modal-card p{margin:8px 0}
|
| 69 |
+
.stats{display:grid;grid-template-columns:repeat(4,1fr);text-align:center;margin:12px 0}
|
| 70 |
+
.stats b{display:block;font-size:26px}
|
| 71 |
+
.stats span{font-size:12px;color:var(--muted)}
|
| 72 |
+
.dist{margin:8px 0}
|
| 73 |
+
.dist-row{display:flex;align-items:center;gap:8px;font-size:13px;margin:3px 0}
|
| 74 |
+
.dist-row .bar-fill{background:var(--petal);color:#fff;border-radius:4px;padding:1px 6px;min-width:22px;text-align:right}
|
| 75 |
+
.share-grid{font-size:30px;letter-spacing:4px;text-align:center;margin:8px 0}
|
| 76 |
+
.big-score{font-size:44px;font-weight:800;text-align:center;color:var(--petal)}
|
| 77 |
+
.countdown{text-align:center;color:var(--muted);font-size:14px}
|
| 78 |
+
.sol{font-variant-numeric:tabular-nums;background:var(--bg);border-radius:10px;padding:8px 12px;font-size:15px}
|
| 79 |
+
.toast{position:fixed;top:70px;left:50%;transform:translateX(-50%);background:var(--ink);color:var(--bg);padding:10px 16px;border-radius:10px;font-weight:600;opacity:0;pointer-events:none;transition:opacity .2s;z-index:20;max-width:90vw;text-align:center}
|
| 80 |
+
.toast.show{opacity:1}
|
| 81 |
+
.how-demo{display:flex;gap:6px;align-items:center;justify-content:center;font-weight:800;font-size:20px;margin:10px 0}
|
| 82 |
+
.how-demo span{display:inline-flex;width:40px;height:40px;border-radius:50%;align-items:center;justify-content:center;border:2px solid var(--tile-line)}
|
| 83 |
+
.how-demo .o{border:0;width:auto}
|
| 84 |
+
@media (max-width:380px){.target{font-size:54px}.op{height:48px}}
|
sw.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Sumbloom offline cache. © 2026 CyberMax. All rights reserved. */
|
| 2 |
+
var CACHE = "sumbloom-v1";
|
| 3 |
+
var FILES = ["./", "index.html", "style.css", "puzzle.js", "game.js", "manifest.webmanifest", "icon.svg", "icon-192.png", "icon-512.png"];
|
| 4 |
+
self.addEventListener("install", function (e) {
|
| 5 |
+
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(FILES); }).then(function () { return self.skipWaiting(); }));
|
| 6 |
+
});
|
| 7 |
+
self.addEventListener("activate", function (e) {
|
| 8 |
+
e.waitUntil(caches.keys().then(function (keys) {
|
| 9 |
+
return Promise.all(keys.filter(function (k) { return k !== CACHE; }).map(function (k) { return caches.delete(k); }));
|
| 10 |
+
}).then(function () { return self.clients.claim(); }));
|
| 11 |
+
});
|
| 12 |
+
self.addEventListener("fetch", function (e) {
|
| 13 |
+
if (e.request.method !== "GET" || new URL(e.request.url).origin !== location.origin) return;
|
| 14 |
+
// Network first so updates arrive quickly; cache as offline fallback.
|
| 15 |
+
e.respondWith(fetch(e.request).then(function (r) {
|
| 16 |
+
var copy = r.clone();
|
| 17 |
+
caches.open(CACHE).then(function (c) { c.put(e.request, copy); });
|
| 18 |
+
return r;
|
| 19 |
+
}).catch(function () { return caches.match(e.request, { ignoreSearch: true }); }));
|
| 20 |
+
});
|