diff --git a/jong-line/src/lib.rs b/jong-line/src/lib.rs index 2161226..07c9b33 100644 --- a/jong-line/src/lib.rs +++ b/jong-line/src/lib.rs @@ -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(()) } diff --git a/jong-line/src/reducers.rs b/jong-line/src/reducers.rs index 2328667..3b30ab1 100644 --- a/jong-line/src/reducers.rs +++ b/jong-line/src/reducers.rs @@ -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![], - hand: tiles, - working_tile: None, - }); - ctx.db.player_clock().insert(PlayerClock { - id: 0, - player_id: p.id, - renewable: 5, - total: 30, - }); - } - PlayerOrBot::Bot { id } if let Some(mut b) = ctx.db.bot().id().find(id) => { - b.hand = tiles; - ctx.db.bot().id().update(b); - } - _ => Err("couldn't find player or bot".to_string())?, - } + ctx.db.player_hand().insert(PlayerHand { + player_id: *player, + turn_state: TurnState::None, + hand: tiles, + pond: vec![], + working_tile: None, + }); } 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,62 +115,44 @@ 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(); - match hand.turn_state { - TurnState::None => { - // trace!("draw a tile"); - if let Some(mut wall) = ctx.db.wall().lobby_id().find(lobby.id) - && let Some(tile) = wall.tiles.pop() - { - hand.working_tile = Some(tile); - hand.turn_state = TurnState::Tsumo; - ctx.db.wall().lobby_id().update(wall); - ctx.db.player_hand().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); - } else { - // TODO auto-discard - } - } - TurnState::Menzen => {} - TurnState::RiichiKan => {} - TurnState::RonChiiPonKan => {} - TurnState::End => {} + let mut hand = ctx.db.player_hand().player_id().find(curr_player).unwrap(); + match hand.turn_state { + TurnState::None => { + // trace!("draw a tile"); + if let Some(mut wall) = ctx.db.wall().lobby_id().find(lobby.id) + && let Some(tile) = wall.tiles.pop() + { + hand.working_tile = Some(tile); + hand.turn_state = TurnState::Tsumo; + ctx.db.wall().lobby_id().update(wall); + ctx.db.player_hand().player_id().update(hand); + } else { + // TODO out of tiles + todo!() } } - 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!(), - _ => {} + TurnState::Tsumo => { + // 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 bot / auto discard } - lobby.next_player(); } + TurnState::Menzen => {} + TurnState::RiichiKan => {} + TurnState::RonChiiPonKan => {} + TurnState::End => {} } } 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 ))?; } diff --git a/jong-line/src/reducers/hand.rs b/jong-line/src/reducers/hand.rs index da67516..936808d 100644 --- a/jong-line/src/reducers/hand.rs +++ b/jong-line/src/reducers/hand.rs @@ -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 { - // dealt from drawn tile - dealt - } else if let Some((i, _)) = hand.hand.iter().enumerate().find(|(_, t)| t.id == tile_id) - { - // dealt from hand - let dealt = hand.hand.remove(i); - hand.hand.push(drawn); - hand.hand.sort_by_key(|t| t.tile); + let dealt = ctx.db.tile().id().find(tile_id).unwrap(); + let drawn = hand.working_tile.unwrap(); - 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 - )); + let dealt_tile = if dealt.id == drawn.id { + // dealt from drawn tile + dealt + } 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 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(()) } diff --git a/jong-line/src/reducers/lobby.rs b/jong-line/src/reducers/lobby.rs index 61f165f..7bb9a7c 100644 --- a/jong-line/src/reducers/lobby.rs +++ b/jong-line/src/reducers/lobby.rs @@ -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(()) diff --git a/jong-line/src/tables.rs b/jong-line/src/tables.rs index a8664aa..11a0611 100644 --- a/jong-line/src/tables.rs +++ b/jong-line/src/tables.rs @@ -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, + pub players: Vec, 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, - - #[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, - pub hand: Vec, - - /// drawn or callable tile - pub working_tile: Option, -} - #[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, pub pond: Vec, + /// drawn or callable tile pub working_tile: Option, } #[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 { 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, @@ -140,38 +143,23 @@ pub struct HandView { #[view(accessor = view_closed_hands, public)] fn view_closed_hands(ctx: &ViewContext) -> Vec { - 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(), - }) - } 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 - } - } + .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(), + }) }) .collect() } else { diff --git a/jong-types/src/lib.rs b/jong-types/src/lib.rs index 6eecb7e..9932bee 100644 --- a/jong-types/src/lib.rs +++ b/jong-types/src/lib.rs @@ -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 }, -}