From 9f38d6612ef091a6a5ef8c9b23ac559c60bb6b0f Mon Sep 17 00:00:00 2001 From: Daniel Olsen Date: Mon, 29 Jan 2024 19:25:56 +0100 Subject: [PATCH] azul: implement main structs, scoring, bonus and initializing game object --- Cargo.lock | 10 +- Cargo.toml | 2 + src/azul.rs | 462 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 + 4 files changed, 472 insertions(+), 4 deletions(-) create mode 100644 src/azul.rs diff --git a/Cargo.lock b/Cargo.lock index 16bc9ce..cf0ce3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,9 +442,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" dependencies = [ "equivalent", "hashbrown", @@ -629,7 +629,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" name = "ozai" version = "0.1.0" dependencies = [ + "rand", "rocket", + "serde", ] [[package]] @@ -989,9 +991,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.112" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 35a7500..d448270 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,4 +7,6 @@ edition = "2021" [dependencies] rocket = { version = "0.5.0", features = ["json"] } +serde = "1.0.196" +rand = "0.8.5" diff --git a/src/azul.rs b/src/azul.rs new file mode 100644 index 0000000..642578c --- /dev/null +++ b/src/azul.rs @@ -0,0 +1,462 @@ +use std::convert::TryFrom; +use std::ops::{Add, AddAssign}; + +use serde::{Deserialize, Serialize}; + +use rand::distributions::WeightedIndex; +use rand::prelude::*; + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct GameState { + current_player: usize, + bag: TileSet, + lid: TileSet, + factories: Vec, + market: TileSetWithStart, + players: Vec, +} + +impl GameState { + pub fn new(n_players: usize) -> Result { + if n_players < 2 { + return Err("Can't create game with less than two players"); + } + if n_players > 4 { + return Err("Can't create game with more than 4 players"); + } + if n_players != 2 { + return Err("Only two players is currently supported"); + } + + let n_factories = 1 + 2 * n_players; + + let mut factories: Vec = Vec::with_capacity(n_factories); + + // let mut factories + for _ in 0..n_factories { + factories.push(TileSet::default()); + } + + let mut players = Vec::with_capacity(n_players); + + for _ in 0..n_players { + players.push(Player::default()) + } + + let game = GameState { + current_player: 0, + bag: TileSet { + blue: 20, + yellow: 20, + red: 20, + black: 20, + white: 20, + }, + lid: TileSet::default(), + factories: factories, + market: TileSetWithStart { + start: 1, + blue: 0, + yellow: 0, + red: 0, + black: 0, + white: 0, + }, + players: players, + }; + + Ok(game) + } + + /// Fills the factories from the bag + /// Will replenish the bag from the lid when empty + /// Will return with partially filled factories if out of tiles + fn fill(&mut self, mut rng: StdRng) -> Result<(), &'static str> { + for factory in &self.factories { + if factory.len() != 0 { + return Err("Cannot fill, factories are not empty"); + } + } + for factory in &mut self.factories { + for _ in 0..4 { + if self.bag.is_empty() && !self.lid.is_empty() { + self.bag = self.lid.clone(); + self.lid = TileSet::default(); + } else if self.bag.is_empty() { + return Ok(()); + } else { + let choices = [ + Color::Blue, + Color::Yellow, + Color::Red, + Color::Black, + Color::White, + ]; + let weights = [ + self.bag.blue, + self.bag.yellow, + self.bag.red, + self.bag.black, + self.bag.white, + ]; + + let dist = WeightedIndex::new(&weights).unwrap(); + let picked = choices[dist.sample(&mut rng)]; + + self.bag.add_color(picked, 1)?; + factory.add_color(picked, 1)?; + } + } + } + Ok(()) + } + + /// Scores the game and clears the boards into the lid + /// Doesn't check if game is ready to be scored! + fn score(&mut self) -> Result<(), &'static str> { + for i in 0..self.players.len() { + let player = &mut self.players[i]; + + for row in 0..5 { + if player.pattern_lines[row].len() == (row + 1) { + let color = player.pattern_lines[row] + .color + .ok_or("patternline filled with None color")?; + let index = Player::get_wall_index(row, color)?; + player.wall[row][index] = true; + player.points += player.connected(row, index); + + self.lid.add_color(color, row)?; + player.pattern_lines[row] = PatternLine::default() + } + } + + let negative = match player.floor.len() { + 0 => 0, + 1 => 1, + 2 => 2, + 3 => 4, + 4 => 6, + 5 => 8, + 6 => 11, + _ => 14, + }; + player.points -= negative; + + if player.floor.start == 1 { + self.current_player = i; + player.floor.start = 0; + } + + self.lid += player.floor.try_into()?; + player.floor = TileSetWithStart::default() + } + + Ok(()) + } + + /// Calculates end-of-game bonus + fn bonus_score(player: &Player) -> Result { + let mut bonus = 0; + + // Horizontal + for row in player.wall { + if row.iter().all(|&x| x == true) { + bonus += 2; + }; + } + + // Vertical + for column in 0..5 { + let mut tiles = 0; + for row in 0..5 { + if player.wall[row][column] == true { + tiles += 1; + } + } + if tiles == 5 { + bonus += 7 + } + } + + // 5 of a color + for color in Color::Blue.into_iter() { + let mut tiles = 0; + for row in 0..5 { + let index = Player::get_wall_index(row, color)?; + if player.wall[row][index] == true { + tiles += 1 + } + } + + if tiles == 5 { + bonus += 10; + } + } + + return Ok(bonus); + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, Copy)] +struct TileSet { + blue: usize, + yellow: usize, + red: usize, + black: usize, + white: usize, +} + +impl TileSet { + fn len(&self) -> usize { + self.blue + self.yellow + self.red + self.black + self.white + } + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn add_color(&mut self, color: Color, n: usize) -> Result<(), &'static str> { + match color { + Color::Blue => self.blue += n, + Color::Yellow => self.yellow += n, + Color::Red => self.red += n, + Color::Black => self.black += n, + Color::White => self.white += n, + Color::Start => return Err("tried to add Start tiles to TileSet"), + } + + Ok(()) + } +} + +impl Add for TileSet { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { + blue: self.blue + other.blue, + yellow: self.yellow + other.yellow, + red: self.red + other.red, + black: self.black + other.black, + white: self.white + other.white, + } + } +} + +impl AddAssign for TileSet { + fn add_assign(&mut self, other: Self) { + *self = *self + other + } +} + +impl TryFrom for TileSet { + type Error = &'static str; + + fn try_from(value: TileSetWithStart) -> Result { + if value.start == 0 { + Ok(TileSet { + blue: value.blue, + yellow: value.yellow, + red: value.red, + black: value.black, + white: value.white, + }) + } else { + Err("Can't convert, tileset had a start tile inside") + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default, Copy)] +struct TileSetWithStart { + start: usize, + blue: usize, + yellow: usize, + red: usize, + black: usize, + white: usize, +} + +impl TileSetWithStart { + fn len(&self) -> usize { + self.blue + self.yellow + self.red + self.black + self.white + self.start + } + fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +struct Player { + points: u8, + pattern_lines: [PatternLine; 5], + wall: Wall, + floor: TileSetWithStart, +} + +impl Player { + fn get_wall_index(row: usize, color: Color) -> Result { + match (row, color) { + (0, Color::Blue) => Ok(0), + (0, Color::Yellow) => Ok(1), + (0, Color::Red) => Ok(2), + (0, Color::Black) => Ok(3), + (0, Color::White) => Ok(4), + + (1, Color::Blue) => Ok(1), + (1, Color::Yellow) => Ok(2), + (1, Color::Red) => Ok(3), + (1, Color::Black) => Ok(4), + (1, Color::White) => Ok(0), + + (2, Color::Blue) => Ok(2), + (2, Color::Yellow) => Ok(3), + (2, Color::Red) => Ok(4), + (2, Color::Black) => Ok(0), + (2, Color::White) => Ok(1), + + (3, Color::Blue) => Ok(3), + (3, Color::Yellow) => Ok(4), + (3, Color::Red) => Ok(0), + (3, Color::Black) => Ok(1), + (3, Color::White) => Ok(2), + + (4, Color::Blue) => Ok(4), + (4, Color::Yellow) => Ok(0), + (4, Color::Red) => Ok(1), + (4, Color::Black) => Ok(2), + (4, Color::White) => Ok(3), + + _ => return Err("Not a valid tile on the wall"), + } + } + + /// given a coordinate on the board, returns how many tiles are in its group + fn connected(&self, row: usize, column: usize) -> u8 { + // This implementation scans for contigious runs of tiles + // until it finds the group the tile we're checking is part of + // Does this for both the row and the column + // then returns the sum + + let wall = self.wall; + + if wall[row][column] == false { + return 0; + } + + let mut sum = 0; + + // Count connected tiles in the on the row + let mut count = 0; + let mut active = false; + for i in 0..5 { + if active == true && wall[row][i] == false { + break; + } else if wall[row][i] == false { + count = 0; + } else if (row, i) == (row, column) { + active = true; + count += 1; + } else { + count += 1 + } + } + sum += count; + + // Count connected tiles in the column + let mut count = 0; + let mut active = false; + for i in 0..5 { + if active == true && wall[i][column] == false { + break; + } else if wall[i][column] == false { + count = 0; + } else if (i, column) == (row, column) { + active = true; + count += 1; + } else { + count += 1 + } + } + sum += count; + + return sum; + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +struct PatternLine { + color: Option, + number: usize, +} + +impl PatternLine { + fn len(&self) -> usize { + self.number + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +enum Color { + Start, + Blue, + Yellow, + Red, + Black, + White, +} + +impl IntoIterator for Color { + type Item = Color; + type IntoIter = ColorIter; + + /// iterates through blue, yellow, red, black, and white + fn into_iter(self) -> Self::IntoIter { + ColorIter { current: self } + } +} + +struct ColorIter { + current: Color, +} + +impl Iterator for ColorIter { + type Item = Color; + + fn next(&mut self) -> Option { + match self.current { + Color::Blue => { + let next = Color::Yellow; + self.current = next; + Some(next) + } + Color::Yellow => { + let next = Color::Red; + self.current = next; + Some(next) + } + Color::Red => { + let next = Color::Black; + self.current = next; + Some(next) + } + Color::Black => { + let next = Color::White; + self.current = next; + Some(next) + } + _ => None, + } + } +} + +type Row = [bool; 5]; +type Wall = [Row; 5]; + +struct GameMove { + player: u8, + market: bool, + factory: u8, + color: Color, + pattern_line: usize, +} diff --git a/src/main.rs b/src/main.rs index bbdbb40..0fe82cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,8 @@ use rocket::serde::{json::Json, Serialize}; #[macro_use] extern crate rocket; +mod azul; + #[get("/")] fn index() -> String { "Hello, world!".to_string()