Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.0 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project
kiln 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).
Code style
- Add
///rustdoc comments to everypubtype, field, and function. The user reads rustdoc in their IDE to understand the code while making changes. - Add inline
//comments inside non-trivial function bodies to explain the why of each logical step — especially inupdateloops, rendering math, and conversion logic. - Keep comments accurate: update them when the code they describe changes.
Finishing an epic
When the user says "finish the epic", do all of the following in order:
- Update
CLAUDE.mdto reflect any new modules, types, or behaviors added during the session. - Update
ARCHITECTURE.mdto reflect the same. - Run the
/simplifyskill on changed code. - Commit everything with a summary message.
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 phases:
- Input handling — arrow keys move the player (Play mode only)
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 withui.painter(); click-to-paint in Edit mode
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).Behavior— plain data struct of runtime behavioral properties:passable: bool,opaque: bool. Returned byArchetype::behavior(); new properties added here require no match arms elsewhere.Archetype(Copy,PartialEq) — enum of named element types:Empty,Wall,Object,ErrorBlock. Each variant providesbehavior(),name()(used in map files), anddefault_glyph()(used by the editor when stamping a cell).ErrorBlockis a sentinel for unknown archetype names — renders as yellow?on red.ALL_ARCHETYPES: &[Archetype]— ordered list of valid editor choices (excludesErrorBlock).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>.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 and frame loop:
AppModeenum (Play|Edit) — gates arrow-key input; toggles the side panel and viewport modeAppholdsGameState,AppMode, andEditorState;updatephases: input → menu bar → editor panel → board → glyph picker dialog- Play mode: arrow keys move player;
render::board_origincenters or player-tracks the viewport - Edit mode:
ScrollArea::both()wraps the board; click-to-paint stamps(editor.glyph, editor.selected); callseditor::show_editor_panelandglyph_picker::show
src/render.rs — cell rendering constants and drawing primitives:
CELL_W = 14.0,CELL_H = 20.0and window sizing constants (DEFAULT_WINDOW_W = 840,DEFAULT_WINDOW_H = 524)draw_glyph(painter, origin, x, y, glyph)— filled rect (bg) + centered monospace char (fg)draw_board(painter, origin, board)— draws all cells then player overlayboard_origin(available, board_w, board_h, player)— centers board or clamps to player with no empty spacepos_to_cell(origin, pos) -> (i32, i32)— pixel → cell coordinates via floor division; negatives signal out-of-bounds
src/editor.rs — editor state and side panel:
EditorTabenum (Palette|Board|World) — which tab is activeEditorState— holdsselected: Archetype,glyph: Glyph,glyph_picker_open: bool,tab: EditorTab; selecting a new archetype resetsglyphto that archetype's defaultshow_editor_panel(ctx, editor, board)— resizable right-side panel (default 200 px); Palette tab shows archetype list and glyph preview button; Board/World tabs are placeholders
src/glyph_picker.rs — floating glyph picker dialog:
show(ctx, open, glyph, board_cells)— takesopen: &mut boolandglyph: &mut Glyphdirectly; no dependency onEditorState- Three sections: board palette (unique glyphs, click to select), FG/BG color pickers, 16×6 printable ASCII character grid
Map file format (maps/*.toml)
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.
[map]
name = "Room Name"
width = 60
height = 25
player_start = [30, 12]
[palette]
" " = { archetype = "empty", ch = " ", fg = "#000000", bg = "#000000" }
"#" = { archetype = "wall", 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(). Unknown archetype names produce an ErrorBlock cell and a logged warning.
Key design decisions
BehaviorandArchetypeare separate types —Archetypeis the named class of a thing (Wall,Empty,Object);Behavioris its runtime properties (passable,opaque). Adding a new property means adding a field toBehavior, 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.ArchetypeisCopyso this is efficient.- Archetypes are referenced by name in map files — so
ALL_ARCHETYPEScan be reordered or extended without breaking saved games. 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 Archetype (behavior) are decoupled — each cell has its own
Glyph(so colors can vary per-cell, e.g. fire flickering) while sharing anArchetypewith other cells of the same type. - 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.