# The time model

> How Forge prices a route: kills, walks, flights, hand-ins, class profiles, riding, the quest log and determinism.

Page: https://polestar.guide/docs/contributors/forge/model (Polestar docs > Contributors > Polestar Forge)

This page explains how Forge prices a route: how long each kill, walk, flight, hearth and hand-in
takes, and so how many hours a character needs to reach 60. The route search ([How a route is made](https://polestar.guide/docs/contributors/forge/routes))
keeps an edit only when this model says it is faster. The judge measures guides in the same model
([The judge](https://polestar.guide/docs/contributors/forge/judge)). For the whole pipeline, start at [Polestar Forge](https://polestar.guide/docs/contributors/forge).

The code is in `rust/crates/forge-sim`: `eval.rs` (the evaluator), `run.rs` (the compiled character
and its tables), `travel.rs`, `params.rs`, `profile.rs` and `xp.rs`.

All hours on this page are **model hours**. They are good for comparing two orders of the same
quests. They are not a promise of real play time: most time parameters are still estimates, and only
quest XP is fitted so far.

## v1's evaluator

The model is v1's evaluator, ported to Rust step for step. With routed walks off and v1's parameters
it gives the same hours as the old JavaScript one, event by event (level within 5e-7, time within
1 ms). The test `tests/parity.rs` pins this. Keeping v1's model means a Forge route and a v1 guide
are always measured with the same ruler.

On top of v1's model, the route profiles turn on a few rules v1 did not have: walked escorts and
timed quests, bag items, along credit, class training stops and v1's travel plan. Each is described
below. Two more fidelity terms (camps and spots) exist but are off in the build.

Every route file reports the same two orders (v1 and the cut) in three models:

| Field | Model |
|---|---|
| `v1`, `improved` | the route's own: the profile, routed walks, the rules above |
| `v1Gate`, `improvedGate` | the gate's: v1's evaluator exactly (walks off, class factor 1, no training stops) |
| `v1Other`, `improvedOther` | the route's own with camps and spots switched (on if the search had them off) |

Check that a cut is faster than v1 in all three before you trust it.

## Steps

The evaluator plays an order one step at a time and keeps the time, level, XP, quest log and
counters.

| Step | What happens |
|---|---|
| accept | Needs the prerequisites and a level of at least the quest's minimum level − 2 and its level − 7. Travel to the giver. If the character is under the minimum level, grind to it; if more than 5 levels under the quest, grind to quest level − 5 (in the route profiles only when the quest has kill or escort work of its own: a quest done over time from what the character carries, The Green Hills of Stranglethorn's pages, is taken at once). A quest started by an item first works that item's task. Then talk. |
| task | One objective at its place. Skipped if earlier kills already covered it. Travel there, then do the work: kills, uses, an escort or a purchase. In the route profiles, kill work on mobs more than `workGap` (3) levels above the character is not solo-safe: the character first grinds to the mobs' level (at most the quest's) − 3, so the search moves such work to a later visit (A7, A8). |
| turn in | Needs every task done (and any quest that must be handed in first). Travel to the ender, talk, gain the quest XP. |
| level | Grind to a level (with a fraction: `level 12.4`), at the grind rate for the current level. |
| train | Travel to a town's class trainers and train. |

**Rare starts.** In the route profiles, a quest started by an item that only mobs drop, each at
under 5 % or an unknown chance (a rare mob's drop does not count; the writer writes these lines
optional), is never farmed for. When the route takes it, the chance the player has the item is
worked out from the kills the route has made so far of its source mobs: 1 − Π (1 − c)^kills, with
1 % per kill for a source of unknown chance. Its reward, and the reward of every quest of its chain
after it, counts at that chance. Its own work still costs its full time.

A step that cannot be done (prerequisite missing, level too low, log full, place unreachable, quest
not active) changes nothing. It only counts as a **no-op**. A kept edit may never add a no-op. After the
last step the character grinds out to 60.

## Costs

The parameters below are the priors in `params.rs`. `data/sim/params.json` replaces their means where
it has a fitted value (its `calibrated` list, for example `questEstScale` and `questXpScale`). `--param name=value`
overrides any of them for one run.

| Parameter | Prior | Used for |
|---|---|---|
| `runSpeed` | 7 yd/s | walking speed |
| `mountLevel`, `mountMul` | 40, 1.6 | from level 40, walking is 1.6 times faster |
| `detour0`, `detour1` | 1.35, 1.3 | straight line × detour where there is no routed walk (Eastern Kingdoms, Kalimdor), within the walk store's radius; beyond it the leg goes by the network (routes.md) |
| `routeMul` | 1 | × routed walk yards |
| `killSec1`, `killSec60` | 11 s, 30 s | time to kill an even-level mob at level 1 and level 60 |
| `killUp`, `killDown` | 0.1, 0.05 | per level the mob is above or below the character |
| `eliteTime` | 3.5 | × kill time for an elite |
| `classFactor` | 1 | × kill time; the profile sets it |
| `spacing` | 1 | × yards between mobs |
| `extraKill` | 0.043 | share of extra kills (pulls you did not need) |
| `dropMul` | 1 | ÷ kills needed for item drops |
| `talkSec` | 6 s | each accept, hand-in, purchase, flight path learned, bind |
| `interactSec` | 8 s | each object used |
| `escortSec` | 240 s | each escort or event |
| `flightOverhead` | 20 s | each flight |
| `flightSpeed` | 30 yd/s | taxi speed over the flight path's length |
| `transportWait` | 1 | × the average wait for a boat, zeppelin or tram |
| `hearthCast`, `hearthCooldown` | 10 s, 3600 s | the hearthstone |
| `hearthMargin` | 0.25 | route profiles: the next cast waits `hearthCooldown` × 1.25 |
| `levelOverhead` | 60 s | each level-up (vendor, repairs, training) |
| `trainSec` | 60 s | each training stop |
| `grindEff` | 0.7 | × the best grind rate |
| `gateGap`, `dangerGap` | 2, 5 | the accept window (above) |
| `workGap` | 3 | most levels kill work's mobs may be above the character (route profiles; above) |
| `deathRisk` | 300 s | each dangerous zone a walk crosses |
| `deathskips` | 0 (off) | deathskips priced (`--deathskips`; see Deathskips) |
| `deathskipSec`, `deathskipLevel`, `sicknessSlow` | 60 s, 10, 0.5 | a deathskip's death; the highest level it is taken at; the cost of each second of resurrection sickness |
| `questXpScale`, `questEstScale` | 1, 1 | × recorded quest XP; × estimated quest XP |

### Kills

The time for one kill at character level L on a mob of level M is:

$$t_{kill} = \Big(killSec1 + (killSec60 - killSec1)\tfrac{L-1}{59}\Big) \cdot f(M-L) \cdot classFactor + \frac{gap \cdot spacing}{runSpeed}$$

where f is 1 + 0.1 per level above, and 1 − 0.05 per level below (never under 0.5), and *gap* is the
yards between mobs at that site. An elite takes `eliteTime` times longer and gives twice the XP.

The kills needed are the objective's count × (1 + `extraKill`). For an item, the count is the items
over the drop chance per kill, ÷ `dropMul`. Kills of one mob also count for another open quest's
objective on the same mob within 100 yd (shared kill credit). An item a friendly vendor sells is
bought at the vendor nearest the giver when that costs less than the best drop (a sure drop plus
the walk against 100 ÷ chance plus the walk: Look to the Stars' Bronze Tube from Herble in Darkshire).

Kill XP is v1's formula: (5L + 45) for an even-level mob, +5 % per level above (up to 4), less below,
and nothing for gray mobs. The model levels up as XP comes in, so the kill time changes mid-camp.

### Grinding

The grind rate at level L is the best XP per second over mobs at L, L+1 and L+2, with the grind camps'
spacing at that level, × `grindEff`. A `level` step and the grind out to 60 use it. The route search
prices grinding at `grindWeight` (2.5) times its seconds ([How a route is made](https://polestar.guide/docs/contributors/forge/routes#2-local-edits-with-a-budget)),
and the hand-in of a group quest's root at `groupRisk` (600 s) per player it wants beyond the first
([How a route is made](https://polestar.guide/docs/contributors/forge/routes#group-quests)); reported hours stay real.

### Quest XP

A quest's full XP is its recorded reward × `questXpScale`. When the reward is not recorded, it is
estimated from the game's quest XP table by level and tier, × `questEstScale` × `questXpScale`.
Repeatable quests without a reward give nothing. At the hand-in the character gets a share by how many
levels it is over the quest: all of it up to 5 levels over, then 80, 60, 40 and 20 %, and 10 % from 10
levels over.

### Walking

A walk between two places is the routed walk from the walk store (`walks-<faction>.bin`, see
[How Forge walks the world](https://polestar.guide/docs/contributors/forge/nav)) × `routeMul` when it has one. Otherwise it is the straight line × the continent's
detour. Seconds are yards ÷ `runSpeed` (× `mountMul` once mounted). Across continents there is no walk.

A walk that crosses zones 5 or more levels above the character (not counting the two end zones) adds
`deathRisk` for each such zone.

**Tauren** walk faster on foot: their racial Plainsrunning (the game's data: spell 1259918 and its
speed aura 1299038) adds 1% run speed every 5 s of a walk, to +30% after 150 s. Each walk starts
without it (a stop resets it), so a walk that takes $t$ at `runSpeed` takes
$5k + \frac{t - d(k)}{1 + 0.01k}$, with $d(k) = 5(k + 0.005\,k(k-1))$ and $k$ the stacks reached
($k \le 30$). For example, 7 minutes become 5 min 41 s. It is on for Tauren starts' route profiles
(not the gate's v1 model), on foot only. `params::plainsrunning_sec`; test `plainsrunning`.

**Calibration** (Fitz's recorded trails on the local test copy, 2026-10-23, walk geometry and timing
only, never data):

- Run speed: `runSpeed` 7.0 stays. On the trails with no lift or portal, the 2D ground covered is
  0.99 of the client's own run speed (0.96-0.97 on ramp- and stair-heavy trails: Booty Bay,
  Deathknell). Stretches in druid travel form (9.8 yd/s, 140%) are left out.
- Routed walks: for the three trails walked end to end along the obvious way (Ironforge gate to the
  tram, Orgrimmar gate to the Valley of Honor, the Cleft of Shadow), the walking grid's yards are 1.006
  of the yards walked, so `routeMul` 1.0 stays.
- Lifts (11 rides: Thunder Bluff 4, Undercity 3, the Great Lift 2, Freewind Post 2): the recorded
  wait at the platform plus the time on the lift averages 25.4 s (Thunder Bluff), 13.6 s (Undercity),
  19.8 s (Great Lift) and 17.5 s (Freewind). The client-derived timetable gives 22, 13.7, 19.5 and
  19.5 s. All four together are within 2%, so the timetable is kept.

### Flights, boats and other transport

For a leg of 400 yd or more, the model also tries the transport network: walk to one of the 3 nearest
nodes, travel the network, walk from one of the 3 nodes nearest the end. It takes whichever is
cheaper. The network uses only flight paths the character knows: each flight costs its path's length
(travel.json `taxi.legs[].yards`) at `flightSpeed` (30 yd/s; checked 2026-10-24 against four flights a
character recorded in game, the addon's learned flight times from boarding to landing: Orgrimmar to
Thunder Bluff and back, the Crossroads to Orgrimmar, Ratchet to the Crossroads, 6745, 6213, 4252 and
2041 yd in 225, 207, 142 and 69 s, 29.9 yd/s overall, each within 1 s of the model; left out one at a
time, the others give 29.9-30.0) plus `flightOverhead` (an estimate: the talk, take-off and landing
are outside the recorded times). (v1 read the TaxiPath's `cost`, its price in copper, as seconds:
Orgrimmar to Thunder Bluff 50 s, Everlook to Orgrimmar 1020 s; forge-sim's JS-parity test keeps
the golden v1 world at 1 yd/s); a boat, zeppelin or tram costs its ride plus its wait × `transportWait`. Network
legs carry no danger.

Flight paths are learned when a step stops within 200 yd of a flight master (one talk). A training
town's flight master within 400 yd of its training stop is learned there too, with the walk there
and back (c15 Horde: Thunder Bluff's, 316 yd from the Elder Rise trainers, was never learned, and
every later Thunder Bluff leg rode through Mulgore). A leg by the network learns every flight master
it stands at on the way: its departure and arrival nodes, and the nodes it walks between (one talk
each): Stormwind's when the tram arrives, Gadgetzan's on the walk on from Freewind's flight to
Steamwheedle Port. The network is priced per set of known flight paths, so it changes as the
character learns them.

### Hearth and binds

When the hearthstone is ready and a leg costs more than 2 minutes, the model may hearth: `hearthCast`
plus the travel from the bound inn. It then waits `hearthCooldown` before the next.

- In the gate's model, the character binds at every inn it stops at, and hearths whenever that is
  more than 30 seconds faster.
- In the route profiles, the character follows **v1's travel plan**: it binds only at v1's `sethome`
  lines, hearths at v1's hearth lines (unless the hearth is more than 2 minutes worse than
  traveling) and wherever else the stone is ready and the hearth beats the leg by 3 minutes or
  more, and learns flight paths at v1's `getfp` lines too. A plan line is attached to the quest
  step right after it in v1's guide. A bind or flight path stays with its place, though: it is taken at
  that step only when the character stands within 200 yd of the inn (300 yd of the flight master);
  otherwise it waits for the next stop there. A moved step no longer binds at Ratchet from Shady Rest.

The model never hearths while the stone cools down. A v1 hearth line the route plays on cooldown
(counted as `planHearthsOnCooldown` at the step that plays it) is taken out of the travel plan before
the search: the route travels there, as it already did. A v1 hearth line the route meets within
300 yd of the bound inn (a hearth to where the character stands: c15 Alliance in Lakeshire, 213 yd
from its inn) goes to the step since the stone was last cast or bound that walks back to the inn's
ground from furthest (400 yd or more, the stone ready then: the Blackrock-camps return), else out of
the plan. Each start's plan is checked against its own
seed order, and for the shared route against its own part and the shared tail. A kept edit never adds
a line on cooldown, so the improved route reports none (F58). In the route profiles the stone's
cooldown runs on the model's time less grinding: a grind top-up is skipped by a player already at
its level (a shared guide's other starts: c16 Silverpine, three top-ups between two Sepulcher
hearths, 20 minutes apart for a Troll), and the grind before a quest taken far under its level is
never written (c16 Booty Bay, 24 minutes around the grind before Scaring Shaky). The route's
hearths are always at least `hearthCooldown` × (1 + `hearthMargin`) apart in that time: 75 minutes,
so the written guide's hearths stay an hour apart on `forge judge check`'s walk, which times straight
legs (on canon c4 its gaps were 52-55 minutes where the route's were 60).

### Deathskips

Off by default; `forge build --deathskips` (or `--param deathskips=1` on `forge sim`) turns them on
for the route profiles (never the gate's model). A leg may then **die and take the Spirit Healer**:
walk to a lethal spot near where it starts, die there, release, take the resurrection at the
graveyard the game sends that death to, and go on from its Spirit Healer. The ghost's run is skipped.

- **The graveyards** are the world's (`World::graveyards`, built by forge-world from the server project
  the server database's graveyard table): per faction, each graveyard whose links name a zone for the faction
  (team 0 or its own), with the Spirit Healer spawn within 100 yd of where its ghosts appear
  (npcs.json's `spirithealer` role; the graveyard's point is the healer's). Of the 88 linked
  graveyards on the two continents, 79 have one; the other 9 (Ironforge's, Stormwind's,
  Rut'theran Village's and the battlegrounds' exits) are left out. Forever's server core database
  was checked against it and is not used: 96 of its 159 linked graveyards there have no Forever
  Spirit Healer within 100 yd, 14 of them graveyards the server project places by one (its places are the later
  game's: New Auberdine, New Feathermoon, Splithoof Heights moved). Which graveyard a death goes to is
  the game's rule (`World::graveyard_for`): of the graveyards the death's zone links to the faction
  on that continent, the nearest (heights counted where both are known).
- **Lethal spots** are the kill camps of mobs (3 or more of npcs with a level: a lone named mob is no
  sure death) within `DEATH_SPOT_YD` (400 yd) of the leg's start whose mobs are at least the
  character's level (Run::set_deaths keeps per graveyard the nearest spot, then each farther one of
  higher mobs).
- **The price**: the walk to the spot (straight × detour), `deathskipSec` (60 s, an estimate: about
  30 s pulling mobs to die to and 30 s for the healer), the resurrection sickness's cost, and the
  travel on from the healer (walks, flights and boats as any leg). The cheapest is taken when it beats
  the leg (or the hearth) by `DEATHSKIP_GAIN_SEC` (30 s, v1's own margin for a hearth), never on
  a leg walked for an escort or a timed quest.
- **Resurrection sickness** follows Polestar's death helper (QoL/Death.lua, #390): none through
  level 10, then 600 s. A route deathskips only up to `deathskipLevel` (10: no sickness); raised,
  each second of sickness costs `sicknessSlow` (0.5) seconds.

The route counts them (`deathskips` in the hours summary, `deathskips` and `sickness_sec` in the
counters) and traces each (`deathskip` item: graveyard, spot, one of its mobs, healer, destination).
The writer writes it as a `die "<Graveyard>"` step at the healer, before the way there, with a tip
naming the mobs and spot to die at (a death elsewhere may go to another graveyard). The addon shows
it only to players who turned on "Use deathskips"; for everyone else it passes and the way after it
takes them there. On the Undead's road from Deathknell to Brill a death goes to Deathknell until past
halfway, so a deathskip there saves under a minute (tests/deathskips.rs).

### Training and level-ups

Each level-up costs `levelOverhead`. In the route profiles the training stops pay for training
themselves, so the per-level overhead drops to `levelOverhead` − `trainSec` / 2 (30 s): only vendoring
and repairs remain.

## Class profiles and the class-agnostic route

The route is planned for a class **profile**, not a class:

| Profile | `classFactor` | Who |
|---|---|---|
| `avg` | the mean over the faction's classes: 0.96875 (Horde), 0.98125 (Alliance) | the build |
| `slow` | 1.15 | the slowest killer (Warrior) |
| `fast` | 0.8 | the fastest killer (Hunter) |
| `gate` | 1 | v1's evaluator as the gate runs it |

The class factors are v1's priors: Warrior 1.15, Paladin 1.1, Priest 1.05, Rogue, Shaman and Druid 1.0,
Mage 0.9, Warlock 0.85, Hunter 0.8. The faction's classes are the eight it plays in Forever.

The other class parts are handled the same way for every class:

- **Class quests are out.** The character is compiled with no class, so quests open to only one to
  four classes are closed. Class chains live in their own Class guides.
- **Training is a stop.** A `train class` step at a town's trainers, where the route passes a town
  with a new class rank to teach ([How a route is made](https://polestar.guide/docs/contributors/forge/routes#training-stops)). A profession's `train`
  line (`train Cooking`) is not one.

The shared route is scored over all three of `slow`, `avg` and `fast`, so it does not favor one kind
of class ([How a route is made](https://polestar.guide/docs/contributors/forge/routes#8-the-shared-12-60-route-and-join-points)).

## Fidelity terms: camps and spots

These two terms are off in the build. Turn them on with `--param camps=1 --param spots=1`.

- **Camps.** Kill work is done at a hunting ground: the objective's camps within 250 yd of each other,
  pooled. Grounds of fewer than 3 mobs and rare camps (respawn over an hour) are left out while another
  exists. The kill time uses the ground's mob level and the yards between its mobs. The mobs that are up
  are killed at the kill rate; after that, one more each time one respawns. The model remembers what
  each ground has left. Without spots, the ground cheapest from the objective's place is used.
- **Spots.** A task goes to the ground that costs least from here, through its work, to where the
  order goes next (looking up to 4 steps ahead; else the quest's ender). This idea comes from
  the route-data project (MIT, used with Joel's permission); see `rust/NOTICE.md`.

With camps on, v1's orders cost about 14 model hours more (low-chance drops spread over many mobs cost
the most). Spots win back about one hour. The cuts gain about as much either way, and each model's cut
is also faster than v1 in the other model. That is why `v1Other` and `improvedOther` are reported.

## Along credit

Route profiles turn on **along credit**. A walked leg that passes within 150 yd of the place of an open
quest's unfinished kill objective (not elite, not the leg's own destination) kills 25 % of what that
objective needs on the way. It pays their kill time and gets their XP, plus the pull out of the line
beyond 60 yd. It only does this when the pull costs no more per kill than that camp's own spacing. The
work left at the objective's own step shrinks, and an objective finished this way needs no trip.

The 25 % share is a modeling estimate, not yet checked against recordings. The route file reports
`alongKills`.

## Pooled kills

Route profiles also **pool kills**. Players kill every quest mob they meet: on the Barrens plains the
plainstriders, zhevra and raptors of three open quests go in one pass. So a kill objective's work also
kills the mobs of every other open quest's unfinished kill objective whose site lies within 200 yd
(not elite, not a mob another quest shares). They are met in proportion to their density against the
work's own, (s_a / s_b)² with s the spacing between mobs (an unknown spacing counts as the work's),
up to what each needs. Each pooled kill pays its kill time and gets its XP. The walk is the pooled
one: all the kills at the spacing of the summed densities, less the walk the work already paid. An
objective finished this way needs no trip, as with along credit. The route file reports
`pooledKills`.

## Riding

v1's model rides from level 40. The route profiles ride only once riding is learned. v1's
`train <npc>` line of a riding trainer (its title says Riding: Kildar, Xar'Ti, Randal Hunter...) is a
**riding step** at the world point nearest the line's place. The seed puts it on the first stop
within 600 yd of the trainer at level 40 or above, else leaves it at v1's line. Below level 40 the
step does nothing. A start with no such line rides from 40, as before. The edit descent also offers
the riding step on the first visit to the trainer at 40 or above (edit kind `ride`). The route
file's order writes it as `{"t":"ride","npc":N}` with v1's line as its origin; in the shared route a
start's own trainer's step is its own (the canonical start's riding step does nothing for another
race).

## Quest log

The game's quest log holds 40 quests (or the world's own limit). When it is full, an accept gives up
an open quest at least 6 levels below the character. If there is none, the accept is a no-op.

The route keeps room for the class quests the writer adds: a kept edit never raises the log peak above
38, or above v1's own peak when v1 already goes higher. The route file reports `logPeak`, `logSkipped`
and `logAbandoned`.

## Timed quests and escorts

In the route profiles:

- while a timed quest runs, every leg is walked: no hearth, flight or boat. The model checks each
  hand-in against the quest's limit and counts late ones (`late`). A kept edit never adds one;
- an escort's own task is always walked from the giver;
- an item dropped by more than 20 kinds of mob (cloth for donations) is taken from the bags: no trip,
  no work.

These rules are off in the gate's model, so the gate stays equal to v1's model.

## Counters, parameter draws and the held-out gain

The priors are uncertain. A route that wins under one guess of the parameters might lose under
another. Forge checks this in two ways.

**Counters.** Almost every second the model spends is a count × a parameter: kills × kill time, walked
yards × detour ÷ speed, talks × `talkSec`, and so on. The evaluator keeps these counts
(`Counters` in `eval.rs`): kills by level weight (elite or not), kill gap yards, respawn waits, talks,
uses, escorts, trainings, level-ups, hearth casts, danger crossings, walked yards (routed or straight,
by continent, mounted or not), network seconds and grind XP by level. Priced under the route's own
parameters, the counters give its time exactly. Under another parameter set they give it as a dot
product, without a replay. The order's decisions and levels are held, so this is an estimate: over 8
draws it came within −4.1 % to +2.1 % of a full replay (`tests/counters.rs`).

**Parameter draws.** A draw samples the time parameters around their means (normal, cut to their
bounds; `SPREAD` in `params.rs`). `params.json`'s fitted mean and spread are used where it has them.
The class factor, levels and the policy parameters are not drawn.

**The held-out gain.** When you pick the best of many candidates by a noisy score, the winner looks
better than it is (the optimizer's curse). So every cut reports its gain over v1 on draws the search
never saw. These 16 **held-out draws** use one fixed seed, the same for every route. For each draw,
v1 and the cut are both replayed in full. The route file's `holdout` gives the mean, min and max gain,
the share of draws where the cut wins (`sharePositive`), the mean gain by counters alone and v1's mean
hours.

On recent cuts the held-out gain is 2.3 to 3.4 model hours, and the cut wins on every draw.

`--search-draws K` makes the search itself run under K draws (scored as their sum). `inSample` then
reports the gain on those draws, and `holdout` still reports the 16 others. `--holdout N` changes the
number of held-out draws.

## Determinism

The same inputs always give the same route, on any machine, with any number of threads, native or in
the browser.

- The evaluator does nothing random. The only random numbers are the parameter draws and the free
  search's moves, from a seeded SplitMix64 generator (`rng.rs`).
- Normal draws use the pure-Rust `libm`. The model uses no other transcendental function, so native
  and WebAssembly builds compute the same bits.
- The network's travel times are kept in 32-bit floats, as v1 did. The table for a set of known flight
  paths is always built from the same parent set, so a route's times never depend on what was priced
  before.
- The free search is counted in tries, not seconds. Its members meet every 250 tries, and ties go to
  the lowest member. The edit descent shares candidates across threads but always takes the same one.

`sim-hash` (a binary of forge-sim: `src/bin/sim-hash.rs`) prints a hash of a route's every step and
its hours, for v1's Orc order and after a short search. `tests/wasm/run.sh` (run from `rust/`) builds it
natively and for `wasm32-wasip1`, runs both (the wasm under node's WASI), and compares: on the goldens
with walks off, camps off and on; with `POLESTAR_SIMDIR` and `POLESTAR_SIM_JS` on a local sim dir and
seed, walks on too. The hashes are equal. The library also builds for the browser:

```sh
cargo check -p forge-sim --lib --no-default-features --target wasm32-unknown-unknown
```

(run from `rust/`, with that target installed).

The property tests in `tests/props.rs` check that replays are deterministic, that a saved state resumes
exactly, that time and level never go back, and that the search's windowed replay of every move kind
equals a replay from scratch.

## How to run and inspect it

Most of the time you read the model's answers in a route file ([How a route is made](https://polestar.guide/docs/contributors/forge/routes#9-what-a-cut-writes)).
To try another parameter or model on one start, rerun the cut with `forge sim route` and compare the
hours blocks:

```sh
forge sim route --faction Horde --start Orc --seed v1-horde-orc-on.json --edits 400 --guard current --param camps=1 --param spots=1 --out cuts-camps/Orc.json
forge sim route --faction Horde --start Orc --seed v1-horde-orc-on.json --edits 400 --guard current --profile slow --out cuts-slow/Orc.json
```

`--walks off` prices every walk as a straight line × detour. `--profile gate --walks off` is v1's
evaluator exactly.

`forge sim replay` (a developer command) replays a saved reference event stream and prints the
first event where the model differs from it.

## When it goes wrong

| Symptom | Likely cause | What to do |
|---|---|---|
| Hours jump after a data rebuild | the world or the walks changed | compare `dataHash` in the route files; rebuild the walks if the console warns they were built on another world |
| `v1` hours differ from the gate's | the route profile differs from v1's evaluator (walks, class factor, training stops, the rules above) | the gate measures v1's evaluator exactly: compare `v1Gate` |
| A route depends on the thread count or machine | a determinism bug | run `tests/wasm/run.sh` and `tests/props.rs`; look for a new float function or unordered sum |
| `holdout` gain much lower than the in-model gain | the cut relies on one parameter guess | look at `meanByCounters` and the min; search under draws (`--search-draws`) |
| A kill objective costs hours | a low drop chance spread over many mobs | check the item's drops in the data; try `--param camps=1` to see it at its camps |
| `late` above 0 in v1 | v1 itself hands a timed quest in late | not an error: kept edits only may not add more |

When you change the model, keep v1's model reachable: a new term gets a parameter that is 0 (off) in
the priors, and the gate's profile must keep its hours equal to v1's (`tests/parity.rs`). Add the
term's counts to `Counters` so draws price it, and check `tests/counters.rs` and `tests/props.rs`.

---

Next page: [How a route is made](https://polestar.guide/docs/contributors/forge/routes.md) · Every docs page: https://polestar.guide/llms.txt · All docs in one file: https://polestar.guide/llms-full.txt
