/* * This file is part of laurelin_client * Copyright (C) 2023 Jonni Liljamo * * Licensed under GPL-3.0-only. * See LICENSE for licensing information. */ use std::collections::HashMap; use fastrand::Rng; use crate::{ api::game::{Action, Command, Game}, game_status::SupplyPile, }; use super::{GameStatus, PlayerState, PlayerStatus}; /// funny unsafe wrapper fn get_invoker_target<'a, K, V>( players: &'a mut HashMap, invoker: &K, target: &K, ) -> (&'a mut V, &'a mut V) where K: Eq + std::hash::Hash, { unsafe { // NOTE: soo... I don't really know the consequences of possibly // having two mutable references to the same value, but I guess // I'll find out! // in many instances where people wanted multiple mutable references // to Vec or HashMap values, they only gave one in wrappers like this, // if the wanted values were the same. // e.g. returning (V, None), if the keys were the same. let invoker_ref = players.get_mut(invoker).unwrap() as *mut _; let target_ref = players.get_mut(target).unwrap() as *mut _; (&mut *invoker_ref, &mut *target_ref) } } pub fn parse(game: &Game) -> Result { let mut game_status = GameStatus { actions: game.actions.as_ref().unwrap().to_vec(), supply_piles: vec![], players: HashMap::new(), }; game_status.players.insert( game.host_id.clone(), PlayerStatus { turn_n: 0, display_name: game.host.as_ref().unwrap().username.clone(), state: PlayerState::Idle, plays: 0, buys: 0, currency: 0, vp: 2, hand: vec![], deck: vec![], discard: vec![], }, ); game_status.players.insert( game.guest_id.clone(), PlayerStatus { turn_n: 1, display_name: game.guest.as_ref().unwrap().username.clone(), state: PlayerState::Idle, plays: 0, buys: 0, currency: 0, vp: 2, hand: vec![], deck: vec![], discard: vec![], }, ); for action in game_status.actions.clone() { parse_action(&action, game, &mut game_status); } Ok(game_status) } macro_rules! current_seed { ($action:ident) => { Some($action.seed.parse::().unwrap()) }; } fn parse_action(action: &Action, game: &Game, game_status: &mut GameStatus) { // invoker: the one who invoked the action // target: the one who the action affects, may also be the invoker, e.g. draw let (invoker, target) = get_invoker_target(&mut game_status.players, &action.invoker, &action.target); let Some(action_pos) = game_status.actions.iter().position(|a| *a == action.clone()) else { panic!("Action was not found in game_status.actions!"); }; match &action.command { Command::InitSupplyPile { card, amount } => { let pile = SupplyPile { card: card.clone(), amount: *amount, }; game_status.supply_piles.push(pile); } Command::TakeFromPile { index, for_cost } => { // index should be within range assert!(*index <= game_status.supply_piles.len()); let pile = &mut game_status .supply_piles .get_mut(*index) .unwrap_or_else(|| unreachable!()); // pile should not be empty assert!(pile.amount > 0); // player should have buys assert!(target.buys > 0); // player should have enough assert!(*for_cost <= target.currency); pile.amount -= 1; target.buys -= 1; target.currency -= for_cost; target.discard.push(pile.card.clone()); } Command::PlayCard { index } => { // index should be within range assert!(*index <= target.hand.len()); // player should have plays assert!(target.plays > 0); target.plays -= 1; let card = target.hand.remove(*index); target.discard.push(card.clone()); for card_action in &card.actions { let action = &Action::new( &game.id, &action.invoker, &action.target, &card_action.command, current_seed!(action), ); game_status.actions.insert(action_pos + 1, action.clone()); parse_action(action, game, game_status); } } Command::Draw { amount } => { for _ in 0..*amount { if target.deck.is_empty() { shuffle_discard_to_deck(target, action.seed.parse::().unwrap()); } // NOTE: deck *might* still be empty, if discard was empty too if !target.deck.is_empty() { target .hand .push(target.deck.pop().unwrap_or_else(|| unreachable!())); } } } Command::Discard { index } => { // index should be within range assert!(*index <= target.hand.len()); target.discard.push(target.hand.remove(*index)); } Command::EndTurn {} => { // NOTE: target will be the next player // set player to idle invoker.state = PlayerState::Idle; // clear stats invoker.currency = 0; invoker.plays = 0; invoker.buys = 0; let start_turn_action = Action::new( &game.id, &action.invoker, &action.target, &Command::StartTurn {}, current_seed!(action), ); game_status .actions .insert(action_pos + 1, start_turn_action.clone()); parse_action(&start_turn_action, game, game_status); } Command::StartTurn {} => { // set the target to the play phase target.state = PlayerState::PlayPhase; // give a play and a buy at the start target.plays = 1; target.buys = 1; let draw_action = Action::new( &game.id, &action.target, &action.target, &Command::Draw { amount: 4 }, current_seed!(action), ); game_status .actions .insert(action_pos + 1, draw_action.clone()); parse_action(&draw_action, game, game_status); } Command::ChangePlayerState { state } => { target.state = *state; } Command::RollForCurrency { min, max } => { target.currency += Rng::with_seed(action.seed.parse::().unwrap()).usize(min..=max); } #[allow(unreachable_patterns)] _ => todo!(), } } fn shuffle_discard_to_deck(target: &mut PlayerStatus, seed: u64) { let cards = target.discard.to_vec(); target.discard.clear(); target.deck = cards; let rng = Rng::with_seed(seed); rng.shuffle(&mut target.deck); }