Update CLAUDE.md and ARCHITECTURE.md for Behavior/Archetype redesign and editor mode
Documents the replacement of Element with Behavior + Archetype, the elimination of the per-board element palette, the new map file format (archetype = "name" instead of passable = bool), the ErrorBlock sentinel, and the editor mode (AppMode, EditorState, palette panel, click-to-paint). Also updates "What's not yet implemented" to reflect editor progress and call out the Object behavior / glyph chooser / file open gaps. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+41
-19
@@ -38,22 +38,32 @@ The types here are the runtime representation of a game world. They are delibera
|
|||||||
**`Glyph`** (`Copy`)
|
**`Glyph`** (`Copy`)
|
||||||
The visual representation of one cell: a character, a foreground color, and a background color. Stored *per cell* (not per element type) so individual cells can animate or vary their appearance independently — e.g. a "fire" element where each flame tile has a slightly different color — without changing their behavior.
|
The visual representation of one cell: a character, a foreground color, and a background color. Stored *per cell* (not per element type) so individual cells can animate or vary their appearance independently — e.g. a "fire" element where each flame tile has a slightly different color — without changing their behavior.
|
||||||
|
|
||||||
**`Element`**
|
**`Behavior`**
|
||||||
The behavioral definition of a tile type. Currently just `passable: bool`; future fields will include `opaque: bool` (for line-of-sight), `shootable`, etc. Elements are stored once in a palette on `Board`; cells reference them by index. Many cells can share the same Element.
|
A plain data struct of runtime behavioral properties — currently `passable: bool` and `opaque: bool`. Returned by `Archetype::behavior()`. Adding a new property (e.g. `shootable`) requires only a new field here; no match arms needed at call sites. This is a deliberate improvement over storing behavior as a bag of booleans on a per-cell or per-archetype basis.
|
||||||
|
|
||||||
**Why Glyph and Element are separate:**
|
**`Archetype`**
|
||||||
In ZZT, each board tile had both a visual (character + color pair) and an element type. The visual could vary per-tile even for the same element. We replicate this: `Glyph` is the visual (per-cell), `Element` is the behavior (shared via palette). This lets you have a wall that's gray in one room and blue in another without creating two "wall" element types.
|
An enum of named element types: `Empty`, `Wall`, `Object`, `ErrorBlock`. Each variant knows:
|
||||||
|
- Its canonical map-file name (e.g. `"wall"`) via `name()`
|
||||||
|
- Its default `Behavior` via `behavior()`
|
||||||
|
- Its default `Glyph` via `default_glyph()` — used by the editor when stamping a cell
|
||||||
|
|
||||||
|
`ErrorBlock` is a sentinel for map files that reference an unknown archetype name. It renders as a yellow `?` on red so malformed maps are immediately visible in-game. It is excluded from `ALL_ARCHETYPES` (the editor's list) because it is not a valid authoring choice.
|
||||||
|
|
||||||
|
**Why `Behavior` and `Archetype` are separate types:**
|
||||||
|
`Archetype` is the *named class* of a thing — it lets you say "this is a wall" in a map file and in the editor. `Behavior` is the *runtime properties* that drive simulation. Keeping them separate means adding a new property (e.g. `pushable`) only requires a new field on `Behavior`; no match arms need to be updated across the codebase.
|
||||||
|
|
||||||
|
**Why Glyph and Archetype are separate:**
|
||||||
|
In ZZT, each board tile had both a visual (character + color pair) and an element type. The visual could vary per-tile even for the same element type. We replicate this: `Glyph` is the visual (per-cell), `Archetype` is the class (shared across cells of the same type). This lets you have a wall that's gray in one room and blue in another without creating two archetype variants.
|
||||||
|
|
||||||
**`Board`**
|
**`Board`**
|
||||||
The complete unit of a game world — one "room" or "screen" in ZZT terminology. Holds:
|
The complete unit of a game world — one "room" or "screen" in ZZT terminology. Holds:
|
||||||
- `elements: Vec<Element>` — the behavior palette for this board
|
- `cells: Vec<(Glyph, Archetype)>` — row-major grid; each cell directly owns its visual and behavioral class
|
||||||
- `cells: Vec<(Glyph, usize)>` — row-major grid; each cell is a visual + an index into `elements`
|
|
||||||
- `player: Player` — current player position on this board
|
- `player: Player` — current player position on this board
|
||||||
- `objects: Vec<ObjectDef>` — scripted objects (parsed, not yet runtime-wired)
|
- `objects: Vec<ObjectDef>` — scripted objects (parsed, not yet runtime-wired)
|
||||||
- `portals: Vec<PortalDef>` — exits to other boards (parsed, not yet runtime-wired)
|
- `portals: Vec<PortalDef>` — exits to other boards (parsed, not yet runtime-wired)
|
||||||
|
|
||||||
**Why `cells` is `Vec<(Glyph, usize)>` not `Vec<Cell>`:**
|
**Why `cells` is `Vec<(Glyph, Archetype)>` with no palette:**
|
||||||
An anonymous tuple prevents the element index from being detached from its board and misused. The `usize` has no meaning on its own — it only makes sense relative to a specific `Board`'s `elements` palette. Wrapping it in a named `Cell` struct would give it false independence.
|
An earlier design had `cells: Vec<(Glyph, usize)>` where the `usize` indexed a per-board `elements: Vec<Element>` palette. This was eliminated because the palette indirection added complexity without benefit: `Archetype` is a `Copy` enum (4 bytes), so storing it per-cell is as efficient as storing an index, and it removes the invariant that the index must stay in sync with a specific board's palette.
|
||||||
|
|
||||||
**Why `Board` is the complete unit (no wrapper struct):**
|
**Why `Board` is the complete unit (no wrapper struct):**
|
||||||
An earlier design had `GameMap { board: Board, player: Player, ... }`. This was eliminated because the split was artificial: there's no meaningful use of a `Board` without a player position, and no meaningful use of a player without a `Board`. ZZT itself treats a board as containing everything — the grid, the objects, and the player entry point. Collapsing to a single struct matches the domain model.
|
An earlier design had `GameMap { board: Board, player: Player, ... }`. This was eliminated because the split was artificial: there's no meaningful use of a `Board` without a player position, and no meaningful use of a player without a `Board`. ZZT itself treats a board as containing everything — the grid, the objects, and the player entry point. Collapsing to a single struct matches the domain model.
|
||||||
@@ -67,7 +77,7 @@ Currently a thin wrapper around `Board` that provides game-logic methods (`try_m
|
|||||||
|
|
||||||
**`MapFile`** and supporting structs are serde deserialization types only — they exist solely to parse TOML and are never used at runtime.
|
**`MapFile`** and supporting structs are serde deserialization types only — they exist solely to parse TOML and are never used at runtime.
|
||||||
|
|
||||||
**`impl From<MapFile> for Board`** — the single conversion point. Reads the palette, builds the `elements` vec, then walks the grid string character-by-character to build `cells`. This is the only place that knows about both the file format and the runtime representation.
|
**`impl From<MapFile> for Board`** — the single conversion point. Reads the palette (each entry maps a character to a `(Glyph, Archetype)` pair), then walks the grid string character-by-character to build `cells`. Unknown archetype names produce an `ErrorBlock` cell and log a warning. This is the only place that knows about both the file format and the runtime representation.
|
||||||
|
|
||||||
**`pub fn load(path: &str) -> Result<Board, ...>`** — reads a file, deserializes, converts. Called from `main()` before the window is created.
|
**`pub fn load(path: &str) -> Result<Board, ...>`** — reads a file, deserializes, converts. Called from `main()` before the window is created.
|
||||||
|
|
||||||
@@ -78,13 +88,18 @@ The window minimum size is derived from `board.width` and `board.height`. eframe
|
|||||||
|
|
||||||
### `main.rs` — app + rendering
|
### `main.rs` — app + rendering
|
||||||
|
|
||||||
`App` holds a `GameState`. The `update` method:
|
`App` holds a `GameState`, an `AppMode` (`Play` | `Edit`), and an `EditorState` (`selected: Archetype`). The `update` method:
|
||||||
1. Reads arrow key input and calls `GameState::try_move`
|
|
||||||
2. Draws the menu bar
|
|
||||||
3. Draws the board: for each cell, a filled background rect then a centered monospace character
|
|
||||||
4. Draws the player on top using `Glyph::player()` (hardcoded `@` in cyan — the player is not a board cell)
|
|
||||||
|
|
||||||
**Cell rendering constants:** `CELL_W = 14.0`, `CELL_H = 20.0` pixels. The board is centered in the CentralPanel when the window is larger than the minimum size.
|
1. Reads arrow key input and calls `GameState::try_move` — **Play mode only**
|
||||||
|
2. Draws the menu bar with a File menu and a Play/Edit mode toggle
|
||||||
|
3. In Edit mode: draws a right-side palette panel listing `ALL_ARCHETYPES` as selectable rows — declared before `CentralPanel` so egui allocates its space first
|
||||||
|
4. Draws the board: for each cell, a filled background rect then a centered monospace character
|
||||||
|
5. Draws the player on top using `Glyph::player()` (hardcoded `@` in cyan — the player is not a board cell)
|
||||||
|
6. In Edit mode: allocates the full viewport as a click target; on click, converts pixel position to cell coordinates and stamps the cell with `(selected.default_glyph(), selected)`
|
||||||
|
|
||||||
|
**Cell rendering constants:** `CELL_W = 14.0`, `CELL_H = 20.0` pixels. The board is centered in the CentralPanel when the window is larger than the minimum size. Window min-size is computed for Play mode; the Edit mode palette panel (`PALETTE_PANEL_W = 120.0` px) may clip the board at minimum size.
|
||||||
|
|
||||||
|
**Click-to-cell math:** `rel = click_pos - origin; cell_x = (rel.x / CELL_W).floor() as i32`. Negative `rel` values stay negative and are caught by a `>= 0` guard. This mirrors the rendering math exactly so clicks land on the correct cell.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -100,8 +115,8 @@ height = 25
|
|||||||
player_start = [30, 12]
|
player_start = [30, 12]
|
||||||
|
|
||||||
[palette]
|
[palette]
|
||||||
" " = { passable = true, ch = " ", fg = "#000000", bg = "#000000" }
|
" " = { archetype = "empty", ch = " ", fg = "#000000", bg = "#000000" }
|
||||||
"#" = { passable = false, ch = "#", fg = "#808080", bg = "#606060" }
|
"#" = { archetype = "wall", ch = "#", fg = "#808080", bg = "#606060" }
|
||||||
|
|
||||||
[grid]
|
[grid]
|
||||||
content = """
|
content = """
|
||||||
@@ -127,8 +142,11 @@ target_entry = "west_door"
|
|||||||
**Why TOML over a custom format:**
|
**Why TOML over a custom format:**
|
||||||
The `toml` crate gives us deserialization with minimal code. Multi-line strings for the grid give a visual representation of the map. Embedded Rhai scripts fit naturally in TOML multi-line strings without escaping issues.
|
The `toml` crate gives us deserialization with minimal code. Multi-line strings for the grid give a visual representation of the map. Embedded Rhai scripts fit naturally in TOML multi-line strings without escaping issues.
|
||||||
|
|
||||||
|
**Why archetypes by name, not by property:**
|
||||||
|
Palette entries used to store `passable = true/false` directly. Switching to `archetype = "wall"` means adding a new behavioral property (e.g. `opaque`) only requires a code change — existing map files don't need to be updated. It also makes files more readable: `archetype = "wall"` is self-documenting. Unknown names produce a visible `ErrorBlock` cell rather than silently defaulting.
|
||||||
|
|
||||||
**Why the palette approach:**
|
**Why the palette approach:**
|
||||||
A direct mapping from palette character → `(Glyph, Element)` means the map file is both human-readable (you can see the shape of the room from the grid string) and efficient (shared element definitions, per-cell visual variation possible by using different palette chars with the same `passable` value but different colors).
|
A direct mapping from palette character → `(Glyph, Archetype)` means the map file is both human-readable (you can see the shape of the room from the grid string) and flexible (per-cell visual variation is possible by using different palette chars with the same archetype but different colors).
|
||||||
|
|
||||||
**Colors** are `"#RRGGBB"` hex strings — universally understood, hand-editable.
|
**Colors** are `"#RRGGBB"` hex strings — universally understood, hand-editable.
|
||||||
|
|
||||||
@@ -142,11 +160,15 @@ A direct mapping from palette character → `(Glyph, Element)` means the map fil
|
|||||||
1. Wire `ObjectDef` scripts to Rhai: when the player moves to an object's cell, fire its `on_touch` handler
|
1. Wire `ObjectDef` scripts to Rhai: when the player moves to an object's cell, fire its `on_touch` handler
|
||||||
2. Define the Rhai API surface (what functions scripts can call: `send_message`, movement, board queries)
|
2. Define the Rhai API surface (what functions scripts can call: `send_message`, movement, board queries)
|
||||||
|
|
||||||
|
**Object behavior from scripts** — `Archetype::Object.behavior()` currently returns a static default (`passable: false, opaque: false`). Eventually, `Board::is_passable` will need a special branch for Object cells that reads the behavior from the cell's Rhai script instead.
|
||||||
|
|
||||||
**Portal navigation** — `PortalDef` stores a `target_map` and `target_entry` but there's no multi-board loading or board switching yet.
|
**Portal navigation** — `PortalDef` stores a `target_map` and `target_entry` but there's no multi-board loading or board switching yet.
|
||||||
|
|
||||||
**Multi-board world** — right now the engine loads a single `maps/start.toml`. Future: a world file or directory of boards, lazy-loaded as the player moves through portals.
|
**Multi-board world** — right now the engine loads a single `maps/start.toml`. Future: a world file or directory of boards, lazy-loaded as the player moves through portals.
|
||||||
|
|
||||||
**Game creation tools** — the long-term goal is an in-app editor. Not started.
|
**Editor glyph chooser** — the editor can paint cells with archetypes (default glyphs), but there's no way yet to choose a custom character or color for a cell. The next editor feature is buttons that open char/color picker dialogs.
|
||||||
|
|
||||||
|
**File open dialog** — the editor has no way to load a different map file from the UI. This requires a native file dialog (e.g. `rfd`). WASM support for file dialogs is an open question.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -27,16 +27,20 @@ cargo fmt # format
|
|||||||
|
|
||||||
The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI. eframe drives a retained-mode UI: the `App::update` method is called every frame and is responsible for both drawing and responding to input.
|
The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI. eframe drives a retained-mode UI: the `App::update` method is called every frame and is responsible for both drawing and responding to input.
|
||||||
|
|
||||||
`update` is structured in two panels:
|
`update` is structured in phases:
|
||||||
- `egui::TopBottomPanel::top` — menu bar (File → Exit)
|
- Input handling — arrow keys move the player (Play mode only)
|
||||||
- `egui::CentralPanel::default` — game viewport; rendered with `ui.painter()`
|
- `egui::TopBottomPanel::top` — menu bar (File → Exit, Play/Edit mode toggle)
|
||||||
|
- `egui::SidePanel::right` — archetype palette panel (Edit mode only; declared before CentralPanel)
|
||||||
|
- `egui::CentralPanel::default` — game viewport; rendered with `ui.painter()`; click-to-paint in Edit mode
|
||||||
|
|
||||||
### Modules
|
### Modules
|
||||||
|
|
||||||
**`src/game.rs`** — all core game types:
|
**`src/game.rs`** — all core game types:
|
||||||
- `Glyph` (`Copy`) — per-cell visual: `ch: char`, `fg/bg: Color32`. `Glyph::player()` is the only constructor used at runtime (colors for board tiles come from the map file).
|
- `Glyph` (`Copy`) — per-cell visual: `ch: char`, `fg/bg: Color32`. `Glyph::player()` is the only constructor used at runtime (colors for board tiles come from the map file).
|
||||||
- `Element` — behavior: `passable: bool` (future: `opaque`, etc.)
|
- `Behavior` — plain data struct of runtime behavioral properties: `passable: bool`, `opaque: bool`. Returned by `Archetype::behavior()`; new properties added here require no match arms elsewhere.
|
||||||
- `Board` — the complete game unit (ZZT-style "board"): `width`, `height`, `elements: Vec<Element>` (behavior palette), `cells: Vec<(Glyph, usize)>` (row-major; usize indexes into `elements`), `player: Player`, `objects: Vec<ObjectDef>`, `portals: Vec<PortalDef>`. `cells` is `pub(crate)`.
|
- `Archetype` (`Copy`, `PartialEq`) — enum of named element types: `Empty`, `Wall`, `Object`, `ErrorBlock`. Each variant provides `behavior()`, `name()` (used in map files), and `default_glyph()` (used by the editor when stamping a cell). `ErrorBlock` is a sentinel for unknown archetype names — renders as yellow `?` on red.
|
||||||
|
- `ALL_ARCHETYPES: &[Archetype]` — ordered list of valid editor choices (excludes `ErrorBlock`).
|
||||||
|
- `Board` — the complete game unit (ZZT-style "board"): `width`, `height`, `cells: Vec<(Glyph, Archetype)>` (row-major; each cell owns its visual and behavioral class directly), `player: Player`, `objects: Vec<ObjectDef>`, `portals: Vec<PortalDef>`. `cells` is `pub(crate)`.
|
||||||
- `Player` — `x: i32, y: i32`
|
- `Player` — `x: i32, y: i32`
|
||||||
- `ObjectDef` / `PortalDef` — parsed from map files, stored on Board; not yet runtime-wired
|
- `ObjectDef` / `PortalDef` — parsed from map files, stored on Board; not yet runtime-wired
|
||||||
- `GameState` — holds `board: Board`; `try_move(dx, dy)` checks passability before moving the player
|
- `GameState` — holds `board: Board`; `try_move(dx, dy)` checks passability before moving the player
|
||||||
@@ -47,16 +51,18 @@ The game is a single Rust binary using **eframe 0.33 / egui 0.33** for the GUI.
|
|||||||
- `pub fn load(path: &str) -> Result<Board, Box<dyn std::error::Error>>` — reads and converts a `.toml` map file
|
- `pub fn load(path: &str) -> Result<Board, Box<dyn std::error::Error>>` — reads and converts a `.toml` map file
|
||||||
|
|
||||||
**`src/main.rs`** — app entry point:
|
**`src/main.rs`** — app entry point:
|
||||||
|
- `AppMode` enum (`Play` | `Edit`) — gates arrow-key input; toggles the palette panel
|
||||||
|
- `EditorState` — holds `selected: Archetype` (the archetype to paint on click)
|
||||||
- Loads `maps/start.toml` before creating the window (so board dimensions drive window size)
|
- Loads `maps/start.toml` before creating the window (so board dimensions drive window size)
|
||||||
- Window min-size = `board.width * CELL_W + 2*PANEL_MARGIN` × `board.height * CELL_H + MENU_H + 2*PANEL_MARGIN`
|
- Window min-size = `board.width * CELL_W + 2*PANEL_MARGIN` × `board.height * CELL_H + MENU_H + 2*PANEL_MARGIN` (sized for Play mode; Edit mode panel may clip at minimum size)
|
||||||
- Board is centered in the CentralPanel when the window is larger than minimum
|
- Board is centered in the CentralPanel when the window is larger than minimum
|
||||||
- `draw_glyph(painter, origin, x, y, glyph)` draws one cell: filled rect (bg) + monospace char (fg)
|
- `draw_glyph(painter, origin, x, y, glyph)` draws one cell: filled rect (bg) + monospace char (fg)
|
||||||
- Player is rendered on top of the board using `Glyph::player()`
|
- Player is rendered on top of the board using `Glyph::player()`
|
||||||
- Arrow key input handled via `ctx.input(|i| ...)` before panel rendering
|
- Click-to-paint converts `mouse_pos - origin` to cell coordinates using the same math as rendering
|
||||||
|
|
||||||
### Map file format (`maps/*.toml`)
|
### Map file format (`maps/*.toml`)
|
||||||
|
|
||||||
XPM-inspired: a `[palette]` maps single characters to `(Glyph, Element)` definitions; `[grid] content` is a TOML multi-line string where each character indexes the palette.
|
XPM-inspired: a `[palette]` maps single characters to `(Glyph, Archetype)` definitions; `[grid] content` is a TOML multi-line string where each character indexes the palette.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[map]
|
[map]
|
||||||
@@ -66,8 +72,8 @@ height = 25
|
|||||||
player_start = [30, 12]
|
player_start = [30, 12]
|
||||||
|
|
||||||
[palette]
|
[palette]
|
||||||
" " = { passable = true, ch = " ", fg = "#000000", bg = "#000000" }
|
" " = { archetype = "empty", ch = " ", fg = "#000000", bg = "#000000" }
|
||||||
"#" = { passable = false, ch = "#", fg = "#808080", bg = "#606060" }
|
"#" = { archetype = "wall", ch = "#", fg = "#808080", bg = "#606060" }
|
||||||
|
|
||||||
[grid]
|
[grid]
|
||||||
content = """
|
content = """
|
||||||
@@ -90,13 +96,15 @@ target_map = "cave"
|
|||||||
target_entry = "west_door"
|
target_entry = "west_door"
|
||||||
```
|
```
|
||||||
|
|
||||||
Colors are `"#RRGGBB"` hex strings. `player_start` is a header field — the player is not a board cell. The grid multi-line string's leading newline is trimmed by TOML; trailing newline is handled correctly by `str::lines()`.
|
Colors are `"#RRGGBB"` hex strings. `player_start` is a header field — the player is not a board cell. The grid multi-line string's leading newline is trimmed by TOML; trailing newline is handled correctly by `str::lines()`. Unknown archetype names produce an `ErrorBlock` cell and a logged warning.
|
||||||
|
|
||||||
### Key design decisions
|
### Key design decisions
|
||||||
|
|
||||||
- **`cells: Vec<(Glyph, usize)>`** — the `usize` element index is intentionally an anonymous tuple field; it only has meaning relative to a specific `Board`'s `elements` palette and cannot be misused as a standalone value.
|
- **`Behavior` and `Archetype` are separate types** — `Archetype` is the named class of a thing (`Wall`, `Empty`, `Object`); `Behavior` is its runtime properties (`passable`, `opaque`). Adding a new property means adding a field to `Behavior`, not a match arm at every call site.
|
||||||
|
- **`cells: Vec<(Glyph, Archetype)>`** — each cell owns its visual and behavioral class directly; there is no per-board element palette or integer indirection. `Archetype` is `Copy` so this is efficient.
|
||||||
|
- **Archetypes are referenced by name in map files** — so `ALL_ARCHETYPES` can be reordered or extended without breaking saved games.
|
||||||
- **`Board` is the complete unit** — grid, player, objects, and portals all live on `Board`, matching how ZZT treats a "board". No separate wrapper struct.
|
- **`Board` is the complete unit** — grid, player, objects, and portals all live on `Board`, matching how ZZT treats a "board". No separate wrapper struct.
|
||||||
- **Glyph (visual) and Element (behavior) are decoupled** — each cell has its own `Glyph` (so colors can vary per-cell, e.g. fire flickering) but shares `Element` definitions from the palette.
|
- **Glyph (visual) and Archetype (behavior) are decoupled** — each cell has its own `Glyph` (so colors can vary per-cell, e.g. fire flickering) while sharing an `Archetype` with other cells of the same type.
|
||||||
- **File loading happens in `main()`** before the window is created, so board dimensions are available for window sizing.
|
- **File loading happens in `main()`** before the window is created, so board dimensions are available for window sizing.
|
||||||
|
|
||||||
eframe runs on its own event loop thread; do not assume single-threaded execution.
|
eframe runs on its own event loop thread; do not assume single-threaded execution.
|
||||||
|
|||||||
+1
-1
@@ -221,4 +221,4 @@ fn draw_glyph(painter: &egui::Painter, origin: Pos2, x: usize, y: usize, glyph:
|
|||||||
let center = rect.center();
|
let center = rect.center();
|
||||||
painter.rect_filled(rect, 0.0, glyph.bg);
|
painter.rect_filled(rect, 0.0, glyph.bg);
|
||||||
painter.text(center, Align2::CENTER_CENTER, glyph.ch, FontId::monospace(CELL_H), glyph.fg);
|
painter.text(center, Align2::CENTER_CENTER, glyph.ch, FontId::monospace(CELL_H), glyph.fg);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -172,4 +172,4 @@ pub fn load(path: &str) -> Result<Board, Box<dyn std::error::Error>> {
|
|||||||
let content = std::fs::read_to_string(path)?;
|
let content = std::fs::read_to_string(path)?;
|
||||||
let map_file: MapFile = toml::from_str(&content)?;
|
let map_file: MapFile = toml::from_str(&content)?;
|
||||||
Ok(Board::from(map_file))
|
Ok(Board::from(map_file))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user