For a JavaScript Tetris learning project, begin with a small falling-block engine whose transitions you can inspect. Keep the active piece separate from the fixed grid, validate a proposed move before committing it, and treat locking plus row clearing as one transaction. The examples below specify an original teaching variant and provide acceptance cases for the local HTML example.

Declare the board and rotation rule
Use 10 columns and 20 rows, indexed from zero. Rows run from 0 at the top to 19 at the bottom; columns run from 0 through 9. An empty fixed cell contains zero. An active piece has a type, orientation, anchor row, anchor column, and four local cell coordinates.
Falling four-block pieces and completed horizontal rows are the basic concepts described in Uri Wilensky's NetLogo Tetris model at Northwestern. This project uses its own presentation and rules rather than that model's code, artwork, scoring, or levels.
Make the coordinates explicit. In the table, each pair is (local row, local column) in the starting orientation.
| Piece | Matrix size | Four occupied cells |
|---|---|---|
| I | 4 × 4 | (1,0), (1,1), (1,2), (1,3) |
| O | 2 × 2 | (0,0), (0,1), (1,0), (1,1) |
| T | 3 × 3 | (0,1), (1,0), (1,1), (1,2) |
| S | 3 × 3 | (0,1), (0,2), (1,0), (1,1) |
| Z | 3 × 3 | (0,0), (0,1), (1,1), (1,2) |
| J | 3 × 3 | (0,0), (1,0), (1,1), (1,2) |
| L | 3 × 3 | (0,2), (1,0), (1,1), (1,2) |
Rotate clockwise inside the declared matrix: (r,c) becomes (c,n−1−r) for matrix size n. Keep the anchor fixed. O stays unchanged. Reject a rotation if any occupied cell falls outside the board or overlaps a fixed cell; empty padding in the matrix does not itself collide. This variant has no wall kicks, lock delay, hold feature, or speed levels. It is not a claim to reproduce official competitive rules.
For ordinary spawning, use anchor (0,3) for the 3 × 3 and 4 × 4 shapes and (0,4) for O. A deterministic fixture may set a different anchor explicitly.
Validate first; lock as a single transition
A candidate move creates proposed coordinates without changing the fixed grid. If every occupied cell is inside the board and empty, commit the active piece's new position. Otherwise, Left, Right, or Rotate leaves the state unchanged.
Down attempts one row. If that position is blocked, lock the current four cells once. Hard Drop finds the last valid row, then performs the same locking transaction once. Purdue's falling-block assignment separates legal movement, fixing a piece, row elimination, and blocked spawning. Its larger board and four-to-seven-square pieces are different from this example.
Give each active piece an identity. Once it locks, retire that identity before spawning another piece. A delayed callback carrying the old identity must have no effect on the new piece. This prevents a stale gravity event from becoming an extra lock or fall.
After writing the four cells, find every full row in the resulting grid. Remove all those rows together, preserve the order of the survivors, and prepend the same number of empty rows. Increase the line count by the number removed. There is no points formula in this variant.
Use a fixture that exposes skipped rows
Start with rows 18 and 19 filled in columns 0 through 7, with columns 8 and 9 empty. Place a fixed marker at (17,2). Put O at anchor (18,8) and lock it.
| Check | Expected result |
|---|---|
| O occupies | (18,8), (18,9), (19,8), (19,9) |
| Rows removed | 18 and 19 together |
| Empty rows added | Two, at the top |
| Marker destination | (19,2) |
| Line count change | +2 |
The marker moves down two rows because both removed rows were below it. Deleting one row and continuing with stale indices can miss the second row.
Also test nonadjacent full rows 17 and 19. A marker in row 18 moves to row 19 because only one removed row was below it. A marker in row 16 moves to row 18. The survivors retain their relative order.
For a terminal fixture, fix a cell at (0,4) and attempt to spawn O at (0,4). The candidate overlaps: enter game over without overwriting the fixed cell. A board does not have to be entirely full for spawning to fail.
Make timing and controls observable
Use an illustrative 500 ms gravity interval and one scheduler. The first screen should distinguish settled cells from the active piece and show the line count, rules, status, and native Left, Right, Rotate, Down, Hard Drop, Pause, Resume, and Reset buttons.
Keyboard commands operate only while the game control area has focus. One key press produces one action; ignore held-key repeats in this bounded example. Pause and a hidden page stop gravity. Resume starts a fresh interval without catching up missed time. Reset cancels prior callbacks and clears the grid, line count, active piece, queue, and terminal state before starting a new session.
Use a fixed piece queue for acceptance cases. For ordinary play, a shuffled bag containing one of each of the seven types is a declared teaching policy. Do not describe the result as an official randomizer or a tested implementation before those checks occur.
Copyable build prompt
Create an original self-contained HTML/CSS/JavaScript falling-block teaching game, with readable source and an acceptance-test report. Use neutral visuals and no copied game code, assets, or branding.
Use a 10-column, 20-row fixed grid, zero-based rows and columns, separate from an active four-cell piece. Declare the initial local cells:
I, 4x4: (1,0),(1,1),(1,2),(1,3).
O, 2x2: (0,0),(0,1),(1,0),(1,1).
T, 3x3: (0,1),(1,0),(1,1),(1,2).
S, 3x3: (0,1),(0,2),(1,0),(1,1).
Z, 3x3: (0,0),(0,1),(1,1),(1,2).
J, 3x3: (0,0),(1,0),(1,1),(1,2).
L, 3x3: (0,2),(1,0),(1,1),(1,2).
Rotate clockwise by (r,c) -> (c,n-1-r) with a fixed anchor; O is unchanged. Check occupied cells only. Reject overlap or out-of-bounds candidates without mutation. No wall kicks, hold, lock delay, levels, or points. Spawn O at (0,4), other types at (0,3).
Down moves one cell or locks once when blocked. Hard Drop finds the last legal row and uses the same single locking transaction. Guard callbacks with session and piece identities. After locking, remove every full row simultaneously, preserve survivor order, prepend empty rows, and add the cleared-row count to lines. Validate the next spawn before writing anything; collision means game over.
Include deterministic fixtures: rows 18/19 have columns 0..7 filled, O at (18,8), marker (17,2): clear two rows, marker -> (19,2), lines +2. Nonadjacent rows 17/19 full: marker row 18 -> 19 and marker row 16 -> 18. Reject an illegal rotation with no state change. Fixed (0,4) blocks O spawning. A stale event after Hard Drop must not lock or move the new piece.
Provide native movement/drop/Pause/Resume/Reset buttons, scoped keyboard controls with no held-key repeat, readable state and rules. Use one 500 ms scheduler, pause when hidden, resume without catch-up, and cancel old callbacks on Reset. Use a fixed test queue and a separately declared shuffled seven-piece bag for ordinary play. Check actual article width and 360 px, focus, timing, and every fixture. Mark unexecuted checks not_tested. Do not claim official-rule fidelity, storage, hosting compatibility, or completed tests without evidence.
Keep the source beside the result
Retain the native source and the fixture results so a changed rotation or clear routine can be checked against the same coordinates. If you prepare an HTML version for NOS, its supplied product documentation supports HTML saving and rendering. Verify this game's controls and timing in the actual workspace before treating that version as playable. Explore NOS when you are ready to keep the prepared HTML alongside the explanation; the two-row marker test remains the quickest way to inspect whether clearing preserves the board correctly.