From 9b99dad50b7fc93ea5a8b2d6edcce46d524ccb59 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Tue, 13 Jan 2026 03:48:53 -0800 Subject: [PATCH 1/8] scheduling --- src/tui/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/tui/mod.rs b/src/tui/mod.rs index f96a68a..c6ea950 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -54,20 +54,20 @@ impl Plugin for RiichiTui { // console .init_state::() - .add_systems(Update, console::draw_console.after_ignore_deferred(draw_mainmenu).after_ignore_deferred(draw_ingame).run_if(in_state(console::ConsoleState::Open))) + .add_systems(Last, console::draw_console.run_if(in_state(console::ConsoleState::Open))) // general setup .init_state::() .add_computed_state::() - .add_systems(Update, input::kb_input_system) + .add_systems(PreUpdate, input::kb_input_system) // main menu - .add_systems(Update, menu::draw_mainmenu.run_if(in_state(TuiState::MainMenu))) + .add_systems(PostUpdate, menu::draw_mainmenu.run_if(in_state(TuiState::MainMenu))) // gaming .init_resource::() - .add_systems(Update, render::ingame::draw_ingame.run_if(in_state(InGame))) .add_systems(Update, render::hand::render_hand.run_if(in_state(InGame).and(in_state(GameState::Play)))) + .add_systems(PostUpdate, render::ingame::draw_ingame.run_if(in_state(InGame))) // semicolon stopper ; From 4112edbf2abf9726395de79991e314e87066b3f4 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Tue, 13 Jan 2026 03:32:08 -0800 Subject: [PATCH 2/8] store rendered tile as entity rather than raw Paragraph --- src/game/hand.rs | 2 +- src/game/mod.rs | 4 ++-- src/game/round.rs | 2 +- src/game/wall.rs | 2 +- src/lib.rs | 2 +- src/{tiles.rs => tile.rs} | 0 src/tui/mod.rs | 2 +- src/tui/render/hand.rs | 25 ++++++++++++++++++------- src/tui/render/ingame.rs | 5 +++-- src/tui/render/mod.rs | 2 +- src/tui/render/tile.rs | 34 ++++++++++++++++++++++++++++++++++ src/tui/render/tiles.rs | 30 ------------------------------ 12 files changed, 63 insertions(+), 47 deletions(-) rename src/{tiles.rs => tile.rs} (100%) create mode 100644 src/tui/render/tile.rs delete mode 100644 src/tui/render/tiles.rs diff --git a/src/game/hand.rs b/src/game/hand.rs index f17b6ba..434fe16 100644 --- a/src/game/hand.rs +++ b/src/game/hand.rs @@ -2,7 +2,7 @@ use bevy::{ecs::relationship::RelationshipSourceCollection, prelude::*}; use crate::{ game::{player::Player /* wall::WallTiles */}, - tiles::Tile, + tile::Tile, }; #[derive(Component)] diff --git a/src/game/mod.rs b/src/game/mod.rs index 8d41908..e42c311 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -6,7 +6,7 @@ use crate::{ player::{MainPlayer, Player}, wall::Wall, }, - tiles::{self, *}, + tile::{self, *}, }; pub mod hand; @@ -31,7 +31,7 @@ impl Plugin for Riichi { .init_state::() .init_resource::() .init_resource::() - .add_systems(Startup, tiles::init_tiles) + .add_systems(Startup, tile::init_tiles) .add_systems(OnEnter(GameState::Setup), setup) .add_systems(OnEnter(GameState::Deal), shuffle_deal) .add_systems(Update, hand::sort_hands.run_if(in_state(GameState::Play))) diff --git a/src/game/round.rs b/src/game/round.rs index fa4873b..5f8a3be 100644 --- a/src/game/round.rs +++ b/src/game/round.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; -use crate::tiles::*; +use crate::tile::*; #[derive(Component)] pub(crate) struct Dice(u8, u8); diff --git a/src/game/wall.rs b/src/game/wall.rs index 4463c26..de6ac75 100644 --- a/src/game/wall.rs +++ b/src/game/wall.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; use rand::seq::SliceRandom; -use crate::tiles::Tile; +use crate::tile::Tile; #[derive(Component)] pub struct Wall; diff --git a/src/lib.rs b/src/lib.rs index 45412c3..a64f794 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ #![allow(unused)] pub mod game; -pub mod tiles; +pub mod tile; pub mod yakus; diff --git a/src/tiles.rs b/src/tile.rs similarity index 100% rename from src/tiles.rs rename to src/tile.rs diff --git a/src/tui/mod.rs b/src/tui/mod.rs index c6ea950..33ac500 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -66,7 +66,7 @@ impl Plugin for RiichiTui { // gaming .init_resource::() - .add_systems(Update, render::hand::render_hand.run_if(in_state(InGame).and(in_state(GameState::Play)))) + .add_systems(PostUpdate, render::hand::render_hands.run_if(in_state(InGame).and(in_state(GameState::Play)))) .add_systems(PostUpdate, render::ingame::draw_ingame.run_if(in_state(InGame))) // semicolon stopper diff --git a/src/tui/render/hand.rs b/src/tui/render/hand.rs index a1b5ebc..939fdf9 100644 --- a/src/tui/render/hand.rs +++ b/src/tui/render/hand.rs @@ -3,19 +3,21 @@ use ratatui::widgets::Paragraph; use jong::{ game::{hand::Hand, player::Player}, - tiles::Tile, + tile::Tile, }; -use crate::tui::render::tiles; +use crate::tui::render::tile::{self, RenderedTile}; #[derive(Resource, Default)] -pub(crate) struct RenderedHand(pub(crate) HashMap>>); +pub(crate) struct RenderedHand(pub(crate) HashMap>); -pub(crate) fn render_hand( +#[allow(clippy::type_complexity)] +pub(crate) fn render_hands( + mut commands: Commands, + mut rendered_hand: ResMut, tiles: Populated<&Tile>, player_hands: Populated<(Entity, &Children), (With, Changed)>, hands: Populated<&Children, (Changed, Without)>, - mut target: ResMut, ) -> Result { let mut rendered = HashMap::new(); @@ -24,12 +26,21 @@ pub(crate) fn render_hand( let tiles = hands .get(hand)? .iter() - .map(|it| tiles.get(it).map(tiles::draw_tile).unwrap()) + .map(|it| { + tiles + .get(it) + // .inspect(|t| debug!("{t:?}")) + .map(tile::draw_tile) + .map(|p| commands.spawn(RenderedTile(p)).id()) + // .inspect(|e| debug!("{e:?}")) + .unwrap() + }) .collect(); rendered.insert(player_ent, tiles); } - target.0 = rendered; + rendered_hand.0 = rendered; + trace!("render_hands"); Ok(()) } diff --git a/src/tui/render/ingame.rs b/src/tui/render/ingame.rs index e573cb9..e0a4b0e 100644 --- a/src/tui/render/ingame.rs +++ b/src/tui/render/ingame.rs @@ -2,10 +2,11 @@ use bevy::prelude::*; use bevy_ratatui::RatatuiContext; use jong::game::player::{MainPlayer, Player}; -use crate::tui::render::hand; +use crate::tui::render::{hand, tile::RenderedTile}; pub(crate) fn draw_ingame( rendered_hand: Res, + rendered_tiles: Populated<&RenderedTile>, main_player: Single, With)>, mut tui_ctx: ResMut, ) -> Result { @@ -22,7 +23,7 @@ pub(crate) fn draw_ingame( // if let Some(hand) = rendered_hand.0.get(&*main_player) { if let Some(hand) = rendered_hand.0.get(&*main_player) { for (tile, area) in hand.iter().zip(areas.iter()) { - frame.render_widget(tile, *area); + frame.render_widget(&rendered_tiles.get(*tile).unwrap().0, *area); } } })?; diff --git a/src/tui/render/mod.rs b/src/tui/render/mod.rs index fc6612a..0e3cc55 100644 --- a/src/tui/render/mod.rs +++ b/src/tui/render/mod.rs @@ -1,3 +1,3 @@ pub(crate) mod hand; pub(crate) mod ingame; -mod tiles; +pub(crate) mod tile; diff --git a/src/tui/render/tile.rs b/src/tui/render/tile.rs new file mode 100644 index 0000000..59d50bd --- /dev/null +++ b/src/tui/render/tile.rs @@ -0,0 +1,34 @@ +use bevy::prelude::*; +use ratatui::widgets::Paragraph; + +use jong::tile::Tile; + +#[derive(Component)] +pub(crate) struct RenderedTile(pub(crate) Paragraph<'static>); + +pub(crate) fn draw_tile(tile: &Tile) -> Paragraph<'static> { + use ratatui::prelude::*; + + let block = ratatui::widgets::Block::bordered(); + + Paragraph::new(match &tile.suit { + jong::tile::Suit::Pin(rank) => format!("{}\np", rank.0), + jong::tile::Suit::Sou(rank) => format!("{}\ns", rank.0), + jong::tile::Suit::Man(rank) => format!("{}\nm", rank.0), + jong::tile::Suit::Wind(wind) => (match wind { + jong::tile::Wind::Ton => "e\nw", + jong::tile::Wind::Nan => "s\nw", + jong::tile::Wind::Shaa => "w\nw", + jong::tile::Wind::Pei => "n\nw", + }) + .into(), + jong::tile::Suit::Dragon(dragon) => (match dragon { + jong::tile::Dragon::Haku => "w\nd", + jong::tile::Dragon::Hatsu => "g\nd", + jong::tile::Dragon::Chun => "r\nd", + }) + .into(), + }) + .block(block) + .centered() +} diff --git a/src/tui/render/tiles.rs b/src/tui/render/tiles.rs deleted file mode 100644 index 620a2d1..0000000 --- a/src/tui/render/tiles.rs +++ /dev/null @@ -1,30 +0,0 @@ -use ratatui::widgets::Paragraph; - -use jong::tiles::Tile; - -pub(crate) fn draw_tile(tile: &Tile) -> Paragraph<'static> { - use ratatui::prelude::*; - - let block = ratatui::widgets::Block::bordered(); - - Paragraph::new(match &tile.suit { - jong::tiles::Suit::Pin(rank) => format!("{}\np", rank.0), - jong::tiles::Suit::Sou(rank) => format!("{}\ns", rank.0), - jong::tiles::Suit::Man(rank) => format!("{}\nm", rank.0), - jong::tiles::Suit::Wind(wind) => (match wind { - jong::tiles::Wind::Ton => "e\nw", - jong::tiles::Wind::Nan => "s\nw", - jong::tiles::Wind::Shaa => "w\nw", - jong::tiles::Wind::Pei => "n\nw", - }) - .into(), - jong::tiles::Suit::Dragon(dragon) => (match dragon { - jong::tiles::Dragon::Haku => "w\nd", - jong::tiles::Dragon::Hatsu => "g\nd", - jong::tiles::Dragon::Chun => "r\nd", - }) - .into(), - }) - .block(block) - .centered() -} From 8a8a702a08300abc6c5230f651473af944e37aa9 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Tue, 13 Jan 2026 04:11:09 -0800 Subject: [PATCH 3/8] hover effect, layout hands and ponds --- src/tui/{input.rs => input/keyboard.rs} | 14 ++-- src/tui/input/mod.rs | 2 + src/tui/input/mouse.rs | 65 ++++++++++++++++++ src/tui/mod.rs | 9 +-- src/tui/render/ingame.rs | 88 ++++++++++++++++++++++--- src/tui/render/mod.rs | 5 ++ 6 files changed, 163 insertions(+), 20 deletions(-) rename src/tui/{input.rs => input/keyboard.rs} (87%) create mode 100644 src/tui/input/mod.rs create mode 100644 src/tui/input/mouse.rs diff --git a/src/tui/input.rs b/src/tui/input/keyboard.rs similarity index 87% rename from src/tui/input.rs rename to src/tui/input/keyboard.rs index 44947c3..ee77c1a 100644 --- a/src/tui/input.rs +++ b/src/tui/input/keyboard.rs @@ -1,13 +1,17 @@ use bevy::prelude::*; -use bevy_ratatui::event::{KeyMessage, MouseMessage}; +use bevy_ratatui::crossterm::event::KeyCode; +use bevy_ratatui::event::KeyMessage; use jong::game::GameState; use crate::tui::{TuiState, console::ConsoleState}; +// TODO change this to handle console open request, esc for menu, etc, then +// route other messages to other systems + #[allow(clippy::too_many_arguments)] -pub(crate) fn kb_input_system( - mut kb_messages: MessageReader, +pub(crate) fn input_system( + mut messages: MessageReader, curr_tuistate: Res>, curr_consolestate: Res>, @@ -19,15 +23,13 @@ pub(crate) fn kb_input_system( mut exit: MessageWriter, ) { - use bevy_ratatui::crossterm::event::KeyCode; - let (ts, cs, gs) = ( curr_tuistate.get(), curr_consolestate.get(), curr_gamestate.get(), ); - for message in kb_messages.read() { + for message in messages.read() { if let KeyCode::Char('`') = message.code { next_consolestate.set(!*curr_consolestate.get()); continue; diff --git a/src/tui/input/mod.rs b/src/tui/input/mod.rs new file mode 100644 index 0000000..d7eb1ae --- /dev/null +++ b/src/tui/input/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod keyboard; +pub(crate) mod mouse; diff --git a/src/tui/input/mouse.rs b/src/tui/input/mouse.rs new file mode 100644 index 0000000..5926222 --- /dev/null +++ b/src/tui/input/mouse.rs @@ -0,0 +1,65 @@ +use bevy::prelude::*; +use bevy_ratatui::{RatatuiContext, event::MouseMessage}; +use ratatui::{crossterm::event::MouseEvent, layout::Position}; + +use crate::tui::render::Hovered; + +#[derive(Component)] +pub(crate) struct PickRegion { + pub(crate) area: ratatui::prelude::Rect, +} + +// enum PickEvent { +// Click { col: u16, row: u16 }, +// Hover { col: u16, row: u16 }, +// } + +pub(crate) fn input_system( + mut commands: Commands, + mut messages: MessageReader, + context: Res, + 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::>() + // ); + 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(()) +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 33ac500..05311b0 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -59,15 +59,16 @@ impl Plugin for RiichiTui { // general setup .init_state::() .add_computed_state::() - .add_systems(PreUpdate, input::kb_input_system) + .add_systems(PreUpdate, input::keyboard::input_system) + .add_systems(PreUpdate, input::mouse::input_system.chain()) // main menu - .add_systems(PostUpdate, menu::draw_mainmenu.run_if(in_state(TuiState::MainMenu))) + .add_systems(Update, menu::draw_mainmenu.run_if(in_state(TuiState::MainMenu))) // gaming .init_resource::() - .add_systems(PostUpdate, render::hand::render_hands.run_if(in_state(InGame).and(in_state(GameState::Play)))) - .add_systems(PostUpdate, render::ingame::draw_ingame.run_if(in_state(InGame))) + .add_systems(Update, render::hand::render_hands.run_if(in_state(InGame).and(in_state(GameState::Play)))) + .add_systems(Update, render::ingame::draw_ingame.run_if(in_state(InGame))) // semicolon stopper ; diff --git a/src/tui/render/ingame.rs b/src/tui/render/ingame.rs index e0a4b0e..1c2a1aa 100644 --- a/src/tui/render/ingame.rs +++ b/src/tui/render/ingame.rs @@ -1,14 +1,22 @@ use bevy::prelude::*; use bevy_ratatui::RatatuiContext; use jong::game::player::{MainPlayer, Player}; +use rand::rand_core::block::BlockRngCore; +use ratatui::style::Styled; +use ratatui::widgets::{Block, Borders, Widget}; -use crate::tui::render::{hand, tile::RenderedTile}; +use crate::tui::{ + input::mouse::PickRegion, + render::{Hovered, hand, tile::RenderedTile}, +}; pub(crate) fn draw_ingame( - rendered_hand: Res, - rendered_tiles: Populated<&RenderedTile>, - main_player: Single, With)>, + mut commands: Commands, mut tui_ctx: ResMut, + main_player: Single, With)>, + hovered_entity: Query>, + rendered_tiles: Populated<&RenderedTile>, + rendered_hand: Res, ) -> Result { use ratatui::layout::Flex; use ratatui::prelude::*; @@ -16,14 +24,74 @@ pub(crate) fn draw_ingame( tui_ctx.draw(|frame| { // debug!("{}", frame.area()); + let term_area = frame.area(); + + // TODO this is gross + let constraints = [Constraint::Fill(3), Constraint::Fill(18)]; + let horizontal_slicer_right = Layout::horizontal(constraints); + let vertical_slicer_bottom = Layout::vertical(constraints); + let horizontal_slicer_left = Layout::horizontal(constraints.iter().rev()); + let vertical_slicer_top = Layout::vertical(constraints.iter().rev()); + let [left_hand, this_hand] = horizontal_slicer_right.areas::<2>(term_area); + let [_, mut left_hand] = vertical_slicer_bottom.areas::<2>(left_hand); + let [_, mut this_hand] = vertical_slicer_top.areas::<2>(this_hand); + let [cross_hand, right_hand] = horizontal_slicer_left.areas::<2>(term_area); + let [mut right_hand, _] = vertical_slicer_top.areas::<2>(right_hand); + let [mut cross_hand, _] = vertical_slicer_bottom.areas::<2>(cross_hand); + + let margin = Margin::new( + (left_hand.width + right_hand.width) / 2, + (cross_hand.height + this_hand.height) / 2, + ); + let pond_area = term_area.inner(margin); + let all_pond = Layout::horizontal([Constraint::Fill(1); 3]); + let cross_pond = Layout::vertical([Constraint::Fill(1), Constraint::Max(1),Constraint::Fill(1)]); + let [mut left_pond, center, mut right_pond] = all_pond.areas::<3>(pond_area); + let [cross_pond, compass, this_pond] = cross_pond.areas::<3>(center); + // let shift = left_pond.height - cross_pond.height; + left_pond.height = cross_pond.height; + left_pond.y += cross_pond.height / 2; + right_pond.height = cross_pond.height; + right_pond.y+= cross_pond.height / 2; + + let debug_block = Block::new().borders(Borders::ALL); + frame.render_widget(debug_block.clone(), this_hand); + frame.render_widget(debug_block.clone(), left_hand); + frame.render_widget(debug_block.clone(), cross_hand); + frame.render_widget(debug_block.clone(), right_hand); + frame.render_widget(debug_block.clone(), this_pond); + frame.render_widget(debug_block.clone(), left_pond); + frame.render_widget(debug_block.clone(), cross_pond); + frame.render_widget(debug_block.clone(), right_pond); + + // TODO attempt to merge blocks on smol term? let layout = Layout::horizontal(vec![Constraint::Max(5); 13]).flex(Flex::Start); - let mut area = frame.area(); - area.height = 4; - let areas = layout.areas::<13>(area); - // if let Some(hand) = rendered_hand.0.get(&*main_player) { + let this_clamped = this_hand.height.abs_diff(5); + if let Some(val) = this_hand.height.checked_sub(this_clamped) { + this_hand.height = val + } else { + // FIXME show error + panic!("terminal too small!"); + } + this_hand.y += this_clamped + 1; + let areas = layout.areas::<13>(this_hand); if let Some(hand) = rendered_hand.0.get(&*main_player) { - for (tile, area) in hand.iter().zip(areas.iter()) { - frame.render_widget(&rendered_tiles.get(*tile).unwrap().0, *area); + for (tile, mut tile_area) in hand.iter().zip(areas.into_iter()) { + tile_area.height = 4; + let mut widget = rendered_tiles.get(*tile).unwrap().0.clone(); + if hovered_entity.contains(*tile) { + widget = widget.add_modifier(Modifier::BOLD); + if let Some(val) = tile_area.y.checked_sub(1) { + tile_area.y = val + } else { + // FIXME show error + panic!("terminal too small!"); + } + } + commands + .entity(*tile) + .insert(PickRegion { area: tile_area }); + frame.render_widget(widget, tile_area); } } })?; diff --git a/src/tui/render/mod.rs b/src/tui/render/mod.rs index 0e3cc55..a8aef30 100644 --- a/src/tui/render/mod.rs +++ b/src/tui/render/mod.rs @@ -1,3 +1,8 @@ +use bevy::prelude::*; + pub(crate) mod hand; pub(crate) mod ingame; pub(crate) mod tile; + +#[derive(Component)] +pub(crate) struct Hovered; From 6e6b792a585ce1fa22a447d1d71f03ce90470146 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:38:41 -0800 Subject: [PATCH 4/8] turnstates --- src/game/hand.rs | 40 ++++++++++++++++++++++++++- src/game/mod.rs | 68 ++++++++++++++++++++++++---------------------- src/game/player.rs | 6 ++++ src/game/round.rs | 46 ++++++++++++++++++++++++++++++- src/game/wall.rs | 6 ++-- src/lib.rs | 6 ++-- src/main.rs | 2 -- 7 files changed, 133 insertions(+), 41 deletions(-) diff --git a/src/game/hand.rs b/src/game/hand.rs index 434fe16..898e6de 100644 --- a/src/game/hand.rs +++ b/src/game/hand.rs @@ -1,13 +1,16 @@ use bevy::{ecs::relationship::RelationshipSourceCollection, prelude::*}; use crate::{ - game::{player::Player /* wall::WallTiles */}, + game::{GameState, player::Player, wall::Wall /* wall::WallTiles */}, tile::Tile, }; #[derive(Component)] pub struct Hand; +#[derive(Component)] +pub struct Drawn(pub Entity); + // #[derive(Component, Default)] // enum SortHand { // #[default] @@ -26,3 +29,38 @@ pub(crate) fn sort_hands( } Ok(()) } + +pub(crate) fn shuffle_deal( + mut commands: Commands, + tiles: Populated>, + players: Populated>, + wall_ent: Single>, + mut next_gamestate: ResMut>, +) -> Result { + use rand::seq::SliceRandom; + + let mut rng = rand::rng(); + let mut walltiles: Vec<_> = tiles.iter().collect(); + walltiles.shuffle(&mut rng); + + // commands.get_entity(*wall_ent)?.replace_children(&walltiles); + + for player_ent in players { + let handtiles = walltiles.split_off(walltiles.len() - 13); + // commands.get_entity(*wall_ent)?.remove_children(&handtiles); + + let hand_ent = commands.spawn(Hand).add_children(&handtiles).id(); + debug!("hand_ent: {hand_ent:?}"); + + commands + .get_entity(player_ent)? + .replace_children(&[hand_ent]); + + debug!("dealt to player_ent {player_ent:?}"); + } + + commands.get_entity(*wall_ent)?.replace_children(&walltiles); + + next_gamestate.set(GameState::Play); + Ok(()) +} diff --git a/src/game/mod.rs b/src/game/mod.rs index e42c311..23e9711 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,9 +1,12 @@ use bevy::prelude::*; +use strum::{EnumCount, FromRepr}; use crate::{ + EnumNextCycle, game::{ - hand::Hand, + hand::{Drawn, Hand}, player::{MainPlayer, Player}, + round::{CurrentPlayer, TurnState, Wind}, wall::Wall, }, tile::{self, *}, @@ -29,50 +32,45 @@ impl Plugin for Riichi { app // start stopper .init_state::() + .add_sub_state::() .init_resource::() .init_resource::() .add_systems(Startup, tile::init_tiles) .add_systems(OnEnter(GameState::Setup), setup) - .add_systems(OnEnter(GameState::Deal), shuffle_deal) + .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), tsumo) // semicolon stopper ; } } -fn shuffle_deal( +// struct TurnEvent { +// pub next: Option, +// pub prev: Option, +// } + +// fn turn_manager() {} + +fn tsumo( mut commands: Commands, - tiles: Populated>, - players: Populated>, + curr_player: Res, + // players: Populated>, wall_ent: Single>, - mut next_gamestate: ResMut>, -) -> Result { - use rand::seq::SliceRandom; + walltiles: Single<&Children, With>, + curr_turnstate: Res>, + mut next_turnstate: ResMut>, +) { + trace!("tsumo for: {:?}", curr_player.0); - let mut rng = rand::rng(); - let mut walltiles: Vec<_> = tiles.iter().collect(); - walltiles.shuffle(&mut rng); + let drawn = walltiles.last().unwrap(); + commands.entity(*wall_ent).remove_child(*drawn); + commands.entity(curr_player.0).insert(Drawn(*drawn)); - // commands.get_entity(*wall_ent)?.replace_children(&walltiles); + debug!("wall: {:?}", *walltiles); - for player_ent in players { - let handtiles = walltiles.split_off(walltiles.len() - 13); - // commands.get_entity(*wall_ent)?.remove_children(&handtiles); - - let hand_ent = commands.spawn(Hand).add_children(&handtiles).id(); - debug!("hand_ent: {hand_ent:?}"); - - commands - .get_entity(player_ent)? - .replace_children(&[hand_ent]); - - debug!("dealt to player_ent {player_ent:?}"); - } - - commands.get_entity(*wall_ent)?.replace_children(&walltiles); - - next_gamestate.set(GameState::Play); - Ok(()) + next_turnstate.set(curr_turnstate.next()); } pub(crate) fn setup( @@ -88,10 +86,16 @@ pub(crate) fn setup( }; let points = player::Points(matchsettings.starting_points); - let bundle = (player, points, Hand); + let bundle = ( + player, + points, + Hand, + Wind::from_repr((i - 1) as usize).unwrap(), + ); if i == 1 { - commands.spawn((bundle, MainPlayer)); + let player = commands.spawn((bundle, MainPlayer)).id(); + commands.insert_resource(CurrentPlayer(player)); } else { commands.spawn(bundle); } diff --git a/src/game/player.rs b/src/game/player.rs index c7c3e6c..d241ba6 100644 --- a/src/game/player.rs +++ b/src/game/player.rs @@ -10,3 +10,9 @@ pub struct Points(pub isize); #[derive(Component)] pub struct MainPlayer; + +#[derive(Component)] +pub struct Dealer; + +#[derive(Component)] +pub struct Tsumo(pub Entity); diff --git a/src/game/round.rs b/src/game/round.rs index 5f8a3be..0d50e3f 100644 --- a/src/game/round.rs +++ b/src/game/round.rs @@ -1,6 +1,7 @@ use bevy::prelude::*; +use strum::{EnumCount, FromRepr}; -use crate::tile::*; +use crate::{EnumNextCycle, game::GameState}; #[derive(Component)] pub(crate) struct Dice(u8, u8); @@ -40,3 +41,46 @@ impl Default for MatchSettings { } } } + +#[derive(Component, Clone, Copy, FromRepr, EnumCount)] +pub enum Wind { + Ton, + Nan, + Shaa, + Pei, +} + +impl EnumNextCycle for Wind { + fn next(&self) -> Self { + if (*self as usize + 1) >= Self::COUNT { + Self::from_repr(0).unwrap() + } else { + Self::from_repr(*self as usize + 1).unwrap() + } + } +} + +#[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 { + Self::from_repr(0).unwrap() + } else { + Self::from_repr(*self as usize + 1).unwrap() + } + } +} diff --git a/src/game/wall.rs b/src/game/wall.rs index de6ac75..e382cf6 100644 --- a/src/game/wall.rs +++ b/src/game/wall.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; -use rand::seq::SliceRandom; - -use crate::tile::Tile; #[derive(Component)] pub struct Wall; + +#[derive(Component)] +pub struct Dead; diff --git a/src/lib.rs b/src/lib.rs index a64f794..480c8a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ -#![allow(unused)] - pub mod game; pub mod tile; pub mod yakus; + +trait EnumNextCycle { + fn next(&self) -> Self; +} diff --git a/src/main.rs b/src/main.rs index 1a8dd9a..aa4e2ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,3 @@ -#![allow(unused)] - use bevy::{log::LogPlugin, prelude::*}; use clap::{Parser, Subcommand}; use tracing::Level; From b5e670b4914cf36311e6a40f2740ce94fdaf20c1 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:07:21 -0800 Subject: [PATCH 5/8] draw tile --- src/game/hand.rs | 2 +- src/game/mod.rs | 26 ++------------- src/game/round.rs | 25 ++++++++++++++- src/tui/console.rs | 2 +- src/tui/render/ingame.rs | 68 +++++++++++++++++++++++++++++++--------- 5 files changed, 82 insertions(+), 41 deletions(-) diff --git a/src/game/hand.rs b/src/game/hand.rs index 898e6de..de7ed08 100644 --- a/src/game/hand.rs +++ b/src/game/hand.rs @@ -9,7 +9,7 @@ use crate::{ pub struct Hand; #[derive(Component)] -pub struct Drawn(pub Entity); +pub struct DrawnTile(pub Entity); // #[derive(Component, Default)] // enum SortHand { diff --git a/src/game/mod.rs b/src/game/mod.rs index 23e9711..8a8720e 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -4,7 +4,7 @@ use strum::{EnumCount, FromRepr}; use crate::{ EnumNextCycle, game::{ - hand::{Drawn, Hand}, + hand::{DrawnTile, Hand}, player::{MainPlayer, Player}, round::{CurrentPlayer, TurnState, Wind}, wall::Wall, @@ -40,7 +40,7 @@ impl Plugin for Riichi { .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), tsumo) + .add_systems(OnEnter(TurnState::Tsumo), round::tsumo) // semicolon stopper ; } @@ -53,31 +53,11 @@ impl Plugin for Riichi { // fn turn_manager() {} -fn tsumo( - mut commands: Commands, - curr_player: Res, - // players: Populated>, - wall_ent: Single>, - walltiles: Single<&Children, With>, - curr_turnstate: Res>, - mut next_turnstate: ResMut>, -) { - trace!("tsumo for: {:?}", curr_player.0); - - let drawn = walltiles.last().unwrap(); - commands.entity(*wall_ent).remove_child(*drawn); - commands.entity(curr_player.0).insert(Drawn(*drawn)); - - debug!("wall: {:?}", *walltiles); - - next_turnstate.set(curr_turnstate.next()); -} - pub(crate) fn setup( mut commands: Commands, matchsettings: Res, // mut compass: ResMut - tiles: Query>, + // tiles: Query>, mut next_gamestate: ResMut>, ) { for i in 1..=matchsettings.player_count { diff --git a/src/game/round.rs b/src/game/round.rs index 0d50e3f..9cf95df 100644 --- a/src/game/round.rs +++ b/src/game/round.rs @@ -1,7 +1,10 @@ use bevy::prelude::*; use strum::{EnumCount, FromRepr}; -use crate::{EnumNextCycle, game::GameState}; +use crate::{ + EnumNextCycle, + game::{GameState, hand::DrawnTile, wall::Wall}, +}; #[derive(Component)] pub(crate) struct Dice(u8, u8); @@ -84,3 +87,23 @@ impl EnumNextCycle for TurnState { } } } + +pub(crate) fn tsumo( + mut commands: Commands, + curr_player: Res, + // players: Populated>, + wall_ent: Single>, + walltiles: Single<&Children, With>, + curr_turnstate: Res>, + mut next_turnstate: ResMut>, +) { + trace!("tsumo for: {:?}", curr_player.0); + + let drawn = walltiles.last().unwrap(); + commands.entity(*wall_ent).remove_child(*drawn); + commands.entity(curr_player.0).insert(DrawnTile(*drawn)); + + debug!("drew: {:?}", drawn); + + next_turnstate.set(curr_turnstate.next()); +} diff --git a/src/tui/console.rs b/src/tui/console.rs index efdcd7a..f668331 100644 --- a/src/tui/console.rs +++ b/src/tui/console.rs @@ -5,8 +5,8 @@ use tui_logger::TuiLoggerWidget; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] pub(crate) enum ConsoleState { - Closed, #[default] + Closed, Open, } diff --git a/src/tui/render/ingame.rs b/src/tui/render/ingame.rs index 1c2a1aa..ce227ab 100644 --- a/src/tui/render/ingame.rs +++ b/src/tui/render/ingame.rs @@ -1,10 +1,13 @@ use bevy::prelude::*; use bevy_ratatui::RatatuiContext; +use jong::game::hand::DrawnTile; use jong::game::player::{MainPlayer, Player}; +use jong::tile::Tile; use rand::rand_core::block::BlockRngCore; use ratatui::style::Styled; use ratatui::widgets::{Block, Borders, Widget}; +use crate::tui::render::tile::draw_tile; use crate::tui::{ input::mouse::PickRegion, render::{Hovered, hand, tile::RenderedTile}, @@ -13,10 +16,12 @@ use crate::tui::{ pub(crate) fn draw_ingame( mut commands: Commands, mut tui_ctx: ResMut, - main_player: Single, With)>, + tiles: Query<&Tile>, hovered_entity: Query>, + main_player: Single, With)>, rendered_tiles: Populated<&RenderedTile>, rendered_hand: Res, + drawn_tile: Option>>, ) -> Result { use ratatui::layout::Flex; use ratatui::prelude::*; @@ -45,14 +50,15 @@ pub(crate) fn draw_ingame( ); let pond_area = term_area.inner(margin); let all_pond = Layout::horizontal([Constraint::Fill(1); 3]); - let cross_pond = Layout::vertical([Constraint::Fill(1), Constraint::Max(1),Constraint::Fill(1)]); + let cross_pond = + Layout::vertical([Constraint::Fill(1), Constraint::Max(1), Constraint::Fill(1)]); let [mut left_pond, center, mut right_pond] = all_pond.areas::<3>(pond_area); let [cross_pond, compass, this_pond] = cross_pond.areas::<3>(center); // let shift = left_pond.height - cross_pond.height; left_pond.height = cross_pond.height; left_pond.y += cross_pond.height / 2; right_pond.height = cross_pond.height; - right_pond.y+= cross_pond.height / 2; + right_pond.y += cross_pond.height / 2; let debug_block = Block::new().borders(Borders::ALL); frame.render_widget(debug_block.clone(), this_hand); @@ -65,19 +71,30 @@ pub(crate) fn draw_ingame( frame.render_widget(debug_block.clone(), right_pond); // TODO attempt to merge blocks on smol term? - let layout = Layout::horizontal(vec![Constraint::Max(5); 13]).flex(Flex::Start); - let this_clamped = this_hand.height.abs_diff(5); - if let Some(val) = this_hand.height.checked_sub(this_clamped) { - this_hand.height = val - } else { - // FIXME show error - panic!("terminal too small!"); - } - this_hand.y += this_clamped + 1; - let areas = layout.areas::<13>(this_hand); if let Some(hand) = rendered_hand.0.get(&*main_player) { - for (tile, mut tile_area) in hand.iter().zip(areas.into_iter()) { - tile_area.height = 4; + let hand_area_layout = Layout::horizontal([ + Constraint::Max(hand.len() as u16 * 5), + Constraint::Max(if drawn_tile.is_some() { 7 } else { 0 }), + Constraint::Fill(1), + ]) + .flex(Flex::SpaceBetween); + let this_clamped = this_hand.height.abs_diff(5); + if let Some(val) = this_hand.height.checked_sub(this_clamped) { + this_hand.height = 4 + } else { + // FIXME show error + panic!("terminal too small!"); + } + this_hand.y += this_clamped + 1; + let [this_hand, mut this_drawn, this_meld] = hand_area_layout.areas::<3>(this_hand); + + // this_hand + let mut constraints = vec![Constraint::Max(5); hand.len()]; + constraints.push(Constraint::Fill(1)); + let layout = Layout::horizontal(constraints).flex(Flex::Start); + let hand_areas = layout.split(this_hand); + for (tile, mut tile_area) in hand.iter().zip(hand_areas.iter().cloned()) { + // tile_area.height = 4; let mut widget = rendered_tiles.get(*tile).unwrap().0.clone(); if hovered_entity.contains(*tile) { widget = widget.add_modifier(Modifier::BOLD); @@ -93,6 +110,27 @@ pub(crate) fn draw_ingame( .insert(PickRegion { area: tile_area }); frame.render_widget(widget, tile_area); } + + // this_drawn + if let Some(tile) = drawn_tile { + // this_drawn.height = 4; + this_drawn.width = 5; + this_drawn.x += 2; + let mut widget = draw_tile(tiles.get(tile.0).unwrap()); + if hovered_entity.contains(tile.0) { + widget = widget.add_modifier(Modifier::BOLD); + if let Some(val) = this_drawn.y.checked_sub(1) { + this_drawn.y = val + } else { + // FIXME show error + panic!("terminal too small!"); + } + } + commands + .entity(tile.0) + .insert(PickRegion { area: this_drawn }); + frame.render_widget(widget, this_drawn); + } } })?; From f46519789606b3b2ff91f3444670e4a059e0bbc6 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:42:13 -0800 Subject: [PATCH 6/8] cargo fix --- src/game/hand.rs | 6 +++--- src/game/mod.rs | 8 +++----- src/tui/input/mouse.rs | 6 +++--- src/tui/menu.rs | 12 ++++-------- src/tui/mod.rs | 2 -- src/tui/render/hand.rs | 1 - src/tui/render/ingame.rs | 16 +++++++--------- src/tui/render/tile.rs | 2 +- 8 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/game/hand.rs b/src/game/hand.rs index de7ed08..fd5a97b 100644 --- a/src/game/hand.rs +++ b/src/game/hand.rs @@ -1,7 +1,7 @@ -use bevy::{ecs::relationship::RelationshipSourceCollection, prelude::*}; +use bevy::prelude::*; use crate::{ - game::{GameState, player::Player, wall::Wall /* wall::WallTiles */}, + game::{GameState, player::Player, wall::Wall}, tile::Tile, }; @@ -21,7 +21,7 @@ pub struct DrawnTile(pub Entity); pub(crate) fn sort_hands( tiles: Populated<&Tile>, - mut hands: Populated<&mut Children, (Changed, Without)>, + hands: Populated<&mut Children, (Changed, Without)>, ) -> Result { for mut hand in hands { hand.sort_unstable_by_key(|e| tiles.get(*e).unwrap().suit); diff --git a/src/game/mod.rs b/src/game/mod.rs index 8a8720e..c296e21 100644 --- a/src/game/mod.rs +++ b/src/game/mod.rs @@ -1,15 +1,13 @@ use bevy::prelude::*; -use strum::{EnumCount, FromRepr}; use crate::{ - EnumNextCycle, game::{ - hand::{DrawnTile, Hand}, - player::{MainPlayer, Player}, + hand::Hand, + player::MainPlayer, round::{CurrentPlayer, TurnState, Wind}, wall::Wall, }, - tile::{self, *}, + tile::{self}, }; pub mod hand; diff --git a/src/tui/input/mouse.rs b/src/tui/input/mouse.rs index 5926222..acc9fa0 100644 --- a/src/tui/input/mouse.rs +++ b/src/tui/input/mouse.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; use bevy_ratatui::{RatatuiContext, event::MouseMessage}; -use ratatui::{crossterm::event::MouseEvent, layout::Position}; +use ratatui::layout::Position; use crate::tui::render::Hovered; @@ -17,7 +17,7 @@ pub(crate) struct PickRegion { pub(crate) fn input_system( mut commands: Commands, mut messages: MessageReader, - context: Res, + _context: Res, entities: Query<(Entity, &PickRegion)>, hovered: Query<(Entity, &PickRegion), With>, ) -> Result { @@ -28,7 +28,7 @@ pub(crate) fn input_system( match event.kind { ratatui::crossterm::event::MouseEventKind::Down(mouse_button) => match mouse_button { ratatui::crossterm::event::MouseButton::Left => { - for (entity, region) in &entities {} + for (_entity, _region) in &entities {} } // ratatui::crossterm::event::MouseButton::Right => todo!(), // ratatui::crossterm::event::MouseButton::Middle => todo!(), diff --git a/src/tui/menu.rs b/src/tui/menu.rs index 104e193..f004f41 100644 --- a/src/tui/menu.rs +++ b/src/tui/menu.rs @@ -1,17 +1,11 @@ use bevy::prelude::*; use bevy_ratatui::RatatuiContext; -use bevy_ratatui::event::KeyMessage; -use ratatui::crossterm::event::KeyCode; use ratatui::layout::Constraint; use ratatui::layout::Layout; -use jong::game::GameState; - -use crate::tui::TuiState; - const MAINMENU_OPTIONS: [&str; 2] = ["(p)lay", "(q)uit"]; -pub(crate) fn draw_mainmenu(mut tui_ctx: ResMut) { +pub(crate) fn draw_mainmenu(mut tui_ctx: ResMut) -> Result { let options = MAINMENU_OPTIONS; let layout = Layout::vertical(vec![Constraint::Min(1); options.len()]); @@ -20,5 +14,7 @@ pub(crate) fn draw_mainmenu(mut tui_ctx: ResMut) { for (opt, area) in options.into_iter().zip(areas.iter()) { frame.render_widget(opt, *area) } - }); + })?; + + Ok(()) } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 05311b0..69c182a 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -2,12 +2,10 @@ use std::time::Duration; use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin}; use bevy_ratatui::RatatuiPlugins; -use ratatui::{text::ToSpan, widgets::Paragraph}; use jong::game::GameState; // use jong::game::wall::InWall; -use crate::tui::{console::ConsoleState, menu::draw_mainmenu, render::ingame::draw_ingame}; mod console; mod menu; diff --git a/src/tui/render/hand.rs b/src/tui/render/hand.rs index 939fdf9..944aa56 100644 --- a/src/tui/render/hand.rs +++ b/src/tui/render/hand.rs @@ -1,5 +1,4 @@ use bevy::{platform::collections::HashMap, prelude::*}; -use ratatui::widgets::Paragraph; use jong::{ game::{hand::Hand, player::Player}, diff --git a/src/tui/render/ingame.rs b/src/tui/render/ingame.rs index ce227ab..bb74910 100644 --- a/src/tui/render/ingame.rs +++ b/src/tui/render/ingame.rs @@ -3,9 +3,7 @@ use bevy_ratatui::RatatuiContext; use jong::game::hand::DrawnTile; use jong::game::player::{MainPlayer, Player}; use jong::tile::Tile; -use rand::rand_core::block::BlockRngCore; -use ratatui::style::Styled; -use ratatui::widgets::{Block, Borders, Widget}; +use ratatui::widgets::{Block, Borders}; use crate::tui::render::tile::draw_tile; use crate::tui::{ @@ -38,11 +36,11 @@ pub(crate) fn draw_ingame( let horizontal_slicer_left = Layout::horizontal(constraints.iter().rev()); let vertical_slicer_top = Layout::vertical(constraints.iter().rev()); let [left_hand, this_hand] = horizontal_slicer_right.areas::<2>(term_area); - let [_, mut left_hand] = vertical_slicer_bottom.areas::<2>(left_hand); + let [_, left_hand] = vertical_slicer_bottom.areas::<2>(left_hand); let [_, mut this_hand] = vertical_slicer_top.areas::<2>(this_hand); let [cross_hand, right_hand] = horizontal_slicer_left.areas::<2>(term_area); - let [mut right_hand, _] = vertical_slicer_top.areas::<2>(right_hand); - let [mut cross_hand, _] = vertical_slicer_bottom.areas::<2>(cross_hand); + let [right_hand, _] = vertical_slicer_top.areas::<2>(right_hand); + let [cross_hand, _] = vertical_slicer_bottom.areas::<2>(cross_hand); let margin = Margin::new( (left_hand.width + right_hand.width) / 2, @@ -53,7 +51,7 @@ pub(crate) fn draw_ingame( let cross_pond = Layout::vertical([Constraint::Fill(1), Constraint::Max(1), Constraint::Fill(1)]); let [mut left_pond, center, mut right_pond] = all_pond.areas::<3>(pond_area); - let [cross_pond, compass, this_pond] = cross_pond.areas::<3>(center); + let [cross_pond, _compass, this_pond] = cross_pond.areas::<3>(center); // let shift = left_pond.height - cross_pond.height; left_pond.height = cross_pond.height; left_pond.y += cross_pond.height / 2; @@ -79,14 +77,14 @@ pub(crate) fn draw_ingame( ]) .flex(Flex::SpaceBetween); let this_clamped = this_hand.height.abs_diff(5); - if let Some(val) = this_hand.height.checked_sub(this_clamped) { + if let Some(_val) = this_hand.height.checked_sub(this_clamped) { this_hand.height = 4 } else { // FIXME show error panic!("terminal too small!"); } this_hand.y += this_clamped + 1; - let [this_hand, mut this_drawn, this_meld] = hand_area_layout.areas::<3>(this_hand); + let [this_hand, mut this_drawn, _this_meld] = hand_area_layout.areas::<3>(this_hand); // this_hand let mut constraints = vec![Constraint::Max(5); hand.len()]; diff --git a/src/tui/render/tile.rs b/src/tui/render/tile.rs index 59d50bd..95bf282 100644 --- a/src/tui/render/tile.rs +++ b/src/tui/render/tile.rs @@ -7,7 +7,7 @@ use jong::tile::Tile; pub(crate) struct RenderedTile(pub(crate) Paragraph<'static>); pub(crate) fn draw_tile(tile: &Tile) -> Paragraph<'static> { - use ratatui::prelude::*; + let block = ratatui::widgets::Block::bordered(); From 3b026c73cd84cb90af0cbd03f9b8dc8d2ab7f4a6 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:46:10 -0800 Subject: [PATCH 7/8] begin zensplash --- src/tile.rs | 2 +- src/tui/console.rs | 7 +++-- src/tui/input/keyboard.rs | 8 ++++- src/tui/menu.rs | 66 ++++++++++++++++++++++++++++++++++++--- src/tui/mod.rs | 14 +++++++-- 5 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/tile.rs b/src/tile.rs index 40879fe..c0941d4 100644 --- a/src/tile.rs +++ b/src/tile.rs @@ -1,7 +1,7 @@ use bevy::{ecs::entity::MapEntities, prelude::*}; use strum::FromRepr; -#[derive(Component, Debug)] +#[derive(Component, Debug, Clone, Copy)] pub struct Tile { pub suit: Suit, } diff --git a/src/tui/console.rs b/src/tui/console.rs index f668331..2390c5e 100644 --- a/src/tui/console.rs +++ b/src/tui/console.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; use bevy_ratatui::RatatuiContext; -use ratatui::widgets::Block; +use ratatui::{layout::Margin, widgets::Block}; use tui_logger::TuiLoggerWidget; #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] @@ -24,7 +24,10 @@ impl std::ops::Not for ConsoleState { pub(crate) fn draw_console(mut tui_ctx: ResMut) -> Result { tui_ctx.draw(|frame| { let block = Block::bordered().title("console"); - frame.render_widget(TuiLoggerWidget::default().block(block), frame.area()); + frame.render_widget( + TuiLoggerWidget::default().block(block), + frame.area(), /* .inner(Margin { horizontal: 8, vertical: 8 }) */ + ); })?; Ok(()) diff --git a/src/tui/input/keyboard.rs b/src/tui/input/keyboard.rs index ee77c1a..2a5e760 100644 --- a/src/tui/input/keyboard.rs +++ b/src/tui/input/keyboard.rs @@ -4,7 +4,7 @@ use bevy_ratatui::event::KeyMessage; use jong::game::GameState; -use crate::tui::{TuiState, console::ConsoleState}; +use crate::tui::{TuiState, ZenState, console::ConsoleState}; // TODO change this to handle console open request, esc for menu, etc, then // route other messages to other systems @@ -16,10 +16,12 @@ pub(crate) fn input_system( curr_tuistate: Res>, curr_consolestate: Res>, curr_gamestate: Res>, + curr_zenstate: Res>, mut next_tuistate: ResMut>, mut next_consolestate: ResMut>, mut next_gamestate: ResMut>, + mut next_zenstate: ResMut>, mut exit: MessageWriter, ) { @@ -58,6 +60,10 @@ pub(crate) fn input_system( next_tuistate.set(TuiState::InGame); next_gamestate.set(GameState::Setup); } + KeyCode::Char('z') => match curr_zenstate.get() { + ZenState::Menu => next_zenstate.set(ZenState::Zen), + ZenState::Zen => next_zenstate.set(ZenState::Menu), + }, KeyCode::Char('q') => { exit.write_default(); } diff --git a/src/tui/menu.rs b/src/tui/menu.rs index f004f41..c1c455c 100644 --- a/src/tui/menu.rs +++ b/src/tui/menu.rs @@ -1,20 +1,78 @@ use bevy::prelude::*; use bevy_ratatui::RatatuiContext; use ratatui::layout::Constraint; +use ratatui::layout::Flex; use ratatui::layout::Layout; +use ratatui::layout::Margin; +use ratatui::widgets::Paragraph; +use ratatui::widgets::{Block, Clear}; -const MAINMENU_OPTIONS: [&str; 2] = ["(p)lay", "(q)uit"]; +use jong::tile::Tile; + +use crate::tui::render::tile; + +const MAINMENU_OPTIONS: [&str; 2] = [ + " ██╗██████╗ ██╗ ██╗ █████╗ ██╗ ██╗██╗ +██╔╝██╔══██╗╚██╗██║ ██╔══██╗╚██╗ ██╔╝██║ +██║ ██████╔╝ ██║██║ ███████║ ╚████╔╝ ██║ +██║ ██╔═══╝ ██║██║ ██╔══██║ ╚██╔╝ ╚═╝ +╚██╗██║ ██╔╝███████╗██║ ██║ ██║ ██╗ + ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ +", + " ██╗ ██████╗ ██╗ ██╗ ██╗██╗████████╗ ██╗ +██╔╝██╔═══██╗╚██╗██║ ██║██║╚══██╔══╝██╗██╔╝ +██║ ██║ ██║ ██║██║ ██║██║ ██║ ╚═╝██║ +██║ ██║▄▄ ██║ ██║██║ ██║██║ ██║ ██╗██║ +╚██╗╚██████╔╝██╔╝╚██████╔╝██║ ██║ ╚═╝╚██╗ + ╚═╝ ╚══▀▀═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ +", +]; pub(crate) fn draw_mainmenu(mut tui_ctx: ResMut) -> Result { let options = MAINMENU_OPTIONS; - let layout = Layout::vertical(vec![Constraint::Min(1); options.len()]); + let layout = + Layout::vertical(vec![Constraint::Fill(1); options.len()]).flex(Flex::SpaceBetween); + let block = Block::bordered(); tui_ctx.draw(|frame| { - let areas = layout.split(frame.area()); + let area = frame + .area() + .centered(Constraint::Max(55), Constraint::Max(19)); + frame.render_widget(Clear, area); + frame.render_widget(block, area); + let areas = layout.split(area.centered(Constraint::Max(45), Constraint::Max(14))); for (opt, area) in options.into_iter().zip(areas.iter()) { - frame.render_widget(opt, *area) + let para = Paragraph::new(opt); + frame.render_widget(para, *area) } })?; Ok(()) } + +pub(crate) fn draw_splash(mut tui_ctx: ResMut, tiles: Populated<&Tile>) -> Result { + let mut tile_it = tiles.into_iter().cycle(); + + tui_ctx.draw(|frame| { + let area = frame.area().outer(Margin { + horizontal: 1, + vertical: 1, + }); + let layout = Layout::horizontal(vec![Constraint::Max(5); (area.width / 5) as usize]); + let areas = layout.split(area); + for area in areas.iter() { + let layout = Layout::vertical(vec![Constraint::Max(4); (area.height / 4) as usize]); + let areas = layout.split(*area); + let tiles: Vec<_> = tile_it + .clone() + .take((area.height / 4 + 1) as usize) + .cloned() + .map(|t| tile::draw_tile(&t)) + .collect(); + for (tile, area) in tiles.iter().zip(areas.into_iter()) { + frame.render_widget(tile, *area); + } + } + })?; + Ok(()) +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 69c182a..f3d95e8 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -27,12 +27,20 @@ impl ComputedStates for InGame { fn compute(sources: Self::SourceStates) -> Option { match sources { - TuiState::MainMenu => None, TuiState::InGame => Some(Self), + _ => None, } } } +#[derive(SubStates, Default, Clone, Copy, PartialEq, Eq, Hash, Debug,)] +#[source(TuiState = TuiState::MainMenu)] +pub(crate) enum ZenState { + #[default] + Menu, + Zen, +} + #[derive(Default)] pub struct RiichiTui; impl Plugin for RiichiTui { @@ -61,7 +69,9 @@ impl Plugin for RiichiTui { .add_systems(PreUpdate, input::mouse::input_system.chain()) // main menu - .add_systems(Update, menu::draw_mainmenu.run_if(in_state(TuiState::MainMenu))) + .add_sub_state::() + .add_systems(Update, menu::draw_splash.run_if(in_state(TuiState::MainMenu))) + .add_systems(Update, menu::draw_mainmenu.after(menu::draw_splash).run_if(in_state(TuiState::MainMenu).and(in_state(ZenState::Menu)))) // gaming .init_resource::() From 2447e60f16349c35c3040022f159194f75baaa41 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Tue, 13 Jan 2026 17:05:56 -0800 Subject: [PATCH 8/8] draw queue --- src/tile.rs | 2 +- src/tui/console.rs | 14 ++++----- src/tui/input/keyboard.rs | 14 +++++---- src/tui/mod.rs | 27 +++++++++++------ src/tui/{ => render}/menu.rs | 56 ++++++++++++++++++++++++------------ src/tui/render/mod.rs | 18 ++++++++++++ 6 files changed, 90 insertions(+), 41 deletions(-) rename src/tui/{ => render}/menu.rs (65%) diff --git a/src/tile.rs b/src/tile.rs index c0941d4..89fe820 100644 --- a/src/tile.rs +++ b/src/tile.rs @@ -6,7 +6,7 @@ pub struct Tile { pub suit: Suit, } -#[derive(MapEntities, Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)] +#[derive(/* MapEntities, */ Debug, PartialEq, PartialOrd, Eq, Ord, Clone, Copy)] pub enum Suit { Man(Rank), Pin(Rank), diff --git a/src/tui/console.rs b/src/tui/console.rs index 2390c5e..7cd2171 100644 --- a/src/tui/console.rs +++ b/src/tui/console.rs @@ -1,8 +1,9 @@ use bevy::prelude::*; -use bevy_ratatui::RatatuiContext; -use ratatui::{layout::Margin, widgets::Block}; +use ratatui::widgets::{Block, Clear}; use tui_logger::TuiLoggerWidget; +use crate::tui::render::WidgetStack; + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] pub(crate) enum ConsoleState { #[default] @@ -21,14 +22,13 @@ impl std::ops::Not for ConsoleState { } } -pub(crate) fn draw_console(mut tui_ctx: ResMut) -> Result { - tui_ctx.draw(|frame| { +pub(crate) fn draw_console(mut widgets: ResMut) { + widgets.0.push(Box::new(|frame| { let block = Block::bordered().title("console"); + frame.render_widget(Clear, frame.area()); frame.render_widget( TuiLoggerWidget::default().block(block), frame.area(), /* .inner(Margin { horizontal: 8, vertical: 8 }) */ ); - })?; - - Ok(()) + })); } diff --git a/src/tui/input/keyboard.rs b/src/tui/input/keyboard.rs index 2a5e760..6e70e66 100644 --- a/src/tui/input/keyboard.rs +++ b/src/tui/input/keyboard.rs @@ -16,7 +16,7 @@ pub(crate) fn input_system( curr_tuistate: Res>, curr_consolestate: Res>, curr_gamestate: Res>, - curr_zenstate: Res>, + curr_zenstate: Option>>, mut next_tuistate: ResMut>, mut next_consolestate: ResMut>, @@ -60,10 +60,14 @@ pub(crate) fn input_system( next_tuistate.set(TuiState::InGame); next_gamestate.set(GameState::Setup); } - KeyCode::Char('z') => match curr_zenstate.get() { - ZenState::Menu => next_zenstate.set(ZenState::Zen), - ZenState::Zen => next_zenstate.set(ZenState::Menu), - }, + 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(); } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index f3d95e8..1b3a25f 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -4,11 +4,12 @@ use bevy::{app::ScheduleRunnerPlugin, prelude::*, state::app::StatesPlugin}; use bevy_ratatui::RatatuiPlugins; use jong::game::GameState; + +use crate::tui::render::{WidgetStack, menu::Splash}; // use jong::game::wall::InWall; mod console; -mod menu; mod render; mod input; @@ -58,25 +59,33 @@ impl Plugin for RiichiTui { )) .add_plugins(StatesPlugin) - // console - .init_state::() - .add_systems(Last, console::draw_console.run_if(in_state(console::ConsoleState::Open))) - // general setup .init_state::() .add_computed_state::() + .init_resource::() + .add_sub_state::() + .init_resource::() + .init_state::() + .add_systems(PostUpdate, console::draw_console.run_if(in_state(console::ConsoleState::Open))) + + // input .add_systems(PreUpdate, input::keyboard::input_system) - .add_systems(PreUpdate, input::mouse::input_system.chain()) + .add_systems(PreUpdate, input::mouse::input_system) // main menu - .add_sub_state::() - .add_systems(Update, menu::draw_splash.run_if(in_state(TuiState::MainMenu))) - .add_systems(Update, menu::draw_mainmenu.after(menu::draw_splash).run_if(in_state(TuiState::MainMenu).and(in_state(ZenState::Menu)))) + .add_systems(PostStartup, render::menu::init_splash) + .insert_resource(Time::::from_seconds(1.0)) + .add_systems(FixedUpdate, render::menu::render_splash.run_if(in_state(TuiState::MainMenu))) + .add_systems(Update, render::menu::draw_splash.run_if(in_state(TuiState::MainMenu))) + .add_systems(Update, render::menu::draw_mainmenu.after(render::menu::draw_splash).run_if(in_state(TuiState::MainMenu).and(in_state(ZenState::Menu)))) // gaming .init_resource::() .add_systems(Update, render::hand::render_hands.run_if(in_state(InGame).and(in_state(GameState::Play)))) .add_systems(Update, render::ingame::draw_ingame.run_if(in_state(InGame))) + + // render + .add_systems(Last, render::draw_system.run_if(not(in_state(InGame)))) // semicolon stopper ; diff --git a/src/tui/menu.rs b/src/tui/render/menu.rs similarity index 65% rename from src/tui/menu.rs rename to src/tui/render/menu.rs index c1c455c..2e58526 100644 --- a/src/tui/menu.rs +++ b/src/tui/render/menu.rs @@ -1,5 +1,7 @@ use bevy::prelude::*; use bevy_ratatui::RatatuiContext; +use rand::rng; +use rand::seq::SliceRandom; use ratatui::layout::Constraint; use ratatui::layout::Flex; use ratatui::layout::Layout; @@ -9,6 +11,7 @@ use ratatui::widgets::{Block, Clear}; use jong::tile::Tile; +use crate::tui::render::WidgetStack; use crate::tui::render::tile; const MAINMENU_OPTIONS: [&str; 2] = [ @@ -28,13 +31,13 @@ const MAINMENU_OPTIONS: [&str; 2] = [ ", ]; -pub(crate) fn draw_mainmenu(mut tui_ctx: ResMut) -> Result { +pub(crate) fn draw_mainmenu(mut widgets: ResMut) { let options = MAINMENU_OPTIONS; let layout = Layout::vertical(vec![Constraint::Fill(1); options.len()]).flex(Flex::SpaceBetween); let block = Block::bordered(); - tui_ctx.draw(|frame| { + widgets.0.push(Box::new(move |frame| { let area = frame .area() .centered(Constraint::Max(55), Constraint::Max(19)); @@ -45,34 +48,49 @@ pub(crate) fn draw_mainmenu(mut tui_ctx: ResMut) -> Result { let para = Paragraph::new(opt); frame.render_widget(para, *area) } - })?; - - Ok(()) + })); } -pub(crate) fn draw_splash(mut tui_ctx: ResMut, tiles: Populated<&Tile>) -> Result { - let mut tile_it = tiles.into_iter().cycle(); +#[derive(Resource, Default)] +pub(crate) struct Splash(pub Vec>); - tui_ctx.draw(|frame| { +pub(crate) fn init_splash(mut commands: Commands, tiles: Populated<&Tile>) { + let tiles: Vec<_> = tiles + .iter() + .copied() + .map(|tile| tile::draw_tile(&tile)) + .collect(); + + commands.insert_resource(Splash(tiles)); + trace!("init_splash") +} + +pub(crate) fn render_splash(mut splash: ResMut) { + let mut rng = rng(); + splash.0.shuffle(&mut rng); +} + +pub(crate) fn draw_splash(mut widgets: ResMut, splash: Res) { + let tiles: Vec<_> = splash.0.clone(); + widgets.0.push(Box::new(move |frame| { let area = frame.area().outer(Margin { horizontal: 1, vertical: 1, }); - let layout = Layout::horizontal(vec![Constraint::Max(5); (area.width / 5) as usize]); + let layout = Layout::horizontal(vec![Constraint::Length(5); (area.width / 5) as usize]); let areas = layout.split(area); + let mut tile_chunks = tiles.chunks(areas.len()).cycle(); for area in areas.iter() { - let layout = Layout::vertical(vec![Constraint::Max(4); (area.height / 4) as usize]); + let layout = Layout::vertical(vec![Constraint::Length(4); (area.height / 4) as usize]); let areas = layout.split(*area); - let tiles: Vec<_> = tile_it - .clone() - .take((area.height / 4 + 1) as usize) - .cloned() - .map(|t| tile::draw_tile(&t)) - .collect(); - for (tile, area) in tiles.iter().zip(areas.into_iter()) { + // let tiles: Vec<_> = tile_it + // .by_ref() + // .take((area.height / 4 + 1) as usize) + // .map(|t| tile::draw_tile(&t)) + // .collect(); + for (tile, area) in tile_chunks.next().unwrap().iter().zip(areas.iter()) { frame.render_widget(tile, *area); } } - })?; - Ok(()) + })); } diff --git a/src/tui/render/mod.rs b/src/tui/render/mod.rs index a8aef30..c206fea 100644 --- a/src/tui/render/mod.rs +++ b/src/tui/render/mod.rs @@ -1,8 +1,26 @@ use bevy::prelude::*; +use bevy_ratatui::RatatuiContext; +use ratatui::Frame; pub(crate) mod hand; pub(crate) mod ingame; +pub(crate) mod menu; pub(crate) mod tile; +#[derive(Resource, Default)] +pub(crate) struct WidgetStack(pub(crate) Vec>); + #[derive(Component)] pub(crate) struct Hovered; + +pub(crate) fn draw_system( + mut tui_ctx: ResMut, + mut widgetstack: ResMut, +) -> Result { + tui_ctx.draw(|frame| { + for widget in widgetstack.0.drain(..) { + widget(frame) + } + })?; + Ok(()) +}