diff --git a/client/src/action_buttons.rs b/client/src/action_buttons.rs index 0ff0895..904efa4 100644 --- a/client/src/action_buttons.rs +++ b/client/src/action_buttons.rs @@ -115,10 +115,10 @@ pub fn base_or_custom_action( action: PlayingActionType, title: &str, custom: &[(&str, CustomActionType)], - f: impl Fn(BaseOrCustomDialog) -> ActiveDialog, + execute: impl Fn(BaseOrCustomDialog) -> ActiveDialog, ) -> StateUpdate { let base = if rc.can_play_action(action) { - Some(f(BaseOrCustomDialog { + Some(execute(BaseOrCustomDialog { custom: BaseOrCustomAction::Base, title: title.to_string(), })) @@ -133,7 +133,7 @@ pub fn base_or_custom_action( .find(|a| custom.iter().any(|(_, b)| **a == *b)) .map(|a| { let advance = custom.iter().find(|(_, b)| *b == *a).unwrap().0; - let dialog = f(BaseOrCustomDialog { + let dialog = execute(BaseOrCustomDialog { custom: BaseOrCustomAction::Custom { custom: a.clone(), advance: advance.to_string(), diff --git a/client/src/collect_ui.rs b/client/src/collect_ui.rs index ac98b8c..d06649a 100644 --- a/client/src/collect_ui.rs +++ b/client/src/collect_ui.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::HashSet; use crate::client_state::{ActiveDialog, StateUpdate}; use crate::dialog_ui::{ @@ -28,7 +29,7 @@ use server::resource_pile::ResourcePile; pub struct CollectResources { player_index: usize, city_position: Position, - possible_collections: HashMap>, + possible_collections: HashMap>, collections: Vec<(Position, ResourcePile)>, custom: BaseOrCustomDialog, } @@ -37,7 +38,7 @@ impl CollectResources { pub fn new( player_index: usize, city_position: Position, - possible_collections: HashMap>, + possible_collections: HashMap>, custom: BaseOrCustomDialog, ) -> CollectResources { CollectResources { @@ -134,7 +135,7 @@ fn click_collect_option( new.collections.push((p, pile.clone())); } - let used = col.collections.clone().into_iter().collect(); + let used = new.collections.clone().into_iter().collect(); new.possible_collections = possible_resource_collections(rc.game, col.city_position, col.player_index, &used); diff --git a/client/src/local_client/bin/main.rs b/client/src/local_client/bin/main.rs index b71517a..3751f54 100644 --- a/client/src/local_client/bin/main.rs +++ b/client/src/local_client/bin/main.rs @@ -113,6 +113,9 @@ pub fn setup_local_game() -> Game { add_terrain(&mut game, "C4", Terrain::Water); add_terrain(&mut game, "C5", Terrain::Water); add_terrain(&mut game, "D1", Terrain::Fertile); + add_terrain(&mut game, "E2", Terrain::Fertile); + add_terrain(&mut game, "B5", Terrain::Fertile); + add_terrain(&mut game, "B6", Terrain::Fertile); add_terrain(&mut game, "D2", Terrain::Water); add_unit(&mut game, "C2", player_index1, UnitType::Infantry); diff --git a/server/src/ability_initializer.rs b/server/src/ability_initializer.rs index 0bd6782..9638e67 100644 --- a/server/src/ability_initializer.rs +++ b/server/src/ability_initializer.rs @@ -1,3 +1,4 @@ +use crate::action::Action; use crate::{ content::custom_actions::CustomActionType, events::EventMut, game::Game, player_events::PlayerEvents, @@ -51,6 +52,33 @@ pub trait AbilityInitializerSetup: Sized { .add_ability_deinitializer(deinitializer) } + fn add_once_per_turn_effect

(self, name: &str, pred: P) -> Self + where + P: Fn(&Action) -> bool + 'static + Clone, + { + let pred2 = pred.clone(); + let name2 = name.to_string(); + let name3 = name.to_string(); + self.add_player_event_listener( + |event| &mut event.after_execute_action, + move |player, action, ()| { + if pred2(action) { + player.played_once_per_turn_effects.push(name2.to_string()); + } + }, + 0, + ) + .add_player_event_listener( + |event| &mut event.before_undo_action, + move |player, action, ()| { + if pred(action) { + player.played_once_per_turn_effects.retain(|a| a != &name3); + } + }, + 0, + ) + } + fn add_custom_action(self, action: CustomActionType) -> Self { let deinitializer_action = action.clone(); self.add_ability_initializer(move |game, player_index| { diff --git a/server/src/collect.rs b/server/src/collect.rs index dc8237b..ed3b6de 100644 --- a/server/src/collect.rs +++ b/server/src/collect.rs @@ -1,9 +1,10 @@ use crate::game::Game; +use crate::map::Terrain; use crate::map::Terrain::{Fertile, Forest, Mountain}; use crate::playing_actions::Collect; use crate::position::Position; use crate::resource_pile::ResourcePile; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::iter; use std::ops::Add; @@ -61,9 +62,10 @@ pub(crate) fn undo_collect(game: &mut Game, player_index: usize, c: Collect) { } pub(crate) struct CollectContext { + pub player_index: usize, pub city_position: Position, - #[allow(dead_code)] // will need for other advances pub used: HashMap, + pub terrain_options: HashMap>, } /// @@ -75,12 +77,19 @@ pub fn possible_resource_collections( city_pos: Position, player_index: usize, used: &HashMap, -) -> HashMap> { - let terrain_options = HashMap::from([ - (Mountain, vec![ResourcePile::ore(1)]), - (Fertile, vec![ResourcePile::food(1)]), - (Forest, vec![ResourcePile::wood(1)]), - ]); +) -> HashMap> { + let set = [ + (Mountain, HashSet::from([ResourcePile::ore(1)])), + (Fertile, HashSet::from([ResourcePile::food(1)])), + (Forest, HashSet::from([ResourcePile::wood(1)])), + ]; + let mut terrain_options = HashMap::from(set); + game.players[player_index] + .events + .as_ref() + .expect("events should be set") + .terrain_collect_options + .trigger(&mut terrain_options, &(), &()); let mut collect_options = city_pos .neighbors() @@ -95,6 +104,7 @@ pub fn possible_resource_collections( None }) .collect(); + game.players[player_index] .events .as_ref() @@ -103,11 +113,21 @@ pub fn possible_resource_collections( .trigger( &mut collect_options, &CollectContext { + player_index, city_position: city_pos, used: used.clone(), + terrain_options, }, game, ); + for (pos, pile) in used { + collect_options + .entry(*pos) + .or_default() + .insert(pile.clone()); + // collect_options.insert(*pos, vec![pile.clone()]); + } + collect_options.retain(|p, _| { game.get_any_city(*p).is_none_or(|c| c.position == city_pos) && game.enemy_player(player_index, *p).is_none() diff --git a/server/src/content/advances.rs b/server/src/content/advances.rs index 196de2c..44a2a8c 100644 --- a/server/src/content/advances.rs +++ b/server/src/content/advances.rs @@ -1,6 +1,9 @@ use super::custom_actions::CustomActionType::*; +use crate::action::Action; use crate::advance::AdvanceBuilder; -use crate::playing_actions::PlayingActionType; +use crate::collect::CollectContext; +use crate::playing_actions::{PlayingAction, PlayingActionType}; +use crate::position::Position; use crate::{ ability_initializer::AbilityInitializerSetup, advance::{Advance, Bonus::*}, @@ -8,6 +11,7 @@ use crate::{ map::Terrain::*, resource_pile::ResourcePile, }; +use std::collections::{HashMap, HashSet}; //names of advances that need special handling pub const NAVIGATION: &str = "Navigation"; @@ -74,36 +78,85 @@ fn agriculture() -> Vec { "Storage", "Your maximum food limit is increased from 2 to 7", ) - .add_one_time_ability_initializer(|game, player_index| { - game.players[player_index].resource_limit.food = 7; - }) - .add_ability_undo_deinitializer(|game, player_index| { - game.players[player_index].resource_limit.food = 2; - }) - .with_advance_bonus(MoodToken), + .add_one_time_ability_initializer(|game, player_index| { + game.players[player_index].resource_limit.food = 7; + }) + .add_ability_undo_deinitializer(|game, player_index| { + game.players[player_index].resource_limit.food = 2; + }) + .with_advance_bonus(MoodToken), Advance::builder( "Irrigation", "Your cities may Collect food from Barren spaces, Ignore Famine events", ) - .add_player_event_listener( - |event| &mut event.collect_options, - |options, c, game| { - c.city_position - .neighbors() - .iter() - .chain(std::iter::once(&c.city_position)) - .filter(|pos| game.map.get(**pos) == Some(&Barren)) - .for_each(|pos| { - options.insert(*pos, vec![ResourcePile::food(1)]); - }); - }, - 0, + .add_player_event_listener( + |event| &mut event.terrain_collect_options, + |m,(),()| { + m.insert(Barren, HashSet::from([ResourcePile::food(1)])); + }, + 0, + ) + .with_advance_bonus(MoodToken), + Advance::builder( + "Husbandry", + "During a Collect Resources Action, you may collect from a Land space that is 2 Land spaces away, rather than 1. If you have the Roads Advance you may collect from two Land spaces that are 2 Land spaces away. This Advance can only be used once per turn.", ) - .with_advance_bonus(MoodToken), + .with_advance_bonus(MoodToken) + .add_player_event_listener( + |event| &mut event.collect_options, + husbandry_collect, + 0, + ) + .add_once_per_turn_effect("Husbandry", is_husbandry_action) ], ) } +fn is_husbandry_action(action: &Action) -> bool { + match action { + Action::Playing(PlayingAction::Collect(collect)) => collect + .collections + .iter() + .any(|c| c.0.distance(collect.city_position) > 1), + _ => false, + } +} + +fn husbandry_collect( + options: &mut HashMap>, + c: &CollectContext, + game: &Game, +) { + let player = &game.players[c.player_index]; + let allowed = if player + .played_once_per_turn_effects + .contains(&"Husbandry".to_string()) + { + 0 + } else if player.has_advance(ROADS) { + 2 + } else { + 1 + }; + + if c.used + .iter() + .filter(|(pos, _)| pos.distance(c.city_position) == 2) + .count() + == allowed + { + return; + } + + game.map + .tiles + .iter() + .filter(|(pos, t)| pos.distance(c.city_position) == 2 && t.is_land()) + .for_each(|(pos, t)| { + options.insert(*pos, c.terrain_options.get(t).cloned().unwrap_or_default()); + }); +} + fn construction() -> Vec { advance_group( "Mining", @@ -125,35 +178,7 @@ fn seafaring() -> Vec { "Fishing", vec![ Advance::builder("Fishing", "Your cities may Collect food from one Sea space") - .add_player_event_listener( - |event| &mut event.collect_options, - |options, c, game| { - let city = game - .get_any_city(c.city_position) - .expect("city should exist"); - let port = city.port_position; - if let Some(position) = port.or_else(|| { - c.city_position - .neighbors() - .into_iter() - .find(|pos| game.map.is_water(*pos)) - }) { - options.insert( - position, - if port.is_some() { - vec![ - ResourcePile::food(1), - ResourcePile::gold(1), - ResourcePile::mood_tokens(1), - ] - } else { - vec![ResourcePile::food(1)] - }, - ); - } - }, - 0, - ) + .add_player_event_listener(|event| &mut event.collect_options, fishing_collect, 0) .with_advance_bonus(MoodToken), Advance::builder( NAVIGATION, @@ -163,6 +188,38 @@ fn seafaring() -> Vec { ) } +fn fishing_collect( + options: &mut HashMap>, + c: &CollectContext, + game: &Game, +) { + let city = game + .get_any_city(c.city_position) + .expect("city should exist"); + let port = city.port_position; + if let Some(position) = + port.filter(|p| game.enemy_player(c.player_index, *p).is_none()) + .or_else(|| { + c.city_position.neighbors().into_iter().find(|p| { + game.map.is_water(*p) && game.enemy_player(c.player_index, *p).is_none() + }) + }) + { + options.insert( + position, + if Some(position) == port { + HashSet::from([ + ResourcePile::food(1), + ResourcePile::gold(1), + ResourcePile::mood_tokens(1), + ]) + } else { + HashSet::from([ResourcePile::food(1)]) + }, + ); + } +} + fn education() -> Vec { advance_group( "Philosophy", diff --git a/server/src/game.rs b/server/src/game.rs index 0eef4d3..e0be1ea 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -304,11 +304,14 @@ impl Game { self.undo(player_index); return; } + if matches!(action, Action::Redo) { assert!(self.can_redo(), "no action can be redone"); self.redo(player_index); return; } + let copy = action.clone(); + self.log.push(log::format_action_log_item(&action, self)); self.add_action_log_item(action.clone()); match self.state.clone() { @@ -366,11 +369,24 @@ impl Game { } Finished => panic!("actions can't be executed when the game is finished"), } + self.after_execute_or_redo(©, player_index); check_for_waste(self, player_index); } + fn after_execute_or_redo(&mut self, action: &Action, player_index: usize) { + self.players[player_index].take_events(|events, player| { + events.after_execute_action.trigger(player, action, &()); + }); + } + fn undo(&mut self, player_index: usize) { - match &self.action_log[self.action_log_index - 1] { + let action = &self.action_log[self.action_log_index - 1]; + + self.players[player_index].take_events(|events, player| { + events.before_undo_action.trigger(player, action, &()); + }); + + match action { Action::Playing(action) => action.clone().undo(self, player_index), Action::StatusPhase(_) => panic!("status phase actions can't be undone"), Action::Movement(action) => { @@ -399,6 +415,7 @@ impl Game { fn redo(&mut self, player_index: usize) { let action_log_item = &self.action_log[self.action_log_index]; + let copy = action_log_item.clone(); self.log .push(log::format_action_log_item(&action_log_item.clone(), self)); match action_log_item { @@ -443,6 +460,7 @@ impl Game { Action::Redo => panic!("redo action can't be redone"), } self.action_log_index += 1; + self.after_execute_or_redo(©, player_index); check_for_waste(self, player_index); } diff --git a/server/src/player.rs b/server/src/player.rs index 162abb0..2467963 100644 --- a/server/src/player.rs +++ b/server/src/player.rs @@ -62,6 +62,7 @@ pub struct Player { pub wonder_cards: Vec, pub next_unit_id: u32, pub played_once_per_turn_actions: Vec, + pub played_once_per_turn_effects: Vec, } impl Clone for Player { @@ -199,6 +200,7 @@ impl Player { .collect(), next_unit_id: data.next_unit_id, played_once_per_turn_actions: data.played_once_per_turn_actions, + played_once_per_turn_effects: data.played_once_per_turn_effects, }; player } @@ -233,6 +235,7 @@ impl Player { .collect(), next_unit_id: self.next_unit_id, played_once_per_turn_actions: self.played_once_per_turn_actions, + played_once_per_turn_effects: self.played_once_per_turn_effects, } } @@ -266,6 +269,7 @@ impl Player { .collect(), next_unit_id: self.next_unit_id, played_once_per_turn_actions: self.played_once_per_turn_actions.clone(), + played_once_per_turn_effects: self.played_once_per_turn_effects.clone(), } } @@ -301,6 +305,7 @@ impl Player { wonders_build: Vec::new(), next_unit_id: 0, played_once_per_turn_actions: Vec::new(), + played_once_per_turn_effects: Vec::new(), } } @@ -1007,4 +1012,7 @@ pub struct PlayerData { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] played_once_per_turn_actions: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "Vec::is_empty")] + played_once_per_turn_effects: Vec, } diff --git a/server/src/player_events.rs b/server/src/player_events.rs index f635ab0..30ad24d 100644 --- a/server/src/player_events.rs +++ b/server/src/player_events.rs @@ -1,12 +1,14 @@ +use crate::action::Action; use crate::collect::CollectContext; use crate::game::Game; +use crate::map::Terrain; use crate::payment::PaymentModel; use crate::playing_actions::PlayingActionType; use crate::{ city::City, city_pieces::Building, events::EventMut, player::Player, position::Position, resource_pile::ResourcePile, wonder::Wonder, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; #[derive(Default)] pub(crate) struct PlayerEvents { @@ -18,9 +20,12 @@ pub(crate) struct PlayerEvents { 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 is_playing_action_available: EventMut, - pub collect_options: EventMut>, CollectContext, Game>, + pub terrain_collect_options: EventMut>, (), ()>, + pub collect_options: EventMut>, CollectContext, Game>, } impl PlayerEvents { diff --git a/server/tests/game_api_tests.rs b/server/tests/game_api_tests.rs index f23b9d3..c59fd36 100644 --- a/server/tests/game_api_tests.rs +++ b/server/tests/game_api_tests.rs @@ -1,11 +1,3 @@ -use std::{ - collections::HashMap, - env, - fs::{self, OpenOptions}, - io::Write, - path::MAIN_SEPARATOR as SEPARATOR, -}; - use server::action::Action::CustomPhase; use server::action::CombatAction; use server::content::custom_actions::CustomAction; @@ -28,6 +20,14 @@ use server::{ resource_pile::ResourcePile, unit::{MovementAction::*, UnitType::*}, }; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::{ + collections::HashMap, + env, + fs::{self, OpenOptions}, + io::Write, + path::MAIN_SEPARATOR as SEPARATOR, +}; #[test] fn basic_actions() { @@ -422,20 +422,59 @@ fn game_path(name: &str) -> String { format!("tests{SEPARATOR}test_games{SEPARATOR}{name}.json") } -fn test_actions(name: &str, player_index: usize, actions: Vec) { +struct TestAction { + action: Action, + undoable: bool, + illegal_action_test: bool, +} + +impl TestAction { + fn illegal(action: Action) -> Self { + Self { + action, + undoable: false, + illegal_action_test: true, + } + } + + fn undoable(action: Action) -> Self { + Self { + action, + undoable: true, + illegal_action_test: false, + } + } + + fn not_undoable(action: Action) -> Self { + Self { + action, + undoable: false, + illegal_action_test: false, + } + } +} + +fn test_actions(name: &str, player_index: usize, actions: Vec) { + let outcome: fn(name: &str, i: usize) -> String = |name, i| { + if i == 0 { + format!("{name}.outcome") + } else { + format!("{name}.outcome{}", i) + } + }; for (i, action) in actions.into_iter().enumerate() { let from = if i == 0 { name.to_string() } else { - format!("{name}.outcome{}", i - 1) + outcome(name, i - 1) }; test_action_internal( &from, - &format!("{name}.outcome{i}"), - action, + outcome(name, i).as_str(), + action.action, player_index, - false, - false, + action.undoable, + action.illegal_action_test, ); } } @@ -469,13 +508,15 @@ fn test_action_internal( let a = serde_json::to_string(&action).expect("action should be serializable"); let a2 = serde_json::from_str(&a).expect("action should be deserializable"); let game = load_game(name); - let game = game_api::execute_action(game, a2, player_index); + if illegal_action_test { - println!( - "execute action was successful but should have panicked because the action is illegal" - ); + let err = catch_unwind(AssertUnwindSafe(|| { + let _ = game_api::execute_action(game, a2, player_index); + })); + assert!(err.is_err(), "execute action should panic"); return; } + let game = game_api::execute_action(game, a2, player_index); let expected_game = read_game_str(outcome); assert_eq_game_json( &expected_game, @@ -830,6 +871,22 @@ fn test_collect() { ); } +#[test] +fn test_collect_husbandry() { + let action = Action::Playing(Collect(playing_actions::Collect { + city_position: Position::from_offset("B3"), + collections: vec![(Position::from_offset("B5"), ResourcePile::food(1))], + })); + test_actions( + "collect_husbandry", + 0, + vec![ + TestAction::undoable(action.clone()), + TestAction::illegal(action.clone()), // illegal because it can't be done again + ], + ); +} + #[test] fn test_collect_free_economy() { test_action( @@ -884,7 +941,6 @@ fn test_construct_port() { } #[test] -#[should_panic(expected = "Illegal action")] fn test_wrong_status_phase_action() { test_action( "illegal_free_advance", @@ -1055,19 +1111,22 @@ fn test_combat_all_modifiers() { "combat_all_modifiers", 0, vec![ - move_action(vec![0, 1, 2, 3, 4, 5], Position::from_offset("C1")), - CustomPhase(CustomPhaseAction::SteelWeaponsAttackerAction( - ResourcePile::ore(1), + TestAction::not_undoable(move_action( + vec![0, 1, 2, 3, 4, 5], + Position::from_offset("C1"), )), - CustomPhase(CustomPhaseAction::SteelWeaponsDefenderAction( + TestAction::not_undoable(CustomPhase(CustomPhaseAction::SteelWeaponsAttackerAction( ResourcePile::ore(1), - )), - CustomPhase(CustomPhaseAction::SiegecraftPaymentAction( + ))), + TestAction::not_undoable(CustomPhase(CustomPhaseAction::SteelWeaponsDefenderAction( + ResourcePile::ore(1), + ))), + TestAction::not_undoable(CustomPhase(CustomPhaseAction::SiegecraftPaymentAction( SiegecraftPayment { ignore_hit: ResourcePile::ore(2), extra_die: ResourcePile::empty(), }, - )), + ))), ], ); } diff --git a/server/tests/test_games/collect_husbandry.json b/server/tests/test_games/collect_husbandry.json new file mode 100644 index 0000000..9dc6f00 --- /dev/null +++ b/server/tests/test_games/collect_husbandry.json @@ -0,0 +1,432 @@ +{ + "state": "Playing", + "players": [ + { + "name": null, + "id": 0, + "resources": { + "food": 2, + "wood": 5, + "ore": 5, + "ideas": 3, + "gold": 5, + "mood_tokens": 7, + "culture_tokens": 9 + }, + "resource_limit": { + "food": 2, + "wood": 7, + "ore": 7, + "ideas": 7, + "gold": 7 + }, + "cities": [ + { + "city_pieces": { + "market": 1 + }, + "mood_state": "Happy", + "activations": 0, + "angry_activation": false, + "player_index": 0, + "position": "A1" + }, + { + "city_pieces": { + "academy": 1, + "port": 1 + }, + "mood_state": "Angry", + "activations": 6, + "angry_activation": true, + "player_index": 0, + "position": "C2", + "port_position": "C3" + }, + { + "city_pieces": { + "obelisk": 1, + "observatory": 1, + "fortress": 1, + "temple": 1 + }, + "mood_state": "Neutral", + "activations": 0, + "angry_activation": false, + "player_index": 0, + "position": "B1" + }, + { + "city_pieces": {}, + "mood_state": "Happy", + "activations": 4, + "angry_activation": false, + "player_index": 0, + "position": "B3" + } + ], + "units": [ + { + "player_index": 0, + "position": "C2", + "unit_type": "Infantry", + "id": 0 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Cavalry", + "id": 1, + "carrier_id": 7 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Elephant", + "id": 2, + "carrier_id": 7 + }, + { + "player_index": 0, + "position": "B3", + "unit_type": "Settler", + "id": 3 + }, + { + "player_index": 0, + "position": "B3", + "unit_type": "Settler", + "id": 4 + }, + { + "player_index": 0, + "position": "B3", + "unit_type": "Settler", + "id": 5 + }, + { + "player_index": 0, + "position": "B3", + "unit_type": "Settler", + "id": 6 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Ship", + "id": 7 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Ship", + "id": 8 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Ship", + "id": 9 + } + ], + "civilization": "test0", + "active_leader": null, + "available_leaders": [ + "Alexander", + "Kleopatra" + ], + "advances": [ + "Farming", + "Free Economy", + "Husbandry", + "Mining", + "Voting" + ], + "unlocked_special_advance": [], + "wonders_build": [], + "game_event_tokens": 3, + "completed_objectives": [], + "captured_leaders": [], + "event_victory_points": 0.0, + "wonder_cards": [ + "Pyramids" + ], + "next_unit_id": 10 + }, + { + "name": null, + "id": 1, + "resources": { + "food": 2, + "wood": 5, + "ore": 5, + "ideas": 5, + "gold": 5, + "mood_tokens": 9, + "culture_tokens": 9 + }, + "resource_limit": { + "food": 2, + "wood": 7, + "ore": 7, + "ideas": 7, + "gold": 7 + }, + "cities": [ + { + "city_pieces": {}, + "mood_state": "Angry", + "activations": 2, + "angry_activation": false, + "player_index": 1, + "position": "C1" + }, + { + "city_pieces": { + "port": 1 + }, + "mood_state": "Neutral", + "activations": 0, + "angry_activation": false, + "player_index": 1, + "position": "B2", + "port_position": "C3" + } + ], + "units": [ + { + "player_index": 1, + "position": "C1", + "unit_type": "Infantry", + "id": 0 + }, + { + "player_index": 1, + "position": "C1", + "unit_type": "Infantry", + "id": 1 + } + ], + "civilization": "test1", + "active_leader": null, + "available_leaders": [], + "advances": [ + "Farming", + "Mining" + ], + "unlocked_special_advance": [], + "wonders_build": [], + "game_event_tokens": 3, + "completed_objectives": [], + "captured_leaders": [], + "event_victory_points": 0.0, + "wonder_cards": [], + "next_unit_id": 2 + } + ], + "map": { + "tiles": [ + [ + "A1", + "Fertile" + ], + [ + "A2", + "Water" + ], + [ + "A3", + { + "Exhausted": "Forest" + } + ], + [ + "A4", + "Mountain" + ], + [ + "A5", + "Fertile" + ], + [ + "B1", + "Mountain" + ], + [ + "B2", + "Forest" + ], + [ + "B3", + "Fertile" + ], + [ + "B4", + "Fertile" + ], + [ + "B5", + "Fertile" + ], + [ + "B6", + "Fertile" + ], + [ + "C1", + "Barren" + ], + [ + "C2", + "Forest" + ], + [ + "C3", + "Water" + ], + [ + "C4", + "Water" + ], + [ + "C5", + "Water" + ], + [ + "D1", + "Fertile" + ], + [ + "D2", + "Water" + ], + [ + "E2", + "Fertile" + ] + ] + }, + "starting_player_index": 0, + "current_player_index": 0, + "action_log": [ + { + "Playing": { + "Advance": { + "advance": "Husbandry", + "payment": { + "ideas": 2 + } + } + } + }, + { + "Playing": { + "Custom": { + "VotingIncreaseHappiness": { + "happiness_increases": [ + [ + "A1", + 0 + ], + [ + "C2", + 0 + ], + [ + "B1", + 0 + ], + [ + "B3", + 2 + ] + ], + "payment": { + "mood_tokens": 2 + } + } + } + } + } + ], + "action_log_index": 2, + "log": [ + "The game has started", + "Age 1 has started", + "Round 1/3", + "Player1 paid 2 ideas to get the Husbandry advance", + "Player1 paid 2 mood tokens to increase happiness in the city at B3 by 2 steps, making it Happy using Voting" + ], + "undo_limit": 1, + "actions_left": 2, + "successful_cultural_influence": false, + "round": 6, + "age": 1, + "messages": [ + "The game has started" + ], + "dice_roll_outcomes": [ + 1, + 1, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10 + ], + "rng": { + "seed": 234162992961072890508432380903651342097 + }, + "dice_roll_log": [], + "dropped_players": [], + "wonders_left": [], + "wonder_amount_left": 1, + "undo_context_stack": [ + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "IncreaseHappiness": { + "angry_activations": [ + "B3" + ] + } + } + ] +} diff --git a/server/tests/test_games/collect_husbandry.outcome.json b/server/tests/test_games/collect_husbandry.outcome.json new file mode 100644 index 0000000..01f123d --- /dev/null +++ b/server/tests/test_games/collect_husbandry.outcome.json @@ -0,0 +1,458 @@ +{ + "state": "Playing", + "players": [ + { + "name": null, + "id": 0, + "resources": { + "food": 2, + "wood": 5, + "ore": 5, + "ideas": 3, + "gold": 5, + "mood_tokens": 7, + "culture_tokens": 9 + }, + "resource_limit": { + "food": 2, + "wood": 7, + "ore": 7, + "ideas": 7, + "gold": 7 + }, + "cities": [ + { + "city_pieces": { + "market": 1 + }, + "mood_state": "Happy", + "activations": 0, + "angry_activation": false, + "player_index": 0, + "position": "A1" + }, + { + "city_pieces": { + "academy": 1, + "port": 1 + }, + "mood_state": "Angry", + "activations": 6, + "angry_activation": true, + "player_index": 0, + "position": "C2", + "port_position": "C3" + }, + { + "city_pieces": { + "obelisk": 1, + "observatory": 1, + "fortress": 1, + "temple": 1 + }, + "mood_state": "Neutral", + "activations": 0, + "angry_activation": false, + "player_index": 0, + "position": "B1" + }, + { + "city_pieces": {}, + "mood_state": "Neutral", + "activations": 5, + "angry_activation": false, + "player_index": 0, + "position": "B3" + } + ], + "units": [ + { + "player_index": 0, + "position": "C2", + "unit_type": "Infantry", + "id": 0 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Cavalry", + "id": 1, + "carrier_id": 7 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Elephant", + "id": 2, + "carrier_id": 7 + }, + { + "player_index": 0, + "position": "B3", + "unit_type": "Settler", + "id": 3 + }, + { + "player_index": 0, + "position": "B3", + "unit_type": "Settler", + "id": 4 + }, + { + "player_index": 0, + "position": "B3", + "unit_type": "Settler", + "id": 5 + }, + { + "player_index": 0, + "position": "B3", + "unit_type": "Settler", + "id": 6 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Ship", + "id": 7 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Ship", + "id": 8 + }, + { + "player_index": 0, + "position": "C3", + "unit_type": "Ship", + "id": 9 + } + ], + "civilization": "test0", + "active_leader": null, + "available_leaders": [ + "Alexander", + "Kleopatra" + ], + "advances": [ + "Farming", + "Free Economy", + "Husbandry", + "Mining", + "Voting" + ], + "unlocked_special_advance": [], + "wonders_build": [], + "game_event_tokens": 3, + "completed_objectives": [], + "captured_leaders": [], + "event_victory_points": 0.0, + "wonder_cards": [ + "Pyramids" + ], + "next_unit_id": 10, + "played_once_per_turn_effects": [ + "Husbandry" + ] + }, + { + "name": null, + "id": 1, + "resources": { + "food": 2, + "wood": 5, + "ore": 5, + "ideas": 5, + "gold": 5, + "mood_tokens": 9, + "culture_tokens": 9 + }, + "resource_limit": { + "food": 2, + "wood": 7, + "ore": 7, + "ideas": 7, + "gold": 7 + }, + "cities": [ + { + "city_pieces": {}, + "mood_state": "Angry", + "activations": 2, + "angry_activation": false, + "player_index": 1, + "position": "C1" + }, + { + "city_pieces": { + "port": 1 + }, + "mood_state": "Neutral", + "activations": 0, + "angry_activation": false, + "player_index": 1, + "position": "B2", + "port_position": "C3" + } + ], + "units": [ + { + "player_index": 1, + "position": "C1", + "unit_type": "Infantry", + "id": 0 + }, + { + "player_index": 1, + "position": "C1", + "unit_type": "Infantry", + "id": 1 + } + ], + "civilization": "test1", + "active_leader": null, + "available_leaders": [], + "advances": [ + "Farming", + "Mining" + ], + "unlocked_special_advance": [], + "wonders_build": [], + "game_event_tokens": 3, + "completed_objectives": [], + "captured_leaders": [], + "event_victory_points": 0.0, + "wonder_cards": [], + "next_unit_id": 2 + } + ], + "map": { + "tiles": [ + [ + "A1", + "Fertile" + ], + [ + "A2", + "Water" + ], + [ + "A3", + { + "Exhausted": "Forest" + } + ], + [ + "A4", + "Mountain" + ], + [ + "A5", + "Fertile" + ], + [ + "B1", + "Mountain" + ], + [ + "B2", + "Forest" + ], + [ + "B3", + "Fertile" + ], + [ + "B4", + "Fertile" + ], + [ + "B5", + "Fertile" + ], + [ + "B6", + "Fertile" + ], + [ + "C1", + "Barren" + ], + [ + "C2", + "Forest" + ], + [ + "C3", + "Water" + ], + [ + "C4", + "Water" + ], + [ + "C5", + "Water" + ], + [ + "D1", + "Fertile" + ], + [ + "D2", + "Water" + ], + [ + "E2", + "Fertile" + ] + ] + }, + "starting_player_index": 0, + "current_player_index": 0, + "action_log": [ + { + "Playing": { + "Advance": { + "advance": "Husbandry", + "payment": { + "ideas": 2 + } + } + } + }, + { + "Playing": { + "Custom": { + "VotingIncreaseHappiness": { + "happiness_increases": [ + [ + "A1", + 0 + ], + [ + "C2", + 0 + ], + [ + "B1", + 0 + ], + [ + "B3", + 2 + ] + ], + "payment": { + "mood_tokens": 2 + } + } + } + } + }, + { + "Playing": { + "Collect": { + "city_position": "B3", + "collections": [ + [ + "B5", + { + "food": 1 + } + ] + ] + } + } + } + ], + "action_log_index": 3, + "log": [ + "The game has started", + "Age 1 has started", + "Round 1/3", + "Player1 paid 2 ideas to get the Husbandry advance", + "Player1 paid 2 mood tokens to increase happiness in the city at B3 by 2 steps, making it Happy using Voting", + "Player1 collects 1 food in the city at B3 making it Neutral. Could not store 1 food" + ], + "undo_limit": 1, + "actions_left": 1, + "successful_cultural_influence": false, + "round": 6, + "age": 1, + "messages": [ + "The game has started" + ], + "dice_roll_outcomes": [ + 1, + 1, + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10 + ], + "rng": { + "seed": 234162992961072890508432380903651342097 + }, + "dice_roll_log": [], + "dropped_players": [], + "wonders_left": [], + "wonder_amount_left": 1, + "undo_context_stack": [ + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "Recruit": {} + }, + { + "IncreaseHappiness": { + "angry_activations": [ + "B3" + ] + } + }, + { + "WastedResources": { + "resources": { + "food": 1 + } + } + } + ] +} diff --git a/server/tests/test_games/combat_all_modifiers.outcome0.json b/server/tests/test_games/combat_all_modifiers.outcome.json similarity index 100% rename from server/tests/test_games/combat_all_modifiers.outcome0.json rename to server/tests/test_games/combat_all_modifiers.outcome.json