Skip to content

Commit

Permalink
Hitting walls and other game over events
Browse files Browse the repository at this point in the history
Ending the game of Snake can happen one of three ways:

- Snake hits a wall
- Snake hits itself
- All tiles are full of Snake (Win condition).

In `[lib.rs](http://lib.rs)` we can use an enum to represent each of these.

```rust
enum GameOverReason {
    HitWall,
    HitSnake,
    Win,
}
```

In `tick`, we’ll need access to the board to detect whether the snake has hit the wall or not, so bring `Board` into scope and set up the `query_board`. Inside of the system, we can get access to the board using `query_board.single()`.

Then we can use the match on the input direction to do hit detection against the walls.

`hit_wall` is going to be an `Option<GameOverReason>`. If we hit a wall we’ll have a game over, if not then we’ll have `None`.

I’ve also put `use controls::Direction::*;` at the top of this file so that we can use `Up` and the other variants rather than typing out the full module paths.

```rust
pub fn tick(
    mut commands: Commands,
    mut snake: ResMut<Snake>,
    positions: Query<(Entity, &Position)>,
    input: Res<controls::Direction>,
    query_food: Query<(Entity, &Position), With<Food>>,
    mut food_events: EventWriter<NewFoodEvent>,
    query_board: Query<&Board>,
) {
    let board = query_board.single();

    let mut next_position = snake.segments[0].clone();
    let hit_wall = match *input {
        Up if next_position.y == board.size - 1 => Some(GameOverReason::HitWall),
        Up => {
            next_position.y += 1;
            None
        }
        Down if next_position.y == 0 => Some(GameOverReason::HitWall),
        Down => {
            next_position.y -= 1;
            None
        }
        Right if next_position.x == board.size - 1 => Some(GameOverReason::HitWall),
        Right => {
            next_position.x += 1;
            None
        }
        Left if next_position.x == 0 => Some(GameOverReason::HitWall),
        Left => {
            next_position.x -= 1;
            None
        }
    };
    ...
}
```

We changed out matches here to use a Rust feature called Guards.

A Guard is a check we can do in the left hand of the match to see if we should use this arm of the expression.

Zooming in on `Up` specifically, if the input direction is up, the first part of the first match matches. We then can immediately use an if statement to check to see if the y position of the next snake segment would be outside of the bounds of the board grid.

If the if expression is true, then we evaluate the expression on the right hand side.

If the if expression is false, then we fall through to the next match, which in this case is just `Up` and therefore would catch the match and return `None`.

```rust
Up if next_position.y == board.size - 1 => Some(GameOverReason::HitWall),
Up => {
    next_position.y += 1;
    None
}
```

We do this guard for each direction, Up against the top of the board, `Right` against the right side, etc.

After this match, we’ll either have the next segment ready to go or we’ll have a game over indicator.

After checking the walls we can check if the snake hit itself. This is a bit simpler as we can use `.contains` to see if the next position is in the snake segments.

```rust
// did the snake hit itself?
let hit_self =
    if snake.segments.contains(&next_position) {
        Some(GameOverReason::HitSnake)
    } else {
        None
    };
```

and of course, the win condition. If the number of snake segments equals the number of tiles in the board then the snake has nowhere else to go and has won the round.

```rust
let has_won = if snake.segments.len()
    == board.size as usize * board.size as usize
{
    Some(GameOverReason::Win)
} else {
    None
};
```

We can then move the rest of our code in this function into a match expression.

`hit_wall.or(hit_self).or(has_won)` uses [the or function](https://doc.rust-lang.org/std/option/enum.Option.html#method.or) on the `Option` type to combine all of the potential game over option values and if any of them are a `Some` value, it will be the value we’re matching on. Otherwise it will be `None`.

If we have any of the game over variants, we transition into the menu state which ends the game. Note that `NextState` is part of the `iyes_loopless::prelude::*`.

Otherwise we continue with the same code we had before.

```rust
match hit_wall.or(hit_self).or(has_won) {
    Some(GameOverReason::HitWall)
    | Some(GameOverReason::HitSnake)
    | Some(GameOverReason::Win) => {
        commands.insert_resource(NextState(
            GameState::Menu,
        ));
    }
    None => {
        snake.segments.push_front(next_position);
        commands.add({
            SpawnSnakeSegment {
                position: next_position,
            }
        });

        // remove old snake segment, unless snake just
        // ate food
        let is_food = query_food
            .iter()
            .find(|(_, pos)| &&next_position == pos);
        match is_food {
            Some((entity, _)) => {
                commands
                    .entity(entity)
                    .despawn_recursive();
                food_events.send(NewFoodEvent);
            }
            None => {
                let old_tail =
                    snake.segments.pop_back().unwrap();
                if let Some((entity, _)) = positions
                    .iter()
                    .find(|(_, pos)| pos == &&old_tail)
                {
                    commands
                        .entity(entity)
                        .despawn_recursive();
                }
            }
        }
    }
}
```

and now the game can end multiple ways!
  • Loading branch information
ChristopherBiscardi committed Apr 22, 2022
1 parent 5656499 commit 7f0ca16
Showing 1 changed file with 86 additions and 27 deletions.
113 changes: 86 additions & 27 deletions src/lib.rs
@@ -1,6 +1,8 @@
use bevy::prelude::*;
use board::{Position, SpawnSnakeSegment};
use board::{Board, Position, SpawnSnakeSegment};
use controls::Direction::*;
use food::{Food, NewFoodEvent};
use iyes_loopless::prelude::*;
use snake::Snake;

pub mod board;
Expand All @@ -15,55 +17,112 @@ pub enum GameState {
Playing,
}

#[derive(PartialEq, Eq, Debug)]
enum GameOverReason {
HitWall,
HitSnake,
Win,
}

pub fn tick(
mut commands: Commands,
mut snake: ResMut<Snake>,
positions: Query<(Entity, &Position)>,
input: Res<controls::Direction>,
query_food: Query<(Entity, &Position), With<Food>>,
mut food_events: EventWriter<NewFoodEvent>,
query_board: Query<&Board>,
) {
let board = query_board.single();

let mut next_position = snake.segments[0].clone();
match *input {
controls::Direction::Up => {
let hit_wall = match *input {
Up if next_position.y == board.size - 1 => {
Some(GameOverReason::HitWall)
}
Up => {
next_position.y += 1;
None
}
Down if next_position.y == 0 => {
Some(GameOverReason::HitWall)
}
controls::Direction::Down => {
Down => {
next_position.y -= 1;
None
}
Right if next_position.x == board.size - 1 => {
Some(GameOverReason::HitWall)
}
controls::Direction::Right => {
Right => {
next_position.x += 1;
None
}
controls::Direction::Left => {
Left if next_position.x == 0 => {
Some(GameOverReason::HitWall)
}
Left => {
next_position.x -= 1;
None
}
};

snake.segments.push_front(next_position);
commands.add({
SpawnSnakeSegment {
position: next_position,
}
});
// did the snake hit itself?
let hit_self =
if snake.segments.contains(&next_position) {
Some(GameOverReason::HitSnake)
} else {
None
};

// remove old snake segment, unless snake just
// ate food
let is_food = query_food
.iter()
.find(|(_, pos)| &&next_position == pos);
match is_food {
Some((entity, _)) => {
commands.entity(entity).despawn_recursive();
food_events.send(NewFoodEvent);
let has_won = if snake.segments.len()
== board.size as usize * board.size as usize
{
Some(GameOverReason::Win)
} else {
None
};

match hit_wall.or(hit_self).or(has_won) {
Some(GameOverReason::HitWall)
| Some(GameOverReason::HitSnake)
| Some(GameOverReason::Win) => {
commands.insert_resource(NextState(
GameState::Menu,
));
}
None => {
let old_tail =
snake.segments.pop_back().unwrap();
if let Some((entity, _)) = positions
snake.segments.push_front(next_position);
commands.add({
SpawnSnakeSegment {
position: next_position,
}
});

// remove old snake segment, unless snake just
// ate food
let is_food = query_food
.iter()
.find(|(_, pos)| pos == &&old_tail)
{
commands.entity(entity).despawn_recursive();
.find(|(_, pos)| &&next_position == pos);
match is_food {
Some((entity, _)) => {
commands
.entity(entity)
.despawn_recursive();
food_events.send(NewFoodEvent);
}
None => {
let old_tail =
snake.segments.pop_back().unwrap();
if let Some((entity, _)) = positions
.iter()
.find(|(_, pos)| pos == &&old_tail)
{
commands
.entity(entity)
.despawn_recursive();
}
}
}
}
}
Expand Down

0 comments on commit 7f0ca16

Please sign in to comment.