diff --git a/index.html b/index.html index 4660850..95e9f52 100644 --- a/index.html +++ b/index.html @@ -15,108 +15,273 @@
- -
-
-
SPRINT
-
0.0
-
LINES 0/40 · PPS 0.00
+ + +
+
TETRIG
+
게스트
+
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
- + +
+
FINISH
+
TIME
0.00
+
+
LINES
0
+
PPS
0.00
+
LEVEL
1
+
+
AD네이티브 광고 300×250 (배포 시)
+ + +
- -
HOLD
-
-
-
180°
-
하드
드롭
⌄⌄
-
-
-
▼ 소프트
+ +
+
설정
← 뒤로
+
133ms +
+
10ms +
+
20× +
+
+
+
+
+ +
+ + +
+
준비 중
← 뒤로
+
이 기능은 서버 단계에서 열립니다.
실시간 멀티·리그전·리더보드·프로필은
Cloudflare 서버 연동 후 제공됩니다.
+ +
- -
-
PAUSE
- - -
-
-
FINISH
-
- -
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
+ + +
+
TETRIG
+
게스트
+
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
- + +
+
FINISH
+
TIME
0.00
+
+
LINES
0
+
PPS
0.00
+
LEVEL
1
+
+
AD네이티브 광고 300×250 (배포 시)
+ + +
- -
HOLD
-
-
-
180°
-
하드
드롭
⌄⌄
-
-
-
▼ 소프트
+ +
+
설정
← 뒤로
+
133ms +
+
10ms +
+
20× +
+
+
+
+
+ +
+ + +
+
준비 중
← 뒤로
+
이 기능은 서버 단계에서 열립니다.
실시간 멀티·리그전·리더보드·프로필은
Cloudflare 서버 연동 후 제공됩니다.
+ +
- -
-
PAUSE
- - -
-
-
FINISH
-
- -
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); +});