import 'package:block_seasons/core/rng.dart'; import 'package:block_seasons/game/engine/game_engine.dart'; import 'package:block_seasons/game/engine/piece_generator.dart'; import 'package:block_seasons/game/models/cell.dart'; import 'package:block_seasons/game/models/grid.dart'; import 'package:block_seasons/game/models/piece_library.dart'; import 'package:block_seasons/game/models/stage.dart'; import 'package:block_seasons/state/providers.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; StageConfig _stage({List>? objectives}) => StageConfig.fromJson({ 'id': 'ui_stage', 'seed': 99, 'moveLimit': 20, 'preset': [ for (var x = 1; x < GridState.size; x++) {'x': x, 'y': 3, 't': 'filled', 'c': 0}, ], 'objectives': objectives ?? [ {'type': 'reachScore', 'target': 999999}, ], 'stars': { 'two': {'movesLeft': 5}, 'three': {'movesLeft': 10}, }, 'generatorProfile': 'mid', }); PieceGenerator _smallPool() => PieceGenerator( SeededRng(1), pool: [ PieceLibrary.byId('mono'), PieceLibrary.byId('domino_h'), PieceLibrary.byId('domino_v'), ], ); void main() { test('starts idle with no session', () { final container = ProviderContainer(); addTearDown(container.dispose); expect(container.read(gameSessionProvider), isNull); }); test('startStage exposes a fresh view state', () { final container = ProviderContainer(); addTearDown(container.dispose); final notifier = container.read(gameSessionProvider.notifier); notifier.startStage(_stage(), generator: _smallPool()); final view = container.read(gameSessionProvider)!; expect(view.phase, GamePhase.playing); expect(view.tray, hasLength(3)); expect(view.score, 0); expect(view.movesLeft, 20); expect(view.grid.cellAt(1, 3).type, CellType.filled); expect(view.fxTick, 0); }); test('tryPlace mutates the view and bumps fxTick', () { final container = ProviderContainer(); addTearDown(container.dispose); final notifier = container.read(gameSessionProvider.notifier); notifier.startStage(_stage(), generator: _smallPool()); final placed = notifier.tryPlace(0, 0, 0); expect(placed, isTrue); final view = container.read(gameSessionProvider)!; expect(view.tray, hasLength(2)); expect(view.score, greaterThan(0)); expect(view.fxTick, 1); expect(view.lastPlacement, isNotNull); }); test('illegal placement leaves state untouched', () { final container = ProviderContainer(); addTearDown(container.dispose); final notifier = container.read(gameSessionProvider.notifier); notifier.startStage(_stage(), generator: _smallPool()); final placed = notifier.tryPlace(0, 1, 3); // occupied preset cell expect(placed, isFalse); final view = container.read(gameSessionProvider)!; expect(view.tray, hasLength(3)); expect(view.fxTick, 0); }); test('winning surfaces stars and cleared lines fx', () { final container = ProviderContainer(); addTearDown(container.dispose); final notifier = container.read(gameSessionProvider.notifier); notifier.startStage( _stage(objectives: [ {'type': 'clearLines', 'count': 1}, ]), generator: _smallPool(), ); final view0 = container.read(gameSessionProvider)!; final monoIndex = view0.tray.indexWhere((p) => p.id == 'mono'); notifier.tryPlace(monoIndex, 0, 3); final view = container.read(gameSessionProvider)!; expect(view.phase, GamePhase.won); expect(view.starsEarned, 3); expect(view.lastPlacement!.clearedRows, [3]); }); test('restart deals a fresh attempt with a different tray sequence', () { final container = ProviderContainer(); addTearDown(container.dispose); final notifier = container.read(gameSessionProvider.notifier); notifier.startStage(_stage()); notifier.tryPlace(0, 0, 0); notifier.restart(); final view = container.read(gameSessionProvider)!; expect(view.phase, GamePhase.playing); expect(view.score, 0); expect(view.movesLeft, 20); expect(view.tray, hasLength(3)); }); }