diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 45a511a2..2fd13871 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -75,18 +75,18 @@ pub enum Action { // The first value in all these is the color of the player. Roll(u8, Option<(u8, u8)>), // None. Log instead sets it to (int, int) rolled. MoveRobber(u8, Coordinate, Option), // Log has extra element of card stolen. - Discard(u8), // value is None|Resource[]. + Discard(u8), // value is None|Resource[]. BuildRoad(u8, EdgeId), BuildSettlement(u8, NodeId), BuildCity(u8, NodeId), BuyDevelopmentCard(u8), // value is None. Log value is card. PlayKnight(u8), - PlayYearOfPlenty(u8, (Option, Option)), - PlayMonopoly(u8, u8), // value is Resource + PlayYearOfPlenty(u8, [u8; 2]), // Two resources to take from bank + PlayMonopoly(u8, u8), // value is Resource PlayRoadBuilding(u8), // First element of tuples is in, last is out. - MaritimeTrade(u8, (FreqDeck, u8)), + MaritimeTrade(u8, (u8, u8, u8)), // (Give Resource, Get Resource, Ratio) OfferTrade(u8, (FreqDeck, FreqDeck)), AcceptTrade(u8, (FreqDeck, FreqDeck)), RejectTrade(u8), diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 0e740177..06b60b5e 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -167,6 +167,13 @@ impl MapInstance { pub fn get_adjacent_tiles(&self, node_id: NodeId) -> Option<&Vec> { self.adjacent_land_tiles.get(&node_id) } + + pub fn get_tiles_by_number(&self, number: u8) -> Vec<&LandTile> { + self.land_tiles + .values() + .filter(|&tile| tile.number == Some(number)) + .collect() + } } impl MapInstance { @@ -192,7 +199,7 @@ impl MapInstance { let mut autoinc = 0; let mut tile_autoinc = 0; let mut port_autoinc = 0; - + for (&coordinate, &tile_slot) in map_template.topology.iter() { let (nodes, edges, new_autoinc) = get_nodes_edges(&hexagons, coordinate, autoinc); autoinc = new_autoinc; diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 597217a2..8c6ecc8d 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -9,9 +9,9 @@ use crate::{ map_instance::{EdgeId, MapInstance, NodeId}, state_vector::{ actual_victory_points_index, initialize_state, player_devhand_slice, player_hand_slice, - seating_order_slice, StateVector, CURRENT_TICK_SEAT_INDEX, FREE_ROADS_AVAILABLE_INDEX, - HAS_PLAYED_DEV_CARD, HAS_ROLLED_INDEX, IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, - IS_MOVING_ROBBER_INDEX, + player_played_devhand_slice, seating_order_slice, StateVector, BANK_RESOURCE_SLICE, + CURRENT_TICK_SEAT_INDEX, FREE_ROADS_AVAILABLE_INDEX, HAS_PLAYED_DEV_CARD, HAS_ROLLED_INDEX, + IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, IS_MOVING_ROBBER_INDEX, }, }; @@ -39,6 +39,8 @@ pub struct State { connected_components: HashMap>>, longest_road_color: Option, longest_road_length: u8, + largest_army_color: Option, + largest_army_count: u8, } mod move_application; @@ -56,6 +58,8 @@ impl State { let connected_components = HashMap::new(); let longest_road_color = None; let longest_road_length = 0; + let largest_army_color = None; + let largest_army_count = 0; Self { config, @@ -69,6 +73,8 @@ impl State { connected_components, longest_road_color, longest_road_length, + largest_army_color, + largest_army_count, } } @@ -134,7 +140,8 @@ impl State { pub fn can_play_dev(&self, dev_card: u8) -> bool { let color = self.get_current_color(); let dev_card_index = dev_card as usize; - let has_one = self.vector[player_devhand_slice(self.config.num_players, color)][dev_card_index] > 0; + let has_one = + self.vector[player_devhand_slice(self.config.num_players, color)][dev_card_index] > 0; let has_played_in_turn = self.vector[HAS_PLAYED_DEV_CARD] == 1; has_one && !has_played_in_turn } @@ -234,7 +241,7 @@ impl State { buildable.into_iter().collect() } - pub fn buildable_node_ids(&self, color: u8,) -> Vec { + pub fn buildable_node_ids(&self, color: u8) -> Vec { let road_subgraphs = match self.connected_components.get(&color) { Some(components) => components, None => &vec![], @@ -245,7 +252,8 @@ impl State { road_connected_nodes.extend(component); } - road_connected_nodes.intersection(&self.board_buildable_ids) + road_connected_nodes + .intersection(&self.board_buildable_ids) .copied() .collect() } @@ -317,12 +325,23 @@ impl State { // Move forward current_path.push(edge); - self.dfs_longest_path(neighbor, Some(node), connected_set, color, current_path, best_path); + self.dfs_longest_path( + neighbor, + Some(node), + connected_set, + color, + current_path, + best_path, + ); current_path.pop(); } } - pub fn longest_acyclic_path(&self, connected_node_set: &HashSet, color: u8) -> Vec { + pub fn longest_acyclic_path( + &self, + connected_node_set: &HashSet, + color: u8, + ) -> Vec { if connected_node_set.is_empty() { return vec![]; } @@ -333,13 +352,84 @@ impl State { let mut current_path = Vec::new(); let mut best_path = Vec::new(); - self.dfs_longest_path(start_node, None, connected_node_set, color, &mut current_path, &mut best_path); + self.dfs_longest_path( + start_node, + None, + connected_node_set, + color, + &mut current_path, + &mut best_path, + ); if best_path.len() > overall_best_path.len() { overall_best_path = best_path; } } overall_best_path } + + pub fn add_dev_card(&mut self, color: u8, card_idx: usize) { + self.vector[player_devhand_slice(self.config.num_players, color)][card_idx] += 1; + } + + pub fn get_dev_card_count(&self, color: u8, card_idx: usize) -> u8 { + self.vector[player_devhand_slice(self.config.num_players, color)][card_idx] + } + + pub fn get_played_dev_card_count(&self, color: u8, card_idx: usize) -> u8 { + self.vector[player_played_devhand_slice(self.config.num_players, color)][card_idx] + } + + pub fn add_played_dev_card(&mut self, color: u8, card_idx: usize) { + self.vector[player_played_devhand_slice(self.config.num_players, color)][card_idx] += 1; + } + + pub fn remove_dev_card(&mut self, color: u8, card_idx: usize) { + self.vector[player_devhand_slice(self.config.num_players, color)][card_idx] -= 1; + } + + pub fn set_has_played_dev_card(&mut self) { + self.vector[HAS_PLAYED_DEV_CARD] = 1; + } + + pub fn set_is_moving_robber(&mut self) { + self.vector[IS_MOVING_ROBBER_INDEX] = 1; + } + + pub fn clear_is_moving_robber(&mut self) { + self.vector[IS_MOVING_ROBBER_INDEX] = 0; + } + + pub fn bank_has_resource(&self, resource: u8) -> bool { + self.vector[BANK_RESOURCE_SLICE][resource as usize] > 0 + } + + pub fn take_from_bank_give_to_player(&mut self, color: u8, resource: u8) { + let resource_idx = resource as usize; + self.vector[BANK_RESOURCE_SLICE][resource_idx] -= 1; + self.get_mut_player_hand(color)[resource_idx] += 1; + } + + pub fn take_from_player_give_to_bank(&mut self, color: u8, resource: u8, amount: u8) { + let resource_idx = resource as usize; + self.get_mut_player_hand(color)[resource_idx] -= amount; + self.vector[BANK_RESOURCE_SLICE][resource_idx] += amount; + } + + pub fn get_player_resource_count(&self, color: u8, resource: u8) -> u8 { + self.get_player_hand(color)[resource as usize] + } + + pub fn take_from_player_give_to_player( + &mut self, + from_color: u8, + to_color: u8, + resource: u8, + amount: u8, + ) { + let resource_idx = resource as usize; + self.get_mut_player_hand(from_color)[resource_idx] -= amount; + self.get_mut_player_hand(to_color)[resource_idx] += amount; + } } #[cfg(test)] @@ -361,7 +451,7 @@ mod tests { assert!(!state.is_moving_robber()); assert!(!state.is_discarding()); } - + #[test] fn test_longest_acyclic_path() { let mut state = State::new_base(); diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 870c4a5e..9f2f38e3 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -16,16 +16,14 @@ impl State { pub fn apply_action(&mut self, action: Action) { match action { Action::BuildSettlement(color, node_id) => { - let (new_owner, new_length) = - self.build_settlement(color, node_id); + let (new_owner, new_length) = self.build_settlement(color, node_id); self.maintain_longest_road(new_owner, new_length); } Action::BuildRoad(color, edge_id) => { - let (new_owner, new_length) = - self.build_road(color, edge_id); + let (new_owner, new_length) = self.build_road(color, edge_id); self.maintain_longest_road(new_owner, new_length); } - Action::BuildCity(color, node_id) =>{ + Action::BuildCity(color, node_id) => { self.build_city(color, node_id); } Action::BuyDevelopmentCard(color) => { @@ -40,6 +38,25 @@ impl State { Action::MoveRobber(color, coord, victim_opt) => { self.move_robber(color, coord, victim_opt); } + Action::PlayKnight(color) => { + self.play_knight(color); + self.maintain_largest_army(); + } + Action::PlayYearOfPlenty(color, resources) => { + self.play_year_of_plenty(color, resources); + } + Action::PlayMonopoly(color, resource) => { + self.play_monopoly(color, resource); + } + Action::PlayRoadBuilding(color) => { + self.play_road_building(color); + } + Action::MaritimeTrade(color, (give, take, ratio)) => { + self.maritime_trade(color, give, take, ratio); + } + Action::EndTurn(color) => { + self.end_turn(color); + } _ => { panic!("Action not implemented: {:?}", action); } @@ -124,17 +141,23 @@ impl State { let mut plowed_edges_by_color: HashMap> = HashMap::new(); for edge in self.map_instance.get_neighbor_edges(node_id) { if let Some(&road_color) = self.roads.get(&edge) { - plowed_edges_by_color.entry(road_color).or_default().push(edge); + plowed_edges_by_color + .entry(road_color) + .or_default() + .push(edge); } } for (plowed_color, plowed_edges) in plowed_edges_by_color { - if plowed_edges.len() != 2 || plowed_color == placing_color { + if plowed_edges.len() != 2 || plowed_color == placing_color { continue; // Skip if no bisection/plow } - if let Some(plowed_component_idx) = self.get_connected_component_index(plowed_color, node_id) { - let outer_nodes: Vec = plowed_edges.iter() + if let Some(plowed_component_idx) = + self.get_connected_component_index(plowed_color, node_id) + { + let outer_nodes: Vec = plowed_edges + .iter() .map(|&edge| if edge.0 == node_id { edge.1 } else { edge.0 }) .collect(); @@ -156,7 +179,8 @@ impl State { // Insert the longest road length for all colors if a road was plowed for (&color, components) in &self.connected_components { - let max_length = components.iter() + let max_length = components + .iter() .map(|component| self.longest_acyclic_path(component, color).len()) .max() .unwrap_or(0); @@ -175,13 +199,14 @@ impl State { // If no road lengths affected, just return the previous longest road (self.longest_road_color, self.longest_road_length) } else { - let max_entry = road_lengths.iter() - .filter(|(_, &len)| len >= 5) - .max_by_key(|(_, &len)| len); + let max_entry = road_lengths + .iter() + .filter(|(_, &len)| len >= 5) + .max_by_key(|(_, &len)| len); match max_entry { Some((&color, &length)) => (Some(color), length), - None => (None, 0) // No player has >= 5 roads + None => (None, 0), // No player has >= 5 roads } }; (new_road_color, new_road_length) @@ -192,7 +217,7 @@ impl State { self.roads.insert(edge_id, placing_color); self.roads.insert(inverted_edge, placing_color); self.roads_by_color[placing_color as usize] += 1; - + let is_initial_build_phase = self.is_initial_build_phase(); let is_free = is_initial_build_phase || self.is_road_building(); if !is_free { @@ -226,41 +251,62 @@ impl State { let affected_component = if a_index.is_none() && !self.is_enemy_node(placing_color, a) { // There has to be a component from b (since roads can only be built in a connected fashion) - let component = self.connected_components.get_mut(&placing_color).unwrap() - .get_mut(b_index.unwrap()).unwrap(); + let component = self + .connected_components + .get_mut(&placing_color) + .unwrap() + .get_mut(b_index.unwrap()) + .unwrap(); component.insert(a); // extend said component by 1 more node component.clone() } else if b_index.is_none() && !self.is_enemy_node(placing_color, b) { // There has to be a component from a (since roads can only be built in a connected fashion) - let component = self.connected_components.get_mut(&placing_color).unwrap() - .get_mut(a_index.unwrap()).unwrap(); + let component = self + .connected_components + .get_mut(&placing_color) + .unwrap() + .get_mut(a_index.unwrap()) + .unwrap(); component.insert(b); component.clone() } else if a_index.is_some() && b_index.is_some() && a_index != b_index { // Merge components into one and delete the other let smaller_idx = a_index.unwrap().min(b_index.unwrap()); let larger_idx = a_index.unwrap().max(b_index.unwrap()); - let removed_component = self.connected_components.get_mut(&placing_color).unwrap() + let removed_component = self + .connected_components + .get_mut(&placing_color) + .unwrap() .remove(larger_idx); - let kept_component = self.connected_components.get_mut(&placing_color).unwrap() - .get_mut(smaller_idx).unwrap(); + let kept_component = self + .connected_components + .get_mut(&placing_color) + .unwrap() + .get_mut(smaller_idx) + .unwrap(); kept_component.extend(&removed_component); kept_component.clone() } else { // Edge is within same component, just get that component // In this case, a_index == b_index, which means that the edge // is already part of one component. No actions needed. - self.connected_components.get(&placing_color).unwrap() - .get(a_index.unwrap()).unwrap().clone() + self.connected_components + .get(&placing_color) + .unwrap() + .get(a_index.unwrap()) + .unwrap() + .clone() }; - + let prev_road_color = self.longest_road_color; - + // Calculate length for affected component - let path_length = self.longest_acyclic_path(&affected_component, placing_color).len() as u8; - - let (new_road_color, new_road_length) = - if path_length >= 5 && path_length > self.longest_road_length{ + let path_length = self + .longest_acyclic_path(&affected_component, placing_color) + .len() as u8; + + let (new_road_color, new_road_length) = + if path_length >= 5 && path_length > self.longest_road_length { (Some(placing_color), path_length) } else { (prev_road_color, self.longest_road_length) @@ -269,9 +315,10 @@ impl State { } fn build_city(&mut self, color: u8, node_id: u8) { - self.buildings.insert(node_id, Building::City(color, node_id)); + self.buildings + .insert(node_id, Building::City(color, node_id)); let buildings = self.buildings_by_color.entry(color).or_default(); - if let Some (pos) = buildings.iter().position(|b| { + if let Some(pos) = buildings.iter().position(|b| { if let Building::Settlement(_, n) = b { *n == node_id } else { @@ -306,7 +353,8 @@ impl State { self.add_victory_points(color, 1); } _ => { - let dev_hand = &mut self.vector[player_devhand_slice(self.config.num_players, color)]; + let dev_hand = + &mut self.vector[player_devhand_slice(self.config.num_players, color)]; dev_hand[card as usize] += 1; } } @@ -326,40 +374,153 @@ impl State { let total = die1 + die2; if total == 7 { - let discarders: Vec = (0..self.get_num_players()) - .map(|c| { - let player_hand = self.get_player_hand(c); - let total_cards: u8 = player_hand.iter().sum(); - total_cards > self.config.discard_limit + self.handle_roll_seven(color); + } else { + self.distribute_roll_yields(total); + self.vector[CURRENT_TICK_SEAT_INDEX] = color; + } + } + + fn handle_roll_seven(&mut self, color: u8) { + // Check who needs to discard + let discarders: Vec = (0..self.get_num_players()) + .map(|c| { + let player_hand = self.get_player_hand(c); + let total_cards: u8 = player_hand.iter().sum(); + total_cards > self.config.discard_limit }) .collect(); - - let should_enter_discard_phase = discarders.iter().any(|&x| x); - if should_enter_discard_phase { - if let Some(first_discarder) = discarders.iter().position(|&x| x) { - self.vector[CURRENT_TICK_SEAT_INDEX] = first_discarder as u8; - self.vector[IS_DISCARDING_INDEX] = 1; - } - } else { - self.vector[IS_MOVING_ROBBER_INDEX] = 1; - self.vector[CURRENT_TICK_SEAT_INDEX] = color; + + let should_enter_discard_phase = discarders.iter().any(|&x| x); + if should_enter_discard_phase { + if let Some(first_discarder) = discarders.iter().position(|&x| x) { + self.vector[CURRENT_TICK_SEAT_INDEX] = first_discarder as u8; + self.vector[IS_DISCARDING_INDEX] = 1; } } else { - // TODO: Yield resources + self.vector[IS_MOVING_ROBBER_INDEX] = 1; self.vector[CURRENT_TICK_SEAT_INDEX] = color; } - // TODO: Set playable_actions??? } + // Returns Vec of (color, resource_index, amount) tuples for what each player should receive + fn collect_roll_yields(&self, roll: u8) -> Vec<(u8, usize, u8)> { + let mut all_yields = Vec::new(); + let matching_tiles = self.map_instance.get_tiles_by_number(roll); + + for tile in matching_tiles { + // Skip robber tile + if self.vector[ROBBER_TILE_INDEX] == tile.id { + continue; + } + + if let Some(resource) = tile.resource { + let resource_idx = resource as usize; + // Collect all yields for this tile + for &node_id in tile.hexagon.nodes.values() { + if let Some(building) = self.buildings.get(&node_id) { + match building { + Building::Settlement(owner_color, _) => { + all_yields.push((*owner_color, resource_idx, 1)); + } + Building::City(owner_color, _) => { + all_yields.push((*owner_color, resource_idx, 2)); + } + } + } + } + } + } + all_yields + } + + fn distribute_roll_yields(&mut self, roll: u8) { + let yields = self.collect_roll_yields(roll); + if yields.is_empty() { + return; + } + + // Calculate total needed by resource type + let mut resource_needs = [0u8; 5]; + for (_, resource_idx, amount) in &yields { + resource_needs[*resource_idx] += amount; + } + + // Check what can be allocated from bank + let bank = &self.vector[BANK_RESOURCE_SLICE]; + let mut distributable = resource_needs; + let mut insufficient = false; + let multiple_recipients = yields + .iter() + .map(|(color, _, _)| color) + .collect::>() + .len() + > 1; + + for i in 0..5 { + if bank[i] < resource_needs[i] { + if multiple_recipients { + // If not enough for everyone, no one gets anything + return; + } + distributable[i] = bank[i]; + insufficient = true; + } + } + + // If we got here, we can distribute something + if insufficient { + // Single player case - give what we can + for (owner_color, resource_idx, amount) in yields { + let available = distributable[resource_idx].min(amount); + if available > 0 { + self.vector[BANK_RESOURCE_SLICE][resource_idx] -= available; + self.get_mut_player_hand(owner_color)[resource_idx] += available; + } + } + } else { + // Full distribution case + for (owner_color, resource_idx, amount) in yields { + self.vector[BANK_RESOURCE_SLICE][resource_idx] -= amount; + self.get_mut_player_hand(owner_color)[resource_idx] += amount; + } + } + } + + /* + * TODO: For now, we're not letting players choose what to discard, to avoid + * the combinatorial explosion of possibilities. Instead, we'll just + * force discards in a way that maximizes resource diversity. + */ fn discard(&mut self, color: u8) { - todo!(); + let mut remaining_hand = self.get_player_hand(color).to_vec(); + let total_cards: u8 = remaining_hand.iter().sum(); + let mut to_discard = total_cards - (total_cards / 2); + let mut discarded = [0u8; 5]; + + while to_discard > 0 { + // Find highest frequency resources + let max_count = *remaining_hand.iter().max().unwrap(); + let max_indices: Vec<_> = (0..5).filter(|&i| remaining_hand[i] == max_count).collect(); + + // Take one card from each highest frequency resource + for &i in &max_indices { + if to_discard > 0 { + remaining_hand[i] -= 1; + discarded[i] += 1; + to_discard -= 1; + } + } + } + + freqdeck_sub(self.get_mut_player_hand(color), discarded); + freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], discarded); + self.vector[IS_DISCARDING_INDEX] = 0; + // TODO: Advance turn; handle discarders left and pass turn to original roller } fn move_robber(&mut self, color: u8, coordinate: (i8, i8, i8), victim_opt: Option) { - self.vector[ROBBER_TILE_INDEX] = self.map_instance - .get_land_tile(coordinate) - .unwrap() - .id; + self.vector[ROBBER_TILE_INDEX] = self.map_instance.get_land_tile(coordinate).unwrap().id; if let Some(victim) = victim_opt { let total_cards: u8 = self.get_player_hand(victim).iter().sum(); @@ -429,6 +590,109 @@ impl State { } visited } + + fn play_knight(&mut self, color: u8) { + // Mark card as played + self.remove_dev_card(color, DevCard::Knight as usize); + self.add_played_dev_card(color, DevCard::Knight as usize); + self.set_has_played_dev_card(); + + // Set state to move robber + self.set_is_moving_robber(); + } + + fn maintain_largest_army(&mut self) { + let prev_owner = self.largest_army_color; + let prev_count = self.largest_army_count; + + // Find player with most knights (if any have 3 or more) + let mut max_knights = 0; + let mut max_knights_color = None; + + for color in 0..self.get_num_players() { + let knights = self.get_played_dev_card_count(color, DevCard::Knight as usize); + if knights >= 3 && knights > max_knights { + max_knights = knights; + max_knights_color = Some(color); + } + } + + // Case where playerB meets playerA's largest army -> no change + if max_knights == prev_count { + return; + } + + self.largest_army_color = max_knights_color; + self.largest_army_count = max_knights; + + // If playerA retains largest army -> no VP changes + if max_knights_color == prev_owner { + return; + } + + if let Some(prev_owner) = prev_owner { + self.sub_victory_points(prev_owner, 2); + } + + if let Some(new_owner) = max_knights_color { + self.add_victory_points(new_owner, 2); + } + } + + fn play_year_of_plenty(&mut self, color: u8, resources: [u8; 2]) { + // Assume move_generation has already checked that player has year of plenty card + // and that bank has enough resources + self.remove_dev_card(color, DevCard::YearOfPlenty as usize); + self.add_played_dev_card(color, DevCard::YearOfPlenty as usize); + self.set_has_played_dev_card(); + + // Give resources to player + for resource in resources { + self.take_from_bank_give_to_player(color, resource); + } + } + + fn play_monopoly(&mut self, color: u8, resource: u8) { + // Assume move_generation has already checked that player has monopoly card. + self.remove_dev_card(color, DevCard::Monopoly as usize); + self.add_played_dev_card(color, DevCard::Monopoly as usize); + self.set_has_played_dev_card(); + + // Steal all resources of type from other players + for victim_color in 0..self.get_num_players() { + if victim_color != color { + let amount = self.get_player_resource_count(victim_color, resource); + if amount > 0 { + self.take_from_player_give_to_player(victim_color, color, resource, amount); + } + } + } + } + + fn play_road_building(&mut self, color: u8) { + // Assume move_generation has already checked that player has road building card. + self.remove_dev_card(color, DevCard::RoadBuilding as usize); + self.add_played_dev_card(color, DevCard::RoadBuilding as usize); + self.set_has_played_dev_card(); + + // Set state for free roads + self.vector[IS_BUILDING_ROAD_INDEX] = 1; + self.vector[FREE_ROADS_AVAILABLE_INDEX] = 2; + } + + fn maritime_trade(&mut self, color: u8, give: u8, take: u8, ratio: u8) { + // Assume move_generation has already checked that player has enough resources + // to give and that bank has enough resources to take + self.take_from_player_give_to_bank(color, give, ratio); + self.take_from_bank_give_to_player(color, take); + } + + fn end_turn(&mut self, _color: u8) { + self.vector[HAS_PLAYED_DEV_CARD] = 0; + self.vector[HAS_ROLLED_INDEX] = 0; + + self.advance_turn(1); + } } #[cfg(test)] @@ -477,7 +741,7 @@ mod tests { ); assert_eq!(state.board_buildable_ids.len(), 50); assert_eq!(state.get_actual_victory_points(color), 1); - + let hand_after = state.get_player_hand(color); for i in 0..5 { assert_eq!(hand_after[i], hand_before[i] - SETTLEMENT_COST[i]); @@ -562,7 +826,7 @@ mod tests { // give color1 6 consecutive roads state.apply_action(Action::BuildSettlement(color1, 0)); - for edge in [(0,1), (1,2), (2,3), (3,4), (4,5), (5,16)] { + for edge in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 16)] { state.apply_action(Action::BuildRoad(color1, edge)); } @@ -621,12 +885,12 @@ mod tests { // give color1 6 consecutive roads state.apply_action(Action::BuildSettlement(color1, 0)); - for edge in [(0,1), (1,2), (2,3), (3,4), (4,5), (5,16)] { + for edge in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 16)] { state.apply_action(Action::BuildRoad(color1, edge)); } // Give color2 5 consecutive roads with potential to bisect/plow color1's road state.apply_action(Action::BuildSettlement(color2, 11)); - for edge in [(11,12),(12,13),(13,14),(14,15),(4,15)] { + for edge in [(11, 12), (12, 13), (13, 14), (14, 15), (4, 15)] { state.apply_action(Action::BuildRoad(color2, edge)); } @@ -648,9 +912,9 @@ mod tests { fn test_extend_own_longest_road() { let mut state = State::new_base(); let color1 = 1; - + state.apply_action(Action::BuildSettlement(color1, 0)); - for edge in [(0,1), (1,2), (2,3), (3,4), (4,5)] { + for edge in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)] { state.apply_action(Action::BuildRoad(color1, edge)); } @@ -672,7 +936,7 @@ mod tests { let color2 = 2; state.apply_action(Action::BuildSettlement(color1, 0)); - for edge in [(0,1), (1,2), (2,3), (3,4), (4,5), (5,16)] { + for edge in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 16)] { state.apply_action(Action::BuildRoad(color1, edge)); } @@ -721,11 +985,17 @@ mod tests { if drawn_card == Some(DevCard::VictoryPoint) { // VP added, devhand not incremented assert_eq!(state.get_actual_victory_points(color), initial_vps + 1); - assert_eq!(devhand_after[drawn_card.unwrap() as usize], initial_devhand[drawn_card.unwrap() as usize]); + assert_eq!( + devhand_after[drawn_card.unwrap() as usize], + initial_devhand[drawn_card.unwrap() as usize] + ); } else { // VP not added, devhand incremented assert_eq!(state.get_actual_victory_points(color), initial_vps); - assert_eq!(devhand_after[drawn_card.unwrap() as usize], initial_devhand[drawn_card.unwrap() as usize] + 1); + assert_eq!( + devhand_after[drawn_card.unwrap() as usize], + initial_devhand[drawn_card.unwrap() as usize] + 1 + ); } } else { // 26th card should not be drawn @@ -735,4 +1005,519 @@ mod tests { } } } + + #[test] + fn test_roll_yields_resources() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + state.build_settlement(color, 0); + + let adjacent_tiles = state.map_instance.get_adjacent_tiles(0).unwrap(); + + let mut chosen_roll = None; + let mut expected_resource_yields = [0; 5]; + + for tile in adjacent_tiles.iter() { + if let (Some(number), Some(resource)) = (tile.number, tile.resource) { + // First valid number we find will be our roll + // Don't pick robber tile + if tile.id != state.vector[ROBBER_TILE_INDEX] { + if chosen_roll.is_none() { + chosen_roll = Some(number); + } + + if Some(number) == chosen_roll { + expected_resource_yields[resource as usize] += 1; + } + } + } + } + + let initial_bank = state.vector[BANK_RESOURCE_SLICE].to_vec(); + let initial_hand = state.get_player_hand(color).to_vec(); + // Roll numbers should sum to chosen_roll + let roll_numbers = (chosen_roll.unwrap() / 2, (chosen_roll.unwrap() + 1) / 2); + + state.apply_action(Action::Roll(color, Some(roll_numbers))); + + for resource_idx in 0..5 { + assert_eq!( + state.vector[BANK_RESOURCE_SLICE][resource_idx], + initial_bank[resource_idx] - expected_resource_yields[resource_idx], + "Bank should have {} fewer resource of {:?}", + expected_resource_yields[resource_idx], + resource_idx + ); + assert_eq!( + state.get_player_hand(color)[resource_idx], + initial_hand[resource_idx] + expected_resource_yields[resource_idx], + "Player should have {} more resource of {:?}", + expected_resource_yields[resource_idx], + resource_idx + ) + } + } + + #[test] + fn test_roll_city_yields_double() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + freqdeck_add(state.get_mut_player_hand(color), CITY_COST); + state.build_settlement(color, 0); + state.build_city(color, 0); + + let adjacent_tiles = state.map_instance.get_adjacent_tiles(0).unwrap(); + + let mut chosen_roll = None; + let mut expected_resource_yields = [0; 5]; + + for tile in adjacent_tiles.iter() { + if let (Some(number), Some(resource)) = (tile.number, tile.resource) { + // Don't pick robber tile + if tile.id != state.vector[ROBBER_TILE_INDEX] { + if chosen_roll.is_none() { + chosen_roll = Some(number); + } + + if Some(number) == chosen_roll { + expected_resource_yields[resource as usize] += 2; + } + } + } + } + + let initial_bank = state.vector[BANK_RESOURCE_SLICE].to_vec(); + let initial_hand = state.get_player_hand(color).to_vec(); + // Roll numbers should sum to chosen_roll + let roll_numbers = (chosen_roll.unwrap() / 2, (chosen_roll.unwrap() + 1) / 2); + + state.apply_action(Action::Roll(color, Some(roll_numbers))); + + for resource_idx in 0..5 { + assert_eq!( + state.vector[BANK_RESOURCE_SLICE][resource_idx], + initial_bank[resource_idx] - expected_resource_yields[resource_idx], + "Bank should have {} fewer resource of {:?}", + expected_resource_yields[resource_idx], + resource_idx + ); + assert_eq!( + state.get_player_hand(color)[resource_idx], + initial_hand[resource_idx] + expected_resource_yields[resource_idx], + "Player should have {} more resource of {:?}", + expected_resource_yields[resource_idx], + resource_idx + ); + } + } + + #[test] + fn test_roll_single_player_partial_payment_when_insufficient_bank() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + let node_id = 0; + state.build_settlement(color, node_id); + freqdeck_add(state.get_mut_player_hand(color), CITY_COST); + state.build_city(color, node_id); + + let adjacent_tiles = state.map_instance.get_adjacent_tiles(node_id).unwrap(); + + let mut chosen_roll = None; + let mut chosen_resource = None; + + for tile in adjacent_tiles.iter() { + if let (Some(number), Some(resource)) = (tile.number, tile.resource) { + if tile.id != state.vector[ROBBER_TILE_INDEX] && chosen_roll.is_none() { + chosen_roll = Some(number); + chosen_resource = Some(resource); + } + } + } + assert!(chosen_roll.is_some(), "Should find at least one valid tile"); + + for i in 0..5 { + state.vector[BANK_RESOURCE_SLICE][i] = 1; + } + let hand_before = state.get_player_hand(color).to_vec(); + + let roll = chosen_roll.unwrap(); + let roll_numbers = (roll / 2, (roll + 1) / 2); + state.roll_dice(color, Some(roll_numbers)); + + let chosen_resource_idx = chosen_resource.unwrap() as usize; + assert_eq!(state.vector[BANK_RESOURCE_SLICE][chosen_resource_idx], 0); + + assert_eq!( + state.get_player_hand(color)[chosen_resource_idx], + hand_before[chosen_resource_idx] + 1 + ); + assert_eq!(state.vector[BANK_RESOURCE_SLICE][chosen_resource_idx], 0) + } + + #[test] + fn test_roll_multiple_player_no_payment_when_insufficient_bank() { + let mut state = State::new_base(); + let color1 = 1; + let color2 = 2; + + let (resource, number, node1, node2) = { + let tile = state + .map_instance + .get_land_tiles() + .values() + .find(|tile| { + tile.resource.is_some() && // Not a desert + tile.id != state.vector[ROBBER_TILE_INDEX] // Not under robber + }) + .expect("Should be at least one valid tile"); + + let node_ids: Vec<_> = tile.hexagon.nodes.values().take(2).copied().collect(); + + ( + tile.resource.unwrap(), + tile.number.unwrap(), + node_ids[0], + node_ids[1], + ) + }; + + // Place two opposing cities on a shared tile with expected yields + state.build_settlement(color1, node1); + state.build_settlement(color2, node2); + freqdeck_add(state.get_mut_player_hand(color1), CITY_COST); + freqdeck_add(state.get_mut_player_hand(color2), CITY_COST); + state.build_city(color1, node1); + state.build_city(color2, node2); + + // Set bank to have only 1 of the needed resource + let resource_idx = resource as usize; + state.vector[BANK_RESOURCE_SLICE][resource_idx] = 1; + + let bank_before = state.vector[BANK_RESOURCE_SLICE][resource_idx]; + let hand1_before = state.get_player_hand(color1)[resource_idx]; + let hand2_before = state.get_player_hand(color2)[resource_idx]; + + // Roll the shared tile's number + let roll_numbers = (number / 2, (number + 1) / 2); + state.roll_dice(color1, Some(roll_numbers)); + + assert_eq!( + state.vector[BANK_RESOURCE_SLICE][resource_idx], bank_before, + "Bank should be unchanged" + ); + // Neither player should get any resources + assert_eq!( + state.get_player_hand(color1)[resource_idx], + hand1_before, + "Player 1 should not receive resources" + ); + assert_eq!( + state.get_player_hand(color2)[resource_idx], + hand2_before, + "Player 2 should not receive resources" + ); + } + + #[test] + fn test_discard() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + // Give the player a known distribution of 17 cards + freqdeck_add(state.get_mut_player_hand(color), [3, 9, 1, 3, 1]); + + let bank_before = state.vector[BANK_RESOURCE_SLICE].to_vec(); + + state.discard(color); + + // After discarding, the player should have half => 17 / 2 = 8. + let total_after: u8 = state.get_player_hand(color).iter().sum(); + assert_eq!(total_after, 8, "Player should have exactly 8 cards left."); + + // Verify discard phase ended + assert_eq!( + state.vector[IS_DISCARDING_INDEX], 0, + "Discard phase should end." + ); + + // The bank should have received exactly 6 more cards in total + let bank_after = &state.vector[BANK_RESOURCE_SLICE]; + let mut total_discarded = 0; + for i in 0..5 { + total_discarded += bank_after[i] - bank_before[i]; + } + assert_eq!( + total_discarded, 9, + "Exactly 9 cards should have been added to the bank." + ); + + // Check the specific distribution after discard + let final_player_hand = state.get_player_hand(color); + assert_eq!( + final_player_hand, + &[2, 2, 1, 2, 1], + "Discard logic should spread discards across highest-frequency resources first." + ); + } + + #[test] + fn test_play_knight() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + state.add_dev_card(color, DevCard::Knight as usize); + assert_eq!(state.get_dev_card_count(color, DevCard::Knight as usize), 1); + assert_eq!( + state.get_played_dev_card_count(color, DevCard::Knight as usize), + 0 + ); + assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 0); + assert_eq!(state.vector[IS_MOVING_ROBBER_INDEX], 0); + + state.play_knight(color); + + assert_eq!(state.get_dev_card_count(color, DevCard::Knight as usize), 0); + assert_eq!( + state.get_played_dev_card_count(color, DevCard::Knight as usize), + 1 + ); + assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 1); + assert_eq!(state.vector[IS_MOVING_ROBBER_INDEX], 1); + } + + #[test] + fn test_play_knight_largest_army() { + let mut state = State::new_base(); + let color1 = 1; + let color2 = 2; + + // Give first player 3 knight cards + for _ in 0..3 { + state.add_dev_card(color1, DevCard::Knight as usize); + } + + // Play knights and verify largest army + for i in 0..3 { + state.vector[HAS_PLAYED_DEV_CARD] = 0; // Reset for each turn + state.apply_action(Action::PlayKnight(color1)); + + // Verify knight was removed and marked as played + assert_eq!( + state.get_dev_card_count(color1, DevCard::Knight as usize), + 2 - i + ); + assert_eq!( + state.get_played_dev_card_count(color1, DevCard::Knight as usize), + i + 1 + ); + assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 1); + assert_eq!(state.vector[IS_MOVING_ROBBER_INDEX], 1); + + // Check largest army status + if i == 2 { + assert_eq!(state.largest_army_color, Some(color1)); + assert_eq!(state.largest_army_count, 3); + assert_eq!(state.get_actual_victory_points(color1), 2); + assert_eq!(state.get_actual_victory_points(color2), 0); + } else { + assert_eq!(state.largest_army_color, None); + assert_eq!(state.largest_army_count, 0); + assert_eq!(state.get_actual_victory_points(color1), 0); + assert_eq!(state.get_actual_victory_points(color2), 0); + } + } + + // Now give second player 4 knight cards and have them take largest army + for _ in 0..4 { + state.add_dev_card(color2, DevCard::Knight as usize); + } + + // Play knights with second player + for i in 0..4 { + state.vector[HAS_PLAYED_DEV_CARD] = 0; // Reset for each turn + state.apply_action(Action::PlayKnight(color2)); + + // Verify knight was removed and marked as played + assert_eq!( + state.get_dev_card_count(color2, DevCard::Knight as usize), + 3 - i + ); + assert_eq!( + state.get_played_dev_card_count(color2, DevCard::Knight as usize), + i + 1 + ); + + // Check largest army status + if i == 3 { + // After 4th knight, should take largest army + assert_eq!(state.largest_army_color, Some(color2)); + assert_eq!(state.largest_army_count, 4); + assert_eq!(state.get_actual_victory_points(color1), 0); // Lost 2 VPs + assert_eq!(state.get_actual_victory_points(color2), 2); // Gained 2 VPs + } else { + // Still held by first player + assert_eq!(state.largest_army_color, Some(color1)); + assert_eq!(state.largest_army_count, 3); + assert_eq!(state.get_actual_victory_points(color1), 2); + assert_eq!(state.get_actual_victory_points(color2), 0); + } + } + } + + #[test] + fn test_play_year_of_plenty() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + // Give player a year of plenty card + state.add_dev_card(color, DevCard::YearOfPlenty as usize); + + let bank_before = state.vector[BANK_RESOURCE_SLICE].to_vec(); + let hand_before = state.get_player_hand(color).to_vec(); + + // Play year of plenty for wood and brick + state.play_year_of_plenty(color, [0, 1]); + + // Verify card was removed from hand + assert_eq!( + state.get_dev_card_count(color, DevCard::YearOfPlenty as usize), + 0 + ); + + // Verify card was marked as played + assert_eq!( + state.get_played_dev_card_count(color, DevCard::YearOfPlenty as usize), + 1 + ); + assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 1); + + // Verify resources were transferred + assert_eq!(state.vector[BANK_RESOURCE_SLICE][0], bank_before[0] - 1); + assert_eq!(state.vector[BANK_RESOURCE_SLICE][1], bank_before[1] - 1); + assert_eq!(state.get_player_hand(color)[0], hand_before[0] + 1); + assert_eq!(state.get_player_hand(color)[1], hand_before[1] + 1); + } + + #[test] + fn test_play_monopoly() { + let mut state = State::new_base(); + let monopolist_color = state.get_current_color(); + + // Give player a monopoly card + state.add_dev_card(monopolist_color, DevCard::Monopoly as usize); + + // Give other players some wood + for other_color in 0..state.get_num_players() { + if other_color != monopolist_color { + state.get_mut_player_hand(other_color)[0] = 3; + } + } + + let initial_wood = state.get_player_hand(monopolist_color)[0]; + let expected_stolen = 3 * (state.get_num_players() - 1) as u8; // 3 wood from each other player + + // Play monopoly on wood (resource index 0) + state.play_monopoly(monopolist_color, 0); + + assert_eq!( + state.get_dev_card_count(monopolist_color, DevCard::Monopoly as usize), + 0 + ); + assert_eq!( + state.get_played_dev_card_count(monopolist_color, DevCard::Monopoly as usize), + 1 + ); + assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 1); + assert_eq!( + state.get_player_hand(monopolist_color)[0], + initial_wood + expected_stolen + ); + + // Verify other players lost their wood + for other_color in 0..state.get_num_players() { + if other_color != monopolist_color { + assert_eq!(state.get_player_hand(other_color)[0], 0); + } + } + } + + #[test] + fn test_play_road_building() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + // Give player a road building card + state.add_dev_card(color, DevCard::RoadBuilding as usize); + assert_eq!( + state.get_dev_card_count(color, DevCard::RoadBuilding as usize), + 1 + ); + assert_eq!( + state.get_played_dev_card_count(color, DevCard::RoadBuilding as usize), + 0 + ); + assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 0); + + // Play road building card + state.play_road_building(color); + + // Verify card was removed from hand + assert_eq!( + state.get_dev_card_count(color, DevCard::RoadBuilding as usize), + 0 + ); + + // Verify card was marked as played + assert_eq!( + state.get_played_dev_card_count(color, DevCard::RoadBuilding as usize), + 1 + ); + assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 1); + + // Verify state was set for free roads + assert_eq!(state.vector[IS_BUILDING_ROAD_INDEX], 1); + assert_eq!(state.vector[FREE_ROADS_AVAILABLE_INDEX], 2); + } + + #[test] + fn test_maritime_trade_basic_rate() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + state.get_mut_player_hand(color)[0] = 4; // 4 wood + + let initial_bank_brick = state.vector[BANK_RESOURCE_SLICE][1]; + + state.apply_action(Action::MaritimeTrade(color, (0, 1, 4))); + + assert_eq!(state.get_player_hand(color)[0], 0); + assert_eq!(state.get_player_hand(color)[1], 1); + assert_eq!(state.vector[BANK_RESOURCE_SLICE][0], 19 + 4); + assert_eq!(state.vector[BANK_RESOURCE_SLICE][1], initial_bank_brick - 1); + } + + #[test] + fn test_end_turn() { + let mut state = State::new_base(); + let starting_color = state.get_current_color(); + let seating_order = state.get_seating_order().to_vec(); + + state.vector[HAS_PLAYED_DEV_CARD] = 1; + state.vector[HAS_ROLLED_INDEX] = 1; + state.apply_action(Action::EndTurn(starting_color)); + + assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 0); + assert_eq!(state.vector[HAS_ROLLED_INDEX], 0); + + assert_eq!(state.get_current_color(), seating_order[1]); + + for _ in 0..(state.get_num_players() - 1) { + state.apply_action(Action::EndTurn(state.get_current_color())); + } + + assert_eq!(state.get_current_color(), starting_color); + } } diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 11e93485..ae031a42 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -246,7 +246,10 @@ mod tests { let mut state = State::new_base(); assert!(state.is_initial_build_phase()); - assert!(matches!(state.get_action_prompt(), ActionPrompt::BuildInitialSettlement)); + assert!(matches!( + state.get_action_prompt(), + ActionPrompt::BuildInitialSettlement + )); let actions = state.generate_playable_actions(); match &actions[0] { @@ -254,7 +257,10 @@ mod tests { _ => panic!("Expected BuildSettlement action to be first action"), } state.apply_action(actions[0]); - - assert!(matches!(state.get_action_prompt(), ActionPrompt::BuildInitialRoad)); + + assert!(matches!( + state.get_action_prompt(), + ActionPrompt::BuildInitialRoad + )); } } diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index fe80a655..c90c1755 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -102,6 +102,11 @@ pub fn player_devhand_slice(num_players: u8, color: u8) -> std::ops::Range std::ops::Range { + let start = PLAYER_STATE_START_INDEX + num_players as usize + 11 + (color as usize * 15); + start..start + 4 +} + // TODO: I'm not sure if it makes more sense to have this in state.rs? pub fn take_next_dev_card(vector: &mut StateVector) -> Option { let ptr = vector[DEV_BANK_PTR_INDEX] as usize;