tetris-io 0.1.1

Terminal-based Tetris game built with Ratatui and Crossterm
Documentation
use super::piece::PieceType;

pub const BOARD_WIDTH: usize = 10;
pub const PLAY_HEIGHT: usize = 20;
pub const BUFFER_HEIGHT: usize = 10;
pub const BOARD_HEIGHT: usize = PLAY_HEIGHT + BUFFER_HEIGHT;
pub const VISIBLE_HEIGHT: usize = BOARD_HEIGHT;
pub const VISIBLE_START_Y: usize = 0;

pub type Cell = Option<PieceType>;

#[derive(Debug, Clone)]
pub struct Board {
    cells: [[Cell; BOARD_WIDTH]; BOARD_HEIGHT],
    garbage: [[bool; BOARD_WIDTH]; BOARD_HEIGHT],
}

impl Default for Board {
    fn default() -> Self {
        Self {
            cells: [[None; BOARD_WIDTH]; BOARD_HEIGHT],
            garbage: [[false; BOARD_WIDTH]; BOARD_HEIGHT],
        }
    }
}

impl Board {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn get(&self, x: i32, y: i32) -> Cell {
        if !self.in_bounds(x, y) {
            return None;
        }
        self.cells[y as usize][x as usize]
    }

    pub fn set(&mut self, x: i32, y: i32, cell: Cell) {
        if !self.in_bounds(x, y) {
            return;
        }
        let yi = y as usize;
        let xi = x as usize;
        self.cells[yi][xi] = cell;
        self.garbage[yi][xi] = false;
    }

    pub fn set_garbage(&mut self, x: i32, y: i32, cell: Cell) {
        if !self.in_bounds(x, y) {
            return;
        }
        let yi = y as usize;
        let xi = x as usize;
        self.cells[yi][xi] = cell;
        self.garbage[yi][xi] = cell.is_some();
    }

    pub fn in_bounds(&self, x: i32, y: i32) -> bool {
        x >= 0 && x < BOARD_WIDTH as i32 && y >= 0 && y < BOARD_HEIGHT as i32
    }

    pub fn clear_full_rows(&mut self) -> usize {
        let mut cleared = 0usize;
        let mut dst = BOARD_HEIGHT as i32 - 1;
        for src in (0..BOARD_HEIGHT as i32).rev() {
            let full = self.is_row_full(src as usize);
            if full {
                cleared += 1;
            } else {
                if dst != src {
                    self.cells[dst as usize] = self.cells[src as usize];
                    self.garbage[dst as usize] = self.garbage[src as usize];
                }
                dst -= 1;
            }
        }
        while dst >= 0 {
            self.cells[dst as usize] = [None; BOARD_WIDTH];
            self.garbage[dst as usize] = [false; BOARD_WIDTH];
            dst -= 1;
        }
        cleared
    }

    pub fn insert_garbage(&mut self, lines: u32, holes: &[usize]) -> bool {
        let lines = lines.min(BOARD_HEIGHT as u32);
        if lines == 0 {
            return false;
        }
        let l = lines as usize;

        let mut top_out = false;
        for y in 0..l {
            if self.cells[y].iter().any(|c| c.is_some()) {
                top_out = true;
                break;
            }
        }

        let mut next = [[None; BOARD_WIDTH]; BOARD_HEIGHT];
        let mut next_garbage = [[false; BOARD_WIDTH]; BOARD_HEIGHT];
        next[..(BOARD_HEIGHT - l)].copy_from_slice(&self.cells[l..(BOARD_HEIGHT - l) + l]);
        next_garbage[..(BOARD_HEIGHT - l)]
            .copy_from_slice(&self.garbage[l..(BOARD_HEIGHT - l) + l]);
        for i in 0..l {
            let row = BOARD_HEIGHT - l + i;
            let hole = holes.get(i).copied().unwrap_or(0) % BOARD_WIDTH;
            let mut new_row = [None; BOARD_WIDTH];
            let mut new_garbage = [false; BOARD_WIDTH];
            for x in 0..BOARD_WIDTH {
                if x != hole {
                    new_row[x] = Some(PieceType::O);
                    new_garbage[x] = true;
                }
            }
            next[row] = new_row;
            next_garbage[row] = new_garbage;
        }

        self.cells = next;
        self.garbage = next_garbage;
        top_out
    }

    fn is_row_full(&self, row: usize) -> bool {
        self.cells[row].iter().all(|c| c.is_some())
    }

    pub fn cells(&self) -> &[[Cell; BOARD_WIDTH]; BOARD_HEIGHT] {
        &self.cells
    }

    pub fn is_garbage_cell(&self, x: i32, y: i32) -> bool {
        if !self.in_bounds(x, y) {
            return false;
        }
        self.garbage[y as usize][x as usize]
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn clear_rows_shifts_down() {
        let mut board = Board::new();
        for x in 0..BOARD_WIDTH as i32 {
            board.set(x, (BOARD_HEIGHT - 1) as i32, Some(PieceType::I));
        }
        board.set(0, (BOARD_HEIGHT - 2) as i32, Some(PieceType::O));
        let cleared = board.clear_full_rows();
        assert_eq!(cleared, 1);
        assert_eq!(board.get(0, (BOARD_HEIGHT - 1) as i32), Some(PieceType::O));
    }

    #[test]
    fn insert_garbage_shifts_up_and_fills() {
        let mut board = Board::new();
        board.set(0, (BOARD_HEIGHT - 1) as i32, Some(PieceType::I));
        let top_out = board.insert_garbage(1, &[0]);
        assert!(!top_out);
        assert_eq!(board.get(0, (BOARD_HEIGHT - 2) as i32), Some(PieceType::I));
        assert_eq!(board.get(0, (BOARD_HEIGHT - 1) as i32), None);
        assert_eq!(board.get(1, (BOARD_HEIGHT - 1) as i32), Some(PieceType::O));
    }

    #[test]
    fn insert_garbage_top_out_when_blocks_pushed_off() {
        let mut board = Board::new();
        board.set(0, 0, Some(PieceType::I));
        let top_out = board.insert_garbage(1, &[0]);
        assert!(top_out);
    }
}