use std::time::Duration; use spacetimedb::{ReducerContext, ScheduleAt::Interval, reducer}; use jong_types::{GameState, TurnState}; use crate::{ reducers::deal::shuffle_deal, tables::{ GameTimer, PlayerClock, PlayerOrBot, bot, game_timer, lobby as _, player_clock, player_hand, wall, }, }; mod deal; mod hand; mod lobby; #[reducer] pub fn advance_game(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 !ctx.sender_auth().is_internal() { return Err("This reducer can only be called by the scheduler".to_string()); } if let Some(mut lobby) = ctx.db.lobby().id().find(game_timer.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 lobby.game_state = GameState::Deal; } GameState::Deal => { // TODO reduce interval beforehand so this can animate? shuffle_deal(ctx, lobby.id); } GameState::Play => { let curr_player = lobby.players.get(lobby.current_idx as usize).unwrap(); match curr_player { PlayerOrBot::Player { id: 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 => { // TODO draw 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 => { 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 } => { let b = ctx.db.bot().id().find(bot_id).unwrap(); } } } 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); } 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; } }