Compare commits

...

5 commits

Author SHA1 Message Date
Tao Tien
7ffef5522b begin better spawn logic 2026-03-01 03:22:57 -08:00
Tao Tien
71ec40ee29 discard_tile is now observer. investigate drawn entity and tileid sychro? 2026-02-28 21:07:43 -08:00
Tao Tien
a39ad4cf7c advance_game stuff 2026-02-28 20:55:20 -08:00
Tao Tien
151f7a3489 view_hand insert handling 2026-02-28 20:31:44 -08:00
Tao Tien
edd389c787 (stash) 2026-02-27 13:17:03 -08:00
11 changed files with 386 additions and 356 deletions

View file

@ -4,19 +4,13 @@
#![allow(unused, clippy::all)] #![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; 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)] #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)] #[sats(crate = __lib)]
pub(super) struct AdvanceGameArgs { pub(super) struct AdvanceGameArgs {}
pub game_timer: GameTimer,
}
impl From<AdvanceGameArgs> for super::Reducer { impl From<AdvanceGameArgs> for super::Reducer {
fn from(args: AdvanceGameArgs) -> Self { fn from(args: AdvanceGameArgs) -> Self {
Self::AdvanceGame { Self::AdvanceGame
game_timer: args.game_timer,
}
} }
} }
@ -35,8 +29,8 @@ pub trait advance_game {
/// The reducer will run asynchronously in the future, /// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status. /// 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. /// /// Use [`advance_game:advance_game_then`] to run a callback after the reducer completes.
fn advance_game(&self, game_timer: GameTimer) -> __sdk::Result<()> { fn advance_game(&self) -> __sdk::Result<()> {
self.advance_game_then(game_timer, |_, _| {}) self.advance_game_then(|_, _| {})
} }
/// Request that the remote module invoke the reducer `advance_game` to run as soon as possible, /// 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`. /// and its status can be observed with the `callback`.
fn advance_game_then( fn advance_game_then(
&self, &self,
game_timer: GameTimer,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send + Send
@ -58,13 +51,12 @@ pub trait advance_game {
impl advance_game for super::RemoteReducers { impl advance_game for super::RemoteReducers {
fn advance_game_then( fn advance_game_then(
&self, &self,
game_timer: GameTimer,
callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>) callback: impl FnOnce(&super::ReducerEventContext, Result<Result<(), String>, __sdk::InternalError>)
+ Send + Send
+ 'static, + 'static,
) -> __sdk::Result<()> { ) -> __sdk::Result<()> {
self.imp self.imp
.invoke_reducer_with_callback(AdvanceGameArgs { game_timer }, callback) .invoke_reducer_with_callback(AdvanceGameArgs {}, callback)
} }
} }

View file

@ -77,7 +77,7 @@ pub use wind_type::Wind;
pub enum Reducer { pub enum Reducer {
AddBot { lobby_id: u32 }, AddBot { lobby_id: u32 },
AdvanceGame { game_timer: GameTimer }, AdvanceGame,
ClearAll, ClearAll,
DiscardTile { tile_id: u32 }, DiscardTile { tile_id: u32 },
JoinOrCreateLobby { lobby_id: u32 }, JoinOrCreateLobby { lobby_id: u32 },
@ -92,7 +92,7 @@ impl __sdk::Reducer for Reducer {
fn reducer_name(&self) -> &'static str { fn reducer_name(&self) -> &'static str {
match self { match self {
Reducer::AddBot { .. } => "add_bot", Reducer::AddBot { .. } => "add_bot",
Reducer::AdvanceGame { .. } => "advance_game", Reducer::AdvanceGame => "advance_game",
Reducer::ClearAll => "clear_all", Reducer::ClearAll => "clear_all",
Reducer::DiscardTile { .. } => "discard_tile", Reducer::DiscardTile { .. } => "discard_tile",
Reducer::JoinOrCreateLobby { .. } => "join_or_create_lobby", 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 { Reducer::AddBot { lobby_id } => __sats::bsatn::to_vec(&add_bot_reducer::AddBotArgs {
lobby_id: lobby_id.clone(), lobby_id: lobby_id.clone(),
}), }),
Reducer::AdvanceGame { game_timer } => { Reducer::AdvanceGame => {
__sats::bsatn::to_vec(&advance_game_reducer::AdvanceGameArgs { __sats::bsatn::to_vec(&advance_game_reducer::AdvanceGameArgs {})
game_timer: game_timer.clone(),
})
} }
Reducer::ClearAll => __sats::bsatn::to_vec(&clear_all_reducer::ClearAllArgs {}), Reducer::ClearAll => __sats::bsatn::to_vec(&clear_all_reducer::ClearAllArgs {}),
Reducer::DiscardTile { tile_id } => { Reducer::DiscardTile { tile_id } => {

View file

@ -15,22 +15,25 @@ mod hand;
mod lobby; mod lobby;
#[reducer] #[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) advance_game_private(ctx, game_timer)
} }
#[reducer] fn shuffle_wall(ctx: &ReducerContext, lobby: &mut Lobby) {
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);
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 tiles = {
let mut rng = ctx.rng(); let mut rng = ctx.rng();
let mut wall: Vec<_> = jong_types::tiles::tiles() let mut wall: Vec<_> = jong_types::tiles::tiles()
@ -46,20 +49,16 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) ->
tiles, tiles,
}); });
lobby.game_state = GameState::Deal; lobby.game_state = GameState::Deal;
} }
GameState::Deal => {
// TODO reduce interval beforehand so this can animate? fn deal_hands(ctx: &ReducerContext, lobby: &mut Lobby) -> Result<(), String> {
// TODO change loop to be per interval somehow?
trace!("deal hands");
let mut wall = ctx.db.wall().lobby_id().find(lobby.id).unwrap(); let mut wall = ctx.db.wall().lobby_id().find(lobby.id).unwrap();
for pob in &lobby.players { for pob in &lobby.players {
let mut tiles = wall.tiles.split_off(wall.tiles.len() - 13); let mut tiles = wall.tiles.split_off(wall.tiles.len() - 13);
wall = ctx.db.wall().lobby_id().update(wall); wall = ctx.db.wall().lobby_id().update(wall);
tiles.sort_by_key(|t| t.tile); tiles.sort_by_key(|t| t.tile);
match pob { match pob {
PlayerOrBot::Player { id } PlayerOrBot::Player { id } if let Some(p) = ctx.db.player().id().find(id) => {
if let Some(p) = ctx.db.player().id().find(id) =>
{
ctx.db.player_hand().insert(PlayerHand { ctx.db.player_hand().insert(PlayerHand {
id: 0, id: 0,
player_id: p.id, player_id: p.id,
@ -83,14 +82,41 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) ->
} }
} }
lobby.game_state = jong_types::states::GameState::Play; lobby.game_state = jong_types::states::GameState::Play;
trace!("dealt hands");
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);
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(());
}
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(());
} }
GameState::Play => { GameState::Play => {
trace!("in play"); // trace!("in play");
let curr_player = lobby.players.get(lobby.current_idx as usize).unwrap(); let curr_player = lobby.players.get(lobby.current_idx as usize).unwrap();
match curr_player { match curr_player {
PlayerOrBot::Player { id: player_id } => { 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 clock = ctx.db.player_clock().player_id().find(player_id).unwrap();
let mut hand = ctx.db.player_hand().player_id().find(player_id).unwrap(); let mut hand = ctx.db.player_hand().player_id().find(player_id).unwrap();
match hand.turn_state { match hand.turn_state {
@ -109,7 +135,7 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) ->
} }
} }
TurnState::Tsumo => { TurnState::Tsumo => {
trace!("wait for discard"); // trace!("wait for discard");
if clock.tick() { if clock.tick() {
ctx.db.player_clock().id().update(clock); ctx.db.player_clock().id().update(clock);
} else { } else {

View file

@ -5,7 +5,7 @@ use spacetimedb::{ReducerContext, Table, rand::seq::SliceRandom, reducer};
use jong_types::PlayerOrBot; use jong_types::PlayerOrBot;
use crate::tables::*; use crate::{reducers::advance_game_private, tables::*};
#[reducer] #[reducer]
pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<(), String> { 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); let lobby = ctx.db.lobby().id().update(lobby);
// TODO should we schedule this outside so that we can clear out stale lobbies? // 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, id: 0,
lobby_id: lobby.id, lobby_id: lobby.id,
scheduled_at: spacetimedb::ScheduleAt::Interval(Duration::from_secs(1).into()), scheduled_at: spacetimedb::ScheduleAt::Interval(Duration::from_secs(1).into()),
}); });
advance_game_private(ctx, game_timer)?;
} else { } else {
// if lobby doesn't exist, reset player state // if lobby doesn't exist, reset player state
player.lobby_id = 0; player.lobby_id = 0;

View file

@ -1,10 +1,9 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy_spacetimedb::{ use bevy_spacetimedb::{ReadInsertMessage, ReadInsertUpdateMessage, ReadUpdateMessage, StdbPlugin};
ReadInsertUpdateMessage, ReadStdbConnectedMessage, ReadStdbDisconnectedMessage,
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::{ use jong_db::{
BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, PlayerTableAccess, RemoteTables, BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, PlayerTableAccess, RemoteTables,
ViewClosedHandsTableAccess, ViewHandTableAccess, ViewClosedHandsTableAccess, ViewHandTableAccess,
@ -12,10 +11,10 @@ use jong_db::{
use jong_types::*; use jong_types::*;
use spacetimedb_sdk::Table; use spacetimedb_sdk::Table;
mod connection;
pub mod player; pub mod player;
use crate::riichi::player::*; use crate::riichi::player::*;
use crate::{SpacetimeDB, creds_store}; use crate::{SpacetimeDB, creds_store};
// pub mod round;
pub struct Riichi; pub struct Riichi;
impl Plugin for Riichi { impl Plugin for Riichi {
@ -24,9 +23,10 @@ impl Plugin for Riichi {
.with_uri("http://localhost:3000") .with_uri("http://localhost:3000")
.with_module_name("jong-line") .with_module_name("jong-line")
.with_run_fn(DbConnection::run_threaded) .with_run_fn(DbConnection::run_threaded)
.add_table(RemoteTables::player)
.add_table(RemoteTables::lobby) .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 // TODO check bevy_spacetimedb PR status
.add_view_with_pk(RemoteTables::view_hand, |p| p.id) .add_view_with_pk(RemoteTables::view_hand, |p| p.id)
.add_view_with_pk(RemoteTables::view_closed_hands, |p| { .add_view_with_pk(RemoteTables::view_closed_hands, |p| {
@ -43,50 +43,42 @@ impl Plugin for Riichi {
app.add_plugins(plugins) app.add_plugins(plugins)
.init_state::<jong_types::states::GameState>() .init_state::<jong_types::states::GameState>()
.add_sub_state::<jong_types::states::TurnState>() .add_sub_state::<jong_types::states::TurnState>()
.add_message::<SyncPlayer>()
.add_message::<SyncOpenHand>()
.add_systems(Startup, subscriptions) .add_systems(Startup, subscriptions)
.add_observer(on_subscribed) .add_systems(Update, (connection::on_connect, connection::on_disconnect))
.add_systems(Update, (on_connect, on_disconnect))
.add_systems(Update, (on_lobby_insert_update, on_player_insert_update))
.add_systems( .add_systems(
Update, 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))), .run_if(in_state(GameState::Play).or(in_state(GameState::Deal))),
); );
} }
} }
fn on_connect(stdb: SpacetimeDB, mut messages: ReadStdbConnectedMessage) { /// on subscribe we need to check:
for msg in messages.read() { /// if we're in a game already
info!("you're now jongline"); /// spawn (restore) all current game state
/// spawn all players and hands
// FIXME hack that doesn't work for startup crash? /// else
while stdb.try_identity().is_none() {} /// spawn self player
/// then
debug!("with identity: {}", stdb.identity()); /// wait for lobbies
creds_store() /// spawn other players
.save(&msg.access_token) /// spawn all hands and ponds
.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) { fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) {
// commands.queue(command); let (tx, rx) = std::sync::mpsc::channel();
let (send, recv) = std::sync::mpsc::channel::<Subscribed>();
stdb.subscription_builder() stdb.subscription_builder()
.on_applied(move |_| { .on_applied(move |_| {
trace!("subs succeeded"); trace!("subs succeeded");
send.send(Subscribed).unwrap(); tx.send(()).unwrap();
}) })
.on_error(|_, err| { .on_error(|_, err| {
error!("subs failed: {err}"); error!("subs failed: {err}");
@ -97,160 +89,164 @@ fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) {
"SELECT p.* FROM player p WHERE p.identity = '{}'", "SELECT p.* FROM player p WHERE p.identity = '{}'",
stdb.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 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 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 b.* FROM bot b JOIN lobby l ON l.id = b.lobby_id".to_string(),
"SELECT * FROM view_hand".to_string(), "SELECT * FROM view_hand".to_string(),
"SELECT * FROM view_closed_hands".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() { while let Ok(()) = rx.recv() {
commands.trigger(event); // todo!()
} }
} }
/// spawns entities to be consistent with server state #[derive(Message)]
// TODO figure out a way to call this for later changes in the various on_ins_upd systems struct SyncPlayer(u32);
fn on_subscribed(
_event: On<Subscribed>,
#[derive(Message)]
struct SyncOpenHand(u32);
#[derive(Message)]
struct SyncClosedHand(PlayerOrBot);
#[derive(Message)]
struct SyncPlayerClock(u32);
fn sync_player(
stdb: SpacetimeDB, stdb: SpacetimeDB,
mut messages: MessageReader<SyncPlayer>,
mut commands: Commands, mut commands: Commands,
mut next_gamestate: ResMut<NextState<GameState>>,
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<SyncOpenHand>,
mut commands: Commands,
tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>,
mut next_turnstate: ResMut<NextState<TurnState>>, mut next_turnstate: ResMut<NextState<TurnState>>,
) { ) {
trace!("on_subscribed"); for SyncOpenHand(id) in messages.read() {
for player in stdb.db().player().iter() { trace!("sync_open_hand");
if player.identity == stdb.identity() { let Some(player_hand) = stdb.db().view_hand().iter().find(|hand| hand.id == *id) else {
// trace!("spawn_main_player"); todo!()
spawn_main_player(&stdb, &mut commands, &mut next_turnstate, &player); };
} else {
// trace!("spawn_other_player");
spawn_other_player(&stdb, &mut commands, &player);
}
}
for bot in stdb.db().bot().iter() { let hand_ent = hands
let id = PlayerOrBot::Bot { id: bot.id };
let hand_view = stdb
.db()
.view_closed_hands()
.iter() .iter()
.find(|v| PlayerOrBot::from(&v.player) == id) .find_map(|(e, h)| (h.owner == PlayerOrBot::Player { id: *id }).then_some(e))
.unwrap(); .unwrap_or_else(|| {
let hand_ent = commands.spawn((Hand, Closed(hand_view.hand_length))).id(); commands
commands.spawn(Player { id }).add_child(hand_ent); .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() { // hand and pond both still need ability to spawn for the reconnect case
next_gamestate.set(lobby.game_state.into()); let hand: Vec<Entity> = player_hand
}
}
fn spawn_main_player(
stdb: &SpacetimeDB,
commands: &mut Commands,
next_turnstate: &mut ResMut<NextState<TurnState>>,
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<NextState<TurnState>>,
main_player: Entity,
player_hand: &PlayerHand,
) {
let hand_tiles: Vec<_> = player_hand
.hand .hand
.iter() .iter()
.map(|dbt| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) .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(); .collect();
let pond_tiles: Vec<_> = player_hand let pond: Vec<Entity> = player_hand
.pond .pond
.iter() .iter()
.map(|dbt| commands.spawn((Tile::from(&dbt.tile), TileId(dbt.id))).id()) .map(|dbt| {
.collect(); tiles
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() .iter()
.find(|v| PlayerOrBot::from(&v.player) == id) .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
{ {
let hand_ent = commands.spawn((Hand, Closed(hand_view.hand_length))).id(); commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id)));
commands.spawn(Player { id }).add_child(hand_ent); }
next_turnstate.set(player_hand.turn_state.into());
} }
} }
fn on_player_insert_update( fn sync_closed_hand(
stdb: SpacetimeDB, stdb: SpacetimeDB,
mut messages: ReadInsertUpdateMessage<jong_db::Player>,
mut events: MessageReader<SyncClosedHand>,
mut commands: Commands, mut commands: Commands,
main_player: Option<Single<&MainPlayer>>, hands: Query<&mut Closed, With<Hand>>,
other_players: Query<&Player, Without<MainPlayer>>, ponds: Query<&mut Children, With<Pond>>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) { ) {
for msg in messages.read() { }
debug!("on_player_insert_update: {:?}", msg.new);
assert_eq!(msg.new.identity, stdb.identity()); fn sync_player_clock() {}
if main_player.is_none() && msg.new.identity == stdb.identity() {
// trace!("spawn_main_player"); fn on_player_insert_update(
spawn_main_player(&stdb, &mut commands, &mut next_turnstate, &msg.new); mut db_messages: ReadInsertUpdateMessage<jong_db::Player>,
} else if other_players.iter().any(|p| {
if let PlayerOrBot::Player { id } = &p.id { mut writer: MessageWriter<SyncPlayer>,
*id == msg.new.id ) {
} else { for msg in db_messages.read() {
false trace!("on_player_insert_update");
} writer.write(SyncPlayer(msg.new.id));
}) {
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( fn on_lobby_insert_update(
stdb: SpacetimeDB, stdb: SpacetimeDB,
mut messages: ReadInsertUpdateMessage<jong_db::Lobby>, mut messages: ReadInsertUpdateMessage<jong_db::Lobby>,
@ -268,7 +264,6 @@ fn on_lobby_insert_update(
.find(&stdb.identity()) .find(&stdb.identity())
.unwrap(); .unwrap();
next_gamestate.set(msg.new.game_state.into());
match msg.new.game_state { match msg.new.game_state {
jong_db::GameState::None => { jong_db::GameState::None => {
trace!("game entered none"); trace!("game entered none");
@ -280,6 +275,7 @@ fn on_lobby_insert_update(
stdb.reducers().add_bot(player.lobby_id).unwrap(); stdb.reducers().add_bot(player.lobby_id).unwrap();
} }
stdb.reducers().set_ready(true).unwrap(); stdb.reducers().set_ready(true).unwrap();
// stdb.reducers().advance_game().unwrap();
} }
} }
jong_db::GameState::Setup => { jong_db::GameState::Setup => {
@ -300,88 +296,84 @@ fn on_lobby_insert_update(
} }
} }
fn on_view_hand_insert(
mut messages: ReadInsertMessage<jong_db::PlayerHand>,
mut writer: MessageWriter<SyncOpenHand>,
) {
for msg in messages.read() {
trace!("on_view_hand_insert");
writer.write(SyncOpenHand(msg.row.id));
}
}
fn on_view_hand_update( fn on_view_hand_update(
stdb: SpacetimeDB,
mut messages: ReadUpdateMessage<jong_db::PlayerHand>, mut messages: ReadUpdateMessage<jong_db::PlayerHand>,
mut writer: MessageWriter<SyncOpenHand>,
// mut commands: Commands,
// tiles: Query<(Entity, &TileId)>,
mut commands: Commands, // main_player: Single<(Entity, &Children), With<MainPlayer>>,
tiles: Query<(Entity, &TileId)>,
main_player: Single<(Entity, Option<&Children>), With<MainPlayer>>, // hand: Query<Entity, With<Hand>>,
// pond: Query<Entity, With<Pond>>,
hand: Query<Entity, With<Hand>>, // // drawn: Option<Single<Entity, With<Drawn>>>,
pond: Query<Entity, With<Pond>>, // mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
// drawn: Option<Single<Entity, With<Drawn>>>,
mut next_turnstate: ResMut<NextState<jong_types::states::TurnState>>,
) { ) {
// TODO can this and similar run at startup or on play/reconnect? // TODO can this and similar run at startup or on play/reconnect?
for msg in messages.read() { for msg in messages.read() {
// trace!("new hand: {:?}", msg.new); trace!("on_view_hand_update");
writer.write(SyncOpenHand(msg.new.player_id));
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();
let hand_tiles: Vec<_> = msg // commands
.new // .entity(hand.iter().find(|e| main_player.1.contains(e)).unwrap())
.hand // .replace_children(&hand_tiles);
.iter() // commands
.map(|dbt| { // .entity(pond.iter().find(|e| main_player.1.contains(e)).unwrap())
tiles // .replace_children(&pond_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 // match msg.new.turn_state {
.entity( // jong_db::TurnState::None => {
hand.iter() // trace!("turnstate none");
.find(|e| main_player.1.is_some_and(|mp| mp.contains(e))) // // TODO do we reconcile hand state here or in ::End?
.unwrap(), // }
) // jong_db::TurnState::Tsumo => {
.replace_children(&hand_tiles); // trace!("turnstate tsumo");
commands // let dbt = msg
.entity( // .new
pond.iter() // .working_tile
.find(|e| main_player.1.is_some_and(|mp| mp.contains(e))) // .as_ref()
.unwrap(), // .expect("entered tsumo without a drawn tile");
) // commands.spawn((Drawn, Tile::from(&dbt.tile), TileId(dbt.id)));
.replace_children(&pond_tiles); // }
// jong_db::TurnState::Menzen => todo!(),
// jong_db::TurnState::RiichiKan => todo!(),
// jong_db::TurnState::RonChiiPonKan => todo!(),
// jong_db::TurnState::End => todo!(),
// }
match msg.new.turn_state { // next_turnstate.set(msg.new.turn_state.into());
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());
}
} }

View file

@ -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);
}
}

View file

@ -4,7 +4,7 @@ use jong_types::PlayerOrBot;
#[derive(Component)] #[derive(Component)]
pub struct Player { pub struct Player {
pub(crate) id: PlayerOrBot, pub id: PlayerOrBot,
} }
#[derive(Component)] #[derive(Component)]
@ -17,13 +17,20 @@ pub struct CurrentPlayer;
pub struct TileId(pub u32); pub struct TileId(pub u32);
#[derive(Component)] #[derive(Component)]
pub struct Hand; pub struct Hand {
pub owner: PlayerOrBot,
}
#[derive(Component)] #[derive(Component)]
pub struct Closed(pub(crate) u8); pub struct Closed {
pub(crate) owner: PlayerOrBot,
pub(crate) length: u8,
}
#[derive(Component)] #[derive(Component)]
pub struct Pond; pub struct Pond {
pub owner: PlayerOrBot,
}
#[derive(Component)] #[derive(Component)]
pub struct Drawn; pub struct Drawn;

View file

@ -72,7 +72,6 @@ impl Plugin for TuiPlugin {
open: true, open: true,
}) })
.init_state::<states::TuiState>() .init_state::<states::TuiState>()
.add_message::<ConfirmSelect>()
.configure_sets( .configure_sets(
Update, Update,
(TuiSet::Input, TuiSet::Layout, TuiSet::Render).chain(), (TuiSet::Input, TuiSet::Layout, TuiSet::Render).chain(),
@ -82,38 +81,38 @@ impl Plugin for TuiPlugin {
(input::keyboard, input::mouse).in_set(TuiSet::Input), (input::keyboard, input::mouse).in_set(TuiSet::Input),
) )
.add_systems(Update, layout::layout.in_set(TuiSet::Layout)) .add_systems(Update, layout::layout.in_set(TuiSet::Layout))
.add_systems(Update, discard_tile.run_if(in_state(TurnState::Tsumo)))
.add_systems( .add_systems(
Update, Update,
( (
(render::render_main_hand, render::render_main_pond) (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, render::render,
) )
.chain() .chain()
.in_set(TuiSet::Render), .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( fn discard_tile(
selected: On<ConfirmSelect>,
stdb: SpacetimeDB, stdb: SpacetimeDB,
mut commands: Commands, mut commands: Commands,
mut selected: MessageReader<ConfirmSelect>,
// main_player: Single<&Children, With<MainPlayer>>, // main_player: Single<&Children, With<MainPlayer>>,
// only main player will have a Drawn tile? // only main player will have a Drawn tile?
drawn: Single<(Entity, &TileId), With<Drawn>>, drawn: Single<(Entity, &TileId), With<Drawn>>,
tiles: Query<&TileId>, 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(selected.0) {
if let Ok(tile_id) = tiles.get(message.0) { trace!("{:?}, {tile_id:?}", selected.0);
stdb.reducers().discard_tile(tile_id.0).unwrap(); stdb.reducers().discard_tile(tile_id.0).unwrap();
stdb.reducers() stdb.reducers().advance_game().unwrap();
.advance_game(stdb.db().game_timer().iter().next().unwrap())
.unwrap();
commands.entity(drawn.0).remove::<Drawn>(); commands.entity(drawn.0).remove::<Drawn>();
} }
}
} }

View file

@ -12,5 +12,5 @@ pub(crate) struct Hovered;
#[derive(Component)] #[derive(Component)]
pub(crate) struct StartSelect; pub(crate) struct StartSelect;
#[derive(Message, Debug)] #[derive(Event, Debug)]
pub(crate) struct ConfirmSelect(pub(crate) Entity); pub(crate) struct ConfirmSelect(pub(crate) Entity);

View file

@ -11,7 +11,6 @@ use crate::tui::{
pub(crate) fn mouse( pub(crate) fn mouse(
mut commands: Commands, mut commands: Commands,
mut mouse_reader: MessageReader<MouseMessage>, mut mouse_reader: MessageReader<MouseMessage>,
mut event_writer: MessageWriter<ConfirmSelect>,
entities: Query<(Entity, &PickRegion)>, entities: Query<(Entity, &PickRegion)>,
hovered: Query<(Entity, &PickRegion), With<Hovered>>, hovered: Query<(Entity, &PickRegion), With<Hovered>>,
startselected: Query<(Entity, &PickRegion), With<StartSelect>>, startselected: Query<(Entity, &PickRegion), With<StartSelect>>,
@ -52,7 +51,7 @@ pub(crate) fn mouse(
for (entity, region) in &startselected { for (entity, region) in &startselected {
if region.area.contains(position) { if region.area.contains(position) {
commands.entity(entity).remove::<StartSelect>(); commands.entity(entity).remove::<StartSelect>();
event_writer.write(ConfirmSelect(entity)); commands.trigger(ConfirmSelect(entity));
} }
} }
} }

View file

@ -150,9 +150,9 @@ pub(crate) fn render_main_hand(
tiles: Query<&jong_types::Tile>, tiles: Query<&jong_types::Tile>,
hovered: Query<Entity, With<Hovered>>, hovered: Query<Entity, With<Hovered>>,
main_player: Single<&Children, With<MainPlayer>>, main_player: Single<&Player, With<MainPlayer>>,
hand: Query<(&Children, Entity), With<Hand>>, hand: Query<(&Hand, &Children)>,
drawn_tile: Option<Single<Entity, With<Drawn>>>, drawn_tile: Option<Single<Entity, With<Drawn>>>,
) -> Result { ) -> Result {
let mut frame = tui.get_frame(); let mut frame = tui.get_frame();
@ -164,14 +164,7 @@ pub(crate) fn render_main_hand(
let hand: Vec<_> = hand let hand: Vec<_> = hand
.iter() .iter()
.find_map(|(c, e)| { .find_map(|(h, c)| (main_player.id == h.owner).then_some(c))
// debug!("main_player children: {:?}", *main_player);
if main_player.contains(&e) {
Some(c)
} else {
None
}
})
.unwrap() .unwrap()
.iter() .iter()
.map(|entity| -> Result<_> { .map(|entity| -> Result<_> {
@ -266,9 +259,9 @@ pub(crate) fn render_main_pond(
tiles: Query<&Tile>, tiles: Query<&Tile>,
hovered: Query<Entity, With<Hovered>>, hovered: Query<Entity, With<Hovered>>,
main_player: Single<&Children, With<MainPlayer>>, main_player: Single<&Player, With<MainPlayer>>,
pond: Query<(&Children, Entity), With<Pond>>, pond: Query<(&Pond, &Children)>,
) -> Result { ) -> Result {
let mut frame = tui.get_frame(); let mut frame = tui.get_frame();
@ -278,13 +271,7 @@ pub(crate) fn render_main_pond(
let pond: Vec<_> = pond let pond: Vec<_> = pond
.iter() .iter()
.find_map(|(c, e)| { .find_map(|(p, c)| (main_player.id == p.owner).then_some(c))
if main_player.contains(&e) {
Some(c)
} else {
None
}
})
.unwrap() .unwrap()
.iter() .iter()
.map(|entity| -> Result<_> { .map(|entity| -> Result<_> {
@ -301,3 +288,5 @@ pub(crate) fn render_main_pond(
Ok(()) Ok(())
} }
pub(crate) fn render_other_hands() {}