Introduction
A score is only one decision. A complete match must remember which categories you used and keep the same dice while you correct an invalid choice. You will finish a three-round game and save a card that can be checked by recomputing its dice and categories.
The scoring rules from the first lab are already installed in this independent project. You should know those rules, Rust functions, enum, match, Option, arrays, and basic terminal paths. This lab introduces the prepared structs, mutable methods, Result, and slices as you use them. Allow about 55 minutes including reading and ordinary mistakes; this is an author estimate.
Commit Only a Legal Choice
In this step, design and implement the state transition for one accepted choice. Start in the Terminal tab. This independent project already has complete scoring rules. It does not use files from the first lab. Set this shell's search path and enter the project; repeat these commands in a new shell:
export PATH=/home/labex/.cargo/bin:$PATH
cd /home/labex/project/dice-game
Start with a normal working score and predict it before running:
cargo run --quiet -- score pair 2 2 4 4 6
Expect the shown dice and pair: 8. This proves the provided rule layer is usable before you add match state. Read the scaffold:
cat src/state.rs
A struct groups named fields describing one object. Round records five dice, a category and points. Game holds current: Option<Dice> and rounds: Vec<Round>. Some(dice) means a choice is pending; None means there is no current roll. Vec<Round> is a growable collection; Vec::new() creates an empty one and push appends an item.
Read the prepared shell's caller too:
cat src/main.rs
Locate match game.submit(input.trim()). Its Err branch prints a rejection. Its Ok branch prints points, then obtains another roll only if fewer than three rounds are recorded. You do not edit this shell. Your method must leave state unchanged on rejection so that the next prompt shows the same dice and roll count.
Predict the fields after each kind of submission:
| Submission | rounds after the call |
current after the call |
|---|---|---|
| No pending dice, unknown word or used category | Unchanged | Unchanged |
| Valid unused category, including a zero score | One complete new record appended | None |
The shell, not submit, supplies the next roll after success. Do not draw dice in your method. impl Game groups methods on Game. &mut self allows this method to update that game through a mutable reference. Result<u16, &'static str> means Ok(points) or Err(message); &'static str is the fixed error-message text type. Unlike Option, Result can explain a failure.
ok_or(message) turns Some into Ok and None into Err. A following ? continues with the successful value or returns the error immediately. For example, the pending-roll check can read:
let dice = self.current.ok_or("match finished")?;
Apply the same idea to Category::parse(input). Before changing any field, borrow the previous records with a for loop over &self.rounds and reject a matching category. You choose how to organize the checks; the table is the contract. After validation, obtain points from score(dice, category). The successful commit has three actions:
self.rounds.push(Round { dice, category, points });
self.current = None;
Ok(points)
The short field names use local variables with those names. A result of zero is still successful. None of these changes belong on a rejected path.
Keep src/main.rs, Cargo.toml, the public types, function names and types, and module exports unchanged. use crate::{score, Category, Dice} imports from the current library; mod state and pub use in src/lib.rs connect the modules. This step edits only submit in src/state.rs, leaving its structs, new method and unfinished replay intact.
Before editing, make a unique backup of this allowed file. As taught earlier, mktemp -d replaces six X characters with a unique suffix. $() stores its printed path; && runs cp -p only after directory creation succeeds. The quoted path remains one argument. If it fails, read the error and obtain a backup before editing.
rs02_before_submit=$(mktemp -d .edit-before-submit.XXXXXX) && cp -p src/state.rs "$rs02_before_submit/state.rs"
Open the file with nano; -c displays the cursor position:
nano -c src/state.rs
Press Ctrl+W, search for pub fn submit, and press Enter. Inspect the opening and closing braces of this method. Rename the unused _input parameter binding to input without changing its type. Replace only the unfinished body with your implementation of the checks and successful commit above. Preserve the other methods and data definitions. Use arrows and Backspace/Delete to edit. Save with Ctrl+O, Enter, then exit with Ctrl+X. Nano's ^ notation means Ctrl. If you have an unsaved accidental edit, Ctrl+X, then N at the save question discards it; it does not reverse an earlier saved edit.
Read the saved method before running:
cat src/state.rs
Use the known fixture to observe your state decisions. --fixture selects disclosed rolls instead of the random source:
cargo run --quiet -- play --fixture
Wait for each prompt, then enter one word and press Enter:
First enter pairs. It is unknown: expect the same [2, 2, 4, 4, 6] and rolls 1 after rejection.
Next enter pair. It scores 8 and advances to [1, 2, 3, 4, 6], rolls 2.
Enter pair again. The used category is rejected, leaving those dice and rolls 2 unchanged.
Then enter straight. This valid choice scores zero and advances to the third roll.
Finally enter sum. It scores 20.
The expected final error is Error: replay not implemented, with status 1. Your submissions worked, but replay is still the original unfinished function, so the shell refuses to save an unchecked card. This is the planned intermediate state, not a successful saved game. If you choose another legal category accidentally, finish the unused choices or use Ctrl+C and intentionally start a new fixture game. Within one game, rejection must not restart or reroll anything.
For a compiler error, read the reported file/line, reopen nano, search for the affected method and repair it. For a saved error you cannot repair, restore your own pre-submit file, then implement and test submit again:
Only when recovering a saved mistake: in the original Terminal, the checkpoint variable still holds its path. In a new Terminal, first repeat the earlier PATH and project-directory commands. Enumerate this stage's backups:
ls -d .edit-before-submit.*
ls -d lists directory names rather than their contents; * matches the suffixes of this phase's unique directories. These are real generated names, so do not type a made-up suffix. If no backup matches, confirm the project directory and whether you made this checkpoint; do not copy from an unrelated phase. To select one of the printed paths, run:
read rs02_before_submit
The shell waits for one line. Paste the exact directory name you selected and press Enter. read stores that line in the named variable for this session. Confirm the backup content before restoring:
cat "$rs02_before_submit/state.rs"
It should contain the starter before submit. If the path or stage is wrong, repeat read with the right printed path and inspect it again. Only then use the restore command. This check applies to the original Terminal too if you are unsure which checkpoint its variable names.
cp -p "$rs02_before_submit/state.rs" src/state.rs
That command intentionally replaces only the editable state file. The backup variable is local to this Terminal; use the conditional recovery instructions in another shell. Do not reset setup or replace the prepared shell to fix a state error.
Recompute and Save a Complete Card
In this step, implement an independent check of each recorded round before accepting a total. Continue in the same Terminal, in /home/labex/project/dice-game; repeat the previous export PATH and cd if needed. Keep your completed submit method.
First hand-check the proposed fixture card: pair on [2, 2, 4, 4, 6] gives 8, straight on [1, 2, 3, 4, 6] gives 0, and sum on [6, 5, 4, 3, 2] gives 20. The recomputed total is 28. If someone wrote 10 instead of 8 in the first row, adding its stored points would conceal the wrong record. A card cannot prove itself with its own stored total or points.
replay receives &[Round], a slice: a borrowed view of records, without ownership transfer or modification. Design the checks before writing. Require exactly three records. For each record, reject faces outside 1 through 6 before calling score, since a scoring implementation may use a face as an array index. Track used categories in a small Vec; contains tests whether the current category has already appeared. Reject duplicates. Recompute points from the actual dice and category and compare them with the stored points. Add only the recomputed result to your total, and return the total only after every record is valid.
You can use an early return Err("...") for a failed condition. In for round in rounds, each round is borrowed; round.dice is a small Copy array you can examine without altering the record. The condition !(1..=6).contains(&face) means that the face is outside the inclusive valid range. Choose meaningful rejection messages; your implementation may organize these decisions differently while preserving the contract.
Save a new undo point containing your already-working submit. It must not be confused with the earlier starter backup:
rs02_before_replay=$(mktemp -d .edit-before-replay.XXXXXX) && cp -p src/state.rs "$rs02_before_replay/state.rs"
Open the same file and search with Ctrl+W for pub fn replay:
nano -c src/state.rs
Rename the unused _rounds parameter binding to rounds, keeping its type, and implement only this function's body. Leave submit, the structs and module connections as they were. Use your ordered checks and accumulator; do not merely return 28, trust stored points or silently skip an invalid record. Save with Ctrl+O, Enter, then exit with Ctrl+X. Read the saved code:
cat src/state.rs
Run the fixture again:
cargo run --quiet -- play --fixture
Enter pair, then straight, then sum, waiting for each prompt. The final output includes:
1 | [2, 2, 4, 4, 6] | pair | 8
2 | [1, 2, 3, 4, 6] | straight | 0
3 | [6, 5, 4, 3, 2] | sum | 20
Replayed total: 28
Saved match.tsv
The prepared shell writes match.tsv only after a complete card passes replay. Each row has five space-separated faces, a tab, category, another tab, and points. The prepared shell handles this format; you do not implement a file parser. A completed new game replaces this course-owned card, while an incomplete game leaves the previous card alone.
Read and replay your actual saved file:
cat match.tsv
cargo run --quiet -- replay match.tsv
Expect the same records and total 28, without Saved match.tsv. Replay reads the exact path without changing it or falling back to a sample. To check that you did not encode the earlier category sequence in submit, play the fixture once more:
cargo run --quiet -- play --fixture
Use straight, sum, then pair. Predict 0, 16 and 0: all three choices are legal, and the recomputed total is 16. This completed game now replaces the earlier card. Replay that current file:
cargo run --quiet -- replay match.tsv
Its records must reflect this second sequence, not the earlier 28-point game. A different total is expected because the rule chosen for each roll changed. The next step uses genuinely random dice with the same decisions.
For a compiler error, reopen the named file and repair the indicated replay line. If necessary restore your own pre-replay checkpoint, which retains submit, then implement replay again:
Only when recovering a saved mistake: in the original Terminal, the checkpoint variable still holds its path. In a new Terminal, first repeat the earlier PATH and project-directory commands. Enumerate this stage's backups:
ls -d .edit-before-replay.*
ls -d lists directory names rather than their contents; * matches the suffixes of this phase's unique directories. These are real generated names, so do not type a made-up suffix. If no backup matches, confirm the project directory and whether you made this checkpoint; do not copy from an unrelated phase. To select one of the printed paths, run:
read rs02_before_replay
The shell waits for one line. Paste the exact directory name you selected and press Enter. read stores that line in the named variable for this session. Confirm the backup content before restoring:
cat "$rs02_before_replay/state.rs"
It should contain the working submit before replay. If the path or stage is wrong, repeat read with the right printed path and inspect it again. Only then use the restore command. This check applies to the original Terminal too if you are unsure which checkpoint its variable names.
cp -p "$rs02_before_replay/state.rs" src/state.rs
Source restoration does not change an existing card. A later completed game replaces it only after replay succeeds; failed games and reading a card do not. The complete reference below is for comparing a specific function after your attempt, not for replacing the module as the normal learning operation.
Complete state reference for comparison and recovery
Keep your fields and earlier successful work. Compare only the function you are repairing, edit it in nano, then run the fixture and replay the actual card again.
use crate::{score, Category, Dice};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Round {
pub dice: Dice,
pub category: Category,
pub points: u16,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Game {
pub current: Option<Dice>,
pub rounds: Vec<Round>,
}
impl Game {
pub fn new(first: Dice) -> Self {
Self { current: Some(first), rounds: Vec::new() }
}
pub fn submit(&mut self, input: &str) -> Result<u16, &'static str> {
let dice = self.current.ok_or("match finished")?;
let category = Category::parse(input).ok_or("unknown category")?;
for round in &self.rounds {
if round.category == category {
return Err("category already used");
}
}
let points = score(dice, category);
self.rounds.push(Round { dice, category, points });
self.current = None;
Ok(points)
}
}
pub fn replay(rounds: &[Round]) -> Result<u16, &'static str> {
if rounds.len() != 3 {
return Err("card needs three rounds");
}
let mut used = Vec::new();
let mut total = 0;
for round in rounds {
for face in round.dice {
if !(1..=6).contains(&face) {
return Err("card has an invalid face");
}
}
if used.contains(&round.category) {
return Err("card repeats a category");
}
let expected = score(round.dice, round.category);
if expected != round.points {
return Err("card has incorrect points");
}
used.push(round.category);
total += expected;
}
Ok(total)
}
Play and Reuse Your Own Match
In this step, use the finished rules with genuinely new dice and recover from a normal file-path error. Continue in the same Terminal, in /home/labex/project/dice-game.
Without --fixture, the prepared shell reads random bytes from /dev/urandom. It produces five faces from 1 through 6 for each roll. This is a local practice game. You do not need a target total or winning strategy; scores must follow the rules. Each of sum, pair, and straight must be used once in any order. Rejected words and repeated categories preserve the current roll.
Start a new match:
cargo run --quiet -- play
Read the first dice, choose an unused category, and press Enter. Repeat for the second and third rolls. You may deliberately type pairs once to see a rejection, then correct it to an available category. Watch the unchanged dice and rolls count during the correction. After three accepted choices, inspect the three records and recomputed total. Your random values may differ from the fixture, and the completed game replaces match.tsv.
Read the new card back:
cargo run --quiet -- replay match.tsv
The records must match your most recent game, including legitimate zeros. To practice an ordinary path mistake, the following command intentionally asks for a nonexistent file. Expect an Error: missing-card.tsv: ... message and status 1; it must not produce a replacement card:
cargo run --quiet -- replay missing-card.tsv
Recover with the real filename:
cargo run --quiet -- replay match.tsv
Your card remains readable and unchanged by either replay. Keep src/main.rs, Cargo.toml, and the public API in place. The check exercises your original implementation in an isolated copy and reads your real match.tsv; it does not create a card for you.
Summary
You modeled a game and its records, returned explicit success or rejection, and changed state only after validation. Invalid choices kept the roll, legal zeros advanced the match, and replay recomputed a complete card. You played new dice and reused the real saved file without relying on an earlier VM.



