challenge: 12

This commit is contained in:
itsscb 2024-12-12 22:40:08 +01:00
parent 2b614ee8c4
commit 81263f7803
10 changed files with 638 additions and 9 deletions

View File

@ -13,6 +13,7 @@ tokio = "1.28.2"
cargo-manifest = { version = "0.17.0", optional = true } cargo-manifest = { version = "0.17.0", optional = true }
serde_yml = { version = "0.0.12", optional = true } serde_yml = { version = "0.0.12", optional = true }
toml = { version = "0.8.19", optional = true } toml = { version = "0.8.19", optional = true }
rand = { version = "0.8.5", optional = true }
[dev-dependencies] [dev-dependencies]
axum-test = "16.4.0" axum-test = "16.4.0"
@ -20,7 +21,7 @@ axum-test = "16.4.0"
[features] [features]
default = [] default = []
task1-9 = ["cargo-manifest", "serde_yml", "toml"] task1-9 = ["cargo-manifest", "serde_yml", "toml"]
task12 = [] task12 = ["rand"]
task16 = [] task16 = []
task19 = [] task19 = []
task23 = [] task23 = []

View File

@ -1,17 +1,20 @@
mod routes; mod routes;
// use axum::routing::{get, post};
#[cfg(feature = "task12")]
use routes::{board, place, random_board, reset, Board};
#[cfg(feature = "task1-9")] #[cfg(feature = "task1-9")]
use routes::{ use routes::{
hello_bird, hello_world, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, milk, minus_one, hello_bird, hello_world, ipv4_dest, ipv4_key, ipv6_dest, ipv6_key, manifest, milk, minus_one,
refill, MilkFactory, refill, MilkFactory,
}; };
#[allow(unused_imports)]
pub fn router() -> axum::Router { pub fn router() -> axum::Router {
#[cfg(feature = "task1-9")] use axum::{
use axum::routing::get; routing::{get, post},
#[cfg(feature = "task1-9")] Router,
use axum::routing::post; };
use axum::Router;
#[cfg(feature = "task1-9")] #[cfg(feature = "task1-9")]
let milk_factory = MilkFactory::new(); let milk_factory = MilkFactory::new();
@ -30,6 +33,13 @@ pub fn router() -> axum::Router {
.with_state(milk_factory) .with_state(milk_factory)
.route("/", get(hello_bird)); .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() Router::new()
} }

View File

@ -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; mod hello_bird;
#[cfg(feature = "task1-9")]
mod hello_world; mod hello_world;
#[cfg(feature = "task1-9")]
mod minus_one; mod minus_one;
#[cfg(feature = "task1-9")]
mod task_five; mod task_five;
#[cfg(feature = "task1-9")]
mod task_nine; mod task_nine;
#[cfg(feature = "task1-9")]
mod task_two; mod task_two;
#[cfg(feature = "task1-9")]
pub use hello_bird::hello_bird; pub use hello_bird::hello_bird;
#[cfg(feature = "task1-9")]
pub use hello_world::hello_world; pub use hello_world::hello_world;
#[cfg(feature = "task1-9")]
pub use minus_one::minus_one; pub use minus_one::minus_one;
#[cfg(feature = "task1-9")]
pub use task_five::manifest; pub use task_five::manifest;
#[cfg(feature = "task1-9")]
pub use task_two::ipv4_dest; pub use task_two::ipv4_dest;
#[cfg(feature = "task1-9")]
pub use task_two::ipv4_key; pub use task_two::ipv4_key;
#[cfg(feature = "task1-9")]
pub use task_two::ipv6_dest; pub use task_two::ipv6_dest;
#[cfg(feature = "task1-9")]
pub use task_two::ipv6_key; pub use task_two::ipv6_key;
#[cfg(feature = "task1-9")]
pub use task_nine::{milk, refill, MilkFactory}; pub use task_nine::{milk, refill, MilkFactory};

View File

@ -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()
}
}
}
}

View File

@ -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<Mutex<[[Option<Slot>; 4]; 4]>>,
seed: Arc<Mutex<StdRng>>,
}
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::<Vec<_>>()
.join("\n")
)
}
pub fn get_seed(&self) -> Arc<Mutex<StdRng>> {
self.seed.clone()
}
pub fn random(&self, random: &mut StdRng) {
let mut columns: [[Option<Slot>; 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::<bool>();
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::<bool>();
// 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<Slot> {
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()
}
}

View File

@ -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<Column> for usize {
fn from(column: Column) -> Self {
column.0
}
}
impl<'de> Deserialize<'de> for Column {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, value: u8) -> Result<Self::Value, E>
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)
}
}

View File

@ -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}");
// }
// }

View File

@ -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<Self, Self::Error> {
match value {
"milk" => Ok(Self::Milk),
"cookie" => Ok(Self::Cookie),
_ => Err(GameError::InvalidTeam),
}
}
}

View File

@ -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<Board>) -> impl IntoResponse {
board.into_response()
}
#[allow(clippy::unused_async)]
pub async fn reset(State(board): State<Board>) -> impl IntoResponse {
board.reset();
board.into_response()
}
#[allow(clippy::unused_async)]
pub async fn place(
State(board): State<Board>,
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<Board>) -> 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()
}

View File

@ -1,4 +1,5 @@
#[cfg(feature = "task12")] #![cfg(feature = "task12")]
#[cfg(test)]
mod task_twelve { mod task_twelve {
use axum_test::TestServer; use axum_test::TestServer;
use itsscb_shuttlings_cch24::router; use itsscb_shuttlings_cch24::router;
@ -6,4 +7,214 @@ mod task_twelve {
fn test_server() -> TestServer { fn test_server() -> TestServer {
TestServer::new(router()).unwrap() 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);
}
} }