← back to Games Agentabrams
games/simon/index.html
633 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Abrams Simon</title>
<style>
:root {
--bg0: #05060a;
--bg1: #0b0f1c;
--ink: #e8f0ff;
--muted: #7b88a8;
--accent: #45e6ff;
--green: #1fd67a;
--red: #ff3b5c;
--yellow: #ffd23b;
--blue: #3b7bff;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background:
radial-gradient(circle at 50% 8%, #12203f 0%, var(--bg1) 45%, var(--bg0) 100%);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}
#app {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: clamp(8px, 2.4vmin, 24px);
gap: clamp(8px, 2vmin, 20px);
}
header {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
text-align: center;
}
h1 {
margin: 0;
font-size: clamp(20px, 5vmin, 40px);
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
background: linear-gradient(90deg, var(--green), var(--yellow), var(--red), var(--blue), var(--accent));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 0 12px rgba(69, 230, 255, 0.35));
}
.subtitle {
font-size: clamp(10px, 1.8vmin, 13px);
color: var(--muted);
letter-spacing: 0.24em;
text-transform: uppercase;
}
.stats {
display: flex;
gap: clamp(10px, 3vmin, 28px);
align-items: stretch;
}
.stat {
min-width: clamp(70px, 18vmin, 110px);
padding: clamp(6px, 1.4vmin, 12px) clamp(10px, 2.4vmin, 18px);
border-radius: 14px;
background: rgba(20, 30, 56, 0.55);
border: 1px solid rgba(90, 120, 190, 0.18);
box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.35);
text-align: center;
}
.stat .label {
font-size: clamp(9px, 1.5vmin, 11px);
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
}
.stat .value {
font-size: clamp(20px, 5vmin, 34px);
font-weight: 800;
line-height: 1.05;
color: var(--accent);
text-shadow: 0 0 14px rgba(69, 230, 255, 0.4);
}
.stat.best .value { color: var(--yellow); text-shadow: 0 0 14px rgba(255, 210, 59, 0.4); }
/* Board */
.board-wrap {
position: relative;
width: min(78vmin, 560px);
height: min(78vmin, 560px);
max-width: 90vw;
max-height: 60vh;
}
.board {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: clamp(8px, 2.2vmin, 20px);
border-radius: 50%;
padding: clamp(8px, 2.2vmin, 20px);
background:
radial-gradient(circle at center, rgba(15, 22, 44, 0.9), rgba(5, 7, 14, 0.9));
box-shadow:
0 0 0 2px rgba(90, 120, 190, 0.15),
0 0 60px rgba(0, 0, 0, 0.6),
inset 0 0 40px rgba(0, 0, 0, 0.7);
}
.pad {
position: relative;
border: none;
cursor: pointer;
background: var(--c);
filter: brightness(0.42) saturate(0.9);
transition: filter 0.09s ease, box-shadow 0.09s ease, transform 0.06s ease;
outline: none;
box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.55);
}
.pad::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: inset 0 0 22px rgba(255, 255, 255, 0.08);
pointer-events: none;
}
.pad.tl { border-top-left-radius: 100%; --c: var(--green); }
.pad.tr { border-top-right-radius: 100%; --c: var(--red); }
.pad.bl { border-bottom-left-radius: 100%; --c: var(--yellow); }
.pad.br { border-bottom-right-radius: 100%; --c: var(--blue); }
.pad.lit {
filter: brightness(1.5) saturate(1.25);
box-shadow:
0 0 46px 8px var(--c),
inset 0 0 40px rgba(255, 255, 255, 0.5);
transform: scale(1.015);
}
.board.playable .pad:not(:disabled):active {
filter: brightness(1.35) saturate(1.2);
}
/* Center hub */
.hub {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 34%;
height: 34%;
border-radius: 50%;
background: radial-gradient(circle at 35% 30%, #16223f, #080b16 72%);
border: 2px solid rgba(90, 120, 190, 0.25);
box-shadow: 0 0 30px rgba(0, 0, 0, 0.7), inset 0 0 26px rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
z-index: 3;
text-align: center;
padding: 4px;
}
.hub .hub-title {
font-size: clamp(10px, 2.1vmin, 15px);
font-weight: 800;
letter-spacing: 0.16em;
color: var(--accent);
text-shadow: 0 0 12px rgba(69, 230, 255, 0.5);
}
.hub .hub-msg {
font-size: clamp(9px, 1.7vmin, 12px);
color: var(--muted);
letter-spacing: 0.06em;
min-height: 1.1em;
}
/* Controls */
.controls {
display: flex;
flex-wrap: wrap;
gap: clamp(8px, 2vmin, 16px);
align-items: center;
justify-content: center;
}
button.ctrl {
font-family: inherit;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
font-size: clamp(11px, 2vmin, 14px);
color: var(--ink);
background: linear-gradient(180deg, rgba(40, 58, 100, 0.75), rgba(20, 30, 56, 0.75));
border: 1px solid rgba(90, 130, 210, 0.35);
border-radius: 12px;
padding: clamp(8px, 1.8vmin, 12px) clamp(16px, 4vmin, 26px);
cursor: pointer;
transition: transform 0.08s ease, box-shadow 0.15s ease, border-color 0.15s ease;
box-shadow: 0 0 0 rgba(69, 230, 255, 0);
}
button.ctrl:hover { border-color: var(--accent); box-shadow: 0 0 18px rgba(69, 230, 255, 0.25); }
button.ctrl:active { transform: translateY(1px) scale(0.98); }
button.start {
background: linear-gradient(180deg, var(--accent), #1b9fbf);
color: #041018;
border-color: rgba(69, 230, 255, 0.6);
box-shadow: 0 0 22px rgba(69, 230, 255, 0.35);
}
button.start:hover { box-shadow: 0 0 30px rgba(69, 230, 255, 0.55); }
.toggle {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: clamp(10px, 1.8vmin, 13px);
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
cursor: pointer;
padding: clamp(7px, 1.6vmin, 11px) clamp(12px, 3vmin, 18px);
border-radius: 12px;
border: 1px solid rgba(90, 130, 210, 0.22);
background: rgba(20, 30, 56, 0.4);
transition: border-color 0.15s ease, color 0.15s ease;
}
.toggle.on { color: var(--ink); border-color: rgba(69, 230, 255, 0.5); }
.toggle .dot {
width: 12px; height: 12px; border-radius: 50%;
background: rgba(120, 140, 180, 0.4);
box-shadow: inset 0 0 4px rgba(0,0,0,0.6);
transition: background 0.15s ease, box-shadow 0.15s ease;
}
.toggle.on .dot {
background: var(--green);
box-shadow: 0 0 10px var(--green);
}
.toggle.strict.on .dot { background: var(--red); box-shadow: 0 0 10px var(--red); }
.board.flash-fail { animation: shake 0.4s ease; }
@keyframes shake {
0%,100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-5px); }
80% { transform: translateX(5px); }
}
@media (max-height: 560px) {
header .subtitle { display: none; }
.board-wrap { max-height: 52vh; }
}
</style>
</head>
<body>
<div id="app">
<header>
<h1>Abrams Simon</h1>
<div class="subtitle">Watch · Remember · Repeat</div>
</header>
<div class="stats">
<div class="stat"><div class="label">Round</div><div class="value" id="roundVal">0</div></div>
<div class="stat best"><div class="label">Best</div><div class="value" id="bestVal">0</div></div>
</div>
<div class="board-wrap">
<div class="board" id="board">
<button class="pad tl" data-i="0" aria-label="green" disabled></button>
<button class="pad tr" data-i="1" aria-label="red" disabled></button>
<button class="pad bl" data-i="2" aria-label="yellow" disabled></button>
<button class="pad br" data-i="3" aria-label="blue" disabled></button>
</div>
<div class="hub">
<div class="hub-title" id="hubTitle">READY?</div>
<div class="hub-msg" id="hubMsg">Press Start</div>
</div>
</div>
<div class="controls">
<button class="ctrl start" id="startBtn">Start</button>
<div class="toggle strict" id="strictToggle" role="switch" aria-checked="false" tabindex="0">
<span class="dot"></span><span>Strict</span>
</div>
<div class="toggle on" id="soundToggle" role="switch" aria-checked="true" tabindex="0">
<span class="dot"></span><span>Sound</span>
</div>
</div>
</div>
<script>
(function () {
"use strict";
// ---- Elements ----
const pads = Array.from(document.querySelectorAll(".pad"));
const board = document.getElementById("board");
const roundVal = document.getElementById("roundVal");
const bestVal = document.getElementById("bestVal");
const hubTitle = document.getElementById("hubTitle");
const hubMsg = document.getElementById("hubMsg");
const startBtn = document.getElementById("startBtn");
const strictToggle = document.getElementById("strictToggle");
const soundToggle = document.getElementById("soundToggle");
// ---- State ----
const BEST_KEY = "abrams-simon-best";
const SOUND_KEY = "abrams-simon-sound";
const STRICT_KEY = "abrams-simon-strict";
let sequence = [];
let userStep = 0;
let acceptingInput = false;
let playing = false;
let busy = false; // during playback / transitions
let best = 0;
// pad tones (green, red, yellow, blue) — pleasant-ish intervals
const TONES = [329.63, 261.63, 220.0, 164.81]; // E4, C4, A3, E3
const FAIL_TONE = 110.0;
// ---- Audio ----
let audioCtx = null;
let soundOn = true;
function ensureAudio() {
if (!audioCtx) {
try {
const AC = window.AudioContext || window.webkitAudioContext;
audioCtx = new AC();
} catch (e) {
audioCtx = null;
}
}
if (audioCtx && audioCtx.state === "suspended") {
audioCtx.resume().catch(function () {});
}
return audioCtx;
}
function playTone(freq, durationMs, type) {
if (!soundOn) return;
const ctx = ensureAudio();
if (!ctx) return;
const now = ctx.currentTime;
const dur = durationMs / 1000;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type || "sine";
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(0.28, now + 0.012);
gain.gain.setValueAtTime(0.28, now + Math.max(0.012, dur - 0.06));
gain.gain.exponentialRampToValueAtTime(0.0001, now + dur);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + dur + 0.02);
}
function playFail() {
if (!soundOn) return;
const ctx = ensureAudio();
if (!ctx) return;
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = "sawtooth";
osc.frequency.setValueAtTime(FAIL_TONE, now);
osc.frequency.exponentialRampToValueAtTime(55, now + 0.7);
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(0.3, now + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.75);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.8);
}
function winChime() {
if (!soundOn) return;
const ctx = ensureAudio();
if (!ctx) return;
[523.25, 659.25, 783.99].forEach(function (f, k) {
setTimeout(function () { playTone(f, 160, "triangle"); }, k * 90);
});
}
// ---- Timing (speeds up with level) ----
function litDuration() {
// starts ~560ms, floors ~200ms
return Math.max(200, 560 - sequence.length * 22);
}
function gapDuration() {
return Math.max(80, 200 - sequence.length * 8);
}
// ---- Visual pad flash ----
function litPad(i, duration) {
return new Promise(function (resolve) {
const pad = pads[i];
pad.classList.add("lit");
playTone(TONES[i], duration, "sine");
setTimeout(function () {
pad.classList.remove("lit");
resolve();
}, duration);
});
}
function sleep(ms) {
return new Promise(function (r) { setTimeout(r, ms); });
}
// ---- Sequence playback ----
async function playSequence() {
busy = true;
acceptingInput = false;
setBoardPlayable(false);
hubTitle.textContent = "WATCH";
hubMsg.textContent = "Round " + sequence.length;
await sleep(500);
const dur = litDuration();
const gap = gapDuration();
for (let k = 0; k < sequence.length; k++) {
await litPad(sequence[k], dur);
await sleep(gap);
}
busy = false;
acceptingInput = true;
userStep = 0;
setBoardPlayable(true);
hubTitle.textContent = "YOUR TURN";
hubMsg.textContent = "0 / " + sequence.length;
}
function setBoardPlayable(on) {
pads.forEach(function (p) { p.disabled = !on; });
board.classList.toggle("playable", on);
}
// ---- Round flow ----
function nextRound() {
sequence.push(Math.floor(Math.random() * 4));
roundVal.textContent = sequence.length;
playSequence();
}
function startGame() {
ensureAudio();
sequence = [];
userStep = 0;
playing = true;
acceptingInput = false;
roundVal.textContent = "0";
startBtn.textContent = "Restart";
board.classList.remove("flash-fail");
hubTitle.textContent = "GO!";
hubMsg.textContent = "";
setTimeout(nextRound, 350);
}
async function handlePress(i) {
if (!acceptingInput || busy || !playing) return;
// flash the pressed pad
const dur = 220;
acceptingInput = false;
await litPad(i, dur);
if (i !== sequence[userStep]) {
gameOver();
return;
}
userStep++;
hubMsg.textContent = userStep + " / " + sequence.length;
if (userStep >= sequence.length) {
// round cleared
updateBest(sequence.length);
winChime();
hubTitle.textContent = "NICE";
hubMsg.textContent = "Round " + sequence.length + " cleared";
acceptingInput = false;
setBoardPlayable(false);
await sleep(650);
nextRound();
} else {
acceptingInput = true;
}
}
async function gameOver() {
playing = false;
acceptingInput = false;
setBoardPlayable(false);
playFail();
board.classList.add("flash-fail");
// flash all pads red-ish
pads.forEach(function (p) { p.classList.add("lit"); });
await sleep(320);
pads.forEach(function (p) { p.classList.remove("lit"); });
board.classList.remove("flash-fail");
const reached = Math.max(0, sequence.length - 1);
hubTitle.textContent = "GAME OVER";
hubMsg.textContent = "Reached round " + reached;
startBtn.textContent = "Start";
if (strictOn) {
// strict: sequence resets fully (already will on Start)
sequence = [];
roundVal.textContent = "0";
}
}
function updateBest(level) {
if (level > best) {
best = level;
bestVal.textContent = best;
try { localStorage.setItem(BEST_KEY, String(best)); } catch (e) {}
}
}
// ---- Toggles ----
let strictOn = false;
function setSound(on) {
soundOn = on;
soundToggle.classList.toggle("on", on);
soundToggle.setAttribute("aria-checked", on ? "true" : "false");
try { localStorage.setItem(SOUND_KEY, on ? "1" : "0"); } catch (e) {}
if (on) ensureAudio();
}
function setStrict(on) {
strictOn = on;
strictToggle.classList.toggle("on", on);
strictToggle.setAttribute("aria-checked", on ? "true" : "false");
try { localStorage.setItem(STRICT_KEY, on ? "1" : "0"); } catch (e) {}
}
// ---- Wire events ----
pads.forEach(function (pad) {
const i = parseInt(pad.getAttribute("data-i"), 10);
// Pointer events cover mouse + touch; prevent double-fire.
pad.addEventListener("pointerdown", function (ev) {
ev.preventDefault();
handlePress(i);
});
// keyboard access
pad.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
handlePress(i);
}
});
});
startBtn.addEventListener("click", function () {
ensureAudio();
startGame();
});
soundToggle.addEventListener("click", function () { setSound(!soundOn); });
soundToggle.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); setSound(!soundOn); }
});
strictToggle.addEventListener("click", function () { setStrict(!strictOn); });
strictToggle.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); setStrict(!strictOn); }
});
// number-key shortcuts 1-4
window.addEventListener("keydown", function (ev) {
if (ev.repeat) return;
const map = { "1": 0, "2": 1, "3": 2, "4": 3 };
if (ev.key in map) { handlePress(map[ev.key]); }
else if (ev.key === "Enter" && !playing) { ensureAudio(); startGame(); }
});
// ---- Init from storage ----
(function init() {
try {
const b = parseInt(localStorage.getItem(BEST_KEY) || "0", 10);
if (!isNaN(b) && b > 0) { best = b; bestVal.textContent = b; }
} catch (e) {}
let s = true, st = false;
try {
const sv = localStorage.getItem(SOUND_KEY);
if (sv !== null) s = sv === "1";
const stv = localStorage.getItem(STRICT_KEY);
if (stv !== null) st = stv === "1";
} catch (e) {}
setSound(s);
setStrict(st);
})();
})();
</script>
</body>
</html>