From 7d5ea0d263705e1a7b97be219d631dc886b63556 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sun, 21 Jul 2024 17:44:00 -0700 Subject: [PATCH] edited ch 11 --- mdbook/src/11-snake-game/README.md | 33 +- mdbook/src/11-snake-game/controls.md | 241 +++------ mdbook/src/11-snake-game/final-assembly.md | 30 ++ mdbook/src/11-snake-game/game-logic.md | 502 +++--------------- .../src/11-snake-game/nonblocking-display.md | 189 ++----- mdbook/src/11-snake-game/src/control.rs | 70 --- mdbook/src/11-snake-game/src/controls.rs | 23 + mdbook/src/11-snake-game/src/controls/init.rs | 38 ++ .../11-snake-game/src/controls/interrupt.rs | 25 + mdbook/src/11-snake-game/src/display.rs | 40 +- .../11-snake-game/src/display/interrupt.rs | 13 + mdbook/src/11-snake-game/src/display/show.rs | 23 + mdbook/src/11-snake-game/src/game.rs | 233 ++------ mdbook/src/11-snake-game/src/game/coords.rs | 38 ++ mdbook/src/11-snake-game/src/game/movement.rs | 36 ++ mdbook/src/11-snake-game/src/game/rng.rs | 30 ++ mdbook/src/11-snake-game/src/game/snake.rs | 74 +++ mdbook/src/11-snake-game/src/main.rs | 16 +- mdbook/src/11-snake-game/src/main_controls.rs | 54 -- mdbook/src/11-snake-game/src/main_take_1.rs | 48 -- mdbook/src/SUMMARY.md | 1 + 21 files changed, 581 insertions(+), 1176 deletions(-) create mode 100644 mdbook/src/11-snake-game/final-assembly.md delete mode 100644 mdbook/src/11-snake-game/src/control.rs create mode 100644 mdbook/src/11-snake-game/src/controls.rs create mode 100644 mdbook/src/11-snake-game/src/controls/init.rs create mode 100644 mdbook/src/11-snake-game/src/controls/interrupt.rs create mode 100644 mdbook/src/11-snake-game/src/display/interrupt.rs create mode 100644 mdbook/src/11-snake-game/src/display/show.rs create mode 100644 mdbook/src/11-snake-game/src/game/coords.rs create mode 100644 mdbook/src/11-snake-game/src/game/movement.rs create mode 100644 mdbook/src/11-snake-game/src/game/rng.rs create mode 100644 mdbook/src/11-snake-game/src/game/snake.rs delete mode 100644 mdbook/src/11-snake-game/src/main_controls.rs delete mode 100644 mdbook/src/11-snake-game/src/main_take_1.rs diff --git a/mdbook/src/11-snake-game/README.md b/mdbook/src/11-snake-game/README.md index 34674f0..555c7c3 100644 --- a/mdbook/src/11-snake-game/README.md +++ b/mdbook/src/11-snake-game/README.md @@ -1,16 +1,25 @@ # Snake game -We're now going to implement a basic [snake](https://en.wikipedia.org/wiki/Snake_(video_game_genre)) game that you can play on a micro:bit v2 using its 5x5 LED matrix as a -display and its two buttons as controls. In doing so, we will build on some of the concepts covered in the earlier -chapters of this book, and also learn about some new peripherals and concepts. +We're now going to implement a basic [snake](https://en.wikipedia.org/wiki/Snake_(video_game_genre)) +game that you can play on an MB2 using its 5×5 LED matrix as a display and its two buttons as +controls. In doing so, we will build on some of the concepts covered in the earlier chapters of this +book, and also learn about some new peripherals and concepts. -In particular, we will be using the concept of hardware interrupts to allow our program to interact with multiple -peripherals at once. Interrupts are a common way to implement concurrency in embedded contexts. There is a good -introduction to concurrency in an embedded context in the [Embedded Rust Book](https://docs.rust-embedded.org/book/concurrency/index.html) that I suggest you read through -before proceeding. +In particular, we will be using the concept of hardware interrupts to allow our program to interact +with multiple peripherals at once. Interrupts are a common way to implement concurrency in embedded +contexts. There is a good introduction to concurrency in an embedded context [here] that +you might read through before proceeding. -> **NOTE** In this chapter, we are going to use later versions of certain libraries that have been used in previous -> chapters. We are going to use version 0.13.0 of the `microbit` library (the preceding chapters have used 0.12.0). -> Version 0.13.0 fixes a couple of bugs in the non-blocking display code that we will be using. We are also going to use -> version 0.8.0 of the `heapless` library (previous chapters used version 0.7.10), which allows us to use certain of its -> data structures with structs that implement Rust's `core::Hash` trait. +[here]: https://docs.rust-embedded.org/book/concurrency/index.html + +## Modularity + +The source code here is more modular than it probably should be. This fine-grained modularity allows +us to look at the source code a little at a time. We will build the code bottom-up: we will first +build three modules — `game`, `controls` and `display`, and then compose these to build the final +program. Each module will have a top-level source file and one or more included source files: for +example, the `game` module will consist of `src/game.rs`, `src/game/coords.rs`, +`src/game/movement.rs`, etc. The Rust `mod` statement is used to combine the various components of +the module. *The Rust Programming Language* has a good [description] of Rust's module system. + +[description]: https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html diff --git a/mdbook/src/11-snake-game/controls.md b/mdbook/src/11-snake-game/controls.md index 11cf828..c9e6fda 100644 --- a/mdbook/src/11-snake-game/controls.md +++ b/mdbook/src/11-snake-game/controls.md @@ -1,202 +1,87 @@ # Controls -Our protagonist will be controlled by the two buttons on the front of the micro:bit. Button A will turn to the (snake's) -left, and button B will turn to the (snake's) right. +Our protagonist will be controlled by the two buttons on the front of the micro:bit. Button A will +turn to the snake's left, and button B will turn to the snake's right. -We will use the `microbit::pac::interrupt` macro to handle button presses in a concurrent way. The interrupt will be -generated by the micro:bit's GPIOTE (**G**eneral **P**urpose **I**nput/**O**utput **T**asks and **E**vents) peripheral. +We will use the `microbit::pac::interrupt` macro to handle button presses in a concurrent way. The +interrupt will be generated by the MB2's General Purpose Input/Output Tasks and Events (GPIOTE) +peripheral. ## The `controls` module -Code in this section should be placed in a separate file, `controls.rs`, in our `src` directory. +We will need to keep track of two separate pieces of global mutable state: A reference to the +`GPIOTE` peripheral, and a record of the selected direction to turn next. -We will need to keep track of two separate pieces of global mutable state: A reference to the `GPIOTE` peripheral, and a -record of the selected direction to turn next. +Shared data is wrapped in a `RefCell` to permit interior mutability and locking. You can learn more +about `RefCell` by reading the [RefCell documentation] and the [interior mutability chapter] of the +Rust Book]. The `RefCell` is, in turn, wrapped in a `cortex_m::interrupt::Mutex` to allow safe +access. The Mutex provided by the `cortex_m` crate uses the concept of a [critical section]. Data +in a Mutex can only be accessed from within a function or closure passed to +`cortex_m::interrupt:free` (renamed here to `interrupt_free` for clarity), which ensures that the +code in the function or closure cannot itself be interrupted. -```rust -use core::cell::RefCell; -use cortex_m::interrupt::Mutex; -use microbit::hal::gpiote::Gpiote; -use crate::game::Turn; - -// ... - -static GPIO: Mutex>> = Mutex::new(RefCell::new(None)); -static TURN: Mutex> = Mutex::new(RefCell::new(Turn::None)); -``` +[RefCell documentation]: https://doc.rust-lang.org/std/cell/struct.RefCell.html +[interior mutability chapter]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html +[critical section]: https://en.wikipedia.org/wiki/Critical_section -The data is wrapped in a `RefCell` to permit interior mutability. You can learn more about `RefCell` by reading -[its documentation](https://doc.rust-lang.org/std/cell/struct.RefCell.html) and the relevant chapter of [the Rust Book](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). -The `RefCell` is, in turn, wrapped in a `cortex_m::interrupt::Mutex` to allow safe access. -The Mutex provided by the `cortex_m` crate uses the concept of a [critical section](https://en.wikipedia.org/wiki/Critical_section). -Data in a Mutex can only be accessed from within a function or closure passed to `cortex_m::interrupt:free`, which -ensures that the code in the function or closure cannot itself be interrupted. +### Initialization -First, we will initialise the buttons. +First, we will initialise the buttons (`src/controls/init.rs`). ```rust -use cortex_m::interrupt::free; -use microbit::{ - board::Buttons, - pac::{self, GPIOTE} -}; - -// ... - -/// Initialise the buttons and enable interrupts. -pub(crate) fn init_buttons(board_gpiote: GPIOTE, board_buttons: Buttons) { - let gpiote = Gpiote::new(board_gpiote); - - let channel0 = gpiote.channel0(); - channel0 - .input_pin(&board_buttons.button_a.degrade()) - .hi_to_lo() - .enable_interrupt(); - channel0.reset_events(); - - let channel1 = gpiote.channel1(); - channel1 - .input_pin(&board_buttons.button_b.degrade()) - .hi_to_lo() - .enable_interrupt(); - channel1.reset_events(); - - free(move |cs| { - *GPIO.borrow(cs).borrow_mut() = Some(gpiote); - - unsafe { - pac::NVIC::unmask(pac::Interrupt::GPIOTE); - } - pac::NVIC::unpend(pac::Interrupt::GPIOTE); - }); -} +{{#include src/controls/init.rs}} ``` -The `GPIOTE` peripheral on the nRF52 has 8 "channels", each of which can be connected to a `GPIO` pin and configured to -respond to certain events, including rising edge (transition from low to high signal) and falling edge (high to low -signal). A button is a `GPIO` pin which has high signal when not pressed and low signal otherwise. Therefore, a button -press is a falling edge. - -We connect `channel0` to `button_a` and `channel1` to `button_b` and, in each case, tell them to generate events on a -falling edge (`hi_to_lo`). We store a reference to our `GPIOTE` peripheral in the `GPIO` Mutex. We then `unmask` `GPIOTE` -interrupts, allowing them to be propagated by the hardware, and call `unpend` to clear any interrupts with pending -status (which may have been generated prior to the interrupts being unmasked). - -Next, we write the code that handles the interrupt. We use -the `interrupt` macro re-exported from the `nrf52833_hal` -crate. We define a function with the same name as the -interrupt we want to handle (you can see them all [here](https://docs.rs/nrf52833-hal/latest/nrf52833_hal/pac/enum.Interrupt.html)) and annotate it with `#[interrupt]`. +The `GPIOTE` peripheral on the nRF52 has 8 "channels", each of which can be connected to a `GPIO` +pin and configured to respond to certain events, including rising edge (transition from low to high +signal) and falling edge (high to low signal). A button is a `GPIO` pin which has high signal when +not pressed and low signal otherwise. Therefore, a button press is a falling edge. + +Note the awkward use of the function `init_channel()` in initialization to avoid copy-pasting the +button initialization code. The types that the various embedded crates for the MB2 have been hiding +from you are sometimes a bit scary. I would encourage you to explore the type structure of the HAL +and PAC crates at some point, as it is a bit odd and takes getting used to. In particular, note that +each pin on the microbit has *its own unique type.* The purpose of the `degrade()` function in +initialization is to convert these to a common type that can reasonably be used as an argument to +`init_channel()` and thence to `input_pin()`. + +We connect `channel0` to `button_a` and `channel1` to `button_b`. In each case, we set the button up +to generate events on a falling edge (`hi_to_lo`). We store a reference to our `GPIOTE` peripheral +in the `GPIO` Mutex. We then `unmask` `GPIOTE` interrupts, allowing them to be propagated by the +hardware, and call `unpend` to clear any interrupts with pending status (which may have been +generated prior to the interrupts being unmasked). + +### Interrupt handler + +Next, we write the code that handles the interrupt. We use the `interrupt` macro re-exported from +the `nrf52833_hal` crate. We define a function with the same name as the interrupt we want to handle +(you can see them all +[here](https://docs.rs/nrf52833-hal/latest/nrf52833_hal/pac/enum.Interrupt.html)) and annotate it +with `#[interrupt]` (`src/controls/interrupt.rs`). ```rust -use microbit::pac::interrupt; - -// ... - -#[interrupt] -fn GPIOTE() { - free(|cs| { - if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() { - let a_pressed = gpiote.channel0().is_event_triggered(); - let b_pressed = gpiote.channel1().is_event_triggered(); - - let turn = match (a_pressed, b_pressed) { - (true, false) => Turn::Left, - (false, true) => Turn::Right, - _ => Turn::None - }; - - gpiote.channel0().reset_events(); - gpiote.channel1().reset_events(); - - *TURN.borrow(cs).borrow_mut() = turn; - } - }); -} +{{#include src/controls/interrupt.rs}} ``` -When a `GPIOTE` interrupt is generated, we check each button to see whether it has been pressed. If only button A has been -pressed, we record that the snake should turn to the left. If only button B has been pressed, we record that the snake -should turn to the right. In any other case, we record that the snake should not make any turn. The relevant turn is -stored in the `TURN` Mutex. All of this happens within a `free` block, to ensure that we cannot be interrupted again -while handling this interrupt. +When a `GPIOTE` interrupt is generated, we check each button to see whether it has been pressed. If +only button A has been pressed, we record that the snake should turn to the left. If only button B +has been pressed, we record that the snake should turn to the right. In any other case, we record +that the snake should not make any turn. (Having both buttons pressed "at the same time" is +exceedingly unlikely: button presses are noted almost instantly, and this interrupt handler runs +very fast — it would be hard to get both buttons down in time for this to happen. Similarly, it +would be hard to press a button for a short enough time for this code to miss it and report that +neither button is pressed. Still, Rust enforces that you plan for these unexpected cases: the code +will not compile unless you check all the possibilities.) The relevant turn is stored in the `TURN` +Mutex. All of this happens within an `interrupt_free` block, to ensure that we cannot be interrupted +by some other event while handling this interrupt. -Finally, we expose a simple function to get the next turn. +Finally, we expose a simple function to get the next turn (`src/controls.rs`). ```rust -/// Get the next turn (i.e., the turn corresponding to the most recently pressed button). -pub fn get_turn(reset: bool) -> Turn { - free(|cs| { - let turn = *TURN.borrow(cs).borrow(); - if reset { - *TURN.borrow(cs).borrow_mut() = Turn::None - } - turn - }) -} +{{#include src/controls.rs}} ``` -This function simply returns the current value of the `TURN` Mutex. It takes a single boolean argument, `reset`. If -`reset` is `true`, the value of `TURN` is reset, i.e., set to `Turn::None`. - -## Updating the `main` file - -Returning to our `main` function, we need to add a call to `init_buttons` before our main loop, and in the game loop, -replace our placeholder `Turn::None` argument to the `game.step` method with the value returned by `get_turn`. - -```rust -#![no_main] -#![no_std] - -mod game; -mod control; - -use cortex_m_rt::entry; -use microbit::{ - Board, - hal::{prelude::*, Rng, Timer}, - display::blocking::Display -}; -use rtt_target::rtt_init_print; -use panic_rtt_target as _; - -use crate::game::{Game, GameStatus}; -use crate::control::{init_buttons, get_turn}; - -#[entry] -fn main() -> ! { - rtt_init_print!(); - let board = Board::take().unwrap(); - let mut timer = Timer::new(board.TIMER0); - let mut rng = Rng::new(board.RNG); - let mut game = Game::new(rng.random_u32()); - - let mut display = Display::new(board.display_pins); - - init_buttons(board.GPIOTE, board.buttons); - - loop { // Main loop - loop { // Game loop - let image = game.game_matrix(9, 9, 9); - // The brightness values are meaningless at the moment as we haven't yet - // implemented a display capable of displaying different brightnesses - display.show(&mut timer, image, game.step_len_ms()); - match game.status { - GameStatus::Ongoing => game.step(get_turn(true)), - _ => { - for _ in 0..3 { - display.clear(); - timer.delay_ms(200u32); - display.show(&mut timer, image, 200); - } - display.clear(); - display.show(&mut timer, game.score_matrix(), 1000); - break - } - } - } - game.reset(); - } -} -``` +This function simply returns the current value of the `TURN` Mutex. It takes a single boolean +argument, `reset`. If `reset` is `true`, the value of `TURN` is reset, i.e., set to `Turn::None`. -Now we can control the snake using the micro:bit's buttons! +Next we will build support for a high-fidelity game display. diff --git a/mdbook/src/11-snake-game/final-assembly.md b/mdbook/src/11-snake-game/final-assembly.md new file mode 100644 index 0000000..b9a5dc2 --- /dev/null +++ b/mdbook/src/11-snake-game/final-assembly.md @@ -0,0 +1,30 @@ +# Snake game: final assembly + +The code in our `src/main.rs` file brings all the previously-discussed machinery together to make +our final game. + +```rust +{{#include src/main.rs}} +``` + +After initialising the board and its timer and RNG peripherals, we initialise a `Game` struct and a +`Display` from the `microbit::display::blocking` module. + +In our "game loop" (which runs inside of the "main loop" we place in our `main` function), we +repeatedly perform the following steps: + +1. Get a 5×5 array of bytes representing the grid. The `Game::get_matrix` method takes three integer + arguments (which should be between 0 and 9, inclusive) which will, eventually, represent how + brightly the head, tail and food should be displayed. + +2. Display the matrix, for an amount of time determined by the `Game::step_len_ms` method. As + currently implemented, this method basically provides for 1 second between steps, reducing by + 200ms every time the player scores 5 points (eating 1 piece of food = 1 point), subject to a + floor of 200ms. + +3. Check the game status. If it is `Ongoing` (which is its initial value), run a step of the game + and update the game state (including its `status` property). Otherwise, the game is over, so + flash the current image three times, then show the player's score (represented as a number of + illuminated LEDs corresponding to the score), and exit the game loop. + +Our main loop just runs the game loop repeatedly, resetting the game's state after each iteration. diff --git a/mdbook/src/11-snake-game/game-logic.md b/mdbook/src/11-snake-game/game-logic.md index b528c3b..0023321 100644 --- a/mdbook/src/11-snake-game/game-logic.md +++ b/mdbook/src/11-snake-game/game-logic.md @@ -1,478 +1,96 @@ # Game logic -First, we are going to describe the game logic. You are probably familiar with snake games, but if not, the basic idea -is that the player guides a snake around a 2D grid. At any given time, there is some "food" at a random location on the -grid and the goal of the game is to get the snake to "eat" as much food as possible. Each time the snake eats some food -it grows in length. The player loses if the snake crashes into its own tail. In some variants of the game, the player -also loses if the snake crashes into the edge of the grid, but given the small size of our grid we are going to -implement a "wraparound" rule where, if the snake goes off one edge of the grid, it will continue from the opposite -edge. +The first module we will build is the game logic. You are probably familiar with [snake] games, but +if not, the basic idea is that the player guides a snake around a 2D grid. At any given time, there +is some "food" at a random location on the grid and the goal of the game is to get the snake to +"eat" as much food as possible. Each time the snake eats food it grows in length. The player loses +if the snake crashes into its own tail. -## The `game` module - -The code in this section should go in a separate file, `game.rs`, in our `src` directory. +[snake]: https://en.wikipedia.org/wiki/Snake_%28video_game_genre%29 -```rust -use heapless::FnvIndexSet; +In some variants of the game, the player also loses if the snake crashes into the edge of the grid, +but given the small size of our grid we are going to implement a "wraparound" rule: if the snake +goes off one edge of the grid, it will continue from the opposite edge. -/// A single point on the grid. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -struct Coords { - // Signed ints to allow negative values (handy when checking if we have gone - // off the top or left of the grid) - row: i8, - col: i8 -} +## The `game` module -impl Coords { - /// Get random coordinates within a grid. `exclude` is an optional set of - /// coordinates which should be excluded from the output. - fn random( - rng: &mut Prng, // We define the Prng struct below - exclude: Option<&FnvIndexSet> - ) -> 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 - } +We will build up the game mechanics in the `game` module. - /// Whether the point is outside the bounds of the grid. - fn is_out_of_bounds(&self) -> bool { - self.row < 0 || self.row >= 5 || self.col < 0 || self.col >= 5 - } -} -``` +### Coordinates -We use a `Coords` struct to refer to a position on the grid. Because `Coords` only contains two integers, we tell the -compiler to derive an implementation of the `Copy` trait for it, so we can pass around `Coords` structs without having -to worry about ownership. - -We define an associated function, `Coords::random`, which will give us a random position on the grid. We will use this -later to determine where to place the snake's food. To do this, we need a source of random numbers. The nRF52833 has a -random number generator (RNG) peripheral, documented at section 6.19 of the [spec sheet](https://infocenter.nordicsemi.com/pdf/nRF52833_PS_v1.3.pdf). The HAL gives us a simple -interface to the RNG via the `microbit::hal::rng::Rng` struct. However, it is a blocking interface, and the time -needed to generate one random byte of data is variable and unpredictable. We therefore define a [pseudo-random](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) -number generator (PRNG) which uses an [xorshift](https://en.wikipedia.org/wiki/Xorshift) algorithm to generate -pseudo-random `u32` values that we can use to determine where to place food. The algorithm is basic and not -cryptographically secure, but it is efficient, easy to implement and good enough for our humble snake game. Our `Prng` -struct requires an initial seed value, which we get from the RNG peripheral. +We start by defining a coordinate system for our game (`src/game/coords.rs`). ```rust -/// A basic pseudo-random number generator. -struct Prng { - value: u32 -} - -impl Prng { - 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. - fn random_u32(&mut self) -> u32 { - self.value = Self::xorshift32(self.value); - self.value - } -} +{{#include src/game/coords.rs}} ``` -We also need to define a few `enum`s that help us manage the game's state: direction of movement, direction to turn, the -current game status and the outcome of a particular "step" in the game (ie, a single movement of the snake). +We use a `Coords` struct to refer to a position on the grid. Because `Coords` only contains two +integers, we tell the compiler to derive an implementation of the `Copy` trait for it, so we can +pass around `Coords` structs without having to worry about ownership. -```rust -/// Define the directions the snake can move. -enum Direction { - Up, - Down, - Left, - Right -} +### Random Number Generation -/// What direction the snake should turn. -#[derive(Debug, Copy, Clone)] -pub enum Turn { - Left, - Right, - None -} +We define an associated function, `Coords::random`, which will give us a random position on the +grid. We will use this later to determine where to place the snake's food. -/// The current status of the game. -pub enum GameStatus { - Won, - Lost, - Ongoing -} +To generate random coordinates, we need a source of random numbers. The nRF52833 has a hardware +random number generator (HWRNG) peripheral, documented at section 6.19 of the [nRF52833 spec]. The +HAL gives us a simple interface to the HWRNG via the `microbit::hal::rng::Rng` struct. The HWRNG may +not be fast enough for a game; it is also convenient for testing to be able to replicate the +sequence of random numbers produced by the generator between runs, which is impossible for the HWRNG +by design. We thus also define a [pseudo-random] number generator (PRNG). The PRNG uses an +[xorshift] algorithm to generate pseudo-random `u32` values. The algorithm is basic and not +cryptographically secure, but it is efficient, easy to implement and good enough for our humble +snake game. Our `Prng` struct requires an initial seed value, which we do get from the RNG +peripheral. -/// The outcome of a single move/step. -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) -} -``` +[nRF52833 spec]: https://infocenter.nordicsemi.com/pdf/nRF52833_PS_v1.3.pdf +[pseudo-random]: https://en.wikipedia.org/wiki/Pseudorandom_number_generator +[xorshift]: https://en.wikipedia.org/wiki/Xorshift -Next up we define a `Snake` struct, which keeps track of the coordinates occupied by the snake and its direction of -travel. We use a queue (`heapless::spsc::Queue`) to keep track of the order of coordinates and a hash set -(`heapless::FnvIndexSet`) to allow for quick collision detection. The `Snake` has methods to allow it to move. +All of this makes up `src/game/rng.rs`. ```rust -use heapless::spsc::Queue; - -// ... - -struct Snake { - /// Coordinates of the snake's head. - head: Coords, - /// Queue of coordinates of the rest of the snake's body. The end of the tail is - /// at the front. - tail: Queue, - /// A set containing all coordinates currently occupied by the snake (for fast - /// collision checking). - coord_set: FnvIndexSet, - /// The direction the snake is currently moving in. - direction: Direction -} - -impl Snake { - fn new() -> 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 = 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. - 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 - } - } - - fn turn(&mut self, direction: Turn) { - match direction { - Turn::Left => self.turn_left(), - Turn::Right => self.turn_right(), - Turn::None => () - } - } -} +{{#include src/game/rng.rs}} ``` -The `Game` struct keeps track of the game state. It holds a `Snake` object, the current coordinates of the food, the -speed of the game (which is used to determine the time that elapses between each movement of the snake), the status of -the game (whether the game is ongoing or the player has won or lost) and the player's score. +### Movement -This struct contains methods to handle each step of the game, determining the snake's next move and updating the game -state accordingly. It also contains two methods--`game_matrix` and `score_matrix`--that output 2D arrays of values -which can be used to display the game state or the player score on the LED matrix (as we will see later). +We also need to define a few `enum`s that help us manage the game's state: direction of movement, +direction to turn, the current game status and the outcome of a particular "step" in the game (ie, a +single movement of the snake). `src/game/movement.rs` contains these. ```rust -/// Struct to hold game state and associated behaviour -pub(crate) struct Game { - rng: Prng, - snake: Snake, - food_coords: Coords, - speed: u8, - pub(crate) status: GameStatus, - score: u8 -} - -impl Game { - pub(crate) fn new(rng_seed: u32) -> Self { - let mut rng = Prng::new(rng_seed); - let mut tail: FnvIndexSet = FnvIndexSet::new(); - tail.insert(Coords { row: 2, col: 1 }).unwrap(); - let snake = Snake::new(); - 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(crate) fn reset(&mut self) { - self.snake = Snake::new(); - 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 % 5 == 0 { - self.speed += 1 - } - GameStatus::Ongoing - }, - StepOutcome::Move(c) => { - self.snake.move_snake(c, false); - GameStatus::Ongoing - } - } - } - - pub(crate) 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(crate) 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(crate) 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(crate) fn score_matrix(&self) -> [[u8; 5]; 5] { - let mut values = [[0u8; 5]; 5]; - let full_rows = (self.score as usize) / 5; - for r in 0..full_rows { - values[r] = [1; 5]; - } - for c in 0..(self.score as usize) % 5 { - values[full_rows][c] = 1; - } - values - } -} +{{#include src/game/movement.rs}} ``` -## The `main` file +### A Snake (*A Snaaake!*) -The following code should be placed in our `main.rs` file. +Next up we define a `Snake` struct, which keeps track of the coordinates occupied by the snake and +its direction of travel. We use a queue (`heapless::spsc::Queue`) to keep track of the order of +coordinates and a hash set (`heapless::FnvIndexSet`) to allow for quick collision detection. The +`Snake` has methods to allow it to move. `src/game/snake.rs` gets this. ```rust -#![no_main] -#![no_std] - -mod game; - -use cortex_m_rt::entry; -use microbit::{ - Board, - hal::{prelude::*, Rng, Timer}, - display::blocking::Display -}; -use rtt_target::rtt_init_print; -use panic_rtt_target as _; -use crate::game::{Game, GameStatus, Turn}; - -#[entry] -fn main() -> ! { - rtt_init_print!(); - let board = Board::take().unwrap(); - let mut timer = Timer::new(board.TIMER0); - let mut rng = Rng::new(board.RNG); - let mut game = Game::new(rng.random_u32()); - let mut display = Display::new(board.display_pins); - - loop { - loop { // Game loop - let image = game.game_matrix(9, 9, 9); - // The brightness values are meaningless at the moment as we haven't yet - // implemented a display capable of displaying different brightnesses - display.show(&mut timer, image, game.step_len_ms()); - match game.status { - GameStatus::Ongoing => game.step(Turn::None), // Placeholder as we - // haven't implemented - // controls yet - _ => { - for _ in 0..3 { - display.clear(); - timer.delay_ms(200u32); - display.show(&mut timer, image, 200); - } - display.clear(); - display.show(&mut timer, game.score_matrix(), 1000); - break - } - } - } - game.reset(); - } -} +{{#include src/game/snake.rs}} ``` -After initialising the board and its timer and RNG peripherals, we initialise a `Game` struct and a `Display` from the -`microbit::display::blocking` module. +### Game Module Top-Level -In our "game loop" (which runs inside of the "main loop" we place in our `main` function), we repeatedly perform the -following steps: +The `Game` struct keeps track of the game state. It holds a `Snake` object, the current coordinates +of the food, the speed of the game (which is used to determine the time that elapses between each +movement of the snake), the status of the game (whether the game is ongoing or the player has won or +lost) and the player's score. -1. Get a 5x5 array of bytes representing the grid. The `Game::get_matrix` method takes three integer arguments (which - should be between 0 and 9, inclusive) which will, eventually, represent how brightly the head, tail and food should be - displayed. The basic `Display` we are using at this point does not support variable brightness, so we just provide - values of 9 for each (but any non-zero value would work) at this stage. -2. Display the matrix, for an amount of time determined by the `Game::step_len_ms` method. As currently implemented, - this method basically provides for 1 second between steps, reducing by 200ms every time the player scores 5 points - (eating 1 piece of food = 1 point), subject to a floor of 200ms. -3. Check the game status. If it is `Ongoing` (which is its initial value), run a step of the game and update the game - state (including its `status` property). Otherwise, the game is over, so flash the current image three times, then - show the player's score (represented as a number of illuminated LEDs corresponding to the score), and exit the game - loop. +This struct contains methods to handle each step of the game, determining the snake's next move and +updating the game state accordingly. It also contains two methods--`game_matrix` and +`score_matrix`--that output 2D arrays of values which can be used to display the game state or the +player score on the LED matrix (as we will see later). -Our main loop just runs the game loop repeatedly, resetting the game's state after each iteration. +We put the `Game` struct at the top of the `game` module, in `src/game.rs`. -If you run this, you should see two LEDs illuminated halfway down the display (the snake's head in the middle and its -tail to the left). You will also see another LED illuminated somewhere on the board, representing the snake's food. -Approximately each second, the snake will move one space to the right. +```rust +{{#include src/game.rs}} +``` -Next we will add an ability to control the snake's movements. +Next we will add the ability to control the snake's movements. diff --git a/mdbook/src/11-snake-game/nonblocking-display.md b/mdbook/src/11-snake-game/nonblocking-display.md index d74880e..68603b8 100644 --- a/mdbook/src/11-snake-game/nonblocking-display.md +++ b/mdbook/src/11-snake-game/nonblocking-display.md @@ -1,157 +1,68 @@ # Using the non-blocking display -We now have a basic functioning snake game. But you might find that when the snake gets a bit longer, it can be -difficult to tell the snake from the food, and to tell which direction the snake is heading, because all LEDs are the -same brightness. Let's fix that. - -The `microbit` library makes available two different interfaces to the LED matrix: a basic, blocking interface, which -we have been using, and a non-blocking interface which allows you to customise the brightness of each LED. At the -hardware level, each LED is either "on" or "off", but the `microbit::display::nonblocking` module simulates ten levels -of brightness for each LED by rapidly switching the LED on and off. - -The code to interact with the non-blocking interface is pretty simple and will follow a similar structure to the code we -used to interact with the buttons. +We will next display the snake and food on the LEDs of the MB2 screen. So far, we have used the +blocking interface, which provides for LEDs to be either maximally bright or turned off. With this, +a basic functioning snake game would be possible. But you might find that when the snake got a bit +longer, it would be difficult to tell the snake from the food, and to tell which direction the snake +was heading. Let's figure out how to allow the LED brightness to vary: we can make the snake's body +a bit dimmer, which will help sort out the clutter. + +The `microbit` library makes available two different interfaces to the LED matrix. There is the +blocking interface we've already seen in previous chapters. There is also a non-blocking interface +which allows you to customise the brightness of each LED. At the hardware level, each LED is either +"on" or "off", but the `microbit::display::nonblocking` module simulates ten levels of brightness +for each LED by rapidly switching the LED on and off. + +(There is no great reason the two display modes of the `microbit` library crate have to be separate +and use separate code. A more complete design would allow either non-blocking or blocking use of a +single display API with variable brightness levels and refresh rates specified by the user. Never +assume that the stuff you have been handed is perfected, or even close. Always think about what you +might do differently. For now, though, we'll work with what we have, which is adequate for our +immediate purpose.) + +The code to interact with the non-blocking interface (`src/display.rs`) is pretty simple and will +follow a similar structure to the code we used to interact with the buttons. This time we'll start +at the top level. + +## Display module ```rust -use core::cell::RefCell; -use cortex_m::interrupt::{free, Mutex}; -use microbit::display::nonblocking::Display; -use microbit::gpio::DisplayPins; -use microbit::pac; -use microbit::pac::TIMER1; - -static DISPLAY: Mutex>>> = Mutex::new(RefCell::new(None)); - -pub(crate) fn init_display(board_timer: TIMER1, board_display: DisplayPins) { - let display = Display::new(board_timer, board_display); - - free(move |cs| { - *DISPLAY.borrow(cs).borrow_mut() = Some(display); - }); - unsafe { - pac::NVIC::unmask(pac::Interrupt::TIMER1) - } -} +{{#include src/display.rs}} ``` -First, we initialise a `microbit::display::nonblocking::Display` struct representing the LED display, passing it the -board's `TIMER1` and `DisplayPins` peripherals. Then we store the display in a Mutex. Finally, we unmask the `TIMER1` -interrupt. +First, we initialise a `microbit::display::nonblocking::Display` struct representing the LED +display, passing it the board's `TIMER1` and `DisplayPins` peripherals. Then we store the display in +a Mutex. Finally, we unmask the `TIMER1` interrupt. + +## Display API -We then define a couple of convenience functions which allow us to easily set (or unset) the image to be displayed. +We then define a couple of convenience functions which allow us to easily set (or unset) the image +to be displayed (`src/display/show.rs`). ```rust -use tiny_led_matrix::Render; - -// ... - -/// Display an image. -pub(crate) fn display_image(image: &impl Render) { - free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.show(image); - } - }) -} - -/// Clear the display (turn off all LEDs). -pub(crate) fn clear_display() { - free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.clear(); - } - }) -} +{{#include src/display/show.rs}} ``` -`display_image` takes an image and tells the display to show it. Like the `Display::show` method that it calls, this -function takes a struct that implements the `tiny_led_matrix::Render` trait. That trait ensures that the struct contains -the data and methods necessary for the `Display` to render it on the LED matrix. The two implementations of `Render` -provided by the `microbit::display::nonblocking` module are `BitImage` and `GreyscaleImage`. In a `BitImage`, each -"pixel" (or LED) is either illuminated or not (like when we used the blocking interface), whereas in a -`GreyscaleImage` each "pixel" can have a different brightness. +`display_image` takes an image and tells the display to show it. Like the `Display::show` method +that it calls, this function takes a struct that implements the `tiny_led_matrix::Render` +trait. That trait ensures that the struct contains the data and methods necessary for the `Display` +to render it on the LED matrix. The two implementations of `Render` provided by the +`microbit::display::nonblocking` module are `BitImage` and `GreyscaleImage`. In a `BitImage`, each +"pixel" (or LED) is either illuminated or not (like when we used the blocking interface), whereas in +a `GreyscaleImage` each "pixel" can have a different brightness. `clear_display` does exactly as the name suggests. -Finally, we use the `interrupt` macro to define a handler for the `TIMER1` interrupt. This interrupt fires many times a -second, and this is what allows the `Display` to rapidly cycle the different LEDs on and off to give the illusion of -varying brightness levels. All our handler code does is call the `Display::handle_display_event` method, which handles -this. +## Display interrupt handling + +Finally, we use the `interrupt` macro to define a handler for the `TIMER1` interrupt. This interrupt +fires many times a second, and this is what allows the `Display` to rapidly cycle the different LEDs +on and off to give the illusion of varying brightness levels. All our handler code does is call the +`Display::handle_display_event` method, which handles this (`src/display/interrupt.rs`). ```rust -use microbit::pac::interrupt; - -// ... - -#[interrupt] -fn TIMER1() { - free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.handle_display_event(); - } - }) -} +{{#include src/display/interrupt.rs}} ``` -Now we just need to update our `main` function to call `init_display` and use the new functions we have defined to -interact with our fancy new display. - -```rust -#![no_main] -#![no_std] - -mod game; -mod control; -mod display; - -use cortex_m_rt::entry; -use microbit::{ - Board, - hal::{prelude::*, Rng, Timer}, - display::nonblocking::{BitImage, GreyscaleImage} -}; -use rtt_target::rtt_init_print; -use panic_rtt_target as _; - -use crate::control::{get_turn, init_buttons}; -use crate::display::{clear_display, display_image, init_display}; -use crate::game::{Game, GameStatus}; - - -#[entry] -fn main() -> ! { - rtt_init_print!(); - let mut board = Board::take().unwrap(); - let mut timer = Timer::new(board.TIMER0).into_periodic(); - let mut rng = Rng::new(board.RNG); - let mut game = Game::new(rng.random_u32()); - - init_buttons(board.GPIOTE, board.buttons); - init_display(board.TIMER1, board.display_pins); - - - loop { - loop { // Game loop - let image = GreyscaleImage::new(&game.game_matrix(6, 3, 9)); - display_image(&image); - timer.delay_ms(game.step_len_ms()); - match game.status { - GameStatus::Ongoing => game.step(get_turn(true)), - _ => { - for _ in 0..3 { - clear_display(); - timer.delay_ms(200u32); - display_image(&image); - timer.delay_ms(200u32); - } - clear_display(); - display_image(&BitImage::new(&game.score_matrix())); - timer.delay_ms(2000u32); - break - } - } - } - game.reset(); - } -} -``` \ No newline at end of file +Now we can understand how our `main` function will do display: we will call `init_display` and use +the new functions we have defined to interact with it. diff --git a/mdbook/src/11-snake-game/src/control.rs b/mdbook/src/11-snake-game/src/control.rs deleted file mode 100644 index 7e1cf69..0000000 --- a/mdbook/src/11-snake-game/src/control.rs +++ /dev/null @@ -1,70 +0,0 @@ -use core::cell::RefCell; -use cortex_m::interrupt::{free, Mutex}; -use microbit::{ - board::Buttons, - hal::gpiote::Gpiote, - pac::{self, interrupt} -}; -use crate::game::Turn; -static GPIO: Mutex>> = Mutex::new(RefCell::new(None)); -static TURN: Mutex> = Mutex::new(RefCell::new(Turn::None)); - -/// Initialise the buttons and enable interrupts. -pub(crate) fn init_buttons(board_gpiote: pac::GPIOTE, board_buttons: Buttons) { - let gpiote = Gpiote::new(board_gpiote); - - let channel0 = gpiote.channel0(); - channel0 - .input_pin(&board_buttons.button_a.degrade()) - .hi_to_lo() - .enable_interrupt(); - channel0.reset_events(); - - let channel1 = gpiote.channel1(); - channel1 - .input_pin(&board_buttons.button_b.degrade()) - .hi_to_lo() - .enable_interrupt(); - channel1.reset_events(); - - free(move |cs| { - *GPIO.borrow(cs).borrow_mut() = Some(gpiote); - - unsafe { - pac::NVIC::unmask(pac::Interrupt::GPIOTE); - } - pac::NVIC::unpend(pac::Interrupt::GPIOTE); - }); -} - -/// Get the next turn (ie, the turn corresponding to the most recently pressed button). -pub fn get_turn(reset: bool) -> Turn { - free(|cs| { - let turn = *TURN.borrow(cs).borrow(); - if reset { - *TURN.borrow(cs).borrow_mut() = Turn::None - } - turn - }) -} - -#[pac::interrupt] -fn GPIOTE() { - free(|cs| { - if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() { - let a_pressed = gpiote.channel0().is_event_triggered(); - let b_pressed = gpiote.channel1().is_event_triggered(); - - let turn = match (a_pressed, b_pressed) { - (true, false) => Turn::Left, - (false, true) => Turn::Right, - _ => Turn::None - }; - - gpiote.channel0().reset_events(); - gpiote.channel1().reset_events(); - - *TURN.borrow(cs).borrow_mut() = turn; - } - }); -} \ No newline at end of file diff --git a/mdbook/src/11-snake-game/src/controls.rs b/mdbook/src/11-snake-game/src/controls.rs new file mode 100644 index 0000000..8f2fca5 --- /dev/null +++ b/mdbook/src/11-snake-game/src/controls.rs @@ -0,0 +1,23 @@ +mod init; +mod interrupt; + +pub use init::init_buttons; + +use core::cell::RefCell; +use cortex_m::interrupt::{free as interrupt_free, Mutex}; +use microbit::{board::Buttons, hal::gpiote::Gpiote}; +use crate::game::Turn; +pub static GPIO: Mutex>> = Mutex::new(RefCell::new(None)); +pub static TURN: Mutex> = Mutex::new(RefCell::new(Turn::None)); + +/// Get the next turn (ie, the turn corresponding to the most recently pressed button). +pub fn get_turn(reset: bool) -> Turn { + interrupt_free(|cs| { + let turn = *TURN.borrow(cs).borrow(); + if reset { + *TURN.borrow(cs).borrow_mut() = Turn::None + } + turn + }) +} + diff --git a/mdbook/src/11-snake-game/src/controls/init.rs b/mdbook/src/11-snake-game/src/controls/init.rs new file mode 100644 index 0000000..500844b --- /dev/null +++ b/mdbook/src/11-snake-game/src/controls/init.rs @@ -0,0 +1,38 @@ +use super::{Buttons, GPIO}; + +use cortex_m::interrupt::free as interrupt_free; +use microbit::{ + hal::{ + gpio::{Pin, Input, Floating}, + gpiote::{Gpiote, GpioteChannel}, + }, + pac, +}; + +/// Initialise the buttons and enable interrupts. +pub fn init_buttons(board_gpiote: pac::GPIOTE, board_buttons: Buttons) { + let gpiote = Gpiote::new(board_gpiote); + + fn init_channel(channel: &GpioteChannel<'_>, button: &Pin>) { + channel + .input_pin(button) + .hi_to_lo() + .enable_interrupt(); + channel.reset_events(); + } + + let channel0 = gpiote.channel0(); + init_channel(&channel0, &board_buttons.button_a.degrade()); + + let channel1 = gpiote.channel1(); + init_channel(&channel1, &board_buttons.button_b.degrade()); + + interrupt_free(move |cs| { + *GPIO.borrow(cs).borrow_mut() = Some(gpiote); + + unsafe { + pac::NVIC::unmask(pac::Interrupt::GPIOTE); + } + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + }); +} diff --git a/mdbook/src/11-snake-game/src/controls/interrupt.rs b/mdbook/src/11-snake-game/src/controls/interrupt.rs new file mode 100644 index 0000000..71c9398 --- /dev/null +++ b/mdbook/src/11-snake-game/src/controls/interrupt.rs @@ -0,0 +1,25 @@ +use super::{GPIO, TURN, Turn}; + +use cortex_m::interrupt::free as interrupt_free; +use microbit::pac::{self, interrupt}; + +#[pac::interrupt] +fn GPIOTE() { + interrupt_free(|cs| { + if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() { + let a_pressed = gpiote.channel0().is_event_triggered(); + let b_pressed = gpiote.channel1().is_event_triggered(); + + let turn = match (a_pressed, b_pressed) { + (true, false) => Turn::Left, + (false, true) => Turn::Right, + _ => Turn::None + }; + + gpiote.channel0().reset_events(); + gpiote.channel1().reset_events(); + + *TURN.borrow(cs).borrow_mut() = turn; + } + }); +} diff --git a/mdbook/src/11-snake-game/src/display.rs b/mdbook/src/11-snake-game/src/display.rs index a608f7f..6cebbb0 100644 --- a/mdbook/src/11-snake-game/src/display.rs +++ b/mdbook/src/11-snake-game/src/display.rs @@ -1,17 +1,21 @@ +pub mod interrupt; +pub mod show; + +pub use show::{display_image, clear_display}; + use core::cell::RefCell; -use cortex_m::interrupt::{free, Mutex}; +use cortex_m::interrupt::{free as interrupt_free, Mutex}; use microbit::display::nonblocking::Display; use microbit::gpio::DisplayPins; use microbit::pac; -use microbit::pac::{interrupt, TIMER1}; -use tiny_led_matrix::Render; +use microbit::pac::TIMER1; static DISPLAY: Mutex>>> = Mutex::new(RefCell::new(None)); -pub(crate) fn init_display(board_timer: TIMER1, board_display: DisplayPins) { +pub fn init_display(board_timer: TIMER1, board_display: DisplayPins) { let display = Display::new(board_timer, board_display); - free(move |cs| { + interrupt_free(move |cs| { *DISPLAY.borrow(cs).borrow_mut() = Some(display); }); unsafe { @@ -19,29 +23,3 @@ pub(crate) fn init_display(board_timer: TIMER1, board_display: DisplayPins) { } } -/// Display an image. -pub(crate) fn display_image(image: &impl Render) { - free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.show(image); - } - }) -} - -/// Clear the display (turn off all LEDs). -pub(crate) fn clear_display() { - free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.clear(); - } - }) -} - -#[interrupt] -fn TIMER1() { - free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.handle_display_event(); - } - }) -} \ No newline at end of file diff --git a/mdbook/src/11-snake-game/src/display/interrupt.rs b/mdbook/src/11-snake-game/src/display/interrupt.rs new file mode 100644 index 0000000..2acebd0 --- /dev/null +++ b/mdbook/src/11-snake-game/src/display/interrupt.rs @@ -0,0 +1,13 @@ +use super::DISPLAY; + +use cortex_m::interrupt::free as interrupt_free; +use microbit::pac::{self, interrupt}; + +#[pac::interrupt] +fn TIMER1() { + interrupt_free(|cs| { + if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { + display.handle_display_event(); + } + }) +} diff --git a/mdbook/src/11-snake-game/src/display/show.rs b/mdbook/src/11-snake-game/src/display/show.rs new file mode 100644 index 0000000..e929be5 --- /dev/null +++ b/mdbook/src/11-snake-game/src/display/show.rs @@ -0,0 +1,23 @@ +use super::DISPLAY; + +use cortex_m::interrupt::free as interrupt_free; + +use tiny_led_matrix::Render; + +/// Display an image. +pub fn display_image(image: &impl Render) { + interrupt_free(|cs| { + if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { + display.show(image); + } + }) +} + +/// Clear the display (turn off all LEDs). +pub fn clear_display() { + interrupt_free(|cs| { + if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { + display.clear(); + } + }) +} diff --git a/mdbook/src/11-snake-game/src/game.rs b/mdbook/src/11-snake-game/src/game.rs index cbc892d..4ab030c 100644 --- a/mdbook/src/11-snake-game/src/game.rs +++ b/mdbook/src/11-snake-game/src/game.rs @@ -1,189 +1,33 @@ -// Imports we will use later on -use heapless::FnvIndexSet; -use heapless::spsc::Queue; - -/// A basic pseudo-random number generator. -struct Prng { - value: u32 -} - -impl Prng { - 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. - fn random_u32(&mut self) -> u32 { - self.value = Self::xorshift32(self.value); - self.value - } -} - -/// A single point on the grid. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -struct Coords { - // Signed ints to allow negative values (handy when checking if we have gone off the top or left - // of the grid) - row: i8, - col: i8 -} +mod coords; +mod movement; +mod rng; +mod snake; -impl Coords { - /// Get random coordinates within a grid. `exclude` is an optional set of coordinates which - /// should be excluded from the output. - fn random( - rng: &mut Prng, - exclude: Option<&FnvIndexSet> - ) -> 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. - fn is_out_of_bounds(&self) -> bool { - self.row < 0 || self.row >= 5 || self.col < 0 || self.col >= 5 - } -} - -/// Define the directions the snake can move. -enum Direction { - Up, - Down, - Left, - Right -} +use crate::Rng; -/// What direction the snake should turn. -#[derive(Debug, Copy, Clone)] -pub enum Turn { - Left, - Right, - None -} +pub use coords::Coords; +pub use movement::{Direction, GameStatus, StepOutcome, Turn}; +pub use rng::Prng; +pub use snake::Snake; -/// The current status of the game. -pub enum GameStatus { - Won, - Lost, - Ongoing -} - -/// The outcome of a single move/step. -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) -} - -struct Snake { - /// Coordinates of the snake's head. - head: Coords, - /// Queue of coordinates of the rest of the snake's body. The end of the tail is at the front. - tail: Queue, - /// A set containing all coordinates currently occupied by the snake (for fast collision - /// checking). - coord_set: FnvIndexSet, - /// The direction the snake is currently moving in. - direction: Direction -} - -impl Snake { - fn new() -> 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 = 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 given coordinates. If `extend` is false, the snake's tail vacates - /// the rearmost tile. - 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 - } - } - - fn turn(&mut self, direction: Turn) { - match direction { - Turn::Left => self.turn_left(), - Turn::Right => self.turn_right(), - Turn::None => () - } - } -} +use heapless::FnvIndexSet; /// Struct to hold game state and associated behaviour -pub(crate) struct Game { +pub struct Game { + pub status: GameStatus, rng: Prng, snake: Snake, food_coords: Coords, speed: u8, - pub(crate) status: GameStatus, score: u8 } impl Game { - - pub(crate) fn new(rng_seed: u32) -> Self { - let mut rng = Prng::new(rng_seed); + pub fn new(rng: &mut Rng) -> Self { + let mut rng = Prng::seeded(rng); let mut tail: FnvIndexSet = FnvIndexSet::new(); tail.insert(Coords { row: 2, col: 1 }).unwrap(); - let snake = Snake::new(); + let snake = Snake::make_snake(); let food_coords = Coords::random(&mut rng, Some(&snake.coord_set)); Self { rng, @@ -196,8 +40,8 @@ impl Game { } /// Reset the game state to start a new game. - pub(crate) fn reset(&mut self) { - self.snake = Snake::new(); + pub fn reset(&mut self) { + self.snake = Snake::make_snake(); self.place_food(); self.speed = 1; self.status = GameStatus::Ongoing; @@ -211,9 +55,9 @@ impl Game { 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. + /// "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 } @@ -226,7 +70,8 @@ impl Game { } } - /// Determine the next tile that the snake will move on to (without actually moving the snake). + /// 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 { @@ -242,13 +87,14 @@ impl Game { } } - /// Assess the snake's next move and return the outcome. Doesn't actually update the game state. + /// 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) + // 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 { @@ -286,16 +132,16 @@ impl Game { } } - pub(crate) fn step(&mut self, turn: Turn) { + 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(crate) fn step_len_ms(&self) -> u32 { + /// 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 @@ -304,9 +150,10 @@ impl Game { } } - /// 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(crate) fn game_matrix( + /// 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, @@ -321,10 +168,10 @@ impl Game { 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(crate) fn score_matrix(&self) -> [[u8; 5]; 5] { + /// 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)] diff --git a/mdbook/src/11-snake-game/src/game/coords.rs b/mdbook/src/11-snake-game/src/game/coords.rs new file mode 100644 index 0000000..74b7127 --- /dev/null +++ b/mdbook/src/11-snake-game/src/game/coords.rs @@ -0,0 +1,38 @@ +use super::Prng; + +use heapless::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> + ) -> 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 + } +} diff --git a/mdbook/src/11-snake-game/src/game/movement.rs b/mdbook/src/11-snake-game/src/game/movement.rs new file mode 100644 index 0000000..e109a2d --- /dev/null +++ b/mdbook/src/11-snake-game/src/game/movement.rs @@ -0,0 +1,36 @@ +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) +} diff --git a/mdbook/src/11-snake-game/src/game/rng.rs b/mdbook/src/11-snake-game/src/game/rng.rs new file mode 100644 index 0000000..3671787 --- /dev/null +++ b/mdbook/src/11-snake-game/src/game/rng.rs @@ -0,0 +1,30 @@ +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 + } +} diff --git a/mdbook/src/11-snake-game/src/game/snake.rs b/mdbook/src/11-snake-game/src/game/snake.rs new file mode 100644 index 0000000..0e90dc9 --- /dev/null +++ b/mdbook/src/11-snake-game/src/game/snake.rs @@ -0,0 +1,74 @@ +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, + /// A set containing all coordinates currently occupied by the snake (for fast + /// collision checking). + pub coord_set: FnvIndexSet, + /// 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 = 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 => () + } + } +} diff --git a/mdbook/src/11-snake-game/src/main.rs b/mdbook/src/11-snake-game/src/main.rs index ab48a21..94b470a 100644 --- a/mdbook/src/11-snake-game/src/main.rs +++ b/mdbook/src/11-snake-game/src/main.rs @@ -1,10 +1,14 @@ #![no_main] #![no_std] -mod game; -mod control; +pub mod game; +mod controls; mod display; +use controls::{get_turn, init_buttons}; +use display::{clear_display, display_image, init_display}; +use game::{Game, GameStatus}; + use cortex_m_rt::entry; use embedded_hal::delay::DelayNs; use microbit::{ @@ -15,23 +19,17 @@ use microbit::{ use rtt_target::rtt_init_print; use panic_rtt_target as _; -use crate::control::{get_turn, init_buttons}; -use crate::display::{clear_display, display_image, init_display}; -use crate::game::{Game, GameStatus}; - - #[entry] fn main() -> ! { rtt_init_print!(); let board = Board::take().unwrap(); let mut timer = Timer::new(board.TIMER0).into_periodic(); let mut rng = Rng::new(board.RNG); - let mut game = Game::new(rng.random_u32()); + let mut game = Game::new(&mut rng); init_buttons(board.GPIOTE, board.buttons); init_display(board.TIMER1, board.display_pins); - loop { loop { // Game loop let image = GreyscaleImage::new(&game.game_matrix(6, 3, 9)); diff --git a/mdbook/src/11-snake-game/src/main_controls.rs b/mdbook/src/11-snake-game/src/main_controls.rs deleted file mode 100644 index 8fefcaf..0000000 --- a/mdbook/src/11-snake-game/src/main_controls.rs +++ /dev/null @@ -1,54 +0,0 @@ -#![no_main] -#![no_std] - -mod game; -mod control; - -use cortex_m_rt::entry; -use embedded_hal::delay::DelayNs; -use microbit::{ - Board, - hal::{prelude::*, Rng, Timer}, - display::blocking::Display -}; -use rtt_target::rtt_init_print; -use panic_rtt_target as _; - -use crate::game::{Game, GameStatus}; -use crate::control::{init_buttons, get_turn}; - -#[entry] -fn main() -> ! { - rtt_init_print!(); - let mut board = Board::take().unwrap(); - let mut timer = Timer::new(board.TIMER0); - let mut rng = Rng::new(board.RNG); - let mut game = Game::new(rng.random_u32()); - - let mut display = Display::new(board.display_pins); - - init_buttons(board.GPIOTE, board.buttons); - - loop { - loop { // Game loop - let image = game.game_matrix(9, 9, 9); - // The brightness values are meaningless at the moment as we haven't yet implemented a display capable of - // displaying different brightnesses - display.show(&mut timer, image, game.step_len_ms()); - match game.status { - GameStatus::Ongoing => game.step(get_turn(true)), - _ => { - for _ in 0..3 { - display.clear(); - timer.delay_ms(200u32); - display.show(&mut timer, image, 200); - } - display.clear(); - display.show(&mut timer, game.score_matrix(), 1000); - break - } - } - } - game.reset(); - } -} diff --git a/mdbook/src/11-snake-game/src/main_take_1.rs b/mdbook/src/11-snake-game/src/main_take_1.rs deleted file mode 100644 index d23d876..0000000 --- a/mdbook/src/11-snake-game/src/main_take_1.rs +++ /dev/null @@ -1,48 +0,0 @@ -#![no_main] -#![no_std] - -mod game; - -use cortex_m_rt::entry; -use embedded_hal::delay::DelayNs; -use microbit::{ - Board, - hal::{prelude::*, Rng, Timer}, - display::blocking::Display -}; -use rtt_target::rtt_init_print; -use panic_rtt_target as _; -use crate::game::{Game, GameStatus, Turn}; - -#[entry] -fn main() -> ! { - rtt_init_print!(); - let mut board = Board::take().unwrap(); - let mut timer = Timer::new(board.TIMER0); - let mut rng = Rng::new(board.RNG); - let mut game = Game::new(rng.random_u32()); - let mut display = Display::new(board.display_pins); - - loop { - loop { // Game loop - let image = game.game_matrix(9, 9, 9); - // The brightness values are meaningless at the moment as we haven't yet implemented a display capable of - // displaying different brightnesses - display.show(&mut timer, image, game.step_len_ms()); - match game.status { - GameStatus::Ongoing => game.step(Turn::None), // Placeholder as we haven't implemented controls yet - _ => { - for _ in 0..3 { - display.clear(); - timer.delay_ms(200u32); - display.show(&mut timer, image, 200); - } - display.clear(); - display.show(&mut timer, game.score_matrix(), 1000); - break - } - } - } - game.reset(); - } -} diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 1bb2598..4c83dc6 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -48,6 +48,7 @@ - [Game logic](11-snake-game/game-logic.md) - [Controls](11-snake-game/controls.md) - [Non-blocking display](11-snake-game/nonblocking-display.md) + - [Final assembly](11-snake-game/final-assembly.md) - [What's left for you to explore](explore.md) ---