pico-website/src/game.rs

267 lines
8.1 KiB
Rust

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<u8, 4096>,
/// State of the board last time it has been sent
last_board: u32,
team: Team,
end: Option<(Instant, Option<Team>)>
}
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<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
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"<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
}
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::<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 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<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(()),
})
}
}