feat(state): coordinate booster use with inventory in the session
This commit is contained in:
@@ -2,12 +2,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import '../game/engine/game_engine.dart';
|
import '../game/engine/game_engine.dart';
|
||||||
import '../game/engine/piece_generator.dart';
|
import '../game/engine/piece_generator.dart';
|
||||||
|
import '../game/models/booster.dart';
|
||||||
import '../game/models/grid.dart';
|
import '../game/models/grid.dart';
|
||||||
import '../game/models/objective.dart';
|
import '../game/models/objective.dart';
|
||||||
import '../game/models/piece.dart';
|
import '../game/models/piece.dart';
|
||||||
import '../game/models/stage.dart';
|
import '../game/models/stage.dart';
|
||||||
import 'providers.dart';
|
import 'providers.dart';
|
||||||
|
|
||||||
|
enum BoosterUseResult { success, noBooster, invalidTarget }
|
||||||
|
|
||||||
/// Immutable snapshot of one engine moment; the only game state the UI sees.
|
/// Immutable snapshot of one engine moment; the only game state the UI sees.
|
||||||
class GameViewState {
|
class GameViewState {
|
||||||
const GameViewState({
|
const GameViewState({
|
||||||
@@ -117,6 +120,41 @@ class GameSessionNotifier extends Notifier<GameViewState?> {
|
|||||||
_publish(lastPlacement: null);
|
_publish(lastPlacement: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<BoosterUseResult> useHammer(int x, int y) =>
|
||||||
|
_useBooster(BoosterType.hammer, () {
|
||||||
|
final engine = _engine;
|
||||||
|
if (engine == null) return false;
|
||||||
|
return engine.useHammer(x, y);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<BoosterUseResult> useShuffle() =>
|
||||||
|
_useBooster(BoosterType.shuffle, () {
|
||||||
|
final engine = _engine;
|
||||||
|
if (engine == null) return false;
|
||||||
|
return engine.useShuffle();
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<BoosterUseResult> useLineBomb({int? row, int? col}) =>
|
||||||
|
_useBooster(BoosterType.lineBomb, () {
|
||||||
|
final engine = _engine;
|
||||||
|
if (engine == null) return false;
|
||||||
|
return engine.useLineBomb(row: row, col: col);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<BoosterUseResult> _useBooster(
|
||||||
|
BoosterType type, bool Function() apply) async {
|
||||||
|
final engine = _engine;
|
||||||
|
if (engine == null) return BoosterUseResult.noBooster;
|
||||||
|
final inv = ref.read(boosterInventoryProvider.notifier);
|
||||||
|
if ((ref.read(boosterInventoryProvider)[type] ?? 0) <= 0) {
|
||||||
|
return BoosterUseResult.noBooster;
|
||||||
|
}
|
||||||
|
if (!apply()) return BoosterUseResult.invalidTarget;
|
||||||
|
await inv.consume(type);
|
||||||
|
_publish(lastPlacement: null);
|
||||||
|
return BoosterUseResult.success;
|
||||||
|
}
|
||||||
|
|
||||||
void _publish({required PlacementResult? lastPlacement}) {
|
void _publish({required PlacementResult? lastPlacement}) {
|
||||||
final engine = _engine!;
|
final engine = _engine!;
|
||||||
state = GameViewState(
|
state = GameViewState(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:block_seasons/data/save_repository.dart';
|
import 'package:block_seasons/data/save_repository.dart';
|
||||||
import 'package:block_seasons/game/models/booster.dart';
|
import 'package:block_seasons/game/models/booster.dart';
|
||||||
import 'package:block_seasons/state/booster_inventory_notifier.dart';
|
|
||||||
import 'package:block_seasons/state/providers.dart';
|
import 'package:block_seasons/state/providers.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:block_seasons/data/save_repository.dart';
|
||||||
|
import 'package:block_seasons/game/engine/piece_generator.dart';
|
||||||
|
import 'package:block_seasons/core/rng.dart';
|
||||||
|
import 'package:block_seasons/game/models/booster.dart';
|
||||||
|
import 'package:block_seasons/game/models/stage.dart';
|
||||||
|
import 'package:block_seasons/state/game_session_notifier.dart';
|
||||||
|
import 'package:block_seasons/state/providers.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
StageConfig _stage() => StageConfig.fromJson({
|
||||||
|
'id': 'gs_b',
|
||||||
|
'seed': 1,
|
||||||
|
'moveLimit': 20,
|
||||||
|
'preset': [
|
||||||
|
{'x': 0, 'y': 0, 't': 'filled', 'c': 3},
|
||||||
|
],
|
||||||
|
'objectives': [
|
||||||
|
{'type': 'reachScore', 'target': 100000},
|
||||||
|
],
|
||||||
|
'stars': {
|
||||||
|
'two': {'movesLeft': 5},
|
||||||
|
'three': {'movesLeft': 10},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
Future<ProviderContainer> container() async {
|
||||||
|
SharedPreferences.setMockInitialValues({});
|
||||||
|
final repo = SaveRepository(await SharedPreferences.getInstance());
|
||||||
|
final c = ProviderContainer(
|
||||||
|
overrides: [saveRepositoryProvider.overrideWithValue(repo)],
|
||||||
|
);
|
||||||
|
c.read(gameSessionProvider.notifier)
|
||||||
|
.startStage(_stage(), generator: PieceGenerator(SeededRng(1)));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('hammer consumes a booster only when one is owned and target valid',
|
||||||
|
() async {
|
||||||
|
final c = await container();
|
||||||
|
final session = c.read(gameSessionProvider.notifier);
|
||||||
|
final inv = c.read(boosterInventoryProvider.notifier);
|
||||||
|
|
||||||
|
// No booster -> noBooster, grid unchanged.
|
||||||
|
expect(await session.useHammer(0, 0), BoosterUseResult.noBooster);
|
||||||
|
|
||||||
|
await inv.grant(BoosterType.hammer, 1);
|
||||||
|
expect(await session.useHammer(0, 0), BoosterUseResult.success);
|
||||||
|
expect(c.read(boosterInventoryProvider)[BoosterType.hammer], 0);
|
||||||
|
expect(c.read(gameSessionProvider)!.grid.isOccupied(0, 0), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an invalid target keeps the booster', () async {
|
||||||
|
final c = await container();
|
||||||
|
final session = c.read(gameSessionProvider.notifier);
|
||||||
|
await c.read(boosterInventoryProvider.notifier).grant(BoosterType.hammer, 1);
|
||||||
|
|
||||||
|
expect(await session.useHammer(5, 5), BoosterUseResult.invalidTarget);
|
||||||
|
expect(c.read(boosterInventoryProvider)[BoosterType.hammer], 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user