use core::fmt::Write; use core::{ops::Not, sync::atomic::Ordering}; use embassy_time::{Duration, Instant}; use heapless::Vec; use pico_website::unwrap; use portable_atomic::{AtomicBool, AtomicU32}; use crate::socket::{HttpRequestType, HttpResCode}; use super::App; static TURN: AtomicBool = AtomicBool::new(false); // bits [0; 8] : player zero board / bits [9; 17] : player one board static BOARD: AtomicU32 = AtomicU32::new(0); pub struct TttApp { res_buf: Vec, /// State of the board last time it has been sent last_board: u32, team: Team, end: Option<(Instant, Option)>, } impl TttApp { pub fn new(team: Team) -> Self { Self { res_buf: Vec::new(), last_board: 0, team, end: None, } } pub fn is_ended(&self, board: u32) -> (bool, Option) { if let Some((_, t)) = self.end { return (true, t); } for (t, m) in [(Team::Zero, 0), (Team::One, 9)] { for w in [ 0b111000000, 0b000111000, 0b000000111, 0b100100100, 0b010010010, 0b001001001, 0b100010001, 0b001010100, ] { if board & (w << m) == (w << m) { return (true, Some(t)); } } } if ((board | (board >> 9)) & 0b111111111) == 0b111111111 { return (true, None); } (false, None) } pub fn update_end_state(&mut self, board: &mut u32) { if let Some((i, _)) = self.end { if i + Duration::from_secs(7) < Instant::now() { self.end = None; BOARD.store(0, Ordering::Release); *board = 0; } } else { if let (true, t) = self.is_ended(*board) { self.end = Some((Instant::now(), t)); } } } /// Generate board html async fn generate_board_res<'a>( &'a mut self, board: u32, turn: Team, outer_html: bool, ) -> &'a [u8] { self.res_buf.clear(); if outer_html { unwrap(self.res_buf.extend_from_slice( b"
", )) .await; } unwrap(write!( self.res_buf, "

Team : {}

", self.team.color(), self.team.name() )) .await; match self.end { Some((_, Some(t))) => { unwrap(write!( self.res_buf, "

Team {} has won!


", t.color(), t.name() )) .await } Some((_, None)) => unwrap(write!(self.res_buf, "

Draw!


",)).await, None => {} } unwrap(self.res_buf.extend_from_slice(b"
")).await; for c in 0..=8 { let picked_by = if board & (1 << c) != 0 { Some(Team::Zero) } else if board & (1 << (9 + c)) != 0 { Some(Team::One) } else { None }; match picked_by { Some(t) => { unwrap(write!( self.res_buf, "
", t.color() )) .await; } None => { if self.team == turn.into() && self.end.is_none() { unwrap(write!( self.res_buf, "", c )).await; } else { unwrap( self.res_buf .extend_from_slice(b"
"), ) .await; } } }; } unwrap(self.res_buf.extend_from_slice(b"
")).await; if outer_html { unwrap(self.res_buf.extend_from_slice(b"
")).await; } &self.res_buf } } impl App for TttApp { fn socket_name(&self) -> &'static str { self.team.name() } async fn handle_request<'a>( &'a mut self, path: &str, _req_type: HttpRequestType, _content: &str, ) -> (HttpResCode, &'static str, &'a [u8]) { match path { "/" | "/index" | "/index.html" | "/ttt" | "/ttt.html" => { (HttpResCode::Ok, "html", include_bytes!("./ttt.html")) } "/ttt/initial_game" => { let board = BOARD.load(Ordering::Acquire); let turn = TURN.load(Ordering::Acquire); ( HttpResCode::Ok, "html", self.generate_board_res(board, turn.into(), true).await, ) } path => { if (path.starts_with("/ttt/cell") && path.len() == 10) || path == "/ttt/game" { let mut board = BOARD.load(Ordering::Acquire); let mut turn = TURN.load(Ordering::Acquire); // just return correct board in case of unauthorized move if path.starts_with("/ttt/cell") && self.team == turn.into() { let clicked_c: Cell = match TryInto::::try_into( unwrap(path.chars().nth(9).ok_or("no 9th char")).await, ) { Ok(c) => c, Err(_) => return (HttpResCode::NotFound, "", &[]), }; if board & ((1 << (clicked_c as u32)) + (1 << (9 + clicked_c as u32))) != 0 { return (HttpResCode::Forbidden, "", &[]); } board = board | (1 << ((self.team as u32 * 9) + clicked_c as u32)); turn = (!self.team).into(); BOARD.store(board, Ordering::Release); TURN.store(turn, Ordering::Release); } self.update_end_state(&mut board); if self.last_board != board { self.last_board = board; ( HttpResCode::Ok, "html", self.generate_board_res(board, turn.into(), false).await, ) } else { (HttpResCode::NoContent, "", &[]) } } else { (HttpResCode::NotFound, "", &[]) } } } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Team { Zero = 0, One = 1, } impl From for Team { fn from(value: bool) -> Self { if value { Team::One } else { Team::Zero } } } impl Into for Team { fn into(self) -> bool { match self { Team::Zero => false, Team::One => true, } } } impl Not for Team { type Output = Team; fn not(self) -> Self::Output { match self { Team::Zero => Team::One, Team::One => Team::Zero, } } } impl Team { fn color(self) -> &'static str { match self { Team::Zero => "dodgerblue", Team::One => "firebrick", } } fn name(self) -> &'static str { match self { Team::Zero => "blue", Team::One => "red", } } } #[derive(Debug, Clone, Copy)] enum Cell { C0 = 0, C1 = 1, C2 = 2, C3 = 3, C4 = 4, C5 = 5, C6 = 6, C7 = 7, C8 = 8, } impl TryFrom for Cell { type Error = (); fn try_from(value: char) -> Result { Ok(match value { '0' => Cell::C0, '1' => Cell::C1, '2' => Cell::C2, '3' => Cell::C3, '4' => Cell::C4, '5' => Cell::C5, '6' => Cell::C6, '7' => Cell::C7, '8' => Cell::C8, _ => return Err(()), }) } } impl TryFrom for Cell { type Error = (); fn try_from(value: u8) -> Result { Ok(match value { 0 => Cell::C0, 1 => Cell::C1, 2 => Cell::C2, 3 => Cell::C3, 4 => Cell::C4, 5 => Cell::C5, 6 => Cell::C6, 7 => Cell::C7, 8 => Cell::C8, _ => return Err(()), }) } }