0
TOOLS#e280adb5
Simon Says
@Owner·deposited 1mo ago·updated 1mo ago·72 views
TOOLS#e280adb5
Simon Says
OW
@Owner
72Views
0Comments
0Forks
0Saves
SHARE · REMIX
Simon Says — a HTML tools widget by @Owner.
CONTROLS
gamememorysimonarcadeclassic
No comments yet. Be the first!
✦ Remix with AI
SDK in this widgetNo Vibes SDK features detected yet
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "Simon Says" by @Owner]
Source: https://itjustvibes.com/Owner/simon-says
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>Simon Says</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Orbitron:wght@400;700;900&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0a0f;
--border: #1e1e2e;
--green: #00ff87; --red: #ff2d55; --yellow: #ffd60a; --blue: #0af;
--green-dim: #003d20; --red-dim: #3d0011; --yellow-dim: #3d3000; --blue-dim: #00253d;
--text: #e0e0ff; --muted: #555577;
}
body {
background: var(--bg); color: var(--text);
font-family: 'Orbitron', monospace;
min-height: 100vh; display: flex; flex-direction: column;
align-items: center; justify-content: center; padding: 20px; overflow: hidden;
}
body::before {
content: ''; position: fixed; inset: 0;
background: radial-gradient(ellipse at 50% 50%, #0d0d2a 0%, #0a0a0f 70%); z-index: -1;
}
.container { display: flex; flex-direction: column; align-items: center; gap: 24px; width: 100%; max-width: 400px; }
.title {
font-size: clamp(1.6rem, 6vw, 2.4rem); font-weight: 900; letter-spacing: 0.15em;
background: linear-gradient(135deg, var(--green), var(--blue));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-transform: uppercase;
}
.subtitle { font-family: 'Share Tech Mono', monospace; font-size: 0.7rem; color: var(--muted); letter-spacing: 0.2em; text-transform: uppercase; }
.score-row { display: flex; gap: 32px; align-items: center; }
.score-block { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.score-label { font-family: 'Share Tech Mono', monospace; font-size: 0.6rem; color: var(--muted); letter-spacing: 0.2em; text-transform: uppercase; }
.score-value { font-size: 2rem; font-weight: 700; color: var(--text); line-height: 1; min-width: 3ch; text-align: center; }
.score-value.pulse { animation: scorePulse 0.3s ease; }
@keyframes scorePulse { 0%{transform:scale(1)} 50%{transform:scale(1.3);color:var(--green)} 100%{transform:scale(1)} }
.board-wrap { position: relative; width: min(320px, 90vw); height: min(320px, 90vw); }
.board { width: 100%; height: 100%; border-radius: 50%; position: relative; overflow: hidden; box-shadow: 0 0 60px rgba(0,0,0,0.8), inset 0 0 40px rgba(0,0,0,0.5); }
.pad { position: absolute; width: 50%; height: 50%; cursor: pointer; border: 3px solid var(--bg); user-select: none; -webkit-tap-highlight-color: transparent; transition: filter 0.05s; }
.pad::after { content: ''; position: absolute; inset: 0; background: rgba(255,255,255,0.08); opacity: 0; transition: opacity 0.1s; }
.pad:hover::after { opacity: 1; }
.pad.active::after { opacity: 0; }
.pad-green { top: 0; left: 0; border-radius: 100% 0 0 0; background: var(--green-dim); }
.pad-red { top: 0; right: 0; border-radius: 0 100% 0 0; background: var(--red-dim); }
.pad-yellow { bottom: 0; left: 0; border-radius: 0 0 0 100%; background: var(--yellow-dim); }
.pad-blue { bottom: 0; right: 0; border-radius: 0 0 100% 0; background: var(--blue-dim); }
.pad-green.active { background: var(--green); box-shadow: 0 0 40px var(--green), 0 0 80px var(--green); filter: brightness(1.2); }
.pad-red.active { background: var(--red); box-shadow: 0 0 40px var(--red), 0 0 80px var(--red); filter: brightness(1.2); }
.pad-yellow.active { background: var(--yellow); box-shadow: 0 0 40px var(--yellow), 0 0 80px var(--yellow); filter: brightness(1.2); }
.pad-blue.active { background: var(--blue); box-shadow: 0 0 40px var(--blue), 0 0 80px var(--blue); filter: brightness(1.2); }
.pad-icon { position: absolute; font-size: 1.6rem; opacity: 0.3; pointer-events: none; transition: opacity 0.1s; }
.pad-green .pad-icon { bottom: 20%; right: 20%; }
.pad-red .pad-icon { bottom: 20%; left: 20%; }
.pad-yellow .pad-icon { top: 20%; right: 20%; }
.pad-blue .pad-icon { top: 20%; left: 20%; }
.pad.active .pad-icon { opacity: 0.8; }
.center-hub {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 36%; height: 36%; border-radius: 50%; background: var(--bg);
border: 3px solid var(--border); display: flex; flex-direction: column;
align-items: center; justify-content: center; z-index: 10; gap: 2px;
box-shadow: 0 0 30px rgba(0,0,0,0.9);
}
.hub-level { font-size: 0.55rem; color: var(--muted); letter-spacing: 0.15em; text-transform: uppercase; }
.hub-number { font-size: 1.6rem; font-weight: 900; color: var(--text); line-height: 1; }
.status-bar { height: 36px; display: flex; align-items: center; justify-content: center; }
.status-text { font-family: 'Share Tech Mono', monospace; font-size: 0.8rem; letter-spacing: 0.12em; color: var(--muted); text-transform: uppercase; text-align: center; transition: color 0.3s; }
.status-text.watching { color: var(--blue); }
.status-text.your-turn { color: var(--green); }
.status-text.wrong { color: var(--red); }
.status-text.success { color: var(--yellow); }
.seq-dots { display: flex; gap: 6px; flex-wrap: wrap; justify-content: center; min-height: 14px; max-width: 300px; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--border); transition: background 0.15s; }
.dot.green { background: var(--green); box-shadow: 0 0 6px var(--green); }
.dot.red { background: var(--red); box-shadow: 0 0 6px var(--red); }
.dot.yellow { background: var(--yellow); box-shadow: 0 0 6px var(--yellow); }
.dot.blue { background: var(--blue); box-shadow: 0 0 6px var(--blue); }
.dot.done { opacity: 0.4; }
.start-btn {
background: transparent; border: 2px solid var(--green); color: var(--green);
font-family: 'Orbitron', monospace; font-weight: 700; font-size: 0.85rem;
letter-spacing: 0.2em; text-transform: uppercase; padding: 12px 36px;
border-radius: 4px; cursor: pointer; transition: background 0.2s, box-shadow 0.2s, transform 0.1s;
box-shadow: 0 0 20px rgba(0,255,135,0.15);
}
.start-btn:hover { background: rgba(0,255,135,0.1); box-shadow: 0 0 30px rgba(0,255,135,0.3); }
.start-btn:active { transform: scale(0.97); }
.start-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.speed-row { display: flex; gap: 8px; align-items: center; }
.speed-label { font-family: 'Share Tech Mono', monospace; font-size: 0.6rem; color: var(--muted); letter-spacing: 0.15em; text-transform: uppercase; }
.speed-bars { display: flex; gap: 3px; align-items: flex-end; height: 16px; }
.speed-bar { width: 5px; border-radius: 2px; background: var(--border); transition: background 0.3s; }
.speed-bar:nth-child(1){height:5px} .speed-bar:nth-child(2){height:8px} .speed-bar:nth-child(3){height:11px} .speed-bar:nth-child(4){height:14px}
.speed-bar.active { background: var(--yellow); box-shadow: 0 0 4px var(--yellow); }
.lose-flash { position: fixed; inset: 0; background: rgba(255,45,85,0.15); pointer-events: none; opacity: 0; z-index: 100; }
.lose-flash.show { animation: loseFlash 0.6s ease; }
@keyframes loseFlash { 0%{opacity:0} 30%{opacity:1} 100%{opacity:0} }
</style>
</head>
<body>
<div class="lose-flash" id="loseFlash"></div>
<div class="container">
<div style="text-align:center;display:flex;flex-direction:column;gap:6px">
<div class="title">Simon Says</div>
<div class="subtitle">Memory Sequence Game</div>
</div>
<div class="score-row">
<div class="score-block"><span class="score-label">Round</span><span class="score-value" id="roundDisplay">—</span></div>
<div class="score-block"><span class="score-label">Best</span><span class="score-value" id="bestDisplay">0</span></div>
</div>
<div class="board-wrap">
<div class="board">
<div class="pad pad-green" id="pad-green" onclick="handlePad('green')"><span class="pad-icon">▲</span></div>
<div class="pad pad-red" id="pad-red" onclick="handlePad('red')"><span class="pad-icon">▶</span></div>
<div class="pad pad-yellow" id="pad-yellow" onclick="handlePad('yellow')"><span class="pad-icon">◀</span></div>
<div class="pad pad-blue" id="pad-blue" onclick="handlePad('blue')"><span class="pad-icon">▼</span></div>
<div class="center-hub">
<span class="hub-level">LVL</span>
<span class="hub-number" id="hubNumber">—</span>
</div>
</div>
</div>
<div class="status-bar"><div class="status-text" id="statusText">Press Start to Play</div></div>
<div class="seq-dots" id="seqDots"></div>
<button class="start-btn" id="startBtn" onclick="startGame()">START</button>
<div class="speed-row">
<span class="speed-label">Speed</span>
<div class="speed-bars">
<div class="speed-bar" id="sb1"></div><div class="speed-bar" id="sb2"></div>
<div class="speed-bar" id="sb3"></div><div class="speed-bar" id="sb4"></div>
</div>
</div>
</div>
<script>
const COLORS=['green','red','yellow','blue'];
const TONES={green:415.30,red:311.13,yellow:252,blue:209};
const SPEEDS=[[600,200],[450,150],[320,120],[200,80]];
let sequence=[],playerIdx=0,phase='idle',best=0,audioCtx=null;
function getAudio(){if(!audioCtx)audioCtx=new(window.AudioContext||window.webkitAudioContext)();return audioCtx;}
function playTone(color,dur=300){try{const ctx=getAudio(),osc=ctx.createOscillator(),gain=ctx.createGain();osc.connect(gain);gain.connect(ctx.destination);osc.type='sine';osc.frequency.setValueAtTime(TONES[color],ctx.currentTime);gain.gain.setValueAtTime(0.4,ctx.currentTime);gain.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+dur/1000);osc.start(ctx.currentTime);osc.stop(ctx.currentTime+dur/1000);}catch(e){}}
function playError(){try{const ctx=getAudio();[160,120].forEach((freq,i)=>{const osc=ctx.createOscillator(),gain=ctx.createGain();osc.connect(gain);gain.connect(ctx.destination);osc.type='sawtooth';osc.frequency.setValueAtTime(freq,ctx.currentTime+i*0.18);gain.gain.setValueAtTime(0.3,ctx.currentTime+i*0.18);gain.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+i*0.18+0.25);osc.start(ctx.currentTime+i*0.18);osc.stop(ctx.currentTime+i*0.18+0.25);});}catch(e){}}
function speedForLevel(lvl){if(lvl<=4)return SPEEDS[0];if(lvl<=8)return SPEEDS[1];if(lvl<=12)return SPEEDS[2];return SPEEDS[3];}
function updateSpeedBars(lvl){const tier=lvl<=4?0:lvl<=8?1:lvl<=12?2:3;for(let i=1;i<=4;i++)document.getElementById('sb'+i).classList.toggle('active',i<=tier+1);}
function flashPad(color,duration){return new Promise(resolve=>{const pad=document.getElementById('pad-'+color);pad.classList.add('active');playTone(color,duration);setTimeout(()=>{pad.classList.remove('active');resolve();},duration);});}
function sleep(ms){return new Promise(r=>setTimeout(r,ms));}
function setStatus(msg,cls=''){const el=document.getElementById('statusText');el.textContent=msg;el.className='status-text'+(cls?' '+cls:'');}
function updateHub(n){document.getElementById('hubNumber').textContent=n===null?'—':n;}
function updateRound(n){const el=document.getElementById('roundDisplay');el.textContent=n===null?'—':n;el.classList.remove('pulse');void el.offsetWidth;if(n!==null)el.classList.add('pulse');}
function renderDots(seq,progress){const c=document.getElementById('seqDots');c.innerHTML='';seq.forEach((color,i)=>{const d=document.createElement('div');d.className='dot';if(i<progress)d.classList.add(color,'done');else if(i===progress)d.classList.add(color);c.appendChild(d);});}
function clearDots(){document.getElementById('seqDots').innerHTML='';}
async function playSequence(){
phase='watching';setStatus('Watch the sequence…','watching');document.getElementById('startBtn').disabled=true;
const[flashDur,pause]=speedForLevel(sequence.length);updateSpeedBars(sequence.length);await sleep(500);
for(let i=0;i<sequence.length;i++){await flashPad(sequence[i],flashDur);await sleep(pause);}
phase='input';playerIdx=0;setStatus('Your turn! Repeat the sequence.','your-turn');renderDots(sequence,0);
}
async function startGame(){
sequence=[];playerIdx=0;phase='idle';clearDots();updateRound(null);updateHub(null);
document.getElementById('startBtn').textContent='START';await nextRound();
}
async function nextRound(){
sequence.push(COLORS[Math.floor(Math.random()*4)]);const lvl=sequence.length;updateRound(lvl);updateHub(lvl);
if(lvl>best){best=lvl;document.getElementById('bestDisplay').textContent=best;}await playSequence();
}
function handlePad(color){
if(phase!=='input')return;flashPad(color,200);const expected=sequence[playerIdx];
if(color!==expected){loseGame();return;}
playerIdx++;renderDots(sequence,playerIdx);
if(playerIdx===sequence.length){phase='idle';setStatus('Correct! Get ready…','success');clearDots();setTimeout(()=>nextRound(),1000);}
}
function loseGame(){
phase='lost';playError();COLORS.forEach(c=>document.getElementById('pad-'+c).classList.remove('active'));
document.getElementById('pad-'+sequence[playerIdx]).classList.add('active');
const flash=document.getElementById('loseFlash');flash.classList.remove('show');void flash.offsetWidth;flash.classList.add('show');
setStatus('Wrong! You reached round '+sequence.length+'.','wrong');clearDots();
setTimeout(()=>{COLORS.forEach(c=>document.getElementById('pad-'+c).classList.remove('active'));updateRound(null);updateHub(null);setStatus('Press Start to Play Again','');document.getElementById('startBtn').disabled=false;document.getElementById('startBtn').textContent='PLAY AGAIN';phase='idle';},1800);
}
</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.load()` 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. **External JS libraries are allowed and encouraged** — load them via a `<script src>` tag from a reputable CDN (jsDelivr, unpkg, or cdnjs) with a pinned version, or inline the library source. The only exception: do NOT add a script tag for the Vibes SDK itself — it is auto-injected (see Rule 2).
6. **Keep total code under 100KB** — 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/simon-says */