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 터치)와 설치용 빌드는 다음 단계.
This commit is contained in:
204
app/test/engine_test.dart
Normal file
204
app/test/engine_test.dart
Normal file
@@ -0,0 +1,204 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user