refactor jong-line

This commit is contained in:
Tao Tien 2026-03-02 23:56:30 -08:00
parent 0c3fe6f87a
commit 147f939179
6 changed files with 242 additions and 278 deletions

View file

@ -3,68 +3,46 @@
use log::{debug, trace};
use spacetimedb::{ReducerContext, Table, reducer};
use crate::tables::*;
mod reducers;
mod tables;
#[reducer]
pub fn clear_all(ctx: &ReducerContext) {
for row in ctx.db.player().iter() {
ctx.db.player().delete(row);
}
for row in ctx.db.lobby().iter() {
ctx.db.lobby().delete(row);
}
for row in ctx.db.bot().iter() {
ctx.db.bot().delete(row);
}
for row in ctx.db.wall().iter() {
ctx.db.wall().delete(row);
}
for row in ctx.db.tile().iter() {
ctx.db.tile().delete(row);
}
}
use crate::tables::*;
#[reducer(client_connected)]
pub fn connect(ctx: &ReducerContext) -> Result<(), String> {
let player = if let Some(player) = ctx.db.logged_out_player().identity().find(ctx.sender()) {
let player = ctx.db.player().insert(player);
ctx.db.logged_out_player().identity().delete(ctx.sender());
let player = if let Some(player) = ctx.db.logged_out_user().identity().find(ctx.sender()) {
let player = ctx.db.user().insert(player);
ctx.db.logged_out_user().identity().delete(ctx.sender());
player
} else {
debug!("inserting new player with identity {:?}", ctx.sender());
ctx.db.player().try_insert(Player {
debug!("inserting new user with identity {:?}", ctx.sender());
ctx.db.user().try_insert(User {
identity: ctx.sender(),
id: 0,
name: None,
name: String::new(),
config_id: 0,
lobby_id: 0,
ready: false,
sort: true,
})?
};
debug!("player connected: {:?}", player);
debug!("user connected: {:?}", player);
Ok(())
}
#[reducer(client_disconnected)]
pub fn disconnect(ctx: &ReducerContext) -> Result<(), String> {
let player = ctx
let user = ctx
.db
.player()
.user()
.identity()
.find(ctx.sender())
.ok_or_else(|| format!("can't find player {} to disconnect", ctx.sender()))?;
let player = ctx.db.logged_out_player().insert(player);
if !ctx.db.player().identity().delete(ctx.sender()) {
Err("can't delete row")?
let user = ctx.db.logged_out_user().insert(user);
if !ctx.db.user().identity().delete(ctx.sender()) {
Err(format!("can't delete user: {user:?}"))?
}
debug!("player disconnected: {:?}", player);
debug!("user disconnected: {:?}", user);
Ok(())
}

View file

@ -6,10 +6,10 @@ use spacetimedb::{
};
use crate::tables::{
DbTile, DbWall, GameTimer, Lobby, PlayerClock, PlayerHand, bot, game_timer, lobby as _, player,
player_clock, player_hand, tile as _, wall,
DbTile, Lobby, LobbyTimer, PlayerClock, PlayerHand, Wall, bot, game_timer, lobby as _,
player_clock, player_config, player_hand, tile, user, wall,
};
use jong_types::{GameState, PlayerOrBot, TurnState};
use jong_types::{GameState, TurnState};
mod hand;
mod lobby;
@ -22,7 +22,7 @@ pub fn advance_game(ctx: &ReducerContext) -> Result<(), String> {
.lobby_id()
.find(
ctx.db
.player()
.user()
.identity()
.find(ctx.sender())
.ok_or("player not in lobby")?
@ -43,8 +43,7 @@ fn shuffle_wall(ctx: &ReducerContext, lobby: &mut Lobby) {
wall.shuffle(&mut rng);
wall
};
ctx.db.wall().insert(DbWall {
// id: 0,
ctx.db.wall().insert(Wall {
lobby_id: lobby.id,
tiles,
});
@ -53,33 +52,17 @@ fn shuffle_wall(ctx: &ReducerContext, lobby: &mut Lobby) {
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 {
for player 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![],
player_id: *player,
turn_state: TurnState::None,
hand: tiles,
pond: vec![],
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;
@ -87,7 +70,10 @@ fn deal_hands(ctx: &ReducerContext, lobby: &mut Lobby) -> Result<(), String> {
}
#[reducer]
pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) -> Result<(), String> {
pub fn advance_game_private(
ctx: &ReducerContext,
mut game_timer: LobbyTimer,
) -> 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?
@ -99,6 +85,21 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) ->
// TODO keep a count to clear stale lobbies
// trace!("shuffle wall");
shuffle_wall(ctx, &mut lobby);
lobby.players.shuffle(&mut ctx.rng());
for player_id in lobby
.players
.iter()
.filter(|id| ctx.db.user().config_id().find(*id).is_some())
{
ctx.db.player_clock().insert(PlayerClock {
player_id: *player_id,
renewable: 5,
total: 20,
});
}
ctx.db.lobby().id().update(lobby);
advance_game_private(ctx, game_timer)?;
return Ok(());
@ -114,11 +115,7 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) ->
GameState::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}");
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(curr_player).unwrap();
match hand.turn_state {
TurnState::None => {
// trace!("draw a tile");
@ -128,18 +125,20 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) ->
hand.working_tile = Some(tile);
hand.turn_state = TurnState::Tsumo;
ctx.db.wall().lobby_id().update(wall);
ctx.db.player_hand().id().update(hand);
ctx.db.player_hand().player_id().update(hand);
} else {
// TODO out of tiles
todo!()
}
}
TurnState::Tsumo => {
// trace!("wait for discard");
if clock.tick() {
ctx.db.player_clock().id().update(clock);
// only real players have clocks?
if let Some(mut clock) = ctx.db.player_clock().player_id().find(curr_player)
&& clock.tick()
{
ctx.db.player_clock().player_id().update(clock);
} else {
// TODO auto-discard
// TODO bot / auto discard
}
}
TurnState::Menzen => {}
@ -148,28 +147,12 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) ->
TurnState::End => {}
}
}
PlayerOrBot::Bot { id: bot_id } => {
debug!("current bot is {bot_id}");
let bot = ctx.db.bot().id().find(bot_id).unwrap();
match bot.turn_state {
// TurnState::None => todo!(),
// TurnState::Tsumo => todo!(),
// TurnState::Menzen => todo!(),
// TurnState::RiichiKan => todo!(),
// TurnState::RonChiiPonKan => todo!(),
// TurnState::End => todo!(),
_ => {}
}
lobby.next_player();
}
}
}
GameState::Exit => {
ctx.db.game_timer().id().delete(game_timer.id);
ctx.db.lobby().id().delete(lobby.id);
// ctx.db.game_timer().id().delete(game_timer.id);
// ctx.db.lobby().id().delete(lobby.id);
// TODO reset all player lobbies, delete bots, etc?
// is there a way to do this automatically, or rely on elsewhere's checks clearing the state?
return Ok(());
todo!("lobby exit cleanup")
}
// TODO handle stale lobbies
@ -180,10 +163,10 @@ pub fn advance_game_private(ctx: &ReducerContext, mut game_timer: GameTimer) ->
// ctx.db.game_timer().id().update(game_timer);
ctx.db.lobby().id().update(lobby);
} else {
ctx.db.game_timer().id().delete(game_timer.id);
// ctx.db.game_timer().id().delete(game_timer.id);
Err(format!(
"ran schedule {} for empty lobby {}",
game_timer.id, game_timer.lobby_id
game_timer.scheduled_id, game_timer.lobby_id
))?;
}

View file

@ -8,56 +8,63 @@ use crate::tables::*;
// TODO make sure this can't be called or just error here?
#[reducer]
pub fn discard_tile(ctx: &ReducerContext, tile_id: u32) -> Result<(), String> {
let player = ctx.db.player().identity().find(ctx.sender()).unwrap();
let mut hand = ctx.db.player_hand().player_id().find(player.id).unwrap();
let player = ctx.db.user().identity().find(ctx.sender()).unwrap();
let mut hand = ctx
.db
.player_hand()
.player_id()
.find(player.config_id)
.unwrap();
// TODO we can probably remove a buncha these errors
let dealt_tile = if let Some(dealt) = ctx.db.tile().id().find(tile_id) {
if let Some(drawn) = hand.working_tile {
if drawn.id == dealt.id {
let dealt = ctx.db.tile().id().find(tile_id).unwrap();
let drawn = hand.working_tile.unwrap();
let dealt_tile = if dealt.id == drawn.id {
// dealt from drawn tile
dealt
} else if let Some((i, _)) = hand.hand.iter().enumerate().find(|(_, t)| t.id == tile_id)
{
} else if let Some((i, _)) = hand.hand.iter().enumerate().find(|(_, t)| dealt.id == t.id) {
// 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)
{
hand.hand.sort_by_key(|t| t.tile);
}
dealt
} else {
return Err(format!(
"player {} attempted to deal tile {} not in hand or drawn",
player.id, tile_id
));
}
} else {
return Err(format!(
"player {} attempted to deal tile {} without having drawn",
player.id, tile_id
));
}
} else {
return Err(format!(
"player {} attempted to deal nonexistant tile {}",
player.id, tile_id
));
// ERROR
Err("dealt tile is missing")?
};
hand.pond.push(dealt_tile);
hand.working_tile = None;
hand.turn_state = TurnState::None;
ctx.db.player_hand().id().update(hand);
ctx.db.player_hand().player_id().update(hand);
let mut clock = ctx.db.player_clock().player_id().find(player.id).unwrap();
let mut clock = ctx
.db
.player_clock()
.player_id()
.find(player.config_id)
.unwrap();
clock.renew();
ctx.db.player_clock().id().update(clock);
ctx.db.player_clock().player_id().update(clock);
let mut lobby = ctx.db.lobby().id().find(player.lobby_id).unwrap();
lobby.next_player();
ctx.db.lobby().id().update(lobby);
debug!("player {} discarded tile {:?}", player.id, dealt_tile.tile);
debug!(
"player {} discarded tile {:?}",
player.identity, dealt_tile.tile
);
Ok(())
}

View file

@ -1,48 +1,52 @@
use std::time::Duration;
use log::info;
use log::{info, warn};
use spacetimedb::{ReducerContext, Table, rand::seq::SliceRandom, reducer};
use jong_types::PlayerOrBot;
use crate::{reducers::advance_game_private, tables::*};
#[reducer]
pub fn join_or_create_lobby(ctx: &ReducerContext, mut lobby_id: u32) -> Result<(), String> {
let mut player = ctx
let mut user = ctx
.db
.player()
.user()
.identity()
.find(ctx.sender())
.ok_or(format!("cannot find player {}", ctx.sender()))?;
if lobby_id == 0 && player.lobby_id == 0 {
// TODO check first if player is already in a lobby
if lobby_id == 0 && user.lobby_id == 0 {
let player = ctx.db.player_config().insert(PlayerConfig {
id: 0,
name: String::new(),
ready: false,
sort: true,
});
let lobby = ctx.db.lobby().insert(Lobby {
id: 0,
players: vec![PlayerOrBot::Player { id: player.id }],
players: vec![player.id],
game_state: jong_types::states::GameState::Lobby,
dealer_idx: 0,
current_idx: 0,
});
lobby_id = lobby.id;
info!("created lobby: {}", lobby.id);
lobby_id = lobby.id;
player.lobby_id = lobby_id;
user.config_id = player.id;
user.lobby_id = lobby.id;
} else {
let lobby = ctx
.db
.lobby()
.id()
.find(player.lobby_id)
.ok_or(format!("can't find lobby {}", player.lobby_id))?;
.find(user.lobby_id)
.ok_or(format!("can't find lobby {}", user.lobby_id))?;
lobby_id = lobby.id;
}
let player = ctx.db.player().identity().update(player);
let user = ctx.db.user().identity().update(user);
info!("user {} joined lobby {}", user.name, lobby_id);
info!("player {} joined lobby {}", player.id, lobby_id);
Ok(())
}
@ -51,19 +55,22 @@ pub fn add_bot(ctx: &ReducerContext, lobby_id: u32) -> Result<(), String> {
if lobby_id == 0 {
Err("cannot add a bot without a lobby".into())
} else if let Some(mut lobby) = ctx.db.lobby().id().find(lobby_id)
&& (ctx.db.player().lobby_id().filter(lobby_id).count()
&& (ctx.db.user().lobby_id().filter(lobby_id).count()
+ ctx.db.bot().lobby_id().filter(lobby_id).count()
< 4)
{
let player = ctx.db.player_config().insert(PlayerConfig {
id: 0,
name: String::new(),
ready: true,
sort: true,
});
let bot = ctx.db.bot().insert(Bot {
id: 0,
lobby_id,
hand: vec![],
pond: vec![],
working_tile: None,
turn_state: jong_types::TurnState::None,
config_id: player.id,
});
lobby.players.push(PlayerOrBot::Bot { id: bot.id });
lobby.players.push(player.id);
ctx.db.lobby().id().update(lobby);
info!("added bot {} to lobby {}", bot.id, lobby_id);
Ok(())
@ -74,33 +81,40 @@ pub fn add_bot(ctx: &ReducerContext, lobby_id: u32) -> Result<(), String> {
#[reducer]
pub fn set_ready(ctx: &ReducerContext, ready: bool) -> Result<(), String> {
let mut player = ctx.db.player().identity().find(ctx.sender()).unwrap();
player.ready = ready;
player = ctx.db.player().identity().update(player);
let mut user = ctx.db.user().identity().find(ctx.sender()).unwrap();
let mut player = ctx.db.player_config().id().find(user.config_id).unwrap();
if let Some(mut lobby) = ctx.db.lobby().id().find(player.lobby_id)
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
&& ctx.db.player().lobby_id().filter(lobby.id).all(|p| p.ready)
&& 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;
lobby.players.shuffle(&mut ctx.rng());
let lobby = ctx.db.lobby().id().update(lobby);
// TODO should we schedule this outside so that we can clear out stale lobbies?
let game_timer = ctx.db.game_timer().insert(GameTimer {
id: 0,
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 {
// if lobby doesn't exist, reset player state
player.lobby_id = 0;
player.ready = false;
player = ctx.db.player().identity().update(player);
// TODO if lobby doesn't exist, reset player state
return Err(format!("couldn't find lobby with id: {}", player.lobby_id));
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

@ -1,13 +1,28 @@
use spacetimedb::{SpacetimeType, ViewContext, table, view};
use spacetimedb::{Identity, SpacetimeType, ViewContext, table, view};
use jong_types::{
PlayerOrBot,
states::{GameState, TurnState},
tiles::Tile,
};
use crate::reducers::advance_game_private;
#[table(accessor = user)]
#[table(accessor = logged_out_user)]
#[derive(Debug)]
pub struct User {
#[primary_key]
pub identity: Identity,
pub name: String,
#[index(btree)]
pub lobby_id: u32,
#[unique]
pub config_id: u32,
}
#[derive(Debug, Clone)]
#[table(accessor = lobby, public)]
pub struct Lobby {
@ -15,7 +30,7 @@ pub struct Lobby {
#[auto_inc]
pub id: u32,
pub players: Vec<PlayerOrBot>,
pub players: Vec<u32>,
pub dealer_idx: u8,
pub current_idx: u8,
@ -23,8 +38,16 @@ pub struct Lobby {
// pub open_hands: bool,
}
// #[table(accessor = lobby_state, public)]
// pub struct LobbyState {
// #[unique]
// lobby_id: u32,
// current_idx: u8,
// }
#[table(accessor = wall)]
pub struct DbWall {
pub struct Wall {
#[primary_key]
pub lobby_id: u32,
@ -41,56 +64,6 @@ pub struct DbTile {
pub tile: Tile,
}
#[table(accessor = player, public)]
#[table(accessor = logged_out_player)]
#[derive(Debug)]
pub struct Player {
#[unique]
#[auto_inc]
pub id: u32,
#[primary_key]
pub identity: spacetimedb::Identity,
pub name: Option<String>,
#[index(btree)]
pub lobby_id: u32,
pub ready: bool,
pub sort: bool,
}
#[table(accessor = player_clock, public)]
pub struct PlayerClock {
#[primary_key]
pub id: u32,
#[unique]
pub player_id: u32,
pub renewable: u16,
pub total: u16,
}
#[table(accessor = player_hand)]
pub struct PlayerHand {
#[primary_key]
#[auto_inc]
pub id: u32,
#[unique]
pub player_id: u32,
pub turn_state: TurnState,
pub pond: Vec<DbTile>,
pub hand: Vec<DbTile>,
/// drawn or callable tile
pub working_tile: Option<DbTile>,
}
#[table(accessor = bot, public)]
pub struct Bot {
#[primary_key]
@ -100,38 +73,68 @@ pub struct Bot {
#[index(btree)]
pub lobby_id: u32,
pub config_id: u32,
}
#[table(accessor = player_config, public)]
#[derive(Debug)]
pub struct PlayerConfig {
#[primary_key]
#[auto_inc]
pub id: u32,
// TODO randomly generate this from contributor names for bots
pub name: String,
pub ready: bool,
pub sort: bool,
}
#[table(accessor = player_clock, public)]
pub struct PlayerClock {
#[primary_key]
pub player_id: u32,
pub renewable: u16,
pub total: u16,
}
#[table(accessor = player_hand)]
pub struct PlayerHand {
#[primary_key]
pub player_id: u32,
pub turn_state: TurnState,
pub hand: Vec<DbTile>,
pub pond: Vec<DbTile>,
/// drawn or callable tile
pub working_tile: Option<DbTile>,
}
#[table(accessor = game_timer, scheduled(advance_game_private), public)]
pub struct GameTimer {
#[primary_key]
#[auto_inc]
pub id: u64,
pub struct LobbyTimer {
#[unique]
pub lobby_id: u32,
#[primary_key]
#[auto_inc]
pub scheduled_id: u64,
pub scheduled_at: spacetimedb::ScheduleAt,
}
#[view(accessor = view_hand, public)]
fn view_hand(ctx: &ViewContext) -> Option<PlayerHand> {
ctx.db
.player()
.user()
.identity()
.find(ctx.sender())
.and_then(|p| ctx.db.player_hand().player_id().find(p.id))
.and_then(|p| ctx.db.player_hand().player_id().find(p.config_id))
}
#[derive(SpacetimeType, Clone)]
pub struct HandView {
pub player: PlayerOrBot,
pub player_id: u32,
pub hand_length: u8,
// pub melds: u8,
pub pond: Vec<DbTile>,
@ -140,38 +143,23 @@ pub struct HandView {
#[view(accessor = view_closed_hands, public)]
fn view_closed_hands(ctx: &ViewContext) -> Vec<HandView> {
if let Some(this_player) = ctx.db.player().identity().find(ctx.sender())
if let Some(this_player) = ctx.db.user().identity().find(ctx.sender())
&& let Some(lobby) = ctx.db.lobby().id().find(this_player.lobby_id)
{
lobby
.players
.iter()
.filter_map(|&player| match player {
PlayerOrBot::Player { id } => {
if let Some(player_hand) = ctx.db.player_hand().player_id().find(id) {
Some(HandView {
player,
hand_length: player_hand.hand.len() as u8,
pond: player_hand.pond,
drawn: player_hand.turn_state == TurnState::Tsumo
&& player_hand.working_tile.is_some(),
.filter_map(|id| {
ctx.db
.player_hand()
.player_id()
.find(id)
.map(|hand| HandView {
player_id: hand.player_id,
hand_length: hand.hand.len() as u8,
pond: hand.pond,
drawn: hand.turn_state == TurnState::Tsumo && hand.working_tile.is_some(),
})
} else {
None
}
}
PlayerOrBot::Bot { id } => {
if let Some(bot) = ctx.db.bot().id().find(id) {
Some(HandView {
player,
hand_length: bot.hand.len() as u8,
pond: bot.pond,
drawn: bot.turn_state == TurnState::Tsumo && bot.working_tile.is_some(),
})
} else {
None
}
}
})
.collect()
} else {

View file

@ -17,9 +17,3 @@ pub mod tiles;
pub use states::*;
pub use tiles::*;
#[derive(Debug, ..Copy, ..Eq, Hash)]
#[derive(SpacetimeType)]
pub enum PlayerOrBot {
Player { id: u32 },
Bot { id: u32 },
}