diff --git a/87-ClaudeCode/03-claude-tetris/CLAUDE.md b/87-ClaudeCode/03-claude-tetris/CLAUDE.md index dc6a082..604dd0a 100644 --- a/87-ClaudeCode/03-claude-tetris/CLAUDE.md +++ b/87-ClaudeCode/03-claude-tetris/CLAUDE.md @@ -20,4 +20,5 @@ All logic lives in a single script, [game.js](game.js), loaded by [index.html](i - **Piece lifecycle**: `lockPiece()` → `merge()` → `clearLines()` (updates score/level/`dropInterval`) → `spawn()` (promotes `next`; calls `endGame()` if the spawn position collides). - **Input** is one `keydown` handler using `e.code`. Rotation (`tryRotate`) is clockwise only, with simple horizontal wall kicks `[0, -1, 1, -2, 2]`. - **Scoring**: line clears use `LINE_SCORES × level`; soft drop gives +1 per cell and hard drop +2 per cell. Level is `floor(lines / 10) + 1`, and `dropInterval = max(100, 1000 - (level-1) * 90)` ms. +- **Theming**: dark is the default. Colors live in CSS custom properties in [style.css](style.css) (`:root` = dark, `:root[data-theme="light"]` = light). The `#theme-toggle` button (ids `theme-toggle`, `theme-toggle-icon`, `theme-toggle-text`) calls `applyTheme()` in `game.js`, which sets `data-theme` on ``, swaps `palette` between `COLORS` and `LIGHT_COLORS`, caches the `--grid` value into `gridColor` (canvas can't read CSS), and repaints via `draw()`/`drawNext()` (the loop is stopped while paused/game over). The choice is persisted in `localStorage` under `tetris-theme`. The button is blurred after click so Space doesn't re-trigger it. Any new color should be a CSS variable, not a hard-coded value. - **Rendering**: `draw()` repaints grid, board, ghost piece (via `ghostY()`, alpha 0.2) and current piece. `drawNext()` renders into the separate preview canvas. diff --git a/87-ClaudeCode/03-claude-tetris/README.md b/87-ClaudeCode/03-claude-tetris/README.md index ae4c040..aaef837 100644 --- a/87-ClaudeCode/03-claude-tetris/README.md +++ b/87-ClaudeCode/03-claude-tetris/README.md @@ -42,6 +42,7 @@ A playable version of classic Tetris with all the mechanics you would expect: - Classic Tetris **scoring system** (100 / 300 / 500 / 800 multiplied by level). - **Levels** that increase every 10 lines and speed up the fall. - **Pause** and **Game Over** with a restart option. +- **Light / dark theme toggle** (top-right button). Dark is the default; the choice is remembered in `localStorage`. --- diff --git a/87-ClaudeCode/03-claude-tetris/game.js b/87-ClaudeCode/03-claude-tetris/game.js index d08fa06..4366acb 100644 --- a/87-ClaudeCode/03-claude-tetris/game.js +++ b/87-ClaudeCode/03-claude-tetris/game.js @@ -15,6 +15,18 @@ const COLORS = [ '#ffb74d', // L - orange ]; +// Slightly deeper tones so pieces and the ghost stay visible on a white board. +const LIGHT_COLORS = [ + null, + '#00acc1', // I + '#f9a825', // O + '#8e24aa', // T + '#43a047', // S + '#e53935', // Z + '#1e88e5', // J + '#fb8c00', // L +]; + const PIECES = [ null, [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], // I @@ -39,6 +51,13 @@ const overlay = document.getElementById('overlay'); const overlayTitle = document.getElementById('overlay-title'); const overlayScore = document.getElementById('overlay-score'); const restartBtn = document.getElementById('restart-btn'); +const themeToggle = document.getElementById('theme-toggle'); +const themeToggleIcon = document.getElementById('theme-toggle-icon'); +const themeToggleText = document.getElementById('theme-toggle-text'); + +const THEME_KEY = 'tetris-theme'; +let gridColor = '#22222e'; +let palette = COLORS; let board, current, next, score, lines, level, paused, gameOver, lastTime, dropAccum, dropInterval, animId; @@ -158,7 +177,7 @@ function updateHUD() { function drawBlock(context, x, y, colorIndex, size, alpha) { if (!colorIndex) return; - const color = COLORS[colorIndex]; + const color = palette[colorIndex]; context.globalAlpha = alpha ?? 1; context.fillStyle = color; context.fillRect(x * size + 1, y * size + 1, size - 2, size - 2); @@ -169,7 +188,7 @@ function drawBlock(context, x, y, colorIndex, size, alpha) { } function drawGrid() { - ctx.strokeStyle = '#22222e'; + ctx.strokeStyle = gridColor; ctx.lineWidth = 0.5; for (let c = 1; c < COLS; c++) { ctx.beginPath(); @@ -301,4 +320,34 @@ document.addEventListener('keydown', e => { restartBtn.addEventListener('click', init); +function loadTheme() { + try { + return localStorage.getItem(THEME_KEY) === 'light' ? 'light' : 'dark'; + } catch { + return 'dark'; + } +} + +function applyTheme(theme) { + const isLight = theme === 'light'; + document.documentElement.dataset.theme = theme; + palette = isLight ? LIGHT_COLORS : COLORS; + themeToggle.setAttribute('aria-pressed', String(isLight)); + themeToggleIcon.textContent = isLight ? '☾' : '☀'; + themeToggleText.textContent = isLight ? 'Dark' : 'Light'; + // The canvas can't read CSS variables, so cache the grid color here. + gridColor = getComputedStyle(document.documentElement).getPropertyValue('--grid').trim() || gridColor; + // The loop is stopped while paused/game over, so repaint explicitly. + if (board) { draw(); drawNext(); } +} + +themeToggle.addEventListener('click', () => { + const theme = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light'; + try { localStorage.setItem(THEME_KEY, theme); } catch { /* storage unavailable */ } + applyTheme(theme); + themeToggle.blur(); // keep Space/Enter from re-triggering the toggle +}); + +applyTheme(loadTheme()); + init(); diff --git a/87-ClaudeCode/03-claude-tetris/index.html b/87-ClaudeCode/03-claude-tetris/index.html index 3e59aca..7a81f7c 100644 --- a/87-ClaudeCode/03-claude-tetris/index.html +++ b/87-ClaudeCode/03-claude-tetris/index.html @@ -7,6 +7,11 @@ + +

TETRIS

diff --git a/87-ClaudeCode/03-claude-tetris/style.css b/87-ClaudeCode/03-claude-tetris/style.css index 5765d27..7aa8577 100644 --- a/87-ClaudeCode/03-claude-tetris/style.css +++ b/87-ClaudeCode/03-claude-tetris/style.css @@ -1,3 +1,44 @@ +/* Dark is the default theme; light overrides apply under [data-theme="light"]. */ +:root { + color-scheme: dark; + --bg: #0f0f17; + --text: #e0e0e0; + --accent: #7aa2f7; + --accent-hover: #9bbdff; + --accent-contrast: #0f0f17; + --board-bg: #1a1a25; + --border: #2a2a3a; + --shadow: rgba(0, 0, 0, 0.6); + --label: #555570; + --muted: #888; + --kbd-bg: #22223a; + --kbd-border: #3a3a5a; + --kbd-text: #aaa; + --overlay-bg: rgba(10, 10, 20, 0.75); + --danger: #e57373; + --grid: #22222e; +} + +:root[data-theme="light"] { + color-scheme: light; + --bg: #f1f3f9; + --text: #22243a; + --accent: #3b5fc0; + --accent-hover: #2f4ea1; + --accent-contrast: #ffffff; + --board-bg: #ffffff; + --border: #c5c9d9; + --shadow: rgba(40, 50, 90, 0.18); + --label: #6a6e88; + --muted: #555a70; + --kbd-bg: #e6e9f4; + --kbd-border: #b8bdd2; + --kbd-text: #3a3e58; + --overlay-bg: rgba(241, 243, 249, 0.8); + --danger: #c62828; + --grid: #e3e6f0; +} + *, *::before, *::after { box-sizing: border-box; margin: 0; @@ -5,8 +46,8 @@ } body { - background: #0f0f17; - color: #e0e0e0; + background: var(--bg); + color: var(--text); font-family: system-ui, -apple-system, sans-serif; display: flex; align-items: center; @@ -26,7 +67,7 @@ body { font-size: 32px; font-weight: 800; letter-spacing: 4px; - color: #7aa2f7; + color: var(--accent); } .play-area { @@ -41,10 +82,10 @@ body { #board { display: block; - border: 1px solid #2a2a3a; - background: #1a1a25; + border: 1px solid var(--border); + background: var(--board-bg); border-radius: 4px; - box-shadow: 0 0 40px rgba(0, 0, 0, 0.6); + box-shadow: 0 0 40px var(--shadow); } /* ---- Panel lateral ---- */ @@ -65,7 +106,7 @@ body { .label { font-size: 10px; letter-spacing: 2px; - color: #555570; + color: var(--label); font-weight: 600; } @@ -73,14 +114,14 @@ body { font-family: 'Courier New', Courier, monospace; font-size: 26px; font-weight: 700; - color: #7aa2f7; + color: var(--accent); letter-spacing: 1px; } #next-canvas { display: block; - border: 1px solid #2a2a3a; - background: #1a1a25; + border: 1px solid var(--border); + background: var(--board-bg); border-radius: 4px; } @@ -94,20 +135,20 @@ body { .controls li { font-size: 12px; - color: #888; + color: var(--muted); display: flex; align-items: center; gap: 4px; } kbd { - background: #22223a; - border: 1px solid #3a3a5a; + background: var(--kbd-bg); + border: 1px solid var(--kbd-border); border-radius: 3px; padding: 1px 6px; font-size: 11px; font-family: inherit; - color: #aaa; + color: var(--kbd-text); } /* ---- Overlay ---- */ @@ -117,7 +158,7 @@ kbd { display: flex; align-items: center; justify-content: center; - background: rgba(10, 10, 20, 0.75); + background: var(--overlay-bg); backdrop-filter: blur(4px); border-radius: 4px; } @@ -138,19 +179,19 @@ kbd { font-size: 28px; font-weight: 800; letter-spacing: 4px; - color: #e57373; + color: var(--danger); } #overlay-score { font-family: 'Courier New', Courier, monospace; font-size: 14px; - color: #7aa2f7; + color: var(--accent); } #restart-btn { margin-top: 8px; - background: #7aa2f7; - color: #0f0f17; + background: var(--accent); + color: var(--accent-contrast); border: none; border-radius: 6px; padding: 10px 28px; @@ -162,5 +203,33 @@ kbd { } #restart-btn:hover { - background: #9bbdff; + background: var(--accent-hover); +} + +/* ---- Theme toggle ---- */ +.theme-toggle { + position: fixed; + top: 16px; + right: 16px; + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--kbd-bg); + color: var(--text); + border: 1px solid var(--border); + border-radius: 999px; + padding: 6px 14px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} + +.theme-toggle:hover { + border-color: var(--accent); +} + +.theme-toggle:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; }