/*
* This file is part of laurelin_client
* Copyright (C) 2023 Jonni Liljamo <jonni@liljamo.com>
*
* 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<K, V>,
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<GameStatus, ()> {
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) => {
$action.seed.parse::<u64>().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::<u64>().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::<u64>().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);
}