1use anyhow::Result;
21use arc_swap::ArcSwap;
22use crossterm::event::{
23 self, Event, KeyCode as CtKeyCode, KeyEventKind, KeyModifiers, MouseButton as CtMouseButton,
24 MouseEvent as CtMouseEvent, MouseEventKind,
25};
26use rand::RngExt;
27use ratatui::{Terminal, prelude::*};
28use std::sync::{
29 Arc,
30 atomic::{AtomicBool, Ordering},
31 mpsc,
32};
33use std::thread;
34use std::time::{Duration, Instant};
35
36use crate::game::achievement::ACHIEVEMENTS;
37use crate::game::fingerer;
38use crate::game::fingerer::FINGERERS;
39use crate::game::powerup::{self, Powerup, PowerupKind};
40use crate::game::state::{GameState, TICK_HZ};
41use crate::game::tree::coord::TreeCoord;
42use crate::input::{
43 self, InputContext, InputEvent, KeyCode as InKeyCode, Modifiers, MouseButton as InMouseButton,
44 UiState, WheelDelta,
45};
46use crate::platform::Persistence;
47use crate::sim::{self, Action, SimGeometry};
48use crate::ui::{self, Mode};
49
50const SAVE_INTERVAL_TICKS: u64 = TICK_HZ as u64 * 10;
51const DEMO_GOLDEN_COOLDOWN: u32 = 40;
54const INPUT_POLL_MS: u64 = 16;
58const MAX_TICK_CATCHUP: u32 = 20;
62
63enum SimMsg {
67 DemoSetMode(Mode),
68 DemoQuit,
69}
70
71pub struct App {
72 state: GameState,
73 debug: bool,
74 demo_seconds: Option<u32>,
75 persistence: Persistence,
76}
77
78impl App {
79 pub fn new(
80 state: GameState,
81 debug: bool,
82 demo_seconds: Option<u32>,
83 persistence: Persistence,
84 ) -> Self {
85 Self {
86 state,
87 debug,
88 demo_seconds,
89 persistence,
90 }
91 }
92
93 pub fn run<B: Backend>(self, terminal: &mut Terminal<B>) -> Result<()>
94 where
95 B::Error: Send + Sync + 'static,
96 {
97 let App {
98 state,
99 debug,
100 demo_seconds,
101 persistence,
102 } = self;
103
104 let snapshot = Arc::new(ArcSwap::from_pointee(state.clone()));
105 let shutdown = Arc::new(AtomicBool::new(false));
106 let (action_tx, action_rx) = mpsc::channel::<Action>();
107 let (sim_msg_tx, sim_msg_rx) = mpsc::channel::<SimMsg>();
108
109 let sim_handle = {
110 let snapshot = snapshot.clone();
111 let shutdown = shutdown.clone();
112 thread::Builder::new()
113 .name("cuque-sim".into())
114 .spawn(move || {
115 sim_loop(
116 state,
117 snapshot,
118 action_rx,
119 sim_msg_tx,
120 shutdown,
121 demo_seconds,
122 persistence,
123 );
124 })
125 .expect("spawn sim thread")
126 };
127
128 let mut ui = UiState::new();
129 let mut layout: ui::DrawOutput = Default::default();
134 let mut actions: Vec<Action> = Vec::with_capacity(4);
137
138 while ui.running && !shutdown.load(Ordering::Relaxed) {
139 for msg in sim_msg_rx.try_iter() {
142 match msg {
143 SimMsg::DemoSetMode(m) => ui.mode = m,
144 SimMsg::DemoQuit => ui.running = false,
145 }
146 }
147
148 let current = snapshot.load_full();
149 terminal.draw(|f| {
150 layout = ui::draw(
151 f,
152 ¤t,
153 ui.mode,
154 ui.zoom_idx,
155 debug,
156 ui.last_mouse_pos,
157 &mut ui.tree_render,
158 ui.prestige_confirm_pending,
159 );
160 })?;
161
162 let _ = action_tx.send(Action::UpdateGeometry {
165 biscuit: layout.biscuit_rect,
166 powerups_paused: ui.mode == Mode::Tree,
167 });
168
169 if event::poll(Duration::from_millis(INPUT_POLL_MS))? {
170 let ctx = InputContext::from_layout(&layout, ¤t, debug);
171 loop {
172 let ev = event::read()?;
173 if let Some(input_ev) = translate_crossterm(ev) {
174 actions.clear();
175 input::process_input_event(input_ev, &mut ui, &ctx, &mut actions);
176 for a in actions.drain(..) {
177 let _ = action_tx.send(a);
178 }
179 }
180 if !event::poll(Duration::ZERO)? {
181 break;
182 }
183 }
184 }
185 }
186
187 shutdown.store(true, Ordering::Relaxed);
189 drop(action_tx);
190 sim_handle.join().expect("sim thread panicked");
191 Ok(())
192 }
193}
194
195fn sim_loop(
198 mut state: GameState,
199 snapshot: Arc<ArcSwap<GameState>>,
200 actions: mpsc::Receiver<Action>,
201 sim_msg_tx: mpsc::Sender<SimMsg>,
202 shutdown: Arc<AtomicBool>,
203 demo_seconds: Option<u32>,
204 persistence: Persistence,
205) {
206 let tick_dt = Duration::from_micros(1_000_000 / TICK_HZ as u64);
207 let mut next_tick = Instant::now() + tick_dt;
208 let mut ticks_since_save: u64 = 0;
209 let mut demo_ticks: u64 = 0;
210 let mut demo_golden_spawns: u32 = 0;
211 let mut geom = SimGeometry::default();
212
213 loop {
214 if shutdown.load(Ordering::Relaxed) {
215 break;
216 }
217
218 let timeout = next_tick.saturating_duration_since(Instant::now());
221 match actions.recv_timeout(timeout) {
222 Ok(action) => sim::apply_action(&mut state, action, &mut geom),
223 Err(mpsc::RecvTimeoutError::Timeout) => {}
224 Err(mpsc::RecvTimeoutError::Disconnected) => break,
225 }
226
227 let mut catchup = 0u32;
231 while Instant::now() >= next_tick {
232 sim::sim_tick(&mut state, &geom);
233 if demo_seconds.is_some() {
237 demo_driver_tick(
238 &mut state,
239 &geom,
240 demo_seconds,
241 &mut demo_ticks,
242 &mut demo_golden_spawns,
243 &sim_msg_tx,
244 );
245 } else {
246 ticks_since_save += 1;
247 if ticks_since_save >= SAVE_INTERVAL_TICKS {
248 ticks_since_save = 0;
249 let _ = persistence.save(&state);
250 }
251 }
252 next_tick += tick_dt;
253 catchup += 1;
254 if catchup >= MAX_TICK_CATCHUP && Instant::now() > next_tick {
255 next_tick = Instant::now() + tick_dt;
256 break;
257 }
258 }
259
260 snapshot.store(Arc::new(state.clone()));
263 }
264
265 if demo_seconds.is_none() {
268 state.tick_achievements();
269 let _ = persistence.save(&state);
270 }
271}
272
273fn demo_driver_tick(
277 state: &mut GameState,
278 geom: &SimGeometry,
279 demo_seconds: Option<u32>,
280 demo_ticks: &mut u64,
281 demo_golden_spawns: &mut u32,
282 sim_msg_tx: &mpsc::Sender<SimMsg>,
283) {
284 *demo_ticks += 1;
285 let t = *demo_ticks;
286 let mut rng = rand::rng();
287
288 if t.is_multiple_of(13) {
290 let r = geom.biscuit;
291 if r.width > 0 && r.height > 0 {
292 state.click((r.x + r.width / 2, r.y + r.height / 2), r);
293 }
294 }
295
296 if state.powerups.is_empty() && (*demo_ticks).is_multiple_of(DEMO_GOLDEN_COOLDOWN as u64) {
304 let kind = match *demo_golden_spawns % 3 {
305 0 => PowerupKind::Buff,
306 1 => PowerupKind::Frenzy,
307 _ => PowerupKind::Lucky,
308 };
309 *demo_golden_spawns += 1;
310 let r = geom.biscuit;
311 if r.width >= 8 && r.height >= 5 {
312 let spawn_id = state.mint_spawn_id();
313 state.powerups.push(Powerup {
314 kind,
315 spawn_id,
316 frac_x: 0.5,
317 frac_y: 0.4,
318 life_ticks: kind.lifetime_ticks(),
319 });
320 }
321 }
322
323 let to_catch: Vec<u64> = state
327 .powerups
328 .iter()
329 .filter(|p| p.life_ticks + 20 < p.kind.lifetime_ticks())
330 .map(|p| p.spawn_id)
331 .collect();
332 for id in to_catch {
333 state.catch_powerup(id);
334 }
335
336 if t.is_multiple_of(80) {
338 let candidates: Vec<usize> = (0..fingerer::count())
339 .filter(|&i| state.can_buy(i))
340 .collect();
341 if !candidates.is_empty() {
342 let idx = candidates[rng.random_range(0..candidates.len())];
343 state.buy_n(idx, rng.random_range(1..=2));
344 }
345 }
346
347 if t.is_multiple_of(160) {
352 let mut best: Option<(TreeCoord, crate::bignum::Mag)> = None;
353 for &owned in &state.tree.bought {
354 for n in crate::game::tree::node::neighbors_of(owned) {
355 if state.tree.bought.contains(&n) {
356 continue;
357 }
358 if !crate::game::tree::node::edge_exists(owned, n) {
359 continue;
360 }
361 if let Some(spec) = crate::game::tree::node::node_at(n.x, n.y)
362 && state.affordable_cuques() >= spec.cost
363 {
364 let cost = spec.cost;
365 if best.map(|(_, c)| cost < c).unwrap_or(true) {
366 best = Some((n, cost));
367 }
368 }
369 }
370 }
371 if state.tree.bought.is_empty() {
373 if let Some(spec) = crate::game::tree::node::node_at(0, 0)
374 && state.affordable_cuques() >= spec.cost
375 {
376 state.buy_tree_node(TreeCoord::ORIGIN);
377 }
378 } else if let Some((lot, _)) = best {
379 state.buy_tree_node(lot);
380 }
381 }
382
383 let phase = t % 300;
385 let panel_swap = if phase == 100 {
386 Some(Mode::Stats)
387 } else if phase == 140 {
388 Some(Mode::Achievements)
389 } else if phase == 180 {
390 Some(Mode::Tree)
391 } else if phase == 220 {
392 Some(Mode::Game)
393 } else {
394 None
395 };
396 if let Some(m) = panel_swap {
397 let _ = sim_msg_tx.send(SimMsg::DemoSetMode(m));
398 }
399
400 if let Some(secs) = demo_seconds
403 && t >= (secs as u64) * (TICK_HZ as u64)
404 {
405 let _ = sim_msg_tx.send(SimMsg::DemoQuit);
406 }
407}
408
409fn translate_crossterm(ev: Event) -> Option<InputEvent> {
415 match ev {
416 Event::Key(k) if k.kind == KeyEventKind::Press => {
417 let code = translate_key_code(k.code)?;
418 Some(InputEvent::KeyPress {
419 code,
420 mods: translate_mods(k.modifiers),
421 })
422 }
423 Event::Mouse(m) => translate_mouse(m),
424 _ => None,
425 }
426}
427
428fn translate_key_code(code: CtKeyCode) -> Option<InKeyCode> {
429 match code {
430 CtKeyCode::Char(c) => Some(InKeyCode::Char(c)),
431 CtKeyCode::Esc => Some(InKeyCode::Esc),
432 CtKeyCode::F(n) => Some(InKeyCode::F(n)),
433 CtKeyCode::Up => Some(InKeyCode::Up),
434 CtKeyCode::Down => Some(InKeyCode::Down),
435 CtKeyCode::Left => Some(InKeyCode::Left),
436 CtKeyCode::Right => Some(InKeyCode::Right),
437 CtKeyCode::Enter => Some(InKeyCode::Enter),
438 _ => None,
439 }
440}
441
442fn translate_mods(mods: KeyModifiers) -> Modifiers {
443 Modifiers {
444 shift: mods.contains(KeyModifiers::SHIFT),
445 alt: mods.contains(KeyModifiers::ALT),
446 ctrl: mods.contains(KeyModifiers::CONTROL),
447 }
448}
449
450fn translate_mouse_button(button: CtMouseButton) -> Option<InMouseButton> {
455 match button {
456 CtMouseButton::Left => Some(InMouseButton::Left),
457 CtMouseButton::Right => Some(InMouseButton::Right),
458 CtMouseButton::Middle => None,
459 }
460}
461
462fn translate_mouse(m: CtMouseEvent) -> Option<InputEvent> {
463 let mods = translate_mods(m.modifiers);
464 match m.kind {
465 MouseEventKind::Down(button) => Some(InputEvent::MouseDown {
466 col: m.column,
467 row: m.row,
468 button: translate_mouse_button(button)?,
469 mods,
470 }),
471 MouseEventKind::Up(button) => Some(InputEvent::MouseUp {
472 col: m.column,
473 row: m.row,
474 button: translate_mouse_button(button)?,
475 }),
476 MouseEventKind::ScrollUp => Some(InputEvent::Wheel {
477 col: m.column,
478 row: m.row,
479 delta: WheelDelta::Up,
480 }),
481 MouseEventKind::ScrollDown => Some(InputEvent::Wheel {
482 col: m.column,
483 row: m.row,
484 delta: WheelDelta::Down,
485 }),
486 MouseEventKind::Moved | MouseEventKind::Drag(CtMouseButton::Left) => {
491 Some(InputEvent::MouseMoved {
492 col: m.column,
493 row: m.row,
494 })
495 }
496 _ => None,
497 }
498}
499
500pub fn build_demo_state() -> GameState {
509 let mut s = GameState {
510 cuques: crate::bignum::Mag::from_f64(500_000.0),
513 lifetime_cuques: crate::bignum::Mag::from_f64(500_000_000.0),
514 total_clicks: 500,
515 total_play_ticks: 3600 * TICK_HZ as u64,
516 prestige: 3,
517 golden_caught: 7,
518 powerup_cooldowns: [0; powerup::N_KINDS],
519 best_fps: crate::bignum::Mag::from_f64(50_000.0),
520 ..GameState::default()
521 };
522 const DEMO_FINGERER_COUNTS: &[u32] = &[40, 40, 35, 30, 25, 20, 15, 10];
530 for (idx, &count) in DEMO_FINGERER_COUNTS.iter().enumerate() {
531 if let Some(f) = FINGERERS.get(idx)
532 && count > 0
533 {
534 s.fingerers_state.entry(f.id.to_string()).or_default().count = count;
535 }
536 }
537 for lot in [
543 TreeCoord::ORIGIN,
544 TreeCoord::new(1, 0),
545 TreeCoord::new(0, 1),
546 TreeCoord::new(-1, 0),
547 TreeCoord::new(0, -1),
548 TreeCoord::new(1, 1),
549 TreeCoord::new(-1, -1),
550 ] {
551 if let Some(spec) = crate::game::tree::node::node_at(lot.x, lot.y) {
552 s.tree.bought.insert(lot);
556 s.tree_aggregate.fold_in_node(&spec);
557 }
558 }
559 for a in ACHIEVEMENTS.iter().take(6) {
561 s.achievements_earned.insert(a.id.to_string());
562 }
563 s
564}