# We Accidentally Built a Game Engine.

Our game works, but its rules and its screen are tangled together in one file of more than four hundred lines. Today we pull them apart, so the same game can run in a terminal now and in a browser later.

Nothing about the game changes. We will not touch a rule.

Nobody planned that file. Each article needed one more thing, and each thing went in the next free spot. But look at what is in there: everything above `main()` knows nothing about a terminal or a screen. Only `main()` does.

So we put a wall between them. When we are done, `main.rs` is fifty-six lines: it creates a game, clicks, and prints. The game itself lives in a library that has never heard of a terminal.

A library like that, made of rules and state that do not know how they are being displayed, is the heart of what people call a game engine.

A small one.

Ours will have a front door and nothing else.

The program still prints the same thing, a few stages cleared and then an hour of waiting:

```text
One hour away.
Stage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410
```

Same output.

Same twenty-one tests, plus one new one.

Different house.

---

## Our starting point

This series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.

If you are only reading along, skip past this code to "The groups are already visible."

If you are building along, create a project with `cargo new rustbound-idle` if you do not already have one. The name matters later.

At this point, `src/main.rs` holds three structs, an enum, three `impl` blocks, twelve small functions, and twenty-one tests:

```rust
struct Player {
    gold: u32,
    xp: u32,
    level: u32,
    click_upgrade_level: u32,
}

struct Enemy {
    name: String,
    health: u32,
    gold_reward: u32,
    xp_reward: u32,
}

struct Game {
    stage: u32,
    player: Player,
    enemy: Enemy,
}

#[derive(Debug, PartialEq)]
enum PurchaseOutcome {
    Purchased,
    NotEnoughGold,
}

impl Player {
    fn new() -> Player {
        Player {
            gold: 0,
            xp: 0,
            level: 1,
            click_upgrade_level: 0,
        }
    }

    fn add_gold(&mut self, amount: u32) {
        self.gold = self.gold.saturating_add(amount);
    }

    fn add_xp(&mut self, amount: u32) {
        self.xp = self.xp.saturating_add(amount);

        while self.xp >= xp_to_next_level(self.level) {
            self.xp -= xp_to_next_level(self.level);
            self.level = self.level.saturating_add(1);
        }
    }

    fn base_damage(&self) -> u32 {
        base_damage_for_level(self.level)
    }

    fn bonus_damage(&self) -> u32 {
        click_upgrade_bonus(self.click_upgrade_level)
    }

    fn buy_click_upgrade(&mut self) -> PurchaseOutcome {
        let cost = click_upgrade_cost(self.click_upgrade_level);

        if self.gold < cost {
            return PurchaseOutcome::NotEnoughGold;
        }

        self.gold -= cost;
        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);

        PurchaseOutcome::Purchased
    }
}

impl Enemy {
    fn for_stage(stage: u32) -> Enemy {
        Enemy {
            name: String::from("Training Slime"),
            health: enemy_health_for_stage(stage),
            gold_reward: gold_reward_for_stage(stage),
            xp_reward: xp_reward_for_stage(stage),
        }
    }
}

impl Game {
    fn new() -> Game {
        Game {
            stage: 1,
            player: Player::new(),
            enemy: Enemy::for_stage(1),
        }
    }

    fn click(&mut self) {
        attack(&self.player, &mut self.enemy);
        self.resolve_hit();
    }

    fn tick(&mut self, seconds: u32) {
        for _ in 0..seconds {
            let damage = idle_damage_for_level(self.player.level);

            self.enemy.health = health_after_hit(self.enemy.health, damage);
            self.resolve_hit();
        }
    }

    fn resolve_hit(&mut self) {
        if is_defeated(self.enemy.health) {
            self.defeat_enemy();
        }
    }

    fn defeat_enemy(&mut self) {
        self.player.add_gold(self.enemy.gold_reward);
        self.player.add_xp(self.enemy.xp_reward);

        self.stage = self.stage.saturating_add(1);
        self.enemy = Enemy::for_stage(self.stage);
    }

    fn buy_click_upgrade(&mut self) -> PurchaseOutcome {
        self.player.buy_click_upgrade()
    }
}

fn click_damage(base: u32, bonus: u32) -> u32 {
    base.saturating_add(bonus)
}

fn health_after_hit(health: u32, damage: u32) -> u32 {
    health.saturating_sub(damage)
}

fn is_defeated(health: u32) -> bool {
    health == 0
}

fn enemy_health_for_stage(stage: u32) -> u32 {
    stage.saturating_mul(10).saturating_add(10)
}

fn gold_reward_for_stage(stage: u32) -> u32 {
    stage.saturating_mul(2).saturating_add(3)
}

fn xp_reward_for_stage(stage: u32) -> u32 {
    stage.saturating_mul(2).saturating_add(4)
}

fn xp_to_next_level(level: u32) -> u32 {
    level
        .saturating_sub(1)
        .saturating_mul(15)
        .saturating_add(20)
}

fn base_damage_for_level(level: u32) -> u32 {
    level.saturating_add(4)
}

fn idle_damage_for_level(level: u32) -> u32 {
    level
}

fn click_upgrade_bonus(upgrades: u32) -> u32 {
    upgrades.saturating_mul(2)
}

fn click_upgrade_cost(upgrades: u32) -> u32 {
    upgrades.saturating_add(1).saturating_mul(10)
}

fn attack(player: &Player, enemy: &mut Enemy) {
    let damage = click_damage(player.base_damage(), player.bonus_damage());

    enemy.health = health_after_hit(enemy.health, damage);
}

fn main() {
    let mut game = Game::new();

    while game.stage <= 3 {
        let stage = game.stage;
        let level = game.player.level;

        game.click();

        if game.stage > stage {
            println!(
                "Stage {stage} cleared!  Gold: {}  XP: {}/{}",
                game.player.gold,
                game.player.xp,
                xp_to_next_level(game.player.level)
            );

            if game.player.level > level {
                println!(
                    "Level up! Now level {}. Click damage: {}",
                    game.player.level,
                    click_damage(game.player.base_damage(), game.player.bonus_damage())
                );
            }

            println!(
                "  Next up: {} with {} health.",
                game.enemy.name, game.enemy.health
            );
        }

        match game.buy_click_upgrade() {
            PurchaseOutcome::Purchased => println!(
                "Bought click upgrade {}! Click damage: {}  Gold left: {}",
                game.player.click_upgrade_level,
                click_damage(game.player.base_damage(), game.player.bonus_damage()),
                game.player.gold
            ),
            PurchaseOutcome::NotEnoughGold => {}
        }
    }

    let stage = game.stage;
    let level = game.player.level;
    let gold = game.player.gold;

    game.tick(3600);

    println!("One hour away.");
    println!(
        "Stage {} -> {}  Level {} -> {}  Gold {} -> {}",
        stage, game.stage, level, game.player.level, gold, game.player.gold
    );
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn click_damage_adds_base_and_bonus() {
        assert_eq!(click_damage(3, 4), 7);
    }

    #[test]
    fn health_never_drops_below_zero() {
        assert_eq!(health_after_hit(3, 10), 0);
    }

    #[test]
    fn zero_health_is_defeated() {
        assert!(is_defeated(0));
        assert!(!is_defeated(1));
    }

    #[test]
    fn attack_reduces_enemy_health() {
        let player = Player::new();

        let mut enemy = Enemy::for_stage(1);
        enemy.health = 10;

        attack(&player, &mut enemy);

        assert_eq!(enemy.health, 5);
    }

    #[test]
    fn later_stages_have_tougher_enemies() {
        assert_eq!(enemy_health_for_stage(1), 20);
        assert_eq!(enemy_health_for_stage(2), 30);
    }

    #[test]
    fn later_stages_pay_better() {
        assert_eq!(gold_reward_for_stage(1), 5);
        assert_eq!(gold_reward_for_stage(2), 7);
        assert_eq!(xp_reward_for_stage(1), 6);
        assert_eq!(xp_reward_for_stage(2), 8);
    }

    #[test]
    fn levels_need_more_xp_as_they_go() {
        assert_eq!(xp_to_next_level(1), 20);
        assert_eq!(xp_to_next_level(2), 35);
    }

    #[test]
    fn leveling_up_keeps_leftover_xp() {
        let mut player = Player::new();

        player.add_xp(25);

        assert_eq!(player.level, 2);
        assert_eq!(player.xp, 5);
    }

    #[test]
    fn a_big_reward_can_skip_levels() {
        let mut player = Player::new();

        player.add_xp(60);

        assert_eq!(player.level, 3);
        assert_eq!(player.xp, 5);
    }

    #[test]
    fn levels_make_the_player_hit_harder() {
        assert_eq!(base_damage_for_level(1), 5);
        assert_eq!(base_damage_for_level(2), 6);
    }

    #[test]
    fn upgrades_cost_more_each_time() {
        assert_eq!(click_upgrade_cost(0), 10);
        assert_eq!(click_upgrade_cost(1), 20);
    }

    #[test]
    fn buying_an_upgrade_needs_gold() {
        let mut player = Player::new();

        assert_eq!(
            player.buy_click_upgrade(),
            PurchaseOutcome::NotEnoughGold
        );
        assert_eq!(player.click_upgrade_level, 0);
        assert_eq!(player.bonus_damage(), 0);
    }

    #[test]
    fn buying_an_upgrade_spends_gold() {
        let mut player = Player::new();
        player.gold = 12;

        assert_eq!(
            player.buy_click_upgrade(),
            PurchaseOutcome::Purchased
        );
        assert_eq!(player.gold, 2);
        assert_eq!(player.click_upgrade_level, 1);
        assert_eq!(player.bonus_damage(), 2);
    }

    #[test]
    fn a_new_game_starts_at_stage_one() {
        let game = Game::new();

        assert_eq!(game.stage, 1);
        assert_eq!(game.enemy.health, 20);
        assert_eq!(game.player.gold, 0);
    }

    #[test]
    fn clicking_damages_the_enemy() {
        let mut game = Game::new();

        game.click();

        assert_eq!(game.enemy.health, 15);
    }

    #[test]
    fn defeating_an_enemy_pays_the_player() {
        let mut game = Game::new();
        game.enemy.health = 1;

        game.click();

        assert_eq!(game.stage, 2);
        assert_eq!(game.enemy.health, 30);
        assert_eq!(game.player.gold, 5);
        assert_eq!(game.player.xp, 6);
    }

    #[test]
    fn idle_damage_grows_with_level() {
        assert_eq!(idle_damage_for_level(1), 1);
        assert_eq!(idle_damage_for_level(2), 2);
    }

    #[test]
    fn enough_time_defeats_the_enemy() {
        let mut game = Game::new();

        game.tick(20);

        assert_eq!(game.stage, 2);
        assert_eq!(game.enemy.health, 30);
        assert_eq!(game.player.gold, 5);
        assert_eq!(game.player.xp, 6);
    }

    #[test]
    fn one_second_is_one_idle_hit() {
        let mut game = Game::new();

        game.tick(1);

        assert_eq!(game.enemy.health, 19);
    }

    #[test]
    fn zero_seconds_changes_nothing() {
        let mut game = Game::new();

        game.tick(0);

        assert_eq!(game.stage, 1);
        assert_eq!(game.enemy.health, 20);
    }

    #[test]
    fn one_long_tick_matches_many_short_ones() {
        let mut long = Game::new();
        let mut short = Game::new();

        long.tick(45);

        for _ in 0..45 {
            short.tick(1);
        }

        assert_eq!(long.stage, short.stage);
        assert_eq!(long.enemy.health, short.enemy.health);
        assert_eq!(long.player.gold, short.player.gold);
        assert_eq!(long.player.xp, short.player.xp);
        assert_eq!(long.player.level, short.player.level);
    }
}
```

Run it, and the player clears three stages, buys an upgrade, and spends an hour away.

Run `cargo test`, and all twenty-one tests pass.

You need a recent Rust version. This project uses edition 2024, which requires Rust 1.85 or newer.

Hold on to that test count.

Everything we do today is a reorganization, and the tests are how we prove we did not break anything.

---

## The groups are already visible

Nobody has to invent categories for this code.

It has been sorting itself since the first article:

```text
rules    click_damage, health_after_hit, is_defeated,
         enemy_health_for_stage, gold_reward_for_stage,
         xp_reward_for_stage, xp_to_next_level,
         base_damage_for_level, idle_damage_for_level,
         click_upgrade_bonus, click_upgrade_cost

enemy    Enemy
player   Player, PurchaseOutcome
game     Game, attack
main     everything that prints
```

Look at the first group.

Those eleven functions take plain values and return plain values.

They never touch a `Player`, an `Enemy`, or a `Game`.

They are the rules of the game, and every other group leans on them.

Rust has a name for a named group of code: a **module**.

For this project, we are going to give each top-level module its own file, one at a time, and run the tests after every move.

---

## Give the rules their own module

Create a new file next to `main.rs`:

```text
src/
├── main.rs
└── rules.rs
```

Cut the eleven rule functions out of `main.rs` and paste them into `rules.rs`.

Leave `attack()` where it is.

It takes a `Player` and an `Enemy`, so it is not one of our plain-value rules. It belongs with the game.

The tests for those rules should move too.

Nine of them check nothing but rules:

```text
click_damage_adds_base_and_bonus
health_never_drops_below_zero
zero_health_is_defeated
later_stages_have_tougher_enemies
later_stages_pay_better
levels_need_more_xp_as_they_go
levels_make_the_player_hit_harder
idle_damage_grows_with_level
upgrades_cost_more_each_time
```

Cut each one out of the `tests` module in `main.rs`, including the `#[test]` line above it, and give `rules.rs` a `tests` module of its own at the bottom:

```rust
#[cfg(test)]
mod tests {
    use super::*;

    // the nine rule tests go here
}
```

The `use super::*;` line means:

> Bring in everything from the module above me.

Now `main.rs` needs to find its rules again.

Add this to the very top of `main.rs`, above everything else:

```rust
mod rules;

use rules::{
    base_damage_for_level, click_damage,
    click_upgrade_bonus, click_upgrade_cost,
    enemy_health_for_stage, gold_reward_for_stage,
    health_after_hit, idle_damage_for_level,
    is_defeated, xp_reward_for_stage,
    xp_to_next_level,
};
```

`mod rules;` says:

> There is a module called `rules`, and its code lives in `src/rules.rs`.

The `use` line brings its functions into `main.rs` by name, so the rest of the file can keep calling `click_damage()` exactly as before.

Build it:

```bash
cargo build
```

Rust refuses:

```text
error[E0603]: function `base_damage_for_level` is private
 --> src/main.rs:4:5
  |
4 |     base_damage_for_level,
  |     ^^^^^^^^^^^^^^^^^^^^^ private function
  |
note: the function `base_damage_for_level` is defined here
 --> src/rules.rs:32:1
  |
32 | fn base_damage_for_level(level: u32) -> u32 {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

You will get one of these for every function.

That is not a bug in what we did.

It is our first encounter with one of Rust's module boundaries:

> **By default, an item is visible inside its module and the modules beneath it. Sibling modules and outside code cannot use it unless we open that boundary.**

`main.rs` sits outside the `rules` module.

It cannot reach those functions yet.

The simplest way to open that boundary is `pub`, short for *public*.

Put it in front of each of the eleven functions:

```rust
pub fn click_damage(base: u32, bonus: u32) -> u32 {
    base.saturating_add(bonus)
}
```

Run both:

```bash
cargo test
cargo run
```

Twenty-one tests pass, and the output is the same as before.

The count should stay at twenty-one after every move in this article.

We moved eleven functions and nine tests into another file, and nothing changed.

That is what a good move looks like.

---

## The enemy

`Enemy` is next because it is the smallest.

Create `src/enemy.rs`, and move the `Enemy` struct and its `impl` block into it.

The new file needs to find the rules it uses, so it starts like this:

```rust
use crate::rules::{
    enemy_health_for_stage,
    gold_reward_for_stage,
    xp_reward_for_stage,
};
```

`crate` means the top of the crate this file belongs to.

A path beginning with `crate::` starts there no matter which module we are currently inside.

For now, all of these modules still belong to the executable crate.

In a moment we are going to move them into a library crate, and that distinction will matter.

Back in `main.rs`, add:

```rust
mod enemy;
```

under `mod rules;`, and:

```rust
use enemy::Enemy;
```

under the rules import.

Build again.

Rust has an opinion:

```text
error[E0603]: struct `Enemy` is private
 --> src/main.rs:4:12
  |
4 | use enemy::Enemy;
  |            ^^^^^ private struct
```

Same boundary, same first fix:

```rust
pub struct Enemy
```

Build again, and Rust has a few more opinions.

Making the struct public does not automatically make all of its fields public.

Each field has its own visibility.

So does `Enemy::for_stage()`.

For now, here is the finished file:

```rust
use crate::rules::{
    enemy_health_for_stage,
    gold_reward_for_stage,
    xp_reward_for_stage,
};

pub struct Enemy {
    pub name: String,
    pub health: u32,
    pub gold_reward: u32,
    pub xp_reward: u32,
}

impl Enemy {
    pub fn for_stage(stage: u32) -> Enemy {
        Enemy {
            name: String::from("Training Slime"),
            health: enemy_health_for_stage(stage),
            gold_reward: gold_reward_for_stage(stage),
            xp_reward: xp_reward_for_stage(stage),
        }
    }
}
```

Everything is `pub` for now.

That is too generous.

We are going to fix it on purpose once the boundaries are in place.

Run:

```bash
cargo test
```

Twenty-one pass.

You may also see warnings that some imports in `main.rs` are unused.

That is true.

`Enemy` was the only caller of `enemy_health_for_stage()`, `gold_reward_for_stage()`, and `xp_reward_for_stage()`, and it just moved.

Delete those names from the `use rules::{...}` list in `main.rs`.

---

## The player

Same move, bigger file.

Create `src/player.rs`.

Move in:

- `Player`
- `PurchaseOutcome`
- the `impl Player` block

`PurchaseOutcome` goes with `Player` because it is the player's vocabulary.

It is how a purchase answers.

At the top of the file, import the rules `Player` uses:

```rust
use crate::rules::{
    base_damage_for_level,
    click_upgrade_bonus,
    click_upgrade_cost,
    xp_to_next_level,
};
```

Then add `pub` to the struct, its four fields, the enum, and every function in `impl Player`.

An enum's variants follow the enum's visibility.

Make `PurchaseOutcome` public, and its `Purchased` and `NotEnoughGold` variants are available with it.

Four tests are about the player and nothing else:

```text
leveling_up_keeps_leftover_xp
a_big_reward_can_skip_levels
buying_an_upgrade_needs_gold
buying_an_upgrade_spends_gold
```

Move those into a `tests` module at the bottom of `player.rs`, the same way we did with the rules.

In `main.rs`, add:

```rust
mod player;
```

and:

```rust
use player::{Player, PurchaseOutcome};
```

`Game` and `attack()` still live in `main.rs` for now, so `main.rs` still needs `Player`.

That import drops out again in the next section.

Run:

```bash
cargo test
```

Twenty-one pass.

Clean up the new unused-import warnings the same way: remove `base_damage_for_level`, `click_upgrade_bonus`, and `click_upgrade_cost` from the `use rules::{...}` list in `main.rs`.

---

## The game

Last one.

Create `src/game.rs` and move in:

- `Game`
- its `impl` block
- `attack()`

It needs three things from the other modules:

```rust
use crate::enemy::Enemy;
use crate::player::{Player, PurchaseOutcome};
use crate::rules::{
    click_damage,
    health_after_hit,
    idle_damage_for_level,
    is_defeated,
};
```

Add `pub` to the struct and its three fields.

Then add it to the four functions the outside world is going to call:

```rust
pub fn new() -> Game
pub fn click(&mut self)
pub fn tick(&mut self, seconds: u32)
pub fn buy_click_upgrade(&mut self) -> PurchaseOutcome
```

Now look at what is **not** on that list.

`resolve_hit()` stays private.

`defeat_enemy()` stays private.

So does `attack()`.

Nothing outside `game.rs` has a reason to call them.

When a slime dies, the game decides what that means.

A caller should not get to say:

> Pretend that enemy just died.

We will close the field loophole shortly.

That is what privacy is for.

It is not about hiding code for the sake of hiding it.

It is about deciding what the rest of the program is allowed to depend on.

Eight tests are about the game, including the one for `attack()`:

```text
attack_reduces_enemy_health
a_new_game_starts_at_stage_one
clicking_damages_the_enemy
defeating_an_enemy_pays_the_player
one_second_is_one_idle_hit
zero_seconds_changes_nothing
enough_time_defeats_the_enemy
one_long_tick_matches_many_short_ones
```

Move them to the bottom of `game.rs`.

In `main.rs`, add:

```rust
mod game;
```

and:

```rust
use game::Game;
```

At this point, the `tests` module in `main.rs` is empty.

Delete it.

You can also remove `Player` from:

```rust
use player::{Player, PurchaseOutcome};
```

because only `Game` needs it now.

Clear out any remaining unused imports.

The top of `main.rs` should look like this:

```rust
mod enemy;
mod game;
mod player;
mod rules;

use game::Game;
use player::PurchaseOutcome;
use rules::{click_damage, xp_to_next_level};
```

Under that, only `main()` is left.

Run both:

```bash
cargo test
cargo run
```

Twenty-one tests.

Same output.

Four files hold the game.

One holds the printing.

But we have not built the wall yet.

`main.rs` still declares all four modules.

They are part of the executable crate.

Nothing outside that executable could use them as a library.

---

## Build the wall

A Cargo package can contain both a library target and a binary target:

```text
src/lib.rs    library
src/main.rs   executable
```

Rust compiles them as separate crates.

The binary can depend on the library.

That gives us the wall we have been looking for.

A **crate** is one compiled unit of Rust code.

Until now, our project has had one crate: the executable.

Now our package will contain two:

```text
library crate
    ↑
binary crate
```

They live in the same project, but Rust treats them as separate pieces of code.

Create `src/lib.rs`, and move the four `mod` lines out of `main.rs` and into it:

```rust
mod enemy;
mod game;
mod player;
mod rules;
```

The project is named:

```text
rustbound-idle
```

Rust turns the hyphen into an underscore when we refer to the library in code:

```text
rustbound_idle
```

At the top of `main.rs`, replace all the old `mod` and `use` lines with:

```rust
use rustbound_idle::{Game, PurchaseOutcome};
```

Build it:

```bash
cargo build
```

Rust complains.

The first problem looks like this:

```text
error[E0432]: unresolved imports
  `rustbound_idle::Game`,
  `rustbound_idle::PurchaseOutcome`
 --> src/main.rs:1:22
  |
  |   no `Game` in the root
  |   no `PurchaseOutcome` in the root
```

The library has a `game` module.

That module has a `Game`.

But `mod game;` is private.

From outside the library, there is no public path to `Game`.

As far as the binary can tell, the library's front door is empty.

We choose what appears there.

That is what `pub use` is for:

```rust
mod enemy;
mod game;
mod player;
mod rules;

pub use enemy::Enemy;
pub use game::Game;
pub use player::{Player, PurchaseOutcome};
```

`pub use` means:

> Take this item from inside the library and make it available through this public path.

Now code using the library can write:

```rust
rustbound_idle::Game
```

Where `Game` actually lives inside the library is no longer the caller's problem.

Build again.

Now the remaining errors are about rules:

```text
error[E0425]: cannot find function `xp_to_next_level` in this scope
error[E0425]: cannot find function `click_damage` in this scope
error[E0425]: cannot find function `click_damage` in this scope
```

Those calls used to work because `main.rs` imported the rules directly.

Now `rules` is private inside the library.

There are two ways we could fix that.

We could put `rules` on the front door too:

```rust
pub mod rules;
```

Then `main.rs` could import those formulas and keep doing this:

```rust
click_damage(
    game.player.base_damage(),
    game.player.bonus_damage()
)
```

That would compile.

But think about what it means.

Every formula in the game becomes part of the library's public API.

And the screen keeps doing game arithmetic.

Today the screen is a terminal.

Later it will be a browser.

If both of them know how to calculate damage, we now have multiple callers depending on an internal game rule.

The rules should live in one place.

The screen should ask for answers.

So we take the second path.

Leave `rules` private.

Let `Player` provide the information the outside world needs.

Add two methods inside `impl Player`, after `add_xp()`:

```rust
pub fn xp_needed(&self) -> u32 {
    xp_to_next_level(self.level)
}

pub fn click_damage(&self) -> u32 {
    click_damage(
        self.base_damage(),
        self.bonus_damage()
    )
}
```

The method and the rule share a name.

That is fine.

Inside the method:

```rust
click_damage(...)
```

with no `self.` in front of it means the imported rule.

The method is the player asking that rule a question about itself.

First, add `click_damage` to the `use crate::rules::{...}` list at the top of `player.rs`.

It was not there before because `Player` used to leave that arithmetic to `attack()`.

While we are in there, `add_xp()` has been calling:

```rust
xp_to_next_level(self.level)
```

twice.

Now that the player has a method for that question, replace its loop with:

```rust
while self.xp >= self.xp_needed() {
    self.xp -= self.xp_needed();
    self.level = self.level.saturating_add(1);
}
```

The loop is still the same loop.

`xp_to_next_level` stays imported because `xp_needed()` uses it.

`attack()` in `game.rs` can lean on the other new method too:

```rust
fn attack(player: &Player, enemy: &mut Enemy) {
    enemy.health =
        health_after_hit(
            enemy.health,
            player.click_damage()
        );
}
```

It no longer needs the `click_damage` rule, so remove that name from the `use` statement at the top of `game.rs`.

Last, `main()` gets simpler.

Where it said:

```rust
xp_to_next_level(game.player.level)
```

it now says:

```rust
game.player.xp_needed()
```

And where it said:

```rust
click_damage(
    game.player.base_damage(),
    game.player.bonus_damage()
)
```

it now says:

```rust
game.player.click_damage()
```

That second replacement happens twice in `main()`.

Build, test, and run:

```bash
cargo test
cargo run
```

Same twenty-one tests.

Same output.

Cargo also prints test blocks for `main.rs` and doc-tests that say:

```text
running 0 tests
```

That is normal.

And `main.rs` no longer knows a single game formula.

---

## Make the front door smaller

We said everything was too `pub`.

Time to fix that.

Right now `main()` can see far more than it needs.

Try it.

Add this line just after:

```rust
let mut game = Game::new();
```

```rust
game.player.add_gold(1_000_000);
```

It compiles.

A screen can hand the player a million gold, and the game never gets a say.

Let us close that off.

Rust has a visibility setting between fully private and fully public:

```rust
pub(crate)
```

It means:

> Visible anywhere inside this crate, but not outside it.

Our library can use the item freely.

`main.rs` belongs to a different crate now, so it cannot.

Go through the files and tighten everything `main()` does not need.

The rule for now is:

> `pub` for what outside code needs. `pub(crate)` or private for everything else.

In `rules.rs`, change all eleven:

```rust
pub fn
```

to:

```rust
pub(crate) fn
```

In `enemy.rs`:

- change `for_stage()` to `pub(crate)`
- make `gold_reward` and `xp_reward` `pub(crate)`
- leave `name` and `health` public because `main()` displays them

In `player.rs`, change:

- `new`
- `add_gold`
- `add_xp`
- `buy_click_upgrade`

to `pub(crate)`.

Take `pub` off `base_damage()` and `bonus_damage()` entirely.

Only `Player` needs those now.

What remains fully public is what the current screen needs:

```text
Game
  new
  click
  tick
  buy_click_upgrade
  stage
  player
  enemy

Player
  gold
  xp
  level
  click_upgrade_level
  xp_needed
  click_damage

Enemy
  name
  health

PurchaseOutcome
```

Now try this again:

```rust
game.player.add_gold(1_000_000);
```

It fails.

`add_gold()` is `pub(crate)`, and `main.rs` is outside the library crate.

Delete that line.

Run:

```bash
cargo test
cargo run
```

Nothing changes.

That is the point.

If tightening the boundary had broken `main()`, it would mean `main()` depended on something we did not actually want to promise publicly.

Now try to cheat another way.

In `main()`, add:

```rust
let secret = game.enemy.gold_reward;
```

Rust says no:

```text
error[E0616]: field `gold_reward` of struct `Enemy` is private
```

Delete it.

Now try reaching for an internal damage detail.

Replace:

```rust
game.player.click_damage()
```

with:

```rust
game.player.base_damage()
```

Rust refuses again:

```text
error[E0624]: method `base_damage` is private
```

Put it back.

Those errors are the wall doing its job.

---

## A test from the outside

Our twenty-one tests all live **inside** the library, next to the code they check.

That is useful.

Those tests can work closely with the modules they belong to.

But it leaves another question:

> Does the library work for code that only gets to use the public front door?

Cargo has a place for that.

Create a folder called `tests` next to `src`, and put a file in it:

```text
rustbound-idle/
├── src/
└── tests/
    └── idle_hour.rs
```

Each Rust file directly inside `tests/` is compiled as a separate **integration test crate**.

It uses the library the same way outside code does: through the public API.

This is the first code in the project that sees the game the way a stranger would.

Put this in `tests/idle_hour.rs`:

```rust
use rustbound_idle::Game;

#[test]
fn an_hour_away_moves_the_game_forward() {
    let mut game = Game::new();

    game.tick(3600);

    assert_eq!(game.stage, 128);
    assert_eq!(game.player.level, 47);
}
```

The terminal program clicked through three stages before it waited an hour.

This test waits an hour from a brand-new game, so the numbers are different:

```text
stage 128
level 47
```

Run everything:

```bash
cargo test
```

Cargo prints a block for each piece it tests.

You should see:

- the twenty-one tests inside the library
- the new integration test in `tests/idle_hour.rs`
- blocks for `main.rs` and doc-tests that say `running 0 tests`

That is fine.

Twenty-two tests in all.

---

## One last complaint

There is one more thing, and it has been waiting for us.

Clippy is Rust's linter.

It looks for patterns that compile but could be clearer, more idiomatic, or less error-prone.

If:

```bash
cargo clippy
```

is not available, install the component first:

```bash
rustup component add clippy
```

Then run:

```bash
cargo clippy
```

Clippy has a suggestion:

```text
warning: you should consider adding a `Default` implementation for `Game`
```

It did not complain before because `Game::new()` was not part of a public API.

Now `Game` is public, and so is its zero-argument `new()`.

Clippy notices that callers may reasonably expect the standard Rust `Default` trait too.

`Default` describes a type's standard starting value.

We already have one:

```rust
Game::new()
```

So we can say that explicitly:

```rust
impl Default for Game {
    fn default() -> Game {
        Game::new()
    }
}
```

Put it in `game.rs`, between the `impl Game` block and `attack()`.

Now:

```rust
Game::new()
```

and:

```rust
Game::default()
```

produce the same starting game.

Run Clippy again.

It should be quiet.

---

## The complete program

If you are only reading along, skip to the last two sections.

If you have been following along in your editor, here is everything in one place.

Your project should now look like this:

```text
rustbound-idle/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── main.rs
│   ├── rules.rs
│   ├── enemy.rs
│   ├── player.rs
│   └── game.rs
└── tests/
    └── idle_hour.rs
```

### `src/lib.rs`

```rust
mod enemy;
mod game;
mod player;
mod rules;

pub use enemy::Enemy;
pub use game::Game;
pub use player::{Player, PurchaseOutcome};
```

### `src/main.rs`

```rust
use rustbound_idle::{Game, PurchaseOutcome};

fn main() {
    let mut game = Game::new();

    while game.stage <= 3 {
        let stage = game.stage;
        let level = game.player.level;

        game.click();

        if game.stage > stage {
            println!(
                "Stage {stage} cleared!  Gold: {}  XP: {}/{}",
                game.player.gold,
                game.player.xp,
                game.player.xp_needed()
            );

            if game.player.level > level {
                println!(
                    "Level up! Now level {}. Click damage: {}",
                    game.player.level,
                    game.player.click_damage()
                );
            }

            println!(
                "  Next up: {} with {} health.",
                game.enemy.name,
                game.enemy.health
            );
        }

        match game.buy_click_upgrade() {
            PurchaseOutcome::Purchased => println!(
                "Bought click upgrade {}! Click damage: {}  Gold left: {}",
                game.player.click_upgrade_level,
                game.player.click_damage(),
                game.player.gold
            ),
            PurchaseOutcome::NotEnoughGold => {}
        }
    }

    let stage = game.stage;
    let level = game.player.level;
    let gold = game.player.gold;

    game.tick(3600);

    println!("One hour away.");
    println!(
        "Stage {} -> {}  Level {} -> {}  Gold {} -> {}",
        stage,
        game.stage,
        level,
        game.player.level,
        gold,
        game.player.gold
    );
}
```

### `src/rules.rs`

```rust
pub(crate) fn click_damage(base: u32, bonus: u32) -> u32 {
    base.saturating_add(bonus)
}

pub(crate) fn health_after_hit(health: u32, damage: u32) -> u32 {
    health.saturating_sub(damage)
}

pub(crate) fn is_defeated(health: u32) -> bool {
    health == 0
}

pub(crate) fn enemy_health_for_stage(stage: u32) -> u32 {
    stage.saturating_mul(10).saturating_add(10)
}

pub(crate) fn gold_reward_for_stage(stage: u32) -> u32 {
    stage.saturating_mul(2).saturating_add(3)
}

pub(crate) fn xp_reward_for_stage(stage: u32) -> u32 {
    stage.saturating_mul(2).saturating_add(4)
}

pub(crate) fn xp_to_next_level(level: u32) -> u32 {
    level
        .saturating_sub(1)
        .saturating_mul(15)
        .saturating_add(20)
}

pub(crate) fn base_damage_for_level(level: u32) -> u32 {
    level.saturating_add(4)
}

pub(crate) fn idle_damage_for_level(level: u32) -> u32 {
    level
}

pub(crate) fn click_upgrade_bonus(upgrades: u32) -> u32 {
    upgrades.saturating_mul(2)
}

pub(crate) fn click_upgrade_cost(upgrades: u32) -> u32 {
    upgrades
        .saturating_add(1)
        .saturating_mul(10)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn click_damage_adds_base_and_bonus() {
        assert_eq!(click_damage(3, 4), 7);
    }

    #[test]
    fn health_never_drops_below_zero() {
        assert_eq!(health_after_hit(3, 10), 0);
    }

    #[test]
    fn zero_health_is_defeated() {
        assert!(is_defeated(0));
        assert!(!is_defeated(1));
    }

    #[test]
    fn later_stages_have_tougher_enemies() {
        assert_eq!(enemy_health_for_stage(1), 20);
        assert_eq!(enemy_health_for_stage(2), 30);
    }

    #[test]
    fn later_stages_pay_better() {
        assert_eq!(gold_reward_for_stage(1), 5);
        assert_eq!(gold_reward_for_stage(2), 7);
        assert_eq!(xp_reward_for_stage(1), 6);
        assert_eq!(xp_reward_for_stage(2), 8);
    }

    #[test]
    fn levels_need_more_xp_as_they_go() {
        assert_eq!(xp_to_next_level(1), 20);
        assert_eq!(xp_to_next_level(2), 35);
    }

    #[test]
    fn levels_make_the_player_hit_harder() {
        assert_eq!(base_damage_for_level(1), 5);
        assert_eq!(base_damage_for_level(2), 6);
    }

    #[test]
    fn idle_damage_grows_with_level() {
        assert_eq!(idle_damage_for_level(1), 1);
        assert_eq!(idle_damage_for_level(2), 2);
    }

    #[test]
    fn upgrades_cost_more_each_time() {
        assert_eq!(click_upgrade_cost(0), 10);
        assert_eq!(click_upgrade_cost(1), 20);
    }
}
```

### `src/enemy.rs`

```rust
use crate::rules::{
    enemy_health_for_stage,
    gold_reward_for_stage,
    xp_reward_for_stage,
};

pub struct Enemy {
    pub name: String,
    pub health: u32,
    pub(crate) gold_reward: u32,
    pub(crate) xp_reward: u32,
}

impl Enemy {
    pub(crate) fn for_stage(stage: u32) -> Enemy {
        Enemy {
            name: String::from("Training Slime"),
            health: enemy_health_for_stage(stage),
            gold_reward: gold_reward_for_stage(stage),
            xp_reward: xp_reward_for_stage(stage),
        }
    }
}
```

### `src/player.rs`

```rust
use crate::rules::{
    base_damage_for_level,
    click_damage,
    click_upgrade_bonus,
    click_upgrade_cost,
    xp_to_next_level,
};

pub struct Player {
    pub gold: u32,
    pub xp: u32,
    pub level: u32,
    pub click_upgrade_level: u32,
}

#[derive(Debug, PartialEq)]
pub enum PurchaseOutcome {
    Purchased,
    NotEnoughGold,
}

impl Player {
    pub(crate) fn new() -> Player {
        Player {
            gold: 0,
            xp: 0,
            level: 1,
            click_upgrade_level: 0,
        }
    }

    pub(crate) fn add_gold(&mut self, amount: u32) {
        self.gold = self.gold.saturating_add(amount);
    }

    pub(crate) fn add_xp(&mut self, amount: u32) {
        self.xp = self.xp.saturating_add(amount);

        while self.xp >= self.xp_needed() {
            self.xp -= self.xp_needed();
            self.level = self.level.saturating_add(1);
        }
    }

    pub fn xp_needed(&self) -> u32 {
        xp_to_next_level(self.level)
    }

    pub fn click_damage(&self) -> u32 {
        click_damage(
            self.base_damage(),
            self.bonus_damage()
        )
    }

    fn base_damage(&self) -> u32 {
        base_damage_for_level(self.level)
    }

    fn bonus_damage(&self) -> u32 {
        click_upgrade_bonus(self.click_upgrade_level)
    }

    pub(crate) fn buy_click_upgrade(&mut self) -> PurchaseOutcome {
        let cost =
            click_upgrade_cost(self.click_upgrade_level);

        if self.gold < cost {
            return PurchaseOutcome::NotEnoughGold;
        }

        self.gold -= cost;
        self.click_upgrade_level =
            self.click_upgrade_level.saturating_add(1);

        PurchaseOutcome::Purchased
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn leveling_up_keeps_leftover_xp() {
        let mut player = Player::new();

        player.add_xp(25);

        assert_eq!(player.level, 2);
        assert_eq!(player.xp, 5);
    }

    #[test]
    fn a_big_reward_can_skip_levels() {
        let mut player = Player::new();

        player.add_xp(60);

        assert_eq!(player.level, 3);
        assert_eq!(player.xp, 5);
    }

    #[test]
    fn buying_an_upgrade_needs_gold() {
        let mut player = Player::new();

        assert_eq!(
            player.buy_click_upgrade(),
            PurchaseOutcome::NotEnoughGold
        );
        assert_eq!(player.click_upgrade_level, 0);
        assert_eq!(player.bonus_damage(), 0);
    }

    #[test]
    fn buying_an_upgrade_spends_gold() {
        let mut player = Player::new();
        player.gold = 12;

        assert_eq!(
            player.buy_click_upgrade(),
            PurchaseOutcome::Purchased
        );
        assert_eq!(player.gold, 2);
        assert_eq!(player.click_upgrade_level, 1);
        assert_eq!(player.bonus_damage(), 2);
    }
}
```

### `src/game.rs`

```rust
use crate::enemy::Enemy;
use crate::player::{Player, PurchaseOutcome};
use crate::rules::{
    health_after_hit,
    idle_damage_for_level,
    is_defeated,
};

pub struct Game {
    pub stage: u32,
    pub player: Player,
    pub enemy: Enemy,
}

impl Game {
    pub fn new() -> Game {
        Game {
            stage: 1,
            player: Player::new(),
            enemy: Enemy::for_stage(1),
        }
    }

    pub fn click(&mut self) {
        attack(&self.player, &mut self.enemy);
        self.resolve_hit();
    }

    pub fn tick(&mut self, seconds: u32) {
        for _ in 0..seconds {
            let damage =
                idle_damage_for_level(self.player.level);

            self.enemy.health =
                health_after_hit(
                    self.enemy.health,
                    damage
                );

            self.resolve_hit();
        }
    }

    fn resolve_hit(&mut self) {
        if is_defeated(self.enemy.health) {
            self.defeat_enemy();
        }
    }

    fn defeat_enemy(&mut self) {
        self.player.add_gold(self.enemy.gold_reward);
        self.player.add_xp(self.enemy.xp_reward);

        self.stage = self.stage.saturating_add(1);
        self.enemy = Enemy::for_stage(self.stage);
    }

    pub fn buy_click_upgrade(
        &mut self
    ) -> PurchaseOutcome {
        self.player.buy_click_upgrade()
    }
}

impl Default for Game {
    fn default() -> Game {
        Game::new()
    }
}

fn attack(player: &Player, enemy: &mut Enemy) {
    enemy.health =
        health_after_hit(
            enemy.health,
            player.click_damage()
        );
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn attack_reduces_enemy_health() {
        let player = Player::new();

        let mut enemy = Enemy::for_stage(1);
        enemy.health = 10;

        attack(&player, &mut enemy);

        assert_eq!(enemy.health, 5);
    }

    #[test]
    fn a_new_game_starts_at_stage_one() {
        let game = Game::new();

        assert_eq!(game.stage, 1);
        assert_eq!(game.enemy.health, 20);
        assert_eq!(game.player.gold, 0);
    }

    #[test]
    fn clicking_damages_the_enemy() {
        let mut game = Game::new();

        game.click();

        assert_eq!(game.enemy.health, 15);
    }

    #[test]
    fn defeating_an_enemy_pays_the_player() {
        let mut game = Game::new();
        game.enemy.health = 1;

        game.click();

        assert_eq!(game.stage, 2);
        assert_eq!(game.enemy.health, 30);
        assert_eq!(game.player.gold, 5);
        assert_eq!(game.player.xp, 6);
    }

    #[test]
    fn one_second_is_one_idle_hit() {
        let mut game = Game::new();

        game.tick(1);

        assert_eq!(game.enemy.health, 19);
    }

    #[test]
    fn zero_seconds_changes_nothing() {
        let mut game = Game::new();

        game.tick(0);

        assert_eq!(game.stage, 1);
        assert_eq!(game.enemy.health, 20);
    }

    #[test]
    fn enough_time_defeats_the_enemy() {
        let mut game = Game::new();

        game.tick(20);

        assert_eq!(game.stage, 2);
        assert_eq!(game.enemy.health, 30);
        assert_eq!(game.player.gold, 5);
        assert_eq!(game.player.xp, 6);
    }

    #[test]
    fn one_long_tick_matches_many_short_ones() {
        let mut long = Game::new();
        let mut short = Game::new();

        long.tick(45);

        for _ in 0..45 {
            short.tick(1);
        }

        assert_eq!(long.stage, short.stage);
        assert_eq!(
            long.enemy.health,
            short.enemy.health
        );
        assert_eq!(
            long.player.gold,
            short.player.gold
        );
        assert_eq!(
            long.player.xp,
            short.player.xp
        );
        assert_eq!(
            long.player.level,
            short.player.level
        );
    }
}
```

### `tests/idle_hour.rs`

```rust
use rustbound_idle::Game;

#[test]
fn an_hour_away_moves_the_game_forward() {
    let mut game = Game::new();

    game.tick(3600);

    assert_eq!(game.stage, 128);
    assert_eq!(game.player.level, 47);
}
```

Run everything:

```bash
cargo run
cargo test
cargo clippy
```

The game should clear three stages, buy one upgrade, and spend an hour away.

All twenty-two tests should pass.

Clippy should be quiet.

---

## What we learned

Along the way, we encountered:

- modules, and `mod` to declare one
- organizing top-level modules into separate files
- `use`, and `crate::` paths
- Rust's default privacy boundaries
- `pub`
- `pub(crate)`
- `pub use`, for choosing what appears on a library's front door
- a library crate and a binary crate in the same Cargo package
- tests that live beside their code
- integration tests in `tests/`
- `Default`, which Clippy asked us to provide

Two ideas are worth keeping.

The first is that privacy is not about hiding.

It is about deciding what everyone else is allowed to depend on.

A private function gives us freedom.

We can rename it, reorganize it, or replace its implementation without outside code depending on that detail, as long as the public behavior stays the same.

`resolve_hit()` can move.

`base_damage()` can change.

Code outside their visibility boundary does not get to build itself around those details.

The second idea is where the line went.

We did not decide to build an engine.

We sorted the code, and the sorting kept asking one question:

> *Does this need to know about a screen?*

For everything except `main()`, the answer was no.

That is the heart of what our little game engine is becoming.

It is not a graphics library.

It does not open a window.

It does not print to a terminal.

It does not know whether a browser even exists.

It owns rules and state.

The game's four main commands are:

```text
new
click
tick
buy_click_upgrade
```

We still expose some state so `main()` can display it.

That part of the front door is wider than I want.

`Game`, `Player`, and `Enemy` still have public fields, which means `main()` could write:

```rust
game.enemy.health = 0;
```

and the compiler would allow it.

That is a loose end.

We are going to lean on it soon.

---

## It cannot remember

Our game can simulate an hour instantly.

It cannot survive the program ending.

Close the terminal and everything is gone:

```text
stage
gold
XP
level
upgrades
```

An idle game that forgets you the moment you leave is not much of an idle game.

Next time, the game learns to save itself.

Not to a file.

Not to a browser.

Not to a server.

The library will not know where saves go, and it should not.

It will only know how to say, in a form that can be written down:

> This is everything about me.

And later, given that same form:

> I remember.

**Small function. Bigger game.**
