--- id: wiki-2026-0508-tetris-project-retrospective title: Tetris Project Retrospective category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Tetris Retro, Tetris Postmortem] duplicate_of: none source_trust_level: B confidence_score: 0.85 verification_status: applied tags: [retrospective, project, tetris, game-development, lessons-learned] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript framework: Canvas/React --- # Tetris Project Retrospective ## 매 한 줄 > **"매 small game project = 매 architecture concept 의 condensed lab"**. Tetris 의 well-defined rule + finite scope 가 SRP, state machine, game loop, testability 의 매 trade-off 를 명확히 노출. 매 retrospective 는 매 lesson 의 codification. ## 매 핵심 ### 매 lesson layers - **Architecture**: pure game logic vs render separation. - **State**: deterministic state machine — 매 replay 가능. - **Testing**: pure logic 의 100% coverage, render 의 visual test. - **Performance**: 60fps 의 frame budget 16.67ms. ### 매 핵심 결정 - **Immutable state**: 매 board 의 매 frame 의 new copy — 매 undo/replay free. - **Tick-based loop**: 매 wall-clock 분리 — deterministic test. - **Wallkick algorithm**: SRS (Super Rotation System) — 매 official Tetris guideline. ### 매 응용 1. 매 게임 logic 의 pure function 추출 — UI swap (Canvas/SVG/Terminal) 자유. 2. 매 deterministic seed RNG — 매 bug reproduction. 3. 매 retrospective template 의 reuse for 다른 small project. ## 💻 패턴 ### Pure game state ```typescript type Cell = 0 | 'I' | 'O' | 'T' | 'S' | 'Z' | 'J' | 'L'; type Board = ReadonlyArray>; interface GameState { readonly board: Board; // 20 x 10 readonly active: Piece; readonly next: Piece[]; readonly hold: Piece | null; readonly score: number; readonly level: number; readonly linesCleared: number; readonly tick: number; readonly status: 'playing' | 'paused' | 'gameover'; } ``` ### Reducer-style transitions ```typescript type Action = | { type: 'MOVE'; dir: -1 | 1 } | { type: 'ROTATE'; cw: boolean } | { type: 'SOFT_DROP' } | { type: 'HARD_DROP' } | { type: 'HOLD' } | { type: 'TICK' }; export function reduce(state: GameState, action: Action): GameState { switch (action.type) { case 'MOVE': { const moved = movePiece(state.active, action.dir, 0); if (collides(state.board, moved)) return state; return { ...state, active: moved }; } case 'ROTATE': return tryRotate(state, action.cw); case 'HARD_DROP': return lockAndSpawn(dropToBottom(state)); case 'TICK': return tickGravity(state); // ... } } ``` ### SRS wallkick ```typescript // JLSTZ piece kick offsets (rotation 0 → R) const KICKS_JLSTZ_0_R: Array<[number, number]> = [ [0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2], ]; function tryRotate(state: GameState, cw: boolean): GameState { const rotated = rotatePiece(state.active, cw); const kicks = lookupKicks(state.active.kind, state.active.rot, cw); for (const [dx, dy] of kicks) { const candidate = { ...rotated, x: rotated.x + dx, y: rotated.y + dy }; if (!collides(state.board, candidate)) { return { ...state, active: candidate }; } } return state; // rotation rejected } ``` ### Deterministic 7-bag RNG ```typescript function* sevenBag(seed: number): Generator { const rng = mulberry32(seed); const kinds: PieceKind[] = ['I', 'O', 'T', 'S', 'Z', 'J', 'L']; while (true) { const bag = [...kinds]; for (let i = bag.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [bag[i], bag[j]] = [bag[j], bag[i]]; } yield* bag; } } ``` ### Game loop with fixed timestep ```typescript const TICK_MS = 16.67; let acc = 0; let last = performance.now(); function frame(now: number) { const dt = now - last; last = now; acc += dt; while (acc >= TICK_MS) { state = reduce(state, { type: 'TICK' }); acc -= TICK_MS; } render(state); if (state.status === 'playing') requestAnimationFrame(frame); } requestAnimationFrame(frame); ``` ### Pure logic test ```typescript it('clears completed line', () => { const board = makeBoard([ /* ... 19 empty rows ... */ ['I','I','I','I','I','I','I','I','I','I'], // bottom full ]); const state = { ...emptyState, board, active: makePiece('O', 0, 0) }; const result = lockPiece(state); expect(result.linesCleared).toBe(1); expect(result.score).toBe(100); // 1 line × level 1 }); ``` ## 매 결정 기준 (lessons) | 결정 | 결과 | 재사용? | |---|---|---| | Immutable state | 매 replay/undo trivial | ✓ | | SRS wallkick | 매 standard 준수 — player familiarity | ✓ | | Canvas render | 매 60fps 안정 | ✓ | | React render (실험) | 매 30fps drop 발생 | ✗ | | Pure reducer | 매 unit test 의 ease | ✓ | | Web Workers (실험) | 매 overhead > benefit (small state) | ✗ | **기본값**: 매 small game = pure reducer + Canvas + fixed-timestep loop. ## 🔗 Graph - 변형: [[State Machine]] - 응용: [[Game Loop]] - Adjacent: [[Testability_Architecture]] · [[Technical-Architecture]] · [[Test-Driven_Development]] ## 🤖 LLM 활용 **언제**: small game architecture 설계 reference, retrospective template, SRS wallkick 구현 question. **언제 X**: 매 specific Tetris ranking algorithm (TGM, etc.) 의 deep detail — 매 specialized guide 필요. ## ❌ 안티패턴 (실수 회고) - **Mutable board**: 매 undo/replay 가 fragile — debug 의 nightmare. - **Render in reducer**: 매 testability 의 손실 — separate concerns. - **Wall-clock dependency**: 매 nondeterministic test — fixed-timestep 가 essential. - **Ad-hoc rotation**: SRS 미준수 — 매 player 의 muscle memory 와 conflict. ## 🧪 검증 / 중복 - Verified (Tetris Guideline 2009; SRS spec; project commit history). - 신뢰도 B (project-specific retrospective, generalizable patterns). ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — full retro with SRS + reducer + lessons table |