Skip to content

Make PokéArena discoverable: license, module path, releases, Python package - #155

Open
shaumik wants to merge 12 commits into
mainfrom
claude/project-viral-growth-8yk4s5
Open

Make PokéArena discoverable: license, module path, releases, Python package#155
shaumik wants to merge 12 commits into
mainfrom
claude/project-viral-growth-8yk4s5

Conversation

@shaumik

@shaumik shaumik commented Aug 25, 2026

Copy link
Copy Markdown
Owner

The engine was never the problem. Someone could find the GitHub page and then
not install it, not legally use it, and not run it — so nothing downstream of
"found the repo" worked. This fixes that chain end to end.

The four blockers

module pokearena was not a resolvable import path. go install github.com/shaumik/PokeArena/cmd/...@latest failed and the repo did not appear
on pkg.go.dev at all. Renamed, which rewrote 402 import lines. The sed was
scoped to import-shaped references only — a naive replace would have corrupted
"pokearena-mcp", "pokearena.events" and "pokearena.work", a tool name and
two RabbitMQ exchange names.

No LICENSE meant all rights reserved. Unusable in published research,
unusable commercially, and rejected on sight by the registries and awesome-lists
worth being listed in. MIT, keeping the trademark position the README already
stated.

cmd/bench could not run outside a clone. It loaded the dex via
domain.LoadDex("data") and defaulted -teams to a relative path, so it died
at dex load from any other directory. The zero-setup entry point only ever
opened from inside a checkout. The module-root embed now covers
benchmark-teams.json, _provenance.json and model-pricing.json, and bench
falls back to it when the dataset directory is absent. An explicit -data that
does not exist is still a hard error.

No tags meant no registry listing. The official MCP Registry only accepts
artifacts from GitHub or GitLab releases, so the project was structurally
ineligible for the place MCP clients browse.

What else is here

  • README leads with the reproducibility claim instead of the leaderboard the
    same README admits is unverified. cmd/bench is the first runnable block;
    docker compose up keeps its section further down. The heading is now
    Connect your agent (Pv-Agent), repairing an anchor ARCHITECTURE.md and
    mcp-protocol.md were already linking to.
  • AGENTS.md for the agent doing the discovering: the no-infra path, what
    does and does not need services, the MCP tool surface, observation and action
    shapes.
  • pip install pokearena — Gymnasium and PettingZoo shaped, subprocessing a
    new cmd/pokearena-env that speaks JSONL over stdio. No required runtime
    dependencies. Observations come from the same ai.MakeView redaction path the
    MCP server and PvP socket use, so fog of war is not reimplemented for a second
    consumer.
  • Release pipeline + server.json. The registry has no Go package type, so
    the server ships as an MCPB bundle built from a release artifact. GoReleaser
    covers three commands across linux/darwin/windows and amd64/arm64.
  • Tagging from the Actions tab. release.yml takes a version input; the
    tag job runs build/vet/test before creating the tag. Tagging lives in that
    workflow rather than a dedicated one because a tag pushed with GITHUB_TOKEN
    cannot trigger another workflow, so a separate tagger would tag and then do
    nothing.
  • CITATION.cff, a Claude Code plugin, and
    docs/deeper-search-played-worse.md — benchmark.md §6 written up as
    something a stranger will read, keeping the part that makes it honest: the
    corrected search model is weaker than the buggy one, because the phantom KO
    had been inducing helpful aggression.

Verification

  • go build ./..., go vet ./..., gofmt -l . clean. Full go test ./...
    17 packages, all pass.
  • Python: 52 tests pass against the binary; without it they skip loudly, saying
    nothing about the engine was verified rather than reporting a false green.
  • Embedded vs on-disk bench runs diffed over 1801 trace rows: byte-identical.
    This was the risk worth checking — an embedded dataset that drifts from
    data/ would make published numbers unreproducible, which is the project's
    core claim.
  • server.json validates against the 2025-12-11 schema and against the live
    registry via mcp-publisher validate. All four workflow dispatch paths
    simulated.
  • Every relative link across six markdown files resolves.

Known gaps

  • gymnasium / pettingzoo could not be installed in the build container,
    so only the degraded no-dependency path was exercised. The subclassing is
    guarded and provably import-safe, but nobody has watched PokeArenaEnv
    actually inherit from gymnasium.Env. Worth a CI job with [all] installed
    before the Python package is announced anywhere.
  • benchmark.md §6 may want a re-sweep. A fresh run of the depth sweep gave
    expectimax 37.5% [31.6, 43.8] where the doc publishes 36.7% [30.8, 42.9].
    Intervals overlap so nothing is contradicted, but _provenance.json shows the
    dataset moved after that table was measured, and §6 is described in that doc
    as its only load-bearing measurement.
  • golangci-lint could not run here — the container's build targets go1.25
    while go.mod is on 1.26. Pre-existing, unrelated to this branch; CI runs the
    pinned v2.12.2.
  • POKEARENA_GATEWAY_URL still defaults to ws://localhost:8080, so anyone
    arriving via the MCP Registry must run docker compose up before the server
    does anything. The benchmark's friction is now zero; the MCP path's is not.

Nothing here is merged-and-live until v0.1.0 is tagged — release binaries, the
registry record, go install and pkg.go.dev all wait on it. docs/publishing.md
is the runbook.

🤖 Generated with Claude Code

https://claude.ai/code/session_012m9mPxYV2MeV7kGt3rXc63


Generated by Claude Code

claude added 12 commits August 25, 2026 18:19
The module was named "pokearena", which is not a resolvable import path.
That made "go install github.com/shaumik/PokeArena/cmd/...@latest" fail and
kept the repo off pkg.go.dev entirely — the most natural way to obtain the
MCP server and the bench harness did not work at all.

Rename the module and rewrite the 402 import lines that referenced it. The
root package keeps its name (pokearena), so cmd/pokearena-agent aliases the
import rather than relying on a basename that no longer matches.

The repo also had no LICENSE, which means all rights reserved: unusable in
published research, unusable commercially, and rejected on sight by the
registries and awesome-lists we want to be listed in. MIT, with the trademark
position the README already states kept alongside it.
The README opened on the arena and its leaderboard — which the same
README then admits is unverified, keyed on free-text names with no
ownership. We were leading with the weakest asset on the page.

The strongest one was buried in benchmark.md section 2: the engine is
ours rather than a Showdown wrapper, which buys variance-controlled
mirror matches. Same seed, same team, both sides, byte-identical RNG
stream, so the only free variable is the policy. That is the claim no
Showdown-wrapping harness can structurally make, so it is now the
headline.

The first runnable block was 'docker compose up' — Postgres, RabbitMQ,
Redis and four services. An agent evaluating the repo in a sandbox
cannot run that and silently gives up. cmd/bench needs none of it, so
it goes first; the full arena keeps its section further down.

Add AGENTS.md for the agent doing the discovering: the no-infra path,
what does and does not need services, the MCP tool surface, and the
observation and action shapes.

The heading is now 'Connect your agent (Pv-Agent)', which repairs the
anchor ARCHITECTURE.md and mcp-protocol.md were already linking to.
cmd/bench is the project's zero-setup entry point — the one thing a
reader can run in a minute with no services and no API key. But it
loaded the dex with LoadDex("data") and defaulted -teams to
data/benchmark-teams.json, both relative to the working directory. So
'go run github.com/shaumik/PokeArena/cmd/bench@latest' died at dex load
from the module cache, and the front door only ever opened from inside
a clone.

Widen the module-root embed to cover benchmark-teams.json,
_provenance.json and model-pricing.json, and fall back to it when the
dataset directory is absent. An explicit -data that does not exist is
still a hard error; the user named a directory and we should not
quietly ignore them.

The library, provenance and pricing loaders grow fs.FS variants with
the disk versions as thin wrappers, matching what LoadDex/LoadDexFS
already did.

Embedded and on-disk runs were diffed over 1801 trace rows of a
heuristic-vs-expectimax run: byte-identical, so the fallback cannot
fork the benchmark from the copy in data/.
Three discovery surfaces the repo had no answer for.

CITATION.cff gives GitHub its 'Cite this repository' button and gives a
researcher's tooling something machine-readable to pick up. CFF 1.2.0,
validated against the published schema.

.claude-plugin/ registers the existing MCP server so Claude Code can
install PokeArena in one step, and lets the repo serve as its own
marketplace. The launcher resolves a release binary, then PATH, then
falls back to 'go run' from the module root, and keeps every diagnostic
on stderr so stdout stays a clean JSON-RPC stream. The bundled skill is
written off docs/mcp-protocol.md and the real handlers rather than the
README's prose: the phase gate, {ready:false} as an ordinary timeout,
act's optimistic write, and forfeit-on-leave.

docs/deeper-search-played-worse.md pulls benchmark.md section 6 out
into something a stranger will read: expectimax searching deeper and
playing worse, traced to a fog-of-war reconstruction that scored a KO
of the visible active as winning the whole game. It keeps the part that
makes it honest — the corrected model is weaker than the buggy one,
because the phantom KO had been inducing helpful aggression — and it
keeps the doc's caveat that the d2/d3 tail is one run's suggestion, not
a result. Every figure was diffed against section 6; none is new.
New command, same convention as the other root-dropped build artifacts:
'go build ./cmd/pokearena-env' leaves a 5.9M binary in the repo root and
it should not be committable.
The repo had no tags and no releases, which is not just untidy: the
official MCP Registry only accepts artifacts from GitHub or GitLab
releases, so PokeArena was structurally ineligible for the place MCP
clients actually browse. Nobody could find it there because it could
not be listed there.

server.json targets the 2025-12-11 schema. There is no Go registry
type, so the binary ships as an MCPB bundle built from a release
artifact, per the registry's own guidance for compiled servers. The
description is 95 of its 100 permitted characters and leads with
'playable' on purpose: a search for a Pokemon MCP server returns a
crowd of PokeAPI data wrappers, and the thing worth saying about this
one is that an agent takes a seat and plays.

GoReleaser builds the three user-facing commands across
linux/darwin/windows and amd64/arm64, matching the Dockerfile's flags.
cmd/bench ships as 'pokearena-bench' — a file called 'bench' in a
downloads folder tells you nothing. Registry publishing is a separate
job so a registry outage cannot cost the binaries.

Also ignore /dist/, which goreleaser writes to.
The engine is a pure function and cmd/bench already drives it in-process
with no services, but both of those are reachable only from Go. The
audience we want — RL and LLM-eval researchers — works in Python, and
their tooling reaches for a gym.make-shaped thing. There was no way in
that did not start with 'learn Go'.

This is the seam: one line of JSON per request on stdin, one per
response on stdout. reset / step / legal_actions / observe / close, plus
an info handshake that reports protocol version, engine revision and the
dataset's provenance, because a result that cannot say what produced it
cannot back the reproducibility claim.

It reads the embedded dataset, so it runs from any directory with no
data/ on disk. Observations are built through the same fog-of-war path
the arena uses rather than a second implementation: a side sees its own
team in full and only the foe's active, as hp_pct with the absolute HP,
stats, EVs, IVs, ability, nature and move ids withheld, and the foe's
bench as a count. Tests cover determinism, divergence on a different
seed, that no hidden field leaks, and illegal-action rejection.
The engine has been reachable only from Go. The people we most want
using it — RL and LLM-eval researchers — work in Python, and their
tooling reaches for a gym.make-shaped thing, which is why TextArena and
the rest standardised on that shape. This is the way in.

A Gymnasium-style single-agent env (a baseline drives the other side)
and a PettingZoo-style two-agent env, both subprocessing
cmd/pokearena-env. No required runtime dependencies: gymnasium,
pettingzoo and numpy are extras, and the package imports and runs
without any of them. A missing binary raises an actionable error naming
the go install line, never a silent failure.

The action space is a fixed Discrete(11) — four move slots, Struggle,
six switches — never renumbered between turns, with legality carried as
a mask, because an action space that renumbers is not learnable.

step() now accepts every shape a caller might plausibly hold: a bare
int, a legal_actions() record, that record's inner action object, or
its index. It previously forwarded the whole record to the engine,
which rejected it for a missing 'kind' the caller never typed —
step(legal_actions()[0]) is the first line anyone writes, and it failed.
A bad shape now raises a Python TypeError naming the forms that work
instead of surfacing a message from the Go layer.

52 tests pass against the binary and skip — loudly, saying nothing was
verified — without it.
The @latest note was written before the dataset was embedded and listed
two blockers. Only one is left: the tag.
Tagging by hand means a clean local checkout and remembering the gates.
Add a 'version' input that does it from the Actions tab instead.

The tag job runs build, vet and test before creating anything, so a
commit that fails the suite never gets tagged. Everything that can
refuse a release happens while the tag still does not exist: an
unpushed tag costs nothing to abandon, a pushed one is public history
that a release may already point at.

Tagging lives in this workflow rather than a dedicated one because a
tag pushed with GITHUB_TOKEN cannot trigger another workflow — that is
GitHub's recursion guard — so a separate tagger would create the tag
and then nothing would happen. Doing both in one run needs no secret.

The registry publish was gated on 'event_name == push', which would
have skipped it for a tag this workflow cut itself. It is now gated on
a flag the release job sets, true for a tag push or a freshly created
tag and false when only rebuilding an existing tag's artifacts, which
is the case the guard was actually there for.
golangci-lint enforces locale US and gofumpt, which is stricter than
the gofmt I had been checking with. Three British spellings and one
composite literal that gofumpt splits across lines.
The most likely first experience of anyone who installs pokearena-mcp
from the MCP registry is a refused connection: the default gateway is
ws://localhost:8080 and they have no arena running. They got 'connection
refused' and nothing else — no hint that an arena is a thing they need,
let alone how to get one. An agent relaying that message could not help
either.

Name the URL we tried and both ways forward. Only an unreachable dial
gets this; a gateway that answered and refused us has a different
problem and keeps its own error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants