/* * 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, util::action_to_log, }; use super::{GameStatus, LogEntry, LogSection, PlayerState, PlayerStatus}; /// funny unsafe wrapper fn get_invoker_target_next<'a>( players: &'a mut HashMap, invoker: &String, target: &String, ) -> (&'a mut PlayerStatus, &'a mut PlayerStatus, String) { 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: *mut PlayerStatus = players.get_mut(invoker).unwrap() as *mut _; let target_ref: *mut PlayerStatus = players.get_mut(target).unwrap() as *mut _; let next_turn_n: usize = if ((*invoker_ref).turn_n + 1) > (players.len() - 1) { 0 } else { (*invoker_ref).turn_n + 1 }; let next_player = players .iter() .find(|np| np.1.turn_n == next_turn_n) .unwrap(); (&mut *invoker_ref, &mut *target_ref, next_player.0.clone()) } } pub fn parse(game: &Game) -> Result { let mut game_status = GameStatus { log: vec![], 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); } // TODO: check for end conditions, declare one player as winner. // update game state in API to ended, set ended date (in API). 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) { game_status.log.push(action_to_log(action, game_status)); // 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, next_player_uuid) = get_invoker_target_next(&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); if card.to_be_trashed { // marked for trash, let it fall into oblivion } else { // discard normally target.discard.push(card.clone()); } for card_action in &card.actions { let action = &Action::new( &game.id, &action.invoker, if card_action.target_self { &action.invoker } else { &next_player_uuid }, &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: 2 }, 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 { amount, sides } => { let mut results: Vec = Vec::with_capacity(*amount); for _ in 0..*amount { let result = Rng::with_seed(action.seed.parse::().unwrap()).usize(1..=*sides); target.currency += result; results.push(result); } if *amount == 1 { game_status .log .push(LogEntry::from_sections([LogSection::bold( &results.first().unwrap().to_string(), )])); } else { game_status .log .push(LogEntry::from_sections([LogSection::bold(&format!( " {} = {}", results .iter() .map(ToString::to_string) .collect::>() .join(" "), results.iter().sum::(), ))])); } } Command::RollForCurrencyAdvanced { amount, sides, pairs } => { let mut results: Vec = Vec::with_capacity(*amount); for _ in 0..*amount { let result = Rng::with_seed(action.seed.parse::().unwrap()).usize(1..=*sides); let mut last = 0; for (pos, res) in pairs { if (last..=*pos).contains(&result) { target.currency += res; } last = *pos; } results.push(result); } if *amount == 1 { game_status .log .push(LogEntry::from_sections([LogSection::bold( &results.first().unwrap().to_string(), )])); } else { game_status .log .push(LogEntry::from_sections([LogSection::bold(&format!( " {} = {}", results .iter() .map(ToString::to_string) .collect::>() .join(" "), results.iter().sum::(), ))])); } } Command::GivePlays { amount } => { target.plays += amount; } Command::RollForPlays { amount, sides } => { let mut results: Vec = Vec::with_capacity(*amount); for _ in 0..*amount { let result = Rng::with_seed(action.seed.parse::().unwrap()).usize(1..=*sides); target.plays += result; results.push(result); } if *amount == 1 { game_status .log .push(LogEntry::from_sections([LogSection::bold( &results.first().unwrap().to_string(), )])); } else { game_status .log .push(LogEntry::from_sections([LogSection::bold(&format!( " {} = {}", results .iter() .map(ToString::to_string) .collect::>() .join(" "), results.iter().sum::(), ))])); } } Command::GiveBuys { amount } => { target.buys += amount; } Command::GiveVP { amount } => { target.vp += amount; } Command::MarkCardInDeckToBeTrashed { index } => { if target.deck.is_empty() { return; } match index { Some(index) => { target.deck.get_mut(*index).unwrap().to_be_trashed = true; } None => { let deck_len = target.deck.len(); target .deck .get_mut( Rng::with_seed(action.seed.parse::().unwrap()).usize(0..deck_len), ) .unwrap() .to_be_trashed = true; } } } #[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); }