diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..23bbeff --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,62 @@ +# Repository guidance for coding agents + +## Project shape + +- This is a build-free JavaScript arcade plus one Node.js service. Browser pages load source files directly; do not introduce a bundler or framework without an explicit architectural decision. +- Modern games share accounts, results, achievements, appearance, navigation, and room UI through `arcade.js`, `arcade.css`, `theme-init.js`, and `scripts/`. +- Game-specific browser code lives in each game directory. Keep testable mechanics separate from DOM/rendering code when practical. +- The Node service in `server/` serves static assets, REST APIs, SQLite persistence, and authoritative WebSocket rooms. +- Classic games are intentionally preserved beside modern versions. Avoid broad rewrites of `classic/` directories. +- Coding-challenge directories such as `leetcode/`, `codeforces/`, and `codewars/` are independent scripts, not arcade runtime components. + +Read `docs/architecture.md` before changing subsystem boundaries. Review the relevant record in `docs/adr/` for themes, persistence, security, results, online rooms, Battle Tanks, or Tetris. Add an ADR when a change makes a durable architectural choice rather than quietly overriding an accepted decision. + +## Working conventions + +- Use Node.js 22 or newer. +- Preserve the dependency-light, no-build browser architecture and existing plain JavaScript style. +- Match the formatting of the file being edited. Much of the CSS and browser markup is intentionally compact. +- Keep shared behavior in shared files and game-specific behavior in the game directory. Do not make game scripts branch on named experience themes. +- Experience themes (`playful`, `cabinet`, `calm`) and color modes (`system`, `light`, `dark`) are independent. Theme palettes and layout contracts belong in CSS custom properties and shared theme selectors. +- Maintain accessibility: semantic controls, useful labels, keyboard support, visible focus, live regions for status, and touch targets that remain usable at phone sizes. +- Treat narrow and short viewports as first-class. For mobile game changes, verify the board/canvas and controls together, account for `env(safe-area-inset-*)`, and test at least one phone-sized viewport such as 390 x 844. For layout changes, also check a laptop/desktop viewport and every experience theme affected. +- Keep mechanics deterministic and independently testable. Rendering and client prediction must not become sources of authoritative online state. + +## Security and data boundaries + +- Never trust client-authored scores. `server/accounts.js` validates bounded result facts and derives scores server-side. +- Online room managers own authoritative multiplayer state. Validate commands, preserve reconnect semantics, and serialize only viewer-safe state. +- Do not expose passcodes, session tokens, resume tokens, concealed opponent state, private implementation files, or database paths. +- Preserve origin checks, payload limits, rate limits, heartbeat handling, and secure cookie behavior when changing HTTP or WebSocket code. +- Add schema changes as the next ordered SQL file in `server/migrations/`. Never edit a migration that may already have shipped. + +## Tests and local verification + +Run from the repository root: + +```sh +npm test +npm run check +``` + +Use focused tests while iterating, for example: + +```sh +node --test tests/tetris.test.js +node --test tests/profile-page.test.js +``` + +Before handing off a change: + +1. Add or update regression tests for the behavior. +2. Run the focused tests, then the full test suite and syntax checks. +3. Run `git diff --check` and review the final diff for unrelated changes. +4. For visual or interaction changes, start `node server/index.js`, open `http://localhost:8080/`, and verify the rendered behavior at relevant desktop and mobile viewports. Reload after source changes because there is no build or hot-reload layer. +5. For online changes, test both local and online paths plus disconnect/resume and rematch behavior where applicable. + +## Git hygiene + +- Inspect `git status` before editing and preserve user-owned changes. +- Base requested feature/fix branches on the latest `master` unless the user specifies another base. +- Keep commits focused and use concise imperative subjects consistent with the repository history. +- Do not commit, push, rewrite history, or open a pull request unless the user asks. diff --git a/profile.css b/profile.css index 8e93603..5c2563e 100644 --- a/profile.css +++ b/profile.css @@ -81,7 +81,8 @@ h2 { margin-top: 0; font-size: clamp(25px, 4vw, 34px); } .stat-metrics div + div { padding-left: 14px; border-left: 1px solid color-mix(in srgb, var(--ink) 18%, transparent); } .stat-metrics strong { display: block; font-size: 26px; line-height: 1.1; } .stat-metrics small { display: block; margin-top: 5px; color: var(--muted); font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; } -.shell form { display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 10px; } +.shell form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)) auto; gap: 10px; } +.shell form input { min-width: 0; } .shell :where(input, select, button) { min-height: 44px; padding: 11px 13px; color: inherit; background: var(--field); border: 2px solid var(--ink); border-radius: 12px; font: inherit; } .shell button { color: #fff; background: var(--button); box-shadow: 3px 3px 0 var(--ink); font-weight: 900; cursor: pointer; } .shell button:hover:not(:disabled) { background: var(--button-hover); box-shadow: 1px 1px 0 var(--ink); transform: translate(2px, 2px); } @@ -109,7 +110,7 @@ th { color: var(--muted); font-size: 11px; letter-spacing: .08em; text-transform .profile-achievement small, .profile-achievement p { color: var(--muted); } .profile-achievement p { margin: 4px 0; font-size: 13px; } .profile-achievement progress { width: 100%; accent-color: var(--accent); } -@media (max-width: 820px) { .shell form { grid-template-columns: 1fr 1fr; } .shell form button { grid-column: 1 / -1; } } +@media (max-width: 820px) { .shell form { grid-template-columns: repeat(2, minmax(0, 1fr)); } .shell form button { grid-column: 1 / -1; } } @media (max-width: 650px) { .shell { width: min(100% - 20px, 560px); } .hero { margin-top: 50px; } diff --git a/tests/profile-page.test.js b/tests/profile-page.test.js index 82264af..1b89b81 100644 --- a/tests/profile-page.test.js +++ b/tests/profile-page.test.js @@ -28,6 +28,11 @@ test('profile controls do not restyle the shared top bar', () => { assert.doesNotMatch(styles, /(?:^|\n)button\s*\{/); }); +test('profile management fields fit the compact Cabinet column', () => { + assert.match(styles, /\.shell form\s*\{[^}]*grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\) auto;/s); + assert.match(styles, /\.shell form input\s*\{[^}]*min-width:\s*0;/s); +}); + test('profile includes accessible game-history pagination', () => { assert.match(profile, /id="history-pagination" aria-label="Game history pages"/); assert.match(profile, /data-page-action="previous"/); diff --git a/tests/tetris.test.js b/tests/tetris.test.js index 14035a9..fd15972 100644 --- a/tests/tetris.test.js +++ b/tests/tetris.test.js @@ -85,3 +85,10 @@ test('Tetris preserves elapsed time while bounding simulation steps and ignores assert.match(app, /event\.defaultPrevented \|\| interactive \|\| document\.querySelector\('dialog\[open\]'\)/); assert.match(app, /button,a,input,select,textarea,dialog/); }); + +test('Tetris keeps phone controls visible alongside the board', () => { + const styles = read('tetris/styles.css'); + assert.match(styles, /@media\(max-width:780px\)[^{]*\{[\s\S]*?\.touch-controls\{[^}]*position:fixed;[^}]*bottom:max\(8px,env\(safe-area-inset-bottom\)\)/); + assert.match(styles, /padding-bottom:calc\(160px \+ env\(safe-area-inset-bottom\)\)/); + assert.match(styles, /\.game-layout \.tetris-stage\{width:min\([^}]*calc\(\(100dvh - 160px - env\(safe-area-inset-bottom\)\)\/2\)\)/); +}); diff --git a/tetris/styles.css b/tetris/styles.css index 79b5643..652486e 100644 --- a/tetris/styles.css +++ b/tetris/styles.css @@ -1,5 +1,5 @@ @import url('../styles/game.css'); .app-shell{width:min(1100px,calc(100% - 32px));margin:auto;padding-bottom:54px}.topbar{display:flex;align-items:center;justify-content:space-between;height:80px}.brand{display:flex;align-items:center;color:var(--ink);text-decoration:none}.intro{display:flex;align-items:end;justify-content:space-between;gap:32px;padding:38px 0}.intro h1{margin:0;font-size:clamp(42px,7vw,76px)}.intro p{color:var(--muted)}.stats{display:grid;grid-template-columns:repeat(2,minmax(82px,1fr));gap:14px;min-width:300px}.stats div{display:grid;gap:3px}.stats span,.piece-panels h2{color:var(--muted);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.game-layout{display:grid;grid-template-columns:minmax(300px,520px) minmax(280px,1fr);gap:42px;align-items:start}.tetris-stage{position:relative;width:min(100%,500px);margin:auto}.tetris-board{display:grid;grid-template-columns:repeat(10,1fr);aspect-ratio:1/2;padding:8px;overflow:hidden;background:var(--tetris-board);border:3px solid var(--tetris-border);border-radius:18px;box-shadow:8px 8px 0 var(--tetris-shadow)}.tetris-cell,.mini-cell{aspect-ratio:1;background:var(--tetris-empty);border:1px solid var(--tetris-grid);border-radius:3px}.tetris-cell.ghost{background:var(--tetris-ghost);box-shadow:inset 0 0 0 2px var(--tetris-ghost-line)}[data-piece="I"]{background:var(--tetris-i)}[data-piece="J"]{background:var(--tetris-j)}[data-piece="L"]{background:var(--tetris-l)}[data-piece="O"]{background:var(--tetris-o)}[data-piece="S"]{background:var(--tetris-s)}[data-piece="T"]{background:var(--tetris-t)}[data-piece="Z"]{background:var(--tetris-z)}.tetris-cell[data-piece],.mini-cell[data-piece]{border-color:var(--tetris-piece-edge);box-shadow:inset 0 -3px 0 #0003}.tetris-controls{display:grid;gap:20px;padding:24px;background:var(--tetris-panel);border:3px solid var(--tetris-border);border-radius:20px;box-shadow:7px 7px 0 var(--tetris-shadow)}.status{min-height:44px;color:var(--tetris-ink);font-weight:700}.piece-panels{display:grid;grid-template-columns:1fr 1.4fr;gap:18px}.piece-panels h2{margin:0 0 8px}.mini-board{display:grid;grid-template-columns:repeat(4,1fr);width:104px}.next-list{display:flex;flex-wrap:wrap;gap:9px}.next-list .mini-board{width:64px}.touch-controls{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}.touch-controls button{min-height:46px;color:var(--tetris-ink);background:var(--tetris-control);border:2px solid var(--tetris-border);font:800 15px inherit}.control-actions,.result-actions{display:flex;gap:10px}.control-actions button,.result-actions button{flex:1;min-height:46px}.instructions{margin:0;color:var(--muted);font-size:12px;line-height:1.6}.result-layer{position:absolute;display:grid;inset:0;place-items:center;padding:18px;background:#0008;border-radius:18px}.result-layer[hidden]{display:none}.result-card{width:min(100%,380px);padding:28px;color:var(--tetris-ink);background:var(--tetris-overlay);border:3px solid var(--tetris-border);border-radius:18px;text-align:center}.result-card h2{margin:8px 0;font-size:34px}.result-card p:not(.eyebrow){color:var(--muted)} -@media(max-width:780px){.intro{display:block;padding:28px 0}.stats{margin-top:22px;min-width:0}.game-layout{grid-template-columns:1fr}.tetris-stage{width:min(82vw,430px)}.tetris-controls{padding:18px}.instructions{text-align:center}} -@media(max-width:430px){.app-shell{width:min(100% - 18px,1100px)}.tetris-stage{width:min(92vw,390px)}.touch-controls{grid-template-columns:repeat(4,1fr)}.piece-panels{grid-template-columns:1fr 1fr}.intro h1{font-size:42px}} +@media(max-width:780px){.app-shell{padding-bottom:calc(160px + env(safe-area-inset-bottom))}.intro{display:block;padding:28px 0}.stats{margin-top:22px;min-width:0}.game-layout{grid-template-columns:1fr}.game-layout .tetris-stage{width:min(82vw,430px,calc((100dvh - 160px - env(safe-area-inset-bottom))/2))}.tetris-controls{padding:18px}.touch-controls{position:fixed;z-index:900;right:max(9px,env(safe-area-inset-right));bottom:max(8px,env(safe-area-inset-bottom));left:max(9px,env(safe-area-inset-left));padding:8px;background:var(--tetris-panel);border:3px solid var(--tetris-border);border-radius:14px;box-shadow:4px 4px 0 var(--tetris-shadow)}.instructions{text-align:center}} +@media(max-width:430px){.app-shell{width:min(100% - 18px,1100px)}.game-layout .tetris-stage{width:min(92vw,390px,calc((100dvh - 160px - env(safe-area-inset-bottom))/2))}.touch-controls{grid-template-columns:repeat(4,1fr)}.piece-panels{grid-template-columns:1fr 1fr}.intro h1{font-size:42px}} .tetris-stage{width:min(100%,380px)}.tetris-cell[data-piece],.mini-cell[data-piece]{box-shadow:inset 0 -3px 0 var(--tetris-piece-shade)}.result-layer{background:var(--tetris-dim)}