Merge branch 'worktree-agent-ab151ac84f067999f' into revise/edge-cases-and-sources
This commit is contained in:
commit
d5d742450c
28 files changed
+293
-32
No files matched your search
@@ -23,3 +23,9 @@ The reference lets a PULL object that can't follow stay behind without stopping
|
|||||||
- Assertions 2: nothing moved: Baba is still at (3,2), the near rock at (2,2) and the far rock at (1,2). A PULL object is solid: Baba can't step onto it, and PULL doesn't push.
|
- Assertions 2: nothing moved: Baba is still at (3,2), the near rock at (2,2) and the far rock at (1,2). A PULL object is solid: Baba can't step onto it, and PULL doesn't push.
|
||||||
|
|
||||||
Before the drop the lexicon doesn't know `PULL`, so `ROCK IS PULL` spells nothing: Baba walks away from the rocks, and in the second move walks back onto the near rock's tile.
|
Before the drop the lexicon doesn't know `PULL`, so `ROCK IS PULL` spells nothing: Baba walks away from the rocks, and in the second move walks back onto the near rock's tile.
|
||||||
|
|
||||||
|
### test_08x_pulled_you_moves_once
|
||||||
|
|
||||||
|
- Setup: `newBaba` with an empty level 5 wide and 4 high. Place `BABA (0,0) IS (1,0) YOU (2,0)` and `KEY (0,1) IS (1,1) YOU (2,1) AND (3,1) PULL (4,1)`. Place an object `key` at (1,3) and an object `baba` at (2,3).
|
||||||
|
- Action: `move("right")`.
|
||||||
|
- Assertions: `posOf(baba)` is (3,3) and `posOf(key)` is (2,3). Baba is furthest ahead, so it moves first, and the key behind it follows. The key is `YOU` as well, but it has already stepped in this move, and an object steps at most once per move: it doesn't take a second step of its own onto Baba's tile.
|
||||||
@@ -30,3 +30,25 @@ fn test_08x_pull_follows() {
|
|||||||
assert_eq!(b.pos_of(near_rock), Some((2, 2)));
|
assert_eq!(b.pos_of(near_rock), Some((2, 2)));
|
||||||
assert_eq!(b.pos_of(far_rock), Some((1, 2)));
|
assert_eq!(b.pos_of(far_rock), Some((1, 2)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_08x_pulled_you_moves_once() {
|
||||||
|
let mut b = new_baba(LevelDef {
|
||||||
|
width: 5,
|
||||||
|
height: 4,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
for (word, x) in [("BABA", 0), ("IS", 1), ("YOU", 2)] {
|
||||||
|
b.place(&format!("text:{word}"), x, 0);
|
||||||
|
}
|
||||||
|
for (word, x) in [("KEY", 0), ("IS", 1), ("YOU", 2), ("AND", 3), ("PULL", 4)] {
|
||||||
|
b.place(&format!("text:{word}"), x, 1);
|
||||||
|
}
|
||||||
|
let key = b.place("key", 1, 3);
|
||||||
|
let baba = b.place("baba", 2, 3);
|
||||||
|
|
||||||
|
b.move_dir(Dir::Right);
|
||||||
|
|
||||||
|
assert_eq!(b.pos_of(baba), Some((3, 3)));
|
||||||
|
assert_eq!(b.pos_of(key), Some((2, 3)));
|
||||||
|
}
|
||||||
@@ -34,3 +34,30 @@ test("test_08x_pull_follows", () => {
|
|||||||
expect(b.posOf(nearRock)).toEqual([2, 2]);
|
expect(b.posOf(nearRock)).toEqual([2, 2]);
|
||||||
expect(b.posOf(farRock)).toEqual([1, 2]);
|
expect(b.posOf(farRock)).toEqual([1, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("test_08x_pulled_you_moves_once", () => {
|
||||||
|
const b = newBaba({ width: 5, height: 4, objects: [] });
|
||||||
|
for (const [word, x] of [
|
||||||
|
["BABA", 0],
|
||||||
|
["IS", 1],
|
||||||
|
["YOU", 2],
|
||||||
|
] as const) {
|
||||||
|
b.place(`text:${word}`, x, 0);
|
||||||
|
}
|
||||||
|
for (const [word, x] of [
|
||||||
|
["KEY", 0],
|
||||||
|
["IS", 1],
|
||||||
|
["YOU", 2],
|
||||||
|
["AND", 3],
|
||||||
|
["PULL", 4],
|
||||||
|
] as const) {
|
||||||
|
b.place(`text:${word}`, x, 1);
|
||||||
|
}
|
||||||
|
const key = b.place("key", 1, 3);
|
||||||
|
const baba = b.place("baba", 2, 3);
|
||||||
|
|
||||||
|
b.move("right");
|
||||||
|
|
||||||
|
expect(b.posOf(baba)).toEqual([3, 3]);
|
||||||
|
expect(b.posOf(key)).toEqual([2, 3]);
|
||||||
|
});
|
||||||
@@ -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 crate 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.
|
- `cards/` (at the crate 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.
|
- `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.rs`: the level format, object kinds and the board. Module 8 adds `Board::find`, `Board::remove`, `Board::shift`, `Board::at` and `Display` for `Kind` (the kind as placed, for `kinds_at`).
|
- `grid.rs`: the level format, object kinds and the board. Module 8 adds `Board::find`, `Board::remove`, `Board::shift`, `Board::at` and `Display` for `Kind` (the kind as placed, for `kinds_at`).
|
||||||
- `movement.rs` (new): validate, then commit. `Planner` (`try_move`, `staying_at`, `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.
|
- `movement.rs` (new): validate, then commit. `Planner` (`try_move`, `staying_at`, `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.rs`: a level in play. Module 8 adds `Level::move_you`: the `YOU` objects move one at a time, furthest ahead first, each planned and then committed; the rules are reparsed once at the end. `has_property` is `false` for a destroyed object.
|
- `level.rs`: a level in play. Module 8 adds `Level::move_you`: 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. `has_property` is `false` for a destroyed object.
|
||||||
- `word.rs`, `parse.rs`, `resolve.rs`: unchanged.
|
- `word.rs`, `parse.rs`, `resolve.rs`: unchanged.
|
||||||
- `mod.rs`: the module's exports.
|
- `mod.rs`: the module's exports.
|
||||||
- The rest of `src/engine/`: Part I, unchanged.
|
- The rest of `src/engine/`: Part I, unchanged.
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
//! A move (Module 8) is planned for each `YOU` object, then committed; the rules are
|
//! A move (Module 8) is planned for each `YOU` object, then committed; the rules are
|
||||||
//! reparsed once, after all of them.
|
//! reparsed once, after all of them.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use super::grid::{Board, Kind, LevelDef, ObjectId};
|
use super::grid::{Board, Kind, LevelDef, ObjectId};
|
||||||
use super::movement::{Dir, Plan, Planner, Response};
|
use super::movement::{Dir, Plan, Planner, Response};
|
||||||
use super::parse::{parse, Rule};
|
use super::parse::{parse, Rule};
|
||||||
@@ -86,6 +88,10 @@ impl Level {
|
|||||||
/// move one at a time, the one furthest ahead in `dir` first (oldest first on a
|
/// 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
|
/// 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.
|
/// 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.
|
||||||
pub fn move_you(&mut self, dir: Dir) {
|
pub fn move_you(&mut self, dir: Dir) {
|
||||||
let (dx, dy) = dir.delta();
|
let (dx, dy) = dir.delta();
|
||||||
let mut you: Vec<_> = self
|
let mut you: Vec<_> = self
|
||||||
@@ -95,14 +101,19 @@ impl Level {
|
|||||||
.map(|o| (-(o.x * dx + o.y * dy), o.id))
|
.map(|o| (-(o.x * dx + o.y * dy), o.id))
|
||||||
.collect();
|
.collect();
|
||||||
you.sort();
|
you.sort();
|
||||||
|
let mut stepped = BTreeSet::new();
|
||||||
for (_, id) in you {
|
for (_, id) in you {
|
||||||
if self.board.find(id).is_none() {
|
if self.board.find(id).is_none() {
|
||||||
continue; // destroyed by an earlier move
|
continue; // destroyed by an earlier move
|
||||||
}
|
}
|
||||||
let mut planner = Planner::new(self, dir);
|
if stepped.contains(&id) {
|
||||||
|
continue; // already moved, by an earlier move
|
||||||
|
}
|
||||||
|
let mut planner = Planner::new(self, dir, &stepped);
|
||||||
if planner.try_move(id) {
|
if planner.try_move(id) {
|
||||||
let plan = planner.into_plan();
|
let plan = planner.into_plan();
|
||||||
self.commit(&plan, dir);
|
self.commit(&plan, dir);
|
||||||
|
stepped.extend(plan.moving.difference(&plan.destroyed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.reparse();
|
self.reparse();
|
||||||
|
|||||||
@@ -59,18 +59,22 @@ pub struct Plan {
|
|||||||
pub destroyed: BTreeSet<ObjectId>,
|
pub destroyed: BTreeSet<ObjectId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
pub struct Planner<'a> {
|
pub struct Planner<'a> {
|
||||||
level: &'a Level,
|
level: &'a Level,
|
||||||
dir: Dir,
|
dir: Dir,
|
||||||
|
stepped: &'a BTreeSet<ObjectId>,
|
||||||
plan: Plan,
|
plan: Plan,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Planner<'a> {
|
impl<'a> Planner<'a> {
|
||||||
pub fn new(level: &'a Level, dir: Dir) -> Planner<'a> {
|
pub fn new(level: &'a Level, dir: Dir, stepped: &'a BTreeSet<ObjectId>) -> Planner<'a> {
|
||||||
Planner {
|
Planner {
|
||||||
level,
|
level,
|
||||||
dir,
|
dir,
|
||||||
|
stepped,
|
||||||
plan: Plan::default(),
|
plan: Plan::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,11 +104,16 @@ impl<'a> Planner<'a> {
|
|||||||
/// Plan object `id` stepping one tile in the move's direction, with whatever
|
/// 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
|
/// 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
|
/// 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.
|
||||||
pub fn try_move(&mut self, id: ObjectId) -> bool {
|
pub fn try_move(&mut self, id: ObjectId) -> bool {
|
||||||
if self.plan.moving.contains(&id) || self.plan.destroyed.contains(&id) {
|
if self.plan.moving.contains(&id) || self.plan.destroyed.contains(&id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if self.stepped.contains(&id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let level = self.level;
|
let level = self.level;
|
||||||
let o = level.board().get(id);
|
let o = level.board().get(id);
|
||||||
let (dx, dy) = self.dir.delta();
|
let (dx, dy) = self.dir.delta();
|
||||||
|
|||||||
@@ -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.
|
- `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.
|
- `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`).
|
- `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.
|
- `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; the rules are reparsed once at the end. `hasProperty` is `false` for a destroyed object.
|
- `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.
|
- `word.ts`, `parse.ts`, `resolve.ts`: unchanged.
|
||||||
- `index.ts`: the module's exports.
|
- `index.ts`: the module's exports.
|
||||||
- The rest of `src/engine/`: Part I, unchanged.
|
- 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
|
* 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
|
* 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.
|
* 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 {
|
moveYou(dir: Dir): void {
|
||||||
const [dx, dy] = delta(dir);
|
const [dx, dy] = delta(dir);
|
||||||
@@ -83,10 +87,15 @@ export class Level {
|
|||||||
.filter((o) => this.hasProperty(o.id, YOU))
|
.filter((o) => this.hasProperty(o.id, YOU))
|
||||||
.sort((a, b) => ahead(b) - ahead(a) || a.id - b.id)
|
.sort((a, b) => ahead(b) - ahead(a) || a.id - b.id)
|
||||||
.map((o) => o.id);
|
.map((o) => o.id);
|
||||||
|
const stepped = new Set<ObjectId>();
|
||||||
for (const id of you) {
|
for (const id of you) {
|
||||||
if (this.board.find(id) === undefined) continue; // destroyed by an earlier move
|
if (this.board.find(id) === undefined) continue; // destroyed by an earlier move
|
||||||
const planner = new Planner(this, dir);
|
if (stepped.has(id)) continue; // already moved, by an earlier move
|
||||||
if (planner.tryMove(id)) this.commit(planner.result(), dir);
|
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();
|
this.reparse();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,13 +63,18 @@ export interface MoveContext {
|
|||||||
responses(): readonly Response[];
|
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 {
|
export class Planner {
|
||||||
private plan: Plan = { moving: new Set(), destroyed: new Set() };
|
private plan: Plan = { moving: new Set(), destroyed: new Set() };
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly level: MoveContext,
|
private readonly level: MoveContext,
|
||||||
private readonly dir: Dir,
|
private readonly dir: Dir,
|
||||||
|
private readonly stepped: ReadonlySet<ObjectId>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Does object `id` have `property`, under the rules in force before the move? */
|
/** 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
|
* 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
|
* 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
|
* 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 {
|
tryMove(id: ObjectId): boolean {
|
||||||
if (this.plan.moving.has(id) || this.plan.destroyed.has(id)) return true;
|
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 o = this.level.board.get(id);
|
||||||
const [dx, dy] = delta(this.dir);
|
const [dx, dy] = delta(this.dir);
|
||||||
const to: Pos = [o.x + dx, o.y + dy];
|
const to: Pos = [o.x + dx, o.y + dy];
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
{ "name": "DOOR", "role": "noun" },
|
{ "name": "DOOR", "role": "noun" },
|
||||||
{ "name": "SKULL", "role": "noun" },
|
{ "name": "SKULL", "role": "noun" },
|
||||||
{ "name": "WATER", "role": "noun" },
|
{ "name": "WATER", "role": "noun" },
|
||||||
|
{ "name": "ALL", "role": "noun", "selects": "things" },
|
||||||
{ "name": "YOU", "role": "property" },
|
{ "name": "YOU", "role": "property" },
|
||||||
{ "name": "WIN", "role": "property" },
|
{ "name": "WIN", "role": "property" },
|
||||||
{ "name": "PUSH", "role": "property" },
|
{ "name": "PUSH", "role": "property" },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ The course's own engine as of the end of Module 9. Part I's card engine is uncha
|
|||||||
- `cards/` (at the crate 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.
|
- `cards/` (at the crate 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`, the property `YOU`, which picks the objects that move, and from Module 9 the properties `WIN` (the win check) and `FLOAT` (which objects share a tile).
|
- `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`, the property `YOU`, which picks the objects that move, and from Module 9 the properties `WIN` (the win check) and `FLOAT` (which objects share a tile).
|
||||||
- `turn.rs` (new): the turn. `Phase`, `TURN` (the phases in order) and `Hazard` (content's rule for what destroys what).
|
- `turn.rs` (new): the turn. `Phase`, `TURN` (the phases in order) and `Hazard` (content's rule for what destroys what).
|
||||||
- `level.rs`: a level in play. Module 9 adds `Level::step`, which runs the phases of `TURN`; the conversion phase (`X IS Y` in place, `X IS X` locks, `X IS NOT X` deletes); the hazards phase, which runs content's hazards in order; the win check; `Level::sharing` (the other objects on a tile at the same float level); `won` and `lost`. `Level::new` takes the hazards. `move_you` is now movement, then a reparse.
|
- `level.rs`: a level in play. Module 9 adds `Level::step`, which runs the phases of `TURN`; the conversion phase (`X IS Y` in place, `X IS X` locks, `X IS NOT X` deletes, and `... IS NOT Y` keeps every object it names from becoming a `Y`, as `has_property` settles negation per object); the hazards phase, which runs content's hazards in order; the win check; `Level::sharing` (the other objects on a tile at the same float level); `won` and `lost`. `Level::new` takes the hazards. `move_you` is now movement, then a reparse.
|
||||||
- `grid.rs`: Module 9 adds `Board::set_kind`.
|
- `grid.rs`: Module 9 adds `Board::set_kind`.
|
||||||
- `word.rs`: Module 9 adds `Lexicon::thing_named` (what a conversion into a noun makes).
|
- `word.rs`: Module 9 adds `Lexicon::thing_named` (what a conversion into a noun makes).
|
||||||
- `movement.rs`, `parse.rs`, `resolve.rs`: unchanged.
|
- `movement.rs`, `parse.rs`, `resolve.rs`: unchanged.
|
||||||
@@ -13,7 +13,7 @@ The course's own engine as of the end of Module 9. Part I's card engine is uncha
|
|||||||
- The rest of `src/engine/`: Part I, unchanged.
|
- The rest of `src/engine/`: Part I, unchanged.
|
||||||
- `src/content/hazards.rs` (new) and `src/content/mod.rs`: the hazards, in the order the engine runs them: `SINK`, `HOT`/`MELT`, `DEFEAT`. What destroys what at the end of a turn is content, not engine.
|
- `src/content/hazards.rs` (new) and `src/content/mod.rs`: the hazards, in the order the engine runs them: `SINK`, `HOT`/`MELT`, `DEFEAT`. What destroys what at the end of a turn is content, not engine.
|
||||||
- `src/content/movement.rs`: unchanged.
|
- `src/content/movement.rs`: unchanged.
|
||||||
- `src/content/words.json`: Module 9 adds `HOT`, `MELT`, `SINK`, `DEFEAT` and `FLOAT`.
|
- `src/content/words.json`: Module 9 adds `HOT`, `MELT`, `SINK`, `DEFEAT` and `FLOAT`, and `ALL`, the Module 7 Expansion Drop, which the Module 9 tests need.
|
||||||
- `src/harness.rs`: the adapter the course tests call. Module 9 adds `Baba::step`, `Baba::won` and `Baba::lost`.
|
- `src/harness.rs`: the adapter the course tests call. Module 9 adds `Baba::step`, `Baba::won` and `Baba::lost`.
|
||||||
|
|
||||||
Run it with the module's tests: `node tools/verify-references.mjs 09` from the course repository.
|
Run it with the module's tests: `node tools/verify-references.mjs 09` from the course repository.
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
{ "name": "DOOR", "role": "noun" },
|
{ "name": "DOOR", "role": "noun" },
|
||||||
{ "name": "SKULL", "role": "noun" },
|
{ "name": "SKULL", "role": "noun" },
|
||||||
{ "name": "WATER", "role": "noun" },
|
{ "name": "WATER", "role": "noun" },
|
||||||
|
{ "name": "ALL", "role": "noun", "selects": "things" },
|
||||||
{ "name": "YOU", "role": "property" },
|
{ "name": "YOU", "role": "property" },
|
||||||
{ "name": "WIN", "role": "property" },
|
{ "name": "WIN", "role": "property" },
|
||||||
{ "name": "PUSH", "role": "property" },
|
{ "name": "PUSH", "role": "property" },
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
//! A turn (Module 9) runs the phases of `TURN` in order: movement, parse,
|
//! A turn (Module 9) runs the phases of `TURN` in order: movement, parse,
|
||||||
//! conversion, parse, content's hazards, the win check, parse.
|
//! conversion, parse, content's hazards, the win check, parse.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use super::grid::{Board, Kind, LevelDef, ObjectId};
|
use super::grid::{Board, Kind, LevelDef, ObjectId};
|
||||||
use super::movement::{Dir, Plan, Planner, Response};
|
use super::movement::{Dir, Plan, Planner, Response};
|
||||||
use super::parse::{parse, Rule};
|
use super::parse::{parse, Rule};
|
||||||
@@ -146,6 +148,10 @@ impl Level {
|
|||||||
/// (oldest first on a tie); each one's move is planned in full, then committed,
|
/// (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
|
/// before the next is planned. Every move uses the rules in force before the
|
||||||
/// first.
|
/// 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.
|
||||||
fn movement(&mut self, dir: Dir) {
|
fn movement(&mut self, dir: Dir) {
|
||||||
let (dx, dy) = dir.delta();
|
let (dx, dy) = dir.delta();
|
||||||
let mut you: Vec<_> = self
|
let mut you: Vec<_> = self
|
||||||
@@ -155,14 +161,19 @@ impl Level {
|
|||||||
.map(|o| (-(o.x * dx + o.y * dy), o.id))
|
.map(|o| (-(o.x * dx + o.y * dy), o.id))
|
||||||
.collect();
|
.collect();
|
||||||
you.sort();
|
you.sort();
|
||||||
|
let mut stepped = BTreeSet::new();
|
||||||
for (_, id) in you {
|
for (_, id) in you {
|
||||||
if self.board.find(id).is_none() {
|
if self.board.find(id).is_none() {
|
||||||
continue; // destroyed by an earlier move
|
continue; // destroyed by an earlier move
|
||||||
}
|
}
|
||||||
let mut planner = Planner::new(self, dir);
|
if stepped.contains(&id) {
|
||||||
|
continue; // already moved, by an earlier move
|
||||||
|
}
|
||||||
|
let mut planner = Planner::new(self, dir, &stepped);
|
||||||
if planner.try_move(id) {
|
if planner.try_move(id) {
|
||||||
let plan = planner.into_plan();
|
let plan = planner.into_plan();
|
||||||
self.commit(&plan, dir);
|
self.commit(&plan, dir);
|
||||||
|
stepped.extend(plan.moving.difference(&plan.destroyed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +193,10 @@ impl Level {
|
|||||||
/// object blocks this. An object named by `X IS NOT X` is deleted, and still
|
/// object blocks this. An object named by `X IS NOT X` is deleted, and still
|
||||||
/// makes its `Y`s, as new objects: the negation cancelled `X IS X`.
|
/// makes its `Y`s, as new objects: the negation cancelled `X IS X`.
|
||||||
///
|
///
|
||||||
|
/// Negation is settled per object, as in `has_property`: an object doesn't
|
||||||
|
/// become a `Y` if any active `... IS NOT Y` names it, whatever noun gave it the
|
||||||
|
/// conversion (`ALL IS Y` with `X IS NOT Y` leaves every X an X).
|
||||||
|
///
|
||||||
/// Every object is decided from the board as movement left it, before any
|
/// Every object is decided from the board as movement left it, before any
|
||||||
/// changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
/// changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
||||||
fn convert(&mut self) {
|
fn convert(&mut self) {
|
||||||
@@ -197,6 +212,10 @@ impl Level {
|
|||||||
let mut into: Vec<Kind> = vec![];
|
let mut into: Vec<Kind> = vec![];
|
||||||
if !locked {
|
if !locked {
|
||||||
for r in about_it.iter().filter(|r| !r.negated && r.is_conversion()) {
|
for r in about_it.iter().filter(|r| !r.negated && r.is_conversion()) {
|
||||||
|
let negated = |n: &&Rule| n.negated && n.complement == r.complement;
|
||||||
|
if about_it.iter().any(negated) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match self.lexicon.thing_named(&r.complement) {
|
match self.lexicon.thing_named(&r.complement) {
|
||||||
Some(kind) if kind != o.kind && !into.contains(&kind) => into.push(kind),
|
Some(kind) if kind != o.kind && !into.contains(&kind) => into.push(kind),
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -59,18 +59,22 @@ pub struct Plan {
|
|||||||
pub destroyed: BTreeSet<ObjectId>,
|
pub destroyed: BTreeSet<ObjectId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
pub struct Planner<'a> {
|
pub struct Planner<'a> {
|
||||||
level: &'a Level,
|
level: &'a Level,
|
||||||
dir: Dir,
|
dir: Dir,
|
||||||
|
stepped: &'a BTreeSet<ObjectId>,
|
||||||
plan: Plan,
|
plan: Plan,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Planner<'a> {
|
impl<'a> Planner<'a> {
|
||||||
pub fn new(level: &'a Level, dir: Dir) -> Planner<'a> {
|
pub fn new(level: &'a Level, dir: Dir, stepped: &'a BTreeSet<ObjectId>) -> Planner<'a> {
|
||||||
Planner {
|
Planner {
|
||||||
level,
|
level,
|
||||||
dir,
|
dir,
|
||||||
|
stepped,
|
||||||
plan: Plan::default(),
|
plan: Plan::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,11 +104,16 @@ impl<'a> Planner<'a> {
|
|||||||
/// Plan object `id` stepping one tile in the move's direction, with whatever
|
/// 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
|
/// 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
|
/// 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.
|
||||||
pub fn try_move(&mut self, id: ObjectId) -> bool {
|
pub fn try_move(&mut self, id: ObjectId) -> bool {
|
||||||
if self.plan.moving.contains(&id) || self.plan.destroyed.contains(&id) {
|
if self.plan.moving.contains(&id) || self.plan.destroyed.contains(&id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if self.stepped.contains(&id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let level = self.level;
|
let level = self.level;
|
||||||
let o = level.board().get(id);
|
let o = level.board().get(id);
|
||||||
let (dx, dy) = self.dir.delta();
|
let (dx, dy) = self.dir.delta();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ The course's own engine as of the end of Module 9. 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.
|
- `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`, the property `YOU`, which picks the objects that move, and from Module 9 the properties `WIN` (the win check) and `FLOAT` (which objects share a tile).
|
- `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`, the property `YOU`, which picks the objects that move, and from Module 9 the properties `WIN` (the win check) and `FLOAT` (which objects share a tile).
|
||||||
- `turn.ts` (new): the turn. `Phase`, `TURN` (the phases in order) and `Hazard` (content's rule for what destroys what).
|
- `turn.ts` (new): the turn. `Phase`, `TURN` (the phases in order) and `Hazard` (content's rule for what destroys what).
|
||||||
- `level.ts`: a level in play. Module 9 adds `Level.step`, which runs the phases of `TURN`; the conversion phase (`X IS Y` in place, `X IS X` locks, `X IS NOT X` deletes); the hazards phase, which runs content's hazards in order; the win check; `Level.sharing` (the other objects on a tile at the same float level); `won` and `lost`. The `Level` constructor takes the hazards. `moveYou` is now movement, then a reparse.
|
- `level.ts`: a level in play. Module 9 adds `Level.step`, which runs the phases of `TURN`; the conversion phase (`X IS Y` in place, `X IS X` locks, `X IS NOT X` deletes, and `... IS NOT Y` keeps every object it names from becoming a `Y`, as `hasProperty` settles negation per object); the hazards phase, which runs content's hazards in order; the win check; `Level.sharing` (the other objects on a tile at the same float level); `won` and `lost`. The `Level` constructor takes the hazards. `moveYou` is now movement, then a reparse.
|
||||||
- `grid.ts`: Module 9 adds `Board.setKind`.
|
- `grid.ts`: Module 9 adds `Board.setKind`.
|
||||||
- `word.ts`: Module 9 adds `Lexicon.thingNamed` (what a conversion into a noun makes).
|
- `word.ts`: Module 9 adds `Lexicon.thingNamed` (what a conversion into a noun makes).
|
||||||
- `movement.ts`, `parse.ts`, `resolve.ts`: unchanged.
|
- `movement.ts`, `parse.ts`, `resolve.ts`: unchanged.
|
||||||
@@ -13,7 +13,7 @@ The course's own engine as of the end of Module 9. Part I's card engine is uncha
|
|||||||
- The rest of `src/engine/`: Part I, unchanged.
|
- The rest of `src/engine/`: Part I, unchanged.
|
||||||
- `src/content/hazards.ts` (new) and `src/content/index.ts`: the hazards, in the order the engine runs them: `SINK`, `HOT`/`MELT`, `DEFEAT`. What destroys what at the end of a turn is content, not engine.
|
- `src/content/hazards.ts` (new) and `src/content/index.ts`: the hazards, in the order the engine runs them: `SINK`, `HOT`/`MELT`, `DEFEAT`. What destroys what at the end of a turn is content, not engine.
|
||||||
- `src/content/movement.ts`: unchanged.
|
- `src/content/movement.ts`: unchanged.
|
||||||
- `src/content/words.json`: Module 9 adds `HOT`, `MELT`, `SINK`, `DEFEAT` and `FLOAT`.
|
- `src/content/words.json`: Module 9 adds `HOT`, `MELT`, `SINK`, `DEFEAT` and `FLOAT`, and `ALL`, the Module 7 Expansion Drop, which the Module 9 tests need.
|
||||||
- `src/harness.ts`: the adapter the course tests call. Module 9 adds `step`, `won` and `lost` on `Baba`.
|
- `src/harness.ts`: the adapter the course tests call. Module 9 adds `step`, `won` and `lost` on `Baba`.
|
||||||
|
|
||||||
Run it with the module's tests: `node tools/verify-references.mjs 09` from the course repository.
|
Run it with the module's tests: `node tools/verify-references.mjs 09` from the course repository.
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
{ "name": "DOOR", "role": "noun" },
|
{ "name": "DOOR", "role": "noun" },
|
||||||
{ "name": "SKULL", "role": "noun" },
|
{ "name": "SKULL", "role": "noun" },
|
||||||
{ "name": "WATER", "role": "noun" },
|
{ "name": "WATER", "role": "noun" },
|
||||||
|
{ "name": "ALL", "role": "noun", "selects": "things" },
|
||||||
{ "name": "YOU", "role": "property" },
|
{ "name": "YOU", "role": "property" },
|
||||||
{ "name": "WIN", "role": "property" },
|
{ "name": "WIN", "role": "property" },
|
||||||
{ "name": "PUSH", "role": "property" },
|
{ "name": "PUSH", "role": "property" },
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ export class Level {
|
|||||||
* The `YOU` objects move one at a time, the one furthest ahead in `dir` first
|
* The `YOU` objects 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,
|
* (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.
|
* 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.
|
||||||
*/
|
*/
|
||||||
private movement(dir: Dir): void {
|
private movement(dir: Dir): void {
|
||||||
const [dx, dy] = delta(dir);
|
const [dx, dy] = delta(dir);
|
||||||
@@ -150,10 +154,15 @@ export class Level {
|
|||||||
.filter((o) => this.hasProperty(o.id, YOU))
|
.filter((o) => this.hasProperty(o.id, YOU))
|
||||||
.sort((a, b) => ahead(b) - ahead(a) || a.id - b.id)
|
.sort((a, b) => ahead(b) - ahead(a) || a.id - b.id)
|
||||||
.map((o) => o.id);
|
.map((o) => o.id);
|
||||||
|
const stepped = new Set<ObjectId>();
|
||||||
for (const id of you) {
|
for (const id of you) {
|
||||||
if (this.board.find(id) === undefined) continue; // destroyed by an earlier move
|
if (this.board.find(id) === undefined) continue; // destroyed by an earlier move
|
||||||
const planner = new Planner(this, dir);
|
if (stepped.has(id)) continue; // already moved, by an earlier move
|
||||||
if (planner.tryMove(id)) this.commit(planner.result(), dir);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +178,10 @@ export class Level {
|
|||||||
* this. An object named by `X IS NOT X` is deleted, and still makes its `Y`s,
|
* this. An object named by `X IS NOT X` is deleted, and still makes its `Y`s,
|
||||||
* as new objects: the negation cancelled `X IS X`.
|
* as new objects: the negation cancelled `X IS X`.
|
||||||
*
|
*
|
||||||
|
* Negation is settled per object, as in `hasProperty`: an object doesn't become
|
||||||
|
* a `Y` if any active `... IS NOT Y` names it, whatever noun gave it the
|
||||||
|
* conversion (`ALL IS Y` with `X IS NOT Y` leaves every X an X).
|
||||||
|
*
|
||||||
* Every object is decided from the board as movement left it, before any
|
* Every object is decided from the board as movement left it, before any
|
||||||
* changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
* changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
||||||
*/
|
*/
|
||||||
@@ -182,6 +195,7 @@ export class Level {
|
|||||||
if (!locked) {
|
if (!locked) {
|
||||||
for (const r of aboutIt) {
|
for (const r of aboutIt) {
|
||||||
if (r.negated || !isConversion(r)) continue;
|
if (r.negated || !isConversion(r)) continue;
|
||||||
|
if (aboutIt.some((n) => n.negated && n.complement === r.complement)) continue;
|
||||||
const kind = this.lexicon.thingNamed(r.complement);
|
const kind = this.lexicon.thingNamed(r.complement);
|
||||||
if (kind === undefined) continue;
|
if (kind === undefined) continue;
|
||||||
const same = (k: Kind) => Kind.format(k) === Kind.format(kind);
|
const same = (k: Kind) => Kind.format(k) === Kind.format(kind);
|
||||||
|
|||||||
@@ -63,13 +63,18 @@ export interface MoveContext {
|
|||||||
responses(): readonly Response[];
|
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 {
|
export class Planner {
|
||||||
private plan: Plan = { moving: new Set(), destroyed: new Set() };
|
private plan: Plan = { moving: new Set(), destroyed: new Set() };
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly level: MoveContext,
|
private readonly level: MoveContext,
|
||||||
private readonly dir: Dir,
|
private readonly dir: Dir,
|
||||||
|
private readonly stepped: ReadonlySet<ObjectId>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Does object `id` have `property`, under the rules in force before the move? */
|
/** 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
|
* 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
|
* 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
|
* 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 {
|
tryMove(id: ObjectId): boolean {
|
||||||
if (this.plan.moving.has(id) || this.plan.destroyed.has(id)) return true;
|
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 o = this.level.board.get(id);
|
||||||
const [dx, dy] = delta(this.dir);
|
const [dx, dy] = delta(this.dir);
|
||||||
const to: Pos = [o.x + dx, o.y + dy];
|
const to: Pos = [o.x + dx, o.y + dy];
|
||||||
|
|||||||
@@ -90,3 +90,14 @@ Words used: the nouns `BABA`, `ROCK`, `FLAG`, `LAVA`, `KEY`, `SKULL`, `WATER`; t
|
|||||||
- `posOf(baba)` is `null`: Baba stepped onto the flag, which is `DEFEAT`.
|
- `posOf(baba)` is `null`: Baba stepped onto the flag, which is `DEFEAT`.
|
||||||
- `won()` is false: `DEFEAT` runs before the win check, so no `YOU` object is left to win.
|
- `won()` is false: `DEFEAT` runs before the win check, so no `YOU` object is left to win.
|
||||||
- `lost()` is true, and `posOf(flag)` is (1,3).
|
- `lost()` is true, and `posOf(flag)` is (1,3).
|
||||||
|
|
||||||
|
### test_09_negated_conversion_beats_group
|
||||||
|
|
||||||
|
This test needs the Module 7 Expansion Drop: the word `ALL` in the lexicon.
|
||||||
|
|
||||||
|
- Setup: `newBaba` with an empty level 4 wide and 5 high. Place `BABA (0,0) IS (1,0) YOU (2,0)`, `ALL (0,1) IS (1,1) ROCK (2,1)` and `BABA (0,2) IS (1,2) NOT (2,2) ROCK (3,2)`. Place an object `baba` at (0,4) and an object `key` at (3,4).
|
||||||
|
- Action: `step("right")`.
|
||||||
|
- Assertions:
|
||||||
|
- `kindsAt(3,4)` is `["rock"]`: the key is named by `ALL`, so `ALL IS ROCK` converted it.
|
||||||
|
- `posOf(baba)` is (1,4) and `kindsAt(1,4)` is `["baba"]`: Baba is named by both `ALL` and `BABA`, and `BABA IS NOT ROCK` keeps it from becoming a rock. A negated conversion is settled per object, as `hasProperty` settles a negated property.
|
||||||
|
- `activeRules()` contains both `ALL IS ROCK` and `BABA IS NOT ROCK`: the two rules have different subjects, so neither cancels the other as written.
|
||||||
@@ -182,3 +182,25 @@ fn test_09_defeat_beats_win() {
|
|||||||
assert!(b.lost());
|
assert!(b.lost());
|
||||||
assert_eq!(b.pos_of(flag), Some((1, 3)));
|
assert_eq!(b.pos_of(flag), Some((1, 3)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_09_negated_conversion_beats_group() {
|
||||||
|
let mut b = new_baba(empty_level(4, 5));
|
||||||
|
write(&mut b, &[("BABA", 0, 0), ("IS", 1, 0), ("YOU", 2, 0)]);
|
||||||
|
write(&mut b, &[("ALL", 0, 1), ("IS", 1, 1), ("ROCK", 2, 1)]);
|
||||||
|
write(
|
||||||
|
&mut b,
|
||||||
|
&[("BABA", 0, 2), ("IS", 1, 2), ("NOT", 2, 2), ("ROCK", 3, 2)],
|
||||||
|
);
|
||||||
|
let baba = b.place("baba", 0, 4);
|
||||||
|
b.place("key", 3, 4);
|
||||||
|
|
||||||
|
b.step(Dir::Right);
|
||||||
|
|
||||||
|
assert_eq!(b.kinds_at(3, 4), vec!["rock"]);
|
||||||
|
assert_eq!(b.pos_of(baba), Some((1, 4)));
|
||||||
|
assert_eq!(b.kinds_at(1, 4), vec!["baba"]);
|
||||||
|
let active = b.active_rules();
|
||||||
|
assert!(active.contains(&"ALL IS ROCK".to_string()));
|
||||||
|
assert!(active.contains(&"BABA IS NOT ROCK".to_string()));
|
||||||
|
}
|
||||||
@@ -248,3 +248,33 @@ test("test_09_defeat_beats_win", () => {
|
|||||||
expect(b.lost()).toBe(true);
|
expect(b.lost()).toBe(true);
|
||||||
expect(b.posOf(flag)).toEqual([1, 3]);
|
expect(b.posOf(flag)).toEqual([1, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("test_09_negated_conversion_beats_group", () => {
|
||||||
|
const b = newBaba(emptyLevel(4, 5));
|
||||||
|
write(b, [
|
||||||
|
["BABA", 0, 0],
|
||||||
|
["IS", 1, 0],
|
||||||
|
["YOU", 2, 0],
|
||||||
|
]);
|
||||||
|
write(b, [
|
||||||
|
["ALL", 0, 1],
|
||||||
|
["IS", 1, 1],
|
||||||
|
["ROCK", 2, 1],
|
||||||
|
]);
|
||||||
|
write(b, [
|
||||||
|
["BABA", 0, 2],
|
||||||
|
["IS", 1, 2],
|
||||||
|
["NOT", 2, 2],
|
||||||
|
["ROCK", 3, 2],
|
||||||
|
]);
|
||||||
|
const baba = b.place("baba", 0, 4);
|
||||||
|
b.place("key", 3, 4);
|
||||||
|
|
||||||
|
b.step("right");
|
||||||
|
|
||||||
|
expect(b.kindsAt(3, 4)).toEqual(["rock"]);
|
||||||
|
expect(b.posOf(baba)).toEqual([1, 4]);
|
||||||
|
expect(b.kindsAt(1, 4)).toEqual(["baba"]);
|
||||||
|
expect(b.activeRules()).toContain("ALL IS ROCK");
|
||||||
|
expect(b.activeRules()).toContain("BABA IS NOT ROCK");
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
{ "name": "DOOR", "role": "noun" },
|
{ "name": "DOOR", "role": "noun" },
|
||||||
{ "name": "SKULL", "role": "noun" },
|
{ "name": "SKULL", "role": "noun" },
|
||||||
{ "name": "WATER", "role": "noun" },
|
{ "name": "WATER", "role": "noun" },
|
||||||
|
{ "name": "ALL", "role": "noun", "selects": "things" },
|
||||||
{ "name": "KEG", "role": "noun" },
|
{ "name": "KEG", "role": "noun" },
|
||||||
{ "name": "BRAMBLE", "role": "noun" },
|
{ "name": "BRAMBLE", "role": "noun" },
|
||||||
{ "name": "HEDGE", "role": "noun" },
|
{ "name": "HEDGE", "role": "noun" },
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
{ "name": "DOOR", "role": "noun" },
|
{ "name": "DOOR", "role": "noun" },
|
||||||
{ "name": "SKULL", "role": "noun" },
|
{ "name": "SKULL", "role": "noun" },
|
||||||
{ "name": "WATER", "role": "noun" },
|
{ "name": "WATER", "role": "noun" },
|
||||||
|
{ "name": "ALL", "role": "noun", "selects": "things" },
|
||||||
{ "name": "KEG", "role": "noun" },
|
{ "name": "KEG", "role": "noun" },
|
||||||
{ "name": "BRAMBLE", "role": "noun" },
|
{ "name": "BRAMBLE", "role": "noun" },
|
||||||
{ "name": "HEDGE", "role": "noun" },
|
{ "name": "HEDGE", "role": "noun" },
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
//! removes them. SpellGrid passes one that sends minions to Part I's Death Creation
|
//! removes them. SpellGrid passes one that sends minions to Part I's Death Creation
|
||||||
//! Step instead.
|
//! Step instead.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use super::grid::{Board, Kind, LevelDef, ObjectId};
|
use super::grid::{Board, Kind, LevelDef, ObjectId};
|
||||||
use super::movement::{Dir, Plan, Planner, Response};
|
use super::movement::{Dir, Plan, Planner, Response};
|
||||||
use super::parse::{parse, Rule};
|
use super::parse::{parse, Rule};
|
||||||
@@ -178,6 +180,10 @@ impl Level {
|
|||||||
/// (oldest first on a tie); each one's move is planned in full, then committed,
|
/// (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
|
/// before the next is planned. Every move uses the rules in force before the
|
||||||
/// first.
|
/// 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.
|
||||||
fn movement(&mut self, dir: Dir, destroy: &mut Destroy) {
|
fn movement(&mut self, dir: Dir, destroy: &mut Destroy) {
|
||||||
let (dx, dy) = dir.delta();
|
let (dx, dy) = dir.delta();
|
||||||
let mut you: Vec<_> = self
|
let mut you: Vec<_> = self
|
||||||
@@ -187,14 +193,19 @@ impl Level {
|
|||||||
.map(|o| (-(o.x * dx + o.y * dy), o.id))
|
.map(|o| (-(o.x * dx + o.y * dy), o.id))
|
||||||
.collect();
|
.collect();
|
||||||
you.sort();
|
you.sort();
|
||||||
|
let mut stepped = BTreeSet::new();
|
||||||
for (_, id) in you {
|
for (_, id) in you {
|
||||||
if self.board.find(id).is_none() {
|
if self.board.find(id).is_none() {
|
||||||
continue; // destroyed by an earlier move
|
continue; // destroyed by an earlier move
|
||||||
}
|
}
|
||||||
let mut planner = Planner::new(self, dir);
|
if stepped.contains(&id) {
|
||||||
|
continue; // already moved, by an earlier move
|
||||||
|
}
|
||||||
|
let mut planner = Planner::new(self, dir, &stepped);
|
||||||
if planner.try_move(id) {
|
if planner.try_move(id) {
|
||||||
let plan = planner.into_plan();
|
let plan = planner.into_plan();
|
||||||
self.commit(&plan, dir, destroy);
|
self.commit(&plan, dir, destroy);
|
||||||
|
stepped.extend(plan.moving.difference(&plan.destroyed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,6 +230,10 @@ impl Level {
|
|||||||
/// object blocks this. An object named by `X IS NOT X` is deleted, and still
|
/// object blocks this. An object named by `X IS NOT X` is deleted, and still
|
||||||
/// makes its `Y`s, as new objects: the negation cancelled `X IS X`.
|
/// makes its `Y`s, as new objects: the negation cancelled `X IS X`.
|
||||||
///
|
///
|
||||||
|
/// Negation is settled per object, as in `has_property`: an object doesn't
|
||||||
|
/// become a `Y` if any active `... IS NOT Y` names it, whatever noun gave it the
|
||||||
|
/// conversion (`ALL IS Y` with `X IS NOT Y` leaves every X an X).
|
||||||
|
///
|
||||||
/// Every object is decided from the board as movement left it, before any
|
/// Every object is decided from the board as movement left it, before any
|
||||||
/// changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
/// changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
||||||
fn convert(&mut self, destroy: &mut Destroy) {
|
fn convert(&mut self, destroy: &mut Destroy) {
|
||||||
@@ -234,6 +249,10 @@ impl Level {
|
|||||||
let mut into: Vec<Kind> = vec![];
|
let mut into: Vec<Kind> = vec![];
|
||||||
if !locked {
|
if !locked {
|
||||||
for r in about_it.iter().filter(|r| !r.negated && r.is_conversion()) {
|
for r in about_it.iter().filter(|r| !r.negated && r.is_conversion()) {
|
||||||
|
let negated = |n: &&Rule| n.negated && n.complement == r.complement;
|
||||||
|
if about_it.iter().any(negated) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
match self.lexicon.thing_named(&r.complement) {
|
match self.lexicon.thing_named(&r.complement) {
|
||||||
Some(kind) if kind != o.kind && !into.contains(&kind) => into.push(kind),
|
Some(kind) if kind != o.kind && !into.contains(&kind) => into.push(kind),
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -59,18 +59,22 @@ pub struct Plan {
|
|||||||
pub destroyed: BTreeSet<ObjectId>,
|
pub destroyed: BTreeSet<ObjectId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
pub struct Planner<'a> {
|
pub struct Planner<'a> {
|
||||||
level: &'a Level,
|
level: &'a Level,
|
||||||
dir: Dir,
|
dir: Dir,
|
||||||
|
stepped: &'a BTreeSet<ObjectId>,
|
||||||
plan: Plan,
|
plan: Plan,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Planner<'a> {
|
impl<'a> Planner<'a> {
|
||||||
pub fn new(level: &'a Level, dir: Dir) -> Planner<'a> {
|
pub fn new(level: &'a Level, dir: Dir, stepped: &'a BTreeSet<ObjectId>) -> Planner<'a> {
|
||||||
Planner {
|
Planner {
|
||||||
level,
|
level,
|
||||||
dir,
|
dir,
|
||||||
|
stepped,
|
||||||
plan: Plan::default(),
|
plan: Plan::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,11 +104,16 @@ impl<'a> Planner<'a> {
|
|||||||
/// Plan object `id` stepping one tile in the move's direction, with whatever
|
/// 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
|
/// 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
|
/// 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.
|
||||||
pub fn try_move(&mut self, id: ObjectId) -> bool {
|
pub fn try_move(&mut self, id: ObjectId) -> bool {
|
||||||
if self.plan.moving.contains(&id) || self.plan.destroyed.contains(&id) {
|
if self.plan.moving.contains(&id) || self.plan.destroyed.contains(&id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if self.stepped.contains(&id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let level = self.level;
|
let level = self.level;
|
||||||
let o = level.board().get(id);
|
let o = level.board().get(id);
|
||||||
let (dx, dy) = self.dir.delta();
|
let (dx, dy) = self.dir.delta();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
{ "name": "DOOR", "role": "noun" },
|
{ "name": "DOOR", "role": "noun" },
|
||||||
{ "name": "SKULL", "role": "noun" },
|
{ "name": "SKULL", "role": "noun" },
|
||||||
{ "name": "WATER", "role": "noun" },
|
{ "name": "WATER", "role": "noun" },
|
||||||
|
{ "name": "ALL", "role": "noun", "selects": "things" },
|
||||||
{ "name": "KEG", "role": "noun" },
|
{ "name": "KEG", "role": "noun" },
|
||||||
{ "name": "BRAMBLE", "role": "noun" },
|
{ "name": "BRAMBLE", "role": "noun" },
|
||||||
{ "name": "HEDGE", "role": "noun" },
|
{ "name": "HEDGE", "role": "noun" },
|
||||||
|
|||||||
@@ -156,6 +156,10 @@ export class Level {
|
|||||||
* The `YOU` objects move one at a time, the one furthest ahead in `dir` first
|
* The `YOU` objects 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,
|
* (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.
|
* 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.
|
||||||
*/
|
*/
|
||||||
private movement(dir: Dir, destroy: Destroy): void {
|
private movement(dir: Dir, destroy: Destroy): void {
|
||||||
const [dx, dy] = delta(dir);
|
const [dx, dy] = delta(dir);
|
||||||
@@ -165,10 +169,15 @@ export class Level {
|
|||||||
.filter((o) => this.hasProperty(o.id, YOU))
|
.filter((o) => this.hasProperty(o.id, YOU))
|
||||||
.sort((a, b) => ahead(b) - ahead(a) || a.id - b.id)
|
.sort((a, b) => ahead(b) - ahead(a) || a.id - b.id)
|
||||||
.map((o) => o.id);
|
.map((o) => o.id);
|
||||||
|
const stepped = new Set<ObjectId>();
|
||||||
for (const id of you) {
|
for (const id of you) {
|
||||||
if (this.board.find(id) === undefined) continue; // destroyed by an earlier move
|
if (this.board.find(id) === undefined) continue; // destroyed by an earlier move
|
||||||
const planner = new Planner(this, dir);
|
if (stepped.has(id)) continue; // already moved, by an earlier move
|
||||||
if (planner.tryMove(id)) this.commit(planner.result(), dir, destroy);
|
const planner = new Planner(this, dir, stepped);
|
||||||
|
if (!planner.tryMove(id)) continue;
|
||||||
|
const plan = planner.result();
|
||||||
|
this.commit(plan, dir, destroy);
|
||||||
|
for (const moved of plan.moving) if (!plan.destroyed.has(moved)) stepped.add(moved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,6 +196,10 @@ export class Level {
|
|||||||
* this. An object named by `X IS NOT X` is deleted, and still makes its `Y`s,
|
* this. An object named by `X IS NOT X` is deleted, and still makes its `Y`s,
|
||||||
* as new objects: the negation cancelled `X IS X`.
|
* as new objects: the negation cancelled `X IS X`.
|
||||||
*
|
*
|
||||||
|
* Negation is settled per object, as in `hasProperty`: an object doesn't become
|
||||||
|
* a `Y` if any active `... IS NOT Y` names it, whatever noun gave it the
|
||||||
|
* conversion (`ALL IS Y` with `X IS NOT Y` leaves every X an X).
|
||||||
|
*
|
||||||
* Every object is decided from the board as movement left it, before any
|
* Every object is decided from the board as movement left it, before any
|
||||||
* changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
* changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
||||||
*/
|
*/
|
||||||
@@ -200,6 +213,7 @@ export class Level {
|
|||||||
if (!locked) {
|
if (!locked) {
|
||||||
for (const r of aboutIt) {
|
for (const r of aboutIt) {
|
||||||
if (r.negated || !isConversion(r)) continue;
|
if (r.negated || !isConversion(r)) continue;
|
||||||
|
if (aboutIt.some((n) => n.negated && n.complement === r.complement)) continue;
|
||||||
const kind = this.lexicon.thingNamed(r.complement);
|
const kind = this.lexicon.thingNamed(r.complement);
|
||||||
if (kind === undefined) continue;
|
if (kind === undefined) continue;
|
||||||
const same = (k: Kind) => Kind.format(k) === Kind.format(kind);
|
const same = (k: Kind) => Kind.format(k) === Kind.format(kind);
|
||||||
|
|||||||
@@ -63,13 +63,18 @@ export interface MoveContext {
|
|||||||
responses(): readonly Response[];
|
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 {
|
export class Planner {
|
||||||
private plan: Plan = { moving: new Set(), destroyed: new Set() };
|
private plan: Plan = { moving: new Set(), destroyed: new Set() };
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly level: MoveContext,
|
private readonly level: MoveContext,
|
||||||
private readonly dir: Dir,
|
private readonly dir: Dir,
|
||||||
|
private readonly stepped: ReadonlySet<ObjectId>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Does object `id` have `property`, under the rules in force before the move? */
|
/** 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
|
* 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
|
* 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
|
* 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 {
|
tryMove(id: ObjectId): boolean {
|
||||||
if (this.plan.moving.has(id) || this.plan.destroyed.has(id)) return true;
|
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 o = this.level.board.get(id);
|
||||||
const [dx, dy] = delta(this.dir);
|
const [dx, dy] = delta(this.dir);
|
||||||
const to: Pos = [o.x + dx, o.y + dy];
|
const to: Pos = [o.x + dx, o.y + dy];
|
||||||
|
|||||||
Reference in new issue
Block a user