weiss-sim 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/Cargo.lock +2 -2
- weiss_sim-0.1.3/PKG-INFO +421 -0
- weiss_sim-0.1.3/README.md +394 -0
- weiss_sim-0.1.3/pyproject.toml +49 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/Cargo.toml +1 -1
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_py/Cargo.toml +1 -1
- weiss_sim-0.1.2/PKG-INFO +0 -5
- weiss_sim-0.1.2/pyproject.toml +0 -25
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/Cargo.toml +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/python/weiss_sim/__init__.py +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/python/weiss_sim/rl.py +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/benches/alloc_benches.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/benches/core_benches.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/bin/carddb_pack.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/bin/replay_dump.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/config.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/db.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/effects.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/encode.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/env/interaction.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/env/modifiers.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/env/movement.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/env/phases.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/env/tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/env/visibility.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/env.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/events.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/fingerprint.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/legal.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/lib.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/pool.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/replay.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/rules.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/state.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/util.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/src/visibility_policy.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/ability_index_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/ability_template_expansion_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/action_space_reachability_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/attack_legality_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/auto_resolve_cap_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/choice_paging_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/combat_basic_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/combat_damage_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/combat_support.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/concede_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/control_change_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/deck_support.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/determinism_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/end_phase_trigger_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/engine_support.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/event_damage_pipeline_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/fingerprint_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/illegal_action_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/level_encore_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/modifier_duration_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/obs_encoding_layout_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/obs_reason_bits_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/priority_action_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/priority_window_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/property_invariants_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/public_obs_invariance_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/replacement_layer_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/replay_bundle_support.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/replay_io_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/stack_and_modifiers_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/targeting_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/trigger_order_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/trigger_refresh_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/trigger_resolution_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/turn_cycle_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/unification_coverage_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/wsdb_io_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_core/tests/zone_move_semantics_tests.rs +0 -0
- {weiss_sim-0.1.2 → weiss_sim-0.1.3}/weiss_py/src/lib.rs +0 -0
|
@@ -1395,7 +1395,7 @@ dependencies = [
|
|
|
1395
1395
|
|
|
1396
1396
|
[[package]]
|
|
1397
1397
|
name = "weiss_core"
|
|
1398
|
-
version = "0.1.
|
|
1398
|
+
version = "0.1.3"
|
|
1399
1399
|
dependencies = [
|
|
1400
1400
|
"anyhow",
|
|
1401
1401
|
"bitvec",
|
|
@@ -1417,7 +1417,7 @@ dependencies = [
|
|
|
1417
1417
|
|
|
1418
1418
|
[[package]]
|
|
1419
1419
|
name = "weiss_py"
|
|
1420
|
-
version = "0.1.
|
|
1420
|
+
version = "0.1.3"
|
|
1421
1421
|
dependencies = [
|
|
1422
1422
|
"anyhow",
|
|
1423
1423
|
"numpy",
|
weiss_sim-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: weiss-sim
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Rust
|
|
12
|
+
Classifier: Topic :: Games/Entertainment
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Requires-Dist: numpy>=1.23
|
|
15
|
+
Summary: Deterministic Weiss Schwarz simulator with a Rust core and Python bindings.
|
|
16
|
+
Keywords: weiss-schwarz,reinforcement-learning,simulation,pyo3,rl
|
|
17
|
+
Author: Lallan
|
|
18
|
+
License: MIT OR Apache-2.0
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
21
|
+
Project-URL: Homepage, https://github.com/victorwp288/weiss-schwarz-simulator
|
|
22
|
+
Project-URL: Repository, https://github.com/victorwp288/weiss-schwarz-simulator
|
|
23
|
+
Project-URL: Documentation, https://victorwp288.github.io/weiss-schwarz-simulator/rustdoc/
|
|
24
|
+
Project-URL: Issues, https://github.com/victorwp288/weiss-schwarz-simulator/issues
|
|
25
|
+
Project-URL: Changelog, https://github.com/victorwp288/weiss-schwarz-simulator/blob/main/CHANGELOG.md
|
|
26
|
+
|
|
27
|
+
# Weiss Schwarz Simulator (Rust core + Python bindings)
|
|
28
|
+
|
|
29
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/actions/workflows/ci.yml)
|
|
30
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/actions/workflows/wheels.yml)
|
|
31
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/actions/workflows/benchmarks.yml)
|
|
32
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/actions/workflows/security.yml)
|
|
33
|
+
[](https://victorwp288.github.io/weiss-schwarz-simulator/rustdoc/)
|
|
34
|
+
[](https://pypi.org/project/weiss-sim/)
|
|
35
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/blob/main/CHANGELOG.md)
|
|
36
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/commits/main)
|
|
37
|
+
|
|
38
|
+
Deterministic, RL-first Weiss Schwarz simulation: **Rust runs the hot loop**, advances until a **decision point**, and exposes a **fixed action space + mask** (and legal action ids) for efficient batched training. Python gets a thin `EnvPool` wrapper for stepping many environments in parallel.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Why this exists
|
|
43
|
+
|
|
44
|
+
Weiss Schwarz has hidden information, branching, and timing windows. For RL you typically want:
|
|
45
|
+
|
|
46
|
+
- **Determinism**: reproduce episodes from a seed and action sequence.
|
|
47
|
+
- **Few boundary crossings**: avoid Python↔Rust overhead for micro-steps.
|
|
48
|
+
- **Stable action space**: fixed-size actions with legality masking.
|
|
49
|
+
- **Introspectability**: canonical action descriptions, replays, and event logs.
|
|
50
|
+
|
|
51
|
+
This repo is built around those constraints.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Highlights
|
|
56
|
+
|
|
57
|
+
- **Advance-until-decision loop**: engine runs internally until a player must act.
|
|
58
|
+
- **Canonical legal actions**: `ActionDesc` list is the single truth source.
|
|
59
|
+
- **Fixed action id space + mask**: derived from canonical actions and **versioned** (`ACTION_ENCODING_VERSION`).
|
|
60
|
+
- **Legal action ids (fast)**: use ids to avoid Python-side mask scans in hot loops.
|
|
61
|
+
- **Fixed-length observations**: `int32` arrays, **versioned** (`OBS_ENCODING_VERSION`).
|
|
62
|
+
- **Multicore stepping**: `EnvPool` uses Rayon; Python binding releases the GIL.
|
|
63
|
+
- **Replays**: deterministic, versioned, optional event stream, public-safe sanitization when enabled.
|
|
64
|
+
- **Curriculum switches**: gate rules/features for training curricula.
|
|
65
|
+
|
|
66
|
+
Each environment is deterministic given its seed and action sequence. Parallel batch stepping does not change outcomes because envs are fully isolated.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Automation & Benchmarks
|
|
71
|
+
|
|
72
|
+
- **CI** runs on every push/PR: Rust fmt/clippy/tests + Python ruff/pytest.
|
|
73
|
+
- **Wheels** build on pushes to `main` (artifacts), and tags `v*` publish to GitHub Releases + PyPI.
|
|
74
|
+
- **Benchmarks** run on pushes to `main`; history + charts are published via GitHub Pages.
|
|
75
|
+
- **Docs** are published to GitHub Pages on pushes to `main`.
|
|
76
|
+
|
|
77
|
+
Latest benchmark history and charts:
|
|
78
|
+
https://victorwp288.github.io/weiss-schwarz-simulator/benchmarks
|
|
79
|
+
|
|
80
|
+
Rust API docs:
|
|
81
|
+
https://victorwp288.github.io/weiss-schwarz-simulator/rustdoc/
|
|
82
|
+
|
|
83
|
+
Note: with only 1–2 benchmark runs, charts can look “empty” until more points exist.
|
|
84
|
+
|
|
85
|
+
### Releases
|
|
86
|
+
|
|
87
|
+
Release automation is handled by Release Please. To ensure downstream workflows (like `Wheels`) run
|
|
88
|
+
automatically when a release tag is created, configure a fine-grained PAT as `RELEASE_PLEASE_TOKEN`
|
|
89
|
+
in GitHub Actions secrets; otherwise you can manually run the `Wheels` workflow for the release tag.
|
|
90
|
+
|
|
91
|
+
### Benchmark Snapshot (main, top 12)
|
|
92
|
+
|
|
93
|
+
<!-- BENCHMARKS:START -->
|
|
94
|
+
_Last updated: 2026-01-05 00:44 UTC_
|
|
95
|
+
|
|
96
|
+
| Benchmark | Time |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| rust/advance_until_decision | 63280 ns/iter |
|
|
99
|
+
| rust/step_batch_64 | 26268 ns/iter |
|
|
100
|
+
| rust/step_batch_fast_256_priority_off | 111592 ns/iter |
|
|
101
|
+
| rust/step_batch_fast_256_priority_on | 109646 ns/iter |
|
|
102
|
+
| rust/legal_actions | 44 ns/iter |
|
|
103
|
+
| rust/legal_actions_forced | 43 ns/iter |
|
|
104
|
+
| rust/on_reverse_decision_frequency_on | 1534 ns/iter |
|
|
105
|
+
| rust/on_reverse_decision_frequency_off | 1540 ns/iter |
|
|
106
|
+
| rust/observation_encode | 228 ns/iter |
|
|
107
|
+
| rust/observation_encode_forced | 233 ns/iter |
|
|
108
|
+
| rust/mask_construction | 455 ns/iter |
|
|
109
|
+
| rust/mask_construction_forced | 412 ns/iter |
|
|
110
|
+
<!-- BENCHMARKS:END -->
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Repo layout
|
|
116
|
+
|
|
117
|
+
- `weiss_core/`: Rust simulator core (state machine, legality, encoding, replay, pool)
|
|
118
|
+
- `weiss_py/`: PyO3 extension module (`weiss_sim`) exposing `EnvPool`
|
|
119
|
+
- `python/weiss_sim/`: Python wrapper that re-exports the extension
|
|
120
|
+
- `python/tests/`: pytest smoke tests + fixture card DB
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Installation
|
|
125
|
+
|
|
126
|
+
### Python (local build via `maturin`)
|
|
127
|
+
|
|
128
|
+
Prerequisites:
|
|
129
|
+
- **Python**: ≥ 3.10
|
|
130
|
+
- **Rust toolchain**: stable (`cargo`, `rustc`)
|
|
131
|
+
- **Bindings**: built with PyO3 0.24 + numpy 0.24 (Rust side)
|
|
132
|
+
|
|
133
|
+
Install (editable):
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
python -m pip install -U pip
|
|
137
|
+
python -m pip install -U maturin
|
|
138
|
+
python -m pip install -e .
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Note (macOS/PyO3): if you build wheels locally, prefer an explicit interpreter to avoid linking errors
|
|
142
|
+
and unsupported system Pythons:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
maturin build --release --manifest-path weiss_py/Cargo.toml --interpreter .venv/bin/python -o dist
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Sanity check:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
python -c "import weiss_sim; print(weiss_sim.__version__, weiss_sim.EnvPool)"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Rust (core only)
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
cargo build -p weiss_core
|
|
158
|
+
cargo test -p weiss_core
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Quickstart (Python): step with a trivial policy
|
|
164
|
+
|
|
165
|
+
The environment exposes a **fixed action space** and an **action mask**. You can select any index where mask==1. For speed, use legal action ids instead of scanning the full mask.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from pathlib import Path
|
|
169
|
+
import numpy as np
|
|
170
|
+
import weiss_sim
|
|
171
|
+
|
|
172
|
+
fixture_dir = Path("python/tests/fixtures")
|
|
173
|
+
db_path = fixture_dir / "cards.wsdb"
|
|
174
|
+
|
|
175
|
+
pool = weiss_sim.EnvPool.new_rl_train(
|
|
176
|
+
1,
|
|
177
|
+
str(db_path),
|
|
178
|
+
deck_lists=[[1] * 50, [2] * 50],
|
|
179
|
+
deck_ids=[1, 2],
|
|
180
|
+
max_decisions=200,
|
|
181
|
+
max_ticks=10_000,
|
|
182
|
+
seed=123,
|
|
183
|
+
num_threads=None, # set to an int to pin a dedicated Rayon pool
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
buf = weiss_sim.EnvPoolBuffers(pool)
|
|
187
|
+
buf.reset()
|
|
188
|
+
|
|
189
|
+
for _ in range(10):
|
|
190
|
+
ids_flat, offsets = buf.legal_action_ids()
|
|
191
|
+
start, end = int(offsets[0]), int(offsets[1])
|
|
192
|
+
action = int(ids_flat[start])
|
|
193
|
+
actions = np.array([action], dtype=np.uint32)
|
|
194
|
+
buf.step(actions)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Debug print (very lightweight):
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
print(pool.render_ansi(env_index=0, perspective=0))
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Examples:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
python python/examples/sb3_maskable_ppo.py
|
|
207
|
+
python python/examples/cleanrl_maskable_ppo.py
|
|
208
|
+
python python/examples/bench_python_boundary.py --num-envs 256 --steps 5000 --mode both
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Python API (what you get)
|
|
214
|
+
|
|
215
|
+
The extension module is `weiss_sim` and the package re-exports it as `import weiss_sim`.
|
|
216
|
+
|
|
217
|
+
### `weiss_sim.EnvPool`
|
|
218
|
+
|
|
219
|
+
Constructors (classmethods):
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
EnvPool.new_rl_train(
|
|
223
|
+
num_envs: int,
|
|
224
|
+
db_path: str,
|
|
225
|
+
deck_lists: list[list[int]],
|
|
226
|
+
deck_ids: list[int] | None = None,
|
|
227
|
+
max_decisions: int = 10_000,
|
|
228
|
+
max_ticks: int = 100_000,
|
|
229
|
+
seed: int = 0,
|
|
230
|
+
reward_json: str | None = None,
|
|
231
|
+
num_threads: int | None = None,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
EnvPool.new_rl_eval(...)
|
|
235
|
+
EnvPool.new_debug(...)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Minimal RL stepping uses `BatchOutMinimal`:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
out = weiss_sim.BatchOutMinimal(num_envs)
|
|
242
|
+
pool.reset_into(out)
|
|
243
|
+
pool.step_into(actions, out)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Core methods:
|
|
247
|
+
- `reset_into(out: BatchOutMinimal) -> None`
|
|
248
|
+
- `reset_indices_into(indices: list[int], out: BatchOutMinimal) -> None`
|
|
249
|
+
- `reset_done_into(done_mask: np.ndarray[bool], out: BatchOutMinimal) -> None`
|
|
250
|
+
- `step_into(actions: np.ndarray[uint32], out: BatchOutMinimal) -> None`
|
|
251
|
+
- `step_debug_into(actions: np.ndarray[uint32], out: BatchOutDebug) -> None`
|
|
252
|
+
- `reset_debug_into(out: BatchOutDebug) -> None`
|
|
253
|
+
- `reset_indices_debug_into(indices: list[int], out: BatchOutDebug) -> None`
|
|
254
|
+
- `legal_action_ids_into(ids: np.ndarray[uint16], offsets: np.ndarray[uint32]) -> int`
|
|
255
|
+
- `auto_reset_on_error_codes_into(engine_status: np.ndarray[uint8], out: BatchOutMinimal) -> int`
|
|
256
|
+
- `engine_error_reset_count() -> int`
|
|
257
|
+
- `reset_engine_error_reset_count() -> None`
|
|
258
|
+
|
|
259
|
+
Debug helpers:
|
|
260
|
+
- `action_lookup_batch() -> list[list[dict | None]]`
|
|
261
|
+
- `describe_action_ids(action_ids: list[int]) -> list[dict | None]`
|
|
262
|
+
- `decision_info_batch() -> list[dict]`
|
|
263
|
+
- `state_fingerprint_batch() -> np.ndarray[uint64]`
|
|
264
|
+
- `events_fingerprint_batch() -> np.ndarray[uint64]`
|
|
265
|
+
- `render_ansi(env_index: int, perspective: int) -> str`
|
|
266
|
+
|
|
267
|
+
Convenience properties:
|
|
268
|
+
- `action_space: int`
|
|
269
|
+
- `obs_len: int`
|
|
270
|
+
- `num_envs: int`
|
|
271
|
+
|
|
272
|
+
Python helper:
|
|
273
|
+
- `EnvPoolBuffers(pool)` allocates persistent numpy buffers and exposes `reset()`, `step()`, and `legal_action_ids()`.
|
|
274
|
+
- `reset_rl(pool)` / `step_rl(pool, actions)` return a `RlStep` dataclass with named fields.
|
|
275
|
+
- `pass_action_id_for_decision_kind(decision_kind)` returns `PASS_ACTION_ID` for convenience.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Encodings (stable + versioned)
|
|
280
|
+
|
|
281
|
+
Encodings are deterministic and **explicitly versioned**:
|
|
282
|
+
|
|
283
|
+
- `weiss_sim.OBS_ENCODING_VERSION` (currently 1)
|
|
284
|
+
- `weiss_sim.ACTION_ENCODING_VERSION` (currently 1)
|
|
285
|
+
|
|
286
|
+
### Observation tensor
|
|
287
|
+
|
|
288
|
+
Observations are fixed-length `int32` arrays. Query the current length via:
|
|
289
|
+
|
|
290
|
+
- `weiss_sim.OBS_LEN` or `pool.obs_len`
|
|
291
|
+
|
|
292
|
+
Visibility modes (`observation_visibility`):
|
|
293
|
+
- `"public"`: opponent hidden zones are masked (filled with `-1`).
|
|
294
|
+
- `"full"`: opponent hidden zones are revealed.
|
|
295
|
+
|
|
296
|
+
The header includes active player, phase, decision kind/player, last action, attack context, and choice pagination metadata. After the per-player blocks (perspective player first), the encoder appends:
|
|
297
|
+
|
|
298
|
+
- **Reason bits**: public-safe flags for phase/resource/target gating.
|
|
299
|
+
- **Reveal history**: recent revealed card ids for the observing player.
|
|
300
|
+
- **Context bits**: priority/choice/stack/encore context.
|
|
301
|
+
|
|
302
|
+
Exact layout is defined in `weiss_core/src/encode.rs` and versioned by `OBS_ENCODING_VERSION`.
|
|
303
|
+
|
|
304
|
+
### Action space
|
|
305
|
+
|
|
306
|
+
Actions are fixed to `ACTION_SPACE_SIZE` (`pool.action_space`). Families include:
|
|
307
|
+
|
|
308
|
+
- pass (contextual; `PASS_ACTION_ID`)
|
|
309
|
+
- mulligan confirm / mulligan select
|
|
310
|
+
- clock
|
|
311
|
+
- main play character/event/climax, moves, activated abilities
|
|
312
|
+
- attack / counter
|
|
313
|
+
- choice select + pagination
|
|
314
|
+
- level up / encore
|
|
315
|
+
- trigger order
|
|
316
|
+
- concede (only legal when `allow_concede=true`)
|
|
317
|
+
|
|
318
|
+
The legal-action **mask** is derived from the canonical `ActionDesc` list and mapped to ids in a versioned way (`ACTION_ENCODING_VERSION`).
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
## Performance & throughput
|
|
324
|
+
|
|
325
|
+
Rust core is already extremely fast; Python-side overhead can dominate.
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
Practical performance tips:
|
|
329
|
+
- Use into-buffer APIs (`reset_into`, `step_into`) with preallocated buffers.
|
|
330
|
+
- Prefer **legal action ids** over mask scans in Python.
|
|
331
|
+
- Pin `num_threads` if you want repeatable multicore behavior.
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## RL-safe defaults (recommended)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
- `EnvPool.new_rl_train(...)` for training and `EnvPool.new_rl_eval(...)` for eval.
|
|
339
|
+
- `observation_visibility = Public`
|
|
340
|
+
- `enable_visibility_policies = true`
|
|
341
|
+
- `allow_concede = false`
|
|
342
|
+
- `priority_allow_pass = true`
|
|
343
|
+
- `strict_priority_mode = false`
|
|
344
|
+
- `enable_priority_windows = false` unless you explicitly train with priority timing windows
|
|
345
|
+
- Treat timeouts as **truncations** (bootstrap value)
|
|
346
|
+
|
|
347
|
+
If you need to deviate, document the assumption and update the PPO guide.
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Replays (WSR1)
|
|
352
|
+
|
|
353
|
+
Replays are binary `WSR1` files written via `ReplayWriter` and serialized with `postcard`.
|
|
354
|
+
|
|
355
|
+
What gets recorded:
|
|
356
|
+
- **Header**: obs/action encoding versions, replay schema version, seed, starting player, deck ids, curriculum id, config hash
|
|
357
|
+
- **Body**: action sequence, per-step metadata (actor/decision kind/flags), optional event stream, final snapshot (terminal + state hash)
|
|
358
|
+
|
|
359
|
+
Replay sampling can be enabled from Rust (`EnvPool::enable_replay_sampling`). The Python
|
|
360
|
+
binding does not currently expose replay sampling toggles.
|
|
361
|
+
|
|
362
|
+
Tooling:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
cargo run -p weiss_core --bin replay_dump -- path/to/episode_00000000.wsr
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Card database (WSDB)
|
|
371
|
+
|
|
372
|
+
The simulator loads a binary card DB:
|
|
373
|
+
|
|
374
|
+
- Magic: `WSDB`
|
|
375
|
+
- Schema version: `u32` little-endian (`WSDB_SCHEMA_VERSION = 1`)
|
|
376
|
+
- Payload: `postcard`-encoded `CardDb`
|
|
377
|
+
|
|
378
|
+
Pack JSON → WSDB:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
cargo run -p weiss_core --bin carddb_pack -- cards.json cards.wsdb
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
See `weiss_core/src/db.rs` for the `CardStatic` schema and supported ability templates.
|
|
385
|
+
|
|
386
|
+
### Scraper + converter pipeline (full card set)
|
|
387
|
+
|
|
388
|
+
The full EN card set is produced by the scraper + converter pipeline:
|
|
389
|
+
|
|
390
|
+
- Scrape: `scraper/scrape.py` → `scraper/out/cards.jsonl`
|
|
391
|
+
- Convert: `scraper/convert.py` → `scraper/out/cards.json` + `scraper/out/cards_raw.json`
|
|
392
|
+
- Pack: `carddb_pack` → `scraper/out/cards.wsdb`
|
|
393
|
+
|
|
394
|
+
Smoke check with Python:
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
PYTHONPATH=python python python/wsdb_smoke.py
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Project status (implemented vs simplified)
|
|
403
|
+
|
|
404
|
+
This is a simulator core built for RL training and determinism first. Some rule systems are intentionally simplified or diverge from the physical game.
|
|
405
|
+
|
|
406
|
+
Examples of current intentional simplifications/deviations:
|
|
407
|
+
- local priority/stack model; official check-timing/play-timing subtleties are not fully replicated
|
|
408
|
+
- card text is limited to implemented `AbilityTemplate` variants (no free-form text engine)
|
|
409
|
+
- trigger icon semantics are simplified :
|
|
410
|
+
- Draw is mandatory (not optional “may”)
|
|
411
|
+
- Shot resolves as immediate 1 damage (not delayed on cancel)
|
|
412
|
+
- Bounce returns a character from **your** stage (not opponent’s)
|
|
413
|
+
- deck-top search/reveal modeled as top‑N reveal to controller
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
## License
|
|
419
|
+
|
|
420
|
+
Dual-licensed under **MIT OR Apache-2.0** (see workspace metadata in `Cargo.toml`).
|
|
421
|
+
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# Weiss Schwarz Simulator (Rust core + Python bindings)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/actions/workflows/ci.yml)
|
|
4
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/actions/workflows/wheels.yml)
|
|
5
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/actions/workflows/benchmarks.yml)
|
|
6
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/actions/workflows/security.yml)
|
|
7
|
+
[](https://victorwp288.github.io/weiss-schwarz-simulator/rustdoc/)
|
|
8
|
+
[](https://pypi.org/project/weiss-sim/)
|
|
9
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/blob/main/CHANGELOG.md)
|
|
10
|
+
[](https://github.com/victorwp288/weiss-schwarz-simulator/commits/main)
|
|
11
|
+
|
|
12
|
+
Deterministic, RL-first Weiss Schwarz simulation: **Rust runs the hot loop**, advances until a **decision point**, and exposes a **fixed action space + mask** (and legal action ids) for efficient batched training. Python gets a thin `EnvPool` wrapper for stepping many environments in parallel.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why this exists
|
|
17
|
+
|
|
18
|
+
Weiss Schwarz has hidden information, branching, and timing windows. For RL you typically want:
|
|
19
|
+
|
|
20
|
+
- **Determinism**: reproduce episodes from a seed and action sequence.
|
|
21
|
+
- **Few boundary crossings**: avoid Python↔Rust overhead for micro-steps.
|
|
22
|
+
- **Stable action space**: fixed-size actions with legality masking.
|
|
23
|
+
- **Introspectability**: canonical action descriptions, replays, and event logs.
|
|
24
|
+
|
|
25
|
+
This repo is built around those constraints.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Highlights
|
|
30
|
+
|
|
31
|
+
- **Advance-until-decision loop**: engine runs internally until a player must act.
|
|
32
|
+
- **Canonical legal actions**: `ActionDesc` list is the single truth source.
|
|
33
|
+
- **Fixed action id space + mask**: derived from canonical actions and **versioned** (`ACTION_ENCODING_VERSION`).
|
|
34
|
+
- **Legal action ids (fast)**: use ids to avoid Python-side mask scans in hot loops.
|
|
35
|
+
- **Fixed-length observations**: `int32` arrays, **versioned** (`OBS_ENCODING_VERSION`).
|
|
36
|
+
- **Multicore stepping**: `EnvPool` uses Rayon; Python binding releases the GIL.
|
|
37
|
+
- **Replays**: deterministic, versioned, optional event stream, public-safe sanitization when enabled.
|
|
38
|
+
- **Curriculum switches**: gate rules/features for training curricula.
|
|
39
|
+
|
|
40
|
+
Each environment is deterministic given its seed and action sequence. Parallel batch stepping does not change outcomes because envs are fully isolated.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Automation & Benchmarks
|
|
45
|
+
|
|
46
|
+
- **CI** runs on every push/PR: Rust fmt/clippy/tests + Python ruff/pytest.
|
|
47
|
+
- **Wheels** build on pushes to `main` (artifacts), and tags `v*` publish to GitHub Releases + PyPI.
|
|
48
|
+
- **Benchmarks** run on pushes to `main`; history + charts are published via GitHub Pages.
|
|
49
|
+
- **Docs** are published to GitHub Pages on pushes to `main`.
|
|
50
|
+
|
|
51
|
+
Latest benchmark history and charts:
|
|
52
|
+
https://victorwp288.github.io/weiss-schwarz-simulator/benchmarks
|
|
53
|
+
|
|
54
|
+
Rust API docs:
|
|
55
|
+
https://victorwp288.github.io/weiss-schwarz-simulator/rustdoc/
|
|
56
|
+
|
|
57
|
+
Note: with only 1–2 benchmark runs, charts can look “empty” until more points exist.
|
|
58
|
+
|
|
59
|
+
### Releases
|
|
60
|
+
|
|
61
|
+
Release automation is handled by Release Please. To ensure downstream workflows (like `Wheels`) run
|
|
62
|
+
automatically when a release tag is created, configure a fine-grained PAT as `RELEASE_PLEASE_TOKEN`
|
|
63
|
+
in GitHub Actions secrets; otherwise you can manually run the `Wheels` workflow for the release tag.
|
|
64
|
+
|
|
65
|
+
### Benchmark Snapshot (main, top 12)
|
|
66
|
+
|
|
67
|
+
<!-- BENCHMARKS:START -->
|
|
68
|
+
_Last updated: 2026-01-05 00:44 UTC_
|
|
69
|
+
|
|
70
|
+
| Benchmark | Time |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| rust/advance_until_decision | 63280 ns/iter |
|
|
73
|
+
| rust/step_batch_64 | 26268 ns/iter |
|
|
74
|
+
| rust/step_batch_fast_256_priority_off | 111592 ns/iter |
|
|
75
|
+
| rust/step_batch_fast_256_priority_on | 109646 ns/iter |
|
|
76
|
+
| rust/legal_actions | 44 ns/iter |
|
|
77
|
+
| rust/legal_actions_forced | 43 ns/iter |
|
|
78
|
+
| rust/on_reverse_decision_frequency_on | 1534 ns/iter |
|
|
79
|
+
| rust/on_reverse_decision_frequency_off | 1540 ns/iter |
|
|
80
|
+
| rust/observation_encode | 228 ns/iter |
|
|
81
|
+
| rust/observation_encode_forced | 233 ns/iter |
|
|
82
|
+
| rust/mask_construction | 455 ns/iter |
|
|
83
|
+
| rust/mask_construction_forced | 412 ns/iter |
|
|
84
|
+
<!-- BENCHMARKS:END -->
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Repo layout
|
|
90
|
+
|
|
91
|
+
- `weiss_core/`: Rust simulator core (state machine, legality, encoding, replay, pool)
|
|
92
|
+
- `weiss_py/`: PyO3 extension module (`weiss_sim`) exposing `EnvPool`
|
|
93
|
+
- `python/weiss_sim/`: Python wrapper that re-exports the extension
|
|
94
|
+
- `python/tests/`: pytest smoke tests + fixture card DB
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Installation
|
|
99
|
+
|
|
100
|
+
### Python (local build via `maturin`)
|
|
101
|
+
|
|
102
|
+
Prerequisites:
|
|
103
|
+
- **Python**: ≥ 3.10
|
|
104
|
+
- **Rust toolchain**: stable (`cargo`, `rustc`)
|
|
105
|
+
- **Bindings**: built with PyO3 0.24 + numpy 0.24 (Rust side)
|
|
106
|
+
|
|
107
|
+
Install (editable):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
python -m pip install -U pip
|
|
111
|
+
python -m pip install -U maturin
|
|
112
|
+
python -m pip install -e .
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Note (macOS/PyO3): if you build wheels locally, prefer an explicit interpreter to avoid linking errors
|
|
116
|
+
and unsupported system Pythons:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
maturin build --release --manifest-path weiss_py/Cargo.toml --interpreter .venv/bin/python -o dist
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Sanity check:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python -c "import weiss_sim; print(weiss_sim.__version__, weiss_sim.EnvPool)"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Rust (core only)
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
cargo build -p weiss_core
|
|
132
|
+
cargo test -p weiss_core
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Quickstart (Python): step with a trivial policy
|
|
138
|
+
|
|
139
|
+
The environment exposes a **fixed action space** and an **action mask**. You can select any index where mask==1. For speed, use legal action ids instead of scanning the full mask.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from pathlib import Path
|
|
143
|
+
import numpy as np
|
|
144
|
+
import weiss_sim
|
|
145
|
+
|
|
146
|
+
fixture_dir = Path("python/tests/fixtures")
|
|
147
|
+
db_path = fixture_dir / "cards.wsdb"
|
|
148
|
+
|
|
149
|
+
pool = weiss_sim.EnvPool.new_rl_train(
|
|
150
|
+
1,
|
|
151
|
+
str(db_path),
|
|
152
|
+
deck_lists=[[1] * 50, [2] * 50],
|
|
153
|
+
deck_ids=[1, 2],
|
|
154
|
+
max_decisions=200,
|
|
155
|
+
max_ticks=10_000,
|
|
156
|
+
seed=123,
|
|
157
|
+
num_threads=None, # set to an int to pin a dedicated Rayon pool
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
buf = weiss_sim.EnvPoolBuffers(pool)
|
|
161
|
+
buf.reset()
|
|
162
|
+
|
|
163
|
+
for _ in range(10):
|
|
164
|
+
ids_flat, offsets = buf.legal_action_ids()
|
|
165
|
+
start, end = int(offsets[0]), int(offsets[1])
|
|
166
|
+
action = int(ids_flat[start])
|
|
167
|
+
actions = np.array([action], dtype=np.uint32)
|
|
168
|
+
buf.step(actions)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Debug print (very lightweight):
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
print(pool.render_ansi(env_index=0, perspective=0))
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Examples:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
python python/examples/sb3_maskable_ppo.py
|
|
181
|
+
python python/examples/cleanrl_maskable_ppo.py
|
|
182
|
+
python python/examples/bench_python_boundary.py --num-envs 256 --steps 5000 --mode both
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Python API (what you get)
|
|
188
|
+
|
|
189
|
+
The extension module is `weiss_sim` and the package re-exports it as `import weiss_sim`.
|
|
190
|
+
|
|
191
|
+
### `weiss_sim.EnvPool`
|
|
192
|
+
|
|
193
|
+
Constructors (classmethods):
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
EnvPool.new_rl_train(
|
|
197
|
+
num_envs: int,
|
|
198
|
+
db_path: str,
|
|
199
|
+
deck_lists: list[list[int]],
|
|
200
|
+
deck_ids: list[int] | None = None,
|
|
201
|
+
max_decisions: int = 10_000,
|
|
202
|
+
max_ticks: int = 100_000,
|
|
203
|
+
seed: int = 0,
|
|
204
|
+
reward_json: str | None = None,
|
|
205
|
+
num_threads: int | None = None,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
EnvPool.new_rl_eval(...)
|
|
209
|
+
EnvPool.new_debug(...)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Minimal RL stepping uses `BatchOutMinimal`:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
out = weiss_sim.BatchOutMinimal(num_envs)
|
|
216
|
+
pool.reset_into(out)
|
|
217
|
+
pool.step_into(actions, out)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Core methods:
|
|
221
|
+
- `reset_into(out: BatchOutMinimal) -> None`
|
|
222
|
+
- `reset_indices_into(indices: list[int], out: BatchOutMinimal) -> None`
|
|
223
|
+
- `reset_done_into(done_mask: np.ndarray[bool], out: BatchOutMinimal) -> None`
|
|
224
|
+
- `step_into(actions: np.ndarray[uint32], out: BatchOutMinimal) -> None`
|
|
225
|
+
- `step_debug_into(actions: np.ndarray[uint32], out: BatchOutDebug) -> None`
|
|
226
|
+
- `reset_debug_into(out: BatchOutDebug) -> None`
|
|
227
|
+
- `reset_indices_debug_into(indices: list[int], out: BatchOutDebug) -> None`
|
|
228
|
+
- `legal_action_ids_into(ids: np.ndarray[uint16], offsets: np.ndarray[uint32]) -> int`
|
|
229
|
+
- `auto_reset_on_error_codes_into(engine_status: np.ndarray[uint8], out: BatchOutMinimal) -> int`
|
|
230
|
+
- `engine_error_reset_count() -> int`
|
|
231
|
+
- `reset_engine_error_reset_count() -> None`
|
|
232
|
+
|
|
233
|
+
Debug helpers:
|
|
234
|
+
- `action_lookup_batch() -> list[list[dict | None]]`
|
|
235
|
+
- `describe_action_ids(action_ids: list[int]) -> list[dict | None]`
|
|
236
|
+
- `decision_info_batch() -> list[dict]`
|
|
237
|
+
- `state_fingerprint_batch() -> np.ndarray[uint64]`
|
|
238
|
+
- `events_fingerprint_batch() -> np.ndarray[uint64]`
|
|
239
|
+
- `render_ansi(env_index: int, perspective: int) -> str`
|
|
240
|
+
|
|
241
|
+
Convenience properties:
|
|
242
|
+
- `action_space: int`
|
|
243
|
+
- `obs_len: int`
|
|
244
|
+
- `num_envs: int`
|
|
245
|
+
|
|
246
|
+
Python helper:
|
|
247
|
+
- `EnvPoolBuffers(pool)` allocates persistent numpy buffers and exposes `reset()`, `step()`, and `legal_action_ids()`.
|
|
248
|
+
- `reset_rl(pool)` / `step_rl(pool, actions)` return a `RlStep` dataclass with named fields.
|
|
249
|
+
- `pass_action_id_for_decision_kind(decision_kind)` returns `PASS_ACTION_ID` for convenience.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Encodings (stable + versioned)
|
|
254
|
+
|
|
255
|
+
Encodings are deterministic and **explicitly versioned**:
|
|
256
|
+
|
|
257
|
+
- `weiss_sim.OBS_ENCODING_VERSION` (currently 1)
|
|
258
|
+
- `weiss_sim.ACTION_ENCODING_VERSION` (currently 1)
|
|
259
|
+
|
|
260
|
+
### Observation tensor
|
|
261
|
+
|
|
262
|
+
Observations are fixed-length `int32` arrays. Query the current length via:
|
|
263
|
+
|
|
264
|
+
- `weiss_sim.OBS_LEN` or `pool.obs_len`
|
|
265
|
+
|
|
266
|
+
Visibility modes (`observation_visibility`):
|
|
267
|
+
- `"public"`: opponent hidden zones are masked (filled with `-1`).
|
|
268
|
+
- `"full"`: opponent hidden zones are revealed.
|
|
269
|
+
|
|
270
|
+
The header includes active player, phase, decision kind/player, last action, attack context, and choice pagination metadata. After the per-player blocks (perspective player first), the encoder appends:
|
|
271
|
+
|
|
272
|
+
- **Reason bits**: public-safe flags for phase/resource/target gating.
|
|
273
|
+
- **Reveal history**: recent revealed card ids for the observing player.
|
|
274
|
+
- **Context bits**: priority/choice/stack/encore context.
|
|
275
|
+
|
|
276
|
+
Exact layout is defined in `weiss_core/src/encode.rs` and versioned by `OBS_ENCODING_VERSION`.
|
|
277
|
+
|
|
278
|
+
### Action space
|
|
279
|
+
|
|
280
|
+
Actions are fixed to `ACTION_SPACE_SIZE` (`pool.action_space`). Families include:
|
|
281
|
+
|
|
282
|
+
- pass (contextual; `PASS_ACTION_ID`)
|
|
283
|
+
- mulligan confirm / mulligan select
|
|
284
|
+
- clock
|
|
285
|
+
- main play character/event/climax, moves, activated abilities
|
|
286
|
+
- attack / counter
|
|
287
|
+
- choice select + pagination
|
|
288
|
+
- level up / encore
|
|
289
|
+
- trigger order
|
|
290
|
+
- concede (only legal when `allow_concede=true`)
|
|
291
|
+
|
|
292
|
+
The legal-action **mask** is derived from the canonical `ActionDesc` list and mapped to ids in a versioned way (`ACTION_ENCODING_VERSION`).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
## Performance & throughput
|
|
298
|
+
|
|
299
|
+
Rust core is already extremely fast; Python-side overhead can dominate.
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
Practical performance tips:
|
|
303
|
+
- Use into-buffer APIs (`reset_into`, `step_into`) with preallocated buffers.
|
|
304
|
+
- Prefer **legal action ids** over mask scans in Python.
|
|
305
|
+
- Pin `num_threads` if you want repeatable multicore behavior.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## RL-safe defaults (recommended)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
- `EnvPool.new_rl_train(...)` for training and `EnvPool.new_rl_eval(...)` for eval.
|
|
313
|
+
- `observation_visibility = Public`
|
|
314
|
+
- `enable_visibility_policies = true`
|
|
315
|
+
- `allow_concede = false`
|
|
316
|
+
- `priority_allow_pass = true`
|
|
317
|
+
- `strict_priority_mode = false`
|
|
318
|
+
- `enable_priority_windows = false` unless you explicitly train with priority timing windows
|
|
319
|
+
- Treat timeouts as **truncations** (bootstrap value)
|
|
320
|
+
|
|
321
|
+
If you need to deviate, document the assumption and update the PPO guide.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Replays (WSR1)
|
|
326
|
+
|
|
327
|
+
Replays are binary `WSR1` files written via `ReplayWriter` and serialized with `postcard`.
|
|
328
|
+
|
|
329
|
+
What gets recorded:
|
|
330
|
+
- **Header**: obs/action encoding versions, replay schema version, seed, starting player, deck ids, curriculum id, config hash
|
|
331
|
+
- **Body**: action sequence, per-step metadata (actor/decision kind/flags), optional event stream, final snapshot (terminal + state hash)
|
|
332
|
+
|
|
333
|
+
Replay sampling can be enabled from Rust (`EnvPool::enable_replay_sampling`). The Python
|
|
334
|
+
binding does not currently expose replay sampling toggles.
|
|
335
|
+
|
|
336
|
+
Tooling:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
cargo run -p weiss_core --bin replay_dump -- path/to/episode_00000000.wsr
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Card database (WSDB)
|
|
345
|
+
|
|
346
|
+
The simulator loads a binary card DB:
|
|
347
|
+
|
|
348
|
+
- Magic: `WSDB`
|
|
349
|
+
- Schema version: `u32` little-endian (`WSDB_SCHEMA_VERSION = 1`)
|
|
350
|
+
- Payload: `postcard`-encoded `CardDb`
|
|
351
|
+
|
|
352
|
+
Pack JSON → WSDB:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
cargo run -p weiss_core --bin carddb_pack -- cards.json cards.wsdb
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
See `weiss_core/src/db.rs` for the `CardStatic` schema and supported ability templates.
|
|
359
|
+
|
|
360
|
+
### Scraper + converter pipeline (full card set)
|
|
361
|
+
|
|
362
|
+
The full EN card set is produced by the scraper + converter pipeline:
|
|
363
|
+
|
|
364
|
+
- Scrape: `scraper/scrape.py` → `scraper/out/cards.jsonl`
|
|
365
|
+
- Convert: `scraper/convert.py` → `scraper/out/cards.json` + `scraper/out/cards_raw.json`
|
|
366
|
+
- Pack: `carddb_pack` → `scraper/out/cards.wsdb`
|
|
367
|
+
|
|
368
|
+
Smoke check with Python:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
PYTHONPATH=python python python/wsdb_smoke.py
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Project status (implemented vs simplified)
|
|
377
|
+
|
|
378
|
+
This is a simulator core built for RL training and determinism first. Some rule systems are intentionally simplified or diverge from the physical game.
|
|
379
|
+
|
|
380
|
+
Examples of current intentional simplifications/deviations:
|
|
381
|
+
- local priority/stack model; official check-timing/play-timing subtleties are not fully replicated
|
|
382
|
+
- card text is limited to implemented `AbilityTemplate` variants (no free-form text engine)
|
|
383
|
+
- trigger icon semantics are simplified :
|
|
384
|
+
- Draw is mandatory (not optional “may”)
|
|
385
|
+
- Shot resolves as immediate 1 damage (not delayed on cancel)
|
|
386
|
+
- Bounce returns a character from **your** stage (not opponent’s)
|
|
387
|
+
- deck-top search/reveal modeled as top‑N reveal to controller
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
## License
|
|
393
|
+
|
|
394
|
+
Dual-licensed under **MIT OR Apache-2.0** (see workspace metadata in `Cargo.toml`).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["maturin>=1.5"]
|
|
3
|
+
build-backend = "maturin"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "weiss-sim"
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
description = "Deterministic Weiss Schwarz simulator with a Rust core and Python bindings."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT OR Apache-2.0" }
|
|
12
|
+
authors = [{ name = "Lallan" }]
|
|
13
|
+
keywords = ["weiss-schwarz", "reinforcement-learning", "simulation", "pyo3", "rl"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"License :: OSI Approved :: Apache Software License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: Rust",
|
|
23
|
+
"Topic :: Games/Entertainment",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"numpy>=1.23",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/victorwp288/weiss-schwarz-simulator"
|
|
32
|
+
Repository = "https://github.com/victorwp288/weiss-schwarz-simulator"
|
|
33
|
+
Documentation = "https://victorwp288.github.io/weiss-schwarz-simulator/rustdoc/"
|
|
34
|
+
Issues = "https://github.com/victorwp288/weiss-schwarz-simulator/issues"
|
|
35
|
+
Changelog = "https://github.com/victorwp288/weiss-schwarz-simulator/blob/main/CHANGELOG.md"
|
|
36
|
+
|
|
37
|
+
[tool.maturin]
|
|
38
|
+
manifest-path = "weiss_py/Cargo.toml"
|
|
39
|
+
python-source = "python"
|
|
40
|
+
module-name = "weiss_sim"
|
|
41
|
+
|
|
42
|
+
[tool.ruff]
|
|
43
|
+
target-version = "py310"
|
|
44
|
+
line-length = 100
|
|
45
|
+
extend-exclude = ["python/examples"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F"]
|
|
49
|
+
ignore = ["E501"]
|
weiss_sim-0.1.2/PKG-INFO
DELETED
weiss_sim-0.1.2/pyproject.toml
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["maturin>=1.5"]
|
|
3
|
-
build-backend = "maturin"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "weiss-sim"
|
|
7
|
-
version = "0.1.2"
|
|
8
|
-
requires-python = ">=3.10"
|
|
9
|
-
dependencies = [
|
|
10
|
-
"numpy>=1.23",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
[tool.maturin]
|
|
14
|
-
manifest-path = "weiss_py/Cargo.toml"
|
|
15
|
-
python-source = "python"
|
|
16
|
-
module-name = "weiss_sim"
|
|
17
|
-
|
|
18
|
-
[tool.ruff]
|
|
19
|
-
target-version = "py310"
|
|
20
|
-
line-length = 100
|
|
21
|
-
extend-exclude = ["python/examples"]
|
|
22
|
-
|
|
23
|
-
[tool.ruff.lint]
|
|
24
|
-
select = ["E", "F"]
|
|
25
|
-
ignore = ["E501"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|