This commit is contained in:
Ultradesu
2026-04-09 20:45:25 +01:00
parent f5c12157e9
commit 1890568ada
7 changed files with 1126 additions and 2 deletions
Generated
+449
View File
@@ -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",
]
+4
View File
@@ -4,3 +4,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0"
crossterm = "0.27"
rand = "0.8"
tokio = { version = "1.0", features = ["full"] }
+91
View File
@@ -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<Vec<Cell>>,
}
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()
}
}
+332
View File
@@ -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<Input>,
}
impl Game {
/// Создает новую игру
pub fn new() -> io::Result<Game> {
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(())
}
}
+68
View File
@@ -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<Input>,
running: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl InputHandler {
/// Создает новый обработчик ввода
pub fn new() -> (InputHandler, mpsc::Receiver<Input>) {
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();
}
}
+12 -2
View File
@@ -1,3 +1,13 @@
fn main() { mod board;
println!("Hello, world!"); mod game;
mod input;
mod piece;
use game::Game;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut game = Game::new()?;
game.run()?;
Ok(())
} }
+170
View File
@@ -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
}
}