From 7fb001faabddada3e6dc8c8bcde16e78e51e8e2b Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Sun, 3 May 2026 18:14:46 +0200 Subject: [PATCH 1/2] Add tick thread and Skirmish state update Introduce `AppEvent::Tick` and a background thread that emits timed events. `App` now handles `Tick` by calling a new `update` method, which advances the board and increments a `turn_counter` in `SkirmishState`. Add required imports, fields, and the `spawn_tick_thread` call in `main`. --- src/app/app.rs | 8 ++++++++ src/app/state.rs | 1 + src/app/states/skirmish.rs | 13 ++++++++++++- src/app/threads/events.rs | 1 + src/app/threads/mod.rs | 2 ++ src/app/threads/tick.rs | 22 ++++++++++++++++++++++ src/main.rs | 10 ++++++++-- 7 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 src/app/threads/tick.rs diff --git a/src/app/app.rs b/src/app/app.rs index bf84680..944529e 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -65,6 +65,7 @@ impl App { .board .change_resize(&window_area, state.skirmish.side_panel); } + AppEvent::Tick => self.update()?, } } @@ -90,4 +91,11 @@ impl App { handle_keybindings(self, key_event); Ok(()) } + + fn update(&mut self) -> Result<()> { + if let Some(state) = self.states_mut() { + state.skirmish.update(&self.args); + } + Ok(()) + } } diff --git a/src/app/state.rs b/src/app/state.rs index 870a269..8eed5ba 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -35,6 +35,7 @@ impl GameStates { false, ), side_panel: false, + turn_counter: 0, }, perk_decks: PerkDecksState { id: 2, diff --git a/src/app/states/skirmish.rs b/src/app/states/skirmish.rs index df072c0..e7c37be 100644 --- a/src/app/states/skirmish.rs +++ b/src/app/states/skirmish.rs @@ -1,4 +1,4 @@ -use crate::app::states::skirmish_states::BoardState; +use crate::{app::states::skirmish_states::BoardState, cli::Cli}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SkirmishState { @@ -6,4 +6,15 @@ pub struct SkirmishState { pub name: &'static str, pub board: BoardState, pub side_panel: bool, + pub turn_counter: u64, +} + +impl SkirmishState { + pub fn update(&mut self, cli: &Cli) { + self.board.advance_turn(); + + // if self.board.is_victory() {} + + self.turn_counter += 1; + } } diff --git a/src/app/threads/events.rs b/src/app/threads/events.rs index 57f17c4..3fbcf9b 100644 --- a/src/app/threads/events.rs +++ b/src/app/threads/events.rs @@ -5,6 +5,7 @@ use std::sync::mpsc::Sender; pub enum AppEvent { Input(KeyEvent), Resize(u16, u16), + Tick, } /// Reads *all* crossterm events and forwards the ones we care about. diff --git a/src/app/threads/mod.rs b/src/app/threads/mod.rs index b2d0250..f2b2e4b 100644 --- a/src/app/threads/mod.rs +++ b/src/app/threads/mod.rs @@ -1,5 +1,7 @@ mod audio; mod events; +mod tick; pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio}; pub use events::{AppEvent, handle_events}; +pub use tick::spawn_tick_thread; diff --git a/src/app/threads/tick.rs b/src/app/threads/tick.rs new file mode 100644 index 0000000..b0e7277 --- /dev/null +++ b/src/app/threads/tick.rs @@ -0,0 +1,22 @@ +use crate::app::threads::AppEvent; +use std::{ + sync::mpsc::Sender, + thread, + time::{Duration, Instant}, +}; + +pub fn spawn_tick_thread(tx: Sender, interval_ms: u64) { + thread::spawn(move || { + let interval: Duration = Duration::from_millis(interval_ms); + loop { + if tx.send(AppEvent::Tick).is_err() { + break; + } + + let elapsed: Duration = Instant::now().elapsed(); + if interval > elapsed { + thread::sleep(interval - elapsed); + } + } + }); +} diff --git a/src/main.rs b/src/main.rs index 09b9723..3b037d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,12 +10,14 @@ use std::{ use war_in_tunnels::{ app::{ App, - threads::{AppEvent, AudioCmd, handle_audio, handle_events}, + threads::{AppEvent, AudioCmd, handle_audio, handle_events, spawn_tick_thread}, }, cli::{Cli, get_args}, logs::init_logger, }; +const TICK_MS: u64 = 33; + /// Starts the terminal UI application. /// /// The function follows the steps outlined in the module‑level documentation. @@ -34,7 +36,11 @@ fn main() -> Result<()> { // }); thread::spawn(move || { - handle_events(app_event_tx); + handle_events(app_event_tx.clone()); + }); + + thread::spawn(move || { + spawn_tick_thread(app_event_tx, TICK_MS); }); let (audio_tx, audio_rx) = channel::(); From 7541db79d8f8c74ee45ce12fc718341c9c3a7d30 Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Sun, 3 May 2026 22:27:08 +0200 Subject: [PATCH 2/2] Refactor App to use unified AppChannels for events App now receives an AppChannels struct; AppEvent enum removed. Event handling, tick generation, and audio communication are now performed through dedicated channels. --- src/app/app.rs | 41 +++++++++++-------------- src/app/states/skirmish.rs | 6 ++-- src/app/threads/app_channels.rs | 37 +++++++++++++++++++++++ src/app/threads/events.rs | 32 +++++++------------- src/app/threads/mod.rs | 6 ++-- src/app/threads/tick.rs | 25 +++++++--------- src/app/views/skirmish.rs | 3 +- src/cli.rs | 25 +++++++++++----- src/main.rs | 53 +++++++++++++-------------------- 9 files changed, 123 insertions(+), 105 deletions(-) create mode 100644 src/app/threads/app_channels.rs diff --git a/src/app/app.rs b/src/app/app.rs index 944529e..1fe22e5 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,17 +1,13 @@ use crate::{ app::{ GameStates, handle_keybindings, - threads::{AppEvent, AudioCmd}, + threads::{AppChannels, AudioCmd}, view::View, }, cli::Cli, }; use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect}; -use std::{ - io::Result, - sync::mpsc::{Receiver, RecvTimeoutError, Sender}, - time::Duration, -}; +use std::{io::Result, sync::mpsc::Sender, thread::sleep, time::Duration}; pub struct App { pub exit: bool, @@ -42,31 +38,29 @@ impl App { self.states.as_mut() } - pub fn run(&mut self, terminal: &mut DefaultTerminal, rx: Receiver) -> Result<()> { + pub fn run(&mut self, terminal: &mut DefaultTerminal, channels: AppChannels) -> Result<()> { while !self.exit { terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; - let event = match rx.recv_timeout(Duration::from_millis(100)) { - Ok(ev) => ev, - Err(RecvTimeoutError::Timeout) => { - continue; - } - Err(_) => break, - }; - match event { - AppEvent::Input(key_event) => self.handle_key_event(key_event)?, - AppEvent::Resize(_, _) => { - let window_area: Rect = self.window_area; - let Some(state) = self.states_mut() else { - panic!("State issue") - }; + if let Ok(key) = channels.input_rx.try_recv() { + self.handle_key_event(key)?; + } + + if let Ok((_, _)) = channels.resize_rx.try_recv() { + let window_area: Rect = self.window_area; + if let Some(state) = self.states_mut() { state .skirmish .board .change_resize(&window_area, state.skirmish.side_panel); } - AppEvent::Tick => self.update()?, } + + if let Ok(()) = channels.tick_rx.try_recv() { + self.update()?; + } + + sleep(Duration::from_millis(10)); } Ok(()) @@ -94,8 +88,9 @@ impl App { fn update(&mut self) -> Result<()> { if let Some(state) = self.states_mut() { - state.skirmish.update(&self.args); + state.skirmish.tick_update(); } + Ok(()) } } diff --git a/src/app/states/skirmish.rs b/src/app/states/skirmish.rs index e7c37be..2540d7e 100644 --- a/src/app/states/skirmish.rs +++ b/src/app/states/skirmish.rs @@ -1,4 +1,4 @@ -use crate::{app::states::skirmish_states::BoardState, cli::Cli}; +use crate::app::states::skirmish_states::BoardState; #[derive(Debug, Clone, PartialEq, Eq)] pub struct SkirmishState { @@ -10,8 +10,8 @@ pub struct SkirmishState { } impl SkirmishState { - pub fn update(&mut self, cli: &Cli) { - self.board.advance_turn(); + pub fn tick_update(&mut self) { + // self.board.advance_turn(); // if self.board.is_victory() {} diff --git a/src/app/threads/app_channels.rs b/src/app/threads/app_channels.rs new file mode 100644 index 0000000..1ff00cb --- /dev/null +++ b/src/app/threads/app_channels.rs @@ -0,0 +1,37 @@ +use crate::app::threads::AudioCmd; +use ratatui::crossterm::event::KeyEvent; +use std::sync::mpsc::{Receiver, Sender, channel}; + +pub struct AppChannels { + pub input_tx: Sender, + pub input_rx: Receiver, + + pub resize_tx: Sender<(u16, u16)>, + pub resize_rx: Receiver<(u16, u16)>, + + pub tick_tx: Sender<()>, + pub tick_rx: Receiver<()>, + + pub audio_tx: Sender, + pub audio_rx: Receiver, +} + +impl AppChannels { + pub fn new() -> Self { + let (input_tx, input_rx) = channel::(); + let (resize_tx, resize_rx) = channel::<(u16, u16)>(); + let (tick_tx, tick_rx) = channel::<()>(); + let (audio_tx, audio_rx) = channel::(); + + Self { + input_tx, + input_rx, + resize_tx, + resize_rx, + tick_tx, + tick_rx, + audio_tx, + audio_rx, + } + } +} diff --git a/src/app/threads/events.rs b/src/app/threads/events.rs index 3fbcf9b..e9d8781 100644 --- a/src/app/threads/events.rs +++ b/src/app/threads/events.rs @@ -1,30 +1,20 @@ -use ratatui::crossterm::event::KeyEvent; -use ratatui::crossterm::event::{Event, read}; +use ratatui::crossterm::event::{Event, KeyEvent, read}; use std::sync::mpsc::Sender; -pub enum AppEvent { - Input(KeyEvent), - Resize(u16, u16), - Tick, -} - -/// Reads *all* crossterm events and forwards the ones we care about. -pub fn handle_events(tx: Sender) { +pub fn handle_ct_events(input_tx: Sender, resize_tx: Sender<(u16, u16)>) { loop { match read() { - Ok(ev) => match ev { - Event::Key(key) => { - if tx.send(AppEvent::Input(key)).is_err() { - break; - } + Ok(Event::Key(k)) => { + if input_tx.send(k).is_err() { + break; } - Event::Resize(cols, rows) => { - if tx.send(AppEvent::Resize(cols, rows)).is_err() { - break; - } + } + Ok(Event::Resize(cols, rows)) => { + if resize_tx.send((cols, rows)).is_err() { + break; } - _ => {} - }, + } + Ok(_) => {} Err(_) => continue, } } diff --git a/src/app/threads/mod.rs b/src/app/threads/mod.rs index f2b2e4b..a08ac80 100644 --- a/src/app/threads/mod.rs +++ b/src/app/threads/mod.rs @@ -1,7 +1,9 @@ +mod app_channels; mod audio; mod events; mod tick; +pub use app_channels::AppChannels; pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio}; -pub use events::{AppEvent, handle_events}; -pub use tick::spawn_tick_thread; +pub use events::handle_ct_events; +pub use tick::handle_tick_event; diff --git a/src/app/threads/tick.rs b/src/app/threads/tick.rs index b0e7277..c3812c2 100644 --- a/src/app/threads/tick.rs +++ b/src/app/threads/tick.rs @@ -1,22 +1,19 @@ -use crate::app::threads::AppEvent; use std::{ sync::mpsc::Sender, thread, time::{Duration, Instant}, }; -pub fn spawn_tick_thread(tx: Sender, interval_ms: u64) { - thread::spawn(move || { - let interval: Duration = Duration::from_millis(interval_ms); - loop { - if tx.send(AppEvent::Tick).is_err() { - break; - } - - let elapsed: Duration = Instant::now().elapsed(); - if interval > elapsed { - thread::sleep(interval - elapsed); - } +pub fn handle_tick_event(tx: Sender<()>, interval_ms: u8) { + let interval: Duration = Duration::from_millis(interval_ms as u64); + loop { + if tx.send(()).is_err() { + break; } - }); + + let elapsed: Duration = Instant::now().elapsed(); + if interval > elapsed { + thread::sleep(interval - elapsed); + } + } } diff --git a/src/app/views/skirmish.rs b/src/app/views/skirmish.rs index 299875f..60b01b4 100644 --- a/src/app/views/skirmish.rs +++ b/src/app/views/skirmish.rs @@ -72,7 +72,8 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) { "Skills points: {} ({}/{}) | ", 1, 20, states.settings.skill_points_limit ), - format!("Perk Deck: {}/9", 5), + format!("Perk Deck: {}/9 | ", 5), + format!("Tick: {}", states.skirmish.turn_counter), ]), ]); diff --git a/src/cli.rs b/src/cli.rs index 0d807d3..472171a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,10 +1,13 @@ -use crate::app::{ - states::{ - PerkDecks, - skirmish_states::{GameMode, ZoomLevel}, +use crate::{ + app::{ + states::{ + PerkDecks, + skirmish_states::{GameMode, ZoomLevel}, + }, + threads::Soundtrack, + view::View, }, - threads::Soundtrack, - view::View, + logs::init_logger, }; use clap::{Error, Parser, error::ErrorKind, value_parser}; use std::num::ParseFloatError; @@ -15,7 +18,7 @@ use std::num::ParseFloatError; /// The `clap` attributes describe the flag name, help text, default value, /// and any validation constraints. The struct derives `Parser` so that /// `Cli::parse()` can be called directly to obtain a populated instance. -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Clone)] #[command(version, about = "War in Tunnels", long_about = "War in Tunnels")] pub struct Cli { /// The initial view/window to display. @@ -169,7 +172,13 @@ pub struct Cli { /// handles argument validation and displays helpful error messages if /// the user supplies invalid input. pub fn get_args() -> Cli { - Cli::parse() + let args: Cli = Cli::parse(); + + if args.log { + init_logger(); + } + + args } /// Parses a string into a floating‑point XP modifier and validates that it diff --git a/src/main.rs b/src/main.rs index 3b037d5..5def346 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,19 @@ -use ratatui::{Terminal, prelude::CrosstermBackend}; +use ratatui::{Terminal, crossterm::event::KeyEvent, prelude::CrosstermBackend}; use std::{ io::{Result, Stdout}, - sync::mpsc::channel, - thread::{ - self, - // JoinHandle - }, + mem::replace, + sync::mpsc::{Receiver, Sender, channel}, + thread::{self, JoinHandle}, }; use war_in_tunnels::{ app::{ App, - threads::{AppEvent, AudioCmd, handle_audio, handle_events, spawn_tick_thread}, + threads::{AppChannels, AudioCmd, handle_audio, handle_ct_events, handle_tick_event}, }, cli::{Cli, get_args}, - logs::init_logger, }; -const TICK_MS: u64 = 33; +const TICK_MS: u8 = 33; /// Starts the terminal UI application. /// @@ -25,43 +22,33 @@ const TICK_MS: u64 = 33; /// terminal, or while running the `App`. fn main() -> Result<()> { let args: Cli = get_args(); - if args.log { - init_logger(); - } - let (app_event_tx, app_event_rx) = channel::(); + let mut channels: AppChannels = AppChannels::new(); - // let app_event_thread: JoinHandle<()> = thread::spawn(move || { - // handle_events(app_event_tx); - // }); + let input_tx: Sender = channels.input_tx.clone(); + let resize_tx: Sender<(u16, u16)> = channels.resize_tx.clone(); + let events_thread: JoinHandle<()> = + thread::spawn(move || handle_ct_events(input_tx, resize_tx)); - thread::spawn(move || { - handle_events(app_event_tx.clone()); - }); - - thread::spawn(move || { - spawn_tick_thread(app_event_tx, TICK_MS); - }); - - let (audio_tx, audio_rx) = channel::(); - - // let audio_event_thread: JoinHandle<()> = thread::spawn(move || { - // handle_audio(audio_rx); - // }); + let tick_tx: Sender<()> = channels.tick_tx.clone(); + let tick_thread: JoinHandle<()> = thread::spawn(move || handle_tick_event(tick_tx, TICK_MS)); + let audio_rx: Receiver = replace(&mut channels.audio_rx, channel().1); + // let audio_thread: JoinHandle<()> = thread::spawn(move || { handle_audio(audio_rx, args.mute, args.sound_track); }); let mut terminal: Terminal> = ratatui::init(); - let mut app: App = App::new(args, audio_tx); + let mut app: App = App::new(args, channels.audio_tx.clone()); - let app_result: Result<()> = app.run(&mut terminal, app_event_rx); + let app_result: Result<()> = app.run(&mut terminal, channels); ratatui::restore(); - // let _ = app_event_thread.; - // let _ = audio_event_thread.join(); + let _ = events_thread.join(); + let _ = tick_thread.join(); + // let _ = audio_thread.join(); // TODO: kill playing music app_result }