From 02b40ef344939b78eb5cdea30798bcccfc266b39 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:37:05 -0800 Subject: [PATCH] discard a tile --- src/{game/mod.rs => game.rs} | 21 ++--- src/game/hand.rs | 12 +-- src/game/round.rs | 135 +++++++++++++++++++++---------- src/lib.rs | 2 +- src/tile.rs | 2 +- src/tui.rs | 30 ++++++- src/tui/input.rs | 153 +++-------------------------------- src/tui/input/keyboard.rs | 85 +++++++++++++++++++ src/tui/input/mouse.rs | 81 +++++++++++++++++++ src/tui/render.rs | 4 +- 10 files changed, 315 insertions(+), 210 deletions(-) rename src/{game/mod.rs => game.rs} (79%) create mode 100644 src/tui/input/keyboard.rs create mode 100644 src/tui/input/mouse.rs diff --git a/src/game/mod.rs b/src/game.rs similarity index 79% rename from src/game/mod.rs rename to src/game.rs index c296e21..7baa21f 100644 --- a/src/game/mod.rs +++ b/src/game.rs @@ -1,4 +1,4 @@ -use bevy::prelude::*; +use bevy::{ecs::query::QueryData, prelude::*}; use crate::{ game::{ @@ -24,6 +24,11 @@ pub enum GameState { Play, } +#[derive(Message)] +pub enum GameMessage { + Discarded(Entity), +} + pub struct Riichi; impl Plugin for Riichi { fn build(&self, app: &mut App) { @@ -33,29 +38,27 @@ impl Plugin for Riichi { .add_sub_state::() .init_resource::() .init_resource::() + .add_message::() .add_systems(Startup, tile::init_tiles) .add_systems(OnEnter(GameState::Setup), setup) .add_systems(OnEnter(GameState::Deal), hand::shuffle_deal) .add_systems(Update, hand::sort_hands.run_if(in_state(GameState::Play))) - // .add_systems(Update, turn_manager.run_if(in_state(GameState::Play))) .add_systems(OnEnter(TurnState::Tsumo), round::tsumo) + .add_systems(OnEnter(TurnState::Menzen), round::menzen) + .add_systems(Update, round::riichi_kan.run_if(in_state(TurnState::RiichiKan))) + .add_systems(Update, round::discard.run_if(in_state(TurnState::Discard))) + // .add_systems(Update, systems) // semicolon stopper ; } } -// struct TurnEvent { -// pub next: Option, -// pub prev: Option, -// } - -// fn turn_manager() {} - pub(crate) fn setup( mut commands: Commands, matchsettings: Res, // mut compass: ResMut // tiles: Query>, + curr_gamestate: Res>, mut next_gamestate: ResMut>, ) { for i in 1..=matchsettings.player_count { diff --git a/src/game/hand.rs b/src/game/hand.rs index be67921..117c741 100644 --- a/src/game/hand.rs +++ b/src/game/hand.rs @@ -8,7 +8,7 @@ use crate::{ #[derive(Component)] pub struct Hand; -#[derive(Component)] +#[derive(Component, Debug)] pub struct DrawnTile(pub Entity); // #[derive(Component, Default)] @@ -36,7 +36,7 @@ pub(crate) fn shuffle_deal( players: Populated>, wall_ent: Single>, mut next_gamestate: ResMut>, -) -> Result { +) { use rand::seq::SliceRandom; let mut rng = rand::rng(); @@ -53,14 +53,10 @@ pub(crate) fn shuffle_deal( let hand_ent = commands.spawn(Hand).add_children(&handtiles).id(); debug!("hand_ent: {hand_ent:?}"); - commands - .get_entity(player_ent)? - .replace_children(&[hand_ent]); - + commands.entity(player_ent).replace_children(&[hand_ent]); } - commands.get_entity(*wall_ent)?.replace_children(&walltiles); + commands.entity(*wall_ent).replace_children(&walltiles); next_gamestate.set(GameState::Play); - Ok(()) } diff --git a/src/game/round.rs b/src/game/round.rs index ff9f522..bfe57a6 100644 --- a/src/game/round.rs +++ b/src/game/round.rs @@ -3,9 +3,23 @@ use strum::{EnumCount, FromRepr}; use crate::{ EnumNextCycle, - game::{GameState, hand::DrawnTile, wall::Wall}, + game::{ + GameMessage, GameState, + hand::{DrawnTile, Hand}, + player::Player, + wall::Wall, + }, }; +#[derive(Resource)] +pub struct CurrentPlayer(pub Entity); + +#[derive(Resource)] +pub(crate) struct MatchSettings { + pub(crate) starting_points: isize, + pub(crate) player_count: u8, +} + #[derive(Component)] pub(crate) struct Dice(u8, u8); @@ -18,33 +32,6 @@ pub(crate) struct Compass { pub(crate) honba: usize, } -impl Default for Compass { - fn default() -> Self { - Self { - prevalent_wind: Wind::Ton, - round: 1, - dealer_wind: Wind::Ton, - riichi: 0, - honba: 0, - } - } -} - -#[derive(Resource)] -pub(crate) struct MatchSettings { - pub(crate) starting_points: isize, - pub(crate) player_count: u8, -} - -impl Default for MatchSettings { - fn default() -> Self { - Self { - starting_points: 25000, - player_count: 4, - } - } -} - #[derive(Component, Clone, Copy, FromRepr, EnumCount, PartialEq)] pub enum Wind { Ton, @@ -59,6 +46,42 @@ pub enum WindRelation { Kamicha, } +#[derive(SubStates, Default, Clone, Copy, PartialEq, Eq, Hash, Debug, FromRepr, EnumCount)] +#[source(GameState = GameState::Play)] +pub(crate) enum TurnState { + #[default] + Tsumo, + Menzen, + RiichiKan, + Discard, + RonChiiPonKan, + End, +} + +#[derive(EntityEvent)] +pub struct Discard(pub Entity); + +impl Default for MatchSettings { + fn default() -> Self { + Self { + starting_points: 25000, + player_count: 4, + } + } +} + +impl Default for Compass { + fn default() -> Self { + Self { + prevalent_wind: Wind::Ton, + round: 1, + dealer_wind: Wind::Ton, + riichi: 0, + honba: 0, + } + } +} + impl EnumNextCycle for Wind { fn next(&self) -> Self { if (*self as usize + 1) >= Self::COUNT { @@ -81,21 +104,6 @@ impl Wind { } } -#[derive(Resource)] -pub(crate) struct CurrentPlayer(pub Entity); - -#[derive(SubStates, Default, Clone, Copy, PartialEq, Eq, Hash, Debug, FromRepr, EnumCount)] -#[source(GameState = GameState::Play)] -pub(crate) enum TurnState { - #[default] - Tsumo, - Menzen, - RiichiKan, - Discard, - RonChiiPonKan, - End, -} - impl EnumNextCycle for TurnState { fn next(&self) -> Self { if (*self as usize + 1) >= Self::COUNT { @@ -125,3 +133,44 @@ pub(crate) fn tsumo( next_turnstate.set(curr_turnstate.next()); } + +pub(crate) fn menzen( + curr_turnstate: Res>, + mut next_turnstate: ResMut>, +) { + next_turnstate.set(curr_turnstate.next()); +} + +pub(crate) fn riichi_kan( + curr_turnstate: Res>, + mut next_turnstate: ResMut>, +) { + next_turnstate.set(curr_turnstate.next()); +} + +pub(crate) fn discard( + mut reader: MessageReader, + + currplayer: Res, + drawntile: Single>, + player_hands: Populated<(&Player, &Children), With>, + hands: Populated<&Children, (With, Without)>, + + curr_turnstate: Res>, + mut next_turnstate: ResMut>, +) { + let curr = currplayer.0; + let hand_ent = player_hands.get(curr).unwrap().1.iter().next().unwrap(); + let hand = hands.get(hand_ent).unwrap(); + + while let Some(message) = reader.read().next() { + if let GameMessage::Discarded(entity) = message { + if *entity == *drawntile { + } else if hand.contains(entity) { + } else { + panic!("discarded illegal player tile?") + } + break; + } + } +} diff --git a/src/lib.rs b/src/lib.rs index a548b53..6fa583d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(stmt_expr_attributes)] +#![feature(stmt_expr_attributes, iter_array_chunks)] pub mod game; pub mod tile; diff --git a/src/tile.rs b/src/tile.rs index 89fe820..01cd5ed 100644 --- a/src/tile.rs +++ b/src/tile.rs @@ -1,4 +1,4 @@ -use bevy::{ecs::entity::MapEntities, prelude::*}; +use bevy::prelude::*; use strum::FromRepr; #[derive(Component, Debug, Clone, Copy)] diff --git a/src/tui.rs b/src/tui.rs index f577ca4..d6b6c22 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -2,10 +2,15 @@ use std::time::Duration; use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin}; use bevy_ratatui::RatatuiPlugins; -use jong::game::GameState; +use jong::game::{ + GameMessage, GameState, + hand::{DrawnTile, Hand}, + player::{MainPlayer, Player}, + round::{CurrentPlayer, Discard}, +}; use tui_logger::TuiWidgetState; -use crate::tui::states::ConsoleWidget; +use crate::tui::{input::ConfirmSelect, states::ConsoleWidget}; mod input; mod layout; @@ -45,11 +50,13 @@ impl Plugin for TuiPlugin { Update, (TuiSet::Input, TuiSet::Layout, TuiSet::Render).chain(), ) + .add_message::() .add_systems( Update, (input::keyboard, input::mouse).in_set(TuiSet::Input), ) .add_systems(Update, layout::layout.in_set(TuiSet::Layout)) + .add_systems(Update, discard_tile.run_if(in_state(GameState::Play))) .add_systems( Update, ( @@ -62,6 +69,25 @@ impl Plugin for TuiPlugin { } } +fn discard_tile( + mut writer: MessageWriter, + mut selected: MessageReader, + drawntile: Single>, + currplayer: Res, + player_hands: Populated<(&Player, &Children), With>, + hands: Populated<&Children, (With, Without)>, +) { + let curr = currplayer.0; + let hand_ent = player_hands.get(curr).unwrap().1.iter().next().unwrap(); + let hand = hands.get(hand_ent).unwrap(); + + while let Some(message) = selected.read().next() + && (message.0 == *drawntile || hand.contains(&message.0)) + { + writer.write(GameMessage::Discarded(message.0)); + } +} + mod states { use bevy::prelude::*; use tui_logger::TuiWidgetState; diff --git a/src/tui/input.rs b/src/tui/input.rs index 915c3bb..317e04e 100644 --- a/src/tui/input.rs +++ b/src/tui/input.rs @@ -1,149 +1,16 @@ +use bevy::prelude::*; + pub(crate) use keyboard::keyboard; pub(crate) use mouse::mouse; -pub(crate) mod keyboard { - use bevy::prelude::*; - use bevy_ratatui::event::KeyMessage; - use ratatui::crossterm::event::KeyCode; - use tui_logger::TuiWidgetEvent; +pub(crate) mod keyboard; +pub(crate) mod mouse; - use jong::game::GameState; +#[derive(Component)] +pub(crate) struct Hovered; - use crate::tui::layout::Overlays; - use crate::tui::states::ConsoleWidget; - use crate::tui::states::TuiState; +#[derive(Component)] +pub(crate) struct StartSelect; - pub(crate) fn keyboard( - mut messages: MessageReader, - mut overlays: ResMut, - mut consolewidget: ResMut, - mut exit: MessageWriter, - - curr_gamestate: Res>, - curr_tuistate: Res>, - - mut next_gamestate: ResMut>, - mut next_tuistate: ResMut>, - ) { - 'message: for message in messages.read() { - let key = message.code; - if consolewidget.handle_keyboard(key) { - continue 'message; - } - for overlay in overlays.stack.iter().rev() { - let consumed = match overlay { - _ => false, - }; - if consumed { - continue 'message; - } - } - - match curr_tuistate.get() { - TuiState::MainMenu => match key { - KeyCode::Char('p') => { - next_tuistate.set(TuiState::InGame); - next_gamestate.set(GameState::Setup); - } - KeyCode::Char('z') => { - // if let Some(ref curr_zenstate) = curr_zenstate { - // match curr_zenstate.get() { - // ZenState::Menu => next_zenstate.set(ZenState::Zen), - // ZenState::Zen => next_zenstate.set(ZenState::Menu), - // } - // } - } - KeyCode::Char('q') => { - exit.write_default(); - } - _ => {} - }, - TuiState::InGame => todo!(), - } - } - } - - impl ConsoleWidget { - pub(crate) fn handle_keyboard(&mut self, key: KeyCode) -> bool { - if key == KeyCode::Char('`') { - self.open = !self.open; - return true; - } - if self.open { - match key { - KeyCode::Up => self.state.transition(TuiWidgetEvent::UpKey), - KeyCode::Down => self.state.transition(TuiWidgetEvent::DownKey), - // KeyCode::Home => self.state.transition(TuiWidgetEvent::), - // KeyCode::End => self.state.transition(TuiWidgetEvent::), - KeyCode::PageUp => self.state.transition(TuiWidgetEvent::PrevPageKey), - KeyCode::PageDown => self.state.transition(TuiWidgetEvent::NextPageKey), - KeyCode::Esc => { - self.open = false; - return true; - } - _ => return false, - } - } - self.open - } - } -} - -pub(crate) mod mouse { - use bevy::prelude::*; - use bevy_ratatui::event::MouseMessage; - use ratatui::layout::Position; - - use crate::tui::render::{Hovered, PickRegion}; - - pub(crate) fn mouse( - mut commands: Commands, - mut messages: MessageReader, - entities: Query<(Entity, &PickRegion)>, - hovered: Query<(Entity, &PickRegion), With>, - ) -> Result { - for message in messages.read() { - let event = message.0; - // let term_size = context.size().unwrap(); - let position = Position::new(event.column, event.row); - match event.kind { - ratatui::crossterm::event::MouseEventKind::Down(mouse_button) => match mouse_button - { - ratatui::crossterm::event::MouseButton::Left => { - for (_entity, _region) in &entities {} - } - // ratatui::crossterm::event::MouseButton::Right => todo!(), - // ratatui::crossterm::event::MouseButton::Middle => todo!(), - _ => {} - }, - // ratatui::crossterm::event::MouseEventKind::Up(mouse_button) => todo!(), - // ratatui::crossterm::event::MouseEventKind::Drag(mouse_button) => todo!(), - ratatui::crossterm::event::MouseEventKind::Moved => { - for (entity, region) in &hovered { - if !region.area.contains(position) { - commands.get_entity(entity)?.remove::(); - } - } - for (entity, region) in &entities { - // debug!( - // "{:?}, {position:?}", - // // region.area.positions().collect::>() - // region.area - // ); - if region.area.contains(position) { - commands.get_entity(entity)?.insert(Hovered); - // trace!("{entity:?} hovered!") - } - } - } - // ratatui::crossterm::event::MouseEventKind::ScrollDown => todo!(), - // ratatui::crossterm::event::MouseEventKind::ScrollUp => todo!(), - // ratatui::crossterm::event::MouseEventKind::ScrollLeft => todo!(), - // ratatui::crossterm::event::MouseEventKind::ScrollRight => todo!(), - _ => {} - } - } - - Ok(()) - } -} +#[derive(Message)] +pub(crate) struct ConfirmSelect(pub(crate) Entity); diff --git a/src/tui/input/keyboard.rs b/src/tui/input/keyboard.rs new file mode 100644 index 0000000..a807d3d --- /dev/null +++ b/src/tui/input/keyboard.rs @@ -0,0 +1,85 @@ +use bevy::prelude::*; +use bevy_ratatui::event::KeyMessage; +use ratatui::crossterm::event::KeyCode; +use tui_logger::TuiWidgetEvent; + +use jong::game::GameState; + +use crate::tui::layout::Overlays; +use crate::tui::states::ConsoleWidget; +use crate::tui::states::TuiState; + +pub(crate) fn keyboard( + mut messages: MessageReader, + mut overlays: ResMut, + mut consolewidget: ResMut, + mut exit: MessageWriter, + + curr_gamestate: Res>, + curr_tuistate: Res>, + + mut next_gamestate: ResMut>, + mut next_tuistate: ResMut>, +) { + 'message: for message in messages.read() { + let key = message.code; + if consolewidget.handle_keyboard(key) { + continue 'message; + } + for overlay in overlays.stack.iter().rev() { + let consumed = match overlay { + _ => false, + }; + if consumed { + continue 'message; + } + } + + match curr_tuistate.get() { + TuiState::MainMenu => match key { + KeyCode::Char('p') => { + next_tuistate.set(TuiState::InGame); + next_gamestate.set(GameState::Setup); + } + KeyCode::Char('z') => { + // if let Some(ref curr_zenstate) = curr_zenstate { + // match curr_zenstate.get() { + // ZenState::Menu => next_zenstate.set(ZenState::Zen), + // ZenState::Zen => next_zenstate.set(ZenState::Menu), + // } + // } + } + KeyCode::Char('q') => { + exit.write_default(); + } + _ => {} + }, + TuiState::InGame => debug!("unhandled keyboard event"), + } + } +} + +impl ConsoleWidget { + pub(crate) fn handle_keyboard(&mut self, key: KeyCode) -> bool { + if key == KeyCode::Char('`') { + self.open = !self.open; + return true; + } + if self.open { + match key { + KeyCode::Up => self.state.transition(TuiWidgetEvent::UpKey), + KeyCode::Down => self.state.transition(TuiWidgetEvent::DownKey), + // KeyCode::Home => self.state.transition(TuiWidgetEvent::), + // KeyCode::End => self.state.transition(TuiWidgetEvent::), + KeyCode::PageUp => self.state.transition(TuiWidgetEvent::PrevPageKey), + KeyCode::PageDown => self.state.transition(TuiWidgetEvent::NextPageKey), + KeyCode::Esc => { + self.open = false; + return true; + } + _ => return false, + } + } + self.open + } +} diff --git a/src/tui/input/mouse.rs b/src/tui/input/mouse.rs new file mode 100644 index 0000000..1808d02 --- /dev/null +++ b/src/tui/input/mouse.rs @@ -0,0 +1,81 @@ +use bevy::prelude::*; +use bevy_ratatui::event::MouseMessage; +use ratatui::layout::Position; + +use crate::tui::{ + input::{ConfirmSelect, Hovered, StartSelect}, + render::PickRegion, +}; + +pub(crate) fn mouse( + mut commands: Commands, + mut mouse_reader: MessageReader, + mut event_writer: MessageWriter, + entities: Query<(Entity, &PickRegion)>, + hovered: Query<(Entity, &PickRegion), With>, + startselected: Query<(Entity, &PickRegion), With>, +) { + for message in mouse_reader.read() { + let event = message.0; + // let term_size = context.size().unwrap(); + let position = Position::new(event.column, event.row); + match event.kind { + #[allow(clippy::single_match)] + ratatui::crossterm::event::MouseEventKind::Down(mouse_button) => match mouse_button { + ratatui::crossterm::event::MouseButton::Left => { + for (entity, region) in &entities { + if region.area.contains(position) { + commands.entity(entity).insert(StartSelect); + } + } + } + + // ratatui::crossterm::event::MouseButton::Right => debug!("unhandled mouse event"), + // ratatui::crossterm::event::MouseButton::Middle => debug!("unhandled mouse event"), + _ => {} + }, + ratatui::crossterm::event::MouseEventKind::Moved => { + for (entity, region) in &hovered { + if !region.area.contains(position) { + commands.entity(entity).remove::(); + } + } + for (entity, region) in &entities { + if region.area.contains(position) { + commands.entity(entity).insert(Hovered); + } + } + } + ratatui::crossterm::event::MouseEventKind::Up(mouse_button) => match mouse_button { + ratatui::crossterm::event::MouseButton::Left => { + for (entity, region) in &startselected { + if region.area.contains(position) { + commands.entity(entity).remove::(); + event_writer.write(ConfirmSelect(entity)); + } + } + } + ratatui::crossterm::event::MouseButton::Right => { + for (entity, _) in &startselected { + commands.entity(entity).remove::(); + } + } + // ratatui::crossterm::event::MouseButton::Middle => debug!("unhandled mouse event"), + _ => {} + }, + ratatui::crossterm::event::MouseEventKind::Drag(mouse_button) => { + debug!("unhandled mouse event") + } + ratatui::crossterm::event::MouseEventKind::ScrollDown => { + debug!("unhandled mouse event") + } + ratatui::crossterm::event::MouseEventKind::ScrollUp => debug!("unhandled mouse event"), + ratatui::crossterm::event::MouseEventKind::ScrollLeft => { + debug!("unhandled mouse event") + } + ratatui::crossterm::event::MouseEventKind::ScrollRight => { + debug!("unhandled mouse event") + } + } + } +} diff --git a/src/tui/render.rs b/src/tui/render.rs index ed61c4e..734920c 100644 --- a/src/tui/render.rs +++ b/src/tui/render.rs @@ -13,12 +13,10 @@ use jong::game::player::{MainPlayer, Player}; use jong::game::round::Wind; use jong::tile::Tile; +use crate::tui::input::Hovered; use crate::tui::layout::*; use crate::tui::states::ConsoleWidget; -#[derive(Component)] -pub(crate) struct Hovered; - #[derive(Component)] pub(crate) struct PickRegion { pub(crate) area: Rect,