From 191de2dc74ffe65f2c8815380058e4b7a7fd5d6a Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 12 Oct 2024 23:07:36 -0400 Subject: [PATCH 01/83] Add small assertion --- tests/models/test_map.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/models/test_map.py b/tests/models/test_map.py index b6892381e..9175c6bb7 100644 --- a/tests/models/test_map.py +++ b/tests/models/test_map.py @@ -4,6 +4,7 @@ MINI_MAP_TEMPLATE, CatanMap, LandTile, + NodeRef, get_nodes_and_edges, get_node_counter_production, DICE_PROBAS, @@ -25,6 +26,7 @@ def test_node_production_of_same_resource_adjacent_tile(): def test_mini_map_can_be_created(): mini = CatanMap.from_template(MINI_MAP_TEMPLATE) + assert mini.tiles[(0, 0, 0)].nodes[NodeRef.NORTH] == 0 assert len(mini.land_tiles) == 7 assert len(mini.land_nodes) == 24 assert len(mini.tiles_by_id) == 7 From db1a88b929948541df6a68de5cd3b033fce2daee Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 12 Oct 2024 23:10:16 -0400 Subject: [PATCH 02/83] cargo new catanatron_rust --- catanatron_rust/Cargo.lock | 7 +++++++ catanatron_rust/Cargo.toml | 6 ++++++ catanatron_rust/src/main.rs | 3 +++ 3 files changed, 16 insertions(+) create mode 100644 catanatron_rust/Cargo.lock create mode 100644 catanatron_rust/Cargo.toml create mode 100644 catanatron_rust/src/main.rs diff --git a/catanatron_rust/Cargo.lock b/catanatron_rust/Cargo.lock new file mode 100644 index 000000000..1b43b0281 --- /dev/null +++ b/catanatron_rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "catanatron_rust" +version = "0.1.0" diff --git a/catanatron_rust/Cargo.toml b/catanatron_rust/Cargo.toml new file mode 100644 index 000000000..a2a9a5ca4 --- /dev/null +++ b/catanatron_rust/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "catanatron_rust" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs new file mode 100644 index 000000000..e7a11a969 --- /dev/null +++ b/catanatron_rust/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} From 0d6abc74daf9dacc94d6a1e8627f327cfc1617c7 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 12 Oct 2024 23:24:59 -0400 Subject: [PATCH 03/83] Example tests --- catanatron_rust/src/lib.rs | 14 ++++++++++++++ catanatron_rust/tests/simple_test.rs | 7 +++++++ 2 files changed, 21 insertions(+) create mode 100644 catanatron_rust/src/lib.rs create mode 100644 catanatron_rust/tests/simple_test.rs diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs new file mode 100644 index 000000000..7d12d9af8 --- /dev/null +++ b/catanatron_rust/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/catanatron_rust/tests/simple_test.rs b/catanatron_rust/tests/simple_test.rs new file mode 100644 index 000000000..7bf3c9bf0 --- /dev/null +++ b/catanatron_rust/tests/simple_test.rs @@ -0,0 +1,7 @@ +// tests/integration_test.rs +use catanatron_rust::add; + +#[test] +fn test_integration() { + assert_eq!(add(10, 5), 15); +} From 8db9a8a8354135350c2824e97ef25f4b180070f0 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 13 Oct 2024 00:41:43 -0400 Subject: [PATCH 04/83] Sample enums and decks (bitfield) implementation --- catanatron_rust/src/decks.rs | 196 +++++++++++++++++++++++++++++++++++ catanatron_rust/src/enums.rs | 98 ++++++++++++++++++ catanatron_rust/src/main.rs | 22 +++- 3 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 catanatron_rust/src/decks.rs create mode 100644 catanatron_rust/src/enums.rs diff --git a/catanatron_rust/src/decks.rs b/catanatron_rust/src/decks.rs new file mode 100644 index 000000000..c04d9fd7c --- /dev/null +++ b/catanatron_rust/src/decks.rs @@ -0,0 +1,196 @@ +use super::enums::Resource; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Deck { + freqdeck: u32, // Store resource counts using 32 bits. Each resource gets 6 bits. +} + +impl Deck { + /// Static constructor: Constructs a deck with all resources set to 0. + pub fn new() -> Self { + Deck { freqdeck: 0 } + } + + // Read operations ===== + /// Counts the number of a specific resource in the deck. + pub fn count(&self, card: Resource) -> u32 { + let shift = Deck::resource_shift(card); + (self.freqdeck >> shift) & 0b111111 // Extract 6 bits (max count 63) + } + + /// Checks if the deck can draw a specific number of a given resource. + pub fn can_draw(&self, amount: u32, card: Resource) -> bool { + let count = self.count(card); + count >= amount + } + + // Write operations ===== + /// Draws a specific number of a given resource from the deck. + pub fn draw(&mut self, amount: u32, card: Resource) { + let count = self.count(card); + let new_count = count.saturating_sub(amount); // Avoid negative values + self.set_resource_count(card, new_count); + } + + /// Replenishes the deck with a specific number of a given resource. + pub fn replenish(&mut self, amount: u32, card: Resource) { + let count = self.count(card); + let new_count = count.saturating_add(amount); // Avoid overflow + self.set_resource_count(card, new_count); + } + + /// Adds two frequency decks together element-wise. + pub fn add(&self, other: &Deck) -> Deck { + let mut result = Deck::new(); + for card in [ + Resource::Wood, + Resource::Brick, + Resource::Sheep, + Resource::Wheat, + Resource::Ore, + ] { + let total = self.count(card) + other.count(card); + result.set_resource_count(card, total); + } + result + } + + /// Subtracts one frequency deck from another element-wise. + pub fn subtract(&self, other: &Deck) -> Deck { + let mut result = Deck::new(); + for card in [ + Resource::Wood, + Resource::Brick, + Resource::Sheep, + Resource::Wheat, + Resource::Ore, + ] { + let diff = self.count(card).saturating_sub(other.count(card)); + result.set_resource_count(card, diff); + } + result + } + + /// Checks if one frequency deck contains all the resources of another deck. + pub fn contains(&self, other: &Deck) -> bool { + for card in [ + Resource::Wood, + Resource::Brick, + Resource::Sheep, + Resource::Wheat, + Resource::Ore, + ] { + if self.count(card) < other.count(card) { + return false; + } + } + true + } + + /// Creates a frequency deck from a list of resources. + pub fn from_listdeck(listdeck: &[Resource]) -> Deck { + let mut deck = Deck::new(); + for &resource in listdeck { + deck.replenish(1, resource); + } + deck + } + + /// Helper function to get the number of bits to shift for a given resource. + fn resource_shift(card: Resource) -> u32 { + match card { + Resource::Wood => 0, // First 6 bits + Resource::Brick => 6, // Next 6 bits + Resource::Sheep => 12, // Next 6 bits + Resource::Wheat => 18, // Next 6 bits + Resource::Ore => 24, // Next 6 bits + } + } + + /// Sets the count of the specified resource using bitwise operations. + fn set_resource_count(&mut self, card: Resource, count: u32) { + let shift = Deck::resource_shift(card); + self.freqdeck &= !(0b111111 << shift); // Clear the bits for the resource + self.freqdeck |= (count & 0b111111) << shift; // Set the new count + } + + /// Static constructor: Constructs a deck with [19, 19, 19, 19, 19] resources. + pub fn starting_resource_bank() -> Self { + Deck::from_counts([19, 19, 19, 19, 19]) + } + + /// Constructs a deck from a list of resource counts + /// The input array must have 5 elements, corresponding to [Wood, Brick, Sheep, Wheat, Ore]. + pub fn from_counts(counts: [u32; 5]) -> Self { + let mut freqdeck = 0; + + // Assign each resource count to its corresponding bitfield using bitwise shifts. + for (i, &count) in counts.iter().enumerate() { + // Ensure each count fits within 6 bits (maximum value of 63) + let count = count & 0b111111; // Mask to fit within 6 bits + freqdeck |= count << (i * 6); // Shift the count to its correct position in the bitfield + } + + Deck { freqdeck } + } + + pub fn total_cards(&self) -> u32 { + let mut total = 0; + + // Iterate over all resources (Wood, Brick, Sheep, Wheat, Ore) + for card in [ + Resource::Wood, + Resource::Brick, + Resource::Sheep, + Resource::Wheat, + Resource::Ore, + ] { + total += self.count(card); // Sum each resource count + } + + total + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resource_freqdeck_init() { + // Test that the starting resource bank is initialized correctly + let deck = Deck::starting_resource_bank(); + assert_eq!(deck.count(Resource::Wood), 19); + } + + #[test] + fn test_resource_freqdeck_can_draw() { + // Test the `can_draw` function + let deck = Deck::starting_resource_bank(); + assert!(deck.can_draw(10, Resource::Brick)); // Should be able to draw 10 bricks + assert!(!deck.can_draw(20, Resource::Brick)); // Should not be able to draw 20 bricks + } + + #[test] + fn test_resource_freqdeck_integration() { + let mut deck = Deck::starting_resource_bank(); + + // Test the initial count and total of all resources + assert_eq!(deck.count(Resource::Wood), 19); + assert_eq!(deck.count(Resource::Brick), 19); + assert_eq!(deck.count(Resource::Sheep), 19); + assert_eq!(deck.count(Resource::Wheat), 19); + assert_eq!(deck.count(Resource::Ore), 19); + + // Test drawing from the deck + assert!(deck.can_draw(10, Resource::Wheat)); + deck.draw(10, Resource::Wheat); + assert_eq!(deck.count(Resource::Wheat), 9); // After drawing 10, there should be 9 Wheat + assert_eq!(deck.total_cards(), 19 * 5 - 10); // Total sum of resources should be 85 + + // Test replenishing the deck + deck.replenish(5, Resource::Wheat); + assert_eq!(deck.count(Resource::Wheat), 14); // After replenishing 5, there should be 14 Wheat + assert_eq!(deck.total_cards(), 19 * 5 - 10 + 5); // Total sum of resources should be 90 + } +} diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs new file mode 100644 index 000000000..02b29de8c --- /dev/null +++ b/catanatron_rust/src/enums.rs @@ -0,0 +1,98 @@ +// Use enums for literal string types +#[derive(Debug, Clone, Copy)] +pub enum Resource { + Wood, + Brick, + Sheep, + Wheat, + Ore, +} + +#[derive(Debug, Clone, Copy)] +pub enum DevCard { + Knight, + YearOfPlenty, + Monopoly, + RoadBuilding, + VictoryPoint, +} + +#[derive(Debug, Clone, Copy)] +pub enum BuildingType { + Settlement, + City, + Road, +} + +// References for nodes and edges on the game board +#[derive(Debug, Clone, Copy)] +pub enum NodeRef { + North, + Northeast, + Southeast, + South, + Southwest, + Northwest, +} + +#[derive(Debug, Clone, Copy)] +pub enum EdgeRef { + East, + Southeast, + Southwest, + West, + Northwest, + Northeast, +} + +#[derive(Debug, Clone, Copy)] +pub enum ActionPrompt { + BuildInitialSettlement, + BuildInitialRoad, + PlayTurn, + Discard, + MoveRobber, + DecideTrade, + DecideAcceptees, +} + +#[derive(Debug, Clone, Copy)] +pub enum ActionType { + Roll, // None. Log instead sets it to (int, int) rolled. + MoveRobber, // value is (coordinate, Color|None). Log has extra element of card stolen. + Discard, // value is None|Resource[]. + BuildRoad, // value is edge_id + BuildSettlement, // value is node_id + BuildCity, // value is node_id + BuyDevelopmentCard, // value is None. Log value is card. + PlayKnightCard, // value is None + PlayYearOfPlenty, // value is (Resource, Resource) + PlayMonopoly, // value is Resource + PlayRoadBuilding, // value is None + MaritimeTrade, // 5-resource tuple, last is resource asked. + OfferTrade, // 10-resource tuple, first 5 is offered, last 5 is receiving. + AcceptTrade, // 10-resource tuple. + RejectTrade, // None + ConfirmTrade, // 11-tuple. First 10 like OfferTrade, last is color of accepting player. + CancelTrade, // None + EndTurn, // None +} + +// Struct for Action (similar to namedtuple in Python) +#[derive(Debug, Clone)] +pub struct Action { + pub color: String, + pub action_type: ActionType, + // TODO: Not sure if this should be a String + pub value: Option, // Use Option for fields that could be None +} + +impl Action { + pub fn new(color: String, action_type: ActionType, value: Option) -> Self { + Action { + color, + action_type, + value, + } + } +} diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index e7a11a969..b316b9dad 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -1,3 +1,23 @@ +use std::time::Instant; + +use enums::Resource; + +mod decks; +mod enums; + fn main() { - println!("Hello, world!"); + // Benchmark deck operations + println!("Starting benchmark of Deck operations..."); + let start = Instant::now(); + let mut deck = decks::Deck::starting_resource_bank(); + for _ in 0..1_000_000 { + if deck.can_draw(2, enums::Resource::Wood) { + deck.draw(2, enums::Resource::Wood); + deck.replenish(1, Resource::Wood); // Replenish after drawing to keep the count consistent + deck.replenish(1, Resource::Wood); // Replenish after drawing to keep the count consistent + } + } + let duration = start.elapsed(); + println!("Time taken for 1,000,000 deck operations: {:?}", duration); + println!("Total cards in deck: {}", deck.total_cards()); } From 9671f840432fa7d21992923c5a74cb233ce68ad0 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 13 Oct 2024 07:34:14 -0400 Subject: [PATCH 05/83] Fixup Deck --- catanatron_rust/src/decks.rs | 203 +++++++++++++++++++++++++++++++---- catanatron_rust/src/enums.rs | 2 +- catanatron_rust/src/main.rs | 2 +- 3 files changed, 187 insertions(+), 20 deletions(-) diff --git a/catanatron_rust/src/decks.rs b/catanatron_rust/src/decks.rs index c04d9fd7c..9826c3e27 100644 --- a/catanatron_rust/src/decks.rs +++ b/catanatron_rust/src/decks.rs @@ -1,20 +1,20 @@ -use super::enums::Resource; +use crate::enums::{DevCard, Resource}; #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Deck { +pub struct ResourceDeck { freqdeck: u32, // Store resource counts using 32 bits. Each resource gets 6 bits. } -impl Deck { +impl ResourceDeck { /// Static constructor: Constructs a deck with all resources set to 0. pub fn new() -> Self { - Deck { freqdeck: 0 } + ResourceDeck { freqdeck: 0 } } // Read operations ===== /// Counts the number of a specific resource in the deck. pub fn count(&self, card: Resource) -> u32 { - let shift = Deck::resource_shift(card); + let shift = ResourceDeck::resource_shift(card); (self.freqdeck >> shift) & 0b111111 // Extract 6 bits (max count 63) } @@ -40,8 +40,8 @@ impl Deck { } /// Adds two frequency decks together element-wise. - pub fn add(&self, other: &Deck) -> Deck { - let mut result = Deck::new(); + pub fn add(&self, other: &ResourceDeck) -> ResourceDeck { + let mut result = ResourceDeck::new(); for card in [ Resource::Wood, Resource::Brick, @@ -56,8 +56,8 @@ impl Deck { } /// Subtracts one frequency deck from another element-wise. - pub fn subtract(&self, other: &Deck) -> Deck { - let mut result = Deck::new(); + pub fn subtract(&self, other: &ResourceDeck) -> ResourceDeck { + let mut result = ResourceDeck::new(); for card in [ Resource::Wood, Resource::Brick, @@ -72,7 +72,7 @@ impl Deck { } /// Checks if one frequency deck contains all the resources of another deck. - pub fn contains(&self, other: &Deck) -> bool { + pub fn contains(&self, other: &ResourceDeck) -> bool { for card in [ Resource::Wood, Resource::Brick, @@ -88,8 +88,8 @@ impl Deck { } /// Creates a frequency deck from a list of resources. - pub fn from_listdeck(listdeck: &[Resource]) -> Deck { - let mut deck = Deck::new(); + pub fn from_listdeck(listdeck: &[Resource]) -> ResourceDeck { + let mut deck = ResourceDeck::new(); for &resource in listdeck { deck.replenish(1, resource); } @@ -109,14 +109,14 @@ impl Deck { /// Sets the count of the specified resource using bitwise operations. fn set_resource_count(&mut self, card: Resource, count: u32) { - let shift = Deck::resource_shift(card); + let shift = ResourceDeck::resource_shift(card); self.freqdeck &= !(0b111111 << shift); // Clear the bits for the resource self.freqdeck |= (count & 0b111111) << shift; // Set the new count } /// Static constructor: Constructs a deck with [19, 19, 19, 19, 19] resources. pub fn starting_resource_bank() -> Self { - Deck::from_counts([19, 19, 19, 19, 19]) + ResourceDeck::from_counts([19, 19, 19, 19, 19]) } /// Constructs a deck from a list of resource counts @@ -131,7 +131,7 @@ impl Deck { freqdeck |= count << (i * 6); // Shift the count to its correct position in the bitfield } - Deck { freqdeck } + ResourceDeck { freqdeck } } pub fn total_cards(&self) -> u32 { @@ -152,6 +152,108 @@ impl Deck { } } +#[derive(Debug, Clone)] +pub struct DevCardDeck { + listdeck: Vec, +} + +impl DevCardDeck { + /// Static constructor: Constructs a deck with all development cards set to 0. + pub fn new() -> Self { + DevCardDeck { + listdeck: Vec::new(), + } + } + + pub fn starting_deck() -> Self { + let mut listdeck = Vec::new(); + listdeck.extend(vec![DevCard::Knight; 14]); + listdeck.extend(vec![DevCard::YearOfPlenty; 2]); + listdeck.extend(vec![DevCard::RoadBuilding; 2]); + listdeck.extend(vec![DevCard::Monopoly; 2]); + listdeck.extend(vec![DevCard::VictoryPoint; 5]); + DevCardDeck { listdeck } + } + + /// Draws a specific number of a given development card from the deck. + pub fn draw(&mut self, amount: u32, card: DevCard) { + let mut count = amount; + let mut i = 0; + while count > 0 && i < self.listdeck.len() { + if self.listdeck[i] == card { + self.listdeck.remove(i); + count -= 1; + } else { + i += 1; + } + } + } + + /// Replenishes the deck with a specific number of a given development card. + pub fn replenish(&mut self, amount: u32, card: DevCard) { + for _ in 0..amount { + self.listdeck.push(card); + } + } + + /// Counts the number of a specific development card in the deck. + pub fn count(&self, card: DevCard) -> u32 { + self.listdeck.iter().filter(|&&c| c == card).count() as u32 + } + + /// Returns the total number of development cards in the deck. + pub fn total_cards(&self) -> u32 { + self.listdeck.len() as u32 + } + + /// Creates a development card deck from a list of development cards. + pub fn from_listdeck(listdeck: &[DevCard]) -> DevCardDeck { + let mut deck = DevCardDeck::new(); + for &card in listdeck { + deck.replenish(1, card); + } + deck + } + + /// Checks if the deck can draw a specific number of a given development card. + pub fn can_draw(&self, amount: u32, card: DevCard) -> bool { + let count = self.count(card); + count >= amount + } + + /// Adds two development card decks together element-wise. + pub fn add(&self, other: &DevCardDeck) -> DevCardDeck { + let mut result = DevCardDeck::new(); + for card in [ + DevCard::Knight, + DevCard::VictoryPoint, + DevCard::RoadBuilding, + DevCard::YearOfPlenty, + DevCard::Monopoly, + ] { + let total = self.count(card) + other.count(card); + result.replenish(total, card); + } + result + } + + /// Subtracts one development card deck from another + pub fn subtract(&self, other: &DevCardDeck) -> DevCardDeck { + let mut result = DevCardDeck::new(); + for card in [ + DevCard::Knight, + DevCard::VictoryPoint, + DevCard::RoadBuilding, + DevCard::YearOfPlenty, + DevCard::Monopoly, + ] { + let diff = self.count(card).saturating_sub(other.count(card)); + result.replenish(diff, card); + } + result + } +} + #[cfg(test)] mod tests { use super::*; @@ -159,21 +261,21 @@ mod tests { #[test] fn test_resource_freqdeck_init() { // Test that the starting resource bank is initialized correctly - let deck = Deck::starting_resource_bank(); + let deck = ResourceDeck::starting_resource_bank(); assert_eq!(deck.count(Resource::Wood), 19); } #[test] fn test_resource_freqdeck_can_draw() { // Test the `can_draw` function - let deck = Deck::starting_resource_bank(); + let deck = ResourceDeck::starting_resource_bank(); assert!(deck.can_draw(10, Resource::Brick)); // Should be able to draw 10 bricks assert!(!deck.can_draw(20, Resource::Brick)); // Should not be able to draw 20 bricks } #[test] fn test_resource_freqdeck_integration() { - let mut deck = Deck::starting_resource_bank(); + let mut deck = ResourceDeck::starting_resource_bank(); // Test the initial count and total of all resources assert_eq!(deck.count(Resource::Wood), 19); @@ -193,4 +295,69 @@ mod tests { assert_eq!(deck.count(Resource::Wheat), 14); // After replenishing 5, there should be 14 Wheat assert_eq!(deck.total_cards(), 19 * 5 - 10 + 5); // Total sum of resources should be 90 } + + #[test] + fn test_can_add() { + let mut a = ResourceDeck::new(); + let mut b = ResourceDeck::new(); + + a.replenish(10, Resource::Ore); + b.replenish(1, Resource::Ore); + + assert_eq!(a.count(Resource::Ore), 10); + assert_eq!(b.count(Resource::Ore), 1); + + b = b.add(&a); + assert_eq!(a.count(Resource::Ore), 10); + assert_eq!(b.count(Resource::Ore), 11); + } + + #[test] + fn test_can_subtract() { + let mut a = ResourceDeck::new(); + let mut b = ResourceDeck::new(); + + a.replenish(13, Resource::Sheep); + b.replenish(4, Resource::Sheep); + + assert_eq!(a.count(Resource::Sheep), 13); + assert_eq!(b.count(Resource::Sheep), 4); + + b.replenish(11, Resource::Sheep); // now has 15 + b = b.subtract(&a); + assert_eq!(a.count(Resource::Sheep), 13); + assert_eq!(b.count(Resource::Sheep), 2); + } + + #[test] + fn test_from_array() { + let a = ResourceDeck::from_listdeck(&[Resource::Brick, Resource::Brick, Resource::Wood]); + assert_eq!(a.total_cards(), 3); + assert_eq!(a.count(Resource::Brick), 2); + assert_eq!(a.count(Resource::Wood), 1); + } + + #[test] + fn test_devcard_freqdeck_init() { + let deck = DevCardDeck::starting_deck(); + assert_eq!(deck.count(DevCard::Knight), 14); + assert_eq!(deck.count(DevCard::VictoryPoint), 5); + assert_eq!(deck.total_cards(), 25); + } + + #[test] + fn test_devcard_can_draw() { + let deck = DevCardDeck::starting_deck(); + assert!(deck.can_draw(10, DevCard::Knight)); + assert!(!deck.can_draw(15, DevCard::Knight)); + } + + #[test] + fn test_devcard_draw() { + let mut deck = DevCardDeck::starting_deck(); + assert_eq!(deck.count(DevCard::Knight), 14); + deck.draw(5, DevCard::Knight); + assert_eq!(deck.count(DevCard::Knight), 9); + assert_eq!(deck.total_cards(), 25 - 5); + } } diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 02b29de8c..9a87e9054 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -8,7 +8,7 @@ pub enum Resource { Ore, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum DevCard { Knight, YearOfPlenty, diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index b316b9dad..f48e2d401 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -9,7 +9,7 @@ fn main() { // Benchmark deck operations println!("Starting benchmark of Deck operations..."); let start = Instant::now(); - let mut deck = decks::Deck::starting_resource_bank(); + let mut deck = decks::ResourceDeck::starting_resource_bank(); for _ in 0..1_000_000 { if deck.can_draw(2, enums::Resource::Wood) { deck.draw(2, enums::Resource::Wood); From c9a3e9ab9303a73fb9025efe66f86870e3e48c4f Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 13 Oct 2024 11:54:49 -0400 Subject: [PATCH 06/83] Copy benchmark --- catanatron_rust/Cargo.lock | 682 ++++++++++++++++++++++ catanatron_rust/Cargo.toml | 8 + catanatron_rust/benches/copy_benchmark.rs | 45 ++ 3 files changed, 735 insertions(+) create mode 100644 catanatron_rust/benches/copy_benchmark.rs diff --git a/catanatron_rust/Cargo.lock b/catanatron_rust/Cargo.lock index 1b43b0281..42e5dab94 100644 --- a/catanatron_rust/Cargo.lock +++ b/catanatron_rust/Cargo.lock @@ -2,6 +2,688 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "catanatron_rust" version = "0.1.0" +dependencies = [ + "criterion", + "rand", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/catanatron_rust/Cargo.toml b/catanatron_rust/Cargo.toml index a2a9a5ca4..4c5b8dd45 100644 --- a/catanatron_rust/Cargo.toml +++ b/catanatron_rust/Cargo.toml @@ -4,3 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] +rand = "0.8" + +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "copy_benchmark" +harness = false diff --git a/catanatron_rust/benches/copy_benchmark.rs b/catanatron_rust/benches/copy_benchmark.rs new file mode 100644 index 000000000..bad614329 --- /dev/null +++ b/catanatron_rust/benches/copy_benchmark.rs @@ -0,0 +1,45 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rand::Rng; // For random number generation + +fn copy_array_benchmark(c: &mut Criterion) { + // Initialize an array of length 1200 + let array = [0u8; 1200]; + + c.bench_function("Copy 1200-length array", |b| { + b.iter(|| { + let mut rng = rand::thread_rng(); + // Copy the array (deep copy) + let mut copied_array = black_box(array); + // Generate a random index between 0 and 1199 + let random_index = rng.gen_range(0..1200); + // Generate a random value between 0 and 255 (u8 range) + let random_value = rng.gen_range(0..=255); + // Modify the array at the random index + copied_array[random_index] = random_value; + black_box(copied_array); + }) + }); +} + +fn copy_vector_benchmark(c: &mut Criterion) { + // Initialize a vector of length 600 + let vector = vec![0u8; 600]; + + c.bench_function("Clone 600-length vector", |b| { + b.iter(|| { + let mut rng = rand::thread_rng(); + // Clone the vector (deep copy) + let mut copied_vector = black_box(vector.clone()); + // Generate a random index between 0 and 599 + let random_index = rng.gen_range(0..600); + // Generate a random value between 0 and 255 (u8 range) + let random_value = rng.gen_range(0..=255); + // Modify the vector at the random index + copied_vector[random_index] = random_value; + black_box(copied_vector); + }) + }); +} + +criterion_group!(benches, copy_array_benchmark, copy_vector_benchmark); +criterion_main!(benches); From ff867571e01d4c556f65e21cd8620a312c73dcc8 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 13 Oct 2024 13:26:24 -0400 Subject: [PATCH 07/83] initialize_state_vector --- catanatron_rust/src/main.rs | 4 ++ catanatron_rust/src/state.rs | 82 ++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 catanatron_rust/src/state.rs diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index f48e2d401..bc6372c91 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -4,6 +4,7 @@ use enums::Resource; mod decks; mod enums; +mod state; fn main() { // Benchmark deck operations @@ -20,4 +21,7 @@ fn main() { let duration = start.elapsed(); println!("Time taken for 1,000,000 deck operations: {:?}", duration); println!("Total cards in deck: {}", deck.total_cards()); + + let vector = state::initialize_state_vector(2); + println!("Vector length: {}", vector.len()); } diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs new file mode 100644 index 000000000..3e7eb15c1 --- /dev/null +++ b/catanatron_rust/src/state.rs @@ -0,0 +1,82 @@ +/// This is a compact representation of the omnipotent state of the game. +/// Fairly close to a bitboard, but not quite. Its a vector of integers. +/// +/// To create a feature-vector from the perspective of a player, +/// remember to mask hidden information and hot-encode as needed. +/// +/// TODO: Is it better to have it already in hot-encoded rep? +/// For now going with compact representation, to allow MCTS rollouts +/// to be faster (without needing to hot-encode). If any workload +/// needs to create samples (e.g. collect RL trajectories), then +/// they'll have to pay the performance of hot-encoding this into +/// a tensor separately. +pub fn initialize_state_vector(_num_players: u8) -> Vec { + // TODO: Is configuration part of state? + // TODO: Hardcoded for BASE_MAP + let n = _num_players as usize; + let num_nodes = 54; + let num_edges = 72; + let num_tiles = 19; + let num_ports = 9; + + let mut size: usize = 0; + // Bank + size += 5; // Bank Resources (Number <= 19) + size += 1; // Bank Development Cards (Number <= 25) + + // Game Controls + size += n; // Color_Seating_Order (Player Index < n) + size += 1; // Current_Player_Index (Player Index < n) + size += 1; // Current_Turn_Index (Player Index < n) + size += 1; // Has_Played_Development_Card (Boolean) + size += 1; // Has_Rolled (Boolean) + size += 1; // Is_Discarding (Boolean) + size += 1; // Is_Moving_Robber (Boolean) + size += 1; // Is_Building_Road (Boolean) + size += 1; // Free_Roads_Available (Number <= 2) + + // Extra (these are needed to make game Markovian (i.e. memoryless)) + // Note: (Largest_Army_Size and Longest_Road_Size are captured by player (_Played) and board state) + size += 1; // Longest_Road_Player_Index (Player Index < n) + size += 1; // Largest_Army_Player_Index (Player Index < n) + + // Players + let mut player_state_size: usize = 0; + player_state_size += 1; // Player_Victory_Points (Number <= 12) + player_state_size += 5; // Player__In_Hand (Number <= 19) + player_state_size += 5; // Player__In_Hand (Number <= 25) + player_state_size += 5; // Player__Played (Number <= 14) + + // This is redundant information (since one can figure out from the board state) + // player_state_size += 5; // Player__Roads_Left + // player_state_size += 5; // Player__Settlements_Left + // player_state_size += 5; // Player__Cities_Left + size += player_state_size * n; + + // Board + size += 1; // Robber_Tile (Tile Index < num_tiles) + size += num_tiles; // Tile_Resource (Resource Index <= 5) + size += num_tiles; // Tile_Number (Number <= 12) + size += num_edges; // Edge_Owner (Player Index | -1 < n + 1) + size += num_nodes; // Node_Owner (Player Index | -1 < n + 1) + size += num_nodes; // Node_Settlement/City (1=Settlement, 2=City, 0=Nothing) + size += num_ports; // Port_Resource (Resource Index <= 5) + + // TODO: This is not the only Data Structure to do rollouts. + // We recommend additional caches and aux data structures for + // faster rollouts. This one is compact optimized for copying. + let vector = vec![0u8; size]; + vector +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_initialize_state_vector() { + let n: usize = 2; + let result = initialize_state_vector(n as u8); + assert_eq!(result.len(), 278); + } +} From 85d068548a74559e934f3f884a3da98a8c201c77 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 13 Oct 2024 13:28:44 -0400 Subject: [PATCH 08/83] Note on Player State --- catanatron_core/catanatron/state.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/catanatron_core/catanatron/state.py b/catanatron_core/catanatron/state.py index 665eab8cb..2cef8f44f 100644 --- a/catanatron_core/catanatron/state.py +++ b/catanatron_core/catanatron/state.py @@ -66,14 +66,14 @@ # Create Player State blueprint PLAYER_INITIAL_STATE = { "VICTORY_POINTS": 0, - "ROADS_AVAILABLE": 15, - "SETTLEMENTS_AVAILABLE": 5, - "CITIES_AVAILABLE": 4, + # de-normalized features (for performance and ease of creating feature vectors) "HAS_ROAD": False, "HAS_ARMY": False, "HAS_ROLLED": False, "HAS_PLAYED_DEVELOPMENT_CARD_IN_TURN": False, - # de-normalized features (for performance since we think they are good features) + "ROADS_AVAILABLE": 15, + "SETTLEMENTS_AVAILABLE": 5, + "CITIES_AVAILABLE": 4, "ACTUAL_VICTORY_POINTS": 0, "LONGEST_ROAD_LENGTH": 0, } From 76d69d6c2cb811f246d095bbffc869b5a54d6953 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 13 Oct 2024 16:14:22 -0400 Subject: [PATCH 09/83] Fix Python Benchmark --- .../catanatron_experimental/benchmarks/benchmark_game_copy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catanatron_experimental/catanatron_experimental/benchmarks/benchmark_game_copy.py b/catanatron_experimental/catanatron_experimental/benchmarks/benchmark_game_copy.py index f4c356c00..78d9b7a45 100644 --- a/catanatron_experimental/catanatron_experimental/benchmarks/benchmark_game_copy.py +++ b/catanatron_experimental/catanatron_experimental/benchmarks/benchmark_game_copy.py @@ -38,7 +38,7 @@ board['connected_components'] = game.state.board.connected_components.copy() state_copy = dict() -state_copy['colors'] = game.state.colors.copy() +state_copy['colors'] = game.state.colors state_copy['board'] = board state_copy['actions'] = game.state.actions.copy() state_copy['resource_freqdeck'] = game.state.resource_freqdeck.copy() From e90f64d9b123e4b2a2e85edc6a8b2e48ce91319f Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Mon, 14 Oct 2024 07:47:29 -0400 Subject: [PATCH 10/83] Copy Benchmark Update --- catanatron_rust/Cargo.toml | 4 ++ catanatron_rust/benches/copy_benchmark.rs | 81 +++++++++++++++-------- catanatron_rust/src/lib.rs | 16 +---- 3 files changed, 60 insertions(+), 41 deletions(-) diff --git a/catanatron_rust/Cargo.toml b/catanatron_rust/Cargo.toml index 4c5b8dd45..48f1d5b9f 100644 --- a/catanatron_rust/Cargo.toml +++ b/catanatron_rust/Cargo.toml @@ -3,6 +3,10 @@ name = "catanatron_rust" version = "0.1.0" edition = "2021" +[lib] +name = "catanatron_rust" +path = "src/lib.rs" + [dependencies] rand = "0.8" diff --git a/catanatron_rust/benches/copy_benchmark.rs b/catanatron_rust/benches/copy_benchmark.rs index bad614329..08d4c265f 100644 --- a/catanatron_rust/benches/copy_benchmark.rs +++ b/catanatron_rust/benches/copy_benchmark.rs @@ -1,45 +1,72 @@ +use catanatron_rust::decks::{DevCardDeck, ResourceDeck}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use rand::Rng; // For random number generation + +fn copy_vector_benchmark(c: &mut Criterion) { + // Initialize a vector of length 600 + let vector = vec![0u8; 600]; + + c.bench_function("Clone 600-length vector", |b| { + let mut iteration_number = 0; + b.iter(|| { + let mut copied_vector = vector.clone(); + let index = iteration_number % 600; + let random_value = (iteration_number % 256) as u8; + copied_vector[index] = random_value; + black_box(copied_vector); + + iteration_number += 1; + }) + }); +} fn copy_array_benchmark(c: &mut Criterion) { // Initialize an array of length 1200 - let array = [0u8; 1200]; + let array = [0u8; 600]; - c.bench_function("Copy 1200-length array", |b| { + c.bench_function("Copy 600-length array", |b| { + let mut iteration_number = 0; b.iter(|| { - let mut rng = rand::thread_rng(); - // Copy the array (deep copy) - let mut copied_array = black_box(array); - // Generate a random index between 0 and 1199 - let random_index = rng.gen_range(0..1200); - // Generate a random value between 0 and 255 (u8 range) - let random_value = rng.gen_range(0..=255); - // Modify the array at the random index - copied_array[random_index] = random_value; + let mut copied_array = array; + let index = iteration_number % 600; + let random_value = (iteration_number % 256) as u8; + copied_array[index] = random_value; black_box(copied_array); + + iteration_number += 1; }) }); } -fn copy_vector_benchmark(c: &mut Criterion) { - // Initialize a vector of length 600 - let vector = vec![0u8; 600]; +#[derive(Debug, Clone)] +pub struct State { + pub bank_resources: ResourceDeck, + pub bank_development_cards: DevCardDeck, +} - c.bench_function("Clone 600-length vector", |b| { +impl State { + pub fn initialize_state() -> State { + State { + bank_resources: ResourceDeck::starting_resource_bank(), + bank_development_cards: DevCardDeck::starting_deck(), + } + } +} + +fn copy_struct_benchmark(c: &mut Criterion) { + let state = State::initialize_state(); + + c.bench_function("Clone Struct1", |b| { b.iter(|| { - let mut rng = rand::thread_rng(); - // Clone the vector (deep copy) - let mut copied_vector = black_box(vector.clone()); - // Generate a random index between 0 and 599 - let random_index = rng.gen_range(0..600); - // Generate a random value between 0 and 255 (u8 range) - let random_value = rng.gen_range(0..=255); - // Modify the vector at the random index - copied_vector[random_index] = random_value; - black_box(copied_vector); + let copied_struct1 = black_box(state.clone()); + black_box(copied_struct1); }) }); } -criterion_group!(benches, copy_array_benchmark, copy_vector_benchmark); +criterion_group!( + benches, + copy_vector_benchmark, + copy_array_benchmark, + copy_struct_benchmark +); criterion_main!(benches); diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 7d12d9af8..f073c6e9f 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,14 +1,2 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +pub mod decks; +pub mod enums; From 79466601a778d5acecd5c827cfe13a6a8ba233bd Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Mon, 14 Oct 2024 07:49:30 -0400 Subject: [PATCH 11/83] More Example Benchmark on Main --- catanatron_rust/src/main.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index bc6372c91..4791e18f4 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -1,6 +1,5 @@ -use std::time::Instant; - use enums::Resource; +use std::time::Instant; mod decks; mod enums; @@ -22,6 +21,32 @@ fn main() { println!("Time taken for 1,000,000 deck operations: {:?}", duration); println!("Total cards in deck: {}", deck.total_cards()); + // Benchmark copy operations + println!("Starting benchmark of Deck operations..."); + let start = Instant::now(); + let vector = vec![0u8; 600]; + let mut copied_vector = vector.clone(); + for i in 0..1_000_000 { + let index = i % 600; + copied_vector[index] = index as u8; + } + let duration = start.elapsed(); + println!("Time taken for 1,000,000 copy operations: {:?}", duration); + println!("Copy Results: {:?}, {:?}", vector, copied_vector); + + // Benchmark array copy operations + println!("Starting benchmark of Array operations..."); + let start = Instant::now(); + let array = [0u8; 1200]; + let mut copied_array = array; + for i in 0..1_000_000 { + let index = i % 1200; + copied_array[index] = index as u8; + } + let duration = start.elapsed(); + println!("Time taken for 1,000,000 array operations: {:?}", duration); + println!("Copy Results: {:?}, {:?}", array, copied_array); + let vector = state::initialize_state_vector(2); println!("Vector length: {}", vector.len()); } From a8dd52d789dbdb3d39e6e4efbe31f40f72583a38 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Mon, 14 Oct 2024 07:52:43 -0400 Subject: [PATCH 12/83] Python deck Benchmark --- .../benchmarks/benchmark_deck_agasint_rust.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 catanatron_experimental/catanatron_experimental/benchmarks/benchmark_deck_agasint_rust.py diff --git a/catanatron_experimental/catanatron_experimental/benchmarks/benchmark_deck_agasint_rust.py b/catanatron_experimental/catanatron_experimental/benchmarks/benchmark_deck_agasint_rust.py new file mode 100644 index 000000000..651949fdc --- /dev/null +++ b/catanatron_experimental/catanatron_experimental/benchmarks/benchmark_deck_agasint_rust.py @@ -0,0 +1,32 @@ +""" +This is the Python equivalent of one of the Rust benchmarks. +It shows Rust implementation can be ~600x faster. +""" + +import time + +from catanatron.models.decks import * + + +print("Starting benchmark of Deck operations...") + +# Get starting time +start = time.time() + +# Run benchmark loop for 1,000,000 iterations +for _ in range(1_000_000): + deck = starting_resource_bank() + if freqdeck_can_draw(deck, 2, WOOD): + freqdeck_draw(deck, 2, WOOD) + freqdeck_replenish( + deck, 1, WOOD + ) # Replenish after drawing to keep the count consistent + freqdeck_replenish( + deck, 1, WOOD + ) # Replenish after drawing to keep the count consistent + +# Get duration +duration = time.time() - start + +# Print the result +print(f"Time taken for 1,000,000 can_draw operations: {duration:.4f} seconds") From 7f5451041e038367f32855923e0ec7f777d562c8 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 07:36:14 -0400 Subject: [PATCH 13/83] Update state.py documentation --- catanatron_core/catanatron/state.py | 1 + 1 file changed, 1 insertion(+) diff --git a/catanatron_core/catanatron/state.py b/catanatron_core/catanatron/state.py index 2cef8f44f..31dbc6e2a 100644 --- a/catanatron_core/catanatron/state.py +++ b/catanatron_core/catanatron/state.py @@ -115,6 +115,7 @@ class State: current_turn_index (int): index per colors array of player whose turn is it. current_prompt (ActionPrompt): DEPRECATED. Not needed; use is_initial_build_phase, is_moving_knight, etc... instead. + is_initial_build_phase (bool): If current player is building initial settlements is_discarding (bool): If current player needs to discard. is_moving_knight (bool): If current player needs to move robber. is_road_building (bool): If current player needs to build free roads per Road From 310020b6571c1884e3565fa59de1a894d02dd98c Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 20:07:44 -0400 Subject: [PATCH 14/83] Simple Vector State --- catanatron_rust/src/decks.rs | 12 +- catanatron_rust/src/enums.rs | 11 +- catanatron_rust/src/lib.rs | 1 + catanatron_rust/src/main.rs | 7 +- catanatron_rust/src/state.rs | 151 +++++++++++++++++----- catanatron_rust/tests/integration_test.rs | 18 +++ catanatron_rust/tests/simple_test.rs | 7 - 7 files changed, 166 insertions(+), 41 deletions(-) create mode 100644 catanatron_rust/tests/integration_test.rs delete mode 100644 catanatron_rust/tests/simple_test.rs diff --git a/catanatron_rust/src/decks.rs b/catanatron_rust/src/decks.rs index 9826c3e27..af4595977 100644 --- a/catanatron_rust/src/decks.rs +++ b/catanatron_rust/src/decks.rs @@ -152,9 +152,19 @@ impl ResourceDeck { } } +pub fn starting_dev_listdeck() -> [u8; 25] { + let mut listdeck: [u8; 25] = [0; 25]; + listdeck[0..14].copy_from_slice(&[DevCard::Knight as u8; 14]); + listdeck[14..16].copy_from_slice(&[DevCard::YearOfPlenty as u8; 2]); + listdeck[16..18].copy_from_slice(&[DevCard::RoadBuilding as u8; 2]); + listdeck[18..20].copy_from_slice(&[DevCard::Monopoly as u8; 2]); + listdeck[20..25].copy_from_slice(&[DevCard::VictoryPoint as u8; 5]); + listdeck +} + #[derive(Debug, Clone)] pub struct DevCardDeck { - listdeck: Vec, + pub listdeck: Vec, } impl DevCardDeck { diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 9a87e9054..0d5a6b19f 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -1,4 +1,13 @@ -// Use enums for literal string types +#[derive(Debug, Clone, Copy)] +pub enum Color { + Red, + Blue, + Orange, + White, +} + +pub const COLORS: [Color; 4] = [Color::Red, Color::Blue, Color::Orange, Color::White]; + #[derive(Debug, Clone, Copy)] pub enum Resource { Wood, diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index f073c6e9f..82ac58ebc 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,2 +1,3 @@ pub mod decks; pub mod enums; +pub mod state; diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 4791e18f4..23f3fe39a 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -47,6 +47,9 @@ fn main() { println!("Time taken for 1,000,000 array operations: {:?}", duration); println!("Copy Results: {:?}, {:?}", array, copied_array); - let vector = state::initialize_state_vector(2); - println!("Vector length: {}", vector.len()); + let size = state::get_state_array_size(2); + println!("Vector length: {}", size); + + let vector = state::initialize_state(); + println!("Vector: {:?}", vector); } diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 3e7eb15c1..9c281f438 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -1,16 +1,13 @@ -/// This is a compact representation of the omnipotent state of the game. -/// Fairly close to a bitboard, but not quite. Its a vector of integers. -/// -/// To create a feature-vector from the perspective of a player, -/// remember to mask hidden information and hot-encode as needed. -/// -/// TODO: Is it better to have it already in hot-encoded rep? -/// For now going with compact representation, to allow MCTS rollouts -/// to be faster (without needing to hot-encode). If any workload -/// needs to create samples (e.g. collect RL trajectories), then -/// they'll have to pay the performance of hot-encoding this into -/// a tensor separately. -pub fn initialize_state_vector(_num_players: u8) -> Vec { +use crate::decks::starting_dev_listdeck; +use rand::seq::SliceRandom; + +use crate::enums::COLORS; + +/// This is in theory not needed since we use a vector and we can +/// .push() to it. But since we made it, leaving in here in case +/// we want to switch to an array implementation and it serves +/// as documentation of the state vector. +pub fn get_state_array_size(_num_players: u8) -> usize { // TODO: Is configuration part of state? // TODO: Hardcoded for BASE_MAP let n = _num_players as usize; @@ -20,14 +17,16 @@ pub fn initialize_state_vector(_num_players: u8) -> Vec { let num_ports = 9; let mut size: usize = 0; + // Trying to have as most fixed-size vector first as possible + // so that we can understand/debug all configurations similarly. // Bank size += 5; // Bank Resources (Number <= 19) - size += 1; // Bank Development Cards (Number <= 25) + size += 25; // Bank Development Cards (DevCard Index <= 5) // Game Controls - size += n; // Color_Seating_Order (Player Index < n) size += 1; // Current_Player_Index (Player Index < n) size += 1; // Current_Turn_Index (Player Index < n) + size += 1; // Is_Initial_Build_Phase (Boolean) size += 1; // Has_Played_Development_Card (Boolean) size += 1; // Has_Rolled (Boolean) size += 1; // Is_Discarding (Boolean) @@ -40,12 +39,22 @@ pub fn initialize_state_vector(_num_players: u8) -> Vec { size += 1; // Longest_Road_Player_Index (Player Index < n) size += 1; // Largest_Army_Player_Index (Player Index < n) - // Players + // Board (dynamically sized based on map template; 228 for BASE_MAP) + size += 1; // Robber_Tile (Tile Index < num_tiles) + size += num_tiles; // Tile_Resource (Resource Index <= 5) + size += num_tiles; // Tile_Number (Number <= 12) 39 + size += num_edges; // Edge_Owner (Player Index | -1 < n + 1) + size += num_nodes; // Node_Owner (Player Index | -1 < n + 1) + size += num_nodes; // Node_Settlement/City (1=Settlement, 2=City, 0=Nothing) + size += num_ports; // Port_Resource (Resource Index <= 5) + + // Players (dynamically sized based on number of players = 1 + 15 x n) + size += n; // Color_Seating_Order (Player Index < n) let mut player_state_size: usize = 0; player_state_size += 1; // Player_Victory_Points (Number <= 12) player_state_size += 5; // Player__In_Hand (Number <= 19) player_state_size += 5; // Player__In_Hand (Number <= 25) - player_state_size += 5; // Player__Played (Number <= 14) + player_state_size += 4; // Player__Played (Number <= 14) VictoryPoint can't be played // This is redundant information (since one can figure out from the board state) // player_state_size += 5; // Player__Roads_Left @@ -53,19 +62,96 @@ pub fn initialize_state_vector(_num_players: u8) -> Vec { // player_state_size += 5; // Player__Cities_Left size += player_state_size * n; + size +} + +/// This is a compact representation of the omnipotent state of the game. +/// Fairly close to a bitboard, but not quite. Its a vector of integers. +/// +/// To create a feature-vector from the perspective of a player, +/// remember to mask hidden information and hot-encode as needed. +/// +/// TODO: Is it better to have it already in hot-encoded rep? +/// For now going with compact representation, to allow MCTS rollouts +/// to be faster (without needing to hot-encode). If any workload +/// needs to create samples (e.g. collect RL trajectories), then +/// they'll have to pay the performance of hot-encoding this into +/// a tensor separately. +/// +/// TODO: This is not the only Data Structure to do rollouts. +/// We recommend additional caches and aux data structures for +/// faster rollouts. This one is compact optimized for copying. +pub fn initialize_state() -> Vec { + let num_players = 2; + let size = get_state_array_size(num_players); + let mut vector = vec![0; size]; + // Initialize Bank + vector[0] = 19; + vector[1] = 19; + vector[2] = 19; + vector[3] = 19; + vector[4] = 19; + // Initialize Bank Development Cards + let listdeck = starting_dev_listdeck(); + vector[5..30].copy_from_slice(&listdeck); + // Initialize Game Controls + vector[30] = 0; // Current_Player_Index + vector[31] = 0; // Current_Turn_Index + vector[32] = 1; // Is_Initial_Build_Phase + vector[33] = 0; // Has_Played_Development_Card + vector[34] = 0; // Has_Rolled + vector[35] = 0; // Is_Discarding + vector[36] = 0; // Is_Moving_Robber + vector[37] = 0; // Is_Building_Road + vector[38] = 2; // Free_Roads_Available + + // Extra (u8::MAX is used to indicate no player) + vector[39] = u8::MAX; // Longest_Road_Player_Index + vector[40] = u8::MAX; // Largest_Army_Player_Index + // Board - size += 1; // Robber_Tile (Tile Index < num_tiles) - size += num_tiles; // Tile_Resource (Resource Index <= 5) - size += num_tiles; // Tile_Number (Number <= 12) - size += num_edges; // Edge_Owner (Player Index | -1 < n + 1) - size += num_nodes; // Node_Owner (Player Index | -1 < n + 1) - size += num_nodes; // Node_Settlement/City (1=Settlement, 2=City, 0=Nothing) - size += num_ports; // Port_Resource (Resource Index <= 5) + // TODO: Generate map from template + // vector[41] = 0; + // size += 1; // Robber_Tile (Tile Index < num_tiles) + // size += num_tiles; // Tile_Resource (Resource Index <= 5) + // size += num_tiles; // Tile_Number (Number <= 12) + // size += num_edges; // Edge_Owner (Player Index | -1 < n + 1) + // size += num_nodes; // Node_Owner (Player Index | -1 < n + 1) + // size += num_nodes; // Node_Settlement/City (1=Settlement, 2=City, 0=Nothing) + // size += num_ports; // Port_Resource (Resource Index <= 5) + + // Initialize Players + // Shuffle player indices + let mut player_indices = COLORS.iter().map(|&x| x as u8).collect::>(); + player_indices.shuffle(&mut rand::thread_rng()); + vector[267..267 + player_indices.len()].copy_from_slice(&player_indices); + let mut player_state_start = 267 + player_indices.len(); + for _ in 0..num_players { + // Player_Victory_Points (Number <= 12) + vector[player_state_start] = 0; // victory points + + // Player__In_Hand (Number <= 19) + vector[player_state_start + 1] = 0; // wood + vector[player_state_start + 2] = 0; // brick + vector[player_state_start + 3] = 0; // sheep + vector[player_state_start + 4] = 0; // wheat + vector[player_state_start + 5] = 0; // ore + + // Player__In_Hand (Number <= 25) + vector[player_state_start + 6] = 0; // knight + vector[player_state_start + 7] = 0; // year of plenty + vector[player_state_start + 8] = 0; // monopoly + vector[player_state_start + 9] = 0; // road building + vector[player_state_start + 10] = 0; // victory point + + // Player__Played (Number <= 14) + vector[player_state_start + 11] = 0; // knight played + vector[player_state_start + 12] = 0; // year of plenty played + vector[player_state_start + 13] = 0; // monopoly played + vector[player_state_start + 14] = 0; // road building played + player_state_start += 15; + } - // TODO: This is not the only Data Structure to do rollouts. - // We recommend additional caches and aux data structures for - // faster rollouts. This one is compact optimized for copying. - let vector = vec![0u8; size]; vector } @@ -76,7 +162,12 @@ mod tests { #[test] fn test_initialize_state_vector() { let n: usize = 2; - let result = initialize_state_vector(n as u8); - assert_eq!(result.len(), 278); + let result = get_state_array_size(n as u8); + assert_eq!(result, 301); + } + + #[test] + fn test_initialize_state() { + initialize_state(); } } diff --git a/catanatron_rust/tests/integration_test.rs b/catanatron_rust/tests/integration_test.rs new file mode 100644 index 000000000..f4db4c179 --- /dev/null +++ b/catanatron_rust/tests/integration_test.rs @@ -0,0 +1,18 @@ +use catanatron_rust::decks; +use catanatron_rust::enums::Resource; +use catanatron_rust::state; + +#[test] +fn test_integration() { + let mut deck = decks::ResourceDeck::starting_resource_bank(); + if deck.can_draw(2, Resource::Wood) { + deck.draw(2, Resource::Wood); + deck.replenish(1, Resource::Wood); // Replenish after drawing to keep the count consistent + deck.replenish(1, Resource::Wood); // Replenish after drawing to keep the count consistent + } + assert_eq!(deck.total_cards(), 95); + + let vector = state::initialize_state(); + let size = state::get_state_array_size(2); + assert_eq!(size, vector.len()); +} diff --git a/catanatron_rust/tests/simple_test.rs b/catanatron_rust/tests/simple_test.rs deleted file mode 100644 index 7bf3c9bf0..000000000 --- a/catanatron_rust/tests/simple_test.rs +++ /dev/null @@ -1,7 +0,0 @@ -// tests/integration_test.rs -use catanatron_rust::add; - -#[test] -fn test_integration() { - assert_eq!(add(10, 5), 15); -} From a912a7af1ccec4cb444ee2f47e871cb1de19b2fb Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 20:07:57 -0400 Subject: [PATCH 15/83] README.md Update --- catanatron_rust/README.md | 79 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 catanatron_rust/README.md diff --git a/catanatron_rust/README.md b/catanatron_rust/README.md new file mode 100644 index 000000000..9f2a2ca78 --- /dev/null +++ b/catanatron_rust/README.md @@ -0,0 +1,79 @@ +# catanatron_rust + +This is a rewrite of Catanatron Core in Rust. + +## Usage + +``` +cargo run +cargo test +cargo bench +``` + +### Benchmarking + +To debug speed + +``` +CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --release --root +cargo flamegraph --dev --root +``` + +## Performance + +We will make sure MoveGeneration is the one that only creates +valid moves. That way, applying moves don't need to validate. + +### State as Vector + +Small experiments (see copy_benchmark) showed its +faster to copy a small vector than a larger array. And cloning +a higher abstracted / complex object like a struct would take +a lot more! So going with a compact Vector to represent it. + +## Actions as BitFields + +Reading GYM actions, that are 289, we prob need at least 9 bits. +To represent Actions as a bitfield number, we would need: + +- 2 bits for the color (4 colors). Or 3 bits for up to 8 colors. +- 5 bits for type of action (18 types) + its reasonable to say that the numbers in the trade freqdecks are 0-4, so we can use 3 bits for that. + biggest value is that of CONFIRM_TRADE, which is 11 3-bit numbers, so 33 bits. +- 33 bits for the value of the action. +- if we were to remove trading, we would have 5 bits for the value of the action. + because of we would need to represent just MAX of: + - 6 bits: 2 numbers of up to 6 (for the dice roll), which is 3 bits each. So 6 bits. + - 11 bits: tiles id (up to 19), which is 5 bits (+ 3 bits of color + 3 bits of resource) (MOVE_ROBBER) + - 7 bits: BUILD_X (up to 72 edges), which is 7 bits (BUILD_ROAD) or 6 bits (BUILD_SETTLEMENT) + +## BitBoard + +We need to support the following operations: + +- Action Generation: + - Yielding Resources: + - Need to tiles for a given number + - Check if robber in given tile + - Get buildings (color and multiplier) around tile + - Road Building: Valid Buildable Edges + - Settlement Posibilities: Buildable Node Ids + - Robber Possibilities: + - Which are land tiles not robber. +- Move Application + - Build a settlement + - Build a road + - Build a city + - Move Robber +- Queries + - Adjacent Tiles to a Node (for yielding resources in initial building phase) + - Edges given a Node Id + - Is friendly road (edgeId, color) + - Is Enemy Node (nodeId, color) + - Expandable Nodes (for ) + +Additional Responsabilities + +- Incrementally Mantain Longest Road (Color + Count) +- Buildable Subgraph for quick Answering Buildable Edges +- Connected Components (this is to, when plowing happens, be able to count by just max(len) pieces after updating data structure). Otherwise we would have to re-BFS all roads. From 6431d4caa27e8af8d107633a2e7242d7a5f87ca7 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 20:28:12 -0400 Subject: [PATCH 16/83] Py: Create map_template.py --- catanatron_core/catanatron/models/map.py | 187 ++---------------- .../catanatron/models/map_template.py | 185 +++++++++++++++++ .../catanatron_gym/envs/catanatron_env.py | 3 +- tests/test_machine_learning.py | 4 +- 4 files changed, 204 insertions(+), 175 deletions(-) create mode 100644 catanatron_core/catanatron/models/map_template.py diff --git a/catanatron_core/catanatron/models/map.py b/catanatron_core/catanatron/models/map.py index 1c4dc0b3e..ec357399d 100644 --- a/catanatron_core/catanatron/models/map.py +++ b/catanatron_core/catanatron/models/map.py @@ -1,10 +1,21 @@ import typing -from dataclasses import dataclass import random from collections import Counter, defaultdict -from typing import Dict, FrozenSet, List, Literal, Mapping, Set, Tuple, Type, Union +from typing import Dict, FrozenSet, List, Literal, Set, Union from catanatron.models.coordinate_system import Direction, add, UNIT_VECTORS +from catanatron.models.map_template import ( + Coordinate, + Tile, + LandTile, + NodeId, + Port, + Water, + MapTemplate, + EdgeId, + BASE_MAP_TEMPLATE, + MINI_MAP_TEMPLATE, +) from catanatron.models.enums import ( FastResource, WOOD, @@ -16,182 +27,12 @@ NodeRef, ) +# TODO: Deprecate in favor of having users specify map type first (BASE, TOURNAMENT, MINI) NUM_NODES = 54 NUM_EDGES = 72 NUM_TILES = 19 -EdgeId = Tuple[int, int] -NodeId = int -Coordinate = Tuple[int, int, int] - - -@dataclass -class LandTile: - id: int - resource: Union[FastResource, None] # None means desert tile - number: Union[int, None] # None if desert - nodes: Dict[NodeRef, NodeId] # node_ref => node_id - edges: Dict[EdgeRef, EdgeId] # edge_ref => edge - - # The id is unique among the tiles, so we can use it as the hash. - def __hash__(self): - return self.id - - -@dataclass -class Port: - id: int - resource: Union[FastResource, None] # None means desert tile - direction: Direction - nodes: Dict[NodeRef, NodeId] # node_ref => node_id - edges: Dict[EdgeRef, EdgeId] # edge_ref => edge - - # The id is unique among the tiles, so we can use it as the hash. - def __hash__(self): - return self.id - - -@dataclass(frozen=True) -class Water: - nodes: Dict[NodeRef, int] - edges: Dict[EdgeRef, EdgeId] - - -Tile = Union[LandTile, Port, Water] - - -@dataclass(frozen=True) -class MapTemplate: - numbers: List[int] - port_resources: List[Union[FastResource, None]] - tile_resources: List[Union[FastResource, None]] - topology: Mapping[ - Coordinate, Union[Type[LandTile], Type[Water], Tuple[Type[Port], Direction]] - ] - - -# Small 7-tile map, no ports. -MINI_MAP_TEMPLATE = MapTemplate( - [3, 4, 5, 6, 8, 9, 10], - [], - [WOOD, None, BRICK, SHEEP, WHEAT, WHEAT, ORE], - { - # center - (0, 0, 0): LandTile, - # first layer - (1, -1, 0): LandTile, - (0, -1, 1): LandTile, - (-1, 0, 1): LandTile, - (-1, 1, 0): LandTile, - (0, 1, -1): LandTile, - (1, 0, -1): LandTile, - # second layer - (2, -2, 0): Water, - (1, -2, 1): Water, - (0, -2, 2): Water, - (-1, -1, 2): Water, - (-2, 0, 2): Water, - (-2, 1, 1): Water, - (-2, 2, 0): Water, - (-1, 2, -1): Water, - (0, 2, -2): Water, - (1, 1, -2): Water, - (2, 0, -2): Water, - (2, -1, -1): Water, - }, -) - -"""Standard 4-player map""" -BASE_MAP_TEMPLATE = MapTemplate( - [2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 8, 9, 9, 10, 10, 11, 11, 12], - [ - # These are 2:1 ports - WOOD, - BRICK, - SHEEP, - WHEAT, - ORE, - # These represet 3:1 ports - None, - None, - None, - None, - ], - [ - # Four wood tiles - WOOD, - WOOD, - WOOD, - WOOD, - # Three brick tiles - BRICK, - BRICK, - BRICK, - # Four sheep tiles - SHEEP, - SHEEP, - SHEEP, - SHEEP, - # Four wheat tiles - WHEAT, - WHEAT, - WHEAT, - WHEAT, - # Three ore tiles - ORE, - ORE, - ORE, - # One desert - None, - ], - # 3 layers, where last layer is water - { - # center - (0, 0, 0): LandTile, - # first layer - (1, -1, 0): LandTile, - (0, -1, 1): LandTile, - (-1, 0, 1): LandTile, - (-1, 1, 0): LandTile, - (0, 1, -1): LandTile, - (1, 0, -1): LandTile, - # second layer - (2, -2, 0): LandTile, - (1, -2, 1): LandTile, - (0, -2, 2): LandTile, - (-1, -1, 2): LandTile, - (-2, 0, 2): LandTile, - (-2, 1, 1): LandTile, - (-2, 2, 0): LandTile, - (-1, 2, -1): LandTile, - (0, 2, -2): LandTile, - (1, 1, -2): LandTile, - (2, 0, -2): LandTile, - (2, -1, -1): LandTile, - # third (water) layer - (3, -3, 0): (Port, Direction.WEST), - (2, -3, 1): Water, - (1, -3, 2): (Port, Direction.NORTHWEST), - (0, -3, 3): Water, - (-1, -2, 3): (Port, Direction.NORTHWEST), - (-2, -1, 3): Water, - (-3, 0, 3): (Port, Direction.NORTHEAST), - (-3, 1, 2): Water, - (-3, 2, 1): (Port, Direction.EAST), - (-3, 3, 0): Water, - (-2, 3, -1): (Port, Direction.EAST), - (-1, 3, -2): Water, - (0, 3, -3): (Port, Direction.SOUTHEAST), - (1, 2, -3): Water, - (2, 1, -3): (Port, Direction.SOUTHWEST), - (3, 0, -3): Water, - (3, -1, -2): (Port, Direction.SOUTHWEST), - (3, -2, -1): Water, - }, -) - - class CatanMap: """Represents a randomly initialized map.""" diff --git a/catanatron_core/catanatron/models/map_template.py b/catanatron_core/catanatron/models/map_template.py new file mode 100644 index 000000000..a00fd651c --- /dev/null +++ b/catanatron_core/catanatron/models/map_template.py @@ -0,0 +1,185 @@ +from dataclasses import dataclass +from typing import Dict, List, Mapping, Tuple, Type, Union + +from catanatron.models.coordinate_system import Direction +from catanatron.models.enums import ( + FastResource, + WOOD, + BRICK, + SHEEP, + WHEAT, + ORE, + EdgeRef, + NodeRef, +) + + +EdgeId = Tuple[int, int] +NodeId = int +Coordinate = Tuple[int, int, int] + + +@dataclass +class LandTile: + id: int + resource: Union[FastResource, None] # None means desert tile + number: Union[int, None] # None if desert + nodes: Dict[NodeRef, NodeId] # node_ref => node_id + edges: Dict[EdgeRef, EdgeId] # edge_ref => edge + + # The id is unique among the tiles, so we can use it as the hash. + def __hash__(self): + return self.id + + +@dataclass +class Port: + id: int + resource: Union[FastResource, None] # None means desert tile + direction: Direction + nodes: Dict[NodeRef, NodeId] # node_ref => node_id + edges: Dict[EdgeRef, EdgeId] # edge_ref => edge + + # The id is unique among the tiles, so we can use it as the hash. + def __hash__(self): + return self.id + + +@dataclass(frozen=True) +class Water: + nodes: Dict[NodeRef, int] + edges: Dict[EdgeRef, EdgeId] + + +Tile = Union[LandTile, Port, Water] + + +@dataclass(frozen=True) +class MapTemplate: + numbers: List[int] + port_resources: List[Union[FastResource, None]] + tile_resources: List[Union[FastResource, None]] + topology: Mapping[ + Coordinate, Union[Type[LandTile], Type[Water], Tuple[Type[Port], Direction]] + ] + + +# Small 7-tile map, no ports. +MINI_MAP_TEMPLATE = MapTemplate( + [3, 4, 5, 6, 8, 9, 10], + [], + [WOOD, None, BRICK, SHEEP, WHEAT, WHEAT, ORE], + { + # center + (0, 0, 0): LandTile, + # first layer + (1, -1, 0): LandTile, + (0, -1, 1): LandTile, + (-1, 0, 1): LandTile, + (-1, 1, 0): LandTile, + (0, 1, -1): LandTile, + (1, 0, -1): LandTile, + # second layer + (2, -2, 0): Water, + (1, -2, 1): Water, + (0, -2, 2): Water, + (-1, -1, 2): Water, + (-2, 0, 2): Water, + (-2, 1, 1): Water, + (-2, 2, 0): Water, + (-1, 2, -1): Water, + (0, 2, -2): Water, + (1, 1, -2): Water, + (2, 0, -2): Water, + (2, -1, -1): Water, + }, +) + +"""Standard 4-player map""" +BASE_MAP_TEMPLATE = MapTemplate( + [2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 8, 9, 9, 10, 10, 11, 11, 12], + [ + # These are 2:1 ports + WOOD, + BRICK, + SHEEP, + WHEAT, + ORE, + # These represet 3:1 ports + None, + None, + None, + None, + ], + [ + # Four wood tiles + WOOD, + WOOD, + WOOD, + WOOD, + # Three brick tiles + BRICK, + BRICK, + BRICK, + # Four sheep tiles + SHEEP, + SHEEP, + SHEEP, + SHEEP, + # Four wheat tiles + WHEAT, + WHEAT, + WHEAT, + WHEAT, + # Three ore tiles + ORE, + ORE, + ORE, + # One desert + None, + ], + # 3 layers, where last layer is water + { + # center + (0, 0, 0): LandTile, + # first layer + (1, -1, 0): LandTile, + (0, -1, 1): LandTile, + (-1, 0, 1): LandTile, + (-1, 1, 0): LandTile, + (0, 1, -1): LandTile, + (1, 0, -1): LandTile, + # second layer + (2, -2, 0): LandTile, + (1, -2, 1): LandTile, + (0, -2, 2): LandTile, + (-1, -1, 2): LandTile, + (-2, 0, 2): LandTile, + (-2, 1, 1): LandTile, + (-2, 2, 0): LandTile, + (-1, 2, -1): LandTile, + (0, 2, -2): LandTile, + (1, 1, -2): LandTile, + (2, 0, -2): LandTile, + (2, -1, -1): LandTile, + # third (water) layer + (3, -3, 0): (Port, Direction.WEST), + (2, -3, 1): Water, + (1, -3, 2): (Port, Direction.NORTHWEST), + (0, -3, 3): Water, + (-1, -2, 3): (Port, Direction.NORTHWEST), + (-2, -1, 3): Water, + (-3, 0, 3): (Port, Direction.NORTHEAST), + (-3, 1, 2): Water, + (-3, 2, 1): (Port, Direction.EAST), + (-3, 3, 0): Water, + (-2, 3, -1): (Port, Direction.EAST), + (-1, 3, -2): Water, + (0, 3, -3): (Port, Direction.SOUTHEAST), + (1, 2, -3): Water, + (2, 1, -3): (Port, Direction.SOUTHWEST), + (3, 0, -3): Water, + (3, -1, -2): (Port, Direction.SOUTHWEST), + (3, -2, -1): Water, + }, +) diff --git a/catanatron_gym/catanatron_gym/envs/catanatron_env.py b/catanatron_gym/catanatron_gym/envs/catanatron_env.py index a5c76d03d..45f7f1846 100644 --- a/catanatron_gym/catanatron_gym/envs/catanatron_env.py +++ b/catanatron_gym/catanatron_gym/envs/catanatron_env.py @@ -4,7 +4,8 @@ from catanatron.game import Game, TURNS_LIMIT from catanatron.models.player import Color, Player, RandomPlayer -from catanatron.models.map import BASE_MAP_TEMPLATE, NUM_NODES, LandTile, build_map +from catanatron.models.map_template import BASE_MAP_TEMPLATE, LandTile +from catanatron.models.map import NUM_NODES, build_map from catanatron.models.enums import RESOURCES, Action, ActionType from catanatron.models.board import get_edges from catanatron_gym.features import ( diff --git a/tests/test_machine_learning.py b/tests/test_machine_learning.py index 9c1f91805..966ba8e6c 100644 --- a/tests/test_machine_learning.py +++ b/tests/test_machine_learning.py @@ -7,9 +7,11 @@ from catanatron.state import player_deck_replenish from catanatron.models.enums import ORE, Action, ActionType, WHEAT, NodeRef from catanatron.models.board import Board, get_edges -from catanatron.models.map import ( +from catanatron.models.map_template import ( BASE_MAP_TEMPLATE, MINI_MAP_TEMPLATE, +) +from catanatron.models.map import ( NUM_EDGES, NUM_NODES, CatanMap, From 1ce791120aa95be062693de825e126d796084efb Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 21:11:18 -0400 Subject: [PATCH 17/83] map_template.rs --- catanatron_rust/src/lib.rs | 1 + catanatron_rust/src/map_template.rs | 98 +++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 catanatron_rust/src/map_template.rs diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 82ac58ebc..25087c483 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,3 +1,4 @@ pub mod decks; pub mod enums; +pub mod map_template; pub mod state; diff --git a/catanatron_rust/src/map_template.rs b/catanatron_rust/src/map_template.rs new file mode 100644 index 000000000..c3a528d97 --- /dev/null +++ b/catanatron_rust/src/map_template.rs @@ -0,0 +1,98 @@ +use crate::enums::Resource; +use std::collections::HashMap; + +pub enum TileSlot { + Land, + Water, + NWPort, + NPort, + NEPort, + EPort, + SEPort, + SPort, + SWPort, + WPort, +} + +// We'll represent a Coordinate as a bitfield of 3 numbers. +// This allows us to create even the 6-8 player base catan map. +pub fn bitfield_coordinate(x: i8, y: i8, z: i8) -> u32 { + let x_packed = (x as u32) & 0xFF; // Mask to ensure we only take 8 bits + let y_packed = (y as u32) & 0xFF; // Mask to ensure we only take 8 bits + let z_packed = (z as u32) & 0xFF; // Mask to ensure we only take 8 bits + + // Shift the second and third values to their respective positions + (x_packed) | (y_packed << 8) | (z_packed << 16) +} + +pub struct MapTemplate { + numbers: Vec, + ports: Vec>, + tiles: Vec>, + topology: HashMap, +} + +#[cfg(test)] +fn unpack_from_bitfield(bitfield: u32) -> (i8, i8, i8) { + let a = (bitfield & 0xFF) as i8; // Extract the first 8 bits + let b = ((bitfield >> 8) & 0xFF) as i8; // Extract the second 8 bits + let c = ((bitfield >> 16) & 0xFF) as i8; // Extract the third 8 bits + + (a, b, c) +} + +mod tests { + use super::*; + + #[test] + fn test_coordinate_to_key() { + assert_eq!(bitfield_coordinate(0, 0, 0), 0); + assert_eq!(bitfield_coordinate(1, 0, 0), 1); + assert_eq!(bitfield_coordinate(0, 1, 0), 256); + assert_eq!(bitfield_coordinate(0, 0, 1), 65536); + assert_eq!(bitfield_coordinate(1, 1, 1), 65793); + } + + #[test] + fn test_map_template_creation() { + let mut topology = HashMap::new(); + // center + topology.insert(bitfield_coordinate(0, 0, 0), TileSlot::Land); + // first layer + topology.insert(bitfield_coordinate(-1, -1, 0), TileSlot::Land); + topology.insert(bitfield_coordinate(0, -1, 1), TileSlot::Land); + topology.insert(bitfield_coordinate(-1, 0, 1), TileSlot::Land); + topology.insert(bitfield_coordinate(-1, 1, 0), TileSlot::Land); + topology.insert(bitfield_coordinate(0, 1, -1), TileSlot::Land); + topology.insert(bitfield_coordinate(1, 0, -1), TileSlot::Land); + // second layer + topology.insert(bitfield_coordinate(2, -2, 0), TileSlot::Water); + topology.insert(bitfield_coordinate(1, -2, 1), TileSlot::Water); + topology.insert(bitfield_coordinate(0, -2, 2), TileSlot::Water); + topology.insert(bitfield_coordinate(-1, -1, 2), TileSlot::Water); + topology.insert(bitfield_coordinate(-2, 0, 2), TileSlot::Water); + topology.insert(bitfield_coordinate(-2, 1, 1), TileSlot::Water); + topology.insert(bitfield_coordinate(-2, 2, 0), TileSlot::Water); + topology.insert(bitfield_coordinate(-1, 2, -1), TileSlot::Water); + topology.insert(bitfield_coordinate(0, 2, -2), TileSlot::Water); + topology.insert(bitfield_coordinate(1, 1, -2), TileSlot::Water); + topology.insert(bitfield_coordinate(2, 0, -2), TileSlot::Water); + topology.insert(bitfield_coordinate(2, -1, -1), TileSlot::Water); + + let map_template = MapTemplate { + numbers: vec![3, 4, 5, 6, 8, 9, 10], + ports: vec![], + tiles: vec![ + Some(Resource::Wood), + None as Option, + Some(Resource::Brick), + Some(Resource::Sheep), + Some(Resource::Wheat), + Some(Resource::Wheat), + Some(Resource::Ore), + ], + topology, + }; + assert_eq!(map_template.topology.len(), 19); + } +} From f8beb2ebf24214bedf57e326ccad3dd9baa869dc Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 21:12:11 -0400 Subject: [PATCH 18/83] State Fixup --- catanatron_rust/src/state.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 9c281f438..d3296f9b8 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -92,6 +92,7 @@ pub fn initialize_state() -> Vec { vector[3] = 19; vector[4] = 19; // Initialize Bank Development Cards + // TODO: Shuffle let listdeck = starting_dev_listdeck(); vector[5..30].copy_from_slice(&listdeck); // Initialize Game Controls @@ -168,6 +169,7 @@ mod tests { #[test] fn test_initialize_state() { - initialize_state(); + let state = initialize_state(); + assert_eq!(state.len(), 301); } } From 53c56374efe14aff7bcc6889c257a3639dc41dd9 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 21:35:42 -0400 Subject: [PATCH 19/83] Global State that inits Base and Mini Templates --- catanatron_rust/src/global_state.rs | 160 ++++++++++++++++++++++++++++ catanatron_rust/src/lib.rs | 1 + catanatron_rust/src/map_template.rs | 84 +-------------- 3 files changed, 166 insertions(+), 79 deletions(-) create mode 100644 catanatron_rust/src/global_state.rs diff --git a/catanatron_rust/src/global_state.rs b/catanatron_rust/src/global_state.rs new file mode 100644 index 000000000..668e77bb2 --- /dev/null +++ b/catanatron_rust/src/global_state.rs @@ -0,0 +1,160 @@ +use std::collections::HashMap; + +use crate::{ + enums::Resource, + map_template::{MapTemplate, TileSlot}, +}; + +pub struct GlobalState { + mini_map_template: MapTemplate, + base_map_template: MapTemplate, +} + +impl GlobalState { + fn new() -> Self { + // Mini Map Template + let mut topology = HashMap::new(); + // center + topology.insert((0, 0, 0), TileSlot::Land); + // first layer + topology.insert((-1, -1, 0), TileSlot::Land); + topology.insert((0, -1, 1), TileSlot::Land); + topology.insert((-1, 0, 1), TileSlot::Land); + topology.insert((-1, 1, 0), TileSlot::Land); + topology.insert((0, 1, -1), TileSlot::Land); + topology.insert((1, 0, -1), TileSlot::Land); + // second layer + topology.insert((2, -2, 0), TileSlot::Water); + topology.insert((1, -2, 1), TileSlot::Water); + topology.insert((0, -2, 2), TileSlot::Water); + topology.insert((-1, -1, 2), TileSlot::Water); + topology.insert((-2, 0, 2), TileSlot::Water); + topology.insert((-2, 1, 1), TileSlot::Water); + topology.insert((-2, 2, 0), TileSlot::Water); + topology.insert((-1, 2, -1), TileSlot::Water); + topology.insert((0, 2, -2), TileSlot::Water); + topology.insert((1, 1, -2), TileSlot::Water); + topology.insert((2, 0, -2), TileSlot::Water); + topology.insert((2, -1, -1), TileSlot::Water); + let mini_map_template = MapTemplate { + numbers: vec![3, 4, 5, 6, 8, 9, 10], + ports: vec![], + tiles: vec![ + Some(Resource::Wood), + None, + Some(Resource::Brick), + Some(Resource::Sheep), + Some(Resource::Wheat), + Some(Resource::Wheat), + Some(Resource::Ore), + ], + topology, + }; + // Base Map Template + let mut topology = HashMap::new(); + // center + topology.insert((0, 0, 0), TileSlot::Land); + // first layer + topology.insert((-1, -1, 0), TileSlot::Land); + topology.insert((0, -1, 1), TileSlot::Land); + topology.insert((-1, 0, 1), TileSlot::Land); + topology.insert((-1, 1, 0), TileSlot::Land); + topology.insert((0, 1, -1), TileSlot::Land); + topology.insert((1, 0, -1), TileSlot::Land); + // second layer + topology.insert((2, -2, 0), TileSlot::Land); + topology.insert((1, -2, 1), TileSlot::Land); + topology.insert((0, -2, 2), TileSlot::Land); + topology.insert((-1, -1, 2), TileSlot::Land); + topology.insert((-2, 0, 2), TileSlot::Land); + topology.insert((-2, 1, 1), TileSlot::Land); + topology.insert((-2, 2, 0), TileSlot::Land); + topology.insert((-1, 2, -1), TileSlot::Land); + topology.insert((0, 2, -2), TileSlot::Land); + topology.insert((1, 1, -2), TileSlot::Land); + topology.insert((2, 0, -2), TileSlot::Land); + topology.insert((2, -1, -1), TileSlot::Land); + // third layer + topology.insert((3, -3, 0), TileSlot::WPort); + topology.insert((2, -3, 1), TileSlot::Water); + topology.insert((1, -3, 2), TileSlot::NWPort); + topology.insert((0, -3, 3), TileSlot::Water); + topology.insert((-1, -2, 3), TileSlot::NWPort); + topology.insert((-2, -1, 3), TileSlot::Water); + topology.insert((-3, 0, 3), TileSlot::NEPort); + topology.insert((-3, 1, 2), TileSlot::Water); + topology.insert((-3, 2, 1), TileSlot::EPort); + topology.insert((-3, 3, 0), TileSlot::Water); + topology.insert((-2, 3, -1), TileSlot::EPort); + topology.insert((-1, 3, -2), TileSlot::Water); + topology.insert((0, 3, -3), TileSlot::SEPort); + topology.insert((1, 2, -3), TileSlot::Water); + topology.insert((2, 1, -3), TileSlot::SWPort); + topology.insert((3, 0, -3), TileSlot::Water); + topology.insert((3, -1, -2), TileSlot::SWPort); + topology.insert((3, -2, -1), TileSlot::Water); + + let base_map_template = MapTemplate { + numbers: vec![2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 8, 9, 9, 10, 10, 11, 11, 12], + ports: vec![ + // These are 2:1 ports + Some(Resource::Wood), + Some(Resource::Brick), + Some(Resource::Sheep), + Some(Resource::Wheat), + Some(Resource::Ore), + // These represet 3:1 ports + None, + None, + None, + None, + ], + tiles: vec![ + // Four wood tiles + Some(Resource::Wood), + Some(Resource::Wood), + Some(Resource::Wood), + Some(Resource::Wood), + // Three brick tiles + Some(Resource::Brick), + Some(Resource::Brick), + Some(Resource::Brick), + // Four sheep tiles + Some(Resource::Sheep), + Some(Resource::Sheep), + Some(Resource::Sheep), + Some(Resource::Sheep), + // Four wheat tiles + Some(Resource::Wheat), + Some(Resource::Wheat), + Some(Resource::Wheat), + Some(Resource::Wheat), + // Three ore tiles + Some(Resource::Ore), + Some(Resource::Ore), + Some(Resource::Ore), + // One desert + None, + ], + topology, + }; + + Self { + mini_map_template, + base_map_template, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_global_state() { + let global_state = GlobalState::new(); + assert_eq!(global_state.mini_map_template.numbers.len(), 7); + assert_eq!(global_state.base_map_template.numbers.len(), 18); + assert_eq!(global_state.mini_map_template.topology.len(), 19); + } +} diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 25087c483..f6c3b8c36 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,4 +1,5 @@ pub mod decks; pub mod enums; +pub mod global_state; pub mod map_template; pub mod state; diff --git a/catanatron_rust/src/map_template.rs b/catanatron_rust/src/map_template.rs index c3a528d97..4afb8014b 100644 --- a/catanatron_rust/src/map_template.rs +++ b/catanatron_rust/src/map_template.rs @@ -14,85 +14,11 @@ pub enum TileSlot { WPort, } -// We'll represent a Coordinate as a bitfield of 3 numbers. -// This allows us to create even the 6-8 player base catan map. -pub fn bitfield_coordinate(x: i8, y: i8, z: i8) -> u32 { - let x_packed = (x as u32) & 0xFF; // Mask to ensure we only take 8 bits - let y_packed = (y as u32) & 0xFF; // Mask to ensure we only take 8 bits - let z_packed = (z as u32) & 0xFF; // Mask to ensure we only take 8 bits - - // Shift the second and third values to their respective positions - (x_packed) | (y_packed << 8) | (z_packed << 16) -} +type Coordinate = (i8, i8, i8); pub struct MapTemplate { - numbers: Vec, - ports: Vec>, - tiles: Vec>, - topology: HashMap, -} - -#[cfg(test)] -fn unpack_from_bitfield(bitfield: u32) -> (i8, i8, i8) { - let a = (bitfield & 0xFF) as i8; // Extract the first 8 bits - let b = ((bitfield >> 8) & 0xFF) as i8; // Extract the second 8 bits - let c = ((bitfield >> 16) & 0xFF) as i8; // Extract the third 8 bits - - (a, b, c) -} - -mod tests { - use super::*; - - #[test] - fn test_coordinate_to_key() { - assert_eq!(bitfield_coordinate(0, 0, 0), 0); - assert_eq!(bitfield_coordinate(1, 0, 0), 1); - assert_eq!(bitfield_coordinate(0, 1, 0), 256); - assert_eq!(bitfield_coordinate(0, 0, 1), 65536); - assert_eq!(bitfield_coordinate(1, 1, 1), 65793); - } - - #[test] - fn test_map_template_creation() { - let mut topology = HashMap::new(); - // center - topology.insert(bitfield_coordinate(0, 0, 0), TileSlot::Land); - // first layer - topology.insert(bitfield_coordinate(-1, -1, 0), TileSlot::Land); - topology.insert(bitfield_coordinate(0, -1, 1), TileSlot::Land); - topology.insert(bitfield_coordinate(-1, 0, 1), TileSlot::Land); - topology.insert(bitfield_coordinate(-1, 1, 0), TileSlot::Land); - topology.insert(bitfield_coordinate(0, 1, -1), TileSlot::Land); - topology.insert(bitfield_coordinate(1, 0, -1), TileSlot::Land); - // second layer - topology.insert(bitfield_coordinate(2, -2, 0), TileSlot::Water); - topology.insert(bitfield_coordinate(1, -2, 1), TileSlot::Water); - topology.insert(bitfield_coordinate(0, -2, 2), TileSlot::Water); - topology.insert(bitfield_coordinate(-1, -1, 2), TileSlot::Water); - topology.insert(bitfield_coordinate(-2, 0, 2), TileSlot::Water); - topology.insert(bitfield_coordinate(-2, 1, 1), TileSlot::Water); - topology.insert(bitfield_coordinate(-2, 2, 0), TileSlot::Water); - topology.insert(bitfield_coordinate(-1, 2, -1), TileSlot::Water); - topology.insert(bitfield_coordinate(0, 2, -2), TileSlot::Water); - topology.insert(bitfield_coordinate(1, 1, -2), TileSlot::Water); - topology.insert(bitfield_coordinate(2, 0, -2), TileSlot::Water); - topology.insert(bitfield_coordinate(2, -1, -1), TileSlot::Water); - - let map_template = MapTemplate { - numbers: vec![3, 4, 5, 6, 8, 9, 10], - ports: vec![], - tiles: vec![ - Some(Resource::Wood), - None as Option, - Some(Resource::Brick), - Some(Resource::Sheep), - Some(Resource::Wheat), - Some(Resource::Wheat), - Some(Resource::Ore), - ], - topology, - }; - assert_eq!(map_template.topology.len(), 19); - } + pub(crate) numbers: Vec, + pub(crate) ports: Vec>, + pub(crate) tiles: Vec>, + pub(crate) topology: HashMap, } From 97bdbcfe246238d079ed8fa92c5d0c0c874ed90a Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 21:38:59 -0400 Subject: [PATCH 20/83] Use Global State in main.rs --- catanatron_rust/src/global_state.rs | 3 ++- catanatron_rust/src/main.rs | 16 +++++++++------- catanatron_rust/src/map_template.rs | 2 ++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/catanatron_rust/src/global_state.rs b/catanatron_rust/src/global_state.rs index 668e77bb2..4492a4b31 100644 --- a/catanatron_rust/src/global_state.rs +++ b/catanatron_rust/src/global_state.rs @@ -5,13 +5,14 @@ use crate::{ map_template::{MapTemplate, TileSlot}, }; +#[derive(Debug)] pub struct GlobalState { mini_map_template: MapTemplate, base_map_template: MapTemplate, } impl GlobalState { - fn new() -> Self { + pub fn new() -> Self { // Mini Map Template let mut topology = HashMap::new(); // center diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 23f3fe39a..4344a1f3c 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -1,18 +1,17 @@ -use enums::Resource; +use catanatron_rust::decks; +use catanatron_rust::enums::Resource; +use catanatron_rust::global_state; +use catanatron_rust::state; use std::time::Instant; -mod decks; -mod enums; -mod state; - fn main() { // Benchmark deck operations println!("Starting benchmark of Deck operations..."); let start = Instant::now(); let mut deck = decks::ResourceDeck::starting_resource_bank(); for _ in 0..1_000_000 { - if deck.can_draw(2, enums::Resource::Wood) { - deck.draw(2, enums::Resource::Wood); + if deck.can_draw(2, Resource::Wood) { + deck.draw(2, Resource::Wood); deck.replenish(1, Resource::Wood); // Replenish after drawing to keep the count consistent deck.replenish(1, Resource::Wood); // Replenish after drawing to keep the count consistent } @@ -47,6 +46,9 @@ fn main() { println!("Time taken for 1,000,000 array operations: {:?}", duration); println!("Copy Results: {:?}, {:?}", array, copied_array); + let global_state = global_state::GlobalState::new(); + println!("Global State: {:?}", global_state); + let size = state::get_state_array_size(2); println!("Vector length: {}", size); diff --git a/catanatron_rust/src/map_template.rs b/catanatron_rust/src/map_template.rs index 4afb8014b..caab8a6bf 100644 --- a/catanatron_rust/src/map_template.rs +++ b/catanatron_rust/src/map_template.rs @@ -1,6 +1,7 @@ use crate::enums::Resource; use std::collections::HashMap; +#[derive(Debug)] pub enum TileSlot { Land, Water, @@ -16,6 +17,7 @@ pub enum TileSlot { type Coordinate = (i8, i8, i8); +#[derive(Debug)] pub struct MapTemplate { pub(crate) numbers: Vec, pub(crate) ports: Vec>, From 6154ea84f803b96232b4dfa33b1979b62f404b3b Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 21:44:08 -0400 Subject: [PATCH 21/83] Rename CatanMap to MapInstance --- catanatron_core/catanatron/game.py | 8 +++--- catanatron_core/catanatron/models/board.py | 10 +++---- catanatron_core/catanatron/models/map.py | 12 ++++---- catanatron_core/catanatron/state.py | 8 ++++-- .../catanatron_experimental/play.py | 6 ++-- .../catanatron_gym/envs/catanatron_env.py | 4 +-- catanatron_gym/catanatron_gym/features.py | 28 +++++++++---------- tests/models/test_board.py | 6 ++-- tests/models/test_map.py | 10 +++---- tests/test_machine_learning.py | 16 +++++------ 10 files changed, 55 insertions(+), 53 deletions(-) diff --git a/catanatron_core/catanatron/game.py b/catanatron_core/catanatron/game.py index 01bdf7e7c..baa7cc1e5 100644 --- a/catanatron_core/catanatron/game.py +++ b/catanatron_core/catanatron/game.py @@ -10,7 +10,7 @@ from catanatron.models.enums import Action, ActionPrompt, ActionType from catanatron.state import State, apply_action from catanatron.state_functions import player_key, player_has_rolled -from catanatron.models.map import CatanMap +from catanatron.models.map import MapInstance from catanatron.models.player import Color, Player # To timeout RandomRobots from getting stuck... @@ -92,7 +92,7 @@ def __init__( seed: Optional[int] = None, discard_limit: int = 7, vps_to_win: int = 10, - catan_map: Optional[CatanMap] = None, + map_instance: Optional[MapInstance] = None, initialize: bool = True, ): """Creates a game (doesn't run it). @@ -102,7 +102,7 @@ def __init__( seed (int, optional): Random seed to use (for reproducing games). Defaults to None. discard_limit (int, optional): Discard limit to use. Defaults to 7. vps_to_win (int, optional): Victory Points needed to win. Defaults to 10. - catan_map (CatanMap, optional): Map to use. Defaults to None. + map_instance (MapInstance, optional): Map to use. Defaults to None. initialize (bool, optional): Whether to initialize. Defaults to True. """ if initialize: @@ -111,7 +111,7 @@ def __init__( self.id = str(uuid.uuid4()) self.vps_to_win = vps_to_win - self.state = State(players, catan_map, discard_limit=discard_limit) + self.state = State(players, map_instance, discard_limit=discard_limit) def play(self, accumulators=[], decide_fn=None): """Executes game until a player wins or exceeded TURNS_LIMIT. diff --git a/catanatron_core/catanatron/models/board.py b/catanatron_core/catanatron/models/board.py index ff2d97d17..7bd082c62 100644 --- a/catanatron_core/catanatron/models/board.py +++ b/catanatron_core/catanatron/models/board.py @@ -11,15 +11,15 @@ BASE_MAP_TEMPLATE, MINI_MAP_TEMPLATE, NUM_NODES, - CatanMap, + MapInstance, NodeId, ) from catanatron.models.enums import FastBuildingType, SETTLEMENT, CITY # Used to find relationships between nodes and edges -base_map = CatanMap.from_template(BASE_MAP_TEMPLATE) -mini_map = CatanMap.from_template(MINI_MAP_TEMPLATE) +base_map = MapInstance.from_template(BASE_MAP_TEMPLATE) +mini_map = MapInstance.from_template(MINI_MAP_TEMPLATE) STATIC_GRAPH = nx.Graph() for tile in base_map.tiles.values(): STATIC_GRAPH.add_nodes_from(tile.nodes.values()) @@ -54,12 +54,12 @@ class Board: robber_coordinate (Coordinate): Coordinate where robber is. """ - def __init__(self, catan_map=None, initialize=True): + def __init__(self, map_instance=None, initialize=True): self.buildable_subgraph: Any = None self.buildable_edges_cache = {} self.player_port_resources_cache = {} if initialize: - self.map: CatanMap = catan_map or CatanMap.from_template( + self.map: MapInstance = map_instance or MapInstance.from_template( BASE_MAP_TEMPLATE ) # Static State (no need to copy) diff --git a/catanatron_core/catanatron/models/map.py b/catanatron_core/catanatron/models/map.py index ec357399d..8dd1d1386 100644 --- a/catanatron_core/catanatron/models/map.py +++ b/catanatron_core/catanatron/models/map.py @@ -33,7 +33,7 @@ NUM_TILES = 19 -class CatanMap: +class MapInstance: """Represents a randomly initialized map.""" def __init__( @@ -60,11 +60,11 @@ def __init__( def from_template(map_template: MapTemplate): tiles = initialize_tiles(map_template) - return CatanMap.from_tiles(tiles) + return MapInstance.from_tiles(tiles) @staticmethod def from_tiles(tiles: Dict[Coordinate, Tile]): - self = CatanMap() + self = MapInstance() self.tiles = tiles self.land_tiles = { @@ -354,13 +354,13 @@ def get_edge_nodes(edge_ref): None, ], ) -TOURNAMENT_MAP = CatanMap.from_tiles(TOURNAMENT_MAP_TILES) +TOURNAMENT_MAP = MapInstance.from_tiles(TOURNAMENT_MAP_TILES) def build_map(map_type: Literal["BASE", "TOURNAMENT", "MINI"]): if map_type == "TOURNAMENT": return TOURNAMENT_MAP # this assumes map is read-only data struct elif map_type == "MINI": - return CatanMap.from_template(MINI_MAP_TEMPLATE) + return MapInstance.from_template(MINI_MAP_TEMPLATE) else: - return CatanMap.from_template(BASE_MAP_TEMPLATE) + return MapInstance.from_template(BASE_MAP_TEMPLATE) diff --git a/catanatron_core/catanatron/state.py b/catanatron_core/catanatron/state.py index 31dbc6e2a..4e6a071e3 100644 --- a/catanatron_core/catanatron/state.py +++ b/catanatron_core/catanatron/state.py @@ -7,7 +7,7 @@ from collections import defaultdict from typing import Any, List, Tuple, Dict, Iterable -from catanatron.models.map import BASE_MAP_TEMPLATE, CatanMap +from catanatron.models.map import BASE_MAP_TEMPLATE, MapInstance from catanatron.models.board import Board from catanatron.models.enums import ( DEVELOPMENT_CARDS, @@ -128,14 +128,16 @@ class State: def __init__( self, players: List[Player], - catan_map=None, + map_instance=None, discard_limit=7, initialize=True, ): if initialize: self.players = random.sample(players, len(players)) self.colors = tuple([player.color for player in self.players]) - self.board = Board(catan_map or CatanMap.from_template(BASE_MAP_TEMPLATE)) + self.board = Board( + map_instance or MapInstance.from_template(BASE_MAP_TEMPLATE) + ) self.discard_limit = discard_limit # feature-ready dictionary diff --git a/catanatron_experimental/catanatron_experimental/play.py b/catanatron_experimental/catanatron_experimental/play.py index 1d796d652..76f930b12 100644 --- a/catanatron_experimental/catanatron_experimental/play.py +++ b/catanatron_experimental/catanatron_experimental/play.py @@ -203,7 +203,7 @@ class OutputOptions: class GameConfigOptions: discard_limit: int = 7 vps_to_win: int = 10 - catan_map: Literal["BASE", "TOURNAMENT", "MINI"] = "BASE" + map_instance: Literal["BASE", "TOURNAMENT", "MINI"] = "BASE" COLOR_TO_RICH_STYLE = { @@ -234,12 +234,12 @@ def play_batch_core(num_games, players, game_config, accumulators=[]): for _ in range(num_games): for player in players: player.reset_state() - catan_map = build_map(game_config.catan_map) + map_instance = build_map(game_config.map_instance) game = Game( players, discard_limit=game_config.discard_limit, vps_to_win=game_config.vps_to_win, - catan_map=catan_map, + map_instance=map_instance, ) game.play(accumulators) yield game diff --git a/catanatron_gym/catanatron_gym/envs/catanatron_env.py b/catanatron_gym/catanatron_gym/envs/catanatron_env.py index 45f7f1846..acad16fa9 100644 --- a/catanatron_gym/catanatron_gym/envs/catanatron_env.py +++ b/catanatron_gym/catanatron_gym/envs/catanatron_env.py @@ -224,13 +224,13 @@ def reset( ): super().reset(seed=seed) - catan_map = build_map(self.map_type) + map_instance = build_map(self.map_type) for player in self.players: player.reset_state() self.game = Game( players=self.players, seed=seed, - catan_map=catan_map, + map_instance=map_instance, vps_to_win=self.vps_to_win, ) self.invalid_actions_count = 0 diff --git a/catanatron_gym/catanatron_gym/features.py b/catanatron_gym/catanatron_gym/features.py index 1d6f5b3a1..a5935cf31 100644 --- a/catanatron_gym/catanatron_gym/features.py +++ b/catanatron_gym/catanatron_gym/features.py @@ -12,7 +12,7 @@ player_num_resource_cards, ) from catanatron.models.board import STATIC_GRAPH, get_edges, get_node_distances -from catanatron.models.map import NUM_TILES, CatanMap, build_map +from catanatron.models.map import NUM_TILES, MapInstance, build_map from catanatron.models.player import Color, SimplePlayer from catanatron.models.enums import ( DEVELOPMENT_CARDS, @@ -126,12 +126,12 @@ def resource_hand_features(game: Game, p0_color: Color): @functools.lru_cache(NUM_TILES * 2) # one for each robber, and acount for Minimap -def map_tile_features(catan_map: CatanMap, robber_coordinate): +def map_tile_features(map_instance: MapInstance, robber_coordinate): # Returns list of functions that take a game and output a feature. # build features like tile0_is_wood, tile0_is_wheat, ..., tile0_proba, tile0_hasrobber features = {} - for tile_id, tile in catan_map.tiles_by_id.items(): + for tile_id, tile in map_instance.tiles_by_id.items(): for resource in RESOURCES: features[f"TILE{tile_id}_IS_{resource}"] = tile.resource == resource features[f"TILE{tile_id}_IS_DESERT"] = tile.resource == None @@ -139,7 +139,7 @@ def map_tile_features(catan_map: CatanMap, robber_coordinate): 0 if tile.resource is None else number_probability(tile.number) ) features[f"TILE{tile_id}_HAS_ROBBER"] = ( - catan_map.tiles[robber_coordinate] == tile + map_instance.tiles[robber_coordinate] == tile ) return features @@ -151,9 +151,9 @@ def tile_features(game: Game, p0_color: Color): @functools.lru_cache(1) -def map_port_features(catan_map): +def map_port_features(map_instance): features = {} - for port_id, port in catan_map.ports_by_id.items(): + for port_id, port in map_instance.ports_by_id.items(): for resource in RESOURCES: features[f"PORT{port_id}_IS_{resource}"] = port.resource == resource features[f"PORT{port_id}_IS_THREE_TO_ONE"] = port.resource is None @@ -166,13 +166,13 @@ def port_features(game, p0_color): @functools.lru_cache(4) -def initialize_graph_features_template(num_players, catan_map: CatanMap): +def initialize_graph_features_template(num_players, map_instance: MapInstance): features = {} for i in range(num_players): - for node_id in range(len(catan_map.land_nodes)): + for node_id in range(len(map_instance.land_nodes)): for building in [SETTLEMENT, CITY]: features[f"NODE{node_id}_P{i}_{building}"] = False - for edge in get_edges(catan_map.land_nodes): + for edge in get_edges(map_instance.land_nodes): features[f"EDGE{edge}_P{i}_ROAD"] = False return features @@ -239,8 +239,8 @@ def production_features(game: Game, p0_color: Color): @functools.lru_cache(maxsize=1000) -def get_node_production(catan_map, node_id, resource): - tiles = catan_map.adjacent_tiles[node_id] +def get_node_production(map_instance, node_id, resource): + tiles = map_instance.adjacent_tiles[node_id] return sum([number_probability(t.number) for t in tiles if t.resource == resource]) @@ -369,10 +369,10 @@ def reachability_features(game: Game, p0_color: Color, levels=REACHABLE_FEATURES @functools.lru_cache(maxsize=1000) -def count_production(nodes, catan_map): +def count_production(nodes, map_instance): production = Counter() for node_id in nodes: - production += catan_map.node_production[node_id] + production += map_instance.node_production[node_id] return production @@ -534,6 +534,6 @@ def get_feature_ordering( SimplePlayer(Color.ORANGE), ] players = players[:num_players] - game = Game(players, catan_map=build_map(map_type)) + game = Game(players, map_instance=build_map(map_type)) sample = create_sample(game, players[0].color) return sorted(sample.keys()) diff --git a/tests/models/test_board.py b/tests/models/test_board.py index c212df89e..f4a55e2f3 100644 --- a/tests/models/test_board.py +++ b/tests/models/test_board.py @@ -1,6 +1,6 @@ import pytest -from catanatron.models.map import MINI_MAP_TEMPLATE, CatanMap +from catanatron.models.map import MINI_MAP_TEMPLATE, MapInstance from catanatron.models.enums import RESOURCES from catanatron.models.board import Board, get_node_distances from catanatron.models.player import Color @@ -98,7 +98,7 @@ def test_buildable_nodes(): def test_buildable_nodes_in_mini_map(): - board = Board(catan_map=CatanMap.from_template(MINI_MAP_TEMPLATE)) + board = Board(map_instance=MapInstance.from_template(MINI_MAP_TEMPLATE)) nodes = board.buildable_node_ids(Color.RED) assert len(nodes) == 0 nodes = board.buildable_node_ids(Color.RED, initial_build_phase=True) @@ -156,7 +156,7 @@ def test_buildable_edges_simple(): def test_buildable_edges_in_mini(): - board = Board(catan_map=CatanMap.from_template(MINI_MAP_TEMPLATE)) + board = Board(map_instance=MapInstance.from_template(MINI_MAP_TEMPLATE)) board.build_settlement(Color.RED, 19, initial_build_phase=True) buildable = board.buildable_edges(Color.RED) assert len(buildable) == 2 diff --git a/tests/models/test_map.py b/tests/models/test_map.py index 9175c6bb7..22294182e 100644 --- a/tests/models/test_map.py +++ b/tests/models/test_map.py @@ -2,7 +2,7 @@ from catanatron.models.map import ( BASE_MAP_TEMPLATE, MINI_MAP_TEMPLATE, - CatanMap, + MapInstance, LandTile, NodeRef, get_nodes_and_edges, @@ -25,7 +25,7 @@ def test_node_production_of_same_resource_adjacent_tile(): def test_mini_map_can_be_created(): - mini = CatanMap.from_template(MINI_MAP_TEMPLATE) + mini = MapInstance.from_template(MINI_MAP_TEMPLATE) assert mini.tiles[(0, 0, 0)].nodes[NodeRef.NORTH] == 0 assert len(mini.land_tiles) == 7 assert len(mini.land_nodes) == 24 @@ -41,9 +41,9 @@ def test_mini_map_can_be_created(): def test_base_map_can_be_created(): - catan_map = CatanMap.from_template(BASE_MAP_TEMPLATE) - assert len(catan_map.land_tiles) == 19 - assert len(catan_map.node_production) == 54 + map_instance = MapInstance.from_template(BASE_MAP_TEMPLATE) + assert len(map_instance.land_tiles) == 19 + assert len(map_instance.node_production) == 54 def test_get_nodes_and_edges_on_empty_board(): diff --git a/tests/test_machine_learning.py b/tests/test_machine_learning.py index 966ba8e6c..6b23cc888 100644 --- a/tests/test_machine_learning.py +++ b/tests/test_machine_learning.py @@ -14,7 +14,7 @@ from catanatron.models.map import ( NUM_EDGES, NUM_NODES, - CatanMap, + MapInstance, ) from catanatron.game import Game from catanatron.models.map import number_probability @@ -128,8 +128,8 @@ def test_reachability_features(): # We do this here to allow Game.__init__ evolve freely. random.seed(123) random.sample(players, len(players)) - catan_map = CatanMap.from_template(BASE_MAP_TEMPLATE) - game = Game(players, seed=123, catan_map=catan_map) + map_instance = MapInstance.from_template(BASE_MAP_TEMPLATE) + game = Game(players, seed=123, map_instance=map_instance) p0_color = game.state.colors[0] game.execute(Action(p0_color, ActionType.BUILD_SETTLEMENT, 5)) @@ -199,7 +199,7 @@ def test_tile_features_in_mini(): SimplePlayer(Color.RED), SimplePlayer(Color.BLUE), ] - game = Game(players, catan_map=CatanMap.from_template(MINI_MAP_TEMPLATE)) + game = Game(players, map_instance=MapInstance.from_template(MINI_MAP_TEMPLATE)) features = tile_features(game, players[0].color) haystack = "".join(features.keys()) @@ -211,7 +211,7 @@ def test_port_features_in_mini(): SimplePlayer(Color.RED), SimplePlayer(Color.BLUE), ] - game = Game(players, catan_map=CatanMap.from_template(MINI_MAP_TEMPLATE)) + game = Game(players, map_instance=MapInstance.from_template(MINI_MAP_TEMPLATE)) features = port_features(game, players[0].color) assert len(features) == 0 @@ -249,7 +249,7 @@ def test_graph_features_in_mini(): SimplePlayer(Color.RED), SimplePlayer(Color.BLUE), ] - game = Game(players, catan_map=CatanMap.from_template(MINI_MAP_TEMPLATE)) + game = Game(players, map_instance=MapInstance.from_template(MINI_MAP_TEMPLATE)) p0_color = game.state.colors[0] game.execute(Action(p0_color, ActionType.BUILD_SETTLEMENT, 3)) game.execute(Action(p0_color, ActionType.BUILD_ROAD, (2, 3))) @@ -380,8 +380,8 @@ def test_resource_proba_planes(): # We do this here to allow Game.__init__ evolve freely. random.seed(123) random.sample(players, len(players)) - catan_map = CatanMap.from_template(BASE_MAP_TEMPLATE) - game = Game(players, seed=123, catan_map=catan_map) + map_instance = MapInstance.from_template(BASE_MAP_TEMPLATE) + game = Game(players, seed=123, map_instance=map_instance) tensor = create_board_tensor(game, players[0].color) assert tensor[0][0][0] == 0 From 1cc30e3c1d3db8f1beb68f57ca9b7ddcd2b8c3d2 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 15 Oct 2024 21:45:09 -0400 Subject: [PATCH 22/83] Rename map.py to map_instance.py --- catanatron_core/catanatron/game.py | 2 +- catanatron_core/catanatron/json.py | 2 +- catanatron_core/catanatron/models/board.py | 2 +- catanatron_core/catanatron/models/{map.py => map_instance.py} | 0 catanatron_core/catanatron/state.py | 2 +- .../machine_learning/players/tree_search_utils.py | 2 +- catanatron_experimental/catanatron_experimental/play.py | 2 +- catanatron_gym/catanatron_gym/board_tensor_features.py | 2 +- catanatron_gym/catanatron_gym/envs/catanatron_env.py | 2 +- catanatron_gym/catanatron_gym/features.py | 4 ++-- tests/models/test_board.py | 2 +- tests/models/test_map.py | 2 +- tests/test_machine_learning.py | 4 ++-- 13 files changed, 14 insertions(+), 14 deletions(-) rename catanatron_core/catanatron/models/{map.py => map_instance.py} (100%) diff --git a/catanatron_core/catanatron/game.py b/catanatron_core/catanatron/game.py index baa7cc1e5..6140779bc 100644 --- a/catanatron_core/catanatron/game.py +++ b/catanatron_core/catanatron/game.py @@ -10,7 +10,7 @@ from catanatron.models.enums import Action, ActionPrompt, ActionType from catanatron.state import State, apply_action from catanatron.state_functions import player_key, player_has_rolled -from catanatron.models.map import MapInstance +from catanatron.models.map_instance import MapInstance from catanatron.models.player import Color, Player # To timeout RandomRobots from getting stuck... diff --git a/catanatron_core/catanatron/json.py b/catanatron_core/catanatron/json.py index a04055f5a..0ec342cc9 100644 --- a/catanatron_core/catanatron/json.py +++ b/catanatron_core/catanatron/json.py @@ -5,7 +5,7 @@ import json from enum import Enum -from catanatron.models.map import Water, Port, LandTile +from catanatron.models.map_instance import Water, Port, LandTile from catanatron.game import Game from catanatron.models.player import Color from catanatron.models.enums import RESOURCES, Action, ActionType diff --git a/catanatron_core/catanatron/models/board.py b/catanatron_core/catanatron/models/board.py index 7bd082c62..d8164b724 100644 --- a/catanatron_core/catanatron/models/board.py +++ b/catanatron_core/catanatron/models/board.py @@ -7,7 +7,7 @@ import networkx as nx # type: ignore from catanatron.models.player import Color -from catanatron.models.map import ( +from catanatron.models.map_instance import ( BASE_MAP_TEMPLATE, MINI_MAP_TEMPLATE, NUM_NODES, diff --git a/catanatron_core/catanatron/models/map.py b/catanatron_core/catanatron/models/map_instance.py similarity index 100% rename from catanatron_core/catanatron/models/map.py rename to catanatron_core/catanatron/models/map_instance.py diff --git a/catanatron_core/catanatron/state.py b/catanatron_core/catanatron/state.py index 4e6a071e3..b6744685a 100644 --- a/catanatron_core/catanatron/state.py +++ b/catanatron_core/catanatron/state.py @@ -7,7 +7,7 @@ from collections import defaultdict from typing import Any, List, Tuple, Dict, Iterable -from catanatron.models.map import BASE_MAP_TEMPLATE, MapInstance +from catanatron.models.map_instance import BASE_MAP_TEMPLATE, MapInstance from catanatron.models.board import Board from catanatron.models.enums import ( DEVELOPMENT_CARDS, diff --git a/catanatron_experimental/catanatron_experimental/machine_learning/players/tree_search_utils.py b/catanatron_experimental/catanatron_experimental/machine_learning/players/tree_search_utils.py index 1142a0411..f3c1f2222 100644 --- a/catanatron_experimental/catanatron_experimental/machine_learning/players/tree_search_utils.py +++ b/catanatron_experimental/catanatron_experimental/machine_learning/players/tree_search_utils.py @@ -1,7 +1,7 @@ import math from collections import defaultdict -from catanatron.models.map import number_probability +from catanatron.models.map_instance import number_probability from catanatron.models.enums import ( DEVELOPMENT_CARDS, RESOURCES, diff --git a/catanatron_experimental/catanatron_experimental/play.py b/catanatron_experimental/catanatron_experimental/play.py index 76f930b12..c49a8d755 100644 --- a/catanatron_experimental/catanatron_experimental/play.py +++ b/catanatron_experimental/catanatron_experimental/play.py @@ -15,7 +15,7 @@ from catanatron.game import Game from catanatron.models.player import Color -from catanatron.models.map import build_map +from catanatron.models.map_instance import build_map from catanatron.state_functions import get_actual_victory_points # try to suppress TF output before any potentially tf-importing modules diff --git a/catanatron_gym/catanatron_gym/board_tensor_features.py b/catanatron_gym/catanatron_gym/board_tensor_features.py index aabdb4cf7..5de9d4bdc 100644 --- a/catanatron_gym/catanatron_gym/board_tensor_features.py +++ b/catanatron_gym/catanatron_gym/board_tensor_features.py @@ -12,7 +12,7 @@ ) from catanatron.models.coordinate_system import offset_to_cube from catanatron.models.board import STATIC_GRAPH -from catanatron.models.map import number_probability +from catanatron.models.map_instance import number_probability from catanatron_gym.features import get_feature_ordering, iter_players # These assume 4 players diff --git a/catanatron_gym/catanatron_gym/envs/catanatron_env.py b/catanatron_gym/catanatron_gym/envs/catanatron_env.py index acad16fa9..7fd662e00 100644 --- a/catanatron_gym/catanatron_gym/envs/catanatron_env.py +++ b/catanatron_gym/catanatron_gym/envs/catanatron_env.py @@ -5,7 +5,7 @@ from catanatron.game import Game, TURNS_LIMIT from catanatron.models.player import Color, Player, RandomPlayer from catanatron.models.map_template import BASE_MAP_TEMPLATE, LandTile -from catanatron.models.map import NUM_NODES, build_map +from catanatron.models.map_instance import NUM_NODES, build_map from catanatron.models.enums import RESOURCES, Action, ActionType from catanatron.models.board import get_edges from catanatron_gym.features import ( diff --git a/catanatron_gym/catanatron_gym/features.py b/catanatron_gym/catanatron_gym/features.py index a5935cf31..8a289ac12 100644 --- a/catanatron_gym/catanatron_gym/features.py +++ b/catanatron_gym/catanatron_gym/features.py @@ -12,7 +12,7 @@ player_num_resource_cards, ) from catanatron.models.board import STATIC_GRAPH, get_edges, get_node_distances -from catanatron.models.map import NUM_TILES, MapInstance, build_map +from catanatron.models.map_instance import NUM_TILES, MapInstance, build_map from catanatron.models.player import Color, SimplePlayer from catanatron.models.enums import ( DEVELOPMENT_CARDS, @@ -24,7 +24,7 @@ VICTORY_POINT, ) from catanatron.game import Game -from catanatron.models.map import number_probability +from catanatron.models.map_instance import number_probability # ===== Helpers diff --git a/tests/models/test_board.py b/tests/models/test_board.py index f4a55e2f3..791dc6647 100644 --- a/tests/models/test_board.py +++ b/tests/models/test_board.py @@ -1,6 +1,6 @@ import pytest -from catanatron.models.map import MINI_MAP_TEMPLATE, MapInstance +from catanatron.models.map_instance import MINI_MAP_TEMPLATE, MapInstance from catanatron.models.enums import RESOURCES from catanatron.models.board import Board, get_node_distances from catanatron.models.player import Color diff --git a/tests/models/test_map.py b/tests/models/test_map.py index 22294182e..72c1cee99 100644 --- a/tests/models/test_map.py +++ b/tests/models/test_map.py @@ -1,5 +1,5 @@ from catanatron import WOOD, BRICK -from catanatron.models.map import ( +from catanatron.models.map_instance import ( BASE_MAP_TEMPLATE, MINI_MAP_TEMPLATE, MapInstance, diff --git a/tests/test_machine_learning.py b/tests/test_machine_learning.py index 6b23cc888..8f1d6a46a 100644 --- a/tests/test_machine_learning.py +++ b/tests/test_machine_learning.py @@ -11,13 +11,13 @@ BASE_MAP_TEMPLATE, MINI_MAP_TEMPLATE, ) -from catanatron.models.map import ( +from catanatron.models.map_instance import ( NUM_EDGES, NUM_NODES, MapInstance, ) from catanatron.game import Game -from catanatron.models.map import number_probability +from catanatron.models.map_instance import number_probability from catanatron.models.player import SimplePlayer, Color from catanatron_gym.features import ( create_sample, From 7cd50f1ca4373a6225ed85bb11ba9a1862ec3abc Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Thu, 17 Oct 2024 07:29:14 -0400 Subject: [PATCH 23/83] OrderedHashMap and TileSlot fix --- catanatron_rust/src/global_state.rs | 11 ++++----- catanatron_rust/src/lib.rs | 1 + catanatron_rust/src/map_template.rs | 22 +++++++++++------ catanatron_rust/src/ordered_hashmap.rs | 34 ++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 14 deletions(-) create mode 100644 catanatron_rust/src/ordered_hashmap.rs diff --git a/catanatron_rust/src/global_state.rs b/catanatron_rust/src/global_state.rs index 4492a4b31..397c827aa 100644 --- a/catanatron_rust/src/global_state.rs +++ b/catanatron_rust/src/global_state.rs @@ -1,20 +1,19 @@ -use std::collections::HashMap; - use crate::{ enums::Resource, map_template::{MapTemplate, TileSlot}, + ordered_hashmap::OrderedHashMap, }; #[derive(Debug)] pub struct GlobalState { - mini_map_template: MapTemplate, - base_map_template: MapTemplate, + pub mini_map_template: MapTemplate, + pub base_map_template: MapTemplate, } impl GlobalState { pub fn new() -> Self { // Mini Map Template - let mut topology = HashMap::new(); + let mut topology = OrderedHashMap::new(); // center topology.insert((0, 0, 0), TileSlot::Land); // first layer @@ -52,7 +51,7 @@ impl GlobalState { topology, }; // Base Map Template - let mut topology = HashMap::new(); + let mut topology = OrderedHashMap::new(); // center topology.insert((0, 0, 0), TileSlot::Land); // first layer diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index f6c3b8c36..b3ddd17a3 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -2,4 +2,5 @@ pub mod decks; pub mod enums; pub mod global_state; pub mod map_template; +mod ordered_hashmap; pub mod state; diff --git a/catanatron_rust/src/map_template.rs b/catanatron_rust/src/map_template.rs index caab8a6bf..5bc8d4778 100644 --- a/catanatron_rust/src/map_template.rs +++ b/catanatron_rust/src/map_template.rs @@ -1,26 +1,32 @@ -use crate::enums::Resource; -use std::collections::HashMap; +use crate::{enums::Resource, ordered_hashmap::OrderedHashMap}; -#[derive(Debug)] +// Define a new struct to wrap the tuple +pub type Coordinate = (i8, i8, i8); + +// Method to add two coordinates +pub fn add_coordinates(a: Coordinate, b: Coordinate) -> Coordinate { + (a.0 + b.0, a.1 + b.1, a.2 + b.2) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TileSlot { Land, Water, NWPort, - NPort, NEPort, EPort, SEPort, - SPort, SWPort, WPort, } -type Coordinate = (i8, i8, i8); - #[derive(Debug)] pub struct MapTemplate { pub(crate) numbers: Vec, pub(crate) ports: Vec>, pub(crate) tiles: Vec>, - pub(crate) topology: HashMap, + + // Ordered, so that when map is built, we keep the same node-id, edge-id, and tile-id. + // that original catanatron uses. + pub(crate) topology: OrderedHashMap, } diff --git a/catanatron_rust/src/ordered_hashmap.rs b/catanatron_rust/src/ordered_hashmap.rs new file mode 100644 index 000000000..325eb99ab --- /dev/null +++ b/catanatron_rust/src/ordered_hashmap.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +#[derive(Debug)] +pub struct OrderedHashMap { + map: HashMap, + keys: Vec, +} + +impl OrderedHashMap { + pub fn new() -> Self { + OrderedHashMap { + map: HashMap::new(), + keys: Vec::new(), + } + } + + pub fn insert(&mut self, key: K, value: V) { + if !self.map.contains_key(&key) { + self.keys.push(key.clone()); + } + self.map.insert(key, value); + } + + pub fn iter(&self) -> impl Iterator { + self.keys + .iter() + .filter_map(move |key| self.map.get(key).map(|value| (key, value))) + } + + #[cfg(test)] + pub fn len(&self) -> usize { + self.keys.len() + } +} From 8566708705a5ba61bbadbe4df0f31c686f74f530 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Thu, 17 Oct 2024 21:25:29 -0400 Subject: [PATCH 24/83] Initial Map Instance --- catanatron_rust/src/enums.rs | 2 +- catanatron_rust/src/global_state.rs | 2 +- catanatron_rust/src/lib.rs | 1 + catanatron_rust/src/main.rs | 5 + catanatron_rust/src/map_instance.rs | 466 ++++++++++++++++++++++++++++ 5 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 catanatron_rust/src/map_instance.rs diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 0d5a6b19f..5ed8ac568 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -8,7 +8,7 @@ pub enum Color { pub const COLORS: [Color; 4] = [Color::Red, Color::Blue, Color::Orange, Color::White]; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Resource { Wood, Brick, diff --git a/catanatron_rust/src/global_state.rs b/catanatron_rust/src/global_state.rs index 397c827aa..bba98bb66 100644 --- a/catanatron_rust/src/global_state.rs +++ b/catanatron_rust/src/global_state.rs @@ -55,7 +55,7 @@ impl GlobalState { // center topology.insert((0, 0, 0), TileSlot::Land); // first layer - topology.insert((-1, -1, 0), TileSlot::Land); + topology.insert((1, -1, 0), TileSlot::Land); topology.insert((0, -1, 1), TileSlot::Land); topology.insert((-1, 0, 1), TileSlot::Land); topology.insert((-1, 1, 0), TileSlot::Land); diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index b3ddd17a3..c2ab6e182 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,6 +1,7 @@ pub mod decks; pub mod enums; pub mod global_state; +pub mod map_instance; pub mod map_template; mod ordered_hashmap; pub mod state; diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 4344a1f3c..f64685c76 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -1,6 +1,7 @@ use catanatron_rust::decks; use catanatron_rust::enums::Resource; use catanatron_rust::global_state; +use catanatron_rust::map_instance::MapInstance; use catanatron_rust::state; use std::time::Instant; @@ -54,4 +55,8 @@ fn main() { let vector = state::initialize_state(); println!("Vector: {:?}", vector); + + let map_instance = MapInstance::new(&global_state.base_map_template, 0); + println!("Map Instance Tiles: {:?}", map_instance.tiles); + println!("Map Instance Land Tiles: {:?}", map_instance.land_tiles); } diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs new file mode 100644 index 000000000..f9a5c7105 --- /dev/null +++ b/catanatron_rust/src/map_instance.rs @@ -0,0 +1,466 @@ +use rand::rngs::StdRng; +use rand::{seq::SliceRandom, SeedableRng}; +use std::collections::HashMap; +use std::hash::Hash; + +use crate::enums::Resource; +use crate::map_template::{add_coordinates, Coordinate, MapTemplate, TileSlot}; + +type NodeId = u8; +type EdgeId = (NodeId, NodeId); + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub enum NodeRef { + North, + NorthEast, + SouthEast, + South, + SouthWest, + NorthWest, +} + +const NODE_REFS: [NodeRef; 6] = [ + NodeRef::North, + NodeRef::NorthEast, + NodeRef::SouthEast, + NodeRef::South, + NodeRef::SouthWest, + NodeRef::NorthWest, +]; + +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub enum EdgeRef { + East, + SouthEast, + SouthWest, + West, + NorthWest, + NorthEast, +} + +const EDGE_REFS: [EdgeRef; 6] = [ + EdgeRef::East, + EdgeRef::SouthEast, + EdgeRef::SouthWest, + EdgeRef::West, + EdgeRef::NorthWest, + EdgeRef::NorthEast, +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + East, + SouthEast, + SouthWest, + West, + NorthWest, + NorthEast, +} + +const DIRECTIONS: [Direction; 6] = [ + Direction::East, + Direction::SouthEast, + Direction::SouthWest, + Direction::West, + Direction::NorthWest, + Direction::NorthEast, +]; + +fn get_unit_vector(direction: Direction) -> (i8, i8, i8) { + match direction { + Direction::NorthEast => (1, 0, -1), + Direction::SouthWest => (-1, 0, 1), + Direction::NorthWest => (0, 1, -1), + Direction::SouthEast => (0, -1, 1), + Direction::East => (1, -1, 0), + Direction::West => (-1, 1, 0), + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Hexagon { + // TODO: id? + pub(crate) nodes: HashMap, + pub(crate) edges: HashMap, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LandTile { + pub(crate) hexagon: Hexagon, + pub(crate) resource: Option, + pub(crate) number: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PortTile { + pub(crate) hexagon: Hexagon, + pub(crate) resource: Option, + pub(crate) direction: Direction, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct WaterTile { + pub(crate) hexagon: Hexagon, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Tile { + Land(LandTile), + Port(PortTile), + Water(WaterTile), +} + +#[derive(Debug)] +pub struct MapInstance { + pub tiles: HashMap, + pub land_tiles: HashMap, +} + +impl MapInstance { + pub fn new(map_template: &MapTemplate, seed: u64) -> Self { + let tiles = Self::initialize_tiles(map_template, seed); + Self::from_tiles(tiles) + } + + fn initialize_tiles(map_template: &MapTemplate, seed: u64) -> HashMap { + let mut rng = StdRng::seed_from_u64(seed); + + // Shuffle the numbers, tiles, and ports + let mut shuffled_numbers = map_template.numbers.clone(); + shuffled_numbers.shuffle(&mut rng); + let mut shuffled_tiles = map_template.tiles.clone(); + shuffled_tiles.shuffle(&mut rng); + let mut shuffled_ports = map_template.ports.clone(); + shuffled_ports.shuffle(&mut rng); + + // Build the Hexagons by iterating over map_template.topology + let mut hexagons: HashMap = HashMap::new(); + let mut tiles: HashMap = HashMap::new(); + let mut 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; + let hexagon = Hexagon { nodes, edges }; + + if tile_slot == TileSlot::Land { + let resource = shuffled_tiles.pop().unwrap(); + if resource == None { + let land_tile = LandTile { + hexagon: hexagon.clone(), + resource, + number: None, + }; + tiles.insert(coordinate, Tile::Land(land_tile)); + } else { + let number = shuffled_numbers.pop().unwrap(); + let land_tile = LandTile { + hexagon: hexagon.clone(), + resource, + number: Some(number), + }; + tiles.insert(coordinate, Tile::Land(land_tile)); + } + } else if tile_slot == TileSlot::Water { + let water_tile = WaterTile { + hexagon: hexagon.clone(), + }; + tiles.insert(coordinate, Tile::Water(water_tile)); + } else { + let direction = match tile_slot { + TileSlot::NWPort => Direction::NorthWest, + TileSlot::NEPort => Direction::NorthEast, + TileSlot::EPort => Direction::East, + TileSlot::SEPort => Direction::SouthEast, + TileSlot::SWPort => Direction::SouthWest, + TileSlot::WPort => Direction::West, + _ => panic!("Invalid port tile slot"), + }; + let resource = shuffled_ports.pop().unwrap(); + let port_tile = PortTile { + hexagon: hexagon.clone(), + resource, + direction, + }; + tiles.insert(coordinate, Tile::Port(port_tile)); + } + + hexagons.insert(coordinate, hexagon); + } + tiles + } + + fn from_tiles(tiles: HashMap) -> Self { + let land_tiles: HashMap = tiles + .clone() + .into_iter() + .filter_map(|(coordinate, tile)| { + if let Tile::Land(land_tile) = tile { + Some((coordinate, land_tile)) + } else { + None + } + }) + .collect(); + + Self { tiles, land_tiles } + } +} + +fn get_nodes_edges( + hexagons: &HashMap, + coordinate: Coordinate, + mut node_autoinc: NodeId, +) -> (HashMap, HashMap, NodeId) { + let mut nodes = HashMap::new(); + let mut edges = HashMap::new(); + + // Insert Pre-existing Nodes and Edges + let neighbor_hexagons: Vec<(Direction, Coordinate)> = DIRECTIONS + .iter() + .map(|&direction| { + let unit_vector = get_unit_vector(direction); + (direction, add_coordinates(coordinate, unit_vector)) + }) + .collect::>(); + for (neighbor_direction, neighbor_coordinate) in neighbor_hexagons { + if hexagons.contains_key(&neighbor_coordinate) { + let neighbor_hexagon = hexagons.get(&neighbor_coordinate).unwrap(); + + if neighbor_direction == Direction::East { + nodes.insert( + NodeRef::NorthEast, + *neighbor_hexagon.nodes.get(&NodeRef::NorthWest).unwrap(), + ); + nodes.insert( + NodeRef::SouthEast, + *neighbor_hexagon.nodes.get(&NodeRef::SouthWest).unwrap(), + ); + edges.insert( + EdgeRef::East, + *neighbor_hexagon.edges.get(&EdgeRef::West).unwrap(), + ); + } else if neighbor_direction == Direction::SouthEast { + nodes.insert( + NodeRef::South, + *neighbor_hexagon.nodes.get(&NodeRef::NorthWest).unwrap(), + ); + nodes.insert( + NodeRef::SouthEast, + *neighbor_hexagon.nodes.get(&NodeRef::North).unwrap(), + ); + edges.insert( + EdgeRef::SouthEast, + *neighbor_hexagon.edges.get(&EdgeRef::NorthWest).unwrap(), + ); + } else if neighbor_direction == Direction::SouthWest { + nodes.insert( + NodeRef::South, + *neighbor_hexagon.nodes.get(&NodeRef::NorthEast).unwrap(), + ); + nodes.insert( + NodeRef::SouthWest, + *neighbor_hexagon.nodes.get(&NodeRef::North).unwrap(), + ); + edges.insert( + EdgeRef::SouthWest, + *neighbor_hexagon.edges.get(&EdgeRef::NorthEast).unwrap(), + ); + } else if neighbor_direction == Direction::West { + nodes.insert( + NodeRef::NorthWest, + *neighbor_hexagon.nodes.get(&NodeRef::NorthEast).unwrap(), + ); + nodes.insert( + NodeRef::SouthWest, + *neighbor_hexagon.nodes.get(&NodeRef::SouthEast).unwrap(), + ); + edges.insert( + EdgeRef::West, + *neighbor_hexagon.edges.get(&EdgeRef::East).unwrap(), + ); + } else if neighbor_direction == Direction::NorthWest { + nodes.insert( + NodeRef::North, + *neighbor_hexagon.nodes.get(&NodeRef::SouthEast).unwrap(), + ); + nodes.insert( + NodeRef::NorthWest, + *neighbor_hexagon.nodes.get(&NodeRef::South).unwrap(), + ); + edges.insert( + EdgeRef::NorthWest, + *neighbor_hexagon.edges.get(&EdgeRef::SouthEast).unwrap(), + ); + } else if neighbor_direction == Direction::NorthEast { + nodes.insert( + NodeRef::North, + *neighbor_hexagon.nodes.get(&NodeRef::SouthWest).unwrap(), + ); + nodes.insert( + NodeRef::NorthEast, + *neighbor_hexagon.nodes.get(&NodeRef::South).unwrap(), + ); + edges.insert( + EdgeRef::NorthEast, + *neighbor_hexagon.edges.get(&EdgeRef::SouthWest).unwrap(), + ); + } else { + panic!("Something went wrong"); + } + } + } + + // Insert New Nodes and Edges + for noderef in NODE_REFS { + if !nodes.contains_key(&noderef) { + nodes.insert(noderef, node_autoinc); + node_autoinc += 1; + } + } + for edgeref in EDGE_REFS { + let (a_noderef, b_noderef) = get_noderefs(edgeref); + if !edges.contains_key(&edgeref) { + let edge_nodes = ( + *nodes.get(&a_noderef).unwrap(), + *nodes.get(&b_noderef).unwrap(), + ); + edges.insert(edgeref, edge_nodes); + } + } + + (nodes, edges, node_autoinc) +} + +fn get_noderefs(edgeref: EdgeRef) -> (NodeRef, NodeRef) { + match edgeref { + EdgeRef::East => (NodeRef::NorthEast, NodeRef::SouthEast), + EdgeRef::SouthEast => (NodeRef::SouthEast, NodeRef::South), + EdgeRef::SouthWest => (NodeRef::South, NodeRef::SouthWest), + EdgeRef::West => (NodeRef::SouthWest, NodeRef::NorthWest), + EdgeRef::NorthWest => (NodeRef::NorthWest, NodeRef::North), + EdgeRef::NorthEast => (NodeRef::North, NodeRef::NorthEast), + } +} + +#[cfg(test)] +mod tests { + use crate::global_state::GlobalState; + + use super::*; + + #[test] + fn test_get_nodes_edges() { + let autoinc = 0; + let (nodes, edges, autoinc) = get_nodes_edges(&HashMap::new(), (0, 0, 0), autoinc); + assert!(nodes.len() == 6); + assert!(edges.len() == 6); + assert_eq!(autoinc, 6); + } + + #[test] + fn test_get_nodes_and_edges_for_east_attachment() { + let mut tiles = HashMap::new(); + let autoinc = 0; + let (nodes1, edges1, autoinc1) = get_nodes_edges(&tiles, (0, 0, 0), autoinc); + + tiles.insert( + (0, 0, 0), + Hexagon { + nodes: nodes1, + edges: edges1, + }, + ); + let (nodes2, edges2, autoinc2) = get_nodes_edges(&tiles, (1, -1, 0), autoinc1); + assert_eq!(nodes2.len(), 6); + assert_eq!(nodes2.get(&NodeRef::SouthEast), Some(&8)); + assert_eq!(nodes2.values().max(), Some(&9)); + assert_eq!(edges2.len(), 6); + assert_eq!(edges2.get(&EdgeRef::East), Some(&(7, 8))); + assert_eq!(autoinc2, 10); + } + + fn assert_node_value( + map_instance: &MapInstance, + coordinates: Coordinate, + node_ref: NodeRef, + expected_value: u8, + ) { + assert_eq!( + map_instance + .land_tiles + .get(&coordinates) + .unwrap() + .hexagon + .nodes + .get(&node_ref), + Some(&expected_value) + ); + } + + #[test] + fn test_map_instance() { + let global_state = GlobalState::new(); + let map_instance = MapInstance::new(&global_state.base_map_template, 0); + + assert_eq!(map_instance.tiles.len(), 37); + assert_eq!(map_instance.land_tiles.len(), 19); + + // Assert tile at 0,0,0 is Land with right resource and number + assert_eq!( + map_instance.tiles.get(&(0, 0, 0)), + Some(&Tile::Land(LandTile { + hexagon: Hexagon { + nodes: HashMap::from([ + (NodeRef::North, 0), + (NodeRef::NorthEast, 1), + (NodeRef::SouthEast, 2), + (NodeRef::South, 3), + (NodeRef::SouthWest, 4), + (NodeRef::NorthWest, 5) + ]), + edges: HashMap::from([ + (EdgeRef::East, (1, 2)), + (EdgeRef::SouthEast, (2, 3)), + (EdgeRef::SouthWest, (3, 4)), + (EdgeRef::West, (4, 5)), + (EdgeRef::NorthWest, (5, 0)), + (EdgeRef::NorthEast, (0, 1)) + ]) + }, + resource: Some(Resource::Wood), + number: Some(10) + })) + ); + assert_eq!( + map_instance.tiles.get(&(1, -1, 0)), + Some(&Tile::Land(LandTile { + hexagon: Hexagon { + nodes: HashMap::from([ + (NodeRef::North, 6), + (NodeRef::NorthEast, 7), + (NodeRef::SouthEast, 8), + (NodeRef::South, 9), + (NodeRef::SouthWest, 2), + (NodeRef::NorthWest, 1) + ]), + edges: HashMap::from([ + (EdgeRef::East, (7, 8)), + (EdgeRef::SouthEast, (8, 9)), + (EdgeRef::SouthWest, (9, 2)), + (EdgeRef::West, (1, 2)), + (EdgeRef::NorthWest, (1, 6)), + (EdgeRef::NorthEast, (6, 7)) + ]) + }, + resource: Some(Resource::Ore), + number: Some(9) + })) + ); + + // Spot-check several node ids + assert_node_value(&map_instance, (-1, 1, 0), NodeRef::SouthWest, 17); + assert_node_value(&map_instance, (1, 0, -1), NodeRef::NorthEast, 23); + assert_node_value(&map_instance, (-1, 2, -1), NodeRef::North, 43); + assert_node_value(&map_instance, (0, -2, 2), NodeRef::NorthWest, 11); + } +} From 075b8426a8ad40472b831bf3d6e5f000705d0530 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Thu, 17 Oct 2024 21:29:34 -0400 Subject: [PATCH 25/83] Make MapInstance immutable --- catanatron_rust/src/main.rs | 7 +++++-- catanatron_rust/src/map_instance.rs | 20 +++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index f64685c76..1c16b0665 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -57,6 +57,9 @@ fn main() { println!("Vector: {:?}", vector); let map_instance = MapInstance::new(&global_state.base_map_template, 0); - println!("Map Instance Tiles: {:?}", map_instance.tiles); - println!("Map Instance Land Tiles: {:?}", map_instance.land_tiles); + println!("Map Instance Tiles: {:?}", map_instance.get_tile((0, 0, 0))); + println!( + "Map Instance Land Tiles: {:?}", + map_instance.get_land_tile((1, 0, -1)) + ); } diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index f9a5c7105..f26d76e30 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -112,8 +112,18 @@ pub enum Tile { #[derive(Debug)] pub struct MapInstance { - pub tiles: HashMap, - pub land_tiles: HashMap, + tiles: HashMap, + land_tiles: HashMap, +} + +impl MapInstance { + pub fn get_tile(&self, coordinate: Coordinate) -> Option<&Tile> { + self.tiles.get(&coordinate) + } + + pub fn get_land_tile(&self, coordinate: Coordinate) -> Option<&LandTile> { + self.land_tiles.get(&coordinate) + } } impl MapInstance { @@ -432,8 +442,8 @@ mod tests { })) ); assert_eq!( - map_instance.tiles.get(&(1, -1, 0)), - Some(&Tile::Land(LandTile { + map_instance.land_tiles.get(&(1, -1, 0)), + Some(&LandTile { hexagon: Hexagon { nodes: HashMap::from([ (NodeRef::North, 6), @@ -454,7 +464,7 @@ mod tests { }, resource: Some(Resource::Ore), number: Some(9) - })) + }) ); // Spot-check several node ids From 42287a690da8b3d5f7370a112e6dd59fa369876d Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 19 Oct 2024 07:59:45 -0400 Subject: [PATCH 26/83] Initial Attempt at Rust CI --- .github/workflows/build.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1bf64f059..c4d37013b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -88,3 +88,28 @@ jobs: working-directory: ./ui - run: npm run build working-directory: ./ui + + build-rust: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + # Verify cargo run, test, and run benchmarks + - name: Run `cargo run` + run: cargo run + - name: Run `cargo run --release` + run: cargo run --release + - name: Run `cargo test` + run: cargo test --verbose + - name: Run `cargo test --release` + run: cargo test --release --verbose + - name: Run `cargo bench` + run: cargo bench From 6b507fa5b076ec4a3affe3df40ef505f067411c7 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 19 Oct 2024 08:02:18 -0400 Subject: [PATCH 27/83] Attempt 2 at GH Actions --- .github/workflows/build.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4d37013b..474003c19 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,14 +102,29 @@ jobs: profile: minimal override: true + - name: Run `cargo fmt` + run: cargo fmt -- --check + working-directory: catanatron_rust + + # TODO: Enable when passing: + # - name: Run `cargo clippy` + # run: cargo clippy -- -D warnings + # working-directory: catanatron_rust + # Verify cargo run, test, and run benchmarks - name: Run `cargo run` run: cargo run + working-directory: catanatron_rust + - name: Run `cargo run --release` run: cargo run --release + working-directory: catanatron_rust - name: Run `cargo test` run: cargo test --verbose + working-directory: catanatron_rust - name: Run `cargo test --release` run: cargo test --release --verbose + working-directory: catanatron_rust - name: Run `cargo bench` run: cargo bench + working-directory: catanatron_rust From 0d4c7b9f7d45fac352ec44ae52323e348f939e81 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 14:03:06 -0400 Subject: [PATCH 28/83] Initial state_functions and game structure --- catanatron_rust/src/actions.rs | 1 + catanatron_rust/src/enums.rs | 28 +++++++-- catanatron_rust/src/game.rs | 73 +++++++++++++++++++++ catanatron_rust/src/lib.rs | 4 ++ catanatron_rust/src/main.rs | 22 ++++++- catanatron_rust/src/player.rs | 18 ++++++ catanatron_rust/src/state.rs | 87 +++++++++++++++++++------- catanatron_rust/src/state_functions.rs | 19 ++++++ 8 files changed, 224 insertions(+), 28 deletions(-) create mode 100644 catanatron_rust/src/actions.rs create mode 100644 catanatron_rust/src/game.rs create mode 100644 catanatron_rust/src/player.rs create mode 100644 catanatron_rust/src/state_functions.rs diff --git a/catanatron_rust/src/actions.rs b/catanatron_rust/src/actions.rs new file mode 100644 index 000000000..23666d277 --- /dev/null +++ b/catanatron_rust/src/actions.rs @@ -0,0 +1 @@ +pub type Action = u64; diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 5ed8ac568..962ec6be3 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -1,9 +1,9 @@ -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Color { - Red, - Blue, - Orange, - White, + Red = 0, + Blue = 1, + Orange = 2, + White = 3, } pub const COLORS: [Color; 4] = [Color::Red, Color::Blue, Color::Orange, Color::White]; @@ -33,7 +33,6 @@ pub enum BuildingType { Road, } -// References for nodes and edges on the game board #[derive(Debug, Clone, Copy)] pub enum NodeRef { North, @@ -87,6 +86,23 @@ pub enum ActionType { EndTurn, // None } +#[derive(Debug)] +pub enum MapType { + Mini, + Base, + Tournament, +} + +// TODO: Make immutable and read-only +#[derive(Debug)] +pub struct GameConfiguration { + pub dicard_limit: u8, + pub vps_to_win: u8, + pub map_type: MapType, + pub num_players: u8, + pub max_turns: u32, +} + // Struct for Action (similar to namedtuple in Python) #[derive(Debug, Clone)] pub struct Action { diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs new file mode 100644 index 000000000..9811f9721 --- /dev/null +++ b/catanatron_rust/src/game.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; + +use crate::enums::GameConfiguration; +use crate::player::Player; +use crate::state::{initialize_state, seating_order_slice, State}; +use crate::state_functions::{get_current_color, winner}; + +pub fn play_game(config: GameConfiguration, players: HashMap>) -> Option { + println!("Playing game with configuration: {:?}", config); + let mut state = initialize_state(); + let mut num_turns = 0; + while winner(&config, &state).is_none() && num_turns < config.max_turns { + play_tick(&config, &players, &mut state); + num_turns += 1; + } + winner(&config, &state) +} + +fn play_tick( + config: &GameConfiguration, + players: &HashMap>, + state: &mut State, +) { + println!("Playing config {:?}", config); + println!("Playing turn {:?}", state); + println!( + "Seating order: {:?}", + &state[seating_order_slice(config.num_players as usize)].to_vec() + ); + let current_color = get_current_color(config, state); + let current_player = players.get(¤t_color).unwrap(); + + let playable_actions = generate_playable_actions(config, state); + let action = current_player.decide(state, &playable_actions); + println!( + "Player {:?} decided to play action {:?}", + current_color, action + ); + + apply_action(config, state, action); +} + +// TODO: Type with Action +fn generate_playable_actions(config: &GameConfiguration, state: &State) -> Vec { + vec![0] +} + +fn apply_action(config: &GameConfiguration, state: &mut State, action: u64) { + println!("Applying action {:?}", action); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{enums::MapType, player::RandomPlayer}; + + #[test] + fn test_game_creation() { + let config = GameConfiguration { + dicard_limit: 7, + vps_to_win: 10, + map_type: MapType::Base, + num_players: 4, + max_turns: 100, + }; + let mut players: HashMap> = HashMap::new(); + players.insert(0, Box::new(RandomPlayer {})); + players.insert(1, Box::new(RandomPlayer {})); + + let result = play_game(config, players); + assert_eq!(result, None); + } +} diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index c2ab6e182..390c66bd6 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,7 +1,11 @@ +pub mod actions; pub mod decks; pub mod enums; +pub mod game; pub mod global_state; pub mod map_instance; pub mod map_template; mod ordered_hashmap; +pub mod player; pub mod state; +pub mod state_functions; diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 1c16b0665..ef36df76c 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -1,8 +1,11 @@ use catanatron_rust::decks; -use catanatron_rust::enums::Resource; +use catanatron_rust::enums::{Color, GameConfiguration, MapType, Resource, COLORS}; +use catanatron_rust::game::play_game; use catanatron_rust::global_state; use catanatron_rust::map_instance::MapInstance; +use catanatron_rust::player::{Player, RandomPlayer}; use catanatron_rust::state; +use std::collections::HashMap; use std::time::Instant; fn main() { @@ -62,4 +65,21 @@ fn main() { "Map Instance Land Tiles: {:?}", map_instance.get_land_tile((1, 0, -1)) ); + + println!("Colors slice: {:?}", state::seating_order_slice(4)); + println!("Colors {:?}", COLORS); + + let config = GameConfiguration { + dicard_limit: 7, + vps_to_win: 10, + map_type: MapType::Base, + num_players: 2, + max_turns: 10, + }; + let mut players: HashMap> = HashMap::new(); + players.insert(Color::Red as u8, Box::new(RandomPlayer {})); + players.insert(Color::Blue as u8, Box::new(RandomPlayer {})); + + let result = play_game(config, players); + println!("Game result: {:?}", result); } diff --git a/catanatron_rust/src/player.rs b/catanatron_rust/src/player.rs new file mode 100644 index 000000000..156afbfa4 --- /dev/null +++ b/catanatron_rust/src/player.rs @@ -0,0 +1,18 @@ +use rand::Rng; + +use crate::actions::Action; +use crate::state::State; + +pub trait Player { + fn decide(&self, state: &State, playable_actions: &Vec) -> u64; +} + +pub struct RandomPlayer {} + +impl Player for RandomPlayer { + fn decide(&self, _state: &State, playable_actions: &Vec) -> u64 { + let mut rng = rand::thread_rng(); + let index = rng.gen_range(0..playable_actions.len()); + playable_actions[index] + } +} diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index d3296f9b8..ff8b26799 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -3,14 +3,16 @@ use rand::seq::SliceRandom; use crate::enums::COLORS; +pub type State = Vec; + /// This is in theory not needed since we use a vector and we can /// .push() to it. But since we made it, leaving in here in case /// we want to switch to an array implementation and it serves /// as documentation of the state vector. -pub fn get_state_array_size(_num_players: u8) -> usize { +pub fn get_state_array_size(num_players: usize) -> usize { // TODO: Is configuration part of state? // TODO: Hardcoded for BASE_MAP - let n = _num_players as usize; + let n = num_players; let num_nodes = 54; let num_edges = 72; let num_tiles = 19; @@ -65,6 +67,23 @@ pub fn get_state_array_size(_num_players: u8) -> usize { size } +pub fn bank_resource_index(resource: u8) -> usize { + if resource > 4 { + panic!("Invalid resource index"); + } + resource as usize +} +const PLAYER_STATE_START_INDEX: usize = 268; +pub fn seating_order_slice(num_players: usize) -> std::ops::Range { + PLAYER_STATE_START_INDEX..PLAYER_STATE_START_INDEX + num_players +} +pub fn actual_victory_points_index(num_players: u8, color: u8) -> usize { + PLAYER_STATE_START_INDEX + num_players as usize + color as usize * 15 +} +pub const DEV_BANK_PTR_INDEX: usize = 30; +pub const CURRENT_PLAYER_INDEX: usize = 31; +pub const CURRENT_TURN_INDEX: usize = 32; + /// This is a compact representation of the omnipotent state of the game. /// Fairly close to a bitboard, but not quite. Its a vector of integers. /// @@ -82,7 +101,7 @@ pub fn get_state_array_size(_num_players: u8) -> usize { /// We recommend additional caches and aux data structures for /// faster rollouts. This one is compact optimized for copying. pub fn initialize_state() -> Vec { - let num_players = 2; + let num_players: usize = 2; let size = get_state_array_size(num_players); let mut vector = vec![0; size]; // Initialize Bank @@ -93,22 +112,25 @@ pub fn initialize_state() -> Vec { vector[4] = 19; // Initialize Bank Development Cards // TODO: Shuffle - let listdeck = starting_dev_listdeck(); + let mut listdeck = starting_dev_listdeck(); + listdeck.shuffle(&mut rand::thread_rng()); vector[5..30].copy_from_slice(&listdeck); + // May be worth storing a pointer to the top of the deck + vector[DEV_BANK_PTR_INDEX] = 0; // Initialize Game Controls - vector[30] = 0; // Current_Player_Index - vector[31] = 0; // Current_Turn_Index - vector[32] = 1; // Is_Initial_Build_Phase - vector[33] = 0; // Has_Played_Development_Card - vector[34] = 0; // Has_Rolled - vector[35] = 0; // Is_Discarding - vector[36] = 0; // Is_Moving_Robber - vector[37] = 0; // Is_Building_Road - vector[38] = 2; // Free_Roads_Available + vector[CURRENT_PLAYER_INDEX] = 0; + vector[CURRENT_TURN_INDEX] = 0; + vector[33] = 1; // Is_Initial_Build_Phase + vector[34] = 0; // Has_Played_Development_Card + vector[35] = 0; // Has_Rolled + vector[36] = 0; // Is_Discarding + vector[37] = 0; // Is_Moving_Robber + vector[38] = 0; // Is_Building_Road + vector[39] = 2; // Free_Roads_Available // Extra (u8::MAX is used to indicate no player) - vector[39] = u8::MAX; // Longest_Road_Player_Index - vector[40] = u8::MAX; // Largest_Army_Player_Index + vector[40] = u8::MAX; // Longest_Road_Player_Index + vector[41] = u8::MAX; // Largest_Army_Player_Index // Board // TODO: Generate map from template @@ -120,15 +142,21 @@ pub fn initialize_state() -> Vec { // size += num_nodes; // Node_Owner (Player Index | -1 < n + 1) // size += num_nodes; // Node_Settlement/City (1=Settlement, 2=City, 0=Nothing) // size += num_ports; // Port_Resource (Resource Index <= 5) + vector[267] = 11; // temporary mark + let mut player_state_start = PLAYER_STATE_START_INDEX; // Initialize Players // Shuffle player indices - let mut player_indices = COLORS.iter().map(|&x| x as u8).collect::>(); - player_indices.shuffle(&mut rand::thread_rng()); - vector[267..267 + player_indices.len()].copy_from_slice(&player_indices); - let mut player_state_start = 267 + player_indices.len(); + let mut color_seating_order = COLORS[0..num_players as usize] + .iter() + .map(|&x| x as u8) + .collect::>(); + color_seating_order.shuffle(&mut rand::thread_rng()); + vector[player_state_start..player_state_start + num_players] + .copy_from_slice(&color_seating_order); + player_state_start += num_players; for _ in 0..num_players { - // Player_Victory_Points (Number <= 12) + // Player_Victory_Points (Number <= 12). i is in order of COLORS vector[player_state_start] = 0; // victory points // Player__In_Hand (Number <= 19) @@ -159,11 +187,12 @@ pub fn initialize_state() -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::enums::Color; #[test] fn test_initialize_state_vector() { let n: usize = 2; - let result = get_state_array_size(n as u8); + let result = get_state_array_size(n); assert_eq!(result, 301); } @@ -172,4 +201,20 @@ mod tests { let state = initialize_state(); assert_eq!(state.len(), 301); } + + #[test] + fn test_colors_slice() { + let result = seating_order_slice(4); + assert_eq!(result, 268..272); + } + + #[test] + fn test_indexing() { + let num_players = 2; + let result = actual_victory_points_index(num_players, Color::Red as u8); + assert_eq!(result, 270); + + let result = actual_victory_points_index(num_players, Color::Blue as u8); + assert_eq!(result, 270 + 15); + } } diff --git a/catanatron_rust/src/state_functions.rs b/catanatron_rust/src/state_functions.rs new file mode 100644 index 000000000..baae8d0aa --- /dev/null +++ b/catanatron_rust/src/state_functions.rs @@ -0,0 +1,19 @@ +use crate::enums::GameConfiguration; +use crate::state::{actual_victory_points_index, seating_order_slice, State, CURRENT_PLAYER_INDEX}; + +pub fn get_current_color(config: &GameConfiguration, state: &State) -> u8 { + let current_player_index = state[CURRENT_PLAYER_INDEX]; + let color_seating_order = &state[seating_order_slice(config.num_players as usize)]; + color_seating_order[current_player_index as usize] +} + +pub fn winner(config: &GameConfiguration, state: &State) -> Option { + let current_color = get_current_color(config, state); + + let actual_victory_points = + state[actual_victory_points_index(config.num_players, current_color)]; + if actual_victory_points >= config.vps_to_win { + return Some(current_color); + } + None +} From 0cdeb5e43ccca491882a63c73c8b390e541820cd Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 14:07:21 -0400 Subject: [PATCH 29/83] Add clippy to CI --- .github/workflows/build.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 474003c19..eaedf96d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -105,26 +105,24 @@ jobs: - name: Run `cargo fmt` run: cargo fmt -- --check working-directory: catanatron_rust + - name: Run `cargo clippy` + run: cargo clippy -- -D warnings + working-directory: catanatron_rust - # TODO: Enable when passing: - # - name: Run `cargo clippy` - # run: cargo clippy -- -D warnings - # working-directory: catanatron_rust - - # Verify cargo run, test, and run benchmarks - name: Run `cargo run` run: cargo run working-directory: catanatron_rust - - name: Run `cargo run --release` run: cargo run --release working-directory: catanatron_rust + - name: Run `cargo test` run: cargo test --verbose working-directory: catanatron_rust - name: Run `cargo test --release` run: cargo test --release --verbose working-directory: catanatron_rust + - name: Run `cargo bench` run: cargo bench working-directory: catanatron_rust From 7454f5d3bcaddb9e71e4ba2536866ac545e96d4a Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 14:09:17 -0400 Subject: [PATCH 30/83] Apply some clippy suggestions --- catanatron_rust/src/player.rs | 4 ++-- catanatron_rust/src/state.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/catanatron_rust/src/player.rs b/catanatron_rust/src/player.rs index 156afbfa4..3426c4d36 100644 --- a/catanatron_rust/src/player.rs +++ b/catanatron_rust/src/player.rs @@ -4,13 +4,13 @@ use crate::actions::Action; use crate::state::State; pub trait Player { - fn decide(&self, state: &State, playable_actions: &Vec) -> u64; + fn decide(&self, state: &State, playable_actions: &[Action]) -> u64; } pub struct RandomPlayer {} impl Player for RandomPlayer { - fn decide(&self, _state: &State, playable_actions: &Vec) -> u64 { + fn decide(&self, _state: &State, playable_actions: &[Action]) -> u64 { let mut rng = rand::thread_rng(); let index = rng.gen_range(0..playable_actions.len()); playable_actions[index] diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index ff8b26799..39e92354a 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -147,7 +147,7 @@ pub fn initialize_state() -> Vec { // Initialize Players // Shuffle player indices - let mut color_seating_order = COLORS[0..num_players as usize] + let mut color_seating_order = COLORS[0..num_players] .iter() .map(|&x| x as u8) .collect::>(); From 1b8cae9e99b6934421013d1314036df0f532ba80 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 14:34:16 -0400 Subject: [PATCH 31/83] Apply clippy suggestions --- catanatron_rust/src/decks.rs | 12 ++++++++++++ catanatron_rust/src/global_state.rs | 6 ++++++ catanatron_rust/src/map_instance.rs | 14 +++++++------- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/catanatron_rust/src/decks.rs b/catanatron_rust/src/decks.rs index af4595977..258ba2639 100644 --- a/catanatron_rust/src/decks.rs +++ b/catanatron_rust/src/decks.rs @@ -5,6 +5,12 @@ pub struct ResourceDeck { freqdeck: u32, // Store resource counts using 32 bits. Each resource gets 6 bits. } +impl Default for ResourceDeck { + fn default() -> Self { + Self::new() + } +} + impl ResourceDeck { /// Static constructor: Constructs a deck with all resources set to 0. pub fn new() -> Self { @@ -167,6 +173,12 @@ pub struct DevCardDeck { pub listdeck: Vec, } +impl Default for DevCardDeck { + fn default() -> Self { + Self::new() + } +} + impl DevCardDeck { /// Static constructor: Constructs a deck with all development cards set to 0. pub fn new() -> Self { diff --git a/catanatron_rust/src/global_state.rs b/catanatron_rust/src/global_state.rs index bba98bb66..dbd288fb1 100644 --- a/catanatron_rust/src/global_state.rs +++ b/catanatron_rust/src/global_state.rs @@ -10,6 +10,12 @@ pub struct GlobalState { pub base_map_template: MapTemplate, } +impl Default for GlobalState { + fn default() -> Self { + Self::new() + } +} + impl GlobalState { pub fn new() -> Self { // Mini Map Template diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index f26d76e30..643bd820e 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -154,7 +154,7 @@ impl MapInstance { if tile_slot == TileSlot::Land { let resource = shuffled_tiles.pop().unwrap(); - if resource == None { + if resource.is_none() { let land_tile = LandTile { hexagon: hexagon.clone(), resource, @@ -322,20 +322,20 @@ fn get_nodes_edges( // Insert New Nodes and Edges for noderef in NODE_REFS { - if !nodes.contains_key(&noderef) { - nodes.insert(noderef, node_autoinc); + if let std::collections::hash_map::Entry::Vacant(e) = nodes.entry(noderef) { + e.insert(node_autoinc); node_autoinc += 1; } } for edgeref in EDGE_REFS { - let (a_noderef, b_noderef) = get_noderefs(edgeref); - if !edges.contains_key(&edgeref) { + edges.entry(edgeref).or_insert_with(|| { + let (a_noderef, b_noderef) = get_noderefs(edgeref); let edge_nodes = ( *nodes.get(&a_noderef).unwrap(), *nodes.get(&b_noderef).unwrap(), ); - edges.insert(edgeref, edge_nodes); - } + edge_nodes + }); } (nodes, edges, node_autoinc) From 887e94b6befe807959472bf310acef6236ee0e85 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 15:55:38 -0400 Subject: [PATCH 32/83] Move generation structure --- catanatron_rust/src/game.rs | 18 ++------- catanatron_rust/src/lib.rs | 1 + catanatron_rust/src/main.rs | 2 +- catanatron_rust/src/move_generation.rs | 41 +++++++++++++++++++ catanatron_rust/src/state.rs | 23 ++++++----- catanatron_rust/src/state_functions.rs | 55 +++++++++++++++++++++++--- 6 files changed, 110 insertions(+), 30 deletions(-) create mode 100644 catanatron_rust/src/move_generation.rs diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 9811f9721..554d1a6d3 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; use crate::enums::GameConfiguration; +use crate::move_generation::generate_playable_actions; use crate::player::Player; -use crate::state::{initialize_state, seating_order_slice, State}; -use crate::state_functions::{get_current_color, winner}; +use crate::state::{initialize_state, State}; +use crate::state_functions::{apply_action, get_current_color, winner}; pub fn play_game(config: GameConfiguration, players: HashMap>) -> Option { println!("Playing game with configuration: {:?}", config); @@ -23,10 +24,6 @@ fn play_tick( ) { println!("Playing config {:?}", config); println!("Playing turn {:?}", state); - println!( - "Seating order: {:?}", - &state[seating_order_slice(config.num_players as usize)].to_vec() - ); let current_color = get_current_color(config, state); let current_player = players.get(¤t_color).unwrap(); @@ -40,15 +37,6 @@ fn play_tick( apply_action(config, state, action); } -// TODO: Type with Action -fn generate_playable_actions(config: &GameConfiguration, state: &State) -> Vec { - vec![0] -} - -fn apply_action(config: &GameConfiguration, state: &mut State, action: u64) { - println!("Applying action {:?}", action); -} - #[cfg(test)] mod tests { use super::*; diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 390c66bd6..1a9993f85 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -5,6 +5,7 @@ pub mod game; pub mod global_state; pub mod map_instance; pub mod map_template; +pub mod move_generation; mod ordered_hashmap; pub mod player; pub mod state; diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index ef36df76c..4ed0e74f4 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -74,7 +74,7 @@ fn main() { vps_to_win: 10, map_type: MapType::Base, num_players: 2, - max_turns: 10, + max_turns: 1, }; let mut players: HashMap> = HashMap::new(); players.insert(Color::Red as u8, Box::new(RandomPlayer {})); diff --git a/catanatron_rust/src/move_generation.rs b/catanatron_rust/src/move_generation.rs new file mode 100644 index 000000000..c399c0eb2 --- /dev/null +++ b/catanatron_rust/src/move_generation.rs @@ -0,0 +1,41 @@ +use std::vec; + +use crate::enums::ActionPrompt; +use crate::{ + actions::Action, + enums::GameConfiguration, + state::State, + state_functions::{get_action_prompt, get_current_color}, +}; + +pub fn generate_playable_actions(config: &GameConfiguration, state: &State) -> Vec { + println!("Generating playable actions"); + let current_color = get_current_color(config, state); + let action_prompt = get_action_prompt(config, state); + match action_prompt { + ActionPrompt::BuildInitialSettlement => { + settlement_possibilities(config, state, current_color, true) + } + ActionPrompt::BuildInitialRoad => todo!(), + ActionPrompt::PlayTurn => todo!(), + ActionPrompt::Discard => todo!(), + ActionPrompt::MoveRobber => todo!(), + // TODO: + ActionPrompt::DecideTrade => todo!(), + ActionPrompt::DecideAcceptees => todo!(), + } +} + +pub fn settlement_possibilities( + config: &GameConfiguration, + state: &State, + color: u8, + is_initial_build_phase: bool, +) -> Vec { + println!( + "Generating settlement possibilities {:?} {:?} {:?} {:?}", + config, state, color, is_initial_build_phase + ); + // TODO: Actually read board and get buildable node ids to build actions with it + vec![0, 1, 2] +} diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 39e92354a..91fbf30f9 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -81,8 +81,13 @@ pub fn actual_victory_points_index(num_players: u8, color: u8) -> usize { PLAYER_STATE_START_INDEX + num_players as usize + color as usize * 15 } pub const DEV_BANK_PTR_INDEX: usize = 30; -pub const CURRENT_PLAYER_INDEX: usize = 31; -pub const CURRENT_TURN_INDEX: usize = 32; +pub const CURRENT_TICK_SEAT_INDEX: usize = 31; +pub const CURRENT_TURN_SEAT_INDEX: usize = 32; +pub const IS_INITIAL_BUILD_PHASE_INDEX: usize = 33; +pub const HAS_PLAYED_DEV_CARD: usize = 34; +pub const HAS_ROLLED_INDEX: usize = 35; +pub const IS_DISCARDING_INDEX: usize = 36; +pub const IS_MOVING_ROBBER_INDEX: usize = 37; /// This is a compact representation of the omnipotent state of the game. /// Fairly close to a bitboard, but not quite. Its a vector of integers. @@ -118,13 +123,13 @@ pub fn initialize_state() -> Vec { // May be worth storing a pointer to the top of the deck vector[DEV_BANK_PTR_INDEX] = 0; // Initialize Game Controls - vector[CURRENT_PLAYER_INDEX] = 0; - vector[CURRENT_TURN_INDEX] = 0; - vector[33] = 1; // Is_Initial_Build_Phase - vector[34] = 0; // Has_Played_Development_Card - vector[35] = 0; // Has_Rolled - vector[36] = 0; // Is_Discarding - vector[37] = 0; // Is_Moving_Robber + vector[CURRENT_TICK_SEAT_INDEX] = 0; + vector[CURRENT_TURN_SEAT_INDEX] = 0; + vector[IS_INITIAL_BUILD_PHASE_INDEX] = 1; // Is_Initial_Build_Phase + vector[HAS_PLAYED_DEV_CARD] = 0; // Has_Played_Development_Card + vector[HAS_ROLLED_INDEX] = 0; // Has_Rolled + vector[IS_DISCARDING_INDEX] = 0; // Is_Discarding + vector[IS_MOVING_ROBBER_INDEX] = 0; // Is_Moving_Robber vector[38] = 0; // Is_Building_Road vector[39] = 2; // Free_Roads_Available diff --git a/catanatron_rust/src/state_functions.rs b/catanatron_rust/src/state_functions.rs index baae8d0aa..69cc6cb11 100644 --- a/catanatron_rust/src/state_functions.rs +++ b/catanatron_rust/src/state_functions.rs @@ -1,10 +1,50 @@ -use crate::enums::GameConfiguration; -use crate::state::{actual_victory_points_index, seating_order_slice, State, CURRENT_PLAYER_INDEX}; +use crate::enums::{ActionPrompt, GameConfiguration}; +use crate::state::{ + actual_victory_points_index, seating_order_slice, State, CURRENT_TICK_SEAT_INDEX, + IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, IS_MOVING_ROBBER_INDEX, +}; + +// ===== Read-only functions ===== +pub fn is_initial_build_phase(state: &State) -> bool { + state[IS_INITIAL_BUILD_PHASE_INDEX] == 1 +} + +pub fn is_moving_robber(state: &State) -> bool { + state[IS_MOVING_ROBBER_INDEX] == 1 +} + +pub fn is_discarding(state: &State) -> bool { + state[IS_DISCARDING_INDEX] == 1 +} pub fn get_current_color(config: &GameConfiguration, state: &State) -> u8 { - let current_player_index = state[CURRENT_PLAYER_INDEX]; - let color_seating_order = &state[seating_order_slice(config.num_players as usize)]; - color_seating_order[current_player_index as usize] + let seating_order = get_seating_order(config, state); + let current_tick_seat = state[CURRENT_TICK_SEAT_INDEX]; + seating_order[current_tick_seat as usize] +} + +/// Returns a slice of Colors in the order of seating +/// e.g. [2, 1, 0, 3] if Orange goes first, then Blue, then Red, and then White +fn get_seating_order<'a>(config: &GameConfiguration, state: &'a [u8]) -> &'a [u8] { + &state[seating_order_slice(config.num_players as usize)] +} + +pub fn get_action_prompt(config: &GameConfiguration, state: &State) -> ActionPrompt { + if is_initial_build_phase(state) { + let num_things_built = 0; + if num_things_built == 2 * config.num_players { + return ActionPrompt::PlayTurn; + } else if num_things_built % 2 == 0 { + return ActionPrompt::BuildInitialSettlement; + } else { + return ActionPrompt::BuildInitialRoad; + } + } else if is_moving_robber(state) { + return ActionPrompt::MoveRobber; + } else if is_discarding(state) { + return ActionPrompt::Discard; + } // TODO: Implement Trading Prompts (DecideTrade, DecideAcceptees) + ActionPrompt::PlayTurn } pub fn winner(config: &GameConfiguration, state: &State) -> Option { @@ -17,3 +57,8 @@ pub fn winner(config: &GameConfiguration, state: &State) -> Option { } None } + +// ===== Mutable functions ===== +pub fn apply_action(config: &GameConfiguration, state: &mut State, action: u64) { + println!("Applying action {:?} {:?} {:?}", config, state, action); +} From 5782d4f0155b247f9b29ba208e93e2f848d07231 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 15:58:52 -0400 Subject: [PATCH 33/83] Rename State to StateVector --- catanatron_rust/src/game.rs | 4 ++-- catanatron_rust/src/move_generation.rs | 6 +++--- catanatron_rust/src/player.rs | 6 +++--- catanatron_rust/src/state.rs | 2 +- catanatron_rust/src/state_functions.rs | 16 ++++++++-------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 554d1a6d3..14393f659 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::enums::GameConfiguration; use crate::move_generation::generate_playable_actions; use crate::player::Player; -use crate::state::{initialize_state, State}; +use crate::state::{initialize_state, StateVector}; use crate::state_functions::{apply_action, get_current_color, winner}; pub fn play_game(config: GameConfiguration, players: HashMap>) -> Option { @@ -20,7 +20,7 @@ pub fn play_game(config: GameConfiguration, players: HashMap fn play_tick( config: &GameConfiguration, players: &HashMap>, - state: &mut State, + state: &mut StateVector, ) { println!("Playing config {:?}", config); println!("Playing turn {:?}", state); diff --git a/catanatron_rust/src/move_generation.rs b/catanatron_rust/src/move_generation.rs index c399c0eb2..cf01a6af4 100644 --- a/catanatron_rust/src/move_generation.rs +++ b/catanatron_rust/src/move_generation.rs @@ -4,11 +4,11 @@ use crate::enums::ActionPrompt; use crate::{ actions::Action, enums::GameConfiguration, - state::State, + state::StateVector, state_functions::{get_action_prompt, get_current_color}, }; -pub fn generate_playable_actions(config: &GameConfiguration, state: &State) -> Vec { +pub fn generate_playable_actions(config: &GameConfiguration, state: &StateVector) -> Vec { println!("Generating playable actions"); let current_color = get_current_color(config, state); let action_prompt = get_action_prompt(config, state); @@ -28,7 +28,7 @@ pub fn generate_playable_actions(config: &GameConfiguration, state: &State) -> V pub fn settlement_possibilities( config: &GameConfiguration, - state: &State, + state: &StateVector, color: u8, is_initial_build_phase: bool, ) -> Vec { diff --git a/catanatron_rust/src/player.rs b/catanatron_rust/src/player.rs index 3426c4d36..8d7834a19 100644 --- a/catanatron_rust/src/player.rs +++ b/catanatron_rust/src/player.rs @@ -1,16 +1,16 @@ use rand::Rng; use crate::actions::Action; -use crate::state::State; +use crate::state::StateVector; pub trait Player { - fn decide(&self, state: &State, playable_actions: &[Action]) -> u64; + fn decide(&self, state: &StateVector, playable_actions: &[Action]) -> u64; } pub struct RandomPlayer {} impl Player for RandomPlayer { - fn decide(&self, _state: &State, playable_actions: &[Action]) -> u64 { + fn decide(&self, _state: &StateVector, playable_actions: &[Action]) -> u64 { let mut rng = rand::thread_rng(); let index = rng.gen_range(0..playable_actions.len()); playable_actions[index] diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 91fbf30f9..dec586474 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -3,7 +3,7 @@ use rand::seq::SliceRandom; use crate::enums::COLORS; -pub type State = Vec; +pub type StateVector = Vec; /// This is in theory not needed since we use a vector and we can /// .push() to it. But since we made it, leaving in here in case diff --git a/catanatron_rust/src/state_functions.rs b/catanatron_rust/src/state_functions.rs index 69cc6cb11..71954f866 100644 --- a/catanatron_rust/src/state_functions.rs +++ b/catanatron_rust/src/state_functions.rs @@ -1,23 +1,23 @@ use crate::enums::{ActionPrompt, GameConfiguration}; use crate::state::{ - actual_victory_points_index, seating_order_slice, State, CURRENT_TICK_SEAT_INDEX, + actual_victory_points_index, seating_order_slice, StateVector, CURRENT_TICK_SEAT_INDEX, IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, IS_MOVING_ROBBER_INDEX, }; // ===== Read-only functions ===== -pub fn is_initial_build_phase(state: &State) -> bool { +pub fn is_initial_build_phase(state: &StateVector) -> bool { state[IS_INITIAL_BUILD_PHASE_INDEX] == 1 } -pub fn is_moving_robber(state: &State) -> bool { +pub fn is_moving_robber(state: &StateVector) -> bool { state[IS_MOVING_ROBBER_INDEX] == 1 } -pub fn is_discarding(state: &State) -> bool { +pub fn is_discarding(state: &StateVector) -> bool { state[IS_DISCARDING_INDEX] == 1 } -pub fn get_current_color(config: &GameConfiguration, state: &State) -> u8 { +pub fn get_current_color(config: &GameConfiguration, state: &StateVector) -> u8 { let seating_order = get_seating_order(config, state); let current_tick_seat = state[CURRENT_TICK_SEAT_INDEX]; seating_order[current_tick_seat as usize] @@ -29,7 +29,7 @@ fn get_seating_order<'a>(config: &GameConfiguration, state: &'a [u8]) -> &'a [u8 &state[seating_order_slice(config.num_players as usize)] } -pub fn get_action_prompt(config: &GameConfiguration, state: &State) -> ActionPrompt { +pub fn get_action_prompt(config: &GameConfiguration, state: &StateVector) -> ActionPrompt { if is_initial_build_phase(state) { let num_things_built = 0; if num_things_built == 2 * config.num_players { @@ -47,7 +47,7 @@ pub fn get_action_prompt(config: &GameConfiguration, state: &State) -> ActionPro ActionPrompt::PlayTurn } -pub fn winner(config: &GameConfiguration, state: &State) -> Option { +pub fn winner(config: &GameConfiguration, state: &StateVector) -> Option { let current_color = get_current_color(config, state); let actual_victory_points = @@ -59,6 +59,6 @@ pub fn winner(config: &GameConfiguration, state: &State) -> Option { } // ===== Mutable functions ===== -pub fn apply_action(config: &GameConfiguration, state: &mut State, action: u64) { +pub fn apply_action(config: &GameConfiguration, state: &mut StateVector, action: u64) { println!("Applying action {:?} {:?} {:?}", config, state, action); } From ca490672e684691f450fae4874e7498aed4d4296 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 16:06:28 -0400 Subject: [PATCH 34/83] Rename state.rs to state_vector.rs --- catanatron_rust/src/game.rs | 2 +- catanatron_rust/src/lib.rs | 2 +- catanatron_rust/src/main.rs | 8 ++++---- catanatron_rust/src/move_generation.rs | 2 +- catanatron_rust/src/player.rs | 2 +- catanatron_rust/src/state_functions.rs | 2 +- catanatron_rust/src/{state.rs => state_vector.rs} | 0 catanatron_rust/tests/integration_test.rs | 6 +++--- 8 files changed, 12 insertions(+), 12 deletions(-) rename catanatron_rust/src/{state.rs => state_vector.rs} (100%) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 14393f659..d0cd3a6f3 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use crate::enums::GameConfiguration; use crate::move_generation::generate_playable_actions; use crate::player::Player; -use crate::state::{initialize_state, StateVector}; use crate::state_functions::{apply_action, get_current_color, winner}; +use crate::state_vector::{initialize_state, StateVector}; pub fn play_game(config: GameConfiguration, players: HashMap>) -> Option { println!("Playing game with configuration: {:?}", config); diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 1a9993f85..0b497def8 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -8,5 +8,5 @@ pub mod map_template; pub mod move_generation; mod ordered_hashmap; pub mod player; -pub mod state; pub mod state_functions; +pub mod state_vector; diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 4ed0e74f4..8c5616ffb 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -4,7 +4,7 @@ use catanatron_rust::game::play_game; use catanatron_rust::global_state; use catanatron_rust::map_instance::MapInstance; use catanatron_rust::player::{Player, RandomPlayer}; -use catanatron_rust::state; +use catanatron_rust::state_vector; use std::collections::HashMap; use std::time::Instant; @@ -53,10 +53,10 @@ fn main() { let global_state = global_state::GlobalState::new(); println!("Global State: {:?}", global_state); - let size = state::get_state_array_size(2); + let size = state_vector::get_state_array_size(2); println!("Vector length: {}", size); - let vector = state::initialize_state(); + let vector = state_vector::initialize_state(); println!("Vector: {:?}", vector); let map_instance = MapInstance::new(&global_state.base_map_template, 0); @@ -66,7 +66,7 @@ fn main() { map_instance.get_land_tile((1, 0, -1)) ); - println!("Colors slice: {:?}", state::seating_order_slice(4)); + println!("Colors slice: {:?}", state_vector::seating_order_slice(4)); println!("Colors {:?}", COLORS); let config = GameConfiguration { diff --git a/catanatron_rust/src/move_generation.rs b/catanatron_rust/src/move_generation.rs index cf01a6af4..176143de0 100644 --- a/catanatron_rust/src/move_generation.rs +++ b/catanatron_rust/src/move_generation.rs @@ -4,8 +4,8 @@ use crate::enums::ActionPrompt; use crate::{ actions::Action, enums::GameConfiguration, - state::StateVector, state_functions::{get_action_prompt, get_current_color}, + state_vector::StateVector, }; pub fn generate_playable_actions(config: &GameConfiguration, state: &StateVector) -> Vec { diff --git a/catanatron_rust/src/player.rs b/catanatron_rust/src/player.rs index 8d7834a19..21655950b 100644 --- a/catanatron_rust/src/player.rs +++ b/catanatron_rust/src/player.rs @@ -1,7 +1,7 @@ use rand::Rng; use crate::actions::Action; -use crate::state::StateVector; +use crate::state_vector::StateVector; pub trait Player { fn decide(&self, state: &StateVector, playable_actions: &[Action]) -> u64; diff --git a/catanatron_rust/src/state_functions.rs b/catanatron_rust/src/state_functions.rs index 71954f866..a73f22e59 100644 --- a/catanatron_rust/src/state_functions.rs +++ b/catanatron_rust/src/state_functions.rs @@ -1,5 +1,5 @@ use crate::enums::{ActionPrompt, GameConfiguration}; -use crate::state::{ +use crate::state_vector::{ actual_victory_points_index, seating_order_slice, StateVector, CURRENT_TICK_SEAT_INDEX, IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, IS_MOVING_ROBBER_INDEX, }; diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state_vector.rs similarity index 100% rename from catanatron_rust/src/state.rs rename to catanatron_rust/src/state_vector.rs diff --git a/catanatron_rust/tests/integration_test.rs b/catanatron_rust/tests/integration_test.rs index f4db4c179..84b0eb274 100644 --- a/catanatron_rust/tests/integration_test.rs +++ b/catanatron_rust/tests/integration_test.rs @@ -1,6 +1,6 @@ use catanatron_rust::decks; use catanatron_rust::enums::Resource; -use catanatron_rust::state; +use catanatron_rust::state_vector; #[test] fn test_integration() { @@ -12,7 +12,7 @@ fn test_integration() { } assert_eq!(deck.total_cards(), 95); - let vector = state::initialize_state(); - let size = state::get_state_array_size(2); + let vector = state_vector::initialize_state(); + let size = state_vector::get_state_array_size(2); assert_eq!(size, vector.len()); } From 8404fd3b0a6823acb2ea7165d167804bf34f0722 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 17:58:16 -0400 Subject: [PATCH 35/83] Bug Fix --- catanatron_rust/src/global_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catanatron_rust/src/global_state.rs b/catanatron_rust/src/global_state.rs index dbd288fb1..c8eb55f30 100644 --- a/catanatron_rust/src/global_state.rs +++ b/catanatron_rust/src/global_state.rs @@ -23,7 +23,7 @@ impl GlobalState { // center topology.insert((0, 0, 0), TileSlot::Land); // first layer - topology.insert((-1, -1, 0), TileSlot::Land); + topology.insert((1, -1, 0), TileSlot::Land); topology.insert((0, -1, 1), TileSlot::Land); topology.insert((-1, 0, 1), TileSlot::Land); topology.insert((-1, 1, 0), TileSlot::Land); From bfd4a8b6df95c49c123973f70fb9a6d7a456c41c Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 18:01:36 -0400 Subject: [PATCH 36/83] Dynamic state_vector --- catanatron_rust/src/game.rs | 4 +++- catanatron_rust/src/main.rs | 2 +- catanatron_rust/src/state_vector.rs | 20 +++++++++----------- catanatron_rust/tests/integration_test.rs | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index d0cd3a6f3..db7d49e92 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -8,7 +8,7 @@ use crate::state_vector::{initialize_state, StateVector}; pub fn play_game(config: GameConfiguration, players: HashMap>) -> Option { println!("Playing game with configuration: {:?}", config); - let mut state = initialize_state(); + let mut state = initialize_state(config.num_players); let mut num_turns = 0; while winner(&config, &state).is_none() && num_turns < config.max_turns { play_tick(&config, &players, &mut state); @@ -54,6 +54,8 @@ mod tests { let mut players: HashMap> = HashMap::new(); players.insert(0, Box::new(RandomPlayer {})); players.insert(1, Box::new(RandomPlayer {})); + players.insert(2, Box::new(RandomPlayer {})); + players.insert(3, Box::new(RandomPlayer {})); let result = play_game(config, players); assert_eq!(result, None); diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 8c5616ffb..8b9183cd8 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -56,7 +56,7 @@ fn main() { let size = state_vector::get_state_array_size(2); println!("Vector length: {}", size); - let vector = state_vector::initialize_state(); + let vector = state_vector::initialize_state(4); println!("Vector: {:?}", vector); let map_instance = MapInstance::new(&global_state.base_map_template, 0); diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index dec586474..082ea67ab 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -105,9 +105,11 @@ pub const IS_MOVING_ROBBER_INDEX: usize = 37; /// TODO: This is not the only Data Structure to do rollouts. /// We recommend additional caches and aux data structures for /// faster rollouts. This one is compact optimized for copying. -pub fn initialize_state() -> Vec { - let num_players: usize = 2; - let size = get_state_array_size(num_players); +/// TODO: Accept a seed for deterministic tests +pub fn initialize_state(num_players: u8) -> Vec { + let n = num_players as usize; + + let size = get_state_array_size(n); let mut vector = vec![0; size]; // Initialize Bank vector[0] = 19; @@ -152,14 +154,10 @@ pub fn initialize_state() -> Vec { // Initialize Players // Shuffle player indices - let mut color_seating_order = COLORS[0..num_players] - .iter() - .map(|&x| x as u8) - .collect::>(); + let mut color_seating_order = COLORS[0..n].iter().map(|&x| x as u8).collect::>(); color_seating_order.shuffle(&mut rand::thread_rng()); - vector[player_state_start..player_state_start + num_players] - .copy_from_slice(&color_seating_order); - player_state_start += num_players; + vector[player_state_start..player_state_start + n].copy_from_slice(&color_seating_order); + player_state_start += n; for _ in 0..num_players { // Player_Victory_Points (Number <= 12). i is in order of COLORS vector[player_state_start] = 0; // victory points @@ -203,7 +201,7 @@ mod tests { #[test] fn test_initialize_state() { - let state = initialize_state(); + let state = initialize_state(2); assert_eq!(state.len(), 301); } diff --git a/catanatron_rust/tests/integration_test.rs b/catanatron_rust/tests/integration_test.rs index 84b0eb274..309fa06f4 100644 --- a/catanatron_rust/tests/integration_test.rs +++ b/catanatron_rust/tests/integration_test.rs @@ -12,7 +12,7 @@ fn test_integration() { } assert_eq!(deck.total_cards(), 95); - let vector = state_vector::initialize_state(); + let vector = state_vector::initialize_state(2); let size = state_vector::get_state_array_size(2); assert_eq!(size, vector.len()); } From 7aba25d47fe9a502b99f5f9d7747fb7cdbcb84a7 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 19:47:33 -0400 Subject: [PATCH 37/83] Deprecation Notice --- catanatron_core/catanatron/models/map_instance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/catanatron_core/catanatron/models/map_instance.py b/catanatron_core/catanatron/models/map_instance.py index 8dd1d1386..18721c10e 100644 --- a/catanatron_core/catanatron/models/map_instance.py +++ b/catanatron_core/catanatron/models/map_instance.py @@ -80,9 +80,11 @@ def from_tiles(tiles: Dict[Coordinate, Tile]): # TODO: Rename to self.node_to_tiles self.adjacent_tiles = init_adjacent_tiles(self.land_tiles) self.node_production = init_node_production(self.adjacent_tiles) + # TODO: Deprecate in favor of using self.land_tiles or some static enumeration over Coordinates self.tiles_by_id = { t.id: t for t in self.tiles.values() if isinstance(t, LandTile) } + # TODO: Deprecate in favor of using self.land_tiles or some static enumeration over Coordinates self.ports_by_id = {p.id: p for p in self.tiles.values() if isinstance(p, Port)} return self From 62326d4587ac45cda6e9ebe45824b817dd4c1c38 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 19:49:57 -0400 Subject: [PATCH 38/83] Further MapInstance --- catanatron_rust/src/enums.rs | 2 +- catanatron_rust/src/global_state.rs | 23 ++++ catanatron_rust/src/lib.rs | 1 + catanatron_rust/src/main.rs | 3 +- catanatron_rust/src/map_instance.rs | 159 ++++++++++++++++++++++++---- catanatron_rust/src/map_template.rs | 2 +- 6 files changed, 168 insertions(+), 22 deletions(-) diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 962ec6be3..c18f10b31 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -8,7 +8,7 @@ pub enum Color { pub const COLORS: [Color; 4] = [Color::Red, Color::Blue, Color::Orange, Color::White]; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Resource { Wood, Brick, diff --git a/catanatron_rust/src/global_state.rs b/catanatron_rust/src/global_state.rs index c8eb55f30..0b12d7f8a 100644 --- a/catanatron_rust/src/global_state.rs +++ b/catanatron_rust/src/global_state.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::{ enums::Resource, map_template::{MapTemplate, TileSlot}, @@ -8,6 +10,24 @@ use crate::{ pub struct GlobalState { pub mini_map_template: MapTemplate, pub base_map_template: MapTemplate, + // TODO: Make a hard-coded static constant + // {2: 0.027777777777777776, 3: 0.05555555555555555, 4: 0.08333333333333333, 5: 0.1111111111111111, 6: 0.1388888888888889, 7: 0.16666666666666669, 8: 0.1388888888888889, 9: 0.1111111111111111, 10: 0.08333333333333333, 11: 0.05555555555555555, 12: 0.027777777777777776} + pub dice_probas: HashMap, +} + +fn build_dice_probas() -> HashMap { + let mut probas: HashMap = HashMap::new(); + + // Iterate over two dice rolls + for i in 1..=6 { + for j in 1..=6 { + let sum = i + j; + let counter = probas.entry(sum).or_insert(0.0); + *counter += 1.0 / 36.0; + } + } + + probas } impl Default for GlobalState { @@ -145,9 +165,12 @@ impl GlobalState { topology, }; + let dice_probas = build_dice_probas(); + Self { mini_map_template, base_map_template, + dice_probas, } } } diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 0b497def8..48002d161 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -8,5 +8,6 @@ pub mod map_template; pub mod move_generation; mod ordered_hashmap; pub mod player; +pub mod state; pub mod state_functions; pub mod state_vector; diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 8b9183cd8..c116cc613 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -59,7 +59,8 @@ fn main() { let vector = state_vector::initialize_state(4); println!("Vector: {:?}", vector); - let map_instance = MapInstance::new(&global_state.base_map_template, 0); + let map_instance = + MapInstance::new(&global_state.base_map_template, global_state.dice_probas, 0); println!("Map Instance Tiles: {:?}", map_instance.get_tile((0, 0, 0))); println!( "Map Instance Land Tiles: {:?}", diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 643bd820e..5c1d4f787 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -1,12 +1,12 @@ use rand::rngs::StdRng; use rand::{seq::SliceRandom, SeedableRng}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::hash::Hash; use crate::enums::Resource; use crate::map_template::{add_coordinates, Coordinate, MapTemplate, TileSlot}; -type NodeId = u8; +pub type NodeId = u8; type EdgeId = (NodeId, NodeId); #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] @@ -88,7 +88,7 @@ pub struct Hexagon { pub struct LandTile { pub(crate) hexagon: Hexagon, pub(crate) resource: Option, - pub(crate) number: Option, + pub(crate) number: Option, } #[derive(Debug, Clone, PartialEq)] @@ -114,9 +114,21 @@ pub enum Tile { pub struct MapInstance { tiles: HashMap, land_tiles: HashMap, + port_nodes: HashSet, + land_nodes: HashSet, + adjacent_land_tiles: HashMap>, + node_production: HashMap>, } impl MapInstance { + pub fn get_tiles(&self) -> &HashMap { + &self.tiles + } + + pub fn get_land_tiles(&self) -> &HashMap { + &self.land_tiles + } + pub fn get_tile(&self, coordinate: Coordinate) -> Option<&Tile> { self.tiles.get(&coordinate) } @@ -127,9 +139,9 @@ impl MapInstance { } impl MapInstance { - pub fn new(map_template: &MapTemplate, seed: u64) -> Self { + pub fn new(map_template: &MapTemplate, dice_probas: HashMap, seed: u64) -> Self { let tiles = Self::initialize_tiles(map_template, seed); - Self::from_tiles(tiles) + Self::from_tiles(tiles, dice_probas) } fn initialize_tiles(map_template: &MapTemplate, seed: u64) -> HashMap { @@ -199,20 +211,61 @@ impl MapInstance { tiles } - fn from_tiles(tiles: HashMap) -> Self { - let land_tiles: HashMap = tiles - .clone() - .into_iter() - .filter_map(|(coordinate, tile)| { - if let Tile::Land(land_tile) = tile { - Some((coordinate, land_tile)) - } else { - None - } - }) - .collect(); + fn from_tiles(tiles: HashMap, dice_probas: HashMap) -> Self { + let mut land_tiles: HashMap = HashMap::new(); + let mut port_nodes: HashSet = HashSet::new(); + let mut land_nodes: HashSet = HashSet::new(); + let mut adjacent_land_tiles: HashMap> = HashMap::new(); + let mut node_production: HashMap> = HashMap::new(); + + for (coordinate, tile) in tiles.iter() { + if let Tile::Land(land_tile) = tile { + land_tiles.insert(coordinate.clone(), land_tile.clone()); + let is_desert = land_tile.resource.is_none(); + land_tile.hexagon.nodes.values().for_each(|&node_id| { + land_nodes.insert(node_id); + adjacent_land_tiles + .entry(node_id) + .or_insert_with(Vec::new) + .push(land_tile.clone()); + + // maybe add this tile's production to the node's production + let production = node_production.entry(node_id).or_insert_with(HashMap::new); + if is_desert { + return; + } + let resource = land_tile.resource.unwrap(); + let number = land_tile.number.unwrap(); + let proba = dice_probas.get(&number).unwrap(); + production.entry(resource).or_insert(0.0); + *production.get_mut(&resource).unwrap() += proba; + }); + } else if let Tile::Port(port_tile) = tile { + let (a_noderef, b_noderef) = get_noderefs_from_port_direction(port_tile.direction); + port_nodes.insert(*port_tile.hexagon.nodes.get(&a_noderef).unwrap()); + port_nodes.insert(*port_tile.hexagon.nodes.get(&b_noderef).unwrap()); + } + } + + Self { + tiles, + land_tiles, + port_nodes, + land_nodes, + adjacent_land_tiles, + node_production, + } + } +} - Self { tiles, land_tiles } +fn get_noderefs_from_port_direction(direction: Direction) -> (NodeRef, NodeRef) { + match direction { + Direction::East => (NodeRef::NorthEast, NodeRef::SouthEast), + Direction::SouthEast => (NodeRef::SouthEast, NodeRef::South), + Direction::SouthWest => (NodeRef::South, NodeRef::SouthWest), + Direction::West => (NodeRef::SouthWest, NodeRef::NorthWest), + Direction::NorthWest => (NodeRef::NorthWest, NodeRef::North), + Direction::NorthEast => (NodeRef::North, NodeRef::NorthEast), } } @@ -389,6 +442,9 @@ mod tests { assert_eq!(autoinc2, 10); } + // TODO: Test production at a node that has two of the same tiles next to each other. + // See https://github.com/bcollazo/catanatron/issues/263. + fn assert_node_value( map_instance: &MapInstance, coordinates: Coordinate, @@ -407,13 +463,78 @@ mod tests { ); } + fn assert_land_tile( + map_instance: &MapInstance, + coordinates: Coordinate, + resource: Option, + number: Option, + ) { + let land_tile = map_instance.get_land_tile(coordinates).unwrap(); + assert_eq!(land_tile.resource, resource); + assert_eq!(land_tile.number, number); + } + + #[test] + fn test_map_mini() { + let global_state = GlobalState::new(); + let map_instance = + MapInstance::new(&global_state.mini_map_template, global_state.dice_probas, 0); + + assert_eq!(map_instance.tiles.len(), 19); + assert_eq!(map_instance.land_tiles.len(), 7); + assert_eq!(map_instance.land_nodes.len(), 24); + assert_eq!(map_instance.port_nodes.len(), 0); + assert_eq!(map_instance.adjacent_land_tiles.len(), 24); + assert_eq!(map_instance.node_production.len(), 24); + + // Test adjacent_tiles + let adjacent_tiles = map_instance.adjacent_land_tiles.get(&0).unwrap(); + assert_eq!(adjacent_tiles.len(), 3); + assert_land_tile(&map_instance, (0, 0, 0), Some(Resource::Ore), Some(9)); + assert_land_tile(&map_instance, (1, 0, -1), Some(Resource::Wheat), Some(4)); + assert_land_tile(&map_instance, (0, 1, -1), Some(Resource::Brick), Some(5)); + // Assert there is a 9 ore in adjacent_tiles + assert!(adjacent_tiles + .iter() + .any(|tile| { tile.resource == Some(Resource::Ore) && tile.number == Some(9) })); + // Spot-check two more nodes + assert_eq!(map_instance.adjacent_land_tiles.get(&14).unwrap().len(), 1); + assert_eq!(map_instance.adjacent_land_tiles.get(&16).unwrap().len(), 2); + + // Test node production + let node_production = map_instance.node_production.get(&0).unwrap(); + assert_eq!( + node_production.get(&Resource::Ore), + Some(&0.1111111111111111) + ); + assert_eq!( + node_production.get(&Resource::Wheat), + Some(&0.08333333333333333) + ); + assert_eq!( + node_production.get(&Resource::Brick), + Some(&0.1111111111111111) + ); + + // Spot-check several node ids + assert_node_value(&map_instance, (0, 0, 0), NodeRef::North, 0); + assert_node_value(&map_instance, (0, 0, 0), NodeRef::NorthEast, 1); + assert_node_value(&map_instance, (1, -1, 0), NodeRef::SouthEast, 8); + assert_node_value(&map_instance, (1, 0, -1), NodeRef::North, 22); + assert_node_value(&map_instance, (-1, 0, 1), NodeRef::South, 13); + } + #[test] fn test_map_instance() { let global_state = GlobalState::new(); - let map_instance = MapInstance::new(&global_state.base_map_template, 0); + let map_instance = + MapInstance::new(&global_state.base_map_template, global_state.dice_probas, 0); assert_eq!(map_instance.tiles.len(), 37); assert_eq!(map_instance.land_tiles.len(), 19); + assert_eq!(map_instance.land_nodes.len(), 54); + assert_eq!(map_instance.port_nodes.len(), 18); + assert_eq!(map_instance.adjacent_land_tiles.len(), 54); // Assert tile at 0,0,0 is Land with right resource and number assert_eq!( diff --git a/catanatron_rust/src/map_template.rs b/catanatron_rust/src/map_template.rs index 5bc8d4778..1ca4f6398 100644 --- a/catanatron_rust/src/map_template.rs +++ b/catanatron_rust/src/map_template.rs @@ -22,7 +22,7 @@ pub enum TileSlot { #[derive(Debug)] pub struct MapTemplate { - pub(crate) numbers: Vec, + pub(crate) numbers: Vec, pub(crate) ports: Vec>, pub(crate) tiles: Vec>, From b51a923b7eca917140aa99d83bf46d22e7238641 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 19:57:08 -0400 Subject: [PATCH 39/83] Introduce State.rs --- catanatron_rust/src/map_instance.rs | 4 +++ catanatron_rust/src/state.rs | 52 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 catanatron_rust/src/state.rs diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 5c1d4f787..984369589 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -129,6 +129,10 @@ impl MapInstance { &self.land_tiles } + pub fn land_nodes(&self) -> &HashSet { + &self.land_nodes + } + pub fn get_tile(&self, coordinate: Coordinate) -> Option<&Tile> { self.tiles.get(&coordinate) } diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs new file mode 100644 index 000000000..064bb7f73 --- /dev/null +++ b/catanatron_rust/src/state.rs @@ -0,0 +1,52 @@ +use std::collections::HashSet; + +use crate::{ + enums::GameConfiguration, + map_instance::{MapInstance, NodeId}, + state_vector::{initialize_state, StateVector}, +}; + +// Helpful Cache of information for algorithms +struct Board { + board_buildable_ids: HashSet, + longest_road_color: Option, + longest_road_length: u8, +} + +impl Board { + pub fn new(map_instance: &MapInstance) -> Self { + let board_buildable_ids = map_instance.land_nodes().clone(); + + // TODO: Keep filling me! + Self { + board_buildable_ids, + longest_road_color: None, + longest_road_length: 0, + } + } +} + +struct State { + // These two are immutable + config: GameConfiguration, + map_instance: MapInstance, + + // This is mutable + vector: StateVector, + + // These are caches for speeding up game state calculations + board: Board, +} + +impl State { + pub fn new(config: GameConfiguration, map_instance: MapInstance) -> Self { + let vector = initialize_state(config.num_players); + let board = Board::new(&map_instance); + Self { + config, + map_instance, + vector, + board, + } + } +} From a965a97234a3a591afc33e53a7bd1c8b34e807dc Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 20 Oct 2024 19:58:41 -0400 Subject: [PATCH 40/83] Fix clippy --- catanatron_rust/src/map_instance.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 984369589..9376d99e2 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -222,19 +222,19 @@ impl MapInstance { let mut adjacent_land_tiles: HashMap> = HashMap::new(); let mut node_production: HashMap> = HashMap::new(); - for (coordinate, tile) in tiles.iter() { + for (&coordinate, tile) in tiles.iter() { if let Tile::Land(land_tile) = tile { - land_tiles.insert(coordinate.clone(), land_tile.clone()); + land_tiles.insert(coordinate, land_tile.clone()); let is_desert = land_tile.resource.is_none(); land_tile.hexagon.nodes.values().for_each(|&node_id| { land_nodes.insert(node_id); adjacent_land_tiles .entry(node_id) - .or_insert_with(Vec::new) + .or_default() .push(land_tile.clone()); // maybe add this tile's production to the node's production - let production = node_production.entry(node_id).or_insert_with(HashMap::new); + let production = node_production.entry(node_id).or_default(); if is_desert { return; } From b4d433a30654973f7ca3c4f188998730439a76e4 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 22 Oct 2024 21:51:57 -0400 Subject: [PATCH 41/83] Object oriented State instead of state_functions --- catanatron_rust/src/game.rs | 45 ++++---- catanatron_rust/src/global_state.rs | 2 - catanatron_rust/src/main.rs | 9 +- catanatron_rust/src/map_instance.rs | 18 ++-- catanatron_rust/src/move_generation.rs | 23 ++-- catanatron_rust/src/player.rs | 7 +- catanatron_rust/src/state.rs | 139 +++++++++++++++++++------ catanatron_rust/src/state_functions.rs | 63 +---------- 8 files changed, 170 insertions(+), 136 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index db7d49e92..68ee9422c 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -1,40 +1,48 @@ use std::collections::HashMap; +use std::rc::Rc; use crate::enums::GameConfiguration; +use crate::global_state::GlobalState; +use crate::map_instance::MapInstance; use crate::move_generation::generate_playable_actions; use crate::player::Player; -use crate::state_functions::{apply_action, get_current_color, winner}; -use crate::state_vector::{initialize_state, StateVector}; +use crate::state::State; +use crate::state_functions::apply_action; -pub fn play_game(config: GameConfiguration, players: HashMap>) -> Option { - println!("Playing game with configuration: {:?}", config); - let mut state = initialize_state(config.num_players); +pub fn play_game( + global_state: GlobalState, + config: GameConfiguration, + players: HashMap>, +) -> Option { + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); + let rc_config = Rc::new(config); + println!("Playing game with configuration: {:?}", rc_config); + let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); let mut num_turns = 0; - while winner(&config, &state).is_none() && num_turns < config.max_turns { - play_tick(&config, &players, &mut state); + while state.winner().is_none() && num_turns < rc_config.max_turns { + play_tick(&players, &mut state); num_turns += 1; } - winner(&config, &state) + state.winner() } -fn play_tick( - config: &GameConfiguration, - players: &HashMap>, - state: &mut StateVector, -) { - println!("Playing config {:?}", config); +fn play_tick(players: &HashMap>, state: &mut State) { println!("Playing turn {:?}", state); - let current_color = get_current_color(config, state); + let current_color = state.get_current_color(); let current_player = players.get(¤t_color).unwrap(); - let playable_actions = generate_playable_actions(config, state); + let playable_actions = generate_playable_actions(state); let action = current_player.decide(state, &playable_actions); println!( "Player {:?} decided to play action {:?}", current_color, action ); - apply_action(config, state, action); + apply_action(state, action); } #[cfg(test)] @@ -44,6 +52,7 @@ mod tests { #[test] fn test_game_creation() { + let global_state = GlobalState::new(); let config = GameConfiguration { dicard_limit: 7, vps_to_win: 10, @@ -57,7 +66,7 @@ mod tests { players.insert(2, Box::new(RandomPlayer {})); players.insert(3, Box::new(RandomPlayer {})); - let result = play_game(config, players); + let result = play_game(global_state, config, players); assert_eq!(result, None); } } diff --git a/catanatron_rust/src/global_state.rs b/catanatron_rust/src/global_state.rs index 0b12d7f8a..78ee73690 100644 --- a/catanatron_rust/src/global_state.rs +++ b/catanatron_rust/src/global_state.rs @@ -10,8 +10,6 @@ use crate::{ pub struct GlobalState { pub mini_map_template: MapTemplate, pub base_map_template: MapTemplate, - // TODO: Make a hard-coded static constant - // {2: 0.027777777777777776, 3: 0.05555555555555555, 4: 0.08333333333333333, 5: 0.1111111111111111, 6: 0.1388888888888889, 7: 0.16666666666666669, 8: 0.1388888888888889, 9: 0.1111111111111111, 10: 0.08333333333333333, 11: 0.05555555555555555, 12: 0.027777777777777776} pub dice_probas: HashMap, } diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index c116cc613..9fc3013c8 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -59,8 +59,11 @@ fn main() { let vector = state_vector::initialize_state(4); println!("Vector: {:?}", vector); - let map_instance = - MapInstance::new(&global_state.base_map_template, global_state.dice_probas, 0); + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); println!("Map Instance Tiles: {:?}", map_instance.get_tile((0, 0, 0))); println!( "Map Instance Land Tiles: {:?}", @@ -81,6 +84,6 @@ fn main() { players.insert(Color::Red as u8, Box::new(RandomPlayer {})); players.insert(Color::Blue as u8, Box::new(RandomPlayer {})); - let result = play_game(config, players); + let result = play_game(global_state, config, players); println!("Game result: {:?}", result); } diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 9376d99e2..4860e9924 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -143,7 +143,7 @@ impl MapInstance { } impl MapInstance { - pub fn new(map_template: &MapTemplate, dice_probas: HashMap, seed: u64) -> Self { + pub fn new(map_template: &MapTemplate, dice_probas: &HashMap, seed: u64) -> Self { let tiles = Self::initialize_tiles(map_template, seed); Self::from_tiles(tiles, dice_probas) } @@ -215,7 +215,7 @@ impl MapInstance { tiles } - fn from_tiles(tiles: HashMap, dice_probas: HashMap) -> Self { + fn from_tiles(tiles: HashMap, dice_probas: &HashMap) -> Self { let mut land_tiles: HashMap = HashMap::new(); let mut port_nodes: HashSet = HashSet::new(); let mut land_nodes: HashSet = HashSet::new(); @@ -481,8 +481,11 @@ mod tests { #[test] fn test_map_mini() { let global_state = GlobalState::new(); - let map_instance = - MapInstance::new(&global_state.mini_map_template, global_state.dice_probas, 0); + let map_instance = MapInstance::new( + &global_state.mini_map_template, + &global_state.dice_probas, + 0, + ); assert_eq!(map_instance.tiles.len(), 19); assert_eq!(map_instance.land_tiles.len(), 7); @@ -531,8 +534,11 @@ mod tests { #[test] fn test_map_instance() { let global_state = GlobalState::new(); - let map_instance = - MapInstance::new(&global_state.base_map_template, global_state.dice_probas, 0); + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); assert_eq!(map_instance.tiles.len(), 37); assert_eq!(map_instance.land_tiles.len(), 19); diff --git a/catanatron_rust/src/move_generation.rs b/catanatron_rust/src/move_generation.rs index 176143de0..f6f79686a 100644 --- a/catanatron_rust/src/move_generation.rs +++ b/catanatron_rust/src/move_generation.rs @@ -1,20 +1,16 @@ use std::vec; +use crate::actions::Action; use crate::enums::ActionPrompt; -use crate::{ - actions::Action, - enums::GameConfiguration, - state_functions::{get_action_prompt, get_current_color}, - state_vector::StateVector, -}; +use crate::state::State; -pub fn generate_playable_actions(config: &GameConfiguration, state: &StateVector) -> Vec { +pub fn generate_playable_actions(state: &State) -> Vec { println!("Generating playable actions"); - let current_color = get_current_color(config, state); - let action_prompt = get_action_prompt(config, state); + let current_color = state.get_current_color(); + let action_prompt = state.get_action_prompt(); match action_prompt { ActionPrompt::BuildInitialSettlement => { - settlement_possibilities(config, state, current_color, true) + settlement_possibilities(state, current_color, true) } ActionPrompt::BuildInitialRoad => todo!(), ActionPrompt::PlayTurn => todo!(), @@ -27,14 +23,13 @@ pub fn generate_playable_actions(config: &GameConfiguration, state: &StateVector } pub fn settlement_possibilities( - config: &GameConfiguration, - state: &StateVector, + state: &State, color: u8, is_initial_build_phase: bool, ) -> Vec { println!( - "Generating settlement possibilities {:?} {:?} {:?} {:?}", - config, state, color, is_initial_build_phase + "Generating settlement possibilities {:?} {:?} {:?}", + state, color, is_initial_build_phase ); // TODO: Actually read board and get buildable node ids to build actions with it vec![0, 1, 2] diff --git a/catanatron_rust/src/player.rs b/catanatron_rust/src/player.rs index 21655950b..b8468a07d 100644 --- a/catanatron_rust/src/player.rs +++ b/catanatron_rust/src/player.rs @@ -1,16 +1,15 @@ use rand::Rng; -use crate::actions::Action; -use crate::state_vector::StateVector; +use crate::{actions::Action, state::State}; pub trait Player { - fn decide(&self, state: &StateVector, playable_actions: &[Action]) -> u64; + fn decide(&self, state: &State, playable_actions: &[Action]) -> u64; } pub struct RandomPlayer {} impl Player for RandomPlayer { - fn decide(&self, _state: &StateVector, playable_actions: &[Action]) -> u64 { + fn decide(&self, _state: &State, playable_actions: &[Action]) -> u64 { let mut rng = rand::thread_rng(); let index = rng.gen_range(0..playable_actions.len()); playable_actions[index] diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 064bb7f73..21129857b 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -1,52 +1,133 @@ -use std::collections::HashSet; +use std::{collections::HashSet, rc::Rc}; use crate::{ - enums::GameConfiguration, + enums::{ActionPrompt, GameConfiguration}, map_instance::{MapInstance, NodeId}, - state_vector::{initialize_state, StateVector}, + state_vector::{ + actual_victory_points_index, initialize_state, seating_order_slice, StateVector, + CURRENT_TICK_SEAT_INDEX, IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, + IS_MOVING_ROBBER_INDEX, + }, }; -// Helpful Cache of information for algorithms -struct Board { +#[derive(Debug)] +pub(crate) struct State { + // These two are immutable + config: Rc, + map_instance: Rc, + + // This is mutable + vector: StateVector, + + // These are caches for speeding up game state calculations board_buildable_ids: HashSet, longest_road_color: Option, longest_road_length: u8, } -impl Board { - pub fn new(map_instance: &MapInstance) -> Self { +impl State { + pub fn new(config: Rc, map_instance: Rc) -> Self { + let vector = initialize_state(config.num_players); + let board_buildable_ids = map_instance.land_nodes().clone(); + let longest_road_color = None; + let longest_road_length = 0; - // TODO: Keep filling me! Self { + config, + map_instance, + vector, board_buildable_ids, - longest_road_color: None, - longest_road_length: 0, + longest_road_color, + longest_road_length, } } -} -struct State { - // These two are immutable - config: GameConfiguration, - map_instance: MapInstance, + // ===== Config Getters ===== + pub fn get_num_players(&self) -> u8 { + self.config.num_players + } + pub fn get_num_players_usize(&self) -> usize { + self.config.num_players as usize + } - // This is mutable - vector: StateVector, + // ===== Getters ===== + pub fn is_initial_build_phase(&self) -> bool { + self.vector[IS_INITIAL_BUILD_PHASE_INDEX] == 1 + } - // These are caches for speeding up game state calculations - board: Board, -} + pub fn is_moving_robber(&self) -> bool { + self.vector[IS_MOVING_ROBBER_INDEX] == 1 + } -impl State { - pub fn new(config: GameConfiguration, map_instance: MapInstance) -> Self { - let vector = initialize_state(config.num_players); - let board = Board::new(&map_instance); - Self { - config, - map_instance, - vector, - board, + pub fn is_discarding(&self) -> bool { + self.vector[IS_DISCARDING_INDEX] == 1 + } + + pub fn get_current_color(&self) -> u8 { + let seating_order = self.get_seating_order(); + let current_tick_seat = self.vector[CURRENT_TICK_SEAT_INDEX]; + seating_order[current_tick_seat as usize] + } + + /// Returns a slice of Colors in the order of seating + /// e.g. [2, 1, 0, 3] if Orange goes first, then Blue, then Red, and then White + pub fn get_seating_order(&self) -> &[u8] { + &self.vector[seating_order_slice(self.config.num_players as usize)] + } + + pub fn get_action_prompt(&self) -> ActionPrompt { + if self.is_initial_build_phase() { + let num_things_built = 0; + if num_things_built == 2 * self.config.num_players { + return ActionPrompt::PlayTurn; + } else if num_things_built % 2 == 0 { + return ActionPrompt::BuildInitialSettlement; + } else { + return ActionPrompt::BuildInitialRoad; + } + } else if self.is_moving_robber() { + return ActionPrompt::MoveRobber; + } else if self.is_discarding() { + return ActionPrompt::Discard; + } // TODO: Implement Trading Prompts (DecideTrade, DecideAcceptees) + ActionPrompt::PlayTurn + } + + pub fn winner(&self) -> Option { + let current_color = self.get_current_color(); + + let actual_victory_points = + self.vector[actual_victory_points_index(self.config.num_players, current_color)]; + if actual_victory_points >= self.config.vps_to_win { + return Some(current_color); } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{enums::MapType, global_state::GlobalState}; + + #[test] + fn test_state_creation() { + let global_state = GlobalState::new(); + let config = GameConfiguration { + dicard_limit: 7, + vps_to_win: 10, + map_type: MapType::Base, + num_players: 4, + max_turns: 100, + }; + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); + let state = State::new(Rc::new(config), Rc::new(map_instance)); + + assert_eq!(state.longest_road_color, None); } } diff --git a/catanatron_rust/src/state_functions.rs b/catanatron_rust/src/state_functions.rs index a73f22e59..0baddc690 100644 --- a/catanatron_rust/src/state_functions.rs +++ b/catanatron_rust/src/state_functions.rs @@ -1,64 +1,7 @@ use crate::enums::{ActionPrompt, GameConfiguration}; -use crate::state_vector::{ - actual_victory_points_index, seating_order_slice, StateVector, CURRENT_TICK_SEAT_INDEX, - IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, IS_MOVING_ROBBER_INDEX, -}; - -// ===== Read-only functions ===== -pub fn is_initial_build_phase(state: &StateVector) -> bool { - state[IS_INITIAL_BUILD_PHASE_INDEX] == 1 -} - -pub fn is_moving_robber(state: &StateVector) -> bool { - state[IS_MOVING_ROBBER_INDEX] == 1 -} - -pub fn is_discarding(state: &StateVector) -> bool { - state[IS_DISCARDING_INDEX] == 1 -} - -pub fn get_current_color(config: &GameConfiguration, state: &StateVector) -> u8 { - let seating_order = get_seating_order(config, state); - let current_tick_seat = state[CURRENT_TICK_SEAT_INDEX]; - seating_order[current_tick_seat as usize] -} - -/// Returns a slice of Colors in the order of seating -/// e.g. [2, 1, 0, 3] if Orange goes first, then Blue, then Red, and then White -fn get_seating_order<'a>(config: &GameConfiguration, state: &'a [u8]) -> &'a [u8] { - &state[seating_order_slice(config.num_players as usize)] -} - -pub fn get_action_prompt(config: &GameConfiguration, state: &StateVector) -> ActionPrompt { - if is_initial_build_phase(state) { - let num_things_built = 0; - if num_things_built == 2 * config.num_players { - return ActionPrompt::PlayTurn; - } else if num_things_built % 2 == 0 { - return ActionPrompt::BuildInitialSettlement; - } else { - return ActionPrompt::BuildInitialRoad; - } - } else if is_moving_robber(state) { - return ActionPrompt::MoveRobber; - } else if is_discarding(state) { - return ActionPrompt::Discard; - } // TODO: Implement Trading Prompts (DecideTrade, DecideAcceptees) - ActionPrompt::PlayTurn -} - -pub fn winner(config: &GameConfiguration, state: &StateVector) -> Option { - let current_color = get_current_color(config, state); - - let actual_victory_points = - state[actual_victory_points_index(config.num_players, current_color)]; - if actual_victory_points >= config.vps_to_win { - return Some(current_color); - } - None -} +use crate::state::State; // ===== Mutable functions ===== -pub fn apply_action(config: &GameConfiguration, state: &mut StateVector, action: u64) { - println!("Applying action {:?} {:?} {:?}", config, state, action); +pub fn apply_action(state: &mut State, action: u64) { + println!("Applying action {:?} {:?}", state, action); } From 317b315708c6d87ed6b0872b3c345a142912b224 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Tue, 22 Oct 2024 22:18:11 -0400 Subject: [PATCH 42/83] Double down on OOP State and Action enum --- catanatron_rust/src/actions.rs | 1 - catanatron_rust/src/enums.rs | 59 +++++------- catanatron_rust/src/game.rs | 3 +- catanatron_rust/src/lib.rs | 2 - catanatron_rust/src/move_generation.rs | 36 ------- catanatron_rust/src/player.rs | 6 +- catanatron_rust/src/state.rs | 10 +- catanatron_rust/src/state/move_generation.rs | 99 ++++++++++++++++++++ catanatron_rust/src/state_functions.rs | 5 +- 9 files changed, 128 insertions(+), 93 deletions(-) delete mode 100644 catanatron_rust/src/actions.rs delete mode 100644 catanatron_rust/src/move_generation.rs create mode 100644 catanatron_rust/src/state/move_generation.rs diff --git a/catanatron_rust/src/actions.rs b/catanatron_rust/src/actions.rs deleted file mode 100644 index 23666d277..000000000 --- a/catanatron_rust/src/actions.rs +++ /dev/null @@ -1 +0,0 @@ -pub type Action = u64; diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index c18f10b31..59c435d28 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -1,3 +1,5 @@ +use crate::map_instance::NodeId; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Color { Red = 0, @@ -64,26 +66,26 @@ pub enum ActionPrompt { DecideAcceptees, } -#[derive(Debug, Clone, Copy)] -pub enum ActionType { - Roll, // None. Log instead sets it to (int, int) rolled. - MoveRobber, // value is (coordinate, Color|None). Log has extra element of card stolen. - Discard, // value is None|Resource[]. - BuildRoad, // value is edge_id - BuildSettlement, // value is node_id - BuildCity, // value is node_id +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Action { + Roll(Color), // None. Log instead sets it to (int, int) rolled. + MoveRobber(Color), // value is (coordinate, Color|None). Log has extra element of card stolen. + Discard(Color), // value is None|Resource[]. + BuildRoad(Color), // value is edge_id + BuildSettlement(u8, NodeId), // value is node_id + BuildCity, // value is node_id BuyDevelopmentCard, // value is None. Log value is card. - PlayKnightCard, // value is None - PlayYearOfPlenty, // value is (Resource, Resource) - PlayMonopoly, // value is Resource - PlayRoadBuilding, // value is None - MaritimeTrade, // 5-resource tuple, last is resource asked. - OfferTrade, // 10-resource tuple, first 5 is offered, last 5 is receiving. - AcceptTrade, // 10-resource tuple. - RejectTrade, // None - ConfirmTrade, // 11-tuple. First 10 like OfferTrade, last is color of accepting player. - CancelTrade, // None - EndTurn, // None + PlayKnightCard, // value is None + PlayYearOfPlenty, // value is (Resource, Resource) + PlayMonopoly, // value is Resource + PlayRoadBuilding, // value is None + MaritimeTrade, // 5-resource tuple, last is resource asked. + OfferTrade, // 10-resource tuple, first 5 is offered, last 5 is receiving. + AcceptTrade, // 10-resource tuple. + RejectTrade, // None + ConfirmTrade, // 11-tuple. First 10 like OfferTrade, last is color of accepting player. + CancelTrade, // None + EndTurn, // None } #[derive(Debug)] @@ -102,22 +104,3 @@ pub struct GameConfiguration { pub num_players: u8, pub max_turns: u32, } - -// Struct for Action (similar to namedtuple in Python) -#[derive(Debug, Clone)] -pub struct Action { - pub color: String, - pub action_type: ActionType, - // TODO: Not sure if this should be a String - pub value: Option, // Use Option for fields that could be None -} - -impl Action { - pub fn new(color: String, action_type: ActionType, value: Option) -> Self { - Action { - color, - action_type, - value, - } - } -} diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 68ee9422c..1e1ee6312 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -4,7 +4,6 @@ use std::rc::Rc; use crate::enums::GameConfiguration; use crate::global_state::GlobalState; use crate::map_instance::MapInstance; -use crate::move_generation::generate_playable_actions; use crate::player::Player; use crate::state::State; use crate::state_functions::apply_action; @@ -35,7 +34,7 @@ fn play_tick(players: &HashMap>, state: &mut State) { let current_color = state.get_current_color(); let current_player = players.get(¤t_color).unwrap(); - let playable_actions = generate_playable_actions(state); + let playable_actions = state.generate_playable_actions(); let action = current_player.decide(state, &playable_actions); println!( "Player {:?} decided to play action {:?}", diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 48002d161..2066b4fda 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,11 +1,9 @@ -pub mod actions; pub mod decks; pub mod enums; pub mod game; pub mod global_state; pub mod map_instance; pub mod map_template; -pub mod move_generation; mod ordered_hashmap; pub mod player; pub mod state; diff --git a/catanatron_rust/src/move_generation.rs b/catanatron_rust/src/move_generation.rs deleted file mode 100644 index f6f79686a..000000000 --- a/catanatron_rust/src/move_generation.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::vec; - -use crate::actions::Action; -use crate::enums::ActionPrompt; -use crate::state::State; - -pub fn generate_playable_actions(state: &State) -> Vec { - println!("Generating playable actions"); - let current_color = state.get_current_color(); - let action_prompt = state.get_action_prompt(); - match action_prompt { - ActionPrompt::BuildInitialSettlement => { - settlement_possibilities(state, current_color, true) - } - ActionPrompt::BuildInitialRoad => todo!(), - ActionPrompt::PlayTurn => todo!(), - ActionPrompt::Discard => todo!(), - ActionPrompt::MoveRobber => todo!(), - // TODO: - ActionPrompt::DecideTrade => todo!(), - ActionPrompt::DecideAcceptees => todo!(), - } -} - -pub fn settlement_possibilities( - state: &State, - color: u8, - is_initial_build_phase: bool, -) -> Vec { - println!( - "Generating settlement possibilities {:?} {:?} {:?}", - state, color, is_initial_build_phase - ); - // TODO: Actually read board and get buildable node ids to build actions with it - vec![0, 1, 2] -} diff --git a/catanatron_rust/src/player.rs b/catanatron_rust/src/player.rs index b8468a07d..20d10c729 100644 --- a/catanatron_rust/src/player.rs +++ b/catanatron_rust/src/player.rs @@ -1,15 +1,15 @@ use rand::Rng; -use crate::{actions::Action, state::State}; +use crate::{enums::Action, state::State}; pub trait Player { - fn decide(&self, state: &State, playable_actions: &[Action]) -> u64; + fn decide(&self, state: &State, playable_actions: &[Action]) -> Action; } pub struct RandomPlayer {} impl Player for RandomPlayer { - fn decide(&self, _state: &State, playable_actions: &[Action]) -> u64 { + fn decide(&self, _state: &State, playable_actions: &[Action]) -> Action { let mut rng = rand::thread_rng(); let index = rng.gen_range(0..playable_actions.len()); playable_actions[index] diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 21129857b..617a6e7b0 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -25,6 +25,8 @@ pub(crate) struct State { longest_road_length: u8, } +mod move_generation; + impl State { pub fn new(config: Rc, map_instance: Rc) -> Self { let vector = initialize_state(config.num_players); @@ -43,14 +45,6 @@ impl State { } } - // ===== Config Getters ===== - pub fn get_num_players(&self) -> u8 { - self.config.num_players - } - pub fn get_num_players_usize(&self) -> usize { - self.config.num_players as usize - } - // ===== Getters ===== pub fn is_initial_build_phase(&self) -> bool { self.vector[IS_INITIAL_BUILD_PHASE_INDEX] == 1 diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs new file mode 100644 index 000000000..75696231e --- /dev/null +++ b/catanatron_rust/src/state/move_generation.rs @@ -0,0 +1,99 @@ +use std::vec; + +use crate::enums::{Action, ActionPrompt}; +use crate::state::State; + +impl State { + pub fn generate_playable_actions(&self) -> Vec { + println!("Generating playable actions"); + let current_color = self.get_current_color(); + let action_prompt = self.get_action_prompt(); + match action_prompt { + ActionPrompt::BuildInitialSettlement => { + self.settlement_possibilities(current_color, true) + } + ActionPrompt::BuildInitialRoad => todo!(), + ActionPrompt::PlayTurn => todo!(), + ActionPrompt::Discard => todo!(), + ActionPrompt::MoveRobber => todo!(), + // TODO: + ActionPrompt::DecideTrade => todo!(), + ActionPrompt::DecideAcceptees => todo!(), + } + } + + pub fn settlement_possibilities(&self, color: u8, is_initial_build_phase: bool) -> Vec { + println!( + "Generating settlement possibilities {:?} {:?} {:?}", + self, color, is_initial_build_phase + ); + if is_initial_build_phase { + self.board_buildable_ids + .iter() + .map(|node_id| Action::BuildSettlement(color, *node_id)) + .collect() + } else { + panic!("Not implemented"); + } + } +} + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use super::*; + use crate::enums::{GameConfiguration, MapType}; + use crate::global_state::GlobalState; + use crate::map_instance::MapInstance; + + fn setup_state() -> State { + let global_state = GlobalState::new(); + let config = GameConfiguration { + dicard_limit: 7, + vps_to_win: 10, + map_type: MapType::Base, + num_players: 2, + max_turns: 10, + }; + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); + State::new(Rc::new(config), Rc::new(map_instance)) + } + + #[test] + fn test_move_generation() { + let state = setup_state(); + let actions = state.generate_playable_actions(); + assert_eq!(actions.len(), 54); + assert!(matches!(actions[0], Action::BuildSettlement(_, _))); + } + + #[test] + fn test_settlement_possibilities() { + let global_state = GlobalState::new(); + let config = GameConfiguration { + dicard_limit: 7, + vps_to_win: 10, + map_type: MapType::Base, + num_players: 2, + max_turns: 10, + }; + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); + let state = State::new(Rc::new(config), Rc::new(map_instance)); + + let initial_build_phase_actions = state.settlement_possibilities(0, true); + assert_eq!(initial_build_phase_actions.len(), 54); + + // TODO: Enable when implemented + // let actions = state.settlement_possibilities(0, false); + // assert_eq!(actions.len(), 3); + } +} diff --git a/catanatron_rust/src/state_functions.rs b/catanatron_rust/src/state_functions.rs index 0baddc690..af85f6257 100644 --- a/catanatron_rust/src/state_functions.rs +++ b/catanatron_rust/src/state_functions.rs @@ -1,7 +1,6 @@ -use crate::enums::{ActionPrompt, GameConfiguration}; -use crate::state::State; +use crate::{enums::Action, state::State}; // ===== Mutable functions ===== -pub fn apply_action(state: &mut State, action: u64) { +pub fn apply_action(state: &mut State, action: Action) { println!("Applying action {:?} {:?}", state, action); } From d7c589a73554039476081b437929bb89edf3a882 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 26 Oct 2024 19:50:02 -0400 Subject: [PATCH 43/83] mutations.rs, 2 turns running --- catanatron_rust/src/deck_slices.rs | 28 ++++++ catanatron_rust/src/enums.rs | 4 +- catanatron_rust/src/game.rs | 36 +++++++- catanatron_rust/src/lib.rs | 2 +- catanatron_rust/src/main.rs | 2 +- catanatron_rust/src/map_instance.rs | 46 +++++++++- catanatron_rust/src/state.rs | 97 +++++++++++++++++--- catanatron_rust/src/state/move_generation.rs | 28 +++++- catanatron_rust/src/state/mutations.rs | 56 +++++++++++ catanatron_rust/src/state_functions.rs | 6 -- catanatron_rust/src/state_vector.rs | 20 ++-- 11 files changed, 280 insertions(+), 45 deletions(-) create mode 100644 catanatron_rust/src/deck_slices.rs create mode 100644 catanatron_rust/src/state/mutations.rs delete mode 100644 catanatron_rust/src/state_functions.rs diff --git a/catanatron_rust/src/deck_slices.rs b/catanatron_rust/src/deck_slices.rs new file mode 100644 index 000000000..65cf2d35f --- /dev/null +++ b/catanatron_rust/src/deck_slices.rs @@ -0,0 +1,28 @@ +type FreqDeck = [u8; 5]; + +pub const SETTLEMENT_COST: FreqDeck = [1, 1, 1, 1, 0]; +pub const ROAD_COST: FreqDeck = [1, 1, 0, 0, 0]; + +pub fn freqdeck_sub(freqdeck: &mut [u8], cost: FreqDeck) { + freqdeck[0] -= cost[0]; + freqdeck[1] -= cost[1]; + freqdeck[2] -= cost[2]; + freqdeck[3] -= cost[3]; + freqdeck[4] -= cost[4]; +} + +pub fn freqdeck_add(freqdeck: &mut [u8], cost: FreqDeck) { + freqdeck[0] += cost[0]; + freqdeck[1] += cost[1]; + freqdeck[2] += cost[2]; + freqdeck[3] += cost[3]; + freqdeck[4] += cost[4]; +} + +pub fn freqdeck_contains(freqdeck: &[u8], cost: FreqDeck) -> bool { + freqdeck[0] >= cost[0] + && freqdeck[1] >= cost[1] + && freqdeck[2] >= cost[2] + && freqdeck[3] >= cost[3] + && freqdeck[4] >= cost[4] +} diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 59c435d28..b9069cb98 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -1,4 +1,4 @@ -use crate::map_instance::NodeId; +use crate::map_instance::{EdgeId, NodeId}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Color { @@ -71,7 +71,7 @@ pub enum Action { Roll(Color), // None. Log instead sets it to (int, int) rolled. MoveRobber(Color), // value is (coordinate, Color|None). Log has extra element of card stolen. Discard(Color), // value is None|Resource[]. - BuildRoad(Color), // value is edge_id + BuildRoad(u8, EdgeId), // value is edge_id BuildSettlement(u8, NodeId), // value is node_id BuildCity, // value is node_id BuyDevelopmentCard, // value is None. Log value is card. diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 1e1ee6312..d8ca444af 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -6,7 +6,6 @@ use crate::global_state::GlobalState; use crate::map_instance::MapInstance; use crate::player::Player; use crate::state::State; -use crate::state_functions::apply_action; pub fn play_game( global_state: GlobalState, @@ -23,6 +22,7 @@ pub fn play_game( let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); let mut num_turns = 0; while state.winner().is_none() && num_turns < rc_config.max_turns { + println!("Playing turn {:?}", num_turns); play_tick(&players, &mut state); num_turns += 1; } @@ -30,18 +30,22 @@ pub fn play_game( } fn play_tick(players: &HashMap>, state: &mut State) { - println!("Playing turn {:?}", state); let current_color = state.get_current_color(); let current_player = players.get(¤t_color).unwrap(); let playable_actions = state.generate_playable_actions(); + println!( + "Player {:?} has {:?} playable actions", + current_color, + playable_actions.len() + ); let action = current_player.decide(state, &playable_actions); println!( "Player {:?} decided to play action {:?}", current_color, action ); - apply_action(state, action); + state.apply_action(action); } #[cfg(test)] @@ -49,8 +53,7 @@ mod tests { use super::*; use crate::{enums::MapType, player::RandomPlayer}; - #[test] - fn test_game_creation() { + fn setup_game() -> (GlobalState, GameConfiguration, HashMap>) { let global_state = GlobalState::new(); let config = GameConfiguration { dicard_limit: 7, @@ -64,8 +67,31 @@ mod tests { players.insert(1, Box::new(RandomPlayer {})); players.insert(2, Box::new(RandomPlayer {})); players.insert(3, Box::new(RandomPlayer {})); + (global_state, config, players) + } + + #[test] + fn test_game_creation() { + let (global_state, config, players) = setup_game(); let result = play_game(global_state, config, players); assert_eq!(result, None); } + + #[test] + fn test_initial_build_phase() { + let (global_state, config, players) = setup_game(); + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); + let rc_config = Rc::new(config); + let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); + + assert_eq!(state.generate_playable_actions().len(), 54); + play_tick(&players, &mut state); + assert_eq!(state.is_initial_build_phase(), true); + assert_eq!(state.generate_playable_actions().len(), 3); + } } diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 2066b4fda..0ffade796 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,3 +1,4 @@ +pub mod deck_slices; pub mod decks; pub mod enums; pub mod game; @@ -7,5 +8,4 @@ pub mod map_template; mod ordered_hashmap; pub mod player; pub mod state; -pub mod state_functions; pub mod state_vector; diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 9fc3013c8..6961cc968 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -78,7 +78,7 @@ fn main() { vps_to_win: 10, map_type: MapType::Base, num_players: 2, - max_turns: 1, + max_turns: 2, }; let mut players: HashMap> = HashMap::new(); players.insert(Color::Red as u8, Box::new(RandomPlayer {})); diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 4860e9924..a76e3c8b9 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -7,7 +7,7 @@ use crate::enums::Resource; use crate::map_template::{add_coordinates, Coordinate, MapTemplate, TileSlot}; pub type NodeId = u8; -type EdgeId = (NodeId, NodeId); +pub type EdgeId = (NodeId, NodeId); #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] pub enum NodeRef { @@ -115,9 +115,23 @@ pub struct MapInstance { tiles: HashMap, land_tiles: HashMap, port_nodes: HashSet, - land_nodes: HashSet, adjacent_land_tiles: HashMap>, node_production: HashMap>, + + // TODO: Since this doesn't change per map_instance but per template, move + // these to MapTemplate. + // Decided to not use Petgraph for now, since needs seem to be: + // - Lookup node neighbors of a node + // - Lookup edges of a node + // - Lookup list of land edges + // - Pairwise distances between nodes + // - Shortest path between nodes + // - BFS capabilities + // all which doesn't sound too bad to implement. + land_nodes: HashSet, + land_edges: HashSet, + node_neighbors: HashMap>, + edge_neighbors: HashMap>, } impl MapInstance { @@ -140,6 +154,14 @@ impl MapInstance { pub fn get_land_tile(&self, coordinate: Coordinate) -> Option<&LandTile> { self.land_tiles.get(&coordinate) } + + pub fn get_neighbor_nodes(&self, node_id: NodeId) -> Vec { + self.node_neighbors.get(&node_id).unwrap().clone() + } + + pub fn get_neighbor_edges(&self, node_id: NodeId) -> Vec { + self.edge_neighbors.get(&node_id).unwrap().clone() + } } impl MapInstance { @@ -218,10 +240,14 @@ impl MapInstance { fn from_tiles(tiles: HashMap, dice_probas: &HashMap) -> Self { let mut land_tiles: HashMap = HashMap::new(); let mut port_nodes: HashSet = HashSet::new(); - let mut land_nodes: HashSet = HashSet::new(); let mut adjacent_land_tiles: HashMap> = HashMap::new(); let mut node_production: HashMap> = HashMap::new(); + let mut land_nodes: HashSet = HashSet::new(); + let mut land_edges: HashSet = HashSet::new(); + let mut node_neighbors: HashMap> = HashMap::new(); + let mut edge_neighbors: HashMap> = HashMap::new(); + for (&coordinate, tile) in tiles.iter() { if let Tile::Land(land_tile) = tile { land_tiles.insert(coordinate, land_tile.clone()); @@ -244,6 +270,14 @@ impl MapInstance { production.entry(resource).or_insert(0.0); *production.get_mut(&resource).unwrap() += proba; }); + + land_tile.hexagon.edges.values().for_each(|&edge_id| { + land_edges.insert(edge_id); + node_neighbors.entry(edge_id.0).or_default().push(edge_id.1); + node_neighbors.entry(edge_id.1).or_default().push(edge_id.0); + edge_neighbors.entry(edge_id.0).or_default().push(edge_id); + edge_neighbors.entry(edge_id.1).or_default().push(edge_id); + }); } else if let Tile::Port(port_tile) = tile { let (a_noderef, b_noderef) = get_noderefs_from_port_direction(port_tile.direction); port_nodes.insert(*port_tile.hexagon.nodes.get(&a_noderef).unwrap()); @@ -255,9 +289,13 @@ impl MapInstance { tiles, land_tiles, port_nodes, - land_nodes, adjacent_land_tiles, node_production, + + land_nodes, + land_edges, + node_neighbors, + edge_neighbors, } } } diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 617a6e7b0..8fd7ac982 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -1,15 +1,24 @@ -use std::{collections::HashSet, rc::Rc}; +use std::{ + collections::{HashMap, HashSet}, + rc::Rc, +}; use crate::{ enums::{ActionPrompt, GameConfiguration}, - map_instance::{MapInstance, NodeId}, + map_instance::{EdgeId, MapInstance, NodeId}, state_vector::{ - actual_victory_points_index, initialize_state, seating_order_slice, StateVector, - CURRENT_TICK_SEAT_INDEX, IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, + actual_victory_points_index, initialize_state, player_hand_slice, seating_order_slice, + StateVector, CURRENT_TICK_SEAT_INDEX, IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, IS_MOVING_ROBBER_INDEX, }, }; +#[derive(Debug)] +enum Building { + Settlement(u8), // Color + City(u8), // Color +} + #[derive(Debug)] pub(crate) struct State { // These two are immutable @@ -21,17 +30,26 @@ pub(crate) struct State { // These are caches for speeding up game state calculations board_buildable_ids: HashSet, + buildings: HashMap, + roads: HashMap, // (Node1, Node2) -> Color + roads_by_color: Vec, // Color -> Count + connected_components: HashMap>>, longest_road_color: Option, longest_road_length: u8, } mod move_generation; +mod mutations; impl State { pub fn new(config: Rc, map_instance: Rc) -> Self { let vector = initialize_state(config.num_players); let board_buildable_ids = map_instance.land_nodes().clone(); + let buildings = HashMap::new(); + let roads = HashMap::new(); + let roads_by_color = vec![0; config.num_players as usize]; + let connected_components = HashMap::new(); let longest_road_color = None; let longest_road_length = 0; @@ -40,11 +58,23 @@ impl State { map_instance, vector, board_buildable_ids, + buildings, + roads, + roads_by_color, + connected_components, longest_road_color, longest_road_length, } } + fn get_num_players(&self) -> u8 { + self.config.num_players + } + + fn get_num_players_usize(&self) -> usize { + self.get_num_players() as usize + } + // ===== Getters ===== pub fn is_initial_build_phase(&self) -> bool { self.vector[IS_INITIAL_BUILD_PHASE_INDEX] == 1 @@ -72,8 +102,8 @@ impl State { pub fn get_action_prompt(&self) -> ActionPrompt { if self.is_initial_build_phase() { - let num_things_built = 0; - if num_things_built == 2 * self.config.num_players { + let num_things_built = self.buildings.len(); + if num_things_built == 2 * self.config.num_players as usize { return ActionPrompt::PlayTurn; } else if num_things_built % 2 == 0 { return ActionPrompt::BuildInitialSettlement; @@ -88,6 +118,14 @@ impl State { ActionPrompt::PlayTurn } + pub fn get_mut_player_hand(&mut self, color: u8) -> &mut [u8] { + &mut self.vector[player_hand_slice(color)] + } + + pub fn get_player_hand(&self, color: u8) -> &[u8] { + &self.vector[player_hand_slice(color)] + } + pub fn winner(&self) -> Option { let current_color = self.get_current_color(); @@ -98,6 +136,30 @@ impl State { } None } + + // ===== Board Getters ===== + // TODO: Potentially cache this implementation + pub fn board_buildable_edges(&self, color: u8) -> Vec { + println!("Building buildable edges {:?}", color); + + let color_components = self.connected_components.get(&color).unwrap(); + let expandable_nodes: Vec = color_components + .iter() + .flat_map(|component| component.iter()) + .cloned() + .collect(); + + let mut buildable = HashSet::new(); + for node in expandable_nodes { + for edge in self.map_instance.get_neighbor_edges(node) { + if !self.roads.contains_key(&edge) { + let sorted_edge = (edge.0.min(edge.1), edge.0.max(edge.1)); + buildable.insert(sorted_edge); + } + } + } + buildable.into_iter().collect() + } } #[cfg(test)] @@ -105,23 +167,36 @@ mod tests { use super::*; use crate::{enums::MapType, global_state::GlobalState}; - #[test] - fn test_state_creation() { + fn setup_state() -> State { let global_state = GlobalState::new(); let config = GameConfiguration { dicard_limit: 7, vps_to_win: 10, map_type: MapType::Base, - num_players: 4, - max_turns: 100, + num_players: 2, + max_turns: 10, }; let map_instance = MapInstance::new( &global_state.base_map_template, &global_state.dice_probas, 0, ); - let state = State::new(Rc::new(config), Rc::new(map_instance)); + State::new(Rc::new(config), Rc::new(map_instance)) + } + + #[test] + fn test_state_creation() { + let state = setup_state(); assert_eq!(state.longest_road_color, None); } + + #[test] + fn test_initial_build_phase() { + let state = setup_state(); + + assert_eq!(state.is_initial_build_phase(), true); + assert_eq!(state.is_moving_robber(), false); + assert_eq!(state.is_discarding(), false); + } } diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 75696231e..581eba550 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -1,8 +1,9 @@ -use std::vec; - +use crate::deck_slices::{freqdeck_contains, ROAD_COST}; use crate::enums::{Action, ActionPrompt}; use crate::state::State; +const TOTAL_ROADS_PER_PLAYER: u8 = 15; + impl State { pub fn generate_playable_actions(&self) -> Vec { println!("Generating playable actions"); @@ -12,7 +13,7 @@ impl State { ActionPrompt::BuildInitialSettlement => { self.settlement_possibilities(current_color, true) } - ActionPrompt::BuildInitialRoad => todo!(), + ActionPrompt::BuildInitialRoad => self.road_possibilities(current_color, true), ActionPrompt::PlayTurn => todo!(), ActionPrompt::Discard => todo!(), ActionPrompt::MoveRobber => todo!(), @@ -24,8 +25,8 @@ impl State { pub fn settlement_possibilities(&self, color: u8, is_initial_build_phase: bool) -> Vec { println!( - "Generating settlement possibilities {:?} {:?} {:?}", - self, color, is_initial_build_phase + "Generating settlement possibilities {:?} {:?}", + color, is_initial_build_phase ); if is_initial_build_phase { self.board_buildable_ids @@ -36,6 +37,23 @@ impl State { panic!("Not implemented"); } } + + pub fn road_possibilities(&self, color: u8, is_free: bool) -> Vec { + println!("Generating road possibilities {:?} {:?}", color, is_free); + let has_roads_available = TOTAL_ROADS_PER_PLAYER - self.roads_by_color[color as usize]; + if has_roads_available == 0 { + return vec![]; + } + + if is_free || freqdeck_contains(self.get_player_hand(color), ROAD_COST) { + self.board_buildable_edges(color) + .iter() + .map(|edge_id| Action::BuildRoad(color, *edge_id)) + .collect() + } else { + vec![] + } + } } #[cfg(test)] diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/mutations.rs new file mode 100644 index 000000000..9b5f6434d --- /dev/null +++ b/catanatron_rust/src/state/mutations.rs @@ -0,0 +1,56 @@ +use std::collections::HashSet; + +use crate::{ + deck_slices::{freqdeck_add, freqdeck_sub, SETTLEMENT_COST}, + enums::Action, + state::Building, + state_vector::{actual_victory_points_index, BANK_RESOURCE_SLICE}, +}; + +use super::State; + +impl State { + pub fn build_settlement(&mut self, color: u8, node_id: u8) { + println!("Building settlement {:?} {:?}", color, node_id); + self.buildings.insert(node_id, Building::Settlement(color)); + + // Maintain caches + // - connected_components + if self.is_initial_build_phase() { + let component = HashSet::from([node_id]); + self.connected_components + .entry(color) + .or_insert(Vec::new()) + .push(component); + } else { + todo!(); + } + // - board_buildable_ids + self.board_buildable_ids.remove(&node_id); + for neighbor_id in self.map_instance.get_neighbor_nodes(node_id) { + self.board_buildable_ids.remove(&neighbor_id); + } + + let n = self.get_num_players(); + self.vector[actual_victory_points_index(n, color)] += 1; + + let is_initial_build_phase = self.is_initial_build_phase(); + if !is_initial_build_phase { + freqdeck_sub(&mut self.get_mut_player_hand(color), SETTLEMENT_COST); + freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], SETTLEMENT_COST); + } + } + + pub fn apply_action(&mut self, action: Action) { + match action { + Action::BuildSettlement(color, node_id) => { + self.build_settlement(color, node_id); + } + _ => { + println!("Action not implemented: {:?}", action); + } + } + + println!("Applying action {:?}", action); + } +} diff --git a/catanatron_rust/src/state_functions.rs b/catanatron_rust/src/state_functions.rs deleted file mode 100644 index af85f6257..000000000 --- a/catanatron_rust/src/state_functions.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::{enums::Action, state::State}; - -// ===== Mutable functions ===== -pub fn apply_action(state: &mut State, action: Action) { - println!("Applying action {:?} {:?}", state, action); -} diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index 082ea67ab..fa5ef703d 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -1,4 +1,4 @@ -use crate::decks::starting_dev_listdeck; +use crate::{decks::starting_dev_listdeck, map_instance::MapInstance}; use rand::seq::SliceRandom; use crate::enums::COLORS; @@ -73,6 +73,7 @@ pub fn bank_resource_index(resource: u8) -> usize { } resource as usize } +pub const BANK_RESOURCE_SLICE: std::ops::Range = 0..5; const PLAYER_STATE_START_INDEX: usize = 268; pub fn seating_order_slice(num_players: usize) -> std::ops::Range { PLAYER_STATE_START_INDEX..PLAYER_STATE_START_INDEX + num_players @@ -89,6 +90,12 @@ pub const HAS_ROLLED_INDEX: usize = 35; pub const IS_DISCARDING_INDEX: usize = 36; pub const IS_MOVING_ROBBER_INDEX: usize = 37; +pub const ROBBER_TILE_INDEX: usize = 42; +pub fn player_hand_slice(color: u8) -> std::ops::Range { + let start = PLAYER_STATE_START_INDEX + 1 + (color as usize * 15); + start..start + 5 +} + /// This is a compact representation of the omnipotent state of the game. /// Fairly close to a bitboard, but not quite. Its a vector of integers. /// @@ -141,15 +148,8 @@ pub fn initialize_state(num_players: u8) -> Vec { // Board // TODO: Generate map from template - // vector[41] = 0; - // size += 1; // Robber_Tile (Tile Index < num_tiles) - // size += num_tiles; // Tile_Resource (Resource Index <= 5) - // size += num_tiles; // Tile_Number (Number <= 12) - // size += num_edges; // Edge_Owner (Player Index | -1 < n + 1) - // size += num_nodes; // Node_Owner (Player Index | -1 < n + 1) - // size += num_nodes; // Node_Settlement/City (1=Settlement, 2=City, 0=Nothing) - // size += num_ports; // Port_Resource (Resource Index <= 5) - vector[267] = 11; // temporary mark + vector[ROBBER_TILE_INDEX] = 0; // Robber_Tile + let mut player_state_start = PLAYER_STATE_START_INDEX; // Initialize Players From adc0be42a8b4385f6b4d3101b34c5b5468df6af6 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 26 Oct 2024 19:50:52 -0400 Subject: [PATCH 44/83] Clippy fixes --- catanatron_rust/src/game.rs | 2 +- catanatron_rust/src/state.rs | 6 +++--- catanatron_rust/src/state/mutations.rs | 4 ++-- catanatron_rust/src/state_vector.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index d8ca444af..937b3d31d 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -91,7 +91,7 @@ mod tests { assert_eq!(state.generate_playable_actions().len(), 54); play_tick(&players, &mut state); - assert_eq!(state.is_initial_build_phase(), true); + assert!(state.is_initial_build_phase()); assert_eq!(state.generate_playable_actions().len(), 3); } } diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 8fd7ac982..9b221693e 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -195,8 +195,8 @@ mod tests { fn test_initial_build_phase() { let state = setup_state(); - assert_eq!(state.is_initial_build_phase(), true); - assert_eq!(state.is_moving_robber(), false); - assert_eq!(state.is_discarding(), false); + assert!(state.is_initial_build_phase()); + assert!(!state.is_moving_robber()); + assert!(!state.is_discarding()); } } diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/mutations.rs index 9b5f6434d..55ddbf647 100644 --- a/catanatron_rust/src/state/mutations.rs +++ b/catanatron_rust/src/state/mutations.rs @@ -20,7 +20,7 @@ impl State { let component = HashSet::from([node_id]); self.connected_components .entry(color) - .or_insert(Vec::new()) + .or_default() .push(component); } else { todo!(); @@ -36,7 +36,7 @@ impl State { let is_initial_build_phase = self.is_initial_build_phase(); if !is_initial_build_phase { - freqdeck_sub(&mut self.get_mut_player_hand(color), SETTLEMENT_COST); + freqdeck_sub(self.get_mut_player_hand(color), SETTLEMENT_COST); freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], SETTLEMENT_COST); } } diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index fa5ef703d..87ca2aba1 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -1,4 +1,4 @@ -use crate::{decks::starting_dev_listdeck, map_instance::MapInstance}; +use crate::decks::starting_dev_listdeck; use rand::seq::SliceRandom; use crate::enums::COLORS; From b818f8a345ab867d57a43cd3debe7498369f81d4 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 26 Oct 2024 19:59:45 -0400 Subject: [PATCH 45/83] Improve Testing --- catanatron_rust/src/game.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 937b3d31d..479254a52 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -51,7 +51,10 @@ fn play_tick(players: &HashMap>, state: &mut State) { #[cfg(test)] mod tests { use super::*; - use crate::{enums::MapType, player::RandomPlayer}; + use crate::{ + enums::{Action, MapType}, + player::RandomPlayer, + }; fn setup_game() -> (GlobalState, GameConfiguration, HashMap>) { let global_state = GlobalState::new(); @@ -89,9 +92,20 @@ mod tests { let rc_config = Rc::new(config); let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); - assert_eq!(state.generate_playable_actions().len(), 54); + let playable_actions = state.generate_playable_actions(); + assert_eq!(playable_actions.len(), 54); + assert!(playable_actions + .iter() + .all(|e| matches!(e, Action::BuildSettlement(_, _)))); + play_tick(&players, &mut state); + + // assert at least 2 actions and all are build road + let playable_actions = state.generate_playable_actions(); + assert!(playable_actions.len() >= 2); + assert!(playable_actions + .iter() + .all(|e| matches!(e, Action::BuildRoad(_, _)))); assert!(state.is_initial_build_phase()); - assert_eq!(state.generate_playable_actions().len(), 3); } } From 92cbeb735863059e60596f19036af28a01a2f9d1 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 26 Oct 2024 20:04:19 -0400 Subject: [PATCH 46/83] Add Test --- catanatron_rust/src/state.rs | 9 +++-- catanatron_rust/src/state/mutations.rs | 50 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 9b221693e..f81ec387a 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -13,7 +13,7 @@ use crate::{ }, }; -#[derive(Debug)] +#[derive(Debug, PartialEq)] enum Building { Settlement(u8), // Color City(u8), // Color @@ -126,11 +126,14 @@ impl State { &self.vector[player_hand_slice(color)] } + pub fn get_actual_victory_points(&self, color: u8) -> u8 { + self.vector[actual_victory_points_index(self.config.num_players, color)] + } + pub fn winner(&self) -> Option { let current_color = self.get_current_color(); - let actual_victory_points = - self.vector[actual_victory_points_index(self.config.num_players, current_color)]; + let actual_victory_points = self.get_actual_victory_points(current_color); if actual_victory_points >= self.config.vps_to_win { return Some(current_color); } diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/mutations.rs index 55ddbf647..295b1c837 100644 --- a/catanatron_rust/src/state/mutations.rs +++ b/catanatron_rust/src/state/mutations.rs @@ -54,3 +54,53 @@ impl State { println!("Applying action {:?}", action); } } + +#[cfg(test)] +mod tests { + use std::rc::Rc; + + use super::*; + use crate::{ + enums::{GameConfiguration, MapType}, + global_state::GlobalState, + map_instance::MapInstance, + }; + + fn setup_state() -> State { + let global_state = GlobalState::new(); + let config = GameConfiguration { + dicard_limit: 7, + vps_to_win: 10, + map_type: MapType::Base, + num_players: 2, + max_turns: 10, + }; + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); + State::new(Rc::new(config), Rc::new(map_instance)) + } + + #[test] + fn test_build_settlement() { + let mut state = setup_state(); + let color = state.get_current_color(); + assert_eq!(state.buildings.get(&0), None); + assert_eq!(state.board_buildable_ids.len(), 54); + assert_eq!(state.get_actual_victory_points(color), 0); + + let node_id = 0; + state.build_settlement(color, node_id); + + assert_eq!( + state.buildings.get(&node_id), + Some(&Building::Settlement(color)) + ); + assert_eq!(state.board_buildable_ids.len(), 50); + assert_eq!(state.get_actual_victory_points(color), 1); + } + + // TODO: Assert build_settlement spends player resources +} From a92b1e64971cac7af69677c7afa1b5146440e03a Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 26 Oct 2024 20:08:03 -0400 Subject: [PATCH 47/83] Create State::new_base to share in tests --- catanatron_rust/src/state.rs | 42 ++++++++++---------- catanatron_rust/src/state/move_generation.rs | 39 +----------------- catanatron_rust/src/state/mutations.rs | 26 +----------- 3 files changed, 24 insertions(+), 83 deletions(-) diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index f81ec387a..9f89a08c5 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -4,7 +4,8 @@ use std::{ }; use crate::{ - enums::{ActionPrompt, GameConfiguration}, + enums::{ActionPrompt, GameConfiguration, MapType}, + global_state::GlobalState, map_instance::{EdgeId, MapInstance, NodeId}, state_vector::{ actual_victory_points_index, initialize_state, player_hand_slice, seating_order_slice, @@ -67,6 +68,23 @@ impl State { } } + pub fn new_base() -> Self { + let global_state = GlobalState::new(); + let config = GameConfiguration { + dicard_limit: 7, + vps_to_win: 10, + map_type: MapType::Base, + num_players: 4, + max_turns: 10, + }; + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); + State::new(Rc::new(config), Rc::new(map_instance)) + } + fn get_num_players(&self) -> u8 { self.config.num_players } @@ -168,35 +186,17 @@ impl State { #[cfg(test)] mod tests { use super::*; - use crate::{enums::MapType, global_state::GlobalState}; - - fn setup_state() -> State { - let global_state = GlobalState::new(); - let config = GameConfiguration { - dicard_limit: 7, - vps_to_win: 10, - map_type: MapType::Base, - num_players: 2, - max_turns: 10, - }; - let map_instance = MapInstance::new( - &global_state.base_map_template, - &global_state.dice_probas, - 0, - ); - State::new(Rc::new(config), Rc::new(map_instance)) - } #[test] fn test_state_creation() { - let state = setup_state(); + let state = State::new_base(); assert_eq!(state.longest_road_color, None); } #[test] fn test_initial_build_phase() { - let state = setup_state(); + let state = State::new_base(); assert!(state.is_initial_build_phase()); assert!(!state.is_moving_robber()); diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 581eba550..5ee5c13e1 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -58,33 +58,11 @@ impl State { #[cfg(test)] mod tests { - use std::rc::Rc; - use super::*; - use crate::enums::{GameConfiguration, MapType}; - use crate::global_state::GlobalState; - use crate::map_instance::MapInstance; - - fn setup_state() -> State { - let global_state = GlobalState::new(); - let config = GameConfiguration { - dicard_limit: 7, - vps_to_win: 10, - map_type: MapType::Base, - num_players: 2, - max_turns: 10, - }; - let map_instance = MapInstance::new( - &global_state.base_map_template, - &global_state.dice_probas, - 0, - ); - State::new(Rc::new(config), Rc::new(map_instance)) - } #[test] fn test_move_generation() { - let state = setup_state(); + let state = State::new_base(); let actions = state.generate_playable_actions(); assert_eq!(actions.len(), 54); assert!(matches!(actions[0], Action::BuildSettlement(_, _))); @@ -92,20 +70,7 @@ mod tests { #[test] fn test_settlement_possibilities() { - let global_state = GlobalState::new(); - let config = GameConfiguration { - dicard_limit: 7, - vps_to_win: 10, - map_type: MapType::Base, - num_players: 2, - max_turns: 10, - }; - let map_instance = MapInstance::new( - &global_state.base_map_template, - &global_state.dice_probas, - 0, - ); - let state = State::new(Rc::new(config), Rc::new(map_instance)); + let state = State::new_base(); let initial_build_phase_actions = state.settlement_possibilities(0, true); assert_eq!(initial_build_phase_actions.len(), 54); diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/mutations.rs index 295b1c837..fe5c32149 100644 --- a/catanatron_rust/src/state/mutations.rs +++ b/catanatron_rust/src/state/mutations.rs @@ -57,35 +57,11 @@ impl State { #[cfg(test)] mod tests { - use std::rc::Rc; - use super::*; - use crate::{ - enums::{GameConfiguration, MapType}, - global_state::GlobalState, - map_instance::MapInstance, - }; - - fn setup_state() -> State { - let global_state = GlobalState::new(); - let config = GameConfiguration { - dicard_limit: 7, - vps_to_win: 10, - map_type: MapType::Base, - num_players: 2, - max_turns: 10, - }; - let map_instance = MapInstance::new( - &global_state.base_map_template, - &global_state.dice_probas, - 0, - ); - State::new(Rc::new(config), Rc::new(map_instance)) - } #[test] fn test_build_settlement() { - let mut state = setup_state(); + let mut state = State::new_base(); let color = state.get_current_color(); assert_eq!(state.buildings.get(&0), None); assert_eq!(state.board_buildable_ids.len(), 54); From de503c7d6163123ef2cf8bbe0baa110637b0c487 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 26 Oct 2024 23:10:14 -0400 Subject: [PATCH 48/83] Progress on Initial Building Phase --- catanatron_rust/src/game.rs | 26 +++- catanatron_rust/src/state.rs | 45 ++++++- catanatron_rust/src/state/move_generation.rs | 19 ++- catanatron_rust/src/state/mutations.rs | 122 +++++++++++++++++-- catanatron_rust/src/state_vector.rs | 6 +- 5 files changed, 182 insertions(+), 36 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 479254a52..54813c787 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -36,8 +36,7 @@ fn play_tick(players: &HashMap>, state: &mut State) { let playable_actions = state.generate_playable_actions(); println!( "Player {:?} has {:?} playable actions", - current_color, - playable_actions.len() + current_color, playable_actions ); let action = current_player.decide(state, &playable_actions); println!( @@ -63,7 +62,7 @@ mod tests { vps_to_win: 10, map_type: MapType::Base, num_players: 4, - max_turns: 100, + max_turns: 8, // TODO: Change! }; let mut players: HashMap> = HashMap::new(); players.insert(0, Box::new(RandomPlayer {})); @@ -92,11 +91,17 @@ mod tests { let rc_config = Rc::new(config); let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); + let first_player = state.get_current_color(); + let playable_actions = state.generate_playable_actions(); assert_eq!(playable_actions.len(), 54); - assert!(playable_actions - .iter() - .all(|e| matches!(e, Action::BuildSettlement(_, _)))); + assert!(playable_actions.iter().all(|e| { + if let Action::BuildSettlement(player, _) = e { + *player == first_player + } else { + false + } + })); play_tick(&players, &mut state); @@ -107,5 +112,14 @@ mod tests { .iter() .all(|e| matches!(e, Action::BuildRoad(_, _)))); assert!(state.is_initial_build_phase()); + + play_tick(&players, &mut state); + + // assert at 50 actions and all are build settlement + let playable_actions = state.generate_playable_actions(); + assert_eq!(playable_actions.len(), 50); + assert!(playable_actions + .iter() + .all(|e| matches!(e, Action::BuildSettlement(_, _)))); } } diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 9f89a08c5..4267b0240 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -9,8 +9,8 @@ use crate::{ map_instance::{EdgeId, MapInstance, NodeId}, state_vector::{ actual_victory_points_index, initialize_state, player_hand_slice, seating_order_slice, - StateVector, CURRENT_TICK_SEAT_INDEX, IS_DISCARDING_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, - IS_MOVING_ROBBER_INDEX, + StateVector, CURRENT_TICK_SEAT_INDEX, FREE_ROADS_AVAILABLE_INDEX, IS_DISCARDING_INDEX, + IS_INITIAL_BUILD_PHASE_INDEX, IS_MOVING_ROBBER_INDEX, }, }; @@ -106,9 +106,18 @@ impl State { self.vector[IS_DISCARDING_INDEX] == 1 } + fn is_road_building(&self) -> bool { + self.vector[IS_INITIAL_BUILD_PHASE_INDEX] == 1 + && self.vector[FREE_ROADS_AVAILABLE_INDEX] == 1 + } + + pub fn get_current_tick_seat(&self) -> u8 { + self.vector[CURRENT_TICK_SEAT_INDEX] + } + pub fn get_current_color(&self) -> u8 { let seating_order = self.get_seating_order(); - let current_tick_seat = self.vector[CURRENT_TICK_SEAT_INDEX]; + let current_tick_seat = self.get_current_tick_seat(); seating_order[current_tick_seat as usize] } @@ -120,7 +129,7 @@ impl State { pub fn get_action_prompt(&self) -> ActionPrompt { if self.is_initial_build_phase() { - let num_things_built = self.buildings.len(); + let num_things_built = self.buildings.len() + self.roads.len() / 2; if num_things_built == 2 * self.config.num_players as usize { return ActionPrompt::PlayTurn; } else if num_things_built % 2 == 0 { @@ -161,8 +170,6 @@ impl State { // ===== Board Getters ===== // TODO: Potentially cache this implementation pub fn board_buildable_edges(&self, color: u8) -> Vec { - println!("Building buildable edges {:?}", color); - let color_components = self.connected_components.get(&color).unwrap(); let expandable_nodes: Vec = color_components .iter() @@ -181,6 +188,32 @@ impl State { } buildable.into_iter().collect() } + + fn get_connected_component_index(&self, color: u8, a: u8) -> Option { + let components = self.connected_components.get(&color).unwrap(); + for (i, component) in components.iter().enumerate() { + if component.contains(&a) { + return Some(i); + } + } + None + } + + fn is_enemy_node(&self, color: u8, a: u8) -> bool { + let node_color = self.get_node_color(a); + match node_color { + None => false, + Some(node_color) => node_color != color, + } + } + + fn get_node_color(&self, a: u8) -> Option { + match self.buildings.get(&a) { + Some(Building::Settlement(color)) => Some(*color), + Some(Building::City(color)) => Some(*color), + None => None, + } + } } #[cfg(test)] diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 5ee5c13e1..419c2f622 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -6,7 +6,6 @@ const TOTAL_ROADS_PER_PLAYER: u8 = 15; impl State { pub fn generate_playable_actions(&self) -> Vec { - println!("Generating playable actions"); let current_color = self.get_current_color(); let action_prompt = self.get_action_prompt(); match action_prompt { @@ -14,20 +13,17 @@ impl State { self.settlement_possibilities(current_color, true) } ActionPrompt::BuildInitialRoad => self.road_possibilities(current_color, true), - ActionPrompt::PlayTurn => todo!(), - ActionPrompt::Discard => todo!(), - ActionPrompt::MoveRobber => todo!(), - // TODO: - ActionPrompt::DecideTrade => todo!(), - ActionPrompt::DecideAcceptees => todo!(), + ActionPrompt::PlayTurn => todo!("generate_playbale_actions for PlayTurn"), + ActionPrompt::Discard => todo!("generate_playbale_actions for Discard"), + ActionPrompt::MoveRobber => todo!("generate_playbale_actions for Move robber"), + ActionPrompt::DecideTrade => todo!("generate_playbale_actions for Decide trade"), + ActionPrompt::DecideAcceptees => { + todo!("generate_playbale_actions for Decide acceptees") + } } } pub fn settlement_possibilities(&self, color: u8, is_initial_build_phase: bool) -> Vec { - println!( - "Generating settlement possibilities {:?} {:?}", - color, is_initial_build_phase - ); if is_initial_build_phase { self.board_buildable_ids .iter() @@ -39,7 +35,6 @@ impl State { } pub fn road_possibilities(&self, color: u8, is_free: bool) -> Vec { - println!("Generating road possibilities {:?} {:?}", color, is_free); let has_roads_available = TOTAL_ROADS_PER_PLAYER - self.roads_by_color[color as usize]; if has_roads_available == 0 { return vec![]; diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/mutations.rs index fe5c32149..425ca6925 100644 --- a/catanatron_rust/src/state/mutations.rs +++ b/catanatron_rust/src/state/mutations.rs @@ -1,20 +1,47 @@ use std::collections::HashSet; use crate::{ - deck_slices::{freqdeck_add, freqdeck_sub, SETTLEMENT_COST}, + deck_slices::{freqdeck_add, freqdeck_sub, ROAD_COST, SETTLEMENT_COST}, enums::Action, + map_instance::EdgeId, state::Building, - state_vector::{actual_victory_points_index, BANK_RESOURCE_SLICE}, + state_vector::{ + actual_victory_points_index, BANK_RESOURCE_SLICE, CURRENT_TICK_SEAT_INDEX, + CURRENT_TURN_SEAT_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, + }, }; use super::State; impl State { + pub fn add_victory_points(&mut self, color: u8, points: u8) { + let n = self.get_num_players(); + self.vector[actual_victory_points_index(n, color)] += points; + } + + pub fn advance_turn(&mut self, step_size: i8) { + // We add an extra num_players to ensure next_index is positive (u8) + let num_players = self.get_num_players() as i8; + let next_index = + ((self.get_current_tick_seat() as i8 + step_size + num_players) % num_players) as u8; + self.vector[CURRENT_TICK_SEAT_INDEX] = next_index; + self.vector[CURRENT_TURN_SEAT_INDEX] = next_index; + } + pub fn build_settlement(&mut self, color: u8, node_id: u8) { - println!("Building settlement {:?} {:?}", color, node_id); self.buildings.insert(node_id, Building::Settlement(color)); - // Maintain caches + let is_free = self.is_initial_build_phase(); + if !is_free { + freqdeck_sub(self.get_mut_player_hand(color), SETTLEMENT_COST); + freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], SETTLEMENT_COST); + } + + self.add_victory_points(color, 1); + + // TODO: If second house, yield resources + + // Maintain caches and longest road ===== // - connected_components if self.is_initial_build_phase() { let component = HashSet::from([node_id]); @@ -23,6 +50,8 @@ impl State { .or_default() .push(component); } else { + // TODO: Mantain connected_components + // TODO: Mantain longest_road_color and longest_road_length (maybe swapping vps) todo!(); } // - board_buildable_ids @@ -30,15 +59,85 @@ impl State { for neighbor_id in self.map_instance.get_neighbor_nodes(node_id) { self.board_buildable_ids.remove(&neighbor_id); } + } - let n = self.get_num_players(); - self.vector[actual_victory_points_index(n, color)] += 1; + fn build_road(&mut self, color: u8, edge_id: EdgeId) { + let inverted_edge = (edge_id.1, edge_id.0); + self.roads.insert(edge_id, color); + self.roads.insert(inverted_edge, color); let is_initial_build_phase = self.is_initial_build_phase(); - if !is_initial_build_phase { - freqdeck_sub(self.get_mut_player_hand(color), SETTLEMENT_COST); - freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], SETTLEMENT_COST); + let is_free = is_initial_build_phase || self.is_road_building(); + if !is_free { + freqdeck_sub(self.get_mut_player_hand(color), ROAD_COST); + freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], ROAD_COST); + } + + if is_initial_build_phase { + let num_settlements = self.buildings.len(); + let num_players = self.config.num_players as usize; + let going_forward = num_settlements < num_players; + let at_midpoint = num_settlements == 2 * num_players; + + if going_forward { + self.advance_turn(1); + } else if at_midpoint { + // do nothing, generate prompt should take care + } else if num_settlements % num_players == 0 { + // just change prompt without advancing turn (since last to place is first to roll) + self.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; + } else { + self.advance_turn(-1); + } } + + // Maintain caches and longest road ===== + // Extend or merge components + let (a, b) = edge_id; + let a_index = self.get_connected_component_index(color, a); + let b_index = self.get_connected_component_index(color, b); + if a_index.is_none() && !self.is_enemy_node(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(&color) + .unwrap() + .get_mut(b_index.unwrap()) + .unwrap(); + component.insert(a); // extend said component by 1 more node + } else if b_index.is_none() && !self.is_enemy_node(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(&color) + .unwrap() + .get_mut(a_index.unwrap()) + .unwrap(); + component.insert(b); // extend said component by 1 more node + } else if !a_index.is_none() && !b_index.is_none() && a_index != b_index { + // Merge components into one and delete the other + let a_component = self + .connected_components + .get_mut(&color) + .unwrap() + .remove(a_index.unwrap()); + let b_component = self + .connected_components + .get_mut(&color) + .unwrap() + .remove(b_index.unwrap()); + let mut new_component = a_component.clone(); + new_component.extend(b_component); + self.connected_components + .get_mut(&color) + .unwrap() + .push(new_component); + } else { + // In this case, a_index == b_index, which means that the edge + // is already part of one component. No actions needed. + } + + // TODO: Return previous road } pub fn apply_action(&mut self, action: Action) { @@ -46,8 +145,11 @@ impl State { Action::BuildSettlement(color, node_id) => { self.build_settlement(color, node_id); } + Action::BuildRoad(color, edge_id) => { + self.build_road(color, edge_id); + } _ => { - println!("Action not implemented: {:?}", action); + panic!("Action not implemented: {:?}", action); } } diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index 87ca2aba1..579509771 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -89,6 +89,8 @@ pub const HAS_PLAYED_DEV_CARD: usize = 34; pub const HAS_ROLLED_INDEX: usize = 35; pub const IS_DISCARDING_INDEX: usize = 36; pub const IS_MOVING_ROBBER_INDEX: usize = 37; +pub const IS_BUILDING_ROAD_INDEX: usize = 38; +pub const FREE_ROADS_AVAILABLE_INDEX: usize = 39; pub const ROBBER_TILE_INDEX: usize = 42; pub fn player_hand_slice(color: u8) -> std::ops::Range { @@ -139,8 +141,8 @@ pub fn initialize_state(num_players: u8) -> Vec { vector[HAS_ROLLED_INDEX] = 0; // Has_Rolled vector[IS_DISCARDING_INDEX] = 0; // Is_Discarding vector[IS_MOVING_ROBBER_INDEX] = 0; // Is_Moving_Robber - vector[38] = 0; // Is_Building_Road - vector[39] = 2; // Free_Roads_Available + vector[IS_BUILDING_ROAD_INDEX] = 0; // Is_Building_Road + vector[FREE_ROADS_AVAILABLE_INDEX] = 2; // Free_Roads_Available // Extra (u8::MAX is used to indicate no player) vector[40] = u8::MAX; // Longest_Road_Player_Index From 07a13c589edb91dd356ca76802d5791b0427d473 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 26 Oct 2024 23:11:59 -0400 Subject: [PATCH 49/83] max_ticks in favor of max_turns --- catanatron_rust/src/enums.rs | 2 +- catanatron_rust/src/game.rs | 10 +++++----- catanatron_rust/src/main.rs | 2 +- catanatron_rust/src/state.rs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index b9069cb98..7327891b2 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -102,5 +102,5 @@ pub struct GameConfiguration { pub vps_to_win: u8, pub map_type: MapType, pub num_players: u8, - pub max_turns: u32, + pub max_ticks: u32, } diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 54813c787..9413111ab 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -20,11 +20,11 @@ pub fn play_game( let rc_config = Rc::new(config); println!("Playing game with configuration: {:?}", rc_config); let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); - let mut num_turns = 0; - while state.winner().is_none() && num_turns < rc_config.max_turns { - println!("Playing turn {:?}", num_turns); + let mut num_ticks = 0; + while state.winner().is_none() && num_ticks < rc_config.max_ticks { + println!("Playing turn {:?}", num_ticks); play_tick(&players, &mut state); - num_turns += 1; + num_ticks += 1; } state.winner() } @@ -62,7 +62,7 @@ mod tests { vps_to_win: 10, map_type: MapType::Base, num_players: 4, - max_turns: 8, // TODO: Change! + max_ticks: 8, // TODO: Change! }; let mut players: HashMap> = HashMap::new(); players.insert(0, Box::new(RandomPlayer {})); diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 6961cc968..339ed1c18 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -78,7 +78,7 @@ fn main() { vps_to_win: 10, map_type: MapType::Base, num_players: 2, - max_turns: 2, + max_ticks: 4, }; let mut players: HashMap> = HashMap::new(); players.insert(Color::Red as u8, Box::new(RandomPlayer {})); diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 4267b0240..d8e460a20 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -75,7 +75,7 @@ impl State { vps_to_win: 10, map_type: MapType::Base, num_players: 4, - max_turns: 10, + max_ticks: 10, }; let map_instance = MapInstance::new( &global_state.base_map_template, From 376b101b554d929db18419056b3b3782cb36cb2e Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 27 Oct 2024 07:55:25 -0400 Subject: [PATCH 50/83] Test 4 player initial build phase (just building sequence) --- catanatron_rust/src/game.rs | 103 ++++++++++++++++++++----- catanatron_rust/src/main.rs | 2 +- catanatron_rust/src/state.rs | 2 +- catanatron_rust/src/state/mutations.rs | 4 +- 4 files changed, 88 insertions(+), 23 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 9413111ab..600f050e7 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -20,9 +20,10 @@ pub fn play_game( let rc_config = Rc::new(config); println!("Playing game with configuration: {:?}", rc_config); let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); + println!("Seat order: {:?}", state.get_seating_order()); let mut num_ticks = 0; while state.winner().is_none() && num_ticks < rc_config.max_ticks { - println!("Playing turn {:?}", num_ticks); + println!("Tick {:?} =====", num_ticks); play_tick(&players, &mut state); num_ticks += 1; } @@ -51,7 +52,7 @@ fn play_tick(players: &HashMap>, state: &mut State) { mod tests { use super::*; use crate::{ - enums::{Action, MapType}, + enums::{Action, ActionPrompt, MapType}, player::RandomPlayer, }; @@ -91,35 +92,99 @@ mod tests { let rc_config = Rc::new(config); let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); - let first_player = state.get_current_color(); + let seating_order = state.get_seating_order(); + let first_player = seating_order[0]; + let second_player = seating_order[1]; + let third_player = seating_order[2]; + let fourth_player = seating_order[3]; + // first player settlement let playable_actions = state.generate_playable_actions(); assert_eq!(playable_actions.len(), 54); - assert!(playable_actions.iter().all(|e| { - if let Action::BuildSettlement(player, _) = e { - *player == first_player - } else { - false - } - })); + assert_all_build_settlements(playable_actions, first_player); + play_tick(&players, &mut state); + // first player road + let playable_actions = state.generate_playable_actions(); + assert!(playable_actions.len() >= 2); + assert_all_build_roads(playable_actions, first_player); + assert!(state.is_initial_build_phase()); + play_tick(&players, &mut state); + + // second player settlement: assert at 50-51 actions and all are build settlement + let playable_actions = state.generate_playable_actions(); + assert!(playable_actions.len() >= 50 && playable_actions.len() <= 51); + assert_all_build_settlements(playable_actions, second_player); play_tick(&players, &mut state); - // assert at least 2 actions and all are build road + // second player road: assert at least 2 actions and all are build road let playable_actions = state.generate_playable_actions(); assert!(playable_actions.len() >= 2); - assert!(playable_actions - .iter() - .all(|e| matches!(e, Action::BuildRoad(_, _)))); + assert_all_build_roads(playable_actions, second_player); + play_tick(&players, &mut state); + + play_tick(&players, &mut state); // third player settlement + + // third player road + let playable_actions = state.generate_playable_actions(); + assert_all_build_roads(playable_actions, third_player); + play_tick(&players, &mut state); + + play_tick(&players, &mut state); // fourth player settlement + play_tick(&players, &mut state); // fourth player road + + // fourth player settlement 2 assert!(state.is_initial_build_phase()); + let playable_actions = state.generate_playable_actions(); + assert_all_build_settlements(playable_actions, fourth_player); + play_tick(&players, &mut state); + play_tick(&players, &mut state); // fourth player road + play_tick(&players, &mut state); // third player settlement 2 + play_tick(&players, &mut state); // third player road play_tick(&players, &mut state); - // assert at 50 actions and all are build settlement + // second player road 2 let playable_actions = state.generate_playable_actions(); - assert_eq!(playable_actions.len(), 50); - assert!(playable_actions - .iter() - .all(|e| matches!(e, Action::BuildSettlement(_, _)))); + assert_all_build_roads(playable_actions, second_player); + play_tick(&players, &mut state); + + play_tick(&players, &mut state); // first player settlement 2 + play_tick(&players, &mut state); // first player road + + // Assert that the initial build phase is over and its the first player's turn + assert!(!state.is_initial_build_phase()); + assert_eq!(state.get_current_color(), first_player); + assert!(matches!(state.get_action_prompt(), ActionPrompt::PlayTurn)); + + // TODO: Assert players have money of their second house + } + + fn assert_all_build_settlements(playable_actions: Vec, player: u8) { + assert!( + playable_actions.iter().all(|e| { + if let Action::BuildSettlement(p, _) = e { + *p == player + } else { + false + } + }), + "Expected all actions to be BuildSettlement for player {:?}", + player + ); + } + + fn assert_all_build_roads(playable_actions: Vec, player: u8) { + assert!( + playable_actions.iter().all(|e| { + if let Action::BuildRoad(p, _) = e { + *p == player + } else { + false + } + }), + "Expected all actions to be BuildRoad for player {:?}", + player + ); } } diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 339ed1c18..06bb7bf98 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -78,7 +78,7 @@ fn main() { vps_to_win: 10, map_type: MapType::Base, num_players: 2, - max_ticks: 4, + max_ticks: 8, }; let mut players: HashMap> = HashMap::new(); players.insert(Color::Red as u8, Box::new(RandomPlayer {})); diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index d8e460a20..ae929b10e 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -130,7 +130,7 @@ impl State { pub fn get_action_prompt(&self) -> ActionPrompt { if self.is_initial_build_phase() { let num_things_built = self.buildings.len() + self.roads.len() / 2; - if num_things_built == 2 * self.config.num_players as usize { + if num_things_built == 4 * self.config.num_players as usize { return ActionPrompt::PlayTurn; } else if num_things_built % 2 == 0 { return ActionPrompt::BuildInitialSettlement; diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/mutations.rs index 425ca6925..e8ef1470b 100644 --- a/catanatron_rust/src/state/mutations.rs +++ b/catanatron_rust/src/state/mutations.rs @@ -77,13 +77,13 @@ impl State { let num_settlements = self.buildings.len(); let num_players = self.config.num_players as usize; let going_forward = num_settlements < num_players; - let at_midpoint = num_settlements == 2 * num_players; + let at_midpoint = num_settlements == num_players; if going_forward { self.advance_turn(1); } else if at_midpoint { // do nothing, generate prompt should take care - } else if num_settlements % num_players == 0 { + } else if num_settlements == 2 * num_players { // just change prompt without advancing turn (since last to place is first to roll) self.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; } else { From 9da7b7743ff83a73b03f632d516ddb42a34241b6 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 27 Oct 2024 08:01:34 -0400 Subject: [PATCH 51/83] Test initial build phase (sequence only) for 2 player catan --- catanatron_rust/src/game.rs | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 600f050e7..3256a5e03 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -56,13 +56,15 @@ mod tests { player::RandomPlayer, }; - fn setup_game() -> (GlobalState, GameConfiguration, HashMap>) { + fn setup_game( + num_players: u8, + ) -> (GlobalState, GameConfiguration, HashMap>) { let global_state = GlobalState::new(); let config = GameConfiguration { dicard_limit: 7, vps_to_win: 10, map_type: MapType::Base, - num_players: 4, + num_players: num_players, max_ticks: 8, // TODO: Change! }; let mut players: HashMap> = HashMap::new(); @@ -75,15 +77,15 @@ mod tests { #[test] fn test_game_creation() { - let (global_state, config, players) = setup_game(); + let (global_state, config, players) = setup_game(4); let result = play_game(global_state, config, players); assert_eq!(result, None); } #[test] - fn test_initial_build_phase() { - let (global_state, config, players) = setup_game(); + fn test_initial_build_phase_four_player() { + let (global_state, config, players) = setup_game(4); let map_instance = MapInstance::new( &global_state.base_map_template, &global_state.dice_probas, @@ -160,6 +162,31 @@ mod tests { // TODO: Assert players have money of their second house } + #[test] + fn test_initial_build_phase_two_player() { + let (global_state, config, players) = setup_game(2); + let map_instance = MapInstance::new( + &global_state.base_map_template, + &global_state.dice_probas, + 0, + ); + let rc_config = Rc::new(config); + let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); + + let seating_order = state.get_seating_order(); + let first_player = seating_order[0]; + + for _ in 0..8 { + assert!(state.is_initial_build_phase()); + play_tick(&players, &mut state); + } + + // Assert that the initial build phase is over and its the first player's turn + assert!(!state.is_initial_build_phase()); + assert_eq!(state.get_current_color(), first_player); + assert!(matches!(state.get_action_prompt(), ActionPrompt::PlayTurn)); + } + fn assert_all_build_settlements(playable_actions: Vec, player: u8) { assert!( playable_actions.iter().all(|e| { From 9f9dc06bdf88c170dab356f066222fd04d08478f Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sun, 27 Oct 2024 08:04:13 -0400 Subject: [PATCH 52/83] Python refactor: Create play_turn_possibilities --- catanatron_core/catanatron/models/actions.py | 77 ++++++++++---------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/catanatron_core/catanatron/models/actions.py b/catanatron_core/catanatron/models/actions.py index 9bb414dc5..b21044acd 100644 --- a/catanatron_core/catanatron/models/actions.py +++ b/catanatron_core/catanatron/models/actions.py @@ -2,6 +2,7 @@ Move-generation functions (these return a list of actions that can be taken by current player). Main function is generate_playable_actions. """ + import operator as op from functools import reduce from typing import Any, Dict, List, Set, Tuple, Union @@ -51,43 +52,7 @@ def generate_playable_actions(state) -> List[Action]: elif action_prompt == ActionPrompt.MOVE_ROBBER: return robber_possibilities(state, color) elif action_prompt == ActionPrompt.PLAY_TURN: - if state.is_road_building: - actions = road_building_possibilities(state, color, False) - elif not player_has_rolled(state, color): - actions = [Action(color, ActionType.ROLL, None)] - if player_can_play_dev(state, color, "KNIGHT"): - actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) - else: - actions = [Action(color, ActionType.END_TURN, None)] - actions.extend(road_building_possibilities(state, color)) - actions.extend(settlement_possibilities(state, color)) - actions.extend(city_possibilities(state, color)) - - can_buy_dev_card = ( - player_can_afford_dev_card(state, color) - and len(state.development_listdeck) > 0 - ) - if can_buy_dev_card: - actions.append(Action(color, ActionType.BUY_DEVELOPMENT_CARD, None)) - - # Play Dev Cards - if player_can_play_dev(state, color, "YEAR_OF_PLENTY"): - actions.extend( - year_of_plenty_possibilities(color, state.resource_freqdeck) - ) - if player_can_play_dev(state, color, "MONOPOLY"): - actions.extend(monopoly_possibilities(color)) - if player_can_play_dev(state, color, "KNIGHT"): - actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) - if ( - player_can_play_dev(state, color, "ROAD_BUILDING") - and len(road_building_possibilities(state, color, False)) > 0 - ): - actions.append(Action(color, ActionType.PLAY_ROAD_BUILDING, None)) - - # Trade - actions.extend(maritime_trade_possibilities(state, color)) - return actions + return play_turn_possibilities(state, color) elif action_prompt == ActionPrompt.DISCARD: return discard_possibilities(color) elif action_prompt == ActionPrompt.DECIDE_TRADE: @@ -240,6 +205,44 @@ def robber_possibilities(state, color) -> List[Action]: return actions +def play_turn_possibilities(state, color) -> List[Action]: + if state.is_road_building: + actions = road_building_possibilities(state, color, False) + elif not player_has_rolled(state, color): + actions = [Action(color, ActionType.ROLL, None)] + if player_can_play_dev(state, color, "KNIGHT"): + actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) + else: + actions = [Action(color, ActionType.END_TURN, None)] + actions.extend(road_building_possibilities(state, color)) + actions.extend(settlement_possibilities(state, color)) + actions.extend(city_possibilities(state, color)) + + can_buy_dev_card = ( + player_can_afford_dev_card(state, color) + and len(state.development_listdeck) > 0 + ) + if can_buy_dev_card: + actions.append(Action(color, ActionType.BUY_DEVELOPMENT_CARD, None)) + + # Play Dev Cards + if player_can_play_dev(state, color, "YEAR_OF_PLENTY"): + actions.extend(year_of_plenty_possibilities(color, state.resource_freqdeck)) + if player_can_play_dev(state, color, "MONOPOLY"): + actions.extend(monopoly_possibilities(color)) + if player_can_play_dev(state, color, "KNIGHT"): + actions.append(Action(color, ActionType.PLAY_KNIGHT_CARD, None)) + if ( + player_can_play_dev(state, color, "ROAD_BUILDING") + and len(road_building_possibilities(state, color, False)) > 0 + ): + actions.append(Action(color, ActionType.PLAY_ROAD_BUILDING, None)) + + # Trade + actions.extend(maritime_trade_possibilities(state, color)) + return actions + + def initial_road_possibilities(state, color) -> List[Action]: # Must be connected to last settlement last_settlement_node_id = state.buildings_by_color[color][SETTLEMENT][-1] From dcce77e97cd8de7f6bdff75324130939c9ad20ac Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 23 Nov 2024 15:01:02 -0400 Subject: [PATCH 53/83] Initial Build Phase --- catanatron_rust/src/deck_slices.rs | 4 +- catanatron_rust/src/enums.rs | 46 ++++++----- catanatron_rust/src/game.rs | 26 +++++- catanatron_rust/src/state.rs | 85 +++++++++++++++----- catanatron_rust/src/state/move_generation.rs | 82 ++++++++++++++++++- catanatron_rust/src/state/mutations.rs | 9 ++- catanatron_rust/src/state_vector.rs | 4 + 7 files changed, 208 insertions(+), 48 deletions(-) diff --git a/catanatron_rust/src/deck_slices.rs b/catanatron_rust/src/deck_slices.rs index 65cf2d35f..10f92d723 100644 --- a/catanatron_rust/src/deck_slices.rs +++ b/catanatron_rust/src/deck_slices.rs @@ -1,7 +1,9 @@ -type FreqDeck = [u8; 5]; +pub type FreqDeck = [u8; 5]; pub const SETTLEMENT_COST: FreqDeck = [1, 1, 1, 1, 0]; pub const ROAD_COST: FreqDeck = [1, 1, 0, 0, 0]; +pub const CITY_COST: FreqDeck = [0, 0, 0, 2, 3]; +pub const DEVCARD_COST: FreqDeck = [0, 0, 1, 1, 1]; pub fn freqdeck_sub(freqdeck: &mut [u8], cost: FreqDeck) { freqdeck[0] -= cost[0]; diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 7327891b2..a6c23f52b 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -1,4 +1,8 @@ -use crate::map_instance::{EdgeId, NodeId}; +use crate::{ + deck_slices::FreqDeck, + map_instance::{EdgeId, NodeId}, + map_template::Coordinate, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Color { @@ -68,24 +72,28 @@ pub enum ActionPrompt { #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { - Roll(Color), // None. Log instead sets it to (int, int) rolled. - MoveRobber(Color), // value is (coordinate, Color|None). Log has extra element of card stolen. - Discard(Color), // value is None|Resource[]. - BuildRoad(u8, EdgeId), // value is edge_id - BuildSettlement(u8, NodeId), // value is node_id - BuildCity, // value is node_id - BuyDevelopmentCard, // value is None. Log value is card. - PlayKnightCard, // value is None - PlayYearOfPlenty, // value is (Resource, Resource) - PlayMonopoly, // value is Resource - PlayRoadBuilding, // value is None - MaritimeTrade, // 5-resource tuple, last is resource asked. - OfferTrade, // 10-resource tuple, first 5 is offered, last 5 is receiving. - AcceptTrade, // 10-resource tuple. - RejectTrade, // None - ConfirmTrade, // 11-tuple. First 10 like OfferTrade, last is color of accepting player. - CancelTrade, // None - EndTurn, // None + // The first value in all these is the color of the player. + Roll(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[]. + 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 + PlayRoadBuilding(u8), + + // First element of tuples is in, last is out. + MaritimeTrade(u8, (FreqDeck, u8)), + OfferTrade(u8, (FreqDeck, FreqDeck)), + AcceptTrade(u8, (FreqDeck, FreqDeck)), + RejectTrade(u8), + ConfirmTrade(u8, (FreqDeck, FreqDeck, u8)), // 11-tuple. First 10 like OfferTrade, last is color of accepting player. + CancelTrade(u8), + + EndTurn(u8), // None } #[derive(Debug)] diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 3256a5e03..71fdc66a9 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::rc::Rc; -use crate::enums::GameConfiguration; +use crate::enums::{Action, GameConfiguration}; use crate::global_state::GlobalState; use crate::map_instance::MapInstance; use crate::player::Player; @@ -30,7 +30,7 @@ pub fn play_game( state.winner() } -fn play_tick(players: &HashMap>, state: &mut State) { +fn play_tick(players: &HashMap>, state: &mut State) -> Action { let current_color = state.get_current_color(); let current_player = players.get(¤t_color).unwrap(); @@ -46,6 +46,7 @@ fn play_tick(players: &HashMap>, state: &mut State) { ); state.apply_action(action); + action } #[cfg(test)] @@ -144,11 +145,28 @@ mod tests { play_tick(&players, &mut state); // fourth player road play_tick(&players, &mut state); // third player settlement 2 play_tick(&players, &mut state); // third player road - play_tick(&players, &mut state); + let second_player_second_settlement_action = play_tick(&players, &mut state); + let second_player_second_node_id; + if let Action::BuildSettlement(player, node_id) = second_player_second_settlement_action { + assert_eq!(player, second_player); + second_player_second_node_id = node_id; + } else { + panic!("Expected Action::BuildSettlement"); + } + println!("{}", second_player_second_node_id); // second player road 2 let playable_actions = state.generate_playable_actions(); - assert_all_build_roads(playable_actions, second_player); + assert_all_build_roads(playable_actions.clone(), second_player); + // assert playable_actions are connected to the last settlement + assert!(playable_actions.iter().all(|e| { + if let Action::BuildRoad(_, edge_id) = e { + second_player_second_node_id == edge_id.0 + || second_player_second_node_id == edge_id.1 + } else { + false + } + })); play_tick(&players, &mut state); play_tick(&players, &mut state); // first player settlement 2 diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index ae929b10e..20381fe14 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -8,16 +8,17 @@ use crate::{ global_state::GlobalState, map_instance::{EdgeId, MapInstance, NodeId}, state_vector::{ - actual_victory_points_index, initialize_state, player_hand_slice, seating_order_slice, - StateVector, CURRENT_TICK_SEAT_INDEX, FREE_ROADS_AVAILABLE_INDEX, IS_DISCARDING_INDEX, - IS_INITIAL_BUILD_PHASE_INDEX, IS_MOVING_ROBBER_INDEX, + 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, }, }; -#[derive(Debug, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] enum Building { - Settlement(u8), // Color - City(u8), // Color + Settlement(u8, NodeId), // Color, NodeId + City(u8, NodeId), // Color, NodeId } #[derive(Debug)] @@ -32,8 +33,9 @@ pub(crate) struct State { // These are caches for speeding up game state calculations board_buildable_ids: HashSet, buildings: HashMap, - roads: HashMap, // (Node1, Node2) -> Color - roads_by_color: Vec, // Color -> Count + buildings_by_color: HashMap>, // Color -> Buildings + roads: HashMap, // (Node1, Node2) -> Color + roads_by_color: Vec, // Color -> Count connected_components: HashMap>>, longest_road_color: Option, longest_road_length: u8, @@ -48,6 +50,7 @@ impl State { let board_buildable_ids = map_instance.land_nodes().clone(); let buildings = HashMap::new(); + let buildings_by_color = HashMap::new(); let roads = HashMap::new(); let roads_by_color = vec![0; config.num_players as usize]; let connected_components = HashMap::new(); @@ -60,6 +63,7 @@ impl State { vector, board_buildable_ids, buildings, + buildings_by_color, roads, roads_by_color, connected_components, @@ -111,6 +115,12 @@ impl State { && self.vector[FREE_ROADS_AVAILABLE_INDEX] == 1 } + /// Returns a slice of Colors in the order of seating + /// e.g. [2, 1, 0, 3] if Orange goes first, then Blue, then Red, and then White + pub fn get_seating_order(&self) -> &[u8] { + &self.vector[seating_order_slice(self.config.num_players as usize)] + } + pub fn get_current_tick_seat(&self) -> u8 { self.vector[CURRENT_TICK_SEAT_INDEX] } @@ -121,10 +131,16 @@ impl State { seating_order[current_tick_seat as usize] } - /// Returns a slice of Colors in the order of seating - /// e.g. [2, 1, 0, 3] if Orange goes first, then Blue, then Red, and then White - pub fn get_seating_order(&self) -> &[u8] { - &self.vector[seating_order_slice(self.config.num_players as usize)] + pub fn current_player_rolled(&self) -> bool { + self.vector[HAS_ROLLED_INDEX] == 1 + } + + 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(color)][dev_card_index] > 0; + let has_played_in_turn = self.vector[HAS_PLAYED_DEV_CARD] == 1; + has_one && !has_played_in_turn } pub fn get_action_prompt(&self) -> ActionPrompt { @@ -145,6 +161,7 @@ impl State { ActionPrompt::PlayTurn } + // TODO: Maybe move to mutations(?) pub fn get_mut_player_hand(&mut self, color: u8) -> &mut [u8] { &mut self.vector[player_hand_slice(color)] } @@ -153,10 +170,6 @@ impl State { &self.vector[player_hand_slice(color)] } - pub fn get_actual_victory_points(&self, color: u8) -> u8 { - self.vector[actual_victory_points_index(self.config.num_players, color)] - } - pub fn winner(&self) -> Option { let current_color = self.get_current_color(); @@ -167,7 +180,35 @@ impl State { None } + pub fn get_actual_victory_points(&self, color: u8) -> u8 { + self.vector[actual_victory_points_index(self.config.num_players, color)] + } + // ===== Board Getters ===== + pub fn get_cities(&self, color: u8) -> Vec { + let buildings = self.buildings_by_color.get(&color); + match buildings { + Some(buildings) => buildings + .iter() + .filter(|building| matches!(building, Building::City(_, _))) + .cloned() + .collect(), + None => vec![], + } + } + + pub fn get_settlements(&self, color: u8) -> Vec { + let buildings = self.buildings_by_color.get(&color); + match buildings { + Some(buildings) => buildings + .iter() + .filter(|building| matches!(building, Building::Settlement(_, _))) + .cloned() + .collect(), + None => vec![], + } + } + // TODO: Potentially cache this implementation pub fn board_buildable_edges(&self, color: u8) -> Vec { let color_components = self.connected_components.get(&color).unwrap(); @@ -209,11 +250,19 @@ impl State { fn get_node_color(&self, a: u8) -> Option { match self.buildings.get(&a) { - Some(Building::Settlement(color)) => Some(*color), - Some(Building::City(color)) => Some(*color), + Some(Building::Settlement(color, a)) => Some(*color), + Some(Building::City(color, a)) => Some(*color), None => None, } } + + fn edge_contains(&self, edge: EdgeId, a: u8) -> bool { + let (node1, node2) = edge; + // println!("Checking if edge {:?} {:?} contains {:?}", node1, node2, a); + let result = node1 == a || node2 == a; + // println!("Result: {:?}", result); + result + } } #[cfg(test)] diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 419c2f622..779cf9c89 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -1,8 +1,11 @@ -use crate::deck_slices::{freqdeck_contains, ROAD_COST}; -use crate::enums::{Action, ActionPrompt}; +use crate::deck_slices::{freqdeck_contains, CITY_COST, ROAD_COST}; +use crate::enums::{Action, ActionPrompt, DevCard}; use crate::state::State; +use super::Building; + const TOTAL_ROADS_PER_PLAYER: u8 = 15; +const TOTAL_CITIES_PER_PLAYER: u8 = 4; impl State { pub fn generate_playable_actions(&self) -> Vec { @@ -12,8 +15,8 @@ impl State { ActionPrompt::BuildInitialSettlement => { self.settlement_possibilities(current_color, true) } - ActionPrompt::BuildInitialRoad => self.road_possibilities(current_color, true), - ActionPrompt::PlayTurn => todo!("generate_playbale_actions for PlayTurn"), + ActionPrompt::BuildInitialRoad => self.initial_road_possibilities(current_color), + ActionPrompt::PlayTurn => self.play_turn_possibilities(current_color), ActionPrompt::Discard => todo!("generate_playbale_actions for Discard"), ActionPrompt::MoveRobber => todo!("generate_playbale_actions for Move robber"), ActionPrompt::DecideTrade => todo!("generate_playbale_actions for Decide trade"), @@ -34,6 +37,20 @@ impl State { } } + pub fn initial_road_possibilities(&self, color: u8) -> Vec { + let last_settlement_building = self.buildings_by_color[&color].last().unwrap(); + let last_node_id = match last_settlement_building { + Building::Settlement(_, node_id) => *node_id, + _ => panic!("Invalid building type"), + }; + + self.board_buildable_edges(color) + .iter() + .filter(|edge_id| self.edge_contains(**edge_id, last_node_id)) + .map(|edge_id| Action::BuildRoad(color, *edge_id)) + .collect() + } + pub fn road_possibilities(&self, color: u8, is_free: bool) -> Vec { let has_roads_available = TOTAL_ROADS_PER_PLAYER - self.roads_by_color[color as usize]; if has_roads_available == 0 { @@ -49,6 +66,63 @@ impl State { vec![] } } + + pub fn city_possibilities(&self, color: u8) -> Vec { + let has_money = freqdeck_contains(self.get_player_hand(color), CITY_COST); + if !has_money { + return vec![]; + } + + let has_cities_available = self.get_cities(color).len() < TOTAL_CITIES_PER_PLAYER as usize; + if has_cities_available { + return vec![]; + } + + self.get_settlements(color) + .iter() + .map(|building| match building { + Building::Settlement(color, node_id) => Action::BuildCity(*color, *node_id), + _ => panic!("Invalid building type"), + }) + .collect() + } + + pub fn play_turn_possibilities(&self, color: u8) -> Vec { + if self.is_road_building() { + return self.road_possibilities(color, true); + } else if !self.current_player_rolled() { + let mut actions = vec![Action::Roll(color)]; + if self.can_play_dev(DevCard::Knight as u8) { + actions.push(Action::PlayKnight(color)); + } + return actions; + } + + let mut actions = vec![Action::EndTurn(color)]; + actions.extend(self.settlement_possibilities(color, false)); + actions.extend(self.road_possibilities(color, false)); + actions.extend(self.city_possibilities(color)); + + if self.can_play_dev(DevCard::Knight as u8) { + actions.push(Action::PlayKnight(color)); + } + if self.can_play_dev(DevCard::YearOfPlenty as u8) { + // TODO: + // actions.push(Action::PlayYearOfPlenty(color)); + } + if self.can_play_dev(DevCard::Monopoly as u8) { + // TOOD: + // actions.push(Action::PlayMonopoly(color)); + } + if self.can_play_dev(DevCard::RoadBuilding as u8) { + // TODO: What if user has no roads left? or is completely blocked? + actions.push(Action::PlayRoadBuilding(color)); + } + + // TODO: Maritime trade possibilities + + actions + } } #[cfg(test)] diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/mutations.rs index e8ef1470b..897a8f35f 100644 --- a/catanatron_rust/src/state/mutations.rs +++ b/catanatron_rust/src/state/mutations.rs @@ -29,7 +29,12 @@ impl State { } pub fn build_settlement(&mut self, color: u8, node_id: u8) { - self.buildings.insert(node_id, Building::Settlement(color)); + self.buildings + .insert(node_id, Building::Settlement(color, node_id)); + self.buildings_by_color + .entry(color) + .or_default() + .push(Building::Settlement(color, node_id)); let is_free = self.is_initial_build_phase(); if !is_free { @@ -174,7 +179,7 @@ mod tests { assert_eq!( state.buildings.get(&node_id), - Some(&Building::Settlement(color)) + Some(&Building::Settlement(color, node_id)) ); assert_eq!(state.board_buildable_ids.len(), 50); assert_eq!(state.get_actual_victory_points(color), 1); diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index 579509771..ee44de503 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -97,6 +97,10 @@ pub fn player_hand_slice(color: u8) -> std::ops::Range { let start = PLAYER_STATE_START_INDEX + 1 + (color as usize * 15); start..start + 5 } +pub fn player_devhand_slice(color: u8) -> std::ops::Range { + let start = PLAYER_STATE_START_INDEX + 6 + (color as usize * 15); + start..start + 5 +} /// This is a compact representation of the omnipotent state of the game. /// Fairly close to a bitboard, but not quite. Its a vector of integers. From 5f09779654e263f47c01c753b23fd58b86483814 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 23 Nov 2024 15:02:54 -0400 Subject: [PATCH 54/83] Fix some warnings --- catanatron_rust/src/state.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 20381fe14..3200f23ff 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -16,13 +16,13 @@ use crate::{ }; #[derive(Debug, Copy, Clone, PartialEq)] -enum Building { +pub enum Building { Settlement(u8, NodeId), // Color, NodeId City(u8, NodeId), // Color, NodeId } #[derive(Debug)] -pub(crate) struct State { +pub struct State { // These two are immutable config: Rc, map_instance: Rc, @@ -93,10 +93,6 @@ impl State { self.config.num_players } - fn get_num_players_usize(&self) -> usize { - self.get_num_players() as usize - } - // ===== Getters ===== pub fn is_initial_build_phase(&self) -> bool { self.vector[IS_INITIAL_BUILD_PHASE_INDEX] == 1 @@ -250,8 +246,8 @@ impl State { fn get_node_color(&self, a: u8) -> Option { match self.buildings.get(&a) { - Some(Building::Settlement(color, a)) => Some(*color), - Some(Building::City(color, a)) => Some(*color), + Some(Building::Settlement(color, _)) => Some(*color), + Some(Building::City(color, _)) => Some(*color), None => None, } } From 9c9216f45670b48a958e6ab59f97862a3422b67a Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 23 Nov 2024 15:05:17 -0400 Subject: [PATCH 55/83] Clippy Fix --- catanatron_rust/src/game.rs | 2 +- catanatron_rust/src/state.rs | 5 +---- catanatron_rust/src/state/mutations.rs | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 71fdc66a9..1ab1757a7 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -65,7 +65,7 @@ mod tests { dicard_limit: 7, vps_to_win: 10, map_type: MapType::Base, - num_players: num_players, + num_players, max_ticks: 8, // TODO: Change! }; let mut players: HashMap> = HashMap::new(); diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 3200f23ff..c3df05971 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -254,10 +254,7 @@ impl State { fn edge_contains(&self, edge: EdgeId, a: u8) -> bool { let (node1, node2) = edge; - // println!("Checking if edge {:?} {:?} contains {:?}", node1, node2, a); - let result = node1 == a || node2 == a; - // println!("Result: {:?}", result); - result + node1 == a || node2 == a } } diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/mutations.rs index 897a8f35f..2251482c7 100644 --- a/catanatron_rust/src/state/mutations.rs +++ b/catanatron_rust/src/state/mutations.rs @@ -119,7 +119,7 @@ impl State { .get_mut(a_index.unwrap()) .unwrap(); component.insert(b); // extend said component by 1 more node - } else if !a_index.is_none() && !b_index.is_none() && a_index != b_index { + } else if a_index.is_some() && b_index.is_some() && a_index != b_index { // Merge components into one and delete the other let a_component = self .connected_components From 581429cb7095aa1ca5f4a994803b43e45e6fc566 Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 23 Nov 2024 15:13:54 -0400 Subject: [PATCH 56/83] Rename mutations.rs into move_application.rs --- catanatron_rust/src/state.rs | 2 +- catanatron_rust/src/state/{mutations.rs => move_application.rs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename catanatron_rust/src/state/{mutations.rs => move_application.rs} (100%) diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index c3df05971..65d3a2f4d 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -41,8 +41,8 @@ pub struct State { longest_road_length: u8, } +mod move_application; mod move_generation; -mod mutations; impl State { pub fn new(config: Rc, map_instance: Rc) -> Self { diff --git a/catanatron_rust/src/state/mutations.rs b/catanatron_rust/src/state/move_application.rs similarity index 100% rename from catanatron_rust/src/state/mutations.rs rename to catanatron_rust/src/state/move_application.rs From 59d1550d558a809c61b37a1c57b5e9ff244ccb2d Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 23 Nov 2024 15:15:32 -0400 Subject: [PATCH 57/83] Reorder methods in State.move_application --- catanatron_rust/src/state/move_application.rs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 2251482c7..b204bcd12 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -14,6 +14,22 @@ use crate::{ use super::State; impl State { + pub fn apply_action(&mut self, action: Action) { + match action { + Action::BuildSettlement(color, node_id) => { + self.build_settlement(color, node_id); + } + Action::BuildRoad(color, edge_id) => { + self.build_road(color, edge_id); + } + _ => { + panic!("Action not implemented: {:?}", action); + } + } + + println!("Applying action {:?}", action); + } + pub fn add_victory_points(&mut self, color: u8, points: u8) { let n = self.get_num_players(); self.vector[actual_victory_points_index(n, color)] += points; @@ -144,22 +160,6 @@ impl State { // TODO: Return previous road } - - pub fn apply_action(&mut self, action: Action) { - match action { - Action::BuildSettlement(color, node_id) => { - self.build_settlement(color, node_id); - } - Action::BuildRoad(color, edge_id) => { - self.build_road(color, edge_id); - } - _ => { - panic!("Action not implemented: {:?}", action); - } - } - - println!("Applying action {:?}", action); - } } #[cfg(test)] From 6ae93a02f12aadce15c1b85b3c0d7ac422a54b7d Mon Sep 17 00:00:00 2001 From: Bryan Collazo Date: Sat, 23 Nov 2024 15:18:21 -0400 Subject: [PATCH 58/83] Add TODO's --- catanatron_rust/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/catanatron_rust/README.md b/catanatron_rust/README.md index 9f2a2ca78..e3a812519 100644 --- a/catanatron_rust/README.md +++ b/catanatron_rust/README.md @@ -77,3 +77,26 @@ Additional Responsabilities - Incrementally Mantain Longest Road (Color + Count) - Buildable Subgraph for quick Answering Buildable Edges - Connected Components (this is to, when plowing happens, be able to count by just max(len) pieces after updating data structure). Otherwise we would have to re-BFS all roads. + +# TODO: + +- Build tests for move_generation.rs +- Build tests for initial_build_phase +- Ensure Victory Points are counted correctly + + - Count Largest Army + - Count Longest Road + +- Implement move_application for Roll +- Implement move_application for MoveRobber +- Implement move_application for Discard +- Implement move_application for EndTurn + +- Implement move_application for BuildCity + +- Implement move_application for Buy DevelopmentCard +- Implement move_application for Play Knight +- Implement move_application for Play YOP +- Implement move_application for Play Mono +- Implement move_application for Play RB +- Implement move_application for MaritimeTrade From b646f5967c77d71322546cf2022cb47a1aea7382 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Fri, 27 Dec 2024 20:48:58 -0600 Subject: [PATCH 59/83] Settlement possibilities --- catanatron_rust/src/enums.rs | 4 +- catanatron_rust/src/game.rs | 2 +- catanatron_rust/src/main.rs | 2 +- catanatron_rust/src/map_instance.rs | 13 +- catanatron_rust/src/state.rs | 18 +- catanatron_rust/src/state/move_application.rs | 184 +++++++++++++++++- catanatron_rust/src/state/move_generation.rs | 129 +++++++++++- 7 files changed, 328 insertions(+), 24 deletions(-) diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index a6c23f52b..45a511a29 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -73,7 +73,7 @@ pub enum ActionPrompt { #[derive(Debug, Clone, Copy, PartialEq)] pub enum Action { // The first value in all these is the color of the player. - Roll(u8), // None. Log instead sets it to (int, int) rolled. + 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[]. BuildRoad(u8, EdgeId), @@ -106,7 +106,7 @@ pub enum MapType { // TODO: Make immutable and read-only #[derive(Debug)] pub struct GameConfiguration { - pub dicard_limit: u8, + pub discard_limit: u8, pub vps_to_win: u8, pub map_type: MapType, pub num_players: u8, diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 1ab1757a7..1131ff985 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -62,7 +62,7 @@ mod tests { ) -> (GlobalState, GameConfiguration, HashMap>) { let global_state = GlobalState::new(); let config = GameConfiguration { - dicard_limit: 7, + discard_limit: 7, vps_to_win: 10, map_type: MapType::Base, num_players, diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 06bb7bf98..a125eee75 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -74,7 +74,7 @@ fn main() { println!("Colors {:?}", COLORS); let config = GameConfiguration { - dicard_limit: 7, + discard_limit: 7, vps_to_win: 10, map_type: MapType::Base, num_players: 2, diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index a76e3c8b9..05097cde1 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -79,13 +79,13 @@ fn get_unit_vector(direction: Direction) -> (i8, i8, i8) { #[derive(Debug, Clone, PartialEq)] pub struct Hexagon { - // TODO: id? pub(crate) nodes: HashMap, pub(crate) edges: HashMap, } #[derive(Debug, Clone, PartialEq)] pub struct LandTile { + pub(crate) id: u8, pub(crate) hexagon: Hexagon, pub(crate) resource: Option, pub(crate) number: Option, @@ -93,6 +93,7 @@ pub struct LandTile { #[derive(Debug, Clone, PartialEq)] pub struct PortTile { + pub(crate) id: u8, pub(crate) hexagon: Hexagon, pub(crate) resource: Option, pub(crate) direction: Direction, @@ -185,6 +186,9 @@ impl MapInstance { let mut hexagons: HashMap = HashMap::new(); let mut tiles: HashMap = HashMap::new(); 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; @@ -194,6 +198,7 @@ impl MapInstance { let resource = shuffled_tiles.pop().unwrap(); if resource.is_none() { let land_tile = LandTile { + id: tile_autoinc, hexagon: hexagon.clone(), resource, number: None, @@ -202,12 +207,14 @@ impl MapInstance { } else { let number = shuffled_numbers.pop().unwrap(); let land_tile = LandTile { + id: tile_autoinc, hexagon: hexagon.clone(), resource, number: Some(number), }; tiles.insert(coordinate, Tile::Land(land_tile)); } + tile_autoinc += 1; } else if tile_slot == TileSlot::Water { let water_tile = WaterTile { hexagon: hexagon.clone(), @@ -225,11 +232,13 @@ impl MapInstance { }; let resource = shuffled_ports.pop().unwrap(); let port_tile = PortTile { + id: port_autoinc, hexagon: hexagon.clone(), resource, direction, }; tiles.insert(coordinate, Tile::Port(port_tile)); + port_autoinc += 1; } hexagons.insert(coordinate, hexagon); @@ -588,6 +597,7 @@ mod tests { assert_eq!( map_instance.tiles.get(&(0, 0, 0)), Some(&Tile::Land(LandTile { + id: 0, hexagon: Hexagon { nodes: HashMap::from([ (NodeRef::North, 0), @@ -613,6 +623,7 @@ mod tests { assert_eq!( map_instance.land_tiles.get(&(1, -1, 0)), Some(&LandTile { + id: 1, hexagon: Hexagon { nodes: HashMap::from([ (NodeRef::North, 6), diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 65d3a2f4d..c7f4ef775 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -75,7 +75,7 @@ impl State { pub fn new_base() -> Self { let global_state = GlobalState::new(); let config = GameConfiguration { - dicard_limit: 7, + discard_limit: 7, vps_to_win: 10, map_type: MapType::Base, num_players: 4, @@ -226,6 +226,22 @@ impl State { buildable.into_iter().collect() } + pub fn buildable_node_ids(&self, color: u8,) -> Vec { + let road_subgraphs = match self.connected_components.get(&color) { + Some(components) => components, + None => &vec![], + }; + + let mut road_connected_nodes: HashSet = HashSet::new(); + for component in road_subgraphs { + road_connected_nodes.extend(component); + } + + road_connected_nodes.intersection(&self.board_buildable_ids) + .copied() + .collect() + } + fn get_connected_component_index(&self, color: u8, a: u8) -> Option { let components = self.connected_components.get(&color).unwrap(); for (i, component) in components.iter().enumerate() { diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index b204bcd12..f86ca51a5 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -1,14 +1,13 @@ use std::collections::HashSet; +use rand::Rng; + use crate::{ - deck_slices::{freqdeck_add, freqdeck_sub, ROAD_COST, SETTLEMENT_COST}, + deck_slices::*, enums::Action, map_instance::EdgeId, state::Building, - state_vector::{ - actual_victory_points_index, BANK_RESOURCE_SLICE, CURRENT_TICK_SEAT_INDEX, - CURRENT_TURN_SEAT_INDEX, IS_INITIAL_BUILD_PHASE_INDEX, - }, + state_vector::*, }; use super::State; @@ -22,6 +21,18 @@ impl State { Action::BuildRoad(color, edge_id) => { self.build_road(color, edge_id); } + Action::BuildCity(color, node_id) =>{ + self.build_city(color, node_id); + } + Action::Roll(color, dice_opt) => { + self.roll_dice(color, dice_opt); + } + Action::Discard(color) => { + self.discard(color); + } + Action::MoveRobber(color, coord, victim_opt) => { + self.move_robber(color, coord, victim_opt); + } _ => { panic!("Action not implemented: {:?}", action); } @@ -40,7 +51,7 @@ impl State { let num_players = self.get_num_players() as i8; let next_index = ((self.get_current_tick_seat() as i8 + step_size + num_players) % num_players) as u8; - self.vector[CURRENT_TICK_SEAT_INDEX] = next_index; + self.vector[CURRENT_TURN_SEAT_INDEX] = next_index; } @@ -160,6 +171,94 @@ impl State { // TODO: Return previous road } + + fn build_city(&mut self, color: u8, node_id: u8) { + 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 Building::Settlement(_, n) = b { + *n == node_id + } else { + false + } + }) { + buildings.remove(pos); + } + freqdeck_sub(self.get_mut_player_hand(color), CITY_COST); + freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], CITY_COST); + self.add_victory_points(color, 1); + } + + fn roll_dice(&mut self, color: u8, dice_opt: Option<(u8, u8)>) { + self.vector[HAS_ROLLED_INDEX] = 1; + let (die1, die2) = dice_opt.unwrap_or_else(|| { + let mut rng = rand::thread_rng(); + (rng.gen_range(1..=6), rng.gen_range(1..=6)) + }); + 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 + }) + .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; + } + } else { + // TODO: Yield resources + self.vector[CURRENT_TICK_SEAT_INDEX] = color; + } + // TODO: Set playable_actions??? + } + + fn discard(&mut self, color: u8) { + todo!(); + } + + 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; + + if let Some(victim) = victim_opt { + let total_cards: u8 = self.get_player_hand(victim).iter().sum(); + + if total_cards > 0 { + // Randomly select card to steal + let mut rng = rand::thread_rng(); + let selected_idx = rng.gen_range(0..total_cards); + + let mut cumsum = 0; + let mut stolen_resource_idx = 0; + for (i, &count) in self.get_player_hand(victim).iter().enumerate() { + cumsum += count; + if selected_idx < cumsum { + stolen_resource_idx = i; + break; + } + } + + let mut stolen_freqdeck = [0; 5]; + stolen_freqdeck[stolen_resource_idx] = 1; + freqdeck_sub(self.get_mut_player_hand(victim), stolen_freqdeck); + freqdeck_add(self.get_mut_player_hand(color), stolen_freqdeck); + } + } + self.vector[IS_MOVING_ROBBER_INDEX] = 0; + } } #[cfg(test)] @@ -167,7 +266,7 @@ mod tests { use super::*; #[test] - fn test_build_settlement() { + fn test_build_settlement_initial_build_phase() { let mut state = State::new_base(); let color = state.get_current_color(); assert_eq!(state.buildings.get(&0), None); @@ -185,5 +284,74 @@ mod tests { assert_eq!(state.get_actual_victory_points(color), 1); } - // TODO: Assert build_settlement spends player resources + #[test] + fn test_build_settlement_spends_resources() { + let mut state = State::new_base(); + let color = state.get_current_color(); + assert_eq!(state.buildings.get(&0), None); + assert_eq!(state.board_buildable_ids.len(), 54); + assert_eq!(state.get_actual_victory_points(color), 0); + + // Exit initial build phase + state.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; + + freqdeck_add(state.get_mut_player_hand(color), SETTLEMENT_COST); + let hand_before = state.get_player_hand(color).to_vec(); + + let node_id = 0; + state.build_settlement(color, node_id); + + assert_eq!( + state.buildings.get(&node_id), + Some(&Building::Settlement(color, node_id)) + ); + 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]); + } + } + + #[test] + fn test_roll_seven_triggers_discard() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + { + let hand = state.get_mut_player_hand(color); + hand[0] = 8; // Give 8 wood cards + } + + state.roll_dice(color, Some((4, 3))); + + assert_eq!(state.vector[HAS_ROLLED_INDEX], 1); + assert_eq!(state.vector[IS_DISCARDING_INDEX], 1); + assert_eq!(state.vector[CURRENT_TICK_SEAT_INDEX], color); + assert_eq!(state.vector[IS_MOVING_ROBBER_INDEX], 0); + } + + #[test] + fn test_roll_seven_no_discard_needed() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + state.roll_dice(color, Some((4, 3))); + + assert_eq!(state.vector[HAS_ROLLED_INDEX], 1); + assert_eq!(state.vector[IS_DISCARDING_INDEX], 0); + assert_eq!(state.vector[CURRENT_TICK_SEAT_INDEX], color); + assert_eq!(state.vector[IS_MOVING_ROBBER_INDEX], 1); + } + + #[test] + fn test_roll_tracks_has_rolled() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + assert_eq!(state.vector[HAS_ROLLED_INDEX], 0); + state.roll_dice(color, Some((2, 3))); + assert_eq!(state.vector[HAS_ROLLED_INDEX], 1); + } } diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 779cf9c89..11e934850 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -1,4 +1,4 @@ -use crate::deck_slices::{freqdeck_contains, CITY_COST, ROAD_COST}; +use crate::deck_slices::{freqdeck_contains, CITY_COST, ROAD_COST, SETTLEMENT_COST}; use crate::enums::{Action, ActionPrompt, DevCard}; use crate::state::State; @@ -33,7 +33,18 @@ impl State { .map(|node_id| Action::BuildSettlement(color, *node_id)) .collect() } else { - panic!("Not implemented"); + let has_resources = freqdeck_contains(self.get_player_hand(color), SETTLEMENT_COST); + let settlements_used = self.get_settlements(color).len(); + let has_settlements_available = settlements_used < 5; + + if has_resources && has_settlements_available { + self.buildable_node_ids(color) + .into_iter() + .map(|node_id| Action::BuildSettlement(color, node_id)) + .collect() + } else { + vec![] + } } } @@ -74,7 +85,7 @@ impl State { } let has_cities_available = self.get_cities(color).len() < TOTAL_CITIES_PER_PLAYER as usize; - if has_cities_available { + if !has_cities_available { return vec![]; } @@ -91,7 +102,7 @@ impl State { if self.is_road_building() { return self.road_possibilities(color, true); } else if !self.current_player_rolled() { - let mut actions = vec![Action::Roll(color)]; + let mut actions = vec![Action::Roll(color, None)]; if self.can_play_dev(DevCard::Knight as u8) { actions.push(Action::PlayKnight(color)); } @@ -127,7 +138,8 @@ impl State { #[cfg(test)] mod tests { - use super::*; + use crate::enums::{Action, ActionPrompt}; + use crate::state::State; #[test] fn test_move_generation() { @@ -139,13 +151,110 @@ mod tests { #[test] fn test_settlement_possibilities() { - let state = State::new_base(); + let mut state = State::new_base(); + let color = state.get_current_color(); - let initial_build_phase_actions = state.settlement_possibilities(0, true); + // Test initial build phase + let initial_build_phase_actions = state.settlement_possibilities(color, true); assert_eq!(initial_build_phase_actions.len(), 54); - // TODO: Enable when implemented - // let actions = state.settlement_possibilities(0, false); - // assert_eq!(actions.len(), 3); + // Give player resources + let hand = state.get_mut_player_hand(color); + for resource in hand.iter_mut() { + *resource = 5; + } + + let action = Action::BuildSettlement(color, 0); + state.apply_action(action); + + let action = Action::BuildRoad(color, (0, 1)); + state.apply_action(action); + + let actions = state.settlement_possibilities(0, false); + assert_eq!(actions.len(), 0); + + let action = Action::BuildRoad(color, (1, 2)); + state.apply_action(action); + + // Should be able to build at node 2 + let actions = state.settlement_possibilities(color, false); + assert_eq!(actions.len(), 1); + assert!(actions.iter().any(|action| { + matches!(action, Action::BuildSettlement(c, node_id) if *c == color && *node_id == 2) + })); + } + + #[test] + fn test_initial_settlement_possibilities() { + let state = State::new_base(); + let curr_color = state.get_current_color(); + let actions = state.settlement_possibilities(curr_color, true); + assert_eq!(actions.len(), 54); // All nodes should be buildable + + for action in actions { + match action { + Action::BuildSettlement(c, _) => assert_eq!(c, curr_color), + _ => panic!("Expected BuildSettlement action"), + } + } + } + + #[test] + fn test_initial_road_possibilities() { + let mut state = State::new_base(); + let curr_color = state.get_current_color(); + + state.build_settlement(curr_color, 0); + + let actions = state.initial_road_possibilities(curr_color); + assert_eq!(actions.len(), 3); + + for action in actions { + match action { + Action::BuildRoad(c, edge) => { + assert_eq!(c, curr_color); + assert!(edge.0 == 0 || edge.1 == 0); + } + _ => panic!("Expected BuildRoad action"), + } + } + } + + #[test] + fn test_settlement_blocks_neighbors() { + let mut state = State::new_base(); + let curr_color = state.get_current_color(); + + state.build_settlement(curr_color, 0); + + let actions = state.settlement_possibilities(curr_color, true); + let neighbors = state.map_instance.get_neighbor_nodes(0); + + for action in actions { + match action { + Action::BuildSettlement(_, node_id) => { + assert_ne!(node_id, 0); + assert!(!neighbors.contains(&node_id)); + } + _ => panic!("Expected BuildSettlement action"), + } + } + } + + #[test] + fn test_play_turn_initial_possibilities() { + let mut state = State::new_base(); + + assert!(state.is_initial_build_phase()); + assert!(matches!(state.get_action_prompt(), ActionPrompt::BuildInitialSettlement)); + + let actions = state.generate_playable_actions(); + match &actions[0] { + Action::BuildSettlement(_, _) => (), + _ => panic!("Expected BuildSettlement action to be first action"), + } + state.apply_action(actions[0]); + + assert!(matches!(state.get_action_prompt(), ActionPrompt::BuildInitialRoad)); } } From c22b422a4a7f5a7c9680435e8c7727135cadde5d Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 28 Dec 2024 20:35:00 -0600 Subject: [PATCH 60/83] Fix player_hand_slice --- catanatron_rust/src/map_instance.rs | 4 ++ catanatron_rust/src/state.rs | 4 +- catanatron_rust/src/state/move_application.rs | 59 +++++++++++++++++-- catanatron_rust/src/state_vector.rs | 4 +- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 05097cde1..9d23eb0af 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -163,6 +163,10 @@ impl MapInstance { pub fn get_neighbor_edges(&self, node_id: NodeId) -> Vec { self.edge_neighbors.get(&node_id).unwrap().clone() } + + pub fn get_adjacent_tiles(&self, node_id: NodeId) -> Option<&Vec> { + self.adjacent_land_tiles.get(&node_id) + } } impl MapInstance { diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index c7f4ef775..987ae9025 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -159,11 +159,11 @@ impl State { // TODO: Maybe move to mutations(?) pub fn get_mut_player_hand(&mut self, color: u8) -> &mut [u8] { - &mut self.vector[player_hand_slice(color)] + &mut self.vector[player_hand_slice(self.config.num_players, color)] } pub fn get_player_hand(&self, color: u8) -> &[u8] { - &self.vector[player_hand_slice(color)] + &self.vector[player_hand_slice(self.config.num_players, color)] } pub fn winner(&self) -> Option { diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index f86ca51a5..710f112b5 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -71,11 +71,33 @@ impl State { self.add_victory_points(color, 1); - // TODO: If second house, yield resources - - // Maintain caches and longest road ===== - // - connected_components if self.is_initial_build_phase() { + let owned_buildings = self.buildings_by_color.get(&color).unwrap(); + let owned_settlements = owned_buildings + .iter() + .filter(|b| matches!(b, Building::Settlement(_, _))) + .count(); + + // If second house, yield resources + if owned_settlements == 2 { + let adjacent_tiles = self.map_instance.get_adjacent_tiles(node_id); + if let Some(adjacent_tiles) = adjacent_tiles { + let mut total_resources = [0; 5]; + for tile in adjacent_tiles { + if let Some(resource) = tile.resource { + total_resources[resource as usize] += 1; + } + } + + let bank = &mut self.vector[BANK_RESOURCE_SLICE]; + freqdeck_sub(bank, total_resources); + + let hand = self.get_mut_player_hand(color); + freqdeck_add(hand, total_resources); + } + } + // Maintain caches and longest road ===== + // - connected_components let component = HashSet::from([node_id]); self.connected_components .entry(color) @@ -354,4 +376,33 @@ mod tests { state.roll_dice(color, Some((2, 3))); assert_eq!(state.vector[HAS_ROLLED_INDEX], 1); } + + #[test] + fn test_second_settlement_yields_resources() { + let mut state = State::new_base(); + let color = state.get_current_color(); + let first_node = 0; + let bank_before = state.vector[BANK_RESOURCE_SLICE].to_vec(); + let hand_before = state.get_player_hand(color).to_vec(); + + state.build_settlement(color, first_node); + + assert_eq!(state.get_player_hand(color), hand_before); + assert_eq!(state.vector[BANK_RESOURCE_SLICE], bank_before); + + let second_node = 3; + let bank_before = state.vector[BANK_RESOURCE_SLICE].to_vec(); + let hand_before = state.get_player_hand(color).to_vec(); + + state.build_settlement(color, second_node); + + assert_ne!(state.get_player_hand(color), hand_before); + assert_ne!(state.vector[BANK_RESOURCE_SLICE], bank_before); + + for i in 0..5 { + let bank_diff = bank_before[i] - state.vector[BANK_RESOURCE_SLICE][i]; + let hand_diff = state.get_player_hand(color)[i] - hand_before[i]; + assert_eq!(bank_diff, hand_diff); + } + } } diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index ee44de503..707b16a73 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -93,8 +93,8 @@ pub const IS_BUILDING_ROAD_INDEX: usize = 38; pub const FREE_ROADS_AVAILABLE_INDEX: usize = 39; pub const ROBBER_TILE_INDEX: usize = 42; -pub fn player_hand_slice(color: u8) -> std::ops::Range { - let start = PLAYER_STATE_START_INDEX + 1 + (color as usize * 15); +pub fn player_hand_slice(num_players: u8, color: u8) -> std::ops::Range { + let start = PLAYER_STATE_START_INDEX + num_players as usize + 1 + (color as usize * 15); start..start + 5 } pub fn player_devhand_slice(color: u8) -> std::ops::Range { From 20e960f9bbd9381050010437d8a3c669d4e75153 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 4 Jan 2025 12:31:11 -0600 Subject: [PATCH 61/83] Maintain longest road --- catanatron_rust/src/map_instance.rs | 14 +- catanatron_rust/src/state.rs | 82 ++++ catanatron_rust/src/state/move_application.rs | 366 +++++++++++++++--- 3 files changed, 402 insertions(+), 60 deletions(-) diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 9d23eb0af..0e7401778 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -288,8 +288,18 @@ impl MapInstance { land_edges.insert(edge_id); node_neighbors.entry(edge_id.0).or_default().push(edge_id.1); node_neighbors.entry(edge_id.1).or_default().push(edge_id.0); - edge_neighbors.entry(edge_id.0).or_default().push(edge_id); - edge_neighbors.entry(edge_id.1).or_default().push(edge_id); + + // Only insert edge into edge_neighbors if not already present + { + let edges_for_node_0 = edge_neighbors.entry(edge_id.0).or_default(); + if !edges_for_node_0.contains(&edge_id) { + edges_for_node_0.push(edge_id); + } + let edges_for_node_1 = edge_neighbors.entry(edge_id.1).or_default(); + if !edges_for_node_1.contains(&edge_id) { + edges_for_node_1.push(edge_id); + } + } }); } else if let Tile::Port(port_tile) = tile { let (a_noderef, b_noderef) = get_noderefs_from_port_direction(port_tile.direction); diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 987ae9025..4369c496f 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -272,6 +272,66 @@ impl State { let (node1, node2) = edge; node1 == a || node2 == a } + + fn dfs_longest_path( + &self, + node: NodeId, + parent: Option, + connected_set: &HashSet, + color: u8, + current_path: &mut Vec, + best_path: &mut Vec, + ) { + // If current_path is longer than what we have, store it + if current_path.len() > best_path.len() { + *best_path = current_path.clone(); + } + + for &neighbor in &self.map_instance.get_neighbor_nodes(node) { + // Must be in the connected component + if !connected_set.contains(&neighbor) { + continue; + } + let edge = (node.min(neighbor), node.max(neighbor)); + + // Avoid going back to parent + if parent == Some(neighbor) { + continue; + } + // Skip roads not owned by us + if self.roads.get(&edge) != Some(&color) { + continue; + } + // Acyclic check + if current_path.contains(&edge) { + continue; + } + + // Move forward + current_path.push(edge); + 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 { + if connected_node_set.is_empty() { + return vec![]; + } + + let mut overall_best_path = Vec::new(); + + for &start_node in connected_node_set { + 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); + if best_path.len() > overall_best_path.len() { + overall_best_path = best_path; + } + } + overall_best_path + } } #[cfg(test)] @@ -293,4 +353,26 @@ mod tests { assert!(!state.is_moving_robber()); assert!(!state.is_discarding()); } + + #[test] + fn test_longest_acyclic_path() { + let mut state = State::new_base(); + let color = 0; + + state.roads.insert((0, 1), color); + state.roads.insert((1, 2), color); + state.roads.insert((2, 3), color); + state.roads.insert((3, 4), color); + state.roads.insert((4, 5), color); + state.roads.insert((0, 5), color); + state.roads.insert((0, 20), color); + state.roads.insert((20, 19), color); + state.roads.insert((20, 22), color); + state.roads.insert((22, 23), color); + state.roads.insert((6, 23), color); + + let all_nodes = HashSet::from([0, 1, 2, 3, 4, 5, 19, 20, 22, 23, 6]); + let path = state.longest_acyclic_path(&all_nodes, color); + assert_eq!(path.len(), 10); + } } diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 710f112b5..f71725017 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -1,11 +1,11 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use rand::Rng; use crate::{ deck_slices::*, enums::Action, - map_instance::EdgeId, + map_instance::{EdgeId, NodeId}, state::Building, state_vector::*, }; @@ -16,10 +16,14 @@ impl State { pub fn apply_action(&mut self, action: Action) { match action { Action::BuildSettlement(color, node_id) => { - 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) => { - 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) =>{ self.build_city(color, node_id); @@ -46,6 +50,11 @@ impl State { self.vector[actual_victory_points_index(n, color)] += points; } + pub fn sub_victory_points(&mut self, color: u8, points: u8) { + let n = self.get_num_players(); + self.vector[actual_victory_points_index(n, color)] -= points; + } + pub fn advance_turn(&mut self, step_size: i8) { // We add an extra num_players to ensure next_index is positive (u8) let num_players = self.get_num_players() as i8; @@ -53,26 +62,29 @@ impl State { ((self.get_current_tick_seat() as i8 + step_size + num_players) % num_players) as u8; self.vector[CURRENT_TURN_SEAT_INDEX] = next_index; + self.vector[CURRENT_TICK_SEAT_INDEX] = next_index; } - pub fn build_settlement(&mut self, color: u8, node_id: u8) { + pub fn build_settlement(&mut self, placing_color: u8, node_id: u8) -> (Option, u8) { self.buildings - .insert(node_id, Building::Settlement(color, node_id)); + .insert(node_id, Building::Settlement(placing_color, node_id)); self.buildings_by_color - .entry(color) + .entry(placing_color) .or_default() - .push(Building::Settlement(color, node_id)); + .push(Building::Settlement(placing_color, node_id)); let is_free = self.is_initial_build_phase(); if !is_free { - freqdeck_sub(self.get_mut_player_hand(color), SETTLEMENT_COST); + freqdeck_sub(self.get_mut_player_hand(placing_color), SETTLEMENT_COST); freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], SETTLEMENT_COST); } - self.add_victory_points(color, 1); + self.add_victory_points(placing_color, 1); - if self.is_initial_build_phase() { - let owned_buildings = self.buildings_by_color.get(&color).unwrap(); + let mut road_lengths: HashMap = HashMap::new(); + + if is_free { + let owned_buildings = self.buildings_by_color.get(&placing_color).unwrap(); let owned_settlements = owned_buildings .iter() .filter(|b| matches!(b, Building::Settlement(_, _))) @@ -92,7 +104,7 @@ impl State { let bank = &mut self.vector[BANK_RESOURCE_SLICE]; freqdeck_sub(bank, total_resources); - let hand = self.get_mut_player_hand(color); + let hand = self.get_mut_player_hand(placing_color); freqdeck_add(hand, total_resources); } } @@ -100,30 +112,88 @@ impl State { // - connected_components let component = HashSet::from([node_id]); self.connected_components - .entry(color) + .entry(placing_color) .or_default() .push(component); } else { - // TODO: Mantain connected_components - // TODO: Mantain longest_road_color and longest_road_length (maybe swapping vps) - todo!(); + // Mantain connected_components + // Mantain longest_road_color and longest_road_length + 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); + } + } + + for (plowed_color, plowed_edges) in plowed_edges_by_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() + .map(|&edge| if edge.0 == node_id { edge.1 } else { edge.0 }) + .collect(); + + // First remove the bisected component + let road_components = self.connected_components.get_mut(&plowed_color).unwrap(); + road_components.remove(plowed_component_idx); + + let mut new_components = Vec::new(); + for outer_node in outer_nodes { + let new_component = self.dfs_walk(outer_node, plowed_color); + if !new_component.is_empty() { + new_components.push(new_component); + } + } + + let road_components = self.connected_components.get_mut(&plowed_color).unwrap(); + road_components.extend(new_components); + } + + // 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() + .map(|component| self.longest_acyclic_path(component, color).len()) + .max() + .unwrap_or(0); + road_lengths.insert(color, max_length as u8); + } + } } // - board_buildable_ids self.board_buildable_ids.remove(&node_id); for neighbor_id in self.map_instance.get_neighbor_nodes(node_id) { self.board_buildable_ids.remove(&neighbor_id); } + + // Determine new longest road holder + let (new_road_color, new_road_length) = if road_lengths.is_empty() { + // 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); + + match max_entry { + Some((&color, &length)) => (Some(color), length), + None => (None, 0) // No player has >= 5 roads + } + }; + (new_road_color, new_road_length) } - fn build_road(&mut self, color: u8, edge_id: EdgeId) { + fn build_road(&mut self, placing_color: u8, edge_id: EdgeId) -> (Option, u8) { let inverted_edge = (edge_id.1, edge_id.0); - self.roads.insert(edge_id, color); - self.roads.insert(inverted_edge, color); - + 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 { - freqdeck_sub(self.get_mut_player_hand(color), ROAD_COST); + freqdeck_sub(self.get_mut_player_hand(placing_color), ROAD_COST); freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], ROAD_COST); } @@ -148,50 +218,51 @@ impl State { // Maintain caches and longest road ===== // Extend or merge components let (a, b) = edge_id; - let a_index = self.get_connected_component_index(color, a); - let b_index = self.get_connected_component_index(color, b); - if a_index.is_none() && !self.is_enemy_node(color, a) { + let a_index = self.get_connected_component_index(placing_color, a); + let b_index = self.get_connected_component_index(placing_color, b); + + 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(&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 - } else if b_index.is_none() && !self.is_enemy_node(color, b) { + 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(&color) - .unwrap() - .get_mut(a_index.unwrap()) - .unwrap(); - component.insert(b); // extend said component by 1 more node + 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 a_component = self - .connected_components - .get_mut(&color) - .unwrap() - .remove(a_index.unwrap()); - let b_component = self - .connected_components - .get_mut(&color) - .unwrap() - .remove(b_index.unwrap()); - let mut new_component = a_component.clone(); - new_component.extend(b_component); - self.connected_components - .get_mut(&color) - .unwrap() - .push(new_component); + 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() + .remove(larger_idx); + 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. - } - - // TODO: Return previous road + 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{ + (Some(placing_color), path_length) + } else { + (prev_road_color, self.longest_road_length) + }; + (new_road_color, new_road_length) } fn build_city(&mut self, color: u8, node_id: u8) { @@ -281,6 +352,48 @@ impl State { } self.vector[IS_MOVING_ROBBER_INDEX] = 0; } + + fn maintain_longest_road(&mut self, new_owner: Option, new_length: u8) { + let prev_owner = self.longest_road_color; + self.longest_road_color = new_owner; + self.longest_road_length = new_length; + + if new_owner == prev_owner { + return; + } + + if let Some(prev_owner) = prev_owner { + self.sub_victory_points(prev_owner, 2); + } + + if let Some(new_owner) = new_owner { + self.add_victory_points(new_owner, 2); + } + } + + fn dfs_walk(&self, start_node: NodeId, color: u8) -> HashSet { + let mut agenda = vec![start_node]; + let mut visited = HashSet::new(); + + while let Some(node) = agenda.pop() { + if visited.contains(&node) { + continue; + } + visited.insert(node); + + if self.is_enemy_node(color, node) { + continue; + } + + for neighbor in self.map_instance.get_neighbor_nodes(node) { + let edge = (node.min(neighbor), node.max(neighbor)); + if self.roads.get(&edge) == Some(&color) { + agenda.push(neighbor); + } + } + } + visited + } } #[cfg(test)] @@ -405,4 +518,141 @@ mod tests { assert_eq!(bank_diff, hand_diff); } } + + #[test] + fn test_settlement_cuts_longest_road() { + let mut state = State::new_base(); + let color1 = 1; + let color2 = 2; + + // 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)] { + state.apply_action(Action::BuildRoad(color1, edge)); + } + + assert_eq!(state.longest_road_color, Some(color1)); + assert_eq!(state.get_actual_victory_points(color1), 3); + assert_eq!(state.get_actual_victory_points(color2), 0); + + // Give color2 a settlement at node 4 to bisect color1's Longest Road + state.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; + freqdeck_add(state.get_mut_player_hand(color2), SETTLEMENT_COST); + state.apply_action(Action::BuildSettlement(color2, 4)); + + assert_eq!(state.longest_road_color, None); + assert_eq!(state.get_actual_victory_points(color1), 1); + assert_eq!(state.get_actual_victory_points(color2), 1); + } + + #[test] + fn test_build_road_maintains_connected_components() { + let mut state = State::new_base(); + let color1 = 1; + + state.build_settlement(color1, 0); + state.build_road(color1, (0, 1)); + + let components = state.connected_components.get(&color1).unwrap(); + assert_eq!(components.len(), 1); + assert_eq!(components[0], HashSet::from([0, 1])); + + state.build_road(color1, (1, 2)); + + let components = state.connected_components.get(&color1).unwrap(); + assert_eq!(components.len(), 1); + assert_eq!(components[0], HashSet::from([0, 1, 2])); + + state.build_settlement(color1, 4); + state.build_road(color1, (3, 4)); + + let components = state.connected_components.get(&color1).unwrap(); + assert_eq!(components.len(), 2); + assert_eq!(components[0], HashSet::from([0, 1, 2])); + assert_eq!(components[1], HashSet::from([3, 4])); + + state.build_road(color1, (2, 3)); + + let components = state.connected_components.get(&color1).unwrap(); + assert_eq!(components.len(), 1); + assert_eq!(components[0], HashSet::from([0, 1, 2, 3, 4])); + } + + #[test] + fn test_settlement_cuts_longest_road_and_transfers() { + let mut state = State::new_base(); + let color1 = 1; + let color2 = 2; + + // 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)] { + 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)] { + state.apply_action(Action::BuildRoad(color2, edge)); + } + + assert_eq!(state.longest_road_color, Some(color1)); + assert_eq!(state.get_actual_victory_points(color1), 3); + assert_eq!(state.get_actual_victory_points(color2), 1); + + // Give color2 a settlement at node 4 to bisect color1's Longest Road + state.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; + freqdeck_add(state.get_mut_player_hand(color2), SETTLEMENT_COST); + state.apply_action(Action::BuildSettlement(color2, 4)); + + assert_eq!(state.longest_road_color, Some(color2)); + assert_eq!(state.get_actual_victory_points(color1), 1); + assert_eq!(state.get_actual_victory_points(color2), 4); + } + + #[test] + 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)] { + state.apply_action(Action::BuildRoad(color1, edge)); + } + + assert_eq!(state.longest_road_color, Some(color1)); + assert_eq!(state.longest_road_length, 5); + assert_eq!(state.get_actual_victory_points(color1), 3); + + state.apply_action(Action::BuildRoad(color1, (5, 16))); + + assert_eq!(state.longest_road_color, Some(color1)); + assert_eq!(state.longest_road_length, 6); + assert_eq!(state.get_actual_victory_points(color1), 3); + } + + #[test] + fn test_bisection_counts_remaining_components() { + let mut state = State::new_base(); + let color1 = 1; + 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)] { + state.apply_action(Action::BuildRoad(color1, edge)); + } + + assert_eq!(state.longest_road_color, Some(color1)); + assert_eq!(state.longest_road_length, 6); + assert_eq!(state.get_actual_victory_points(color1), 3); + + state.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; + freqdeck_add(state.get_mut_player_hand(color2), SETTLEMENT_COST); + state.apply_action(Action::BuildSettlement(color2, 5)); + + assert_eq!(state.longest_road_color, Some(color1)); + assert_eq!(state.longest_road_length, 5); + assert_eq!(state.connected_components.get(&color1).unwrap().len(), 2); + assert_eq!(state.get_actual_victory_points(color1), 3); + assert_eq!(state.get_actual_victory_points(color2), 1); + } } From d9540625f6e69300626a4f3e052913dd52e75cd7 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sun, 5 Jan 2025 11:57:05 -0600 Subject: [PATCH 62/83] Buy Dev Cards --- catanatron_rust/src/state.rs | 10 ++- catanatron_rust/src/state/move_application.rs | 82 ++++++++++++++++++- catanatron_rust/src/state_vector.rs | 15 +++- 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 4369c496f..597217a26 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -134,7 +134,7 @@ 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(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 } @@ -166,6 +166,14 @@ impl State { &self.vector[player_hand_slice(self.config.num_players, color)] } + pub fn get_mut_player_devhand(&mut self, color: u8) -> &mut [u8] { + &mut self.vector[player_devhand_slice(self.config.num_players, color)] + } + + pub fn get_player_devhand(&self, color: u8) -> &[u8] { + &self.vector[player_devhand_slice(self.config.num_players, color)] + } + pub fn winner(&self) -> Option { let current_color = self.get_current_color(); diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index f71725017..870c4a5e8 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -4,7 +4,7 @@ use rand::Rng; use crate::{ deck_slices::*, - enums::Action, + enums::{Action, DevCard}, map_instance::{EdgeId, NodeId}, state::Building, state_vector::*, @@ -28,6 +28,9 @@ impl State { Action::BuildCity(color, node_id) =>{ self.build_city(color, node_id); } + Action::BuyDevelopmentCard(color) => { + self.buy_development_card(color); + } Action::Roll(color, dice_opt) => { self.roll_dice(color, dice_opt); } @@ -282,6 +285,38 @@ impl State { self.add_victory_points(color, 1); } + fn buy_development_card(&mut self, color: u8) -> Option { + // Get next card from deck + if let Some(card) = take_next_dev_card(&mut self.vector) { + // Pay for the card + freqdeck_sub(self.get_mut_player_hand(color), DEVCARD_COST); + freqdeck_add(&mut self.vector[BANK_RESOURCE_SLICE], DEVCARD_COST); + + let dev_card = match card { + 0 => DevCard::Knight, + 1 => DevCard::YearOfPlenty, + 2 => DevCard::Monopoly, + 3 => DevCard::RoadBuilding, + 4 => DevCard::VictoryPoint, + _ => panic!("Invalid dev card index"), + }; + + match dev_card { + DevCard::VictoryPoint => { + self.add_victory_points(color, 1); + } + _ => { + let dev_hand = &mut self.vector[player_devhand_slice(self.config.num_players, color)]; + dev_hand[card as usize] += 1; + } + } + + Some(dev_card) + } else { + None + } + } + fn roll_dice(&mut self, color: u8, dice_opt: Option<(u8, u8)>) { self.vector[HAS_ROLLED_INDEX] = 1; let (die1, die2) = dice_opt.unwrap_or_else(|| { @@ -655,4 +690,49 @@ mod tests { assert_eq!(state.get_actual_victory_points(color1), 3); assert_eq!(state.get_actual_victory_points(color2), 1); } + + #[test] + fn test_buy_development_cards() { + let mut state = State::new_base(); + let color = state.get_current_color(); + let mut cards_drawn = 0; + + while cards_drawn < 26 { + freqdeck_add(state.get_mut_player_hand(color), DEVCARD_COST); + let initial_hand: [u8; 5] = state.get_player_hand(color).try_into().unwrap(); + let initial_devhand = state.get_player_devhand(color).to_vec(); + let initial_bank = state.vector[BANK_RESOURCE_SLICE].to_vec(); + let initial_vps = state.get_actual_victory_points(color); + + let drawn_card = state.buy_development_card(color); + cards_drawn += 1; + + println!("Cards Drawn: {}, Drawn card: {:?}", cards_drawn, drawn_card); + + if cards_drawn < 26 { + let hand_after = state.get_player_hand(color); + let bank_after = &state.vector[BANK_RESOURCE_SLICE]; + for i in 0..5 { + assert_eq!(hand_after[i], initial_hand[i] - DEVCARD_COST[i]); + assert_eq!(bank_after[i], initial_bank[i] + DEVCARD_COST[i]); + } + let devhand_after = state.get_player_devhand(color); + + 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]); + } 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); + } + } else { + // 26th card should not be drawn + assert!(drawn_card.is_none()); + assert_eq!(state.get_player_hand(color), initial_hand); + assert_eq!(&state.vector[BANK_RESOURCE_SLICE], initial_bank); + } + } + } } diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index 707b16a73..fe80a6555 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -97,11 +97,22 @@ pub fn player_hand_slice(num_players: u8, color: u8) -> std::ops::Range { let start = PLAYER_STATE_START_INDEX + num_players as usize + 1 + (color as usize * 15); start..start + 5 } -pub fn player_devhand_slice(color: u8) -> std::ops::Range { - let start = PLAYER_STATE_START_INDEX + 6 + (color as usize * 15); +pub fn player_devhand_slice(num_players: u8, color: u8) -> std::ops::Range { + let start = PLAYER_STATE_START_INDEX + num_players as usize + 6 + (color as usize * 15); start..start + 5 } +// 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; + if ptr >= 25 { + return None; + } + let card = vector[5 + ptr]; + vector[DEV_BANK_PTR_INDEX] += 1; + Some(card) +} + /// This is a compact representation of the omnipotent state of the game. /// Fairly close to a bitboard, but not quite. Its a vector of integers. /// From d0893e4adc3a343c732fe3c07dc09078f7dfb36c Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sun, 12 Jan 2025 10:51:03 -0600 Subject: [PATCH 63/83] cargo fmt --- catanatron_rust/src/enums.rs | 2 +- catanatron_rust/src/map_instance.rs | 2 +- catanatron_rust/src/state.rs | 34 ++++- catanatron_rust/src/state/move_application.rs | 130 +++++++++++------- catanatron_rust/src/state/move_generation.rs | 12 +- 5 files changed, 119 insertions(+), 61 deletions(-) diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 45a511a29..1c12d772a 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -75,7 +75,7 @@ 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), diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 0e7401778..e027bb64a 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -192,7 +192,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 597217a26..335b0b99d 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -134,7 +134,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 +235,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 +246,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 +319,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,7 +346,14 @@ 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; } @@ -361,7 +381,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 870c4a5e8..c7f1bfc7a 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) => { @@ -124,17 +122,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 +160,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 +180,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 +198,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 +232,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 +296,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 +334,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; } } @@ -331,9 +360,9 @@ impl State { let player_hand = self.get_player_hand(c); let total_cards: u8 = player_hand.iter().sum(); total_cards > self.config.discard_limit - }) - .collect(); - + }) + .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) { @@ -356,10 +385,7 @@ impl State { } 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(); @@ -477,7 +503,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 +588,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 +647,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 +674,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 +698,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 +747,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 diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 11e934850..ae031a429 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 + )); } } From 640fe4eeea104310d81cd1f93b9b0d07f4f8941a Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sun, 19 Jan 2025 11:32:42 -0600 Subject: [PATCH 64/83] roll_dice yields --- catanatron_rust/src/map_instance.rs | 7 + catanatron_rust/src/state/move_application.rs | 144 +++++++++++++++++- 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index e027bb64a..06b60b5ee 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 { diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index c7f1bfc7a..92238184a 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -355,6 +355,7 @@ impl State { let total = die1 + die2; if total == 7 { + // Discard phase let discarders: Vec = (0..self.get_num_players()) .map(|c| { let player_hand = self.get_player_hand(c); @@ -374,10 +375,42 @@ impl State { self.vector[CURRENT_TICK_SEAT_INDEX] = color; } } else { - // TODO: Yield resources + // First collect all yields we need to do + let mut all_yields = Vec::new(); + + { + let matching_tiles = self.map_instance.get_tiles_by_number(total); + 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)); + } + } + } + } + } + } + } + // TODO: Handle if bank is empty + for (owner_color, resource_idx, amount) in all_yields { + self.vector[BANK_RESOURCE_SLICE][resource_idx] -= amount; + self.get_mut_player_hand(owner_color)[resource_idx] += amount; + } self.vector[CURRENT_TICK_SEAT_INDEX] = color; } - // TODO: Set playable_actions??? } fn discard(&mut self, color: u8) { @@ -767,4 +800,111 @@ 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 + ); + } + } } From 8523bc3fbd2567939c1f49a520e40e596bc11915 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Mon, 20 Jan 2025 09:04:15 -0600 Subject: [PATCH 65/83] Dice yields with insufficient bank --- catanatron_rust/src/state/move_application.rs | 251 +++++++++++++++--- 1 file changed, 208 insertions(+), 43 deletions(-) diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 92238184a..57748c050 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -355,61 +355,116 @@ impl State { let total = die1 + die2; if total == 7 { - // Discard phase - 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(); + self.handle_roll_seven(color); + } else { + self.distribute_roll_yields(total); + 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 { - self.vector[IS_MOVING_ROBBER_INDEX] = 1; - 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 { - // First collect all yields we need to do - let mut all_yields = Vec::new(); - - { - let matching_tiles = self.map_instance.get_tiles_by_number(total); - for tile in matching_tiles { - // Skip robber tile - if self.vector[ROBBER_TILE_INDEX] == tile.id { - continue; - } + self.vector[IS_MOVING_ROBBER_INDEX] = 1; + self.vector[CURRENT_TICK_SEAT_INDEX] = color; + } + } - 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)); - } - } + // 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)); } } } } } - // TODO: Handle if bank is empty - for (owner_color, resource_idx, amount) in all_yields { + } + 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; } - self.vector[CURRENT_TICK_SEAT_INDEX] = color; } } @@ -907,4 +962,114 @@ mod tests { ); } } + + #[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; + let mut chosen_tile_id = 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); + chosen_tile_id = Some(tile.id); + } + } + } + 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" + ); + } } From df6efea29e8a6c64bbc8645a13b5deb914e1e0db Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Mon, 20 Jan 2025 09:44:01 -0600 Subject: [PATCH 66/83] Discard --- catanatron_rust/src/state/move_application.rs | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 57748c050..99bfcf3f2 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -468,8 +468,36 @@ impl State { } } + /* + * 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) { @@ -1072,4 +1100,46 @@ mod tests { "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." + ); + } } From 042bcd5866464c119aa4b84f20143953440d69f1 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Mon, 20 Jan 2025 09:47:30 -0600 Subject: [PATCH 67/83] Unused var --- catanatron_rust/src/state/move_application.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 99bfcf3f2..e4d332f47 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -1005,14 +1005,12 @@ mod tests { let mut chosen_roll = None; let mut chosen_resource = None; - let mut chosen_tile_id = 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); - chosen_tile_id = Some(tile.id); } } } From bc679dd332e3e826f3f68bd7e22b6d2d3016992f Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Mon, 20 Jan 2025 17:02:14 -0600 Subject: [PATCH 68/83] Play Dev Cards --- catanatron_rust/src/enums.rs | 4 +- catanatron_rust/src/state.rs | 70 +++- catanatron_rust/src/state/move_application.rs | 321 ++++++++++++++++++ catanatron_rust/src/state_vector.rs | 5 + 4 files changed, 395 insertions(+), 5 deletions(-) diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 1c12d772a..15bfc1534 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -81,8 +81,8 @@ pub enum Action { 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. diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 335b0b99d..61800fe96 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, } } @@ -360,6 +366,64 @@ impl State { } 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 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)] diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index e4d332f47..8bb44c2dd 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -38,6 +38,19 @@ 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); + } _ => { panic!("Action not implemented: {:?}", action); } @@ -571,6 +584,95 @@ 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; + } } #[cfg(test)] @@ -1140,4 +1242,223 @@ mod tests { "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); + } } diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index fe80a6555..c90c1755d 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; From 15fe2fbe231eca7c948161381d9a9185b2189b2a Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Mon, 20 Jan 2025 17:39:03 -0600 Subject: [PATCH 69/83] Maritime trade --- catanatron_rust/src/enums.rs | 2 +- catanatron_rust/src/state.rs | 6 +++++ catanatron_rust/src/state/move_application.rs | 27 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 15bfc1534..2fd138711 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -86,7 +86,7 @@ pub enum Action { 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/state.rs b/catanatron_rust/src/state.rs index 61800fe96..8c6ecc8d6 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -409,6 +409,12 @@ impl State { 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] } diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 8bb44c2dd..fffc5c72c 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -51,6 +51,9 @@ impl State { Action::PlayRoadBuilding(color) => { self.play_road_building(color); } + Action::MaritimeTrade(color, (give, take, ratio)) => { + self.maritime_trade(color, give, take, ratio); + } _ => { panic!("Action not implemented: {:?}", action); } @@ -673,6 +676,13 @@ impl State { 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); + } } #[cfg(test)] @@ -1461,4 +1471,21 @@ mod tests { 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); + } } From 6ce7c12f0e2a9eaca2cff88718f470d011805177 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Mon, 20 Jan 2025 18:12:27 -0600 Subject: [PATCH 70/83] End Turn --- catanatron_rust/src/state/move_application.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index fffc5c72c..9f2f38e33 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -54,6 +54,9 @@ impl State { 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); } @@ -683,6 +686,13 @@ impl State { 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)] @@ -1488,4 +1498,26 @@ mod tests { 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); + } } From f1e5ecaf9b799e60fd8d2c62358a051323e10722 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 25 Jan 2025 11:00:15 -0600 Subject: [PATCH 71/83] Refactor struct enums --- catanatron_rust/src/enums.rs | 90 +++++++++--- catanatron_rust/src/game.rs | 16 ++- catanatron_rust/src/state/move_application.rs | 136 +++++++++++++----- catanatron_rust/src/state/move_generation.rs | 67 ++++++--- 4 files changed, 223 insertions(+), 86 deletions(-) diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 2fd138711..4a3d6e1a0 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -72,28 +72,74 @@ pub enum ActionPrompt { #[derive(Debug, Clone, Copy, PartialEq)] 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[]. - BuildRoad(u8, EdgeId), - BuildSettlement(u8, NodeId), - BuildCity(u8, NodeId), - BuyDevelopmentCard(u8), // value is None. Log value is card. - PlayKnight(u8), - 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, (u8, u8, u8)), // (Give Resource, Get Resource, Ratio) - OfferTrade(u8, (FreqDeck, FreqDeck)), - AcceptTrade(u8, (FreqDeck, FreqDeck)), - RejectTrade(u8), - ConfirmTrade(u8, (FreqDeck, FreqDeck, u8)), // 11-tuple. First 10 like OfferTrade, last is color of accepting player. - CancelTrade(u8), - - EndTurn(u8), // None + Roll { + color: u8, + dice_opt: Option<(u8, u8)>, + }, + MoveRobber { + color: u8, + coordinate: Coordinate, + victim_opt: Option, + }, + Discard { + color: u8, + }, + BuildRoad { + color: u8, + edge_id: EdgeId, + }, + BuildSettlement { + color: u8, + node_id: NodeId, + }, + BuildCity { + color: u8, + node_id: NodeId, + }, + BuyDevelopmentCard { + color: u8, + }, + PlayKnight { + color: u8, + }, + PlayYearOfPlenty { + color: u8, + resources: [u8; 2], + }, + PlayMonopoly { + color: u8, + resource: u8, + }, + PlayRoadBuilding { + color: u8, + }, + MaritimeTrade { + color: u8, + give: u8, + take: u8, + ratio: u8, + }, + OfferTrade { + color: u8, + trade: (FreqDeck, FreqDeck), + }, + AcceptTrade { + color: u8, + trade: (FreqDeck, FreqDeck), + }, + RejectTrade { + color: u8, + }, + ConfirmTrade { + color: u8, + trade: (FreqDeck, FreqDeck, u8), + }, + CancelTrade { + color: u8, + }, + EndTurn { + color: u8, + }, } #[derive(Debug)] diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 1131ff985..9b114d7f1 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -147,7 +147,11 @@ mod tests { play_tick(&players, &mut state); // third player road let second_player_second_settlement_action = play_tick(&players, &mut state); let second_player_second_node_id; - if let Action::BuildSettlement(player, node_id) = second_player_second_settlement_action { + if let Action::BuildSettlement { + color: player, + node_id, + } = second_player_second_settlement_action + { assert_eq!(player, second_player); second_player_second_node_id = node_id; } else { @@ -160,7 +164,7 @@ mod tests { assert_all_build_roads(playable_actions.clone(), second_player); // assert playable_actions are connected to the last settlement assert!(playable_actions.iter().all(|e| { - if let Action::BuildRoad(_, edge_id) = e { + if let Action::BuildRoad { edge_id, .. } = e { second_player_second_node_id == edge_id.0 || second_player_second_node_id == edge_id.1 } else { @@ -208,8 +212,8 @@ mod tests { fn assert_all_build_settlements(playable_actions: Vec, player: u8) { assert!( playable_actions.iter().all(|e| { - if let Action::BuildSettlement(p, _) = e { - *p == player + if let Action::BuildSettlement { color, .. } = e { + *color == player } else { false } @@ -222,8 +226,8 @@ mod tests { fn assert_all_build_roads(playable_actions: Vec, player: u8) { assert!( playable_actions.iter().all(|e| { - if let Action::BuildRoad(p, _) = e { - *p == player + if let Action::BuildRoad { color, .. } = e { + *color == player } else { false } diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 9f2f38e33..3cb4d8392 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -15,46 +15,55 @@ use super::State; impl State { pub fn apply_action(&mut self, action: Action) { match action { - Action::BuildSettlement(color, node_id) => { + Action::BuildSettlement { 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) => { + Action::BuildRoad { 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) => { + Action::BuyDevelopmentCard { color } => { self.buy_development_card(color); } - Action::Roll(color, dice_opt) => { + Action::Roll { color, dice_opt } => { self.roll_dice(color, dice_opt); } - Action::Discard(color) => { + Action::Discard { color } => { self.discard(color); } - Action::MoveRobber(color, coord, victim_opt) => { - self.move_robber(color, coord, victim_opt); + Action::MoveRobber { + color, + coordinate, + victim_opt, + } => { + self.move_robber(color, coordinate, victim_opt); } - Action::PlayKnight(color) => { + Action::PlayKnight { color } => { self.play_knight(color); self.maintain_largest_army(); } - Action::PlayYearOfPlenty(color, resources) => { + Action::PlayYearOfPlenty { color, resources } => { self.play_year_of_plenty(color, resources); } - Action::PlayMonopoly(color, resource) => { + Action::PlayMonopoly { color, resource } => { self.play_monopoly(color, resource); } - Action::PlayRoadBuilding(color) => { + Action::PlayRoadBuilding { color } => { self.play_road_building(color); } - Action::MaritimeTrade(color, (give, take, ratio)) => { + Action::MaritimeTrade { + color, + give, + take, + ratio, + } => { self.maritime_trade(color, give, take, ratio); } - Action::EndTurn(color) => { + Action::EndTurn { color } => { self.end_turn(color); } _ => { @@ -825,9 +834,15 @@ mod tests { let color2 = 2; // give color1 6 consecutive roads - state.apply_action(Action::BuildSettlement(color1, 0)); + state.apply_action(Action::BuildSettlement { + color: color1, + node_id: 0, + }); for edge in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 16)] { - state.apply_action(Action::BuildRoad(color1, edge)); + state.apply_action(Action::BuildRoad { + color: color1, + edge_id: edge, + }); } assert_eq!(state.longest_road_color, Some(color1)); @@ -837,7 +852,10 @@ mod tests { // Give color2 a settlement at node 4 to bisect color1's Longest Road state.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; freqdeck_add(state.get_mut_player_hand(color2), SETTLEMENT_COST); - state.apply_action(Action::BuildSettlement(color2, 4)); + state.apply_action(Action::BuildSettlement { + color: color2, + node_id: 4, + }); assert_eq!(state.longest_road_color, None); assert_eq!(state.get_actual_victory_points(color1), 1); @@ -884,14 +902,26 @@ mod tests { let color2 = 2; // give color1 6 consecutive roads - state.apply_action(Action::BuildSettlement(color1, 0)); + state.apply_action(Action::BuildSettlement { + color: color1, + node_id: 0, + }); for edge in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 16)] { - state.apply_action(Action::BuildRoad(color1, edge)); + state.apply_action(Action::BuildRoad { + color: color1, + edge_id: edge, + }); } // Give color2 5 consecutive roads with potential to bisect/plow color1's road - state.apply_action(Action::BuildSettlement(color2, 11)); + state.apply_action(Action::BuildSettlement { + color: color2, + node_id: 11, + }); for edge in [(11, 12), (12, 13), (13, 14), (14, 15), (4, 15)] { - state.apply_action(Action::BuildRoad(color2, edge)); + state.apply_action(Action::BuildRoad { + color: color2, + edge_id: edge, + }); } assert_eq!(state.longest_road_color, Some(color1)); @@ -901,7 +931,10 @@ mod tests { // Give color2 a settlement at node 4 to bisect color1's Longest Road state.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; freqdeck_add(state.get_mut_player_hand(color2), SETTLEMENT_COST); - state.apply_action(Action::BuildSettlement(color2, 4)); + state.apply_action(Action::BuildSettlement { + color: color2, + node_id: 4, + }); assert_eq!(state.longest_road_color, Some(color2)); assert_eq!(state.get_actual_victory_points(color1), 1); @@ -913,16 +946,25 @@ mod tests { let mut state = State::new_base(); let color1 = 1; - state.apply_action(Action::BuildSettlement(color1, 0)); + state.apply_action(Action::BuildSettlement { + color: color1, + node_id: 0, + }); for edge in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)] { - state.apply_action(Action::BuildRoad(color1, edge)); + state.apply_action(Action::BuildRoad { + color: color1, + edge_id: edge, + }); } assert_eq!(state.longest_road_color, Some(color1)); assert_eq!(state.longest_road_length, 5); assert_eq!(state.get_actual_victory_points(color1), 3); - state.apply_action(Action::BuildRoad(color1, (5, 16))); + state.apply_action(Action::BuildRoad { + color: color1, + edge_id: (5, 16), + }); assert_eq!(state.longest_road_color, Some(color1)); assert_eq!(state.longest_road_length, 6); @@ -935,9 +977,15 @@ mod tests { let color1 = 1; let color2 = 2; - state.apply_action(Action::BuildSettlement(color1, 0)); + state.apply_action(Action::BuildSettlement { + color: color1, + node_id: 0, + }); for edge in [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 16)] { - state.apply_action(Action::BuildRoad(color1, edge)); + state.apply_action(Action::BuildRoad { + color: color1, + edge_id: edge, + }); } assert_eq!(state.longest_road_color, Some(color1)); @@ -946,7 +994,10 @@ mod tests { state.vector[IS_INITIAL_BUILD_PHASE_INDEX] = 0; freqdeck_add(state.get_mut_player_hand(color2), SETTLEMENT_COST); - state.apply_action(Action::BuildSettlement(color2, 5)); + state.apply_action(Action::BuildSettlement { + color: color2, + node_id: 5, + }); assert_eq!(state.longest_road_color, Some(color1)); assert_eq!(state.longest_road_length, 5); @@ -1039,7 +1090,10 @@ mod tests { // 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))); + state.apply_action(Action::Roll { + color, + dice_opt: Some(roll_numbers), + }); for resource_idx in 0..5 { assert_eq!( @@ -1093,7 +1147,10 @@ mod tests { // 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))); + state.apply_action(Action::Roll { + color, + dice_opt: Some(roll_numbers), + }); for resource_idx in 0..5 { assert_eq!( @@ -1302,7 +1359,7 @@ mod tests { // 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)); + state.apply_action(Action::PlayKnight { color: color1 }); // Verify knight was removed and marked as played assert_eq!( @@ -1338,7 +1395,7 @@ mod tests { // 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)); + state.apply_action(Action::PlayKnight { color: color2 }); // Verify knight was removed and marked as played assert_eq!( @@ -1491,7 +1548,12 @@ mod tests { let initial_bank_brick = state.vector[BANK_RESOURCE_SLICE][1]; - state.apply_action(Action::MaritimeTrade(color, (0, 1, 4))); + state.apply_action(Action::MaritimeTrade { + color, + give: 0, + take: 1, + ratio: 4, + }); assert_eq!(state.get_player_hand(color)[0], 0); assert_eq!(state.get_player_hand(color)[1], 1); @@ -1507,7 +1569,9 @@ mod tests { state.vector[HAS_PLAYED_DEV_CARD] = 1; state.vector[HAS_ROLLED_INDEX] = 1; - state.apply_action(Action::EndTurn(starting_color)); + state.apply_action(Action::EndTurn { + color: starting_color, + }); assert_eq!(state.vector[HAS_PLAYED_DEV_CARD], 0); assert_eq!(state.vector[HAS_ROLLED_INDEX], 0); @@ -1515,7 +1579,9 @@ mod tests { 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())); + state.apply_action(Action::EndTurn { + color: 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 ae031a429..008656ce7 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -30,7 +30,10 @@ impl State { if is_initial_build_phase { self.board_buildable_ids .iter() - .map(|node_id| Action::BuildSettlement(color, *node_id)) + .map(|node_id| Action::BuildSettlement { + color, + node_id: *node_id, + }) .collect() } else { let has_resources = freqdeck_contains(self.get_player_hand(color), SETTLEMENT_COST); @@ -40,7 +43,7 @@ impl State { if has_resources && has_settlements_available { self.buildable_node_ids(color) .into_iter() - .map(|node_id| Action::BuildSettlement(color, node_id)) + .map(|node_id| Action::BuildSettlement { color, node_id }) .collect() } else { vec![] @@ -58,7 +61,10 @@ impl State { self.board_buildable_edges(color) .iter() .filter(|edge_id| self.edge_contains(**edge_id, last_node_id)) - .map(|edge_id| Action::BuildRoad(color, *edge_id)) + .map(|edge_id| Action::BuildRoad { + color, + edge_id: *edge_id, + }) .collect() } @@ -71,7 +77,10 @@ impl State { if is_free || freqdeck_contains(self.get_player_hand(color), ROAD_COST) { self.board_buildable_edges(color) .iter() - .map(|edge_id| Action::BuildRoad(color, *edge_id)) + .map(|edge_id| Action::BuildRoad { + color, + edge_id: *edge_id, + }) .collect() } else { vec![] @@ -92,7 +101,10 @@ impl State { self.get_settlements(color) .iter() .map(|building| match building { - Building::Settlement(color, node_id) => Action::BuildCity(*color, *node_id), + Building::Settlement(color, node_id) => Action::BuildCity { + color: *color, + node_id: *node_id, + }, _ => panic!("Invalid building type"), }) .collect() @@ -102,32 +114,35 @@ impl State { if self.is_road_building() { return self.road_possibilities(color, true); } else if !self.current_player_rolled() { - let mut actions = vec![Action::Roll(color, None)]; + let mut actions = vec![Action::Roll { + color, + dice_opt: None, + }]; if self.can_play_dev(DevCard::Knight as u8) { - actions.push(Action::PlayKnight(color)); + actions.push(Action::PlayKnight { color }); } return actions; } - let mut actions = vec![Action::EndTurn(color)]; + let mut actions = vec![Action::EndTurn { color }]; actions.extend(self.settlement_possibilities(color, false)); actions.extend(self.road_possibilities(color, false)); actions.extend(self.city_possibilities(color)); if self.can_play_dev(DevCard::Knight as u8) { - actions.push(Action::PlayKnight(color)); + actions.push(Action::PlayKnight { color }); } if self.can_play_dev(DevCard::YearOfPlenty as u8) { // TODO: - // actions.push(Action::PlayYearOfPlenty(color)); + // actions.push(Action::PlayYearOfPlenty { color, resources }); } if self.can_play_dev(DevCard::Monopoly as u8) { // TOOD: - // actions.push(Action::PlayMonopoly(color)); + // actions.push(Action::PlayMonopoly { color, resource }); } if self.can_play_dev(DevCard::RoadBuilding as u8) { // TODO: What if user has no roads left? or is completely blocked? - actions.push(Action::PlayRoadBuilding(color)); + actions.push(Action::PlayRoadBuilding { color }); } // TODO: Maritime trade possibilities @@ -146,7 +161,7 @@ mod tests { let state = State::new_base(); let actions = state.generate_playable_actions(); assert_eq!(actions.len(), 54); - assert!(matches!(actions[0], Action::BuildSettlement(_, _))); + assert!(matches!(actions[0], Action::BuildSettlement { .. })); } #[test] @@ -164,23 +179,29 @@ mod tests { *resource = 5; } - let action = Action::BuildSettlement(color, 0); + let action = Action::BuildSettlement { color, node_id: 0 }; state.apply_action(action); - let action = Action::BuildRoad(color, (0, 1)); + let action = Action::BuildRoad { + color, + edge_id: (0, 1), + }; state.apply_action(action); let actions = state.settlement_possibilities(0, false); assert_eq!(actions.len(), 0); - let action = Action::BuildRoad(color, (1, 2)); + let action = Action::BuildRoad { + color, + edge_id: (1, 2), + }; state.apply_action(action); // Should be able to build at node 2 let actions = state.settlement_possibilities(color, false); assert_eq!(actions.len(), 1); assert!(actions.iter().any(|action| { - matches!(action, Action::BuildSettlement(c, node_id) if *c == color && *node_id == 2) + matches!(action, Action::BuildSettlement { color: c, node_id } if *c == color && *node_id == 2) })); } @@ -193,7 +214,7 @@ mod tests { for action in actions { match action { - Action::BuildSettlement(c, _) => assert_eq!(c, curr_color), + Action::BuildSettlement { color, .. } => assert_eq!(color, curr_color), _ => panic!("Expected BuildSettlement action"), } } @@ -211,9 +232,9 @@ mod tests { for action in actions { match action { - Action::BuildRoad(c, edge) => { - assert_eq!(c, curr_color); - assert!(edge.0 == 0 || edge.1 == 0); + Action::BuildRoad { color, edge_id } => { + assert_eq!(color, curr_color); + assert!(edge_id.0 == 0 || edge_id.1 == 0); } _ => panic!("Expected BuildRoad action"), } @@ -232,7 +253,7 @@ mod tests { for action in actions { match action { - Action::BuildSettlement(_, node_id) => { + Action::BuildSettlement { color: _, node_id } => { assert_ne!(node_id, 0); assert!(!neighbors.contains(&node_id)); } @@ -253,7 +274,7 @@ mod tests { let actions = state.generate_playable_actions(); match &actions[0] { - Action::BuildSettlement(_, _) => (), + Action::BuildSettlement { .. } => (), _ => panic!("Expected BuildSettlement action to be first action"), } state.apply_action(actions[0]); From a1cfe13c54f5618fb631d2c61c769b3f4b482f37 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 25 Jan 2025 12:00:13 -0600 Subject: [PATCH 72/83] Rename signature --- catanatron_rust/src/state.rs | 6 +++--- catanatron_rust/src/state/move_application.rs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 8c6ecc8d6..ce1f2a066 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -403,13 +403,13 @@ impl State { self.vector[BANK_RESOURCE_SLICE][resource as usize] > 0 } - pub fn take_from_bank_give_to_player(&mut self, color: u8, resource: u8) { + pub fn from_bank_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) { + pub fn from_player_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; @@ -419,7 +419,7 @@ impl State { self.get_player_hand(color)[resource as usize] } - pub fn take_from_player_give_to_player( + pub fn from_player_to_player( &mut self, from_color: u8, to_color: u8, diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 3cb4d8392..73940a79f 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -657,7 +657,7 @@ impl State { // Give resources to player for resource in resources { - self.take_from_bank_give_to_player(color, resource); + self.from_bank_to_player(color, resource); } } @@ -672,7 +672,7 @@ impl State { 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); + self.from_player_to_player(victim_color, color, resource, amount); } } } @@ -692,8 +692,8 @@ impl State { 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); + self.from_player_to_bank(color, give, ratio); + self.from_bank_to_player(color, take); } fn end_turn(&mut self, _color: u8) { From 2cbece62cddbe15444d1d5037e775ee8260e2889 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 25 Jan 2025 12:35:34 -0600 Subject: [PATCH 73/83] Clarify state_vector --- catanatron_rust/src/state_vector.rs | 177 ++++++++++++++++------------ 1 file changed, 103 insertions(+), 74 deletions(-) diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index c90c1755d..a58a9a68e 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -5,6 +5,52 @@ use crate::enums::COLORS; pub type StateVector = Vec; +// Board dimensions (BASE_MAP) +pub const NUM_NODES: usize = 54; +pub const NUM_EDGES: usize = 72; +pub const NUM_TILES: usize = 19; +pub const NUM_PORTS: usize = 9; + +// Bank indices +pub const BANK_RESOURCE_SLICE: std::ops::Range = 0..5; +pub const DEV_BANK_START_INDEX: usize = 5; +pub const DEV_BANK_END_INDEX: usize = 30; +pub const DEV_BANK_PTR_INDEX: usize = 30; + +// Game control indices +pub const CURRENT_TICK_SEAT_INDEX: usize = 31; +pub const CURRENT_TURN_SEAT_INDEX: usize = 32; +pub const IS_INITIAL_BUILD_PHASE_INDEX: usize = 33; +pub const HAS_PLAYED_DEV_CARD: usize = 34; +pub const HAS_ROLLED_INDEX: usize = 35; +pub const IS_DISCARDING_INDEX: usize = 36; +pub const IS_MOVING_ROBBER_INDEX: usize = 37; +pub const IS_BUILDING_ROAD_INDEX: usize = 38; +pub const FREE_ROADS_AVAILABLE_INDEX: usize = 39; + +// Extra state indices +pub const LONGEST_ROAD_PLAYER_INDEX: usize = 40; +pub const LARGEST_ARMY_PLAYER_INDEX: usize = 41; +pub const ROBBER_TILE_INDEX: usize = 42; + +// Player state indices and sizes +pub const PLAYER_STATE_START_INDEX: usize = 268; +pub const PLAYER_STATE_SIZE: usize = 15; // Size of each player's state block +pub const PLAYER_VP_OFFSET: usize = 0; +pub const PLAYER_RESOURCES_OFFSET: usize = 1; +pub const PLAYER_RESOURCES_SIZE: usize = 5; +pub const PLAYER_DEVCARDS_OFFSET: usize = 6; +pub const PLAYER_DEVCARDS_SIZE: usize = 5; +pub const PLAYER_PLAYED_DEVCARDS_OFFSET: usize = 11; +pub const PLAYER_PLAYED_DEVCARDS_SIZE: usize = 4; + +// Resource constants +pub const MAX_RESOURCE_COUNT: u8 = 19; +pub const MAX_DEV_CARDS: usize = 25; +pub const MAX_VICTORY_POINTS: u8 = 12; +pub const NUM_RESOURCES: usize = 5; +pub const FREE_ROADS_MAX: u8 = 2; + /// This is in theory not needed since we use a vector and we can /// .push() to it. But since we made it, leaving in here in case /// we want to switch to an array implementation and it serves @@ -13,17 +59,13 @@ pub fn get_state_array_size(num_players: usize) -> usize { // TODO: Is configuration part of state? // TODO: Hardcoded for BASE_MAP let n = num_players; - let num_nodes = 54; - let num_edges = 72; - let num_tiles = 19; - let num_ports = 9; let mut size: usize = 0; // Trying to have as most fixed-size vector first as possible // so that we can understand/debug all configurations similarly. // Bank - size += 5; // Bank Resources (Number <= 19) - size += 25; // Bank Development Cards (DevCard Index <= 5) + size += NUM_RESOURCES; // Bank Resources + size += MAX_DEV_CARDS; // Bank Development Cards // Game Controls size += 1; // Current_Player_Index (Player Index < n) @@ -40,29 +82,19 @@ pub fn get_state_array_size(num_players: usize) -> usize { // Note: (Largest_Army_Size and Longest_Road_Size are captured by player (_Played) and board state) size += 1; // Longest_Road_Player_Index (Player Index < n) size += 1; // Largest_Army_Player_Index (Player Index < n) + size += 1; // Robber_Tile (Tile Index < num_tiles) // Board (dynamically sized based on map template; 228 for BASE_MAP) - size += 1; // Robber_Tile (Tile Index < num_tiles) - size += num_tiles; // Tile_Resource (Resource Index <= 5) - size += num_tiles; // Tile_Number (Number <= 12) 39 - size += num_edges; // Edge_Owner (Player Index | -1 < n + 1) - size += num_nodes; // Node_Owner (Player Index | -1 < n + 1) - size += num_nodes; // Node_Settlement/City (1=Settlement, 2=City, 0=Nothing) - size += num_ports; // Port_Resource (Resource Index <= 5) - - // Players (dynamically sized based on number of players = 1 + 15 x n) - size += n; // Color_Seating_Order (Player Index < n) - let mut player_state_size: usize = 0; - player_state_size += 1; // Player_Victory_Points (Number <= 12) - player_state_size += 5; // Player__In_Hand (Number <= 19) - player_state_size += 5; // Player__In_Hand (Number <= 25) - player_state_size += 4; // Player__Played (Number <= 14) VictoryPoint can't be played - - // This is redundant information (since one can figure out from the board state) - // player_state_size += 5; // Player__Roads_Left - // player_state_size += 5; // Player__Settlements_Left - // player_state_size += 5; // Player__Cities_Left - size += player_state_size * n; + size += NUM_TILES; // Tile resources + size += NUM_TILES; // Tile numbers + size += NUM_EDGES; // Edge owners + size += NUM_NODES; // Node owners + size += NUM_NODES; // Node building types + size += NUM_PORTS; // Port resources + + // Players state + size += n; // Color seating order + size += (1 + PLAYER_RESOURCES_SIZE + PLAYER_DEVCARDS_SIZE + PLAYER_PLAYED_DEVCARDS_SIZE) * n; size } @@ -73,47 +105,46 @@ pub fn bank_resource_index(resource: u8) -> usize { } resource as usize } -pub const BANK_RESOURCE_SLICE: std::ops::Range = 0..5; -const PLAYER_STATE_START_INDEX: usize = 268; + pub fn seating_order_slice(num_players: usize) -> std::ops::Range { PLAYER_STATE_START_INDEX..PLAYER_STATE_START_INDEX + num_players } + pub fn actual_victory_points_index(num_players: u8, color: u8) -> usize { - PLAYER_STATE_START_INDEX + num_players as usize + color as usize * 15 + PLAYER_STATE_START_INDEX + num_players as usize + color as usize * PLAYER_STATE_SIZE } -pub const DEV_BANK_PTR_INDEX: usize = 30; -pub const CURRENT_TICK_SEAT_INDEX: usize = 31; -pub const CURRENT_TURN_SEAT_INDEX: usize = 32; -pub const IS_INITIAL_BUILD_PHASE_INDEX: usize = 33; -pub const HAS_PLAYED_DEV_CARD: usize = 34; -pub const HAS_ROLLED_INDEX: usize = 35; -pub const IS_DISCARDING_INDEX: usize = 36; -pub const IS_MOVING_ROBBER_INDEX: usize = 37; -pub const IS_BUILDING_ROAD_INDEX: usize = 38; -pub const FREE_ROADS_AVAILABLE_INDEX: usize = 39; -pub const ROBBER_TILE_INDEX: usize = 42; pub fn player_hand_slice(num_players: u8, color: u8) -> std::ops::Range { - let start = PLAYER_STATE_START_INDEX + num_players as usize + 1 + (color as usize * 15); - start..start + 5 + let start = PLAYER_STATE_START_INDEX + + num_players as usize + + PLAYER_RESOURCES_OFFSET + + (color as usize * PLAYER_STATE_SIZE); + start..start + PLAYER_RESOURCES_SIZE } + pub fn player_devhand_slice(num_players: u8, color: u8) -> std::ops::Range { - let start = PLAYER_STATE_START_INDEX + num_players as usize + 6 + (color as usize * 15); - start..start + 5 + let start = PLAYER_STATE_START_INDEX + + num_players as usize + + PLAYER_DEVCARDS_OFFSET + + (color as usize * PLAYER_STATE_SIZE); + start..start + PLAYER_DEVCARDS_SIZE } pub fn player_played_devhand_slice(num_players: u8, color: u8) -> std::ops::Range { - let start = PLAYER_STATE_START_INDEX + num_players as usize + 11 + (color as usize * 15); - start..start + 4 + let start = PLAYER_STATE_START_INDEX + + num_players as usize + + PLAYER_PLAYED_DEVCARDS_OFFSET + + (color as usize * PLAYER_STATE_SIZE); + start..start + PLAYER_PLAYED_DEVCARDS_SIZE } // 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; - if ptr >= 25 { + if ptr >= MAX_DEV_CARDS { return None; } - let card = vector[5 + ptr]; + let card = vector[DEV_BANK_START_INDEX + ptr]; vector[DEV_BANK_PTR_INDEX] += 1; Some(card) } @@ -140,46 +171,44 @@ pub fn initialize_state(num_players: u8) -> Vec { let size = get_state_array_size(n); let mut vector = vec![0; size]; - // Initialize Bank - vector[0] = 19; - vector[1] = 19; - vector[2] = 19; - vector[3] = 19; - vector[4] = 19; + + // Initialize Bank Resources + for i in BANK_RESOURCE_SLICE { + vector[i] = MAX_RESOURCE_COUNT; + } + // Initialize Bank Development Cards // TODO: Shuffle let mut listdeck = starting_dev_listdeck(); listdeck.shuffle(&mut rand::thread_rng()); - vector[5..30].copy_from_slice(&listdeck); - // May be worth storing a pointer to the top of the deck + vector[DEV_BANK_START_INDEX..DEV_BANK_END_INDEX].copy_from_slice(&listdeck); vector[DEV_BANK_PTR_INDEX] = 0; + // Initialize Game Controls vector[CURRENT_TICK_SEAT_INDEX] = 0; vector[CURRENT_TURN_SEAT_INDEX] = 0; - vector[IS_INITIAL_BUILD_PHASE_INDEX] = 1; // Is_Initial_Build_Phase - vector[HAS_PLAYED_DEV_CARD] = 0; // Has_Played_Development_Card - vector[HAS_ROLLED_INDEX] = 0; // Has_Rolled - vector[IS_DISCARDING_INDEX] = 0; // Is_Discarding - vector[IS_MOVING_ROBBER_INDEX] = 0; // Is_Moving_Robber - vector[IS_BUILDING_ROAD_INDEX] = 0; // Is_Building_Road - vector[FREE_ROADS_AVAILABLE_INDEX] = 2; // Free_Roads_Available - - // Extra (u8::MAX is used to indicate no player) - vector[40] = u8::MAX; // Longest_Road_Player_Index - vector[41] = u8::MAX; // Largest_Army_Player_Index - - // Board - // TODO: Generate map from template - vector[ROBBER_TILE_INDEX] = 0; // Robber_Tile + vector[IS_INITIAL_BUILD_PHASE_INDEX] = 1; + vector[HAS_PLAYED_DEV_CARD] = 0; + vector[HAS_ROLLED_INDEX] = 0; + vector[IS_DISCARDING_INDEX] = 0; + vector[IS_MOVING_ROBBER_INDEX] = 0; + vector[IS_BUILDING_ROAD_INDEX] = 0; + vector[FREE_ROADS_AVAILABLE_INDEX] = 0; // Initially no free roads available (road building dev card) - let mut player_state_start = PLAYER_STATE_START_INDEX; + // Initialize Extra State + vector[LONGEST_ROAD_PLAYER_INDEX] = u8::MAX; + vector[LARGEST_ARMY_PLAYER_INDEX] = u8::MAX; + vector[ROBBER_TILE_INDEX] = 0; // Initialize Players + let mut player_state_start = PLAYER_STATE_START_INDEX; + // Shuffle player indices let mut color_seating_order = COLORS[0..n].iter().map(|&x| x as u8).collect::>(); color_seating_order.shuffle(&mut rand::thread_rng()); vector[player_state_start..player_state_start + n].copy_from_slice(&color_seating_order); player_state_start += n; + for _ in 0..num_players { // Player_Victory_Points (Number <= 12). i is in order of COLORS vector[player_state_start] = 0; // victory points @@ -203,7 +232,7 @@ pub fn initialize_state(num_players: u8) -> Vec { vector[player_state_start + 12] = 0; // year of plenty played vector[player_state_start + 13] = 0; // monopoly played vector[player_state_start + 14] = 0; // road building played - player_state_start += 15; + player_state_start += PLAYER_STATE_SIZE; } vector From e13903a939049dc8fa1a41f01be9d719ee03b932 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 25 Jan 2025 15:40:53 -0600 Subject: [PATCH 74/83] Robber Possibilities --- catanatron_rust/src/map_instance.rs | 4 + catanatron_rust/src/state.rs | 9 ++ catanatron_rust/src/state/move_application.rs | 12 +- catanatron_rust/src/state/move_generation.rs | 128 +++++++++++++++++- 4 files changed, 144 insertions(+), 9 deletions(-) diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 06b60b5ee..2f98f06cd 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -148,6 +148,10 @@ impl MapInstance { &self.land_nodes } + pub fn get_port_nodes(&self) -> &HashSet { + &self.port_nodes + } + pub fn get_tile(&self, coordinate: Coordinate) -> Option<&Tile> { self.tiles.get(&coordinate) } diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index ce1f2a066..5a2fe6c1a 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -12,6 +12,7 @@ use crate::{ 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, + ROBBER_TILE_INDEX, }, }; @@ -430,6 +431,14 @@ impl State { self.get_mut_player_hand(from_color)[resource_idx] -= amount; self.get_mut_player_hand(to_color)[resource_idx] += amount; } + + pub fn get_robber_tile(&self) -> u8 { + self.vector[ROBBER_TILE_INDEX] + } + + pub fn set_robber_tile(&mut self, tile_id: u8) { + self.vector[ROBBER_TILE_INDEX] = tile_id; + } } #[cfg(test)] diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 73940a79f..fe978865e 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -419,7 +419,7 @@ impl State { for tile in matching_tiles { // Skip robber tile - if self.vector[ROBBER_TILE_INDEX] == tile.id { + if self.get_robber_tile() == tile.id { continue; } @@ -529,7 +529,7 @@ impl State { } 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.set_robber_tile(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(); @@ -1073,7 +1073,7 @@ mod tests { 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 tile.id != state.get_robber_tile() { if chosen_roll.is_none() { chosen_roll = Some(number); } @@ -1130,7 +1130,7 @@ mod tests { 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 tile.id != state.get_robber_tile() { if chosen_roll.is_none() { chosen_roll = Some(number); } @@ -1187,7 +1187,7 @@ mod tests { 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() { + if tile.id != state.get_robber_tile() && chosen_roll.is_none() { chosen_roll = Some(number); chosen_resource = Some(resource); } @@ -1227,7 +1227,7 @@ mod tests { .values() .find(|tile| { tile.resource.is_some() && // Not a desert - tile.id != state.vector[ROBBER_TILE_INDEX] // Not under robber + tile.id != state.get_robber_tile() // Not under robber }) .expect("Should be at least one valid tile"); diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 008656ce7..a7a3b38b1 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -1,6 +1,7 @@ use crate::deck_slices::{freqdeck_contains, CITY_COST, ROAD_COST, SETTLEMENT_COST}; use crate::enums::{Action, ActionPrompt, DevCard}; use crate::state::State; +use std::collections::HashSet; use super::Building; @@ -16,9 +17,9 @@ impl State { self.settlement_possibilities(current_color, true) } ActionPrompt::BuildInitialRoad => self.initial_road_possibilities(current_color), + ActionPrompt::MoveRobber => self.robber_possibilities(current_color), ActionPrompt::PlayTurn => self.play_turn_possibilities(current_color), ActionPrompt::Discard => todo!("generate_playbale_actions for Discard"), - ActionPrompt::MoveRobber => todo!("generate_playbale_actions for Move robber"), ActionPrompt::DecideTrade => todo!("generate_playbale_actions for Decide trade"), ActionPrompt::DecideAcceptees => { todo!("generate_playbale_actions for Decide acceptees") @@ -125,6 +126,8 @@ impl State { } let mut actions = vec![Action::EndTurn { color }]; + + // Add all possible actions actions.extend(self.settlement_possibilities(color, false)); actions.extend(self.road_possibilities(color, false)); actions.extend(self.city_possibilities(color)); @@ -149,12 +152,58 @@ impl State { actions } + + pub fn robber_possibilities(&self, color: u8) -> Vec { + let mut actions = vec![]; + let current_robber_tile = self.get_robber_tile(); + + for (coordinate, tile) in self.map_instance.get_land_tiles() { + // Skip current robber location + if tile.id == current_robber_tile { + continue; + } + + // Find players to steal from at this tile + let mut victims = HashSet::new(); + for node_id in tile.hexagon.nodes.values() { + if let Some(building) = self.buildings.get(node_id) { + match building { + Building::Settlement(victim_color, _) | Building::City(victim_color, _) => { + // Can't steal from yourself and victim must have resources + if *victim_color != color + && self.get_player_hand(*victim_color).iter().sum::() > 0 + { + victims.insert(*victim_color); + } + } + } + } + } + + if victims.is_empty() { + actions.push(Action::MoveRobber { + color, + coordinate: *coordinate, + victim_opt: None, + }); + } else { + for victim in victims { + actions.push(Action::MoveRobber { + color, + coordinate: *coordinate, + victim_opt: Some(victim), + }); + } + } + } + + actions + } } #[cfg(test)] mod tests { - use crate::enums::{Action, ActionPrompt}; - use crate::state::State; + use super::*; #[test] fn test_move_generation() { @@ -284,4 +333,77 @@ mod tests { ActionPrompt::BuildInitialRoad )); } + + #[test] + fn test_robber_possibilities() { + let mut state = State::new_base(); + let color1 = 1; + let color2 = 2; + + state.build_settlement(color2, 0); + + // Give resources to color2 + let hand = state.get_mut_player_hand(color2); + hand[1] = 1; // Use Brick's index (1) directly + + let actions = state.robber_possibilities(color1); + + // Should be able to move robber to any land tile except current location + let num_land_tiles = state.map_instance.get_land_tiles().len(); + assert_eq!(actions.len(), num_land_tiles - 1); + + // Count how many actions involve stealing + let steal_actions_count = actions.iter().filter(|action| { + matches!(action, Action::MoveRobber { victim_opt: Some(v), .. } if *v == color2) + }).count(); + + // Node 0 is connected to 3 tiles, but one might be the robber's current location + assert!( + steal_actions_count == 2 || steal_actions_count == 3, + "Should have 2 or 3 tiles where stealing is possible" + ); + + // Verify can't steal from player with no resources + let hand = state.get_mut_player_hand(color2); + hand[1] = 0; // Clear Brick resource + let actions = state.robber_possibilities(color1); + assert_eq!(actions.len(), num_land_tiles - 1); // All tiles except current + + // Verify no stealing actions when victim has no resources + let steal_actions_count = actions + .iter() + .filter(|action| { + matches!( + action, + Action::MoveRobber { + victim_opt: Some(_), + .. + } + ) + }) + .count(); + assert_eq!( + steal_actions_count, 0, + "Should have no stealing actions when victim has no resources" + ); + } + + #[test] + fn test_robber_cant_stay_in_place() { + let state = State::new_base(); + let color = state.get_current_color(); + let actions = state.robber_possibilities(color); + + // Get current robber tile + let current_robber_tile = state.get_robber_tile(); + + // Verify no action tries to move robber to current location + assert!(actions.iter().all(|action| { + if let Action::MoveRobber { coordinate, .. } = action { + state.map_instance.get_land_tile(*coordinate).unwrap().id != current_robber_tile + } else { + false + } + })); + } } From 42b13bb6dbefdecc83f59f09c862a280e42883d5 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sun, 26 Jan 2025 14:37:03 -0600 Subject: [PATCH 75/83] Year of Plenty Possibilities --- catanatron_rust/src/enums.rs | 2 +- catanatron_rust/src/state.rs | 8 ++ catanatron_rust/src/state/move_application.rs | 13 ++- catanatron_rust/src/state/move_generation.rs | 97 ++++++++++++++++++- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 4a3d6e1a0..323942c44 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -104,7 +104,7 @@ pub enum Action { }, PlayYearOfPlenty { color: u8, - resources: [u8; 2], + resources: (u8, Option), }, PlayMonopoly { color: u8, diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 5a2fe6c1a..52c649c88 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -439,6 +439,14 @@ impl State { pub fn set_robber_tile(&mut self, tile_id: u8) { self.vector[ROBBER_TILE_INDEX] = tile_id; } + + pub fn get_bank_resources(&self) -> &[u8] { + &self.vector[BANK_RESOURCE_SLICE] + } + + pub fn set_bank_resource(&mut self, resource_index: usize, count: u8) { + self.vector[BANK_RESOURCE_SLICE.start + resource_index] = count; + } } #[cfg(test)] diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index fe978865e..d4a7b098c 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -648,16 +648,19 @@ impl State { } } - fn play_year_of_plenty(&mut self, color: u8, resources: [u8; 2]) { + fn play_year_of_plenty(&mut self, color: u8, resources: (u8, Option)) { // 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.from_bank_to_player(color, resource); + // Give first resource to player + self.from_bank_to_player(color, resources.0); + + // Give second resource if specified + if let Some(resource2) = resources.1 { + self.from_bank_to_player(color, resource2); } } @@ -1436,7 +1439,7 @@ mod tests { 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]); + state.play_year_of_plenty(color, (0, Some(1))); // Verify card was removed from hand assert_eq!( diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index a7a3b38b1..a4a6aefb4 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -111,6 +111,50 @@ impl State { .collect() } + pub fn year_of_plenty_possibilities(&self, color: u8) -> Vec { + let bank_resources = self.get_bank_resources(); + let mut actions = Vec::new(); + + // First try all same-resource pairs + for (resource, &count) in bank_resources.iter().enumerate() { + if count > 1 { + actions.push(Action::PlayYearOfPlenty { + color, + resources: (resource as u8, Some(resource as u8)), + }); + } + } + + // Then try different-resource pairs + for (resource1, &count1) in bank_resources.iter().enumerate() { + if count1 > 0 { + for (resource2, &count2) in bank_resources.iter().enumerate().skip(resource1 + 1) { + if count2 > 0 { + actions.push(Action::PlayYearOfPlenty { + color, + resources: (resource1 as u8, Some(resource2 as u8)), + }); + } + } + } + } + + // If no two-resource actions possible, try single resources + if actions.is_empty() { + for (resource, &count) in bank_resources.iter().enumerate() { + if count > 0 { + actions.push(Action::PlayYearOfPlenty { + color, + resources: (resource as u8, None), + }); + break; // Only need first available resource + } + } + } + + actions + } + pub fn play_turn_possibilities(&self, color: u8) -> Vec { if self.is_road_building() { return self.road_possibilities(color, true); @@ -136,8 +180,7 @@ impl State { actions.push(Action::PlayKnight { color }); } if self.can_play_dev(DevCard::YearOfPlenty as u8) { - // TODO: - // actions.push(Action::PlayYearOfPlenty { color, resources }); + actions.extend(self.year_of_plenty_possibilities(color)); } if self.can_play_dev(DevCard::Monopoly as u8) { // TOOD: @@ -406,4 +449,54 @@ mod tests { } })); } + + #[test] + fn test_year_of_plenty_possibilities() { + 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); + + // Test with full bank - should have 15 actions: + // - 5 same-resource actions (Wood+Wood, Brick+Brick, etc.) + // - 10 different-resource combinations (5 choose 2) + let actions = state.year_of_plenty_possibilities(color); + assert_eq!(actions.len(), 15); + + // Test with empty bank - should have 0 actions + for i in 0..5 { + // 5 resource types + state.set_bank_resource(i, 0); + } + let actions = state.year_of_plenty_possibilities(color); + assert_eq!(actions.len(), 0); + + // Test with only 2 ORE available + state.set_bank_resource(4, 2); + let actions = state.year_of_plenty_possibilities(color); + assert_eq!(actions.len(), 1); // Can only take ORE+ORE + assert!(matches!( + actions[0], + Action::PlayYearOfPlenty { + resources: (4, Some(4)), + .. + } + )); + + // Test with only 1 WHEAT available + for i in 0..5 { + state.set_bank_resource(i, 0); + } + state.set_bank_resource(3, 1); + let actions = state.year_of_plenty_possibilities(color); + assert_eq!(actions.len(), 1); // Can take just one WHEAT when that's all that's available + assert!(matches!( + actions[0], + Action::PlayYearOfPlenty { + resources: (3, None), + .. + } + )); + } } From b61bdfc9f21f6a1012ad338292f8a46fd08b8ebe Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sun, 26 Jan 2025 16:21:17 -0600 Subject: [PATCH 76/83] Node production --- catanatron_rust/src/map_instance.rs | 8 +++++ catanatron_rust/src/state.rs | 47 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 2f98f06cd..2d5ae9c05 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -152,6 +152,14 @@ impl MapInstance { &self.port_nodes } + pub fn get_node_production(&self, node_id: NodeId) -> Option<&HashMap> { + self.node_production.get(&node_id) + } + + pub fn get_all_node_production(&self) -> &HashMap> { + &self.node_production + } + pub fn get_tile(&self, coordinate: Coordinate) -> Option<&Tile> { self.tiles.get(&coordinate) } diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 52c649c88..63bc9a014 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -447,6 +447,53 @@ impl State { pub fn set_bank_resource(&mut self, resource_index: usize, count: u8) { self.vector[BANK_RESOURCE_SLICE.start + resource_index] = count; } + + /// Calculates effective production (considering robber) for a player + pub fn get_effective_production(&self, color: u8) -> Vec { + self.get_player_production_internal(color, true) + } + + /// Calculates total production (ignoring robber) for a player + pub fn get_total_production(&self, color: u8) -> Vec { + self.get_player_production_internal(color, false) + } + + fn get_player_production_internal(&self, color: u8, consider_robber: bool) -> Vec { + let mut production = vec![0.0; 5]; // One for each resource + let robber_tile = if consider_robber { + Some(self.get_robber_tile()) + } else { + None + }; + + // Get all buildings for this player + if let Some(buildings) = self.buildings_by_color.get(&color) { + for building in buildings { + let (node_id, multiplier) = match building { + Building::Settlement(_, node) => (*node, 1.0), + Building::City(_, node) => (*node, 2.0), + }; + + // Skip if robber is blocking this node + if let Some(robber_id) = robber_tile { + if let Some(adjacent_tiles) = self.map_instance.get_adjacent_tiles(node_id) { + if adjacent_tiles.iter().any(|tile| tile.id == robber_id) { + continue; + } + } + } + + // Get production for this node + if let Some(node_prod) = self.map_instance.get_node_production(node_id) { + for (resource, prob) in node_prod { + production[*resource as usize] += prob * multiplier; + } + } + } + } + + production + } } #[cfg(test)] From 67df80928b83ee9802197c171e6441bdcc268ff8 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sun, 26 Jan 2025 16:46:49 -0600 Subject: [PATCH 77/83] Mark dead code to satisfy clippy --- catanatron_rust/src/map_instance.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index 2d5ae9c05..a4cdcaf84 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -130,6 +130,9 @@ pub struct MapInstance { // - BFS capabilities // all which doesn't sound too bad to implement. land_nodes: HashSet, + + // TODO: Track valid edges for building roads. + #[allow(dead_code)] land_edges: HashSet, node_neighbors: HashMap>, edge_neighbors: HashMap>, From e5046bb7578788d95c912339a5107787f9d1caa0 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sun, 9 Feb 2025 12:22:48 -0600 Subject: [PATCH 78/83] Maritime trade possibilities --- catanatron_rust/src/map_instance.rs | 16 +- catanatron_rust/src/state/move_generation.rs | 248 ++++++++++++++++++- catanatron_rust/src/state_vector.rs | 4 + 3 files changed, 256 insertions(+), 12 deletions(-) diff --git a/catanatron_rust/src/map_instance.rs b/catanatron_rust/src/map_instance.rs index a4cdcaf84..d6bafe6ff 100644 --- a/catanatron_rust/src/map_instance.rs +++ b/catanatron_rust/src/map_instance.rs @@ -115,7 +115,7 @@ pub enum Tile { pub struct MapInstance { tiles: HashMap, land_tiles: HashMap, - port_nodes: HashSet, + port_nodes: HashMap>, adjacent_land_tiles: HashMap>, node_production: HashMap>, @@ -151,7 +151,7 @@ impl MapInstance { &self.land_nodes } - pub fn get_port_nodes(&self) -> &HashSet { + pub fn get_port_nodes(&self) -> &HashMap> { &self.port_nodes } @@ -274,7 +274,7 @@ impl MapInstance { fn from_tiles(tiles: HashMap, dice_probas: &HashMap) -> Self { let mut land_tiles: HashMap = HashMap::new(); - let mut port_nodes: HashSet = HashSet::new(); + let mut port_nodes: HashMap> = HashMap::new(); let mut adjacent_land_tiles: HashMap> = HashMap::new(); let mut node_production: HashMap> = HashMap::new(); @@ -325,8 +325,14 @@ impl MapInstance { }); } else if let Tile::Port(port_tile) = tile { let (a_noderef, b_noderef) = get_noderefs_from_port_direction(port_tile.direction); - port_nodes.insert(*port_tile.hexagon.nodes.get(&a_noderef).unwrap()); - port_nodes.insert(*port_tile.hexagon.nodes.get(&b_noderef).unwrap()); + port_nodes.insert( + *port_tile.hexagon.nodes.get(&a_noderef).unwrap(), + port_tile.resource, + ); + port_nodes.insert( + *port_tile.hexagon.nodes.get(&b_noderef).unwrap(), + port_tile.resource, + ); } } diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index a4a6aefb4..2e68a05cd 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -1,6 +1,7 @@ use crate::deck_slices::{freqdeck_contains, CITY_COST, ROAD_COST, SETTLEMENT_COST}; use crate::enums::{Action, ActionPrompt, DevCard}; use crate::state::State; +use crate::state_vector::get_free_roads_available; use std::collections::HashSet; use super::Building; @@ -19,10 +20,10 @@ impl State { ActionPrompt::BuildInitialRoad => self.initial_road_possibilities(current_color), ActionPrompt::MoveRobber => self.robber_possibilities(current_color), ActionPrompt::PlayTurn => self.play_turn_possibilities(current_color), - ActionPrompt::Discard => todo!("generate_playbale_actions for Discard"), - ActionPrompt::DecideTrade => todo!("generate_playbale_actions for Decide trade"), + ActionPrompt::Discard => self.discard_possibilities(current_color), + ActionPrompt::DecideTrade => todo!("generate_playable_actions for Decide trade"), ActionPrompt::DecideAcceptees => { - todo!("generate_playbale_actions for Decide acceptees") + todo!("generate_playable_actions for Decide acceptees") } } } @@ -117,7 +118,7 @@ impl State { // First try all same-resource pairs for (resource, &count) in bank_resources.iter().enumerate() { - if count > 1 { + if count >= 2 { actions.push(Action::PlayYearOfPlenty { color, resources: (resource as u8, Some(resource as u8)), @@ -157,6 +158,9 @@ impl State { pub fn play_turn_possibilities(&self, color: u8) -> Vec { if self.is_road_building() { + if get_free_roads_available(&self.vector) == 0 { + return vec![]; + } return self.road_possibilities(color, true); } else if !self.current_player_rolled() { let mut actions = vec![Action::Roll { @@ -183,19 +187,80 @@ impl State { actions.extend(self.year_of_plenty_possibilities(color)); } if self.can_play_dev(DevCard::Monopoly as u8) { - // TOOD: - // actions.push(Action::PlayMonopoly { color, resource }); + for resource in 0..5 { + actions.push(Action::PlayMonopoly { color, resource }); + } } if self.can_play_dev(DevCard::RoadBuilding as u8) { // TODO: What if user has no roads left? or is completely blocked? actions.push(Action::PlayRoadBuilding { color }); } - // TODO: Maritime trade possibilities + // Add maritime trade possibilities + actions.extend(self.maritime_trade_possibilities(color)); + + // TODO: Domestic trading is temporarily disabled to reduce the state space explosion + // This simplification allows us to first build a superhuman AI player without + // the complexity of domestic trading. actions } + fn calculate_port_rates(&self, color: u8) -> [u8; 5] { + let mut port_rates = [4; 5]; // Default 4:1 rate for all resources + + let Some(player_buildings) = self.buildings_by_color.get(&color) else { + return port_rates; + }; + + // For each player building, check if it's on a port and update rates + for building in player_buildings { + let node_id = match building { + Building::Settlement(_, id) | Building::City(_, id) => id, + }; + + if let Some(&port_resource) = self.map_instance.get_port_nodes().get(node_id) { + match port_resource { + Some(resource) => port_rates[resource as usize] = 2, + None => port_rates + .iter_mut() + .for_each(|rate| *rate = (*rate).min(3)), + } + } + } + + port_rates + } + + pub fn maritime_trade_possibilities(&self, color: u8) -> Vec { + let hand = self.get_player_hand(color); + let bank = self.get_bank_resources(); + let port_rates = self.calculate_port_rates(color); + + hand.iter() + .enumerate() + .flat_map(|(give_idx, &give_count)| { + let rate = port_rates[give_idx]; + if give_count >= rate { + (0..5) + .filter(|&take_idx| { + // Ensure bank has enough resources and it's a different resource + take_idx != give_idx && bank[take_idx] > 0 + }) + .map(|take_idx| Action::MaritimeTrade { + color, + give: give_idx as u8, + take: take_idx as u8, + ratio: rate, + }) + .collect::>() + } else { + Vec::new() + } + }) + .collect() + } + pub fn robber_possibilities(&self, color: u8) -> Vec { let mut actions = vec![]; let current_robber_tile = self.get_robber_tile(); @@ -242,11 +307,40 @@ impl State { actions } + + pub fn discard_possibilities(&self, color: u8) -> Vec { + let hand = self.get_player_hand(color); + let total_cards: u8 = hand.iter().sum(); + + // If player has 7 or fewer cards, they don't need to discard + if total_cards <= 7 { + return vec![]; + } + + // For now, just generate a single discard action + // This is to prevent state space explosion + vec![Action::Discard { color }] + } } #[cfg(test)] mod tests { use super::*; + use crate::enums::Resource; + + fn find_port_node_by_type(state: &State, resource: Option) -> Option { + state + .map_instance + .get_port_nodes() + .iter() + .find_map(|(&node_id, &port_resource)| { + if port_resource == resource { + Some(node_id) + } else { + None + } + }) + } #[test] fn test_move_generation() { @@ -499,4 +593,144 @@ mod tests { } )); } + + #[test] + fn test_discard_possibilities() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + // Give player 8 cards (above discard limit) + { + let hand = state.get_mut_player_hand(color); + hand[0] = 8; // Give 8 wood cards + } + + let actions = state.discard_possibilities(color); + assert_eq!(actions.len(), 1); + assert!(matches!(actions[0], Action::Discard { color: c } if c == color)); + + // Test with 7 cards (at discard limit) + { + let hand = state.get_mut_player_hand(color); + hand[0] = 7; + } + let actions = state.discard_possibilities(color); + assert_eq!(actions.len(), 0); + } + + #[test] + fn test_maritime_trade_possibilities() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + // Give player 4 wood (enough for 4:1 trade) + { + let hand = state.get_mut_player_hand(color); + hand[0] = 4; + } + + let actions = state.maritime_trade_possibilities(color); + + // Should be able to trade 4 wood for any other resource + assert_eq!(actions.len(), 4); // Can trade for brick, sheep, wheat, ore + assert!(actions.iter().all(|action| matches!( + action, + Action::MaritimeTrade { + color: c, + give, + take, + ratio + } if *c == color && *give == 0 && *ratio == 4 && *take != 0 + ))); + + // Test with insufficient resources + { + let hand = state.get_mut_player_hand(color); + hand[0] = 3; + } + let actions = state.maritime_trade_possibilities(color); + assert_eq!(actions.len(), 0); + } + + #[test] + fn test_maritime_trade_with_empty_bank() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + // Give player resources for trading + let hand = state.get_mut_player_hand(color); + hand[0] = 4; // Give 4 wood for 4:1 trade + + // Empty the bank + for i in 0..5 { + state.set_bank_resource(i, 0); + } + + let actions = state.maritime_trade_possibilities(color); + assert_eq!( + actions.len(), + 0, + "Should not be able to trade with empty bank" + ); + } + + #[test] + fn test_maritime_trade_with_two_to_one_port() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + // Find a 2:1 wood port node + let wood_port_node = find_port_node_by_type(&state, Some(Resource::Wood)).unwrap(); + + // Build settlement at wood port + state.build_settlement(color, wood_port_node); + + // Give player 2 wood (enough for 2:1 trade) + let hand = state.get_mut_player_hand(color); + hand[0] = 2; + + let actions = state.maritime_trade_possibilities(color); + assert!( + actions.iter().any(|action| matches!( + action, + Action::MaritimeTrade { + color: c, + give, + take, + ratio + } if *c == color && *give == 0 && *ratio == 2 && *take != 0 + )), + "Should be able to use 2:1 wood port" + ); + } + + #[test] + fn test_maritime_trade_with_three_to_one_port() { + let mut state = State::new_base(); + let color = state.get_current_color(); + + // Find a 3:1 port node + let port_node = find_port_node_by_type(&state, None).unwrap(); + + // Build settlement at 3:1 port + state.build_settlement(color, port_node); + + // Give player 3 brick (enough for 3:1 trade) + let hand = state.get_mut_player_hand(color); + hand[1] = 3; // Give 3 brick + + let actions = state.maritime_trade_possibilities(color); + assert!( + actions.iter().any(|action| matches!( + action, + Action::MaritimeTrade { + color: c, + give, + take, + ratio + } if *c == color && *give == 1 && *ratio == 3 && *take != 0 + )), + "Should be able to use 3:1 port for brick" + ); + } } diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index a58a9a68e..80cc3f149 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -138,6 +138,10 @@ pub fn player_played_devhand_slice(num_players: u8, color: u8) -> std::ops::Rang start..start + PLAYER_PLAYED_DEVCARDS_SIZE } +pub fn get_free_roads_available(vector: &StateVector) -> u8 { + vector[FREE_ROADS_AVAILABLE_INDEX] +} + // 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; From bf6542fe6f9e259b6163b859373a3124ce9ce3c5 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sun, 23 Feb 2025 13:46:20 -0600 Subject: [PATCH 79/83] initial pyo3 testing --- catanatron_core/catanatron/test_rust_game.py | 10 ++ catanatron_rust/Cargo.lock | 167 ++++++++++++++++++- catanatron_rust/Cargo.toml | 6 + catanatron_rust/pyproject.toml | 13 ++ catanatron_rust/src/enums.rs | 7 +- catanatron_rust/src/game.rs | 48 +++++- catanatron_rust/src/lib.rs | 10 +- catanatron_rust/src/player.rs | 17 -- catanatron_rust/src/players/mod.rs | 10 ++ catanatron_rust/src/players/random_player.rs | 17 ++ 10 files changed, 276 insertions(+), 29 deletions(-) create mode 100644 catanatron_core/catanatron/test_rust_game.py create mode 100644 catanatron_rust/pyproject.toml delete mode 100644 catanatron_rust/src/player.rs create mode 100644 catanatron_rust/src/players/mod.rs create mode 100644 catanatron_rust/src/players/random_player.rs diff --git a/catanatron_core/catanatron/test_rust_game.py b/catanatron_core/catanatron/test_rust_game.py new file mode 100644 index 000000000..a381e20ff --- /dev/null +++ b/catanatron_core/catanatron/test_rust_game.py @@ -0,0 +1,10 @@ +def test_rust_game_creation(): + try: + from catanatron_rust import Game + game = Game(4) + assert game.get_num_players() == 4 + print("\nStarting game simulation...") + game.play() + except ImportError as e: + print(f"Failed to import catanatron_rust: {e}") + raise \ No newline at end of file diff --git a/catanatron_rust/Cargo.lock b/catanatron_rust/Cargo.lock index 42e5dab94..da2496e6c 100644 --- a/catanatron_rust/Cargo.lock +++ b/catanatron_rust/Cargo.lock @@ -29,6 +29,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + [[package]] name = "bumpalo" version = "3.16.0" @@ -52,6 +58,7 @@ name = "catanatron_rust" version = "0.1.0" dependencies = [ "criterion", + "pyo3", "rand", ] @@ -213,6 +220,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + [[package]] name = "is-terminal" version = "0.4.13" @@ -254,6 +267,16 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -266,6 +289,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -287,6 +319,29 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + [[package]] name = "plotters" version = "0.3.7" @@ -333,6 +388,66 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pyo3" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.37" @@ -392,6 +507,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.11.0" @@ -436,6 +560,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.210" @@ -453,7 +583,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.79", ] [[package]] @@ -468,6 +598,23 @@ dependencies = [ "serde", ] +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.79" @@ -479,6 +626,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tinytemplate" version = "1.2.1" @@ -495,6 +648,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unindent" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" + [[package]] name = "walkdir" version = "2.5.0" @@ -533,7 +692,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.79", "wasm-bindgen-shared", ] @@ -555,7 +714,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.79", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -685,5 +844,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.79", ] diff --git a/catanatron_rust/Cargo.toml b/catanatron_rust/Cargo.toml index 48f1d5b9f..957dab36b 100644 --- a/catanatron_rust/Cargo.toml +++ b/catanatron_rust/Cargo.toml @@ -6,9 +6,15 @@ edition = "2021" [lib] name = "catanatron_rust" path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] + +[[bin]] +name = "catanatron_rust" +path = "src/main.rs" [dependencies] rand = "0.8" +pyo3 = { version = "0.19", features = ["extension-module"] } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/catanatron_rust/pyproject.toml b/catanatron_rust/pyproject.toml new file mode 100644 index 000000000..cc4779a4c --- /dev/null +++ b/catanatron_rust/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[project] +name = "catanatron_rust" +version = "0.1.0" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] \ No newline at end of file diff --git a/catanatron_rust/src/enums.rs b/catanatron_rust/src/enums.rs index 323942c44..8ccdc9b35 100644 --- a/catanatron_rust/src/enums.rs +++ b/catanatron_rust/src/enums.rs @@ -59,7 +59,7 @@ pub enum EdgeRef { Northeast, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum ActionPrompt { BuildInitialSettlement, BuildInitialRoad, @@ -142,15 +142,14 @@ pub enum Action { }, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum MapType { Mini, Base, Tournament, } -// TODO: Make immutable and read-only -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct GameConfiguration { pub discard_limit: u8, pub vps_to_win: u8, diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 9b114d7f1..04a0c0022 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -1,11 +1,12 @@ use std::collections::HashMap; use std::rc::Rc; -use crate::enums::{Action, GameConfiguration}; +use crate::enums::{Action, GameConfiguration, MapType}; use crate::global_state::GlobalState; use crate::map_instance::MapInstance; -use crate::player::Player; +use crate::players::{Player, RandomPlayer}; use crate::state::State; +use pyo3::prelude::*; pub fn play_game( global_state: GlobalState, @@ -54,7 +55,7 @@ mod tests { use super::*; use crate::{ enums::{Action, ActionPrompt, MapType}, - player::RandomPlayer, + players::RandomPlayer, }; fn setup_game( @@ -237,3 +238,44 @@ mod tests { ); } } + +#[pyclass(unsendable)] +pub struct Game { + num_players: usize, + config: GameConfiguration, +} + +#[pymethods] +impl Game { + #[new] + fn new(num_players: usize) -> Self { + let config = GameConfiguration { + discard_limit: 7, + vps_to_win: 10, + map_type: MapType::Base, + num_players: num_players as u8, + max_ticks: 1000, + }; + Game { + num_players, + config, + } + } + + fn play(&self) { + let global_state = GlobalState::new(); + let mut players = HashMap::new(); + for i in 0..self.num_players { + players.insert(i as u8, Box::new(RandomPlayer {}) as Box); + } + if let Some(winner) = play_game(global_state, self.config.clone(), players) { + println!("Player {} won!", winner); + } else { + println!("Game ended without a winner"); + } + } + + fn get_num_players(&self) -> usize { + self.num_players + } +} diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index 0ffade796..bc8da9772 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,3 +1,5 @@ +use pyo3::prelude::*; + pub mod deck_slices; pub mod decks; pub mod enums; @@ -6,6 +8,12 @@ pub mod global_state; pub mod map_instance; pub mod map_template; mod ordered_hashmap; -pub mod player; +pub mod players; pub mod state; pub mod state_vector; + +#[pymodule] +fn catanatron_rust(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/catanatron_rust/src/player.rs b/catanatron_rust/src/player.rs deleted file mode 100644 index 20d10c729..000000000 --- a/catanatron_rust/src/player.rs +++ /dev/null @@ -1,17 +0,0 @@ -use rand::Rng; - -use crate::{enums::Action, state::State}; - -pub trait Player { - fn decide(&self, state: &State, playable_actions: &[Action]) -> Action; -} - -pub struct RandomPlayer {} - -impl Player for RandomPlayer { - fn decide(&self, _state: &State, playable_actions: &[Action]) -> Action { - let mut rng = rand::thread_rng(); - let index = rng.gen_range(0..playable_actions.len()); - playable_actions[index] - } -} diff --git a/catanatron_rust/src/players/mod.rs b/catanatron_rust/src/players/mod.rs new file mode 100644 index 000000000..c09ddf39c --- /dev/null +++ b/catanatron_rust/src/players/mod.rs @@ -0,0 +1,10 @@ +mod random_player; + +use crate::enums::Action; +use crate::state::State; + +pub trait Player { + fn decide(&self, state: &State, playable_actions: &[Action]) -> Action; +} + +pub use random_player::RandomPlayer; \ No newline at end of file diff --git a/catanatron_rust/src/players/random_player.rs b/catanatron_rust/src/players/random_player.rs new file mode 100644 index 000000000..ce92659a1 --- /dev/null +++ b/catanatron_rust/src/players/random_player.rs @@ -0,0 +1,17 @@ +use rand::prelude::*; + +use crate::enums::Action; +use crate::state::State; +use super::Player; + +pub struct RandomPlayer {} + +impl Player for RandomPlayer { + fn decide(&self, _state: &State, playable_actions: &[Action]) -> Action { + let mut rng = rand::thread_rng(); + playable_actions + .choose(&mut rng) + .expect("There should always be at least one playable action") + .clone() + } +} \ No newline at end of file From 40a2a625ed9e1355edce2bb027e2b35fd65d4728 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 1 Mar 2025 08:12:18 -0600 Subject: [PATCH 80/83] debug logs --- catanatron_rust/Cargo.lock | 97 ++++++++++++++++++- catanatron_rust/Cargo.toml | 2 + catanatron_rust/src/game.rs | 24 ++--- catanatron_rust/src/lib.rs | 9 ++ catanatron_rust/src/main.rs | 44 +++++---- catanatron_rust/src/players/mod.rs | 2 +- catanatron_rust/src/players/random_player.rs | 4 +- catanatron_rust/src/state.rs | 5 +- catanatron_rust/src/state/move_application.rs | 19 +++- catanatron_rust/src/state/move_generation.rs | 9 +- 10 files changed, 172 insertions(+), 43 deletions(-) diff --git a/catanatron_rust/Cargo.lock b/catanatron_rust/Cargo.lock index da2496e6c..66381aee2 100644 --- a/catanatron_rust/Cargo.lock +++ b/catanatron_rust/Cargo.lock @@ -17,12 +17,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -58,6 +102,8 @@ name = "catanatron_rust" version = "0.1.0" dependencies = [ "criterion", + "env_logger", + "log", "pyo3", "rand", ] @@ -120,6 +166,12 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "criterion" version = "0.5.1" @@ -193,6 +245,29 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -220,6 +295,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "indoc" version = "1.0.9" @@ -237,6 +318,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -279,9 +366,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "memchr" @@ -654,6 +741,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/catanatron_rust/Cargo.toml b/catanatron_rust/Cargo.toml index 957dab36b..e22dc33f3 100644 --- a/catanatron_rust/Cargo.toml +++ b/catanatron_rust/Cargo.toml @@ -15,6 +15,8 @@ path = "src/main.rs" [dependencies] rand = "0.8" pyo3 = { version = "0.19", features = ["extension-module"] } +log = "0.4.26" +env_logger = "0.11.6" [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 04a0c0022..6db17391f 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -1,3 +1,4 @@ +use log::{debug, info}; use std::collections::HashMap; use std::rc::Rc; @@ -19,12 +20,12 @@ pub fn play_game( 0, ); let rc_config = Rc::new(config); - println!("Playing game with configuration: {:?}", rc_config); + info!("Playing game with configuration: {:?}", rc_config); let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); - println!("Seat order: {:?}", state.get_seating_order()); + info!("Seat order: {:?}", state.get_seating_order()); let mut num_ticks = 0; while state.winner().is_none() && num_ticks < rc_config.max_ticks { - println!("Tick {:?} =====", num_ticks); + debug!("Tick {:?} =====", num_ticks); play_tick(&players, &mut state); num_ticks += 1; } @@ -36,12 +37,12 @@ fn play_tick(players: &HashMap>, state: &mut State) -> Actio let current_player = players.get(¤t_color).unwrap(); let playable_actions = state.generate_playable_actions(); - println!( + debug!( "Player {:?} has {:?} playable actions", current_color, playable_actions ); let action = current_player.decide(state, &playable_actions); - println!( + debug!( "Player {:?} decided to play action {:?}", current_color, action ); @@ -158,7 +159,7 @@ mod tests { } else { panic!("Expected Action::BuildSettlement"); } - println!("{}", second_player_second_node_id); + debug!("{}", second_player_second_node_id); // second player road 2 let playable_actions = state.generate_playable_actions(); @@ -256,22 +257,21 @@ impl Game { num_players: num_players as u8, max_ticks: 1000, }; - Game { + Game { num_players, config, } } - + fn play(&self) { let global_state = GlobalState::new(); let mut players = HashMap::new(); for i in 0..self.num_players { players.insert(i as u8, Box::new(RandomPlayer {}) as Box); } - if let Some(winner) = play_game(global_state, self.config.clone(), players) { - println!("Player {} won!", winner); - } else { - println!("Game ended without a winner"); + match play_game(global_state, self.config.clone(), players) { + Some(winner) => info!("Game completed - Player {} won!", winner), + None => info!("Game ended without a winner"), } } diff --git a/catanatron_rust/src/lib.rs b/catanatron_rust/src/lib.rs index bc8da9772..589a8c45e 100644 --- a/catanatron_rust/src/lib.rs +++ b/catanatron_rust/src/lib.rs @@ -1,4 +1,6 @@ +use log::info; use pyo3::prelude::*; +use std::sync::Once; pub mod deck_slices; pub mod decks; @@ -12,8 +14,15 @@ pub mod players; pub mod state; pub mod state_vector; +// Ensure logger is initialized only once for Python module +static PYTHON_LOGGER_INIT: Once = Once::new(); + #[pymodule] fn catanatron_rust(_py: Python, m: &PyModule) -> PyResult<()> { + PYTHON_LOGGER_INIT.call_once(|| { + env_logger::init(); + info!("Initialized catanatron_rust logging"); + }); m.add_class::()?; Ok(()) } diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index a125eee75..13e1ef562 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -3,14 +3,18 @@ use catanatron_rust::enums::{Color, GameConfiguration, MapType, Resource, COLORS use catanatron_rust::game::play_game; use catanatron_rust::global_state; use catanatron_rust::map_instance::MapInstance; -use catanatron_rust::player::{Player, RandomPlayer}; +use catanatron_rust::players::{Player, RandomPlayer}; use catanatron_rust::state_vector; +use env_logger; +use log::{debug, info}; use std::collections::HashMap; use std::time::Instant; fn main() { + env_logger::init(); + // Benchmark deck operations - println!("Starting benchmark of Deck operations..."); + info!("Starting benchmark of Deck operations..."); let start = Instant::now(); let mut deck = decks::ResourceDeck::starting_resource_bank(); for _ in 0..1_000_000 { @@ -21,11 +25,11 @@ fn main() { } } let duration = start.elapsed(); - println!("Time taken for 1,000,000 deck operations: {:?}", duration); - println!("Total cards in deck: {}", deck.total_cards()); + info!("Time taken for 1,000,000 deck operations: {:?}", duration); + info!("Total cards in deck: {}", deck.total_cards()); // Benchmark copy operations - println!("Starting benchmark of Deck operations..."); + info!("Starting benchmark of Deck operations..."); let start = Instant::now(); let vector = vec![0u8; 600]; let mut copied_vector = vector.clone(); @@ -34,11 +38,11 @@ fn main() { copied_vector[index] = index as u8; } let duration = start.elapsed(); - println!("Time taken for 1,000,000 copy operations: {:?}", duration); - println!("Copy Results: {:?}, {:?}", vector, copied_vector); + info!("Time taken for 1,000,000 copy operations: {:?}", duration); + debug!("Copy Results: {:?}, {:?}", vector, copied_vector); // Benchmark array copy operations - println!("Starting benchmark of Array operations..."); + info!("Starting benchmark of Array operations..."); let start = Instant::now(); let array = [0u8; 1200]; let mut copied_array = array; @@ -47,43 +51,43 @@ fn main() { copied_array[index] = index as u8; } let duration = start.elapsed(); - println!("Time taken for 1,000,000 array operations: {:?}", duration); - println!("Copy Results: {:?}, {:?}", array, copied_array); + info!("Time taken for 1,000,000 array operations: {:?}", duration); + debug!("Copy Results: {:?}, {:?}", array, copied_array); let global_state = global_state::GlobalState::new(); - println!("Global State: {:?}", global_state); + debug!("Global State: {:?}", global_state); let size = state_vector::get_state_array_size(2); - println!("Vector length: {}", size); + debug!("Vector length: {}", size); let vector = state_vector::initialize_state(4); - println!("Vector: {:?}", vector); + debug!("Vector: {:?}", vector); let map_instance = MapInstance::new( &global_state.base_map_template, &global_state.dice_probas, 0, ); - println!("Map Instance Tiles: {:?}", map_instance.get_tile((0, 0, 0))); - println!( + debug!("Map Instance Tiles: {:?}", map_instance.get_tile((0, 0, 0))); + debug!( "Map Instance Land Tiles: {:?}", map_instance.get_land_tile((1, 0, -1)) ); - println!("Colors slice: {:?}", state_vector::seating_order_slice(4)); - println!("Colors {:?}", COLORS); + debug!("Colors slice: {:?}", state_vector::seating_order_slice(4)); + debug!("Colors {:?}", COLORS); let config = GameConfiguration { discard_limit: 7, vps_to_win: 10, map_type: MapType::Base, - num_players: 2, - max_ticks: 8, + num_players: 4, + max_ticks: 10000, }; let mut players: HashMap> = HashMap::new(); players.insert(Color::Red as u8, Box::new(RandomPlayer {})); players.insert(Color::Blue as u8, Box::new(RandomPlayer {})); let result = play_game(global_state, config, players); - println!("Game result: {:?}", result); + info!("Game result: {:?}", result); } diff --git a/catanatron_rust/src/players/mod.rs b/catanatron_rust/src/players/mod.rs index c09ddf39c..d116d275d 100644 --- a/catanatron_rust/src/players/mod.rs +++ b/catanatron_rust/src/players/mod.rs @@ -7,4 +7,4 @@ pub trait Player { fn decide(&self, state: &State, playable_actions: &[Action]) -> Action; } -pub use random_player::RandomPlayer; \ No newline at end of file +pub use random_player::RandomPlayer; diff --git a/catanatron_rust/src/players/random_player.rs b/catanatron_rust/src/players/random_player.rs index ce92659a1..c4fc19be2 100644 --- a/catanatron_rust/src/players/random_player.rs +++ b/catanatron_rust/src/players/random_player.rs @@ -1,8 +1,8 @@ use rand::prelude::*; +use super::Player; use crate::enums::Action; use crate::state::State; -use super::Player; pub struct RandomPlayer {} @@ -14,4 +14,4 @@ impl Player for RandomPlayer { .expect("There should always be at least one playable action") .clone() } -} \ No newline at end of file +} diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 63bc9a014..5628e9202 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -56,7 +56,10 @@ impl State { let buildings_by_color = HashMap::new(); let roads = HashMap::new(); let roads_by_color = vec![0; config.num_players as usize]; - let connected_components = HashMap::new(); + let mut connected_components = HashMap::new(); + for color in 0..config.num_players { + connected_components.insert(color, Vec::new()); + } let longest_road_color = None; let longest_road_length = 0; let largest_army_color = None; diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index d4a7b098c..44871c1f3 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -1,3 +1,4 @@ +use log::debug; use std::collections::{HashMap, HashSet}; use rand::Rng; @@ -16,11 +17,27 @@ impl State { pub fn apply_action(&mut self, action: Action) { match action { Action::BuildSettlement { color, node_id } => { + debug!( + "Building settlement for player {} at node {}", + color, node_id + ); let (new_owner, new_length) = self.build_settlement(color, node_id); + debug!( + "Settlement built. New longest road: owner={:?}, length={}", + new_owner, new_length + ); self.maintain_longest_road(new_owner, new_length); } Action::BuildRoad { color, edge_id } => { + debug!( + "Building road for player {} at edge ({}, {})", + color, edge_id.0, edge_id.1 + ); let (new_owner, new_length) = self.build_road(color, edge_id); + debug!( + "Road built. New longest road: owner={:?}, length={}", + new_owner, new_length + ); self.maintain_longest_road(new_owner, new_length); } Action::BuildCity { color, node_id } => { @@ -71,7 +88,7 @@ impl State { } } - println!("Applying action {:?}", action); + debug!("Finished applying action: {:?}", action); } pub fn add_victory_points(&mut self, color: u8, points: u8) { diff --git a/catanatron_rust/src/state/move_generation.rs b/catanatron_rust/src/state/move_generation.rs index 2e68a05cd..f4259c9a5 100644 --- a/catanatron_rust/src/state/move_generation.rs +++ b/catanatron_rust/src/state/move_generation.rs @@ -159,7 +159,7 @@ impl State { pub fn play_turn_possibilities(&self, color: u8) -> Vec { if self.is_road_building() { if get_free_roads_available(&self.vector) == 0 { - return vec![]; + return vec![Action::EndTurn { color }]; // Always provide EndTurn } return self.road_possibilities(color, true); } else if !self.current_player_rolled() { @@ -312,9 +312,9 @@ impl State { let hand = self.get_player_hand(color); let total_cards: u8 = hand.iter().sum(); - // If player has 7 or fewer cards, they don't need to discard + // If player has 7 or fewer cards, they can just end their turn if total_cards <= 7 { - return vec![]; + return vec![Action::EndTurn { color }]; } // For now, just generate a single discard action @@ -615,7 +615,8 @@ mod tests { hand[0] = 7; } let actions = state.discard_possibilities(color); - assert_eq!(actions.len(), 0); + assert_eq!(actions.len(), 1); + assert!(matches!(actions[0], Action::EndTurn { color: c } if c == color)); } #[test] From 7dfbba68c74e3bac55532e9685a4ed170c985c47 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 1 Mar 2025 10:16:49 -0600 Subject: [PATCH 81/83] debug logs and bugfixes --- catanatron_rust/src/game.rs | 43 ++++- catanatron_rust/src/main.rs | 2 + catanatron_rust/src/state.rs | 38 +++- catanatron_rust/src/state/move_application.rs | 176 +++++++++++++----- catanatron_rust/src/state_vector.rs | 21 ++- 5 files changed, 230 insertions(+), 50 deletions(-) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 6db17391f..946c0a403 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -14,6 +14,12 @@ pub fn play_game( config: GameConfiguration, players: HashMap>, ) -> Option { + debug!( + "play_game: config={:?}, players={:?}", + config, + players.keys().collect::>() + ); + let map_instance = MapInstance::new( &global_state.base_map_template, &global_state.dice_probas, @@ -22,6 +28,13 @@ pub fn play_game( let rc_config = Rc::new(config); info!("Playing game with configuration: {:?}", rc_config); let mut state = State::new(rc_config.clone(), Rc::new(map_instance)); + + debug!( + "State initialized: current_tick_seat={}, current_color={}", + state.get_current_tick_seat(), + state.get_current_color() + ); + info!("Seat order: {:?}", state.get_seating_order()); let mut num_ticks = 0; while state.winner().is_none() && num_ticks < rc_config.max_ticks { @@ -34,7 +47,24 @@ pub fn play_game( fn play_tick(players: &HashMap>, state: &mut State) -> Action { let current_color = state.get_current_color(); - let current_player = players.get(¤t_color).unwrap(); + debug!( + "play_tick: current_color={}, players={:?}, action_prompt={:?}", + current_color, + players.keys().collect::>(), + state.get_action_prompt() + ); + + let current_player = match players.get(¤t_color) { + Some(player) => player, + None => { + debug!( + "ERROR: No player found for color {}. Available players: {:?}", + current_color, + players.keys().collect::>() + ); + panic!("No player found for color {}", current_color); + } + }; let playable_actions = state.generate_playable_actions(); debug!( @@ -244,6 +274,7 @@ mod tests { pub struct Game { num_players: usize, config: GameConfiguration, + winner: Option, } #[pymethods] @@ -260,16 +291,18 @@ impl Game { Game { num_players, config, + winner: None, } } - fn play(&self) { + fn play(&mut self) { let global_state = GlobalState::new(); let mut players = HashMap::new(); for i in 0..self.num_players { players.insert(i as u8, Box::new(RandomPlayer {}) as Box); } - match play_game(global_state, self.config.clone(), players) { + self.winner = play_game(global_state, self.config.clone(), players); + match self.winner { Some(winner) => info!("Game completed - Player {} won!", winner), None => info!("Game ended without a winner"), } @@ -278,4 +311,8 @@ impl Game { fn get_num_players(&self) -> usize { self.num_players } + + fn get_winner(&self) -> Option { + self.winner + } } diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 13e1ef562..66998c1b1 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -87,6 +87,8 @@ fn main() { let mut players: HashMap> = HashMap::new(); players.insert(Color::Red as u8, Box::new(RandomPlayer {})); players.insert(Color::Blue as u8, Box::new(RandomPlayer {})); + players.insert(Color::Orange as u8, Box::new(RandomPlayer {})); + players.insert(Color::White as u8, Box::new(RandomPlayer {})); let result = play_game(global_state, config, players); info!("Game result: {:?}", result); diff --git a/catanatron_rust/src/state.rs b/catanatron_rust/src/state.rs index 5628e9202..e74d6028b 100644 --- a/catanatron_rust/src/state.rs +++ b/catanatron_rust/src/state.rs @@ -1,3 +1,4 @@ +use log::debug; use std::{ collections::{HashMap, HashSet}, rc::Rc, @@ -49,7 +50,17 @@ mod move_generation; impl State { pub fn new(config: Rc, map_instance: Rc) -> Self { + debug!( + "State::new: config={:?}, num_players={}", + config, config.num_players + ); + let vector = initialize_state(config.num_players); + debug!( + "State::new: vector initialized, length={}, seating_order={:?}", + vector.len(), + &vector[seating_order_slice(config.num_players as usize)] + ); let board_buildable_ids = map_instance.land_nodes().clone(); let buildings = HashMap::new(); @@ -124,7 +135,15 @@ impl State { /// Returns a slice of Colors in the order of seating /// e.g. [2, 1, 0, 3] if Orange goes first, then Blue, then Red, and then White pub fn get_seating_order(&self) -> &[u8] { - &self.vector[seating_order_slice(self.config.num_players as usize)] + let slice = seating_order_slice(self.config.num_players as usize); + debug!( + "get_seating_order: num_players={}, slice={:?}, vector.len()={}", + self.config.num_players, + slice, + self.vector.len() + ); + + &self.vector[slice] } pub fn get_current_tick_seat(&self) -> u8 { @@ -134,6 +153,23 @@ impl State { pub fn get_current_color(&self) -> u8 { let seating_order = self.get_seating_order(); let current_tick_seat = self.get_current_tick_seat(); + debug!( + "get_current_color: seating_order={:?}, current_tick_seat={}, seating_order.len()={}", + seating_order, + current_tick_seat, + seating_order.len() + ); + + if current_tick_seat as usize >= seating_order.len() { + debug!( + "ERROR: current_tick_seat {} is out of bounds for seating_order of length {}", + current_tick_seat, + seating_order.len() + ); + // Return a default value to avoid panic + return 0; + } + seating_order[current_tick_seat as usize] } diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 44871c1f3..2df842c7b 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -15,6 +15,15 @@ use super::State; impl State { pub fn apply_action(&mut self, action: Action) { + debug!("Applying action: {:?}", action); + debug!("Current state: current_color={}, action_prompt={:?}, is_initial_build_phase={}, buildings={}, roads={}", + self.get_current_color(), + self.get_action_prompt(), + self.is_initial_build_phase(), + self.buildings.len(), + self.roads.len() / 2 + ); + match action { Action::BuildSettlement { color, node_id } => { debug!( @@ -89,6 +98,13 @@ impl State { } debug!("Finished applying action: {:?}", action); + debug!("Updated state: current_color={}, action_prompt={:?}, is_initial_build_phase={}, buildings={}, roads={}", + self.get_current_color(), + self.get_action_prompt(), + self.is_initial_build_phase(), + self.buildings.len(), + self.roads.len() / 2 + ); } pub fn add_victory_points(&mut self, color: u8, points: u8) { @@ -275,26 +291,55 @@ impl State { let a_index = self.get_connected_component_index(placing_color, a); let b_index = self.get_connected_component_index(placing_color, b); + // Make sure the connected_components for this color exists + if !self.connected_components.contains_key(&placing_color) { + self.connected_components.insert(placing_color, Vec::new()); + } + 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(); - component.insert(a); // extend said component by 1 more node - component.clone() + if let Some(b_idx) = b_index { + let component = self + .connected_components + .get_mut(&placing_color) + .unwrap() + .get_mut(b_idx) + .unwrap(); + component.insert(a); // extend said component by 1 more node + component.clone() + } else { + // Neither a nor b are in any component, create a new one + let mut new_component = HashSet::new(); + new_component.insert(a); + new_component.insert(b); + self.connected_components + .get_mut(&placing_color) + .unwrap() + .push(new_component.clone()); + new_component + } } 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(); - component.insert(b); - component.clone() + if let Some(a_idx) = a_index { + let component = self + .connected_components + .get_mut(&placing_color) + .unwrap() + .get_mut(a_idx) + .unwrap(); + component.insert(b); + component.clone() + } else { + // This should not happen, but handle it anyway + let mut new_component = HashSet::new(); + new_component.insert(a); + new_component.insert(b); + self.connected_components + .get_mut(&placing_color) + .unwrap() + .push(new_component.clone()); + new_component + } } 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()); @@ -312,7 +357,7 @@ impl State { .unwrap(); kept_component.extend(&removed_component); kept_component.clone() - } else { + } else if a_index.is_some() { // 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. @@ -322,6 +367,16 @@ impl State { .get(a_index.unwrap()) .unwrap() .clone() + } else { + // Neither a nor b are in any component, create a new one + let mut new_component = HashSet::new(); + new_component.insert(a); + new_component.insert(b); + self.connected_components + .get_mut(&placing_color) + .unwrap() + .push(new_component.clone()); + new_component }; let prev_road_color = self.longest_road_color; @@ -466,6 +521,8 @@ impl State { return; } + debug!("Roll {} yields: {:?}", roll, yields); + // Calculate total needed by resource type let mut resource_needs = [0u8; 5]; for (_, resource_idx, amount) in &yields { @@ -474,43 +531,74 @@ impl State { // 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; + debug!( + "Current bank: [{}, {}, {}, {}, {}]", + bank[0], bank[1], bank[2], bank[3], bank[4] + ); + debug!("Total resource needs: {:?}", resource_needs); + + // For each resource type, determine if multiple players need it + let mut resource_recipients = [Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new()]; + for (color, resource_idx, _) in &yields { + if !resource_recipients[*resource_idx].contains(color) { + resource_recipients[*resource_idx].push(*color); + } + } + // Determine which resources can be distributed + let mut can_distribute = [true; 5]; for i in 0..5 { if bank[i] < resource_needs[i] { - if multiple_recipients { - // If not enough for everyone, no one gets anything - return; + // Resource is insufficient + if resource_recipients[i].len() > 1 { + // Multiple players need this resource - no one gets it + debug!( + "Resource {}: insufficient for multiple recipients ({}), no one gets it", + i, + resource_recipients[i].len() + ); + can_distribute[i] = false; + } else { + debug!("Resource {}: insufficient for single recipient, will distribute what's available", i); + // Single player - they get what's available (handled during distribution) } - 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; - } + // Make a copy of bank resources to track what's distributed + let mut remaining = [0u8; 5]; + for i in 0..5 { + remaining[i] = bank[i]; + } + + // Distribute resources according to the rules + for (owner_color, resource_idx, amount) in yields { + if !can_distribute[resource_idx] { + // Skip resources that can't be distributed + continue; } - } 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; + + // Calculate how much to give (either full amount or what's available) + let available = remaining[resource_idx].min(amount); + if available > 0 { + // Update tracking of what's left + remaining[resource_idx] -= available; + + // Update actual game state + self.vector[BANK_RESOURCE_SLICE][resource_idx] -= available; + self.get_mut_player_hand(owner_color)[resource_idx] += available; + + debug!( + "Distributed {} of resource {} to player {}", + available, resource_idx, owner_color + ); } } + + debug!( + "Bank after distribution: {:?}", + &self.vector[BANK_RESOURCE_SLICE] + ); } /* diff --git a/catanatron_rust/src/state_vector.rs b/catanatron_rust/src/state_vector.rs index 80cc3f149..1482a7ec0 100644 --- a/catanatron_rust/src/state_vector.rs +++ b/catanatron_rust/src/state_vector.rs @@ -60,6 +60,8 @@ pub fn get_state_array_size(num_players: usize) -> usize { // TODO: Hardcoded for BASE_MAP let n = num_players; + log::debug!("get_state_array_size: num_players={}, n={}", num_players, n); + let mut size: usize = 0; // Trying to have as most fixed-size vector first as possible // so that we can understand/debug all configurations similarly. @@ -107,7 +109,14 @@ pub fn bank_resource_index(resource: u8) -> usize { } pub fn seating_order_slice(num_players: usize) -> std::ops::Range { - PLAYER_STATE_START_INDEX..PLAYER_STATE_START_INDEX + num_players + let slice = PLAYER_STATE_START_INDEX..PLAYER_STATE_START_INDEX + num_players; + log::debug!( + "seating_order_slice: num_players={}, PLAYER_STATE_START_INDEX={}, slice={:?}", + num_players, + PLAYER_STATE_START_INDEX, + slice + ); + slice } pub fn actual_victory_points_index(num_players: u8, color: u8) -> usize { @@ -171,9 +180,17 @@ pub fn take_next_dev_card(vector: &mut StateVector) -> Option { /// faster rollouts. This one is compact optimized for copying. /// TODO: Accept a seed for deterministic tests pub fn initialize_state(num_players: u8) -> Vec { - let n = num_players as usize; + log::debug!( + "initialize_state: num_players={}, PLAYER_STATE_START_INDEX={}", + num_players, + PLAYER_STATE_START_INDEX + ); + let n = num_players as usize; let size = get_state_array_size(n); + + log::debug!("initialize_state: size={}, n={}", size, n); + let mut vector = vec![0; size]; // Initialize Bank Resources From a6a93eaa8767e7038ace57a3d3209492bbf958f1 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 1 Mar 2025 10:17:07 -0600 Subject: [PATCH 82/83] Update maturin consumer --- catanatron_core/catanatron/test_rust_game.py | 43 ++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/catanatron_core/catanatron/test_rust_game.py b/catanatron_core/catanatron/test_rust_game.py index a381e20ff..691b2d93c 100644 --- a/catanatron_core/catanatron/test_rust_game.py +++ b/catanatron_core/catanatron/test_rust_game.py @@ -1,10 +1,47 @@ -def test_rust_game_creation(): +#!/usr/bin/env python +""" +Simple script to run a Catan game and display the winner. +""" +import time + + +def main(): try: from catanatron_rust import Game + + # Create and play the game game = Game(4) - assert game.get_num_players() == 4 + print(f"Created game with {game.get_num_players()} players") + print("Game configuration:") + print("- Victory points to win: 10") + print("- Maximum ticks: 1000") + print("\nStarting game simulation...") + start_time = time.time() game.play() + end_time = time.time() + elapsed_time = end_time - start_time + + # Get and display the winner + winner = game.get_winner() + + print("\n" + "=" * 50) + print(f"Game completed in {elapsed_time:.2f} seconds") + + if winner is not None: + print(f"WINNER: Player {winner} reached 10 victory points!") + else: + print("NO WINNER: Game reached 1000 ticks without a winner") + print("This happens when no player reaches 10 victory points") + print("within the maximum number of turns.") + print("=" * 50) + except ImportError as e: print(f"Failed to import catanatron_rust: {e}") - raise \ No newline at end of file + return 1 + + return 0 + + +if __name__ == "__main__": + main() From 6962369c1b93ce67c6062067b97e78615d7b78a3 Mon Sep 17 00:00:00 2001 From: Mason Zarns Date: Sat, 1 Mar 2025 12:58:10 -0600 Subject: [PATCH 83/83] Fix maintain connected components --- catanatron_core/catanatron/test_rust_game.py | 4 +- catanatron_rust/src/game.rs | 4 +- catanatron_rust/src/main.rs | 1 - catanatron_rust/src/players/random_player.rs | 3 +- catanatron_rust/src/state/move_application.rs | 235 +++++++++++------- 5 files changed, 152 insertions(+), 95 deletions(-) diff --git a/catanatron_core/catanatron/test_rust_game.py b/catanatron_core/catanatron/test_rust_game.py index 691b2d93c..73829c340 100644 --- a/catanatron_core/catanatron/test_rust_game.py +++ b/catanatron_core/catanatron/test_rust_game.py @@ -14,7 +14,7 @@ def main(): print(f"Created game with {game.get_num_players()} players") print("Game configuration:") print("- Victory points to win: 10") - print("- Maximum ticks: 1000") + print("- Maximum ticks: 10000") print("\nStarting game simulation...") start_time = time.time() @@ -31,7 +31,7 @@ def main(): if winner is not None: print(f"WINNER: Player {winner} reached 10 victory points!") else: - print("NO WINNER: Game reached 1000 ticks without a winner") + print("NO WINNER: Game reached 10000 ticks without a winner") print("This happens when no player reaches 10 victory points") print("within the maximum number of turns.") print("=" * 50) diff --git a/catanatron_rust/src/game.rs b/catanatron_rust/src/game.rs index 946c0a403..07ced0470 100644 --- a/catanatron_rust/src/game.rs +++ b/catanatron_rust/src/game.rs @@ -286,7 +286,7 @@ impl Game { vps_to_win: 10, map_type: MapType::Base, num_players: num_players as u8, - max_ticks: 1000, + max_ticks: 10000, }; Game { num_players, @@ -311,7 +311,7 @@ impl Game { fn get_num_players(&self) -> usize { self.num_players } - + fn get_winner(&self) -> Option { self.winner } diff --git a/catanatron_rust/src/main.rs b/catanatron_rust/src/main.rs index 66998c1b1..a4ff4a285 100644 --- a/catanatron_rust/src/main.rs +++ b/catanatron_rust/src/main.rs @@ -5,7 +5,6 @@ use catanatron_rust::global_state; use catanatron_rust::map_instance::MapInstance; use catanatron_rust::players::{Player, RandomPlayer}; use catanatron_rust::state_vector; -use env_logger; use log::{debug, info}; use std::collections::HashMap; use std::time::Instant; diff --git a/catanatron_rust/src/players/random_player.rs b/catanatron_rust/src/players/random_player.rs index c4fc19be2..1d9d39b3e 100644 --- a/catanatron_rust/src/players/random_player.rs +++ b/catanatron_rust/src/players/random_player.rs @@ -9,9 +9,8 @@ pub struct RandomPlayer {} impl Player for RandomPlayer { fn decide(&self, _state: &State, playable_actions: &[Action]) -> Action { let mut rng = rand::thread_rng(); - playable_actions + *playable_actions .choose(&mut rng) .expect("There should always be at least one playable action") - .clone() } } diff --git a/catanatron_rust/src/state/move_application.rs b/catanatron_rust/src/state/move_application.rs index 2df842c7b..9ac9c411f 100644 --- a/catanatron_rust/src/state/move_application.rs +++ b/catanatron_rust/src/state/move_application.rs @@ -292,92 +292,11 @@ impl State { let b_index = self.get_connected_component_index(placing_color, b); // Make sure the connected_components for this color exists - if !self.connected_components.contains_key(&placing_color) { - self.connected_components.insert(placing_color, Vec::new()); - } + self.connected_components.entry(placing_color).or_default(); - 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) - if let Some(b_idx) = b_index { - let component = self - .connected_components - .get_mut(&placing_color) - .unwrap() - .get_mut(b_idx) - .unwrap(); - component.insert(a); // extend said component by 1 more node - component.clone() - } else { - // Neither a nor b are in any component, create a new one - let mut new_component = HashSet::new(); - new_component.insert(a); - new_component.insert(b); - self.connected_components - .get_mut(&placing_color) - .unwrap() - .push(new_component.clone()); - new_component - } - } 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) - if let Some(a_idx) = a_index { - let component = self - .connected_components - .get_mut(&placing_color) - .unwrap() - .get_mut(a_idx) - .unwrap(); - component.insert(b); - component.clone() - } else { - // This should not happen, but handle it anyway - let mut new_component = HashSet::new(); - new_component.insert(a); - new_component.insert(b); - self.connected_components - .get_mut(&placing_color) - .unwrap() - .push(new_component.clone()); - new_component - } - } 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() - .remove(larger_idx); - 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 if a_index.is_some() { - // 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() - } else { - // Neither a nor b are in any component, create a new one - let mut new_component = HashSet::new(); - new_component.insert(a); - new_component.insert(b); - self.connected_components - .get_mut(&placing_color) - .unwrap() - .push(new_component.clone()); - new_component - }; + // Update connected components based on the new road + let affected_component = + self.update_connected_components(placing_color, a, b, a_index, b_index); let prev_road_color = self.longest_road_color; @@ -395,6 +314,82 @@ impl State { (new_road_color, new_road_length) } + /// Updates the road network when a new road is built + /// + /// This method maintains the connected components for a player's road network: + /// - Merges components when a road connects two previously separate networks + /// - Extends an existing component when a road connects to it + /// - Creates a new component for isolated roads + /// + /// The function also handles enemy settlements that would block connections. + /// + /// Returns the affected component that contains the new road. + fn update_connected_components( + &mut self, + placing_color: u8, + a: NodeId, + b: NodeId, + a_index: Option, + b_index: Option, + ) -> HashSet { + // Pre-compute node validity before mutable borrow + let a_valid = !self.is_enemy_node(placing_color, a); + let b_valid = !self.is_enemy_node(placing_color, b); + + // Get the components list for this color, creating it if it doesn't exist + let components = self.connected_components.entry(placing_color).or_default(); + + // Case 1: Both nodes are in components + if let (Some(a_idx), Some(b_idx)) = (a_index, b_index) { + if a_idx == b_idx { + // Both in same component - no change needed + return components[a_idx].clone(); + } + + // Merge components - always merge into the component with smaller index + // to minimize shifts in the vector + let (keep_idx, remove_idx) = if a_idx < b_idx { + (a_idx, b_idx) + } else { + (b_idx, a_idx) + }; + + let removed = components.remove(remove_idx); + components[keep_idx].extend(removed); + return components[keep_idx].clone(); + } + + // Case 2: Only one node is in a component - extend that component + if let Some(idx) = a_index.or(b_index) { + let component = &mut components[idx]; + + // Add the node that isn't in a component if it's valid + let new_node = if a_index.is_some() { b } else { a }; + let is_valid = if a_index.is_some() { b_valid } else { a_valid }; + + if is_valid { + component.insert(new_node); + } + + return component.clone(); + } + + // Case 3: Neither node is in a component - create a new one with valid nodes + let mut new_component = HashSet::new(); + if a_valid { + new_component.insert(a); + } + if b_valid { + new_component.insert(b); + } + + if !new_component.is_empty() { + components.push(new_component.clone()); + } + + new_component + } + fn build_city(&mut self, color: u8, node_id: u8) { self.buildings .insert(node_id, Building::City(color, node_id)); @@ -567,9 +562,7 @@ impl State { // Make a copy of bank resources to track what's distributed let mut remaining = [0u8; 5]; - for i in 0..5 { - remaining[i] = bank[i]; - } + remaining.copy_from_slice(&bank[..5]); // Distribute resources according to the rules for (owner_color, resource_idx, amount) in yields { @@ -1694,4 +1687,70 @@ mod tests { assert_eq!(state.get_current_color(), starting_color); } + + #[test] + fn test_update_connected_components() { + // Create a test state + let mut state = State::new_base(); + let color = 0; // Red player + + // Create two separate components + let mut comp1 = HashSet::new(); + comp1.insert(0); + comp1.insert(1); + + let mut comp2 = HashSet::new(); + comp2.insert(3); + comp2.insert(4); + + state.connected_components.insert(color, vec![comp1, comp2]); + + // Add roads to connect the components + state.roads.insert((1, 2), color); + state.roads.insert((2, 3), color); + + // First update: Connect node 1 to node 2 + // Node 1 is in component index 0, node 2 is not in any component + let _updated_component = state.update_connected_components(color, 1, 2, Some(0), None); + + // Verify node 2 was added to the first component + let components = state.connected_components.get(&color).unwrap(); + assert_eq!(components.len(), 2, "Should still have two components"); + assert!( + components[0].contains(&2), + "First component should now contain node 2" + ); + + // Second update: Connect node 2 to node 3 + // After the first update, node 2 is now in component index 0 + // Node 3 is in component index 1 + let _updated_component = state.update_connected_components(color, 2, 3, Some(0), Some(1)); + + // Verify that the components were merged + let components = state.connected_components.get(&color).unwrap(); + assert_eq!(components.len(), 1, "Components should be merged into one"); + + // Verify the merged component contains all nodes + let merged = &components[0]; + assert!( + merged.contains(&0), + "Merged component should contain node 0" + ); + assert!( + merged.contains(&1), + "Merged component should contain node 1" + ); + assert!( + merged.contains(&2), + "Merged component should contain node 2" + ); + assert!( + merged.contains(&3), + "Merged component should contain node 3" + ); + assert!( + merged.contains(&4), + "Merged component should contain node 4" + ); + } }