# Idle Is Not a Timer.

Our game has a progression loop now.

Kill a slime, get paid, level up, buy damage, kill a tougher slime.

It still stops the moment you stop clicking.

So today we are going to make time part of the game.

Not a timer. Not a clock. Not `sleep`.

Just a number.

Because the game will never look at a clock, we can test a full hour away in a fraction of a second.

By the end, an hour away from the game is one line:

```rust
game.tick(3600);
```

And here is what the game looks like when you come back:

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

---

## Our starting point

If you are only reading along, you can skip past this code to "Time is just a number."

If you are building along, you need three structs, an enum, three `impl` blocks, eleven small functions, and sixteen 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);

        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 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 <= 8 {
        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 => {}
        }
    }
}

#[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);
    }
}
```

Run it, and the player clears eight stages and buys three upgrades.

Then the program is over, and so is all the progress, because nothing in it ever happens on its own.

---

## Time is just a number

An idle game has to keep going while nobody is looking.

The obvious way to build that is to give the game a clock.

Start a timer. Every second, do some work. When the player leaves, keep the timer running.

That sounds simple, and it is a trap.

A game that watches a clock is a game that has to be tested by waiting, unless you build a fake clock, and then you have rebuilt the idea we are about to use anyway.

Want to know what happens after an hour away? Wait an hour.

So let's turn it around.

The game will not ask what time it is.

Whoever is running the game will tell it how much time went by:

```rust
game.tick(10);
```

Ten seconds passed. What happened?

Game code often uses the word *tick* for advancing simulation time, so we will borrow the name.

The contract is one sentence:

> Tell me how many seconds passed. I will tell you what the game became.

`tick` will be a method on `Game`, and the only thing it takes is that number.

The number is the only thing about time that Rust ever sees.

Later in this series, a web page will measure real time and hand the game that number.

For now, our tests can hand it any number they like.

---

## What does a player do while nobody is clicking?

Before the game can work out what happened, it needs to know what the player does with spare seconds.

Our player is going to keep fighting.

One hit per second, and the harder the player has leveled up, the harder it hits.

That is a game rule, and we know what to do with game rules by now.

Give it a small function, next to the others:

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

One damage per second at level 1.

Two at level 2.

It is deliberately small. It is not balanced and it does not need to be yet.

It also ignores the click upgrades on purpose. Those are for clicking.

What matters is that the rule has a name and a test.

Add it at the bottom of the `tests` module:

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

Notice what the function does not take.

It does not take a `Player`. It does not take a `Game`.

It takes a level and returns a number. It is the same kind of function as `base_damage_for_level()`, and it asks the same question: what does this level of progress get you?

If you run `cargo build` now, Rust warns that `idle_damage_for_level` is never used.

We are about to use it.

---

## One second at a time

Now for `tick()`.

The player hits once per second, so ten seconds is ten hits.

We know exactly how many times to repeat: once per second that passed.

That is not a job for `while`.

A `while` loop keeps going until a condition stops being true, like our level-up loop, which cannot know how many levels one reward will cross.

When you know the count in advance, Rust has a loop for that:

```rust
for _ in 0..seconds {
    // one second of work
}
```

`0..seconds` is a range.

It counts from 0 up to, but not including, `seconds`.

So `0..3` gives you 0, 1, and 2. That is three trips through the loop, which is exactly what we want for three seconds.

The `_` is a placeholder that means:

> I do not need this value.

A `for` loop hands us the current number from the range on every trip.

We do not care which second we are on. We only care that one passed.

If we gave the number a name like `second`, Rust would warn that we never use it.

The `_` says that is on purpose.

Write the first version inside the `impl Game` block, right under `click()`. It is indented because it lives inside that block:

```rust
    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);
        }
    }
```

Each trip through the loop works out the damage for the player's current level and hits the enemy with it.

It uses `health_after_hit()`, the same small rule that has been protecting enemy health since the first article.

That looks finished.

It is not, and we should have a test to say so.

---

## The slime that died and kept being hit

We have a rule for how fast the player hits.

What we need next is a test for the thing a player cares about: if I wait long enough, does the slime go down?

At level 1, our player hits for 1 per second, and the first slime has 20 health.

Twenty seconds should do it:

```rust
#[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);
}
```

Those last numbers should look familiar.

They are the same ones the click version of this test checks: stage 2, a 30 health enemy, 5 gold, 6 XP.

It does not matter whether a slime dies from a click or from a wait.

The player should get paid the same either way.

Run it:

```bash
cargo test enough_time
```

and it fails:

```text
assertion `left == right` failed
  left: 1
 right: 2
```

(Rust prints more than that, including a line number and a note about backtraces. This is the part that matters.)

`left` is what the game did. `right` is what the test expected.

The stage is still 1.

Twenty seconds passed. The slime's health reached zero on the twentieth hit.

And then nothing happened.

Our loop never asks whether the enemy is defeated.

That check lives in `click()`:

```rust
        if is_defeated(self.enemy.health) {
            self.defeat_enemy();
        }
```

`tick()` took the hit from `click()` but left the aftermath behind.

---

## Two ways to land a hit

There are two ways to hurt an enemy now: a click, and a second of waiting.

Both of them need the same thing to happen afterward.

If we paste the `if` into `tick()`, we have the same three lines in two places.

Then the next time defeating an enemy needs to change, we have to remember both.

We have seen this before.

When the same enemy was being built in three different places, we gave the building a name, and `Enemy::for_stage()` became the one place that builds an enemy.

Same fix here.

Add a method inside the same `impl Game` block, under `tick()`:

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

Read it out loud:

> A hit just landed. If the enemy went down, handle the defeat.

Now both ways of landing a hit can finish the same way.

Replace the whole `click()` method with this one. The old `if` is gone, and a call to `resolve_hit()` takes its place:

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

And replace `tick()` with this. Only the last line inside the loop is new:

```rust
    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();
        }
    }
```

Run the tests again:

```bash
cargo test
```

All eighteen pass, including the clicking tests we did not touch.

That is what a good extraction looks like.

`click()` does the same thing it did before.

It just does it by calling a method that `tick()` can call too.

Now follow the path of the twentieth second:

```text
tick()
    ↓
health_after_hit() → 0
    ↓
resolve_hit()
    ↓
defeat_enemy()
    ↓
gold, XP, next stage, next enemy
```

That bottom half is the same code a click reaches.

The slime can die from a click or from a second of waiting.

Either way, it dies the same way.

---

## Test time without waiting

Look at what we just tested.

Twenty seconds of game time ran, and the test finished instantly.

Nothing slept. Nothing waited.

The game does not know what a second is.

It was handed the number 20 and did twenty steps of work.

That is the payoff for the design.

Time is an input, so a test can supply as much of it as it likes.

Three more tests are worth adding. Put each one at the bottom of the `tests` module.

One second should be one hit:

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

    game.tick(1);

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

Zero seconds should change nothing:

```rust
#[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);
}
```

We did not write any code to handle zero.

We did not need to.

`0..0` is an empty range, so the loop runs zero times and the game stays exactly as it was.

The last test is the one that matters most.

Waiting forty-five seconds in one go should give the same game as waiting one second forty-five times:

```rust
#[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);
}
```

This one passes by construction today, because `tick(45)` literally is forty-five single steps.

Today it almost looks redundant. Later, when we replace thousands of tiny steps with arithmetic, it becomes the test that proves the optimization did not change the game.

It is a promise to our future selves.

Later, a browser will sometimes call `tick(1)` every second.

Other times, it will come back after a long absence and call `tick(7200)` once.

Those two have to be the same game.

If a change ever makes them different, this test will say so.

Run them all:

```bash
cargo test
```

Twenty-one tests should pass.

---

## Leave for an hour

Time to try it for real.

We are going to leave the loop in `main()` exactly as it is, with one change: stop at stage 3, so the output is short.

```rust
    while game.stage <= 3 {
```

Everything inside the loop stays the same.

Then, after the loop, the player leaves.

Add this after the closing brace of the `while` loop, still inside `main()`:

```rust
    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
    );
```

Three thousand six hundred is the number of seconds in an hour.

Run it:

```bash
cargo run
```

You should see:

```text
Stage 1 cleared!  Gold: 5  XP: 6/20
  Next up: Training Slime with 30 health.
Stage 2 cleared!  Gold: 12  XP: 14/20
  Next up: Training Slime with 40 health.
Bought click upgrade 1! Click damage: 7  Gold left: 2
Stage 3 cleared!  Gold: 11  XP: 4/35
Level up! Now level 2. Click damage: 8
  Next up: Training Slime with 50 health.
One hour away.
Stage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410
```

The first half is the player, clicking their way through three stages.

The last two lines are the hour.

One hour, with nobody at the keyboard, took the game from stage 4 to stage 131.

The player also came home with 17,410 gold and nothing spent. Noted.

Look at the level.

It went from 2 to 48.

That was not a bug.

Every trip through the `for` loop asks for the damage at the player's *current* level.

When a slime pays out enough XP, the player levels up in the middle of the hour, and every second after that hits a little harder.

Waiting makes the player stronger, which makes waiting faster.

That is the shape of an idle game.

Is it balanced? No.

A stage 131 slime after an hour is a lot.

We will tune that later. We have a rule with a name and a test, so tuning it is changing one function.

What matters today is that the engine can answer a question it could not answer before:

> What happened while I was away?

---

## Who owns the clock?

The game never asked what time it was.

It was handed 3600, and it did 3600 seconds of work.

That is going to become one of the most important lines in this series:

```text
Rust owns the rules.
Whatever runs the game owns time.
```

Today, that caller is a test or `main()`. Later, it will be a web page.

The caller measures how much real time passed and hands that fact to the game.

Rust decides what that time means for gameplay.

That distinction matters. A browser might report that 28,800 seconds passed while the player was away. The game can still decide that only part of that time should count.

In other words:

```text
caller measures elapsed time
        ↓
Rust receives the number
        ↓
Rust decides how much time counts
        ↓
Rust applies the game rules
```

That keeps balance decisions inside the game instead of hiding them in whichever environment happens to run it.

There is one honest cost.

`tick()` does one trip through the loop for every second.

An hour is 3,600 trips. A day is 86,400.

That is still manageable for this tiny program, but the cost grows directly with the number of seconds we simulate.

A number like `u32::MAX` would be a very long walk.

Later, we can cap credited away-time or replace repeated steps with arithmetic. The caller will still report elapsed time; Rust will still own the policy and the rules.

---

## 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.

At this point, `src/main.rs` should look like this:

```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 both:

```bash
cargo run
cargo test
```

The game should clear three stages, buy one upgrade, spend an hour away, and all twenty-one tests should pass.

---

## What we learned

Along the way, we encountered:

- time as a plain number, passed in by the caller
- the `tick` method
- `for` loops and ranges like `0..seconds`
- `_` for a value we do not need
- extracting a shared method so two paths finish the same way
- tests that run an hour without waiting

Two ideas are worth keeping.

The first is how to choose between the two loops we now know.

Ask whether you know the count.

The level-up loop could not know how many levels one reward would cross, so it was a `while`.

`tick()` knows exactly how many seconds it was given, so it is a `for`.

The second is `resolve_hit()`.

The bug was not in the loop. The loop did what we told it.

The bug was that two ways of hurting an enemy had to finish the same way, and only one of them did.

Giving the finish a name fixed it, and now there is one place where "the enemy went down" turns into gold, XP, and a new slime.

---

## You accidentally built a game engine

Open `src/main.rs` and scroll.

Three structs. An enum. Three `impl` blocks. Twelve small rules. A click, a purchase, a clock that is not a clock, and twenty-one tests.

All of it is in one file, and the file is getting long.

Nobody planned that.

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, a screen, or a clock.

Only `main()` does.

Today they share a file. Next time, we put a wall between them, and find out that the part on the far side of the wall is already a small game engine.

**Small function. Bigger game.**
