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
@@ -8,6 +8,7 @@
|
||||
{ "name": "DOOR", "role": "noun" },
|
||||
{ "name": "SKULL", "role": "noun" },
|
||||
{ "name": "WATER", "role": "noun" },
|
||||
{ "name": "ALL", "role": "noun", "selects": "things" },
|
||||
{ "name": "KEG", "role": "noun" },
|
||||
{ "name": "BRAMBLE", "role": "noun" },
|
||||
{ "name": "HEDGE", "role": "noun" },
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//! removes them. SpellGrid passes one that sends minions to Part I's Death Creation
|
||||
//! Step instead.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::grid::{Board, Kind, LevelDef, ObjectId};
|
||||
use super::movement::{Dir, Plan, Planner, Response};
|
||||
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,
|
||||
/// 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.
|
||||
fn movement(&mut self, dir: Dir, destroy: &mut Destroy) {
|
||||
let (dx, dy) = dir.delta();
|
||||
let mut you: Vec<_> = self
|
||||
@@ -187,14 +193,19 @@ impl Level {
|
||||
.map(|o| (-(o.x * dx + o.y * dy), o.id))
|
||||
.collect();
|
||||
you.sort();
|
||||
let mut stepped = BTreeSet::new();
|
||||
for (_, id) in you {
|
||||
if self.board.find(id).is_none() {
|
||||
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) {
|
||||
let plan = planner.into_plan();
|
||||
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
|
||||
/// 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
|
||||
/// changes, so `X IS Y` and `Y IS X` swap the Xs and the Ys.
|
||||
fn convert(&mut self, destroy: &mut Destroy) {
|
||||
@@ -234,6 +249,10 @@ impl Level {
|
||||
let mut into: Vec<Kind> = vec![];
|
||||
if !locked {
|
||||
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) {
|
||||
Some(kind) if kind != o.kind && !into.contains(&kind) => into.push(kind),
|
||||
_ => {}
|
||||
|
||||
@@ -59,18 +59,22 @@ pub struct Plan {
|
||||
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> {
|
||||
level: &'a Level,
|
||||
dir: Dir,
|
||||
stepped: &'a BTreeSet<ObjectId>,
|
||||
plan: Plan,
|
||||
}
|
||||
|
||||
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 {
|
||||
level,
|
||||
dir,
|
||||
stepped,
|
||||
plan: Plan::default(),
|
||||
}
|
||||
}
|
||||
@@ -100,11 +104,16 @@ impl<'a> Planner<'a> {
|
||||
/// 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.
|
||||
pub fn try_move(&mut self, id: ObjectId) -> bool {
|
||||
if self.plan.moving.contains(&id) || self.plan.destroyed.contains(&id) {
|
||||
return true;
|
||||
}
|
||||
if self.stepped.contains(&id) {
|
||||
return false;
|
||||
}
|
||||
let level = self.level;
|
||||
let o = level.board().get(id);
|
||||
let (dx, dy) = self.dir.delta();
|
||||
|
||||
Reference in new issue
Block a user