# Bots, replays and a frame

Source: https://zafnok.github.io/shuyi-engine/learn/tutorial/09-test/

The game plays and looks just as it did in the last step. So why is this a step of its own? Because of how
you'll keep it working. As a game grows, a change here breaks something over there, and no one can replay
every level by hand after every change. So you teach the game to test itself.

Everything in this step is possible because of step 3. The rules are deterministic, so **a game can be tested
by playing it**, and the same play always gives the same result.

| File | What changes |
| ---- | ------------ |
| `crates/game/src/tools.rs` | New: the game's own tools |
| `crates/game/src/stages.rs` | `tools()` returns them |
| `crates/game/src/lib.rs` | `pub mod tools;`, only for the tools build |
| `crates/game/src/rules.rs` | Counts its turns, for bots |
| `crates/game/files/lines/1.ron` to `5.ron` | New: a winning line for each level |
| `crates/game/tests/tools.rs` | New: tests through the tools |
| `crates/game/tests/frames/level1.txt` | New: a frame kept as a file |

## The game's own tools

`cargo shuyi` has run your game since step 1, using `BasicTools`, the engine's ready-made tools. Those can't know
about your rules: they have no idea what a move is. So the game now writes its own, `Tools`, which adds what's
special to this game, and calls `BasicTools` for the rest.

> **Shuyi:** `GameTools` is the trait for that. It's the bridge between your game and the engine's tools,
> whether a person runs them from a terminal or a coding assistant calls them. Your `Tools` is
> [in this step's `tools.rs`](https://github.com/Zafnok/shuyi-engine/blob/main/site/tutorial/09-test/crates/game/src/tools.rs).
> Most of its functions just pass the call on to `self.basic()`. This page shows the parts that aren't that.

In `lib.rs`, the module is only part of a build with the tools in it, so it never ends up in the game you ship:

```rust
#[cfg(feature = "tools")]
pub mod tools;
```

> **Rust:** `#[cfg(feature = "tools")]` means "only include this if the `tools` feature is on". A **feature**
> is an optional part of a crate, listed in its `Cargo.toml`. That's what `--features tools` was doing in the
> `shuyi` alias, in your `.cargo/config.toml`, all along.

And `stages::tools` returns it:

```rust
/// The game's tools: the engine's, and its scene, bot and replay on top.
#[cfg(feature = "tools")]
pub fn tools() -> crate::tools::Tools {
    crate::tools::Tools::new(crate::game())
}
```

## The scene: what a program can read

A person looks at the screen. A program needs the same facts as data:

```rust
/// The board's scene in `h`: its map, and what a program asks of it.
pub fn scene(h: &Harness<Data>) -> Result<Scene, String> {
    let board = h
        .screen::<Board>()
        .ok_or_else(|| format!("no level is open (the top screen is `{}`)", h.top_screen()))?;
    let map = board.scene(Board::view_tiles());
    let mut scene = Scene::new("board", "grid map", map.to_text(&Terrain));
    scene.data = json!({
        "level": board.level,
        "moves": board.history.len(),
        "won": board.puzzle.outcome().is_some(),
    });
    Ok(scene)
}
```

Try asking:

```bash
cargo shuyi scene --json --script "Enter Enter Right Right"
```

That plays those keys with no window, and answers with the map as text, plus your `moves` and `won`. `json!` writes
**JSON**, the most common format for programs to exchange data. Here is how a test uses it:

```rust
#[test]
fn the_scene_says_the_moves_and_whether_it_is_won() {
    let tools = game::stages::tools();
    let mut input = RunInput::default();
    input.script = keys("Enter Enter Right");
    let answer = scene(&tools, &input);
    let data = &answer.result.as_ref().unwrap().scene.as_ref().unwrap().data;
    assert_eq!(data["moves"], 1, "{data}");
    assert_eq!(data["won"], false, "{data}");
    input.script = keys("Enter Enter Right Right");
    let answer = scene(&tools, &input);
    let data = &answer.result.as_ref().unwrap().scene.as_ref().unwrap().data;
    assert_eq!(data["moves"], 2, "{data}");
    assert_eq!(data["won"], true, "{data}");
}
```

## A bot

> **Game idea:** a **bot** is a player made of code. It looks at the state, and picks the next command. Bots test a
> game from outside it, playing whole games in the blink of an eye. In a game with opponents, they can also be the
> opponents.

A bot needs to know who moves, for games with several players. The puzzle has one, so its side is `()`, a value with
nothing in it. It also needs to know the turn, so the puzzle now counts them. In `rules.rs`, add `turn: u32` to the
state, start it at `1`, and add 1 in `apply`. (`u32` is a whole number that can't go below zero.) Then:

```rust
/// Who moves, for the bots: the puzzle has one player, so its side is `()`.
impl Sides for Puzzle {
    type Side = ();

    fn side_to_move(&self) {}

    fn turn(&self) -> u32 {
        self.turn
    }
}
```

This bot isn't clever. It plays a line kept in a file: one in `files/lines/` for each level, such as `files/lines/2.ron`:

```ron
// A line that wins level 2, one step a move.
[Right, Right, Down, Left, Left, Down, Right, Right]

```

Take the five from
[this step's copy](https://github.com/Zafnok/shuyi-engine/tree/main/site/tutorial/09-test/crates/game/files/lines). Those
are the winning moves for each level, written by hand. Here's the bot that plays one:

```rust
/// A bot that plays a line kept in the game's files: each level's is in
/// `files/lines/`.
#[derive(Debug, Clone)]
pub struct LineBot {
    /// The steps left, last first.
    steps: Vec<Dir>,
}

impl LineBot {
    /// The bot that plays level `level`'s line, counted from 0.
    pub fn new(bundle: &Bundle, level: usize) -> Result<Self, String> {
        // check-text: not player text, a file name
        let name = format!("lines/{}.ron", level + 1);
        let text = bundle.file(&name).ok_or(format!("no {name}"))?;
        let mut steps: Vec<Dir> = ron::from_str(text).map_err(|e| format!("{name}: {e}"))?;
        steps.reverse();
        Ok(Self { steps })
    }
}

impl Bot<Puzzle> for LineBot {
    fn choose(&mut self, puzzle: &Puzzle) -> Command {
        // A line played out with no win steps wherever it can: the play's
        // turn limit ends it.
        let legal = || puzzle.legal_commands().first().copied();
        self.steps
            .pop()
            .map(Command::Step)
            .or_else(legal)
            .unwrap_or(Command::Step(Dir::Up))
    }
}
```

> **Rust:** `Vec<Dir>` read straight from the file by `ron::from_str`, thanks to serde. `steps.reverse()` puts the last step
> first, because `.pop()` takes from the end of a list, and that's quick. `.or_else(legal)` says "or, if that gave `None`, try
> this". So when the line is used up, the bot makes any legal step, and the turn limit will end the play.

Why does this matter? Here's the test:

```rust
#[test]
fn every_levels_line_wins_it() {
    let bundle = game::game().bundle();
    for (n, file) in level::read_all(&bundle).unwrap().iter().enumerate() {
        let mut puzzle = Puzzle::new(file).unwrap();
        let mut bot = LineBot::new(&bundle, n).unwrap();
        let report = play(&mut puzzle, &mut [((), &mut bot)], 500).unwrap();
        assert!(report.outcome.is_some(), "level {}", n + 1);
    }
}

#[test]
fn the_playtest_tool_wins_level_one() {
    let mut input = PlaytestInput::default();
    input.tries = Some(3);
    let answer = playtest(&game::stages::tools(), &input);
    assert!(answer.ok, "{answer:?}");
    let line = &answer.result.as_ref().unwrap()[0].entrants[0];
    assert_eq!(line.outcomes.get("Won"), Some(&3), "{line:?}");
}
```

It plays each level's line, and checks it wins. **So every level is proven winnable, every time you run your tests.** Edit a
level so it can't be won, and this test fails, and says which one. For a puzzle game, that's the most valuable test there is.

To run the bot by hand:

```bash
cargo shuyi playtest --tries 20
```

The tools know how to do that because of `playtest_plan`, in `tools.rs`: one bot, `line`, with no checks to judge it,
each try played through the engine's `play` and kept as a record you can replay. Look it up in this step's copy.

## A replay

A **record** is a level and its commands, and nothing more. Since the rules are deterministic, that's the whole play:

```rust
/// A record of a level: which one and the commands sent to its rules, and
/// what made it. The game's record format for `replay`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LevelRecord {
    /// The level, counted from 0.
    pub level: usize,
    /// The commands, in order.
    pub commands: Vec<Command>,
    /// What made it ([`crate::made`]); a record without one still reads.
    #[serde(default)]
    pub made: Option<Made>,
}
```

> **Shuyi:** the `replay` tool plays a record **twice** and compares a hash of the state at every step. A **hash** is a short
> number worked out from all of a value, so if any of it changes, the number will too. So anything that isn't deterministic
> shows up at once, at the very step where it went wrong. If someone reports a bug, a record of what they did will reproduce
> it exactly.

```rust
#[test]
fn a_record_replays_with_a_hash_at_every_step() {
    let dir = std::env::temp_dir().join(format!("box-pusher-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let record = LevelRecord {
        level: 0,
        commands: vec![Command::Step(Dir::Right), Command::Step(Dir::Right)],
        made: Some(game::made()),
    };
    std::fs::write(dir.join("one.ron"), ron::to_string(&record).unwrap()).unwrap();
    let answer = replay(&game::stages::tools(), &dir, &ReplayInput::new("one.ron"));
    std::fs::remove_dir_all(&dir).unwrap();
    assert!(answer.ok, "{answer:?}");
}
```

`made` says which build made a record, since a record from another build may not play alike, and the tool refuses to compare those.

## A frame

Most tests read the state. So how do you test a look? Here's the first level:

![The first level with no moves made, the frame the test keeps as text.](https://zafnok.github.io/shuyi-engine/tutorial-tries/test-frame/before.png)

And here's the same frame as text. `h.snapshot()` gives you this, its characters first, and then a map of each cell's colors:

```text
 First push                            Moves: 0 
                                                
                                                
                                                
                                                
                                                
                                                
                                                
                                                
                ▓▓▓▓▓▓▓▓▓▓▓▓▓▓                  
                ▓▓          ▓▓                  
                ▓▓  @ []  <>▓▓                  
                ▓▓          ▓▓                  
                ▓▓▓▓▓▓▓▓▓▓▓▓▓▓                  
                                                
                                                
                                                
                                                
                                                
                                                
                                                
                                                
                                                
 arrows walk · u undo · Backspace levels        
--- colours ---
abbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaacccccccca
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaddddddddddddddaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaddeeeeeeeeeeddaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaddeefeggeebbddaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaddeeeeeeeeeeddaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaddddddddddddddaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhaaaaaaaa
--- legend ---
a = fg:panel bg:panel
b = fg:highlight bg:panel
c = fg:text bg:panel
d = fg:border bg:wall_bg
e = fg:floor_fg bg:panel
f = fg:bright bg:panel
g = fg:box bg:panel
h = fg:text_dim bg:panel

```

Notice that the colors are named by their role (`fg:box`), not by a number. So this test doesn't fail if you make the box blue, as
that's a change of look on purpose, but it does if the box stops being drawn with its `box` color. The test compares this with the
file you keep:

```rust
/// Compares the first level's frame with the file kept beside this test;
/// `UPDATE_FRAMES=1` writes it, to read before keeping it.
#[test]
fn the_first_level_looks_as_it_did() {
    let mut h = game::game().harness();
    h.keys("Enter Enter");
    let frame = h.snapshot();
    let file = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/frames/level1.txt");
    if std::env::var_os("UPDATE_FRAMES").is_some() {
        std::fs::write(&file, &frame).unwrap();
    }
    let kept = std::fs::read_to_string(&file).unwrap().replace('\r', "");
    assert_eq!(frame, kept);
}
```

To make the file the first time, or after you've changed a look on purpose, set `UPDATE_FRAMES`, which is what the test checks for:

```bash
UPDATE_FRAMES=1 cargo test --workspace the_first_level_looks_as_it_did
```

On Windows, in PowerShell, the same thing takes two lines:

```bash
$env:UPDATE_FRAMES = "1"
cargo test --workspace the_first_level_looks_as_it_did
```

(In PowerShell, close that terminal afterwards, or that variable stays set.) **Read the new file before you keep it.** It'll
be what the test holds the game to from now on.

Keep these tests few. A frame test fails on every change of look, while a test of the state only fails if something's really wrong.
So test what happened through the state, as you've been doing, and keep a frame or two for the screens you care about most.

## See them catch something

Change `'@'` to `'P'` in `Pieces`, as you tried back in step 2, and run `cargo test --workspace`. `the_first_level_looks_as_it_did`
fails, and shows you the difference. Put it back. Now change one line of `files/lines/1.ron`, from `[Right, Right]` to `[Right]`, and
run the tests: `every_levels_line_wins_it` fails, naming level 1. Put that back too.

That is the feeling you want. You can change anything, because if it breaks, you'll know within a few seconds.

## What you learned

- **Rust:** features and `#[cfg]`.
- **Game ideas:** bots, records and replays, hashes, and when a test should read the picture.
- **Shuyi:** `GameTools`, scenes for programs, `Bot` and `play`, `replay`, and `h.snapshot()`.

[Chapter 8](https://zafnok.github.io/shuyi-engine/learn/book/testing/index.md) has more on testing, and [chapter 10](https://zafnok.github.io/shuyi-engine/learn/book/tools-for-agents/index.md) on the tools. Next: hand it out.
