GPT-6 Luna
by OpenAIReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 6 min 35 s
- Cost
- $0.026
- Output tokens
- 52,583
- Spent thinking
- 37,587 tokens
- Speed
- 135 tok/s
- Console errors
- 0
- First token after
- 5.4 s
- First visible text after
- 5 min 12 s
- Prompt tokens
- 493
- Code size
- 47.3 KB
- Finish reason
- stop
- Served by
- OpenAI
- Model id
openai/gpt-6-luna- Reasoning sent
{"effort":"max"}- Max output
- 64,000 tokens
- Generated
- Nov 10, 2026
What it was asked
The prompt
Build a polished Snake game in a single HTML file using vanilla JavaScript and a <canvas>. Requirements: - A grid of at least 20×20 cells, centered on the page. - Arrow keys and WASD control the snake. Quick successive key presses must both be honored, but the snake can never reverse onto itself. - Eating food adds 10 points and grows the snake by one cell; new food never spawns on the snake. - Game over when the snake hits a wall or itself, with an overlay showing the score; Space or Enter restarts, P pauses. - Visual polish: smooth movement, a snake with a distinct head and eyes facing its direction, and a small particle burst when eating. - Display the current score and the best score of the session.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 1826 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Snake — Arcade</title>
<style>
:root {
color-scheme: dark;
--bg: #07100d;
--panel: #0d1713;
--panel-soft: #111d18;
--line: rgba(168, 208, 179, .13);
--text: #e9f2e9;
--muted: #82968a;
--soft: #b2c3b6;
--lime: #baf27b;
--mint: #78ddb0;
--orange: #ff9a66;
}
* { box-sizing: border-box; }
html, body {
min-height: 100%;
margin: 0;
}
body {
min-height: 100vh;
overflow-x: hidden;
color: var(--text);
background:
radial-gradient(ellipse at 50% -20%, rgba(74, 147, 103, .16), transparent 54%),
radial-gradient(ellipse at 100% 72%, rgba(29, 87, 67, .12), transparent 38%),
var(--bg);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
}
body::before {
position: fixed;
z-index: 0;
inset: 0;
content: "";
pointer-events: none;
opacity: .18;
background-image:
linear-gradient(rgba(184, 225, 195, .055) 1px, transparent 1px),
linear-gradient(90deg, rgba(184, 225, 195, .055) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: linear-gradient(to bottom, black, transparent 88%);
}
.page-shell {
position: relative;
z-index: 1;
width: min(880px, calc(100% - 40px));
margin: 0 auto;
padding: 22px 0 18px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 12px;
border-bottom: 1px solid rgba(176, 214, 187, .12);
}
.brand {
display: flex;
align-items: center;
gap: 11px;
min-width: 0;
}
.brand-mark {
display: grid;
width: 34px;
height: 34px;
flex: 0 0 34px;
place-items: center;
border: 1px solid rgba(186, 242, 123, .25);
border-radius: 11px;
background: linear-gradient(145deg, rgba(146, 205, 113, .18), rgba(66, 126, 91, .08));
box-shadow: inset 0 1px 0 rgba(255,255,255,.06), 0 5px 20px rgba(0,0,0,.18);
}
.brand-mark svg { width: 26px; height: 26px; }
.brand-name {
color: #f0f6ed;
font-size: 14px;
font-weight: 800;
letter-spacing: .19em;
line-height: 1.1;
}
.brand-name span { color: var(--lime); }
.brand-subtitle {
margin-top: 4px;
color: #789083;
font-size: 8px;
font-weight: 700;
letter-spacing: .19em;
}
.top-right {
display: flex;
align-items: center;
gap: 12px;
color: #789083;
font-size: 9px;
font-weight: 750;
letter-spacing: .15em;
white-space: nowrap;
}
.top-status {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 7px 10px;
border: 1px solid rgba(138, 205, 151, .16);
border-radius: 999px;
background: rgba(18, 38, 27, .54);
color: #a6dca7;
font-size: 8px;
letter-spacing: .13em;
transition: color .2s ease, border-color .2s ease;
}
.top-status .tiny-dot,
.phase-dot,
.beacon {
position: relative;
display: inline-block;
width: 6px;
height: 6px;
flex: 0 0 6px;
border-radius: 50%;
background: #91e58d;
box-shadow: 0 0 10px rgba(145, 229, 141, .7);
}
.top-status.phase-playing { color: #c6ee91; border-color: rgba(186, 242, 123, .27); }
.top-status.phase-paused { color: #e6c780; border-color: rgba(230, 199, 128, .24); }
.top-status.phase-over { color: #e59682; border-color: rgba(229, 150, 130, .23); }
.top-status.phase-paused .tiny-dot { background: #e5c278; box-shadow: 0 0 9px rgba(229,194,120,.55); }
.top-status.phase-over .tiny-dot { background: #e88770; box-shadow: 0 0 9px rgba(232,135,112,.55); }
.top-divider {
width: 1px;
height: 17px;
background: rgba(176, 214, 187, .15);
}
.session-label { color: #7d9184; }
.game-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 248px;
align-items: stretch;
gap: 18px;
margin-top: 16px;
}
.arena-card {
min-width: 0;
padding: 14px;
border: 1px solid rgba(157, 199, 169, .15);
border-radius: 20px;
background:
linear-gradient(145deg, rgba(17, 30, 23, .96), rgba(9, 17, 14, .98));
box-shadow:
0 22px 62px rgba(0, 0, 0, .34),
inset 0 1px 0 rgba(255, 255, 255, .035);
}
.arena-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 18px;
margin-bottom: 10px;
}
.arena-ident {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.arena-index {
display: grid;
width: 19px;
height: 18px;
place-items: center;
border: 1px solid rgba(186, 242, 123, .22);
border-radius: 5px;
color: var(--lime);
background: rgba(186, 242, 123, .07);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 9px;
font-weight: 800;
}
.arena-label {
color: #c0d0c3;
font-size: 9px;
font-weight: 800;
letter-spacing: .16em;
white-space: nowrap;
}
.grid-label {
color: #75887c;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 9px;
letter-spacing: .08em;
white-space: nowrap;
}
.grid-label b {
margin-right: 4px;
color: #9eb6a4;
font-weight: 600;
}
.arena-phase {
display: inline-flex;
align-items: center;
gap: 6px;
color: #9ed69c;
font-size: 8px;
font-weight: 800;
letter-spacing: .12em;
white-space: nowrap;
}
.arena-phase.phase-playing { color: #c5ec88; }
.arena-phase.phase-paused { color: #e6c780; }
.arena-phase.phase-over { color: #e59682; }
.arena-phase.phase-paused .phase-dot {
background: #e5c278;
box-shadow: 0 0 8px rgba(229,194,120,.6);
}
.arena-phase.phase-over .phase-dot {
background: #e88770;
box-shadow: 0 0 8px rgba(232,135,112,.6);
}
.canvas-wrap {
position: relative;
width: 100%;
aspect-ratio: 1;
overflow: hidden;
border: 1px solid rgba(139, 188, 151, .16);
border-radius: 13px;
background: #09130f;
box-shadow:
inset 0 0 0 1px rgba(0, 0, 0, .26),
0 8px 22px rgba(0, 0, 0, .2);
}
.canvas-wrap.is-startable { cursor: pointer; }
#gameCanvas {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
}
.overlay {
position: absolute;
z-index: 2;
inset: 0;
display: grid;
place-items: center;
padding: 16px;
background: rgba(4, 10, 7, .29);
backdrop-filter: blur(1.1px);
-webkit-backdrop-filter: blur(1.1px);
}
.overlay[hidden] { display: none; }
.prompt-card {
width: min(330px, calc(100% - 8px));
padding: 17px 20px 15px;
border: 1px solid rgba(181, 225, 174, .25);
border-radius: 17px;
background:
linear-gradient(145deg, rgba(15, 31, 23, .94), rgba(8, 18, 13, .96));
box-shadow:
0 16px 48px rgba(0, 0, 0, .4),
inset 0 1px 0 rgba(255,255,255,.06),
0 0 36px rgba(105, 184, 113, .08);
text-align: center;
animation: card-in .38s cubic-bezier(.2,.8,.25,1) both;
}
@keyframes card-in {
from { opacity: 0; transform: translateY(8px) scale(.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.prompt-kicker {
display: inline-flex;
align-items: center;
gap: 7px;
color: #a2d8a0;
font-size: 8px;
font-weight: 850;
letter-spacing: .18em;
}
.prompt-kicker .tiny-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--lime);
box-shadow: 0 0 10px rgba(186, 242, 123, .75);
animation: blink 1.5s ease-in-out infinite;
}
@keyframes blink {
50% { opacity: .4; box-shadow: 0 0 3px rgba(186, 242, 123, .4); }
}
.prompt-card h2 {
margin: 10px 0 5px;
color: #f0f7e8;
font-size: 22px;
font-weight: 750;
letter-spacing: -.045em;
line-height: 1.1;
}
.prompt-main {
margin: 0;
color: #c3d1c4;
font-size: 11px;
font-weight: 600;
letter-spacing: .01em;
}
.prompt-foot {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 13px;
padding-top: 11px;
border-top: 1px solid rgba(179, 214, 183, .12);
color: #7f9485;
font-size: 8px;
font-weight: 750;
letter-spacing: .12em;
}
.prompt-foot .prompt-arrow {
color: var(--lime);
font-size: 13px;
line-height: 1;
}
.final-score {
display: flex;
align-items: center;
justify-content: space-between;
margin: 13px 0 8px;
padding: 9px 12px;
border: 1px solid rgba(186, 242, 123, .15);
border-radius: 10px;
background: rgba(186, 242, 123, .055);
}
.final-score span {
color: #91a496;
font-size: 8px;
font-weight: 800;
letter-spacing: .16em;
}
.final-score strong {
color: var(--lime);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 22px;
font-weight: 750;
letter-spacing: .05em;
font-variant-numeric: tabular-nums;
}
.record-line {
margin: 0 0 11px;
color: #9aab9d;
font-size: 10px;
}
.restart-hint {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
color: #87998c;
font-size: 8px;
font-weight: 700;
letter-spacing: .04em;
}
kbd {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
border: 1px solid rgba(187, 211, 190, .19);
border-radius: 5px;
background: linear-gradient(180deg, rgba(40, 56, 44, .9), rgba(23, 35, 27, .95));
box-shadow: 0 2px 0 rgba(0, 0, 0, .28);
color: #c5d5c6;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 9px;
font-weight: 750;
line-height: 1;
}
.arena-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 14px;
margin-top: 9px;
color: #788b7e;
font-size: 8px;
font-weight: 750;
letter-spacing: .1em;
}
.foot-left {
display: inline-flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.foot-left .food-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--orange);
box-shadow: 0 0 7px rgba(255, 154, 102, .56);
}
.foot-divider {
width: 1px;
height: 10px;
margin: 0 1px;
background: rgba(170, 200, 175, .2);
}
.foot-accent { color: #b9d99f; }
.foot-right { color: #61766a; white-space: nowrap; }
.side-panel {
display: flex;
min-width: 0;
flex-direction: column;
justify-content: center;
gap: 11px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
}
.stat-card {
min-width: 0;
min-height: 105px;
padding: 13px 12px 11px;
border: 1px solid rgba(165, 200, 173, .13);
border-radius: 15px;
background: linear-gradient(145deg, rgba(18, 31, 24, .96), rgba(11, 20, 16, .98));
box-shadow: inset 0 1px 0 rgba(255,255,255,.025);
}
.score-card {
border-color: rgba(177, 227, 133, .22);
background:
radial-gradient(ellipse at 85% 0%, rgba(141, 196, 99, .11), transparent 58%),
linear-gradient(145deg, rgba(22, 37, 26, .98), rgba(11, 20, 16, .98));
}
.stat-head {
display: flex;
align-items: center;
justify-content: space-between;
color: #8ba091;
font-size: 8px;
font-weight: 850;
letter-spacing: .14em;
white-space: nowrap;
}
.stat-symbol {
display: grid;
width: 17px;
height: 17px;
place-items: center;
border: 1px solid rgba(186, 242, 123, .19);
border-radius: 6px;
color: var(--lime);
background: rgba(186, 242, 123, .07);
font-size: 10px;
}
.best-card .stat-symbol {
color: #e5c77a;
border-color: rgba(229, 199, 122, .2);
background: rgba(229, 199, 122, .065);
}
.stat-number {
margin-top: 9px;
color: #f0f6ed;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 28px;
font-weight: 750;
letter-spacing: -.045em;
line-height: .95;
font-variant-numeric: tabular-nums;
}
.score-card .stat-number { color: #d8f9a8; }
.stat-foot {
display: flex;
align-items: center;
gap: 5px;
margin-top: 8px;
color: #6f8577;
font-size: 7px;
font-weight: 800;
letter-spacing: .1em;
white-space: nowrap;
}
.stat-foot .accent { color: #a8d17b; }
.status-card,
.controls-card {
border: 1px solid rgba(165, 200, 173, .13);
border-radius: 15px;
background: linear-gradient(145deg, rgba(17, 29, 23, .96), rgba(10, 19, 15, .98));
box-shadow: inset 0 1px 0 rgba(255,255,255,.025);
}
.status-card { padding: 14px 14px 12px; }
.card-topline,
.controls-topline {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: #819588;
font-size: 8px;
font-weight: 850;
letter-spacing: .15em;
}
.status-serial,
.controls-serial {
color: #5f7568;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 8px;
letter-spacing: .06em;
}
.status-main {
display: flex;
align-items: center;
gap: 8px;
margin-top: 13px;
color: #d8ead8;
font-size: 12px;
font-weight: 800;
letter-spacing: .08em;
}
.beacon { width: 7px; height: 7px; flex-basis: 7px; }
.phase-paused .beacon {
background: #e5c278;
box-shadow: 0 0 9px rgba(229, 194, 120, .55);
}
.phase-over .beacon {
background: #e88770;
box-shadow: 0 0 9px rgba(232, 135, 112, .55);
}
.status-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 8px;
}
.status-caption {
color: #72877a;
font-size: 9px;
line-height: 1.4;
}
.signal-bars {
display: flex;
align-items: end;
gap: 3px;
height: 14px;
flex: 0 0 auto;
opacity: .65;
}
.signal-bars i {
display: block;
width: 3px;
border-radius: 3px;
background: #72bd84;
}
.signal-bars i:nth-child(1) { height: 5px; }
.signal-bars i:nth-child(2) { height: 9px; }
.signal-bars i:nth-child(3) { height: 7px; }
.signal-bars i:nth-child(4) { height: 13px; }
.signal-bars i:nth-child(5) { height: 9px; }
.controls-card { padding: 14px 13px 13px; }
.controls-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 13px;
}
.steer-wrap {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.arrow-grid {
display: grid;
grid-template-columns: repeat(3, 19px);
grid-template-rows: repeat(2, 19px);
gap: 3px;
flex: 0 0 auto;
}
.arrow-grid kbd {
width: 19px;
height: 19px;
min-width: 0;
padding: 0;
border-radius: 5px;
font-size: 10px;
}
.arrow-grid .key-up { grid-column: 2; grid-row: 1; }
.arrow-grid .key-left { grid-column: 1; grid-row: 2; }
.arrow-grid .key-down { grid-column: 2; grid-row: 2; }
.arrow-grid .key-right { grid-column: 3; grid-row: 2; }
.steer-copy { min-width: 0; }
.steer-copy strong {
display: block;
margin-bottom: 4px;
color: #b8c9bb;
font-size: 8px;
font-weight: 850;
letter-spacing: .11em;
}
.steer-copy span {
display: block;
color: #74887a;
font-size: 7px;
font-weight: 750;
line-height: 1.55;
letter-spacing: .08em;
white-space: nowrap;
}
.action-list {
display: flex;
flex-direction: column;
gap: 7px;
min-width: 0;
padding-left: 8px;
border-left: 1px solid rgba(171, 204, 178, .11);
}
.action-item {
display: flex;
align-items: center;
gap: 5px;
min-width: 0;
color: #819487;
font-size: 7px;
font-weight: 800;
letter-spacing: .08em;
white-space: nowrap;
}
.action-item kbd {
height: 19px;
min-width: 20px;
padding: 0 5px;
font-size: 8px;
}
.action-item .wide-key {
padding: 0 5px;
font-size: 6px;
letter-spacing: -.02em;
}
.side-note {
display: flex;
align-items: center;
gap: 8px;
padding: 2px 3px 0;
color: #64796c;
font-size: 8px;
font-weight: 750;
letter-spacing: .06em;
}
.side-note span:first-child {
color: #9dc680;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.side-note::before {
width: 13px;
height: 1px;
content: "";
background: rgba(157, 198, 128, .55);
}
@media (max-width: 760px) {
.page-shell {
width: calc(100% - 28px);
padding: 13px 0 15px;
}
.topbar { padding-bottom: 10px; }
.brand { gap: 9px; }
.brand-mark { width: 32px; height: 32px; flex-basis: 32px; }
.brand-mark svg { width: 24px; height: 24px; }
.brand-name { font-size: 13px; }
.brand-subtitle { font-size: 7px; }
.top-right { gap: 8px; font-size: 8px; }
.top-status { padding: 6px 8px; font-size: 7px; }
.session-label { display: none; }
.top-divider { display: none; }
.game-layout {
grid-template-columns: minmax(0, 1fr);
gap: 10px;
margin-top: 11px;
}
.arena-card {
padding: 10px 10px 9px;
border-radius: 17px;
}
.arena-head { margin-bottom: 8px; }
.arena-label { font-size: 8px; }
.arena-phase { font-size: 7px; }
.grid-label { font-size: 8px; }
.canvas-wrap { border-radius: 11px; }
.arena-foot { margin-top: 8px; font-size: 7px; }
.side-panel {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-content: start;
gap: 8px;
}
.stats-grid {
grid-column: 1 / -1;
gap: 8px;
}
.stat-card {
min-height: 76px;
padding: 9px 11px 8px;
border-radius: 12px;
}
.stat-head { font-size: 7px; }
.stat-symbol { width: 15px; height: 15px; font-size: 9px; }
.stat-number { margin-top: 6px; font-size: 24px; }
.stat-foot { margin-top: 5px; font-size: 6px; }
.status-card {
display: grid;
grid-column: 1 / -1;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 4px 10px;
padding: 9px 12px;
border-radius: 12px;
}
.card-topline { grid-column: 1; font-size: 7px; }
.status-main { grid-column: 1; margin-top: 2px; font-size: 10px; }
.status-main .beacon { width: 6px; height: 6px; flex-basis: 6px; }
.status-bottom {
grid-column: 2;
grid-row: 1 / span 2;
justify-content: flex-end;
margin-top: 0;
}
.status-caption {
max-width: 120px;
text-align: right;
font-size: 8px;
}
.signal-bars { display: none; }
.controls-card {
grid-column: 1 / -1;
padding: 10px 12px 11px;
border-radius: 12px;
}
.controls-topline { font-size: 7px; }
.controls-content { justify-content: flex-start; gap: 14px; margin-top: 9px; }
.steer-wrap { gap: 8px; }
.arrow-grid {
grid-template-columns: repeat(3, 18px);
grid-template-rows: repeat(2, 18px);
gap: 2px;
}
.arrow-grid kbd { width: 18px; height: 18px; font-size: 9px; }
.steer-copy strong { font-size: 7px; margin-bottom: 2px; }
.steer-copy span { font-size: 6px; }
.action-list {
flex-direction: row;
gap: 10px;
padding-left: 11px;
}
.action-item { gap: 4px; font-size: 6px; }
.action-item kbd { height: 18px; min-width: 18px; font-size: 7px; }
.action-item .wide-key { font-size: 5.5px; }
.side-note { display: none; }
.prompt-card { padding: 15px 15px 13px; }
.prompt-card h2 { font-size: 20px; }
.prompt-main { font-size: 10px; }
}
@media (max-width: 390px) {
.page-shell { width: calc(100% - 24px); }
.top-status { gap: 5px; padding: 6px 7px; }
.arena-card { padding-right: 9px; padding-left: 9px; }
.controls-content { gap: 10px; }
.action-list { gap: 7px; padding-left: 8px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
scroll-behavior: auto !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}
</style>
</head>
<body>
<main class="page-shell">
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 32 32" fill="none">
<path d="M7 9.5h8v6.5h8v7.5h-8V17H7V9.5Z" fill="#B9F17A"/>
<path d="M7 9.5h8v6.5h8" stroke="#E7FFC5" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" opacity=".75"/>
<circle cx="23.1" cy="23.5" r="1.45" fill="#173322"/>
<circle cx="23.1" cy="23.5" r=".55" fill="#F1FFD9"/>
</svg>
</div>
<div>
<div class="brand-name">SNAKE<span>.</span></div>
<div class="brand-subtitle">ARCADE / FIELD 01</div>
</div>
</div>
<div class="top-right">
<div class="top-status phase-demo" id="headerStatus">
<span class="tiny-dot"></span>
<span id="headerMode">DEMO LIVE</span>
</div>
<span class="top-divider"></span>
<span class="session-label">SESSION 01</span>
</div>
</header>
<div class="game-layout">
<section class="arena-card" aria-label="Snake game">
<div class="arena-head">
<div class="arena-ident">
<span class="arena-index">01</span>
<span class="arena-label">PLAY FIELD</span>
</div>
<span class="grid-label"><b>+</b>24 × 24</span>
<span class="arena-phase phase-demo" id="arenaPhase">
<span class="phase-dot"></span><span>DEMO LIVE</span>
</span>
</div>
<div class="canvas-wrap is-startable" id="boardSurface">
<canvas id="gameCanvas" aria-label="Snake game board, 24 by 24 grid"></canvas>
<div class="overlay" id="overlay" aria-live="polite"></div>
</div>
<div class="arena-foot">
<span class="foot-left">
<span class="food-dot"></span>
<span>COLLECT TO GROW</span>
<span class="foot-divider"></span>
<span class="foot-accent">+10 PTS</span>
</span>
<span class="foot-right">24 × 24 GRID</span>
</div>
</section>
<aside class="side-panel" aria-label="Score and controls">
<div class="stats-grid">
<section class="stat-card score-card" aria-label="Current score">
<div class="stat-head">
<span>SCORE</span>
<span class="stat-symbol">+</span>
</div>
<div class="stat-number" id="scoreValue">000</div>
<div class="stat-foot"><span class="accent">+10</span><span>PER FOOD</span></div>
</section>
<section class="stat-card best-card" aria-label="Best score this session">
<div class="stat-head">
<span>SESSION BEST</span>
<span class="stat-symbol">✦</span>
</div>
<div class="stat-number" id="bestValue">000</div>
<div class="stat-foot"><span>THIS SESSION</span></div>
</section>
</div>
<section class="status-card" aria-label="Round status">
<div class="card-topline">
<span>ROUND STATUS</span>
<span class="status-serial">FIELD / 01</span>
</div>
<div class="status-main">
<span class="beacon"></span>
<span id="modeText">AUTOPILOT</span>
</div>
<div class="status-bottom">
<span class="status-caption" id="modeCaption">AI is mapping the next bite.</span>
<span class="signal-bars" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span>
</div>
</section>
<section class="controls-card" aria-label="Controls">
<div class="controls-topline">
<span>QUICK CONTROLS</span>
<span class="controls-serial">INPUT / 02</span>
</div>
<div class="controls-content">
<div class="steer-wrap">
<div class="arrow-grid" aria-hidden="true">
<kbd class="key-up">↑</kbd>
<kbd class="key-left">←</kbd>
<kbd class="key-down">↓</kbd>
<kbd class="key-right">→</kbd>
</div>
<div class="steer-copy">
<strong>STEER</strong>
<span>ARROWS<br>OR WASD</span>
</div>
</div>
<div class="action-list">
<div class="action-item"><kbd>P</kbd><span>PAUSE</span></div>
<div class="action-item"><kbd class="wide-key">SPACE / ENTER</kbd><span>RESTART</span></div>
</div>
</div>
</section>
<div class="side-note"><span>01</span><span>Find the food. Stay in bounds.</span></div>
</aside>
</div>
</main>
<script>
(() => {
"use strict";
const GRID = 24;
const canvas = document.getElementById("gameCanvas");
const boardSurface = document.getElementById("boardSurface");
const ctx = canvas.getContext("2d", { alpha: false });
const scoreValue = document.getElementById("scoreValue");
const bestValue = document.getElementById("bestValue");
const overlay = document.getElementById("overlay");
const headerStatus = document.getElementById("headerStatus");
const headerMode = document.getElementById("headerMode");
const arenaPhase = document.getElementById("arenaPhase");
const modeText = document.getElementById("modeText");
const modeCaption = document.getElementById("modeCaption");
const UP = Object.freeze({ x: 0, y: -1 });
const RIGHT = Object.freeze({ x: 1, y: 0 });
const DOWN = Object.freeze({ x: 0, y: 1 });
const LEFT = Object.freeze({ x: -1, y: 0 });
const DIRECTIONS = [UP, RIGHT, DOWN, LEFT];
const keyDirections = {
arrowup: UP,
w: UP,
arrowright: RIGHT,
d: RIGHT,
arrowdown: DOWN,
s: DOWN,
arrowleft: LEFT,
a: LEFT
};
let snake = [];
let previousSnake = [];
let direction = RIGHT;
let food = null;
let score = 0;
let bestScore = 0;
let turnQueue = [];
let phase = "demo";
let pausedFrom = "playing";
let endReason = "";
let endedByDemo = false;
let demoRestartAt = 0;
let accumulator = 0;
let lastFrameTime = 0;
let particles = [];
let boardWidth = 0;
let boardHeight = 0;
let cellWidth = 0;
let cellHeight = 0;
function copyCells(cells) {
return cells.map(cell => ({ x: cell.x, y: cell.y }));
}
function setupWorld() {
snake = [
{ x: 7, y: 18 },
{ x: 6, y: 18 },
{ x: 5, y: 18 },
{ x: 4, y: 18 }
];
previousSnake = copyCells(snake);
direction = RIGHT;
turnQueue = [];
food = { x: 14, y: 18 };
score = 0;
particles = [];
endReason = "";
accumulator = 0;
}
function formatScore(value) {
return String(value).padStart(3, "0");
}
function syncScores() {
scoreValue.textContent = formatScore(score);
bestValue.textContent = formatScore(bestScore);
}
function phaseInfo() {
switch (phase) {
case "demo":
return {
header: "DEMO LIVE",
arena: "DEMO LIVE",
status: "AUTOPILOT",
caption: "AI is mapping the next bite."
};
case "playing":
return {
header: "PLAYER RUN",
arena: "PLAYER LIVE",
status: "YOUR RUN",
caption: "One turn at a time. Stay sharp."
};
case "paused":
return {
header: "PAUSED",
arena: "PAUSED",
status: "ON HOLD",
caption: "Press P to pick up the run."
};
default:
return {
header: "ROUND ENDED",
arena: "GAME OVER",
status: "RUN ENDED",
caption: "Space or Enter to play again."
};
}
}
function syncModeUI() {
const info = phaseInfo();
headerMode.textContent = info.header;
headerStatus.className = "top-status phase-" + phase;
arenaPhase.className = "arena-phase phase-" + phase;
arenaPhase.innerHTML = '<span class="phase-dot"></span><span>' + info.arena + '</span>';
modeText.textContent = info.status;
modeCaption.textContent = phase === "over" && endReason
? endReason === "wall" ? "The edge got you this time."
: endReason === "self" ? "You crossed your own trail."
: "A perfect clear. Nicely played."
: info.caption;
boardSurface.classList.toggle("is-startable", phase !== "playing");
}
function renderOverlay() {
if (phase === "playing") {
overlay.hidden = true;
overlay.innerHTML = "";
return;
}
overlay.hidden = false;
if (phase === "demo") {
overlay.innerHTML = `
<div class="prompt-card">
<div class="prompt-kicker"><span class="tiny-dot"></span><span>AUTOPILOT / LIVE DEMO</span></div>
<h2>Take the wheel.</h2>
<p class="prompt-main">Click or press a key to start</p>
<div class="prompt-foot"><span class="prompt-arrow">↗</span><span>ARROWS OR WASD TO STEER</span></div>
</div>`;
return;
}
if (phase === "paused") {
const fromDemo = pausedFrom === "demo";
overlay.innerHTML = `
<div class="prompt-card">
<div class="prompt-kicker"><span class="tiny-dot"></span><span>${fromDemo ? "DEMO PAUSED" : "ROUND PAUSED"}</span></div>
<h2>${fromDemo ? "Ready when you are." : "Take a breath."}</h2>
<p class="prompt-main">${fromDemo ? "P to resume · any other key to play" : "Press P or click the field to resume"}</p>
<div class="prompt-foot"><span class="prompt-arrow">Ⅱ</span><span>YOUR RUN IS ON HOLD</span></div>
</div>`;
return;
}
const title = endReason === "wall"
? "Out of bounds."
: endReason === "self"
? "Trail crossed."
: "Perfect clear.";
overlay.innerHTML = `
<div class="prompt-card">
<div class="prompt-kicker"><span class="tiny-dot"></span><span>${endReason === "clear" ? "FIELD CLEARED" : "RUN COMPLETE"}</span></div>
<h2>${title}</h2>
<div class="final-score"><span>FINAL SCORE</span><strong>${formatScore(score)}</strong></div>
<p class="record-line">Session best <strong>${formatScore(bestScore)}</strong></p>
<div class="restart-hint"><kbd>SPACE</kbd><span>or</span><kbd>ENTER</kbd><span>to restart</span></div>
</div>`;
}
function setPhase(nextPhase) {
phase = nextPhase;
syncModeUI();
renderOverlay();
}
function startGame(firstDirection) {
demoRestartAt = 0;
endedByDemo = false;
setupWorld();
syncScores();
setPhase("playing");
if (firstDirection) queueDirection(firstDirection);
}
function queueDirection(nextDirection) {
const plannedDirection = turnQueue.length
? turnQueue[turnQueue.length - 1]
: direction;
if (
nextDirection.x === plannedDirection.x &&
nextDirection.y === plannedDirection.y
) return;
if (
nextDirection.x === -plannedDirection.x &&
nextDirection.y === -plannedDirection.y
) return;
if (turnQueue.length < 3) {
turnQueue.push(nextDirection);
}
}
function isOpposite(a, b) {
return a.x === -b.x && a.y === -b.y;
}
function cellKey(x, y) {
return x + "," + y;
}
function spawnFood() {
const occupied = new Uint8Array(GRID * GRID);
for (const part of snake) occupied[part.y * GRID + part.x] = 1;
const openCells = [];
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if (!occupied[y * GRID + x]) openCells.push({ x, y });
}
}
if (openCells.length === 0) return null;
return openCells[Math.floor(Math.random() * openCells.length)];
}
function reachableArea(startX, startY, blocked) {
const queue = [{ x: startX, y: startY }];
const visited = new Set([cellKey(startX, startY)]);
let count = 0;
for (let index = 0; index < queue.length; index++) {
const current = queue[index];
count++;
for (const move of DIRECTIONS) {
const x = current.x + move.x;
const y = current.y + move.y;
const key = cellKey(x, y);
if (
x < 0 || x >= GRID || y < 0 || y >= GRID ||
visited.has(key) || blocked.has(key)
) continue;
visited.add(key);
queue.push({ x, y });
}
}
return count;
}
function safeAIDirection() {
const head = snake[0];
const blocked = new Set(
snake.slice(0, -1).map(part => cellKey(part.x, part.y))
);
const options = [];
for (const move of DIRECTIONS) {
if (isOpposite(move, direction)) continue;
const x = head.x + move.x;
const y = head.y + move.y;
const key = cellKey(x, y);
if (x < 0 || x >= GRID || y < 0 || y >= GRID || blocked.has(key)) continue;
options.push({
move,
area: reachableArea(x, y, blocked),
forward: move.x === direction.x && move.y === direction.y
});
}
options.sort((a, b) => b.area - a.area || Number(b.forward) - Number(a.forward));
return options.length ? options[0].move : direction;
}
function getAIDirection() {
if (!food || snake.length === 0) return direction;
const head = snake[0];
const blocked = new Set(
snake.slice(0, -1).map(part => cellKey(part.x, part.y))
);
const queue = [{ x: head.x, y: head.y, first: null }];
const visited = new Set([cellKey(head.x, head.y)]);
for (let index = 0; index < queue.length; index++) {
const node = queue[index];
if (node.x === food.x && node.y === food.y && node.first) return node.first;
for (const move of DIRECTIONS) {
if (!node.first && isOpposite(move, direction)) continue;
const x = node.x + move.x;
const y = node.y + move.y;
const key = cellKey(x, y);
if (
x < 0 || x >= GRID || y < 0 || y >= GRID ||
visited.has(key) || blocked.has(key)
) continue;
visited.add(key);
queue.push({
x,
y,
first: node.first || move
});
}
}
return safeAIDirection();
}
function spawnBurst(cell) {
const colors = ["#fff1c5", "#ffc17d", "#ff9464", "#b9f27d"];
for (let i = 0; i < 15; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = 1.0 + Math.random() * 3.8;
const life = .35 + Math.random() * .32;
particles.push({
x: cell.x + .5,
y: cell.y + .5,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 0,
maxLife: life,
radius: .045 + Math.random() * .04,
color: colors[i % colors.length]
});
}
}
function finishGame(reason) {
if (phase !== "demo" && phase !== "playing") return;
endedByDemo = phase === "demo";
endReason = reason;
previousSnake = copyCells(snake);
accumulator = 0;
demoRestartAt = endedByDemo ? performance.now() + 1700 : 0;
setPhase("over");
}
function stepGame() {
if (phase !== "demo" && phase !== "playing") return;
previousSnake = copyCells(snake);
let nextDirection;
if (phase === "demo") {
nextDirection = getAIDirection();
} else {
nextDirection = turnQueue.length ? turnQueue.shift() : direction;
if (isOpposite(nextDirection, direction)) nextDirection = direction;
}
direction = nextDirection;
const head = snake[0];
const nextHead = {
x: head.x + direction.x,
y: head.y + direction.y
};
if (
nextHead.x < 0 || nextHead.x >= GRID ||
nextHead.y < 0 || nextHead.y >= GRID
) {
finishGame("wall");
return;
}
const willEat = food && nextHead.x === food.x && nextHead.y === food.y;
const bodyToCheck = willEat ? snake : snake.slice(0, -1);
if (bodyToCheck.some(part => part.x === nextHead.x && part.y === nextHead.y)) {
finishGame("self");
return;
}
snake.unshift(nextHead);
if (willEat) {
const eatenCell = food;
spawnBurst(eatenCell);
score += 10;
bestScore = Math.max(bestScore, score);
food = spawnFood();
syncScores();
if (!food) {
finishGame("clear");
return;
}
} else {
snake.pop();
}
}
function stepDuration() {
return Math.max(82, 142 - score * .22);
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const particle = particles[i];
particle.life += dt;
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
particle.vx *= Math.max(0, 1 - dt * 1.5);
particle.vy *= Math.max(0, 1 - dt * 1.5);
if (particle.life >= particle.maxLife) particles.splice(i, 1);
}
}
function roundedRectPath(context, x, y, width, height, radius) {
const r = Math.min(radius, width / 2, height / 2);
context.beginPath();
context.moveTo(x + r, y);
context.arcTo(x + width, y, x + width, y + height, r);
context.arcTo(x + width, y + height, x, y + height, r);
context.arcTo(x, y + height, x, y, r);
context.arcTo(x, y, x + width, y, r);
context.closePath();
}
function drawFood(now) {
if (!food) return;
const x = (food.x + .5) * cellWidth;
const y = (food.y + .5) * cellHeight;
const cell = Math.min(cellWidth, cellHeight);
const pulse = .5 + .5 * Math.sin(now * .0042);
const glowRadius = cell * (.78 + pulse * .12);
const glow = ctx.createRadialGradient(x, y, 0, x, y, glowRadius);
glow.addColorStop(0, "rgba(255, 147, 91, .26)");
glow.addColorStop(.42, "rgba(255, 147, 91, .10)");
glow.addColorStop(1, "rgba(255, 147, 91, 0)");
ctx.fillStyle = glow;
ctx.fillRect(x - glowRadius, y - glowRadius, glowRadius * 2, glowRadius * 2);
ctx.save();
ctx.beginPath();
ctx.arc(x, y, cell * (.31 + pulse * .035), 0, Math.PI * 2);
ctx.strokeStyle = "rgba(255, 183, 129, " + (.25 + pulse * .18) + ")";
ctx.lineWidth = Math.max(1, cell * .035);
ctx.stroke();
ctx.shadowColor = "rgba(255, 133, 82, .72)";
ctx.shadowBlur = cell * .34;
ctx.beginPath();
ctx.arc(x, y, cell * .205, 0, Math.PI * 2);
const fruit = ctx.createRadialGradient(
x - cell * .07, y - cell * .09, cell * .02,
x, y, cell * .24
);
fruit.addColorStop(0, "#ffd19a");
fruit.addColorStop(.38, "#ffac72");
fruit.addColorStop(1, "#ef704b");
ctx.fillStyle = fruit;
ctx.fill();
ctx.shadowBlur = 0;
ctx.beginPath();
ctx.ellipse(x + cell * .075, y - cell * .235, cell * .075, cell * .13, -.65, 0, Math.PI * 2);
ctx.fillStyle = "#a8dc79";
ctx.fill();
ctx.beginPath();
ctx.arc(x - cell * .07, y - cell * .075, cell * .045, 0, Math.PI * 2);
ctx.fillStyle = "rgba(255, 249, 217, .85)";
ctx.fill();
ctx.restore();
}
function drawSnake(alpha) {
if (!snake.length) return;
const cell = Math.min(cellWidth, cellHeight);
const inset = cell * .065;
const baseSize = cell - inset * 2;
function interpolatedPosition(index) {
const current = snake[index];
const previous = previousSnake[Math.min(index, previousSnake.length - 1)] || current;
return {
x: previous.x + (current.x - previous.x) * alpha,
y: previous.y + (current.y - previous.y) * alpha
};
}
for (let i = snake.length - 1; i >= 1; i--) {
const position = interpolatedPosition(i);
const isGrowingTail = i >= previousSnake.length;
const scale = isGrowingTail ? Math.max(.04, alpha) : 1;
const size = baseSize * scale;
const x = (position.x + .5) * cellWidth - size / 2;
const y = (position.y + .5) * cellHeight - size / 2;
const ratio = i / Math.max(1, snake.length - 1);
const hue = 108 + ratio * 48;
const lightness = 62 - ratio * 26;
ctx.save();
if (i < 4) {
ctx.shadowColor = "rgba(99, 224, 151, .25)";
ctx.shadowBlur = cell * .22;
}
roundedRectPath(ctx, x, y, size, size, size * .31);
ctx.fillStyle = "hsl(" + hue + ", 59%, " + lightness + "%)";
ctx.fill();
ctx.restore();
}
const headPosition = interpolatedPosition(0);
const headSize = baseSize * 1.015;
const headX = (headPosition.x + .5) * cellWidth - headSize / 2;
const headY = (headPosition.y + .5) * cellHeight - headSize / 2;
ctx.save();
ctx.shadowColor = "rgba(157, 245, 127, .5)";
ctx.shadowBlur = cell * .48;
roundedRectPath(ctx, headX, headY, headSize, headSize, headSize * .31);
const headGradient = ctx.createLinearGradient(headX, headY, headX + headSize, headY + headSize);
headGradient.addColorStop(0, "#e5ffb4");
headGradient.addColorStop(.48, "#bdf283");
headGradient.addColorStop(1, "#82d77c");
ctx.fillStyle = headGradient;
ctx.fill();
ctx.restore();
const centerX = (headPosition.x + .5) * cellWidth;
const centerY = (headPosition.y + .5) * cellHeight;
const perpendicular = { x: -direction.y, y: direction.x };
const forward = headSize * .145;
const spread = headSize * .215;
const eyeRadius = headSize * .087;
for (const side of [-1, 1]) {
const eyeX = centerX + direction.x * forward + perpendicular.x * spread * side;
const eyeY = centerY + direction.y * forward + perpendicular.y * spread * side;
ctx.beginPath();
ctx.arc(eyeX, eyeY, eyeRadius, 0, Math.PI * 2);
ctx.fillStyle = "#f5ffdf";
ctx.fill();
ctx.beginPath();
ctx.arc(
eyeX + direction.x * eyeRadius * .28,
eyeY + direction.y * eyeRadius * .28,
eyeRadius * .51,
0,
Math.PI * 2
);
ctx.fillStyle = "#183724";
ctx.fill();
}
}
function drawParticles() {
const cell = Math.min(cellWidth, cellHeight);
for (const particle of particles) {
const life = Math.max(0, 1 - particle.life / particle.maxLife);
const x = particle.x * cellWidth;
const y = particle.y * cellHeight;
const radius = cell * particle.radius * (.55 + life * .7);
ctx.save();
ctx.globalAlpha = life;
ctx.fillStyle = particle.color;
ctx.shadowColor = particle.color;
ctx.shadowBlur = cell * .14;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
function drawBoard(now) {
if (boardWidth < 1 || boardHeight < 1) return;
const width = boardWidth;
const height = boardHeight;
const cw = cellWidth;
const ch = cellHeight;
ctx.clearRect(0, 0, width, height);
const base = ctx.createLinearGradient(0, 0, width, height);
base.addColorStop(0, "#0b1712");
base.addColorStop(.52, "#0a1511");
base.addColorStop(1, "#09130f");
ctx.fillStyle = base;
ctx.fillRect(0, 0, width, height);
const ambient = ctx.createRadialGradient(width * .72, height * .27, 0, width * .72, height * .27, width * .82);
ambient.addColorStop(0, "rgba(82, 143, 91, .11)");
ambient.addColorStop(1, "rgba(30, 67, 46, 0)");
ctx.fillStyle = ambient;
ctx.fillRect(0, 0, width, height);
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if ((x + y) % 2 === 0) {
ctx.fillStyle = "rgba(168, 220, 176, .018)";
ctx.fillRect(x * cw, y * ch, cw, ch);
}
}
}
ctx.beginPath();
for (let i = 0; i <= GRID; i++) {
const x = i * cw;
const y = i * ch;
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(160, 210, 171, .045)";
ctx.stroke();
ctx.beginPath();
for (let i = 0; i <= GRID; i += 6) {
ctx.moveTo(i * cw, 0);
ctx.lineTo(i * cw, height);
ctx.moveTo(0, i * ch);
ctx.lineTo(width, i * ch);
}
ctx.lineWidth = 1;
ctx.strokeStyle = "rgba(169, 218, 174, .055)";
ctx.stroke();
const vignette = ctx.createRadialGradient(
width * .48, height * .46, Math.min(width, height) * .16,
width * .48, height * .46, Math.min(width, height) * .78
);
vignette.addColorStop(0, "rgba(0, 0, 0, 0)");
vignette.addColorStop(1, "rgba(0, 5, 3, .43)");
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, width, height);
drawFood(now);
const alpha = phase === "over"
? 0
: Math.max(0, Math.min(1, accumulator / stepDuration()));
drawSnake(alpha);
drawParticles();
ctx.strokeStyle = "rgba(178, 225, 179, .075)";
ctx.lineWidth = 1;
ctx.strokeRect(.5, .5, width - 1, height - 1);
}
function resizeCanvas() {
const rect = boardSurface.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const pixelWidth = Math.max(1, Math.round(rect.width * dpr));
const pixelHeight = Math.max(1, Math.round(rect.height * dpr));
if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
canvas.width = pixelWidth;
canvas.height = pixelHeight;
}
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
boardWidth = rect.width;
boardHeight = rect.height;
cellWidth = boardWidth / GRID;
cellHeight = boardHeight / GRID;
}
function animationFrame(now) {
if (!lastFrameTime) lastFrameTime = now;
const dt = Math.min(.05, Math.max(0, (now - lastFrameTime) / 1000));
lastFrameTime = now;
if (phase === "demo" || phase === "playing") {
accumulator += dt * 1000;
let steps = 0;
while (
accumulator >= stepDuration() &&
steps < 4 &&
(phase === "demo" || phase === "playing")
) {
accumulator -= stepDuration();
stepGame();
steps++;
}
} else if (phase === "over" && endedByDemo && now >= demoRestartAt) {
setupWorld();
syncScores();
endedByDemo = false;
setPhase("demo");
}
if (phase !== "paused" && phase !== "over") updateParticles(dt);
drawBoard(now);
requestAnimationFrame(animationFrame);
}
function togglePause() {
if (phase === "demo" || phase === "playing") {
pausedFrom = phase;
setPhase("paused");
} else if (phase === "paused") {
setPhase(pausedFrom);
}
}
function handleKeyDown(event) {
const key = event.key.toLowerCase();
const move = keyDirections[key];
const isSpace = event.code === "Space" || event.key === " ";
const isEnter = key === "enter";
if (move || isSpace) event.preventDefault();
if (key === "p") {
if (event.repeat) return;
togglePause();
return;
}
if (phase === "over") {
if ((isSpace || isEnter) && !event.repeat) startGame();
return;
}
if (phase === "paused") {
if (pausedFrom === "demo") {
if (move) startGame(move);
else if (isSpace || isEnter) startGame();
else startGame();
} else {
setPhase("playing");
if (move) queueDirection(move);
}
return;
}
if (phase === "demo") {
startGame(move || null);
return;
}
if (phase === "playing" && move) {
queueDirection(move);
}
}
boardSurface.addEventListener("click", () => {
if (phase === "demo" || phase === "over") {
startGame();
} else if (phase === "paused") {
if (pausedFrom === "demo") startGame();
else setPhase("playing");
}
});
document.addEventListener("keydown", handleKeyDown, { passive: false });
setupWorld();
syncScores();
syncModeUI();
renderOverlay();
resizeCanvas();
if ("ResizeObserver" in window) {
const observer = new ResizeObserver(resizeCanvas);
observer.observe(boardSurface);
} else {
window.addEventListener("resize", resizeCanvas, { passive: true });
}
requestAnimationFrame(animationFrame);
})();
</script>
</body>
</html>Reasoning trace (37,587 tokens)
**Designing demo snake game**
I need to create a demo where the snake moves and eats food, making sure the overlay message doesn't obscure the gameplay. I can show a translucent prompt at the top center and have the game run autonomously. The game should start in demo mode with a “Click or press a key to start” message. First, I need to choose a futuristic dark gradient design, and I'll go with a canvas board of at least 20x20 cells.**Creating canvas logic**
I need to draw in logical coordinates using `ctx.setTransform` with the device pixel ratio (DPR). The board grid is 24x24, and I have to compute each cell based on the board width logically. I’ll set the canvas actual width and height to match the CSS ratio. For resizing, I’ll use CSS size multiplied by DPR and adjust the rendering accordingly. I want a subtle grid and glow for the board background, and I need to draw a rounded rectangle border outside the canvas using CSS.**Designing Game Controls**
I need to create controls for the game interface. First, I want to show a score card that displays the current score prominently, maybe using a large "000" format starting with something like 30 for the demo. There should also be a card highlighting the best score. Then, I’ll include a status card saying "RUNNING / Autonomous warm-up".
The controls should be visible, with arrow keys for movement, and a pause key (P), plus a restart option with Space. I'll add a chip at the bottom indicating "Food +10 • Every bite adds length."
Lastly, to communicate that it's a demo, I'll use an overlay prompt guiding users to click or press any key to start the game. It’s essential that this overlay is clear and visible to users while being centered on the board.**Improving Demo Overlay**
I think I should upgrade the demo interface by adding a better top-center pill or even a full dim overlay featuring a centered card. This design would display the message "Click or press a key to start" over a screenshot of the game in action, demonstrating the self-playing AI.
The overlay should be translucent so users can still see the game moving behind it. After three seconds, I can take a screenshot of the AI controlling the snake, which should create a nice effect.**Resetting the game input**
I’m thinking about starting fresh to ensure a deterministic approach. If I don't select a direction at first, I could just go with the default. I want to ensure I keep track of both the current session and the best score from gameplay and demo sessions. The best score likely reflects my current session, but I wonder if the demo can update that best score before I play, setting a new baseline.**Evaluating game direction and controls**
I'm thinking about the game's initial orientation. If I set the starting direction to the left, it seems invalid since the standard is to face right. Pressing left shouldn't cause a reverse; I need to establish that the initial direction must be right. Now, regarding the pause function: if it's a demo, pressing 'P' should pause it and show a paused overlay. It's important to clarify these controls for a better gameplay experience.**Considering game controls**
I'm thinking about game controls, particularly how to handle starting and pausing. It seems like any key should start the game, and pressing 'P' could pause it. If the user presses 'P' while in demo mode, the game should start and pause immediately. I also want to ensure that the design is clear, maybe having an input sequence that manages these states effectively. Keeping it simple is the goal, and I should define a clear state machine with phases for 'demo', 'playing', 'paused', and 'over'.**Refining game controls in demo**
I'm considering how the 'P' key should work during the demo. If pressed, it could toggle a paused state and show an overlay saying "DEMO PAUSED / P to resume • Click or press a key to start." It’s also crucial to manage key mappings clearly. For instance, if the phase is 'demo', pressing 'P' should either start a new game in a paused state or pause the demo while allowing resumption. If already playing, 'P' should pause the game, with clear instructions on how to resume, minimizing confusion for the user.**Defining game phases**
I'm establishing the game phases. If in the demo phase, I should start the game and set the direction based on an arrow key. If the game is over, pressing Space or Enter should trigger a restart, with any arrow key also possibly restarting the game and queuing a direction. For the paused state, I’m considering allowing unpausing with any key related to gameplay, but primarily focusing on 'P' or Space, so it resumes and queues the direction properly.**Considering a design demo**
I'm thinking about a demo layout with a transparent background and a centered card that might use a backdrop-filter blur. It could have a minimum width of 72% and an auto height, with a slightly dim board. I envision using an overlay to darken the entire area with a linear-gradient effect. The overlay would have some blur, and I might include a heading like "TAKE THE WHEEL" for clarity. The message should be clearly visible too!**Visualizing game interface**
I’m thinking about how to design the game interface. It should have a header with the title "BUILT TO KEEP MOVING" and a game board panel featuring a 24 × 24 grid. I’d include a dynamic “DEMO RUNNING” indicator and use small toggles like “USE ARROWS / WASD TO STEER” at the bottom. Additionally, there are mini stats cards for scoring and best scores, along with a round status panel and visual keycaps for controls. This all seems very engaging!**Designing game layout**
I'm considering using keycaps for arrow keys and WASD, creating a grid for them labeled “ARROW KEYS / OR WASD.” There'll be a pause button and a restart option too! I should add a food card with an orange pellet icon and "+10 POINTS." The mobile layout needs to show stats and controls in a two-column grid. For the desktop layout, I want a good aspect ratio while ensuring content width fits nicely. I might limit the app’s max width to 1020 for a balanced view!**Calculating layout dimensions**
I'm figuring out the main width, which will be set at 1000. For the grid columns, I'm using a layout of `minmax(0, 1fr) 280px` with a gap of 22. This gives me a board panel width of 698. Once I add padding of 17 on each side for the board content, it leaves me with around 664 for the canvas. I want to ensure everything fits nicely and looks balanced within this space!**Creating a grid design**
I'm thinking about drawing a 24x24 grid, where each cell is 24 pixels. I want to use alternating fills with very subtle alpha effects to distinguish the cells. Oh, and I'll make the grid lines faint to keep the focus on the overall design without being too distracting. It’s all about balance and making sure it looks visually appealing while keeping the details subtle. Let's make this grid come to life!**Calculating food distance and speed**
I'm looking at the food distance of 6 units moving at a speed of 7 meters per second. That means I should be able to eat it within about 0.85 seconds. So, with that in mind, I need to ensure everything is in place to make this quick — and it's great to know that the overlay keeps functioning smoothly during the process! This sounds like a manageable task.**Considering gameplay controls**
I’m thinking about the key functions in gameplay. Pressing any key starts things without a problem, but I wonder if hitting "Enter" during gameplay could cause a reset—though it’s not necessarily required and might have no effect.
Also, the "P" key toggles something, but if I'm already over "P," does it not function? In demo mode, "P" might pause it, so I need to think through what overlay states are necessary.**Designing initial setup**
I need to choose initial cell positions within a 24x24 grid. For the snake, the head is at (5,18) and the body is at (4,18), (3,18), and (2,18), with food at (11,18). After about 6 steps, which takes roughly 0.85 seconds, the snake will be moving across the lower part of the grid. I’m planning to overlay the center at (12,12) while ensuring there's no occlusion, aside from the food at (11,18). That sounds nice!**Designing gameplay mechanics**
I'm thinking about how to set up the initial board for the game. Perhaps starting at coordinates x=10, y=12, or a lower left position would work well. If the user clicks, it could restart with demo positions, but I'm considering how the placement of food fits in that. Establishing consistent initial positions for the snake is important for a visual representation. Setting the grid size to a fixed dimension makes sense and should accommodate both desktop and mobile views. I want to ensure that click resets are intuitive.**Evaluating AI pathfinding**
I’m considering the AI's strategy for reaching food. Maybe I could have it start routing to a random location three seconds after the first food is eaten. The demo might show at least one score, with speed set at 128ms per tick, taking roughly 0.77 seconds to reach food. I think using BFS for the AI is solid, so I'll implement a function to determine the best direction by avoiding blocked paths, ensuring it doesn’t reverse direction, and constantly checking for safety as the game progresses.**Refining AI movement strategy**
I'm contemplating how the AI can target its food route through cells that may become its body later on. The BFS approach is quite conservative, as it avoids all parts of the snake except the tail. If the AI gets trapped with no available path, I could implement a fallback strategy that finds a safe movement option maximizing distance. I’ll use `safeDirection()` to check for non-reverse choices that avoid collisions with walls or the body, ensuring a forward preference when possible to steer clear of dead ends.**Clarifying demo controls**
I need to clarify the controls while in demo mode. When I press `P`, it pauses, and the overlay indicates "P to resume" and "Any key to take control." While paused, the overlay shows "PAUSED" and some prompts. If I want to resume, can I also use the arrow keys or space? The controls seem to need some organization. I also want to handle mouse clicks properly so that it doesn't interfere with the demo state. A dynamic header status could help, like switching between `LIVE DEMO`, `PLAYING`, and `PAUSED`.**Designing the initial page layout**
I’m thinking about the initial page and how to screenshot it effectively. I want to include the header logo on the right. The board should have a large, dark grid visible, with a bright snake positioned at the lower left and glowing food. Also, I need to add an overlay in the center that features an "AUTO-PILOT" badge along with a prompt like "Take the wheel." On the right side, a stats display for the score and best score should be present, making sure no text gets cut off by using a CSS overlay.**Refining score updates**
I’m thinking about using `padStart(3, '0')` to ensure a minimum length and handling scores above 999. The best update for the session should happen after eating, and I should label it as "SESSION BEST." The `userBest` variable only updates when there’s a new score, without resetting when a new game starts. I guess it would make sense to update the best during a demo too, but I want to ensure it reflects actual performance rather than inflating numbers. The highest score achieved in a session should be strictly recorded.**Clarifying score handling**
The user likely wants to see the actual score along with the session score. I can display the best score in both demo and actual modes. If the score exceeds the best, I’ll update it accordingly. This could result in best scores being shown as 010 or 020, which is nice to see after just 3 seconds of play. I also need to ensure that when the game starts, the score resets, but the best score should be retained.**Explaining snake movement rendering**
I'm working on rendering the snake's movements in a game. To achieve smooth motion, I'll track the previous snake position and interpolate between it and the new one using frame updates. I’ll calculate a value, `t`, that indicates how far along the interpolation is during each frame. After each simulation step, the visuals will take the previous positions into account. Although the movement might seem offset by one tick, it'll still appear smooth and consistent.**Handling snake logic and rendering**
I'm figuring out how to manage the snake's movement and rendering during gameplay. When the game ticks after 140ms, the logical state of the snake updates, but visually it stays in position momentarily before moving. I'm considering how to handle collisions effectively, especially when the game is paused. If I pause, I could reset the accumulator to zero, so the visuals reflect the previous positions accurately. When resuming, I should preserve the accumulator to prevent jumps in movement. Plus, I need to ensure that during a game over, the snake's state is correctly maintained.**Preserving snake interpolation**
I want to make sure I'm preserving the current position of the snake during interpolation. If the accumulator reaches the interval before an update, I'll have to set `t` to 1, indicating a collision or game over state. This means subtracting the interval from the accumulator to eventually render an alpha of 0, which could lead to the snake appearing to jump back one cell upon death. It's crucial to correctly maintain the snake's state between updates to avoid any unexpected movements.**Calculating body segment colors**
I'm thinking about how to draw body segment colors. I might use a formula like `ratio=i/(snake.length-1)` to get the right proportions. For the color itself, I could create a custom string with `hsl(${120 + ratio*40}, 57%, ${58-ratio*26}%)`. This way, the colors will change as the segments of the body are drawn, and I can ensure they look visually appealing!**Considering panel dimensions**
I'm trying to figure out the panel dimensions for the app. For the canvas width, I'm considering whether 614 columns will work for app880. I want to make sure that everything fits nicely and looks good on the screen. There might be specific guidelines for the dimensions I need to adhere to, so I’ll check those before finalizing it. Let’s make sure the layout will be functional and visually appealing!**Analyzing game mechanics**
I need to update the accumulator in milliseconds and keep track of loops while the game is active. If the accumulator exceeds the interval, I’ll step the game forward. It seems like resetting the accumulator after a game-over makes sense. I should ensure the rendering phase updates correctly, especially when the game is paused or in demo mode. Also, I'll need to manage particle updates based on the game state, ensuring they freeze upon game over.**Designing AI Movement Logic**
I'm working on the AI direction function. If there’s no food, I want to return a safe direction. I need to consider that the cells blocked include the snake's head. Using a set for blocked cells is good because it handles collisions well. The visited set needs to track the head’s position too. If food can't be reached due to the body blocking the path but the tail is clear, maybe there's still a viable route. Let's refine this further!**Planning AI functionality**
I’m considering how a better AI could run consistently without needing to restart, possibly showing a game over screen after 15 seconds and resetting after 2 seconds only in demo mode. If the demo crashes, it should restart automatically. Users could start it again with a key. I’m thinking about safety as the AI would fill spaces over time, so it makes sense to keep things simple yet effective. I'll also want to implement features for pausing and clear communication for users.**Adjusting game controls**
I’m considering using the "P" key for pausing the game, which could initially confuse users since they might expect it to start a new demo. I could manage this by treating "P" as a pause function, making it clear with an overlay message saying, "P to resume or any other key to take control." This way, if users press the WASD keys, they can begin the game, and I’d ensure the status reflects that it’s paused until resumed correctly.**Defining game phases**
I'm thinking about how to manage the game's phases when it’s paused. If the pause originated from the demo, I could restart the game with the current direction. Otherwise, I’d just set it to "playing" and queue the direction input. If the game has ended, I should handle that by setting the phase to "over." I’m also considering if I need a variable like "gameMode" to differentiate between previous demos and active play.**Evaluating canvas aspect ratio**
I'm working on a situation where I need to set a square `<canvas>` element using CSS with an intrinsic size of 300x150, but only setting the width and having height auto. I think using a wrapper with a defined aspect ratio might help. I'll set up the wrapper and make sure the canvas fills it properly with absolute positioning. This way, I can maintain a square dimension for responsive designs. I'll focus on getting that right!**Rendering grid background**
I’m focusing on rendering a grid background for my canvas. First, I need to create a gradient fill and a radial haze. I’ll set up a checkerboard pattern while managing different cell colors based on parity. I'll also include faint grid lines that are more visible every four cells. After drawing elements like the food and snake, I want to apply an overlay vignette for depth. Finally, I might add center marks or dots for better visibility. Looking good so far!**Evaluating food mechanics**
I'm thinking about how eating contributes to growth, and it seems like new food doesn't spawn on the snake itself. I feel positive about the current session since it seems to be the best so far. However, there's this concern about ensuring the best update when demo food is involved. I wonder how that plays into the overall mechanics! It's interesting to consider how food dynamics could affect gameplay experience.**Refining layout dimensions**
I'm considering the layout dimensions for different widths. At a width of 701, I’ve got an app width of 661 with a sidebar at 248. The canvas is around 365, and the vertical board is 500. I want to ensure the grid switches at a max width of 740 to prevent narrow layouts, so I’ll set a breakpoint at 760. At 800 width, the app width is 760, with a nice height of 550. I’ll block at 375 and use `@media (max-width:760px)`.**Examining canvas setup**
I'm working on a canvas setup, adjusting for different devices—375 for mobile and 1280 for desktop. I'll ensure there's no external web font and need to be precise with the HTML and JavaScript syntax. The viewport meta tag is in play, and I'm focusing on using a 2D canvas context with alpha set to false for no transparency. Then I've got to manage resizing the canvas while keeping track of variables carefully to avoid issues if observers trigger before everything is initialized. I’ll make sure to schedule updates smoothly!**Adjusting canvas dimensions**
I need to ensure that if the width and height are not equal, I wrap the aspect to 1. I'll assign `logicalX` and `logicalY` to the rectangle's dimensions and calculate the `boardSize` with the minimum of those dimensions. The canvas size will be set with consideration for device pixel ratio (dpr), adjusting the width and height accordingly. I'm also focusing on using grid cells computed from the board dimensions, ensuring everything is rendered as square cells for my snake rectangles!**Considering demo phase logic**
I'm analyzing how the transition from the paused state to the demo phase in the system should work. When I set `setPhase('demo')`, the accumulator might still be in a partial state. So, I think it's necessary to reset the accumulator to 0 when the demo auto restarts. This way, I ensure that the system starts fresh and operates correctly. It’s important to keep things clear and functional!**Examining game controls**
I’m working on the game controls, specifically the pause function with the "P" key. I need to ensure `preventDefault` works well for the up, down, left, and right keys, but I'm questioning if it should also apply to the WASD keys. It seems arrows and space should be prevented, but maybe WASD can be left alone. When a key is pressed, I should start the game and handle directional input right away. I'm fine with how reverse inputs are managed too.**Building game overlay design**
I’m working on the game overlay that uses the `score` even after the last death, which is interesting. When the game restarts, the scoreboard shows 000, but the best score is retained, which is a nice feature. I need to consider the status text as "PLAYER CONTROL" and ensure the dynamic header label is distinct. I’ll also make sure there’s no text cluttering the display. For the styling, I’ll utilize CSS with a linear-gradient background and set up a fixed pseudo grid. I’ll consider adding animations for the dots and overlay as well!**Refining game mechanics**
I don’t need to call window.alert for anything. I want to display the current score and the best score in HTML. The game can be restarted with Space or Enter after a game over. I'm considering whether to show the score in an overlay during the game over, but also on the side. For key events, I'll trigger a restart if the event code is either Space or Enter. The demo run also needs an initial click to start.**Assembling code details**
I need to focus on the score labels and control details to ensure they’re clear and functional. It's essential to assemble the full code accurately. I want to double-check everything because even minor errors could lead to confusion during gameplay. This means being meticulous with how each component integrates. I’m thinking through all necessary elements to ensure a smooth gaming experience for users, aiming for clarity and precision. Let's keep pushing forward!