Adds course/11-conditions: lesson, scenarios, nine test_11 tests in both tracks, the cumulative reference (Module 10's plus conditions) and the NEAR drop. The engine learns a condition role, rules carry conditions, and Level.hasProperty and conversion evaluate content's condition checks against the board whenever a rule is used. Resolution pairs only rules with the same conditions. Catalog, harness contract, research notes and the diff-tracks generator follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
1.9 KiB
Rust
55 lines
1.9 KiB
Rust
//! Part II: what each condition word means (Module 11). The engine asks these checks
|
|
//! every time it is about to use a rule with a condition, for one object, against the
|
|
//! board as it is then. A new condition word is one entry in `words.json`, with role
|
|
//! "condition", and one check here.
|
|
|
|
use std::collections::HashSet;
|
|
|
|
use crate::engine::puzzle::{ConditionCheck, ConditionChecks, Level, ObjectId};
|
|
|
|
/// The condition checks, by condition word.
|
|
pub fn conditions() -> ConditionChecks {
|
|
ConditionChecks::from([("ON".to_string(), on as ConditionCheck)])
|
|
}
|
|
|
|
/// Can each noun be given an object of its own among `others`, one the noun names?
|
|
/// `ON ROCK AND ROCK` needs two rocks (the wiki: "AND in the noun2 part requires at
|
|
/// least as many objects as are mentioned"). Tries every assignment.
|
|
fn each_has_one(level: &Level, nouns: &[String], others: &[ObjectId]) -> bool {
|
|
fn assign(
|
|
level: &Level,
|
|
nouns: &[String],
|
|
others: &[ObjectId],
|
|
taken: &mut HashSet<ObjectId>,
|
|
) -> bool {
|
|
let Some((noun, rest)) = nouns.split_first() else {
|
|
return true;
|
|
};
|
|
for &other in others {
|
|
if taken.contains(&other) || !level.names(noun, other) {
|
|
continue;
|
|
}
|
|
taken.insert(other);
|
|
if assign(level, rest, others, taken) {
|
|
return true;
|
|
}
|
|
taken.remove(&other);
|
|
}
|
|
false
|
|
}
|
|
assign(level, nouns, others, &mut HashSet::new())
|
|
}
|
|
|
|
/// ON: for each noun, another object on the object's tile that the noun names, a
|
|
/// different one for each noun. Float levels don't matter ("ON ignores FLOAT levels").
|
|
fn on(level: &Level, id: ObjectId, nouns: &[String]) -> bool {
|
|
let o = level.board().get(id);
|
|
let others: Vec<ObjectId> = level
|
|
.board()
|
|
.at(o.x, o.y)
|
|
.map(|other| other.id)
|
|
.filter(|&other| other != id)
|
|
.collect();
|
|
each_has_one(level, nouns, &others)
|
|
}
|