refactor
Browse files- README.md +2 -2
- fasthtml_ui.py +0 -134
- pyproject.toml +5 -4
- src/ai.rs +16 -16
- src/{board.rs → game.rs} +26 -9
- src/lib.rs +5 -5
- ui.py +150 -0
- uv.lock +48 -4
README.md
CHANGED
@@ -12,9 +12,9 @@ license: apache-2.0
|
|
12 |
|
13 |
Othello Game implemented in Rust and python.
|
14 |
|
15 |
-
##
|
16 |
|
17 |
```bash
|
18 |
uv sync
|
19 |
-
python
|
20 |
```
|
|
|
12 |
|
13 |
Othello Game implemented in Rust and python.
|
14 |
|
15 |
+
## Dev
|
16 |
|
17 |
```bash
|
18 |
uv sync
|
19 |
+
python ui.py
|
20 |
```
|
fasthtml_ui.py
DELETED
@@ -1,134 +0,0 @@
|
|
1 |
-
from fasthtml.common import (
|
2 |
-
fast_app,
|
3 |
-
Div,
|
4 |
-
serve,
|
5 |
-
Script,
|
6 |
-
Span,
|
7 |
-
)
|
8 |
-
from othello import Board, AlphaBeta
|
9 |
-
|
10 |
-
|
11 |
-
app, rt = fast_app(
|
12 |
-
hdrs=[
|
13 |
-
Script(src="https://cdn.tailwindcss.com"),
|
14 |
-
],
|
15 |
-
pico=False,
|
16 |
-
ws_hdr=True,
|
17 |
-
live=True,
|
18 |
-
)
|
19 |
-
board = Board.default()
|
20 |
-
bot = AlphaBeta(7)
|
21 |
-
|
22 |
-
|
23 |
-
@rt("/")
|
24 |
-
def get():
|
25 |
-
state = board.state
|
26 |
-
cells = [make_cell(state.cells[i], i) for i in range(64)]
|
27 |
-
|
28 |
-
return Div(
|
29 |
-
Div(
|
30 |
-
Div(
|
31 |
-
"Black:",
|
32 |
-
make_score(state.black_score, "black-score"),
|
33 |
-
cls="bg-black text-white w-32 h-12 text-center content-center shadow-md",
|
34 |
-
),
|
35 |
-
Div(make_status(state), cls="content-center"),
|
36 |
-
Div(
|
37 |
-
"White:",
|
38 |
-
make_score(state.white_score, "white-score"),
|
39 |
-
cls="bg-white text-black w-32 h-12 text-center content-center shadow-md",
|
40 |
-
),
|
41 |
-
cls="mx-auto flex w-[32rem] justify-between",
|
42 |
-
),
|
43 |
-
Div(
|
44 |
-
*cells,
|
45 |
-
cls="mx-auto mt-5 grid w-[32rem] grid-cols-8 gap-0 bg-green-300",
|
46 |
-
hx_ext="ws",
|
47 |
-
ws_connect="/wscon",
|
48 |
-
),
|
49 |
-
cls="m-auto max-w-2xl bg-gray-200 p-12 mt-12",
|
50 |
-
)
|
51 |
-
|
52 |
-
|
53 |
-
def make_cell(v, pos):
|
54 |
-
stone = None
|
55 |
-
if v == "?":
|
56 |
-
stone = Div(
|
57 |
-
hx_trigger="click",
|
58 |
-
hx_vals=f'{{"pos": {pos}}}',
|
59 |
-
ws_send=True,
|
60 |
-
hx_swap_oob="true",
|
61 |
-
id=f"cell-{pos}",
|
62 |
-
# cls="h-16 w-16 cursor-pointer bg-purple-200 hover:bg-purple-400",
|
63 |
-
cls="mx-2 my-2 h-12 w-12 rounded-full cursor-pointer bg-purple-200 hover:bg-purple-300",
|
64 |
-
)
|
65 |
-
elif v == "B":
|
66 |
-
stone = Div(
|
67 |
-
cls="mx-2 my-2 h-12 w-12 rounded-full bg-black shadow-sm shadow-white"
|
68 |
-
)
|
69 |
-
elif v == "W":
|
70 |
-
stone = Div(
|
71 |
-
cls="mx-2 my-2 h-12 w-12 rounded-full bg-white shadow-sm shadow-black"
|
72 |
-
)
|
73 |
-
return Div(
|
74 |
-
stone,
|
75 |
-
id=f"cell-{pos}",
|
76 |
-
cls="h-16 w-16 border border-sky-100",
|
77 |
-
hx_swap_oob="true",
|
78 |
-
)
|
79 |
-
|
80 |
-
|
81 |
-
def make_score(v, id):
|
82 |
-
return Span(v, id=id, hx_swap_oob="true")
|
83 |
-
|
84 |
-
|
85 |
-
def make_status(state):
|
86 |
-
status = "Black turn"
|
87 |
-
if state.ended:
|
88 |
-
if state.white_score > state.black_score:
|
89 |
-
status = "White won!"
|
90 |
-
elif state.white_score < state.black_score:
|
91 |
-
status = "Black won!"
|
92 |
-
else:
|
93 |
-
status = "Game draw!"
|
94 |
-
elif state.player == "W":
|
95 |
-
status = "White turn"
|
96 |
-
return Span(status, id="status", hx_swap_oob="true")
|
97 |
-
|
98 |
-
|
99 |
-
@app.ws("/wscon")
|
100 |
-
async def ws(pos: int, send):
|
101 |
-
# Human
|
102 |
-
state = await move(pos, send)
|
103 |
-
if state.ended:
|
104 |
-
return
|
105 |
-
|
106 |
-
# Bot
|
107 |
-
while True:
|
108 |
-
pos = bot.find_move(board) if state.can_move else -1
|
109 |
-
state = await move(pos, send)
|
110 |
-
if not state.can_move and not state.ended:
|
111 |
-
# Human has no move
|
112 |
-
await move(-1, send)
|
113 |
-
else:
|
114 |
-
break
|
115 |
-
|
116 |
-
|
117 |
-
async def move(pos: int, send):
|
118 |
-
prev_state = board.state
|
119 |
-
state = board.make_move(pos) if pos >= 0 else board.pass_move()
|
120 |
-
|
121 |
-
await send(make_cell(state.cells[pos], pos))
|
122 |
-
# await asyncio.sleep(1)
|
123 |
-
|
124 |
-
for i, (c1, c2) in enumerate(zip(prev_state.cells, (state.cells))):
|
125 |
-
if i != pos and c1 != c2:
|
126 |
-
await send(make_cell(c2, i))
|
127 |
-
|
128 |
-
await send(make_score(state.white_score, "white-score"))
|
129 |
-
await send(make_score(state.black_score, "black-score"))
|
130 |
-
await send(make_status(state))
|
131 |
-
return state
|
132 |
-
|
133 |
-
|
134 |
-
serve()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pyproject.toml
CHANGED
@@ -5,12 +5,8 @@ description = "Add your description here"
|
|
5 |
readme = "README.md"
|
6 |
requires-python = ">=3.11"
|
7 |
dependencies = [
|
8 |
-
"maturin>=1.7.1",
|
9 |
"python-fasthtml>=0.4.5",
|
10 |
]
|
11 |
-
dev-dependencies = [
|
12 |
-
"ruff>=0.6.2",
|
13 |
-
]
|
14 |
|
15 |
[build-system]
|
16 |
requires = ["maturin>=1,<2"]
|
@@ -18,3 +14,8 @@ build-backend = "maturin"
|
|
18 |
|
19 |
[tool.uv]
|
20 |
managed = true
|
|
|
|
|
|
|
|
|
|
|
|
5 |
readme = "README.md"
|
6 |
requires-python = ">=3.11"
|
7 |
dependencies = [
|
|
|
8 |
"python-fasthtml>=0.4.5",
|
9 |
]
|
|
|
|
|
|
|
10 |
|
11 |
[build-system]
|
12 |
requires = ["maturin>=1,<2"]
|
|
|
14 |
|
15 |
[tool.uv]
|
16 |
managed = true
|
17 |
+
dev-dependencies = [
|
18 |
+
"ruff>=0.6.2",
|
19 |
+
"pip>=24.2",
|
20 |
+
"maturin>=1,<2"
|
21 |
+
]
|
src/ai.rs
CHANGED
@@ -1,18 +1,18 @@
|
|
1 |
-
use crate::{bits::BitBoard,
|
2 |
use pyo3::prelude::*;
|
3 |
use rand::prelude::*;
|
4 |
|
5 |
pub trait AI {
|
6 |
-
fn
|
7 |
}
|
8 |
|
9 |
#[pyclass]
|
10 |
-
pub struct
|
11 |
depth: usize,
|
12 |
}
|
13 |
|
14 |
-
impl AI for
|
15 |
-
fn
|
16 |
let (_, move_) = self.do_search(
|
17 |
&mut rand::thread_rng(),
|
18 |
&board.board,
|
@@ -25,18 +25,19 @@ impl AI for AlphaBeta {
|
|
25 |
}
|
26 |
|
27 |
#[pymethods]
|
28 |
-
impl
|
29 |
#[new]
|
30 |
pub fn new(depth: usize) -> Self {
|
31 |
Self { depth }
|
32 |
}
|
33 |
|
34 |
-
|
35 |
-
|
|
|
36 |
}
|
37 |
}
|
38 |
|
39 |
-
impl
|
40 |
fn do_search(
|
41 |
&self,
|
42 |
rng: &mut ThreadRng,
|
@@ -109,21 +110,21 @@ impl AlphaBeta {
|
|
109 |
|
110 |
#[cfg(test)]
|
111 |
mod tests {
|
112 |
-
use crate::{bits::BitBoard,
|
113 |
|
114 |
-
use super::{
|
115 |
|
116 |
#[test]
|
117 |
fn test() {
|
118 |
-
let mut b =
|
119 |
-
let ai = [
|
120 |
|
121 |
for i in 0..80 {
|
122 |
if b.available_moves().is_empty() {
|
123 |
b.pass_move();
|
124 |
println!("PASS");
|
125 |
} else {
|
126 |
-
let move_ = ai[i % 2].
|
127 |
let state = b.make_move(move_);
|
128 |
println!("{}\n---\n", b.__repr__());
|
129 |
if state.ended {
|
@@ -131,12 +132,11 @@ mod tests {
|
|
131 |
}
|
132 |
}
|
133 |
}
|
134 |
-
assert!(false);
|
135 |
}
|
136 |
|
137 |
#[test]
|
138 |
fn test_evaluate() {
|
139 |
-
let ai =
|
140 |
let b = BitBoard(3, 12);
|
141 |
println!("{:?}", b);
|
142 |
assert_eq!(ai.evaluate(&BitBoard(3, 12)), 245);
|
|
|
1 |
+
use crate::{bits::BitBoard, consts::ALPHA_BETA_SCORES, game::Game};
|
2 |
use pyo3::prelude::*;
|
3 |
use rand::prelude::*;
|
4 |
|
5 |
pub trait AI {
|
6 |
+
fn find_move(&self, board: &Game) -> usize;
|
7 |
}
|
8 |
|
9 |
#[pyclass]
|
10 |
+
pub struct AlphaBetaBot {
|
11 |
depth: usize,
|
12 |
}
|
13 |
|
14 |
+
impl AI for AlphaBetaBot {
|
15 |
+
fn find_move(&self, board: &Game) -> usize {
|
16 |
let (_, move_) = self.do_search(
|
17 |
&mut rand::thread_rng(),
|
18 |
&board.board,
|
|
|
25 |
}
|
26 |
|
27 |
#[pymethods]
|
28 |
+
impl AlphaBetaBot {
|
29 |
#[new]
|
30 |
pub fn new(depth: usize) -> Self {
|
31 |
Self { depth }
|
32 |
}
|
33 |
|
34 |
+
#[pyo3(name = "find_move")]
|
35 |
+
fn run(&self, board: &Game) -> usize {
|
36 |
+
self.find_move(board)
|
37 |
}
|
38 |
}
|
39 |
|
40 |
+
impl AlphaBetaBot {
|
41 |
fn do_search(
|
42 |
&self,
|
43 |
rng: &mut ThreadRng,
|
|
|
110 |
|
111 |
#[cfg(test)]
|
112 |
mod tests {
|
113 |
+
use crate::{bits::BitBoard, game::Game};
|
114 |
|
115 |
+
use super::{AlphaBetaBot, AI};
|
116 |
|
117 |
#[test]
|
118 |
fn test() {
|
119 |
+
let mut b = Game::default();
|
120 |
+
let ai = [AlphaBetaBot { depth: 5 }, AlphaBetaBot { depth: 6 }];
|
121 |
|
122 |
for i in 0..80 {
|
123 |
if b.available_moves().is_empty() {
|
124 |
b.pass_move();
|
125 |
println!("PASS");
|
126 |
} else {
|
127 |
+
let move_ = ai[i % 2].find_move(&b);
|
128 |
let state = b.make_move(move_);
|
129 |
println!("{}\n---\n", b.__repr__());
|
130 |
if state.ended {
|
|
|
132 |
}
|
133 |
}
|
134 |
}
|
|
|
135 |
}
|
136 |
|
137 |
#[test]
|
138 |
fn test_evaluate() {
|
139 |
+
let ai = AlphaBetaBot { depth: 3 };
|
140 |
let b = BitBoard(3, 12);
|
141 |
println!("{:?}", b);
|
142 |
assert_eq!(ai.evaluate(&BitBoard(3, 12)), 245);
|
src/{board.rs → game.rs}
RENAMED
@@ -6,7 +6,7 @@ const PLAYERS: [char; 2] = ['B', 'W'];
|
|
6 |
|
7 |
#[pyclass]
|
8 |
#[derive(Clone)]
|
9 |
-
pub struct
|
10 |
pub board: BitBoard,
|
11 |
pub current_player: usize, // 0 or 1
|
12 |
#[pyo3(get)]
|
@@ -32,7 +32,7 @@ impl State {
|
|
32 |
}
|
33 |
|
34 |
#[pymethods]
|
35 |
-
impl
|
36 |
#[staticmethod]
|
37 |
pub fn default() -> Self {
|
38 |
let board = BitBoard::default();
|
@@ -75,9 +75,9 @@ impl Board {
|
|
75 |
}
|
76 |
if state.ended {
|
77 |
s.push_str(match state.black_score.cmp(&state.white_score) {
|
78 |
-
Equal => "
|
79 |
-
Less => "
|
80 |
-
Greater => "
|
81 |
});
|
82 |
} else {
|
83 |
s.push_str(&format!(
|
@@ -90,7 +90,7 @@ impl Board {
|
|
90 |
}
|
91 |
}
|
92 |
|
93 |
-
impl
|
94 |
fn compute_state(board: &BitBoard, current_player: usize) -> State {
|
95 |
let (cnt0, cnt1) = board.count();
|
96 |
let moves = board.available_moves();
|
@@ -121,11 +121,12 @@ impl Board {
|
|
121 |
|
122 |
#[cfg(test)]
|
123 |
mod tests {
|
124 |
-
use super::
|
|
|
125 |
|
126 |
#[test]
|
127 |
-
fn
|
128 |
-
let mut b =
|
129 |
|
130 |
assert_eq!(b.available_moves(), &[19, 26, 37, 44]);
|
131 |
b.make_move(44);
|
@@ -139,4 +140,20 @@ mod tests {
|
|
139 |
|
140 |
assert_eq!(b.__repr__(), "\n........\n........\n..?????.\n...WWW..\n...BB...\n....B...\n........\n........\nB to play. Available moves: [18, 19, 20, 21, 22]");
|
141 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
142 |
}
|
|
|
6 |
|
7 |
#[pyclass]
|
8 |
#[derive(Clone)]
|
9 |
+
pub struct Game {
|
10 |
pub board: BitBoard,
|
11 |
pub current_player: usize, // 0 or 1
|
12 |
#[pyo3(get)]
|
|
|
32 |
}
|
33 |
|
34 |
#[pymethods]
|
35 |
+
impl Game {
|
36 |
#[staticmethod]
|
37 |
pub fn default() -> Self {
|
38 |
let board = BitBoard::default();
|
|
|
75 |
}
|
76 |
if state.ended {
|
77 |
s.push_str(match state.black_score.cmp(&state.white_score) {
|
78 |
+
Equal => "\nGame draw!",
|
79 |
+
Less => "\nWhite won!",
|
80 |
+
Greater => "\nBlack won!",
|
81 |
});
|
82 |
} else {
|
83 |
s.push_str(&format!(
|
|
|
90 |
}
|
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();
|
|
|
121 |
|
122 |
#[cfg(test)]
|
123 |
mod tests {
|
124 |
+
use super::Game;
|
125 |
+
use crate::bits::BitBoard;
|
126 |
|
127 |
#[test]
|
128 |
+
fn test_1() {
|
129 |
+
let mut b = Game::default();
|
130 |
|
131 |
assert_eq!(b.available_moves(), &[19, 26, 37, 44]);
|
132 |
b.make_move(44);
|
|
|
140 |
|
141 |
assert_eq!(b.__repr__(), "\n........\n........\n..?????.\n...WWW..\n...BB...\n....B...\n........\n........\nB to play. Available moves: [18, 19, 20, 21, 22]");
|
142 |
}
|
143 |
+
|
144 |
+
#[test]
|
145 |
+
fn test_2() {
|
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);
|
153 |
+
g.pass_move();
|
154 |
+
assert_eq!(g.state.can_move, true);
|
155 |
+
g.make_move(2);
|
156 |
+
assert_eq!(g.state.ended, true);
|
157 |
+
assert_eq!(g.state.white_score, 3);
|
158 |
+
}
|
159 |
}
|
src/lib.rs
CHANGED
@@ -1,18 +1,18 @@
|
|
1 |
-
use ai::
|
2 |
use bits::BitBoard;
|
3 |
-
use
|
4 |
use pyo3::prelude::*;
|
5 |
|
6 |
pub mod ai;
|
7 |
pub mod bits;
|
8 |
-
pub mod board;
|
9 |
pub mod consts;
|
|
|
10 |
|
11 |
#[pymodule]
|
12 |
fn othello(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
13 |
m.add_class::<BitBoard>()?;
|
14 |
-
m.add_class::<
|
15 |
-
m.add_class::<
|
16 |
m.add_class::<State>()?;
|
17 |
Ok(())
|
18 |
}
|
|
|
1 |
+
use ai::AlphaBetaBot;
|
2 |
use bits::BitBoard;
|
3 |
+
use game::{Game, State};
|
4 |
use pyo3::prelude::*;
|
5 |
|
6 |
pub mod ai;
|
7 |
pub mod bits;
|
|
|
8 |
pub mod consts;
|
9 |
+
pub mod game;
|
10 |
|
11 |
#[pymodule]
|
12 |
fn othello(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
13 |
m.add_class::<BitBoard>()?;
|
14 |
+
m.add_class::<Game>()?;
|
15 |
+
m.add_class::<AlphaBetaBot>()?;
|
16 |
m.add_class::<State>()?;
|
17 |
Ok(())
|
18 |
}
|
ui.py
ADDED
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from uuid import uuid1
|
2 |
+
from fasthtml.common import (
|
3 |
+
fast_app,
|
4 |
+
Div,
|
5 |
+
serve,
|
6 |
+
Script,
|
7 |
+
Span,
|
8 |
+
cookie,
|
9 |
+
A,
|
10 |
+
RedirectResponse,
|
11 |
+
)
|
12 |
+
from othello import Game, AlphaBetaBot
|
13 |
+
|
14 |
+
|
15 |
+
app, rt = fast_app(
|
16 |
+
hdrs=[Script(src="https://cdn.tailwindcss.com")],
|
17 |
+
pico=False,
|
18 |
+
ws_hdr=True,
|
19 |
+
live=True,
|
20 |
+
)
|
21 |
+
|
22 |
+
games = {}
|
23 |
+
bot = AlphaBetaBot(7)
|
24 |
+
|
25 |
+
|
26 |
+
@rt("/")
|
27 |
+
def get(uuid: str = None):
|
28 |
+
if not uuid:
|
29 |
+
uuid = str(uuid1())
|
30 |
+
|
31 |
+
if uuid not in games:
|
32 |
+
games[uuid] = Game.default()
|
33 |
+
|
34 |
+
return cookie("uuid", uuid), make_app(uuid)
|
35 |
+
|
36 |
+
|
37 |
+
@app.get("/new")
|
38 |
+
def new(uuid: str = None):
|
39 |
+
if uuid is not None:
|
40 |
+
del games[uuid]
|
41 |
+
return RedirectResponse("/")
|
42 |
+
|
43 |
+
|
44 |
+
def make_app(uuid):
|
45 |
+
state = games[uuid].state
|
46 |
+
return Div(
|
47 |
+
Div(
|
48 |
+
make_status_bar(state),
|
49 |
+
Div(
|
50 |
+
*(make_cell(state.cells[i], i, uuid) for i in range(64)),
|
51 |
+
cls="grid grid-cols-8 gap-0 bg-green-300 mb-5 lg:mt-5",
|
52 |
+
hx_ext="ws",
|
53 |
+
ws_connect="/wscon",
|
54 |
+
),
|
55 |
+
A("New Game", href="/new", cls="rounded-md bg-teal-600 mt-5 px-5 py-2"),
|
56 |
+
cls="m-auto w-fit",
|
57 |
+
),
|
58 |
+
cls="m-auto max-w-2xl bg-gray-200 lg:pt-12 pb-12 lg:mt-12",
|
59 |
+
)
|
60 |
+
|
61 |
+
|
62 |
+
def make_cell(v, pos, uuid):
|
63 |
+
style = "m-2 size-8 lg:size-12 rounded-full"
|
64 |
+
stone = None
|
65 |
+
if v == "?":
|
66 |
+
stone = Div(
|
67 |
+
hx_trigger="click",
|
68 |
+
hx_vals=f'{{"pos": {pos}, "uuid": "{uuid}"}}',
|
69 |
+
ws_send=True,
|
70 |
+
cls=f"{style} cursor-pointer bg-purple-200 hover:bg-purple-300",
|
71 |
+
)
|
72 |
+
elif v == "B":
|
73 |
+
stone = Div(cls=f"{style} shadow-sm bg-black shadow-white")
|
74 |
+
elif v == "W":
|
75 |
+
stone = Div(cls=f"{style} shadow-sm bg-white shadow-black")
|
76 |
+
return Div(
|
77 |
+
stone,
|
78 |
+
id=f"cell-{pos}",
|
79 |
+
cls="size-12 xl:size-16 border border-sky-100",
|
80 |
+
hx_swap_oob="true",
|
81 |
+
)
|
82 |
+
|
83 |
+
|
84 |
+
def make_status_bar(state):
|
85 |
+
status = get_status(state)
|
86 |
+
return Div(
|
87 |
+
Div(
|
88 |
+
f"Black: {state.black_score}",
|
89 |
+
cls="bg-black text-white w-32 h-12 text-center content-center",
|
90 |
+
),
|
91 |
+
Div(Span(status, id="status", hx_swap_oob="true"), cls="content-center"),
|
92 |
+
Div(
|
93 |
+
f"White: {state.white_score}",
|
94 |
+
cls="bg-white text-black w-32 h-12 text-center content-center",
|
95 |
+
),
|
96 |
+
cls="flex justify-between",
|
97 |
+
id="status-bar",
|
98 |
+
hx_swap_oob="true",
|
99 |
+
)
|
100 |
+
|
101 |
+
|
102 |
+
def get_status(state):
|
103 |
+
status = "Black turn"
|
104 |
+
if state.ended:
|
105 |
+
if state.white_score > state.black_score:
|
106 |
+
status = "White won!"
|
107 |
+
elif state.white_score < state.black_score:
|
108 |
+
status = "Black won!"
|
109 |
+
else:
|
110 |
+
status = "Game draw!"
|
111 |
+
elif state.player == "W":
|
112 |
+
status = "White turn"
|
113 |
+
return Span(status, id="status", hx_swap_oob="true")
|
114 |
+
|
115 |
+
|
116 |
+
@app.ws("/wscon")
|
117 |
+
async def ws(uuid: str, pos: int, send):
|
118 |
+
game = games[uuid]
|
119 |
+
|
120 |
+
async def play(pos: int):
|
121 |
+
prev_state = game.state
|
122 |
+
state = game.make_move(pos) if pos >= 0 else game.pass_move()
|
123 |
+
|
124 |
+
await send(make_cell(state.cells[pos], pos, uuid))
|
125 |
+
# await asyncio.sleep(1)
|
126 |
+
|
127 |
+
for i, (c1, c2) in enumerate(zip(prev_state.cells, state.cells)):
|
128 |
+
if i != pos and c1 != c2:
|
129 |
+
await send(make_cell(c2, i, uuid))
|
130 |
+
await send(make_status_bar(state))
|
131 |
+
return state
|
132 |
+
|
133 |
+
# Human
|
134 |
+
state = await play(pos)
|
135 |
+
if state.ended:
|
136 |
+
return
|
137 |
+
|
138 |
+
# Bot
|
139 |
+
while True:
|
140 |
+
pos = bot.find_move(game) if state.can_move else -1
|
141 |
+
state = await play(pos)
|
142 |
+
if not state.can_move and not state.ended:
|
143 |
+
# Human has no move
|
144 |
+
state = await play(-1)
|
145 |
+
assert pos != -1 and state.can_move and not state.ended
|
146 |
+
else:
|
147 |
+
break
|
148 |
+
|
149 |
+
|
150 |
+
serve()
|
uv.lock
CHANGED
@@ -193,14 +193,24 @@ name = "othello"
|
|
193 |
version = "0.1.0"
|
194 |
source = { editable = "." }
|
195 |
dependencies = [
|
196 |
-
{ name = "maturin" },
|
197 |
{ name = "python-fasthtml" },
|
198 |
]
|
199 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
[package.metadata]
|
201 |
-
requires-dist = [
|
202 |
-
|
203 |
-
|
|
|
|
|
|
|
|
|
204 |
]
|
205 |
|
206 |
[[package]]
|
@@ -212,6 +222,15 @@ wheels = [
|
|
212 |
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
|
213 |
]
|
214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
215 |
[[package]]
|
216 |
name = "python-dateutil"
|
217 |
version = "2.9.0.post0"
|
@@ -298,6 +317,31 @@ wheels = [
|
|
298 |
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
299 |
]
|
300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
301 |
[[package]]
|
302 |
name = "six"
|
303 |
version = "1.16.0"
|
|
|
193 |
version = "0.1.0"
|
194 |
source = { editable = "." }
|
195 |
dependencies = [
|
|
|
196 |
{ name = "python-fasthtml" },
|
197 |
]
|
198 |
|
199 |
+
[package.dev-dependencies]
|
200 |
+
dev = [
|
201 |
+
{ name = "maturin" },
|
202 |
+
{ name = "pip" },
|
203 |
+
{ name = "ruff" },
|
204 |
+
]
|
205 |
+
|
206 |
[package.metadata]
|
207 |
+
requires-dist = [{ name = "python-fasthtml", specifier = ">=0.4.5" }]
|
208 |
+
|
209 |
+
[package.metadata.requires-dev]
|
210 |
+
dev = [
|
211 |
+
{ name = "maturin", specifier = ">=1,<2" },
|
212 |
+
{ name = "pip", specifier = ">=24.2" },
|
213 |
+
{ name = "ruff", specifier = ">=0.6.2" },
|
214 |
]
|
215 |
|
216 |
[[package]]
|
|
|
222 |
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
|
223 |
]
|
224 |
|
225 |
+
[[package]]
|
226 |
+
name = "pip"
|
227 |
+
version = "24.2"
|
228 |
+
source = { registry = "https://pypi.org/simple" }
|
229 |
+
sdist = { url = "https://files.pythonhosted.org/packages/4d/87/fb90046e096a03aeab235e139436b3fe804cdd447ed2093b0d70eba3f7f8/pip-24.2.tar.gz", hash = "sha256:5b5e490b5e9cb275c879595064adce9ebd31b854e3e803740b72f9ccf34a45b8", size = 1922041 }
|
230 |
+
wheels = [
|
231 |
+
{ url = "https://files.pythonhosted.org/packages/d4/55/90db48d85f7689ec6f81c0db0622d704306c5284850383c090e6c7195a5c/pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2", size = 1815170 },
|
232 |
+
]
|
233 |
+
|
234 |
[[package]]
|
235 |
name = "python-dateutil"
|
236 |
version = "2.9.0.post0"
|
|
|
317 |
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
318 |
]
|
319 |
|
320 |
+
[[package]]
|
321 |
+
name = "ruff"
|
322 |
+
version = "0.6.2"
|
323 |
+
source = { registry = "https://pypi.org/simple" }
|
324 |
+
sdist = { url = "https://files.pythonhosted.org/packages/23/f4/279d044f66b79261fd37df76bf72b64471afab5d3b7906a01499c4451910/ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be", size = 2460281 }
|
325 |
+
wheels = [
|
326 |
+
{ url = "https://files.pythonhosted.org/packages/72/4b/47dd7a69287afb4069fa42c198e899463605460a58120196711bfcf0446b/ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c", size = 9695871 },
|
327 |
+
{ url = "https://files.pythonhosted.org/packages/ae/c3/8aac62ac4638c14a740ee76a755a925f2d0d04580ab790a9887accb729f6/ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570", size = 9459354 },
|
328 |
+
{ url = "https://files.pythonhosted.org/packages/2f/cf/77fbd8d4617b9b9c503f9bffb8552c4e3ea1a58dc36975e7a9104ffb0f85/ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158", size = 9163871 },
|
329 |
+
{ url = "https://files.pythonhosted.org/packages/05/1c/765192bab32b79efbb498b06f0b9dcb3629112b53b8777ae1d19b8209e09/ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534", size = 10096250 },
|
330 |
+
{ url = "https://files.pythonhosted.org/packages/08/d0/86f3cb0f6934c99f759c232984a5204d67a26745cad2d9edff6248adf7d2/ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b", size = 9475376 },
|
331 |
+
{ url = "https://files.pythonhosted.org/packages/cd/cc/4c8d0e225b559a3fae6092ec310d7150d3b02b4669e9223f783ef64d82c0/ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d", size = 10295634 },
|
332 |
+
{ url = "https://files.pythonhosted.org/packages/db/96/d2699cfb1bb5a01c68122af43454c76c31331e1c8a9bd97d653d7c82524b/ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66", size = 11024941 },
|
333 |
+
{ url = "https://files.pythonhosted.org/packages/8b/a9/6ecd66af8929e0f2a1ed308a4137f3521789f28f0eb97d32c2ca3aa7000c/ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8", size = 10606894 },
|
334 |
+
{ url = "https://files.pythonhosted.org/packages/e4/73/2ee4cd19f44992fedac1cc6db9e3d825966072f6dcbd4032f21cbd063170/ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1", size = 11552886 },
|
335 |
+
{ url = "https://files.pythonhosted.org/packages/60/4c/c0f1cd35ce4a93c54a6bb1ee6934a3a205fa02198dd076678193853ceea1/ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1", size = 10264945 },
|
336 |
+
{ url = "https://files.pythonhosted.org/packages/c4/89/e45c9359b9cdd4245512ea2b9f2bb128a997feaa5f726fc9e8c7a66afadf/ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23", size = 10100007 },
|
337 |
+
{ url = "https://files.pythonhosted.org/packages/06/74/0bd4e0a7ed5f6908df87892f9bf60a2356c0fd74102d8097298bd9b4f346/ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a", size = 9559267 },
|
338 |
+
{ url = "https://files.pythonhosted.org/packages/54/03/3dc6dc9419f276f05805bf888c279e3e0b631284abd548d9e87cebb93aec/ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c", size = 9905304 },
|
339 |
+
{ url = "https://files.pythonhosted.org/packages/5c/5b/d6a72a6a6bbf097c09de468326ef5fa1c9e7aa5e6e45979bc0d984b0dbe7/ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56", size = 10341480 },
|
340 |
+
{ url = "https://files.pythonhosted.org/packages/79/a9/0f2f21fe15ba537c46598f96aa9ae4a3d4b9ec64926664617ca6a8c772f4/ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da", size = 7961901 },
|
341 |
+
{ url = "https://files.pythonhosted.org/packages/b0/80/fff12ffe11853d9f4ea3e5221e6dd2e93640a161c05c9579833e09ad40a7/ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2", size = 8783320 },
|
342 |
+
{ url = "https://files.pythonhosted.org/packages/56/91/577cdd64cce5e74d3f8b5ecb93f29566def569c741eb008aed4f331ef821/ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9", size = 8225886 },
|
343 |
+
]
|
344 |
+
|
345 |
[[package]]
|
346 |
name = "six"
|
347 |
version = "1.16.0"
|