From 19ce383be15192754e8ce98441b31b7a9884e652 Mon Sep 17 00:00:00 2001 From: ZacJW Date: Sun, 13 Jul 2025 21:27:21 +0100 Subject: [PATCH] Added snake example --- Cargo.lock | 12 ++ examples/snake/Cargo.toml | 26 ++++ examples/snake/build.rs | 52 +++++++ examples/snake/readme.md | 11 ++ examples/snake/src/main.rs | 269 +++++++++++++++++++++++++++++++++++++ 5 files changed, 370 insertions(+) create mode 100644 examples/snake/Cargo.toml create mode 100644 examples/snake/build.rs create mode 100644 examples/snake/readme.md create mode 100644 examples/snake/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index b034a16..eba6097 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1106,6 +1106,18 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "snake" +version = "0.1.0" +dependencies = [ + "cardputer-bsc-nostd", + "embedded-graphics", + "esp-bootloader-esp-idf", + "esp-hal", + "esp-println", + "heapless", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/examples/snake/Cargo.toml b/examples/snake/Cargo.toml new file mode 100644 index 0000000..d53646a --- /dev/null +++ b/examples/snake/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "snake" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "snake" +harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors + +[profile.release] +opt-level = "s" +debug = true +panic = "abort" + +[profile.dev] +debug = true # Symbols are nice and they don't increase the size on Flash +opt-level = "z" +panic = "abort" + +[dependencies] +cardputer-bsc-nostd = { version = "0.1.0", path = "../.." } +embedded-graphics = "0.8.1" +esp-hal = {version = "=1.0.0-beta.1", features = ["esp32s3", "unstable"]} +esp-println = { version = "0.14.0", features = ["esp32s3"] } +esp-bootloader-esp-idf = "0.1.0" +heapless = "0.8.0" diff --git a/examples/snake/build.rs b/examples/snake/build.rs new file mode 100644 index 0000000..a76b496 --- /dev/null +++ b/examples/snake/build.rs @@ -0,0 +1,52 @@ +fn main() { + linker_be_nice(); + // make sure linkall.x is the last linker script (otherwise might cause problems with flip-link) + println!("cargo:rustc-link-arg=-Tlinkall.x"); +} + +fn linker_be_nice() { + let args: Vec = std::env::args().collect(); + if args.len() > 1 { + let kind = &args[1]; + let what = &args[2]; + + match kind.as_str() { + "undefined-symbol" => match what.as_str() { + "_defmt_timestamp" => { + eprintln!(); + eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`"); + eprintln!(); + } + "_stack_start" => { + eprintln!(); + eprintln!("💡 Is the linker script `linkall.x` missing?"); + eprintln!(); + } + "esp_wifi_preempt_enable" + | "esp_wifi_preempt_yield_task" + | "esp_wifi_preempt_task_create" => { + eprintln!(); + eprintln!("💡 `esp-wifi` has no scheduler enabled. Make sure you have the `builtin-scheduler` feature enabled, or that you provide an external scheduler."); + eprintln!(); + } + "embedded_test_linker_file_not_added_to_rustflags" => { + eprintln!(); + eprintln!("💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests"); + eprintln!(); + } + _ => (), + }, + // we don't have anything helpful for "missing-lib" yet + _ => { + std::process::exit(1); + } + } + + std::process::exit(0); + } + + println!( + "cargo:rustc-link-arg=-Wl,--error-handling-script={}", + std::env::current_exe().unwrap().display() + ); +} diff --git a/examples/snake/readme.md b/examples/snake/readme.md new file mode 100644 index 0000000..835d1d8 --- /dev/null +++ b/examples/snake/readme.md @@ -0,0 +1,11 @@ +# snake example + +This is a more complex example, using the cardputer's display and keyboard to implement the game 'snake'. + +## Run this example + +With this repo cloned, run the following from either the root or this directory: + +```shell +cargo espflash flash --release --package snake +``` diff --git a/examples/snake/src/main.rs b/examples/snake/src/main.rs new file mode 100644 index 0000000..239ef83 --- /dev/null +++ b/examples/snake/src/main.rs @@ -0,0 +1,269 @@ +#![no_std] +#![no_main] + +use core::{ops::Rem, panic::PanicInfo}; + +use embedded_graphics::prelude::RgbColor; +use esp_hal::main; +use esp_println::println; + +const GRID_SQUARE_SIZE: u16 = 15; +const GRID_WIDTH: i32 = 16; +const GRID_HEIGHT: i32 = 9; + +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + println!("{info}"); + esp_hal::system::software_reset() +} + +esp_bootloader_esp_idf::esp_app_desc!(); + +#[main] +fn entrypoint() -> ! { + _main(); + println!("main exited, entering infinite loop"); + loop {} +} + +fn _main() { + let peripherals = esp_hal::init(esp_hal::Config::default()); + + let mut keyboard = cardputer_bsc_nostd::keyboard::Keyboard::new( + peripherals.GPIO8, + peripherals.GPIO9, + peripherals.GPIO11, + peripherals.GPIO13, + peripherals.GPIO15, + peripherals.GPIO3, + peripherals.GPIO4, + peripherals.GPIO5, + peripherals.GPIO6, + peripherals.GPIO7, + ); + + cardputer_bsc_nostd::setup_backlight! { + let (mut ledc, mut timer) = setup_backlight(peripherals.LEDC); + } + + let mut display = cardputer_bsc_nostd::display::Display::new( + peripherals.SPI2, + peripherals.GPIO36, + peripherals.GPIO35, + peripherals.GPIO37, + peripherals.GPIO34, + peripherals.GPIO33, + &ledc, + &timer, + peripherals.GPIO38, + ) + .unwrap(); + + let mut rng = esp_hal::rng::Rng::new(peripherals.RNG); + + clear_screen(&mut display, embedded_graphics::pixelcolor::Rgb565::BLACK); + display.set_backlight_brightness(100).unwrap(); + + let mut direction = (1, 0); + let mut snake_tiles = + heapless::Deque::<_, { GRID_WIDTH as usize * GRID_HEIGHT as usize }>::new(); + starting_snake(&mut snake_tiles); + + let mut apple = random_tile(&mut rng); + + loop { + for _ in 0..20 { + handle_input(&mut keyboard, &mut direction, &snake_tiles); + esp_hal::delay::Delay::new().delay_millis(10); + } + + let front_tile = *snake_tiles.front().unwrap(); + + let new_tile = ( + (front_tile.0 + direction.0).rem_euclid(GRID_WIDTH), + (front_tile.1 + direction.1).rem_euclid(GRID_HEIGHT), + ); + + if snake_tiles.iter().any(|&tile| tile == new_tile) { + game_over( + &mut display, + &mut rng, + &mut direction, + &mut snake_tiles, + &mut apple, + ); + continue; + } + + snake_tiles.push_front(new_tile).unwrap(); + + let old_tile = if new_tile == apple { + apple = random_tile(&mut rng); + None + } else { + snake_tiles.pop_back() + }; + if let Some(old_tile) = old_tile { + draw_tile( + &mut display, + old_tile.0, + old_tile.1, + embedded_graphics::pixelcolor::Rgb565::BLACK, + ); + } + draw_snake(&mut display, &snake_tiles); + draw_apple(&mut display, apple); + } +} + +fn handle_input( + keyboard: &mut cardputer_bsc_nostd::keyboard::Keyboard<'_>, + direction: &mut (i32, i32), + snake_tiles: &heapless::Deque<(i32, i32), N>, +) { + keyboard.scan(); + let pressed_keys = keyboard.pressed_keys(); + + let key_directions = [ + (cardputer_bsc_nostd::keyboard::Key::SemiColon, (0, -1)), + (cardputer_bsc_nostd::keyboard::Key::Comma, (-1, 0)), + (cardputer_bsc_nostd::keyboard::Key::Period, (0, 1)), + (cardputer_bsc_nostd::keyboard::Key::Slash, (1, 0)), + ]; + + for (key, new_direction) in key_directions { + if pressed_keys.contains(key) { + match (snake_tiles.iter().nth(0), snake_tiles.iter().nth(1)) { + (Some(&first), Some(&second)) => { + let mut disallowed_direction = ( + (second.0 - first.0).rem(GRID_WIDTH), + (second.1 - first.1).rem(GRID_HEIGHT), + ); + if disallowed_direction.0 == GRID_WIDTH - 1 { + disallowed_direction.0 = -1; + } else if disallowed_direction.0 == 1 - GRID_WIDTH { + disallowed_direction.0 = 1; + } else if disallowed_direction.1 == GRID_HEIGHT - 1 { + disallowed_direction.1 = -1; + } else if disallowed_direction.1 == 1 - GRID_HEIGHT { + disallowed_direction.1 = 1; + } + + if disallowed_direction != new_direction { + *direction = new_direction; + } + } + _ => { + *direction = new_direction; + } + } + } + } + keyboard.clear_pressed_keys(); +} + +fn random_tile(rng: &mut esp_hal::rng::Rng) -> (i32, i32) { + ( + (rng.random() as i32).rem_euclid(GRID_WIDTH), + (rng.random() as i32).rem_euclid(GRID_HEIGHT), + ) +} + +fn starting_snake(snake_tiles: &mut heapless::Deque<(i32, i32), N>) { + let start_position = (GRID_WIDTH / 2, GRID_HEIGHT / 2); + snake_tiles.push_back(start_position).unwrap(); + snake_tiles + .push_back((start_position.0 - 1, start_position.1)) + .unwrap(); + snake_tiles + .push_back((start_position.0 - 2, start_position.1)) + .unwrap(); + snake_tiles + .push_back((start_position.0 - 3, start_position.1)) + .unwrap(); +} + +fn draw_apple(display: &mut cardputer_bsc_nostd::display::Display<'_>, apple: (i32, i32)) { + draw_tile( + display, + apple.0, + apple.1, + embedded_graphics::pixelcolor::Rgb565::GREEN, + ); +} + +fn draw_snake( + display: &mut cardputer_bsc_nostd::display::Display<'_>, + snake_tiles: &heapless::Deque<(i32, i32), N>, +) { + for &tile in snake_tiles { + draw_tile( + display, + tile.0, + tile.1, + embedded_graphics::pixelcolor::Rgb565::RED, + ); + } +} + +fn clear_screen( + display: &mut cardputer_bsc_nostd::display::Display<'_>, + colour: embedded_graphics::pixelcolor::Rgb565, +) { + display + .set_pixels( + 0, + 0, + cardputer_bsc_nostd::display::DISPLAY_SIZE_WIDTH - 1, + cardputer_bsc_nostd::display::DISPLAY_SIZE_HEIGHT - 1, + core::iter::repeat_n( + colour, + cardputer_bsc_nostd::display::DISPLAY_SIZE_WIDTH as usize + * cardputer_bsc_nostd::display::DISPLAY_SIZE_HEIGHT as usize, + ), + ) + .unwrap(); +} + +fn draw_tile( + display: &mut cardputer_bsc_nostd::display::Display<'_>, + x: i32, + y: i32, + colour: embedded_graphics::pixelcolor::Rgb565, +) { + display + .set_pixels( + x as u16 * GRID_SQUARE_SIZE, + y as u16 * GRID_SQUARE_SIZE, + (x + 1) as u16 * GRID_SQUARE_SIZE - 1, + (y + 1) as u16 * GRID_SQUARE_SIZE - 1, + core::iter::repeat_n(colour, (GRID_SQUARE_SIZE as usize).pow(2)), + ) + .unwrap(); +} + +fn game_over( + display: &mut cardputer_bsc_nostd::display::Display<'_>, + rng: &mut esp_hal::rng::Rng, + direction: &mut (i32, i32), + snake_tiles: &mut heapless::Deque<(i32, i32), N>, + apple: &mut (i32, i32), +) { + for x in 0..GRID_WIDTH { + for y in 0..GRID_HEIGHT { + draw_tile( + display, + x, + y, + embedded_graphics::pixelcolor::Rgb565::new(31, 63, 31), + ); + esp_hal::delay::Delay::new().delay_millis(20); + } + } + esp_hal::delay::Delay::new().delay_millis(1000); + clear_screen(display, embedded_graphics::pixelcolor::Rgb565::BLACK); + snake_tiles.clear(); + *direction = (1, 0); + starting_snake(snake_tiles); + *apple = random_tile(rng); +}