305 lines
9.1 KiB
Rust
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(()),
|
|
})
|
|
}
|
|
}
|