fix(boosters): count hammer/line-bomb clears toward objectives and win

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>
This commit is contained in:
2026-06-18 20:15:00 +09:00
parent 1695684fc9
commit 42deeaf242
2 changed files with 119 additions and 9 deletions
+89
View File
@@ -19,6 +19,56 @@ StageConfig _stage() => StageConfig.fromJson({
},
});
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());
@@ -101,4 +151,43 @@ void main() {
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);
});
}