Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions 87-ClaudeCode/03-claude-tetris/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<html>`, 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.
1 change: 1 addition & 0 deletions 87-ClaudeCode/03-claude-tetris/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

---

Expand Down
53 changes: 51 additions & 2 deletions 87-ClaudeCode/03-claude-tetris/game.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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();
5 changes: 5 additions & 0 deletions 87-ClaudeCode/03-claude-tetris/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
<link rel="stylesheet" href="style.css" />
</head>
<body>
<button id="theme-toggle" class="theme-toggle" type="button" aria-pressed="false" aria-label="Light theme">
<span id="theme-toggle-icon" aria-hidden="true">&#9728;</span>
<span id="theme-toggle-text" aria-hidden="true">Light</span>
</button>

<div class="wrapper">
<h1 class="title">TETRIS</h1>

Expand Down
109 changes: 89 additions & 20 deletions 87-ClaudeCode/03-claude-tetris/style.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,53 @@
/* 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;
padding: 0;
}

body {
background: #0f0f17;
color: #e0e0e0;
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, sans-serif;
display: flex;
align-items: center;
Expand All @@ -26,7 +67,7 @@ body {
font-size: 32px;
font-weight: 800;
letter-spacing: 4px;
color: #7aa2f7;
color: var(--accent);
}

.play-area {
Expand All @@ -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 ---- */
Expand All @@ -65,22 +106,22 @@ body {
.label {
font-size: 10px;
letter-spacing: 2px;
color: #555570;
color: var(--label);
font-weight: 600;
}

.value {
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;
}

Expand All @@ -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 ---- */
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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;
}
Loading