diff --git a/jong-db/src/db/advance_game_reducer.rs b/jong-db/src/db/advance_game_reducer.rs index a0133cf..d8e3ce9 100644 --- a/jong-db/src/db/advance_game_reducer.rs +++ b/jong-db/src/db/advance_game_reducer.rs @@ -4,13 +4,19 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::game_timer_type::GameTimer; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] -pub(super) struct AdvanceGameArgs {} +pub(super) struct AdvanceGameArgs { + pub game_timer: GameTimer, +} impl From for super::Reducer { fn from(args: AdvanceGameArgs) -> Self { - Self::AdvanceGame + Self::AdvanceGame { + game_timer: args.game_timer, + } } } @@ -29,8 +35,8 @@ pub trait advance_game { /// The reducer will run asynchronously in the future, /// and this method provides no way to listen for its completion status. /// /// Use [`advance_game:advance_game_then`] to run a callback after the reducer completes. - fn advance_game(&self) -> __sdk::Result<()> { - self.advance_game_then(|_, _| {}) + fn advance_game(&self, game_timer: GameTimer) -> __sdk::Result<()> { + self.advance_game_then(game_timer, |_, _| {}) } /// Request that the remote module invoke the reducer `advance_game` to run as soon as possible, @@ -41,6 +47,7 @@ pub trait advance_game { /// and its status can be observed with the `callback`. fn advance_game_then( &self, + game_timer: GameTimer, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send @@ -51,12 +58,13 @@ pub trait advance_game { impl advance_game for super::RemoteReducers { fn advance_game_then( &self, + game_timer: GameTimer, callback: impl FnOnce(&super::ReducerEventContext, Result, __sdk::InternalError>) + Send + 'static, ) -> __sdk::Result<()> { self.imp - .invoke_reducer_with_callback(AdvanceGameArgs {}, callback) + .invoke_reducer_with_callback(AdvanceGameArgs { game_timer }, callback) } } diff --git a/jong-db/src/db/mod.rs b/jong-db/src/db/mod.rs index 83e4053..234f175 100644 --- a/jong-db/src/db/mod.rs +++ b/jong-db/src/db/mod.rs @@ -77,7 +77,7 @@ pub use wind_type::Wind; pub enum Reducer { AddBot { lobby_id: u32 }, - AdvanceGame, + AdvanceGame { game_timer: GameTimer }, ClearAll, DiscardTile { tile_id: u32 }, JoinOrCreateLobby { lobby_id: u32 }, @@ -92,7 +92,7 @@ impl __sdk::Reducer for Reducer { fn reducer_name(&self) -> &'static str { match self { Reducer::AddBot { .. } => "add_bot", - Reducer::AdvanceGame => "advance_game", + Reducer::AdvanceGame { .. } => "advance_game", Reducer::ClearAll => "clear_all", Reducer::DiscardTile { .. } => "discard_tile", Reducer::JoinOrCreateLobby { .. } => "join_or_create_lobby", @@ -106,8 +106,10 @@ impl __sdk::Reducer for Reducer { Reducer::AddBot { lobby_id } => __sats::bsatn::to_vec(&add_bot_reducer::AddBotArgs { lobby_id: lobby_id.clone(), }), - Reducer::AdvanceGame => { - __sats::bsatn::to_vec(&advance_game_reducer::AdvanceGameArgs {}) + Reducer::AdvanceGame { game_timer } => { + __sats::bsatn::to_vec(&advance_game_reducer::AdvanceGameArgs { + game_timer: game_timer.clone(), + }) } Reducer::ClearAll => __sats::bsatn::to_vec(&clear_all_reducer::ClearAllArgs {}), Reducer::DiscardTile { tile_id } => { diff --git a/jong-line/src/reducers.rs b/jong-line/src/reducers.rs index 0f3dda9..3b07aa3 100644 --- a/jong-line/src/reducers.rs +++ b/jong-line/src/reducers.rs @@ -15,108 +15,82 @@ mod hand; mod lobby; #[reducer] -pub fn advance_game(ctx: &ReducerContext) -> Result<(), String> { - let game_timer = ctx - .db - .game_timer() - .lobby_id() - .find( - ctx.db - .player() - .identity() - .find(ctx.sender()) - .ok_or("player not in lobby")? - .lobby_id, - ) - .ok_or("no such lobby")?; - +pub fn advance_game(ctx: &ReducerContext, mut game_timer: GameTimer) -> Result<(), String> { advance_game_private(ctx, game_timer) } -fn shuffle_wall(ctx: &ReducerContext, lobby: &mut Lobby) { - let tiles = { - let mut rng = ctx.rng(); - let mut wall: Vec<_> = jong_types::tiles::tiles() - .into_iter() - .map(|tile| ctx.db.tile().insert(DbTile { id: 0, tile })) - .collect(); - wall.shuffle(&mut rng); - wall - }; - ctx.db.wall().insert(DbWall { - // id: 0, - lobby_id: lobby.id, - tiles, - }); - lobby.game_state = GameState::Deal; -} - -fn deal_hands(ctx: &ReducerContext, lobby: &mut Lobby) -> Result<(), String> { - let mut wall = ctx.db.wall().lobby_id().find(lobby.id).unwrap(); - for pob in &lobby.players { - let mut tiles = wall.tiles.split_off(wall.tiles.len() - 13); - wall = ctx.db.wall().lobby_id().update(wall); - tiles.sort_by_key(|t| t.tile); - match pob { - PlayerOrBot::Player { id } if let Some(p) = ctx.db.player().id().find(id) => { - ctx.db.player_hand().insert(PlayerHand { - id: 0, - player_id: p.id, - turn_state: jong_types::TurnState::None, - pond: vec![], - hand: tiles, - working_tile: None, - }); - ctx.db.player_clock().insert(PlayerClock { - id: 0, - player_id: p.id, - renewable: 5, - total: 30, - }); - } - PlayerOrBot::Bot { id } if let Some(mut b) = ctx.db.bot().id().find(id) => { - b.hand = tiles; - ctx.db.bot().id().update(b); - } - _ => Err("couldn't find player or bot".to_string())?, - } - } - lobby.game_state = jong_types::states::GameState::Play; - - Ok(()) -} - #[reducer] pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) -> Result<(), String> { // checks every second (or more? when users make moves) on whether to advance the game's various states // TODO this, or allow player/debug to call this? if let Some(mut lobby) = ctx.db.lobby().id().find(game_timer.lobby_id) { - // trace!("running schedule for lobby {}", lobby.id); + trace!("running schedule for lobby {}", lobby.id); match lobby.game_state { GameState::Setup => { // TODO reduce interval beforehand so we don't wait a second? // TODO keep a count to clear stale lobbies - // trace!("shuffle wall"); - shuffle_wall(ctx, &mut lobby); - ctx.db.lobby().id().update(lobby); - advance_game_private(ctx, game_timer)?; - return Ok(()); + trace!("shuffle wall"); + let tiles = { + let mut rng = ctx.rng(); + let mut wall: Vec<_> = jong_types::tiles::tiles() + .into_iter() + .map(|tile| ctx.db.tile().insert(DbTile { id: 0, tile })) + .collect(); + wall.shuffle(&mut rng); + wall + }; + ctx.db.wall().insert(DbWall { + // id: 0, + lobby_id: lobby.id, + tiles, + }); + lobby.game_state = GameState::Deal; } GameState::Deal => { // TODO reduce interval beforehand so this can animate? // TODO change loop to be per interval somehow? - deal_hands(ctx, &mut lobby)?; - ctx.db.lobby().id().update(lobby); - advance_game_private(ctx, game_timer)?; - return Ok(()); + trace!("deal hands"); + let mut wall = ctx.db.wall().lobby_id().find(lobby.id).unwrap(); + for pob in &lobby.players { + let mut tiles = wall.tiles.split_off(wall.tiles.len() - 13); + wall = ctx.db.wall().lobby_id().update(wall); + tiles.sort_by_key(|t| t.tile); + match pob { + PlayerOrBot::Player { id } + if let Some(p) = ctx.db.player().id().find(id) => + { + ctx.db.player_hand().insert(PlayerHand { + id: 0, + player_id: p.id, + turn_state: jong_types::TurnState::None, + pond: vec![], + hand: tiles, + working_tile: None, + }); + ctx.db.player_clock().insert(PlayerClock { + id: 0, + player_id: p.id, + renewable: 5, + total: 30, + }); + } + PlayerOrBot::Bot { id } if let Some(mut b) = ctx.db.bot().id().find(id) => { + b.hand = tiles; + ctx.db.bot().id().update(b); + } + _ => Err("couldn't find player or bot".to_string())?, + } + } + lobby.game_state = jong_types::states::GameState::Play; + trace!("dealt hands"); } GameState::Play => { - // trace!("in play"); + trace!("in play"); let curr_player = lobby.players.get(lobby.current_idx as usize).unwrap(); match curr_player { PlayerOrBot::Player { id: player_id } => { - // trace!("current player is {player_id}"); + trace!("current player is {player_id}"); let mut clock = ctx.db.player_clock().player_id().find(player_id).unwrap(); let mut hand = ctx.db.player_hand().player_id().find(player_id).unwrap(); match hand.turn_state { @@ -135,7 +109,7 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) -> } } TurnState::Tsumo => { - // trace!("wait for discard"); + trace!("wait for discard"); if clock.tick() { ctx.db.player_clock().id().update(clock); } else { diff --git a/jong-line/src/reducers/lobby.rs b/jong-line/src/reducers/lobby.rs index 17b658a..46bcf96 100644 --- a/jong-line/src/reducers/lobby.rs +++ b/jong-line/src/reducers/lobby.rs @@ -5,7 +5,7 @@ use spacetimedb::{ReducerContext, Table, rand::seq::SliceRandom, reducer}; use jong_types::PlayerOrBot; -use crate::{reducers::advance_game_private, tables::*}; +use crate::tables::*; #[reducer] pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<(), String> { @@ -87,13 +87,11 @@ pub fn set_ready(ctx: &ReducerContext, ready: bool) -> Result<(), String> { let lobby = ctx.db.lobby().id().update(lobby); // TODO should we schedule this outside so that we can clear out stale lobbies? - let game_timer = ctx.db.game_timer().insert(GameTimer { + ctx.db.game_timer().insert(GameTimer { id: 0, lobby_id: lobby.id, scheduled_at: spacetimedb::ScheduleAt::Interval(Duration::from_secs(1).into()), }); - - advance_game_private(ctx, game_timer)?; } else { // if lobby doesn't exist, reset player state player.lobby_id = 0; diff --git a/jong/src/riichi.rs b/jong/src/riichi.rs index d7a817e..7b2b902 100644 --- a/jong/src/riichi.rs +++ b/jong/src/riichi.rs @@ -1,9 +1,10 @@ use bevy::prelude::*; -use bevy_spacetimedb::{ReadInsertMessage, ReadInsertUpdateMessage, ReadUpdateMessage, StdbPlugin}; - -use jong_db::{ - self, GameTimerTableAccess, PlayerClockTableAccess, add_bot, advance_game, set_ready, +use bevy_spacetimedb::{ + ReadInsertUpdateMessage, ReadStdbConnectedMessage, ReadStdbDisconnectedMessage, + ReadUpdateMessage, StdbPlugin, }; + +use jong_db::{self, GameTimerTableAccess, add_bot, set_ready}; use jong_db::{ BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, PlayerTableAccess, RemoteTables, ViewClosedHandsTableAccess, ViewHandTableAccess, @@ -11,10 +12,10 @@ use jong_db::{ use jong_types::*; use spacetimedb_sdk::Table; -mod connection; pub mod player; use crate::riichi::player::*; use crate::{SpacetimeDB, creds_store}; +// pub mod round; pub struct Riichi; impl Plugin for Riichi { @@ -23,10 +24,9 @@ impl Plugin for Riichi { .with_uri("http://localhost:3000") .with_module_name("jong-line") .with_run_fn(DbConnection::run_threaded) - .add_table(RemoteTables::lobby) .add_table(RemoteTables::player) - .add_table(RemoteTables::bot) - .add_table(RemoteTables::player_clock) + .add_table(RemoteTables::lobby) + .add_table(RemoteTables::game_timer) // 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| { @@ -43,42 +43,50 @@ impl Plugin for Riichi { app.add_plugins(plugins) .init_state::() .add_sub_state::() - .add_message::() - .add_message::() .add_systems(Startup, subscriptions) - .add_systems(Update, (connection::on_connect, connection::on_disconnect)) + .add_observer(on_subscribed) + .add_systems(Update, (on_connect, on_disconnect)) + .add_systems(Update, (on_lobby_insert_update, on_player_insert_update)) .add_systems( 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)) + (on_view_hand_update) .run_if(in_state(GameState::Play).or(in_state(GameState::Deal))), ); } } -/// 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 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; fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) { - let (tx, rx) = std::sync::mpsc::channel(); + // commands.queue(command); + let (send, recv) = std::sync::mpsc::channel::(); stdb.subscription_builder() .on_applied(move |_| { trace!("subs succeeded"); - tx.send(()).unwrap(); + send.send(Subscribed).unwrap(); }) .on_error(|_, err| { error!("subs failed: {err}"); @@ -89,164 +97,160 @@ fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) { "SELECT p.* FROM player p WHERE p.identity = '{}'", stdb.identity() ), - // 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 l.* FROM lobby l JOIN player p ON l.id = p.lobby_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(), + "SELECT g.* FROM game_timer g JOIN player p ON g.lobby_id = p.lobby_id".to_string(), ]); - while let Ok(()) = rx.recv() { - // todo!() + while let Ok(event) = recv.recv() { + commands.trigger(event); } } -#[derive(Message)] -struct SyncPlayer(u32); +/// 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 SyncOpenHand(u32); - -#[derive(Message)] -struct SyncClosedHand(PlayerOrBot); - -#[derive(Message)] -struct SyncPlayerClock(u32); - -fn sync_player( stdb: SpacetimeDB, - mut messages: MessageReader, mut commands: Commands, - - 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_gamestate: ResMut>, mut next_turnstate: ResMut>, ) { - 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!() - }; - - let hand_ent = hands - .iter() - .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() - }); - - // 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))); + 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); } + } - next_turnstate.set(player_hand.turn_state.into()); + for bot in stdb.db().bot().iter() { + let id = PlayerOrBot::Bot { id: bot.id }; + let hand_view = stdb + .db() + .view_closed_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); + } + + if let Some(lobby) = stdb.db().lobby().iter().next() { + next_gamestate.set(lobby.game_state.into()); } } -fn sync_closed_hand( - stdb: SpacetimeDB, +fn spawn_main_player( + stdb: &SpacetimeDB, - mut events: MessageReader, - mut commands: Commands, + commands: &mut Commands, + next_turnstate: &mut ResMut>, - hands: Query<&mut Closed, With>, - ponds: Query<&mut Children, With>, + 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 sync_player_clock() {} +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( - mut db_messages: ReadInsertUpdateMessage, + stdb: SpacetimeDB, + mut messages: ReadInsertUpdateMessage, - mut writer: MessageWriter, + mut commands: Commands, + + main_player: Option>, + other_players: Query<&Player, Without>, + + mut next_turnstate: ResMut>, ) { - for msg in db_messages.read() { - trace!("on_player_insert_update"); - writer.write(SyncPlayer(msg.new.id)); + 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() { + // trace!("spawn_main_player"); + 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 on_bot_insert_update() {} - fn on_lobby_insert_update( stdb: SpacetimeDB, mut messages: ReadInsertUpdateMessage, @@ -264,6 +268,7 @@ 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"); @@ -275,7 +280,6 @@ fn on_lobby_insert_update( stdb.reducers().add_bot(player.lobby_id).unwrap(); } stdb.reducers().set_ready(true).unwrap(); - // stdb.reducers().advance_game().unwrap(); } } jong_db::GameState::Setup => { @@ -296,84 +300,88 @@ fn on_lobby_insert_update( } } -fn on_view_hand_insert( - mut messages: ReadInsertMessage, - mut writer: MessageWriter, -) { - for msg in messages.read() { - 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)>, - // main_player: Single<(Entity, &Children), With>, + mut commands: Commands, + tiles: Query<(Entity, &TileId)>, - // hand: Query>, - // pond: Query>, - // // drawn: Option>>, - // mut next_turnstate: ResMut>, + main_player: Single<(Entity, Option<&Children>), With>, + + 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() { - trace!("on_view_hand_update"); - writer.write(SyncOpenHand(msg.new.player_id)); + // trace!("new hand: {:?}", msg.new); + + if main_player.1.is_none() { + // trace!("spawn_main_hand, {:?}", *main_player); + spawn_main_hand(&mut commands, &mut next_turnstate, main_player.0, &msg.new); + continue; + } + + 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.is_some_and(|mp| mp.contains(e))) + .unwrap(), + ) + .replace_children(&hand_tiles); + commands + .entity( + pond.iter() + .find(|e| main_player.1.is_some_and(|mp| mp.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()); } - // 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 deleted file mode 100644 index c6d220b..0000000 --- a/jong/src/riichi/connection.rs +++ /dev/null @@ -1,26 +0,0 @@ -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 a2dc497..a9d7d47 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 id: PlayerOrBot, + pub(crate) id: PlayerOrBot, } #[derive(Component)] @@ -17,20 +17,13 @@ pub struct CurrentPlayer; pub struct TileId(pub u32); #[derive(Component)] -pub struct Hand { - pub owner: PlayerOrBot, -} +pub struct Hand; #[derive(Component)] -pub struct Closed { - pub(crate) owner: PlayerOrBot, - pub(crate) length: u8, -} +pub struct Closed(pub(crate) u8); #[derive(Component)] -pub struct Pond { - pub owner: PlayerOrBot, -} +pub struct Pond; #[derive(Component)] pub struct Drawn; diff --git a/jong/src/tui.rs b/jong/src/tui.rs index 1db3695..ce195ee 100644 --- a/jong/src/tui.rs +++ b/jong/src/tui.rs @@ -72,6 +72,7 @@ impl Plugin for TuiPlugin { open: true, }) .init_state::() + .add_message::() .configure_sets( Update, (TuiSet::Input, TuiSet::Layout, TuiSet::Render).chain(), @@ -81,38 +82,39 @@ impl Plugin for TuiPlugin { (input::keyboard, input::mouse).in_set(TuiSet::Input), ) .add_systems(Update, layout::layout.in_set(TuiSet::Layout)) + .add_systems(Update, discard_tile.run_if(in_state(TurnState::Tsumo))) .add_systems( Update, ( (render::render_main_hand, render::render_main_pond) - .run_if(in_state(GameState::Play).or(in_state(GameState::Deal))), + .run_if(in_state(GameState::Play)), render::render, ) .chain() .in_set(TuiSet::Render), - ) - // .add_systems(Update, discard_tile.run_if(in_state(TurnState::Tsumo))); - .add_observer(discard_tile) // TODO check run_if here feature is out - ; + ); } } fn discard_tile( - selected: On, - stdb: SpacetimeDB, mut commands: Commands, + mut selected: MessageReader, // main_player: Single<&Children, With>, // only main player will have a Drawn tile? drawn: Single<(Entity, &TileId), With>, tiles: Query<&TileId>, ) { + // FIXME why is this not consuming the messages? // TODO disable this when we're not current player? - if let Ok(tile_id) = tiles.get(selected.0) { - trace!("{:?}, {tile_id:?}", selected.0); - stdb.reducers().discard_tile(tile_id.0).unwrap(); - stdb.reducers().advance_game().unwrap(); - commands.entity(drawn.0).remove::(); + while let Some(message) = selected.read().next() { + if let Ok(tile_id) = tiles.get(message.0) { + stdb.reducers().discard_tile(tile_id.0).unwrap(); + stdb.reducers() + .advance_game(stdb.db().game_timer().iter().next().unwrap()) + .unwrap(); + commands.entity(drawn.0).remove::(); + } } } diff --git a/jong/src/tui/input.rs b/jong/src/tui/input.rs index 94a0b2a..9ef831d 100644 --- a/jong/src/tui/input.rs +++ b/jong/src/tui/input.rs @@ -12,5 +12,5 @@ pub(crate) struct Hovered; #[derive(Component)] pub(crate) struct StartSelect; -#[derive(Event, Debug)] +#[derive(Message, Debug)] pub(crate) struct ConfirmSelect(pub(crate) Entity); diff --git a/jong/src/tui/input/mouse.rs b/jong/src/tui/input/mouse.rs index 984b2ce..c828eec 100644 --- a/jong/src/tui/input/mouse.rs +++ b/jong/src/tui/input/mouse.rs @@ -11,6 +11,7 @@ use crate::tui::{ pub(crate) fn mouse( mut commands: Commands, mut mouse_reader: MessageReader, + mut event_writer: MessageWriter, entities: Query<(Entity, &PickRegion)>, hovered: Query<(Entity, &PickRegion), With>, startselected: Query<(Entity, &PickRegion), With>, @@ -51,7 +52,7 @@ pub(crate) fn mouse( for (entity, region) in &startselected { if region.area.contains(position) { commands.entity(entity).remove::(); - commands.trigger(ConfirmSelect(entity)); + event_writer.write(ConfirmSelect(entity)); } } } diff --git a/jong/src/tui/render.rs b/jong/src/tui/render.rs index 566c2d4..be3e717 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<&Player, With>, + main_player: Single<&Children, With>, - hand: Query<(&Hand, &Children)>, + hand: Query<(&Children, Entity), With>, drawn_tile: Option>>, ) -> Result { let mut frame = tui.get_frame(); @@ -164,7 +164,14 @@ pub(crate) fn render_main_hand( let hand: Vec<_> = hand .iter() - .find_map(|(h, c)| (main_player.id == h.owner).then_some(c)) + .find_map(|(c, e)| { + // debug!("main_player children: {:?}", *main_player); + if main_player.contains(&e) { + Some(c) + } else { + None + } + }) .unwrap() .iter() .map(|entity| -> Result<_> { @@ -259,9 +266,9 @@ pub(crate) fn render_main_pond( tiles: Query<&Tile>, hovered: Query>, - main_player: Single<&Player, With>, + main_player: Single<&Children, With>, - pond: Query<(&Pond, &Children)>, + pond: Query<(&Children, Entity), With>, ) -> Result { let mut frame = tui.get_frame(); @@ -271,7 +278,13 @@ pub(crate) fn render_main_pond( let pond: Vec<_> = pond .iter() - .find_map(|(p, c)| (main_player.id == p.owner).then_some(c)) + .find_map(|(c, e)| { + if main_player.contains(&e) { + Some(c) + } else { + None + } + }) .unwrap() .iter() .map(|entity| -> Result<_> { diff --git a/spacetime.json b/spacetime.json index 339050e..865cbce 100644 --- a/spacetime.json +++ b/spacetime.json @@ -2,7 +2,6 @@ "dev": { "run": "" }, - "_source-config": "spacetime.local.json", "module-path": "jong-line", "server": "local", "database": "jong-line"