diff --git a/jong-db/src/db/advance_game_reducer.rs b/jong-db/src/db/advance_game_reducer.rs index d8e3ce9..a0133cf 100644 --- a/jong-db/src/db/advance_game_reducer.rs +++ b/jong-db/src/db/advance_game_reducer.rs @@ -4,19 +4,13 @@ #![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 game_timer: GameTimer, -} +pub(super) struct AdvanceGameArgs {} impl From for super::Reducer { fn from(args: AdvanceGameArgs) -> Self { - Self::AdvanceGame { - game_timer: args.game_timer, - } + Self::AdvanceGame } } @@ -35,8 +29,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, game_timer: GameTimer) -> __sdk::Result<()> { - self.advance_game_then(game_timer, |_, _| {}) + fn advance_game(&self) -> __sdk::Result<()> { + self.advance_game_then(|_, _| {}) } /// Request that the remote module invoke the reducer `advance_game` to run as soon as possible, @@ -47,7 +41,6 @@ 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 @@ -58,13 +51,12 @@ 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 { game_timer }, callback) + .invoke_reducer_with_callback(AdvanceGameArgs {}, callback) } } diff --git a/jong-db/src/db/mod.rs b/jong-db/src/db/mod.rs index 234f175..83e4053 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 { game_timer: GameTimer }, + AdvanceGame, 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,10 +106,8 @@ impl __sdk::Reducer for Reducer { Reducer::AddBot { lobby_id } => __sats::bsatn::to_vec(&add_bot_reducer::AddBotArgs { lobby_id: lobby_id.clone(), }), - Reducer::AdvanceGame { game_timer } => { - __sats::bsatn::to_vec(&advance_game_reducer::AdvanceGameArgs { - game_timer: game_timer.clone(), - }) + Reducer::AdvanceGame => { + __sats::bsatn::to_vec(&advance_game_reducer::AdvanceGameArgs {}) } 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 3b07aa3..0f3dda9 100644 --- a/jong-line/src/reducers.rs +++ b/jong-line/src/reducers.rs @@ -15,82 +15,108 @@ mod hand; mod lobby; #[reducer] -pub fn advance_game(ctx: &ReducerContext, mut game_timer: GameTimer) -> Result<(), String> { +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")?; + 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"); - 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; + // trace!("shuffle wall"); + shuffle_wall(ctx, &mut lobby); + ctx.db.lobby().id().update(lobby); + advance_game_private(ctx, game_timer)?; + return Ok(()); } GameState::Deal => { // TODO reduce interval beforehand so this can animate? // TODO change loop to be per interval somehow? - 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"); + deal_hands(ctx, &mut lobby)?; + ctx.db.lobby().id().update(lobby); + advance_game_private(ctx, game_timer)?; + return Ok(()); } 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 { @@ -109,7 +135,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 46bcf96..17b658a 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::tables::*; +use crate::{reducers::advance_game_private, tables::*}; #[reducer] pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<(), String> { @@ -87,11 +87,13 @@ 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? - ctx.db.game_timer().insert(GameTimer { + let game_timer = 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 7b2b902..d7a817e 100644 --- a/jong/src/riichi.rs +++ b/jong/src/riichi.rs @@ -1,10 +1,9 @@ use bevy::prelude::*; -use bevy_spacetimedb::{ - ReadInsertUpdateMessage, ReadStdbConnectedMessage, ReadStdbDisconnectedMessage, - ReadUpdateMessage, StdbPlugin, -}; +use bevy_spacetimedb::{ReadInsertMessage, ReadInsertUpdateMessage, ReadUpdateMessage, StdbPlugin}; -use jong_db::{self, GameTimerTableAccess, add_bot, 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,10 +11,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 { @@ -24,9 +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::game_timer) + .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| { @@ -43,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) - .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_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}"); @@ -97,160 +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(), - "SELECT g.* FROM game_timer g JOIN player p ON g.lobby_id = p.lobby_id".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() { - // 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 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, @@ -268,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"); @@ -280,6 +275,7 @@ 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 => { @@ -300,88 +296,84 @@ 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)>, - mut commands: Commands, - tiles: Query<(Entity, &TileId)>, + // main_player: Single<(Entity, &Children), With>, - main_player: Single<(Entity, Option<&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() { - // 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()); + 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.rs b/jong/src/tui.rs index ce195ee..1db3695 100644 --- a/jong/src/tui.rs +++ b/jong/src/tui.rs @@ -72,7 +72,6 @@ impl Plugin for TuiPlugin { open: true, }) .init_state::() - .add_message::() .configure_sets( Update, (TuiSet::Input, TuiSet::Layout, TuiSet::Render).chain(), @@ -82,39 +81,38 @@ 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)), + .run_if(in_state(GameState::Play).or(in_state(GameState::Deal))), 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? - 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::(); - } + 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::(); } } diff --git a/jong/src/tui/input.rs b/jong/src/tui/input.rs index 9ef831d..94a0b2a 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(Message, Debug)] +#[derive(Event, 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 c828eec..984b2ce 100644 --- a/jong/src/tui/input/mouse.rs +++ b/jong/src/tui/input/mouse.rs @@ -11,7 +11,6 @@ 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>, @@ -52,7 +51,7 @@ pub(crate) fn mouse( for (entity, region) in &startselected { if region.area.contains(position) { commands.entity(entity).remove::(); - event_writer.write(ConfirmSelect(entity)); + commands.trigger(ConfirmSelect(entity)); } } } 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<_> { diff --git a/spacetime.json b/spacetime.json index 865cbce..339050e 100644 --- a/spacetime.json +++ b/spacetime.json @@ -2,6 +2,7 @@ "dev": { "run": "" }, + "_source-config": "spacetime.local.json", "module-path": "jong-line", "server": "local", "database": "jong-line"