diff --git a/client/src/advance_ui.rs b/client/src/advance_ui.rs index 05fb9584..db0acbdc 100644 --- a/client/src/advance_ui.rs +++ b/client/src/advance_ui.rs @@ -30,9 +30,9 @@ pub enum AdvanceState { fn new_advance_payment(rc: &RenderContext, name: &str) -> Payment { let p = rc.shown_player; - let model = &p.advance_cost(name); + let options = &p.advance_cost(name); let available = &p.resources; - Payment::new(model, available, name, false) + Payment::new(options, available, name, false) } pub fn show_paid_advance_menu(rc: &RenderContext) -> StateUpdate { diff --git a/client/src/client_state.rs b/client/src/client_state.rs index 62c32780..f50c64f0 100644 --- a/client/src/client_state.rs +++ b/client/src/client_state.rs @@ -509,7 +509,7 @@ impl State { r.iter() .map(|p| { Payment::new( - &p.model, + &p.options, &game.get_player(game.active_player()).resources, &p.name, p.optional, @@ -517,9 +517,9 @@ impl State { }) .collect(), ), - CustomPhaseRequest::Reward(r) => ActiveDialog::CustomPhaseRewardRequest( - Payment::new_gain(r.model.clone(), &r.name), - ), + CustomPhaseRequest::Reward(r) => { + ActiveDialog::CustomPhaseRewardRequest(Payment::new_gain(&r.options, &r.name)) + } }; } match &game.state { diff --git a/client/src/construct_ui.rs b/client/src/construct_ui.rs index bc98b72f..0ed505c0 100644 --- a/client/src/construct_ui.rs +++ b/client/src/construct_ui.rs @@ -7,7 +7,7 @@ use server::city::City; use server::city_pieces::Building; use server::content::custom_actions::CustomAction; use server::map::Terrain; -use server::payment::PaymentModel; +use server::payment::PaymentOptions; use server::playing_actions::{Construct, PlayingAction, Recruit}; use server::position::Position; use server::unit::UnitType; @@ -113,7 +113,7 @@ impl ConstructionPayment { .unwrap() .cost .clone(), - ConstructionProject::Units(sel) => PaymentModel::resources( + ConstructionProject::Units(sel) => PaymentOptions::resources( sel.amount .units .clone() diff --git a/client/src/happiness_ui.rs b/client/src/happiness_ui.rs index bcea4ef9..107e169e 100644 --- a/client/src/happiness_ui.rs +++ b/client/src/happiness_ui.rs @@ -34,7 +34,10 @@ impl IncreaseHappinessConfig { let city = p.get_city(*pos).unwrap(); p.increase_happiness_cost(city, *steps).unwrap() }) - .reduce(|a, b| a + b) + .reduce(|mut a, b| { + a.default += b.default; + a + }) .unwrap(); let available = &p.resources; diff --git a/client/src/payment_ui.rs b/client/src/payment_ui.rs index 13e3d5d0..1e56d152 100644 --- a/client/src/payment_ui.rs +++ b/client/src/payment_ui.rs @@ -6,11 +6,9 @@ use crate::resource_ui::{new_resource_map, resource_name}; use crate::select_ui; use crate::select_ui::{CountSelector, HasCountSelectableObject}; use macroquad::math::{bool, vec2}; -use server::payment::{PaymentModel, SumPaymentOptions}; +use server::payment::PaymentOptions; use server::resource::ResourceType; -use server::resource_pile::{PaymentOptions, ResourcePile}; -use std::cmp; -use std::cmp::min; +use server::resource_pile::ResourcePile; #[derive(PartialEq, Eq, Debug, Clone)] pub struct ResourcePayment { @@ -36,63 +34,32 @@ impl HasCountSelectableObject for ResourcePayment { #[derive(PartialEq, Eq, Debug, Clone)] pub struct Payment { pub name: String, - pub model: PaymentModel, + pub options: PaymentOptions, pub available: ResourcePile, pub optional: bool, pub current: Vec, - pub discount_used: u32, } impl Payment { #[must_use] pub fn new( - model: &PaymentModel, + model: &PaymentOptions, available: &ResourcePile, name: &str, optional: bool, ) -> Payment { - let mut discount_used = 0; - let resources = match model { - PaymentModel::Sum(options) => sum_payment(options, available), - PaymentModel::Resources(a) => { - let options = a.get_payment_options(available); - discount_used = a.discount - options.discount_left; - resource_payment(&options) - } - }; - Self { name: name.to_string(), - model: model.clone(), + options: model.clone(), available: available.clone(), optional, - current: resources, - discount_used, + current: resource_payment(model, available), } } #[must_use] - pub fn new_gain(model: PaymentModel, name: &str) -> Payment { - let sum = if let PaymentModel::Sum(m) = &model { - m.clone() - } else { - panic!("No trade route reward") - }; - let available = sum - .types_by_preference - .iter() - .map(|t| ResourcePile::of(*t, sum.cost)) - .reduce(|a, b| a + b) - .expect("sum"); - - Payment { - name: name.to_string(), - model, - optional: false, - current: sum_payment(&sum, &available), - available, - discount_used: 0, - } + pub fn new_gain(options: &PaymentOptions, name: &str) -> Payment { + Self::new(options, &options.default, name, false) } pub fn to_resource_pile(&self) -> ResourcePile { @@ -157,12 +124,12 @@ pub fn multi_payment_dialog( for (i, payment) in payments.iter().enumerate() { let name = &payment.name; - let model = payment.model.clone(); - let types = show_types(&model); + let options = payment.options.clone(); + let types = options.possible_resource_types(); let offset = vec2(0., i as f32 * -100.); bottom_centered_text_with_offset( rc, - &format!("{name} for {model}"), + &format!("{name} for {options}"), offset + vec2(0., -30.), ); let result = select_ui::count_dialog( @@ -218,17 +185,17 @@ fn ok_tooltip(payments: &[Payment], mut available: ResourcePile) -> OkTooltip { let mut invalid: Vec = vec![]; for payment in payments { - let model = &payment.model; + let options = &payment.options; let pile = payment.to_resource_pile(); let name = &payment.name; let tooltip = if payment.optional && pile.is_empty() { OkTooltip::Valid(format!("Pay nothing for {name}")) - } else if model.can_afford(&available) && model.is_valid_payment(&pile) { + } else if options.can_afford(&available) && options.is_valid_payment(&pile) { // make sure that we can afford all the payments available -= payment.to_resource_pile(); OkTooltip::Valid(format!("Pay {pile} for {name}")) } else { - OkTooltip::Invalid(format!("You don't have {:?} for {}", payment.model, name)) + OkTooltip::Invalid(format!("You don't have {:?} for {}", payment.options, name)) }; match tooltip { OkTooltip::Valid(v) => valid.push(v), @@ -255,46 +222,18 @@ fn replace_updated_payment(payment: &Payment, all: &[Payment]) -> Vec { .collect::>() } -fn sum_payment(a: &SumPaymentOptions, available: &ResourcePile) -> Vec { - let mut cost_left = a.cost; - - a.types_by_preference - .iter() - .map(|t| { - let have = available.get(*t); - let used = min(have, cost_left); - cost_left -= used; - ResourcePayment::new(*t, used, 0, have) - }) - .collect() -} - #[must_use] -fn resource_payment(options: &PaymentOptions) -> Vec { +fn resource_payment(options: &PaymentOptions, available: &ResourcePile) -> Vec { let mut resources: Vec = new_resource_map(&options.default) .into_iter() .map(|e| { let resource_type = e.0; - let amount = e.1; - match resource_type { - ResourceType::Gold => ResourcePayment { - resource: resource_type, - selectable: CountSelector { - current: amount, - min: amount, - max: amount, - }, - }, - _ => ResourcePayment { - resource: resource_type, - selectable: CountSelector { - current: amount, - min: cmp::max( - 0, - amount as i32 - options.discount_left as i32 - options.gold_left as i32, - ) as u32, - max: amount, - }, + ResourcePayment { + resource: resource_type, + selectable: CountSelector { + current: e.1, + min: 0, + max: available.get(&resource_type), }, } }) @@ -304,49 +243,12 @@ fn resource_payment(options: &PaymentOptions) -> Vec { resources } -#[must_use] -pub fn show_types(model: &PaymentModel) -> Vec { - match model { - PaymentModel::Sum(options) => options.types_by_preference.clone(), - PaymentModel::Resources(options) => options.cost.types(), - } -} - pub fn plus(mut payment: Payment, t: ResourceType) -> Payment { - match payment.model { - PaymentModel::Sum(_) => { - payment.get_mut(t).selectable.current += 1; - } - PaymentModel::Resources(_) => { - { - let gold = payment.get_mut(ResourceType::Gold); - if gold.selectable.current > 0 { - gold.selectable.current -= 1; - } else { - payment.discount_used += 1; - } - } - payment.get_mut(t).selectable.current += 1; - } - } + payment.get_mut(t).selectable.current += 1; payment } pub fn minus(mut payment: Payment, t: ResourceType) -> Payment { - match payment.model { - PaymentModel::Sum(_) => { - payment.get_mut(t).selectable.current -= 1; - } - PaymentModel::Resources(_) => { - { - if payment.discount_used > 0 { - payment.discount_used -= 1; - } else { - payment.get_mut(ResourceType::Gold).selectable.current += 1; - } - } - payment.get_mut(t).selectable.current -= 1; - } - } + payment.get_mut(t).selectable.current -= 1; payment } diff --git a/server/src/ability_initializer.rs b/server/src/ability_initializer.rs index e2379eb1..1de0defe 100644 --- a/server/src/ability_initializer.rs +++ b/server/src/ability_initializer.rs @@ -193,7 +193,7 @@ pub(crate) trait AbilityInitializerSetup: Sized { for (request, payment) in requests.iter().zip(payments.iter()) { let zero_payment = payment.is_empty() && request.optional; assert!( - zero_payment || request.model.is_valid_payment(payment), + zero_payment || request.options.is_valid_payment(payment), "Invalid payment" ); game.players[player_index].loose_resources(payment.clone()); @@ -226,13 +226,13 @@ pub(crate) trait AbilityInitializerSetup: Sized { move |game, player_index, _player_name| { let req = request(game, player_index); if let Some(r) = &req { - if r.model.possible_resource_types().len() == 1 { + if r.options.possible_resource_types().len() == 1 { let player_name = game.players[player_index].get_name(); g( game, player_index, &player_name, - &r.model.default_payment(), + &r.options.default_payment(), false, ); return None; @@ -243,7 +243,7 @@ pub(crate) trait AbilityInitializerSetup: Sized { move |game, player_index, player_name, action, request| { if let CustomPhaseRequest::Reward(request) = &request { if let CustomPhaseEventAction::Reward(reward) = action { - assert!(request.model.is_valid_payment(&reward), "Invalid payment"); + assert!(request.options.is_valid_payment(&reward), "Invalid payment"); gain_reward(game, player_index, player_name, &reward, true); return; } diff --git a/server/src/content/advances.rs b/server/src/content/advances.rs index a4a3f43f..f523fc99 100644 --- a/server/src/content/advances.rs +++ b/server/src/content/advances.rs @@ -8,7 +8,7 @@ use crate::combat::{Combat, CombatModifier, CombatStrength}; use crate::content::custom_phase_actions::{CustomPhasePaymentRequest, CustomPhaseRewardRequest}; use crate::content::trade_routes::{gain_trade_route_reward, trade_route_reward}; use crate::game::GameState; -use crate::payment::PaymentModel; +use crate::payment::{PaymentConversion, PaymentOptions}; use crate::playing_actions::{PlayingAction, PlayingActionType}; use crate::position::Position; use crate::resource::ResourceType; @@ -27,7 +27,6 @@ pub const NAVIGATION: &str = "Navigation"; pub const ROADS: &str = "Roads"; pub const STEEL_WEAPONS: &str = "Steel Weapons"; pub const METALLURGY: &str = "Metallurgy"; -pub const RITUALS: &str = "Rituals"; pub const TACTICS: &str = "Tactics"; pub const BARTERING: &str = "Bartering"; pub const CURRENCY: &str = "Currency"; @@ -304,8 +303,8 @@ fn warfare() -> Vec { |game, player_index| { let GameState::Combat(c) = &game.state else { panic!("Invalid state") }; - let extra_die = PaymentModel::sum(2, vec![ResourceType::Wood, ResourceType::Gold]); - let ignore_hit = PaymentModel::sum(2, vec![ResourceType::Ore, ResourceType::Gold]); + let extra_die = PaymentOptions::sum(2, &[ResourceType::Wood, ResourceType::Gold]); + let ignore_hit = PaymentOptions::sum(2, &[ResourceType::Ore, ResourceType::Gold]); let player = &game.players[player_index]; if game @@ -315,12 +314,12 @@ fn warfare() -> Vec { { Some(vec![ CustomPhasePaymentRequest { - model: extra_die, + options: extra_die, name: "Cancel fortress ability to add an extra die in the first round of combat".to_string(), optional: true, }, CustomPhasePaymentRequest { - model: ignore_hit, + options: ignore_hit, name: "Cancel fortress ability to ignore the first hit in the first round of combat".to_string(), optional: true, }, @@ -374,7 +373,7 @@ fn warfare() -> Vec { if player.can_afford(&cost) { Some(vec![CustomPhasePaymentRequest { - model: cost, + options: cost, name: "Use steel weapons".to_string(), optional: true, }]) @@ -407,14 +406,14 @@ fn add_steel_weapons(player_index: usize, c: &mut Combat) { } #[must_use] -fn steel_weapons_cost(game: &Game, combat: &Combat, player_index: usize) -> PaymentModel { +fn steel_weapons_cost(game: &Game, combat: &Combat, player_index: usize) -> PaymentOptions { let player = &game.players[player_index]; let attacker = &game.players[combat.attacker]; let defender = &game.players[combat.defender]; let both_steel_weapons = attacker.has_advance(STEEL_WEAPONS) && defender.has_advance(STEEL_WEAPONS); let cost = u32::from(!player.has_advance(METALLURGY) || both_steel_weapons); - PaymentModel::sum(cost, vec![ResourceType::Ore, ResourceType::Gold]) + PaymentOptions::sum(cost, &[ResourceType::Ore, ResourceType::Gold]) } fn fortress(s: &mut CombatStrength, c: &Combat, game: &Game) { @@ -464,8 +463,23 @@ fn spirituality() -> Vec { Advance::builder("Myths", "not implemented") .with_advance_bonus(MoodToken) .with_unlocked_building(Temple), - Advance::builder(RITUALS, "When you perform the Increase Happiness Action you may spend any Resources as a substitute for mood tokens. This is done at a 1:1 ratio") - .with_advance_bonus(CultureToken), + Advance::builder("Rituals", "When you perform the Increase Happiness Action you may spend any Resources as a substitute for mood tokens. This is done at a 1:1 ratio") + .with_advance_bonus(CultureToken) + .add_player_event_listener( + |event| &mut event.happiness_cost, + |cost, (), ()| { + for r in &[ + ResourceType::Food, + ResourceType::Wood, + ResourceType::Ore, + ResourceType::Ideas, + ResourceType::Gold, + ] { + cost.conversions.push(PaymentConversion::unlimited(vec![ResourcePile::mood_tokens(1)],ResourcePile::of(*r, 1))); + } + }, + 0, + ), ], ) } @@ -499,7 +513,7 @@ fn trade_routes() -> AdvanceBuilder { |game, _player_index| { trade_route_reward(game).map(|(reward, _routes)| { CustomPhaseRewardRequest { - model: reward, + options: reward, name: "Collect trade routes reward".to_string(), } }) diff --git a/server/src/content/custom_phase_actions.rs b/server/src/content/custom_phase_actions.rs index 17000257..ac1772bc 100644 --- a/server/src/content/custom_phase_actions.rs +++ b/server/src/content/custom_phase_actions.rs @@ -1,6 +1,6 @@ use crate::ability_initializer::EventOrigin; use crate::game::{Game, UndoContext}; -use crate::payment::PaymentModel; +use crate::payment::PaymentOptions; use crate::resource_pile::ResourcePile; use serde::{Deserialize, Serialize}; @@ -20,14 +20,14 @@ impl CustomPhaseEventType { #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub struct CustomPhasePaymentRequest { - pub model: PaymentModel, + pub options: PaymentOptions, pub name: String, pub optional: bool, } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] pub struct CustomPhaseRewardRequest { - pub model: PaymentModel, + pub options: PaymentOptions, pub name: String, } diff --git a/server/src/content/trade_routes.rs b/server/src/content/trade_routes.rs index 9adfe3b8..56393065 100644 --- a/server/src/content/trade_routes.rs +++ b/server/src/content/trade_routes.rs @@ -1,7 +1,7 @@ use crate::city::{City, MoodState}; use crate::content::advances::CURRENCY; use crate::game::Game; -use crate::payment::PaymentModel; +use crate::payment::PaymentOptions; use crate::player::Player; use crate::position::Position; use crate::resource::ResourceType; @@ -16,7 +16,7 @@ pub struct TradeRoute { } #[must_use] -pub fn trade_route_reward(game: &Game) -> Option<(PaymentModel, Vec)> { +pub fn trade_route_reward(game: &Game) -> Option<(PaymentOptions, Vec)> { let p = game.current_player_index; let trade_routes = find_trade_routes(game, &game.players[p]); if trade_routes.is_empty() { @@ -25,12 +25,12 @@ pub fn trade_route_reward(game: &Game) -> Option<(PaymentModel, Vec) Some(( if game.players[p].has_advance(CURRENCY) { - PaymentModel::sum( + PaymentOptions::sum( trade_routes.len() as u32, - vec![ResourceType::Gold, ResourceType::Food], + &[ResourceType::Gold, ResourceType::Food], ) } else { - PaymentModel::sum(trade_routes.len() as u32, vec![ResourceType::Food]) + PaymentOptions::sum(trade_routes.len() as u32, &[ResourceType::Food]) }, trade_routes, )) diff --git a/server/src/content/wonders.rs b/server/src/content/wonders.rs index 0362f4cf..b60c63c4 100644 --- a/server/src/content/wonders.rs +++ b/server/src/content/wonders.rs @@ -2,7 +2,7 @@ use crate::ability_initializer::AbilityInitializerSetup; use crate::content::advances::IRRIGATION; use crate::game::Game; use crate::map::Terrain::Fertile; -use crate::payment::PaymentModel; +use crate::payment::PaymentOptions; use crate::position::Position; use crate::{resource_pile::ResourcePile, wonder::Wonder}; use std::collections::HashSet; @@ -14,7 +14,7 @@ pub fn get_all() -> Vec { Wonder::builder( "Pyramids", "todo", - PaymentModel::resources_with_discount(ResourcePile::new(3, 3, 3, 0, 0, 0, 4), 1), + PaymentOptions::resources_with_discount(ResourcePile::new(3, 3, 3, 0, 0, 0, 4), 1), vec![], ) .build(), @@ -22,7 +22,7 @@ pub fn get_all() -> Vec { Wonder::builder( "Great Gardens", "The city with this wonder may Collect any type of resource from Grassland spaces including ideas and gold.", - PaymentModel::resources_with_discount(ResourcePile::new(5, 5, 2, 0, 0, 0, 5), 0), + PaymentOptions::resources(ResourcePile::new(5, 5, 2, 0, 0, 0, 5)), vec![IRRIGATION], ) .add_player_event_listener( diff --git a/server/src/game.rs b/server/src/game.rs index 648edadd..670dc00d 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -1859,7 +1859,7 @@ pub mod tests { use super::{Game, GameState::Playing}; use crate::content::custom_phase_actions::CustomPhaseEventState; - use crate::payment::PaymentModel; + use crate::payment::PaymentOptions; use crate::utils::tests::FloatEq; use crate::{ city::{City, MoodState::*}, @@ -1909,7 +1909,7 @@ pub mod tests { let old = Player::new(civilizations::tests::get_test_civilization(), 0); let new = Player::new(civilizations::tests::get_test_civilization(), 1); - let wonder = Wonder::builder("wonder", "test", PaymentModel::free(), vec![]).build(); + let wonder = Wonder::builder("wonder", "test", PaymentOptions::free(), vec![]).build(); let mut game = test_game(); game.players.push(old); game.players.push(new); diff --git a/server/src/payment.rs b/server/src/payment.rs index d51f67b3..a1e36e07 100644 --- a/server/src/payment.rs +++ b/server/src/payment.rs @@ -1,79 +1,116 @@ use crate::resource::ResourceType; -use crate::resource_pile::{CostWithDiscount, ResourcePile}; +use crate::resource_pile::ResourcePile; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use std::fmt::Display; -use std::ops::{Add, SubAssign}; #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] -pub struct SumPaymentOptions { - pub cost: u32, - pub types_by_preference: Vec, +pub struct PaymentConversion { + pub from: Vec, // alternatives + pub to: ResourcePile, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, } -impl SumPaymentOptions { +impl PaymentConversion { #[must_use] - pub fn is_valid_payment(&self, payment: &ResourcePile) -> bool { - self.types_by_preference - .iter() - .map(|t| payment.get(*t)) - .sum::() - == self.cost + pub fn unlimited(from: Vec, to: ResourcePile) -> Self { + PaymentConversion { + from, + to, + limit: None, + } } } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)] -pub enum PaymentModel { - Sum(SumPaymentOptions), - Resources(CostWithDiscount), +pub struct PaymentOptions { + pub default: ResourcePile, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + pub conversions: Vec, } -impl PaymentModel { +impl PaymentOptions { #[must_use] - pub const fn free() -> Self { - Self::resources(ResourcePile::empty()) + pub fn first_valid_payment(&self, available: &ResourcePile) -> Option { + let discount_left = self + .conversions + .iter() + .filter_map(|c| if c.to.is_empty() { c.limit } else { None }) + .sum::(); + if discount_left == 0 && available.has_at_least(&self.default, 1) { + return Some(self.default.clone()); + } + + self.conversions + .iter() + .permutations(self.conversions.len()) + .find_map(|conversions| { + can_convert(available, &self.default, &conversions, 0, discount_left) + }) } #[must_use] - pub const fn sum(cost: u32, types_by_preference: Vec) -> Self { - PaymentModel::Sum(SumPaymentOptions { - cost, - types_by_preference, - }) + pub fn is_valid_payment(&self, payment: &ResourcePile) -> bool { + self.first_valid_payment(payment).is_some() } #[must_use] - pub const fn resources_with_discount(cost: ResourcePile, discount: u32) -> Self { - PaymentModel::Resources(CostWithDiscount { cost, discount }) + pub fn free() -> Self { + Self::resources(ResourcePile::empty()) } + #[must_use] - pub const fn resources(cost: ResourcePile) -> Self { - Self::resources_with_discount(cost, 0) + pub fn sum(cost: u32, types_by_preference: &[ResourceType]) -> Self { + let mut conversions = vec![]; + types_by_preference.windows(2).for_each(|pair| { + conversions.push(PaymentConversion::unlimited( + vec![ResourcePile::of(pair[0], 1)], + ResourcePile::of(pair[1], 1), + )); + }); + PaymentOptions { + default: ResourcePile::of(types_by_preference[0], cost), + conversions, + } } #[must_use] - pub fn can_afford(&self, available: &ResourcePile) -> bool { - match self { - PaymentModel::Sum(options) => { - options - .types_by_preference - .iter() - .map(|t| available.get(*t)) - .sum::() - >= options.cost - } - PaymentModel::Resources(c) => c.can_afford(available), + pub fn resources_with_discount(cost: ResourcePile, discount: u32) -> Self { + let base_resources = vec![ + ResourcePile::food(1), + ResourcePile::wood(1), + ResourcePile::ore(1), + ResourcePile::ideas(1), + ]; + let mut conversions = vec![PaymentConversion { + from: base_resources.clone(), + to: ResourcePile::gold(1), + limit: None, + }]; + if discount > 0 { + conversions.push(PaymentConversion { + from: base_resources.clone(), + to: ResourcePile::empty(), + limit: Some(discount), + }); + } + PaymentOptions { + default: cost, + conversions, } } #[must_use] - pub fn is_valid_payment(&self, payment: &ResourcePile) -> bool { - match self { - PaymentModel::Sum(options) => options.is_valid_payment(payment), - PaymentModel::Resources(c) => { - c.can_afford(payment) - && c.cost.resource_amount() - c.discount == payment.resource_amount() - } - } + pub fn resources(cost: ResourcePile) -> Self { + Self::resources_with_discount(cost, 0) + } + + #[must_use] + pub fn can_afford(&self, available: &ResourcePile) -> bool { + self.first_valid_payment(available).is_some() } #[must_use] @@ -83,96 +120,285 @@ impl PaymentModel { #[must_use] pub fn possible_resource_types(&self) -> Vec { - match self { - PaymentModel::Sum(options) => options.types_by_preference.clone(), - PaymentModel::Resources(c) => c.cost.types(), + let mut vec = self.default.types(); + for conversion in &self.conversions { + vec.extend(conversion.to.types()); } + vec } #[must_use] pub fn default_payment(&self) -> ResourcePile { - match self { - PaymentModel::Sum(options) => { - ResourcePile::of(options.types_by_preference[0], options.cost) - } - PaymentModel::Resources(c) => c.cost.clone(), - } + self.default.clone() } } -impl Display for PaymentModel { +impl Display for PaymentOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PaymentModel::Sum(options) => write!( - f, - "a total of {} from {}", - options.cost, - options - .types_by_preference - .iter() - .map(|t| format!("{t:?}")) - .collect::>() - .join(", ") - ), - PaymentModel::Resources(c) => { - if c.discount > 0 { - write!(f, "{} with discount {}", c.cost, c.discount) - } else { - c.cost.fmt(f) - } + write!(f, "{}", self.default)?; + // this is a bit ugly, make it nicer + for conversion in &self.conversions { + write!(f, " > {}", conversion.to.types().first().expect("no type"))?; + if let Some(limit) = conversion.limit { + write!(f, " (limit: {limit})")?; } } + Ok(()) } } -impl Add for PaymentModel { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - match self { - PaymentModel::Sum(s) => match rhs { - PaymentModel::Sum(r) => { - assert_eq!(s.types_by_preference, r.types_by_preference); - PaymentModel::sum(s.cost + r.cost, s.types_by_preference) - } - PaymentModel::Resources(_) => { - panic!("Cannot add Sum and Resources") - } - }, - PaymentModel::Resources(r) => match rhs { - PaymentModel::Sum(_) => { - panic!("Cannot add Resources and Sum") - } - PaymentModel::Resources(r2) => PaymentModel::resources_with_discount( - r.cost + r2.cost, - r.discount + r2.discount, - ), - }, +#[must_use] +pub fn can_convert( + available: &ResourcePile, + current: &ResourcePile, + conversions: &[&PaymentConversion], + skip_from: usize, + discount_left: u32, +) -> Option { + if available.has_at_least(current, 1) && discount_left == 0 { + return Some(current.clone()); + } + + if conversions.is_empty() { + return None; + } + let conversion = &conversions[0]; + if skip_from >= conversion.from.len() { + return can_convert(available, current, &conversions[1..], 0, discount_left); + } + let from = &conversion.from[skip_from]; + + let upper_limit = conversion.limit.unwrap_or(u32::MAX); + for amount in 1..=upper_limit { + if !current.has_at_least(from, amount) + || (conversion.to.is_empty() && amount > discount_left) + { + return can_convert( + available, + current, + conversions, + skip_from + 1, + discount_left, + ); + } + + let mut current = current.clone(); + for _ in 0..amount { + current -= from.clone(); + current += conversion.to.clone(); + } + let new_discount_left = if conversion.to.is_empty() { + discount_left - amount + } else { + discount_left + }; + + let can = can_convert( + available, + ¤t, + conversions, + skip_from + 1, + new_discount_left, + ); + if can.is_some() { + return can; } } + None } -impl SubAssign for PaymentModel { - fn sub_assign(&mut self, rhs: Self) { - match self { - PaymentModel::Sum(s) => match rhs { - PaymentModel::Sum(r) => { - assert_eq!(s.types_by_preference, r.types_by_preference); - s.cost -= r.cost; - } - PaymentModel::Resources(_) => { - panic!("Cannot subtract Resources from Sum") - } +#[cfg(test)] +mod tests { + use super::*; + + struct ValidPaymentTestCase { + name: String, + options: PaymentOptions, + valid: Vec, + invalid: Vec, + } + + #[test] + fn test_find_valid_payment() { + let options = PaymentOptions { + default: ResourcePile::food(1), + conversions: vec![PaymentConversion { + from: vec![ResourcePile::food(1)], + to: ResourcePile::wood(1), + limit: None, + }], + }; + let available = ResourcePile::wood(1) + ResourcePile::ore(1); + assert_eq!( + Some(ResourcePile::wood(1)), + options.first_valid_payment(&available) + ); + } + + #[test] + fn test_is_valid_payment() { + let test_cases = vec![ + ValidPaymentTestCase { + name: "no conversions".to_string(), + options: PaymentOptions { + default: ResourcePile::food(1), + conversions: vec![], + }, + valid: vec![ResourcePile::food(1)], + invalid: vec![ResourcePile::food(2)], + }, + ValidPaymentTestCase { + name: "food to wood".to_string(), + options: PaymentOptions { + default: ResourcePile::food(1), + conversions: vec![PaymentConversion { + from: vec![ResourcePile::food(1)], + to: ResourcePile::wood(1), + limit: None, + }], + }, + valid: vec![ResourcePile::food(1), ResourcePile::wood(1)], + invalid: vec![ResourcePile::food(2), ResourcePile::ore(1)], + }, + ValidPaymentTestCase { + name: "food to wood with amount".to_string(), + options: PaymentOptions { + default: ResourcePile::food(2), + conversions: vec![PaymentConversion { + from: vec![ResourcePile::food(1)], + to: ResourcePile::wood(1), + limit: None, + }], + }, + valid: vec![ + ResourcePile::food(2), + ResourcePile::food(1) + ResourcePile::wood(1), + ResourcePile::wood(2), + ], + invalid: vec![ResourcePile::ore(2)], }, - PaymentModel::Resources(r) => match rhs { - PaymentModel::Sum(_) => { - panic!("Cannot subtract Sum from Resources") - } - PaymentModel::Resources(r2) => { - r.cost -= r2.cost; - r.discount -= r2.discount; - } + ValidPaymentTestCase { + name: "food to wood with limit".to_string(), + options: PaymentOptions { + default: ResourcePile::food(2), + conversions: vec![PaymentConversion { + from: vec![ResourcePile::food(1)], + to: ResourcePile::wood(1), + limit: Some(1), + }], + }, + valid: vec![ + ResourcePile::food(2), + ResourcePile::wood(1) + ResourcePile::food(1), + ], + invalid: vec![ResourcePile::wood(2)], }, + ValidPaymentTestCase { + name: "discount must be used".to_string(), + options: PaymentOptions { + default: ResourcePile::food(3), + conversions: vec![PaymentConversion { + from: vec![ResourcePile::food(1)], + to: ResourcePile::empty(), + limit: Some(2), + }], + }, + valid: vec![ResourcePile::food(1)], + invalid: vec![ResourcePile::food(2)], + }, + ValidPaymentTestCase { + name: "food to wood to ore".to_string(), + options: PaymentOptions { + default: ResourcePile::food(1), + conversions: vec![ + PaymentConversion { + from: vec![ResourcePile::food(1)], + to: ResourcePile::wood(1), + limit: None, + }, + PaymentConversion { + from: vec![ResourcePile::wood(1)], + to: ResourcePile::ore(1), + limit: None, + }, + ], + }, + valid: vec![ + ResourcePile::food(1), + ResourcePile::wood(1), + ResourcePile::ore(1), + ], + invalid: vec![ResourcePile::ideas(2)], + }, + ValidPaymentTestCase { + name: "food to wood to ore with reversed conversion order".to_string(), + options: PaymentOptions { + default: ResourcePile::food(1), + conversions: vec![ + PaymentConversion { + from: vec![ResourcePile::wood(1)], + to: ResourcePile::ore(1), + limit: None, + }, + PaymentConversion { + from: vec![ResourcePile::food(1)], + to: ResourcePile::wood(1), + limit: None, + }, + ], + }, + valid: vec![ + ResourcePile::food(1), + ResourcePile::wood(1), + ResourcePile::ore(1), + ], + invalid: vec![ResourcePile::ideas(2)], + }, + ValidPaymentTestCase { + name: "gold can replace anything but mood and culture tokens".to_string(), + options: PaymentOptions { + default: ResourcePile::food(1) + + ResourcePile::wood(1) + + ResourcePile::mood_tokens(1), + conversions: vec![PaymentConversion { + from: vec![ + ResourcePile::food(1), + ResourcePile::wood(1), + ResourcePile::ore(1), + ResourcePile::ideas(1), + ], + to: ResourcePile::gold(1), + limit: None, + }], + }, + valid: vec![ + ResourcePile::food(1) + ResourcePile::wood(1) + ResourcePile::mood_tokens(1), + ResourcePile::food(1) + ResourcePile::gold(1) + ResourcePile::mood_tokens(1), + ResourcePile::wood(1) + ResourcePile::gold(1) + ResourcePile::mood_tokens(1), + ResourcePile::gold(2) + ResourcePile::mood_tokens(1), + ], + invalid: vec![ResourcePile::gold(3)], + }, + ]; + for test_case in test_cases { + for (i, valid) in test_case.valid.iter().enumerate() { + assert_eq!( + Some(valid.clone()), + test_case.options.first_valid_payment(valid), + "{} valid {}", + test_case.name, + i + ); + } + for (i, invalid) in test_case.invalid.iter().enumerate() { + assert_ne!( + Some(invalid.clone()), + test_case.options.first_valid_payment(invalid), + "{} invalid {}", + test_case.name, + i + ); + } } } } diff --git a/server/src/player.rs b/server/src/player.rs index 028b8e9f..97376ece 100644 --- a/server/src/player.rs +++ b/server/src/player.rs @@ -1,10 +1,10 @@ use crate::advance::Advance; -use crate::content::advances::{get_advance_by_name, RITUALS}; +use crate::content::advances::get_advance_by_name; use crate::game::CurrentMove; use crate::game::GameState::Movement; use crate::movement::move_routes; use crate::movement::{is_valid_movement_type, MoveRoute}; -use crate::payment::PaymentModel; +use crate::payment::PaymentOptions; use crate::resource::ResourceType; use crate::unit::{carried_units, get_current_move, MovementRestriction, UnitData}; use crate::{ @@ -423,11 +423,11 @@ impl Player { #[must_use] pub fn can_afford_resources(&self, cost: &ResourcePile) -> bool { - self.can_afford(&PaymentModel::resources(cost.clone())) + self.can_afford(&PaymentOptions::resources(cost.clone())) } #[must_use] - pub fn can_afford(&self, cost: &PaymentModel) -> bool { + pub fn can_afford(&self, cost: &PaymentOptions) -> bool { cost.can_afford(&self.resources) } @@ -581,16 +581,16 @@ impl Player { } #[must_use] - pub fn construct_cost(&self, building: Building, city: &City) -> PaymentModel { + pub fn construct_cost(&self, building: Building, city: &City) -> PaymentOptions { let mut cost = CONSTRUCT_COST; self.get_events() .construct_cost .trigger(&mut cost, city, &building); - PaymentModel::resources(cost) + PaymentOptions::resources(cost) } #[must_use] - pub fn wonder_cost(&self, wonder: &Wonder, city: &City) -> PaymentModel { + pub fn wonder_cost(&self, wonder: &Wonder, city: &City) -> PaymentOptions { let mut cost = wonder.cost.clone(); self.get_events() .wonder_cost @@ -599,37 +599,29 @@ impl Player { } #[must_use] - pub fn increase_happiness_cost(&self, city: &City, steps: u32) -> Option { + pub fn increase_happiness_cost(&self, city: &City, steps: u32) -> Option { let max_steps = 2 - city.mood_state.clone() as u32; let cost = city.size() as u32 * steps; if steps > max_steps { None - } else if self.has_advance(RITUALS) { - Some(PaymentModel::sum( - cost, - vec![ - ResourceType::Food, - ResourceType::Wood, - ResourceType::Ore, - ResourceType::Ideas, - ResourceType::MoodTokens, - ResourceType::Gold, - ], - )) } else { - Some(PaymentModel::sum(cost, vec![ResourceType::MoodTokens])) + let mut options = PaymentOptions::sum(cost, &[ResourceType::MoodTokens]); + self.get_events() + .happiness_cost + .trigger(&mut options, &(), &()); + Some(options) } } #[must_use] - pub fn advance_cost(&self, advance: &str) -> PaymentModel { + pub fn advance_cost(&self, advance: &str) -> PaymentOptions { let mut cost = ADVANCE_COST; self.get_events() .advance_cost .trigger(&mut cost, &advance.to_string(), &()); - PaymentModel::sum( + PaymentOptions::sum( cost, - vec![ResourceType::Ideas, ResourceType::Food, ResourceType::Gold], + &[ResourceType::Ideas, ResourceType::Food, ResourceType::Gold], ) } @@ -780,7 +772,7 @@ impl Player { if !city.can_activate() { return false; } - let cost = PaymentModel::resources(units.iter().map(UnitType::cost).sum()); + let cost = PaymentOptions::resources(units.iter().map(UnitType::cost).sum()); if !self.can_afford(&cost) { return false; } diff --git a/server/src/player_events.rs b/server/src/player_events.rs index 3dc726ab..0077f982 100644 --- a/server/src/player_events.rs +++ b/server/src/player_events.rs @@ -6,7 +6,7 @@ use crate::content::custom_phase_actions::{ }; use crate::game::Game; use crate::map::Terrain; -use crate::payment::PaymentModel; +use crate::payment::PaymentOptions; use crate::playing_actions::PlayingActionType; use crate::{ city::City, city_pieces::Building, events::EventMut, player::Player, position::Position, @@ -23,12 +23,13 @@ pub(crate) struct PlayerEvents { pub construct_cost: EventMut, pub on_construct_wonder: EventMut, pub on_undo_construct_wonder: EventMut, - pub wonder_cost: EventMut, + pub wonder_cost: EventMut, pub on_advance: EventMut, pub on_undo_advance: EventMut, pub after_execute_action: EventMut, pub before_undo_action: EventMut, pub advance_cost: EventMut, + pub happiness_cost: EventMut, pub is_playing_action_available: EventMut, pub terrain_collect_options: EventMut>, (), ()>, pub collect_options: EventMut>, CollectContext, Game>, diff --git a/server/src/playing_actions.rs b/server/src/playing_actions.rs index 26a383dd..77eb5d7e 100644 --- a/server/src/playing_actions.rs +++ b/server/src/playing_actions.rs @@ -6,7 +6,7 @@ use crate::action::Action; use crate::city::MoodState; use crate::collect::{collect, undo_collect}; use crate::game::{CulturalInfluenceResolution, GameState}; -use crate::payment::PaymentModel; +use crate::payment::PaymentOptions; use crate::unit::Unit; use crate::{ city::City, @@ -185,7 +185,7 @@ impl PlayingAction { collect(game, player_index, &c); } Recruit(r) => { - let cost = PaymentModel::resources( + let cost = PaymentOptions::resources( r.units.iter().map(UnitType::cost).sum::(), ); let player = &mut game.players[player_index]; @@ -404,7 +404,7 @@ impl ActionType { pub(crate) fn increase_happiness(game: &mut Game, player_index: usize, i: IncreaseHappiness) { let player = &mut game.players[player_index]; - let mut total_cost: PaymentModel = PaymentModel::free(); + let mut total_cost = PaymentOptions::free(); let mut angry_activations = vec![]; for (city_position, steps) in i.happiness_increases { let city = player.get_city(city_position).expect("Illegal action"); @@ -420,7 +420,7 @@ pub(crate) fn increase_happiness(game: &mut Game, player_index: usize, i: Increa if total_cost.is_free() { total_cost = cost; } else { - total_cost = total_cost + cost; + total_cost.default += cost.default; } let city = player.get_city_mut(city_position).expect("Illegal action"); for _ in 0..steps { diff --git a/server/src/resource_pile.rs b/server/src/resource_pile.rs index 79c10f0e..b81ff188 100644 --- a/server/src/resource_pile.rs +++ b/server/src/resource_pile.rs @@ -2,7 +2,6 @@ use crate::resource::ResourceType; use crate::utils; use serde::{Deserialize, Serialize}; use std::{ - cmp, fmt::Display, iter::Sum, ops::{Add, AddAssign, Mul, SubAssign}, @@ -68,7 +67,7 @@ impl ResourcePile { } #[must_use] - pub fn get(&self, resource_type: ResourceType) -> u32 { + pub fn get(&self, resource_type: &ResourceType) -> u32 { match resource_type { ResourceType::Food => self.food, ResourceType::Wood => self.wood, @@ -80,6 +79,17 @@ impl ResourcePile { } } + #[must_use] + pub fn has_at_least(&self, other: &ResourcePile, times: u32) -> bool { + self.food >= other.food * times + && self.wood >= other.wood * times + && self.ore >= other.ore * times + && self.ideas >= other.ideas * times + && self.gold >= other.gold * times + && self.mood_tokens >= other.mood_tokens * times + && self.culture_tokens >= other.culture_tokens * times + } + /// /// # Panics /// Panics if `resource_type` is `Discount` @@ -362,113 +372,27 @@ impl CostWithDiscount { && available.mood_tokens >= cost.mood_tokens && available.culture_tokens >= cost.culture_tokens } - - //this function assumes that `self` can afford `cost` - #[must_use] - pub fn get_payment_options(&self, available: &ResourcePile) -> PaymentOptions { - let cost = &self.cost; - let mut jokers_left = self.discount; - - let mut gold_left = available.gold; - let mut gold_cost = cost.gold; - gold_left -= gold_cost; - - if cost.food > available.food { - let joker_cost = cost.food - available.food; - if joker_cost > jokers_left { - gold_left -= joker_cost - jokers_left; - gold_cost += joker_cost - jokers_left; - } - jokers_left = jokers_left.saturating_sub(joker_cost); - } - if cost.wood > available.wood { - let joker_cost = cost.wood - available.wood; - if joker_cost > jokers_left { - gold_left -= joker_cost - jokers_left; - gold_cost += joker_cost - jokers_left; - } - jokers_left = jokers_left.saturating_sub(joker_cost); - } - if cost.ore > available.ore { - let joker_cost = cost.ore - available.ore; - if joker_cost > jokers_left { - gold_left -= joker_cost - jokers_left; - gold_cost += joker_cost - jokers_left; - } - jokers_left = jokers_left.saturating_sub(joker_cost); - } - if cost.ideas > available.ideas { - let joker_cost = cost.ideas - available.ideas; - if joker_cost > jokers_left { - gold_left -= joker_cost - jokers_left; - gold_cost += joker_cost - jokers_left; - } - jokers_left = jokers_left.saturating_sub(joker_cost); - } - let default = ResourcePile::new( - cmp::min(cost.food, available.food), - cmp::min(cost.wood, available.wood), - cmp::min(cost.ore, available.ore), - cmp::min(cost.ideas, available.ideas), - gold_cost, - cost.mood_tokens, - cost.culture_tokens, - ); - PaymentOptions::new(default, gold_left, jokers_left) - } -} - -#[derive(PartialEq, Eq, Debug, Clone)] -pub struct PaymentOptions { - pub default: ResourcePile, - pub gold_left: u32, - pub discount_left: u32, -} - -impl PaymentOptions { - #[must_use] - pub fn new(default: ResourcePile, gold_left: u32, discount_left: u32) -> Self { - Self { - default, - gold_left, - discount_left, - } - } } #[cfg(test)] mod tests { - use super::{CostWithDiscount, PaymentOptions, ResourcePile}; - use crate::payment::PaymentModel; + use super::ResourcePile; + use crate::payment::PaymentOptions; fn assert_can_afford(name: &str, cost: &ResourcePile, discount: u32) { let player_has = ResourcePile::new(1, 2, 3, 4, 5, 6, 7); let can_afford = - PaymentModel::resources_with_discount(cost.clone(), discount).can_afford(&player_has); + PaymentOptions::resources_with_discount(cost.clone(), discount).can_afford(&player_has); assert!(can_afford, "{name}"); } fn assert_cannot_afford(name: &str, cost: &ResourcePile, discount: u32) { let player_has = ResourcePile::new(1, 2, 3, 4, 5, 6, 7); let can_afford = - PaymentModel::resources_with_discount(cost.clone(), discount).can_afford(&player_has); + PaymentOptions::resources_with_discount(cost.clone(), discount).can_afford(&player_has); assert!(!can_afford, "{name}"); } - fn assert_payment_options( - name: &str, - cost: &ResourcePile, - discount: u32, - want: &PaymentOptions, - ) { - let budget = ResourcePile::new(1, 2, 3, 4, 5, 6, 7); - let c = CostWithDiscount { - cost: cost.clone(), - discount, - }; - assert_eq!(want, &c.get_payment_options(&budget), "{name}"); - } - fn assert_to_string(resource_pile: &ResourcePile, expected: &str) { assert_eq!( expected.to_string(), @@ -521,28 +445,6 @@ mod tests { assert_eq!(ResourcePile::new(0, 1, 2, 0, 0, 0, 0), waste); } - #[test] - fn payment_options_test() { - assert_payment_options( - "no gold use", - &ResourcePile::new(1, 1, 3, 2, 0, 2, 4), - 0, - &(PaymentOptions::new(ResourcePile::new(1, 1, 3, 2, 0, 2, 4), 5, 0)), - ); - assert_payment_options( - "use some gold", - &ResourcePile::new(2, 2, 3, 5, 2, 0, 0), - 0, - &(PaymentOptions::new(ResourcePile::new(1, 2, 3, 4, 4, 0, 0), 1, 0)), - ); - assert_payment_options( - "jokers", - &(ResourcePile::ore(4) + ResourcePile::ideas(4)), - 3, - &(PaymentOptions::new(ResourcePile::ore(3) + ResourcePile::ideas(4), 5, 2)), - ); - } - #[test] fn resource_pile_display_test() { assert_to_string(&ResourcePile::empty(), "nothing"); diff --git a/server/src/status_phase.rs b/server/src/status_phase.rs index 7dd7ec1b..6b784f55 100644 --- a/server/src/status_phase.rs +++ b/server/src/status_phase.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; -use crate::payment::PaymentModel; +use crate::payment::PaymentOptions; use crate::{ content::advances, game::{Game, GameState::*}, @@ -202,7 +202,7 @@ fn play_status_phase_for_player( player.cities.len() > 1 && player.cities.iter().any(|city| city.size() == 1) } StatusPhaseState::ChangeGovernmentType => { - let cost = &PaymentModel::resources(CHANGE_GOVERNMENT_COST); + let cost = &PaymentOptions::resources(CHANGE_GOVERNMENT_COST); player.can_afford(cost) && player.government().is_some_and(|government| { advances::get_governments().iter().any(|(g, a)| { diff --git a/server/src/wonder.rs b/server/src/wonder.rs index b2f62fae..512e7b02 100644 --- a/server/src/wonder.rs +++ b/server/src/wonder.rs @@ -1,5 +1,5 @@ use crate::ability_initializer::EventOrigin; -use crate::payment::PaymentModel; +use crate::payment::PaymentOptions; use crate::{ ability_initializer::{self, AbilityInitializer, AbilityInitializerSetup}, game::Game, @@ -11,7 +11,7 @@ type PlacementChecker = Box bool>; pub struct Wonder { pub name: String, pub description: String, - pub cost: PaymentModel, + pub cost: PaymentOptions, pub required_advances: Vec, pub placement_requirement: Option, pub player_initializer: AbilityInitializer, @@ -24,7 +24,7 @@ impl Wonder { pub fn builder( name: &str, description: &str, - cost: PaymentModel, + cost: PaymentOptions, required_advances: Vec<&str>, ) -> WonderBuilder { WonderBuilder::new( @@ -42,7 +42,7 @@ impl Wonder { pub struct WonderBuilder { name: String, descriptions: Vec, - cost: PaymentModel, + cost: PaymentOptions, required_advances: Vec, placement_requirement: Option, player_initializers: Vec, @@ -55,7 +55,7 @@ impl WonderBuilder { fn new( name: &str, description: &str, - cost: PaymentModel, + cost: PaymentOptions, required_advances: Vec, ) -> Self { Self { diff --git a/server/tests/test_games/combat_all_modifiers.outcome.json b/server/tests/test_games/combat_all_modifiers.outcome.json index 4838614b..3c273fed 100644 --- a/server/tests/test_games/combat_all_modifiers.outcome.json +++ b/server/tests/test_games/combat_all_modifiers.outcome.json @@ -1,35 +1,35 @@ { "state": { "Combat": { - "initiation": { - "Movement": { - "movement_actions_left": 2, - "moved_units": [ - 0, - 1, - 2, - 3, - 4, - 5 - ] - } - }, - "round": 1, - "phase": "Retreat", - "defender": 1, - "defender_position": "C1", - "attacker": 0, - "attacker_position": "C2", - "attackers": [ - 0, - 1, - 2, - 3, - 4, - 5 - ], - "can_retreat": true - } + "initiation": { + "Movement": { + "movement_actions_left": 2, + "moved_units": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + } + }, + "round": 1, + "phase": "Retreat", + "defender": 1, + "defender_position": "C1", + "attacker": 0, + "attacker_position": "C2", + "attackers": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "can_retreat": true + } }, "state_change_event_state": { "event_used": [], @@ -41,14 +41,22 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 1, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 1 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Use steel weapons", "optional": true @@ -407,4 +415,4 @@ } } ] -} +} \ No newline at end of file diff --git a/server/tests/test_games/combat_all_modifiers.outcome1.json b/server/tests/test_games/combat_all_modifiers.outcome1.json index 2d2e92fc..6c64b6bc 100644 --- a/server/tests/test_games/combat_all_modifiers.outcome1.json +++ b/server/tests/test_games/combat_all_modifiers.outcome1.json @@ -44,27 +44,43 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 2, - "types_by_preference": [ - "Wood", - "Gold" - ] - } + "options": { + "default": { + "wood": 2 + }, + "conversions": [ + { + "from": [ + { + "wood": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Cancel fortress ability to add an extra die in the first round of combat", "optional": true }, { - "model": { - "Sum": { - "cost": 2, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 2 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Cancel fortress ability to ignore the first hit in the first round of combat", "optional": true @@ -443,14 +459,22 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 1, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 1 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Use steel weapons", "optional": true diff --git a/server/tests/test_games/combat_all_modifiers.outcome2.json b/server/tests/test_games/combat_all_modifiers.outcome2.json index 2e5e4810..e6108780 100644 --- a/server/tests/test_games/combat_all_modifiers.outcome2.json +++ b/server/tests/test_games/combat_all_modifiers.outcome2.json @@ -47,14 +47,22 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 1, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 1 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Use steel weapons", "optional": true @@ -444,14 +452,22 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 1, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 1 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Use steel weapons", "optional": true @@ -476,27 +492,43 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 2, - "types_by_preference": [ - "Wood", - "Gold" - ] - } + "options": { + "default": { + "wood": 2 + }, + "conversions": [ + { + "from": [ + { + "wood": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Cancel fortress ability to add an extra die in the first round of combat", "optional": true }, { - "model": { - "Sum": { - "cost": 2, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 2 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Cancel fortress ability to ignore the first hit in the first round of combat", "optional": true diff --git a/server/tests/test_games/combat_all_modifiers.outcome3.json b/server/tests/test_games/combat_all_modifiers.outcome3.json index 105216fd..29b99bc5 100644 --- a/server/tests/test_games/combat_all_modifiers.outcome3.json +++ b/server/tests/test_games/combat_all_modifiers.outcome3.json @@ -393,14 +393,22 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 1, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 1 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Use steel weapons", "optional": true @@ -425,27 +433,43 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 2, - "types_by_preference": [ - "Wood", - "Gold" - ] - } + "options": { + "default": { + "wood": 2 + }, + "conversions": [ + { + "from": [ + { + "wood": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Cancel fortress ability to add an extra die in the first round of combat", "optional": true }, { - "model": { - "Sum": { - "cost": 2, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 2 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Cancel fortress ability to ignore the first hit in the first round of combat", "optional": true @@ -472,14 +496,22 @@ "request": { "Payment": [ { - "model": { - "Sum": { - "cost": 1, - "types_by_preference": [ - "Ore", - "Gold" - ] - } + "options": { + "default": { + "ore": 1 + }, + "conversions": [ + { + "from": [ + { + "ore": 1 + } + ], + "to": { + "gold": 1 + } + } + ] }, "name": "Use steel weapons", "optional": true diff --git a/server/tests/test_games/trade_routes_with_currency.outcome.json b/server/tests/test_games/trade_routes_with_currency.outcome.json index 69e7aa23..97c04d5c 100644 --- a/server/tests/test_games/trade_routes_with_currency.outcome.json +++ b/server/tests/test_games/trade_routes_with_currency.outcome.json @@ -9,14 +9,22 @@ "player_index": 1, "request": { "Reward": { - "model": { - "Sum": { - "cost": 2, - "types_by_preference": [ - "Gold", - "Food" - ] - } + "options": { + "default": { + "gold": 2 + }, + "conversions": [ + { + "from": [ + { + "gold": 1 + } + ], + "to": { + "food": 1 + } + } + ] }, "name": "Collect trade routes reward" } @@ -635,4 +643,4 @@ } } ] -} +} \ No newline at end of file diff --git a/server/tests/test_games/trade_routes_with_currency.outcome1.json b/server/tests/test_games/trade_routes_with_currency.outcome1.json index 8981704b..6c5b9d60 100644 --- a/server/tests/test_games/trade_routes_with_currency.outcome1.json +++ b/server/tests/test_games/trade_routes_with_currency.outcome1.json @@ -627,14 +627,22 @@ "player_index": 1, "request": { "Reward": { - "model": { - "Sum": { - "cost": 2, - "types_by_preference": [ - "Gold", - "Food" - ] - } + "options": { + "default": { + "gold": 2 + }, + "conversions": [ + { + "from": [ + { + "gold": 1 + } + ], + "to": { + "food": 1 + } + } + ] }, "name": "Collect trade routes reward" } @@ -647,4 +655,4 @@ } } ] -} +} \ No newline at end of file