Skip to content
Open
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
102 changes: 102 additions & 0 deletions 20260218-3files/.claude/docs/PL-001-tetris/findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Findings & Decisions

## Requirements
- [x] 7종 표준 테트로미노 (I, O, T, S, Z, L, J)
- [x] 좌우 이동, 회전, 소프트 드롭, 하드 드롭
- [x] 라인 클리어 및 점수 계산
- [x] 레벨 시스템 (10줄 클리어마다 레벨 업, 속도 증가)
- [x] 다음 피스 미리보기
- [x] 고스트 피스 (착지 위치 표시)
- [x] 게임 오버 조건 (새 피스 spawn 불가 시)
- [x] 외부 의존성 없음 (순수 Kotlin)

## Research Findings

### 터미널 입력 처리
- Java/Kotlin 표준 라이브러리에 raw 키 입력 API 없음
- `stty -icanon min 1 -echo` 명령으로 터미널을 raw 모드로 전환 가능
- `Runtime.getRuntime().exec()` 로 stty 호출
- 방향키는 3바이트 escape sequence: `ESC [ A` (↑), `ESC [ B` (↓), `ESC [ C` (→), `ESC [ D` (←)
- **주의**: 게임 종료 시 반드시 `stty sane`으로 터미널 복원 필요

### ANSI Escape Code
- 커서 이동: `\u001b[{row};{col}H`
- 색상: `\u001b[{color}m` (31=빨강, 32=초록, 33=노랑, 34=파랑, 35=자홍, 36=청록, 37=흰색)
- 배경색: `\u001b[4{n}m`
- 화면 지우기: `\u001b[2J`
- 커서 숨기기: `\u001b[?25l`, 보이기: `\u001b[?25h`
- **한계**: Windows cmd는 ANSI 미지원 (Windows Terminal은 지원)

### 테트리스 회전 시스템 (SRS)
- Super Rotation System: 공식 테트리스 가이드라인의 표준 회전 방식
- 4가지 회전 상태: 0 → R → 2 → L → 0
- 월킥: 기본 회전 위치가 충돌하면 최대 4개의 대체 위치를 순서대로 시도
- I 피스는 별도 월킥 테이블 사용 (다른 피스와 다름)
- 참고: https://tetris.wiki/Super_Rotation_System

### 게임 속도
- 레벨 1: 1000ms 간격 (1초에 1칸 낙하)
- 레벨별 공식: `max(100, 1000 - (level - 1) * 80)` ms
- 레벨 12 이상: 100ms 고정 (최대 속도)

## Technical Decisions

| Decision | Options Considered | Chosen | Rationale |
|----------|-------------------|--------|-----------|
| 회전 시스템 | ① 단순 90도 회전 ② SRS (표준) | **② SRS** | 단순 회전은 벽 근처에서 회전 불가 → UX 나쁨. SRS + 월킥이 표준이고, 오프셋 테이블만 추가하면 되어 구현 비용 낮음 |
| 렌더링 | ① println 매 프레임 ② ANSI escape code | **② ANSI** | println은 스크롤 발생하여 게임 불가. ANSI로 커서 이동 + 제자리 덮어쓰기 필수 |
| 깜빡임 방지 | ① 전체 다시 그리기 ② 더블 버퍼링 (변경 셀만) | **② 더블 버퍼링** | 매 프레임 전체 출력 시 눈에 보이는 깜빡임 발생. 이전 프레임과 diff하여 변경된 셀만 업데이트 |
| 게임 루프 | ① Thread.sleep ② ScheduledExecutor ③ Coroutine | **① Thread.sleep** | 이 규모에서 ScheduledExecutor는 과도. Coroutine은 kotlinx 의존성 필요 (외부 라이브러리 금지). Thread.sleep으로 충분 |
| 입력 처리 | ① 메인 스레드 polling ② 별도 스레드 blocking read | **② 별도 스레드** | System.in.read()는 blocking. 메인 스레드에서 호출하면 게임 루프 정지. 별도 daemon 스레드에서 읽고 큐에 적재 |
| 피스 데이터 | ① 2D Boolean 배열 ② 좌표 리스트 ③ 비트마스크 | **① 2D 배열** | 회전 시 행렬 변환이 직관적. 좌표 리스트는 회전 계산 복잡. 비트마스크는 가독성 나쁨 |
| 고스트 피스 | ① 구현 ② 미구현 | **① 구현** | 하드 드롭 착지 위치를 보여줘야 UX 좋음. 현재 피스를 바닥까지 시뮬레이션하면 되어 구현 비용 낮음 |
| 네트워크 대전 | ① 구현 ② 미구현 | **② 미구현** | 과도함 검토에서 YAGNI 판정. 소켓 통신, 동기화 등 복잡도 대비 데모 목적에 불필요 |

## Issues Encountered

### 1. 회전 시 ArrayIndexOutOfBounds
**문제**: 피스가 오른쪽 벽에 붙어있을 때 시계 방향 회전하면 배열 범위 초과
**원인**: 회전된 좌표가 Board 범위(0..9) 밖으로 나감
**해결**: 월킥 오프셋 테이블 구현. 기본 위치 충돌 시 최대 4개 대체 위치 순서대로 시도. 모두 실패하면 회전 취소
**결과**: 벽 근처, 바닥 근처 모든 위치에서 자연스러운 회전 가능

### 2. 라인 클리어 후 블록이 내려오지 않음
**문제**: 꽉 찬 줄을 제거했지만, 위의 블록들이 그대로 떠 있음
**원인**: 행 제거만 하고 위 행을 아래로 이동하는 로직 누락
**해결**: 클리어된 행 위의 모든 행을 아래로 한 줄씩 복사. 아래쪽 행부터 처리하여 덮어쓰기 방지
**결과**: 동시 4줄 클리어(테트리스)까지 정상 동작 확인

### 3. 게임 종료 시 터미널 복원 안 됨
**문제**: Ctrl+C로 종료하면 터미널이 raw 모드로 남아서 입력이 깨짐
**원인**: 정상 종료 경로에서만 `stty sane` 호출. 시그널 종료 시 미호출
**해결**: `Runtime.addShutdownHook`으로 종료 훅 등록. 정상 종료 + Ctrl+C + 예외 모두에서 터미널 복원 보장
**결과**: 어떤 방식으로 종료해도 터미널 정상 복원

### 4. 렌더링 깜빡임
**문제**: 매 프레임마다 화면을 지우고 다시 그리면 눈에 보이는 깜빡임 발생
**원인**: `\u001b[2J` (화면 지우기) + 전체 다시 그리기 사이에 빈 화면이 노출됨
**해결**: 더블 버퍼링 도입. 이전 프레임 상태를 저장하고, 변경된 셀만 ANSI 커서 이동으로 업데이트. 전체 출력을 StringBuilder에 모아 한 번에 flush
**결과**: 깜빡임 완전 제거

### 5. 하드 드롭 후 다음 피스 지연
**문제**: 스페이스바 하드 드롭 후 다음 피스가 한 프레임 늦게 등장
**원인**: 하드 드롭 → lock → 다음 프레임에서 spawn 순서로 되어 있어 1프레임 공백
**해결**: 하드 드롭 → lock → 라인 클리어 → spawn을 같은 프레임 내에서 처리
**결과**: 하드 드롭 후 즉시 다음 피스 등장

## Review Findings

| 검토 관점 | 발견 사항 | 조치 |
|-----------|-----------|------|
| 목적 부합 | 7종 피스, 회전, 클리어, 점수 모두 정상 | 없음 |
| 버그/보안 | Board 배열 접근 시 범위 검증 일부 누락 | require() 추가 |
| 개선 부작용 | 월킥 추가로 기존 회전 영향 없음 확인 | 없음 |
| 사이드 이펙트 | 터미널 raw 모드 복원 누락 가능성 | shutdown hook 추가 |
| 불필요한 코드 | 디버그용 printBoard() 잔존 | 제거 |
| 코드 품질 | 매직 넘버(10, 20) 산재 | WIDTH, HEIGHT 상수 추출 |

## Resources
- Tetris Guideline: https://tetris.wiki/Tetris_Guideline
- SRS 회전: https://tetris.wiki/Super_Rotation_System
- ANSI Escape Codes: https://en.wikipedia.org/wiki/ANSI_escape_code
- Kotlin Thread: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.concurrent/thread.html
134 changes: 134 additions & 0 deletions 20260218-3files/.claude/docs/PL-001-tetris/progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Progress Log

## Session 2026-02-15

### Phase 1: Requirements & Discovery
- **Status:** complete
- **Started:** 10:00
- Actions taken:
- 게임 규칙 정의: 표준 테트리스 가이드라인 기반
- 기술 제약 확인: 순수 Kotlin, 외부 라이브러리 금지
- 터미널 입력 방식 조사: `stty` + `System.in` raw read
- ANSI escape code 동작 확인: macOS Terminal, iTerm2에서 정상
- 7종 테트로미노 형태 및 색상 정의
- Files created/modified:
- `.claude/docs/PL-001-tetris/tasks.md` (계획 수립)
- `.claude/docs/PL-001-tetris/findings.md` (조사 결과 기록)

### Phase 2: Planning & Structure
- **Status:** complete
- **Started:** 11:00
- Actions taken:
- 모듈 구조 설계: 7개 파일로 분리 (Main, Game, Board, Piece, Input, Renderer, Score)
- 회전 시스템 결정: SRS (단순 회전 대비 UX 우수, 구현 비용 낮음)
- 렌더링 결정: ANSI escape code (외부 의존성 금지 조건)
- 깜빡임 방지: 더블 버퍼링 방식 채택
- 게임 루프: Thread.sleep (Coroutine은 kotlinx 필요하므로 제외)
- **과도함 검토**: 네트워크 대전 모드 제거 (YAGNI), 사운드 제거 (터미널 한계)
- Files created/modified:
- `.claude/docs/PL-001-tetris/tasks.md` (Phase 2 업데이트)
- `.claude/docs/PL-001-tetris/findings.md` (기술 결정 기록)

---

## Session 2026-02-16

### Phase 3: Implementation
- **Status:** complete
- **Started:** 09:00
- Actions taken:
- `Piece.kt` 구현: 7종 테트로미노 정의, SRS 회전 매트릭스, 월킥 오프셋 테이블
- `Board.kt` 구현: 10x20 그리드, 충돌 감지, 라인 클리어, 행 이동
- `Score.kt` 구현: 점수 계산 (1줄=100, 2줄=300, 3줄=500, 4줄=800) × 레벨
- `Input.kt` 구현: stty raw 모드, 방향키 escape sequence 파싱, 입력 큐
- `Renderer.kt` 구현: ANSI 컬러 렌더링, 더블 버퍼링, 고스트 피스
- `Game.kt` 구현: 게임 루프, 자동 낙하, 상태 관리 (PLAYING/GAME_OVER)
- `Main.kt` 구현: 진입점, 터미널 설정/복원, shutdown hook
- Files created/modified:
- `src/tetris/Piece.kt` (신규)
- `src/tetris/Board.kt` (신규)
- `src/tetris/Score.kt` (신규)
- `src/tetris/Input.kt` (신규)
- `src/tetris/Renderer.kt` (신규)
- `src/tetris/Game.kt` (신규)
- `src/tetris/Main.kt` (신규)

### Error Log (Session 2026-02-16)
| Timestamp | Error | Attempt | Resolution |
|-----------|-------|---------|------------|
| 09:45 | 회전 시 ArrayIndexOutOfBounds | 월킥 오프셋 테이블 구현 | 5개 위치 순서대로 시도, 모두 실패 시 회전 취소 |
| 11:30 | 라인 클리어 후 블록 안 내려옴 | 위→아래 순서 행 이동 | 아래쪽 행부터 처리하여 정상 동작 |
| 14:00 | Ctrl+C 시 터미널 복원 안 됨 | Runtime.addShutdownHook | 모든 종료 경로에서 stty sane 보장 |
| 15:20 | 렌더링 깜빡임 | 더블 버퍼링 도입 | 변경 셀만 업데이트, StringBuilder로 일괄 출력 |
| 16:00 | 하드 드롭 후 다음 피스 지연 | 같은 프레임 내 처리 | drop→lock→clear→spawn 순서를 한 프레임에 처리 |

---

## Session 2026-02-17

### Phase 4: Multi-perspective Review
- **Status:** complete
- **Started:** 09:00
- Actions taken:
- 11개 관점 순차 검토 수행
- Board 배열 접근 시 범위 검증 추가 (보안 검토)
- 디버그용 printBoard() 제거 (불필요한 코드 검토)
- WIDTH=10, HEIGHT=20 상수 추출 (코드 품질 검토)
- 연쇄 검토 2회차에서 새 문제 없음 확인, 종료
- Files modified:
- `src/tetris/Board.kt` (범위 검증 + 상수 추출)
- `src/tetris/Game.kt` (디버그 코드 제거)

## Review Log
| 검토 단계 | 관점 | 결과 | 발견된 문제 |
|-----------|------|------|-------------|
| 1 | 목적 부합 | PASS | 7종 피스, 회전, 클리어, 점수 모두 정상 |
| 2 | 버그/보안 | ISSUE | Board 배열 범위 검증 누락 → require() 추가 |
| 3 | 개선 부작용 | PASS | 월킥 추가가 기존 회전에 영향 없음 |
| 4 | 함수/파일 크기 | PASS | Game.kt 120줄, 최대 파일도 적절 |
| 5 | 코드 통합/재사용 | PASS | Piece 회전 데이터 상수로 통합 완료 |
| 6 | 사이드 이펙트 | ISSUE | 터미널 raw 모드 복원 누락 가능성 → shutdown hook |
| 7 | 전체 변경 사항 | PASS | 전체 diff 정상 |
| 8 | 불필요한 코드 | ISSUE | 디버그용 printBoard() 잔존 → 제거 |
| 9 | 코드 품질 | ISSUE | 매직 넘버 산재 → WIDTH, HEIGHT 상수 추출 |
| 10 | 사용자 흐름 | PASS | 시작 → 플레이 → 게임오버 → 점수 표시 정상 |
| 11-1 | 연쇄 검토 1회차 | PASS | require() 추가로 인한 새 문제 없음 |
| 11-2 | 연쇄 검토 2회차 | PASS | 상수 추출로 인한 새 문제 없음, 종료 |

---

## Session 2026-02-17 (오후)

### Phase 5: Final Gate & Delivery
- **Status:** complete
- **Started:** 14:00
- Actions taken:
- 배포 준비도 평가: 통과
- 전체 플레이 테스트 수행: 시작→레벨3까지 플레이→게임오버 정상
- progress.md 최종 기록
- 커밋: `feat: implement console tetris in pure Kotlin`

## Test Results
| Test | Input | Expected | Actual | Status |
|------|-------|----------|--------|--------|
| 좌우 이동 | ← → 키 | 피스 좌우 1칸 이동 | 정상 이동 | PASS |
| 벽 충돌 | 왼쪽 벽에서 ← | 이동 안 됨 | 이동 안 됨 | PASS |
| SRS 회전 | ↑ 키 | 시계 방향 90도 회전 | 정상 회전 | PASS |
| 월킥 | 벽 근처에서 ↑ | 자동 위치 보정 후 회전 | 정상 월킥 | PASS |
| 소프트 드롭 | ↓ 키 | 1칸 빠른 낙하 | 정상 낙하 | PASS |
| 하드 드롭 | Space | 즉시 착지 | 즉시 착지 | PASS |
| 1줄 클리어 | 한 줄 완성 | 100 × 레벨 점수 | 정상 계산 | PASS |
| 4줄 클리어 | 4줄 동시 완성 | 800 × 레벨 점수 | 정상 계산 | PASS |
| 레벨 업 | 10줄 클리어 | 레벨 2, 속도 증가 | 정상 전환 | PASS |
| 게임 오버 | 블록 쌓여서 꼭대기 도달 | GAME_OVER 표시 | 정상 표시 | PASS |
| 고스트 피스 | 플레이 중 | 착지 위치에 반투명 표시 | 정상 표시 | PASS |
| 종료 | q 키 | 게임 종료, 터미널 복원 | 정상 종료 | PASS |

## 5-Question Reboot Check
| Question | Answer |
|----------|--------|
| 1. 현재 어느 단계인가? | Phase 5 - 완료 |
| 2. 다음에 할 일은? | PR 리뷰 대기 |
| 3. 목표는? | 순수 Kotlin 콘솔 테트리스 구현 |
| 4. 지금까지 배운 것? | See findings.md (SRS, ANSI, 더블 버퍼링) |
| 5. 완료한 작업은? | 전체 구현 + 11단계 검토 + 배포 준비 완료 |
85 changes: 85 additions & 0 deletions 20260218-3files/.claude/docs/PL-001-tetris/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Project: [PL-001] 콘솔 테트리스

## Goal
외부 의존성 없이 순수 Kotlin으로 콘솔 테트리스를 구현한다.
ANSI escape code로 컬러 렌더링하고, 표준 테트리스 규칙(SRS 회전, 월킥, 레벨 시스템)을 따른다.

## Current Phase
Phase 5: Final Gate & Delivery

## Phases

### Phase 1: Requirements & Discovery
- [x] 게임 규칙 정의: 표준 테트리스 (Tetris Guideline)
- [x] 기술 제약 확인: 순수 Kotlin, 외부 라이브러리 금지
- [x] 플랫폼 확인: macOS/Linux 터미널 (ANSI 지원 필수)
- [x] 입력 방식 조사: 터미널 raw 모드로 실시간 키 입력
- **Status:** complete

### Phase 2: Planning & Structure
- [x] 모듈 구조 설계: Main, Game, Board, Piece, Input, Renderer, Score
- [x] 회전 시스템 결정: SRS (Super Rotation System)
- [x] 렌더링 방식 결정: ANSI escape code + 더블 버퍼링
- [x] 게임 루프 방식 결정: Thread.sleep 기반
- [x] 과도함 검토: 네트워크 대전 모드 제거, 사운드 제거 (YAGNI)
- **Status:** complete

### Phase 3: Implementation
- [x] Board: 10x20 그리드, 충돌 감지, 라인 클리어
- [x] Piece: 7종 테트로미노, SRS 회전, 월킥
- [x] Game: 게임 루프, 상태 관리, 자동 낙하
- [x] Input: 터미널 raw 모드, 키보드 입력 스레드
- [x] Renderer: ANSI 컬러 렌더링, 더블 버퍼링
- [x] Score: 점수 계산, 레벨 시스템, 속도 증가
- [x] Main: 진입점, 터미널 설정/복원
- **Status:** complete

### Phase 4: Multi-perspective Review
- [x] 목적 부합: 7종 피스, 회전, 라인 클리어, 점수 모두 동작
- [x] 버그/보안: 배열 인덱스 범위 검증 강화
- [x] 개선 부작용: 월킥 추가 후 기존 회전 정상 동작 확인
- [x] 함수/파일 크기: Game.kt 120줄, 적절
- [x] 코드 통합/재사용: Piece의 회전 데이터를 상수로 통합
- [x] 사이드 이펙트: 터미널 raw 모드 복원 누락 가능성 → shutdown hook 추가
- [x] 전체 변경 사항 통합 검토
- [x] 불필요한 코드: 디버그용 printBoard() 제거
- [x] 코드 품질: Kotlin 컨벤션 준수
- [x] 사용자 흐름: 시작 → 플레이 → 게임오버 → 점수 표시 정상
- [x] 연쇄 검토: 2회차에서 새 문제 없음, 종료
- **Status:** complete

### Phase 5: Final Gate & Delivery
- [x] 전체 테스트 확인
- [x] progress.md 기록 완료
- [x] 커밋 및 PR 작성
- **Status:** complete

## Key Questions
1. ~~회전 시스템을 어떤 걸 쓸지?~~ → SRS (findings.md 참고)
2. ~~콘솔 렌더링을 어떻게 할지?~~ → ANSI escape code + 더블 버퍼링
3. ~~게임 루프를 어떻게 구현할지?~~ → Thread.sleep + 별도 입력 스레드
4. ~~Windows 지원 필요한지?~~ → 불필요 (macOS/Linux만)

## Decisions Made
| Decision | Rationale |
|----------|-----------|
| SRS 회전 | 단순 회전은 벽 근처에서 UX 나쁨. SRS + 월킥이 표준이고 구현 비용 낮음 |
| ANSI 렌더링 | 외부 의존성 금지 조건. ncurses 등 불가. ANSI는 표준 터미널에서 지원 |
| 더블 버퍼링 | 매 프레임 전체 다시 그리면 깜빡임 심함. 변경 셀만 업데이트로 해결 |
| Thread.sleep 루프 | ScheduledExecutor는 이 규모에서 과도. sleep으로 충분히 정확 |
| 월킥 구현 | 월킥 없이 벽 근처 회전 불가 → 유저 불만 예상. 표준 오프셋 테이블로 해결 |
| 네트워크 대전 제거 | 과도함 검토에서 YAGNI 판정. 싱글 플레이로 충분 |

## Errors Encountered
| Error | Attempt | Resolution |
|-------|---------|------------|
| 회전 시 ArrayIndexOutOfBounds | 월킥 오프셋 적용 전 범위 체크 추가 | 5개 월킥 위치를 순서대로 시도, 모두 실패 시 회전 취소 |
| 라인 클리어 후 위 블록 안 내려옴 | 클리어 후 위→아래 순서로 행 이동 | 클리어된 행 위의 모든 행을 한 줄씩 아래로 복사 |
| 터미널 raw 모드 복원 안 됨 | Runtime.addShutdownHook으로 복원 | 정상 종료 + Ctrl+C 모두에서 `stty sane` 실행 보장 |
| 빠른 키 입력 시 프레임 스킵 | 입력 큐에 최신 1개만 유지 | ConcurrentLinkedQueue 사용, 프레임마다 큐 drain 후 마지막 입력만 처리 |
| 하드 드롭 후 다음 피스 즉시 등장 안 함 | lock → spawn 순서 수정 | 하드 드롭 → 즉시 lock → 라인 클리어 → 새 피스 spawn 순서로 변경 |

## Notes
- Phase 상태를 업데이트하세요: pending -> in_progress -> complete
- 중요한 결정 전에 이 계획을 다시 읽으세요 (attention manipulation)
- 모든 오류를 기록하세요 - 같은 실수를 반복하지 않습니다
Loading