Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ゲームロジック

最初に作成するモジュールは、ゲームロジックです。あなたはおそらく snake ゲームをご存じでしょうが、 もし知らなくても、基本的なアイデアは、プレイヤーが 2D グリッド上でヘビを動かすというものです。常に、 グリッド上のどこかランダムな位置に「食べ物」があり、ゲームの目標はヘビにできるだけ多くの食べ物を 「食べさせる」ことです。ヘビは食べ物を食べるたびに長くなります。ヘビが自分の尻尾に衝突すると、 プレイヤーの負けです。

ゲームのバリエーションによっては、ヘビがグリッドの端に衝突してもプレイヤーの負けになりますが、 今回のグリッドは小さいため、「ラップアラウンド」ルールを実装します。つまり、ヘビがグリッドの一方の端から 外に出た場合、反対側の端から続けて現れます。

game モジュール

ゲームの仕組みは game モジュールの中で組み立てていきます。

座標

まず、ゲーム用の座標系を定義します(src/game/coords.rs)。

#![allow(unused)]
fn main() {
use super::Prng;

use heapless::index_set::FnvIndexSet;

/// A single point on the grid.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Coords {
    // Signed ints to allow negative values (handy when checking if we have gone
    // off the top or left of the grid)
    pub row: i8,
    pub col: i8,
}

impl Coords {
    /// Get random coordinates within a grid. `exclude` is an optional set of
    /// coordinates which should be excluded from the output.
    pub fn random(rng: &mut Prng, exclude: Option<&FnvIndexSet<Coords, 32>>) -> Self {
        let mut coords = Coords {
            row: ((rng.random_u32() as usize) % 5) as i8,
            col: ((rng.random_u32() as usize) % 5) as i8,
        };
        while exclude.is_some_and(|exc| exc.contains(&coords)) {
            coords = Coords {
                row: ((rng.random_u32() as usize) % 5) as i8,
                col: ((rng.random_u32() as usize) % 5) as i8,
            }
        }
        coords
    }

    /// Whether the point is outside the bounds of the grid.
    pub fn is_out_of_bounds(&self) -> bool {
        self.row < 0 || self.row >= 5 || self.col < 0 || self.col >= 5
    }
}
}

グリッド上の位置を表すために Coords 構造体を使います。Coords は 2 つの整数しか含まないため、 所有権を気にせずに Coords 構造体を受け渡しできるよう、コンパイラに Copy トレイトの実装を derive させます。

乱数生成

関連関数 Coords::random を定義します。これにより、グリッド上のランダムな位置を取得できます。 これは後で、ヘビの食べ物をどこに配置するかを決めるために使います。

ランダムな座標を生成するには、乱数の供給源が必要です。nRF52833 にはハードウェア乱数生成器 (HWRNG)ペリフェラルがあり、これは nRF52833 spec の 6.19 節に記載されています。HAL は microbit::hal::rng::Rng 構造体を介して HWRNG へのシンプルなインターフェースを提供してくれます。HWRNG は ゲーム用途には十分な速度でない可能性があります。またテストでは、実行ごとに生成器が出力する乱数列を 再現できると便利ですが、HWRNG では設計上それは不可能です。そこで、pseudo-random 数生成器(PRNG)も 定義します。PRNG は xorshift アルゴリズムを用いて、擬似乱数の u32 値を生成します。このアルゴリズムは 基本的なもので、暗号学的に安全ではありませんが、効率的で実装も簡単であり、ささやかな snake ゲームには 十分です。Prng 構造体には初期シード値が必要ですが、それは RNG ペリフェラルから取得します。

これらすべてをまとめたものが src/game/rng.rs です。

#![allow(unused)]
fn main() {
use crate::Rng;

/// A basic pseudo-random number generator.
pub struct Prng {
    value: u32,
}

impl Prng {
    pub fn seeded(rng: &mut Rng) -> Self {
        Self::new(rng.random_u32())
    }

    pub fn new(seed: u32) -> Self {
        Self { value: seed }
    }

    /// Basic xorshift PRNG function: see <https://en.wikipedia.org/wiki/Xorshift>
    fn xorshift32(mut input: u32) -> u32 {
        input ^= input << 13;
        input ^= input >> 17;
        input ^= input << 5;
        input
    }

    /// Return a pseudo-random u32.
    pub fn random_u32(&mut self) -> u32 {
        self.value = Self::xorshift32(self.value);
        self.value
    }
}
}

移動

ゲームの状態管理に役立つ、いくつかの enum も定義する必要があります。移動方向、曲がる方向、 現在のゲーム状態、そしてゲーム内の特定の「ステップ」(つまり、ヘビの 1 回の移動)の結果です。 これらは src/game/movement.rs に含まれています。

#![allow(unused)]
fn main() {
use super::Coords;

/// Define the directions the snake can move.
pub enum Direction {
    Up,
    Down,
    Left,
    Right,
}

/// What direction the snake should turn.
#[derive(Debug, Copy, Clone)]
pub enum Turn {
    Left,
    Right,
    None,
}

/// The current status of the game.
pub enum GameStatus {
    Won,
    Lost,
    Ongoing,
}

/// The outcome of a single move/step.
pub enum StepOutcome {
    /// Grid full (player wins)
    Full,
    /// Snake has collided with itself (player loses)
    Collision,
    /// Snake has eaten some food
    Eat(Coords),
    /// Snake has moved (and nothing else has happened)
    Move(Coords),
}
}

Snake(A Snaaake!

次に、ヘビが占有している座標と進行方向を追跡する Snake 構造体を定義します。座標の順序を管理するために キュー(heapless::spsc::Queue)を使い、高速に衝突判定を行えるようにハッシュセット (heapless::FnvIndexSet)を使います。Snake には移動を行うためのメソッドがあります。これを src/game/snake.rs に実装します。

#![allow(unused)]
fn main() {
use super::{Coords, Direction, FnvIndexSet, Turn};

use heapless::spsc::Queue;

pub struct Snake {
    /// Coordinates of the snake's head.
    pub head: Coords,
    /// Queue of coordinates of the rest of the snake's body. The end of the tail is
    /// at the front.
    pub tail: Queue<Coords, 32>,
    /// A set containing all coordinates currently occupied by the snake (for fast
    /// collision checking).
    pub coord_set: FnvIndexSet<Coords, 32>,
    /// The direction the snake is currently moving in.
    pub direction: Direction,
}

impl Snake {
    pub fn make_snake() -> Self {
        let head = Coords { row: 2, col: 2 };
        let initial_tail = Coords { row: 2, col: 1 };
        let mut tail = Queue::new();
        tail.enqueue(initial_tail).unwrap();
        let mut coord_set: FnvIndexSet<Coords, 32> = FnvIndexSet::new();
        coord_set.insert(head).unwrap();
        coord_set.insert(initial_tail).unwrap();
        Self {
            head,
            tail,
            coord_set,
            direction: Direction::Right,
        }
    }

    /// Move the snake onto the tile at the given coordinates. If `extend` is false,
    /// the snake's tail vacates the rearmost tile.
    pub fn move_snake(&mut self, coords: Coords, extend: bool) {
        // Location of head becomes front of tail
        self.tail.enqueue(self.head).unwrap();
        // Head moves to new coords
        self.head = coords;
        self.coord_set.insert(coords).unwrap();
        if !extend {
            let back = self.tail.dequeue().unwrap();
            self.coord_set.remove(&back);
        }
    }

    fn turn_right(&mut self) {
        self.direction = match self.direction {
            Direction::Up => Direction::Right,
            Direction::Down => Direction::Left,
            Direction::Left => Direction::Up,
            Direction::Right => Direction::Down,
        }
    }

    fn turn_left(&mut self) {
        self.direction = match self.direction {
            Direction::Up => Direction::Left,
            Direction::Down => Direction::Right,
            Direction::Left => Direction::Down,
            Direction::Right => Direction::Up,
        }
    }

    pub fn turn(&mut self, direction: Turn) {
        match direction {
            Turn::Left => self.turn_left(),
            Turn::Right => self.turn_right(),
            Turn::None => (),
        }
    }
}
}

ゲームモジュールのトップレベル

Game 構造体はゲーム状態を追跡します。これには Snake オブジェクト、現在の食べ物の座標、 ゲームの速度(ヘビの各移動の間に経過する時間を決めるために使われます)、ゲームの状態 (ゲームが進行中か、プレイヤーが勝ったか負けたか)、そしてプレイヤーのスコアが含まれます。

この構造体には、ゲームの各ステップを処理し、ヘビの次の動きを決定して、それに応じてゲーム状態を 更新するメソッドが含まれます。また、game_matrixscore_matrix という 2 つのメソッドもあり、 LED マトリクス上にゲーム状態やプレイヤーのスコアを表示するために使える値の 2 次元配列を出力します (これについては後で見ます)。

Game 構造体は、game モジュールの最上位である src/game.rs に配置します。

#![allow(unused)]
fn main() {
mod coords;
mod movement;
mod rng;
mod snake;

use crate::Rng;

pub use coords::Coords;
pub use movement::{Direction, GameStatus, StepOutcome, Turn};
pub use rng::Prng;
pub use snake::Snake;

use heapless::index_set::FnvIndexSet;

/// Struct to hold game state and associated behaviour
pub struct Game {
    pub status: GameStatus,
    rng: Prng,
    snake: Snake,
    food_coords: Coords,
    speed: u8,
    score: u8,
}

impl Game {
    pub fn new(rng: &mut Rng) -> Self {
        let mut rng = Prng::seeded(rng);
        let snake = Snake::make_snake();
        let food_coords = Coords::random(&mut rng, Some(&snake.coord_set));
        Self {
            rng,
            snake,
            food_coords,
            speed: 1,
            status: GameStatus::Ongoing,
            score: 0,
        }
    }

    /// Reset the game state to start a new game.
    pub fn reset(&mut self) {
        self.snake = Snake::make_snake();
        self.place_food();
        self.speed = 1;
        self.status = GameStatus::Ongoing;
        self.score = 0;
    }

    /// Randomly place food on the grid.
    fn place_food(&mut self) -> Coords {
        let coords = Coords::random(&mut self.rng, Some(&self.snake.coord_set));
        self.food_coords = coords;
        coords
    }

    /// "Wrap around" out of bounds coordinates (eg, coordinates that are off to the
    /// left of the grid will appear in the rightmost column). Assumes that
    /// coordinates are out of bounds in one dimension only.
    fn wraparound(&self, coords: Coords) -> Coords {
        if coords.row < 0 {
            Coords { row: 4, ..coords }
        } else if coords.row >= 5 {
            Coords { row: 0, ..coords }
        } else if coords.col < 0 {
            Coords { col: 4, ..coords }
        } else {
            Coords { col: 0, ..coords }
        }
    }

    /// Determine the next tile that the snake will move on to (without actually
    /// moving the snake).
    fn get_next_move(&self) -> Coords {
        let head = &self.snake.head;
        let next_move = match self.snake.direction {
            Direction::Up => Coords {
                row: head.row - 1,
                col: head.col,
            },
            Direction::Down => Coords {
                row: head.row + 1,
                col: head.col,
            },
            Direction::Left => Coords {
                row: head.row,
                col: head.col - 1,
            },
            Direction::Right => Coords {
                row: head.row,
                col: head.col + 1,
            },
        };
        if next_move.is_out_of_bounds() {
            self.wraparound(next_move)
        } else {
            next_move
        }
    }

    /// Assess the snake's next move and return the outcome. Doesn't actually update
    /// the game state.
    fn get_step_outcome(&self) -> StepOutcome {
        let next_move = self.get_next_move();
        if self.snake.coord_set.contains(&next_move) {
            // We haven't moved the snake yet, so if the next move is at the end of
            // the tail, there won't actually be any collision (as the tail will have
            // moved by the time the head moves onto the tile)
            if next_move != *self.snake.tail.peek().unwrap() {
                StepOutcome::Collision
            } else {
                StepOutcome::Move(next_move)
            }
        } else if next_move == self.food_coords {
            if self.snake.tail.len() == 23 {
                StepOutcome::Full
            } else {
                StepOutcome::Eat(next_move)
            }
        } else {
            StepOutcome::Move(next_move)
        }
    }

    /// Handle the outcome of a step, updating the game's internal state.
    fn handle_step_outcome(&mut self, outcome: StepOutcome) {
        self.status = match outcome {
            StepOutcome::Collision => GameStatus::Lost,
            StepOutcome::Full => GameStatus::Won,
            StepOutcome::Eat(c) => {
                self.snake.move_snake(c, true);
                self.place_food();
                self.score += 1;
                if self.score.is_multiple_of(5) {
                    self.speed += 1
                }
                GameStatus::Ongoing
            }
            StepOutcome::Move(c) => {
                self.snake.move_snake(c, false);
                GameStatus::Ongoing
            }
        }
    }

    pub fn step(&mut self, turn: Turn) {
        self.snake.turn(turn);
        let outcome = self.get_step_outcome();
        self.handle_step_outcome(outcome);
    }

    /// Calculate the length of time to wait between game steps, in milliseconds.
    /// Generally this will get lower as the player's score increases, but need to
    /// be careful it cannot result in a value below zero.
    pub fn step_len_ms(&self) -> u32 {
        let result = 1000 - (200 * ((self.speed as i32) - 1));
        if result < 200 {
            200u32
        } else {
            result as u32
        }
    }

    /// Return an array representing the game state, which can be used to display the
    /// state on the microbit's LED matrix. Each `_brightness` parameter should be a
    /// value between 0 and 9.
    pub fn game_matrix(
        &self,
        head_brightness: u8,
        tail_brightness: u8,
        food_brightness: u8,
    ) -> [[u8; 5]; 5] {
        let mut values = [[0u8; 5]; 5];
        values[self.snake.head.row as usize][self.snake.head.col as usize] = head_brightness;
        for t in &self.snake.tail {
            values[t.row as usize][t.col as usize] = tail_brightness
        }
        values[self.food_coords.row as usize][self.food_coords.col as usize] = food_brightness;
        values
    }

    /// Return an array representing the game score, which can be used to display the
    /// score on the microbit's LED matrix (by illuminating the equivalent number of
    /// LEDs, going left->right and top->bottom).
    pub fn score_matrix(&self) -> [[u8; 5]; 5] {
        let mut values = [[0u8; 5]; 5];
        let full_rows = (self.score as usize) / 5;
        #[allow(clippy::needless_range_loop)]
        for r in 0..full_rows {
            values[r] = [1; 5];
        }
        #[allow(clippy::needless_range_loop)]
        for c in 0..(self.score as usize) % 5 {
            values[full_rows][c] = 1;
        }
        values
    }
}
}

次に、ヘビの動きを操作できるようにします。