refactor
Browse files- .gitignore +1 -0
- Cargo.toml +1 -2
- README.md +6 -1
- pyproject.toml +11 -1
- python/benchmark.py +38 -0
- python/othello/__init__.py +3 -0
- ui.py → python/othello/ui.py +8 -5
- src/ai.rs +54 -37
- src/consts.rs +0 -12
- src/lib.rs +1 -1
- uv.lock +14 -0
.gitignore
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
.venv
|
2 |
.sesskey
|
|
|
3 |
target/
|
4 |
__pycache__/
|
|
|
1 |
.venv
|
2 |
.sesskey
|
3 |
+
*.so
|
4 |
target/
|
5 |
__pycache__/
|
Cargo.toml
CHANGED
@@ -4,10 +4,9 @@ version = "0.1.0"
|
|
4 |
edition = "2021"
|
5 |
|
6 |
[lib]
|
7 |
-
name = "
|
8 |
|
9 |
crate-type = ["cdylib", "rlib"]
|
10 |
-
# crate-type = ["cdylib"]
|
11 |
|
12 |
[dependencies]
|
13 |
pyo3 = { version = "0.22.2", features = ["extension-module"] }
|
|
|
4 |
edition = "2021"
|
5 |
|
6 |
[lib]
|
7 |
+
name = "_othello"
|
8 |
|
9 |
crate-type = ["cdylib", "rlib"]
|
|
|
10 |
|
11 |
[dependencies]
|
12 |
pyo3 = { version = "0.22.2", features = ["extension-module"] }
|
README.md
CHANGED
@@ -12,9 +12,14 @@ license: apache-2.0
|
|
12 |
|
13 |
Othello Game implemented in Rust and python.
|
14 |
|
|
|
|
|
|
|
|
|
|
|
15 |
## Dev
|
16 |
|
17 |
```bash
|
18 |
uv sync
|
19 |
-
python ui.py
|
20 |
```
|
|
|
12 |
|
13 |
Othello Game implemented in Rust and python.
|
14 |
|
15 |
+
```bash
|
16 |
+
pip install .
|
17 |
+
othello-ui
|
18 |
+
```
|
19 |
+
|
20 |
## Dev
|
21 |
|
22 |
```bash
|
23 |
uv sync
|
24 |
+
python python/othello/ui.py
|
25 |
```
|
pyproject.toml
CHANGED
@@ -17,5 +17,15 @@ managed = true
|
|
17 |
dev-dependencies = [
|
18 |
"ruff>=0.6.2",
|
19 |
"pip>=24.2",
|
20 |
-
"maturin>=1,<2"
|
|
|
21 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
dev-dependencies = [
|
18 |
"ruff>=0.6.2",
|
19 |
"pip>=24.2",
|
20 |
+
"maturin>=1,<2",
|
21 |
+
"tqdm>=4.66.5",
|
22 |
]
|
23 |
+
|
24 |
+
[tool.maturin]
|
25 |
+
python-source = "python"
|
26 |
+
module-name = "othello._othello"
|
27 |
+
bindings = 'pyo3'
|
28 |
+
features = ["pyo3/extension-module"]
|
29 |
+
|
30 |
+
[project.scripts]
|
31 |
+
othello-ui = "othello.ui:main"
|
python/benchmark.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from othello import Game, AlphaBetaBot
|
2 |
+
import tqdm
|
3 |
+
|
4 |
+
|
5 |
+
def run_match(bot1, bot2):
|
6 |
+
game = Game.default()
|
7 |
+
while not game.state.ended:
|
8 |
+
bot = bot1 if game.state.player == "B" else bot2
|
9 |
+
pos = bot.find_move(game)
|
10 |
+
if pos >= 0:
|
11 |
+
game.make_move(pos)
|
12 |
+
else:
|
13 |
+
game.pass_move()
|
14 |
+
w, b = game.state.white_score, game.state.black_score
|
15 |
+
return 1 if b > w else 2 if w > b else 0
|
16 |
+
|
17 |
+
|
18 |
+
def run_matches(bot1, bot2, n):
|
19 |
+
cnts = [0, 0, 0]
|
20 |
+
for _ in tqdm.tqdm(range(n)):
|
21 |
+
result = run_match(bot1, bot2)
|
22 |
+
cnts[result] += 1
|
23 |
+
return cnts
|
24 |
+
|
25 |
+
|
26 |
+
if __name__ == "__main__":
|
27 |
+
for i in range(2, 10):
|
28 |
+
bot1 = AlphaBetaBot(i)
|
29 |
+
bot2 = AlphaBetaBot(i + 1)
|
30 |
+
print(f"AlphaBetaBot({bot1.depth}) vs AlphaBetaBot({bot2.depth})")
|
31 |
+
draw, win, lost = run_matches(bot1, bot2, 10)
|
32 |
+
print(f"Win: {win} | Draw: {draw} | Lost: {lost}")
|
33 |
+
print("----")
|
34 |
+
|
35 |
+
print(f"AlphaBetaBot({bot2.depth}) vs AlphaBetaBot({bot1.depth})")
|
36 |
+
draw, win, lost = run_matches(bot2, bot1, 10)
|
37 |
+
print(f"Win: {win} | Draw: {draw} | Lost: {lost}")
|
38 |
+
print("----")
|
python/othello/__init__.py
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
from othello._othello import Game, AlphaBetaBot
|
2 |
+
|
3 |
+
__all__ = ["Game", "AlphaBetaBot"]
|
ui.py → python/othello/ui.py
RENAMED
@@ -4,7 +4,6 @@ from fasthtml.common import (
|
|
4 |
Div,
|
5 |
serve,
|
6 |
Script,
|
7 |
-
Span,
|
8 |
cookie,
|
9 |
A,
|
10 |
RedirectResponse,
|
@@ -16,7 +15,6 @@ 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 = {}
|
@@ -88,7 +86,7 @@ def make_status_bar(state):
|
|
88 |
f"Black: {state.black_score}",
|
89 |
cls="bg-black text-white w-32 h-12 text-center content-center",
|
90 |
),
|
91 |
-
Div(
|
92 |
Div(
|
93 |
f"White: {state.white_score}",
|
94 |
cls="bg-white text-black w-32 h-12 text-center content-center",
|
@@ -110,7 +108,7 @@ def get_status(state):
|
|
110 |
status = "Game draw!"
|
111 |
elif state.player == "W":
|
112 |
status = "White turn"
|
113 |
-
return
|
114 |
|
115 |
|
116 |
@app.ws("/wscon")
|
@@ -147,4 +145,9 @@ async def ws(uuid: str, pos: int, send):
|
|
147 |
break
|
148 |
|
149 |
|
150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
4 |
Div,
|
5 |
serve,
|
6 |
Script,
|
|
|
7 |
cookie,
|
8 |
A,
|
9 |
RedirectResponse,
|
|
|
15 |
hdrs=[Script(src="https://cdn.tailwindcss.com")],
|
16 |
pico=False,
|
17 |
ws_hdr=True,
|
|
|
18 |
)
|
19 |
|
20 |
games = {}
|
|
|
86 |
f"Black: {state.black_score}",
|
87 |
cls="bg-black text-white w-32 h-12 text-center content-center",
|
88 |
),
|
89 |
+
Div(status, cls="content-center"),
|
90 |
Div(
|
91 |
f"White: {state.white_score}",
|
92 |
cls="bg-white text-black w-32 h-12 text-center content-center",
|
|
|
108 |
status = "Game draw!"
|
109 |
elif state.player == "W":
|
110 |
status = "White turn"
|
111 |
+
return status
|
112 |
|
113 |
|
114 |
@app.ws("/wscon")
|
|
|
145 |
break
|
146 |
|
147 |
|
148 |
+
def main():
|
149 |
+
serve("othello.ui", reload=False)
|
150 |
+
|
151 |
+
|
152 |
+
if __name__ == "__main__":
|
153 |
+
serve()
|
src/ai.rs
CHANGED
@@ -1,21 +1,26 @@
|
|
1 |
-
use crate::{bits::BitBoard,
|
2 |
use pyo3::prelude::*;
|
3 |
use rand::prelude::*;
|
4 |
|
5 |
-
pub trait
|
6 |
-
|
|
|
|
|
7 |
}
|
8 |
|
9 |
#[pyclass]
|
10 |
pub struct AlphaBetaBot {
|
|
|
11 |
depth: usize,
|
12 |
}
|
13 |
|
14 |
-
impl
|
15 |
-
fn find_move(&self,
|
|
|
16 |
let (_, move_) = self.do_search(
|
|
|
17 |
&mut rand::thread_rng(),
|
18 |
-
&
|
19 |
self.depth,
|
20 |
-i32::MAX,
|
21 |
i32::MAX,
|
@@ -32,7 +37,7 @@ impl AlphaBetaBot {
|
|
32 |
}
|
33 |
|
34 |
#[pyo3(name = "find_move")]
|
35 |
-
fn run(&self, board: &Game) ->
|
36 |
self.find_move(board)
|
37 |
}
|
38 |
}
|
@@ -40,24 +45,25 @@ impl AlphaBetaBot {
|
|
40 |
impl AlphaBetaBot {
|
41 |
fn do_search(
|
42 |
&self,
|
|
|
43 |
rng: &mut ThreadRng,
|
44 |
board: &BitBoard,
|
45 |
depth: usize,
|
46 |
mut alpha: i32,
|
47 |
beta: i32,
|
48 |
-
) -> (i32,
|
49 |
if depth == 0 {
|
50 |
-
return (
|
51 |
}
|
52 |
let mut moves = board.available_moves_list();
|
53 |
if moves.is_empty() {
|
54 |
let board = board.pass_move();
|
55 |
let moves = board.available_moves();
|
56 |
if moves == 0 {
|
57 |
-
return (
|
58 |
}
|
59 |
-
let (score, _) = self.do_search(rng, &board, depth - 1, -beta, -alpha);
|
60 |
-
return (-score,
|
61 |
}
|
62 |
moves.shuffle(rng);
|
63 |
let mut best_move = moves[0];
|
@@ -66,6 +72,7 @@ impl AlphaBetaBot {
|
|
66 |
break;
|
67 |
}
|
68 |
let (score, _) = self.do_search(
|
|
|
69 |
rng,
|
70 |
&board.make_move(move_).unwrap(),
|
71 |
depth - 1,
|
@@ -77,23 +84,31 @@ impl AlphaBetaBot {
|
|
77 |
best_move = move_
|
78 |
}
|
79 |
}
|
80 |
-
(alpha, best_move)
|
81 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
82 |
|
|
|
83 |
fn evaluate(&self, board: &BitBoard) -> i32 {
|
84 |
-
// let (sc1, sc2) = board.count();
|
85 |
-
// if sc1 + sc2 > 54 {
|
86 |
-
// return 5 * (sc1 - sc2);
|
87 |
-
// }
|
88 |
let scorer = |mask: u64| {
|
89 |
(0..64)
|
90 |
.filter(|i| mask >> i & 1 == 1)
|
91 |
-
.map(|i|
|
92 |
.sum::<i32>()
|
93 |
};
|
94 |
let n_moves0 = board.available_moves().count_ones() as i32;
|
95 |
let n_moves1 = board.pass_move().available_moves().count_ones() as i32;
|
96 |
-
scorer(board.0) - scorer(board.1) + 10 * n_moves0 - 10 * n_moves1
|
|
|
|
|
|
|
|
|
|
|
97 |
}
|
98 |
|
99 |
fn final_evaluate(&self, board: &BitBoard) -> i32 {
|
@@ -106,39 +121,41 @@ impl AlphaBetaBot {
|
|
106 |
0
|
107 |
}
|
108 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
}
|
110 |
|
111 |
#[cfg(test)]
|
112 |
mod tests {
|
113 |
-
use crate::
|
114 |
|
115 |
-
use super::{AlphaBetaBot,
|
116 |
|
117 |
#[test]
|
118 |
fn test() {
|
119 |
let mut b = Game::default();
|
120 |
-
let ai = [AlphaBetaBot { depth:
|
121 |
|
122 |
-
|
123 |
-
|
|
|
124 |
b.pass_move();
|
125 |
println!("PASS");
|
126 |
} else {
|
127 |
-
|
128 |
-
let state = b.make_move(move_);
|
129 |
println!("{}\n---\n", b.__repr__());
|
130 |
-
if state.ended {
|
131 |
-
break;
|
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);
|
143 |
}
|
144 |
}
|
|
|
1 |
+
use crate::{bits::BitBoard, game::Game};
|
2 |
use pyo3::prelude::*;
|
3 |
use rand::prelude::*;
|
4 |
|
5 |
+
pub trait Bot {
|
6 |
+
/// Find the next move (an int between 0 and 63)
|
7 |
+
/// Return -1 if there is no legal move
|
8 |
+
fn find_move(&self, board: &Game) -> i32;
|
9 |
}
|
10 |
|
11 |
#[pyclass]
|
12 |
pub struct AlphaBetaBot {
|
13 |
+
#[pyo3(get)]
|
14 |
depth: usize,
|
15 |
}
|
16 |
|
17 |
+
impl Bot for AlphaBetaBot {
|
18 |
+
fn find_move(&self, g: &Game) -> i32 {
|
19 |
+
let count = (g.board.0 + g.board.1).count_ones() as usize;
|
20 |
let (_, move_) = self.do_search(
|
21 |
+
&AlphaBetaEval { count },
|
22 |
&mut rand::thread_rng(),
|
23 |
+
&g.board,
|
24 |
self.depth,
|
25 |
-i32::MAX,
|
26 |
i32::MAX,
|
|
|
37 |
}
|
38 |
|
39 |
#[pyo3(name = "find_move")]
|
40 |
+
fn run(&self, board: &Game) -> i32 {
|
41 |
self.find_move(board)
|
42 |
}
|
43 |
}
|
|
|
45 |
impl AlphaBetaBot {
|
46 |
fn do_search(
|
47 |
&self,
|
48 |
+
eval: &AlphaBetaEval,
|
49 |
rng: &mut ThreadRng,
|
50 |
board: &BitBoard,
|
51 |
depth: usize,
|
52 |
mut alpha: i32,
|
53 |
beta: i32,
|
54 |
+
) -> (i32, i32) {
|
55 |
if depth == 0 {
|
56 |
+
return (eval.evaluate(board), -1);
|
57 |
}
|
58 |
let mut moves = board.available_moves_list();
|
59 |
if moves.is_empty() {
|
60 |
let board = board.pass_move();
|
61 |
let moves = board.available_moves();
|
62 |
if moves == 0 {
|
63 |
+
return (eval.final_evaluate(&board), -1);
|
64 |
}
|
65 |
+
let (score, _) = self.do_search(eval, rng, &board, depth - 1, -beta, -alpha);
|
66 |
+
return (-score, -1);
|
67 |
}
|
68 |
moves.shuffle(rng);
|
69 |
let mut best_move = moves[0];
|
|
|
72 |
break;
|
73 |
}
|
74 |
let (score, _) = self.do_search(
|
75 |
+
eval,
|
76 |
rng,
|
77 |
&board.make_move(move_).unwrap(),
|
78 |
depth - 1,
|
|
|
84 |
best_move = move_
|
85 |
}
|
86 |
}
|
87 |
+
(alpha, best_move as i32)
|
88 |
}
|
89 |
+
}
|
90 |
+
|
91 |
+
struct AlphaBetaEval {
|
92 |
+
// Number of pieces at the beginning of search
|
93 |
+
count: usize,
|
94 |
+
}
|
95 |
|
96 |
+
impl AlphaBetaEval {
|
97 |
fn evaluate(&self, board: &BitBoard) -> i32 {
|
|
|
|
|
|
|
|
|
98 |
let scorer = |mask: u64| {
|
99 |
(0..64)
|
100 |
.filter(|i| mask >> i & 1 == 1)
|
101 |
+
.map(|i| Self::POSITION_SCORES[i])
|
102 |
.sum::<i32>()
|
103 |
};
|
104 |
let n_moves0 = board.available_moves().count_ones() as i32;
|
105 |
let n_moves1 = board.pass_move().available_moves().count_ones() as i32;
|
106 |
+
let mut score = scorer(board.0) - scorer(board.1) + 10 * n_moves0 - 10 * n_moves1;
|
107 |
+
if self.count > 54 {
|
108 |
+
let (cnt0, cnt1) = board.count();
|
109 |
+
score += 2 * (self.count as i32 - 54) * (cnt0 - cnt1) as i32;
|
110 |
+
}
|
111 |
+
score
|
112 |
}
|
113 |
|
114 |
fn final_evaluate(&self, board: &BitBoard) -> i32 {
|
|
|
121 |
0
|
122 |
}
|
123 |
}
|
124 |
+
|
125 |
+
#[rustfmt::skip]
|
126 |
+
const POSITION_SCORES: [i32; 64] = [
|
127 |
+
300, -40, 20, 5, 5, 20, -40, 300,
|
128 |
+
-40, -80, -5, -5, -5, -5, -80, -40,
|
129 |
+
20, -5, 15, 1, 1, 15, -5, 20,
|
130 |
+
5, -5, 1, 1, 1, 1, -5, 5,
|
131 |
+
5, -5, 1, 1, 1, 1, -5, 5,
|
132 |
+
20, -5, 15, 1, 1, 15, -5, 20,
|
133 |
+
-40, -80, -5, -5, -5, -5, -80, -40,
|
134 |
+
300, -40, 20, 5, 5, 20, -40, 300,
|
135 |
+
];
|
136 |
}
|
137 |
|
138 |
#[cfg(test)]
|
139 |
mod tests {
|
140 |
+
use crate::game::Game;
|
141 |
|
142 |
+
use super::{AlphaBetaBot, Bot};
|
143 |
|
144 |
#[test]
|
145 |
fn test() {
|
146 |
let mut b = Game::default();
|
147 |
+
let ai = [AlphaBetaBot { depth: 6 }, AlphaBetaBot { depth: 3 }];
|
148 |
|
149 |
+
while !b.state.ended {
|
150 |
+
let pos = ai[b.current_player].find_move(&b);
|
151 |
+
if pos < 0 {
|
152 |
b.pass_move();
|
153 |
println!("PASS");
|
154 |
} else {
|
155 |
+
b.make_move(pos as usize);
|
|
|
156 |
println!("{}\n---\n", b.__repr__());
|
|
|
|
|
|
|
157 |
}
|
158 |
}
|
159 |
+
assert!(b.state.black_score > b.state.white_score)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
}
|
161 |
}
|
src/consts.rs
CHANGED
@@ -1,17 +1,5 @@
|
|
1 |
#![allow(clippy::unreadable_literal)]
|
2 |
|
3 |
-
#[rustfmt::skip]
|
4 |
-
pub static ALPHA_BETA_SCORES: [i32; 64] = [
|
5 |
-
300, -40, 20, 5, 5, 20, -40, 300,
|
6 |
-
-40, -80, -5, -5, -5, -5, -80, -40,
|
7 |
-
20, -5, 15, 1, 1, 15, -5, 20,
|
8 |
-
5, -5, 1, 1, 1, 1, -5, 5,
|
9 |
-
5, -5, 1, 1, 1, 1, -5, 5,
|
10 |
-
20, -5, 15, 1, 1, 15, -5, 20,
|
11 |
-
-40, -80, -5, -5, -5, -5, -80, -40,
|
12 |
-
300, -40, 20, 5, 5, 20, -40, 300,
|
13 |
-
];
|
14 |
-
|
15 |
pub static MASK: [[[u64; 2]; 4]; 64] = [
|
16 |
[
|
17 |
[252, 126],
|
|
|
1 |
#![allow(clippy::unreadable_literal)]
|
2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
pub static MASK: [[[u64; 2]; 4]; 64] = [
|
4 |
[
|
5 |
[252, 126],
|
src/lib.rs
CHANGED
@@ -9,7 +9,7 @@ pub mod consts;
|
|
9 |
pub mod game;
|
10 |
|
11 |
#[pymodule]
|
12 |
-
fn
|
13 |
m.add_class::<BitBoard>()?;
|
14 |
m.add_class::<Game>()?;
|
15 |
m.add_class::<AlphaBetaBot>()?;
|
|
|
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>()?;
|
uv.lock
CHANGED
@@ -201,6 +201,7 @@ dev = [
|
|
201 |
{ name = "maturin" },
|
202 |
{ name = "pip" },
|
203 |
{ name = "ruff" },
|
|
|
204 |
]
|
205 |
|
206 |
[package.metadata]
|
@@ -211,6 +212,7 @@ dev = [
|
|
211 |
{ name = "maturin", specifier = ">=1,<2" },
|
212 |
{ name = "pip", specifier = ">=24.2" },
|
213 |
{ name = "ruff", specifier = ">=0.6.2" },
|
|
|
214 |
]
|
215 |
|
216 |
[[package]]
|
@@ -393,6 +395,18 @@ wheels = [
|
|
393 |
{ url = "https://files.pythonhosted.org/packages/c1/60/d976da9998e4f4a99e297cda09d61ce305919ea94cbeeb476dba4fece098/starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff", size = 72020 },
|
394 |
]
|
395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
396 |
[[package]]
|
397 |
name = "uvicorn"
|
398 |
version = "0.30.6"
|
|
|
201 |
{ name = "maturin" },
|
202 |
{ name = "pip" },
|
203 |
{ name = "ruff" },
|
204 |
+
{ name = "tqdm" },
|
205 |
]
|
206 |
|
207 |
[package.metadata]
|
|
|
212 |
{ name = "maturin", specifier = ">=1,<2" },
|
213 |
{ name = "pip", specifier = ">=24.2" },
|
214 |
{ name = "ruff", specifier = ">=0.6.2" },
|
215 |
+
{ name = "tqdm", specifier = ">=4.66.5" },
|
216 |
]
|
217 |
|
218 |
[[package]]
|
|
|
395 |
{ url = "https://files.pythonhosted.org/packages/c1/60/d976da9998e4f4a99e297cda09d61ce305919ea94cbeeb476dba4fece098/starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff", size = 72020 },
|
396 |
]
|
397 |
|
398 |
+
[[package]]
|
399 |
+
name = "tqdm"
|
400 |
+
version = "4.66.5"
|
401 |
+
source = { registry = "https://pypi.org/simple" }
|
402 |
+
dependencies = [
|
403 |
+
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
404 |
+
]
|
405 |
+
sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 }
|
406 |
+
wheels = [
|
407 |
+
{ url = "https://files.pythonhosted.org/packages/48/5d/acf5905c36149bbaec41ccf7f2b68814647347b72075ac0b1fe3022fdc73/tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd", size = 78351 },
|
408 |
+
]
|
409 |
+
|
410 |
[[package]]
|
411 |
name = "uvicorn"
|
412 |
version = "0.30.6"
|