Skip to content

Commit 8f0f8c6

Browse files
committed
Format Dart files
1 parent 811f1ad commit 8f0f8c6

7 files changed

Lines changed: 404 additions & 217 deletions

lib/screens/settings_screen.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
219219
),
220220
child: Row(children: [
221221
Expanded(
222-
child: Text(
223-
_themeModeLabel(widget.settings.themeMode))),
222+
child: Text(_themeModeLabel(widget.settings.themeMode))),
224223
const Icon(Icons.arrow_drop_down, size: 20),
225224
]),
226225
),
@@ -237,9 +236,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
237236
alignment: Alignment.centerLeft,
238237
),
239238
child: Row(children: [
240-
Expanded(
241-
child:
242-
Text(_styleLabel(widget.settings.style))),
239+
Expanded(child: Text(_styleLabel(widget.settings.style))),
243240
const Icon(Icons.arrow_drop_down, size: 20),
244241
]),
245242
),
@@ -303,7 +300,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
303300
),
304301
),
305302
_SectionHeader(label: 'Instructions', colorScheme: colorScheme),
306-
_InstructionsCard(colorScheme: colorScheme, style: widget.settings.style),
303+
_InstructionsCard(
304+
colorScheme: colorScheme, style: widget.settings.style),
307305
const SizedBox(height: 8),
308306
],
309307
),

test/audio_service_test.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,46 @@ void main() {
6161
return (mock, stateController);
6262
}
6363

64+
group('AudioService — musicEnabled flag', () {
65+
test(
66+
'startMusic does not call play() when musicEnabled is false',
67+
() async {
68+
final (mockMusic, stateController) = makeMusicPlayer();
69+
70+
final service = AudioService(
71+
musicEnabled: false,
72+
musicPlayer: mockMusic,
73+
sfxPlayerFactory: makeSfxPlayer,
74+
);
75+
await service.init();
76+
await service.startMusic();
77+
78+
verifyNever(() => mockMusic.play(any()));
79+
80+
await stateController.close();
81+
},
82+
);
83+
84+
test(
85+
'startMusic calls play() when musicEnabled is true',
86+
() async {
87+
final (mockMusic, stateController) = makeMusicPlayer();
88+
89+
final service = AudioService(
90+
musicEnabled: true,
91+
musicPlayer: mockMusic,
92+
sfxPlayerFactory: makeSfxPlayer,
93+
);
94+
await service.init();
95+
await service.startMusic();
96+
97+
verify(() => mockMusic.play(any())).called(1);
98+
99+
await stateController.close();
100+
},
101+
);
102+
});
103+
64104
group('AudioService — unexpected music pause recovery', () {
65105
test(
66106
'calls resume() (not play()) when the music player is externally paused',

test/game_logic_test.dart

Lines changed: 163 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,27 +246,95 @@ void main() {
246246
expect(expectedGhostY, greaterThanOrEqualTo(initialY));
247247
});
248248

249-
test('should update score and level when lines are cleared', () {
249+
test('dropPiece should be blocked during grace period', () {
250250
gameLogic.startGame();
251251

252-
// Fill bottom row except one column
253-
for (int col = 0; col < GameConstants.boardWidth - 1; col++) {
254-
gameLogic.board[GameConstants.boardHeight +
255-
GameConstants.previewRows -
256-
1][col] = Colors.red;
257-
}
252+
// Grace period is always active immediately after a piece spawns
253+
expect(gameLogic.isNewPieceGracePeriod, true);
254+
255+
gameLogic.dropPiece(); // Should return early without placing anything
256+
257+
// The board should still be empty — no blocks placed
258+
bool boardHasPlacedBlocks =
259+
gameLogic.board.any((row) => row.any((cell) => cell != null));
260+
expect(boardHasPlacedBlocks, false,
261+
reason:
262+
'Hard drop during grace period must not place the piece on the board');
263+
});
264+
265+
test('should start line clear animation when a full row is detected', () {
266+
gameLogic.startGame();
258267

259-
// Place a piece to complete the line
260-
gameLogic.board[GameConstants.boardHeight + GameConstants.previewRows - 1]
261-
[GameConstants.boardWidth - 1] = Colors.blue;
268+
// Fill the bottom row completely
269+
final bottomRow =
270+
GameConstants.boardHeight + GameConstants.previewRows - 1;
271+
for (int col = 0; col < GameConstants.boardWidth; col++) {
272+
gameLogic.board[bottomRow][col] = Colors.red;
273+
}
262274

263275
gameLogic.clearLines();
264276

265-
// Wait for animation to complete
266277
expect(gameLogic.clearingLines.length, 1);
267278
expect(gameLogic.isAnimatingClear, true);
268279
});
269280

281+
test('should update score correctly after single line clear', () async {
282+
gameLogic.startGame();
283+
284+
final bottomRow =
285+
GameConstants.boardHeight + GameConstants.previewRows - 1;
286+
for (int col = 0; col < GameConstants.boardWidth; col++) {
287+
gameLogic.board[bottomRow][col] = Colors.red;
288+
}
289+
290+
gameLogic.clearLines();
291+
// Wait for the 350 ms clear animation timer to fire
292+
await Future.delayed(const Duration(milliseconds: 400));
293+
294+
expect(gameLogic.score, GameConstants.lineClearScores[1] * 1); // 100
295+
expect(gameLogic.linesCleared, 1);
296+
expect(gameLogic.isAnimatingClear, false);
297+
});
298+
299+
test('should award full TETRIS bonus for clearing 4 lines at once',
300+
() async {
301+
gameLogic.startGame();
302+
303+
final totalRows = GameConstants.boardHeight + GameConstants.previewRows;
304+
for (int row = totalRows - 4; row < totalRows; row++) {
305+
for (int col = 0; col < GameConstants.boardWidth; col++) {
306+
gameLogic.board[row][col] = Colors.red;
307+
}
308+
}
309+
310+
gameLogic.clearLines();
311+
await Future.delayed(const Duration(milliseconds: 400));
312+
313+
expect(gameLogic.score, GameConstants.lineClearScores[4] * 1); // 800
314+
expect(gameLogic.linesCleared, 4);
315+
expect(gameLogic.clearBonusLabel, 'TETRIS!');
316+
});
317+
318+
test('should increase level after clearing linesPerLevel lines', () async {
319+
gameLogic.startGame();
320+
321+
// Fill exactly linesPerLevel full rows
322+
final totalRows = GameConstants.boardHeight + GameConstants.previewRows;
323+
for (int row = totalRows - GameConstants.linesPerLevel;
324+
row < totalRows;
325+
row++) {
326+
for (int col = 0; col < GameConstants.boardWidth; col++) {
327+
gameLogic.board[row][col] = Colors.red;
328+
}
329+
}
330+
331+
gameLogic.clearLines();
332+
await Future.delayed(const Duration(milliseconds: 400));
333+
334+
expect(gameLogic.linesCleared, GameConstants.linesPerLevel);
335+
expect(gameLogic.level, 2);
336+
});
337+
270338
test('should detect game over correctly', () {
271339
gameLogic.startGame();
272340

@@ -286,6 +354,90 @@ void main() {
286354
expect(gameLogic.isGameRunning, false);
287355
});
288356

357+
test('should pause and resume game correctly', () {
358+
gameLogic.startGame();
359+
expect(gameLogic.isPaused, false);
360+
expect(gameLogic.isGameRunning, true);
361+
362+
gameLogic.pauseGame();
363+
expect(gameLogic.isPaused, true);
364+
expect(gameLogic.isGameRunning, true); // still running, just paused
365+
expect(gameLogic.isGameOver, false);
366+
367+
gameLogic.resumeGame();
368+
expect(gameLogic.isPaused, false);
369+
expect(gameLogic.isGameRunning, true);
370+
});
371+
372+
test('pauseGame should be idempotent', () {
373+
gameLogic.startGame();
374+
gameLogic.pauseGame();
375+
gameLogic.pauseGame(); // second call should be a no-op
376+
expect(gameLogic.isPaused, true);
377+
});
378+
379+
test('should receive garbage rows from opponent', () {
380+
gameLogic.startGame();
381+
382+
final totalRows = GameConstants.boardHeight + GameConstants.previewRows;
383+
// Board bottom should be empty initially
384+
expect(gameLogic.board[totalRows - 1][0], isNull);
385+
386+
gameLogic.receiveGarbage(2);
387+
388+
// Count filled vs gap cells in the bottom 2 rows
389+
int filledCells = 0;
390+
int gapCells = 0;
391+
for (int row = totalRows - 2; row < totalRows; row++) {
392+
for (int col = 0; col < GameConstants.boardWidth; col++) {
393+
if (gameLogic.board[row][col] != null) {
394+
filledCells++;
395+
} else {
396+
gapCells++;
397+
}
398+
}
399+
}
400+
401+
// Each garbage row has exactly one gap column
402+
expect(filledCells, (GameConstants.boardWidth - 1) * 2);
403+
expect(gapCells, 2);
404+
});
405+
406+
test('receiveGarbage should be a no-op when lines <= 0', () {
407+
gameLogic.startGame();
408+
final boardBefore =
409+
gameLogic.board.map((row) => List<Color?>.from(row)).toList();
410+
411+
gameLogic.receiveGarbage(0);
412+
413+
for (int row = 0; row < gameLogic.board.length; row++) {
414+
expect(gameLogic.board[row], equals(boardBefore[row]));
415+
}
416+
});
417+
418+
test('should export board snapshot with correct dimensions', () {
419+
gameLogic.startGame();
420+
421+
final snapshot = gameLogic.exportBoardSnapshot();
422+
423+
// Snapshot covers only the visible board (no preview rows)
424+
expect(
425+
snapshot.length,
426+
GameConstants.boardWidth * GameConstants.boardHeight,
427+
);
428+
// All values must be valid palette indices
429+
expect(snapshot.every((cell) => cell >= 0 && cell <= 8), true);
430+
});
431+
432+
test('exportBoardSnapshot should include the current piece', () {
433+
gameLogic.startGame();
434+
435+
final snapshot = gameLogic.exportBoardSnapshot();
436+
437+
// Current piece should appear somewhere in the snapshot as a non-zero index
438+
expect(snapshot.any((cell) => cell > 0), true);
439+
});
440+
289441
test('should get board with current piece correctly', () {
290442
gameLogic.startGame();
291443

test/settings_provider_test.dart

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:shared_preferences/shared_preferences.dart';
4+
import 'package:block_drop/settings/settings_provider.dart';
5+
6+
void main() {
7+
group('SettingsProvider', () {
8+
setUp(() {
9+
SharedPreferences.setMockInitialValues({});
10+
});
11+
12+
test('starts with correct default values', () {
13+
final settings = SettingsProvider();
14+
15+
expect(settings.themeMode, AppThemeMode.system);
16+
expect(settings.style, AppStyle.classic);
17+
expect(settings.musicEnabled, false);
18+
expect(settings.sfxEnabled, false);
19+
expect(settings.highScore, 0);
20+
});
21+
22+
test('updateHighScore only updates when the new score is higher', () async {
23+
final settings = SettingsProvider();
24+
25+
await settings.updateHighScore(500);
26+
expect(settings.highScore, 500);
27+
28+
await settings.updateHighScore(200); // lower — should be ignored
29+
expect(settings.highScore, 500);
30+
31+
await settings.updateHighScore(1000); // higher — should update
32+
expect(settings.highScore, 1000);
33+
});
34+
35+
test('updateHighScore does not update when score equals current high score',
36+
() async {
37+
final settings = SettingsProvider();
38+
39+
await settings.updateHighScore(300);
40+
expect(settings.highScore, 300);
41+
42+
await settings.updateHighScore(300); // equal — should not trigger notify
43+
expect(settings.highScore, 300);
44+
});
45+
46+
test('isBlackMode is true only for AppThemeMode.black', () async {
47+
final settings = SettingsProvider();
48+
49+
expect(settings.isBlackMode, false); // default is system
50+
51+
await settings.setThemeMode(AppThemeMode.black);
52+
expect(settings.isBlackMode, true);
53+
54+
await settings.setThemeMode(AppThemeMode.dark);
55+
expect(settings.isBlackMode, false);
56+
57+
await settings.setThemeMode(AppThemeMode.light);
58+
expect(settings.isBlackMode, false);
59+
60+
await settings.setThemeMode(AppThemeMode.system);
61+
expect(settings.isBlackMode, false);
62+
});
63+
64+
test('flutterThemeMode maps each AppThemeMode to the correct ThemeMode',
65+
() async {
66+
final settings = SettingsProvider();
67+
68+
await settings.setThemeMode(AppThemeMode.system);
69+
expect(settings.flutterThemeMode, ThemeMode.system);
70+
71+
await settings.setThemeMode(AppThemeMode.light);
72+
expect(settings.flutterThemeMode, ThemeMode.light);
73+
74+
await settings.setThemeMode(AppThemeMode.dark);
75+
expect(settings.flutterThemeMode, ThemeMode.dark);
76+
77+
// Black AMOLED mode piggybacks on dark theme
78+
await settings.setThemeMode(AppThemeMode.black);
79+
expect(settings.flutterThemeMode, ThemeMode.dark);
80+
});
81+
82+
test('setMusicEnabled and setSfxEnabled update in-memory state', () async {
83+
final settings = SettingsProvider();
84+
85+
expect(settings.musicEnabled, false);
86+
expect(settings.sfxEnabled, false);
87+
88+
await settings.setMusicEnabled(true);
89+
expect(settings.musicEnabled, true);
90+
91+
await settings.setSfxEnabled(true);
92+
expect(settings.sfxEnabled, true);
93+
94+
await settings.setMusicEnabled(false);
95+
expect(settings.musicEnabled, false);
96+
});
97+
98+
test('setStyle updates the current style', () async {
99+
final settings = SettingsProvider();
100+
101+
expect(settings.style, AppStyle.classic);
102+
103+
await settings.setStyle(AppStyle.neon);
104+
expect(settings.style, AppStyle.neon);
105+
106+
await settings.setStyle(AppStyle.retro);
107+
expect(settings.style, AppStyle.retro);
108+
});
109+
});
110+
}

0 commit comments

Comments
 (0)