use std::time::Duration; use spacetimedb::{ ReducerContext, ScheduleAt::Interval, Table as _, rand::seq::SliceRandom, reducer, }; use jong_types::{GameState, TurnState}; use crate::{ reducers::deal::{deal_hands, new_shuffled_wall, shuffle_deal}, tables::{ DbTile, DbWall, GameTimer, PlayerClock, PlayerHand, PlayerOrBot, bot, game_timer, lobby as _, player, player_clock, player_hand, tile as _, 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 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? 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, }); } 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; } 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); 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; } }