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>
136 lines
4.6 KiB
Rust
136 lines
4.6 KiB
Rust
//! A level in play: the board, the words content provides, and the rules compiled
|
|
//! from them. Every change to the board recompiles the rules from scratch: parse,
|
|
//! then resolve. Every question about what an object is goes through `has_property`.
|
|
//!
|
|
//! A move (Module 8) is planned for each `YOU` object, then committed; the rules are
|
|
//! reparsed once, after all of them.
|
|
|
|
use std::collections::BTreeSet;
|
|
|
|
use super::grid::{Board, Kind, LevelDef, ObjectId};
|
|
use super::movement::{Dir, Plan, Planner, Response};
|
|
use super::parse::{parse, Rule};
|
|
use super::resolve::resolve;
|
|
use super::word::{Lexicon, WordDef};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Level {
|
|
board: Board,
|
|
lexicon: Lexicon,
|
|
responses: Vec<Response>,
|
|
rules: Vec<Rule>,
|
|
active: Vec<Rule>,
|
|
}
|
|
|
|
/// The property that makes an object move when the player moves.
|
|
pub const YOU: &str = "YOU";
|
|
|
|
impl Level {
|
|
/// A level from its definition, the words content provides, and content's
|
|
/// movement responses, in the order they are to run.
|
|
pub fn new(def: &LevelDef, words: &[WordDef], responses: Vec<Response>) -> Level {
|
|
let mut level = Level {
|
|
board: Board::new(def.width, def.height),
|
|
lexicon: Lexicon::new(words),
|
|
responses,
|
|
rules: vec![],
|
|
active: vec![],
|
|
};
|
|
for o in &def.objects {
|
|
level.board.place(Kind::parse(&o.kind), o.x, o.y);
|
|
}
|
|
level.reparse();
|
|
level
|
|
}
|
|
|
|
pub fn place(&mut self, kind: &str, x: i32, y: i32) -> ObjectId {
|
|
let id = self.board.place(Kind::parse(kind), x, y);
|
|
self.reparse();
|
|
id
|
|
}
|
|
|
|
pub fn board(&self) -> &Board {
|
|
&self.board
|
|
}
|
|
|
|
/// The movement responses, in the order they run.
|
|
pub fn responses(&self) -> &[Response] {
|
|
&self.responses
|
|
}
|
|
|
|
/// Every rule the text spells, as parsed: negated rules and duplicates included.
|
|
pub fn rules(&self) -> &[Rule] {
|
|
&self.rules
|
|
}
|
|
|
|
/// The rules in force after the resolution pass.
|
|
pub fn active_rules(&self) -> &[Rule] {
|
|
&self.active
|
|
}
|
|
|
|
/// Does object `id` have `property`? Yes if an active rule gives it to a noun
|
|
/// that names the object and no active negated rule takes it from one. A
|
|
/// destroyed object has no properties.
|
|
pub fn has_property(&self, id: ObjectId, property: &str) -> bool {
|
|
let Some(object) = self.board.find(id) else {
|
|
return false;
|
|
};
|
|
let kind = &object.kind;
|
|
let about_it: Vec<&Rule> = self
|
|
.active
|
|
.iter()
|
|
.filter(|r| r.complement == property && self.lexicon.names(&r.subject, kind))
|
|
.collect();
|
|
about_it.iter().any(|r| !r.negated) && !about_it.iter().any(|r| r.negated)
|
|
}
|
|
|
|
/// Move every `YOU` object one tile in `dir`, then reparse. 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, 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.
|
|
pub fn move_you(&mut self, dir: Dir) {
|
|
let (dx, dy) = dir.delta();
|
|
let mut you: Vec<_> = self
|
|
.board
|
|
.objects()
|
|
.filter(|o| self.has_property(o.id, YOU))
|
|
.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
|
|
}
|
|
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);
|
|
stepped.extend(plan.moving.difference(&plan.destroyed));
|
|
}
|
|
}
|
|
self.reparse();
|
|
}
|
|
|
|
fn commit(&mut self, plan: &Plan, dir: Dir) {
|
|
for &id in &plan.destroyed {
|
|
self.board.remove(id);
|
|
}
|
|
for &id in plan.moving.difference(&plan.destroyed) {
|
|
self.board.shift(id, dir.delta());
|
|
}
|
|
}
|
|
|
|
fn reparse(&mut self) {
|
|
self.rules = parse(&self.board, &self.lexicon);
|
|
self.active = resolve(&self.rules);
|
|
}
|
|
}
|