Skip to main content

blockworx/tutorial/levels/
mod.rs

1//! The level registry: one embedded, self-contained `.kdl` file per level,
2//! in play order. To add a level, write the file (format:
3//! `docs/tutorial-levels.md`) and append it to `SOURCES`; the parse test in
4//! `level.rs` and the golden-replay test in `runner.rs` keep it honest.
5
6use egui::Rect;
7
8use super::level::Level;
9
10/// The view when a script forgot its opening `camera` command.
11pub const FALLBACK_CAMERA: Rect = Rect {
12    min: egui::Pos2 { x: 0.0, y: 0.0 },
13    max: egui::Pos2 {
14        x: 28.0 * crate::grid::GRID_SIZE,
15        y: 20.0 * crate::grid::GRID_SIZE,
16    },
17};
18
19const SOURCES: &[&str] = &[
20    include_str!("01_first_block.kdl"),
21    include_str!("02_first_route.kdl"),
22    include_str!("03_resize_move.kdl"),
23];
24
25/// The golden end states the demos must replay to, one per level in
26/// `SOURCES` order: (level id, path from the crate root for regeneration,
27/// contents). Each file holds `runner.rs`'s id-free projection of the
28/// document the demo ends in — nothing serializes the document back into a
29/// diagram format, so the projection IS the stored state. Held against the
30/// replay by `runner.rs`; regenerate with
31/// `BLOCKWORX_UPDATE_GOLDENS=1 cargo test`, or
32/// `cargo xtask tutorial golden`, and review the diff.
33#[cfg(test)]
34pub(super) const GOLDENS: &[(&str, &str, &str)] = &[
35    (
36        "first-block",
37        "src/tutorial/levels/01_first_block.golden.txt",
38        include_str!("01_first_block.golden.txt"),
39    ),
40    (
41        "first-route",
42        "src/tutorial/levels/02_first_route.golden.txt",
43        include_str!("02_first_route.golden.txt"),
44    ),
45    (
46        "resize-move",
47        "src/tutorial/levels/03_resize_move.golden.txt",
48        include_str!("03_resize_move.golden.txt"),
49    ),
50];
51
52pub fn all() -> Vec<Level> {
53    SOURCES
54        .iter()
55        .filter_map(|source| match Level::parse(source) {
56            Ok(level) => Some(level),
57            // Unreachable when the parse test passes; skip rather than
58            // take the app down over one bad embedded file.
59            Err(e) => {
60                tracing::error!("tutorial level failed to parse: {e}");
61                None
62            }
63        })
64        .collect()
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn every_source_parses() {
73        assert_eq!(all().len(), SOURCES.len());
74    }
75
76    #[test]
77    fn goldens_cover_every_level() {
78        let levels = all();
79        assert_eq!(levels.len(), GOLDENS.len());
80        for (level, (id, _, _)) in levels.iter().zip(GOLDENS) {
81            assert_eq!(&level.id, id, "GOLDENS must match SOURCES order");
82        }
83    }
84}