Files
tetrig/TETRIG-최종명세서.md
2026-08-21 22:18:02 +09:00

39 KiB
Raw Blame History

TETRIG (테트리그) — 최종 구현 명세서 v3.0

이 문서는 AI(Claude)에게 구현을 맡기기 위한 핸드오프 문서다. 지금까지 사용자와 확정한 모든 설계 결정·수치·에셋이 담겨 있으며, 이 문서 하나로 프로젝트 전체를 구현할 수 있다.


0. 이 문서를 받은 AI에게 — 작업 지침 (가장 먼저 읽을 것)

무엇을 만드는가: TETR.IO 스타일의 모바일 실시간 대전 블록 스태커 웹 게임 "테트리그 (TETRIG)". 솔로 3모드 + 인원 무제한 커스텀 룸(FFA) + Glicko-2 랭크 리그전.

산출물 2개:

  1. 클라이언트 — 단일 HTML 파일 (HTML+CSS+JS, Canvas 2D, 외부 에셋 0개, 사운드는 Web Audio 신스 생성)
  2. 서버 — Cloudflare Workers 프로젝트 (wrangler 표준 구조: Durable Objects 2종 + D1 + Cron)

작업 규칙:

  • §15 로드맵의 Phase 순서대로 진행하고, 각 Phase 완료 시 사용자 확인 후 다음으로 넘어간다.
  • 각 Phase 산출물 = 실행 가능한 파일 + 코어 로직의 jsdom(또는 node) 단위 테스트 통과 결과.
  • 이 문서의 결정사항은 이미 확정된 것이다. 재논의·재제안하지 말고 그대로 구현하라. 문서에 값이 명시된 것은 그 값을 쓰고, 없는 것만 질문하라.
  • 앱 이름 "테트리그"는 사용자가 상표 리스크를 인지하고 확정한 결정이다. 다시 경고하지 말 것. 단, §2의 완화책(BRAND 상수 등)은 반드시 구현할 것.
  • 모든 응답과 코드 내 사용자 노출 텍스트는 한국어 (게임 용어는 SPRINT, HOLD 등 영문 허용).
  • 튜닝 대상 수치(중력, TR 커브, 콤보 테이블 등)는 코드 상단에 상수로 분리해 나중에 조정하기 쉽게 한다.
  • 이 문서와 함께 첨부되는 파일 — 반드시 함께 확인할 것: tetrig-ux-mockup.png(9화면 UX 목업, UI 구현의 시각 기준 — 가장 먼저 열어볼 것), tetrig-logo.svg·tetrig-icon.svg(실사용 로고 에셋, 부록 A에 동일 소스 수록), tetrig-logo.png·tetrig-icon.png(로고 렌더 미리보기). §12의 좌표 명세는 목업을 수치로 보조하는 자료다 — 시각 판단은 목업 이미지를 따른다.

절대 금지 (IP):

  • 사용자 노출 텍스트(UI·스토어 문구·게임 내 판정 텍스트)에 "Tetris/테트리스" 단어 사용 금지 — 4줄 클리어는 "QUAD"
  • 테트리스 관련 음악(Korobeiniki 등) 사용 금지 — 사운드는 100% Web Audio 신스
  • 표준 가이드라인 피스 색상 1:1 복제 금지 — §13의 자체 팔레트 사용
  • TETR.IO의 에셋·명칭(TETRA LEAGUE 등) 복제 금지

1. 아키텍처 개요

[모바일 브라우저]                      [Cloudflare]
클라이언트 (단일 HTML, Canvas)  ⇄ WSS ⇄  Workers
 ├ 게임 엔진 (SRS+, 판정, 렌더)          ├ MatchmakerDO (리그 매칭 큐, 싱글턴)
 ├ 솔로 모드 3종                        ├ GameRoomDO (경기방 — 1:1 리그 & N인 커스텀 공용)
 └ 멀티/커스텀/프로필/리더보드 UI        ├ D1 (계정·레이팅·전적·리더보드)
                                        └ Cron (RD 감쇠, 시즌 처리)
  • 클라이언트는 Cloudflare Pages(또는 Workers 정적 자산)로 호스팅. 주소는 무료 서브도메인(*.pages.dev)으로 시작.
  • 서버는 무료 티어로 운영 가능 (DO는 SQLite 백엔드, WebSocket Hibernation 사용. WS 수신 메시지 20개=1요청 집계).
  • 로컬 개발: wrangler dev + 브라우저 탭 여러 개로 다인전 테스트.

2. IP 리스크 방침

앱 이름: 테트리그 (TETRIG) — 확정. "TETR-" 접두어의 상표 유사성 리스크를 사용자가 인지하고 채택했다. 완화책 3가지를 구현에 내장할 것:

  1. BRAND 상수: 앱 이름·로고 문구를 코드 전체에서 상수 1곳(const BRAND = "TETRIG" / BRAND_KO = "테트리그")으로 관리 — 언제든 1분 내 리브랜딩 가능해야 함
  2. 단계별 운용: 개인·지인 배포는 테트리그로 진행, 스토어 출시·수익화 직전 이름 재검토 (사용자 판단 사항)
  3. 이름 외 모든 표현은 자체 제작: 모드명(SPRINT/TIME ATTACK/ENDLESS/리그전), "QUAD" 용어, §13 자체 팔레트·자체 로고·자체 신스 사운드

3. 화면 구조 & 내비게이션

테트리그 메인
├── 솔로  ──(바텀시트)──▶ SPRINT / TIME ATTACK / ENDLESS
├── 멀티  ──▶ 리그전 (매칭 큐 → 1:1 → 결과)
└── 커스텀 ──▶ 방 만들기(코드 발급→로비) / 참가하기(코드 입력→로비)
하단 탭바: 홈 · 리더보드 · 프로필 · 설정  (전 메뉴 화면 공통)

전체 화면 목록 (상세 좌표는 §12): ① 메인 ② 솔로 인게임 ③ 리그전 1:1 ④ 커스텀 FFA ⑤ 커스텀 로비 ⑥ 리그 결과 ⑦ 프로필(게스트→로그인) ⑧ 솔로 모드 선택 시트 ⑨ 일반전(커스텀) 결과. 추가로 리더보드·설정 화면은 동일 디자인 언어로 구성.

4. 게임 코어 규칙 (전 모드 공통)

  • 보드: 10열 × 40행(내부), 하단 20행만 표시. 스폰은 21~22행(숨김 영역 하단). 스폰 위치가 이미 막혀 있으면 톱아웃.
  • 랜덤: 7-bag (7종 피스를 가방 단위로 셔플).
  • 넥스트: 5개 표시. 홀드: 1회/피스, 스왑 후 해당 피스 락다운까지 재사용 불가.
  • 고스트 피스: 항상 표시 (설정에서 끔 가능).
  • 락 딜레이: 500ms. 이동/회전 성공 시 리셋, 최대 15회(초과 시 즉시 락).
  • 중력 초기값 (튜닝 상수): 리그/커스텀 1000ms/칸 고정, SPRINT 800ms 고정, TIME ATTACK·ENDLESS는 레벨제 — 10라인=1레벨, 낙하간격(초) = 0.8^(level-1) 근사, 하한 50ms.
  • 소프트드롭 속도 = 중력 × SDF.

4.1 회전 시스템 — SRS + 180°

좌표 규약: 킥 오프셋 (dx, dy), dy 양수 = 위쪽. 회전 상태 0=스폰, 1=CW, 2=180, 3=CCW. 각 킥을 순서대로 시도해 처음 충돌 없는 위치 적용, 전부 실패 시 회전 취소.

JLSTZ 킥테이블 (표준 SRS)

전이 킥 순서
0→1 (0,0) (-1,0) (-1,1) (0,-2) (-1,-2)
1→0 (0,0) (1,0) (1,-1) (0,2) (1,2)
1→2 (0,0) (1,0) (1,-1) (0,2) (1,2)
2→1 (0,0) (-1,0) (-1,1) (0,-2) (-1,-2)
2→3 (0,0) (1,0) (1,1) (0,-2) (1,-2)
3→2 (0,0) (-1,0) (-1,-1) (0,2) (-1,2)
3→0 (0,0) (-1,0) (-1,-1) (0,2) (-1,2)
0→3 (0,0) (1,0) (1,1) (0,-2) (1,-2)

I 킥테이블 (표준 SRS)

전이 킥 순서
0→1 (0,0) (-2,0) (1,0) (-2,-1) (1,2)
1→0 (0,0) (2,0) (-1,0) (2,1) (-1,-2)
1→2 (0,0) (-1,0) (2,0) (-1,2) (2,-1)
2→1 (0,0) (1,0) (-2,0) (1,-2) (-2,1)
2→3 (0,0) (2,0) (-1,0) (2,1) (-1,-2)
3→2 (0,0) (-2,0) (1,0) (-2,-1) (1,2)
3→0 (0,0) (1,0) (-2,0) (1,-2) (-2,1)
0→3 (0,0) (-1,0) (2,0) (-1,2) (2,-1)

180° 회전 킥 (SRS+ 계열) — JLSTZ:

전이 킥 순서
0→2 (0,0) (0,1) (1,1) (-1,1) (1,0) (-1,0)
2→0 (0,0) (0,-1) (-1,-1) (1,-1) (-1,0) (1,0)
1→3 (0,0) (1,0) (1,2) (1,1) (0,2) (0,1)
3→1 (0,0) (-1,0) (-1,2) (-1,1) (0,2) (0,1)

I 피스 180°는 (0,0)만. O 피스는 회전해도 변화 없음(킥 불필요). ※ 킥테이블·T-spin 판정은 반드시 단위 테스트로 검증할 것.

4.2 판정 정의

  • T-spin: T 피스가 마지막 조작이 회전으로 락다운 + T 중심 기준 4모서리 중 3칸 이상 채움(보드 밖도 채움 취급). 전면(포인팅 방향) 2모서리가 모두 채워졌으면 T-spin, 아니면 T-spin mini.
  • B2B (Back-to-Back): Quad·T-spin 계열 클리어가 다른 클리어(Single~Triple 일반 클리어) 없이 연속되면 유지. 판정 팝업에 B2B ×n 표시.
  • 콤보: 연속 피스가 각각 라인 클리어하면 +1씩 누적.
  • All Clear (퍼펙트 클리어): 클리어 후 보드가 완전히 빔.
  • 판정 팝업 텍스트 예: QUAD!, T-SPIN DOUBLE!, B2B ×2, ALL CLEAR!, 3 COMBO

5. 솔로 모드 3종 + 점수

모드 규칙 기록
SPRINT 40줄 클리어 최속 시간 (ms)
TIME ATTACK 2분 점수 어택, 레벨 가속 점수
ENDLESS 무한, 게임오버 없음(톱아웃 시 보드만 리셋, 점수 유지) 누적 점수·라인

점수표 (×레벨, ENDLESS/TIME ATTACK 적용. SPRINT는 시간만 기록)

  • Single 100 / Double 300 / Triple 500 / Quad 800
  • T-spin: Mini 100 / Mini Single 200 / Single 800 / Double 1200 / Triple 1600
  • B2B 대상 클리어: ×1.5
  • 콤보: +50 × 콤보수 × 레벨
  • All Clear 보너스: Single 800 / Double 1200 / Triple 1800 / Quad 2000 (B2B Quad AC 3200)
  • 소프트드롭 +1/칸, 하드드롭 +2/칸

6. 대전 규칙 (리그전·커스텀 공통)

가비지 전송 테이블

클리어 전송 줄
Single 0
Double / Triple / Quad 1 / 2 / 4
T-spin Mini / Mini Single 0 / 1
T-spin Single / Double / Triple 2 / 4 / 6
B2B 보너스 +1
All Clear +5 (B2B 카운트 유지)
콤보 보너스 (콤보 2부터) [1,1,2,2,3,3,4,4,4,5] 이후 5 고정 (초기값)
  • 상쇄(counter): 대기 중인 수신 가비지에서 내 공격량을 먼저 차감, 남은 만큼만 실제 전송.
  • 투입: 수신 가비지는 게이지에 누적 표시 → 다음 피스 락다운 시 하단 삽입. 구멍 열은 랜덤(70% 확률로 직전과 같은 열 유지 — 초기값).
  • 가비지 한 번에 최대 삽입: 8줄/회 (초과분은 대기 유지).
  • 장기전 방지: 라운드 90초 경과 후 30초마다 가비지 배율 +25% (커스텀에선 호스트가 끔 가능).
  • 톱아웃 = 라운드 패배. FFA는 최후 생존자가 라운드 승리.

7. 커스텀 룸 — 인원 무제한 FFA

  • 방 만들기: 6자리 영숫자 코드 발급(혼동 문자 I/1/O/0 제외) → 로비. 참가하기: 코드 입력.
  • 최대 인원 제한 없음. 2명 = 1:1, 3명+ = FFA(라운드마다 최후 생존자 승).
  • 라운드 무한 반복 + 승수 누적. 호스트가 목표 승수 설정: FT1 / FT3 / FT5 (기본 무제한 반복).
  • 호스트 설정: 가비지 배율 0.5×~2.0×, 목표 승수, 시드 모드(개인 bag 기본 / 동일 bag 공정전), 장기전 배율 on/off.
  • 타겟팅: 기본 랜덤(8초마다 자동 변경) + 상대 미니보드 탭으로 수동 지정. KO 피드 표시(A → B 격추!).
  • KO된 플레이어는 라운드 끝까지 관전. 도중 입장 가능(다음 라운드부터 참전). 호스트 퇴장 시 다음 입장자에게 위임.
  • 레이팅 미반영(언랭크). 전적에 "커스텀" 표기. 결과창에서 리매치 투표(과반 동의 시 즉시 재시작).

8. 리그전 — 레이팅·랭크·매치메이킹

8.1 Glicko-2 & TR

  • Glicko-2 전체 구현 (rating R, deviation RD, volatility σ). 초기값 R=1500, RD=350, σ=0.06, τ=0.5.
  • 경기 승패로만 갱신 (라운드 무관). 경기 종료 시 서버에서 양측 동시 갱신.
  • 표시용 TR (0~25,000): TR = round(25000 / (1 + 10^((1600 (R RD)) / 300))) — 상수 1600/300은 튜닝 대상(Phase 6에서 분포 보고 조정).
  • 배치 10경기 완료 전 TR 비공개("배치 n/10" 표시). RD < 100일 때만 랭크 표시.
  • RD 하한 58. 7일 미접속 시 하루 +1씩 증가(최대 350) — Cron으로 일일 처리.

8.2 랭크 18단계 (초기 고정 TR 컷 — 운영 데이터로 조정)

D 0 · D+ 800 · C- 1,400 · C 2,000 · C+ 2,600 · B- 3,300 · B 4,000 · B+ 4,800
A- 5,700 · A 6,700 · A+ 7,800 · S- 9,200 · S 10,800 · S+ 12,400 · SS 14,200
U 16,500 · X 19,500 · X+ 22,500

(각 값 이상이면 해당 랭크. 인구가 충분해지면 퍼센타일 방식으로 전환.) 랭크 뱃지는 자체 아트, 승급/강등 연출 포함.

8.3 경기 형식 & 매치메이킹

  • 형식: 두 플레이어 중 높은 쪽 랭크 기준 — A+ 이하 FT3, S- 이상 FT5. (FT7 없음 — 최대 FT5 확정)
  • MatchmakerDO(싱글턴) 큐. 매칭 조건: 두 플레이어의 글리코 R ± 2×RD 구간이 겹치면 성사.
  • 대기 30초마다 자기 구간을 ×1.2씩 확장. 120초+ 무매칭 시 "커스텀 룸에서 친구와 플레이" 안내 노출(큐 유지).
  • 게스트는 리그전 참가 불가 (§9).

9. 계정 · 온보딩 (게스트 우선)

온보딩

  • 첫 실행 시 가입 없이 즉시 플레이. 랜덤 게스트명 자동 부여: 형용사+명사+숫자3 조합 (예: "재빠른미노482"). 단어 풀은 각 20개 내외로 코드에 정의. [게스트] 뱃지 표시.
  • 게스트 신원: 기기 로컬 ID(localStorage UUID) + 서버 발급 게스트 토큰.
  • 게스트 가능: 솔로 전 모드, 커스텀 생성/참가. 불가: 리그전, 리더보드 등록, 닉 변경.

로그인 (프로필 화면)

  1. Google 로그인: Google Identity Services(웹 OAuth). 클라가 받은 ID 토큰을 서버로 전송 → Worker가 Google JWKS로 서명 검증 → google_sub로 계정 매칭/생성. ※ Play Games가 아닌 구글 계정 로그인(웹 배포 특성). 사전 준비물: Google Cloud Console OAuth 클라이언트 ID (사용자에게 발급 절차 안내할 것).
  2. 이메일+비밀번호: PBKDF2(WebCrypto, 반복 ≥100k, salt 개별) 해시. v1은 비밀번호 재설정 미지원(문구로 안내), v2에서 메일 인프라와 함께.
  • 로그인 시 게스트 기록 자동 이전: 솔로 베스트·커스텀 전적을 계정으로 병합(더 좋은 기록 우선).
  • 세션: HMAC 서명 토큰(만료 30일), 자동 로그인.

닉네임 정책

  • 로그인 유저만 변경 가능. UNIQUE 보장(D1), 2~12자(한글/영문/숫자/_), 실시간 중복 검사 API.
  • 랜덤 게스트명 패턴은 시스템 예약(사칭 방지). 변경 쿨다운 30일.

10. 광고 & 수익화

  • 원칙: 플레이 중 화면(②③④)에는 광고 0 — 조작 공간 보호 + 우발 클릭 방지(애드센스 정책 위반 리스크 회피).
  • 하단 배너 (320×50/적응형): ① 메인(탭바 위), ⑤ 커스텀 로비. 자리는 §12 좌표 참고.
  • 결과창 네이티브 카드 (300×250급): ⑥ 리그 결과, ⑨ 일반전 결과, 솔로 결과 — 모든 결과 화면 공통 슬롯.
  • 전면(interstitial): 결과 확인 → 다음 행동 사이에만. 3판당 1회, 세션 첫 판 제외.
  • 보상형: v2.
  • 구현: AdSense 배너 + H5 Games Ads(Ad Placement API). 개발 중에는 광고 위치에 플레이스홀더 박스(점선 테두리 + "AD" 라벨)를 렌더하고, 실제 광고 코드는 배포 단계에서 사용자 애드센스 계정 ID로 끼운다.
  • ⚠ 광고 게시 = 수익화 단계 → §2 이름 재검토 트리거 (구현 AI는 언급만, 판단은 사용자).

11. 조작 — 확정 배치 (390×844 기준 좌표)

기본: 가상 버튼. 조작 존 y612~812. 멀티터치 지원(이동 누른 채 회전 가능). touchstart 즉시 반응(클릭 지연 제거), 햅틱(vibrate 10ms) 옵션.

버튼 x y w h 동작
HOLD 16 612 144 56 홀드
↺ 좌회전 16 676 68 136 CCW
↻ 우회전 92 676 68 136 CW
180° 168 612 54 56 180 회전
하드드롭 168 676 54 136 즉시 낙하+락 (마젠타 강조)
230 612 68 128 좌 이동 (홀드 시 DAS)
306 612 68 128 우 이동 (홀드 시 DAS)
▼ 소프트 230 748 144 64 소프트드롭
  • 핸들링 초기값(설정 슬라이더): DAS 133ms(80300), ARR 10ms(083, 0=즉시), SDF 20×(5~40).
  • 옵션: 스와이프 모드(설정 전환): 좌우 드래그=이동(거리=칸), 아래 빠른 플릭=하드, 아래 느린 드래그=소프트, 우측 탭=CW/좌측 탭=CCW, 홀드는 HOLD 박스 탭.
  • 버튼 크기/투명도 3프리셋(소/중/대) 제공.

12. 화면별 상세 명세 (뷰포트 390×844, 세로 고정)

공통: 테마 이원화 — 메뉴·로비·결과 화면(①⑤⑥⑦⑧⑨)은 BONE(#EDE7DA) 라이트 테마, 인게임(②③④)은 INK(#17161B) 다크 테마. 팔레트·타이포·컴포넌트 규칙은 §13 디자인 시스템 v2를 따른다. 모든 메뉴 화면 하단 탭바(y764~844): 홈·리더보드·프로필·설정 (활성 탭 = SAFETY 오렌지 블록 + 잉크 보더).

① 메인: 상단 TETRIG 워드마크(24,46) + 우측 닉네임·랭크 칩. 시즌 바(24,76,342,40): "SEASON n · D-xx" + TR. 카드 3장(24,y,342,148 / y=132,300,468): 솔로(민트 좌측바)·멀티/리그전(마젠타)·커스텀(바이올렛), 각각 타이틀+설명+우측 쉐브론. 배너 광고(24,700,342,52). 탭바.

② 솔로 인게임: 일시정지(16,16,40,36). 상단 중앙: 모드명/시간(대)/LINES·PPS. 보드(20,92, 셀 25px → 250×500). 우측 열(292~374): HOLD 박스(82×56) → NEXT 라벨 → 넥스트 5개(각 82×54). 판정 팝업은 보드 중앙 상단에 페이드 텍스트. 조작 존 §11.

③ 리그전 1:1: 상단: 좌 내 칩 / 중앙 "2 : 1"+"리그전·FT3" / 우 상대 칩. 내 보드(16,86, 셀 22 → 220×440). 가비지 게이지(244,86,10,440): 수신 대기량을 아래부터 마젠타로 채움 + 수치. 우측 열(262~374): 상대 실시간 미니보드(셀 10 → 100×200, 닉·랭크 라벨) → 내 HOLD(100×50) → NEXT 3개. 조작 존 동일. 라운드 종료 시 중앙 오버레이(승/패 + 스코어), 경기 종료 시 ⑥으로.

④ 커스텀 FFA: 상단 좌 "ALIVE 9/24", 우 타겟 칩("타겟: 랜덤/수동") + "미니보드 탭=타겟" 힌트. 그 아래 KO 피드 2줄. 내 보드(16,92, 셀 20 → 200×400). 우측 그리드(x 236/308, y 96/248/400): 상대 미니보드 2열(각 66×132, 닉 라벨, 타겟은 마젠타 테두리+조준 배지, KO는 회색+KO 표시), 마지막 타일 "+n명". 보드 아래 가로 스트립(y552): HOLD(70×52)+NEXT 5(각 52×52). 조작 존 동일.

⑤ 커스텀 로비: 헤더 "커스텀 룸"+뒤로. 코드 카드(20,70,350,124): "초대 코드" 라벨 + 코드 6자 대형(자간 넓게, 탭=복사) + [복사] 버튼 + "인원 제한 없음 · 현재 n명". 규칙 카드(20,210,350,58): 칩 3개(FTn·가비지 x.x×·bag 모드) + 호스트만 [변경]. 참가자 리스트(20,300~, 행 350×48·간격 8): 랭크칩+HOST칩+닉+준비상태(●/○). 배너 광고(20, 리스트 아래, 350×52). 하단 힌트 + [게임 시작](20,744,350,66, 마젠타 — 호스트) / 게스트는 [준비] 토글.

⑥ 리그 결과: "VICTORY"(민트)/"DEFEAT"(#FF6B6B) 대형 + "2:1 · 리그전 FT3". 레이팅 카드(30,220,330,160): TR 이전→이후 + 증감(민트/코럴) + 랭크칩 + 다음 랭크 게이지+%. 스탯 카드(30,398,330,86): APM/PPS/VS 3열. 상대 행(30,502,330,54): "vs 닉 · 랭크" + [전적 보기]. 광고 네이티브 카드(30,566,330,108). [다시 매칭](30,688,330,62, 마젠타) 메인으로.

⑦ 프로필 (게스트 상태): 신원 카드(20,70,350,110): 아바타(블록 문양)+게스트명+[게스트]칩+"기록이 이 기기에만 임시 저장 중". 기록 카드(20,196,350,84): SPRINT/TIME ATTACK/플레이수 3열. 로그인 카드(20,300,350,262, 바이올렛 테두리): "로그인하고 이름을 가져가세요" + 혜택 3줄(고유 닉네임 / 기록·레이팅 영구 보존 / 리그전 참가) + [Google로 계속하기](흰 배경 40,446,310,52) + [이메일로 계속하기](40,508,310,44) + "로그인 시 게스트 기록이 계정으로 이전됩니다". 로그인 후에는 이 카드가 닉네임+[이름 변경]+로그아웃으로 대체. 탭바(프로필 활성).

⑧ 솔로 모드 선택: 메인 위 딤 오버레이 + 바텀시트(y404~844, 상단 핸들바): "솔로" 타이틀 + 행 3개(24,y,342,96 / y=486,594,702): SPRINT(민트바)·TIME ATTACK(앰버바)·ENDLESS(바이올렛바), 각각 설명+내 베스트+쉐브론.

⑨ 일반전(커스텀) 결과: "DEFEAT"(#FF6B6B) 또는 "VICTORY" + "n위/m명 · 커스텀 FFA"(1:1이면 스코어). 스탯 카드(30,220,330,86): KO/APM/PPS. 광고 카드(30,330,330,190, 300×250급). 순위 요약 행(30,540,330,54). [리매치 투표 n/m](30,688,330,62, 바이올렛) 로비로.

13. 비주얼 & 브랜드 — 디자인 시스템 v2 (NEOBRUTAL)

방법론: 사람 디자이너들이 실제로 쓰는 네오브루탈리즘 컴포넌트 체계(ekmas/neobrutalism-components 계열 규칙: 2px 잉크 보더, 블러 없는 하드 오프셋 섀도, 고대비 플랫 컬러, 최소 라운딩)를 기반으로, 이 게임 고유의 시그니처 "해저드 시스템"(가비지=위험에서 따온 45° 스트라이프 + 챔퍼컷 모서리)을 결합했다. 다크 네이비 배경·네온 글로우·그라디언트·블러 섀도·과도한 라운딩은 전면 금지 — AI 생성 티가 나는 디폴트이므로 사용하지 않는다.

팔레트

이름 HEX 용도
BONE #EDE7DA 라이트 배경·밝은 패널
PAPER #E1D9C7 보조 패널
INK #17161B 텍스트·보더·하드 섀도·인게임 배경
SAFETY #FF4E11 주 액션·공격·랭크 강조
COBALT #2B3FE0 리그·정보·보조 액션
DWELL / DPNL / DGRID #101015 / #1F1F26 / #26262E 보드 웰 / 다크 패널 / 그리드
SUB / DSUB #6E6A5E / #8D8A80 보조 텍스트 (라이트/다크)

피스 7색 (v2 확정 — 프린트 톤)

I O T S Z J L 가비지
#3EC1B6 #F5C531 #A76BF2 #7FCC4C #F25C4C #4C7DF2 #F2913D #46464F

타이포 (3역할 — 구현 시 웹폰트)

  • 디스플레이: 각진 헤비 산세리프 — 'Black Han Sans'(구글 폰트, 무료) 권장 + skewX(-8°) 이탤릭 처리. 큰 숫자·VICTORY/DEFEAT·모드명·판정 팝업에만 절제 사용
  • 본문: 'Pretendard' 또는 시스템 산세리프
  • 유틸/모노: 'Nanum Gothic Coding' 또는 'IBM Plex Mono' — 초대 코드·TR·APM/PPS·라벨·시즌 표기

컴포넌트 규칙

  • 패널: 잉크 22.5px 보더 + **하드 오프셋 섀도(46px, 블러 0, 단색 잉크)** + 우하단 챔퍼컷 14px
  • 버튼: 주 액션 = SAFETY 채움 + 잉크 텍스트·보더·섀도 / 보조 = BONE·PAPER 채움. 라운딩은 0~10px만
  • 태그·랭크 뱃지: 사각 스티커 + 잉크 보더. 랭크 뱃지는 -3~-4° 틸트 (스티커 감성)
  • 해저드 스트라이프 용법 (남용 금지 — 위험·공격·광고·난투에만): 가비지 게이지(다크: SAFETY×INK 스트라이프), 광고 슬롯 프레임(라이트: INK×BONE), 커스텀 카드 표식, DEFEAT 테이프
  • 인게임 다크 패널: #1F1F26 채움 + BONE 1.5~2px 보더, 섀도 없음(플랫 콘솔 느낌)
  • 판정 팝업: 디스플레이체 skew + SAFETY 밑줄 바 (글로우 금지)
  • 연출도 플랫하게: 라인 클리어 파티클 = 사각 파편, 셰이크는 유지. 사운드 방향(Web Audio 신스)은 동일

로고 (부록 A v2)

  • 워드마크: BONE 바탕 + 잉크 셀 레터폼 + SAFETY 하드 오프셋 섀도. 첫 T 상단에 오렌지 T-미노(잉크 보더), 마지막 G의 모서리 셀 하나가 11° 틀어져 튀어나온 디테일(의도된 불완전성 = 사람 손맛), 하단 해저드 테이프 + 모노 캡션 "RANKED STACK BATTLE"
  • 앱 아이콘: BONE 스퀘어클 + 잉크 T-미노(-7° 틸트) + SAFETY 하드 섀도 + 하단 해저드 밴드 + 잉크 링 보더

14. 서버 상세

14.1 구성

  • GameRoomDO (경기당 1개, 리그·커스텀 공용): WS 허브, 가비지 원장·타겟 라우팅, 라운드/FT 관리, 시드 배포, 재접속 유예(15초, 리그는 초과 시 몰수), 리매치 투표, 결과를 D1에 기록 후 리그면 레이팅 갱신 호출.
  • MatchmakerDO (싱글턴): 리그 큐, §8.3 규칙으로 페어링 → GameRoomDO 생성·양측 이관.
  • D1: 아래 스키마. Cron: 일 1회 RD 감쇠, 시즌 마감 처리.
  • N인 방 부하: 보드 스냅샷은 피스 락 시 + 플레이어당 초당 2회 상한, 방 인원이 클수록 주기 자동 완화(관전자·타겟 우선 전달). WebSocket Hibernation 사용.
  • 최소 안티치트: PPS 상한(5.0), 피스당 공격량 상한, 라운드 이벤트 로그 보관(v2 정밀 검증 대비).

14.2 D1 스키마 초안 (구현 시 조정 가능)

CREATE TABLE users(
  id TEXT PRIMARY KEY, nickname TEXT UNIQUE NOT NULL,
  is_guest INTEGER NOT NULL DEFAULT 1,
  email TEXT UNIQUE, google_sub TEXT UNIQUE, pw_hash TEXT, pw_salt TEXT,
  created_at INTEGER, nickname_changed_at INTEGER
);
CREATE TABLE ratings(
  user_id TEXT PRIMARY KEY REFERENCES users(id),
  season INTEGER NOT NULL, glicko REAL, rd REAL, vol REAL,
  tr INTEGER, games INTEGER DEFAULT 0, wins INTEGER DEFAULT 0, last_played INTEGER
);
CREATE TABLE matches(
  id TEXT PRIMARY KEY, season INTEGER, mode TEXT,          -- 'league' | 'custom'
  finished_at INTEGER, room_size INTEGER,
  p1 TEXT, p2 TEXT, score1 INTEGER, score2 INTEGER,        -- 커스텀 FFA는 p1=본인, score1=순위
  tr_delta1 INTEGER, tr_delta2 INTEGER
);
CREATE TABLE solo_records(
  user_id TEXT, mode TEXT, best INTEGER, updated_at INTEGER,
  PRIMARY KEY(user_id, mode)
);

14.3 WebSocket 프로토콜 (JSON, 초안)

클라 → 서버: {t:"join", room, token} · {t:"ready"} · {t:"attack", lines, kind}
             {t:"board", grid}(압축 비트필드) · {t:"topout"} · {t:"target", id}
             {t:"rematch", vote} · {t:"leave"}
서버 → 클라: {t:"lobby", players[], rules} · {t:"seed", bag, mode}
             {t:"round_start", n} · {t:"garbage", from, lines}
             {t:"board", id, grid} · {t:"ko", who, by} · {t:"round_end", winner, score}
             {t:"match_end", result, trDelta?} · {t:"rematch_state", votes}

가비지 계산은 클라 보고 기반이되 서버가 원장 관리(상쇄·라우팅·상한 검증)로 desync 방지.

15. 개발 로드맵 (이 순서로 진행)

단계 내용 완료 기준
Phase 1 코어 엔진: 보드·피스·SRS+180 킥·7-bag·홀드·고스트·락딜레이·클리어 키보드로 플레이 가능 + 킥/판정 단위 테스트 통과
Phase 2 판정·점수 + 솔로 3모드 + 솔로 결과 화면(광고 슬롯 포함) 3모드 정상 플레이·기록 저장
Phase 3 §11 가상 버튼 + 스와이프 + 핸들링 설정 + §12 메뉴 화면들 모바일 브라우저 실기 조작 확인
Phase 4 서버 뼈대: 게스트 토큰·계정(구글/이메일)·기록 이전·GameRoomDO·WS·가비지 원장 + 커스텀 1:1 탭 2개로 코드 방 1:1 대전 성사
Phase 5 커스텀 FFA 확장: 다인 타겟팅·미니뷰 그리드·KO 관전·도중 입장·리매치 투표 탭 4개+ FFA 라운드 정상 진행
Phase 6 MatchmakerDO + Glicko-2 + TR/랭크 + 리더보드·프로필·전적 + 리그 결과 화면 매칭→경기→TR 반영 전체 사이클
Phase 7 연출·사운드·시즌·광고 슬롯 연결·재접속/부하 시나리오 점검·폴리시 v1 배포 (wrangler deploy + Pages)

16. 스토어/랜딩 문구 (확정 카피)

  • 태그라인: "쌓아라, 부숴라, 올라가라." (예비: "손끝에서 터지는 실시간 블록 대전" / "리그는 열렸다. 랭크로 증명해라.")
  • 짧은 설명: 실시간 1:1 리그전부터 인원 무제한 커스텀 난투까지 — 모바일에 최적화된 대전 블록 스태커
  • 전체 설명 구성: 인트로(모바일 재설계·핸들링·랭크) → ■리그전(Glicko-2·18랭크·배치10·시즌) → ■커스텀(6자리 코드·인원 무제한·규칙 설정) → ■솔로(SPRINT/TIME ATTACK/ENDLESS) → ■손맛(SRS+·7-bag·T-spin·B2B·핸들링) → ■기록(리더보드·APM·PPS) → 클로징("배치 10경기를 끝내보세요")
  • 영문 소개: TETRIG — a real-time competitive block stacker built for mobile. Ranked 1v1 league with 18 tiers, unlimited-size custom rooms with invite codes, and solo modes with full handling customization.
  • ASO 키워드: 블록 대전, 실시간 대전, 퍼즐 배틀, 스태커, 랭크 게임, 블록 퍼즐, 1:1 대결. ⚠ 설명·키워드에 "테트리스/Tetris" 직접 사용 금지.

17. 미확정 사항 (구현을 막지 않음 — 필요 시점에 사용자에게 확인)

  1. 도메인: tetrig.pages.dev 무료로 시작. 커스텀 도메인(tetrig.com 등)은 사용자가 추후 결정
  2. 시즌 길이 (제안 기본값: 2개월)
  3. Google OAuth 클라이언트 ID (Phase 4 시작 시 사용자에게 발급 안내)
  4. 애드센스 계정 ID (Phase 7 광고 연결 시)

부록 A. 로고 SVG 원본 소스 (그대로 파일로 저장해 사용)

A-1. tetrig-logo.svg (가로형 워드마크)

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1322 448" width="1322" height="448">
<defs><pattern id="hz" width="18" height="18" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
<rect width="18" height="18" fill="#EDE7DA"/><rect width="9" height="18" fill="#17161B"/></pattern></defs>
<rect width="1322" height="448" fill="#EDE7DA"/>
<rect x="87" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="139" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="191" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="139" y="139" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="139" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="139" y="243" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="139" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="132" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="132" y="236" width="46" height="46" rx="3" fill="#17161B" />
<rect x="132" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="80" y="80" width="46" height="46" rx="3" fill="#FF4E11" stroke="#17161B" stroke-width="3"/>
<rect x="132" y="80" width="46" height="46" rx="3" fill="#FF4E11" stroke="#17161B" stroke-width="3"/>
<rect x="184" y="80" width="46" height="46" rx="3" fill="#FF4E11" stroke="#17161B" stroke-width="3"/>
<rect x="132" y="132" width="46" height="46" rx="3" fill="#FF4E11" stroke="#17161B" stroke-width="3"/>
<rect x="279" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="331" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="383" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="279" y="139" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="279" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="331" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="383" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="279" y="243" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="279" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="331" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="383" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="272" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="324" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="376" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="272" y="132" width="46" height="46" rx="3" fill="#17161B" />
<rect x="272" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="324" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="376" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="272" y="236" width="46" height="46" rx="3" fill="#17161B" />
<rect x="272" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="324" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="376" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="471" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="523" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="575" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="523" y="139" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="523" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="523" y="243" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="523" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="464" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="516" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="568" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="516" y="132" width="46" height="46" rx="3" fill="#17161B" />
<rect x="516" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="516" y="236" width="46" height="46" rx="3" fill="#17161B" />
<rect x="516" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="663" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="715" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="663" y="139" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="767" y="139" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="663" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="715" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="663" y="243" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="767" y="243" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="663" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="767" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="656" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="708" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="656" y="132" width="46" height="46" rx="3" fill="#17161B" />
<rect x="760" y="132" width="46" height="46" rx="3" fill="#17161B" />
<rect x="656" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="708" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="656" y="236" width="46" height="46" rx="3" fill="#17161B" />
<rect x="760" y="236" width="46" height="46" rx="3" fill="#17161B" />
<rect x="656" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="760" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="855" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="907" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="959" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="907" y="139" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="907" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="907" y="243" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="855" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="907" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="959" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="848" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="900" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="952" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="900" y="132" width="46" height="46" rx="3" fill="#17161B" />
<rect x="900" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="900" y="236" width="46" height="46" rx="3" fill="#17161B" />
<rect x="848" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="900" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="952" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1047" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1099" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1151" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1203" y="87" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1047" y="139" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1047" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1151" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1203" y="191" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1047" y="243" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1203" y="243" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1047" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1099" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1151" y="295" width="46" height="46" rx="3" fill="#FF4E11" />
<rect x="1040" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1092" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1144" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1196" y="80" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1040" y="132" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1040" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1144" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1196" y="184" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1040" y="236" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1196" y="236" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1040" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1092" y="288" width="46" height="46" rx="3" fill="#17161B" />
<rect x="1144" y="288" width="46" height="46" rx="3" fill="#17161B" />
<g transform="rotate(11 1229.0 319.0)"><rect x="1209" y="297" width="46" height="46" rx="3" fill="#FF4E11" stroke="#17161B" stroke-width="3"/></g>
<rect x="80" y="350" width="216" height="12" fill="url(#hz)" stroke="#17161B" stroke-width="2"/>
<text x="312" y="361" font-family="NanumGothicCoding, monospace" font-size="15" font-weight="700" fill="#17161B" letter-spacing="2">RANKED STACK BATTLE</text>
</svg>

A-2. tetrig-icon.svg (앱 아이콘 512×512)

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs><pattern id="hz" width="18" height="18" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
<rect width="18" height="18" fill="#EDE7DA"/><rect width="9" height="18" fill="#17161B"/></pattern></defs>
<rect width="512" height="512" rx="96" fill="#EDE7DA"/>
<clipPath id="cp"><rect width="512" height="512" rx="96"/></clipPath>
<g clip-path="url(#cp)">
<rect x="0" y="428" width="512" height="84" fill="url(#hz)"/>
<rect x="0" y="424" width="512" height="5" fill="#17161B"/>
<g transform="rotate(-7 250.0 261.0)">
<rect x="92.0" y="162" width="104" height="104" rx="6" fill="#FF4E11" />
<rect x="210.0" y="162" width="104" height="104" rx="6" fill="#FF4E11" />
<rect x="328.0" y="162" width="104" height="104" rx="6" fill="#FF4E11" />
<rect x="210.0" y="280" width="104" height="104" rx="6" fill="#FF4E11" />
<rect x="80.0" y="150" width="104" height="104" rx="6" fill="#17161B" />
<rect x="198.0" y="150" width="104" height="104" rx="6" fill="#17161B" />
<rect x="316.0" y="150" width="104" height="104" rx="6" fill="#17161B" />
<rect x="198.0" y="268" width="104" height="104" rx="6" fill="#17161B" />
</g>
</g>
<rect x="7" y="7" width="498" height="498" rx="90" fill="none" stroke="#17161B" stroke-width="10"/>
</svg>

부록 B. UX 목업 파일 (첨부됨)

  • 이 문서와 함께 tetrig-ux-mockup.png (9화면 시트, 1390×2944)가 첨부된다. UI·레이아웃·색·버튼 배치는 이 목업을 시각 기준으로 구현하고, §11~§13의 좌표·팔레트 수치를 함께 사용해 정확도를 맞춘다.
  • 목업의 화면 번호(①~⑨)는 §12의 화면 명세 번호와 1:1 대응한다.
  • 로고는 첨부된 tetrig-logo.svg / tetrig-icon.svg를 그대로 사용한다 (부록 A에 동일 소스).