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);
|