use anyhow::Result;
use arc_swap::ArcSwap;
use crossterm::event::{
self, Event, KeyCode as CtKeyCode, KeyEventKind, KeyModifiers, MouseButton as CtMouseButton,
MouseEvent as CtMouseEvent, MouseEventKind,
};
use rand::RngExt;
use ratatui::{Terminal, prelude::*};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
mpsc,
};
use std::thread;
use std::time::{Duration, Instant};
use crate::game::achievement::ACHIEVEMENTS;
use crate::game::fingerer;
use crate::game::fingerer::FINGERERS;
use crate::game::powerup::{self, Powerup, PowerupKind};
use crate::game::state::{GameState, TICK_HZ};
use crate::game::tree::coord::TreeCoord;
use crate::input::{
self, InputContext, InputEvent, KeyCode as InKeyCode, Modifiers, MouseButton as InMouseButton,
UiState, WheelDelta,
};
use crate::platform::Persistence;
use crate::sim::{self, Action, SimGeometry};
use crate::ui::{self, Mode};
const SAVE_INTERVAL_TICKS: u64 = TICK_HZ as u64 * 10;
const DEMO_GOLDEN_COOLDOWN: u32 = 40;
const INPUT_POLL_MS: u64 = 16;
const MAX_TICK_CATCHUP: u32 = 20;
enum SimMsg {
DemoSetMode(Mode),
DemoQuit,
}
pub struct App {
state: GameState,
debug: bool,
demo_seconds: Option<u32>,
persistence: Persistence,
}
impl App {
pub fn new(
state: GameState,
debug: bool,
demo_seconds: Option<u32>,
persistence: Persistence,
) -> Self {
Self {
state,
debug,
demo_seconds,
persistence,
}
}
pub fn run<B: Backend>(self, terminal: &mut Terminal<B>) -> Result<()>
where
B::Error: Send + Sync + 'static,
{
let App {
state,
debug,
demo_seconds,
persistence,
} = self;
let snapshot = Arc::new(ArcSwap::from_pointee(state.clone()));
let shutdown = Arc::new(AtomicBool::new(false));
let (action_tx, action_rx) = mpsc::channel::<Action>();
let (sim_msg_tx, sim_msg_rx) = mpsc::channel::<SimMsg>();
let sim_handle = {
let snapshot = snapshot.clone();
let shutdown = shutdown.clone();
thread::Builder::new()
.name("cuque-sim".into())
.spawn(move || {
sim_loop(
state,
snapshot,
action_rx,
sim_msg_tx,
shutdown,
demo_seconds,
persistence,
);
})
.expect("spawn sim thread")
};
let mut ui = UiState::new();
let mut layout: ui::DrawOutput = Default::default();
let mut actions: Vec<Action> = Vec::with_capacity(4);
while ui.running && !shutdown.load(Ordering::Relaxed) {
for msg in sim_msg_rx.try_iter() {
match msg {
SimMsg::DemoSetMode(m) => ui.mode = m,
SimMsg::DemoQuit => ui.running = false,
}
}
let current = snapshot.load_full();
terminal.draw(|f| {
layout = ui::draw(
f,
¤t,
ui.mode,
ui.zoom_idx,
debug,
ui.last_mouse_pos,
&mut ui.tree_render,
ui.prestige_confirm_pending,
);
})?;
let _ = action_tx.send(Action::UpdateGeometry {
biscuit: layout.biscuit_rect,
powerups_paused: ui.mode == Mode::Tree,
});
if event::poll(Duration::from_millis(INPUT_POLL_MS))? {
let ctx = InputContext::from_layout(&layout, ¤t, debug);
loop {
let ev = event::read()?;
if let Some(input_ev) = translate_crossterm(ev) {
actions.clear();
input::process_input_event(input_ev, &mut ui, &ctx, &mut actions);
for a in actions.drain(..) {
let _ = action_tx.send(a);
}
}
if !event::poll(Duration::ZERO)? {
break;
}
}
}
}
shutdown.store(true, Ordering::Relaxed);
drop(action_tx);
sim_handle.join().expect("sim thread panicked");
Ok(())
}
}
fn sim_loop(
mut state: GameState,
snapshot: Arc<ArcSwap<GameState>>,
actions: mpsc::Receiver<Action>,
sim_msg_tx: mpsc::Sender<SimMsg>,
shutdown: Arc<AtomicBool>,
demo_seconds: Option<u32>,
persistence: Persistence,
) {
let tick_dt = Duration::from_micros(1_000_000 / TICK_HZ as u64);
let mut next_tick = Instant::now() + tick_dt;
let mut ticks_since_save: u64 = 0;
let mut demo_ticks: u64 = 0;
let mut demo_golden_spawns: u32 = 0;
let mut geom = SimGeometry::default();
loop {
if shutdown.load(Ordering::Relaxed) {
break;
}
let timeout = next_tick.saturating_duration_since(Instant::now());
match actions.recv_timeout(timeout) {
Ok(action) => sim::apply_action(&mut state, action, &mut geom),
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
let mut catchup = 0u32;
while Instant::now() >= next_tick {
sim::sim_tick(&mut state, &geom);
if demo_seconds.is_some() {
demo_driver_tick(
&mut state,
&geom,
demo_seconds,
&mut demo_ticks,
&mut demo_golden_spawns,
&sim_msg_tx,
);
} else {
ticks_since_save += 1;
if ticks_since_save >= SAVE_INTERVAL_TICKS {
ticks_since_save = 0;
let _ = persistence.save(&state);
}
}
next_tick += tick_dt;
catchup += 1;
if catchup >= MAX_TICK_CATCHUP && Instant::now() > next_tick {
next_tick = Instant::now() + tick_dt;
break;
}
}
snapshot.store(Arc::new(state.clone()));
}
if demo_seconds.is_none() {
state.tick_achievements();
let _ = persistence.save(&state);
}
}
fn demo_driver_tick(
state: &mut GameState,
geom: &SimGeometry,
demo_seconds: Option<u32>,
demo_ticks: &mut u64,
demo_golden_spawns: &mut u32,
sim_msg_tx: &mpsc::Sender<SimMsg>,
) {
*demo_ticks += 1;
let t = *demo_ticks;
let mut rng = rand::rng();
if t.is_multiple_of(13) {
let r = geom.biscuit;
if r.width > 0 && r.height > 0 {
state.click((r.x + r.width / 2, r.y + r.height / 2), r);
}
}
if state.powerups.is_empty() && (*demo_ticks).is_multiple_of(DEMO_GOLDEN_COOLDOWN as u64) {
let kind = match *demo_golden_spawns % 3 {
0 => PowerupKind::Buff,
1 => PowerupKind::Frenzy,
_ => PowerupKind::Lucky,
};
*demo_golden_spawns += 1;
let r = geom.biscuit;
if r.width >= 8 && r.height >= 5 {
let spawn_id = state.mint_spawn_id();
state.powerups.push(Powerup {
kind,
spawn_id,
frac_x: 0.5,
frac_y: 0.4,
life_ticks: kind.lifetime_ticks(),
});
}
}
let to_catch: Vec<u64> = state
.powerups
.iter()
.filter(|p| p.life_ticks + 20 < p.kind.lifetime_ticks())
.map(|p| p.spawn_id)
.collect();
for id in to_catch {
state.catch_powerup(id);
}
if t.is_multiple_of(80) {
let candidates: Vec<usize> = (0..fingerer::count())
.filter(|&i| state.can_buy(i))
.collect();
if !candidates.is_empty() {
let idx = candidates[rng.random_range(0..candidates.len())];
state.buy_n(idx, rng.random_range(1..=2));
}
}
if t.is_multiple_of(160) {
let mut best: Option<(TreeCoord, crate::bignum::Mag)> = None;
for &owned in &state.tree.bought {
for n in crate::game::tree::node::neighbors_of(owned) {
if state.tree.bought.contains(&n) {
continue;
}
if !crate::game::tree::node::edge_exists(owned, n) {
continue;
}
if let Some(spec) = crate::game::tree::node::node_at(n.x, n.y)
&& state.affordable_cuques() >= spec.cost
{
let cost = spec.cost;
if best.map(|(_, c)| cost < c).unwrap_or(true) {
best = Some((n, cost));
}
}
}
}
if state.tree.bought.is_empty() {
if let Some(spec) = crate::game::tree::node::node_at(0, 0)
&& state.affordable_cuques() >= spec.cost
{
state.buy_tree_node(TreeCoord::ORIGIN);
}
} else if let Some((lot, _)) = best {
state.buy_tree_node(lot);
}
}
let phase = t % 300;
let panel_swap = if phase == 100 {
Some(Mode::Stats)
} else if phase == 140 {
Some(Mode::Achievements)
} else if phase == 180 {
Some(Mode::Tree)
} else if phase == 220 {
Some(Mode::Game)
} else {
None
};
if let Some(m) = panel_swap {
let _ = sim_msg_tx.send(SimMsg::DemoSetMode(m));
}
if let Some(secs) = demo_seconds
&& t >= (secs as u64) * (TICK_HZ as u64)
{
let _ = sim_msg_tx.send(SimMsg::DemoQuit);
}
}
fn translate_crossterm(ev: Event) -> Option<InputEvent> {
match ev {
Event::Key(k) if k.kind == KeyEventKind::Press => {
let code = translate_key_code(k.code)?;
Some(InputEvent::KeyPress {
code,
mods: translate_mods(k.modifiers),
})
}
Event::Mouse(m) => translate_mouse(m),
_ => None,
}
}
fn translate_key_code(code: CtKeyCode) -> Option<InKeyCode> {
match code {
CtKeyCode::Char(c) => Some(InKeyCode::Char(c)),
CtKeyCode::Esc => Some(InKeyCode::Esc),
CtKeyCode::F(n) => Some(InKeyCode::F(n)),
CtKeyCode::Up => Some(InKeyCode::Up),
CtKeyCode::Down => Some(InKeyCode::Down),
CtKeyCode::Left => Some(InKeyCode::Left),
CtKeyCode::Right => Some(InKeyCode::Right),
CtKeyCode::Enter => Some(InKeyCode::Enter),
_ => None,
}
}
fn translate_mods(mods: KeyModifiers) -> Modifiers {
Modifiers {
shift: mods.contains(KeyModifiers::SHIFT),
alt: mods.contains(KeyModifiers::ALT),
ctrl: mods.contains(KeyModifiers::CONTROL),
}
}
fn translate_mouse_button(button: CtMouseButton) -> Option<InMouseButton> {
match button {
CtMouseButton::Left => Some(InMouseButton::Left),
CtMouseButton::Right => Some(InMouseButton::Right),
CtMouseButton::Middle => None,
}
}
fn translate_mouse(m: CtMouseEvent) -> Option<InputEvent> {
let mods = translate_mods(m.modifiers);
match m.kind {
MouseEventKind::Down(button) => Some(InputEvent::MouseDown {
col: m.column,
row: m.row,
button: translate_mouse_button(button)?,
mods,
}),
MouseEventKind::Up(button) => Some(InputEvent::MouseUp {
col: m.column,
row: m.row,
button: translate_mouse_button(button)?,
}),
MouseEventKind::ScrollUp => Some(InputEvent::Wheel {
col: m.column,
row: m.row,
delta: WheelDelta::Up,
}),
MouseEventKind::ScrollDown => Some(InputEvent::Wheel {
col: m.column,
row: m.row,
delta: WheelDelta::Down,
}),
MouseEventKind::Moved | MouseEventKind::Drag(CtMouseButton::Left) => {
Some(InputEvent::MouseMoved {
col: m.column,
row: m.row,
})
}
_ => None,
}
}
pub fn build_demo_state() -> GameState {
let mut s = GameState {
cuques: crate::bignum::Mag::from_f64(500_000.0),
lifetime_cuques: crate::bignum::Mag::from_f64(500_000_000.0),
total_clicks: 500,
total_play_ticks: 3600 * TICK_HZ as u64,
prestige: 3,
golden_caught: 7,
powerup_cooldowns: [0; powerup::N_KINDS],
best_fps: crate::bignum::Mag::from_f64(50_000.0),
..GameState::default()
};
const DEMO_FINGERER_COUNTS: &[u32] = &[40, 40, 35, 30, 25, 20, 15, 10];
for (idx, &count) in DEMO_FINGERER_COUNTS.iter().enumerate() {
if let Some(f) = FINGERERS.get(idx)
&& count > 0
{
s.fingerers_state.entry(f.id.to_string()).or_default().count = count;
}
}
for lot in [
TreeCoord::ORIGIN,
TreeCoord::new(1, 0),
TreeCoord::new(0, 1),
TreeCoord::new(-1, 0),
TreeCoord::new(0, -1),
TreeCoord::new(1, 1),
TreeCoord::new(-1, -1),
] {
if let Some(spec) = crate::game::tree::node::node_at(lot.x, lot.y) {
s.tree.bought.insert(lot);
s.tree_aggregate.fold_in_node(&spec);
}
}
for a in ACHIEVEMENTS.iter().take(6) {
s.achievements_earned.insert(a.id.to_string());
}
s
}