import 'package:flutter_test/flutter_test.dart'; import 'package:tetrig/engine.dart'; Set _cellSet(List> cells) => cells.map((c) => '${c[0]},${c[1]}').toSet(); String _kickStr(List> 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.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); }); }