fix(puzzle): an object steps once per movement phase; negated conversion is settled per object

Bug 1 (modules 08-10): a YOU object pulled by an earlier mover took a second step of its own. Level movement now keeps the ids that stepped in a committed plan this phase; such a YOU object starts no move, and Planner try_move answers no for it. Regression: test_08x_pulled_you_moves_once (Module 8 Expansion Drop).

Bug 2 (modules 09-10): convert honoured negation only per rule, so ALL IS ROCK beat BABA IS NOT ROCK. A target Y is now skipped for an object if any active negated rule IS NOT Y names it. Module 9 and 10 references (and their drop solutions) now carry ALL, the Module 7 Expansion Drop. Regression: test_09_negated_conversion_beats_group.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Fable 5.2 committed 2026-11-11 09:18:00 +01:00
1 parent 638549161f
commit 8467c7929e
28 files changed
+293 -32

No files matched your search

@@ -63,13 +63,18 @@ export interface MoveContext {
responses(): readonly Response[];
}
/** Plans one move on a board it only reads. */
/**
* Plans one move on a board it only reads. `stepped` holds the objects that have
* already stepped in this movement phase, in an earlier committed plan: an object
* steps at most once per phase, so none of them can step again.
*/
export class Planner {
private plan: Plan = { moving: new Set(), destroyed: new Set() };
constructor(
private readonly level: MoveContext,
private readonly dir: Dir,
private readonly stepped: ReadonlySet<ObjectId>,
) {}
/** Does object `id` have `property`, under the rules in force before the move? */
@@ -98,10 +103,13 @@ export class Planner {
* Plan object `id` stepping one tile in the move's direction, with whatever
* the responses add. Returns whether it can. If it can't, the plan is left as
* it was before the call. An object already planned to move, or to be
* destroyed, is out of the way: the answer is yes, and nothing is added.
* destroyed, is out of the way: the answer is yes, and nothing is added. An
* object that has already stepped in this movement phase can't step again: the
* answer is no.
*/
tryMove(id: ObjectId): boolean {
if (this.plan.moving.has(id) || this.plan.destroyed.has(id)) return true;
if (this.stepped.has(id)) return false;
const o = this.level.board.get(id);
const [dx, dy] = delta(this.dir);
const to: Pos = [o.x + dx, o.y + dy];