feat: Module 11 becomes 13; diff-tracks fuzzer; align the two reference tracks
- Rename course/11-break-your-engine to 13-break-your-engine and update every link and mention; start-module explains that Module 13 has no tests to start. - tools/diff-tracks.mjs (npm run diff:tracks): stages both references of a module, generates a seeded random SpellGrid level and script per seed, pipes it to the Rust and TypeScript CLIs and diffs their output; crashes on one or both sides fail. stagePlay takes a module number. - register_assembled: the TypeScript track compared only the text of an existing Assembled card, so a base re-registered with a new cost (or a different split) silently rewrote the earlier card. It now compares every field the engine reads, as Rust does (Modules 5-10). New test_05_assemble_never_rewrites_an_earlier_card. - SpellGrid CLIs: coordinates and chooser numbers are 1-9 plain digits in both tracks (Rust accepted signs, TypeScript accepted "+2", "0x2", "1e0"); Rust quotes card names verbatim instead of with Debug escapes; Rust joins the minion list with "; " instead of rewriting ", ". Two new CLI tests per track. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b36f4605cc
commit
06a1b34c5d
32 files changed
+645
-55
No files matched your search
@@ -82,26 +82,29 @@ pub fn render(g: &SpellGrid, level: &LevelDef) -> String {
|
||||
}
|
||||
}
|
||||
}
|
||||
let or = |list: Vec<String>, none: &str| {
|
||||
let or = |list: Vec<String>, separator: &str, none: &str| {
|
||||
if list.is_empty() {
|
||||
none.to_string()
|
||||
} else {
|
||||
list.join(", ")
|
||||
list.join(separator)
|
||||
}
|
||||
};
|
||||
lines.push(format!(
|
||||
"Minions: {}",
|
||||
or(minions, "none").replace(", ", "; ")
|
||||
));
|
||||
lines.push(format!("Hand: {}", or(g.hand(), "empty")));
|
||||
lines.push(format!("Minions: {}", or(minions, "; ", "none")));
|
||||
lines.push(format!("Hand: {}", or(g.hand(), ", ", "empty")));
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
/// A coordinate, or a chooser's number: one to nine digits, nothing else (no sign, no point).
|
||||
fn number(word: &str) -> Option<i32> {
|
||||
let digits = (1..=9).contains(&word.len()) && word.bytes().all(|b| b.is_ascii_digit());
|
||||
digits.then(|| word.parse().expect("nine digits fit an i32"))
|
||||
}
|
||||
|
||||
/// A `c` command: `c <card words...> [x y]`.
|
||||
fn parse_cast(args: &[&str]) -> (String, Option<(i32, i32)>) {
|
||||
let n = args.len();
|
||||
if n >= 3 {
|
||||
if let (Ok(x), Ok(y)) = (args[n - 2].parse(), args[n - 1].parse()) {
|
||||
if let (Some(x), Some(y)) = (number(args[n - 2]), number(args[n - 1])) {
|
||||
return (args[..n - 2].join(" "), Some((x, y)));
|
||||
}
|
||||
}
|
||||
@@ -131,10 +134,9 @@ pub fn run(level_text: &str, input: Box<dyn BufRead>, output: Box<dyn Write>) ->
|
||||
if options.contains(&answer) {
|
||||
return answer;
|
||||
}
|
||||
let picked = answer
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
.and_then(|n| options.get(n.wrapping_sub(1)));
|
||||
let picked = number(&answer)
|
||||
.and_then(|n| usize::try_from(n - 1).ok())
|
||||
.and_then(|i| options.get(i));
|
||||
picked.unwrap_or(&options[0]).clone()
|
||||
});
|
||||
play(&mut g, &level, &io)
|
||||
|
||||
@@ -200,7 +200,7 @@ impl SpellGrid {
|
||||
)
|
||||
});
|
||||
match target {
|
||||
None if chooses => Err(format!("{card:?} needs a target minion")),
|
||||
None if chooses => Err(format!("\"{card}\" needs a target minion")),
|
||||
None => Ok(()),
|
||||
Some(id) => {
|
||||
// Every id in `entities` was issued by the board, so `find` may ask about it.
|
||||
@@ -218,7 +218,9 @@ impl SpellGrid {
|
||||
pub fn check_play(&self, card: &str, x: i32, y: i32) -> Result<(), String> {
|
||||
self.side.check_hand(card, CardType::Minion)?;
|
||||
if self.side.world.card(card).def.noun.is_none() {
|
||||
return Err(format!("{card:?} has no noun: it can't stand on the grid"));
|
||||
return Err(format!(
|
||||
"\"{card}\" has no noun: it can't stand on the grid"
|
||||
));
|
||||
}
|
||||
if !self.level.board().in_bounds(x, y) {
|
||||
return Err(format!("({x}, {y}) is off the grid"));
|
||||
@@ -303,14 +305,14 @@ impl CardSide {
|
||||
|
||||
fn check_hand(&self, card: &str, card_type: CardType) -> Result<(), String> {
|
||||
if self.in_hand(card).is_none() {
|
||||
return Err(format!("{card:?} is not in the hand"));
|
||||
return Err(format!("\"{card}\" is not in the hand"));
|
||||
}
|
||||
if self.world.card(card).def.card_type != card_type {
|
||||
let kind = match card_type {
|
||||
CardType::Minion => "minion",
|
||||
CardType::Spell => "spell",
|
||||
};
|
||||
return Err(format!("{card:?} is not a {kind}"));
|
||||
return Err(format!("\"{card}\" is not a {kind}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user