Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.cells feature #9

Merged
merged 23 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 18 additions & 20 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "cgol-tui"
version = "0.4.0"
authors = ["Jeromos Kovacs <iITsnot.me214@proton.me>"]
version = "0.5.2"
authors = ["Jeromos Kovacs <iitsnotme214@proton.me>"]
edition = "2021"
description = "Conway's Game of Life implementation with a TUI"
license = "MIT OR Apache-2.0"
Expand All @@ -12,6 +12,4 @@ categories = ["games"]

[dependencies]
fastrand = "2.1.1"
fern = "0.6.2"
log = "0.4.22"
ratatui = "0.28.1"
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
# Conway's Game of Life tui in Rust
# Conway's Game of Life TUI in Rust

## Usage

either
installation methods

- `cargo install cgol-tui`
- `cargo install --locked --git "https://github.com/JeromeSchmied/cgol-tui-rs"`
- clone the repo and run `cargo r`
- clone the repo and run `cargo install --locked --path .`

after

`[curl "https://conwaylife.com/patterns/<pattern>.cells" | ] cgol-tui [[-],<pattern>.cells,...]`
eg.:

- `cgol-tui`
- `curl https://conwaylife.com/patterns/fx153.cells | cgol-tui -` the `-` stands for `stdin`
- `cgol-tui my_own_pattern.cells fx153.cells`

## Sample

Expand All @@ -19,14 +28,15 @@ either
- [x] error handling
- [x] publishing to crates.io
- [x] changing to `Canvas` for rendering viewer block
- [ ] the ability to parse `.cells` files, from [conwaylife.com](https://conwaylife.com/patterns)
- [x] the ability to parse `.cells` files, from [conwaylife.com](https://conwaylife.com/patterns)
- [ ] display the names of patterns

## Acknowledgements

- The core of this app is adapted from the [Rust-Wasm tutorial](https://rustwasm.github.io/docs/book/).
- main dependencies:
- ratatui: ui
- crossterm: ratatui backend
- [ratatui](https://ratatui.rs): ui
- [crossterm](https://github.com/crossterm-rs/crossterm): ratatui backend

## License

Expand Down
148 changes: 77 additions & 71 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
pub use area::Area;
pub use cell::Cell;
use ratatui::crossterm::event::{self, poll, Event, KeyEventKind};
use ratatui::crossterm::event::{self, Event, KeyEventKind};
use ratatui::{backend::Backend, Terminal};
pub use shapes::HandleError;
use std::io;
use std::time::Duration;
pub use std::str::FromStr;
use std::{io, time::Duration};
pub use universe::Universe;

/// Default poll duration
pub const DEF_DUR: Duration = Duration::from_millis(400);
const DEF_DUR: Duration = Duration::from_millis(400);

mod area;
mod cell;
/// Keymaps to handle input events
mod kmaps;
/// Starting shapes
mod shapes;
pub mod shapes;
/// ui
mod ui;
/// Conway's Game of Life universe
Expand All @@ -24,47 +24,10 @@ mod universe;
#[cfg(test)]
mod tests;

impl App {
pub fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
let mut prev_poll_t = self.poll_t;

loop {
terminal.draw(|f| ui::ui(f, self))?;

// Wait up to `poll_t` for another event
if poll(self.poll_t)? {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
kmaps::QUIT => break,
kmaps::SLOWER => self.slower(false),
kmaps::FASTER => self.faster(false),
kmaps::PLAY_PAUSE => self.play_pause(&mut prev_poll_t),
kmaps::RESTART => self.restart(),
kmaps::NEXT => self.next(),
kmaps::PREV => self.prev(),
kmaps::RESET => *self = Self::default(),
_ => {}
}
} else {
// resize and restart
self.restart();
}
} else {
// Timeout expired, updating life state
self.tick();
}
}

Ok(())
}
}

pub struct App {
pub universe: Universe,
pub i: usize,
pub available_universes: Vec<Universe>,
universe: Universe,
i: usize,
pub poll_t: Duration,
pub paused: bool,
pub area: Area,
Expand All @@ -77,19 +40,46 @@ impl Default for App {
i: 0,
poll_t: DEF_DUR,
paused: false,
available_universes: shapes::all(),
}
}
}
impl App {
pub fn new(area: Area, universe: Universe, poll_t: Duration) -> Self {
pub fn with_universes(self, universes: Vec<Universe>) -> Self {
Self {
available_universes: [universes, shapes::all()].concat(),
..self
}
}
pub fn new(area: Area, available_universes: Vec<Universe>, poll_t: Duration) -> Self {
App {
area,
universe,
universe: available_universes[0].clone(),
i: 0,
poll_t,
paused: false,
available_universes,
}
}
pub fn len(&self) -> usize {
self.available_universes.len() + shapes::N
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn get(&self) -> Universe {
let true_len = self.available_universes.len();
if self.i < true_len {
self.available_universes
.get(self.i)
.expect("display area is too small to fit current shape")
.clone()
} else {
shapes::get_special(self.i - true_len, self.area)
}
}

// pub fn render_universe(&self) {
// println!("{}", self.universe);
// }
Expand All @@ -104,9 +94,8 @@ impl App {
self.paused = !self.paused;
}
pub fn restart(&mut self) {
self.universe = shapes::get(self.area, self.i)
.inspect_err(|e| log::error!("{e:?}"))
.expect("display area is too small to fit current shape");
let univ = self.get();
self.universe = Universe::from_figur(self.area, univ).unwrap();
}

pub fn tick(&mut self) {
Expand All @@ -133,37 +122,54 @@ impl App {
}

pub fn next(&mut self) {
if self.i + 1 == shapes::N as usize {
if self.i + 1 == self.len() {
self.i = 0;
} else {
self.i += 1;
}
if let Ok(shape) = shapes::get(self.area, self.i) {
self.universe = shape;
} else {
log::error!(
"couldn't switch to next shape: number of shapes: {}, idx: {}, universe: {:?}",
shapes::N,
self.i,
self.universe
);
}
self.restart();
}
pub fn prev(&mut self) {
if self.i > 0 {
self.i -= 1;
} else {
self.i = shapes::N as usize - 1;
self.i = self.len() - 1;
}
if let Ok(shape) = shapes::get(self.area, self.i) {
self.universe = shape;
} else {
log::error!(
"couldn't switch to previous shape: number of shapes: {}, idx: {}, universe: {:?}",
shapes::N,
self.i,
self.universe
);
self.restart();
}
pub fn run<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> io::Result<()> {
let mut prev_poll_t = self.poll_t;

loop {
terminal.draw(|f| ui::ui(f, self))?;

// Wait up to `poll_t` for another event
if event::poll(self.poll_t)? {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
kmaps::QUIT => break,
kmaps::SLOWER => self.slower(false),
kmaps::FASTER => self.faster(false),
kmaps::PLAY_PAUSE => self.play_pause(&mut prev_poll_t),
kmaps::RESTART => self.restart(),
kmaps::NEXT => self.next(),
kmaps::PREV => self.prev(),
kmaps::RESET => *self = Self::default(),
_ => {}
}
} else {
// resize and restart
self.restart();
}
} else {
// Timeout expired, updating life state
self.tick();
}
}

Ok(())
}
}
15 changes: 10 additions & 5 deletions src/app/cell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ impl From<bool> for Cell {
}
}
}
impl Cell {
fn toggle(&mut self) {
*self = match *self {
Cell::Dead => Cell::Alive,
Cell::Alive => Cell::Dead,
impl TryFrom<char> for Cell {
type Error = String;

fn try_from(ch: char) -> Result<Self, Self::Error> {
match ch {
'O' => Ok(Cell::Alive),
'.' => Ok(Cell::Dead),
_ => Err(format!(
"parse error: {ch:?} is an invalid character, should be either '.' or 'O'"
)),
}
}
}
Loading
Loading