From 1d9577ba4244f4eb9bcb27b491f5fc3e442e3591 Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:37:12 -0800 Subject: [PATCH 1/2] render pond --- jong-line/src/reducers/hand.rs | 2 ++ jong/src/riichi.rs | 28 ++++++++++++++++++++ jong/src/riichi/player.rs | 2 +- jong/src/tui.rs | 4 +-- jong/src/tui/render.rs | 47 +++++++++++++++++++++++++++++++--- 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/jong-line/src/reducers/hand.rs b/jong-line/src/reducers/hand.rs index 1e3f18a..842926a 100644 --- a/jong-line/src/reducers/hand.rs +++ b/jong-line/src/reducers/hand.rs @@ -49,6 +49,7 @@ pub fn discard_tile(ctx: &ReducerContext, tile_id: u32) -> Result<(), String> { let dealt_tile = if let Some(dealt) = ctx.db.tile().id().find(tile_id) { if let Some(drawn) = player.drawn_tile { if drawn.id == dealt.id { + // dealt from drawn tile dealt } else if let Some((i, _)) = player .hand @@ -56,6 +57,7 @@ pub fn discard_tile(ctx: &ReducerContext, tile_id: u32) -> Result<(), String> { .enumerate() .find(|(_, t)| t.id == tile_id) { + // dealt from hand let dealt = player.hand.remove(i); player.hand.push(drawn); player.hand.sort_by_key(|t| t.tile); diff --git a/jong/src/riichi.rs b/jong/src/riichi.rs index 0cd9eb9..61ad092 100644 --- a/jong/src/riichi.rs +++ b/jong/src/riichi.rs @@ -101,15 +101,24 @@ fn on_player_insert_update( mut messages: ReadInsertUpdateMessage, mut commands: Commands, + pond: Option>>, hand: Option>>, tiles: Query<(Entity, &TileId)>, ) { let hand = if hand.is_none() { let hand = commands.spawn(Hand).id(); + commands.spawn(Pond); hand } else { *hand.unwrap() }; + let pond = if pond.is_none() { + let pond = commands.spawn(Pond).id(); + commands.spawn(Pond); + pond + } else { + *pond.unwrap() + }; for msg in messages.read() { let hand_tiles: Vec<_> = msg @@ -125,11 +134,30 @@ fn on_player_insert_update( }) .collect(); + let pond_tiles: Vec<_> = msg + .new + .pond + .iter() + .map(|dbt| { + tiles + .iter() + .find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None }) + .expect(&format!( + "dealt tiles should still be around, couldn't find {:?}. Tiles: {:?}", + dbt, + tiles.iter().map(|(_, t)| t).collect::>() + )) + }) + .collect(); + debug!("hand_tiles: {hand_tiles:?}"); commands.entity(hand).replace_children(&hand_tiles); + commands.entity(pond).replace_children(&pond_tiles); + // drawn tile is always a new tile to us until wall isn't fake if let Some(dbt) = &msg.new.drawn_tile { + debug!("drew tile with id: {}", dbt.id); commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id), Drawn)); } } diff --git a/jong/src/riichi/player.rs b/jong/src/riichi/player.rs index 8c7ee4c..1622423 100644 --- a/jong/src/riichi/player.rs +++ b/jong/src/riichi/player.rs @@ -9,7 +9,7 @@ pub struct MainPlayer; #[derive(Component)] pub struct CurrentPlayer; -#[derive(Component, PartialEq, Eq)] +#[derive(Component, PartialEq, Eq, Debug)] pub struct TileId(pub u32); #[derive(Component)] diff --git a/jong/src/tui.rs b/jong/src/tui.rs index 18b994c..9198051 100644 --- a/jong/src/tui.rs +++ b/jong/src/tui.rs @@ -85,7 +85,7 @@ impl Plugin for TuiPlugin { .add_systems( Update, ( - render::render_hand.run_if(in_state(GameState::Play)), + (render::render_hand, render::render_pond).run_if(in_state(GameState::Play)), render::render, ) .chain() @@ -106,7 +106,7 @@ fn discard_tile( while let Some(message) = selected.read().next() { if let Ok(tile_id) = tiles.get(message.0) { stdb.reducers().discard_tile(tile_id.0).unwrap(); - commands.get_entity(drawn.0).unwrap().despawn(); + commands.entity(drawn.0).remove::(); } } } diff --git a/jong/src/tui/render.rs b/jong/src/tui/render.rs index f1a243c..409f7e2 100644 --- a/jong/src/tui/render.rs +++ b/jong/src/tui/render.rs @@ -6,10 +6,8 @@ use ratatui::layout::{Constraint, Flex, Layout, Offset, Rect, Size}; use ratatui::style::{Modifier, Stylize}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; -use jong::riichi::player::{CurrentPlayer, MainPlayer, Player}; -use jong::riichi::player::{Drawn, Hand}; -// use jong::riichi::round::Wind; -// use jong_types::*; +use jong::riichi::player::*; +use jong_types::*; use crate::tui::input::Hovered; use crate::tui::layout::*; @@ -197,3 +195,44 @@ pub(crate) fn render_hand( Ok(()) } + +pub(crate) fn render_pond( + mut commands: Commands, + mut tui: ResMut, + + hovered: Query>, + layouts: Res, + + pond: Single<(&Children, Entity), With>, + tiles: Query<&Tile>, +) -> Result { + let mut frame = tui.get_frame(); + + let pond: Vec<_> = pond + .0 + .iter() + .map(|entity| -> Result<_> { + let tile = tiles.get(entity).unwrap(); + let widget = render_tile(tile, false); + + Ok((entity, widget)) + }) + .collect::>()?; + + let mut this_pond = layouts.this_pond; + let row_constraints = [Constraint::Max(4); 3]; + let col_constraints = [Constraint::Max(5); 6]; + let row_layouts = Layout::vertical(row_constraints).flex(Flex::Start); + let col_layouts = Layout::horizontal(col_constraints).flex(Flex::Start); + let mut rows = row_layouts.areas::<3>(this_pond); + + for (rect, (_, tile)) in rows + .iter() + .flat_map(|row| col_layouts.areas::<6>(*row)) + .zip(pond) + { + frame.render_widget(tile, rect); + } + + Ok(()) +} From c12667938e350a7a719a6f34469e1022aaed1e4f Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Fri, 20 Feb 2026 03:20:29 -0800 Subject: [PATCH 2/2] (stash) state advancer reducer --- docs/states | 18 --- jong-db/src/db/advance_state_timer_table.rs | 144 ++++++++++++++++++++ jong-db/src/db/advance_state_timer_type.rs | 17 +++ jong-db/src/db/mod.rs | 21 +++ jong-line/src/lib.rs | 5 +- jong-line/src/reducers/lobby.rs | 3 +- jong-line/src/tables.rs | 58 +++++++- jong-line/src/tables/player.rs | 44 ------ jong/src/riichi.rs | 25 ++-- 9 files changed, 252 insertions(+), 83 deletions(-) delete mode 100644 docs/states create mode 100644 jong-db/src/db/advance_state_timer_table.rs create mode 100644 jong-db/src/db/advance_state_timer_type.rs delete mode 100644 jong-line/src/tables/player.rs diff --git a/docs/states b/docs/states deleted file mode 100644 index 13e0595..0000000 --- a/docs/states +++ /dev/null @@ -1,18 +0,0 @@ -gamestate - - none - join_or_create_lobby() - - lobby - set_ready() - add_bot() - start_game() - - setup - shuffle_deal() - - deal - animations?? - - play - discards?? diff --git a/jong-db/src/db/advance_state_timer_table.rs b/jong-db/src/db/advance_state_timer_table.rs new file mode 100644 index 0000000..a6096e0 --- /dev/null +++ b/jong-db/src/db/advance_state_timer_table.rs @@ -0,0 +1,144 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::advance_state_timer_type::AdvanceStateTimer; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `advance_state_timer`. +/// +/// Obtain a handle from the [`AdvanceStateTimerTableAccess::advance_state_timer`] method on [`super::RemoteTables`], +/// like `ctx.db.advance_state_timer()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.advance_state_timer().on_insert(...)`. +pub struct AdvanceStateTimerTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `advance_state_timer`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait AdvanceStateTimerTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`AdvanceStateTimerTableHandle`], which mediates access to the table `advance_state_timer`. + fn advance_state_timer(&self) -> AdvanceStateTimerTableHandle<'_>; +} + +impl AdvanceStateTimerTableAccess for super::RemoteTables { + fn advance_state_timer(&self) -> AdvanceStateTimerTableHandle<'_> { + AdvanceStateTimerTableHandle { + imp: self + .imp + .get_table::("advance_state_timer"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct AdvanceStateTimerInsertCallbackId(__sdk::CallbackId); +pub struct AdvanceStateTimerDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for AdvanceStateTimerTableHandle<'ctx> { + type Row = AdvanceStateTimer; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = AdvanceStateTimerInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> AdvanceStateTimerInsertCallbackId { + AdvanceStateTimerInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: AdvanceStateTimerInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = AdvanceStateTimerDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> AdvanceStateTimerDeleteCallbackId { + AdvanceStateTimerDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: AdvanceStateTimerDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("advance_state_timer"); + _table.add_unique_constraint::("scheduled_id", |row| &row.scheduled_id); +} +pub struct AdvanceStateTimerUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for AdvanceStateTimerTableHandle<'ctx> { + type UpdateCallbackId = AdvanceStateTimerUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> AdvanceStateTimerUpdateCallbackId { + AdvanceStateTimerUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: AdvanceStateTimerUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::TableUpdate<__ws::BsatnFormat>, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +/// Access to the `scheduled_id` unique index on the table `advance_state_timer`, +/// which allows point queries on the field of the same name +/// via the [`AdvanceStateTimerScheduledIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.advance_state_timer().scheduled_id().find(...)`. +pub struct AdvanceStateTimerScheduledIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> AdvanceStateTimerTableHandle<'ctx> { + /// Get a handle on the `scheduled_id` unique index on the table `advance_state_timer`. + pub fn scheduled_id(&self) -> AdvanceStateTimerScheduledIdUnique<'ctx> { + AdvanceStateTimerScheduledIdUnique { + imp: self.imp.get_unique_constraint::("scheduled_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> AdvanceStateTimerScheduledIdUnique<'ctx> { + /// Find the subscribed row whose `scheduled_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &u64) -> Option { + self.imp.find(col_val) + } +} diff --git a/jong-db/src/db/advance_state_timer_type.rs b/jong-db/src/db/advance_state_timer_type.rs new file mode 100644 index 0000000..8411902 --- /dev/null +++ b/jong-db/src/db/advance_state_timer_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct AdvanceStateTimer { + pub scheduled_id: u64, + pub scheduled_at: __sdk::ScheduleAt, + pub lobby_id: u32, +} + +impl __sdk::InModule for AdvanceStateTimer { + type Module = super::RemoteModule; +} diff --git a/jong-db/src/db/mod.rs b/jong-db/src/db/mod.rs index fcf6370..3eb7040 100644 --- a/jong-db/src/db/mod.rs +++ b/jong-db/src/db/mod.rs @@ -7,6 +7,8 @@ use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; pub mod add_bot_reducer; +pub mod advance_state_timer_table; +pub mod advance_state_timer_type; pub mod bot_table; pub mod bot_type; pub mod clear_all_reducer; @@ -36,6 +38,8 @@ pub mod wall_table; pub mod wind_type; pub use add_bot_reducer::{add_bot, set_flags_for_add_bot, AddBotCallbackId}; +pub use advance_state_timer_table::*; +pub use advance_state_timer_type::AdvanceStateTimer; pub use bot_table::*; pub use bot_type::Bot; pub use clear_all_reducer::{clear_all, set_flags_for_clear_all, ClearAllCallbackId}; @@ -189,6 +193,7 @@ impl TryFrom<__ws::ReducerCallInfo<__ws::BsatnFormat>> for Reducer { #[allow(non_snake_case)] #[doc(hidden)] pub struct DbUpdate { + advance_state_timer: __sdk::TableUpdate, bot: __sdk::TableUpdate, lobby: __sdk::TableUpdate, player: __sdk::TableUpdate, @@ -202,6 +207,9 @@ impl TryFrom<__ws::DatabaseUpdate<__ws::BsatnFormat>> for DbUpdate { let mut db_update = DbUpdate::default(); for table_update in raw.tables { match &table_update.table_name[..] { + "advance_state_timer" => db_update + .advance_state_timer + .append(advance_state_timer_table::parse_table_update(table_update)?), "bot" => db_update .bot .append(bot_table::parse_table_update(table_update)?), @@ -243,6 +251,12 @@ impl __sdk::DbUpdate for DbUpdate { ) -> AppliedDiff<'_> { let mut diff = AppliedDiff::default(); + diff.advance_state_timer = cache + .apply_diff_to_table::( + "advance_state_timer", + &self.advance_state_timer, + ) + .with_updates_by_pk(|row| &row.scheduled_id); diff.bot = cache .apply_diff_to_table::("bot", &self.bot) .with_updates_by_pk(|row| &row.id); @@ -267,6 +281,7 @@ impl __sdk::DbUpdate for DbUpdate { #[allow(non_snake_case)] #[doc(hidden)] pub struct AppliedDiff<'r> { + advance_state_timer: __sdk::TableAppliedDiff<'r, AdvanceStateTimer>, bot: __sdk::TableAppliedDiff<'r, Bot>, lobby: __sdk::TableAppliedDiff<'r, Lobby>, player: __sdk::TableAppliedDiff<'r, Player>, @@ -285,6 +300,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { event: &EventContext, callbacks: &mut __sdk::DbCallbacks, ) { + callbacks.invoke_table_row_callbacks::( + "advance_state_timer", + &self.advance_state_timer, + event, + ); callbacks.invoke_table_row_callbacks::("bot", &self.bot, event); callbacks.invoke_table_row_callbacks::("lobby", &self.lobby, event); callbacks.invoke_table_row_callbacks::("player", &self.player, event); @@ -1009,6 +1029,7 @@ impl __sdk::SpacetimeModule for RemoteModule { type SubscriptionHandle = SubscriptionHandle; fn register_tables(client_cache: &mut __sdk::ClientCache) { + advance_state_timer_table::register_table(client_cache); bot_table::register_table(client_cache); lobby_table::register_table(client_cache); player_table::register_table(client_cache); diff --git a/jong-line/src/lib.rs b/jong-line/src/lib.rs index b626822..28bf92a 100644 --- a/jong-line/src/lib.rs +++ b/jong-line/src/lib.rs @@ -1,7 +1,9 @@ use log::debug; use spacetimedb::{ReducerContext, Table, reducer}; -use crate::tables::{player::player, *}; +use jong_types::TurnState; + +use crate::tables::*; mod reducers { mod deal; @@ -44,6 +46,7 @@ pub fn login_or_add_player(ctx: &ReducerContext) { hand: vec![], pond: vec![], drawn_tile: None, + turn_state: TurnState::None, }) { debug!("added player: {:?}", player); } else { diff --git a/jong-line/src/reducers/lobby.rs b/jong-line/src/reducers/lobby.rs index 1a4df28..f169dfc 100644 --- a/jong-line/src/reducers/lobby.rs +++ b/jong-line/src/reducers/lobby.rs @@ -1,7 +1,7 @@ use log::info; use spacetimedb::{ReducerContext, Table, rand::seq::SliceRandom, reducer}; -use crate::tables::{player::player, *}; +use crate::tables::*; #[reducer] pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<(), String> { @@ -19,7 +19,6 @@ pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<( host_player_id: player.id, players: vec![PlayerOrBot::Player { id: player.id }], game_state: jong_types::states::GameState::Lobby, - turn_state: jong_types::states::TurnState::None, dealer_idx: 0, current_idx: 0, }); diff --git a/jong-line/src/tables.rs b/jong-line/src/tables.rs index 17ec267..5939c67 100644 --- a/jong-line/src/tables.rs +++ b/jong-line/src/tables.rs @@ -1,13 +1,10 @@ use spacetimedb::{SpacetimeType, table}; use jong_types::{ - tiles::Tile, states::{GameState, TurnState}, + tiles::Tile, }; -pub mod player; -pub use player::*; - #[derive(Debug, Clone)] #[table(name = lobby, public)] pub struct Lobby { @@ -22,7 +19,6 @@ pub struct Lobby { pub current_idx: u8, pub game_state: GameState, - pub turn_state: TurnState, } #[table(name = wall)] @@ -48,3 +44,55 @@ pub enum PlayerOrBot { Player { id: u32 }, Bot { id: u32 }, } + +#[table(name = advance_state_timer)] +pub struct AdvanceStateTimer { + #[primary_key] + #[auto_inc] + scheduled_id: u64, + scheduled_at: spacetimedb::ScheduleAt, + + lobby_id: u32, +} + +// FIXME this shant be public, use views +#[table(name = player, public)] +#[derive(Debug)] +pub struct Player { + #[primary_key] + pub identity: spacetimedb::Identity, + + #[unique] + #[auto_inc] + pub id: u32, + + pub name: Option, + + #[index(btree)] + pub lobby_id: u32, + pub ready: bool, + + pub turn_state: TurnState, + + pub sort: bool, + + pub hand: Vec, + pub pond: Vec, + + pub drawn_tile: Option, +} + +#[table(name = bot)] +pub struct Bot { + #[primary_key] + #[auto_inc] + pub id: u32, + + #[index(btree)] + pub lobby_id: u32, + + pub hand: Vec, + pub pond: Vec, + + pub drawn_tile: Option, +} diff --git a/jong-line/src/tables/player.rs b/jong-line/src/tables/player.rs deleted file mode 100644 index c3bf342..0000000 --- a/jong-line/src/tables/player.rs +++ /dev/null @@ -1,44 +0,0 @@ -use spacetimedb::Identity; -use spacetimedb::{SpacetimeType, table}; - -use super::DbTile; - -// FIXME this shant be public, use views -#[table(name = player, public)] -#[derive(Debug)] -pub struct Player { - #[primary_key] - pub identity: Identity, - - #[unique] - #[auto_inc] - pub id: u32, - - pub name: Option, - - #[index(btree)] - pub lobby_id: u32, - pub ready: bool, - - pub sort: bool, - - pub hand: Vec, - pub pond: Vec, - - pub drawn_tile: Option, -} - -#[table(name = bot)] -pub struct Bot { - #[primary_key] - #[auto_inc] - pub id: u32, - - #[index(btree)] - pub lobby_id: u32, - - pub hand: Vec, - pub pond: Vec, - - pub drawn_tile: Option, -} diff --git a/jong/src/riichi.rs b/jong/src/riichi.rs index 61ad092..38afe95 100644 --- a/jong/src/riichi.rs +++ b/jong/src/riichi.rs @@ -121,6 +121,18 @@ fn on_player_insert_update( }; for msg in messages.read() { + match msg.new.turn_state { + jong_db::TurnState::None => {} + jong_db::TurnState::Tsumo => { + stdb.reducers().draw_tile().unwrap(); + } + jong_db::TurnState::Menzen => todo!(), + jong_db::TurnState::RiichiKan => todo!(), + jong_db::TurnState::RonChiiPonKan => { + stdb.reducers().skip_call().unwrap(); + } + jong_db::TurnState::End => todo!(), + } let hand_tiles: Vec<_> = msg .new .hand @@ -205,19 +217,6 @@ fn on_lobby_insert_update( } jong_db::GameState::Play => { trace!("game entered play"); - match msg.new.turn_state { - jong_db::TurnState::None => {} - jong_db::TurnState::Tsumo => { - stdb.reducers().draw_tile().unwrap(); - } - jong_db::TurnState::Menzen => todo!(), - jong_db::TurnState::RiichiKan => todo!(), - jong_db::TurnState::RonChiiPonKan => { - stdb.reducers().skip_call().unwrap(); - } - jong_db::TurnState::End => todo!(), - } - next_turnstate.set(msg.new.turn_state.into()); } jong_db::GameState::Exit => { trace!("game enetered exit");