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 core::fmt::Write; use crate::socket::HttpResCode; 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 GameClient { res_buf: Vec, /// State of the board last time it has been sent last_board: u32, team: Team, end: Option<(Instant, Option)> } impl GameClient { 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<>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 pub 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< { 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 } pub async fn handle_request<'a>(&'a mut self, path: &str) -> (HttpResCode, &'static str, &'a [u8]) { 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 if path == "/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) } 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(()), }) } }