12 - Combat (log) integration

Jul 14, 2021 tdd rust srl | Prev | Next | Code

This post is part of the series Making a Roguelike - A test-driven approach.

Finally, we want to see those combat events in the actual game. Let’s add some missing pieces and ship it.

Real combat log

The first thing we need is a “real” combat log. It will be written right next to the dungeon. The test is a good example of how terminal output is verified:

#[test]
fn renders_combat_log_to_the_right_of_dugeon() {
    let mut renderer = TerminalRenderer::new(3, 2);
    renderer.append_combat_log("combat log line 1");
    renderer.append_combat_log("combat log line 2");
    renderer.render_at(0, 0, Player);
    renderer.render_at(1, 1, Wall);
    renderer.render_at(2, 1, Enemy);

    verify_flush_writes(&mut renderer, vec![
        "@..  combat log line 1",
        ".#E  combat log line 2"]);
}

fn verify_flush_writes(renderer: &mut TerminalRenderer, expected: Vec<&str>) {
    let mut buffer = Cursor::new(Vec::new());
    renderer.flush(&mut buffer);
    let actual = str::from_utf8(&buffer.get_ref()).unwrap();
    assert_eq!(actual, expected.join("\n"));
}

The helper function will pass a Cursor object to TerminalRenderer::flush, which takes anything that implements Write. The cursor lets us spy nicely on what was written to the terminal.

RandomizedCombatEngine

And we need some kind of combat to happen. As a stepping stone, let’s implement something simple and random:

pub struct RandomizedCombatEngine {}

impl CombatEngine for RandomizedCombatEngine {
    fn is_hit(&self, _attacker: DungeonCoords, _victim: DungeonCoords) -> bool {
        return thread_rng().gen_bool(0.5)
    }

    fn roll_damage(&self, _attacker: DungeonCoords) -> u32 {
        thread_rng().gen_range(1..=10)
    }
}

It will hit 50% of the time and produce random damage between 1 and 10. Just so we have some action on the screen.

We can simpy use RandomizedCombatEngine as the new default, since most tests don’t care about combat and don’t check the combat log. This might change in the future, but for now it works, and no change in main.rs is necessary:

 1impl Game {
 2    pub fn generate_with<T: DungeonGenerator>(generator: &T) -> Self {
 3        Self {
 4            combat_engine: Box::from(RandomizedCombatEngine {}),
 5            dungeon: generator.generate(),
 6            combat_events: Vec::new(),
 7        }
 8    }
 9    // ...
10}

Great! But …

We are done with combat! Right?
Well, no. All we do is write out some messages, but state-wise, nothing is happening. No damage is done to the player or the enemies.
How is that going to happen? What is the next increment? Have we coded ourselves into a corner? Well well. You will have to tune in next time, after a short summer break.

In the meantime, here is the gif I promised: