pico-website/src/apps/ttt.rs
2025-05-02 00:37:11 +02:00

305 lines
9.1 KiB
Rust

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<u8, 4096>,
/// State of the board last time it has been sent
last_board: u32,
team: Team,
end: Option<(Instant, Option<Team>)>,
}
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<Team>) {
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"<div \
id=\"game\" \
hx-get=\"/ttt/game\" \
hx-swap=\"innerHTML\" \
hx-trigger=\"every 100ms\" \
hx-target=\"this\"\
>",
))
.await;
}
unwrap(write!(
self.res_buf,
"<h3>Team : <span style=\"color:{}\">{}</span></h3>",
self.team.color(),
self.team.name()
))
.await;
match self.end {
Some((_, Some(t))) => {
unwrap(write!(
self.res_buf,
"<br><h3>Team <span style=\"color:{}\">{}</span> has won!</h3><br>",
t.color(),
t.name()
))
.await
}
Some((_, None)) => unwrap(write!(self.res_buf, "<br><h3>Draw!</h3><br>",)).await,
None => {}
}
unwrap(self.res_buf.extend_from_slice(b"<div id=\"grid\">")).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,
"<div class=\"cell\" style=\"background-color:{}\"></div>",
t.color()
))
.await;
}
None => {
if self.team == turn.into() && self.end.is_none() {
unwrap(write!(
self.res_buf,
"<button class=\"cell\" hx-post=\"/ttt/cell{}\" hx-trigger=\"click\" hx-target=\"#game\" hx-swap=\"innerHTML\"></button>",
c
)).await;
} else {
unwrap(
self.res_buf
.extend_from_slice(b"<div class=\"cell\"></div>"),
)
.await;
}
}
};
}
unwrap(self.res_buf.extend_from_slice(b"</div>")).await;
if outer_html {
unwrap(self.res_buf.extend_from_slice(b"</div>")).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::<Cell>::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<bool> for Team {
fn from(value: bool) -> Self {
if value { Team::One } else { Team::Zero }
}
}
impl Into<bool> 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<char> for Cell {
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error> {
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<u8> for Cell {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
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(()),
})
}
}