Essay 01

Reinforcement Learning for Runescape

Building a Fight Caves simulator, teaching an agent to finish it, and finding out what the reward forgot to say.

Jordan Bailey 27 min read

The Fight Caves viewer showing the player facing Jad in a lava-lined arena, with the minimap and equipment panel visible.
Figure 1. The playable Fight Caves viewer, built with the Raylib graphics library. Select a screenshot to open it at full size.

Before we enter the cave

Imagine giving a program a cave full of enemies, a few controls, and a score. You write down the rules of the world, but you want the program to choose its own moves. How does it learn a strategy?

We’ll build up the answer through my Fight Caves environment: first the world, then what the agent can observe and do, then the learning loop. A reward that accidentally pays for the wrong behavior will give us something useful to debug.

By the end, you should be able to:

  • Describe a reinforcement learning task in terms of observations, actions, and rewards.
  • Trace how simulated experience becomes a policy update.
  • Explain why a high score can hide a bad reward or an unreliable training recipe.

What you need: Basic programming and comfort reading simple equations. I’ll introduce the RuneScape and reinforcement learning terms as we need them.

I. A question with 63 waves

Can an agent learn an encounter without being handed the strategy?

Fight Caves has a wonderfully compact job description: survive 63 waves of enemies, defeat TzTok-Jad, and earn a fire cape. The implementation details are less forgiving. You need to choose targets, manage resources, move around obstacles, and change protection prayers before an attack becomes a very expensive mistake.

I wanted to know whether an agent could learn those decisions through reinforcement learning. No demonstrations. No scripted sequence telling it where to stand or when to switch prayers. Give it a world, a way to act, and feedback. Then see what it discovers.1

In reinforcement learning, the agent learns by interacting with an environment. Here, the environment is my simulator. Each tick, the agent receives an observation, chooses an action, and gets a reward after the world advances. Across many attempts, training adjusts its policy—the rule it uses to choose actions—to increase future reward. The reward tells it how an outcome was scored, not which move it should have made.

That question turned into three connected projects: a game engine fast enough to generate experience, a training system that could learn from it, and a viewer that let me inspect what either of them was doing. The viewer became especially useful when “learning to win” looked suspiciously like “learning to stand there.”

The agent I’ll follow throughout this article comes from one training run, which I’ll call the main run. It completed 9,570 of 10,800 full caves: 88.61%, with a Twisted bow and no food or Prayer potions. A later search over training settings produced an agent that completed 93.40%. Those numbers are encouraging. The interesting part is what had to become correct before they meant anything.

II. Build a world worth learning

The simulator defines the problem the agent actually solves.

Fight Caves is a combat encounter in Old School RuneScape. Its waves introduce enemies separately, then combine them. Protection prayers defend against different attack styles, but consume a limited resource. Positioning changes which enemies can reach you, whether they have line of sight, and whether you can attack them.

The simulated arena is a 64 × 64 tile grid with 15 possible spawn patterns, called rotations. Each game tick represents 0.6 seconds. Training advances those ticks as fast as the computer allows; it doesn’t wait for the clock on the wall.

The main threats, in the order the encounter introduces them.
First waveEnemyNew problem
1Tz-KihMelee pressure and Prayer drain.
3Tz-KekA large enemy that splits into two smaller enemies.
7Tok-XilRanged attacks, with melee at close range.
15Yt-MejKotHealing that can undo damage already dealt.
31Ket-ZekMagic attacks, contact melee, and a large footprint.
63TzTok-JadProtection timing, plus healers that restore his HP.

In the main run, the player starts with strong equipment: a Twisted bow, Dragon arrows, Masori armour, and 99 HP, Prayer, Defence, and Ranged. There are no sharks or Prayer-potion doses. Natural HP regeneration remains active. These conditions matter: better equipment shortens fights and changes how many mistakes the player can survive.

The viewer at wave 1 before the first action, showing the equipment, skills, and inventory panels.
Starting conditions. Equipment, skills, and inventory in the playable viewer at wave 1, tick 0, before the first action. This is the gear the main run’s agent trained with: the simulator’s strongest Twisted bow setup, with no sharks and no Prayer-potion doses.

One engine, several ways to use it

The project began as a prototype in Kotlin. I later rewrote the simulation in C, then wrote tools that read the game’s own data files to produce terrain, objects, collision maps, and visual assets. Raylib, a small graphics library, draws the viewer.

The useful architectural decision was to keep gameplay in one place. Drawing a projectile and resolving its damage are different jobs. If the viewer and trainer implement combat independently, a convincing replay can hide a different training environment. One Jad was quite enough; I didn’t need two implementations disagreeing about how to kill me.

The components and the responsibilities they keep.
ComponentResponsibility
C simulationOwn ticks, movement, collision, attacks, Prayer, healing, waves, observations, and reward events.
Training adapterExpose observations, legal-action masks, actions, rewards, and episode statistics to PufferLib, an open-source reinforcement learning library built for fast simulations.
PufferLib learnerCollect parallel experience and update the recurrent policy with PPO.
Raylib viewerSupport manual play, replays of a saved policy (a checkpoint), and visual debugging through the same simulation.
Validation and run recordsCheck that the same actions always produce the same game states, and keep the code, assets, settings, and weights behind each result.
One simulation, two ways to use itArchitecture of the Fight Caves project. A single C simulation owns every game rule. On the training side, it sends observations and legal-action masks to a recurrent policy, receives movement, target and Prayer actions, and passes experience to a rollout buffer; the PPO learner returns updated weights to the policy. On the inspection side, the Raylib viewer draws the same state and sends player or checkpoint actions through the same step call, and deterministic replay tests compare state hashes. The viewer adds no rules of its own.aTrainingPufferLib · 4,096 caves step in parallelbInspectionRaylib · one cave at a time, same rulesC simulationthe only implementation of the game rulesticks of 0.6 smovement, collisionline of sightattacks, hit delaysPrayer and drainhealing, Tz-Kek splits63 waves, 15 rotationsobservations, masksreward events64 × 64 tiles, 5 spawn points×4,096 copies in training×1 in the viewerRecurrent policy3 MinGRU layers × 512 · 2,541,056 parameters320 inputsmaskplayer · 8 enemies · wavehMinGRU × 3move 17target 9Prayer 8valuePPO learnerclipped objectiveV-trace, replayMuon optimizerRollout buffer256 ticks × 4,096 caves= 1,048,576 agent stepsper policy updateRaylib viewerdraws the simulation’s state; owns no rulesmanual playcheckpoint replay, on the CPUdebug overlays: collision, line of sight,routes, footprints, pending attacksDeterministic replay and testssame seed + same actions → same stateshashes expose divergencesaved test cases reproducea failure, tick for tickrun Arun Bobservation + legal-action maskfc_write_obs · fc_write_maskmove · target · Prayerfc_step(actions)experienceobs, actions, rewards, valuesupdated weights for the next rolloutentities + events, read-onlyfc_fill_render_*player or policy actionsfc_step(actions)seed + recorded actionsfc_reset · fc_stepstate hashesfc_state_hash
Figure 2. One simulation, two ways to use it. Training (a) and inspection (b) drive the same C simulation through the same calls. The viewer draws that state and adds no rules of its own. The small arena map in the diagram is the simulator’s actual 64 × 64 grid of walkable tiles, with its five spawn points.

Speed made iteration practical. Running 64 caves at once, the C simulation went from roughly 336,000 to 3.35 million steps per second as I optimized it.2

Correctness needed a different set of tools. Restarting from the same random seed and replaying the same actions makes a failure repeatable. Comparing fingerprints (hashes) of the game state shows the exact tick where two runs stop matching. Viewer overlays expose collision, line of sight, routes, footprints, and pending attacks. An agent willing to repeat a bad interaction millions of times is a remarkably persistent test case.

The Fight Caves debugging view, with colored tile overlays, enemy footprints, and line-of-sight checks.
Figure 3. The viewer’s debugging overlays. They help tell a policy mistake apart from a movement or combat rule that needs fixing.

III. Give the agent eyes and hands

What the policy can observe and choose is part of the experiment.

A policy maps observations to action probabilities. This one starts with randomly initialized weights. It doesn’t know that a large red creature on wave 63 deserves special attention. It doesn’t see a large red creature at all. Jad arrives as numbers, which is a fairly undignified entrance for a final boss.

A 320-number view of the cave

Each decision receives 320 floating-point values. Structured observations let me study combat decisions without first solving image recognition or mouse control.

What the policy receives each tick: 286 game-state values plus 34 legal-action flags.
BlockValuesExamples
Player23HP, Prayer, position, attack timer, target, and run energy.
Nearby enemies8 × 31Type, HP, distance, line of sight, attack style, healing, and protection deadlines.
Wave and progress15Wave, rotation, remaining enemies, required work, and time without progress.
Legal actions34Which movement, target, and Prayer choices are available.

The simulation can hold 16 enemies, but the policy sees the eight nearest. Slots are sorted by distance to each enemy’s footprint, then by spawn index. Empty slots contain zeros. A slot is a place in the current observation, not a permanent enemy identity, so the engine resolves target selections before movement can reorder the list.

Which timing and debugging values does the policy see?

Nine summary values about incoming hits are set to zero, while each enemy’s own attack timing remains available. The policy does not receive the 20 reward-breakdown values the simulator records for debugging.

Three decisions per tick

The network has three action heads: separate outputs for movement, target selection, and Prayer. It samples one choice from each.

Policy actions when the player has no food or potions.
HeadChoicesMeaning
Movement17Stay still, walk in eight directions, or run in eight directions.
Attack9Make no new selection, or select one of the eight observed enemies.
Prayer8Keep, switch off, select a protection, or flick one of three protections.

An action mask removes illegal choices before sampling. Those same legality bits are included in the observation.

Targets persist, so “no new selection” can still lead to an attack. A shot that fires consumes that tick’s movement opportunity. Prayer flicking is an explicit action: the engine handles the off/on transition, and the policy chooses when to request it. Eating, drinking, gear switching, and spell selection are outside this policy’s action space.

A deadline before the damage

Timing is where a seemingly small implementation choice changes the task. In this simulator, a non-melee Jad attack launched at tick T locks its protection decision at the boundary T + 2, before that tick’s action. The action at T + 1 can still change the protection used for the hit. Impact occurs at T + 3 or later.

Jad's protection deadlineTick-by-tick timeline of one non-melee Jad attack. Jad launches during tick T. The policy first observes the attack at T + 1, which is its only chance to change the protection used for the hit. The protection locks at the T + 2 boundary, before that tick’s action. Impact follows at T + 3 at the earliest and up to T + 5 for ranged attacks; the longer flight does not extend the response window.Tick boundarygame timeT0.0 sT + 10.6 sT + 21.2 sT + 31.8 sT + 42.4 sT + 53.0 sprotection locks before the T + 2 actionJadmagic or ranged attackObservationprayer-window flagPolicy actionPrayer head, once a tickProtectionused for this hitDamageresolved with the locklaunchstyle set, tell beginsin flightlonger flight possibleno pending attackopen: style + deadlineclosed: hit pending, choice no longer countsstyle not yet chosenlast response:set protectioncannot change this hittracks the active prayerlocked snapshotimpact at T + 3 or later (ranged: T + 5)actionable window: one decision (0.6 s)
Figure 4. Jad’s protection deadline for a magic or ranged attack. An attack launched during tick T first appears in the observation at T + 1. The protection used for the hit locks at the T + 2 boundary, before that tick’s action. Impact follows at T + 3 or later, and the extra flight time adds no response time.

IV. Turn experience into a policy

Try an action, observe the consequence, update, repeat.

Acting and learning are separate jobs

To act, the policy uses its current weights and recurrent state to produce action probabilities. To learn, the trainer uses collected experience to update those weights. Running an evaluation freezes the weights while the policy continues to choose actions and update its recurrent state.

One way to write the objective is a discounted return:

Gt=∑k=0H−1γkrt+k+1(1)

Here, r is reward, H is the number of remaining steps being considered, and γ discounts distant rewards. A reward contributes less to this sum the further away it is.3

Finishing the cave is the goal, but a learner that almost never reaches the end needs useful feedback along the way. The reward design will determine what counts as useful.

The learner uses Proximal Policy Optimization (PPO). A critic estimates future reward; an advantage estimate measures how an outcome compares with that expectation. The policy update encourages actions with positive estimated advantages and discourages those with negative ones. PPO clips its probability-ratio objective to reduce the incentive for overly large changes in a single update.

PufferLib also learns from each batch of experience more than once, and applies a correction called V-trace when the updated policy differs from the policy that collected it. An entropy term encourages action diversity. These tools help with learning; none substitutes for a sensible environment.

Memory and parallel caves

The policy has three recurrent layers of a lightweight type called MinGRU, each 512 units wide, with 2,541,056 parameters in total. Recurrence gives the network a memory to carry between decisions. The actor produces action probabilities, the critic predicts value, and an optimizer called Muon applies the updates to the parameters.

Training collects 256 steps from each of 4,096 agents per rollout, a batch of experience: 1,048,576 agent steps. Two buffers and 16 environment threads keep the pipeline supplied. A requested 500 million steps becomes 476 complete rollouts, or 499,122,176 steps.4

Learning loop · Python-style pseudocode
# Conceptual flow, not the PufferLib API.
for rollout in training:
    memory = zero_state(num_agents)
    for tick in range(256):
        obs, masks = caves.observe()
        logits, value, memory = policy(obs, memory)
        actions = sample_masked(logits, masks)
        rewards, done = caves.step(actions)
        rollout.store(obs, actions, rewards, done, value)
        memory[done] = 0
    learner.update(rollout)

The sketch shows how the main run trained: the policy’s memory resets at the start of every 256-step rollout and whenever an episode ends. Final evaluation keeps the memory across rollouts and resets it only between episodes. That difference belongs in a run’s records; changing it silently changes the experiment.

The main run’s training settings

These are some of the main run’s settings (random seed 73). This excerpt is for reference; the reproduction steps at the end of the article give every setting you need.

Selected configuration values
learning_rate = 0.00123960734
momentum = 0.991584122
ent_coef = 0.00026363763
replay_ratio = 2.20972681
clip_coef = 0.0250000004
gamma = 0.9991261141073255
gae_lambda = 0.9

Training was synchronous (collecting experience and updating the policy took turns), with 32,768-step minibatches and a memory reset every rollout. The trainer’s source code shows how these settings are used.

V. Reward progress that stays made

The agent optimizes the feedback it receives, including the parts I got wrong.

Survival can be a dead end

An early run of 500 million steps reached mean wave 27.6 and completed zero caves in 10,174 episodes. Later, I tried a 200,000-tick time limit per episode with a budget of 2 billion steps. Mean episode length reached 199,939 ticks. Mean wave stayed near 30. There were still no completions.

I had made room for an exceptionally long unsuccessful cave. The extra survival time was not becoming progress.

Rewarding protection introduced another problem. In one failed run, the correct-prayer component contributed 86.87 reward per episode, while progress contributed 2.92 before total-reward clipping. The policy spent about 83% of its time without a target. I was paying it well to defend a fight it wasn’t finishing. The policy had found a steady job. I had meant to offer a quest.

Later, I trained two otherwise identical runs for 750 million steps each: one kept the remaining reward for using the correct protection prayer against dangerous attacks, and one removed it. Removing it improved completion from 88.02% to 92.74%: the extra payment wasn’t helping.

Count the work still required

The reward design I kept gives dense feedback for reducing the work left in the cave. “Dense” means feedback arrives along the way, rather than only at the final kill.

Gross damage counts each hit as another accomplishment. Remaining-work accounting also records when that accomplishment has been undone. If damage and healing cancel, the progress component should cancel too.

Let W be remaining work and S be work at the start of the wave, measured in the engine’s internal tenths of HP. Progress through the wave is q; overall cave progress is C:

q=clamp(1−WS,0,1)(2)
C=wave−1+q63(3)

The progress component is 0.001 × ΔC × 63 × S. Within a wave, that works out to 0.001 times the net work removed. When a new wave starts, the comparison starts over, so the newly spawned enemies are not charged as negative progress.

Required work includes the children a large Tz-Kek will spawn. Splitting therefore doesn’t create surprise work. On the final wave, it tracks Jad’s HP. Killing a healer is not required to finish the cave, but healing Jad increases the work remaining.

reward_example.py · A within-wave illustration
def progress_reward(before_hp, after_hp):
    # Ten internal units equal one game HP.
    work_removed = (before_hp - after_hp) * 10
    return 0.001 * work_removed

damage = progress_reward(100, 90)
healing = progress_reward(90, 100) - 0.005
print(f"{damage:+.3f}, {healing:+.3f}, {damage + healing:+.3f}")
+0.100, -0.105, -0.005

Damage and healing cancel in the progress reward, and each healing event adds its own small cost. The example leaves out the other reward terms.

Damage, healing, and work remainingWorked example of the progress reward. An enemy drops from 100 to 90 HP and is healed back to 100 HP: the hit earns +0.100 progress, the heal costs −0.100 progress plus a −0.005 healing-event cost, so the cycle nets −0.005. Paying gross damage instead would earn +0.100 for every repeat. Smaller panels show that a Tz-Kek’s two children are counted as work in advance, so its split adds none, and that Jad’s healers matter only through the Jad HP they restore.aOne damage–heal cycle on one enemy050100enemy HPstart100 HPW = 1,000after a 10 HP hit90 HPW = 900after a 10 HP heal100 HPW = 1,000removedrestored+0.100 progress100 units removed−0.100 progress−0.005 heal event100 units restoredbReward for the cycle, by component+0.10+0.050+0.100−0.100−0.005−0.005hitprogresshealprogresshealeventnetcycle+0.100grossdamageper repeatcTz-Kek: its children are counted in advancespawnTz-Kek 201010W = 40 HPparent at 0 HP1010W = 20 HPafter the split1010W = 20 HP20 HP removed: +0.200 progresssplit adds no work: W unchangedlive HPchild not yet spawned, already counteddJad: healers count only through his HPJad at 140 HPW = 140 HPa healer healsW = 145 HPa healer diesW = 145 HP250+5 HP restored: −0.050 progress, −0.005 heal eventHealer HP is not required work; killing one changes W by 0.required workReward weights: progress 0.001 per work unit (1 HP = 10 units);each enemy heal event −0.005, including healer heals on Jad. Components are shown before the total is clipped to [−1, +1].
Figure 5. Damage, healing, and work remaining. (a, b) In one damage–heal cycle the progress components cancel and the healing event costs −0.005, so repeating the cycle never pays. (c) A Tz-Kek’s two children are counted as work when it spawns, so splitting adds none. (d) On wave 63, required work is Jad’s HP alone; healers matter through the HP they restore.

Other terms penalize damage taken, Prayer spent, time, invalid actions, and sustained inactivity. Completion earns +1 and death earns −1. The trainer clips the combined reward to [−1, +1]. Gross damage, individual kills, wave clears, and correct prayers earn no direct reward in the main run.

Check the rules before tuning the penalty

Some stalls were simulation problems. Some enemies stopped chasing when they were technically in attack range but couldn’t see the player or reach them for a melee hit. Fixing that movement bug, with everything else unchanged, raised the mean wave from 40.82 to 49.17. No caves were completed yet, but the fight was less broken.

Accuracy corrections can lower the score, too. Adding a missing Magic attack bonus to Ket-Zek and Jad lowered an otherwise identical run from 92.74% to 91.49% completion. I would rather measure the harder, more accurate task than preserve a nicer number by leaving a combat rule wrong.

VI. What the agent learned

A strong trained agent and a dependable training recipe are different results.

The main run used PufferLib 5.0’s native trainer, which is written in C and CUDA. Its settings came from a hyperparameter search: 90 training runs, each trying different training settings, of which this one scored highest. It trained for 499,122,176 steps in 434.08 seconds on one NVIDIA RTX 5070 Ti graphics card, not counting the final evaluation. That is the cost of this run. It does not include building the simulator or searching for the recipe. The GPU got the short assignment.

The training log records one average for each quarter of training, plus a final evaluation. In the first two quarters, the agent progresses through ordinary waves. In the third, it begins reaching Jad but almost never finishes. By the fourth, it reaches him in most episodes, and completion has increased sharply.

Learning to reach Jad, then finishThree aligned panels against agent steps for the main training run: mean wave reached, mean episode length in ticks, and the share of episodes reaching Jad and completing the cave. Each point is the average over one quarter of training, and its horizontal bar spans that quarter. A separate diamond at 499.12 million steps marks the final evaluation of 10,800 caves: 89.25% reached Jad and 88.61% completed.average over one quarter of training; bar spans that quarterfinal evaluation, 10,800 cavesaMean wave reached0204060bMean episode length (ticks)02,0004,0006,000cEpisodes that reach Jad and that complete the cave0%25%50%75%100%0100M200M300M400MAgent steps499.12Mwave 63: Jad62.34,931≈ 49 min game time89.25% reached Jad88.61% completed14.6% reach Jad0.2% complete86.2%63.5%
Figure 6. Learning to reach Jad, then finish. Each point is the average over one quarter of training, and its horizontal bar spans that quarter. Diamonds mark the final evaluation over 10,800 caves: 89.25% reached Jad and 88.61% completed.

Start at wave 1 and count outcomes

Final evaluation reloads the checkpoint and samples actions from the masked policy. It starts full caves at wave 1, keeps the policy’s memory across rollouts, and resets it between episodes. Evaluation plays caves in large batches: it asked for at least 10,000 episodes and finished with 10,800.

Final evaluation of the main run’s agent.
OutcomeEpisodesShare
Failed before Jad≈1,16110.75%
Reached Jad, then failed≈690.64%
Completed the cave9,57088.61%

The policy reached Jad in 89.25% of episodes. Conditional on reaching him, it finished about 99.28% of the time. Most of this agent’s failures happened earlier. That directs the next investigation toward the preceding waves, rather than assuming the final boss is the main problem.5

Mean episode length was 4,930.82 ticks: about 49.31 minutes of simulated game time, averaged over successes and failures. Accelerated simulation makes it possible to collect that experience without waiting 49 minutes per cave.

Frames from a replay of the main run’s agent on wave 63 showing one Jad ranged attack: the attack becomes visible, the policy switches to Protect from Missiles, and the hit lands for 0.
One Jad attack, decided at T + 1. Frames from the main run’s agent on wave 63. At T + 1 the ranged attack is visible and the observation marks its deadline. The policy’s answer, Protect from Missiles, locks at T + 2. At T + 5 the hit lands for 0 although the policy has flicked back to Protect from Magic. The strip below the frames comes from the simulator’s tick-by-tick log.

Train again with a different seed

A random seed is the number that fixes a run’s random choices. Evaluating one checkpoint across many caves tests those weights. Training again from scratch tests the recipe: a new seed changes the starting weights and the experience the agent meets while learning.

The main run’s settings, trained from scratch with four random seeds. Seed 73 is the main run itself, and the seed the hyperparameter search used to choose these settings.
Training seedEvaluated cavesCompletion
73 · the main run10,80088.61%
10110,16538.51%
20210,53174.56%
30310,84081.07%

Hyperparameter searches helped substantially, but the successful settings act together. Learning rate, entropy, clipping, replay, and advantage estimation all affect the updates. A hyperparameter search can identify a useful combination without proving which individual change caused the improvement.

VII. Beyond the Fight Caves

Reuse the engineering; test whether the learning transfers.

I want to take this approach to more RuneScape encounters and content. Fight Caves is a useful starting point because it combines a long sequence of decisions with a clear terminal objective. Other encounters can test different kinds of planning, resource use, and coordination.

The next step within this environment is to understand the seeds that trained poorly. I want to compare their HP and Prayer when they reach Jad, their protection timing, their target choices, and how they handle healers. I also want to test how much the policy depends on exact timing features, recurrence, and fixed equipment.

Possible next experiments.
DirectionWhat carries overNew work
Another wave encounter, such as the InfernoTicks, collision, line of sight, parallel simulation, and wave metrics.Enemy mechanics, hazards, objectives, and sufficient observation capacity.
Bosses that require gear or combat-style changesCombat state, viewer controls, and replay diagnostics.Switching actions, inventory observations, and action costs or delays.
Longer or cooperative contentDeterministic simulation and the experiment pipeline.Persistent resources, stage goals, other players’ state, and coordination.

Reusing a simulator framework saves engineering time. Reusing learned weights is a separate claim. A useful first transfer experiment would implement one bounded encounter, then compare training from scratch with a Fight Caves initialization under the same budget and across several seeds.

That is the curiosity I want to keep following: which decisions become reusable skills, and which depend on the particular world I built? For now, I can load a checkpoint, start at wave 1, and watch it finish the cave repeatedly. Getting there required learning how to ask the question as much as learning how to answer it.

VIII. Code and experiment notes

The results in this article come from one version of the code: commit 9ad3633d of my fork of PufferLib, which adds Fight Caves to PufferLib 5.0. The main run and the 93.40% run were both trained with it. The earlier experiments described along the way ran on older versions, and the project has kept changing since, so newer code may not reproduce these numbers.

  1. My PufferLib fork at that commit (branch fight-caves-5.0): the code behind these results. Start with its Fight Caves guide, or go straight to the simulation and policy interface, the training adapter, and the network and optimizer.
  2. Fight Caves RL: the project’s original repository, built on PufferLib 4.0, with the viewer, earlier experiments, and implementation notes. It describes an older version of the project, trained under different conditions.
  3. Training dashboards on Weights & Biases, an experiment-tracking site: the main run and the retrained 93.40% run. Some details may require access.
  4. Proximal Policy Optimization Algorithms, Schulman et al. (2017), for the clipped policy objective.
  5. Results data: the main run’s metrics, the four-seed retraining results, and the record of the exact 93.40% retraining, in one JSON file.

Reproduce the results

These steps train the main run again from scratch and then evaluate it. Trained checkpoints aren’t published, so training is how you get the agent.

What you need: Linux and an NVIDIA graphics card; the trainer does not run without one. The build also needs NVIDIA’s CUDA toolkit, NCCL, the Clang compiler with OpenMP, ccache, and Python 3. PufferTank 5.0, PufferLib’s ready-made Docker environment, includes all of them.

  1. Download the code. The “5.0” in the branch name, fight-caves-5.0, is the PufferLib version Fight Caves is integrated with. The branch holds the exact code behind these results, commit 9ad3633d, with the main run’s settings saved in config/fight_caves.ini.

    Terminal
    git clone --depth 1 --branch fight-caves-5.0 \
      https://github.com/jordanbailey00/PufferLib.git fight-caves
    cd fight-caves
  2. Build. This downloads the game’s maps and graphics, then compiles everything into one program, ./puffer.

    Terminal
    ./build.sh fight_caves
  3. Train and evaluate. Training stops after 499,122,176 steps, then evaluates the agent on at least 10,000 caves. In the final summary, jad_kill_rate is the share of caves completed: 0.886 means 88.6%. The full results are saved in logs/fight_caves/.

    Terminal
    ./puffer train
  4. Watch it play. This opens the viewer with the agent you just trained, so it needs a display. Press 1, 2, 4, or 0 for 1×, 2×, 4×, or 10× speed, and Q to quit. If the viewer is slow inside PufferTank, run unset __GLX_VENDOR_LIBRARY_NAME first.

    Terminal
    ./puffer eval latest

To train the 93.40% run instead, use fight-caves-best-run in step 1. Everything else is the same.

Keeping a result reproducible

A result needs more than a settings file. Keep the simulation’s source code, the compiled program, the game assets, the starting gear, the training settings, the random seed, and the checkpoint together.

To record a replay you can compare later, also note the random seed, whether actions were sampled or always the most likely choice, how the policy’s memory was handled, which ticks you captured, and the playback speed. A replay on the CPU and one on a graphics card can pick different actions from the same weights, because their arithmetic differs slightly.