use std::fmt::Debug;
use std::time::Duration;
use retroglyph_core::event::{Event, KeyCode};
use retroglyph_core::grid::Rect;
use retroglyph_core::{App, Backend, Flow, Frame, Terminal};
use retroglyph_widgets::Theme;
use turnbase::{Game, PlayerId};
use turnbase_match::Simulator;
use crate::dashboard::{Layout, actions_panel, draw_board_stats_log, draw_menu, print_rows};
pub trait PrintableGame: Game {
fn draw_viewport<B: Backend>(&self, view: &Self::View, term: &mut Terminal<B>, area: Rect);
fn get_stats(&self, view: &Self::View) -> Vec<(String, String)>;
fn format_action(&self, action: &Self::Action) -> String;
}
pub struct SimulationRunner<G: PrintableGame> {
simulator: Simulator<G>,
selected: usize,
ai_tick: Duration,
ai_elapsed: Duration,
viewer: Option<PlayerId>,
}
impl<G: PrintableGame> SimulationRunner<G> {
#[must_use]
pub fn new(simulator: Simulator<G>, ai_tick: Duration) -> Self {
let viewer = simulator.primary_human();
Self {
simulator,
selected: 0,
ai_tick,
ai_elapsed: Duration::ZERO,
viewer,
}
}
#[must_use]
pub fn is_terminal(&self) -> bool {
self.simulator.is_terminal()
}
#[must_use]
pub fn into_simulator(self) -> Simulator<G> {
self.simulator
}
}
impl<G, B> App<B> for SimulationRunner<G>
where
G: PrintableGame,
G::Action: Debug,
B: Backend,
{
fn update(&mut self, term: &mut Terminal<B>, frame: &Frame) -> Flow {
let events: Vec<Event> = term.drain_events().collect();
if key_pressed(&events, KeyCode::Escape) {
return Flow::Exit;
}
let over = self.simulator.is_terminal();
if over {
if key_pressed(&events, KeyCode::Enter) {
return Flow::Exit;
}
} else {
match self.simulator.awaiting_human() {
Some(player) => self.handle_human_input(player, &events),
None => self.tick_ai(frame),
}
}
term.reset_style();
let layout = Layout::new(term.area());
let view = self
.simulator
.game()
.view(self.simulator.state(), self.viewer);
let theme = Theme::DARK;
let _ = draw_board_stats_log(
self.simulator.game(),
&view,
self.simulator.log_history(),
term,
&layout,
theme,
0,
);
if over {
let inner = actions_panel(term, layout.actions, theme, None);
print_rows(
term,
inner,
0,
std::iter::once("match over -- Enter/Esc to exit".to_owned()),
);
} else if let Some(player) = self.simulator.awaiting_human() {
let actions = self
.simulator
.game()
.legal_actions(self.simulator.state(), player);
let game = self.simulator.game();
let labels: Vec<String> = actions.iter().map(|a| game.format_action(a)).collect();
let selected = self.selected.min(labels.len().saturating_sub(1));
let inner = actions_panel(
term,
layout.actions,
theme,
Some((selected + 1, labels.len())),
);
draw_menu(term, inner, 0, &labels, selected);
} else {
let inner = actions_panel(term, layout.actions, theme, None);
print_rows(
term,
inner,
0,
std::iter::once("(AI thinking...)".to_owned()),
);
}
let _ = term.present();
Flow::Continue
}
}
fn key_pressed(events: &[Event], code: KeyCode) -> bool {
events
.iter()
.any(|event| matches!(event, Event::Key(key) if key.is_down() && key.code == code))
}
impl<G> SimulationRunner<G>
where
G: PrintableGame,
G::Action: Debug,
{
fn handle_human_input(&mut self, player: turnbase::PlayerId, events: &[Event]) {
let count = self
.simulator
.game()
.legal_actions(self.simulator.state(), player)
.len();
if count > 0 {
self.selected = self.selected.min(count - 1);
}
let mut confirm = false;
for event in events {
let Event::Key(key) = event else { continue };
if !key.is_down() {
continue;
}
match key.code {
KeyCode::Up if count > 0 => {
self.selected = self.selected.checked_sub(1).unwrap_or(count - 1);
}
KeyCode::Down if count > 0 => {
self.selected = (self.selected + 1) % count;
}
KeyCode::Enter => confirm = true,
_ => {}
}
}
if confirm && count > 0 {
let mut actions = self
.simulator
.game()
.legal_actions(self.simulator.state(), player);
let action = actions.swap_remove(self.selected);
let _ = self.simulator.select_human_action(player, action);
self.selected = 0;
}
}
fn tick_ai(&mut self, frame: &Frame) {
self.ai_elapsed = self.ai_elapsed.saturating_add(frame.delta);
if self.ai_elapsed >= self.ai_tick {
self.ai_elapsed = Duration::ZERO;
let _ = self.simulator.step();
}
}
}
#[cfg(feature = "crossterm")]
pub fn run<G>(simulator: Simulator<G>, ai_tick: Duration) -> std::io::Result<()>
where
G: PrintableGame,
G::Action: Debug,
{
retroglyph_crossterm::Crossterm::run(SimulationRunner::new(simulator, ai_tick))
}