# Determinism

> Tick one box and an enemy fights the same fight every time: the same actions in the same order, from a seed you choose.

Markdown version of https://rigbak.com/docs/soulslike-combat/determinism, a page of the Soulslike Enemy Combat documentation. Link that page when citing it.

> Every roll an enemy makes is built from one number. Give it the same number and it makes the same choices, in the same order, every run.

## Set it up (1 minute)

1. **Tick Deterministic on the AI Config**

   Open the Enemy AI Config and find the **Determinism** category at the bottom. Tick **Deterministic** and **Default Seed** appears.

   **Category: Determinism**

   | Property | Value |
   | --- | --- |
   | Deterministic | true |
   | Default Seed | 1923 |
   | Start Barrier Seconds | 0.5 |

   Any seed works. Change it for a different but equally repeatable fight.
2. **Leave Default State Tree empty**

   Scroll up to **Behavior** and clear **Default State Tree**. An empty field runs the built-in brain.

   **Category: Behavior**

   | Property | Value |
   | --- | --- |
   | Default State Tree | None |
   | Decision Context Params | 5 members |
3. **Give the project a fixed frame rate**

   **Project Settings > Engine > General Settings > Framerate.** Tick **Use Fixed Frame Rate** and set **Fixed Frame Rate** to 60.

   In **Project Settings**, under Framerate, set:

   | Property | Value |
   | --- | --- |
   | Use Fixed Frame Rate | true (replaces false) |
   | Fixed Frame Rate | 60.0 (replaces 30.0) |

   *Movement and animation advance by frame time, so a varying frame rate moves enemies to different places.*
4. **Play it twice**

   Press Play, watch the opening of the fight, stop, and play again. The same enemies commit to the same attacks in the same order.

> **Warning**
>
> A State Tree cannot repeat. Unreal seeds a State Tree's randomness from the machine's clock and offers no way to hand it a seed, so transition delays and random state picks land differently every run. Drive a deterministic enemy with the built-in brain instead.

---

## What repeats, and what only nearly repeats

Every choice repeats: which action an enemy picks, how long its cooldowns run, which way it strafes, when it sets off on patrol, which spot in a wander area it walks to, which target it picks.

Where enemies end up standing repeats closely rather than exactly. Character movement, animation and pathfinding belong to Unreal, and they accumulate arithmetic error no seed reaches. Over a thirty second fight with nineteen enemies, positions drift by a fraction of a millimetre while every decision stays identical.

So a scripted boss, a tutorial fight, a QA repro or a puzzle encounter all work. A replay system that expects frame-exact positions does not, and no plugin above the engine can make it so.

---

## Seed one enemy, or a whole level

Three places supply the seed. The nearest one wins.

| Level | Where | Use it for |
| --- | --- | --- |
| **This enemy** | **Set Seed** on its SEC Combat Controller | Pinning one boss to a fixed script while the rest of the level varies |
| **This level** | **Set World Combat Seed** on the SEC Determinism subsystem | A roguelike run, where one number re-rolls the whole encounter |
| **This config** | **Default Seed** on the AI Config | The everyday case, set once and left alone |

Call **Set World Combat Seed** as a run begins and every deterministic enemy builds its choices on that number. Enemies already fighting restart their randomness on their next decision; enemies spawned later pick it up when they start. An enemy holding its own **Set Seed** ignores a level reseed and keeps running where it was.

Two enemies sharing one config still fight differently from each other, because each mixes the seed with a hash of its pawn's name. One config with one seed gives a room full of enemies distinct, repeatable behavior.

<details>
<summary>Handing a seed back (Advanced)</summary>

**Clear Seed** on the controller drops that enemy's own seed and hands it back to whichever seed the level or its config supplies. **Clear World Combat Seed** does the same at the level.

**Get Root Seed** reports the number an enemy's choices are built from, after all three levels have been weighed and the pawn's name mixed in. It reads 0 for an enemy that is not deterministic, which is a quick way to check a config took.

</details>

<details>
<summary>Renaming a placed enemy changes its behavior (Advanced)</summary>

The name hash is stable across runs: a placed enemy carries its name in the level, and a spawned one is named by a counter that starts fresh with the level.

Rename a placed enemy in the World Outliner and it draws different numbers from the same seed. Worth knowing if a fight you tuned plays differently after a tidy-up.

</details>

---

## The start barrier

A level takes a variable number of frames to settle after it loads. A brain that starts inside that window starts a few frames earlier or later each run, which is enough to send a repeatable fight down a different path.

**Start Barrier Seconds** holds every deterministic enemy's brain until the same moment on the world clock, so they all begin thinking on one frame. It ships at 0.5 seconds, counted from the start of play. Set it to 0 for an encounter that opens on a trigger, where the settling window is long past.

<details>
<summary>Picking the number (Advanced)</summary>

Too short and the level has not settled, so the fight still forks. Too long and enemies stand still at the start of play for no reason. Half a second suits a level that loads quickly; raise it if two runs part company in the first seconds, and check the result with the trace recorder rather than by eye.

The barrier applies to enemies present when play begins. An enemy spawned later starts as soon as it is possessed.

</details>

---

## Prove it repeats

The trace recorder writes down what every enemy did, frame by frame, and compares two runs for you. Its `SEC.Determinism` console commands are development tools, so a Shipping build does not have them.

## Record two runs and compare them (2 minutes)

1. **Turn the recorder on**

   In the console, `SEC.Determinism.AutoTrace 30`. Every level that starts from now records its first thirty seconds.
2. **Play twice**

   Press Play, wait for the recording to finish, stop. Do it again. The two runs land in `Saved/SECDeterminism` as `Run_0001.csv` and `Run_0002.csv`.
3. **Compare them**

   `SEC.Determinism.Compare` reads the two most recent runs. It either reports a match across every row, or names the first row where they disagree: the enemy, the frame, the value that diverged and by how much.

Each row holds one enemy on one frame: its seed, combat role, brain phase, running action, target and focus, awareness state, steering direction, velocity and position at full precision.

The column that diverges first tells you where to look. `Action` or `Role` first means a decision parted company. `PosX` first, with every decision column still matching, means the enemies chose identically and Unreal's movement put them a fraction apart.

<details>
<summary>Driving the recorder by hand (Advanced)</summary>

`SEC.Determinism.Trace 30` starts a recording at once, for a fight that opens partway through a level.

`SEC.Determinism.Compare 1 2` compares two runs by number. A third argument is a tolerance: `SEC.Determinism.Compare 1 2 0.01` treats numbers within a hundredth as equal, which reads past arithmetic noise to the first divergence that would show on screen.

**Start Trace**, **Stop Trace** and **Get Trace Run Index** sit on the SEC Determinism subsystem, for a Blueprint that records on a cue.

</details>

> **Note**
>
> Your own input has to match between runs. Move the player differently the second time and the traces part company for a reason that has nothing to do with the AI. Record a fight the enemies drive themselves, or stay off the controls.

---

## When two runs still differ

The trace recorder answers each of these.

| Check | What it looks like |
| --- | --- |
| **Deterministic is ticked** | `Get Root Seed` reads 0 on an enemy that should be seeded |
| **Default State Tree is empty** | The `Action` column parts company a second or two into the fight |
| **Fixed Frame Rate is on** | Two runs record different sample counts for the same duration |
| **Start Barrier is long enough** | The first divergence lands in the opening frames, before the fight is under way |
| **Nothing else drives the enemy** | A Blueprint calling `Set Seed`, or a random node in an ability graph |

<details>
<summary>Randomness the plugin does not own (Advanced)</summary>

A **Random Float in Range** node in an ability graph of your own rolls outside the seed. The plugin repeats what it rolls, and nothing else.

Unreal's perception system spreads its sight queries across frames against a wall-clock budget, so a level with many enemies watching one target can resolve who noticed first differently between runs. If the `Awareness` column is where two traces part, raise the sight budget in `DefaultEngine.ini`:

```ini
[/Script/AIModule.AISense_Sight]
MaxTracesPerTick=64
MaxAsyncTracesPerTick=64
MinQueriesPerTimeSliceCheck=1000
MaxTimeSlicePerTick=1.0
```

That lifts the caps past what your enemy count generates, so every query resolves every update. It costs perception time at high enemy counts.

</details>

<details>
<summary>Multiplayer</summary>

The AI runs on the server, so the server is the only machine that reads a seed. Nothing here replicates, and none of it is lockstep or rollback: two machines running the same seed are not kept in step with each other.

A multiplayer game gets a server that plays the same encounter the same way twice, which covers a QA repro and a scripted boss.

</details>

---

## Where to go next

- [Action System](https://rigbak.com/docs/soulslike-combat/action-system) for the scoring the seed feeds
- [Configuration Reference](https://rigbak.com/docs/soulslike-combat/configuration) for the rest of the AI Config
- [Territory](https://rigbak.com/docs/soulslike-combat/territory) for patrol and wander timings, which the seed also covers
