diff --git a/Cargo.toml b/Cargo.toml index bc4add1..e287100 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ tokio = "1.28.2" cargo-manifest = { version = "0.17.0", optional = true } serde_yml = { version = "0.0.12", optional = true } toml = { version = "0.8.19", optional = true } +rand = { version = "0.8.5", optional = true } [dev-dependencies] axum-test = "16.4.0" @@ -20,7 +21,7 @@ axum-test = "16.4.0" [features] default = [] task1-9 = ["cargo-manifest", "serde_yml", "toml"] -task12 = [] +task12 = ["rand"] task16 = [] task19 = [] task23 = [] diff --git a/src/lib.rs b/src/lib.rs index 11d9414..1343882 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,20 @@ mod routes; +// use axum::routing::{get, post}; +#[cfg(feature = "task12")] +use routes::{board, place, random_board, reset, Board}; #[cfg(feature = "task1-9")] use routes::{ hello_bird, hello_world, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, milk, minus_one, refill, MilkFactory, }; +#[allow(unused_imports)] pub fn router() -> axum::Router { - #[cfg(feature = "task1-9")] - use axum::routing::get; - #[cfg(feature = "task1-9")] - use axum::routing::post; - use axum::Router; + use axum::{ + routing::{get, post}, + Router, + }; #[cfg(feature = "task1-9")] let milk_factory = MilkFactory::new(); @@ -30,6 +33,13 @@ pub fn router() -> axum::Router { .with_state(milk_factory) .route("/", get(hello_bird)); - // #[cfg(feature="task12")] + #[cfg(feature = "task12")] + Router::new() + .route("/12/board", get(board)) + .route("/12/reset", post(reset)) + .route("/12/place/:team/:column", post(place)) + .route("/12/random-board", get(random_board)) + .with_state(Board::new()); + Router::new() } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 318bbb4..10727d8 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,18 +1,38 @@ -#![cfg(feature = "task1-9")] +#[cfg(feature = "task12")] +mod task_twelve; +#[cfg(feature = "task12")] +pub use task_twelve::{board, game::Board, place, random_board, reset}; + +#[cfg(feature = "task1-9")] mod hello_bird; + +#[cfg(feature = "task1-9")] mod hello_world; +#[cfg(feature = "task1-9")] mod minus_one; +#[cfg(feature = "task1-9")] mod task_five; +#[cfg(feature = "task1-9")] mod task_nine; +#[cfg(feature = "task1-9")] mod task_two; +#[cfg(feature = "task1-9")] pub use hello_bird::hello_bird; +#[cfg(feature = "task1-9")] pub use hello_world::hello_world; +#[cfg(feature = "task1-9")] pub use minus_one::minus_one; +#[cfg(feature = "task1-9")] pub use task_five::manifest; +#[cfg(feature = "task1-9")] pub use task_two::ipv4_dest; +#[cfg(feature = "task1-9")] pub use task_two::ipv4_key; +#[cfg(feature = "task1-9")] pub use task_two::ipv6_dest; +#[cfg(feature = "task1-9")] pub use task_two::ipv6_key; +#[cfg(feature = "task1-9")] pub use task_nine::{milk, refill, MilkFactory}; diff --git a/src/routes/task_twelve/error.rs b/src/routes/task_twelve/error.rs new file mode 100644 index 0000000..338a384 --- /dev/null +++ b/src/routes/task_twelve/error.rs @@ -0,0 +1,38 @@ +use std::fmt::Display; + +use axum::response::IntoResponse; + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GameError { + ColumnFull, + GameOver, + InvalidColumn, + InvalidTeam, +} + +impl Display for GameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ColumnFull => write!(f, "Column is full"), + Self::InvalidColumn => write!(f, "Invalid column"), + Self::GameOver => write!(f, "Game is over"), + Self::InvalidTeam => write!(f, "Invalid team"), + } + } +} + +impl IntoResponse for GameError { + fn into_response(self) -> axum::response::Response { + match self { + Self::ColumnFull | Self::GameOver | Self::InvalidColumn => ( + axum::http::StatusCode::SERVICE_UNAVAILABLE, + self.to_string(), + ) + .into_response(), + Self::InvalidTeam => { + (axum::http::StatusCode::BAD_REQUEST, self.to_string()).into_response() + } + } + } +} diff --git a/src/routes/task_twelve/game/board.rs b/src/routes/task_twelve/game/board.rs new file mode 100644 index 0000000..24cafff --- /dev/null +++ b/src/routes/task_twelve/game/board.rs @@ -0,0 +1,196 @@ +use std::{ + fmt::Display, + sync::{Arc, Mutex}, +}; + +pub const DEFAULT_SEED: u64 = 2024; + +use axum::response::IntoResponse; +use rand::{rngs::StdRng, Rng, SeedableRng}; + +use super::{super::error::GameError, column::Column, Slot, EMPTY, WALL}; + +#[derive(Clone, Debug)] +pub struct Board { + columns: Arc; 4]; 4]>>, + seed: Arc>, +} + +impl Board { + #[allow(dead_code)] + pub fn new() -> Self { + Self { + columns: Arc::new(Mutex::new([[None; 4]; 4])), + seed: Arc::new(Mutex::new(StdRng::seed_from_u64(DEFAULT_SEED))), + } + } + + pub fn display(&self) -> String { + format!( + "{}\n", + self.to_string() + .split_terminator('\n') + .take(5) + .collect::>() + .join("\n") + ) + } + + pub fn get_seed(&self) -> Arc> { + self.seed.clone() + } + + pub fn random(&self, random: &mut StdRng) { + let mut columns: [[Option; 4]; 4] = [[None; 4]; 4]; + // let mut seed = self.seed.lock().unwrap(); + for i in (0..4).rev() { + (0..4).for_each(|j| { + let random = random.gen::(); + let slot = if random { + Some(Slot::Cookie) + } else { + Some(Slot::Milk) + }; + columns[j][i] = slot; + }); + } + // for column in &mut columns.iter_mut() { + // for slot in column.iter_mut() { + // let random = random.gen::(); + // if random { + // *slot = Some(Slot::Cookie); + // } else { + // *slot = Some(Slot::Milk); + // } + // } + // } + // drop(seed); + + { + let mut cols = self.columns.lock().unwrap(); + *cols = columns; + } + } + + pub fn is_full(&self) -> bool { + let columns = self.columns.lock().unwrap(); + columns + .iter() + .all(|column| column.iter().all(std::option::Option::is_some)) + } + + pub fn check(&self) -> Option { + let columns = self.columns.lock().unwrap(); + + // Check rows and columns + for i in 0..4 { + if columns[i][0].is_some() + && columns[i][0] == columns[i][1] + && columns[i][0] == columns[i][2] + && columns[i][0] == columns[i][3] + { + return columns[i][0]; + } + if columns[0][i].is_some() + && columns[0][i] == columns[1][i] + && columns[0][i] == columns[2][i] + && columns[0][i] == columns[3][i] + { + return columns[0][i]; + } + } + + // Check diagonals + if columns[0][0].is_some() + && columns[0][0] == columns[1][1] + && columns[0][0] == columns[2][2] + && columns[0][0] == columns[3][3] + { + return columns[0][0]; + } + if columns[0][3].is_some() + && columns[0][3] == columns[1][2] + && columns[0][3] == columns[2][1] + && columns[0][3] == columns[3][0] + { + return columns[0][3]; + } + + None + } + + #[allow(dead_code)] + pub fn reset(&self) { + { + let mut columns = self.columns.lock().unwrap(); + for i in 0..4 { + for j in 0..4 { + columns[i][j] = None; + } + } + } + { + let mut seed = self.seed.lock().unwrap(); + *seed = StdRng::seed_from_u64(DEFAULT_SEED); + } + } + + #[allow(dead_code)] + pub fn insert(&self, column: Column, slot: Slot) -> Result<(), GameError> { + if self.check().is_some() { + return Err(GameError::GameOver); + } + { + let column: usize = column.into(); + let column = column - 1; + let mut columns = self.columns.lock().unwrap(); + + if column > columns.len() - 1 { + return Err(GameError::InvalidColumn); + } + for i in 0..4 { + if columns[column][i].is_none() { + columns[column][i] = Some(slot); + return Ok(()); + } + } + drop(columns); + } + Err(GameError::ColumnFull) + } +} + +impl Display for Board { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + { + let columns = self.columns.lock().unwrap(); + for j in (0..4).rev() { + write!(f, "{WALL}")?; + for i in 0..4 { + match columns[i][j] { + Some(slot) => write!(f, "{slot}")?, + None => write!(f, "{EMPTY}")?, + } + } + writeln!(f, "{WALL}")?; + } + } + for _ in 0..6 { + write!(f, "{WALL}")?; + } + + writeln!(f)?; + if let Some(winner) = self.check() { + writeln!(f, "{winner} wins!")?; + } else if self.is_full() { + writeln!(f, "No winner.")?; + } + Ok(()) + } +} + +impl IntoResponse for Board { + fn into_response(self) -> axum::response::Response { + (axum::http::StatusCode::OK, self.to_string()).into_response() + } +} diff --git a/src/routes/task_twelve/game/column.rs b/src/routes/task_twelve/game/column.rs new file mode 100644 index 0000000..6a83121 --- /dev/null +++ b/src/routes/task_twelve/game/column.rs @@ -0,0 +1,47 @@ +use serde::de::{self, Deserializer, Visitor}; +use serde::Deserialize; +use std::fmt::{self, Display}; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Column(usize); + +impl Display for Column { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for usize { + fn from(column: Column) -> Self { + column.0 + } +} + +impl<'de> Deserialize<'de> for Column { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct ColumnVisitor; + + impl Visitor<'_> for ColumnVisitor { + type Value = Column; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer between 1 and 4") + } + + fn visit_u8(self, value: u8) -> Result + where + E: de::Error, + { + match value { + 1..=4 => Ok(Column(value as usize)), + _ => Err(de::Error::custom("value must be between 1 and 4")), + } + } + } + + deserializer.deserialize_u8(ColumnVisitor) + } +} diff --git a/src/routes/task_twelve/game/mod.rs b/src/routes/task_twelve/game/mod.rs new file mode 100644 index 0000000..da8a858 --- /dev/null +++ b/src/routes/task_twelve/game/mod.rs @@ -0,0 +1,28 @@ +pub mod board; +pub mod column; +pub mod slot; + +pub use board::Board; +pub use slot::Slot; + +const EMPTY: &str = "⬛"; +const COOKIE: &str = "🍪"; +const MILK: &str = "🥛"; +const WALL: &str = "⬜"; + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn test_board() { +// let board = Board::new(); +// println!("{board}"); + +// for _ in 0..4 { +// assert!(board.insert(0, Slot::Milk).is_ok()); +// } + +// println!("{board}"); +// } +// } diff --git a/src/routes/task_twelve/game/slot.rs b/src/routes/task_twelve/game/slot.rs new file mode 100644 index 0000000..ed72424 --- /dev/null +++ b/src/routes/task_twelve/game/slot.rs @@ -0,0 +1,34 @@ +use std::fmt::Display; + +use serde::Deserialize; + +use super::{super::error::GameError, COOKIE, MILK}; + +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Slot { + Milk, + Cookie, +} + +impl Display for Slot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Milk => write!(f, "{MILK}"), + Self::Cookie => write!(f, "{COOKIE}"), + } + } +} + +impl TryFrom<&str> for Slot { + type Error = GameError; + + fn try_from(value: &str) -> Result { + match value { + "milk" => Ok(Self::Milk), + "cookie" => Ok(Self::Cookie), + _ => Err(GameError::InvalidTeam), + } + } +} diff --git a/src/routes/task_twelve/mod.rs b/src/routes/task_twelve/mod.rs new file mode 100644 index 0000000..974d2c9 --- /dev/null +++ b/src/routes/task_twelve/mod.rs @@ -0,0 +1,44 @@ +use axum::{ + extract::{Path, State}, + response::IntoResponse, +}; +use game::{column::Column, Board, Slot}; + +pub mod error; +pub mod game; + +#[allow(clippy::unused_async)] +pub async fn board(State(board): State) -> impl IntoResponse { + board.into_response() +} + +#[allow(clippy::unused_async)] +pub async fn reset(State(board): State) -> impl IntoResponse { + board.reset(); + board.into_response() +} + +#[allow(clippy::unused_async)] +pub async fn place( + State(board): State, + Path((team, column)): Path<(Slot, Column)>, +) -> impl IntoResponse { + board.insert(column, team).map_err(|err| match err { + error::GameError::ColumnFull | error::GameError::GameOver => ( + axum::http::StatusCode::SERVICE_UNAVAILABLE, + board.to_string(), + ) + .into_response(), + e => e.into_response(), + })?; + Ok::<_, axum::response::Response>(board.into_response()) +} + +#[allow(clippy::unused_async)] +pub async fn random_board(State(board): State) -> impl IntoResponse { + // let mut random = rand::rngs::StdRng::seed_from_u64(2024); + let seed = board.get_seed(); + let mut random = seed.lock().unwrap(); + board.random(&mut random); + board.display().into_response() +} diff --git a/tests/task_twelve/main.rs b/tests/task_twelve/main.rs index 0f61fde..e81b451 100644 --- a/tests/task_twelve/main.rs +++ b/tests/task_twelve/main.rs @@ -1,4 +1,5 @@ -#[cfg(feature = "task12")] +#![cfg(feature = "task12")] +#[cfg(test)] mod task_twelve { use axum_test::TestServer; use itsscb_shuttlings_cch24::router; @@ -6,4 +7,214 @@ mod task_twelve { fn test_server() -> TestServer { TestServer::new(router()).unwrap() } + const EMPTY_BOARD: &str = "⬜⬛⬛⬛⬛⬜ +⬜⬛⬛⬛⬛⬜ +⬜⬛⬛⬛⬛⬜ +⬜⬛⬛⬛⬛⬜ +⬜⬜⬜⬜⬜⬜ +"; + + #[tokio::test] + async fn test_task_1() { + let server = test_server(); + + let response = server.get("/12/board").await; + response.assert_status_ok(); + response.assert_text(EMPTY_BOARD); + + let response = server.post("/12/reset").await; + response.assert_status_ok(); + response.assert_text(EMPTY_BOARD); + } + + #[tokio::test] + async fn test_task_2() { + let server = test_server(); + let response = server.post("/12/reset").await; + response.assert_status_ok(); + response.assert_text(EMPTY_BOARD); + + let want = "\ +⬜⬛⬛⬛⬛⬜ +⬜⬛⬛⬛⬛⬜ +⬜⬛⬛⬛⬛⬜ +⬜🍪⬛⬛⬛⬜ +⬜⬜⬜⬜⬜⬜ +"; + + let response = server.post("/12/place/cookie/1").await; + response.assert_status_ok(); + response.assert_text(want); + + let want = "\ +⬜🍪⬛⬛⬛⬜ +⬜🍪⬛⬛⬛⬜ +⬜🍪⬛⬛⬛⬜ +⬜🍪⬛⬛⬛⬜ +⬜⬜⬜⬜⬜⬜ +🍪 wins! +"; + + let response = server.post("/12/place/cookie/1").await; + response.assert_status_ok(); + + let response = server.post("/12/place/cookie/1").await; + response.assert_status_ok(); + let response = server.post("/12/place/cookie/1").await; + response.assert_status_ok(); + response.assert_text(want); + + let response = server.post("/12/place/milk/2").await; + response.assert_status_service_unavailable(); + response.assert_text(want); + + let mut response = server.post("/12/reset").await; + response.assert_status_ok(); + response.assert_text(EMPTY_BOARD); + + let want = "⬜🥛🍪🥛🍪⬜ +⬜🍪🥛🍪🥛⬜ +⬜🍪🥛🍪🥛⬜ +⬜🍪🥛🍪🥛⬜ +⬜⬜⬜⬜⬜⬜ +No winner. +"; + + for i in 1..5 { + for _ in 0..3 { + let slot = if i % 2 == 0 { "milk" } else { "cookie" }; + response = server.post(&format!("/12/place/{slot}/{i}")).await; + response.assert_status_ok(); + } + } + for i in 1..5 { + let slot = if i % 2 == 0 { "cookie" } else { "milk" }; + + response = server.post(&format!("/12/place/{slot}/{i}")).await; + response.assert_status_ok(); + } + response.assert_text(want); + + let response = server.post("/12/place/milk/1").await; + response.assert_status_service_unavailable(); + response.assert_text(want); + + let response = server.post("/12/reset").await; + response.assert_status_ok(); + response.assert_text(EMPTY_BOARD); + + let want = "⬜⬛⬛⬛🍪⬜ +⬜⬛⬛🍪🥛⬜ +⬜⬛🍪🥛🥛⬜ +⬜🍪🥛🥛🥛⬜ +⬜⬜⬜⬜⬜⬜ +🍪 wins! +"; + + let response = server.post("/12/place/cookie/1").await; + response.assert_status_ok(); + + for i in 2..5 { + let response = server.post(&format!("/12/place/milk/{i}")).await; + response.assert_status_ok(); + } + let response = server.post("/12/place/cookie/2").await; + response.assert_status_ok(); + + for i in 3..5 { + let response = server.post(&format!("/12/place/milk/{i}")).await; + response.assert_status_ok(); + } + let response = server.post("/12/place/cookie/3").await; + response.assert_status_ok(); + + // for i in 4..5 { + let response = server.post("/12/place/milk/4").await; + response.assert_status_ok(); + // } + let response = server.post("/12/place/cookie/4").await; + response.assert_status_ok(); + + response.assert_text(want); + } + + #[tokio::test] + async fn test_task_3() { + let server = test_server(); + + let response = server.post("/12/reset").await; + response.assert_status_ok(); + response.assert_text(EMPTY_BOARD); + + let want = "\ +⬜🍪🍪🍪🍪⬜ +⬜🥛🍪🍪🥛⬜ +⬜🥛🥛🥛🥛⬜ +⬜🍪🥛🍪🥛⬜ +⬜⬜⬜⬜⬜⬜ +"; + let response = server.get("/12/random-board").await; + // dbg!(response.text()); + response.assert_status_ok(); + response.assert_text(want); + + let want = "\ +⬜🍪🥛🍪🍪⬜ +⬜🥛🍪🥛🍪⬜ +⬜🥛🍪🍪🍪⬜ +⬜🍪🥛🥛🥛⬜ +⬜⬜⬜⬜⬜⬜ +"; + let response = server.get("/12/random-board").await; + response.assert_status_ok(); + response.assert_text(want); + + let want = "\ +⬜🍪🍪🥛🍪⬜ +⬜🍪🥛🍪🍪⬜ +⬜🥛🍪🍪🥛⬜ +⬜🍪🥛🍪🍪⬜ +⬜⬜⬜⬜⬜⬜ +"; + let response = server.get("/12/random-board").await; + response.assert_status_ok(); + response.assert_text(want); + + let want = "\ +⬜🥛🍪🍪🥛⬜ +⬜🥛🍪🍪🍪⬜ +⬜🍪🥛🥛🥛⬜ +⬜🍪🥛🍪🥛⬜ +⬜⬜⬜⬜⬜⬜ +"; + let response = server.get("/12/random-board").await; + response.assert_status_ok(); + response.assert_text(want); + + let want = "\ +⬜🥛🥛🥛🍪⬜ +⬜🍪🍪🍪🥛⬜ +⬜🥛🍪🍪🥛⬜ +⬜🍪🥛🥛🍪⬜ +⬜⬜⬜⬜⬜⬜ +"; + let response = server.get("/12/random-board").await; + response.assert_status_ok(); + response.assert_text(want); + + let response = server.post("/12/reset").await; + response.assert_status_ok(); + response.assert_text(EMPTY_BOARD); + + let want = "\ +⬜🍪🍪🍪🍪⬜ +⬜🥛🍪🍪🥛⬜ +⬜🥛🥛🥛🥛⬜ +⬜🍪🥛🍪🥛⬜ +⬜⬜⬜⬜⬜⬜ +"; + let response = server.get("/12/random-board").await; + response.assert_status_ok(); + response.assert_text(want); + } }