Compare commits

...

3 Commits

Author SHA1 Message Date
Paul Brinkmeier
18dcff18b1 Add debug box 2025-12-04 22:27:33 +01:00
Paul Brinkmeier
50f2645093 cargo fmt 2025-12-01 22:20:51 +01:00
Paul Brinkmeier
282d41ceb4 Replace all unwraps 2025-12-01 22:09:15 +01:00
6 changed files with 106 additions and 46 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
target target
*.swp

2
activate Normal file
View File

@ -0,0 +1,2 @@
export PATH=$PATH:/Users/paul/Source/cdungeon/target/debug
alias cdg='cd $(cdungeon)'

View File

@ -40,3 +40,10 @@ Single-char
⎕ Music ⎕ something ⎕ Music ⎕ something
was wäre mit
- seitwärts gehen
- mehr räume anzeigen
- aktuellen raum live updaten
- beep boops/animation wenn irgendwas im aktuellen raum passiert
- position beim hochgehen merken
- hintergrund rendern

View File

@ -1,13 +1,11 @@
use std::{fs, path::PathBuf}; use std::{
error::Error, ffi::{OsString}, fmt::{self}, fs, path::PathBuf, process::Command
};
use color_eyre::eyre::Result;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use ratatui::{ use ratatui::{
Frame, buffer::{Buffer, Cell}, layout::{Constraint, Layout, Rect}, prelude::Backend, style::Color, text::{Line, Text}, widgets::{Block, Widget}, Frame, Terminal
buffer::{Buffer, Cell},
layout::{Constraint, Layout, Rect},
style::Color,
text::{Line, Text},
widgets::{Block, Widget},
}; };
use crate::geometry::{Grid, P2, V2}; use crate::geometry::{Grid, P2, V2};
@ -18,7 +16,7 @@ pub struct GameModel {
room: Room, room: Room,
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub enum GameEvent { pub enum GameEvent {
Quit, Quit,
Nop, Nop,
@ -26,9 +24,10 @@ pub enum GameEvent {
PlayerDash(V2<isize>), PlayerDash(V2<isize>),
Interact, Interact,
Navigate(NavigationTarget), Navigate(NavigationTarget),
Run(OsString, Vec<OsString>),
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub enum NavigationTarget { pub enum NavigationTarget {
Path(PathBuf), Path(PathBuf),
Parent, Parent,
@ -62,7 +61,7 @@ struct Room {
tiles: Grid<Tile>, tiles: Grid<Tile>,
} }
#[derive(Clone)] #[derive(Clone, Debug)]
struct Tile { struct Tile {
style: TileStyle, style: TileStyle,
action: Option<GameEvent>, action: Option<GameEvent>,
@ -95,21 +94,40 @@ impl Tile {
} }
} }
pub enum UpdateResult {
ShouldExit(PathBuf),
NewModel(GameModel),
}
#[derive(Debug)]
struct GameError {}
impl fmt::Display for GameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ouch game broken")
}
}
impl Error for GameError {}
impl GameModel { impl GameModel {
pub fn new(path: PathBuf) -> Self { pub fn new(path: PathBuf) -> Result<Self> {
let path = path.canonicalize().unwrap(); let path = path.canonicalize()?;
let mut entries: Vec<PathBuf> = fs::read_dir(&path) let mut entries: Vec<PathBuf> = fs::read_dir(&path)?
.unwrap() .filter_map(|entry| entry.ok().map(|entry| entry.path()))
.map(|entry| entry.unwrap().path())
.collect(); .collect();
entries.sort(); entries.sort();
let max_entry_length = entries let max_entry_length = entries
.iter() .iter()
.map(|entry| entry.file_name().unwrap().to_string_lossy().chars().count()) .filter_map(|entry| {
entry
.file_name()
.map(|entry| entry.to_string_lossy().chars().count())
})
.max() .max()
.unwrap_or(0); .unwrap_or(0);
Self { Ok(Self {
player_pos: P2::new(10, 2), player_pos: P2::new(10, 2),
room: Room { room: Room {
tiles: { tiles: {
@ -148,12 +166,14 @@ impl GameModel {
} else { } else {
tiles tiles
.get_mut(P2::new(10, 2 + i as isize)) .get_mut(P2::new(10, 2 + i as isize))
.map(|tile| *tile = Tile::new(TileStyle::Box)); .map(|tile| *tile = Tile::new(TileStyle::Box).with_action(GameEvent::Run("vim".into(), vec![
entry.into()
])));
} }
for (j, c) in entry for (j, c) in entry
.file_name() .file_name()
.unwrap() .ok_or(GameError {})?
.to_string_lossy() .to_string_lossy()
.chars() .chars()
.enumerate() .enumerate()
@ -169,25 +189,26 @@ impl GameModel {
}, },
path, path,
} })
} }
pub fn update(self, event: Event) -> Result<Self, PathBuf> { pub fn update<T: Backend>(self, event: Event, terminal: &mut Terminal<T>) -> Result<UpdateResult> {
self.update_game(GameEvent::from_crossterm(event)) self.update_game(GameEvent::from_crossterm(event), terminal)
} }
fn update_game(self, event: GameEvent) -> Result<Self, PathBuf> { fn update_game<T: Backend>(self, event: GameEvent, terminal: &mut Terminal<T>) -> Result<UpdateResult> {
match event { use UpdateResult::*;
GameEvent::Quit => Err(self.path), Ok(match event {
GameEvent::Nop => Ok(self), GameEvent::Quit => ShouldExit(self.path),
GameEvent::Nop => NewModel(self),
GameEvent::PlayerMove(direction) => { GameEvent::PlayerMove(direction) => {
let player_pos = self.player_pos + direction; let player_pos = self.player_pos + direction;
if let Some(target_tile) = self.room.tiles.get(player_pos) if let Some(target_tile) = self.room.tiles.get(player_pos)
&& target_tile.may_enter() && target_tile.may_enter()
{ {
Ok(Self { player_pos, ..self }) NewModel(Self { player_pos, ..self })
} else { } else {
Ok(self) NewModel(self)
} }
} }
GameEvent::PlayerDash(direction) => { GameEvent::PlayerDash(direction) => {
@ -206,7 +227,7 @@ impl GameModel {
break; break;
} }
} }
Ok(Self { player_pos, ..self }) NewModel(Self { player_pos, ..self })
} }
GameEvent::Interact => { GameEvent::Interact => {
let opt_action = self let opt_action = self
@ -216,27 +237,36 @@ impl GameModel {
.and_then(|tile| tile.action.clone()); .and_then(|tile| tile.action.clone());
if let Some(action) = opt_action { if let Some(action) = opt_action {
self.update_game(action) self.update_game(action, terminal)?
} else { } else {
Ok(self) NewModel(self)
} }
} }
GameEvent::Navigate(target) => { GameEvent::Navigate(target) => {
let path = match target { let path = match target {
NavigationTarget::Path(path) => path, NavigationTarget::Path(path) => path,
NavigationTarget::Parent => self.path.parent().unwrap().to_path_buf(), NavigationTarget::Parent => {
self.path.parent().ok_or(GameError {})?.to_path_buf()
}
}; };
Ok(Self::new(path))
NewModel(Self::new(path)?)
} }
} GameEvent::Run(cmd, args) => {
Command::new(&cmd).args(args).spawn().unwrap().wait().unwrap();
// TODO: Figure out why terminal isn't cleared on exit.
terminal.clear().unwrap();
NewModel(self)
}
})
} }
pub fn render(&self, frame: &mut Frame) { pub fn render(&self, frame: &mut Frame) {
let layout = let layout =
Layout::vertical([Constraint::Fill(1), Constraint::Length(3)]).split(frame.area()); Layout::vertical([Constraint::Fill(1), Constraint::Length(4)]).split(frame.area());
let camera_block_area = layout[0]; let camera_block_area = layout[0];
let info_block_area = layout[1]; let bottom_bar_area = layout[1];
let camera_block = Block::bordered() let camera_block = Block::bordered()
.title("camera") .title("camera")
@ -244,15 +274,30 @@ impl GameModel {
let camera_area = camera_block.inner(camera_block_area); let camera_area = camera_block.inner(camera_block_area);
let camera = CameraWidget::new(self); let camera = CameraWidget::new(self);
let bottom_bar_layout = Layout::horizontal([
Constraint::Fill(1),
Constraint::Fill(1),
]).split(bottom_bar_area);
let info_block_area = bottom_bar_layout[0];
let debug_block_area = bottom_bar_layout[1];
let info_block = Block::bordered().title("cwd"); let info_block = Block::bordered().title("cwd");
let info_area = info_block.inner(info_block_area); let info_area = info_block.inner(info_block_area);
let info = Text::raw(self.path.to_string_lossy()); let info = Text::raw(self.path.to_string_lossy());
let debug_block = Block::bordered().title("debug");
let debug_area = info_block.inner(debug_block_area);
let debug = Text::raw(self.room.tiles.get(self.player_pos).map(|tile| format!("{:?}\n{:?}", tile.style, tile.action)).unwrap_or("".to_string()));
frame.render_widget(camera_block, camera_block_area); frame.render_widget(camera_block, camera_block_area);
frame.render_widget(camera, camera_area); frame.render_widget(camera, camera_area);
frame.render_widget(info_block, info_block_area); frame.render_widget(info_block, info_block_area);
frame.render_widget(info, info_area); frame.render_widget(info, info_area);
frame.render_widget(debug_block, debug_block_area);
frame.render_widget(debug, debug_area);
} }
fn render_tiles(&self) -> Grid<TileStyle> { fn render_tiles(&self) -> Grid<TileStyle> {
@ -260,7 +305,7 @@ impl GameModel {
self.room self.room
.tiles .tiles
.get(P2::new(x as isize, y as isize)) .get(P2::new(x as isize, y as isize))
.unwrap() .expect("programmer error: copying a grid shouldnt yield OOB errors")
.style .style
.clone() .clone()
}); });
@ -273,7 +318,7 @@ impl GameModel {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
enum TileStyle { enum TileStyle {
Floor, Floor,
Player, Player,
@ -283,12 +328,12 @@ enum TileStyle {
Box, Box,
} }
#[derive(Clone)] #[derive(Clone, Debug)]
struct Portal { struct Portal {
kind: PortalKind, kind: PortalKind,
} }
#[derive(Clone)] #[derive(Clone, Debug)]
enum PortalKind { enum PortalKind {
Ladder, Ladder,
Portal, Portal,

View File

@ -104,4 +104,4 @@ impl<T> Grid<T> {
None None
} }
} }
} }

View File

@ -4,13 +4,18 @@ mod geometry;
use std::path::PathBuf; use std::path::PathBuf;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use crossterm::{event::{self, DisableMouseCapture, EnableMouseCapture}, execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}}; use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{ use ratatui::{
Terminal, Terminal,
prelude::{Backend, CrosstermBackend}, prelude::{Backend, CrosstermBackend},
}; };
use game::GameModel; use game::GameModel;
use game::UpdateResult;
fn main() -> Result<()> { fn main() -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
@ -33,14 +38,14 @@ fn main() -> Result<()> {
} }
fn main_loop<T: Backend>(terminal: &mut Terminal<T>) -> Result<PathBuf> { fn main_loop<T: Backend>(terminal: &mut Terminal<T>) -> Result<PathBuf> {
let mut model = GameModel::new(PathBuf::from(".")); let mut model = GameModel::new(PathBuf::from("."))?;
loop { loop {
terminal.draw(|frame| model.render(frame))?; terminal.draw(|frame| model.render(frame))?;
let term_event = event::read()?; let term_event = event::read()?;
match model.update(term_event) { match model.update(term_event, terminal)? {
Ok(new_model) => model = new_model, UpdateResult::NewModel(new_model) => model = new_model,
Err(path) => break Ok(path), UpdateResult::ShouldExit(path) => break Ok(path),
} }
} }
} }