3 - Player movement

Jun 4, 2021 tdd rust srl | Prev | Next | Code

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

Next on the list is the ability to move the player:

- [X] Render tiles
- [ ] Move player
- [ ] Run in terminal

In our game, the player will be able to move around using the arrow keys on the keyboard. This will come to fruition in the next step (Run in terminal), but we can lay most of the groundwork now.

The second failing test

So let’s write a test that will verify that the game can act on some kind of changing input.
Since we are only interested in the player here, we don’t need to add any walls or enemies.

#[test]
fn player_moves_according_to_input() {
    let mut game = Game::new();
    game.set_player_position(2, 1);
    let mut input = InputSimulator::new();
    let mut renderer = RenderingSpy::new(5, 4);

    game.render(&mut renderer);
    renderer.assert_frame(vec![
        ". . . . .",
        ". . @ . .",
        ". . . . .",
        ". . . . ."
    ]);

    input.simulate_move_right();
    game.tick(&input);

    game.render(&mut renderer);
    renderer.assert_frame(vec![
        ". . . . .",
        ". . . @ .",
        ". . . . .",
        ". . . . ."
    ]);

    // assert other directions below
}
This test does the following:

Again very minimal design work: The tick method will take some kind of input object and use it to determine the player’s intentions.

Specifically, Game::tick takes a trait called Input:

pub fn tick<T: Input>(&mut self, input: &T) { 
    // does nothing yet
}
pub trait Input {
    fn move_left(&self) -> bool;
    fn move_right(&self) -> bool;
    fn move_up(&self) -> bool;
    fn move_down(&self) -> bool;
}

The tick code is expected to use these functions to determine if and where to move the player.

Simulating Input

We can’t use “real” input in this test, so we need a way to simulate it. So, in addition to implementing the Input trait, InputSimulator has additonal functions like simulate_move_right, to control the return value of the trait functions. This way we can trick the game code into thinking the player wants to move in a given direction.

To make the test compile (and fail) we implement an empty InputSimulator:

impl InputSimulator {
    pub fn simulate_move_left(&mut self) {}

    pub fn simulate_move_right(&mut self) {}

    pub fn simulate_move_up(&mut self) {}

    pub fn simulate_move_down(&mut self) {}
}

impl Input for InputSimulator {
    fn move_left(&self) -> bool { false }

    fn move_right(&self) -> bool { false }

    fn move_up(&self) -> bool { false }

    fn move_down(&self) -> bool { false }
}

We achieve test failure:

Because Game::tick does nothing.

The complete InputSimulator is not very interesting, but here is a snippet:

impl InputSimulator {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn simulate_move_right(&mut self) {
        self.simulating_move_right = true
    }
    // ... 
}

impl Input for InputSimulator {
    fn move_right(&self) -> bool {
        self.simulating_move_right
    }
    /// ...
}

Querying input

The Game::tick function is only slightly more interesting:

pub fn tick<T: Input>(&mut self, input: &T) {
    let (mut player_x, mut player_y) = self.player;
    if input.move_left() { player_x -= 1 }
    if input.move_right() { player_x += 1 }

    if input.move_up() { player_y -= 1 }
    if input.move_down() { player_y += 1 }

    self.set_player_position(player_x, player_y);
}

We are fast forwarding here - in reality I was only writing as much code as needed to make the initial test pass, which only asserts that the player can move to the right.

At any rate, the above code passes the test for all four directions. The full test tells the story of the moving player:

fn player_moves_according_to_input() {
    let mut game = Game::new();
    game.set_player_position(2, 1);
    let mut input = InputSimulator::new();
    let mut renderer = RenderingSpy::new(5, 4);

    game.render(&mut renderer);

    renderer.assert_frame(vec![
        ". . . . .",
        ". . @ . .",
        ". . . . .",
        ". . . . ."
    ]);

    input.simulate_move_right();
    game.tick(&input);
    game.render(&mut renderer);

    renderer.assert_frame(vec![
        ". . . . .",
        ". . . @ .",
        ". . . . .",
        ". . . . ."
    ]);

    input.simulate_move_down();
    game.tick(&input);
    game.render(&mut renderer);

    renderer.assert_frame(vec![
        ". . . . .",
        ". . . . .",
        ". . . @ .",
        ". . . . ."
    ]);

    input.simulate_move_left();
    game.tick(&input);
    game.render(&mut renderer);

    renderer.assert_frame(vec![
        ". . . . .",
        ". . . . .",
        ". . @ . .",
        ". . . . ."
    ]);

    input.simulate_move_up();
    game.tick(&input);
    game.render(&mut renderer);

    renderer.assert_frame(vec![
        ". . . . .",
        ". . @ . .",
        ". . . . .",
        ". . . . ."
    ]);

And that’s “Move player”:

- [X] Render tiles
- [X] Move player
- [ ] Run in terminal

In the next installment, input and rendering will finally see some “real” implementations, and we will roll everything into an executable. Should be fun!