diff --git a/src/engine.js b/src/engine.js
index 79e1e68..7fdeb4c 100644
--- a/src/engine.js
+++ b/src/engine.js
@@ -206,6 +206,33 @@ export function isDifficult(lines, tspin) {
return (lines === 4) || (tspin !== 'none' && lines >= 1);
}
+// ── 점수·레벨 (명세서 §5) ─────────────────────────────────────────
+// 레벨: 10라인=1레벨. 낙하간격(초)=0.8^(level-1), 하한 50ms.
+export function levelForLines(lines) {
+ return Math.floor(lines / 10) + 1;
+}
+export function gravityMsForLevel(level) {
+ return Math.max(Math.round(Math.pow(0.8, level - 1) * 1000), 50);
+}
+
+// 라인 클리어 1회 점수 (×레벨 적용). res = lock() 결과, level = 현재 레벨.
+export function scoreClear(res, level = 1) {
+ const { lines, tspin, allClear, combo, b2b } = res;
+ let base;
+ if (tspin === 'tspin') base = [0, 800, 1200, 1600][lines] || 0;
+ else if (tspin === 'mini') base = lines >= 1 ? 200 : 100; // Mini 100 / Mini Single 200
+ else base = [0, 100, 300, 500, 800][lines] || 0; // Single~Quad
+ if (b2b >= 1 && isDifficult(lines, tspin)) base = Math.round(base * 1.5); // B2B ×1.5
+ let score = base * level;
+ if (lines > 0 && combo >= 1) score += 50 * combo * level; // 콤보 +50×콤보수×레벨
+ if (allClear) {
+ let ac = [0, 800, 1200, 1800, 2000][lines] || 0; // All Clear 보너스
+ if (lines === 4 && b2b >= 1) ac = 3200; // B2B Quad AC
+ score += ac * level;
+ }
+ return score;
+}
+
// ── 게임 상태 (클라이언트가 타이밍을 구동) ─────────────────────────
export class TetrigGame {
constructor({ seed = 1 } = {}) {
diff --git a/src/index.template.html b/src/index.template.html
index e7f425f..fbda2a8 100644
--- a/src/index.template.html
+++ b/src/index.template.html
@@ -15,279 +15,507 @@
-
-
-
-
SPRINT
-
0.0
-
LINES 0/40 · PPS 0.00
+
+
+
+
+ 게스트—
+ SEASON 1 · D-42배치 0/10
+
+
솔로
+
SPRINT · TIME ATTACK · ENDLESS
›
+
+
+
멀티 · 리그전
+
실시간 1:1 랭크 매치 · 배치 10경기
›
+
+
+
커스텀
+
코드로 만들고 참가 · 인원 제한 없음
›
+
+ AD배너 광고 320×50 (배포 시 애드센스)
+
+
+
+
+
+
+
+
솔로
+
+
SPRINT
+
40줄 최속 클리어
—
›
+
+
+
TIME ATTACK
+
2분 점수 어택 · 레벨 가속
—
›
+
+
+
ENDLESS
+
무한 · 톱아웃해도 계속
—
›
+
-
-
-
+
+
+
+
+
SPRINT
+
0.0
+
LINES 0 · PPS 0.00
+
+
+
+ HOLD
+
+ NEXT
+
+ HOLD
+ ↺
+ ↻
+ 180°
+ 하드
드롭
⌄⌄
+ ◀
+ ▶
+ ▼ 소프트
+
+
PAUSE
+
+
+
+
+
-
-
HOLD
-
-
NEXT
-
+
+
+
+
+
+ AD네이티브 광고 300×250 (배포 시)
+
+
+
-
-
HOLD
-
↺
-
↻
-
180°
-
하드
드롭
⌄⌄
-
◀
-
▶
-
▼ 소프트
+
+
+
+
+
+ 준비 중
← 뒤로
+ 이 기능은 서버 단계에서 열립니다.
실시간 멀티·리그전·리더보드·프로필은
Cloudflare 서버 연동 후 제공됩니다.
+
+
-
-
-
diff --git a/test/engine.test.js b/test/engine.test.js
index 0e033f3..39f241e 100644
--- a/test/engine.test.js
+++ b/test/engine.test.js
@@ -4,6 +4,7 @@ import {
CONFIG, PIECE_TYPES, STATES, kickTable, makeRng, SevenBag,
createBoard, pieceCells, collides, clearLines, isBoardEmpty,
detectTSpin, isDifficult, TetrigGame,
+ levelForLines, gravityMsForLevel, scoreClear,
} from '../src/engine.js';
// ── 피스 상태/회전 ────────────────────────────────────────────────
@@ -219,3 +220,35 @@ test('톱아웃: 스폰 위치가 막히면 over', () => {
assert.equal(ok, false);
assert.equal(g.over, true);
});
+
+// ── 점수·레벨 (명세서 §5) ──
+test('레벨/중력 곡선', () => {
+ assert.equal(levelForLines(0), 1);
+ assert.equal(levelForLines(9), 1);
+ assert.equal(levelForLines(10), 2);
+ assert.equal(levelForLines(35), 4);
+ assert.equal(gravityMsForLevel(1), 1000);
+ assert.equal(gravityMsForLevel(2), 800);
+ assert.equal(gravityMsForLevel(20), 50); // 하한
+});
+
+test('점수표: 기본 클리어 ×레벨', () => {
+ const mk = (lines, tspin = 'none', allClear = false, combo = -1, b2b = -1) =>
+ ({ lines, tspin, allClear, combo, b2b });
+ assert.equal(scoreClear(mk(1), 1), 100); // Single
+ assert.equal(scoreClear(mk(2), 1), 300); // Double
+ assert.equal(scoreClear(mk(4), 1), 800); // Quad
+ assert.equal(scoreClear(mk(1), 3), 300); // ×레벨
+});
+
+test('점수표: T-spin / B2B / 콤보 / All Clear', () => {
+ // T-spin Double = 1200
+ assert.equal(scoreClear({ lines: 2, tspin: 'tspin', allClear: false, combo: -1, b2b: -1 }, 1), 1200);
+ // B2B Quad = 800×1.5 = 1200
+ assert.equal(scoreClear({ lines: 4, tspin: 'none', allClear: false, combo: -1, b2b: 1 }, 1), 1200);
+ // 콤보: Single + 콤보1 → 100 + 50×1 = 150
+ assert.equal(scoreClear({ lines: 1, tspin: 'none', allClear: false, combo: 1, b2b: -1 }, 1), 150);
+ // All Clear Quad = 800 + 2000 = 2800; B2B AC Quad = 1200 + 3200 = 4400
+ assert.equal(scoreClear({ lines: 4, tspin: 'none', allClear: true, combo: -1, b2b: -1 }, 1), 2800);
+ assert.equal(scoreClear({ lines: 4, tspin: 'none', allClear: true, combo: -1, b2b: 1 }, 1), 4400);
+});