poke-crucible 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/battle/pairing.d.ts +25 -0
- package/dist/battle/pairing.js +59 -0
- package/dist/battle/pairing.js.map +1 -0
- package/dist/battle/pool.d.ts +25 -0
- package/dist/battle/pool.js +97 -0
- package/dist/battle/pool.js.map +1 -0
- package/dist/battle/run-battle.d.ts +13 -0
- package/dist/battle/run-battle.js +49 -0
- package/dist/battle/run-battle.js.map +1 -0
- package/dist/battle/worker.d.ts +1 -0
- package/dist/battle/worker.js +21 -0
- package/dist/battle/worker.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +326 -0
- package/dist/cli.js.map +1 -0
- package/dist/critic/agent-sdk-model.d.ts +27 -0
- package/dist/critic/agent-sdk-model.js +21 -0
- package/dist/critic/agent-sdk-model.js.map +1 -0
- package/dist/critic/critique.d.ts +21 -0
- package/dist/critic/critique.js +51 -0
- package/dist/critic/critique.js.map +1 -0
- package/dist/critic/evidence.d.ts +24 -0
- package/dist/critic/evidence.js +17 -0
- package/dist/critic/evidence.js.map +1 -0
- package/dist/critic/llm.d.ts +34 -0
- package/dist/critic/llm.js +23 -0
- package/dist/critic/llm.js.map +1 -0
- package/dist/critic/parse.d.ts +5 -0
- package/dist/critic/parse.js +84 -0
- package/dist/critic/parse.js.map +1 -0
- package/dist/critic/prompt.d.ts +14 -0
- package/dist/critic/prompt.js +56 -0
- package/dist/critic/prompt.js.map +1 -0
- package/dist/critic/types.d.ts +52 -0
- package/dist/critic/types.js +2 -0
- package/dist/critic/types.js.map +1 -0
- package/dist/designer/agent-sdk-model.d.ts +30 -0
- package/dist/designer/agent-sdk-model.js +62 -0
- package/dist/designer/agent-sdk-model.js.map +1 -0
- package/dist/designer/design.d.ts +12 -0
- package/dist/designer/design.js +218 -0
- package/dist/designer/design.js.map +1 -0
- package/dist/designer/fitness.d.ts +25 -0
- package/dist/designer/fitness.js +24 -0
- package/dist/designer/fitness.js.map +1 -0
- package/dist/designer/model.d.ts +14 -0
- package/dist/designer/model.js +27 -0
- package/dist/designer/model.js.map +1 -0
- package/dist/designer/mutate.d.ts +31 -0
- package/dist/designer/mutate.js +209 -0
- package/dist/designer/mutate.js.map +1 -0
- package/dist/designer/rng.d.ts +17 -0
- package/dist/designer/rng.js +40 -0
- package/dist/designer/rng.js.map +1 -0
- package/dist/designer/types.d.ts +103 -0
- package/dist/designer/types.js +2 -0
- package/dist/designer/types.js.map +1 -0
- package/dist/evaluate.d.ts +11 -0
- package/dist/evaluate.js +99 -0
- package/dist/evaluate.js.map +1 -0
- package/dist/gauntlet/gauntlet.d.ts +18 -0
- package/dist/gauntlet/gauntlet.js +41 -0
- package/dist/gauntlet/gauntlet.js.map +1 -0
- package/dist/gauntlet/teams-g1.d.ts +23 -0
- package/dist/gauntlet/teams-g1.js +476 -0
- package/dist/gauntlet/teams-g1.js.map +1 -0
- package/dist/gauntlet/teams-g2.d.ts +14 -0
- package/dist/gauntlet/teams-g2.js +725 -0
- package/dist/gauntlet/teams-g2.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/agent-query.d.ts +21 -0
- package/dist/llm/agent-query.js +70 -0
- package/dist/llm/agent-query.js.map +1 -0
- package/dist/pilot/damage.d.ts +34 -0
- package/dist/pilot/damage.js +75 -0
- package/dist/pilot/damage.js.map +1 -0
- package/dist/pilot/eval-pilot.d.ts +39 -0
- package/dist/pilot/eval-pilot.js +161 -0
- package/dist/pilot/eval-pilot.js.map +1 -0
- package/dist/pilot/pilot.d.ts +71 -0
- package/dist/pilot/pilot.js +290 -0
- package/dist/pilot/pilot.js.map +1 -0
- package/dist/ranking.d.ts +25 -0
- package/dist/ranking.js +40 -0
- package/dist/ranking.js.map +1 -0
- package/dist/seed.d.ts +13 -0
- package/dist/seed.js +42 -0
- package/dist/seed.js.map +1 -0
- package/dist/team.d.ts +29 -0
- package/dist/team.js +100 -0
- package/dist/team.js.map +1 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 johnjohto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# poke-crucible
|
|
2
|
+
|
|
3
|
+
Run masses of headless **Gen 3 OU** battles to rank Teams by how often they win.
|
|
4
|
+
Both sides of every Battle are driven by one fixed, competent heuristic **Pilot**,
|
|
5
|
+
so a result measures the **Team**, not the player. Three capabilities, each a
|
|
6
|
+
single seam built on the last:
|
|
7
|
+
|
|
8
|
+
- **`evaluate`** (Slice 1) — rank Teams by **Strength** against a fixed Gauntlet, or round-robin.
|
|
9
|
+
- **`critique`** (Slice 2) — an LLM **Critic** explains one Team's weaknesses and matchup holes.
|
|
10
|
+
- **`design`** (Slice 3) — the **Designer** evolves stronger Teams, with the LLM proposing mutations and naming the Archetypes it discovers.
|
|
11
|
+
|
|
12
|
+
Vocabulary lives in [`CONTEXT.md`](CONTEXT.md); the specs are the
|
|
13
|
+
[PRDs under `docs/`](docs); decisions are the [ADRs](docs/adr).
|
|
14
|
+
|
|
15
|
+
## Install & build
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install
|
|
19
|
+
npm run build # compiles TypeScript to dist/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires Node ≥ 20. The stack is in-process TypeScript on the `@pkmn/*` toolkit
|
|
23
|
+
(`@pkmn/sim`, `@pkmn/sets`, `@pkmn/data`) — no server, no network, no Python
|
|
24
|
+
(ADR 0001).
|
|
25
|
+
|
|
26
|
+
## CLI
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Absolute Strength vs the fixed Gauntlet
|
|
30
|
+
node dist/cli.js eval --mode gauntlet team.txt
|
|
31
|
+
|
|
32
|
+
# Relative Strength: rank several of your own Teams round-robin
|
|
33
|
+
node dist/cli.js eval --mode round-robin teamA.txt teamB.txt teamC.txt
|
|
34
|
+
|
|
35
|
+
# Pipe a single Team paste in on stdin
|
|
36
|
+
cat team.txt | node dist/cli.js eval
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Options: `--mode <gauntlet|round-robin>` (default `gauntlet`), `--battles <N>`
|
|
40
|
+
(default 100), `--seed <string>`, `--gauntlet-version <v>`, `--out <file>`
|
|
41
|
+
(default `poke-crucible-results.json`). Each team file is standard Showdown
|
|
42
|
+
export/paste text; multiple Teams in one file are separated by
|
|
43
|
+
`=== [gen3ou] Name ===` header lines.
|
|
44
|
+
|
|
45
|
+
The CLI prints a ranked Strength table (with a 95% confidence interval and a
|
|
46
|
+
per-matchup breakdown) and writes the full structured result to `--out`.
|
|
47
|
+
|
|
48
|
+
## Programmatic API
|
|
49
|
+
|
|
50
|
+
The base seam is `evaluate` — the Critic and Designer build on it, not on the CLI:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { evaluate } from 'poke-crucible';
|
|
54
|
+
|
|
55
|
+
const result = await evaluate({
|
|
56
|
+
teams: showdownPasteText, // string or array of pastes/{name,paste}
|
|
57
|
+
opposition: { mode: 'gauntlet' }, // or { mode: 'round-robin' }
|
|
58
|
+
battlesPerPairing: 100,
|
|
59
|
+
seed: 'my-seed',
|
|
60
|
+
});
|
|
61
|
+
// result: ranked Strength rows (per-matchup win-rates tagged with Archetype),
|
|
62
|
+
// the seed, the Gauntlet version, and any illegal-Team validation errors.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Illegal Teams never throw past the seam — they come back as `result.errors`,
|
|
66
|
+
named, so a bad Team can't silently poison a run.
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
- **Pilot** (ADR 0002): one shared Gen 3 heuristic, identical on both sides,
|
|
71
|
+
built in competence layers — secure a KO → pivot out of a losing matchup →
|
|
72
|
+
recover → lay Spikes / spin → spread Toxic/burn/paralysis → set up → max
|
|
73
|
+
damage. Every Strength number is relative to this Pilot.
|
|
74
|
+
- **Determinism**: a master seed threads through `@pkmn/sim`'s PRNG; each
|
|
75
|
+
pairing's Battles derive sub-seeds deterministically and sides are swapped
|
|
76
|
+
50/50, so a fixed seed reproduces identical results.
|
|
77
|
+
- **Gauntlet** (ADR 0003): a fixed, **versioned** roster of Archetype-labeled
|
|
78
|
+
Gen 3 OU Teams. Every result records the version, so `62% Strength` means the
|
|
79
|
+
same thing next week. Two versions ship: **`g1`** (default; hand-authored) and
|
|
80
|
+
**`g2`** (real [Smogon ADV OU sample teams](https://www.smogon.com/forums/threads/adv-ou-sample-teams.3687813/),
|
|
81
|
+
fetched verbatim, `--gauntlet-version g2`). `g1` stays the default baseline;
|
|
82
|
+
swapping it is a deliberate rebaseline.
|
|
83
|
+
- **Parallelism**: `--concurrency <N>` (or `POKE_CONCURRENCY`) runs Battles across
|
|
84
|
+
`N` worker threads. All Battles of a run are dispatched together so the pool
|
|
85
|
+
stays saturated; results are **byte-identical** to serial (each Battle keeps
|
|
86
|
+
its own seed). Default is 1 (inline). It pays off on big, expensive runs — the
|
|
87
|
+
Designer, large evaluations — where it gives roughly a 2× speedup on 4 workers;
|
|
88
|
+
for small/fast runs the thread overhead can make it a wash, hence the default.
|
|
89
|
+
|
|
90
|
+
### Gauntlet provenance (a deliberate deviation)
|
|
91
|
+
|
|
92
|
+
The design envisioned sourcing the Gauntlet from Smogon's sample teams + usage
|
|
93
|
+
stats via fetch-and-parse. That is deferred: the `g1` roster is **hand-authored**
|
|
94
|
+
from canonical Smogon Gen 3 OU sets (see
|
|
95
|
+
[`src/gauntlet/teams-g1.ts`](src/gauntlet/teams-g1.ts)). Because the Gauntlet is
|
|
96
|
+
versioned and every result records its version, re-sourcing later is a `g2`
|
|
97
|
+
bump, not a rewrite — nothing silently drifts.
|
|
98
|
+
|
|
99
|
+
## Critic (LLM analysis — Slice 2)
|
|
100
|
+
|
|
101
|
+
Turn an evaluation into a readable, grounded critique of one Team:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
node dist/cli.js critique team.txt
|
|
105
|
+
node dist/cli.js critique --model claude-sonnet-5 --effort low --battles 100 team.txt
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The Critic runs the evaluation, then asks Claude (via the **Claude Agent SDK**)
|
|
109
|
+
to explain the Team's weaknesses, the Archetypes that exploit them, the
|
|
110
|
+
mechanism, and concrete legal fixes — writing a structured `Critique` JSON. It
|
|
111
|
+
is told the Pilot under-rates stall (ADR 0004), so it caveats defense-dependent
|
|
112
|
+
verdicts rather than presenting them as ground truth.
|
|
113
|
+
|
|
114
|
+
**Auth (individual use):** the Critic runs on your **Claude subscription**, not
|
|
115
|
+
a pay-per-token key. Log in with Claude Code (or set `CLAUDE_CODE_OAUTH_TOKEN`),
|
|
116
|
+
and make sure **`ANTHROPIC_API_KEY` is unset** — it shadows the subscription
|
|
117
|
+
OAuth token and reverts to per-token billing. The model is called with all tools
|
|
118
|
+
disabled and low reasoning effort; nothing runs on your filesystem.
|
|
119
|
+
|
|
120
|
+
Programmatically the seam is `critique(input, model)` / `critiqueTeam(paste,
|
|
121
|
+
model, opts)`; the model is any `CritiqueModel` (use `StubCritiqueModel` in tests,
|
|
122
|
+
`AgentSdkCritiqueModel` live). See [`docs/prd-slice-2-critic.md`](docs/prd-slice-2-critic.md).
|
|
123
|
+
|
|
124
|
+
## Designer (discovery — Slice 3)
|
|
125
|
+
|
|
126
|
+
Evolve stronger Teams automatically. A population of legal Teams is scored each
|
|
127
|
+
generation by a **composite fitness** (Gauntlet Strength + peer round-robin + a
|
|
128
|
+
worst-matchup robustness term), bred by mutation and crossover, with the LLM
|
|
129
|
+
proposing variants — driven by the Critic's findings — and naming the Archetypes
|
|
130
|
+
it discovers.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Evolve from a starting Team (omit the file to search from scratch)
|
|
134
|
+
node dist/cli.js design --population 8 --generations 4 team.txt
|
|
135
|
+
|
|
136
|
+
# Random operators only — no LLM, no subscription call
|
|
137
|
+
node dist/cli.js design --no-llm --population 8 --generations 4
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Options: `--population`, `--generations`, `--battles` (per fitness pairing),
|
|
141
|
+
`--model` / `--effort` (Designer LLM), `--no-llm`, `--seed`, `--gauntlet-version`,
|
|
142
|
+
`--out`. Every candidate is validated legal Gen 3 OU; the run is deterministic
|
|
143
|
+
under its seed and reports each Team's lineage, Archetype name, fitness
|
|
144
|
+
components, plus the budget used and total Battles run. To bound compute it
|
|
145
|
+
caches unchanged Teams, screens cheaply before spending full battles on
|
|
146
|
+
contenders, and honours a hard battle cap.
|
|
147
|
+
|
|
148
|
+
Programmatically the seam is `design(input, model, options)`; the model is any
|
|
149
|
+
`DesignerModel` (`StubDesignerModel` for tests, `AgentSdkDesignerModel` live),
|
|
150
|
+
and an optional `critic` lets the Critic drive the mutation proposals. Auth is
|
|
151
|
+
the same subscription path as the Critic. See
|
|
152
|
+
[`docs/prd-slice-3-designer.md`](docs/prd-slice-3-designer.md).
|
|
153
|
+
|
|
154
|
+
**Fitness is deliberately not raw win-rate (ADR 0004)** — it rewards well-rounded
|
|
155
|
+
Teams over glass cannons and dampens the Pilot's offense lean (see the limitation
|
|
156
|
+
below for how far that goes).
|
|
157
|
+
|
|
158
|
+
## Testing (the trust gate)
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm run typecheck
|
|
162
|
+
npm test
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The suite asserts external behavior only, through the `evaluate()` seam (never
|
|
166
|
+
the Pilot's individual move choices): determinism, sanity ordering (a strong
|
|
167
|
+
Team out-Strengths a weak one), **archetype non-bias** (a solid stall Team stays
|
|
168
|
+
competitive in a head-to-head ladder of the eight archetypes and can beat
|
|
169
|
+
offense — the live check that the Pilot isn't biased against defense), legality,
|
|
170
|
+
and stable win-rates. Every Gauntlet Team is asserted legal Gen 3 OU.
|
|
171
|
+
|
|
172
|
+
### Known limitation: the Pilot's offense lean
|
|
173
|
+
|
|
174
|
+
The shared greedy Pilot systematically **under-rates stall** — offense plays
|
|
175
|
+
near-optimally under greedy heuristics while stall needs long-term planning. In
|
|
176
|
+
an archetype-vs-archetype ladder the best stall Team lands mid-lower (≈38%
|
|
177
|
+
Strength, 6th of 8), not mid-pack.
|
|
178
|
+
|
|
179
|
+
This was probed exhaustively ([ADR 0004](docs/adr/0004-greedy-pilot-underrates-stall-search-deferred.md)):
|
|
180
|
+
three Pilot improvements (anticipatory switching, tempo tweaks, a positional-eval
|
|
181
|
+
Pilot) all failed to lift stall; real-sim look-ahead is infeasible (`@pkmn/sim`
|
|
182
|
+
state can't be resumed after deserialization); and the Designer's composite
|
|
183
|
+
fitness only *partly* corrects the lean — stall's worst matchups are genuinely
|
|
184
|
+
~0% under the Pilot, so a robustness term can't recover them.
|
|
185
|
+
|
|
186
|
+
The lean is therefore a **known, bounded, documented property of the greedy-Pilot
|
|
187
|
+
design**, not a bug — every Strength number is explicitly relative to this Pilot
|
|
188
|
+
(ADR 0002). Closing it needs a stronger Pilot (a shallow search or ML policy —
|
|
189
|
+
deferred) or a non-Pilot signal of a Team's merit; both are future work. The
|
|
190
|
+
Critic caveats defense-dependent verdicts and the Designer's fitness avoids
|
|
191
|
+
rewarding glass cannons, so the slices degrade gracefully rather than silently
|
|
192
|
+
trusting the lean.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface PairingRecord {
|
|
2
|
+
/** Battles Team A won outright. */
|
|
3
|
+
wins: number;
|
|
4
|
+
ties: number;
|
|
5
|
+
battles: number;
|
|
6
|
+
/** Team A's win-rate in [0, 1]; ties count as half. */
|
|
7
|
+
winRate: number;
|
|
8
|
+
}
|
|
9
|
+
/** One Team-vs-opponent pairing to run, tagged with a caller key. */
|
|
10
|
+
export interface MatchupSpec {
|
|
11
|
+
key: string;
|
|
12
|
+
aPacked: string;
|
|
13
|
+
bPacked: string;
|
|
14
|
+
/** Stable indices for reproducible per-Battle seeds (same as RunPairingOptions). */
|
|
15
|
+
teamIndex: number;
|
|
16
|
+
opponentIndex: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Run many pairings at once. Every Battle across every pairing is flattened into
|
|
20
|
+
* one job list and dispatched together, so a worker pool stays saturated instead
|
|
21
|
+
* of idling between pairings (per-pairing dispatch left workers waiting on a
|
|
22
|
+
* single long stall mirror). Seeds are unchanged, so results are identical to
|
|
23
|
+
* running the pairings one at a time.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runMatchups(specs: MatchupSpec[], battles: number, master: string, maxTurns?: number): Promise<Map<string, PairingRecord>>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pairing runner — sits on top of {@link runBattle}. Given a batch of Team-vs-
|
|
3
|
+
* opponent pairings, it runs every Battle (sides swapped 50/50, so any lead or
|
|
4
|
+
* turn-order edge cancels out) from deterministically derived sub-seeds and
|
|
5
|
+
* returns a win-rate record per pairing. Battles are flattened into one job list
|
|
6
|
+
* so a worker pool stays saturated.
|
|
7
|
+
*/
|
|
8
|
+
import { runBattle } from './run-battle.js';
|
|
9
|
+
import { getBattlePool } from './pool.js';
|
|
10
|
+
import { deriveSeed } from '../seed.js';
|
|
11
|
+
/**
|
|
12
|
+
* Run many pairings at once. Every Battle across every pairing is flattened into
|
|
13
|
+
* one job list and dispatched together, so a worker pool stays saturated instead
|
|
14
|
+
* of idling between pairings (per-pairing dispatch left workers waiting on a
|
|
15
|
+
* single long stall mirror). Seeds are unchanged, so results are identical to
|
|
16
|
+
* running the pairings one at a time.
|
|
17
|
+
*/
|
|
18
|
+
export async function runMatchups(specs, battles, master, maxTurns) {
|
|
19
|
+
const tally = new Map();
|
|
20
|
+
for (const s of specs)
|
|
21
|
+
tally.set(s.key, { wins: 0, ties: 0 });
|
|
22
|
+
const record = (key, aIsP1, res) => {
|
|
23
|
+
const t = tally.get(key);
|
|
24
|
+
if (res.winner === null)
|
|
25
|
+
t.ties++;
|
|
26
|
+
else if (aIsP1 ? res.winner === 'p1' : res.winner === 'p2')
|
|
27
|
+
t.wins++;
|
|
28
|
+
};
|
|
29
|
+
const jobs = [];
|
|
30
|
+
for (const s of specs) {
|
|
31
|
+
for (let b = 0; b < battles; b++) {
|
|
32
|
+
const aIsP1 = b % 2 === 0;
|
|
33
|
+
jobs.push({
|
|
34
|
+
key: s.key,
|
|
35
|
+
aIsP1,
|
|
36
|
+
p1: aIsP1 ? s.aPacked : s.bPacked,
|
|
37
|
+
p2: aIsP1 ? s.bPacked : s.aPacked,
|
|
38
|
+
seed: deriveSeed(master, s.teamIndex, s.opponentIndex, b),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const pool = getBattlePool();
|
|
43
|
+
if (pool) {
|
|
44
|
+
await Promise.all(jobs.map((j) => pool.run({ p1: j.p1, p2: j.p2, seed: j.seed, maxTurns }).then((res) => record(j.key, j.aIsP1, res))));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
for (const j of jobs) {
|
|
48
|
+
const res = await runBattle(j.p1, j.p2, j.seed, { maxTurns });
|
|
49
|
+
record(j.key, j.aIsP1, res);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const out = new Map();
|
|
53
|
+
for (const s of specs) {
|
|
54
|
+
const { wins, ties } = tally.get(s.key);
|
|
55
|
+
out.set(s.key, { wins, ties, battles, winRate: battles > 0 ? (wins + 0.5 * ties) / battles : 0 });
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=pairing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/battle/pairing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,SAAS,EAAqB,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAiB,MAAM,YAAY,CAAC;AAqBvD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAAoB,EACpB,OAAe,EACf,MAAc,EACd,QAAiB;IAEjB,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0C,CAAC;IAChE,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,CAAC,GAAW,EAAE,KAAc,EAAE,GAAiB,EAAQ,EAAE;QACtE,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC;QAC1B,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI;YAAE,CAAC,CAAC,IAAI,EAAE,CAAC;aAC7B,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,IAAI;YAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACvE,CAAC,CAAC;IASF,MAAM,IAAI,GAAU,EAAE,CAAC;IACvB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;YACjC,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,IAAI,CAAC;gBACR,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,KAAK;gBACL,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;gBACjC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;gBACjC,IAAI,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;aAC1D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;IAC7B,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,OAAO,CAAC,GAAG,CACf,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACb,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CACpG,CACF,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC9D,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAE,CAAC;QACzC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BattleResult } from './run-battle.js';
|
|
2
|
+
import type { PRNGSeed } from '../seed.js';
|
|
3
|
+
export interface BattleJob {
|
|
4
|
+
p1: string;
|
|
5
|
+
p2: string;
|
|
6
|
+
seed: PRNGSeed;
|
|
7
|
+
maxTurns?: number;
|
|
8
|
+
}
|
|
9
|
+
/** One unit of the pool — wraps a real thread, or a fake in tests. */
|
|
10
|
+
export interface PoolWorker {
|
|
11
|
+
exec(job: BattleJob): Promise<BattleResult>;
|
|
12
|
+
terminate(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare class BattlePool {
|
|
15
|
+
private readonly workers;
|
|
16
|
+
private readonly idle;
|
|
17
|
+
private readonly queue;
|
|
18
|
+
constructor(size: number, spawn: () => PoolWorker);
|
|
19
|
+
/** Run one Battle, queued until a worker is free. */
|
|
20
|
+
run(job: BattleJob): Promise<BattleResult>;
|
|
21
|
+
private pump;
|
|
22
|
+
close(): void;
|
|
23
|
+
}
|
|
24
|
+
/** The shared pool for this process, or null when concurrency ≤ 1 (run inline). */
|
|
25
|
+
export declare function getBattlePool(): BattlePool | null;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bounded worker-thread pool for running Battles in parallel. The sim is
|
|
3
|
+
* CPU-bound and single-threaded, so real speedup needs threads, not just async.
|
|
4
|
+
*
|
|
5
|
+
* The pool is deterministic-safe: every Battle carries its own derived seed, and
|
|
6
|
+
* callers aggregate results order-independently, so parallel and serial runs
|
|
7
|
+
* produce identical numbers.
|
|
8
|
+
*
|
|
9
|
+
* Concurrency comes from `POKE_CONCURRENCY` (default 1 = run inline, no pool).
|
|
10
|
+
* The worker spawn is injectable so the scheduling logic is unit-testable
|
|
11
|
+
* without real threads.
|
|
12
|
+
*/
|
|
13
|
+
import { Worker } from 'node:worker_threads';
|
|
14
|
+
export class BattlePool {
|
|
15
|
+
workers;
|
|
16
|
+
idle;
|
|
17
|
+
queue = [];
|
|
18
|
+
constructor(size, spawn) {
|
|
19
|
+
this.workers = Array.from({ length: Math.max(1, size) }, spawn);
|
|
20
|
+
this.idle = [...this.workers];
|
|
21
|
+
}
|
|
22
|
+
/** Run one Battle, queued until a worker is free. */
|
|
23
|
+
run(job) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
this.queue.push({ job, resolve, reject });
|
|
26
|
+
this.pump();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
pump() {
|
|
30
|
+
while (this.idle.length > 0 && this.queue.length > 0) {
|
|
31
|
+
const worker = this.idle.pop();
|
|
32
|
+
const { job, resolve, reject } = this.queue.shift();
|
|
33
|
+
worker.exec(job).then((res) => {
|
|
34
|
+
this.idle.push(worker);
|
|
35
|
+
resolve(res);
|
|
36
|
+
this.pump();
|
|
37
|
+
}, (err) => {
|
|
38
|
+
this.idle.push(worker);
|
|
39
|
+
reject(err);
|
|
40
|
+
this.pump();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
close() {
|
|
45
|
+
for (const w of this.workers)
|
|
46
|
+
w.terminate();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** A PoolWorker backed by a real `worker_threads` thread running `worker.js`. */
|
|
50
|
+
class ThreadWorker {
|
|
51
|
+
worker;
|
|
52
|
+
pending = null;
|
|
53
|
+
constructor() {
|
|
54
|
+
this.worker = new Worker(new URL('./worker.js', import.meta.url));
|
|
55
|
+
this.worker.on('message', (msg) => {
|
|
56
|
+
const p = this.settle();
|
|
57
|
+
if (!p)
|
|
58
|
+
return;
|
|
59
|
+
if (msg.ok && msg.res)
|
|
60
|
+
p.resolve(msg.res);
|
|
61
|
+
else
|
|
62
|
+
p.reject(new Error(msg.error ?? 'battle worker failed'));
|
|
63
|
+
});
|
|
64
|
+
this.worker.on('error', (err) => this.settle()?.reject(err));
|
|
65
|
+
// Idle workers don't keep the process alive; a worker is ref'd only while it
|
|
66
|
+
// has a job in flight (see exec), so the run stays alive but can still exit.
|
|
67
|
+
this.worker.unref();
|
|
68
|
+
}
|
|
69
|
+
/** Clear the pending state and stop keeping the loop alive. */
|
|
70
|
+
settle() {
|
|
71
|
+
const p = this.pending;
|
|
72
|
+
this.pending = null;
|
|
73
|
+
if (p)
|
|
74
|
+
this.worker.unref();
|
|
75
|
+
return p;
|
|
76
|
+
}
|
|
77
|
+
exec(job) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
this.pending = { resolve, reject };
|
|
80
|
+
this.worker.ref(); // hold the loop open until this Battle finishes
|
|
81
|
+
this.worker.postMessage(job);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
terminate() {
|
|
85
|
+
void this.worker.terminate();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
let cached;
|
|
89
|
+
/** The shared pool for this process, or null when concurrency ≤ 1 (run inline). */
|
|
90
|
+
export function getBattlePool() {
|
|
91
|
+
if (cached !== undefined)
|
|
92
|
+
return cached;
|
|
93
|
+
const n = Math.floor(Number(process.env['POKE_CONCURRENCY'] ?? '1'));
|
|
94
|
+
cached = Number.isFinite(n) && n > 1 ? new BattlePool(n, () => new ThreadWorker()) : null;
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/battle/pool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type SideID } from '../pilot/pilot.js';
|
|
2
|
+
import type { PRNGSeed } from '../seed.js';
|
|
3
|
+
export interface BattleResult {
|
|
4
|
+
/** 'p1', 'p2', or null for a tie. */
|
|
5
|
+
winner: SideID | null;
|
|
6
|
+
turns: number;
|
|
7
|
+
}
|
|
8
|
+
export interface RunBattleOptions {
|
|
9
|
+
/** Safety net against pathological stall mirrors; forces a tiebreak past this. */
|
|
10
|
+
maxTurns?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Run a single Battle between two packed Teams and report the winner. */
|
|
13
|
+
export declare function runBattle(p1Packed: string, p2Packed: string, seed: PRNGSeed, options?: RunBattleOptions): Promise<BattleResult>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battle runner — runs one Gen 3 OU Battle in-process through `@pkmn/sim`'s
|
|
3
|
+
* BattleStream, both sides driven by the {@link PilotPlayer}, from a fixed seed,
|
|
4
|
+
* and reports the winner. No server, no network.
|
|
5
|
+
*/
|
|
6
|
+
import { BattleStreams } from '@pkmn/sim';
|
|
7
|
+
import { FORMAT } from '../types.js';
|
|
8
|
+
import { PilotPlayer } from '../pilot/pilot.js';
|
|
9
|
+
/** Run a single Battle between two packed Teams and report the winner. */
|
|
10
|
+
export async function runBattle(p1Packed, p2Packed, seed, options = {}) {
|
|
11
|
+
const maxTurns = options.maxTurns ?? 1000;
|
|
12
|
+
const stream = new BattleStreams.BattleStream();
|
|
13
|
+
const streams = BattleStreams.getPlayerStreams(stream);
|
|
14
|
+
const getBattle = () => stream.battle;
|
|
15
|
+
const p1 = new PilotPlayer(streams.p1, getBattle, 'p1');
|
|
16
|
+
const p2 = new PilotPlayer(streams.p2, getBattle, 'p2');
|
|
17
|
+
void p1.start();
|
|
18
|
+
void p2.start();
|
|
19
|
+
let winner = null;
|
|
20
|
+
let turns = 0;
|
|
21
|
+
let forced = false;
|
|
22
|
+
const consume = (async () => {
|
|
23
|
+
for await (const chunk of streams.omniscient) {
|
|
24
|
+
for (const line of chunk.split('\n')) {
|
|
25
|
+
if (line.startsWith('|turn|')) {
|
|
26
|
+
turns = parseInt(line.slice('|turn|'.length), 10) || turns;
|
|
27
|
+
if (!forced && turns > maxTurns) {
|
|
28
|
+
forced = true;
|
|
29
|
+
void stream.write('>tiebreak');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (line.startsWith('|win|')) {
|
|
33
|
+
const name = line.slice('|win|'.length).trim();
|
|
34
|
+
winner = name === 'A' ? 'p1' : name === 'B' ? 'p2' : null;
|
|
35
|
+
}
|
|
36
|
+
else if (line.startsWith('|tie')) {
|
|
37
|
+
winner = null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
const spec = { formatid: FORMAT, seed };
|
|
43
|
+
void stream.write(`>start ${JSON.stringify(spec)}`);
|
|
44
|
+
void stream.write(`>player p1 ${JSON.stringify({ name: 'A', team: p1Packed })}`);
|
|
45
|
+
void stream.write(`>player p2 ${JSON.stringify({ name: 'B', team: p2Packed })}`);
|
|
46
|
+
await consume;
|
|
47
|
+
return { winner, turns };
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=run-battle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-battle.js","sourceRoot":"","sources":["../../src/battle/run-battle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-thread entry: runs one Battle per message and posts the winner back.
|
|
3
|
+
* Battles are CPU-bound and synchronous in the sim, so a pool of these threads
|
|
4
|
+
* is what turns a many-battle run from serial into parallel. Loaded by the pool
|
|
5
|
+
* as the compiled `dist/battle/worker.js` (never imported directly).
|
|
6
|
+
*/
|
|
7
|
+
import { parentPort } from 'node:worker_threads';
|
|
8
|
+
import { runBattle } from './run-battle.js';
|
|
9
|
+
if (parentPort) {
|
|
10
|
+
const port = parentPort;
|
|
11
|
+
port.on('message', async (job) => {
|
|
12
|
+
try {
|
|
13
|
+
const res = await runBattle(job.p1, job.p2, job.seed, { maxTurns: job.maxTurns });
|
|
14
|
+
port.postMessage({ ok: true, res });
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
port.postMessage({ ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/battle/worker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAU5C,IAAI,UAAU,EAAE,CAAC;IACf,MAAM,IAAI,GAAG,UAAU,CAAC;IACxB,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,GAAQ,EAAE,EAAE;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YAClF,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACrF,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/cli.d.ts
ADDED