mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-03-25 06:26:31 +01:00
feat: add RandomService, seed diagnostics endpoint, and random mode docs (#59)
Some checks are pending
CI / build (push) Waiting to run
Some checks are pending
CI / build (push) Waiting to run
This commit is contained in:
parent
7e5a29dd74
commit
4aa41adb20
10 changed files with 697 additions and 4 deletions
121
docs/random_mode/seed_infrastructure.md
Normal file
121
docs/random_mode/seed_infrastructure.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# Seed Infrastructure
|
||||
|
||||
**Module**: `code/random_util.py`
|
||||
**Updated**: 2026-03-20
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Random Mode builds use a deterministic, seeded RNG so that any build can be exactly reproduced from its seed value. Every random operation flows through isolated `random.Random` instances — the module-level PRNG is never mutated.
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
### `code/random_util.py`
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `derive_seed_from_string` | `(seed: int \| str) -> int` | Stable 63-bit seed from int or string (SHA-256 for strings) |
|
||||
| `set_seed` | `(seed: int \| str) -> random.Random` | Create a seeded `Random` instance |
|
||||
| `get_random` | `(seed: int \| str \| None) -> random.Random` | Convenience wrapper; unseeded when `None` |
|
||||
| `generate_seed` | `() -> int` | High-entropy 63-bit seed via `secrets.randbits` |
|
||||
|
||||
### `code/web/services/random_service.py`
|
||||
|
||||
Thin service wrapper following the R9 `BaseService` pattern. Adds input validation and a standardised interface for route handlers.
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `derive_seed(seed)` | Validated seed derivation (raises `InvalidSeedError` on bad input) |
|
||||
| `create_rng(seed)` | Return seeded or unseeded `random.Random` |
|
||||
| `generate_seed()` | Delegates to `random_util.generate_seed()` |
|
||||
| `validate_seed(seed)` | Validates type and range; raises `InvalidSeedError` |
|
||||
|
||||
---
|
||||
|
||||
## Seed Types
|
||||
|
||||
### Integer seeds
|
||||
- Must be non-negative
|
||||
- Normalised to 63-bit via `abs(n) & ((1 << 63) - 1)`
|
||||
- Zero is a valid, deterministic seed
|
||||
|
||||
### String seeds
|
||||
- Encoded as UTF-8 bytes
|
||||
- Hashed with SHA-256; first 8 bytes taken as big-endian unsigned int
|
||||
- Masked to 63 bits for consistency with int path
|
||||
- Empty string is valid (produces a fixed deterministic seed)
|
||||
|
||||
### None (auto-seed)
|
||||
- `get_random(None)` returns an unseeded `random.Random()`
|
||||
- `generate_seed()` returns a fresh `secrets.randbits(63)` value each call
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### DeckBuilder
|
||||
```python
|
||||
# code/deck_builder/builder.py
|
||||
builder = DeckBuilder(...)
|
||||
builder.set_seed(12345) # Sets builder.seed and recreates builder.rng
|
||||
rng = builder.rng # Seeded random.Random instance
|
||||
```
|
||||
|
||||
### Random entrypoint
|
||||
```python
|
||||
# code/deck_builder/random_entrypoint.py
|
||||
result = build_random_full_deck(
|
||||
seed=12345,
|
||||
theme="dragons",
|
||||
# ...
|
||||
)
|
||||
# result.seed == 12345 (or auto-generated if None was passed)
|
||||
```
|
||||
|
||||
### CLI
|
||||
```bash
|
||||
python code/headless_runner.py --random-seed 12345
|
||||
# or via environment variable
|
||||
RANDOM_SEED=12345 python code/headless_runner.py
|
||||
# or in deck.json config
|
||||
{ "random_seed": 12345 }
|
||||
```
|
||||
Seed resolution order: `--random-seed` CLI arg > `RANDOM_SEED` env var > `random_seed` in JSON config.
|
||||
|
||||
### Web UI
|
||||
- Seed input field on the Random Build page
|
||||
- Seed persisted in session across rerolls
|
||||
- Reroll increments the seed by 1 for deterministic variation (`seed + 1`)
|
||||
- Favorite seeds stored in session for quick reuse
|
||||
|
||||
---
|
||||
|
||||
## Edge Cases
|
||||
|
||||
| Scenario | Behaviour |
|
||||
|----------|-----------|
|
||||
| Negative int seed | Normalised via `abs()` before masking |
|
||||
| `MAX_INT` seed | Masked to 63 bits — stays valid |
|
||||
| Empty string `""` | Valid; SHA-256 hash of empty bytes used |
|
||||
| String with special chars / Unicode | UTF-8 encoded; `errors="strict"`, fallback to `errors="ignore"` |
|
||||
| `None` seed | Unseeded `random.Random()` — non-deterministic |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
| File | Coverage |
|
||||
|------|----------|
|
||||
| `code/tests/test_random_util.py` | Core function contracts |
|
||||
| `code/tests/test_random_determinism_comprehensive.py` | End-to-end determinism validation |
|
||||
| `code/tests/test_random_features_comprehensive.py` | Random mode feature integration |
|
||||
| `code/tests/test_random_api_comprehensive.py` | API-level behaviour |
|
||||
| `code/tests/test_random_service.py` | `RandomService` unit tests |
|
||||
|
||||
Run the fast subset:
|
||||
```powershell
|
||||
.venv/Scripts/python.exe -m pytest -q code/tests/test_random_util.py code/tests/test_random_service.py
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue