Current source: curriculum/arrowfall/checkpoints/02-state · Flecha 0.3.0 · 2/7.
The program at this checkpoint
Previously: Checkpoint 01 prints a recognizable room and has an exact visual baseline.
Checkpoint 01 draws an archer, but the @ character has no relationship to a game object. Moving it would require changing the literal row, and the coordinate printed elsewhere could silently disagree.
This checkpoint keeps the room visually unchanged while replacing that duplication with one source of truth. Game owns the current Position; every visible representation asks Game for that value.
Current limitation
Changing a character in a string cannot express movement. The renderer has no source of truth for the archer position.
Required next behavior: Keep the same room, but derive @ and the coordinate readout from data.
Model and invariants
Let Game own a Position. Drawing receives a copied value from state instead of owning the player.
Position groups the two coordinates that must change together. Game owns that position, and its build method establishes the starting coordinates in one place.
Position groups x and y because a location is only meaningful when both coordinates travel together. Game owns one Position privately, so code outside Game can observe the location but cannot replace the owned field directly.
Game::playerPosition takes a checked const reference to self and returns the small Position by value. The caller receives a copy for rendering; ownership of the authoritative player field remains in Game. This is the boundary between model and presentation in the first stateful version.
Rendering traverses the three interior columns and compares each coordinate with the copied player position. Exactly one comparison is true, so @ is derived from state. The exit remains a fixed rule for the last column and every other cell becomes floor.
The invariant is that Game::start establishes a valid initial position once. Drawing never changes it, and both the map glyph and the coordinate line use the same copied value. Input and mutation remain out of scope for this checkpoint.
Implementation
The implementation is split into the changes below. Each excerpt is taken verbatim from the qualified checkpoint and is explained in the part of the data flow where it is used. The excerpts are not standalone programs; the complete compiled source is available after the run.
- Introduce Position and Game.
- Construct state with build.
- Render @ by comparing the current coordinate.
Owned position and starting invariant
The first excerpt defines the ownership boundary. Position contains the two coordinates; Game contains one private Position and is the only owner of the authoritative player location.
Excerpt: src/main.fl:3–28. The complete file appears in the source section below.
structure Position { i32 x; i32 y;};
structure Game { private Position player;};
Position Position::at(i32 x, i32 y) { return build Position { self.x = x; self.y = y; };}
Game Game::start() { return build Game { self.player = Position::at(1, 1); };}
Position Game::playerPosition(&const self) { return self->player;}Position::at constructs both fields together, avoiding partially initialized coordinates. Game::start then chooses (1, 1) in one place instead of scattering the start cell through rendering code.
playerPosition borrows Game as const, so observing the location cannot mutate the model. Returning a copy is appropriate for this two-integer value; a reference would couple the renderer to Game storage without providing a benefit here.
Rendering from state
main obtains one snapshot of the player position and passes it to draw_row. The same snapshot is then used for the textual coordinate, preventing the two outputs from consulting different state.
Excerpt: src/main.fl:30–51. The complete file appears in the source section below.
state := Game::start(); player := state.playerPosition(); stdoutln("ARROWFALL"); stdoutln("#####"); draw_row(player); stdoutln("#####"); stdoutln("Player: (", player.x, ", ", player.y, ")");}
void draw_row(Position player) { stdout("#"); loop i32 x = 1; x < 4; x++ { if player.x == x and player.y == 1 { stdout("@"); } else if x == 3 { stdout(">"); } else { stdout("."); } } stdoutln("#");}draw_row visits x coordinates 1, 2, and 3. The first branch places @ only when both coordinates match; the second reserves column 3 for the exit; the final branch fills the remaining cell with floor.
The traversal is deliberately small and explicit. A full map array would be unnecessary until walls become data in checkpoint 04, but rendering from a coordinate already establishes the source-of-truth rule that the later map renderer keeps.
Run and inspect the result
The command uses the checkpoint’s recorded input, so the behavior described above can be reproduced without manual timing.
longbow run tutorial/checkpoints/02-state < tutorial/checkpoints/02-state/tests/input.txtThe frame still looks like checkpoint 01, then prints Player: (1, 1). That extra line is useful evidence: the @ glyph and the reported coordinate came from the same Position snapshot.
Changing Game::start to (2, 1) for the exercise must move both representations. If only one changes, rendering has reintroduced a second source of truth.
Accessible frame transcript
ARROWFALL######@.>######Player: (1, 1)Complete recorded stdout
ARROWFALL######@.>######Player: (1, 1)What this checkpoint establishes
The picture is now a view of owned state. Construction, observation, and traversal have separate responsibilities, and rendering cannot mutate Game through its const borrow.
The state still never changes after construction. The next checkpoint adds a typed input boundary and gives Game the only mutation operation for player movement.
Exercise
Start at (2, 1). Both @ and the printed coordinate must move. Restore (1, 1) afterward.
Complete source and verification
The sections below preserve the complete qualified source, the exact predecessor diff, and the command sequence used by CI. They are supporting evidence for the implementation explained above.
Complete tested source (3 files)
longbow.toml
[package]name = "arrowfall-step-02"version = "0.1.0"kind = "program"flecha = ">=0.3.0, <0.4.0"
[dependencies]longbow.lock
{ "schema": 1, "root": "root", "packages": [ { "id": "root", "identity": "local/arrowfall_step_02", "source": { "kind": "workspace", "path": "." }, "version": "0.1.0", "flecha": ">=0.3.0, <0.4.0", "dependencies": { "std": "toolchain-std" } }, { "id": "toolchain-std", "identity": "flecha/std", "source": { "kind": "toolchain" }, "dependencies": {} } ]}src/main.fl
module main;
structure Position { i32 x; i32 y;};
structure Game { private Position player;};
Position Position::at(i32 x, i32 y) { return build Position { self.x = x; self.y = y; };}
Game Game::start() { return build Game { self.player = Position::at(1, 1); };}
Position Game::playerPosition(&const self) { return self->player;}
void main() { state := Game::start(); player := state.playerPosition(); stdoutln("ARROWFALL"); stdoutln("#####"); draw_row(player); stdoutln("#####"); stdoutln("Player: (", player.x, ", ", player.y, ")");}
void draw_row(Position player) { stdout("#"); loop i32 x = 1; x < 4; x++ { if player.x == x and player.y == 1 { stdout("@"); } else if x == 3 { stdout(">"); } else { stdout("."); } } stdoutln("#");}Exact diff from the preceding version
diff -rN -U0 --exclude target tutorial/checkpoints/01-scene/README.md tutorial/checkpoints/02-state/README.md--- tutorial/checkpoints/01-scene/README.md+++ tutorial/checkpoints/02-state/README.md@@ -1 +1 @@-# 01 — A room appears+# 02 — The picture becomes state@@ -5 +5 @@-There is no previous checkpoint: we begin with an empty Flecha program.+Checkpoint 01 prints a recognisable but hard-coded room.@@ -9,2 +9,2 @@-A terminal game first needs something recognisable on screen. Before state,-input, or rules, a static room gives the project a visible identity.+Changing a character in a string cannot express movement. The player needs a+position owned by game state, and drawing must read that position.@@ -14,2 +14,2 @@-Running the program prints Arrowfall's title and a tiny room containing the-player (`@`), floor (`.`), walls (`#`), and exit (`>`).+`Game` owns a `Position`; the scene derives `@` from it and reports `(1, 1)`.+The visible room is unchanged, but its source of truth is now data.@@ -28,2 +28,2 @@-The exact source diff is `../../diffs/01-scene.patch`. This directory contains-the complete current code, not a fragment.+See `../../diffs/02-state.patch` for the exact predecessor diff. All current+source is present in this directory.@@ -34,4 +34,4 @@-longbow check tutorial/checkpoints/01-scene-longbow test tutorial/checkpoints/01-scene-longbow build tutorial/checkpoints/01-scene-longbow run tutorial/checkpoints/01-scene < tutorial/checkpoints/01-scene/tests/input.txt+longbow check tutorial/checkpoints/02-state+longbow test tutorial/checkpoints/02-state+longbow build tutorial/checkpoints/02-state+longbow run tutorial/checkpoints/02-state < tutorial/checkpoints/02-state/tests/input.txt@@ -40,3 +39,0 @@-Expected stdout is `tests/output.txt`; the representative frame is-`tests/frame.txt` and `tests/frame.svg`.-@@ -45 +42,2 @@-1. **A room appears** — a static scene establishes the game's visual goal.+1. A room appears — static terminal scene.+2. **The picture becomes state** — structs and methods own the player position.@@ -49,2 +47,2 @@-Change one floor dot to a wall, rebuild, and verify exactly one character in-the frame changes. Restore it before moving on.+Set the start position to `(2, 1)`: both `@` and the reported coordinate must+move. Restore `(1, 1)` afterward.diff -rN -U0 --exclude target tutorial/checkpoints/01-scene/longbow.lock tutorial/checkpoints/02-state/longbow.lock--- tutorial/checkpoints/01-scene/longbow.lock+++ tutorial/checkpoints/02-state/longbow.lock@@ -7 +7 @@- "identity": "local/arrowfall_step_01",+ "identity": "local/arrowfall_step_02",diff -rN -U0 --exclude target tutorial/checkpoints/01-scene/longbow.toml tutorial/checkpoints/02-state/longbow.toml--- tutorial/checkpoints/01-scene/longbow.toml+++ tutorial/checkpoints/02-state/longbow.toml@@ -2 +2 @@-name = "arrowfall-step-01"+name = "arrowfall-step-02"diff -rN -U0 --exclude target tutorial/checkpoints/01-scene/src/main.fl tutorial/checkpoints/02-state/src/main.fl--- tutorial/checkpoints/01-scene/src/main.fl+++ tutorial/checkpoints/02-state/src/main.fl@@ -2,0 +3,26 @@+structure Position {+ i32 x;+ i32 y;+};++structure Game {+ private Position player;+};++Position Position::at(i32 x, i32 y) {+ return build Position {+ self.x = x;+ self.y = y;+ };+}++Game Game::start() {+ return build Game {+ self.player = Position::at(1, 1);+ };+}++Position Game::playerPosition(&const self) {+ return self->player;+}+@@ -3,0 +30,2 @@+ state := Game::start();+ player := state.playerPosition();@@ -6 +34 @@- stdoutln("#@.>#");+ draw_row(player);@@ -7,0 +36,15 @@+ stdoutln("Player: (", player.x, ", ", player.y, ")");+}++void draw_row(Position player) {+ stdout("#");+ loop i32 x = 1; x < 4; x++ {+ if player.x == x and player.y == 1 {+ stdout("@");+ } else if x == 3 {+ stdout(">");+ } else {+ stdout(".");+ }+ }+ stdoutln("#");diff -rN -U0 --exclude target tutorial/checkpoints/01-scene/tests/frame.svg tutorial/checkpoints/02-state/tests/frame.svg--- tutorial/checkpoints/01-scene/tests/frame.svg+++ tutorial/checkpoints/02-state/tests/frame.svg@@ -2,2 +2,2 @@-<svg xmlns="http://www.w3.org/2000/svg" width="360" height="124" viewBox="0 0 360 124" role="img">- <title>01-scene representative frame</title>+<svg xmlns="http://www.w3.org/2000/svg" width="360" height="143" viewBox="0 0 360 143" role="img">+ <title>02-state representative frame</title>@@ -9,0 +10 @@+ <text x="24" y="113" class="terminal">Player: (1, 1)</text>diff -rN -U0 --exclude target tutorial/checkpoints/01-scene/tests/frame.txt tutorial/checkpoints/02-state/tests/frame.txt--- tutorial/checkpoints/01-scene/tests/frame.txt+++ tutorial/checkpoints/02-state/tests/frame.txt@@ -4,0 +5 @@+Player: (1, 1)diff -rN -U0 --exclude target tutorial/checkpoints/01-scene/tests/output.txt tutorial/checkpoints/02-state/tests/output.txt--- tutorial/checkpoints/01-scene/tests/output.txt+++ tutorial/checkpoints/02-state/tests/output.txt@@ -4,0 +5 @@+Player: (1, 1)Check, test, build, and run commands
longbow check tutorial/checkpoints/02-statelongbow test tutorial/checkpoints/02-statelongbow build tutorial/checkpoints/02-statelongbow run tutorial/checkpoints/02-state < tutorial/checkpoints/02-state/tests/input.txtVersions in this tutorial
- 1. Render the first room
- 2. Represent game state
Next version
Rendering now reads state, but no operation changes it. The next version maps standard input to movement commands.
References
Source: Arrowfall commit 56bb003, verified with official Flecha 0.3.0. The source, input, output, diff, and frame on this page are read from that committed snapshot.