Created
November 30, 2023 07:23
-
-
Save kujirahand/30a59131b2d949fe556c5050f7340240 to your computer and use it in GitHub Desktop.
Rustで作ったターミナル迷路ゲーム
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate rand; | |
use rand::seq::SliceRandom; | |
use rand::thread_rng; | |
use std::io::{stdout, Result}; | |
use crossterm::{ | |
cursor, execute, ExecutableCommand, terminal, | |
style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor}, | |
event::{read, Event, KeyCode}, | |
}; | |
#[derive(Clone, Copy, PartialEq, Eq)] | |
pub enum Cell { | |
Wall, | |
Empty, | |
} | |
pub struct Maze { | |
pub cols: isize, | |
pub rows: isize, | |
pub data: Vec<Cell>, | |
} | |
impl Maze { | |
// 空の迷路を生成する | |
pub fn new(cols: isize, rows: isize) -> Maze { | |
let mut data = vec![]; | |
for _ in 0..rows * cols { | |
data.push(Cell::Wall); | |
} | |
Maze { cols, rows, data } | |
} | |
// (x, y)が迷路の範囲内かどうかを判定 | |
fn in_range(&self, x: isize, y: isize) -> bool { | |
(x >= 0 && x < self.cols as isize) && (y >= 0 && y < self.rows as isize) | |
} | |
// (x, y)のセルを設定する | |
fn set(&mut self, x: isize, y: isize, cell: Cell) { | |
if !self.in_range(x, y) { return; } | |
self.data[(y * self.cols + x) as usize] = cell; | |
} | |
// (x, y)のセルを取得する | |
fn get(&self, x: isize, y: isize) -> Cell { | |
if self.in_range(x, y) { | |
self.data[(y * self.cols + x) as usize] | |
} else { | |
Cell::Wall | |
} | |
} | |
// 再帰的に迷路を生成 | |
fn generate(&mut self, x: isize, y: isize) { | |
self.set(x, y, Cell::Empty); | |
let mut dirs = vec![(1, 0), (-1, 0), (0, 1), (0, -1)]; | |
let mut rng = thread_rng(); // ランダムジェネレータを作成 | |
dirs.shuffle(&mut rng); // 進行方向を選ぶ | |
for (dx, dy) in dirs { | |
// 2マス先が範囲外か通路ならスキップ | |
let (nx, ny) = (x + dx * 2, y + dy * 2); | |
if !self.in_range(nx, ny) || self.get(nx, ny) == Cell::Empty { continue; } | |
// 進行方向を掘って、再帰的に迷路を生成 | |
self.set(x + dx, y + dy, Cell::Empty); | |
self.generate(nx, ny); | |
} | |
} | |
} | |
// 迷路を表示する | |
fn display(maze: &Maze, px: isize, py: isize) -> Result<()> { | |
let put_ch = |x, y, fg, bg, ch| -> Result<()> { | |
execute!( | |
stdout(), | |
cursor::MoveTo(x as u16, y as u16), | |
SetForegroundColor(fg), | |
SetBackgroundColor(bg), | |
Print(ch), | |
) | |
}; | |
for y in 0..maze.rows { | |
for x in 0..maze.cols { | |
// プレイヤーを描画 | |
if x == px && y == py { | |
put_ch(x, y, Color::Yellow, Color::Black, "@")?; | |
continue; | |
} | |
// マップを描画 | |
match maze.get(x, y) { | |
Cell::Wall => put_ch(x, y, Color::Black, Color::DarkMagenta, "#")?, | |
Cell::Empty => put_ch(x, y, Color::Black, Color::Black, " ")?, | |
} | |
} | |
execute!(stdout(), Print("\r\n"), ResetColor)?; | |
} | |
Ok(()) | |
} | |
fn maze_main() -> Result<()> { | |
// ターミナルのサイズを取得 | |
let (cols, rows) = terminal::size()?; | |
// 画面サイズ内で最大の奇数にする | |
let cols = (cols as isize) / 2 * 2 - 1; | |
let rows = (rows as isize) / 2 * 2 - 1; | |
// 迷路を生成 | |
let mut maze = Maze::new(cols as isize, rows as isize); | |
maze.generate(1, 1); | |
// 画面クリア | |
stdout().execute(terminal::Clear(terminal::ClearType::All))?; | |
// 画面表示 | |
terminal::enable_raw_mode()?; | |
let mut px = 1; // プレイヤーの初期座標 | |
let mut py = 1; | |
loop { | |
// 画面描画 | |
display(&maze, px, py)?; | |
let mut tx = px; | |
let mut ty = py; | |
// プレイヤーの移動 | |
let event = read()?; | |
match event { | |
Event::Key(e) => { | |
match e.code { | |
KeyCode::Esc => break, | |
KeyCode::Char('q') => break, | |
// vi キーバインド | |
KeyCode::Char('h') => tx -= 1, | |
KeyCode::Char('l') => tx += 1, | |
KeyCode::Char('k') => ty -= 1, | |
KeyCode::Char('j') => ty += 1, | |
// カーソルキー | |
KeyCode::Left => tx -= 1, | |
KeyCode::Right => tx += 1, | |
KeyCode::Up => ty -= 1, | |
KeyCode::Down => ty += 1, | |
_ => {} | |
} | |
}, | |
_ => {} | |
} | |
if maze.in_range(tx, ty) && maze.get(tx, ty) == Cell::Empty { | |
px = tx; | |
py = ty; | |
// ゲームクリア? | |
if px == maze.cols - 2 && py == maze.rows - 2 { | |
execute!(stdout(), Print("\r\n"))?; | |
println!("ゲームクリア!!"); | |
break; | |
} | |
} | |
} | |
terminal::disable_raw_mode()?; | |
Ok(()) | |
} | |
#[allow(dead_code)] | |
fn test_show() -> Result<()> { | |
// execute! マクロの中で処理を記述 | |
execute!( | |
stdout(), | |
// 画面クリア | |
terminal::Clear(terminal::ClearType::All), | |
// カーソル移動 | |
cursor::MoveTo(2, 2), | |
// 色を赤色に | |
SetForegroundColor(Color::Red), | |
// 文字を表示 | |
Print("Hello, World!\n\n\n") | |
)?; | |
Ok(()) | |
} | |
#[allow(dead_code)] | |
fn test_raw_mode() -> Result<()> { | |
// Rawモードに変更 | |
terminal::enable_raw_mode()?; | |
// 無限ループの中でキーを取得する | |
loop { | |
// イベントの取得 | |
let event = read()?; | |
match event { | |
Event::Key(e) => { | |
match e.code { | |
// キーに合わせて処理 | |
KeyCode::Esc => break, | |
KeyCode::Char('q') => break, | |
KeyCode::Char('h') => { | |
execute!(stdout(), cursor::MoveToColumn(0))?; | |
println!("[push h key]"); | |
} | |
_ => {} | |
} | |
}, | |
_ => {} | |
} | |
} | |
terminal::disable_raw_mode()?; | |
Ok(()) | |
} | |
fn main() -> Result<()> { | |
maze_main() | |
// test_show() | |
// test_raw_mode() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment