Files
tetrig/app/test/engine_test.dart
tkrmagid cbb8dea2eb Switch to Flutter (Android + iOS) for iPhone support; port engine to Dart
아이폰 지원 요구로 Android 전용 Kotlin 스택을 크로스플랫폼 Flutter로 교체.
- app/: Flutter 프로젝트(android+ios). 코어 로직은 순수 Dart 모듈 app/lib/engine.dart
  로 이식(SRS+180 킥·7-bag·홀드·고스트·락딜레이·T-spin/mini·콤보/B2B). 웹/Kotlin 판과 동일 알고리즘.
- app/test/engine_test.dart: Dart 테스트 24개 전부 통과, flutter analyze 0 issue.
- 이전 android/(Kotlin) 모듈 제거(Flutter로 대체). README에 Flutter 전환 + iOS 빌드 제약(맥 필요) 명시.
- 게임 UI(목업 ② + §11 터치)와 설치용 빌드는 다음 단계.
2026-08-22 14:40:03 +09:00

205 lines
5.7 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:tetrig/engine.dart';
Set<String> _cellSet(List<List<int>> cells) =>
cells.map((c) => '${c[0]},${c[1]}').toSet();
String _kickStr(List<List<int>> k) => k.map((c) => '${c[0]},${c[1]}').join(' ');
void main() {
// ── 피스 상태/회전 ──
test('T 스폰 셀 = 위-포인팅', () {
expect(_cellSet(states['T']![0]), {'0,1', '1,1', '2,1', '1,2'});
});
test('T CW(state1) = 오른쪽-포인팅', () {
expect(_cellSet(states['T']![1]), {'1,0', '1,1', '1,2', '2,1'});
});
test('I CW(state1) = 세로', () {
expect(_cellSet(states['I']![1]), {'2,0', '2,1', '2,2', '2,3'});
});
test('O는 회전 불변', () {
final s0 = _cellSet(states['O']![0]);
expect(_cellSet(states['O']![1]), s0);
expect(_cellSet(states['O']![3]), s0);
});
test('모든 피스 4상태', () {
for (final t in pieceTypes) {
expect(states[t]!.length, 4);
}
});
// ── 킥테이블 ──
test('킥테이블 내용 (명세서 §4.1)', () {
expect(_kickStr(kickTable('T', 0, 1)), '0,0 -1,0 -1,1 0,-2 -1,-2');
expect(_kickStr(kickTable('I', 0, 1)), '0,0 -2,0 1,0 -2,-1 1,2');
expect(_kickStr(kickTable('T', 0, 2)), '0,0 0,1 1,1 -1,1 1,0 -1,0');
expect(_kickStr(kickTable('I', 0, 2)), '0,0');
expect(_kickStr(kickTable('O', 0, 1)), '0,0');
});
test('벽킥: (0,0) 막히면 다음 오프셋으로 성사', () {
final board = createBoard();
board[5][5] = 'X';
expect(collides(board, pieceCells('T', 1, 4, 5)), true);
expect(collides(board, pieceCells('T', 1, 3, 5)), false); // (-1,0)
});
// ── 7-bag ──
test('7-bag: 연속 7개는 7종 모두', () {
final bag = SevenBag(makeRng(42));
for (int r = 0; r < 20; r++) {
final seven = [for (int i = 0; i < 7; i++) bag.next()]..sort();
expect(seven, (List<String>.from(pieceTypes)..sort()));
}
});
test('peek(5)==소비순서', () {
final bag = SevenBag(makeRng(7));
final peek = bag.peek(5);
expect(peek.length, 5);
for (int i = 0; i < 5; i++) {
expect(bag.next(), peek[i]);
}
});
test('시드 동일 → 동일 시퀀스', () {
final a = SevenBag(makeRng(123)), b = SevenBag(makeRng(123));
for (int i = 0; i < 30; i++) {
expect(a.next(), b.next());
}
});
// ── 충돌/클리어 ──
test('collides 벽/바닥/점유', () {
final bd = createBoard();
expect(collides(bd, [[-1, 0]]), true);
expect(collides(bd, [[10, 0]]), true);
expect(collides(bd, [[0, -1]]), true);
expect(collides(bd, [[5, 45]]), false);
bd[3][4] = 'T';
expect(collides(bd, [[4, 3]]), true);
});
test('clearLines 제거+하강', () {
final bd = createBoard();
for (int x = 0; x < 10; x++) bd[0][x] = 'I';
bd[1][0] = 'T';
expect(clearLines(bd), 1);
expect(bd[0][0], 'T');
for (int x = 1; x < 10; x++) expect(bd[0][x], null);
});
test('isBoardEmpty', () {
final bd = createBoard();
expect(isBoardEmpty(bd), true);
bd[0][0] = 'I';
expect(isBoardEmpty(bd), false);
});
// ── T-spin 판정 ──
test('T-spin full', () {
final bd = createBoard(10, 6);
bd[1][4] = 'X'; bd[1][6] = 'X'; bd[3][4] = 'X';
expect(detectTSpin(bd, 'T', 2, 4, 1, true), 'tspin');
});
test('T-spin mini', () {
final bd = createBoard(10, 6);
bd[1][4] = 'X'; bd[3][4] = 'X'; bd[3][6] = 'X';
expect(detectTSpin(bd, 'T', 2, 4, 1, true), 'mini');
});
test('T-spin none (모서리 부족)', () {
final bd = createBoard(10, 6);
bd[1][4] = 'X';
expect(detectTSpin(bd, 'T', 2, 4, 1, true), 'none');
});
test('T-spin none (회전 아님)', () {
final bd = createBoard(10, 6);
bd[1][4] = 'X'; bd[1][6] = 'X'; bd[3][4] = 'X';
expect(detectTSpin(bd, 'T', 2, 4, 1, false), 'none');
});
test('T-spin 바닥은 채움 취급', () {
final bd = createBoard(10, 6);
expect(detectTSpin(bd, 'T', 2, 4, -1, true), 'none');
bd[1][4] = 'X';
expect(detectTSpin(bd, 'T', 2, 4, -1, true), 'tspin');
});
test('isDifficult 규칙', () {
expect(isDifficult(4, 'none'), true);
expect(isDifficult(2, 'tspin'), true);
expect(isDifficult(1, 'mini'), true);
expect(isDifficult(3, 'none'), false);
expect(isDifficult(0, 'tspin'), false);
});
// ── 게임 흐름 ──
test('락 딜레이 리셋 최대 15', () {
final g = TetrigGame(seed: 1);
int ok = 0;
for (int i = 0; i < 25; i++) {
if (g.resetLockDelay()) ok++;
}
expect(ok, Config.maxLockResets);
});
test('스폰/하드드롭 사이클', () {
final g = TetrigGame(seed: 3);
expect(g.over, false);
final before = g.active.type;
final res = g.hardDrop()!;
expect(res.type, before);
expect(g.holdUsed, false);
});
test('홀드 1회/피스', () {
final g = TetrigGame(seed: 9);
final first = g.active.type;
expect(g.holdSwap(), true);
expect(g.hold, first);
expect(g.holdSwap(), false);
g.hardDrop();
expect(g.holdUsed, false);
expect(g.holdSwap(), true);
});
test('콤보/B2B 누적', () {
final g = TetrigGame(seed: 2);
LockResult quad() {
for (int y = 0; y < 4; y++) {
for (int x = 1; x < 10; x++) g.board[y][x] = 'X';
}
final a = g.active;
a.type = 'I';
a.state = 1;
a.x = -2;
a.y = 0;
g.lastMoveRotation = false;
return g.hardDrop()!;
}
final r1 = quad();
expect(r1.lines, 4);
expect(r1.b2b, 0);
final r2 = quad();
expect(r2.lines, 4);
expect(r2.b2b, 1);
});
test('톱아웃', () {
final g = TetrigGame(seed: 1);
for (int y = 18; y < 24; y++) {
for (int x = 0; x < 10; x++) g.board[y][x] = 'X';
}
expect(g.spawn('T'), false);
expect(g.over, true);
});
}