use crate::app::{App, GenerationEvent, InputEvent};
use color_eyre::{Result, eyre::Context};
use crossbeam::channel;
use radiate_engines::{
Chromosome, Engine, EngineState, EngineStream, Generation, GenerationView, GeneticEngine,
error::RadiateResult, events::LogEvent, sync::IntoPair,
};
use radiate_engines::{EngineRuntime, EvolutionContext, ThreadSync};
use std::{
sync::{Arc, atomic::Ordering},
time::Duration,
};
const KEY_REPEAT_DELAY: Duration = Duration::from_millis(100);
pub struct TuiEngine<C, T>
where
C: Chromosome,
T: Clone + Send + Sync + 'static,
{
inner: GeneticEngine<C, T>,
control: ThreadSync,
dispatcher: channel::Sender<InputEvent<C>>,
app_thread: Option<std::thread::JoinHandle<Result<()>>>,
key_thread: Option<std::thread::JoinHandle<Result<()>>>,
}
impl<C, T> TuiEngine<C, T>
where
C: Chromosome + Clone + 'static,
T: Clone + Send + Sync + 'static,
{
pub fn new(mut inner: GeneticEngine<C, T>, render_interval: Duration) -> Self {
let control = inner.control();
let app = App::new(render_interval, control.clone());
let (dispatch_one, dispatch_two) = app.dispatcher().into_pair();
let stop_flag = control.stop_flag();
let app_thread = std::thread::spawn(move || {
let terminal = ratatui::init();
app.run(terminal)?;
ratatui::restore();
Ok(())
});
let key_thread = std::thread::spawn(move || {
while !stop_flag.load(Ordering::Relaxed) {
if crossterm::event::poll(KEY_REPEAT_DELAY)? {
let event = crossterm::event::read()?;
dispatch_two
.send(InputEvent::Crossterm(event))
.context("Failed to send Crossterm event")?;
}
}
Ok(())
});
Self {
inner,
control,
dispatcher: dispatch_one,
app_thread: Some(app_thread),
key_thread: Some(key_thread),
}
}
pub fn iter(self) -> EngineRuntime<Self> {
let dispatcher = self.dispatcher.clone();
EngineRuntime::new(self).subscribe::<LogEvent>(move |event: &LogEvent| {
dispatcher
.send(InputEvent::Log(event.0, event.1.clone()))
.map_err(|_| eprintln!("Failed to send log event: {:?}", event))
.unwrap();
})
}
}
impl<C, T> Engine for TuiEngine<C, T>
where
C: Chromosome + Clone + 'static,
T: Clone + Send + Sync + 'static,
{
type Ctx = EvolutionContext<C, T>;
type Epoch = Generation<C, T>;
fn context(&self) -> &Self::Ctx {
self.inner.context()
}
fn epoch(&self) -> Self::Epoch {
self.inner.epoch()
}
fn state(&self) -> EngineState {
self.inner.state()
}
fn start(&mut self) {
self.inner.start();
}
fn stop(&mut self) {
self.inner.stop();
}
#[inline]
fn step(&mut self) -> RadiateResult<()> {
self.inner.step()?;
let state = self.inner.state();
let current = self.inner.context();
if matches!(state, EngineState::Stopped) {
return Ok(());
}
if current.index() == 1 {
self.dispatcher
.send(InputEvent::EngineStart(Arc::clone(¤t.front())))
.unwrap();
}
let event = GenerationEvent::from(current);
self.dispatcher
.send(InputEvent::EpochComplete(event))
.unwrap();
Ok(())
}
}
impl<C, T> EngineStream for TuiEngine<C, T>
where
C: Chromosome + Clone + 'static,
T: Clone + Send + Sync + 'static,
{
type View<'a>
= GenerationView<'a, C, T>
where
Self: 'a;
fn run<F>(mut self, limit: F) -> RadiateResult<Self::Epoch>
where
F: Fn(Self::View<'_>) -> bool + 'static,
{
loop {
self.step()?;
if matches!(self.state(), EngineState::Stopped) {
break Ok(self.epoch());
}
let current = self.inner.context();
if limit(GenerationView::new(current)) {
break Ok(self.epoch());
}
}
}
}
impl<C, T> Drop for TuiEngine<C, T>
where
C: Chromosome,
T: Clone + Send + Sync + 'static,
{
fn drop(&mut self) {
if !self.control.is_stopped() {
self.dispatcher.send(InputEvent::EngineStop).unwrap();
}
self.control.set_paused(false);
if let Some(event_listener) = self.app_thread.take()
&& let Err(e) = event_listener.join()
{
eprintln!("Error joining app thread: {:?}", e);
}
self.control.stop();
if let Some(key_listener) = self.key_thread.take()
&& let Err(e) = key_listener.join()
{
eprintln!("Error joining key listener thread: {:?}", e);
}
}
}