Impl basic loop

This commit is contained in:
Paul Brinkmeier 2025-11-29 14:56:53 +01:00
parent c00dd3d37a
commit 7ef25034fd
3 changed files with 243 additions and 50 deletions

149
src/game.rs Normal file
View File

@ -0,0 +1,149 @@
use crossterm::event::{Event, KeyCode};
use ratatui::{
Frame, buffer::{Buffer, Cell}, layout::Rect, style::Color, widgets::{Block, Widget}
};
use crate::geometry::{Grid, P2, V2};
pub struct GameModel {
player_pos: P2<isize>,
room: Room,
}
struct Room {
size: V2<usize>,
}
impl GameModel {
pub fn new() -> Self {
Self {
player_pos: P2::new(0, 0),
room: Room {
size: V2::new(20, 10),
},
}
}
pub fn update(self, event: Event) -> Option<Self> {
match event {
Event::Key(key_event) => match key_event.code {
KeyCode::Char('q') => return None,
KeyCode::Char('j') => {
return Some(Self {
player_pos: self.player_pos + V2::new(0, 1),
..self
})
},
KeyCode::Char('k') => {
return Some(Self {
player_pos: self.player_pos + V2::new(0, -1),
..self
})
},
KeyCode::Char('h') => {
return Some(Self {
player_pos: self.player_pos + V2::new(-1, 0),
..self
})
},
KeyCode::Char('l') => {
return Some(Self {
player_pos: self.player_pos + V2::new(1, 0),
..self
})
},
_ => (),
},
_ => (),
}
Some(self)
}
pub fn render(&self, frame: &mut Frame) {
let camera_block = Block::bordered().title("camera");
let camera_area = camera_block.inner(frame.area());
let camera = CameraWidget::new(self);
frame.render_widget(camera_block, frame.area());
frame.render_widget(camera, camera_area);
}
fn render_tiles(&self) -> Grid<Tile> {
let mut tiles = Grid::from_fn(self.room.size.x, self.room.size.y, |_x, _y| Tile::Floor);
tiles
.get_mut(P2::new(1, 1))
.map(|tile| *tile = Tile::Ladder);
tiles
.get_mut(P2::new(5, 2))
.map(|tile| *tile = Tile::Amphora);
tiles
.get_mut(P2::new(8, 5))
.map(|tile| *tile = Tile::Frog);
tiles
.get_mut(self.player_pos)
.map(|tile| *tile = Tile::Player);
tiles
}
}
enum Tile {
Floor,
Ladder,
Player,
Amphora,
Frog,
}
impl Tile {
fn render(&self, cell: &mut Cell) {
match self {
Tile::Floor => {
cell.set_symbol(" ");
}
Tile::Ladder => {
cell.set_symbol("Ħ");
}
Tile::Player => {
cell.set_symbol("▲̶͈̊");
}
Tile::Amphora => {
cell.set_symbol("").set_fg(Color::LightRed);
}
Tile::Frog => {
cell.set_symbol("ä̃").set_fg(Color::Green);
}
}
}
}
struct CameraWidget<'a> {
model: &'a GameModel,
}
impl<'a> CameraWidget<'a> {
fn new(model: &'a GameModel) -> Self {
Self { model }
}
}
impl<'a> Widget for CameraWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let tiles = self.model.render_tiles();
for y in 0..area.height {
for x in 0..area.width {
let cell = &mut buf[(area.left() + x, area.top() + y)];
if let Some(tile) = tiles.get(P2::new(x as isize, y as isize)) {
tile.render(cell);
} else {
cell.set_symbol(".");
}
}
}
}
}

View File

@ -1,64 +1,106 @@
use std::ops::{Add, Div, Sub};
#[derive(Clone, Copy, Debug, PartialEq)]
struct P2 {
x: i32,
y: i32,
pub struct P2<T> {
x: T,
y: T,
}
impl P2 {
fn new(x: i32, y: i32) -> Self {
impl<T> P2<T> {
pub fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl Add<V2> for P2 {
type Output = P2;
fn add(self, rhs: V2) -> Self::Output {
impl<T: Copy + Add<T>> Add<V2<T>> for P2<T> {
type Output = P2<<T as Add>::Output>;
fn add(self, rhs: V2<T>) -> Self::Output {
P2::new(self.x + rhs.x, self.y + rhs.y)
}
}
impl Sub<P2> for P2 {
type Output = V2;
impl<T: Copy + Sub<T>> Sub<P2<T>> for P2<T> {
type Output = V2<<T as Sub>::Output>;
fn sub(self, rhs: P2) -> Self::Output {
fn sub(self, rhs: P2<T>) -> Self::Output {
V2::new(self.x - rhs.x, self.y - rhs.y)
}
}
impl Sub<V2> for P2 {
type Output = P2;
impl<T: Copy + Sub<T>> Sub<V2<T>> for P2<T> {
type Output = P2<<T as Sub>::Output>;
fn sub(self, rhs: V2) -> Self::Output {
fn sub(self, rhs: V2<T>) -> Self::Output {
P2::new(self.x - rhs.x, self.y - rhs.y)
}
}
impl Div<i32> for P2 {
type Output = P2;
impl<T: Copy + Div<T>> Div<T> for P2<T> {
type Output = P2<<T as Div>::Output>;
fn div(self, rhs: i32) -> Self::Output {
Self::new(self.x / rhs, self.y / rhs)
fn div(self, rhs: T) -> Self::Output {
P2::new(self.x / rhs, self.y / rhs)
}
}
#[derive(Debug)]
struct V2 {
x: i32,
y: i32,
pub struct V2<T> {
pub x: T,
pub y: T,
}
impl V2 {
fn new(x: i32, y: i32) -> Self {
impl<T> V2<T> {
pub fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl Div<i32> for V2 {
type Output = V2;
impl<T: Copy + Div<T>> Div<T> for V2<T> {
type Output = V2<<T as Div>::Output>;
fn div(self, rhs: i32) -> Self::Output {
fn div(self, rhs: T) -> Self::Output {
V2::new(self.x / rhs, self.y / rhs)
}
}
pub struct Grid<T> {
dims: V2<usize>,
elements: Vec<T>,
}
impl<T> Grid<T> {
pub fn from_fn(width: usize, height: usize, mut f: impl FnMut(usize, usize) -> T) -> Self {
let mut elements = Vec::new();
for y in 0..height {
for x in 0..width {
elements.push(f(x, y));
}
}
Self {
dims: V2::new(width, height),
elements,
}
}
pub fn get(&self, index: P2<isize>) -> Option<&T> {
self.valid_index(index).map(|index| &self.elements[index])
}
pub fn get_mut(&mut self, index: P2<isize>) -> Option<&mut T> {
self.valid_index(index)
.map(|index| &mut self.elements[index])
}
fn valid_index(&self, index: P2<isize>) -> Option<usize> {
if index.x >= 0
&& (index.x as usize) < self.dims.x
&& index.y >= 0
&& (index.y as usize) < self.dims.y
{
Some((index.y as usize) * self.dims.x + (index.x as usize))
} else {
None
}
}
}

View File

@ -1,29 +1,31 @@
use std::cmp::max;
mod game;
mod geometry;
mod noise;
mod terrain;
use noise::Noise;
use color_eyre::eyre::Result;
use crossterm::event;
use ratatui::DefaultTerminal;
fn main() {
let width = 50;
let height = 20;
use game::GameModel;
let long_side = max(width, height) as f32;
let mut rand = rand::rng();
let height_noise = Noise::from_freqs(&mut rand, 12, 16, 3);
let tree_cover_noise = Noise::from_freqs(&mut rand, 12, 16, 4);
for y in 0..height {
for x in 0..width {
let h = height_noise.sample(x as f32 / long_side, y as f32 / long_side);
let t = tree_cover_noise.sample(x as f32 / long_side, y as f32 / long_side);
let terrain = terrain::terrain(h, t);
print!("{}", terrain.printable());
}
println!();
}
fn main() -> Result<()> {
let mut terminal = ratatui::init();
let result = main_loop(&mut terminal);
ratatui::restore();
result
}
fn main_loop(terminal: &mut DefaultTerminal) -> Result<()> {
let mut model = GameModel::new();
loop {
terminal.draw(|frame| {
model.render(frame)
})?;
let term_event = event::read()?;
match model.update(term_event) {
Some(new_model) => model = new_model,
None => break Ok(()),
}
}
}