42deeaf242
Owner playtesting found that hammering a gem (or line-bombing a gem line) removed it visually but did not satisfy the clear-gems objective and never completed the stage — it felt broken. Per owner decision, booster clears now count: a hammered gem emits gems:1, a line-bomb emits lines:1 + the line's gem count, both folded through the objectives. After any booster the engine resolves the phase (completed objective -> won, else re-check stuck), which was the direct cause of the stage not finishing. Score and the move counter remain untouched. Reverses the earlier no-objective-credit rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
194 lines
5.7 KiB
Dart
194 lines
5.7 KiB
Dart
import 'package:block_seasons/game/engine/game_engine.dart';
|
|
import 'package:block_seasons/game/models/stage.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
StageConfig _stage() => StageConfig.fromJson({
|
|
'id': 'b_test',
|
|
'seed': 1,
|
|
'moveLimit': 20,
|
|
'preset': [
|
|
{'x': 0, 'y': 0, 't': 'filled', 'c': 3},
|
|
{'x': 1, 'y': 0, 't': 'filled', 'c': 4},
|
|
],
|
|
'objectives': [
|
|
{'type': 'reachScore', 'target': 100000},
|
|
],
|
|
'stars': {
|
|
'two': {'movesLeft': 5},
|
|
'three': {'movesLeft': 10},
|
|
},
|
|
});
|
|
|
|
const _stars = {
|
|
'two': {'movesLeft': 5},
|
|
'three': {'movesLeft': 10},
|
|
};
|
|
|
|
// Two gems plus one plain filled cell; objective is to clear both gems.
|
|
StageConfig _gemStage() => StageConfig.fromJson({
|
|
'id': 'b_gem',
|
|
'seed': 1,
|
|
'moveLimit': 20,
|
|
'preset': [
|
|
{'x': 0, 'y': 0, 't': 'gem'},
|
|
{'x': 3, 'y': 3, 't': 'gem'},
|
|
{'x': 5, 'y': 5, 't': 'filled', 'c': 2},
|
|
],
|
|
'objectives': [
|
|
{'type': 'clearGems', 'count': 2},
|
|
],
|
|
'stars': _stars,
|
|
});
|
|
|
|
// Two gems sitting on row 2; objective is to clear both gems.
|
|
StageConfig _gemRowStage() => StageConfig.fromJson({
|
|
'id': 'b_gemrow',
|
|
'seed': 1,
|
|
'moveLimit': 20,
|
|
'preset': [
|
|
{'x': 1, 'y': 2, 't': 'gem'},
|
|
{'x': 4, 'y': 2, 't': 'gem'},
|
|
],
|
|
'objectives': [
|
|
{'type': 'clearGems', 'count': 2},
|
|
],
|
|
'stars': _stars,
|
|
});
|
|
|
|
// Objective is to clear one line.
|
|
StageConfig _lineStage() => StageConfig.fromJson({
|
|
'id': 'b_line',
|
|
'seed': 1,
|
|
'moveLimit': 20,
|
|
'preset': [
|
|
{'x': 0, 'y': 0, 't': 'filled', 'c': 1},
|
|
],
|
|
'objectives': [
|
|
{'type': 'clearLines', 'count': 1},
|
|
],
|
|
'stars': _stars,
|
|
});
|
|
|
|
void main() {
|
|
test('useHammer empties a filled cell without scoring or spending a move', () {
|
|
final e = GameEngine(_stage());
|
|
final score0 = e.score;
|
|
final moves0 = e.movesUsed;
|
|
|
|
expect(e.grid.isOccupied(0, 0), isTrue);
|
|
final ok = e.useHammer(0, 0);
|
|
|
|
expect(ok, isTrue);
|
|
expect(e.grid.isOccupied(0, 0), isFalse);
|
|
expect(e.score, score0, reason: 'no points from a booster');
|
|
expect(e.movesUsed, moves0, reason: 'no move spent');
|
|
});
|
|
|
|
test('useHammer on an empty cell returns false and changes nothing', () {
|
|
final e = GameEngine(_stage());
|
|
expect(e.grid.isOccupied(5, 5), isFalse);
|
|
expect(e.useHammer(5, 5), isFalse);
|
|
expect(e.grid.isOccupied(5, 5), isFalse);
|
|
});
|
|
|
|
test('useHammer is rejected once the stage is won or lost', () {
|
|
final e = GameEngine(_stage());
|
|
e.declineAndLose();
|
|
expect(e.useHammer(0, 0), isFalse);
|
|
});
|
|
|
|
test('useShuffle replaces the tray without spending a move or scoring', () {
|
|
final e = GameEngine(_stage());
|
|
final before = List.of(e.tray);
|
|
final score0 = e.score;
|
|
final moves0 = e.movesUsed;
|
|
|
|
final ok = e.useShuffle();
|
|
|
|
expect(ok, isTrue);
|
|
expect(e.tray.length, before.length);
|
|
expect(e.score, score0);
|
|
expect(e.movesUsed, moves0);
|
|
});
|
|
|
|
test('useShuffle is rejected after the attempt finishes', () {
|
|
final e = GameEngine(_stage());
|
|
e.declineAndLose();
|
|
expect(e.useShuffle(), isFalse);
|
|
});
|
|
|
|
test('useLineBomb(row:) empties that row, no scoring or move', () {
|
|
final e = GameEngine(_stage()); // row 0 has cells at x=0,1
|
|
final score0 = e.score;
|
|
final moves0 = e.movesUsed;
|
|
|
|
final ok = e.useLineBomb(row: 0);
|
|
|
|
expect(ok, isTrue);
|
|
for (var x = 0; x < 8; x++) {
|
|
expect(e.grid.isOccupied(x, 0), isFalse, reason: 'col $x');
|
|
}
|
|
expect(e.score, score0);
|
|
expect(e.movesUsed, moves0);
|
|
});
|
|
|
|
test('useLineBomb(col:) empties that column', () {
|
|
final e = GameEngine(_stage()); // (0,0) filled
|
|
expect(e.useLineBomb(col: 0), isTrue);
|
|
for (var y = 0; y < 8; y++) {
|
|
expect(e.grid.isOccupied(0, y), isFalse, reason: 'row $y');
|
|
}
|
|
});
|
|
|
|
test('useLineBomb requires exactly one of row/col', () {
|
|
final e = GameEngine(_stage());
|
|
expect(e.useLineBomb(), isFalse);
|
|
expect(e.useLineBomb(row: 0, col: 0), isFalse);
|
|
});
|
|
|
|
test('useLineBomb is rejected after the attempt finishes', () {
|
|
final e = GameEngine(_stage());
|
|
e.declineAndLose();
|
|
expect(e.useLineBomb(row: 0), isFalse);
|
|
});
|
|
|
|
// --- Boosters count toward objectives (owner decision 2026-06-18) ---
|
|
|
|
test('useHammer on a gem counts toward the gem objective and wins the stage '
|
|
'when it clears the last one', () {
|
|
final e = GameEngine(_gemStage()); // 2 gems, objective clearGems(2)
|
|
expect(e.objectives.first.current, 0);
|
|
|
|
expect(e.useHammer(0, 0), isTrue); // first gem
|
|
expect(e.objectives.first.current, 1);
|
|
expect(e.phase, GamePhase.playing);
|
|
|
|
expect(e.useHammer(3, 3), isTrue); // last gem -> objective complete
|
|
expect(e.objectives.first.current, 2);
|
|
expect(e.phase, GamePhase.won, reason: 'clearing the last gem wins');
|
|
});
|
|
|
|
test('useHammer on a plain filled cell does not change the gem objective', () {
|
|
final e = GameEngine(_gemStage());
|
|
expect(e.useHammer(5, 5), isTrue); // a non-gem filled cell
|
|
expect(e.objectives.first.current, 0);
|
|
expect(e.phase, GamePhase.playing);
|
|
});
|
|
|
|
test('useLineBomb counts the gems in the cleared line toward the objective',
|
|
() {
|
|
final e = GameEngine(_gemRowStage()); // 2 gems on row 2, clearGems(2)
|
|
expect(e.useLineBomb(row: 2), isTrue);
|
|
expect(e.objectives.first.current, 2);
|
|
expect(e.phase, GamePhase.won);
|
|
});
|
|
|
|
test('useLineBomb counts as a cleared line toward the line objective', () {
|
|
final e = GameEngine(_lineStage()); // clearLines(1)
|
|
expect(e.objectives.first.current, 0);
|
|
expect(e.useLineBomb(row: 0), isTrue);
|
|
expect(e.objectives.first.current, 1);
|
|
expect(e.phase, GamePhase.won);
|
|
});
|
|
}
|