From 7ffef5522b88d9d179e8b96ed91af8da7416c7db Mon Sep 17 00:00:00 2001 From: Tao Tien <29749622+taotien@users.noreply.github.com> Date: Sun, 1 Mar 2026 00:20:39 -0800 Subject: [PATCH] begin better spawn logic --- jong/src/riichi.rs | 454 +++++++++++++++++----------------- jong/src/riichi/connection.rs | 26 ++ jong/src/riichi/player.rs | 15 +- jong/src/tui/render.rs | 25 +- 4 files changed, 267 insertions(+), 253 deletions(-) create mode 100644 jong/src/riichi/connection.rs diff --git a/jong/src/riichi.rs b/jong/src/riichi.rs index 74dbc4f..d7a817e 100644 --- a/jong/src/riichi.rs +++ b/jong/src/riichi.rs @@ -1,10 +1,9 @@ use bevy::prelude::*; -use bevy_spacetimedb::{ - ReadInsertMessage, ReadInsertUpdateMessage, ReadStdbConnectedMessage, - ReadStdbDisconnectedMessage, ReadUpdateMessage, StdbPlugin, -}; +use bevy_spacetimedb::{ReadInsertMessage, ReadInsertUpdateMessage, ReadUpdateMessage, StdbPlugin}; -use jong_db::{self, GameTimerTableAccess, add_bot, advance_game, set_ready}; +use jong_db::{ + self, GameTimerTableAccess, PlayerClockTableAccess, add_bot, advance_game, set_ready, +}; use jong_db::{ BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, PlayerTableAccess, RemoteTables, ViewClosedHandsTableAccess, ViewHandTableAccess, @@ -12,6 +11,7 @@ use jong_db::{ use jong_types::*; use spacetimedb_sdk::Table; +mod connection; pub mod player; use crate::riichi::player::*; use crate::{SpacetimeDB, creds_store}; @@ -23,8 +23,10 @@ impl Plugin for Riichi { .with_uri("http://localhost:3000") .with_module_name("jong-line") .with_run_fn(DbConnection::run_threaded) - .add_table(RemoteTables::player) .add_table(RemoteTables::lobby) + .add_table(RemoteTables::player) + .add_table(RemoteTables::bot) + .add_table(RemoteTables::player_clock) // TODO check bevy_spacetimedb PR status .add_view_with_pk(RemoteTables::view_hand, |p| p.id) .add_view_with_pk(RemoteTables::view_closed_hands, |p| { @@ -41,50 +43,42 @@ impl Plugin for Riichi { app.add_plugins(plugins) .init_state::() .add_sub_state::() + .add_message::() + .add_message::() .add_systems(Startup, subscriptions) - .add_observer(on_subscribed) // TODO fire once then get removed? or keep around for reconnect logic? - .add_systems(Update, (on_connect, on_disconnect)) - .add_systems(Update, (on_lobby_insert_update, on_player_insert_update)) + .add_systems(Update, (connection::on_connect, connection::on_disconnect)) .add_systems( Update, - (on_view_hand_insert, on_view_hand_update) + ( + (on_player_insert_update, on_lobby_insert_update), + (sync_player), + ), + ) + .add_systems( + Update, + ((on_view_hand_insert, on_view_hand_update), (sync_open_hand)) .run_if(in_state(GameState::Play).or(in_state(GameState::Deal))), ); } } -fn on_connect(stdb: SpacetimeDB, mut messages: ReadStdbConnectedMessage) { - for msg in messages.read() { - info!("you're now jongline"); - - // FIXME hack that doesn't work for startup crash? - while stdb.try_identity().is_none() {} - - debug!("with identity: {}", stdb.identity()); - creds_store() - .save(&msg.access_token) - .expect("i/o error saving token"); - } -} - -// TODO how reconnect? -fn on_disconnect(_stdb: SpacetimeDB, mut messages: ReadStdbDisconnectedMessage) { - for msg in messages.read() { - warn!("lost connection: {:#?}", msg.err); - } -} - -// TODO we can make this hold more info in the future -#[derive(Event)] -struct Subscribed; +/// on subscribe we need to check: +/// if we're in a game already +/// spawn (restore) all current game state +/// spawn all players and hands +/// else +/// spawn self player +/// then +/// wait for lobbies +/// spawn other players +/// spawn all hands and ponds fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) { - // commands.queue(command); - let (send, recv) = std::sync::mpsc::channel::(); + let (tx, rx) = std::sync::mpsc::channel(); stdb.subscription_builder() .on_applied(move |_| { trace!("subs succeeded"); - send.send(Subscribed).unwrap(); + tx.send(()).unwrap(); }) .on_error(|_, err| { error!("subs failed: {err}"); @@ -95,158 +89,164 @@ fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) { "SELECT p.* FROM player p WHERE p.identity = '{}'", stdb.identity() ), - "SELECT p.* FROM player p JOIN lobby l ON p.lobby_id = l.id".to_string(), + // TODO add filter for lobby id for all of these later "SELECT l.* FROM lobby l JOIN player p ON l.id = p.lobby_id".to_string(), + "SELECT p.* FROM player p JOIN lobby l ON p.lobby_id = l.id".to_string(), "SELECT c.* FROM player_clock c JOIN player p ON c.player_id = p.id".to_string(), "SELECT b.* FROM bot b JOIN lobby l ON l.id = b.lobby_id".to_string(), "SELECT * FROM view_hand".to_string(), "SELECT * FROM view_closed_hands".to_string(), ]); - while let Ok(event) = recv.recv() { - commands.trigger(event); + while let Ok(()) = rx.recv() { + // todo!() } } -/// spawns entities to be consistent with server state -// TODO figure out a way to call this for later changes in the various on_ins_upd systems -fn on_subscribed( - _event: On, +#[derive(Message)] +struct SyncPlayer(u32); +#[derive(Message)] +struct SyncOpenHand(u32); + +#[derive(Message)] +struct SyncClosedHand(PlayerOrBot); + +#[derive(Message)] +struct SyncPlayerClock(u32); + +fn sync_player( stdb: SpacetimeDB, + mut messages: MessageReader, mut commands: Commands, - mut next_gamestate: ResMut>, + + players: Query<(Entity, &Player)>, +) { + for SyncPlayer(id) in messages.read() { + trace!("sync_player"); + let Some(player) = stdb.db().player().id().find(id) else { + todo!() + }; + + let player_ent = players + .iter() + .find_map(|(e, p)| (p.id == PlayerOrBot::Player { id: player.id }).then_some(e)) + .unwrap_or_else(|| { + commands + .spawn(Player { + id: PlayerOrBot::Player { id: player.id }, + }) + .id() + }); + + if player.identity == stdb.identity() { + commands.entity(player_ent).insert(MainPlayer); + } else { + } + } +} + +fn sync_open_hand( + stdb: SpacetimeDB, + + mut messages: MessageReader, + mut commands: Commands, + + tiles: Query<(Entity, &TileId)>, + hands: Query<(Entity, &Hand)>, + ponds: Query<(Entity, &Pond)>, + mut next_turnstate: ResMut>, ) { - trace!("on_subscribed"); - for player in stdb.db().player().iter() { - if player.identity == stdb.identity() { - // trace!("spawn_main_player"); - spawn_main_player(&stdb, &mut commands, &mut next_turnstate, &player); - } else { - // trace!("spawn_other_player"); - spawn_other_player(&stdb, &mut commands, &player); - } - } + for SyncOpenHand(id) in messages.read() { + trace!("sync_open_hand"); + let Some(player_hand) = stdb.db().view_hand().iter().find(|hand| hand.id == *id) else { + todo!() + }; - for bot in stdb.db().bot().iter() { - let id = PlayerOrBot::Bot { id: bot.id }; - let hand_view = stdb - .db() - .view_closed_hands() + let hand_ent = hands .iter() - .find(|v| PlayerOrBot::from(&v.player) == id) - .unwrap(); - let hand_ent = commands.spawn((Hand, Closed(hand_view.hand_length))).id(); - commands.spawn(Player { id }).add_child(hand_ent); - } + .find_map(|(e, h)| (h.owner == PlayerOrBot::Player { id: *id }).then_some(e)) + .unwrap_or_else(|| { + commands + .spawn(Hand { + owner: PlayerOrBot::Player { id: *id }, + }) + .id() + }); + let pond_ent = ponds + .iter() + .find_map(|(e, h)| (h.owner == PlayerOrBot::Player { id: *id }).then_some(e)) + .unwrap_or_else(|| { + commands + .spawn(Pond { + owner: PlayerOrBot::Player { id: *id }, + }) + .id() + }); - if let Some(lobby) = stdb.db().lobby().iter().next() { - next_gamestate.set(lobby.game_state.into()); + // hand and pond both still need ability to spawn for the reconnect case + let hand: Vec = player_hand + .hand + .iter() + .map(|dbt| { + tiles + .iter() + .find_map(|(e, i)| (i.0 == dbt.id).then_some(e)) + .unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) + }) + .collect(); + let pond: Vec = player_hand + .pond + .iter() + .map(|dbt| { + tiles + .iter() + .find_map(|(e, i)| (i.0 == dbt.id).then_some(e)) + .unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) + }) + .collect(); + + commands.entity(hand_ent).replace_children(&hand); + commands.entity(pond_ent).replace_children(&pond); + + if let Some(dbt) = player_hand.working_tile + && player_hand.turn_state == jong_db::TurnState::Tsumo + { + commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id))); + } + + next_turnstate.set(player_hand.turn_state.into()); } } -fn spawn_main_player( - stdb: &SpacetimeDB, - - commands: &mut Commands, - next_turnstate: &mut ResMut>, - - player: &jong_db::Player, -) { - // trace!("spawn_main_player"); - let main_player = commands - .spawn(( - Player { - id: PlayerOrBot::Player { id: player.id }, - }, - MainPlayer, - )) - .id(); - if let Some(player_hand) = stdb.db().view_hand().iter().next() { - spawn_main_hand(commands, next_turnstate, main_player, &player_hand); - } -} - -fn spawn_main_hand( - commands: &mut Commands, - next_turnstate: &mut ResMut>, - main_player: Entity, - - player_hand: &PlayerHand, -) { - let hand_tiles: Vec<_> = player_hand - .hand - .iter() - .map(|dbt| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) - .collect(); - let pond_tiles: Vec<_> = player_hand - .pond - .iter() - .map(|dbt| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) - .collect(); - let hand = commands.spawn(Hand).add_children(&hand_tiles).id(); - let pond = commands.spawn(Pond).add_children(&pond_tiles).id(); - commands.entity(main_player).add_children(&[hand, pond]); - - debug!("main_hand: {:?}\n main_pond: {:?}", hand_tiles, pond_tiles); - - if player_hand.turn_state == jong_db::TurnState::Tsumo - && let Some(drawn_dbt) = &player_hand.working_tile - { - let drawn = commands - .spawn((Drawn, Tile::from(&drawn_dbt.tile), TileId(drawn_dbt.id))) - .id(); - commands.entity(main_player).add_child(drawn); - } - next_turnstate.set(player_hand.turn_state.into()); -} - -fn spawn_other_player(stdb: &SpacetimeDB, commands: &mut Commands, player: &jong_db::Player) { - let id = PlayerOrBot::Player { id: player.id }; - if let Some(hand_view) = stdb - .db() - .view_closed_hands() - .iter() - .find(|v| PlayerOrBot::from(&v.player) == id) - { - let hand_ent = commands.spawn((Hand, Closed(hand_view.hand_length))).id(); - commands.spawn(Player { id }).add_child(hand_ent); - } -} - -fn on_player_insert_update( +fn sync_closed_hand( stdb: SpacetimeDB, - mut messages: ReadInsertUpdateMessage, + mut events: MessageReader, mut commands: Commands, - main_player: Option>, - other_players: Query<&Player, Without>, - - mut next_turnstate: ResMut>, + hands: Query<&mut Closed, With>, + ponds: Query<&mut Children, With>, ) { - for msg in messages.read() { - debug!("on_player_insert_update: {:?}", msg.new); - assert_eq!(msg.new.identity, stdb.identity()); - if main_player.is_none() && msg.new.identity == stdb.identity() { - spawn_main_player(&stdb, &mut commands, &mut next_turnstate, &msg.new); - } else if other_players.iter().any(|p| { - if let PlayerOrBot::Player { id } = &p.id { - *id == msg.new.id - } else { - false - } - }) { - trace!("spawn_other_player"); - spawn_other_player(&stdb, &mut commands, &msg.new); - } else { - // TODO update case - } +} + +fn sync_player_clock() {} + +fn on_player_insert_update( + mut db_messages: ReadInsertUpdateMessage, + + mut writer: MessageWriter, +) { + for msg in db_messages.read() { + trace!("on_player_insert_update"); + writer.write(SyncPlayer(msg.new.id)); } } +fn on_bot_insert_update() {} + fn on_lobby_insert_update( stdb: SpacetimeDB, mut messages: ReadInsertUpdateMessage, @@ -264,7 +264,6 @@ fn on_lobby_insert_update( .find(&stdb.identity()) .unwrap(); - next_gamestate.set(msg.new.game_state.into()); match msg.new.game_state { jong_db::GameState::None => { trace!("game entered none"); @@ -299,87 +298,82 @@ fn on_lobby_insert_update( fn on_view_hand_insert( mut messages: ReadInsertMessage, - - mut commands: Commands, - main_player: Single<(Entity, Option<&Children>), With>, - - mut next_turnstate: ResMut>, + mut writer: MessageWriter, ) { for msg in messages.read() { - trace!("insert hand: {:?}", msg.row); - - if main_player.1.is_none() { - spawn_main_hand(&mut commands, &mut next_turnstate, main_player.0, &msg.row); - } + trace!("on_view_hand_insert"); + writer.write(SyncOpenHand(msg.row.id)); } } fn on_view_hand_update( - stdb: SpacetimeDB, mut messages: ReadUpdateMessage, + mut writer: MessageWriter, + // mut commands: Commands, + // tiles: Query<(Entity, &TileId)>, - mut commands: Commands, - tiles: Query<(Entity, &TileId)>, + // main_player: Single<(Entity, &Children), With>, - main_player: Single<(Entity, &Children), With>, - - hand: Query>, - pond: Query>, - // drawn: Option>>, - mut next_turnstate: ResMut>, + // hand: Query>, + // pond: Query>, + // // drawn: Option>>, + // mut next_turnstate: ResMut>, ) { // TODO can this and similar run at startup or on play/reconnect? for msg in messages.read() { - let hand_tiles: Vec<_> = msg - .new - .hand - .iter() - .map(|dbt| { - tiles - .iter() - .find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None }) - .unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) - }) - .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 }) - .unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) - }) - .collect(); - - commands - .entity(hand.iter().find(|e| main_player.1.contains(e)).unwrap()) - .replace_children(&hand_tiles); - commands - .entity(pond.iter().find(|e| main_player.1.contains(e)).unwrap()) - .replace_children(&pond_tiles); - - match msg.new.turn_state { - jong_db::TurnState::None => { - trace!("turnstate none"); - // TODO do we reconcile hand state here or in ::End? - } - jong_db::TurnState::Tsumo => { - trace!("turnstate tsumo"); - let dbt = msg - .new - .working_tile - .as_ref() - .expect("entered tsumo without a drawn tile"); - commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id))); - } - jong_db::TurnState::Menzen => todo!(), - jong_db::TurnState::RiichiKan => todo!(), - jong_db::TurnState::RonChiiPonKan => todo!(), - jong_db::TurnState::End => todo!(), - } - - next_turnstate.set(msg.new.turn_state.into()); + trace!("on_view_hand_update"); + writer.write(SyncOpenHand(msg.new.player_id)); } + // let hand_tiles: Vec<_> = msg + // .new + // .hand + // .iter() + // .map(|dbt| { + // tiles + // .iter() + // .find_map(|(e, t)| if *t == TileId(dbt.id) { Some(e) } else { None }) + // .unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) + // }) + // .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 }) + // .unwrap_or_else(|| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) + // }) + // .collect(); + + // commands + // .entity(hand.iter().find(|e| main_player.1.contains(e)).unwrap()) + // .replace_children(&hand_tiles); + // commands + // .entity(pond.iter().find(|e| main_player.1.contains(e)).unwrap()) + // .replace_children(&pond_tiles); + + // match msg.new.turn_state { + // jong_db::TurnState::None => { + // trace!("turnstate none"); + // // TODO do we reconcile hand state here or in ::End? + // } + // jong_db::TurnState::Tsumo => { + // trace!("turnstate tsumo"); + // let dbt = msg + // .new + // .working_tile + // .as_ref() + // .expect("entered tsumo without a drawn tile"); + // commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id))); + // } + // jong_db::TurnState::Menzen => todo!(), + // jong_db::TurnState::RiichiKan => todo!(), + // jong_db::TurnState::RonChiiPonKan => todo!(), + // jong_db::TurnState::End => todo!(), + // } + + // next_turnstate.set(msg.new.turn_state.into()); + // } } diff --git a/jong/src/riichi/connection.rs b/jong/src/riichi/connection.rs new file mode 100644 index 0000000..c6d220b --- /dev/null +++ b/jong/src/riichi/connection.rs @@ -0,0 +1,26 @@ +use bevy_spacetimedb::{ReadStdbConnectedMessage, ReadStdbDisconnectedMessage}; +use log::{debug, info, warn}; + +use crate::SpacetimeDB; +use crate::creds_store; + +pub(crate) fn on_connect(stdb: SpacetimeDB, mut messages: ReadStdbConnectedMessage) { + for msg in messages.read() { + info!("you're now jongline"); + + // FIXME hack that doesn't work for startup crash? + while stdb.try_identity().is_none() {} + + debug!("with identity: {}", stdb.identity()); + creds_store() + .save(&msg.access_token) + .expect("i/o error saving token"); + } +} + +// TODO how reconnect? +pub(crate) fn on_disconnect(_stdb: SpacetimeDB, mut messages: ReadStdbDisconnectedMessage) { + for msg in messages.read() { + warn!("lost connection: {:#?}", msg.err); + } +} diff --git a/jong/src/riichi/player.rs b/jong/src/riichi/player.rs index a9d7d47..a2dc497 100644 --- a/jong/src/riichi/player.rs +++ b/jong/src/riichi/player.rs @@ -4,7 +4,7 @@ use jong_types::PlayerOrBot; #[derive(Component)] pub struct Player { - pub(crate) id: PlayerOrBot, + pub id: PlayerOrBot, } #[derive(Component)] @@ -17,13 +17,20 @@ pub struct CurrentPlayer; pub struct TileId(pub u32); #[derive(Component)] -pub struct Hand; +pub struct Hand { + pub owner: PlayerOrBot, +} #[derive(Component)] -pub struct Closed(pub(crate) u8); +pub struct Closed { + pub(crate) owner: PlayerOrBot, + pub(crate) length: u8, +} #[derive(Component)] -pub struct Pond; +pub struct Pond { + pub owner: PlayerOrBot, +} #[derive(Component)] pub struct Drawn; diff --git a/jong/src/tui/render.rs b/jong/src/tui/render.rs index be3e717..566c2d4 100644 --- a/jong/src/tui/render.rs +++ b/jong/src/tui/render.rs @@ -150,9 +150,9 @@ pub(crate) fn render_main_hand( tiles: Query<&jong_types::Tile>, hovered: Query>, - main_player: Single<&Children, With>, + main_player: Single<&Player, With>, - hand: Query<(&Children, Entity), With>, + hand: Query<(&Hand, &Children)>, drawn_tile: Option>>, ) -> Result { let mut frame = tui.get_frame(); @@ -164,14 +164,7 @@ pub(crate) fn render_main_hand( let hand: Vec<_> = hand .iter() - .find_map(|(c, e)| { - // debug!("main_player children: {:?}", *main_player); - if main_player.contains(&e) { - Some(c) - } else { - None - } - }) + .find_map(|(h, c)| (main_player.id == h.owner).then_some(c)) .unwrap() .iter() .map(|entity| -> Result<_> { @@ -266,9 +259,9 @@ pub(crate) fn render_main_pond( tiles: Query<&Tile>, hovered: Query>, - main_player: Single<&Children, With>, + main_player: Single<&Player, With>, - pond: Query<(&Children, Entity), With>, + pond: Query<(&Pond, &Children)>, ) -> Result { let mut frame = tui.get_frame(); @@ -278,13 +271,7 @@ pub(crate) fn render_main_pond( let pond: Vec<_> = pond .iter() - .find_map(|(c, e)| { - if main_player.contains(&e) { - Some(c) - } else { - None - } - }) + .find_map(|(p, c)| (main_player.id == p.owner).then_some(c)) .unwrap() .iter() .map(|entity| -> Result<_> {