Documents all implemented modules, data structures, map file format, and key design decisions made during this session. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project
viberogue is a ZZT-inspired game-making system written in Rust (edition 2024). The goal is a system where players can create games using a scripting language, similar to the classic DOS game ZZT. It uses Rhai for scripting (WASM-compatible, sandboxed, pure Rust).
Commands
cargo build # compile
cargo run # build and run (loads maps/start.toml)
cargo test # run all tests
cargo test <name> # run a single test by name (substring match)
cargo clippy # lint
cargo fmt # format
Architecture
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:
egui::TopBottomPanel::top— menu bar (File → Exit)egui::CentralPanel::default— game viewport; rendered withui.painter()
Modules
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).Element— behavior:passable: bool(future:opaque, etc.)Board— the complete game unit (ZZT-style "board"):width,height,elements: Vec<Element>(behavior palette),cells: Vec<(Glyph, usize)>(row-major; usize indexes intoelements),player: Player,objects: Vec<ObjectDef>,portals: Vec<PortalDef>.cellsispub(crate).Player—x: i32, y: i32ObjectDef/PortalDef— parsed from map files, stored on Board; not yet runtime-wiredGameState— holdsboard: Board;try_move(dx, dy)checks passability before moving the player
src/map_file.rs — map file loading:
MapFileand friends — serdeDeserializetypes for TOML map filesimpl From<MapFile> for Board— converts a parsed file into a ready-to-useBoardpub fn load(path: &str) -> Result<Board, Box<dyn std::error::Error>>— reads and converts a.tomlmap file
src/main.rs — app entry point:
- Loads
maps/start.tomlbefore 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 - 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)- Player is rendered on top of the board using
Glyph::player() - Arrow key input handled via
ctx.input(|i| ...)before panel rendering
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.
[map]
name = "Room Name"
width = 60
height = 25
player_start = [30, 12]
[palette]
" " = { passable = true, ch = " ", fg = "#000000", bg = "#000000" }
"#" = { passable = false, ch = "#", fg = "#808080", bg = "#606060" }
[grid]
content = """
############################################################
# #
############################################################
"""
[[objects]] # optional; parsed but not yet runtime-wired
x = 10
y = 5
script = """
on_touch(|| { send_message("open"); });
"""
[[portals]] # optional; parsed but not yet runtime-wired
x = 59
y = 12
target_map = "cave"
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().
Key design decisions
cells: Vec<(Glyph, usize)>— theusizeelement index is intentionally an anonymous tuple field; it only has meaning relative to a specificBoard'selementspalette and cannot be misused as a standalone value.Boardis the complete unit — grid, player, objects, and portals all live onBoard, 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 sharesElementdefinitions from the palette. - 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.