From a0c6d15e1989a514518a5381a2f4264985cd83f8 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 09:47:26 +0100 Subject: [PATCH 01/10] husbandry --- server/src/collect.rs | 2 +- server/src/content/advances.rs | 183 ++++++++++++++++++++++++--------- server/src/game.rs | 14 ++- server/src/player.rs | 1 + server/src/player_events.rs | 5 +- 5 files changed, 151 insertions(+), 54 deletions(-) diff --git a/server/src/collect.rs b/server/src/collect.rs index dc8237b..6a825e8 100644 --- a/server/src/collect.rs +++ b/server/src/collect.rs @@ -61,8 +61,8 @@ 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, } diff --git a/server/src/content/advances.rs b/server/src/content/advances.rs index 196de2c..5cfad93 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; //names of advances that need special handling pub const NAVIGATION: &str = "Navigation"; @@ -74,36 +78,111 @@ 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.collect_options, + irrigation_collect, + 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), + //todo advance bonus? + .add_player_event_listener( + |event| &mut event.collect_options, + husbandry_collect, + 0, + ) + .add_player_event_listener( + |event| &mut event.on_execute_action, + |player, action, ()| { + if is_husbandry_action(action) { + player.played_once_per_turn_effects.push("Husbandry".to_string()); + } + }, + 0 + ) + .add_player_event_listener( + |event| &mut event.on_undo_action, + |player, action, ()| { + if is_husbandry_action(action) { + player.played_once_per_turn_effects.retain(|a| a != "Husbandry"); + } + }, + 0 + ) ], ) } +fn irrigation_collect( + options: &mut HashMap>, + c: &CollectContext, + game: &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)]); + }); +} + +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 + .into_iter() + .filter(|(pos, t)| pos.distance(c.city_position) == 2) + .for_each(|pos| { + + options.insert(*pos, vec![ResourcePile::food(1)]); + }); +} + fn construction() -> Vec { advance_group( "Mining", @@ -125,35 +204,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 +214,36 @@ 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.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)] + }, + ); + } +} + fn education() -> Vec { advance_group( "Philosophy", diff --git a/server/src/game.rs b/server/src/game.rs index 0eef4d3..2e68593 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -304,6 +304,12 @@ impl Game { self.undo(player_index); return; } + + // both for redo and for executing the action + self.players[player_index].take_events(|events, player| { + events.on_execute_action.trigger(player, &action, &()); + }); + if matches!(action, Action::Redo) { assert!(self.can_redo(), "no action can be redone"); self.redo(player_index); @@ -370,7 +376,13 @@ impl Game { } 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.on_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) => { diff --git a/server/src/player.rs b/server/src/player.rs index 162abb0..5943d25 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 { diff --git a/server/src/player_events.rs b/server/src/player_events.rs index f635ab0..dddcedc 100644 --- a/server/src/player_events.rs +++ b/server/src/player_events.rs @@ -1,3 +1,4 @@ +use crate::action::Action; use crate::collect::CollectContext; use crate::game::Game; use crate::payment::PaymentModel; @@ -16,8 +17,10 @@ pub(crate) struct PlayerEvents { pub on_construct_wonder: EventMut, pub on_undo_construct_wonder: EventMut, pub wonder_cost: EventMut, - pub on_advance: EventMut, + pub on_advance: EventMut, //todo use on_execute pub on_undo_advance: EventMut, + pub on_execute_action: EventMut, + pub on_undo_action: EventMut, pub advance_cost: EventMut, pub is_playing_action_available: EventMut, pub collect_options: EventMut>, CollectContext, Game>, From 2cc5ba0a5e9c59fb7cfee3fd38bd71e18b9b96f0 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 10:33:45 +0100 Subject: [PATCH 02/10] husbandry --- client/src/action_buttons.rs | 6 +-- client/src/collect_ui.rs | 7 ++-- client/src/local_client/bin/main.rs | 3 ++ server/src/collect.rs | 31 +++++++++++---- server/src/content/advances.rs | 58 +++++++++++++---------------- server/src/player.rs | 7 ++++ server/src/player_events.rs | 6 ++- 7 files changed, 70 insertions(+), 48 deletions(-) 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..d787c93 100644 --- a/client/src/collect_ui.rs +++ b/client/src/collect_ui.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::collections::HashMap; use crate::client_state::{ActiveDialog, StateUpdate}; @@ -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/collect.rs b/server/src/collect.rs index 6a825e8..621ced2 100644 --- a/server/src/collect.rs +++ b/server/src/collect.rs @@ -3,9 +3,10 @@ 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; +use crate::map::Terrain; /// /// # Panics @@ -64,6 +65,7 @@ pub(crate) struct CollectContext { pub player_index: usize, pub city_position: Position, 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,18 @@ 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 5cfad93..4efe5d5 100644 --- a/server/src/content/advances.rs +++ b/server/src/content/advances.rs @@ -2,6 +2,7 @@ use super::custom_actions::CustomActionType::*; use crate::action::Action; use crate::advance::AdvanceBuilder; use crate::collect::CollectContext; +use crate::map::Terrain; use crate::playing_actions::{PlayingAction, PlayingActionType}; use crate::position::Position; use crate::{ @@ -11,7 +12,7 @@ use crate::{ map::Terrain::*, resource_pile::ResourcePile, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; //names of advances that need special handling pub const NAVIGATION: &str = "Navigation"; @@ -90,8 +91,10 @@ fn agriculture() -> Vec { "Your cities may Collect food from Barren spaces, Ignore Famine events", ) .add_player_event_listener( - |event| &mut event.collect_options, - irrigation_collect, + |event| &mut event.terrain_collect_options, + |m,_,_| { + m.insert(Barren, HashSet::from([ResourcePile::food(1)])); + }, 0, ) .with_advance_bonus(MoodToken), @@ -99,13 +102,13 @@ fn agriculture() -> Vec { "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.", ) - //todo advance bonus? + .with_advance_bonus(MoodToken) .add_player_event_listener( |event| &mut event.collect_options, husbandry_collect, 0, ) - .add_player_event_listener( + .add_player_event_listener( // todo extract to a function |event| &mut event.on_execute_action, |player, action, ()| { if is_husbandry_action(action) { @@ -127,21 +130,6 @@ fn agriculture() -> Vec { ) } -fn irrigation_collect( - options: &mut HashMap>, - c: &CollectContext, - game: &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)]); - }); -} - fn is_husbandry_action(action: &Action) -> bool { match action { Action::Playing(PlayingAction::Collect(collect)) => collect @@ -153,7 +141,7 @@ fn is_husbandry_action(action: &Action) -> bool { } fn husbandry_collect( - options: &mut HashMap>, + options: &mut HashMap>, c: &CollectContext, game: &Game, ) { @@ -168,18 +156,22 @@ fn husbandry_collect( } else { 1 }; - - if c.used.iter().filter(|(pos, _)| pos.distance(c.city_position) == 2).count() == allowed { + + if c.used + .iter() + .filter(|(pos, _)| pos.distance(c.city_position) == 2) + .count() + == allowed + { return; } - game.map + &game.map .tiles - .into_iter() - .filter(|(pos, t)| pos.distance(c.city_position) == 2) - .for_each(|pos| { - - options.insert(*pos, vec![ResourcePile::food(1)]); + .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()); }); } @@ -215,7 +207,7 @@ fn seafaring() -> Vec { } fn fishing_collect( - options: &mut HashMap>, + options: &mut HashMap>, c: &CollectContext, game: &Game, ) { @@ -232,13 +224,13 @@ fn fishing_collect( options.insert( position, if port.is_some() { - vec![ + HashSet::from([ ResourcePile::food(1), ResourcePile::gold(1), ResourcePile::mood_tokens(1), - ] + ] ) } else { - vec![ResourcePile::food(1)] + HashSet::from([ResourcePile::food(1)]) }, ); } diff --git a/server/src/player.rs b/server/src/player.rs index 5943d25..2467963 100644 --- a/server/src/player.rs +++ b/server/src/player.rs @@ -200,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 } @@ -234,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, } } @@ -267,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(), } } @@ -302,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(), } } @@ -1008,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 dddcedc..c506a5b 100644 --- a/server/src/player_events.rs +++ b/server/src/player_events.rs @@ -7,7 +7,8 @@ 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}; +use crate::map::Terrain; #[derive(Default)] pub(crate) struct PlayerEvents { @@ -23,7 +24,8 @@ pub(crate) struct PlayerEvents { pub on_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 { From 72a7af15008da010e0555d2fce2d2795972339ee Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 10:51:26 +0100 Subject: [PATCH 03/10] husbandry --- server/src/game.rs | 16 +- server/tests/game_api_tests.rs | 17 + .../tests/test_games/collect_husbandry.json | 432 +++++++++++++++++ .../test_games/collect_husbandry.outcome.json | 458 ++++++++++++++++++ 4 files changed, 918 insertions(+), 5 deletions(-) create mode 100644 server/tests/test_games/collect_husbandry.json create mode 100644 server/tests/test_games/collect_husbandry.outcome.json diff --git a/server/src/game.rs b/server/src/game.rs index 2e68593..eb87784 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -305,16 +305,13 @@ impl Game { return; } - // both for redo and for executing the action - self.players[player_index].take_events(|events, player| { - events.on_execute_action.trigger(player, &action, &()); - }); - 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() { @@ -372,9 +369,16 @@ impl Game { } Finished => panic!("actions can't be executed when the game is finished"), } + self.on_execute_or_redo(©, player_index); check_for_waste(self, player_index); } + fn on_execute_or_redo(&mut self, action: &Action, player_index: usize) { + self.players[player_index].take_events(|events, player| { + events.on_execute_action.trigger(player, &action, &()); + }); + } + fn undo(&mut self, player_index: usize) { let action = &self.action_log[self.action_log_index - 1]; @@ -411,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 { @@ -455,6 +460,7 @@ impl Game { Action::Redo => panic!("redo action can't be redone"), } self.action_log_index += 1; + self.on_execute_or_redo(©, player_index); check_for_waste(self, player_index); } diff --git a/server/tests/game_api_tests.rs b/server/tests/game_api_tests.rs index f23b9d3..a9e7cdc 100644 --- a/server/tests/game_api_tests.rs +++ b/server/tests/game_api_tests.rs @@ -830,6 +830,23 @@ fn test_collect() { ); } +#[test] +fn test_collect_husbandry() { + // todo: test that the action is legal it can't be done again + test_action( + "collect_husbandry", + Action::Playing(Collect(playing_actions::Collect { + city_position: Position::from_offset("B3"), + collections: vec![ + (Position::from_offset("B5"), ResourcePile::food(1)), + ], + })), + 0, + true, + false, + ); +} + #[test] fn test_collect_free_economy() { test_action( 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 + } + } + } + ] +} From 996e06540fc94937ee759be58d33643c934ebffe Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 10:58:33 +0100 Subject: [PATCH 04/10] husbandry --- server/tests/game_api_tests.rs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/server/tests/game_api_tests.rs b/server/tests/game_api_tests.rs index a9e7cdc..542f96c 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() { @@ -469,13 +469,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, @@ -837,9 +839,7 @@ fn test_collect_husbandry() { "collect_husbandry", Action::Playing(Collect(playing_actions::Collect { city_position: Position::from_offset("B3"), - collections: vec![ - (Position::from_offset("B5"), ResourcePile::food(1)), - ], + collections: vec![(Position::from_offset("B5"), ResourcePile::food(1))], })), 0, true, @@ -901,7 +901,6 @@ fn test_construct_port() { } #[test] -#[should_panic(expected = "Illegal action")] fn test_wrong_status_phase_action() { test_action( "illegal_free_advance", From 90f300d6a9b0bd1fcdf627fef99a0f6a976971a9 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 11:06:15 +0100 Subject: [PATCH 05/10] husbandry --- server/tests/game_api_tests.rs | 84 ++++++++++++++----- ...json => combat_all_modifiers.outcome.json} | 0 2 files changed, 63 insertions(+), 21 deletions(-) rename server/tests/test_games/{combat_all_modifiers.outcome0.json => combat_all_modifiers.outcome.json} (100%) diff --git a/server/tests/game_api_tests.rs b/server/tests/game_api_tests.rs index 542f96c..713487d 100644 --- a/server/tests/game_api_tests.rs +++ b/server/tests/game_api_tests.rs @@ -422,20 +422,58 @@ 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, ); } } @@ -834,16 +872,17 @@ fn test_collect() { #[test] fn test_collect_husbandry() { - // todo: test that the action is legal it can't be done again - test_action( + 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", - Action::Playing(Collect(playing_actions::Collect { - city_position: Position::from_offset("B3"), - collections: vec![(Position::from_offset("B5"), ResourcePile::food(1))], - })), 0, - true, - false, + vec![ + TestAction::undoable(action.clone()), + TestAction::illegal(action.clone()), // illegal because it can't be done again + ], ); } @@ -1071,19 +1110,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/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 From bf4e0a7f04bf9fbc9965e576895d7c8b38138421 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 11:07:11 +0100 Subject: [PATCH 06/10] husbandry --- client/src/collect_ui.rs | 2 +- server/src/collect.rs | 7 +++++-- server/src/content/advances.rs | 10 +++++----- server/src/game.rs | 6 +++--- server/src/player_events.rs | 2 +- server/tests/game_api_tests.rs | 5 +++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/client/src/collect_ui.rs b/client/src/collect_ui.rs index d787c93..d06649a 100644 --- a/client/src/collect_ui.rs +++ b/client/src/collect_ui.rs @@ -1,5 +1,5 @@ -use std::collections::HashSet; use std::collections::HashMap; +use std::collections::HashSet; use crate::client_state::{ActiveDialog, StateUpdate}; use crate::dialog_ui::{ diff --git a/server/src/collect.rs b/server/src/collect.rs index 621ced2..ed3b6de 100644 --- a/server/src/collect.rs +++ b/server/src/collect.rs @@ -1,4 +1,5 @@ use crate::game::Game; +use crate::map::Terrain; use crate::map::Terrain::{Fertile, Forest, Mountain}; use crate::playing_actions::Collect; use crate::position::Position; @@ -6,7 +7,6 @@ use crate::resource_pile::ResourcePile; use std::collections::{HashMap, HashSet}; use std::iter; use std::ops::Add; -use crate::map::Terrain; /// /// # Panics @@ -121,7 +121,10 @@ pub fn possible_resource_collections( game, ); for (pos, pile) in used { - collect_options.entry(*pos).or_default().insert(pile.clone()); + collect_options + .entry(*pos) + .or_default() + .insert(pile.clone()); // collect_options.insert(*pos, vec![pile.clone()]); } diff --git a/server/src/content/advances.rs b/server/src/content/advances.rs index 4efe5d5..e81a2c4 100644 --- a/server/src/content/advances.rs +++ b/server/src/content/advances.rs @@ -2,7 +2,6 @@ use super::custom_actions::CustomActionType::*; use crate::action::Action; use crate::advance::AdvanceBuilder; use crate::collect::CollectContext; -use crate::map::Terrain; use crate::playing_actions::{PlayingAction, PlayingActionType}; use crate::position::Position; use crate::{ @@ -92,7 +91,7 @@ fn agriculture() -> Vec { ) .add_player_event_listener( |event| &mut event.terrain_collect_options, - |m,_,_| { + |m,(),()| { m.insert(Barren, HashSet::from([ResourcePile::food(1)])); }, 0, @@ -166,12 +165,13 @@ fn husbandry_collect( return; } - &game.map + 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()); + options.insert(*pos, c.terrain_options.get(t).cloned().unwrap_or_default()); }); } @@ -228,7 +228,7 @@ fn fishing_collect( ResourcePile::food(1), ResourcePile::gold(1), ResourcePile::mood_tokens(1), - ] ) + ]) } else { HashSet::from([ResourcePile::food(1)]) }, diff --git a/server/src/game.rs b/server/src/game.rs index eb87784..d9ef695 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -311,7 +311,7 @@ impl Game { 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() { @@ -375,7 +375,7 @@ impl Game { fn on_execute_or_redo(&mut self, action: &Action, player_index: usize) { self.players[player_index].take_events(|events, player| { - events.on_execute_action.trigger(player, &action, &()); + events.on_execute_action.trigger(player, action, &()); }); } @@ -383,7 +383,7 @@ impl Game { let action = &self.action_log[self.action_log_index - 1]; self.players[player_index].take_events(|events, player| { - events.on_undo_action.trigger(player, &action, &()); + events.on_undo_action.trigger(player, action, &()); }); match action { diff --git a/server/src/player_events.rs b/server/src/player_events.rs index c506a5b..e11616a 100644 --- a/server/src/player_events.rs +++ b/server/src/player_events.rs @@ -1,6 +1,7 @@ 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::{ @@ -8,7 +9,6 @@ use crate::{ resource_pile::ResourcePile, wonder::Wonder, }; use std::collections::{HashMap, HashSet}; -use crate::map::Terrain; #[derive(Default)] pub(crate) struct PlayerEvents { diff --git a/server/tests/game_api_tests.rs b/server/tests/game_api_tests.rs index 713487d..c59fd36 100644 --- a/server/tests/game_api_tests.rs +++ b/server/tests/game_api_tests.rs @@ -455,12 +455,13 @@ impl TestAction { } fn test_actions(name: &str, player_index: usize, actions: Vec) { - let outcome: fn(name: &str, i: usize) -> String = |name, i| + 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() From eb33685eba3174e769d514de190528dab77f5ede Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 11:18:37 +0100 Subject: [PATCH 07/10] husbandry --- server/src/ability_initializer.rs | 32 +++++++++++++++++++++++++++++++ server/src/content/advances.rs | 19 +----------------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/server/src/ability_initializer.rs b/server/src/ability_initializer.rs index 0bd6782..070bcd2 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,37 @@ 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.on_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.on_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/content/advances.rs b/server/src/content/advances.rs index e81a2c4..022bee0 100644 --- a/server/src/content/advances.rs +++ b/server/src/content/advances.rs @@ -107,24 +107,7 @@ fn agriculture() -> Vec { husbandry_collect, 0, ) - .add_player_event_listener( // todo extract to a function - |event| &mut event.on_execute_action, - |player, action, ()| { - if is_husbandry_action(action) { - player.played_once_per_turn_effects.push("Husbandry".to_string()); - } - }, - 0 - ) - .add_player_event_listener( - |event| &mut event.on_undo_action, - |player, action, ()| { - if is_husbandry_action(action) { - player.played_once_per_turn_effects.retain(|a| a != "Husbandry"); - } - }, - 0 - ) + .add_once_per_turn_effect("Husbandry", is_husbandry_action) ], ) } From 92fb6df4651c98a735b16431740464f8c6eb0e1d Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 11:20:25 +0100 Subject: [PATCH 08/10] husbandry --- server/src/ability_initializer.rs | 4 ++-- server/src/game.rs | 10 +++++----- server/src/player_events.rs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/server/src/ability_initializer.rs b/server/src/ability_initializer.rs index 070bcd2..6318d27 100644 --- a/server/src/ability_initializer.rs +++ b/server/src/ability_initializer.rs @@ -60,7 +60,7 @@ pub trait AbilityInitializerSetup: Sized { let name2 = name.to_string(); let name3 = name.to_string(); self.add_player_event_listener( - |event| &mut event.on_execute_action, + |event| &mut event.after_execute_action, move |player, action, ()| { if pred2(action) { player @@ -71,7 +71,7 @@ pub trait AbilityInitializerSetup: Sized { 0, ) .add_player_event_listener( - |event| &mut event.on_undo_action, + |event| &mut event.before_undo_action, move |player, action, ()| { if pred(action) { player diff --git a/server/src/game.rs b/server/src/game.rs index d9ef695..e0be1ea 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -369,13 +369,13 @@ impl Game { } Finished => panic!("actions can't be executed when the game is finished"), } - self.on_execute_or_redo(©, player_index); + self.after_execute_or_redo(©, player_index); check_for_waste(self, player_index); } - fn on_execute_or_redo(&mut self, action: &Action, player_index: usize) { + fn after_execute_or_redo(&mut self, action: &Action, player_index: usize) { self.players[player_index].take_events(|events, player| { - events.on_execute_action.trigger(player, action, &()); + events.after_execute_action.trigger(player, action, &()); }); } @@ -383,7 +383,7 @@ impl Game { let action = &self.action_log[self.action_log_index - 1]; self.players[player_index].take_events(|events, player| { - events.on_undo_action.trigger(player, action, &()); + events.before_undo_action.trigger(player, action, &()); }); match action { @@ -460,7 +460,7 @@ impl Game { Action::Redo => panic!("redo action can't be redone"), } self.action_log_index += 1; - self.on_execute_or_redo(©, player_index); + self.after_execute_or_redo(©, player_index); check_for_waste(self, player_index); } diff --git a/server/src/player_events.rs b/server/src/player_events.rs index e11616a..6ccb72b 100644 --- a/server/src/player_events.rs +++ b/server/src/player_events.rs @@ -18,10 +18,10 @@ pub(crate) struct PlayerEvents { pub on_construct_wonder: EventMut, pub on_undo_construct_wonder: EventMut, pub wonder_cost: EventMut, - pub on_advance: EventMut, //todo use on_execute + pub on_advance: EventMut, pub on_undo_advance: EventMut, - pub on_execute_action: EventMut, - pub on_undo_action: EventMut, + pub after_execute_action: EventMut, + pub before_undo_action: EventMut, pub advance_cost: EventMut, pub is_playing_action_available: EventMut, pub terrain_collect_options: EventMut>, (), ()>, From 91c8a1435a7a0514121b09e644f239751eeab278 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 11:32:56 +0100 Subject: [PATCH 09/10] fix fishing --- server/src/content/advances.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/content/advances.rs b/server/src/content/advances.rs index 022bee0..1ebfd74 100644 --- a/server/src/content/advances.rs +++ b/server/src/content/advances.rs @@ -198,15 +198,17 @@ fn fishing_collect( .get_any_city(c.city_position) .expect("city should exist"); let port = city.port_position; - if let Some(position) = port.or_else(|| { + 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(|pos| game.map.is_water(*pos)) + .find(|p| game.map.is_water(*p) && game.enemy_player(c.player_index, *p).is_none()) }) { options.insert( position, - if port.is_some() { + if Some(position) == port { HashSet::from([ ResourcePile::food(1), ResourcePile::gold(1), From 99c8d601685a4d0ade530c7c75d39470d804cfa0 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 26 Jan 2025 11:34:23 +0100 Subject: [PATCH 10/10] fix fishing --- server/src/ability_initializer.rs | 10 +++------- server/src/content/advances.rs | 19 +++++++++---------- server/src/player_events.rs | 2 +- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/server/src/ability_initializer.rs b/server/src/ability_initializer.rs index 6318d27..9638e67 100644 --- a/server/src/ability_initializer.rs +++ b/server/src/ability_initializer.rs @@ -54,7 +54,7 @@ pub trait AbilityInitializerSetup: Sized { fn add_once_per_turn_effect

(self, name: &str, pred: P) -> Self where - P: Fn(&Action) -> bool + 'static + Clone, + P: Fn(&Action) -> bool + 'static + Clone, { let pred2 = pred.clone(); let name2 = name.to_string(); @@ -63,9 +63,7 @@ pub trait AbilityInitializerSetup: Sized { |event| &mut event.after_execute_action, move |player, action, ()| { if pred2(action) { - player - .played_once_per_turn_effects - .push(name2.to_string()); + player.played_once_per_turn_effects.push(name2.to_string()); } }, 0, @@ -74,9 +72,7 @@ pub trait AbilityInitializerSetup: Sized { |event| &mut event.before_undo_action, move |player, action, ()| { if pred(action) { - player - .played_once_per_turn_effects - .retain(|a| a != &name3); + player.played_once_per_turn_effects.retain(|a| a != &name3); } }, 0, diff --git a/server/src/content/advances.rs b/server/src/content/advances.rs index 1ebfd74..44a2a8c 100644 --- a/server/src/content/advances.rs +++ b/server/src/content/advances.rs @@ -148,8 +148,7 @@ fn husbandry_collect( return; } - game - .map + game.map .tiles .iter() .filter(|(pos, t)| pos.distance(c.city_position) == 2 && t.is_land()) @@ -198,14 +197,14 @@ fn fishing_collect( .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()) - }) { + 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 { diff --git a/server/src/player_events.rs b/server/src/player_events.rs index 6ccb72b..30ad24d 100644 --- a/server/src/player_events.rs +++ b/server/src/player_events.rs @@ -18,7 +18,7 @@ pub(crate) struct PlayerEvents { pub on_construct_wonder: EventMut, pub on_undo_construct_wonder: EventMut, pub wonder_cost: EventMut, - pub on_advance: EventMut, + pub on_advance: EventMut, pub on_undo_advance: EventMut, pub after_execute_action: EventMut, pub before_undo_action: EventMut,