0
TOOLS#f1dcf143
Math Rush — Calculator Speed Game
@Owner·deposited 13h ago·updated 13h ago·2 views
TOOLS#f1dcf143
Math Rush — Calculator Speed Game
OW
@Owner
2Views
0Comments
0Forks
0Saves
SHARE · REMIX
Math Rush — Calculator Speed Game — a HTML Tools widget by @Owner.
CONTROLS
mathgamecalculatorarcadespeed
No comments yet. Be the first!
✦ Remix with AI
SDK in this widget
- localStorage: localStorage detected. Prefer vibes.save/load for per-user persistence.
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "Math Rush — Calculator Speed Game" by @Owner]
Source: https://itjustvibes.com/Owner/math-rush-calculator-speed-game
Type: HTML
--- SOURCE CODE ---
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Math Rush</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Orbitron:wght@400;700;900&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0a12;
--surface: #12121e;
--border: #1e1e2e;
--neon: #00f5c4;
--neon-dim: #00f5c430;
--neon-glow: 0 0 8px #00f5c4, 0 0 24px #00f5c440;
--warn: #ff6b35;
--warn-glow: 0 0 8px #ff6b35, 0 0 24px #ff6b3540;
--accent: #b44bff;
--accent-glow: 0 0 8px #b44bff, 0 0 24px #b44bff40;
--text: #e8e8f0;
--muted: #5a5a7a;
--correct: #39d98a;
--wrong: #ff4d6d;
--font-display: 'Orbitron', monospace;
--font-mono: 'Space Mono', monospace;
}
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-mono);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background-image: radial-gradient(ellipse at 50% 0%, #0d0d2a 0%, #0a0a12 70%);
}
.scanlines {
position: fixed;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.05) 2px,rgba(0,0,0,0.05) 4px);
z-index: 0;
}
.container { position: relative; z-index: 1; width: 100%; max-width: 480px; }
.logo {
font-family: var(--font-display);
font-size: clamp(1.8rem, 6vw, 2.8rem);
font-weight: 900;
color: var(--neon);
text-shadow: var(--neon-glow);
text-align: center;
letter-spacing: 0.08em;
margin-bottom: 4px;
}
.tagline {
text-align: center;
color: var(--muted);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 28px;
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px 24px;
}
#menu { display: flex; flex-direction: column; gap: 16px; }
.diff-label { font-size: 0.68rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted); margin-bottom: 6px; }
.diff-group { display: flex; gap: 8px; }
.diff-btn {
flex: 1; padding: 10px 0; border-radius: 8px; border: 1px solid var(--border);
background: transparent; color: var(--muted); font-family: var(--font-mono);
font-size: 0.8rem; cursor: pointer; transition: all 0.15s; letter-spacing: 0.05em;
}
.diff-btn:hover { border-color: var(--neon); color: var(--neon); }
.diff-btn.active { border-color: var(--neon); background: var(--neon-dim); color: var(--neon); box-shadow: var(--neon-glow); }
.start-btn {
width: 100%; padding: 16px; border-radius: 10px; border: none;
background: var(--neon); color: #000; font-family: var(--font-display);
font-size: 1rem; font-weight: 700; letter-spacing: 0.1em; cursor: pointer;
transition: all 0.15s; text-transform: uppercase;
}
.start-btn:hover { box-shadow: var(--neon-glow); transform: translateY(-1px); }
.start-btn:active { transform: translateY(0); }
.hi-score-row {
display: flex; justify-content: space-between; font-size: 0.75rem;
color: var(--muted); border-top: 1px solid var(--border); padding-top: 14px;
}
.hi-score-row span:last-child { color: var(--accent); font-weight: 700; }
#game { display: none; flex-direction: column; gap: 16px; }
.hud { display: flex; justify-content: space-between; align-items: flex-start; }
.hud-block { display: flex; flex-direction: column; }
.hud-label { font-size: 0.6rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted); }
.hud-value { font-family: var(--font-display); font-size: 1.4rem; font-weight: 700; color: var(--neon); text-shadow: var(--neon-glow); }
.hud-value.warn { color: var(--warn); text-shadow: var(--warn-glow); }
.timer-bar-track { height: 5px; border-radius: 3px; background: var(--border); overflow: hidden; }
.timer-bar-fill { height: 100%; border-radius: 3px; background: var(--neon); box-shadow: var(--neon-glow); transition: width 0.1s linear, background 0.3s; }
.timer-bar-fill.warn { background: var(--warn); box-shadow: var(--warn-glow); }
.problem-card {
background: var(--bg); border: 1px solid var(--border); border-radius: 10px;
padding: 28px 20px; text-align: center; position: relative; overflow: hidden;
}
.problem-card::before {
content: ''; position: absolute; inset: 0;
background: radial-gradient(ellipse at 50% 120%, var(--neon-dim) 0%, transparent 70%);
pointer-events: none;
}
.streak-badge { font-size: 0.65rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--accent); margin-bottom: 8px; min-height: 1em; }
.problem-text { font-family: var(--font-display); font-size: clamp(2rem, 9vw, 3.2rem); font-weight: 900; color: var(--text); letter-spacing: 0.04em; line-height: 1; }
.feedback { font-size: 0.9rem; font-weight: 700; text-align: center; min-height: 1.2em; transition: opacity 0.3s; letter-spacing: 0.05em; }
.feedback.correct { color: var(--correct); }
.feedback.wrong { color: var(--wrong); }
.input-row { display: flex; gap: 8px; }
.answer-input {
flex: 1; padding: 14px 16px; border-radius: 8px; border: 1px solid var(--border);
background: var(--bg); color: var(--text); font-family: var(--font-display);
font-size: 1.3rem; font-weight: 700; text-align: center; outline: none;
transition: border-color 0.15s, box-shadow 0.15s; -moz-appearance: textfield;
}
.answer-input::-webkit-inner-spin-button, .answer-input::-webkit-outer-spin-button { -webkit-appearance: none; }
.answer-input:focus { border-color: var(--neon); box-shadow: 0 0 0 2px var(--neon-dim); }
.answer-input.shake { animation: shake 0.3s ease; border-color: var(--wrong); }
.answer-input.correct-flash { border-color: var(--correct); box-shadow: 0 0 0 2px #39d98a30; }
.check-btn { padding: 14px 20px; border-radius: 8px; border: none; background: var(--neon); color: #000; font-family: var(--font-display); font-size: 1rem; font-weight: 700; cursor: pointer; transition: all 0.15s; }
.check-btn:hover { box-shadow: var(--neon-glow); }
.numpad { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.np-btn {
padding: 12px 0; border-radius: 7px; border: 1px solid var(--border);
background: var(--bg); color: var(--text); font-family: var(--font-mono);
font-size: 1rem; font-weight: 700; cursor: pointer; transition: all 0.1s;
user-select: none; -webkit-tap-highlight-color: transparent;
}
.np-btn:hover { border-color: var(--neon); color: var(--neon); background: var(--neon-dim); }
.np-btn:active { transform: scale(0.94); }
.np-btn.neg { color: var(--accent); border-color: var(--accent); }
.np-btn.neg:hover { background: #b44bff20; }
.np-btn.del { color: var(--warn); }
.np-btn.del:hover { border-color: var(--warn); background: #ff6b3520; }
.np-btn.enter { background: var(--neon); color: #000; border-color: var(--neon); font-size: 0.75rem; letter-spacing: 0.08em; }
.np-btn.enter:hover { box-shadow: var(--neon-glow); }
@keyframes shake {
0%,100% { transform: translateX(0); }
20% { transform: translateX(-6px); }
40% { transform: translateX(6px); }
60% { transform: translateX(-4px); }
80% { transform: translateX(4px); }
}
#results { display: none; flex-direction: column; gap: 18px; }
.result-title { font-family: var(--font-display); font-size: 1.5rem; font-weight: 900; text-align: center; color: var(--neon); text-shadow: var(--neon-glow); }
.result-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.result-stat { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px 12px; text-align: center; }
.result-stat .lbl { font-size: 0.6rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted); }
.result-stat .val { font-family: var(--font-display); font-size: 1.6rem; font-weight: 700; margin-top: 4px; }
.result-stat .val.neon { color: var(--neon); text-shadow: var(--neon-glow); }
.result-stat .val.accent { color: var(--accent); text-shadow: var(--accent-glow); }
.result-stat .val.warn { color: var(--warn); }
.result-stat .val.ok { color: var(--correct); }
.new-record { text-align: center; font-size: 0.75rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--accent); text-shadow: var(--accent-glow); min-height: 1em; }
.btn-row { display: flex; gap: 8px; }
.btn-row .start-btn { flex: 1; }
.menu-btn { flex: 1; padding: 16px; border-radius: 10px; border: 1px solid var(--border); background: transparent; color: var(--muted); font-family: var(--font-display); font-size: 0.85rem; font-weight: 700; cursor: pointer; transition: all 0.15s; text-transform: uppercase; letter-spacing: 0.08em; }
.menu-btn:hover { border-color: var(--neon); color: var(--neon); }
.log-wrap { max-height: 120px; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); }
.log-row { display: flex; justify-content: space-between; padding: 6px 12px; font-size: 0.75rem; border-bottom: 1px solid var(--border); }
.log-row:last-child { border-bottom: none; }
.log-row .log-q { color: var(--muted); }
.log-row .log-a { font-weight: 700; }
.log-row.correct .log-a { color: var(--correct); }
.log-row.wrong .log-a { color: var(--wrong); }
.log-section-label { font-size: 0.6rem; letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted); margin-bottom: 4px; }
</style>
</head>
<body>
<div class="scanlines"></div>
<div class="container">
<div class="logo">MATH RUSH</div>
<div class="tagline">Speed · Accuracy · Glory</div>
<div class="panel" id="menu">
<div>
<div class="diff-label">Difficulty</div>
<div class="diff-group">
<button class="diff-btn active" data-diff="easy">Easy</button>
<button class="diff-btn" data-diff="medium">Medium</button>
<button class="diff-btn" data-diff="hard">Hard</button>
</div>
</div>
<button class="start-btn" id="startBtn">START GAME</button>
<div class="hi-score-row">
<span>High Score</span>
<span id="menuHiScore">—</span>
</div>
</div>
<div class="panel" id="game">
<div class="hud">
<div class="hud-block">
<span class="hud-label">Score</span>
<span class="hud-value" id="scoreDisplay">0</span>
</div>
<div class="hud-block" style="text-align:center">
<span class="hud-label">Q <span id="qNum">1</span> / <span id="qTotal">10</span></span>
<span class="hud-value" id="timeDisplay">30</span>
</div>
<div class="hud-block" style="text-align:right">
<span class="hud-label">Streak</span>
<span class="hud-value" id="streakDisplay">0</span>
</div>
</div>
<div class="timer-bar-track">
<div class="timer-bar-fill" id="timerBar"></div>
</div>
<div class="problem-card">
<div class="streak-badge" id="streakBadge"></div>
<div class="problem-text" id="problemText">— + — = ?</div>
</div>
<div class="feedback" id="feedback"></div>
<div class="input-row">
<input class="answer-input" id="answerInput" type="text" inputmode="numeric" placeholder="?" autocomplete="off">
<button class="check-btn" id="checkBtn">✓</button>
</div>
<div class="numpad" id="numpad">
<button class="np-btn" data-val="7">7</button>
<button class="np-btn" data-val="8">8</button>
<button class="np-btn" data-val="9">9</button>
<button class="np-btn del" id="npDel">⌫</button>
<button class="np-btn" data-val="4">4</button>
<button class="np-btn" data-val="5">5</button>
<button class="np-btn" data-val="6">6</button>
<button class="np-btn neg" id="npNeg">±</button>
<button class="np-btn" data-val="1">1</button>
<button class="np-btn" data-val="2">2</button>
<button class="np-btn" data-val="3">3</button>
<button class="np-btn enter" id="npEnter">ENTER</button>
<button class="np-btn" data-val="0" style="grid-column:span 2">0</button>
<button class="np-btn" data-val=".">.</button>
</div>
</div>
<div class="panel" id="results">
<div class="result-title" id="resultTitle">GAME OVER</div>
<div class="result-grid">
<div class="result-stat"><div class="lbl">Final Score</div><div class="val neon" id="rScore">0</div></div>
<div class="result-stat"><div class="lbl">Accuracy</div><div class="val ok" id="rAccuracy">0%</div></div>
<div class="result-stat"><div class="lbl">Best Streak</div><div class="val accent" id="rStreak">0</div></div>
<div class="result-stat"><div class="lbl">Avg Time</div><div class="val warn" id="rAvgTime">—</div></div>
</div>
<div class="new-record" id="newRecord"></div>
<div>
<div class="log-section-label">Problem History</div>
<div class="log-wrap" id="logWrap"></div>
</div>
<div class="btn-row">
<button class="start-btn" id="playAgainBtn">PLAY AGAIN</button>
<button class="menu-btn" id="backMenuBtn">MENU</button>
</div>
</div>
</div>
<script>
const DIFFICULTIES = {
easy: { ops:['+','-'], range:[1,20], questions:10, time:30, bonus:5 },
medium: { ops:['+','-','x','d'], range:[1,20], questions:12, time:25, bonus:10 },
hard: { ops:['+','-','x','d'], range:[2,50], questions:15, time:20, bonus:15 },
};
const STREAK_LABELS = ['','','DOUBLE!','TRIPLE!','QUAD!','ON FIRE!','UNSTOPPABLE!!'];
let state = {
diff: 'easy', score: 0, streak: 0, bestStreak: 0,
qNum: 0, answer: 0, timerLeft: 30, timerMax: 30,
timerInterval: null, correct: 0, wrong: 0, history: [],
questionStartTime: 0, totalTime: 0, currentQ: '',
};
let hiScores = { easy: 0, medium: 0, hard: 0 };
try {
const saved = JSON.parse(localStorage.getItem('mathRushHi') || '{}');
hiScores = { easy: saved.easy||0, medium: saved.medium||0, hard: saved.hard||0 };
} catch(e) {}
const $menu = document.getElementById('menu');
const $game = document.getElementById('game');
const $results = document.getElementById('results');
const $score = document.getElementById('scoreDisplay');
const $time = document.getElementById('timeDisplay');
const $streak = document.getElementById('streakDisplay');
const $streakBadge = document.getElementById('streakBadge');
const $problem = document.getElementById('problemText');
const $feedback = document.getElementById('feedback');
const $input = document.getElementById('answerInput');
const $timerBar = document.getElementById('timerBar');
const $qNum = document.getElementById('qNum');
const $qTotal = document.getElementById('qTotal');
const $menuHi = document.getElementById('menuHiScore');
const $logWrap = document.getElementById('logWrap');
function rand(a, b) { return Math.floor(Math.random() * (b - a + 1)) + a; }
function generateProblem(cfg) {
const op = cfg.ops[Math.floor(Math.random() * cfg.ops.length)];
const [lo, hi] = cfg.range;
let a = rand(lo, hi), b = rand(lo, hi), q, ans, display;
if (op === '+') { q = a + b; display = a + ' + ' + b + ' = ?'; ans = q; }
else if (op === '-') {
if (cfg.diff === 'easy' && a < b) [a,b]=[b,a];
display = a + ' \u2212 ' + b + ' = ?'; ans = a - b;
}
else if (op === 'x') {
a = rand(2, cfg.diff==='hard'?15:9); b = rand(2, cfg.diff==='hard'?12:9);
display = a + ' \u00d7 ' + b + ' = ?'; ans = a * b;
}
else {
b = rand(2, cfg.diff==='hard'?12:9); ans = rand(2, cfg.diff==='hard'?12:9); a = b * ans;
display = a + ' \u00f7 ' + b + ' = ?';
}
return { display, ans };
}
function updateHud() {
$score.textContent = state.score;
$streak.textContent = state.streak;
$qNum.textContent = state.qNum;
}
function updateTimerBar() {
const pct = (state.timerLeft / state.timerMax) * 100;
$timerBar.style.width = pct + '%';
const isWarn = pct < 30;
$timerBar.classList.toggle('warn', isWarn);
$time.textContent = Math.ceil(state.timerLeft);
$time.classList.toggle('warn', isWarn);
}
function clearFeedback() { $feedback.textContent = ''; $feedback.className = 'feedback'; }
function showFeedback(msg, cls) { $feedback.textContent = msg; $feedback.className = 'feedback ' + cls; }
function updateStreakBadge() {
const idx = Math.min(state.streak, STREAK_LABELS.length - 1);
$streakBadge.textContent = STREAK_LABELS[idx] || '';
}
function updateHiScore() {
const hi = hiScores[state.diff] || 0;
$menuHi.textContent = hi > 0 ? hi : '\u2014';
}
function saveHiScore(score) {
if (score > (hiScores[state.diff] || 0)) {
hiScores[state.diff] = score;
try { localStorage.setItem('mathRushHi', JSON.stringify(hiScores)); } catch(e) {}
return true;
}
return false;
}
function showMenu() {
$menu.style.display = 'flex'; $game.style.display = 'none'; $results.style.display = 'none';
updateHiScore();
}
function showGame() {
$menu.style.display = 'none'; $game.style.display = 'flex'; $results.style.display = 'none';
}
function showResults() {
$menu.style.display = 'none'; $game.style.display = 'none'; $results.style.display = 'flex';
}
function startGame() {
const cfg = { ...DIFFICULTIES[state.diff], diff: state.diff };
Object.assign(state, { score:0, streak:0, bestStreak:0, qNum:0, correct:0, wrong:0, history:[], totalTime:0 });
state.timerMax = cfg.time; state.timerLeft = cfg.time;
$qTotal.textContent = cfg.questions;
showGame();
nextQuestion(cfg);
}
function nextQuestion(cfg) {
if (state.qNum >= cfg.questions) { endGame(); return; }
state.qNum++;
updateHud(); clearFeedback();
$input.value = '';
$input.classList.remove('shake','correct-flash');
state.timerLeft = cfg.time;
updateTimerBar(); updateStreakBadge();
const { display, ans } = generateProblem(cfg);
state.answer = ans;
state.currentQ = display.replace(' = ?','');
$problem.textContent = display;
state.questionStartTime = performance.now();
clearInterval(state.timerInterval);
state.timerInterval = setInterval(() => {
state.timerLeft -= 0.1;
if (state.timerLeft <= 0) {
state.timerLeft = 0; updateTimerBar();
clearInterval(state.timerInterval);
handleTimeout(cfg);
} else { updateTimerBar(); }
}, 100);
setTimeout(() => $input.focus(), 50);
}
function handleSubmit(cfg) {
const raw = $input.value.trim();
if (raw === '' || raw === '-') return;
const num = parseFloat(raw);
if (isNaN(num)) return;
clearInterval(state.timerInterval);
const elapsed = (performance.now() - state.questionStartTime) / 1000;
state.totalTime += elapsed;
const isCorrect = Math.abs(num - state.answer) < 0.01;
const timeBonus = isCorrect ? Math.max(0, Math.floor((state.timerLeft / cfg.time) * cfg.bonus)) : 0;
const streakBonus = isCorrect ? Math.min(state.streak, 4) : 0;
const pts = isCorrect ? 10 + timeBonus + streakBonus * 2 : 0;
state.history.push({ q: state.currentQ, ans: state.answer, given: num, correct: isCorrect });
if (isCorrect) {
state.score += pts; state.streak++; state.bestStreak = Math.max(state.bestStreak, state.streak); state.correct++;
showFeedback('\u2713 +' + pts + ' pts' + (pts > 10 ? ' (speed bonus!)' : ''), 'correct');
$input.classList.add('correct-flash');
} else {
state.streak = 0; state.wrong++;
showFeedback('\u2717 Answer was ' + state.answer, 'wrong');
$input.classList.add('shake');
}
updateHud();
setTimeout(() => nextQuestion(cfg), 900);
}
function handleTimeout(cfg) {
state.history.push({ q: state.currentQ, ans: state.answer, given: null, correct: false });
state.streak = 0; state.wrong++;
showFeedback('\u23f1 Time! Answer was ' + state.answer, 'wrong');
$input.classList.add('shake');
updateHud();
setTimeout(() => nextQuestion(cfg), 900);
}
function endGame() {
clearInterval(state.timerInterval);
const total = state.correct + state.wrong;
const acc = total ? Math.round((state.correct / total) * 100) : 0;
const avgTime = state.history.length ? (state.totalTime / state.history.length).toFixed(1) : '\u2014';
const isNew = saveHiScore(state.score);
document.getElementById('rScore').textContent = state.score;
document.getElementById('rAccuracy').textContent = acc + '%';
document.getElementById('rStreak').textContent = state.bestStreak;
document.getElementById('rAvgTime').textContent = avgTime + 's';
document.getElementById('newRecord').textContent = isNew ? '\ud83c\udfc6 NEW HIGH SCORE!' : '';
document.getElementById('resultTitle').textContent = acc >= 80 ? 'EXCELLENT!' : acc >= 50 ? 'GOOD EFFORT' : 'GAME OVER';
$logWrap.innerHTML = '';
state.history.forEach(h => {
const row = document.createElement('div');
row.className = 'log-row ' + (h.correct ? 'correct' : 'wrong');
const given = h.given !== null ? h.given : 'timeout';
row.innerHTML = '<span class="log-q">' + h.q + ' = </span><span class="log-a">' + given + ' (' + h.ans + ')</span>';
$logWrap.appendChild(row);
});
showResults();
}
document.querySelectorAll('.diff-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.diff-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
state.diff = btn.dataset.diff;
updateHiScore();
});
});
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('checkBtn').addEventListener('click', () => handleSubmit({ ...DIFFICULTIES[state.diff], diff: state.diff }));
$input.addEventListener('keydown', e => { if (e.key === 'Enter') handleSubmit({ ...DIFFICULTIES[state.diff], diff: state.diff }); });
document.querySelectorAll('.np-btn[data-val]').forEach(btn => {
btn.addEventListener('click', () => {
const v = btn.dataset.val;
if (v === '.') { if ($input.value.includes('.')) return; $input.value += ($input.value === '' || $input.value === '-') ? '0.' : '.'; }
else { $input.value += v; }
$input.focus();
});
});
document.getElementById('npDel').addEventListener('click', () => { $input.value = $input.value.slice(0,-1); $input.focus(); });
document.getElementById('npNeg').addEventListener('click', () => { $input.value = $input.value.startsWith('-') ? $input.value.slice(1) : '-' + $input.value; $input.focus(); });
document.getElementById('npEnter').addEventListener('click', () => handleSubmit({ ...DIFFICULTIES[state.diff], diff: state.diff }));
document.getElementById('playAgainBtn').addEventListener('click', startGame);
document.getElementById('backMenuBtn').addEventListener('click', showMenu);
updateHiScore();
showMenu();
</script>
</body>
</html>
```
[REQUESTED CHANGES]
(no specific request — apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget — tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** — do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT — Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state — it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected — do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` — state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Agent-Accessible State (vibes.ai.*)
State written with the standard `vibes.save` / `vibes.load` methods is private to each user and is **NOT readable by AI agents**. To share state with an AI agent (via the MCP connector), use the `vibes.ai` sub-namespace:
| Method | Description |
|--------|-------------|
| `await vibes.ai.setState(key, value)` | Write agent-readable state. Key is stored internally as `ai/<key>`. |
| `await vibes.ai.getState(key)` | Read agent-readable state. Returns `null` if key absent. |
| `await vibes.ai.listKeys()` | List all agent-readable keys (without the `ai/` prefix). |
**Important rules:**
- Regular `vibes.save()` / `vibes.setState()` is **NOT agent-accessible** — use `vibes.ai.*` for state you want agents to read.
- The widget **owner** must enable agent access in **Manage → Agent tab** before any agent can read or write `vibes.ai.*` state.
- Keys are auto-prefixed to `ai/` internally; you supply just the short key (e.g. `'context'`).
```js
vibes.onReady(async () => {
// Write state an AI agent can later read
await vibes.ai.setState('context', { currentLevel: 3, score: 1500 });
// Read it back (same auto-prefix applies)
const ctx = await vibes.ai.getState('context');
// List all agent-accessible keys for this widget
const keys = await vibes.ai.listKeys(); // e.g. ['context']
});
```
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** — they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** — it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** — the SDK may not be ready immediately.
4. **Await all SDK calls** — every method (except `vibes.shared.get`) returns a Promise.
5. **Do NOT import external JS libraries via script tags** — they will be blocked. Include library code inline or use a CDN link in a `<link>` tag for CSS only.
6. **Keep total code under 80KB** — that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser — no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained HTML file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/Owner/math-rush-calculator-speed-game */