//! Runs TETRIS on display from sk6812 RGBW LED strip and buttons using interrupts.
//!
//! The following wiring is assumed:
//! - LED => GPIO8
//! - RIGHT_BUTTON => GPIO0 -> GND
//! - MIDDLE_BUTTON => GPIO1 -> GND
//! - LEFT_BUTTON => GPIO2 -> GND
//! - LED_STRIP_DATA => GPIO4
//!
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use esp_backtrace as _;
use esp_hal::{
delay::Delay,
gpio::{Input, InputConfig, Level, Output, OutputConfig, Pull},
handler, main,
rmt::{PulseCode, Rmt, TxChannelAsync, TxChannelConfig, TxChannelCreatorAsync},
rng::Rng,
time::{self, Rate},
};
use esp_println::println;
use no_std_tetris::{RandomGenerator, Tetris, Color};
// global config
const BOARD_WIDTH: usize = 10;
const BOARD_HEIGHT: usize = 20;
const FALL_INTERVAL: u64 = 500; // TODO: should be function of score -> higher score faster speed
const BRIGHTNESS: u8 = 12;
// // neopixel
const T0H: u16 = 35; // 437.5 ns
const T0L: u16 = 90; // 1125 ns
const T1H: u16 = 70; // 875 ns
const T1L: u16 = 55; // 687.5 ns
struct TetrisRng(Rng);
impl RandomGenerator for TetrisRng {
fn next_random(&mut self) -> usize {
self.next_random() as usize
}
}
// neopixel LED strip config
fn create_led_bits(r: u8, g: u8, b: u8) -> [u32; 25] {
let mut data = [PulseCode::empty(); 25];
// WS2812B expects GRB order
let bytes = [g, r, b];
let mut idx = 0;
for byte in bytes {
for bit in (0..8).rev() {
data[idx] = if (byte & (1 << bit)) != 0 {
PulseCode::new(Level::High, T1H, Level::Low, T1L)
} else {
PulseCode::new(Level::High, T0H, Level::Low, T0L)
};
idx += 1;
}
}
data[24] = PulseCode::new(Level::Low, 800, Level::Low, 0); // Reset code
data
}
fn color_to_rgb(color: Color) -> (u8, u8, u8) {
match color {
Color::Red => (BRIGHTNESS, 0, 0),
Color::Green => (0, BRIGHTNESS, 0),
Color::Blue => (0, 0, BRIGHTNESS),
Color::Yellow => (BRIGHTNESS / 2, BRIGHTNESS / 2, 0),
Color::Cyan => (0, BRIGHTNESS / 2, BRIGHTNESS / 2),
Color::Magenta => (BRIGHTNESS / 2, 0, BRIGHTNESS / 2),
Color::White => (BRIGHTNESS / 3, BRIGHTNESS / 3, BRIGHTNESS / 3),
_ => (0, 0, 0),
}
}
fn board_to_led_index(x: usize, y: usize, flip_y: bool) -> usize {
let y_mapped = if flip_y { BOARD_HEIGHT - 1 - y } else { y };
let col_start = x * BOARD_HEIGHT; // Each column has 20 LEDs
if x % 2 == 0 {
// Even columns: y=0 at top (or bottom if flipped), y=19 at bottom (or top)
col_start + y_mapped
} else {
// Odd columns: y=0 at bottom (or top if flipped), y=19 at top (or bottom)
col_start + (BOARD_HEIGHT - 1 - y_mapped)
}
}
#[esp_hal_embassy::main]
async fn main(_spawner: Spawner) {
let peripherals = esp_hal::init(esp_hal::Config::default());
let out_config = OutputConfig::default();
let led = Output::new(peripherals.GPIO8, Level::High, out_config);
let in_config = InputConfig::default().with_pull(Pull::Up); // Use pull-up resistor for button
let right_button = Input::new(peripherals.GPIO0, in_config);
let middle_button = Input::new(peripherals.GPIO1, in_config);
let left_button = Input::new(peripherals.GPIO2, in_config);
let freq = Rate::from_mhz(80);
let delay = Delay::new();
let rng = Rng::new(peripherals.RNG);
let rmt = Rmt::new(peripherals.RMT, freq).unwrap().into_async();
let mut channel = match rmt.channel0.configure(
peripherals.GPIO4,
TxChannelConfig::default().with_clk_divider(1),
) {
Ok(channel) => channel,
Err(err) => {
panic!(
"Failed to configure RMT channel for led controll: {:?}",
err
);
}
};
let trandom = TetrisRng(rng);
let mut game = Tetris::new(trandom);
let mut last_update = time::Instant::now();
let fall_interval = time::Duration::from_millis(FALL_INTERVAL);
let mut last_key_time = time::Instant::now();
let debounce_duration = time::Duration::from_millis(250); // 100ms debounce
println!("Tetris game started!");
// Game loop
'game_loop: loop {
// Handle timing
let now = time::Instant::now();
if now - last_update >= fall_interval {
game.move_down();
last_update = now;
}
if right_button.is_low() {
println!("right_button pressed!");
if now - last_key_time > debounce_duration {
last_key_time = now;
game.move_right();
}
}
if left_button.is_low() {
println!("left_button pressed!");
if now - last_key_time > debounce_duration {
last_key_time = now;
game.move_left();
}
}
if middle_button.is_low() {
println!("middle_button pressed!");
if now - last_key_time > debounce_duration {
last_key_time = now;
game.rotate();
}
}
let mut led_colors = [(0u8, 0u8, 0u8); 200]; // Frame buffer for 200 LEDs
// Render board state
for y in 0..BOARD_HEIGHT {
for x in 0..BOARD_WIDTH {
if let Some(color) = game.board[y][x] {
let led_idx = board_to_led_index(x, y, true);
led_colors[led_idx] = color_to_rgb(color);
}
}
}
// Render current piece (if not game over)
if !game.is_game_over() {
for &(dx, dy) in &game.current_piece.shape {
let x = (game.piece_pos.0 + dx as i8) as usize;
let y = (game.piece_pos.1 + dy as i8) as usize;
if x < BOARD_WIDTH && y < BOARD_HEIGHT {
let led_idx = board_to_led_index(x, y, true);
led_colors[led_idx] = color_to_rgb(game.current_piece.color);
}
}
}
// Send data to LED strip
// Transmit one LED at a time
for (i, &(r, g, b)) in led_colors.iter().enumerate() {
let data = create_led_bits(r, g, b);
match channel.transmit(&data).await {
Ok(_) => {},
Err(err) => {
println!("Error transmitting LED {}: {:?}", i, err);
break;
}
}
}
if game.is_game_over() {
break 'game_loop;
}
}
println!("Thanks for playing! You scored {} points.", game.score);
loop {} // Keep the program running
}
Board not found
wokwi-custom-board
wokwi-custom-board