extern crate termios; use nonblock::NonBlockingReader; use rand::seq::SliceRandom; use std::io::Write; const FIELD_COLS: usize = 10; const FIELD_ROWS_VISIBLE: usize = 20; const FIELD_ROWS: usize = FIELD_ROWS_VISIBLE + 20; const FIELD_SIZE: usize = FIELD_COLS * FIELD_ROWS; const FIGURE_SIZE_COLS: usize = 4; const FIGURE_SIZE_ROWS: usize = 4; const FIGURE_SIZE: usize = FIGURE_SIZE_COLS * FIGURE_SIZE_ROWS; const CELLS_I: [u8; FIGURE_SIZE] = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0]; const CELLS_L2: [u8; FIGURE_SIZE] = [0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0]; const CELLS_L: [u8; FIGURE_SIZE] = [0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0]; const CELLS_B: [u8; FIGURE_SIZE] = [0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0]; const CELLS_S: [u8; FIGURE_SIZE] = [0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0]; const CELLS_T: [u8; FIGURE_SIZE] = [0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0]; const CELLS_Z: [u8; FIGURE_SIZE] = [0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0]; const NEXT_COUNT: usize = 4; const DISPLAY_NEXT_OFFSET_DOWN: usize = 4; const DISPLAY_HOLD_OFFSET_DOWN: usize = 4; const GHOST_MASK: u8 = 8; enum StepResult { StateChanged, Quit, } #[derive(Clone, Debug)] enum FigureKind { I, J, L, O, S, T, Z, } struct Timeout { beginning: nix::sys::time::TimeSpec, duration: std::time::Duration, } impl Default for Timeout { #[inline] fn default() -> Self { Self { beginning: nix::sys::time::TimeSpec::new(0, 0), duration: std::time::Duration::default(), } } } impl Timeout { fn from_duration(d: std::time::Duration) -> Self { Timeout { beginning: nix::time::clock_gettime(nix::time::ClockId::CLOCK_MONOTONIC).unwrap(), duration: d, } } fn is_elapsed(&self) -> bool { std::time::Duration::from(self.beginning) + self.duration <= std::time::Duration::from( nix::time::clock_gettime(nix::time::ClockId::CLOCK_MONOTONIC).unwrap(), ) } } #[derive(Clone)] struct Figure { kind: FigureKind, cells: [u8; FIGURE_SIZE], } impl Figure { fn from_kind(kind: FigureKind) -> Self { match kind { FigureKind::I => Self { cells: CELLS_I, kind, }, FigureKind::J => Self { cells: CELLS_L2, kind, }, FigureKind::L => Self { cells: CELLS_L, kind, }, FigureKind::O => Self { cells: CELLS_B, kind, }, FigureKind::S => Self { cells: CELLS_S, kind, }, FigureKind::T => Self { cells: CELLS_T, kind, }, FigureKind::Z => Self { cells: CELLS_Z, kind, }, } } fn rotate_cw_4x4(&mut self) { let mut tmp: [u8; 4 * 4] = Default::default(); for y in 0..4 { for x in 0..4 { tmp[y + (3 - x) * 4] = self.cells[x + y * 4]; } } self.cells = tmp; } fn rotate_cw_3x3(&mut self) { let mut tmp: [u8; 3 * 3] = Default::default(); for y in 0..3 { for x in 0..3 { tmp[y + (2 - x) * 3] = self.cells[x + y * 4]; } } for y in 0..3 { for x in 0..3 { self.cells[x + y * 4] = tmp[x + y * 3]; } } } fn rotate(&mut self) { match self.kind { FigureKind::I => self.rotate_cw_4x4(), FigureKind::J => self.rotate_cw_3x3(), FigureKind::L => self.rotate_cw_3x3(), FigureKind::O => {} FigureKind::S => self.rotate_cw_3x3(), FigureKind::T => self.rotate_cw_3x3(), FigureKind::Z => self.rotate_cw_3x3(), } } } trait Cell { fn is_ghost(&self) -> bool; fn set_ghost(&mut self, ghost: bool); fn color(&self) -> Self; } impl Cell for u8 { fn is_ghost(&self) -> bool { *self & GHOST_MASK != 0 } fn set_ghost(&mut self, ghost: bool) { if ghost { *self |= GHOST_MASK; } else { *self &= !GHOST_MASK; } } fn color(&self) -> Self { self & !GHOST_MASK } } #[derive(Clone)] struct Field { cells: [u8; FIELD_SIZE], } impl Default for Field { #[inline] fn default() -> Self { Self { cells: [0; FIELD_SIZE], } } } impl Field { fn render(&mut self, figure: &Figure, x: isize, y: isize) { for i in 0..FIGURE_SIZE { let f_x = (i % FIGURE_SIZE_COLS) as isize + x; let f_y = (i / FIGURE_SIZE_ROWS) as isize + y; if f_x < 0 || f_x >= FIELD_COLS as isize || f_y < 0 || f_y >= FIELD_ROWS as isize { continue; } let color = figure.cells[i].color(); let current = f_x as usize + f_y as usize * FIELD_COLS; if color != 0 && (self.cells[current].is_ghost() || self.cells[current] == 0) { self.cells[current] = figure.cells[i]; } } } fn has_collision(&self, figure: &Figure, x: isize, y: isize) -> bool { for i in 0..FIGURE_SIZE { let f_x = (i % FIGURE_SIZE_COLS) as isize + x; let f_y = (i / FIGURE_SIZE_ROWS) as isize + y; let color = figure.cells[i].color(); if color != 0 { if f_x < 0 || f_x >= FIELD_COLS as isize || f_y < 0 || f_y >= FIELD_ROWS as isize { return true; } let fcolor = self.cells[f_x as usize + f_y as usize * FIELD_COLS].color(); if fcolor != 0 { return true; } } } false } fn is_line_complete(&self, line: usize) -> bool { assert!(line < FIELD_ROWS); self.cells[FIELD_COLS * line..][..FIELD_COLS] .iter() .fold(1, |a, e| a & e) != 0 } fn kill_line(&mut self, line: usize) { for row in line..FIELD_ROWS - 1 { for col in 0..FIELD_COLS { self.cells[col + row * FIELD_COLS] = self.cells[col + (row + 1) * FIELD_COLS]; } } for col in 0..FIELD_COLS { self.cells[col + (FIELD_ROWS - 1) * FIELD_COLS] = 0; } } } struct Bag { index: u8, pieces: [u8; 7], } impl Bag { fn next(&mut self) -> FigureKind { if self.index >= 7 { self.index = 0; self.pieces.shuffle(&mut rand::thread_rng()); } let i = self.index; self.index += 1; match self.pieces[i as usize] { 1 => FigureKind::I, 2 => FigureKind::J, 3 => FigureKind::L, 5 => FigureKind::O, 6 => FigureKind::S, 4 => FigureKind::T, 7 => FigureKind::Z, _ => unreachable!(), } } } impl Default for Bag { #[inline] fn default() -> Self { let mut pieces: [u8; 7] = [1, 2, 3, 4, 5, 6, 7]; pieces.shuffle(&mut rand::thread_rng()); Self { index: 0, pieces } } } enum MenuAction { Run, Quit, } enum MenuContent { Action(MenuAction), } struct Menu { name: &'static str, content: MenuContent, } enum GameState { Menu, InGame, } struct Game { state: GameState, menu: Vec, menu_selected: usize, field: Field, figure: Figure, x: isize, y: isize, fall_timeout: Timeout, place_timeout: Option, bag: Bag, figure_next: [Figure; NEXT_COUNT], figure_on_hold: Figure, } impl Default for Game { #[inline] fn default() -> Self { let menu = vec![ Menu{ name: "Play", content: MenuContent::Action(MenuAction::Run), }, Menu{ name: "Quit", content: MenuContent::Action(MenuAction::Quit), }, ]; let mut bag = Bag::default(); let figure_on_hold = Figure::from_kind(bag.next()); let figure = Figure::from_kind(bag.next()); let figure_next = [ Figure::from_kind(bag.next()), Figure::from_kind(bag.next()), Figure::from_kind(bag.next()), Figure::from_kind(bag.next()), ]; Self { state: GameState::InGame, menu, menu_selected: 0, field: Field::default(), figure, x: 3, y: 17, fall_timeout: Timeout::from_duration(std::time::Duration::from_millis(500)), place_timeout: None, bag, figure_next, figure_on_hold, } } } impl Game { fn start_game(&mut self) { self.bag = Bag::default(); self.figure_on_hold = Figure::from_kind(self.bag.next()); self.figure = Figure::from_kind(self.bag.next()); self.figure_next = [ Figure::from_kind(self.bag.next()), Figure::from_kind(self.bag.next()), Figure::from_kind(self.bag.next()), Figure::from_kind(self.bag.next()), ]; self.field = Field::default(); self.state = GameState::InGame; (self.x, self.y) = (3, 17); self.fall_timeout = Timeout::from_duration(std::time::Duration::from_millis(500)); self.place_timeout = None; } fn place_figure(&mut self) { self.field.render(&self.figure, self.x, self.y); self.figure = self.figure_next[0].clone(); for i in 0..NEXT_COUNT - 1 { self.figure_next[i] = self.figure_next[i + 1].clone(); } self.figure_next[NEXT_COUNT - 1] = Figure::from_kind(self.bag.next()); (self.x, self.y) = (3, 17); for line in 0..FIELD_ROWS - 1 { while self.field.is_line_complete(line) { self.field.kill_line(line); } } self.stop_place_timeout(); } fn hold_figure(&mut self) { (self.figure_on_hold, self.figure) = ( Figure::from_kind(self.figure.kind.clone()), self.figure_on_hold.clone(), ); (self.x, self.y) = (3, 17); } fn reset_fall_timeout(&mut self) { self.fall_timeout = Timeout::from_duration(std::time::Duration::from_millis(500)); } fn start_place_timeout(&mut self) { self.place_timeout = Some(Timeout::from_duration(std::time::Duration::from_millis( 500, ))); } fn stop_place_timeout(&mut self) { self.place_timeout = None } fn reset_place_timeout(&mut self) { if self.place_timeout.is_some() { self.start_place_timeout(); } } fn handle_input_ingame(&mut self, c: char) -> Option { match c { 'q' => return Some(StepResult::Quit), ' ' => { while !self.field.has_collision(&self.figure, self.x, self.y - 1) { self.y -= 1; } self.place_figure(); self.stop_place_timeout(); } 'j' => { if !self.field.has_collision(&self.figure, self.x - 1, self.y) { self.x -= 1; } } 'k' => { if !self.field.has_collision(&self.figure, self.x, self.y - 1) { self.y -= 1; self.reset_fall_timeout(); } else { self.start_place_timeout(); } } ';' | 'i' => { let mut figure = self.figure.clone(); figure.rotate(); if !self.field.has_collision(&figure, self.x, self.y) { self.figure.rotate() } } 'l' => { if !self.field.has_collision(&self.figure, self.x + 1, self.y) { self.x += 1; } } 'h' => { self.hold_figure(); self.reset_fall_timeout(); self.stop_place_timeout(); } _ => return None, } self.reset_place_timeout(); Some(StepResult::StateChanged) } fn advance_figure(&mut self) { if self.field.has_collision(&self.figure, self.x, self.y - 1) { self.place_figure(); } else { self.y -= 1; if self.field.has_collision(&self.figure, self.x, self.y - 1) { self.start_place_timeout(); } else { self.stop_place_timeout(); } } self.reset_fall_timeout(); } fn step_game(&mut self, c_optional: Option) -> Option { let handle_input_result = if let Some(c) = c_optional { self.handle_input_ingame(c) } else { None }; let advance_figure_result = if let Some(t) = &self.place_timeout { if t.is_elapsed() { self.advance_figure(); Some(StepResult::StateChanged) } else { None } } else if self.fall_timeout.is_elapsed() { self.advance_figure(); Some(StepResult::StateChanged) } else { None }; if handle_input_result.is_none() { advance_figure_result } else { handle_input_result } } fn handle_input_menu(&mut self, c: char) -> Option { match c { 'q' => return Some(StepResult::Quit), 'k' => { if self.menu_selected + 1 < self.menu.len() { self.menu_selected += 1; Some(StepResult::StateChanged) } else { None } } 'i' => { if self.menu_selected > 0 { self.menu_selected -= 1; Some(StepResult::StateChanged) } else { None } } 'l' | ' ' | '\n' => { match &self.menu[self.menu_selected].content { MenuContent::Action(action) => match action { MenuAction::Run => { self.start_game(); Some(StepResult::StateChanged) }, MenuAction::Quit => Some(StepResult::Quit), }, } } _ => None, } } fn step_menu(&mut self, c_optional: Option) -> Option { if let Some(c) = c_optional { self.handle_input_menu(c) } else { None } } fn step(&mut self, c_optional: Option) -> Option { match self.state { GameState::Menu => self.step_menu(c_optional), GameState::InGame => self.step_game(c_optional), } } } struct Tui { state: termios::Termios, } impl Tui { fn display_hold(&self, figure: &Figure, y_global: usize) { let offset = DISPLAY_HOLD_OFFSET_DOWN; let at_figure = y_global >= (1 + offset) && y_global < FIGURE_SIZE_ROWS + (1 + offset); if y_global == offset { print!(" Hold: "); } else if at_figure { print!(" "); let y = FIGURE_SIZE_ROWS - 1 - (y_global - 1 - offset); for x in 0..FIGURE_SIZE_COLS { let c = figure.cells[y * FIGURE_SIZE_COLS + x]; if c == 0 { print!(" "); } else { print!("██"); } } print!(" "); } else { print!(" "); } } fn display_next(&self, figures: &[Figure; NEXT_COUNT], y_global: usize) { let offset = DISPLAY_NEXT_OFFSET_DOWN; if y_global == offset { return print!(" Next: "); } for (i, figure) in figures.iter().enumerate().take(NEXT_COUNT) { let at_figure = y_global >= (1 + offset + FIGURE_SIZE_ROWS * i) && y_global < (1 + offset + FIGURE_SIZE_ROWS * (i + 1)); if at_figure { print!(" "); let y = FIGURE_SIZE_ROWS - 1 - (y_global - 1 - offset - FIGURE_SIZE_ROWS * i); for x in 0..FIGURE_SIZE_COLS { let c = figure.cells[y * FIGURE_SIZE_COLS + x]; if c == 0 { print!(" "); } else { print!("██"); } } break; } } } fn display_field(&self, field: &Field, y: usize) { print!("|"); for x in 0..FIELD_COLS { let cell = field.cells[(FIELD_ROWS_VISIBLE - 1 - y) * FIELD_COLS + x]; if cell.color() == 0 { print!(" "); } else if cell.is_ghost() { print!("░░"); } else { print!("██"); } } print!("|"); } fn display_game_ingame(&self, game: &Game) { self.cursor_to_home(); let mut field = game.field.clone(); let mut ghost = game.figure.clone(); for i in 0..FIGURE_SIZE { ghost.cells[i].set_ghost(true); } let mut y_ghost = game.y; while !field.has_collision(&ghost, game.x, y_ghost) { y_ghost -= 1; } y_ghost += 1; field.render(&ghost, game.x, y_ghost); field.render(&game.figure, game.x, game.y); for y in 0..FIELD_ROWS_VISIBLE { self.display_hold(&game.figure_on_hold, y); self.display_field(&field, y); self.display_next(&game.figure_next, y); print!("\r\x1b[1B"); } if game.field.has_collision(&game.figure, game.x, game.y) { println!("Collision!"); } else { self.clear_line(); } } fn display_game_menu(&self, game: &Game) { self.cursor_to_home(); for y in 0..FIELD_ROWS_VISIBLE { print!(" |"); if y == 10 { if game.menu_selected == 0 { print!("\x1b[7m"); } print!("{:^20}\x1b[0m", game.menu[0].name); } else if y == 11 { if game.menu_selected == 1 { print!("\x1b[7m"); } print!("{:^20}\x1b[0m", game.menu[1].name); } else { print!(" "); } print!("|"); print!("\r\x1b[1B"); } } fn display_game(&self, game: &Game) { match game.state { GameState::Menu => self.display_game_menu(game), GameState::InGame => self.display_game_ingame(game), } std::io::stdout().flush().unwrap(); } fn clear_screen(&self) { print!("\x1B[2J"); } fn cursor_to_home(&self) { print!("\x1B[H"); } fn clear_line(&self) { println!("\x1B[2K"); } fn hide_cursor(&self) { println!("\x1B[?25l"); } fn show_cursor(&self) { println!("\x1B[?25h"); } } impl Default for Tui { fn default() -> Self { let termios_state_initial = termios::Termios::from_fd(0).unwrap(); let mut termios_state = termios_state_initial; let c_lflag = termios_state.c_lflag; termios_state.c_lflag &= !(termios::ICANON | termios::ECHO); termios::tcsetattr(0, termios::TCSADRAIN, &termios_state).unwrap(); termios_state.c_lflag = c_lflag; let tui = Self { state: termios_state_initial, }; tui.clear_screen(); tui.hide_cursor(); tui } } impl Drop for Tui { fn drop(&mut self) { self.show_cursor(); self.cursor_to_home(); termios::tcsetattr(0, termios::TCSADRAIN, &self.state).unwrap(); } } fn main() { let tui = Tui::default(); let mut game = Game::default(); if std::env::args().len() > 1 { game.state = GameState::Menu; } tui.display_game(&game); let mut noblock_stdin = NonBlockingReader::from_fd(std::io::stdin()).unwrap(); 'outer: while !noblock_stdin.is_eof() { let mut buf = String::new(); noblock_stdin.read_available_to_string(&mut buf).unwrap(); let mut changed = false; for c in buf.bytes() { if let Some(result) = game.step(Some(c as char)) { match result { StepResult::StateChanged => changed = true, StepResult::Quit => break 'outer, } } } if let Some(result) = game.step(None) { match result { StepResult::StateChanged => changed = true, StepResult::Quit => break 'outer, } } if changed { tui.display_game(&game); } std::thread::sleep(std::time::Duration::from_millis(1)); } }