# Architecture

Technical reference for the Tetris codebase. Read this before making structural changes.

---

## Overview

The game is a **vanilla JavaScript** application with two runtime modes:

| Mode | Entry file | Use case |
|------|------------|----------|
| **Production** | `js/app.js` (bundled IIFE) | `index.html` — works over `file://` and HTTP |
| **Development** | `js/main.js` (ES module) | Local HTTP server with `type="module"` |

```
index.html
    └── js/app.js  (production — generated)
            └── IIFE containing all modules below

index.html  (dev only)
    └── js/main.js
            ├── TetrisGame       (js/tetris.js)      — game logic
            ├── GameUI           (js/ui.js)          — DOM updates
            ├── bindControls     (js/controls.js)    — input
            ├── setupBoardScaling (js/board-scaling.js) — responsive canvas
            └── BackgroundAmbience (js/ambience.js)  — decorative BG
```

### Build workflow

Source files in `js/` are ES modules. After editing them, regenerate the production bundle:

```bash
node scripts/bundle.js
```

`scripts/bundle.js` strips `import`/`export` and concatenates modules in dependency order into a single `(function(){ 'use strict'; … })()`.

**Important:** Do not use `import { foo as bar }` in source files — the bundler removes import lines but cannot rewrite alias names. Use a local const instead (see `board.js → makePiece`).

---

## Module Responsibilities

### Entry & wiring

| Module | Role |
|--------|------|
| `main.js` | Creates canvas refs, `GameUI`, `TetrisGame`, starts ambience, binds controls. Keep thin (~70 lines). |
| `controls.js` | Keyboard map, pause/start, auto toggle, mobile touch (with debounce). |
| `board-scaling.js` | `setupBoardScaling()` — sizes game/particle canvases to fill `.board-frame`, sets CSS vars for overlay alignment. |
| `ui.js` | `GameUI` class: stats panel, overlays, line-clear celebrations, auto-mode UI state. |

### Game engine

| Module | Role |
|--------|------|
| `tetris.js` | `TetrisGame` orchestrator: game loop, piece lifecycle, line clears, scoring hooks, action dispatch, auto-mode integration. |
| `board.js` | Live board state: 7-bag queue, delegates collision/lock/line logic to `grid.js`. |
| `grid.js` | **Shared pure grid helpers** — collision, lock, line clear, ghost Y. Used by `board.js` and `auto.js`. |
| `piece.js` | Piece definitions, `createPiece`, `rotateMatrix`, 7-bag shuffle, SRS wall kick tables. |
| `scoring.js` | Line score lookup, combo bonus, level from lines, high score load/save (with storage fallback). |
| `das.js` | `DasController` — delayed auto shift + auto repeat rate for horizontal movement. |
| `lock-delay.js` | `LockDelay` — grounded detection, 500ms lock timer, move-reset extensions. |
| `auto.js` | `AutoPlayer` — heuristic AI that evaluates placements and executes moves asynchronously. |

### Rendering & effects

| Module | Role |
|--------|------|
| `renderer.js` | Canvas drawing: blocks, grid, ghost, hold/next previews, line-clear shimmer. |
| `particles.js` | Particle bursts on line clear, lock sparks, hard-drop trail. Capped at 100 particles. |
| `ambience.js` | Full-screen background starfield (separate canvas). |
| `presentation.js` | DOM callouts (`SINGLE!`, `TETRIS!`), screen shake, level banner. |
| `audio.js` | Web Audio API tones for actions, line clears, music loop via `setInterval`. |

### Configuration

| Module | Role |
|--------|------|
| `config.js` | Board dimensions, timing constants, score table, `GAME_STATE`, `ACTIONS`, drop speed curve. |

---

## Layout (HTML + CSS)

```
.app (100vh flex column)
└── main.game-layout (flex: 1 — fills remaining height)
    ├── aside.panel--left
    │   ├── header.header--side   ← logo, tagline, help link
    │   ├── Hold preview
    │   └── Stats (score, lines, level, best, combo)
    ├── section.board-section (flex: 1)
    │   ├── #auto-badge
    │   └── .board-frame
    │       ├── #game-canvas      (internal 300×600, CSS-scaled)
    │       ├── #particle-canvas
    │       ├── #callout-layer
    │       └── #overlay
    └── aside.panel--right
        ├── Next queue (3 pieces)
        └── Controls + Auto Mode button
├── .mobile-controls (≤600px)
└── footer
```

The board scales to fill available vertical space. `board-scaling.js` writes `--board-display-w` / `--board-display-h` on `.board-frame` so the overlay and callout layer stay aligned with the scaled canvas.

### Responsive breakpoints

| Width | Behavior |
|-------|----------|
| > 900px | Full 3-column layout; sidebar branding |
| ≤ 900px | Hold/stats/controls list hidden; branding + next queue + auto button remain |
| ≤ 600px | Column layout; mobile touch buttons appear |

---

## Game State Machine

```
idle ──start()──► playing ◄──resume()── paused
                    │                      ▲
                    │ pause()              │
                    └──────────────────────┘
                    │
         lock + full rows
                    ▼
                 clearing ──anim done──► playing
                    │
              spawn blocked
                    ▼
                gameover ──start()──► playing
```

| State | Loop running? | Input accepted? |
|-------|---------------|-----------------|
| `idle` | No | Start only |
| `playing` | Yes (`requestAnimationFrame`) | All game actions (unless auto mode on) |
| `paused` | No | Pause/resume/start |
| `clearing` | No (dedicated anim RAF) | None |
| `gameover` | No | Start/retry |

### Auto mode interaction

- When `AutoPlayer.enabled`, `handleAction()` ignores manual input (pause still works via keyboard).
- Auto cancels on pause, game over, and new game start.
- Auto reschedules after each piece spawn, hold swap, and line clear.
- Toggle via **A** key or **Auto Mode** button; UI updated through `emitUpdate({ autoMode })`.

### Critical loop behavior

- The main loop (`tetris.js → loop()`) only runs when `state === playing`.
- Line clears set `state = clearing`, which stops the main loop.
- When the line-clear animation finishes, **`loop()` must be called again** — otherwise gravity stops permanently.
- Pause clears lock-delay state and cancels auto execution; resume re-triggers lock delay if the piece is still grounded.

---

## Game Loop (per frame)

```
loop(now)
  ├── gravity tick (softDropStep if interval elapsed)
  ├── renderer.setTime / setLockPulse
  ├── particles.update()
  ├── render() → drawBoard, drawHold, drawNext
  ├── particles.draw() (if any remain)
  └── requestAnimationFrame(loop)
```

Input actions (move, rotate, drop) call `render()` immediately for responsive feedback — they do not wait for the next loop tick.

---

## Piece Lifecycle

```
nextPiece() from 7-bag queue
    │
    ▼
active piece (move / rotate / hold / drop)
    │                              auto.schedule() if enabled
    ▼
touches ground → LockDelay.touchDown() → 500ms timer
    │                                      │
    │ slide/rotate while grounded          │ timer expires
    │ → reset timer (≤15 times)            ▼
    │                                  lockPiece()
    ▼                                      │
hard drop ──────────────────────────────────┘
                                           │
                              ┌────────────┴────────────┐
                              │                         │
                         no full rows              full rows
                              │                         │
                         combo = 0              handleLineClear()
                         spawnPiece()                  │
                         auto.schedule()         animate → burst
                                                   spawnPiece()
                                                   auto.schedule()
                                                   loop() restart
```

---

## Data Flow: Engine → UI

`TetrisGame` calls `onUpdate(data)` after state changes. `GameUI.update()` in `ui.js` handles it.

```javascript
{
  score, lines, level, highScore, state,   // always sent
  lineClear, combo, pointsEarned,          // on line clear
  levelUp, newLevel, isTetris,             // on level up / tetris
  hardDrop, isNewRecord,                   // event flags
  autoMode,                                // when auto mode toggled or on start
}
```

The engine never touches the DOM directly (except via Renderer/Particle canvases).

---

## Canvas Layers

On the main board (`board-frame`), back to front:

1. `#game-canvas` — board grid, locked blocks, active piece, ghost
2. `#particle-canvas` — particles (pointer-events: none, centered over game canvas)
3. `#callout-layer` — DOM callouts (`SINGLE!`, score popups)

Separate canvases:

- `#hold-canvas` — held piece preview (left panel)
- `#next-canvas` — next 3 pieces (right panel)
- `#bg-canvas` — full-page starfield background

Internal resolution is always **300×600** (10×20 cells at `BLOCK=30`). Display size is controlled by CSS + `board-scaling.js`.

---

## Auto-Play AI

`auto.js` uses a placement heuristic (height, holes, bumpiness, line clears):

1. For each rotation and column, simulate drop on a **copy** of the grid via `grid.js`
2. Score the resulting board; pick the best placement
3. Execute: rotate → move → hard drop (with small delays for visual feedback)

The AI does not use hold. Simulation does not apply SRS wall kicks (plans may differ slightly from human play near walls).

---

## Scoring Formula

```
linePoints = LINE_SCORE[count] × currentLevel
comboBonus = (combo - 1) × 50 × currentLevel   (when combo > 1)
total      = linePoints + comboBonus

soft drop  = 1 pt per cell
hard drop  = 2 pt per cell
```

Level is computed **before** applying the clear: `floor(lines / 10) + 1`.

High score is saved to `localStorage` key `tetris-highscore` only when the final score **strictly beats** the previous best. Storage access is wrapped in try/catch for restricted environments.

---

## Input Actions

Actions are string constants defined in `config.js → ACTIONS`. Both keyboard and mobile buttons dispatch the same action strings to `TetrisGame.handleAction()`.

```
left | right | stopDas | rotateCW | rotateCCW | softDrop | hardDrop | hold
```

Additional keyboard shortcuts handled in `controls.js` (not `ACTIONS`):

| Key | Action |
|-----|--------|
| Enter | Start / resume / retry |
| P | Pause / resume |
| A | Toggle auto mode |

Mobile HTML uses matching `data-action` attributes on buttons in `index.html`.

---

## Shared Grid Layer (`grid.js`)

Centralizes board math so live gameplay and AI simulation stay in sync:

| Export | Purpose |
|--------|---------|
| `createEmptyGrid()` | New 10×20 grid |
| `isValidOnGrid()` | Collision test |
| `lockPieceOnGrid()` | Mutate grid in place |
| `lockPieceOnGridCopy()` | Immutable lock for AI |
| `getFullRowIndices()` | Find complete rows |
| `removeRowsFromGrid()` | Remove rows, pad top |
| `clearFullRows()` | Remove + return cleared count |
| `dropPieceToBottom()` | Drop piece in simulation |
| `getGhostYOnGrid()` | Ghost piece Y position |

`board.js` wraps these for the live `Board` instance.

### `board.js` naming note

`Board.createPiece(type)` shadows the imported `createPiece` from `piece.js`. The module uses:

```javascript
const makePiece = createPiece;
```

Never call `createPiece()` inside `Board.createPiece()` — that causes infinite recursion. After bundling, the import line is stripped but `makePiece` remains.

---

## 7-Bag Randomizer

`piece.js → createBag()` shuffles all 7 piece types. `board.js` maintains a bag and a queue (size 5). Each `nextPiece()` shift refills the queue from the bag; a new bag is created when empty. This guarantees at most 4 of the same piece in a 7-piece window.

---

## SRS Wall Kicks

Rotation uses Super Rotation System kick tables from `piece.js`:

- `WALL_KICKS` — J, L, S, T, Z pieces
- `I_WALL_KICKS` — I piece (different pivot offsets)

`getWallKicks(type, fromRot, toRot)` returns an array of `[dx, dy]` offset attempts tried in order until one fits.

---

## Dependency Graph

```
main.js
├── tetris.js
│   ├── config.js
│   ├── piece.js → config.js
│   ├── scoring.js → config.js
│   ├── board.js → config.js, piece.js, grid.js → config.js
│   ├── renderer.js → config.js, piece.js
│   ├── particles.js
│   ├── audio.js
│   ├── das.js → config.js
│   ├── lock-delay.js → config.js
│   └── auto.js → config.js, piece.js, grid.js
├── ui.js → config.js, presentation.js
├── controls.js → config.js, board-scaling.js
└── ambience.js
```

No circular dependencies. Max depth: 3 levels from `main.js` (through `board.js → grid.js`).

---

## Files Removed (historical)

| Old file | Replaced by |
|----------|-------------|
| `js/constants.js` | `js/config.js` + `js/piece.js` |
| `js/effects.js` | `js/ambience.js` + `js/presentation.js` |

Do not re-add these names — update imports to use the new modules.
