Atrinik playtester is an autonomous, headless end-to-end player for Atrinik. It connects through ordinary player protocols, observes live game state, and runs navigation, quest, combat, equipment, economy, and survival policies without privileged server access.
The product goal is one reproducible unattended campaign from a new account and character through a versioned full-game completion contract. The initial golden path is tracked by issue #1: complete all 16 formal quests, reach level 115, defeat Zechna, verify every outcome from authoritative game state, and emit a machine-readable result.
The migrated implementation provides pinned, encrypted Classic QUIC transport, account and character setup, a live world model, content-derived navigation, 16 formal quest executors, adaptive farming through level 115, persistent memory, and a loopback operator dashboard.
The current autoplay command completes the introductory flow and then levels;
the all-quest runner remains separate, and Zechna is not yet an automated
completion gate. The roadmap issues track the work needed to turn these pieces
into the single start-to-finish campaign described above.
Build and install the wheel from the repository root in any clean Python 3.11+ environment. The protocol wheel and native pathfinding archive are selected by the compatibility lock; the project does not bundle or relicense either dependency. Preflight and install the protocol wheel from that lock before building the package:
python3 -m venv /path/to/playtester-environment
/path/to/playtester-environment/bin/python scripts/install_locked_protocol.py
/path/to/playtester-environment/bin/pip install .The installer verifies the locked repository, release tag, exact commit, asset,
download URL, and SHA-256 digest before invoking pip. The
atrinik-playtester preflight command repeats that remote availability check,
while doctor --preflight reports the same check alongside local cache and
installed-input diagnostics. When GITHUB_TOKEN is set, these GitHub API
requests are authenticated (the token is sent only to api.github.com), which
avoids the anonymous rate limit that shared CI runners hit.
For a coordinated unreleased Classic protocol change, retain the immutable lock and point only the development process at that checkout's generated bindings:
export ATRINIK_PLAYTESTER_PROTOCOL_SOURCE=/path/to/classic/protocol/generated/pythonThe source directory must contain atrinik_protocol. Playtester requires the
protocol revision 1085 and rejects an older installed wheel. The immutable
v5.106.0 lock identifies the matching released wire-1085 wheel, so normal
installation and doctor require no source override. Do not use the override
for a released run; it validates the exact generated-source module origin
rather than relabeling an installed artifact.
Install the exact content compiler/catalog and Classic runtime selected by
dependencies.lock.json. The default cache is
$XDG_CACHE_HOME/atrinik/playtester or ~/.cache/atrinik/playtester and can be
overridden with --cache or ATRINIK_PLAYTESTER_CACHE.
/path/to/playtester-environment/bin/atrinik-playtester dependencies
/path/to/playtester-environment/bin/atrinik-playtester doctor
/path/to/playtester-environment/bin/atrinik-playtester preflightdependencies downloads the content source and Classic-runtime archives, the
farming analyzer release asset, and the Classic server source archive over
HTTPS. It verifies their immutable SHA-256 coordinates, validates their bounded
layouts, and installs all four as one atomic cache generation. To seed a machine
from already downloaded files without network access:
atrinik-playtester dependencies \
--source-archive /media/atrinik-content-1.14.0.tar.gz \
--runtime-archive /media/atrinik-content-1.14.0-classic-runtime.tar.gz \
--analyzer-archive /media/atrinik-tools-farming-analyzer-3.8.0.tar.gz \
--server-archive /media/atrinik-classic-server-5.99.0.tar.gz \
--offlineAfter one successful installation, atrinik-playtester dependencies --offline
verifies and reuses the cache. doctor performs a full tree-integrity check and
diagnoses missing, mismatched, or corrupt content, protocol, and pathfinding
inputs. Gameplay commands discover the verified content tooling and runtime
from that cache; no aggregate repository or ambient sibling checkout is needed.
Requirements are Python 3.11+, CMake 3.21+, Ninja, and a C17 compiler. The
native adapter uses the checksum-pinned Classic libatrinik pathfinding release.
Run scripts/install_locked_protocol.py with the Python environment that will
build the package before configuring CMake. Install the required aioquic
transport dependency in that environment for the live TLS regression tests.
cmake -S . -B build/playtester -G Ninja -DBUILD_TESTING=ON
cmake --build build/playtester --parallel
ctest --test-dir build/playtester --output-on-failureThe complete Python suite uses the same installed bundle. Build the package, acquire it once, configure the verified paths through the package API, and run the regression suite:
PYTHONPATH=build/playtester/python python3 -m atrinik_bot dependencies
PYTHONPATH=build/playtester/python python3 -m atrinik_bot doctor
PYTHONPATH=build/playtester/python python3 - <<'PY'
import unittest
from atrinik_bot.compatibility import (
configure_cached_bundle,
default_cache_root,
load_lock,
)
configure_cached_bundle(default_cache_root(), load_lock())
result = unittest.TextTestRunner(verbosity=2).run(
unittest.defaultTestLoader.loadTestsFromName("atrinik_bot.test_bot")
)
raise SystemExit(not result.wasSuccessful())
PYRun commands from the build directory so the assembled atrinik_bot package,
including its native extension, takes precedence over the source files at the
repository root.
Install the package or invoke the assembled package. Credentials and mutable state belong outside the source tree and should be supplied through environment variables or a private launcher:
ATRINIK_BOT_ACCOUNT=playtester-account \
ATRINIK_BOT_PASSWORD='replace-me' \
ATRINIK_BOT_CERTIFICATE_SHA256='replace-with-server-certificate-sha256' \
ATRINIK_BOT_CHARACTER=Playtester \
ATRINIK_BOT_RUNTIME_STATE=/private/state/playtester.sqlite3 \
atrinik-playtester autoplay--runtime-content remains available as an explicit runtime override for
diagnostics, but ordinary runs use the locked cached runtime by default.
Use atrinik-playtester --help for transport, registration, character,
dashboard, navigation, quest, farming, banking, selling, and storage options.
The dashboard binds to loopback and rejects cross-origin control requests; use
an authenticated local tunnel instead of exposing it directly.
atrinik-playtester --version, the startup log line, and the dashboard header
name the build: its release tag (with +N when the commit is N commits past
it) and short commit, linked to GitHub. CMake records them from Git on every
build; a tree without Git metadata reports an unknown build.
The dashboard serves GET /metrics in the Prometheus text format 0.0.4
(text/plain; version=0.0.4; charset=utf-8), so a scraper can record what the
bot does over hours for analysis. The endpoint has the same access rules as
/api/state: it listens only on loopback, rejects a non-loopback Host
header, and needs no other credential. It is read-only and holds no secrets.
It never includes the account, password, invite code or certificate. Scrape it
from the same machine or through the same authenticated tunnel as the
dashboard. Deploying Prometheus or Grafana (scrape configuration and
dashboards) is out of scope here.
Every series is named atrinik_playtester_* and carries character (in lower
case) and server labels. Counters are kept in memory and start again from zero when
the process restarts, which rate() and increase() handle. Every other
label comes from a bounded set: server-named monsters (at most 300), skills,
attacks, damage types, spells, potions, action kinds (at most 200) and task
types each have a fixed cap. Values past a cap are counted as other. Chat and
other player-typed lines are never parsed into labels; a PvP kill's victim is
the one character name that can appear, within the monster cap.
Gauges, read from live state on each scrape:
connected(1 while playing),hp,maxhp,sp,maxsp,food,level,experience,regen_hp,regen_sp,depletion_points;skill_level{skill},skill_experience{skill};money_carried_copper,money_banked_copper(only once a balance is known);farm_predicted_xp_per_hour{objective}: the farming analyzer's estimate for its first-ranked spot;task_info{task}andphase_info{phase}: 1 for the running top-level task type and autoplay phase.
Counters:
| Metric | Labels | Counts |
|---|---|---|
kills_total |
monster |
"You killed …" lines |
experience_gained_total |
skill |
experience from "You got N exp in skill …" |
damage_dealt_total, hits_dealt_total |
attack |
"You hit … for N with …"; the server sends one line per attack type of a swing, so hits count damaging lines, not swings |
damage_taken_total, hits_taken_total |
type |
"… hit you for N … damage", one damaging line per attack type |
spells_cast_total |
spell |
spell casts ordered (including any the server refuses) |
mana_spent_total |
each ordered cast's listed spell cost | |
heals_total |
source |
her own healing spells on herself (spell) and healing potions applied (potion) |
healed_hp_total |
health restored by her healing spells on herself | |
potions_used_total |
potion |
potions applied |
deaths_total |
cause |
deaths, by the post-mortem cause |
rests_total |
rests started to regain spell points | |
actions_total |
kind |
every recorded action and decision, such as retreat, melee-kite or farm-pack-split |
purchases_total, sales_total |
shop purchases and sales | |
money_spent_copper_total |
reason |
money paid in shops (purchase) |
money_gained_copper_total |
source |
money received for sales (sale) |
item_moves_total |
direction |
item moves ordered: store, retrieve, loot (corpse moves and /take commands), pickup, drop |
chat_lines_total |
direction, channel |
player chat heard and answered |
connections_total, reconnects_total, disconnects_total |
game server connections; a failed attempt also counts as a disconnect | |
tasks_started_total |
task |
top-level tasks started |
tasks_finished_total |
task, status |
top-level tasks that completed, failed or were stopped from the dashboard (stopping counts as failed); a task replaced by another is not counted |
state_seconds_total |
state |
seconds fighting, resting, travelling, shopping (shops, temple, NPC dialogue), busy, idle or disconnected (not playing) |
The time in each state follows the latest recorded action that marks one. For
example, a target or a kite step means fighting, and a step or route decision
means travelling. Actions that mark none, such as applying an item, keep the
current state. A task that has not yet done anything that marks a state is
busy; with no task running, she is idle.
Autoplay and the dashboard rank farm spots with the locked atrinik/tools
farming analyzer instead of hard-coded level bands. The dependency installer
caches its release asset and the Classic server source whose experience tables
it reads. During web and autoplay, a background task checks the character
every 30 seconds. When the killing weapon school's level, the player doll's DAM,
WC, weapon speed, movement speed, max HP, AC or protections change materially, or the last
ranking is six hours old, it starts one nice analyzer process with
--ranking all --json (about 10 to 30 seconds) while the bot keeps playing.
Runs are at least ten minutes apart, failures back off for fifteen minutes, and
a failed run keeps the previous ranking. Only melee weapon schools are
simulated so far.
The dashboard's Farming tab shows the run status, the simulated profile and the top spots for the preferred objective: XP, money or loot per hour, balanced, or safety. The choice is saved per character and needs no new run, because one run ranks every objective. The rates are model estimates for comparing spots, not live measurements.
Autoplay farms the preferred objective's #1 usable spot. The exception is depletion: while the live depletion force is present and her depletion count is at the temple-service threshold (3), it farms the safety ranking instead, until the depletion is removed. If no safety spot is usable, it keeps the preferred objective's spot, because the circuit removes depletion before any farming whenever an item or the temple fee allows, and farming is what pays that fee. The Farming tab then shows the ranking she actually farms, and notes the depletion override; the objective toggle keeps showing your choice.
There are no level bands or curated spots: the analyzer result's map paths become the circuit's legs (a multi-map circuit rotates through every map), and its listed monsters become the targets. Spots whose residents include aggressive spawns get cautious recovery margins (flee at 80% HP, heal at 95%) and isolated pulls from squares outside other residents' detection radii; other spots use 75% and 90%. Until the first ranking exists, autoplay holds position safely. The Tasks tab's Farm picker lists the same ranked spots.
A death or emergency recall at the spot does not make autoplay give it up. Instead it reviews the last minute of combat messages and decisions to find out why, and adapts:
- pack: two or more monsters were hitting at once;
- resources: spell points ran short of the next heal;
- damage-type:TYPE: one non-physical damage type dealt most of the damage;
- out-damaged: a single attacker simply hit harder than she could recover;
- route: the failure happened on the way there, not at the spot.
Each failure raises the spot's adaptation level (up to 4). Each level raises the flee threshold by 3% and the heal threshold by 2%, capped at 90% and 98%. The lessons are persisted per character, and the dashboard's Farming tab shows them, together with improvement goals aggregated across spots (area spells, protection types, offence, healing). Only when the spot keeps failing at the maximum level (three failures there within an hour) does autoplay pause it for an hour and farm the next-ranked spot meanwhile.
A spot is still quarantined for six hours, and autoplay moves to the next-ranked spot, after:
- ten minutes there without any character XP;
- a route there that the world graph cannot plan.
Explicitly configured circuits keep the old behaviour: a death or emergency recall there quarantines the spot.
Before farming a circuit, autoplay moves the character's savebed (the "bed to reality" a death respawns at) to the usable bed with the shortest walk to the circuit's maps, when that saves at least 20 tiles over the current one. Inn and tavern beds work for anyone; apartment and house beds only in the starter apartment. The walk is estimated along the world graph's route from the bed, and the search spreads its routes over ticks so it never stalls combat. A bed that cannot be reached or applied is skipped for the next-nearest one. The dashboard's Farming tab shows where she respawns.
When the analyzer gets a spot wrong, the fix belongs in the analyzer.
The playtester source is independent of its external protocol, pathfinding,
content-tooling, farming-analyzer, and game-runtime inputs. Their immutable coordinates and licenses are recorded
in dependencies.lock.json and
THIRD_PARTY_NOTICES.md. In particular, a wheel or
binary linked with GPL libatrinik pathfinding is not an MIT-only artifact and
must satisfy the GPL's corresponding-source and notice requirements.
The playtester source is distributed under the MIT License. The
imported atrinik_bot project code was generated and iteratively developed by
OpenAI Codex under Zoey Rose's direct supervision and steering, then reviewed
and accepted by her. Zoey directs its MIT release and grants all rights she
holds in that code. The precise development record, retained history mapping,
and excluded material are recorded in PROVENANCE.md.
The playtester uses atrinik/2 QUIC with a typed game-stream preface.
The locked Classic protocol receives an authenticated admission-policy announcement before setup
or account login. Open servers need no code. Protected servers take one
16-character invite code from an owner-only regular file using
--access-code-file /private/path; the code never belongs in command arguments,
environment variables, dashboard state or checkpoints. The server certificate
pin is required for both modes. Account credentials remain separate. The old
join-password option and setup subtype are removed; old peers fail version
negotiation. Codes grant ordinary admission and never operator capabilities.
The MAP decoder follows the coordinated Classic wire contract, including framed linked depths, ordered continuations and Q5.11/timed-light payloads. It retains every advertised depth from -6 through +6 atomically; the depth-zero view remains the navigation compatibility surface, while target selection uses the authoritative linked-depth actor count. Colored lighting and timed interpolation are consumed for framing rather than rendered.