From 1890568ada689301e94f23735c7e09a2f824005d Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Thu, 9 Apr 2026 20:45:25 +0100 Subject: [PATCH] local --- Cargo.lock | 449 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 + src/board.rs | 91 +++++++++++ src/game.rs | 332 +++++++++++++++++++++++++++++++++++++ src/input.rs | 68 ++++++++ src/main.rs | 14 +- src/piece.rs | 170 +++++++++++++++++++ 7 files changed, 1126 insertions(+), 2 deletions(-) create mode 100644 Cargo.lock create mode 100644 src/board.rs create mode 100644 src/game.rs create mode 100644 src/input.rs create mode 100644 src/piece.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3dc2f8e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,449 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tetris" +version = "0.1.0" +dependencies = [ + "anyhow", + "crossterm", + "rand", + "tokio", +] + +[[package]] +name = "tokio" +version = "1.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +dependencies = [ + "bytes", + "libc", + "mio 1.2.0", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 8037e1f..03f0734 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1.0" +crossterm = "0.27" +rand = "0.8" +tokio = { version = "1.0", features = ["full"] } diff --git a/src/board.rs b/src/board.rs new file mode 100644 index 0000000..78cfaea --- /dev/null +++ b/src/board.rs @@ -0,0 +1,91 @@ +use crate::piece::Piece; + +/// Размеры игрового поля +pub const BOARD_WIDTH: i32 = 10; +pub const BOARD_HEIGHT: i32 = 20; + +/// Тип клетки на доске: пустая или цветная +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Cell { + Empty, + Occupied(u8), // ANSI цвет +} + +/// Игровое поле +#[derive(Clone, Debug)] +pub struct Board { + pub grid: Vec>, +} + +impl Board { + /// Создает новую пустую доску + pub fn new() -> Board { + let grid = vec![ + vec![Cell::Empty; BOARD_WIDTH as usize]; + BOARD_HEIGHT as usize + ]; + Board { grid } + } + + /// Проверяет, можно ли разместить фигуру на доске + pub fn can_place(&self, piece: &Piece) -> bool { + for (x, y) in piece.positions() { + if x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT { + return false; + } + if !self.is_empty((x, y)) { + return false; + } + } + true + } + + /// Проверяет, пуста ли клетка + pub fn is_empty(&self, pos: (i32, i32)) -> bool { + let (x, y) = pos; + if x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT { + return false; + } + matches!(self.grid[y as usize][x as usize], Cell::Empty) + } + + /// Фиксирует фигуру на доске + pub fn lock_piece(&mut self, piece: &Piece) { + let color = piece.piece_type.color(); + for (x, y) in piece.positions() { + if y >= 0 && y < BOARD_HEIGHT && x >= 0 && x < BOARD_WIDTH { + self.grid[y as usize][x as usize] = Cell::Occupied(color); + } + } + } + + /// Удаляет заполненные линии и возвращает количество очков + pub fn clear_lines(&mut self) -> usize { + let mut lines_cleared = 0; + let mut new_grid = vec![vec![Cell::Empty; BOARD_WIDTH as usize]; BOARD_HEIGHT as usize]; + let mut new_y = (BOARD_HEIGHT - 1) as usize; + + for y in (0..BOARD_HEIGHT as usize).rev() { + if self.grid[y].iter().all(|c| !matches!(c, Cell::Empty)) { + lines_cleared += 1; + } else { + new_grid[new_y] = self.grid[y].clone(); + new_y -= 1; + } + } + + self.grid = new_grid; + lines_cleared + } + + /// Проверяет, закончилась ли игра (фигура не может появиться) + pub fn is_game_over(&self) -> bool { + !self.grid[0].iter().all(|c| matches!(c, Cell::Empty)) + } +} + +impl Default for Board { + fn default() -> Self { + Board::new() + } +} diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..a6d76de --- /dev/null +++ b/src/game.rs @@ -0,0 +1,332 @@ +use crate::board::{self, Board}; +use crate::input::{self, Input}; +use crate::piece::{Piece, PieceType}; +use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture}; +use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; +use crossterm::{cursor, execute}; +use std::io::{self, Stdout, Write}; +use std::sync::mpsc; +use std::time::{Duration, Instant}; + +/// Количество кадров в секунду для рендеринга +const FPS: u64 = 60; +/// Базовое время падения фигуры (в миллисекундах) +const DROP_INTERVAL: u64 = 1000; + +/// Состояние игры +#[derive(Clone, PartialEq, Debug)] +pub enum GameStatus { + Running, + Paused, + GameOver, +} + +/// Игра Тетрис +pub struct Game { + board: Board, + current_piece: Piece, + next_piece: Piece, + score: usize, + lines_cleared: usize, + level: usize, + drop_interval: u64, + last_drop: u64, + status: GameStatus, + input_rx: mpsc::Receiver, +} + +impl Game { + /// Создает новую игру + pub fn new() -> io::Result { + let current_piece = Piece::new(PieceType::random()); + let next_piece = Piece::new(PieceType::random()); + let (_, input_rx) = input::InputHandler::new(); + + Ok(Game { + board: Board::new(), + current_piece, + next_piece, + score: 0, + lines_cleared: 0, + level: 1, + drop_interval: DROP_INTERVAL, + last_drop: 0, + status: GameStatus::Running, + input_rx, + }) + } + + /// Запускает игровой цикл + pub fn run(&mut self) -> io::Result<()> { + let mut stdout = io::stdout(); + self.setup_terminal(&mut stdout)?; + + let mut last_tick = Instant::now(); + + while self.status == GameStatus::Running { + // Обработка ввода + self.handle_input(); + + // Проверка состояния игры + if self.status == GameStatus::GameOver { + self.render_game_over(&mut stdout)?; + break; + } + + if self.status == GameStatus::Paused { + self.render_pause(&mut stdout)?; + continue; + } + + // Обновление игры + let now = Instant::now(); + let delta = now.duration_since(last_tick).as_millis() as u64; + last_tick = now; + + self.update(delta)?; + self.render(&mut stdout)?; + + // Небольшая пауза для контроля FPS + let elapsed = last_tick.duration_since(now).as_millis() as u64; + if elapsed < 1000 / FPS { + std::thread::sleep(Duration::from_millis(1000 / FPS - elapsed)); + } + } + + self.restore_terminal(&mut stdout)?; + Ok(()) + } + + /// Настройка терминала для игры + fn setup_terminal(&self, stdout: &mut Stdout) -> io::Result<()> { + enable_raw_mode()?; + execute!( + stdout, + EnterAlternateScreen, + EnableMouseCapture, + cursor::Hide + )?; + Ok(()) + } + + /// Восстановление терминала + fn restore_terminal(&self, stdout: &mut Stdout) -> io::Result<()> { + disable_raw_mode()?; + execute!( + stdout, + LeaveAlternateScreen, + DisableMouseCapture, + cursor::Show + )?; + Ok(()) + } + + /// Обработка ввода + fn handle_input(&mut self) { + while let Ok(input) = self.input_rx.try_recv() { + match input { + Input::Left => self.move_left(), + Input::Right => self.move_right(), + Input::Down => { + self.try_move_down(); + self.last_drop = 0; + } + Input::Rotate => self.rotate(), + Input::Drop => { + self.drop(); + self.last_drop = 0; + } + Input::Quit => { + self.status = GameStatus::GameOver; + break; + } + Input::None => {} + } + } + } + + /// Обновление состояния игры + fn update(&mut self, delta: u64) -> io::Result<()> { + self.last_drop += delta; + + if self.last_drop >= self.drop_interval { + self.last_drop = 0; + self.try_move_down(); + } + + Ok(()) + } + + /// Попытка переместить фигуру вниз + fn try_move_down(&mut self) { + let mut new_piece = self.current_piece.clone(); + new_piece.move_down(); + + if self.board.can_place(&new_piece) { + self.current_piece = new_piece; + } else { + self.board.lock_piece(&self.current_piece); + self.spawn_new_piece(); + } + } + + /// Спавн новой фигуры + fn spawn_new_piece(&mut self) { + // Проверка на заполнение - конец игры + if !self.board.can_place(&self.next_piece) { + self.status = GameStatus::GameOver; + return; + } + + self.current_piece = self.next_piece.clone(); + self.next_piece = Piece::new(PieceType::random()); + + // Очистка линий + let lines = self.board.clear_lines(); + if lines > 0 { + self.score += lines * lines * 100; + self.lines_cleared += lines; + self.level = self.lines_cleared / 10 + 1; + // Ускорение игры с уровнем + self.drop_interval = DROP_INTERVAL.saturating_sub((self.level - 1) as u64 * 50); + } + } + + /// Попытка переместить фигуру влево + pub fn move_left(&mut self) { + let mut new_piece = self.current_piece.clone(); + new_piece.move_left(); + if self.board.can_place(&new_piece) { + self.current_piece = new_piece; + } + } + + /// Попытка переместить фигуру вправо + pub fn move_right(&mut self) { + let mut new_piece = self.current_piece.clone(); + new_piece.move_right(); + if self.board.can_place(&new_piece) { + self.current_piece = new_piece; + } + } + + /// Попытка повернуть фигуру + pub fn rotate(&mut self) { + let rotated = self.current_piece.rotated(); + if self.board.can_place(&rotated) { + self.current_piece = rotated; + } + } + + /// Быстрый сброс фигуры + pub fn drop(&mut self) { + loop { + let mut new_piece = self.current_piece.clone(); + new_piece.move_down(); + if !self.board.can_place(&new_piece) { + break; + } + self.current_piece = new_piece; + } + // Фиксируем фигуру + self.board.lock_piece(&self.current_piece); + self.spawn_new_piece(); + } + + /// Рендер игры + fn render(&self, stdout: &mut Stdout) -> io::Result<()> { + stdout.write_all(b"\x1B[2J")?; // Очистка экрана + + // Заголовок + writeln!( + stdout, + "Tetris - Score: {} - Lines: {} - Level: {}", + self.score, self.lines_cleared, self.level + )?; + + // Отрисовка доски + for y in 0..board::BOARD_HEIGHT { + stdout.write_all(b"|")?; + for x in 0..board::BOARD_WIDTH { + let cell = self.board.grid[y as usize][x as usize]; + match cell { + board::Cell::Empty => { + // Проверяем, есть ли там текущая фигура + if self.current_piece.positions().contains(&(x as i32, y as i32)) { + let color = self.current_piece.piece_type.color(); + write!(stdout, "\x1B[{}m#\x1B[0m", color)?; + } else { + stdout.write_all(b" ")?; + } + } + board::Cell::Occupied(color) => { + write!(stdout, "\x1B[{}m#\x1B[0m", color)?; + } + } + } + stdout.write_all(b"|\n")?; + } + + // Нижняя граница + stdout.write_all(b"+")?; + for _ in 0..board::BOARD_WIDTH { + stdout.write_all(b"-")?; + } + stdout.write_all(b"+\n")?; + + // Следующая фигура + writeln!(stdout, "Next:")?; + self.render_preview(stdout, &self.next_piece)?; + + // Управление + writeln!(stdout, "Controls: Arrow Keys - Move | Space - Drop | Q - Quit")?; + + stdout.flush()?; + Ok(()) + } + + /// Рендер превью фигуры + fn render_preview(&self, stdout: &mut Stdout, piece: &Piece) -> io::Result<()> { + let mut preview_grid = vec![vec![' '; 4]; 4]; + for block in &piece.blocks { + let x = (block.x + 1) as usize; + let y = (block.y + 1) as usize; + if x < 4 && y < 4 { + preview_grid[y][x] = '#'; + } + } + + let color = piece.piece_type.color(); + for row in preview_grid { + for c in row { + if c == '#' { + write!(stdout, "\x1B[{}m#\x1B[0m", color)?; + } else { + stdout.write_all(b" ")?; + } + } + stdout.write_all(b"\n")?; + } + Ok(()) + } + + /// Рендер экрана Game Over + fn render_game_over(&self, stdout: &mut Stdout) -> io::Result<()> { + writeln!(stdout, "Game Over! Final Score: {}", self.score)?; + writeln!(stdout, "Press any key to exit...")?; + stdout.flush()?; + // Ожидание нажатия клавиши + let _ = event::read(); + Ok(()) + } + + /// Рендер экрана паузы + fn render_pause(&mut self, stdout: &mut Stdout) -> io::Result<()> { + writeln!(stdout, "Paused - Press any key to resume...")?; + stdout.flush()?; + // Ожидание нажатия клавиши + let _ = event::read(); + self.status = GameStatus::Running; + Ok(()) + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..9a3d9a4 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,68 @@ +use crossterm::event::{self, Event, KeyCode, KeyModifiers}; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; + +/// Типы команд ввода +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Input { + Left, + Right, + Down, + Rotate, + Drop, + Quit, + None, +} + +/// Обработчик ввода +pub struct InputHandler { + tx: mpsc::Sender, + running: std::sync::Arc, +} + +impl InputHandler { + /// Создает новый обработчик ввода + pub fn new() -> (InputHandler, mpsc::Receiver) { + let (tx, rx) = mpsc::channel(); + let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + + let running_clone = running.clone(); + let tx_clone = tx.clone(); + + thread::spawn(move || { + while running_clone.load(std::sync::atomic::Ordering::SeqCst) { + if event::poll(Duration::from_millis(50)).unwrap() { + if let Event::Key(key) = event::read().unwrap() { + let input = match key { + _ if key.code == KeyCode::Left => Input::Left, + _ if key.code == KeyCode::Right => Input::Right, + _ if key.code == KeyCode::Down => Input::Down, + _ if key.code == KeyCode::Up => Input::Rotate, + _ if key.code == KeyCode::Char(' ') => Input::Drop, + _ if key.code == KeyCode::Char('q') || key.code == KeyCode::Char('Q') => Input::Quit, + _ => Input::None, + }; + // Игнорируем модификаторы для основных клавиш + if key.modifiers == KeyModifiers::NONE { + let _ = tx_clone.send(input); + } + } + } + } + }); + + (InputHandler { tx, running }, rx) + } + + /// Останавливает обработчик ввода + pub fn stop(&self) { + self.running.store(false, std::sync::atomic::Ordering::SeqCst); + } +} + +impl Drop for InputHandler { + fn drop(&mut self) { + self.stop(); + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..d84171b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,13 @@ -fn main() { - println!("Hello, world!"); +mod board; +mod game; +mod input; +mod piece; + +use game::Game; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let mut game = Game::new()?; + game.run()?; + Ok(()) } diff --git a/src/piece.rs b/src/piece.rs new file mode 100644 index 0000000..31d3fb6 --- /dev/null +++ b/src/piece.rs @@ -0,0 +1,170 @@ +use rand::Rng; + +/// Тип фигуры тетриса +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum PieceType { + I, + O, + T, + S, + Z, + J, + L, +} + +impl PieceType { + /// Возвращает все возможные типы фигур + pub fn all() -> [PieceType; 7] { + [PieceType::I, PieceType::O, PieceType::T, PieceType::S, PieceType::Z, PieceType::J, PieceType::L] + } + + /// Генерирует случайную фигуру + pub fn random() -> PieceType { + let mut rng = rand::thread_rng(); + let idx: u8 = rng.gen_range(0..7); + match idx { + 0 => PieceType::I, + 1 => PieceType::O, + 2 => PieceType::T, + 3 => PieceType::S, + 4 => PieceType::Z, + 5 => PieceType::J, + 6 => PieceType::L, + _ => PieceType::I, + } + } + + /// Возвращает цвет (ANSI код) для фигуры + pub fn color(&self) -> u8 { + match self { + PieceType::I => 36, // Cyan + PieceType::O => 33, // Yellow + PieceType::T => 35, // Magenta + PieceType::S => 32, // Green + PieceType::Z => 31, // Red + PieceType::J => 34, // Blue + PieceType::L => 37, // White + } + } +} + +/// Блок фигуры - относительная координата +#[derive(Clone, Copy, Debug)] +pub struct Block { + pub x: i32, + pub y: i32, +} + +/// Фигура тетриса с позицией +#[derive(Clone, Debug)] +pub struct Piece { + pub piece_type: PieceType, + pub blocks: [Block; 4], + pub center: (i32, i32), +} + +impl Piece { + /// Создает новую фигуру заданного типа в центре сверху + pub fn new(piece_type: PieceType) -> Piece { + let blocks = match piece_type { + PieceType::I => [ + Block { x: -1, y: 0 }, + Block { x: 0, y: 0 }, + Block { x: 1, y: 0 }, + Block { x: 2, y: 0 }, + ], + PieceType::O => [ + Block { x: 0, y: 0 }, + Block { x: 1, y: 0 }, + Block { x: 0, y: 1 }, + Block { x: 1, y: 1 }, + ], + PieceType::T => [ + Block { x: -1, y: 0 }, + Block { x: 0, y: 0 }, + Block { x: 1, y: 0 }, + Block { x: 0, y: 1 }, + ], + PieceType::S => [ + Block { x: 0, y: 0 }, + Block { x: 1, y: 0 }, + Block { x: -1, y: 1 }, + Block { x: 0, y: 1 }, + ], + PieceType::Z => [ + Block { x: -1, y: 0 }, + Block { x: 0, y: 0 }, + Block { x: 0, y: 1 }, + Block { x: 1, y: 1 }, + ], + PieceType::J => [ + Block { x: -1, y: 0 }, + Block { x: 0, y: 0 }, + Block { x: 1, y: 0 }, + Block { x: -1, y: 1 }, + ], + PieceType::L => [ + Block { x: -1, y: 0 }, + Block { x: 0, y: 0 }, + Block { x: 1, y: 0 }, + Block { x: 1, y: 1 }, + ], + }; + + Piece { + piece_type, + blocks, + center: (5, 0), + } + } + + /// Возвращает позиции всех блоков в абсолютных координатах + pub fn positions(&self) -> Vec<(i32, i32)> { + self.blocks + .iter() + .map(|b| (self.center.0 + b.x, self.center.1 + b.y)) + .collect() + } + + /// Поворачивает фигуру на 90 градусов по часовой стрелке + pub fn rotate(&mut self) { + for block in &mut self.blocks { + let (x, y) = (block.x, block.y); + block.x = -y; + block.y = x; + } + } + + /// Возвращает новую фигуру с тем же типом, но повернутую + pub fn rotated(&self) -> Piece { + let mut new = self.clone(); + new.rotate(); + new + } + + /// Двигает фигуру влево + pub fn move_left(&mut self) { + self.center.0 -= 1; + } + + /// Двигает фигуру вправо + pub fn move_right(&mut self) { + self.center.0 += 1; + } + + /// Двигает фигуру вниз + pub fn move_down(&mut self) { + self.center.1 += 1; + } + + /// Проверяет, находится ли фигура в допустимой области + pub fn is_within_bounds(&self, width: i32, height: i32) -> bool { + for block in &self.blocks { + let (x, y) = (self.center.0 + block.x, self.center.1 + block.y); + if x < 0 || x >= width || y >= height { + return false; + } + } + true + } +}