Add playable core UI: board painter, drag-and-drop, HUD, result overlay
CustomPainter board with gems/ghost/clear-flash, finger-lifted drag with snap preview, combo text effect, HUD chips, phase overlays with rescue stubs, demo stage. E2E widget test drives a real drag gesture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
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<Map<String, dynamic>>? 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));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user