tetris-io 0.1.1

Terminal-based Tetris game built with Ratatui and Crossterm
Documentation
use std::collections::VecDeque;

use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};

use super::board::{BOARD_HEIGHT, BOARD_WIDTH, Board, PLAY_HEIGHT};
use super::garbage::{GarbageChunk, GarbageConfig, GarbageQueue};
use super::piece::{Piece, PieceType, Rotation};
use super::rng::{BagRng, PieceRng};

const SPAWN_X: i32 = 3;
const SPAWN_Y: i32 = 0;

#[derive(Debug)]
pub struct GameState {
    pub board: Board,
    pub active: Piece,
    pub hold: Option<PieceType>,
    pub hold_used: bool,
    pub queue: VecDeque<PieceType>,
    pub rng: BagRng,
    pub garbage_rng: StdRng,
    pub garbage_queue: GarbageQueue,
    pub garbage_config: GarbageConfig,
    pub pieces_placed: u32,
    pub game_over: bool,
    pub gravity_accum_ms: u64,
    pub lock_delay_ms: u64,
    pub lock_reset_count: u32,
    pub grounded: bool,
    pub lock_started: bool,
    pub combo: u32,
    pub b2b: u32,
    pub last_clear_label: Option<String>,
    pub last_action_rotation: bool,
    pub lines: u32,
    pub score: u32,
    pub level: u32,
    pub garbage_rows: [bool; BOARD_HEIGHT],
    pub garbage_remaining: u32,
}

impl GameState {
    pub fn new(seed: u64) -> Self {
        let mut rng = BagRng::new(seed);
        let garbage_rng = StdRng::seed_from_u64(seed ^ 0xD1B54A32D192ED03);
        let mut queue = VecDeque::new();
        for _ in 0..6 {
            queue.push_back(rng.next_piece());
        }
        let active = Piece::new(queue.pop_front().unwrap_or(PieceType::I), SPAWN_X, SPAWN_Y);
        Self {
            board: Board::new(),
            active,
            hold: None,
            hold_used: false,
            queue,
            rng,
            garbage_rng,
            garbage_queue: GarbageQueue::default(),
            garbage_config: GarbageConfig::default(),
            pieces_placed: 0,
            game_over: false,
            gravity_accum_ms: 0,
            lock_delay_ms: 0,
            lock_reset_count: 0,
            grounded: false,
            lock_started: false,
            combo: 0,
            b2b: 0,
            last_clear_label: None,
            last_action_rotation: false,
            lines: 0,
            score: 0,
            level: 1,
            garbage_rows: [false; BOARD_HEIGHT],
            garbage_remaining: 0,
        }
    }

    pub fn reset_active(&mut self, kind: PieceType) {
        self.active = Piece {
            kind,
            rotation: Rotation(0),
            x: SPAWN_X,
            y: SPAWN_Y,
        };
        self.gravity_accum_ms = 0;
        self.lock_delay_ms = 0;
        self.lock_reset_count = 0;
        self.grounded = false;
        self.lock_started = false;
        self.last_action_rotation = false;
    }

    pub fn pop_next(&mut self) -> PieceType {
        while self.queue.len() < 6 {
            self.queue.push_back(self.rng.next_piece());
        }
        self.queue
            .pop_front()
            .unwrap_or_else(|| self.rng.next_piece())
    }

    pub fn peek_next(&self, count: usize) -> Vec<PieceType> {
        self.queue.iter().take(count).copied().collect()
    }

    pub fn seed_cheese(&mut self, target_lines: u32, seed: u64) {
        let max_lines = PLAY_HEIGHT as u32;
        let lines = target_lines.min(max_lines);
        if lines == 0 {
            return;
        }

        self.garbage_rows = [false; BOARD_HEIGHT];
        self.garbage_remaining = lines;

        let mut rng = StdRng::seed_from_u64(seed ^ 0x9E3779B97F4A7C15);
        for i in 0..lines {
            let row = BOARD_HEIGHT - 1 - i as usize;
            self.garbage_rows[row] = true;
            let hole = rng.gen_range(0..BOARD_WIDTH);
            for x in 0..BOARD_WIDTH {
                if x != hole {
                    self.board
                        .set_garbage(x as i32, row as i32, Some(PieceType::O));
                }
            }
        }
    }

    pub fn apply_row_clear_mask(&mut self, cleared_mask: &[bool; BOARD_HEIGHT]) -> u32 {
        if self.garbage_remaining == 0 {
            return 0;
        }

        let mut cleared_garbage = 0u32;
        let mut dst = BOARD_HEIGHT as i32 - 1;
        for src in (0..BOARD_HEIGHT as i32).rev() {
            if cleared_mask[src as usize] {
                if self.garbage_rows[src as usize] {
                    cleared_garbage = cleared_garbage.saturating_add(1);
                }
            } else {
                if dst != src {
                    self.garbage_rows[dst as usize] = self.garbage_rows[src as usize];
                }
                dst -= 1;
            }
        }
        while dst >= 0 {
            self.garbage_rows[dst as usize] = false;
            dst -= 1;
        }

        self.garbage_remaining = self.garbage_remaining.saturating_sub(cleared_garbage);
        cleared_garbage
    }

    pub fn opener_active(&self) -> bool {
        self.pieces_placed < self.garbage_config.opener_phase_pieces
    }

    pub fn pending_garbage(&self) -> u32 {
        self.garbage_queue.total_lines()
    }

    pub fn receive_garbage(&mut self, lines: u32) {
        if lines == 0 {
            return;
        }
        let hole = self.garbage_rng.gen_range(0..BOARD_WIDTH);
        self.garbage_queue.push(GarbageChunk { lines, hole });
    }

    pub fn cancel_garbage(&mut self, lines: u32) -> u32 {
        self.garbage_queue.cancel_lines(lines)
    }

    pub fn apply_pending_garbage(&mut self) -> bool {
        let chunks = self.garbage_queue.drain_all();
        let mut top_out = false;
        for chunk in chunks {
            let holes = vec![chunk.hole; chunk.lines as usize];
            if self.board.insert_garbage(chunk.lines, &holes) {
                top_out = true;
            }
        }
        top_out
    }
}