use std::{io::{stdout, Result, Stdout}, time::Duration};
use clap::Parser;
use nate_engine::{Engine, Renderer, system, world};
use rand::random;
use crossterm::{
event::{self, KeyCode, KeyEventKind},
terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen,
LeaveAlternateScreen,
},
ExecutableCommand,
};
use ratatui::{
prelude::*,
widgets::{canvas::Canvas, Block, Borders},
};
const WIDTH: isize = 212;
const MIN_X: isize = -106;
const MAX_X: isize = 105;
const HEIGHT: isize = 50;
const MIN_Y: isize = -25;
const MAX_Y: isize = 24;
const MAX_HEALTH: usize = 10;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Status {
Dead,
Low,
Medium,
High,
}
#[world(singular=[living_entities, canvas])]
pub struct ToyWorld {
position: (isize, isize),
velocity: (isize, isize),
acceleration: (isize, isize),
health: usize,
health_changes: isize,
living_entities: usize,
canvas: [[Status; WIDTH as usize]; HEIGHT as usize],
}
#[system(world=ToyWorld, write=[position, velocity, acceleration])]
fn position_update_system() {
let left_right = match random::<usize>() % 4 {
0 => 1,
1 => -1,
_ => 0,
};
let up_down = match random::<usize>() % 4 {
0 => 1,
1 => -1,
_ => 0,
};
*acceleration = (left_right, up_down);
*velocity = (velocity.0 + acceleration.0, velocity.1 + acceleration.1);
*position = (position.0 + velocity.0, position.1 + velocity.1);
}
#[system(world=ToyWorld, read=[position, health], _write=[canvas=[[Status::Dead; WIDTH as usize]; HEIGHT as usize]])]
fn update_canvas_system() {
let x = (position.0.clamp(MIN_X, MAX_X) + WIDTH / 2) as usize;
let y = (position.1.clamp(MIN_Y, MAX_Y) + HEIGHT / 2) as usize;
match health {
7.. => canvas[y][x] = Status::High,
4..=6 => canvas[y][x] = Status::Medium,
1..=3 => canvas[y][x] = Status::Low,
_ => (),
}
}
#[system(world=ToyWorld, read=[health_changes], write=[health])]
fn health_update_system() {
*health = health.saturating_add_signed(*health_changes).clamp(0, MAX_HEALTH);
}
#[system(world=ToyWorld, write=[health_changes])]
fn health_changes_system() {
if random() {
*health_changes = random::<isize>() % 2;
} else {
*health_changes = -random::<isize>() % 2;
}
}
#[system(world=ToyWorld, _write=[living_entities])]
fn alive_entities_display_system() {
*living_entities = world.health.read().unwrap().iter().map(|v| v.is_some() && v.unwrap() > 0).count();
}
pub struct ToyTerminalRenderer {
terminal: Terminal<CrosstermBackend<Stdout>>,
}
impl ToyTerminalRenderer {
pub fn new(terminal: Terminal<CrosstermBackend<Stdout>>) -> Self {
Self {
terminal,
}
}
}
impl Renderer<ToyWorld> for ToyTerminalRenderer {
type Error = String;
fn render(&mut self, world: std::sync::Arc<std::sync::RwLock<ToyWorld>>) -> std::prelude::v1::Result<(), Self::Error> {
let world = world.read().unwrap();
let _err = self.terminal.draw(|frame| {
let area = frame.size();
frame.render_widget(
Canvas::default()
.block(
Block::default()
.borders(Borders::ALL)
.title(
format!(
"Living Entities: {}",
(*world.living_entities.read().unwrap()).unwrap(),
)
)
)
.background_color(Color::Black)
.x_bounds([0.0, WIDTH as f64])
.y_bounds([0.0, HEIGHT as f64])
.paint(|ctx| {
let canvas = (*world.canvas.read().unwrap()).unwrap();
for (y, row) in canvas.iter().enumerate() {
for (x, item) in row.iter().enumerate() {
match *item {
Status::Dead => ctx.print(x as f64, y as f64, "_".gray()),
Status::Low => ctx.print(x as f64, y as f64, "X".red()),
Status::Medium => ctx.print(x as f64, y as f64, "X".yellow()),
Status::High => ctx.print(x as f64, y as f64, "X".green()),
}
}
}
}),
area
)
});
if event::poll(Duration::from_millis(5)).unwrap() {
if let event::Event::Key(key) = event::read().unwrap() {
if key.kind == KeyEventKind::Press &&
key.code == KeyCode::Char('q') {
return Err("Interrupt Pressed".into());
}
}
}
Ok(())
}
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
#[arg(short, long, default_value_t = 100_000)]
entities: usize,
#[arg(short, long, default_value_t = 3)]
workers: usize,
}
fn main() -> Result<()> {
let args = Args::parse();
let world = ToyWorld::new();
{
let mut world = world.write().unwrap();
let entity_ids = world.add_entities(args.entities);
let positions = entity_ids.iter().map(|_v| (random::<isize>().clamp(MIN_X, MAX_X), random::<isize>().clamp(MIN_Y, MAX_Y))).collect();
world.set_positions(&entity_ids, positions);
let velocities = entity_ids.iter().map(|_v| (0, 0)).collect();
world.set_velocitys(&entity_ids, velocities);
let accelerations = entity_ids.iter().map(|_v| (0, 0)).collect();
world.set_accelerations(&entity_ids, accelerations);
let healths = entity_ids.iter().map(|_v| random::<usize>() % 100).collect();
world.set_healths(&entity_ids, healths);
let health_changes = entity_ids.iter().map(|_v| 0).collect();
world.set_health_changess(&entity_ids, health_changes);
world.set_living_entities(args.entities);
world.set_canvas([[Status::Dead; WIDTH as usize]; HEIGHT as usize]);
}
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
let mut engine = Engine::new(
30,
args.workers,
world,
vec![
(position_update_system, 100_000),
(update_canvas_system, 100_000),
(health_update_system, 100_000),
(alive_entities_display_system, 100_000),
],
Box::new(ToyTerminalRenderer::new(terminal))
);
engine.run();
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}