feat: Highlight last move
Browse files- python/othello/ui.py +24 -18
- src/bits.rs +0 -30
- src/game.rs +7 -5
python/othello/ui.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
from uuid import uuid1
|
| 2 |
from fasthtml.common import (
|
| 3 |
fast_app,
|
|
@@ -18,7 +20,7 @@ app, rt = fast_app(
|
|
| 18 |
)
|
| 19 |
|
| 20 |
games = {}
|
| 21 |
-
bot = AlphaBetaBot(
|
| 22 |
|
| 23 |
|
| 24 |
@rt("/")
|
|
@@ -34,7 +36,7 @@ def get(uuid: str = None):
|
|
| 34 |
|
| 35 |
@app.get("/new")
|
| 36 |
def new(uuid: str = None):
|
| 37 |
-
if uuid is not None:
|
| 38 |
del games[uuid]
|
| 39 |
return RedirectResponse("/")
|
| 40 |
|
|
@@ -45,7 +47,13 @@ def make_app(uuid):
|
|
| 45 |
Div(
|
| 46 |
make_status_bar(state),
|
| 47 |
Div(
|
| 48 |
-
*(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
cls="grid grid-cols-8 gap-0 bg-green-300 mb-5 lg:mt-5",
|
| 50 |
hx_ext="ws",
|
| 51 |
ws_connect="/wscon",
|
|
@@ -57,26 +65,23 @@ def make_app(uuid):
|
|
| 57 |
)
|
| 58 |
|
| 59 |
|
| 60 |
-
def
|
| 61 |
style = "m-2 size-8 lg:size-12 rounded-full"
|
| 62 |
-
|
|
|
|
|
|
|
| 63 |
if v == "?":
|
| 64 |
-
stone =
|
| 65 |
hx_trigger="click",
|
| 66 |
hx_vals=f'{{"pos": {pos}, "uuid": "{uuid}"}}',
|
| 67 |
ws_send=True,
|
| 68 |
cls=f"{style} cursor-pointer bg-purple-200 hover:bg-purple-300",
|
| 69 |
)
|
| 70 |
elif v == "B":
|
| 71 |
-
stone =
|
| 72 |
elif v == "W":
|
| 73 |
-
stone =
|
| 74 |
-
return Div(
|
| 75 |
-
stone,
|
| 76 |
-
id=f"cell-{pos}",
|
| 77 |
-
cls="size-12 xl:size-16 border border-sky-100",
|
| 78 |
-
hx_swap_oob="true",
|
| 79 |
-
)
|
| 80 |
|
| 81 |
|
| 82 |
def make_status_bar(state):
|
|
@@ -119,12 +124,11 @@ async def ws(uuid: str, pos: int, send):
|
|
| 119 |
prev_state = game.state
|
| 120 |
state = game.make_move(pos) if pos >= 0 else game.pass_move()
|
| 121 |
|
| 122 |
-
await send(
|
| 123 |
-
# await asyncio.sleep(1)
|
| 124 |
|
| 125 |
for i, (c1, c2) in enumerate(zip(prev_state.cells, state.cells)):
|
| 126 |
-
if i != pos and c1 != c2:
|
| 127 |
-
await send(
|
| 128 |
await send(make_status_bar(state))
|
| 129 |
return state
|
| 130 |
|
|
@@ -135,7 +139,9 @@ async def ws(uuid: str, pos: int, send):
|
|
| 135 |
|
| 136 |
# Bot
|
| 137 |
while True:
|
|
|
|
| 138 |
pos = bot.find_move(game) if state.can_move else -1
|
|
|
|
| 139 |
state = await play(pos)
|
| 140 |
if not state.can_move and not state.ended:
|
| 141 |
# Human has no move
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import time
|
| 3 |
from uuid import uuid1
|
| 4 |
from fasthtml.common import (
|
| 5 |
fast_app,
|
|
|
|
| 20 |
)
|
| 21 |
|
| 22 |
games = {}
|
| 23 |
+
bot = AlphaBetaBot(8)
|
| 24 |
|
| 25 |
|
| 26 |
@rt("/")
|
|
|
|
| 36 |
|
| 37 |
@app.get("/new")
|
| 38 |
def new(uuid: str = None):
|
| 39 |
+
if uuid is not None and uuid in games:
|
| 40 |
del games[uuid]
|
| 41 |
return RedirectResponse("/")
|
| 42 |
|
|
|
|
| 47 |
Div(
|
| 48 |
make_status_bar(state),
|
| 49 |
Div(
|
| 50 |
+
*(
|
| 51 |
+
Div(
|
| 52 |
+
make_stone(state.cells[i], i, uuid),
|
| 53 |
+
cls="size-12 xl:size-16 border border-sky-100",
|
| 54 |
+
)
|
| 55 |
+
for i in range(64)
|
| 56 |
+
),
|
| 57 |
cls="grid grid-cols-8 gap-0 bg-green-300 mb-5 lg:mt-5",
|
| 58 |
hx_ext="ws",
|
| 59 |
ws_connect="/wscon",
|
|
|
|
| 65 |
)
|
| 66 |
|
| 67 |
|
| 68 |
+
def make_stone(v, pos, uuid, highlight=False):
|
| 69 |
style = "m-2 size-8 lg:size-12 rounded-full"
|
| 70 |
+
if highlight:
|
| 71 |
+
style += " border-indigo-500 border-2"
|
| 72 |
+
stone = {}
|
| 73 |
if v == "?":
|
| 74 |
+
stone = dict(
|
| 75 |
hx_trigger="click",
|
| 76 |
hx_vals=f'{{"pos": {pos}, "uuid": "{uuid}"}}',
|
| 77 |
ws_send=True,
|
| 78 |
cls=f"{style} cursor-pointer bg-purple-200 hover:bg-purple-300",
|
| 79 |
)
|
| 80 |
elif v == "B":
|
| 81 |
+
stone = dict(cls=f"{style} shadow-sm bg-black shadow-white")
|
| 82 |
elif v == "W":
|
| 83 |
+
stone = dict(cls=f"{style} shadow-sm bg-white shadow-black")
|
| 84 |
+
return Div(**stone, id=f"cell-{pos}", hx_swap_oob="true")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
|
| 87 |
def make_status_bar(state):
|
|
|
|
| 124 |
prev_state = game.state
|
| 125 |
state = game.make_move(pos) if pos >= 0 else game.pass_move()
|
| 126 |
|
| 127 |
+
await send(make_stone(state.cells[pos], pos, uuid, highlight=True))
|
|
|
|
| 128 |
|
| 129 |
for i, (c1, c2) in enumerate(zip(prev_state.cells, state.cells)):
|
| 130 |
+
if i != pos and c1 != c2 or i == prev_state.last_move:
|
| 131 |
+
await send(make_stone(c2, i, uuid))
|
| 132 |
await send(make_status_bar(state))
|
| 133 |
return state
|
| 134 |
|
|
|
|
| 139 |
|
| 140 |
# Bot
|
| 141 |
while True:
|
| 142 |
+
now = time.time()
|
| 143 |
pos = bot.find_move(game) if state.can_move else -1
|
| 144 |
+
await asyncio.sleep(abs(1 - time.time() + now))
|
| 145 |
state = await play(pos)
|
| 146 |
if not state.can_move and not state.ended:
|
| 147 |
# Human has no move
|
src/bits.rs
CHANGED
|
@@ -1,18 +1,9 @@
|
|
| 1 |
-
use std::fmt::Write;
|
| 2 |
-
|
| 3 |
use pyo3::prelude::*;
|
| 4 |
|
| 5 |
#[pyclass]
|
| 6 |
#[derive(Clone)]
|
| 7 |
pub struct BitBoard(pub u64, pub u64);
|
| 8 |
|
| 9 |
-
impl core::fmt::Debug for BitBoard {
|
| 10 |
-
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
| 11 |
-
f.write_str(&self.to_string(('B', 'W')))?;
|
| 12 |
-
Ok(())
|
| 13 |
-
}
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
#[pymethods]
|
| 17 |
impl BitBoard {
|
| 18 |
#[new]
|
|
@@ -20,27 +11,6 @@ impl BitBoard {
|
|
| 20 |
Self(0x0000_0008_1000_0000, 0x0000_0010_0800_0000)
|
| 21 |
}
|
| 22 |
|
| 23 |
-
fn __repr__(&self) -> String {
|
| 24 |
-
self.to_string(('B', 'W'))
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
pub fn to_string(&self, players: (char, char)) -> String {
|
| 28 |
-
let mut s = String::with_capacity(64 + 8);
|
| 29 |
-
for i in (0..64).rev() {
|
| 30 |
-
s.write_char(match (self.0 >> i & 1, self.1 >> i & 1) {
|
| 31 |
-
(0, 0) => '.',
|
| 32 |
-
(1, 0) => players.0,
|
| 33 |
-
(0, 1) => players.1,
|
| 34 |
-
(_, _) => unreachable!(),
|
| 35 |
-
})
|
| 36 |
-
.unwrap();
|
| 37 |
-
if i % 8 == 0 {
|
| 38 |
-
s.write_char('\n').unwrap();
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
s
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
/// Returns bitboards of `self`.
|
| 45 |
#[must_use]
|
| 46 |
pub const fn get(&self) -> [u64; 2] {
|
|
|
|
|
|
|
|
|
|
| 1 |
use pyo3::prelude::*;
|
| 2 |
|
| 3 |
#[pyclass]
|
| 4 |
#[derive(Clone)]
|
| 5 |
pub struct BitBoard(pub u64, pub u64);
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
#[pymethods]
|
| 8 |
impl BitBoard {
|
| 9 |
#[new]
|
|
|
|
| 11 |
Self(0x0000_0008_1000_0000, 0x0000_0010_0800_0000)
|
| 12 |
}
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
/// Returns bitboards of `self`.
|
| 15 |
#[must_use]
|
| 16 |
pub const fn get(&self) -> [u64; 2] {
|
src/game.rs
CHANGED
|
@@ -22,6 +22,7 @@ pub struct State {
|
|
| 22 |
pub white_score: i32,
|
| 23 |
pub cells: Vec<char>,
|
| 24 |
pub can_move: bool,
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
#[pymethods]
|
|
@@ -36,7 +37,7 @@ impl Game {
|
|
| 36 |
#[staticmethod]
|
| 37 |
pub fn default() -> Self {
|
| 38 |
let board = BitBoard::default();
|
| 39 |
-
let state = Self::compute_state(&board, 0);
|
| 40 |
Self {
|
| 41 |
board,
|
| 42 |
current_player: 0,
|
|
@@ -52,7 +53,7 @@ impl Game {
|
|
| 52 |
pub fn pass_move(&mut self) -> State {
|
| 53 |
self.board = self.board.pass_move();
|
| 54 |
self.current_player = 1 - self.current_player;
|
| 55 |
-
self.state = Self::compute_state(&self.board, self.current_player);
|
| 56 |
self.state.clone()
|
| 57 |
}
|
| 58 |
|
|
@@ -60,7 +61,7 @@ impl Game {
|
|
| 60 |
let next = self.board.make_move(place).unwrap();
|
| 61 |
self.current_player = 1 - self.current_player;
|
| 62 |
self.board = next;
|
| 63 |
-
self.state = Self::compute_state(&self.board, self.current_player);
|
| 64 |
self.state.clone()
|
| 65 |
}
|
| 66 |
|
|
@@ -91,7 +92,7 @@ impl Game {
|
|
| 91 |
}
|
| 92 |
|
| 93 |
impl Game {
|
| 94 |
-
fn compute_state(board: &BitBoard, current_player: usize) -> State {
|
| 95 |
let (cnt0, cnt1) = board.count();
|
| 96 |
let moves = board.available_moves();
|
| 97 |
let cells: Vec<_> = (0..64)
|
|
@@ -115,6 +116,7 @@ impl Game {
|
|
| 115 |
white_score: if player == 'W' { cnt0 } else { cnt1 },
|
| 116 |
cells,
|
| 117 |
can_move: moves != 0,
|
|
|
|
| 118 |
}
|
| 119 |
}
|
| 120 |
}
|
|
@@ -146,7 +148,7 @@ mod tests {
|
|
| 146 |
let b = BitBoard(2, 1);
|
| 147 |
let mut g = Game {
|
| 148 |
current_player: 0,
|
| 149 |
-
state: Game::compute_state(&b, 0),
|
| 150 |
board: b,
|
| 151 |
};
|
| 152 |
assert_eq!(g.state.can_move, false);
|
|
|
|
| 22 |
pub white_score: i32,
|
| 23 |
pub cells: Vec<char>,
|
| 24 |
pub can_move: bool,
|
| 25 |
+
pub last_move: i32,
|
| 26 |
}
|
| 27 |
|
| 28 |
#[pymethods]
|
|
|
|
| 37 |
#[staticmethod]
|
| 38 |
pub fn default() -> Self {
|
| 39 |
let board = BitBoard::default();
|
| 40 |
+
let state = Self::compute_state(&board, 0, -1);
|
| 41 |
Self {
|
| 42 |
board,
|
| 43 |
current_player: 0,
|
|
|
|
| 53 |
pub fn pass_move(&mut self) -> State {
|
| 54 |
self.board = self.board.pass_move();
|
| 55 |
self.current_player = 1 - self.current_player;
|
| 56 |
+
self.state = Self::compute_state(&self.board, self.current_player, -1);
|
| 57 |
self.state.clone()
|
| 58 |
}
|
| 59 |
|
|
|
|
| 61 |
let next = self.board.make_move(place).unwrap();
|
| 62 |
self.current_player = 1 - self.current_player;
|
| 63 |
self.board = next;
|
| 64 |
+
self.state = Self::compute_state(&self.board, self.current_player, place as i32);
|
| 65 |
self.state.clone()
|
| 66 |
}
|
| 67 |
|
|
|
|
| 92 |
}
|
| 93 |
|
| 94 |
impl Game {
|
| 95 |
+
fn compute_state(board: &BitBoard, current_player: usize, last_move: i32) -> State {
|
| 96 |
let (cnt0, cnt1) = board.count();
|
| 97 |
let moves = board.available_moves();
|
| 98 |
let cells: Vec<_> = (0..64)
|
|
|
|
| 116 |
white_score: if player == 'W' { cnt0 } else { cnt1 },
|
| 117 |
cells,
|
| 118 |
can_move: moves != 0,
|
| 119 |
+
last_move,
|
| 120 |
}
|
| 121 |
}
|
| 122 |
}
|
|
|
|
| 148 |
let b = BitBoard(2, 1);
|
| 149 |
let mut g = Game {
|
| 150 |
current_player: 0,
|
| 151 |
+
state: Game::compute_state(&b, 0, -1),
|
| 152 |
board: b,
|
| 153 |
};
|
| 154 |
assert_eq!(g.state.can_move, false);
|