bugfixing

This commit is contained in:
Tao Tien 2026-03-04 23:05:56 -08:00
parent b66a4e63f1
commit 3457e0d024
13 changed files with 495 additions and 444 deletions

View file

@ -1,190 +0,0 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::lobby_timer_type::LobbyTimer;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `game_timer`.
///
/// Obtain a handle from the [`GameTimerTableAccess::game_timer`] method on [`super::RemoteTables`],
/// like `ctx.db.game_timer()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.game_timer().on_insert(...)`.
pub struct GameTimerTableHandle<'ctx> {
imp: __sdk::TableHandle<LobbyTimer>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `game_timer`.
///
/// Implemented for [`super::RemoteTables`].
pub trait GameTimerTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`GameTimerTableHandle`], which mediates access to the table `game_timer`.
fn game_timer(&self) -> GameTimerTableHandle<'_>;
}
impl GameTimerTableAccess for super::RemoteTables {
fn game_timer(&self) -> GameTimerTableHandle<'_> {
GameTimerTableHandle {
imp: self.imp.get_table::<LobbyTimer>("game_timer"),
ctx: std::marker::PhantomData,
}
}
}
pub struct GameTimerInsertCallbackId(__sdk::CallbackId);
pub struct GameTimerDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for GameTimerTableHandle<'ctx> {
type Row = LobbyTimer;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = LobbyTimer> + '_ {
self.imp.iter()
}
type InsertCallbackId = GameTimerInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> GameTimerInsertCallbackId {
GameTimerInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: GameTimerInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = GameTimerDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> GameTimerDeleteCallbackId {
GameTimerDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: GameTimerDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct GameTimerUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for GameTimerTableHandle<'ctx> {
type UpdateCallbackId = GameTimerUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> GameTimerUpdateCallbackId {
GameTimerUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: GameTimerUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `lobby_id` unique index on the table `game_timer`,
/// which allows point queries on the field of the same name
/// via the [`GameTimerLobbyIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.game_timer().lobby_id().find(...)`.
pub struct GameTimerLobbyIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<LobbyTimer, u32>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> GameTimerTableHandle<'ctx> {
/// Get a handle on the `lobby_id` unique index on the table `game_timer`.
pub fn lobby_id(&self) -> GameTimerLobbyIdUnique<'ctx> {
GameTimerLobbyIdUnique {
imp: self.imp.get_unique_constraint::<u32>("lobby_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> GameTimerLobbyIdUnique<'ctx> {
/// Find the subscribed row whose `lobby_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &u32) -> Option<LobbyTimer> {
self.imp.find(col_val)
}
}
/// Access to the `scheduled_id` unique index on the table `game_timer`,
/// which allows point queries on the field of the same name
/// via the [`GameTimerScheduledIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.game_timer().scheduled_id().find(...)`.
pub struct GameTimerScheduledIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<LobbyTimer, u64>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> GameTimerTableHandle<'ctx> {
/// Get a handle on the `scheduled_id` unique index on the table `game_timer`.
pub fn scheduled_id(&self) -> GameTimerScheduledIdUnique<'ctx> {
GameTimerScheduledIdUnique {
imp: self.imp.get_unique_constraint::<u64>("scheduled_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> GameTimerScheduledIdUnique<'ctx> {
/// Find the subscribed row whose `scheduled_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &u64) -> Option<LobbyTimer> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<LobbyTimer>("game_timer");
_table.add_unique_constraint::<u32>("lobby_id", |row| &row.lobby_id);
_table.add_unique_constraint::<u64>("scheduled_id", |row| &row.scheduled_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<LobbyTimer>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<LobbyTimer>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `LobbyTimer`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait game_timerQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `LobbyTimer`.
fn game_timer(&self) -> __sdk::__query_builder::Table<LobbyTimer>;
}
impl game_timerQueryTableAccess for __sdk::QueryTableAccessor {
fn game_timer(&self) -> __sdk::__query_builder::Table<LobbyTimer> {
__sdk::__query_builder::Table::new("game_timer")
}
}

View file

@ -14,7 +14,6 @@ pub mod db_tile_type;
pub mod discard_tile_reducer;
pub mod dragon_type;
pub mod game_state_type;
pub mod game_timer_table;
pub mod hand_view_type;
pub mod join_or_create_lobby_reducer;
pub mod lobby_table;
@ -30,6 +29,7 @@ pub mod set_ready_reducer;
pub mod suit_type;
pub mod tile_type;
pub mod turn_state_type;
pub mod user_table;
pub mod user_type;
pub mod view_closed_hands_table;
pub mod view_hand_table;
@ -44,7 +44,6 @@ pub use db_tile_type::DbTile;
pub use discard_tile_reducer::discard_tile;
pub use dragon_type::Dragon;
pub use game_state_type::GameState;
pub use game_timer_table::*;
pub use hand_view_type::HandView;
pub use join_or_create_lobby_reducer::join_or_create_lobby;
pub use lobby_table::*;
@ -60,6 +59,7 @@ pub use set_ready_reducer::set_ready;
pub use suit_type::Suit;
pub use tile_type::Tile;
pub use turn_state_type::TurnState;
pub use user_table::*;
pub use user_type::User;
pub use view_closed_hands_table::*;
pub use view_hand_table::*;
@ -130,10 +130,10 @@ impl __sdk::Reducer for Reducer {
#[doc(hidden)]
pub struct DbUpdate {
bot: __sdk::TableUpdate<Bot>,
game_timer: __sdk::TableUpdate<LobbyTimer>,
lobby: __sdk::TableUpdate<Lobby>,
player_clock: __sdk::TableUpdate<PlayerClock>,
player_config: __sdk::TableUpdate<PlayerConfig>,
user: __sdk::TableUpdate<User>,
view_closed_hands: __sdk::TableUpdate<HandView>,
view_hand: __sdk::TableUpdate<PlayerHand>,
}
@ -147,9 +147,6 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"bot" => db_update
.bot
.append(bot_table::parse_table_update(table_update)?),
"game_timer" => db_update
.game_timer
.append(game_timer_table::parse_table_update(table_update)?),
"lobby" => db_update
.lobby
.append(lobby_table::parse_table_update(table_update)?),
@ -159,6 +156,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate {
"player_config" => db_update
.player_config
.append(player_config_table::parse_table_update(table_update)?),
"user" => db_update
.user
.append(user_table::parse_table_update(table_update)?),
"view_closed_hands" => db_update
.view_closed_hands
.append(view_closed_hands_table::parse_table_update(table_update)?),
@ -194,9 +194,6 @@ impl __sdk::DbUpdate for DbUpdate {
diff.bot = cache
.apply_diff_to_table::<Bot>("bot", &self.bot)
.with_updates_by_pk(|row| &row.id);
diff.game_timer = cache
.apply_diff_to_table::<LobbyTimer>("game_timer", &self.game_timer)
.with_updates_by_pk(|row| &row.scheduled_id);
diff.lobby = cache
.apply_diff_to_table::<Lobby>("lobby", &self.lobby)
.with_updates_by_pk(|row| &row.id);
@ -206,6 +203,9 @@ impl __sdk::DbUpdate for DbUpdate {
diff.player_config = cache
.apply_diff_to_table::<PlayerConfig>("player_config", &self.player_config)
.with_updates_by_pk(|row| &row.id);
diff.user = cache
.apply_diff_to_table::<User>("user", &self.user)
.with_updates_by_pk(|row| &row.identity);
diff.view_closed_hands =
cache.apply_diff_to_table::<HandView>("view_closed_hands", &self.view_closed_hands);
diff.view_hand = cache.apply_diff_to_table::<PlayerHand>("view_hand", &self.view_hand);
@ -219,9 +219,6 @@ impl __sdk::DbUpdate for DbUpdate {
"bot" => db_update
.bot
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"game_timer" => db_update
.game_timer
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"lobby" => db_update
.lobby
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@ -231,6 +228,9 @@ impl __sdk::DbUpdate for DbUpdate {
"player_config" => db_update
.player_config
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"user" => db_update
.user
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
"view_closed_hands" => db_update
.view_closed_hands
.append(__sdk::parse_row_list_as_inserts(table_rows.rows)?),
@ -253,9 +253,6 @@ impl __sdk::DbUpdate for DbUpdate {
"bot" => db_update
.bot
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"game_timer" => db_update
.game_timer
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"lobby" => db_update
.lobby
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@ -265,6 +262,9 @@ impl __sdk::DbUpdate for DbUpdate {
"player_config" => db_update
.player_config
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"user" => db_update
.user
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
"view_closed_hands" => db_update
.view_closed_hands
.append(__sdk::parse_row_list_as_deletes(table_rows.rows)?),
@ -287,10 +287,10 @@ impl __sdk::DbUpdate for DbUpdate {
#[doc(hidden)]
pub struct AppliedDiff<'r> {
bot: __sdk::TableAppliedDiff<'r, Bot>,
game_timer: __sdk::TableAppliedDiff<'r, LobbyTimer>,
lobby: __sdk::TableAppliedDiff<'r, Lobby>,
player_clock: __sdk::TableAppliedDiff<'r, PlayerClock>,
player_config: __sdk::TableAppliedDiff<'r, PlayerConfig>,
user: __sdk::TableAppliedDiff<'r, User>,
view_closed_hands: __sdk::TableAppliedDiff<'r, HandView>,
view_hand: __sdk::TableAppliedDiff<'r, PlayerHand>,
__unused: std::marker::PhantomData<&'r ()>,
@ -307,7 +307,6 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
callbacks: &mut __sdk::DbCallbacks<RemoteModule>,
) {
callbacks.invoke_table_row_callbacks::<Bot>("bot", &self.bot, event);
callbacks.invoke_table_row_callbacks::<LobbyTimer>("game_timer", &self.game_timer, event);
callbacks.invoke_table_row_callbacks::<Lobby>("lobby", &self.lobby, event);
callbacks.invoke_table_row_callbacks::<PlayerClock>(
"player_clock",
@ -319,6 +318,7 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> {
&self.player_config,
event,
);
callbacks.invoke_table_row_callbacks::<User>("user", &self.user, event);
callbacks.invoke_table_row_callbacks::<HandView>(
"view_closed_hands",
&self.view_closed_hands,
@ -970,19 +970,19 @@ impl __sdk::SpacetimeModule for RemoteModule {
fn register_tables(client_cache: &mut __sdk::ClientCache<Self>) {
bot_table::register_table(client_cache);
game_timer_table::register_table(client_cache);
lobby_table::register_table(client_cache);
player_clock_table::register_table(client_cache);
player_config_table::register_table(client_cache);
user_table::register_table(client_cache);
view_closed_hands_table::register_table(client_cache);
view_hand_table::register_table(client_cache);
}
const ALL_TABLE_NAMES: &'static [&'static str] = &[
"bot",
"game_timer",
"lobby",
"player_clock",
"player_config",
"user",
"view_closed_hands",
"view_hand",
];

View file

@ -0,0 +1,192 @@
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
#![allow(unused, clippy::all)]
use super::user_type::User;
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
/// Table handle for the table `user`.
///
/// Obtain a handle from the [`UserTableAccess::user`] method on [`super::RemoteTables`],
/// like `ctx.db.user()`.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.user().on_insert(...)`.
pub struct UserTableHandle<'ctx> {
imp: __sdk::TableHandle<User>,
ctx: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
#[allow(non_camel_case_types)]
/// Extension trait for access to the table `user`.
///
/// Implemented for [`super::RemoteTables`].
pub trait UserTableAccess {
#[allow(non_snake_case)]
/// Obtain a [`UserTableHandle`], which mediates access to the table `user`.
fn user(&self) -> UserTableHandle<'_>;
}
impl UserTableAccess for super::RemoteTables {
fn user(&self) -> UserTableHandle<'_> {
UserTableHandle {
imp: self.imp.get_table::<User>("user"),
ctx: std::marker::PhantomData,
}
}
}
pub struct UserInsertCallbackId(__sdk::CallbackId);
pub struct UserDeleteCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::Table for UserTableHandle<'ctx> {
type Row = User;
type EventContext = super::EventContext;
fn count(&self) -> u64 {
self.imp.count()
}
fn iter(&self) -> impl Iterator<Item = User> + '_ {
self.imp.iter()
}
type InsertCallbackId = UserInsertCallbackId;
fn on_insert(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> UserInsertCallbackId {
UserInsertCallbackId(self.imp.on_insert(Box::new(callback)))
}
fn remove_on_insert(&self, callback: UserInsertCallbackId) {
self.imp.remove_on_insert(callback.0)
}
type DeleteCallbackId = UserDeleteCallbackId;
fn on_delete(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static,
) -> UserDeleteCallbackId {
UserDeleteCallbackId(self.imp.on_delete(Box::new(callback)))
}
fn remove_on_delete(&self, callback: UserDeleteCallbackId) {
self.imp.remove_on_delete(callback.0)
}
}
pub struct UserUpdateCallbackId(__sdk::CallbackId);
impl<'ctx> __sdk::TableWithPrimaryKey for UserTableHandle<'ctx> {
type UpdateCallbackId = UserUpdateCallbackId;
fn on_update(
&self,
callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static,
) -> UserUpdateCallbackId {
UserUpdateCallbackId(self.imp.on_update(Box::new(callback)))
}
fn remove_on_update(&self, callback: UserUpdateCallbackId) {
self.imp.remove_on_update(callback.0)
}
}
/// Access to the `identity` unique index on the table `user`,
/// which allows point queries on the field of the same name
/// via the [`UserIdentityUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.user().identity().find(...)`.
pub struct UserIdentityUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<User, __sdk::Identity>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> UserTableHandle<'ctx> {
/// Get a handle on the `identity` unique index on the table `user`.
pub fn identity(&self) -> UserIdentityUnique<'ctx> {
UserIdentityUnique {
imp: self
.imp
.get_unique_constraint::<__sdk::Identity>("identity"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> UserIdentityUnique<'ctx> {
/// Find the subscribed row whose `identity` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &__sdk::Identity) -> Option<User> {
self.imp.find(col_val)
}
}
/// Access to the `config_id` unique index on the table `user`,
/// which allows point queries on the field of the same name
/// via the [`UserConfigIdUnique::find`] method.
///
/// Users are encouraged not to explicitly reference this type,
/// but to directly chain method calls,
/// like `ctx.db.user().config_id().find(...)`.
pub struct UserConfigIdUnique<'ctx> {
imp: __sdk::UniqueConstraintHandle<User, u32>,
phantom: std::marker::PhantomData<&'ctx super::RemoteTables>,
}
impl<'ctx> UserTableHandle<'ctx> {
/// Get a handle on the `config_id` unique index on the table `user`.
pub fn config_id(&self) -> UserConfigIdUnique<'ctx> {
UserConfigIdUnique {
imp: self.imp.get_unique_constraint::<u32>("config_id"),
phantom: std::marker::PhantomData,
}
}
}
impl<'ctx> UserConfigIdUnique<'ctx> {
/// Find the subscribed row whose `config_id` column value is equal to `col_val`,
/// if such a row is present in the client cache.
pub fn find(&self, col_val: &u32) -> Option<User> {
self.imp.find(col_val)
}
}
#[doc(hidden)]
pub(super) fn register_table(client_cache: &mut __sdk::ClientCache<super::RemoteModule>) {
let _table = client_cache.get_or_make_table::<User>("user");
_table.add_unique_constraint::<__sdk::Identity>("identity", |row| &row.identity);
_table.add_unique_constraint::<u32>("config_id", |row| &row.config_id);
}
#[doc(hidden)]
pub(super) fn parse_table_update(
raw_updates: __ws::v2::TableUpdate,
) -> __sdk::Result<__sdk::TableUpdate<User>> {
__sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| {
__sdk::InternalError::failed_parse("TableUpdate<User>", "TableUpdate")
.with_cause(e)
.into()
})
}
#[allow(non_camel_case_types)]
/// Extension trait for query builder access to the table `User`.
///
/// Implemented for [`__sdk::QueryTableAccessor`].
pub trait userQueryTableAccess {
#[allow(non_snake_case)]
/// Get a query builder for the table `User`.
fn user(&self) -> __sdk::__query_builder::Table<User>;
}
impl userQueryTableAccess for __sdk::QueryTableAccessor {
fn user(&self) -> __sdk::__query_builder::Table<User> {
__sdk::__query_builder::Table::new("user")
}
}

View file

@ -5,9 +5,12 @@ use spacetimedb::{
ReducerContext, ScheduleAt::Interval, Table as _, rand::seq::SliceRandom, reducer,
};
use crate::tables::{
use crate::{
reducers::hand::{discard_tile, discard_tile_private},
tables::{
DbTile, Lobby, LobbyTimer, PlayerClock, PlayerHand, Wall, bot, game_timer, lobby as _,
player_clock, player_config, player_hand, tile, user, wall,
},
};
use jong_types::{GameState, TurnState};
@ -139,6 +142,14 @@ pub fn advance_game_private(
ctx.db.player_clock().player_id().update(clock);
} else {
// TODO bot / auto discard
discard_tile_private(
ctx,
lobby.id,
ctx.db.player_config().id().find(curr_player).unwrap(),
hand.working_tile.unwrap().id,
)
.unwrap();
return Ok(());
}
}
TurnState::Menzen => {}

View file

@ -9,14 +9,26 @@ use crate::tables::*;
#[reducer]
pub fn discard_tile(ctx: &ReducerContext, tile_id: u32) -> Result<(), String> {
let player = ctx.db.user().identity().find(ctx.sender()).unwrap();
let config = ctx.db.player_config().id().find(player.config_id).unwrap();
discard_tile_private(ctx, player.lobby_id, config, tile_id)?;
Ok(())
}
pub(crate) fn discard_tile_private(
ctx: &ReducerContext,
lobby_id: u32,
player_config: PlayerConfig,
tile_id: u32,
) -> Result<(), String> {
let mut hand = ctx
.db
.player_hand()
.player_id()
.find(player.config_id)
.find(player_config.id)
.unwrap();
// TODO we can probably remove a buncha these errors
let dealt = ctx.db.tile().id().find(tile_id).unwrap();
let drawn = hand.working_tile.unwrap();
@ -27,13 +39,7 @@ pub fn discard_tile(ctx: &ReducerContext, tile_id: u32) -> Result<(), String> {
// dealt from hand
let dealt = hand.hand.remove(i);
hand.hand.push(drawn);
if ctx
.db
.player_config()
.id()
.find(player.config_id)
.is_some_and(|c| c.sort)
{
if player_config.sort {
hand.hand.sort_by_key(|t| t.tile);
}
@ -48,24 +54,15 @@ pub fn discard_tile(ctx: &ReducerContext, tile_id: u32) -> Result<(), String> {
hand.turn_state = TurnState::None;
ctx.db.player_hand().player_id().update(hand);
let mut clock = ctx
.db
.player_clock()
.player_id()
.find(player.config_id)
.unwrap();
if let Some(mut clock) = ctx.db.player_clock().player_id().find(player_config.id) {
clock.renew();
ctx.db.player_clock().player_id().update(clock);
}
let mut lobby = ctx.db.lobby().id().find(player.lobby_id).unwrap();
let mut lobby = ctx.db.lobby().id().find(lobby_id).unwrap();
lobby.next_player();
ctx.db.lobby().id().update(lobby);
debug!(
"player {} discarded tile {:?}",
player.identity, dealt_tile.tile
);
Ok(())
}

View file

@ -1,6 +1,6 @@
use std::time::Duration;
use log::{info, warn};
use log::{debug, info, warn};
use spacetimedb::{ReducerContext, Table, rand::seq::SliceRandom, reducer};
use crate::{reducers::advance_game_private, tables::*};
@ -50,6 +50,50 @@ pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<(
Ok(())
}
#[reducer]
pub fn set_ready(ctx: &ReducerContext, ready: bool) -> Result<(), String> {
let mut user = ctx.db.user().identity().find(ctx.sender()).unwrap();
let mut player = ctx.db.player_config().id().find(user.config_id).unwrap();
player.ready = ready;
let player = ctx.db.player_config().id().update(player);
if player.ready {
debug!("player readied");
} else {
debug!("player unreadied");
}
if let Some(mut lobby) = ctx.db.lobby().id().find(user.lobby_id)
&& lobby.players.len() == 4
&& lobby.players.iter().all(|id| {
ctx.db
.player_config()
.id()
.find(id)
.is_some_and(|p| p.ready)
})
&& ctx.db.game_timer().lobby_id().find(user.lobby_id).is_none()
{
lobby.game_state = jong_types::states::GameState::Setup;
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(LobbyTimer {
lobby_id: lobby.id,
scheduled_id: 0,
scheduled_at: spacetimedb::ScheduleAt::Interval(Duration::from_secs(1).into()),
});
advance_game_private(ctx, game_timer)?;
} else {
// TODO if lobby doesn't exist, reset player state?
// FIXME return error here
}
Ok(())
}
#[reducer]
pub fn add_bot(ctx: &ReducerContext, lobby_id: u32) -> Result<(), String> {
if lobby_id == 0 {
@ -63,7 +107,7 @@ pub fn add_bot(ctx: &ReducerContext, lobby_id: u32) -> Result<(), String> {
id: 0,
name: String::new(),
ready: true,
sort: true,
sort: false,
});
let bot = ctx.db.bot().insert(Bot {
id: 0,
@ -78,44 +122,3 @@ pub fn add_bot(ctx: &ReducerContext, lobby_id: u32) -> Result<(), String> {
Err(format!("lobby {lobby_id} doesn't exist or is full"))
}
}
#[reducer]
pub fn set_ready(ctx: &ReducerContext, ready: bool) -> Result<(), String> {
let mut user = ctx.db.user().identity().find(ctx.sender()).unwrap();
let mut player = ctx.db.player_config().id().find(user.config_id).unwrap();
player.ready = ready;
let player = ctx.db.player_config().id().update(player);
if let Some(mut lobby) = ctx.db.lobby().id().find(user.lobby_id)
&& lobby.players.len() == 4
&& lobby.players.iter().all(|id| {
ctx.db
.player_config()
.id()
.find(id)
.is_some_and(|p| p.ready)
})
{
lobby.game_state = jong_types::states::GameState::Setup;
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(LobbyTimer {
lobby_id: lobby.id,
scheduled_id: 0,
scheduled_at: spacetimedb::ScheduleAt::Interval(Duration::from_secs(1).into()),
});
advance_game_private(ctx, game_timer)?;
} else {
// TODO if lobby doesn't exist, reset player state
user.lobby_id = 0;
user = ctx.db.user().identity().update(user);
return Err(format!("couldn't find lobby with id: {}", user.lobby_id));
}
Ok(())
}

View file

@ -7,7 +7,7 @@ use jong_types::{
use crate::reducers::advance_game_private;
#[table(accessor = user)]
#[table(accessor = user, public)]
#[table(accessor = logged_out_user)]
#[derive(Debug)]
pub struct User {
@ -18,7 +18,6 @@ pub struct User {
#[index(btree)]
pub lobby_id: u32,
#[unique]
pub config_id: u32,
}
@ -112,7 +111,7 @@ pub struct PlayerHand {
pub working_tile: Option<DbTile>,
}
#[table(accessor = game_timer, scheduled(advance_game_private), public)]
#[table(accessor = game_timer, scheduled(advance_game_private))]
pub struct LobbyTimer {
#[unique]
pub lobby_id: u32,

View file

@ -5,10 +5,12 @@ use bevy::render::render_resource::AsBindGroupShaderType;
use bevy_spacetimedb::{ReadInsertMessage, ReadInsertUpdateMessage, ReadUpdateMessage, StdbPlugin};
use jong_db::{
self, GameTimerTableAccess, PlayerClockTableAccess, add_bot, advance_game, set_ready,
self, PlayerClockTableAccess, PlayerConfigTableAccess, UserTableAccess, add_bot, advance_game,
join_or_create_lobby, lobbyQueryTableAccess, player_configQueryTableAccess, set_ready,
userQueryTableAccess,
};
use jong_db::{
BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, PlayerTableAccess, RemoteTables,
BotTableAccess, DbConnection, LobbyTableAccess, PlayerHand, RemoteTables,
ViewClosedHandsTableAccess, ViewHandTableAccess,
};
use jong_types::*;
@ -26,15 +28,14 @@ impl Plugin for Riichi {
.with_uri("http://localhost:3000")
.with_module_name("jong-line")
.with_run_fn(DbConnection::run_threaded)
.add_table(RemoteTables::user)
.add_table(RemoteTables::lobby)
.add_table(RemoteTables::player)
.add_table(RemoteTables::bot)
.add_table(RemoteTables::player_config)
.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| {
PlayerOrBot::from(&p.player)
});
.add_view_with_pk(RemoteTables::view_hand, |p| p.player_id)
.add_view_with_pk(RemoteTables::view_closed_hands, |p| p.player_id);
let plugins =
if let Some(token) = creds_store().load().expect("i/o error loading credentials") {
// FIXME patch plugin so this takes Option?
@ -52,9 +53,10 @@ impl Plugin for Riichi {
.add_systems(
Update,
(
on_user_insert_update,
on_lobby_insert_update,
on_player_insert_update,
on_bot_insert_update,
on_player_config_insert_update,
),
)
.add_systems(
@ -65,17 +67,6 @@ impl Plugin for Riichi {
}
}
/// 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) {
while stdb.try_identity().is_none() {
bevy::platform::thread::sleep(Duration::from_secs(1));
@ -90,116 +81,20 @@ fn subscriptions(stdb: SpacetimeDB, mut commands: Commands) {
.on_error(|_, err| {
error!("subs failed: {err}");
})
.subscribe([
// TODO change these to sub/unsub based on being in lobby and some such
format!(
"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 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(),
]);
.add_query(|q| q.from.user().r#where(|u| u.identity.eq(stdb.identity())))
.subscribe();
// TODO more sub/unsub based on being in lobby and some such, current done in tui::keybaord
while let Ok(()) = rx.recv() {
// todo!()
}
}
fn sync_closed_hand(
stdb: SpacetimeDB,
mut messages: MessageReader<SyncClosedHand>,
mut commands: Commands,
tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &mut Closed, &Hand)>,
ponds: Query<(Entity, &Pond)>,
) {
for SyncClosedHand(id) in messages.read() {
let Some(hand_view) = stdb
.db()
.view_closed_hands()
.iter()
.find(|hand| PlayerOrBot::from(&hand.player) == *id)
else {
todo!()
};
let hand_ent = hands
.iter()
.find_map(|(e, c, h)| (h.owner == *id).then_some(e))
.unwrap_or_else(|| commands.spawn(Hand { owner: *id }).id());
let pond_ent = ponds
.iter()
.find_map(|(e, p)| (p.owner == *id).then_some(e))
.unwrap_or_else(|| commands.spawn(Pond { owner: *id }).id());
let pond: Vec<Entity> = hand_view
.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).insert(Closed {
length: hand_view.hand_length,
});
commands.entity(pond_ent).replace_children(&pond);
if hand_view.drawn {
commands.spawn(Drawn);
}
}
}
fn sync_player_clock() {}
fn on_player_insert_update(
stdb: SpacetimeDB,
mut messages: ReadInsertUpdateMessage<jong_db::Player>,
mut commands: Commands,
players: Query<(Entity, &Player)>,
) {
for msg in messages.read() {
trace!("on_player_insert_update");
let player = &msg.new;
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: msg.new.id },
})
.id()
});
if player.identity == stdb.identity() {
commands.entity(player_ent).insert(MainPlayer);
} else {
}
}
}
fn on_bot_insert_update(stdb: SpacetimeDB, mut messages: ReadInsertUpdateMessage<jong_db::Bot>) {
for msg in messages.read() {}
}
fn on_user_insert_update() {}
#[derive(Resource, Default)]
pub struct Lobby {
pub players: Vec<PlayerOrBot>,
pub players: Vec<u32>,
}
fn on_lobby_insert_update(
@ -216,11 +111,12 @@ fn on_lobby_insert_update(
for msg in messages.read() {
// trace!("on_lobby_insert_update msg:\n{:#?}", msg.new);
let player = stdb
let player = stdb.db().user().identity().find(&stdb.identity()).unwrap();
let player_config = stdb
.db()
.player()
.identity()
.find(&stdb.identity())
.player_config()
.id()
.find(&player.config_id)
.unwrap();
match msg.new.game_state {
@ -229,7 +125,7 @@ fn on_lobby_insert_update(
}
jong_db::GameState::Lobby => {
// trace!("game entered lobby");
if !player.ready {
if !player_config.ready {
for _ in 0..3 {
stdb.reducers().add_bot(player.lobby_id).unwrap();
}
@ -251,54 +147,106 @@ fn on_lobby_insert_update(
}
}
lobby.players = msg.new.players.iter().map(|p| p.into()).collect();
lobby.players = msg.new.players.clone();
next_gamestate.set(msg.new.game_state.into());
}
}
fn on_view_hand_insert(mut messages: ReadInsertMessage<jong_db::PlayerHand>) {
fn on_bot_insert_update(stdb: SpacetimeDB, mut messages: ReadInsertUpdateMessage<jong_db::Bot>) {
for msg in messages.read() {}
}
fn on_player_config_insert_update(
stdb: SpacetimeDB,
mut messages: ReadInsertUpdateMessage<jong_db::PlayerConfig>,
mut commands: Commands,
players: Query<(Entity, &Player)>,
) {
for msg in messages.read() {
trace!("on_view_hand_insert");
trace!("on_player_insert_update");
let player = &msg.new;
let player_ent = players
.iter()
.find_map(|(e, p)| (p.id == player.id).then_some(e))
.unwrap_or_else(|| commands.spawn(Player { id: player.id }).id());
if player.id
== stdb
.db()
.user()
.identity()
.find(&stdb.identity())
.unwrap()
.config_id
{
commands.entity(player_ent).insert(MainPlayer);
} else {
}
}
}
fn on_view_hand_update(mut messages: ReadUpdateMessage<jong_db::PlayerHand>) {
fn on_view_hand_insert(
mut messages: ReadInsertMessage<jong_db::PlayerHand>,
mut commands: Commands,
tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>,
mut next_turnstate: ResMut<NextState<TurnState>>,
) {
for msg in messages.read() {
trace!("on_view_hand_insert");
sync_open_hand(
&mut commands,
tiles,
hands,
ponds,
&mut next_turnstate,
&msg.row,
);
}
}
fn on_view_hand_update(
mut messages: ReadUpdateMessage<jong_db::PlayerHand>,
mut commands: Commands,
tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>,
mut next_turnstate: ResMut<NextState<TurnState>>,
) {
for msg in messages.read() {
trace!("on_view_hand_update");
sync_open_hand(
&mut commands,
tiles,
hands,
ponds,
&mut next_turnstate,
&msg.new,
);
}
}
fn sync_open_hand(
commands: Commands,
commands: &mut Commands,
tiles: Query<(Entity, &TileId)>,
hands: Query<(Entity, &Hand)>,
ponds: Query<(Entity, &Pond)>,
next_turnstate: &mut ResMut<NextState<TurnState>>,
player_hand: PlayerHand,
player_hand: &PlayerHand,
) {
let player_id = player_hand.player_id;
let hand_ent = hands
.iter()
.find_map(|(e, h)| (h.owner == player_hand.player_id).then_some(e))
.unwrap_or_else(|| {
commands
.spawn(Hand {
owner: PlayerOrBot::Player { id: *id },
})
.id()
});
.find_map(|(e, h)| (h.player_id == player_id).then_some(e))
.unwrap_or_else(|| commands.spawn(Hand { player_id }).id());
let pond_ent = ponds
.iter()
.find_map(|(e, p)| (p.owner == PlayerOrBot::Player { id: *id }).then_some(e))
.unwrap_or_else(|| {
commands
.spawn(Pond {
owner: PlayerOrBot::Player { id: *id },
})
.id()
});
.find_map(|(e, p)| (p.player_id == player_id).then_some(e))
.unwrap_or_else(|| commands.spawn(Pond { player_id }).id());
// hand and pond both still need ability to spawn for the reconnect case
let hand: Vec<Entity> = player_hand
@ -325,7 +273,7 @@ fn sync_open_hand(
commands.entity(hand_ent).replace_children(&hand);
commands.entity(pond_ent).replace_children(&pond);
if let Some(dbt) = player_hand.working_tile
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)));
@ -333,3 +281,55 @@ fn sync_open_hand(
next_turnstate.set(player_hand.turn_state.into());
}
// fn sync_closed_hand(
// stdb: SpacetimeDB,
// mut messages: MessageReader<SyncClosedHand>,
// mut commands: Commands,
// tiles: Query<(Entity, &TileId)>,
// hands: Query<(Entity, &mut Closed, &Hand)>,
// ponds: Query<(Entity, &Pond)>,
// ) {
// for SyncClosedHand(id) in messages.read() {
// let Some(hand_view) = stdb
// .db()
// .view_closed_hands()
// .iter()
// .find(|hand| PlayerOrBot::from(&hand.player) == *id)
// else {
// todo!()
// };
// let hand_ent = hands
// .iter()
// .find_map(|(e, c, h)| (h.owner == *id).then_some(e))
// .unwrap_or_else(|| commands.spawn(Hand { owner: *id }).id());
// let pond_ent = ponds
// .iter()
// .find_map(|(e, p)| (p.owner == *id).then_some(e))
// .unwrap_or_else(|| commands.spawn(Pond { owner: *id }).id());
// let pond: Vec<Entity> = hand_view
// .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).insert(Closed {
// length: hand_view.hand_length,
// });
// commands.entity(pond_ent).replace_children(&pond);
// if hand_view.drawn {
// commands.spawn(Drawn);
// }
// }
// }

View file

@ -1,10 +1,8 @@
use bevy::prelude::*;
use jong_types::PlayerOrBot;
#[derive(Component)]
pub struct Player {
pub id: PlayerOrBot,
pub id: u32,
}
#[derive(Component)]
@ -25,17 +23,18 @@ pub struct TileId(pub u32);
#[derive(Component)]
pub struct Hand {
pub owner: PlayerOrBot,
pub player_id: u32,
}
#[derive(Component)]
pub struct Closed {
pub player_id: u32,
pub(crate) length: u8,
}
#[derive(Component)]
pub struct Pond {
pub owner: PlayerOrBot,
pub player_id: u32,
}
#[derive(Component)]

View file

@ -8,7 +8,7 @@ use spacetimedb_sdk::Table;
use tui_logger::TuiWidgetState;
use jong::{SpacetimeDB, riichi::player::*};
use jong_db::{self, GameTimerTableAccess, advance_game, discard_tile as _};
use jong_db::{self, advance_game, discard_tile as _};
use jong_types::states::{GameState, TurnState};
mod input;

View file

@ -1,11 +1,16 @@
use bevy::prelude::*;
use bevy_ratatui::crossterm::event::KeyCode;
use bevy_ratatui::event::KeyMessage;
use spacetimedb_sdk::DbContext;
use spacetimedb_sdk::Table;
use tui_logger::TuiWidgetEvent;
use jong::SpacetimeDB;
use jong_db::PlayerTableAccess;
use jong_db::join_or_create_lobby;
use jong_db::{
LobbyTableAccess, UserTableAccess, botQueryTableAccess,
join_or_create_lobby, lobbyQueryTableAccess, player_clockQueryTableAccess,
player_configQueryTableAccess, view_closed_handsQueryTableAccess, view_handQueryTableAccess,
};
use jong_types::GameState;
use crate::tui::layout::Overlays;
@ -48,7 +53,42 @@ pub(crate) fn keyboard(
TuiState::MainMenu => match key {
KeyCode::Char('p') => {
// TODO this should be a msg/signal and handle the correct lobby
stdb.reducers().join_or_create_lobby(0).unwrap();
stdb.reducers()
.join_or_create_lobby_then(0, |ctx, res| {
if res.is_ok() {
let user =
ctx.db().user().identity().find(&ctx.identity()).unwrap();
let lobby_id = user.lobby_id;
ctx.subscription_builder()
.on_applied(|ctx| {
// TODO subscribe other player configs?
// ctx.subscription_builder()
// .add_query(|q| {
// q.from.player_config().left_semijoin(right, on)
// })
// .subscribe();
})
.add_query(|q| q.from.lobby().r#where(|l| l.id.eq(lobby_id)))
.add_query(|q| {
q.from.bot().r#where(|b| b.lobby_id.eq(lobby_id))
})
.add_query(|q| {
q.from
.player_config()
.r#where(|pc| pc.id.eq(user.config_id))
})
.add_query(|q| {
q.from
.player_clock()
.r#where(|pk| pk.player_id.eq(user.config_id))
})
.add_query(|q| q.from.view_hand())
.add_query(|q| q.from.view_closed_hands())
.subscribe();
}
})
.unwrap();
next_tuistate.set(TuiState::InGame);
}
KeyCode::Char('z') => {

View file

@ -164,7 +164,7 @@ pub(crate) fn render_main_hand(
let hand: Vec<_> = hands
.iter()
.find_map(|(h, c)| (main_player.id == h.owner).then_some(c))
.find_map(|(h, c)| (main_player.id == h.player_id).then_some(c))
.unwrap()
.iter()
.map(|entity| -> Result<_> {
@ -271,7 +271,7 @@ 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(|(p, c)| (main_player.id == p.player_id).then_some(c))
.unwrap()
.iter()
.map(|entity| -> Result<_> {

View file

@ -18,7 +18,7 @@ spacetime:
devenv up
spacetime_dev:
spacetime dev --module-bindings-path jong-db/src/db jong-line --delete-data=on-conflict --yes --server-only
spacetime dev --module-bindings-path jong-db/src/db jong-line --delete-data=always --yes --server-only
spacetime_generate-bindings:
spacetime generate --lang rust --out-dir jong-db/src/db --module-path jong-line