Skip to content

Repository files navigation

ozbot-re — a self-learning, demonstration-augmented Quake II bot on the 40 Hz re-release engine

ozbot-re is the q2repro (Quake II re-release / KEX-era, 40 Hz) port of ozbot. It lives entirely inside the game DLL (gamex86_64.dll, x64) — no engine changes — and keeps ozbot's core idea: the bot's code and design are authored entirely by an AI agent (Anthropic's Claude), in an autonomous build → simulate → analyze → improve loop, and its navigation is self-learned by playing, not hand-drawn.

What makes this port distinct is that it is hybrid. For a small number of trick traversals the self-learned follower physically cannot reproduce — the q2dm1 Megahealth strafe-jump, the upper-Railgun walkway, a chaingun-ledge descent — ozbot-re replays short human-recorded input clips ("playbooks") stitched into the nav graph as first-class links. And the shipped bot runs on human-curated nav snapshots — self-learned graphs matured through play and frozen per map (baselines/nav_shipped/, with targeted human curation) — a layer ozbot deliberately forbids: it stays strictly cold-only, because shipping a tuned nav would contradict its all-AI identity. So navigation here is learned-by-playing, curated, and demonstration-augmented; the AI still writes every line of code, and combat and goal selection remain fully self-tuned. That hybrid is the headline capability of the RE rig, and it unlocked the first item the 10 Hz bot could never reach.

What's different from ozbot

ozbot (original) ozbot-re (this repo)
Engine q2pro, 32-bit / x86 q2repro (re-release), 64-bit / x64 (gamex86_64.dll, PE 0x8664)
Tick rate 10 Hz 40 Hz (10 Hz brain, 40 Hz body)
Hermeticity every launch passes com_rerelease -1 (q2repro auto-detects Steam/GoG/OneDrive)
Nav baseline cold-only — self-learned, no shipped/curated graph (a hard identity requirement) ships a human-curated / matured nav per map (baselines/nav_shipped/), benched against a cold graph
Hand-authored content none human-recorded playbooks for a few trick moves
Fastsim exe q2proded_fast.exe q2reproded.exe (built from q2repro)
Gamedir engine/ozbot/ engine/ozbotre/

10 Hz brain, 40 Hz body. The vanilla game logic (weapons, animations, damage cadences) and the bot's combat decision layer run on FRAMESYNC keyframes (every 4th frame = 10 Hz); physics, steering, and movement execution run at the full 40 Hz tick. Per-tick bot dynamics are normalized with BOT_TICK_RATIO / Bot_TickGain (see src/bot.h) — never leave a raw per-tick constant, it will be 4× off at 40 Hz. sv_fps 40 is the standard; it is read once per InitGame, and without it (or on an engine lacking variable-FPS) everything runs vanilla 10 Hz and is bit-identical to the pre-port DLL — that byte-for-byte identity was the port's regression gate.

A new metrics epoch — do not compare to 10 Hz history. 40 Hz baseline (16 seeds × 90 s): ~418 pickups / 47% ITEM / 235 frags, versus the same build at 10 Hz: 488 / 53% / 157. The +50% kill intensity is emergent and provably symmetric — fire rates are unchanged (tools/verify_timing.py confirms all 8 weapon periods identical at 10 vs 40 Hz); frags-per-fire rises +67% because the smoother 40 Hz velocity tracks better between the 10 Hz aim samples. The lower ITEM% is death-interrupted attempts, not worse navigation (giveups/pathfails flat, standing re-decides ~1%).

The live cross-map scoreboard (all 8 q2dm maps, regenerated by tools/benchmark.py) lives in STATS.md; each snapshot runs the repro rig at a fixed seed against two nav baselines — shipped (the real bot on its hand-seeded navs) and cold (the same build learning from a scratch graph) — so rows isolate code changes and the gap shows what the manual nav curation is worth per map. Add one with py tools/benchmark.py --note "<what changed>".

Playbooks — the demonstration-augmented capability

Some q2dm1 items sit behind maneuvers the learned follower can't execute (a precise strafe-jump onto a 120u ledge, a one-tile-wide walkway). Playbooks solve exactly those, by replaying a human's recorded inputs:

  1. Recordrecord_inputs.bat on the q2repro client logs the human's per-usercmd stream at full 40 Hz fidelity (bot_inputlog). Bot segments can be captured with bot_cmdlog 1.
  2. Bakepy tools/make_playbook.py <log.jsonl> --slot N --start T0 --end T1 --name mh_jump --out engine/ozbotre/playbooks/q2dm1.pbk segments the capture, resamples the usercmd stream to the 25 ms tick grid (OR-ing momentary buttons so brief jump presses survive), and emits per entry an anchor (origin/yaw/velocity preconditions + tolerances), the tick-indexed usercmd stream, the expected origin timeline, and the exit point.
  3. Runbot_playback.c registers each entry as a NAV_LINK_PLAYBOOK link so A* routes through a recorded move like any other capability link (cost = duration). An approach controller drives the bot into the anchor tolerance, then does time-driven open-loop replay with a drift monitor against the recorded origin timeline; on divergence it aborts → Nav_PenalizeLink → normal repath (aborted replays self-select out). bot_playbook is default on and inert without a .pbk.

The canonical q2dm1.pbk carries 6 entries: mh_jump, rl_walkway, cg_descend, ya_box_jump, djump_rockets, bhop_rockets. Two hard-won executor lessons live in the bot_playback.c header: pure position-following is hopeless (the cursor must be time-driven with bounded slip — a position follower pins on the pre-launch tick and never jumps), and rail-matching needs a capped closed-loop yaw bias over the open-loop stream.

Results (port, executed 2026-07-04)

The port ran as a phased campaign, each phase gated:

  • R0 — engine. q2repro built x64 with -Dvariable-fps=true; fastsim patch ported tick-rate-aware (injects one sv_fps-sized tick per loop iteration). com_rerelease -1 required for hermeticity.
  • R1 — 10 Hz parity. x64 DLL through the game3 proxy: same-seed md5 bit-exact, and an 8-seed parity vs the q2pro rig (268/265 pickups, 55%/55% ITEM, 79/78 frags). run_parallel --repro is the rig switch.
  • R2 — variable FPS. GMF_VARIABLE_FPS + FRAMETIME/FRAMESYNC/ANIMTIME conversion, proven bit-invisible at sv_fps 10 and timing-identical (fire periods + respawn scheduler) at 10 vs 40 Hz.
  • R3 — bot at 40 Hz. Per-tick dynamics normalized; combat decisions FRAMESYNC-keyframed (per-tick decisions are worth +28–41% frags from fresher information). Established the new metrics epoch above. An A/B of also keyframing combat movement showed no lethality gain at a pickup cost → rejected; the 10 Hz-brain/40 Hz-body split is the shipped shape.
  • R4 — playbooks. Full pipeline shipped and validated with bot-recorded segments (57 engages → 26 completed replays over 8×180 s; failed entries self-penalize; no stuck bots). NAV_MAX_LINKS raised 8→12 so playbook links aren't dropped on saturated nodes. Reference: baselines/validation-q2dm1.pbk.
  • R4b — Megahealth jump baked. From 7 clean human takes: the q2dm1 route is a pure strafe-jump (launch pad (688,1168,792) → upper ledge → leap onto the mega). Result: item_health_mega 0 → 10 across 8×90 s — the first previously-impossible item (control 0; unreachable without the replay), ~85% climb success, headline metrics flat.

Since the port: rl_walkway unlocks the upper Railgun (0 → 5, no_pathok) — the first narrow-walkway win. The "stuck above the chaingun" stall was fixed in two parts — a Bot_LiftThink ascent-gate (lift-timeout −48%) plus a cg_descend rescue playbook with a per-entry dwell gate (stall −24%, giveups −7%). 40 Hz humanization polish landed: bot_aimsmooth (40 Hz view-glide toward the 10 Hz aim, teleports −75%, kills neutral) and bot_gazelife. bot_reroute shipped (giveup −0.5 pt).

q2dm3 nav curation (2026-07-15). Human input captures under engine/ozbotre/logs/q2dm3/ diagnosed a vertically fragmented graph (floor only reached z≤42; PLAT=0 vs ~65 on q2dm1/2). Surgical ONAV edits via tools/nav_edit.py — not playbooks — added a func_plat column and ladder1/ladder5 climb-mouth JUMP hops so A* routes into the ladder before the vertical hop (bot_ladder / bot_lift execute). The win is upper-item reachability, not headline ITEM%: shipped-solo now picks up Railgun (16× in the 16×90s bench; cold-solo still 0) and Combat Armor appears in dm, while Megahealth’s ladder2→ledge path and rj1 stay deferred as pure nav (climb stalls / no rocket-jump). A mh_ladder playbook from logs/q2dm3/dm3-mh.jsonl is shipped in playbooks/q2dm3.pbk — MH is graph-reachable and bots engage, but open-loop replay still drift-aborts on the strafe-into-ladder segment (needs a cleaner take or looser executor). Shipped pin baselines/nav_shipped/q2dm3.nav now diverges from cold — see STATS.md.

Asymmetric-harness note. In symmetric self-play, death is zero-sum, so raw "survive longer" behaviors can't show a net win. bot_survive (seek health + flee when low) was built and tested for exactly this, and proven counterproductive (deaths +11.8%) — it is default off. bot_survivetest is the id-parity A/B tool (even bots survive, odd control) kept for future asymmetric measurement.

Inherited from ozbot. All of ozbot's validated wins carry over on the same codebase — route-cost scoring, swim/lift/strafe-jump locomotion, decisiveness, target-leading, fight-or-flight, and the 6-behavior humanization stack. See ozbot's README for that full history and the documented negative results; they are not re-listed here because their numbers belong to the 10 Hz epoch.

Building and running

The RE engine is 64-bit — the DLL must be built x64 (MSVC via vcvarsall.bat x64; VS2022). Verify a built DLL's PE machine is 0x8664 if in doubt.

build.bat          :: compile src/*.c -> dist/gamex86_64.dll (x64)
deploy.bat         :: copy dist/gamex86_64.dll -> %Q2DIR%/ozbotre/
run_server.bat     :: build + deploy + launch a dedicated server with bots
play.bat           :: launch a q2repro listen server you can play IN / chase-cam the bots
record_inputs.bat  :: q2repro listen server with bot_inputlog on + a synced demo (capture YOUR inputs)
run_parallel.bat   :: build + deploy + N parallel headless sims + merged analysis (pass --repro)
build_engine.bat   :: build q2repro (x64, meson) -> %Q2DIR%/q2reproded.exe + q2repro.exe
build_gen_nav.bat  :: PyInstaller-freeze tools/gen_nav.py -> dist/gen_nav.exe (no-Python nav generator)

%Q2DIR% defaults to the in-repo engine (this folder's own runtime). The build is self-contained — the vanilla Quake II v3.19 headers live in src/ (quake2-source is a reference mirror, not a build input); the analysis tooling (pure-Python stdlib) is this repo's own tools/. Every launch passes com_rerelease -1 and +set sv_fps 40 (all scripts and run_parallel --repro do this for you).

Typical A/B run (16 seeds is the RE combat standard; always --repro):

run_parallel.bat --repro --fastsim --instances 16 --seconds 90 --bots 5 --seed 700 --cvar sv_fps 40

With --fastsim, --seconds counts simulated game seconds (each server self-quits via bot_quitafter), so every seed simulates the same game time regardless of CPU load. Fastsim is bit-exact vs real time.

Runtime knobs

RE-specific / changed knobs (defaults shown):

Cvar Default Meaning
bot_playbook 1 replay recorded input clips as NAV_LINK_PLAYBOOK links (inert without a .pbk); aborted replays self-penalize
bot_cmdlog 0 log bot usercmds in the input-log schema (for recording bot segments to bake)
bot_navlearn 1 runtime nav learning + autosave; 0 = play a fixed graph (a NAVHDR_FROZEN nav is frozen regardless)
bot_teleport 1 seed one-way NAV_LINK_TELEPORT links (misc_teleporter_dest) at map load; regenerated each load, never saved
bot_aimsmooth 1 40 Hz view-glide toward the 10 Hz aim target (anti-judder; teleports −75%)
bot_gazelife 1 glance around between fire windows (40 Hz humanization)
bot_aimflick 1 flick-speed cap multiplier (1 = stock 20–60°/tick)
bot_reroute 1 penalize the stalled hop on a pure-nav giveup, so the next route avoids it
bot_survive 0 survival instinct (seek health + flee when low) — counterproductive, kept off
bot_survivetest 0 diagnostic: id-parity survival A/B (even bots survive, odd control)
engine sv_fps 40 tick rate; read once at InitGamenever flip mid-session; 10 = vanilla, bit-identical to pre-port
engine com_rerelease −1 mandatory: keep q2repro hermetic (ignore auto-detected Steam/GoG/OneDrive installs)

The full shared knob set — bot_count, bot_skill, bot_pathcost, bot_goalbudget, bot_budgetcap, bot_itemfail, bot_swim, bot_lift, bot_strafejump, bot_claim, bot_decisive, bot_rollout, bot_lead, bot_flee, the bot_gaze/turnrate/aimtexture/fov/hop/fidget humanization stack, and the bot_*test id-parity diagnostics — is identical to ozbot; see ozbot's README for their descriptions.

Server console: sv bot_add N / sv bot_remove N / sv bot_clear. The learned graph saves to <gamedir>/nav/<map>.nav (autosaved ~30 s) in the ONAV v2 format — a uint32 header flags word after the node count (v1 files still load, treated as learnable); a graph with the NAVHDR_FROZEN bit is played as-is and never re-saved. Playbooks live in <gamedir>/playbooks/<map>.pbk; telemetry to <gamedir>/logs/<map>_<ts>.jsonl. To bootstrap a brand-new map, just run bots on it — the graph self-builds (bot_navlearn 1, always-on); playbooks are added per map as you record them. For surgical nav curation (add/remove links or seed climb mouths without bulk maturation), use py tools/nav_edit.py against an ONAV file — same workflow that produced the q2dm3 shipped pin (diagnose with traj_view / input_view on a human capture, then edit).

Generating / shipping navs

Runtime nav learning is always-on (bot_navlearn 1) and autosaves, so a brand-new map self-builds its graph on first server run. To ship a right-sized nav for your rotation, bake it frozen:

  1. Bakegen_nav.exe q2dm3 --out engine/ozbotre/nav/q2dm3.nav --report q2dm3.json. The standalone generator (no Python needed — build once with build_gen_nav.bat, which PyInstaller-freezes tools/gen_nav.py; needs py -m pip install pyinstaller) matures-to-peak: it grows the graph in cold checkpoints of increasing duration, probes solo item-collection (COLLECT%) at each with the graph frozen, and emits the PEAK graph stamped NAVHDR_FROZEN — the map-specific node-count sweet spot, not max coverage. It auto-detects the engine + DLL.
  2. Review the report — the JSON lists the peak (nodes, COLLECT%), the full checkpoint curve, and a gated-item list: reach-oracle items still no_path/gated — your to-do list for playbooks or surgical nav_edit.py links.
  3. Augment (optional) — record playbooks or add nav_edit.py links for the gated items; drop the .pbk at engine/ozbotre/playbooks/<map>.pbk.
  4. Ship — bundle the loose nav/<map>.nav (frozen, so a live server never over-grows it past its sweet spot) + optional playbooks/<map>.pbk next to the DLL. Navs are raw files (tens of KB/map), not pak/VFS. Unbaked maps simply self-build + autosave on first run.

Freeze/unfreeze any nav by hand with py tools/nav_edit.py freeze <file.nav> (--unfreeze); dump shows [FROZEN].

License

Based on the id Software Quake II v3.19 game source, licensed under the GNU General Public License v2. The bot code (src/bot_*) is released under the same license.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages