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,46 @@
|
||||
import 'package:block_seasons/game/models/piece_library.dart';
|
||||
import 'package:block_seasons/ui/widgets/board_geometry.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
const geo = BoardGeometry(boardSize: 320); // cell = 40
|
||||
|
||||
group('cellAt', () {
|
||||
test('maps points inside cells', () {
|
||||
expect(geo.cellAt(const Offset(5, 5)), (0, 0));
|
||||
expect(geo.cellAt(const Offset(60, 100)), (1, 2));
|
||||
expect(geo.cellAt(const Offset(319, 319)), (7, 7));
|
||||
});
|
||||
|
||||
test('returns null outside the board', () {
|
||||
expect(geo.cellAt(const Offset(-1, 10)), isNull);
|
||||
expect(geo.cellAt(const Offset(10, 321)), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('cellRect', () {
|
||||
test('returns the drawn rect for a cell', () {
|
||||
final rect = geo.cellRect(2, 3);
|
||||
expect(rect.left, 80);
|
||||
expect(rect.top, 120);
|
||||
expect(rect.width, 40);
|
||||
expect(rect.height, 40);
|
||||
});
|
||||
});
|
||||
|
||||
group('snapAnchor', () {
|
||||
final mono = PieceLibrary.byId('mono');
|
||||
final line5h = PieceLibrary.byId('line5_h');
|
||||
|
||||
test('rounds to the nearest cell', () {
|
||||
expect(geo.snapAnchor(mono, const Offset(78, 122)), (2, 3));
|
||||
expect(geo.snapAnchor(mono, const Offset(99, 99)), (2, 2));
|
||||
});
|
||||
|
||||
test('clamps so the piece bounding box stays on the board', () {
|
||||
// line5_h is 5 wide: anchor x can be at most 3.
|
||||
expect(geo.snapAnchor(line5h, const Offset(310, 0)), (3, 0));
|
||||
expect(geo.snapAnchor(line5h, const Offset(-50, -50)), (0, 0));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:block_seasons/core/rng.dart';
|
||||
import 'package:block_seasons/game/engine/piece_generator.dart';
|
||||
import 'package:block_seasons/game/models/piece_library.dart';
|
||||
import 'package:block_seasons/game/models/stage.dart';
|
||||
import 'package:block_seasons/l10n/gen/app_localizations.dart';
|
||||
import 'package:block_seasons/state/providers.dart';
|
||||
import 'package:block_seasons/ui/screens/game_screen.dart';
|
||||
import 'package:block_seasons/ui/widgets/board_widget.dart';
|
||||
import 'package:block_seasons/ui/widgets/piece_painter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
final _stage = StageConfig.fromJson({
|
||||
'id': 'drag_stage',
|
||||
'seed': 11,
|
||||
'moveLimit': 20,
|
||||
'preset': const <Map<String, dynamic>>[],
|
||||
'objectives': [
|
||||
{'type': 'reachScore', 'target': 999999},
|
||||
],
|
||||
'stars': {
|
||||
'two': {'movesLeft': 5},
|
||||
'three': {'movesLeft': 10},
|
||||
},
|
||||
'generatorProfile': 'mid',
|
||||
});
|
||||
|
||||
PieceGenerator _smallPool() => PieceGenerator(
|
||||
SeededRng(3),
|
||||
pool: [
|
||||
PieceLibrary.byId('mono'),
|
||||
PieceLibrary.byId('domino_h'),
|
||||
PieceLibrary.byId('domino_v'),
|
||||
],
|
||||
);
|
||||
|
||||
void main() {
|
||||
testWidgets('dragging a tray piece onto the board places it',
|
||||
(tester) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
container
|
||||
.read(gameSessionProvider.notifier)
|
||||
.startStage(_stage, generator: _smallPool());
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: GameScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final view0 = container.read(gameSessionProvider)!;
|
||||
final monoIndex = view0.tray.indexWhere((p) => p.id == 'mono');
|
||||
expect(monoIndex, greaterThanOrEqualTo(0));
|
||||
|
||||
final boardRect = tester.getRect(find.byType(BoardWidget));
|
||||
final cell = boardRect.width / 8;
|
||||
|
||||
// The dragged piece floats 70px above the finger, so aim the finger
|
||||
// below the intended landing cell (0, 0).
|
||||
final targetCenter = boardRect.topLeft + Offset(cell * 0.5, cell * 0.5);
|
||||
final fingerEnd = targetCenter + Offset(0, 70 + cell / 2);
|
||||
|
||||
final start = tester.getCenter(find.byType(PieceWidget).at(monoIndex));
|
||||
final gesture = await tester.startGesture(start);
|
||||
await gesture.moveBy(const Offset(0, -30));
|
||||
await tester.pump();
|
||||
await gesture.moveTo(fingerEnd);
|
||||
await tester.pump();
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final view = container.read(gameSessionProvider)!;
|
||||
expect(view.grid.isOccupied(0, 0), isTrue,
|
||||
reason: 'mono should land on (0,0)');
|
||||
expect(view.tray, hasLength(2));
|
||||
expect(view.score, 1);
|
||||
});
|
||||
|
||||
testWidgets('result overlay appears when out of moves', (tester) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
final oneMove = StageConfig.fromJson({
|
||||
..._stage.toJson(),
|
||||
'moveLimit': 1,
|
||||
});
|
||||
container
|
||||
.read(gameSessionProvider.notifier)
|
||||
.startStage(oneMove, generator: _smallPool());
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: GameScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
container.read(gameSessionProvider.notifier).tryPlace(0, 0, 0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Out of moves'), findsOneWidget);
|
||||
expect(find.text('+5 moves (ad)'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user