use std::time::Duration; use log::{debug, trace}; use spacetimedb::{ ReducerContext, ScheduleAt::Interval, Table as _, rand::seq::SliceRandom, reducer, }; use crate::tables::{ DbTile, DbWall, GameTimer, Lobby, PlayerClock, PlayerHand, bot, game_timer, lobby as _, player, player_clock, player_hand, tile as _, wall, }; use jong_types::{GameState, PlayerOrBot, TurnState}; mod hand; mod lobby; #[reducer] pub fn advance_game(ctx: &ReducerContext, mut game_timer: GameTimer) -> Result<(), String> { advance_game_private(ctx, game_timer) } #[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"); let tiles = { let mut rng = ctx.rng(); let mut wall: Vec<_> = jong_types::tiles::tiles() .into_iter() .map(|tile| ctx.db.tile().insert(DbTile { id: 0, tile })) .collect(); wall.shuffle(&mut rng); wall }; ctx.db.wall().insert(DbWall { // id: 0, lobby_id: lobby.id, tiles, }); lobby.game_state = GameState::Deal; } GameState::Deal => { // TODO reduce interval beforehand so this can animate? // TODO change loop to be per interval somehow? trace!("deal hands"); let mut wall = ctx.db.wall().lobby_id().find(lobby.id).unwrap(); for pob in &lobby.players { let mut tiles = wall.tiles.split_off(wall.tiles.len() - 13); wall = ctx.db.wall().lobby_id().update(wall); tiles.sort_by_key(|t| t.tile); match pob { PlayerOrBot::Player { id } if let Some(p) = ctx.db.player().id().find(id) => { ctx.db.player_hand().insert(PlayerHand { id: 0, player_id: p.id, turn_state: jong_types::TurnState::None, pond: vec![], hand: tiles, working_tile: None, }); ctx.db.player_clock().insert(PlayerClock { id: 0, player_id: p.id, renewable: 5, total: 30, }); } PlayerOrBot::Bot { id } if let Some(mut b) = ctx.db.bot().id().find(id) => { b.hand = tiles; ctx.db.bot().id().update(b); } _ => Err("couldn't find player or bot".to_string())?, } } lobby.game_state = jong_types::states::GameState::Play; trace!("dealt hands"); } 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 => {} } } 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); // 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 handle stale lobbies // TODO should this delete the timer? _ => Err(format!("lobby {} in impossible state", lobby.id))?, } // ctx.db.game_timer().id().update(game_timer); ctx.db.lobby().id().update(lobby); } else { ctx.db.game_timer().id().delete(game_timer.id); Err(format!( "ran schedule {} for empty lobby {}", game_timer.id, game_timer.lobby_id ))?; } Ok(()) } impl PlayerClock { fn tick(&mut self) -> bool { if self.renewable > 0 { self.renewable -= 1; true } else if self.total > 0 { self.total -= 1; true } else { false } } fn renew(&mut self) { self.renewable = 5; } } impl Lobby { fn next_player(&mut self) { if self.current_idx + 1 >= 4 { self.current_idx = 0 } else { self.current_idx += 1; } } }