multi board

This commit is contained in:
2026-06-13 01:25:58 -05:00
parent e5c6affc41
commit 73c8a9bd3e
13 changed files with 456 additions and 445 deletions
+94
View File
@@ -0,0 +1,94 @@
//! World type: a named collection of boards loaded from a single `.toml` file.
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::board::Board;
use crate::map_file::MapFile;
use serde::Deserialize;
/// A named world containing one or more boards, loaded from a `.toml` file.
///
/// The file groups all boards under a `[boards]` table. Each board key maps
/// to a full board definition (the same structure as a single-board map file,
/// but nested under `[boards.<key>.*]`). [`World::start`] names the board the
/// player enters first.
///
/// Scripts live at the world level ([`World::scripts`]) rather than on each
/// board, so multiple boards can reference the same script name without
/// duplicating source text.
pub struct World {
/// Human-readable name for this world, from the `[world]` header.
pub name: String,
/// Key of the starting board; matches a key in [`World::boards`].
pub start: String,
/// Named Rhai scripts shared across all boards: script name → source text.
///
/// Objects on any board reference a script by name via
/// [`ObjectDef::script_name`](crate::object_def::ObjectDef). The script pool is
/// passed to [`GameState::from_world`](crate::game::GameState::from_world) at startup.
pub scripts: HashMap<String, String>,
/// All boards in this world, keyed by their identifier string.
///
/// Every board is always present here as a shared `Rc<RefCell<Board>>` — no board
/// is ever "removed" when active. [`GameState`](crate::game::GameState) holds a
/// clone of the active board's `Rc` and borrows through it.
pub boards: HashMap<String, Rc<RefCell<Board>>>,
}
/// Serde target for the top-level `.toml` world file.
#[derive(Deserialize)]
struct WorldFile {
world: WorldHeader,
/// The `[scripts]` table: script name → Rhai source. Shared across all boards.
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
scripts: HashMap<String, String>,
/// Each value deserializes as a [`MapFile`], reusing the existing per-board
/// serde types from `map_file.rs`.
boards: HashMap<String, MapFile>,
}
/// The `[world]` header section of a world file.
#[derive(Deserialize)]
struct WorldHeader {
/// Human-readable name for this world.
name: String,
/// Key of the board to start on; must match a key in `[boards]`.
start: String,
}
/// Load a world from a `.toml` world file at `path`.
///
/// Returns an error if the file cannot be read or parsed, if any board fails
/// the hard-error conversion (only a grid-dimension mismatch qualifies — all
/// other problems are recorded on [`Board::load_errors`]), or if [`World::start`]
/// does not match any board key in the file.
pub fn load(path: &str) -> Result<World, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)?;
let wf: WorldFile = toml::from_str(&text)?;
// Convert each MapFile into a Board. Grid-dimension mismatches are the only
// hard errors; everything else is nonfatal and lands on Board::load_errors.
let mut boards = HashMap::new();
for (key, map_file) in wf.boards {
let board = Board::try_from(map_file)
.map_err(|e| format!("board '{}': {}", key, e))?;
boards.insert(key, Rc::new(RefCell::new(board)));
}
// Guard: the start key must actually exist in the boards map.
if !boards.contains_key(&wf.world.start) {
return Err(format!(
"start board '{}' not found in world '{}'",
wf.world.start, wf.world.name
)
.into());
}
Ok(World {
name: wf.world.name,
start: wf.world.start,
scripts: wf.scripts,
boards,
})
}