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:
1 parent
638549161f
commit
8467c7929e
28 files changed
+293
-32
No files matched your search
@@ -5,8 +5,8 @@ The course's own engine as of the end of Module 8. Part I's card engine is uncha
|
||||
- `cards/` (at the package root; not in this directory): Part I's card data, a copy of `course/cards/` staged by `tools/verify-references.mjs`. Part II doesn't read it.
|
||||
- `src/engine/puzzle/`: the Part II engine. It names no noun: the engine's own words are the operators `IS`, `AND`, `NOT`, the meta-noun `TEXT`, the property `PUSH` of the base rule `TEXT IS PUSH`, and from Module 8 the property `YOU`, which picks the objects that move.
|
||||
- `grid.ts`: the level format, object kinds and the board. Module 8 adds `Board.find`, `Board.remove`, `Board.shift`, `Board.at` and `Kind.format` (the kind as placed, for `kindsAt`).
|
||||
- `movement.ts` (new): validate, then commit. `Planner` (`tryMove`, `stayingAt`, `destroy`, `has`), `Step`, `Plan`, `Response` and `Dir`. The planner reads the board and plans one step at a time; for each step it runs content's movement responses in order, and rolls back everything a step added if a response blocks it. The edge of the level blocks every step.
|
||||
- `level.ts`: a level in play. Module 8 adds `Level.moveYou`: the `YOU` objects move one at a time, furthest ahead first, each planned and then committed; the rules are reparsed once at the end. `hasProperty` is `false` for a destroyed object.
|
||||
- `movement.ts` (new): validate, then commit. `Planner` (`tryMove`, `stayingAt`, `destroy`, `has`), `Step`, `Plan`, `Response` and `Dir`. The planner reads the board and plans one step at a time; for each step it runs content's movement responses in order, and rolls back everything a step added if a response blocks it. The edge of the level blocks every step, and an object that has already stepped in the same move can't step again.
|
||||
- `level.ts`: a level in play. Module 8 adds `Level.moveYou`: the `YOU` objects move one at a time, furthest ahead first, each planned and then committed, and an object steps at most once per move (a `YOU` object that an earlier move carried along doesn't start its own); the rules are reparsed once at the end. `hasProperty` is `false` for a destroyed object.
|
||||
- `word.ts`, `parse.ts`, `resolve.ts`: unchanged.
|
||||
- `index.ts`: the module's exports.
|
||||
- The rest of `src/engine/`: Part I, unchanged.
|
||||
|
||||
@@ -74,6 +74,10 @@ export class Level {
|
||||
* move one at a time, the one furthest ahead in `dir` first (oldest first on a
|
||||
* tie); each one's move is planned in full, then committed, before the next is
|
||||
* planned. Every move uses the rules in force before the first.
|
||||
*
|
||||
* An object steps at most once in a movement phase. A `YOU` object that an
|
||||
* earlier move already carried along (pushed, or moved by any other response)
|
||||
* doesn't start a move of its own, and no later move can make it step again.
|
||||
*/
|
||||
moveYou(dir: Dir): void {
|
||||
const [dx, dy] = delta(dir);
|
||||
@@ -83,10 +87,15 @@ export class Level {
|
||||
.filter((o) => this.hasProperty(o.id, YOU))
|
||||
.sort((a, b) => ahead(b) - ahead(a) || a.id - b.id)
|
||||
.map((o) => o.id);
|
||||
const stepped = new Set<ObjectId>();
|
||||
for (const id of you) {
|
||||
if (this.board.find(id) === undefined) continue; // destroyed by an earlier move
|
||||
const planner = new Planner(this, dir);
|
||||
if (planner.tryMove(id)) this.commit(planner.result(), dir);
|
||||
if (stepped.has(id)) continue; // already moved, by an earlier move
|
||||
const planner = new Planner(this, dir, stepped);
|
||||
if (!planner.tryMove(id)) continue;
|
||||
const plan = planner.result();
|
||||
this.commit(plan, dir);
|
||||
for (const moved of plan.moving) if (!plan.destroyed.has(moved)) stepped.add(moved);
|
||||
}
|
||||
this.reparse();
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user