codex: rate_model lifecycle derivation + executable HLA tier selection (bounce-fix attempt 5, owner guidance 10331, issue #5)
This commit is contained in:
parent
34f36d2275
commit
589767b195
@ -3,32 +3,39 @@ title: Low-Level Alchemy (F2P)
|
||||
domain: economy
|
||||
tags: [alching, magic-training, f2p, loss-training]
|
||||
status: verified
|
||||
version: 2 # 2: bounce-fix 3 2026-08-28 (owner guidance) — realized rates incl. mandatory breaks + banking, break.every in CAST counts, numeric carried_batch/restock_threshold, pinned worked example
|
||||
version: 3 # 3: bounce-fix 4 2026-08-28 (owner guidance 10331, PR #10) — explicit rate_model (session-realized rates incl. startup over a 2h horizon: xp 23,000, gp -109,500..-65,600), complete GE acquisition path (open -> fill -> collect) for the initial buy AND every restock (bank-stock fast path), fail-closed staff equip, monotonic break example (550/1,100/1,650 independent of 901-cast restocks), assets-based capital stops
|
||||
updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
|
||||
|
||||
summary: >
|
||||
Magic training from level 21 by low-alching F2P items with the highest
|
||||
lowalch-value among affordable, high-limit items. At current prices it is
|
||||
a LOSS activity (money in exchange for Magic XP); run it only for XP.
|
||||
Model pin (owner guidance 2026-08-28): one iteration = one cast; breaks
|
||||
Model pins (owner guidance 2026-08-28): one iteration = one cast; breaks
|
||||
every 400-700 COMPLETED CASTS (cast-count based, independent of bank
|
||||
visits); restock when candidate or nature drops below the numeric
|
||||
restock_threshold (100) against a carried_batch of 1,000/1,000 — worked
|
||||
example pinned below; all rates are REALIZED (delays + breaks + banking).
|
||||
visits — midpoint example 550, 1,100, 1,650, ...); restock when candidate
|
||||
or nature drops below the numeric restock_threshold (100) against a
|
||||
carried_batch of 1,000/1,000 (restock cadence 901 casts, monotonic and
|
||||
independent of breaks). EVERY purchase follows the complete GE path open
|
||||
-> place -> fill/wait -> collect, for the initial acquisition and for
|
||||
every restock (a bank-stock fast path applies only when the bank actually
|
||||
holds enough stock). Staff-of-fire equip failure is FAIL-CLOSED (bounded
|
||||
retries, then log ERROR + alert + stop — no fire-rune fallback is
|
||||
modeled). Session horizon 2 h; all rates are SESSION-REALIZED under the
|
||||
declared rate_model (startup + restock + breaks included; see economy).
|
||||
|
||||
accounts:
|
||||
membership: f2p
|
||||
min_level: 21 # Magic 21 to cast Low Level Alchemy (wiki-verified)
|
||||
requires_quest: []
|
||||
requires_other:
|
||||
- "Staff of Fire equipped (removes 3 fire runes from the cost; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources)"
|
||||
- "A bank with GE access within reach (alch spot = the bank booth; restock trip 60-120s)"
|
||||
- "Working capital >= 292,000 gp for one carried_batch (1,000 x 148 gp gold necklaces + 1,000 x 144 gp natures, live 2026-08-27)"
|
||||
- "Staff of Fire equipped (removes 3 fire runes from the cost; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources). Equip failure is FAIL-CLOSED: bounded retries (3), then log ERROR + alert + stop — there is no fire-rune fallback in this model"
|
||||
- "A bank with GE access within reach (alch spot = the bank booth; restock trip [60, 120] s)"
|
||||
- "Starting capital >= ~1.30M gp CASH (staff hard cap 5,000 + first batch 1,000/1,000 at MAX allowed fill 306,600 + session loss reserve 684,400 = 6 worst-case 901-cast batches at live-low+5% fills (covers Magic 21 -> 54) + resting-offer float 306,600). The capital stop checks TOTAL ASSETS (cash + supplies at current buy cost), never raw cash — cash legitimately converts into supplies mid-run"
|
||||
|
||||
best_practice:
|
||||
equipment:
|
||||
- item: staff of fire
|
||||
why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved); equip requirement: none documented on the OSRS wiki (verified 2026-08-28)"
|
||||
why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved); equip requirement: none documented on the OSRS wiki (verified 2026-08-28); if it cannot be equipped after bounded retries the run FAILS CLOSED (no fallback modeled)"
|
||||
source: "OSRS wiki — Staff of fire (id 1387: no requirements row in the infobox, none in the prose — contrast Battlestaff 'requires 30 Attack and 30 Magic'); Low Level Alchemy (Cost)"
|
||||
techniques:
|
||||
- name: cast on noted (stacked) items
|
||||
@ -41,31 +48,40 @@ best_practice:
|
||||
economy:
|
||||
# Verified 2026-08-27 against prices.runescape.wiki (live 'latest')
|
||||
# + OSRS wiki Low_Level_Alchemy infobox.
|
||||
# Bounce-fix 3 (2026-08-28, owner guidance): all rate fields are REALIZED
|
||||
# figures — mandatory breaks + banking included, not just the cast loop.
|
||||
# Bounce-fix 4 (2026-08-28, owner guidance 10331): all rate fields are
|
||||
# SESSION-REALIZED — startup + restock + mandatory breaks included over
|
||||
# the declared 2 h session horizon (rate_model).
|
||||
item: Gold necklace # example anchor; rank by cheapest candidate live
|
||||
item_id: 1654
|
||||
cost_per_cycle: 292 # necklace buy 148 + nature rune 144 (staff of fire)
|
||||
cost_per_cycle: 292 # necklace buy 148 + nature rune 144 (staff of fire, live low)
|
||||
income_per_cycle: 180 # lowalch of gold necklace (40% of value 450)
|
||||
net_per_cycle: -112
|
||||
gp_per_hour_low: -110500 # realized (incl. restock + breaks), fastest cycle: 986.4 casts/hr x -112 = -110,477
|
||||
gp_per_hour_high: -74000 # realized (incl. restock + breaks), slowest cycle: 660.6 casts/hr x -112 = -73,987
|
||||
xp_per_hour: 24500 # realized midpoint: 791.3 casts/hr x 31 xp = 24,530 (pin 24,500)
|
||||
# ---- realized-rate derivation (pinned; owner guidance 2026-08-28) ----
|
||||
# Cast: 1.8s (3 ticks x 0.6s) + anti_ban.delay [1.6, 3.4]s ADDITIVE after the
|
||||
# action (docs/action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s.
|
||||
gp_per_hour_low: -109500 # session-realized fast bound: 977.1 casts/hr x -112 = -109,435 (3.4 s cycle, 300 s startup, 2 h horizon)
|
||||
gp_per_hour_high: -65600 # session-realized slow bound: 585.9 casts/hr x -112 = -65,621 (5.2 s cycle, 600 s startup, 2 h horizon)
|
||||
xp_per_hour: 23000 # session-realized midpoint: 741.8 casts/hr x 31 = 22,995 (pin 23,000)
|
||||
# ---- rate model (derived; enforced by tools/validate.py rate_model_errors) ----
|
||||
# Cast: 1.8s (3 ticks x 0.6s, rate_model.cast_ticks) + anti_ban.delay
|
||||
# [1.6, 3.4]s ADDITIVE (action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s.
|
||||
# BATCH model: carried_batch 1,000 candidate + 1,000 natures; restock_threshold
|
||||
# 100 -> 901-cast restock cadence (first restock after cast 901). Overhead per
|
||||
# 901-cast batch: 90s restock trip (60-120s, GE-adjacent booth) + 135s breaks
|
||||
# (every [400, 700] casts, mid 550, for [45, 120]s -> 901/550 x 82.5s) = 225s.
|
||||
# REALIZED casts/hr = 901 / ((901 x cycle_s + 225) / 3600):
|
||||
# fast 3.4s -> 986.4; midpoint 4.3s -> 791.3; slow 5.2s -> 660.6.
|
||||
# xp_per_hour = 791.3 x 31 = 24,530 (pin 24,500).
|
||||
# gp_per_hour = casts/hr x -112: fast -110,477 (low), slow -73,987 (high),
|
||||
# midpoint -88,626.
|
||||
# Superseded figures (fail the realized gate on purpose): loop-only 837
|
||||
# casts/hr (25,953 xp/hr) ignored breaks + banking; the 8-14-cast break
|
||||
# regime (~305 casts/hr) and the 1,800-cast regime ignored the additive delay.
|
||||
# 100 -> 901-cast restock cadence. Overhead per 901-cast batch: restock trip
|
||||
# [60, 120]s (mid 90) + breaks (every [400, 700] casts, mid 550, for
|
||||
# [45, 120]s, mid 82.5s -> 82.5 x 901/550 = 135.2s).
|
||||
# STEADY-STATE casts/hr = 901 x 3600 / (901 x cycle + trip + break_share x 901):
|
||||
# fast 3.4s -> 1,019.7; midpoint 4.3s -> 791.3; slow 5.2s -> 639.2.
|
||||
# SESSION-REALIZED (2 h horizon = 7,200 s; startup 300-600 s, mid 450 s ->
|
||||
# 450/7200 = 6.25 % amortised): 977.1 / 741.8 / 585.9 casts/hr.
|
||||
# xp_per_hour = 741.8 x 31 = 22,995 (pin 23,000);
|
||||
# gp_per_hour = casts/hr x -112: low -109,435, high -65,621 (mid -83,082).
|
||||
# BATCH ECONOMICS (901 casts; deposits 901 x 180 = 162,180 gp per batch):
|
||||
# input cost at live low: 901 x 292 = 263,092 -> net -100,912/batch.
|
||||
# input cost at MAX fill (live low + 5 %: 1,000 x 155.4 + 1,000 x 151.2 =
|
||||
# 306,600 per batch): 901 x 306.6 = 276,247 -> net -114,067/batch (worst
|
||||
# case). Reserve = 6 worst-case batches (Magic 21 -> 54) = 684,400 gp;
|
||||
# the capital stops check TOTAL ASSETS, not raw cash (accounts + stop_conditions).
|
||||
# Superseded figures (all fail the derived gate on purpose): loop-only 837
|
||||
# casts/hr (25,953 xp/hr); the 8-14-cast-break regime (~305 casts/hr); the
|
||||
# 1,800-cast regime; the pre-startup realized 24,500 (791.3 x 31) — startup
|
||||
# amortisation is part of the model.
|
||||
supplies:
|
||||
- item: gold necklace
|
||||
qty_per_cycle: 1
|
||||
@ -79,54 +95,86 @@ economy:
|
||||
profit_drivers:
|
||||
- "NONE at current prices — XP is the product, not GP"
|
||||
- "Pick the candidate with the least-negative net each run (live rank)"
|
||||
- "Batch-scale economics (worst case 1 nature/cast): carried_batch 1,000/1,000, restock every 901 casts — one batch = 901 x -112 = -100,912 gp, +27,931 xp"
|
||||
- "Batch-scale economics (worst case 1 nature/cast): carried_batch 1,000/1,000, restock every 901 casts — one batch = 901 x -112 = -100,912 gp at live-low fills (-114,067 at MAX allowed fills), +27,931 xp, deposits 162,180 gp"
|
||||
|
||||
# Explicit activity/rate discriminator (owner guidance 2026-08-28, bounce 4):
|
||||
# this entry OPTS IN to the derived-rate gate; every rate derives from the
|
||||
# entry's own declared lifecycle and the validator re-derives it.
|
||||
rate_model:
|
||||
anchor_tier: la # xp_per_hour pins the LA tier's realized rate
|
||||
cast_ticks: 3 # LA is a 3-tick cast -> 1.8 s (0.6 s/tick)
|
||||
restock_trip_s: [60, 120] # GE-adjacent booth trip per restock
|
||||
session_horizon_s: 7200 # 2 h session; startup is amortised over it
|
||||
startup_s: [300, 600] # one-time GE acquisition + loadout
|
||||
alch_carry_batch: 1000 # stacked candidates + natures
|
||||
alch_restock_threshold: 100 # -> 901-cast LA restock cadence
|
||||
|
||||
state_machine:
|
||||
initial: idle
|
||||
terminal: [complete, failed]
|
||||
states:
|
||||
idle:
|
||||
goal: "reach the bank, acquire + equip the staff of fire, GE-buy candidate + natures, build the starting inventory to carried_batch (1,000 candidate + 1,000 nature)"
|
||||
next: [casting, complete, failed]
|
||||
goal: "session start at the GE bank booth. Open GE (a2), then place the initial offers: staff of fire (a3), candidate 1,000 (a4), nature runes 1,000 (a5) at live low + 5% (staff: +25% up to the 5,000 cap). Record initial capital in the cash ledger (total assets = cash + supplies at current buy cost). Nothing is bought or collected without an open GE interface"
|
||||
next: [wait_offers, complete, failed]
|
||||
wait_offers:
|
||||
goal: "wait for the pending offers (ge_offers_done); when filled, collect every purchase + refund from the collection box (a6 — never leave purchases in the box). PARTIAL FILLS: collect the filled portion, re-place the remainder at the current edge (a4/a5); 3 consecutive no-fill rounds -> failed. An offer still unfilled at session end is left resting by design — its coins are tied up (float reserve) and return when it fills or the operator aborts it (the single operator-touch point)"
|
||||
next: [ready, wait_offers, failed]
|
||||
ready:
|
||||
goal: "equip the staff of fire (a7 — bounded retries, then FAIL CLOSED) and build the loadout to carried_batch 1,000 candidate + 1,000 nature (a8; purchases from a6 are already in the inventory — deposit overflow, withdraw only the shortfall)"
|
||||
next: [casting, failed]
|
||||
casting:
|
||||
goal: "cast Low Level Alchemy on the current candidate item — each cast consumes 1 candidate + 1 nature and pays 180 gp (lowalch), +31 xp, net -112 gp; take a break when the completed-cast counter hits run_control.break.every ([400, 700] casts)"
|
||||
goal: "cast Low Level Alchemy on the current candidate item (a8) — each cast consumes 1 candidate + 1 nature and pays 180 gp (lowalch), +31 xp, net -112 gp; take a break when the completed-cast counter hits run_control.break.every ([400, 700] casts, via a12); the counter is NEVER reset by restocking"
|
||||
next: [bank_supplies, complete, failed]
|
||||
bank_supplies:
|
||||
goal: "deposit coins, withdraw candidate + natures back to carried_batch (1,000/1,000), resume casting"
|
||||
next: [casting, failed]
|
||||
goal: "restock to carried_batch: deposit coins + products and withdraw ALL candidate/natures the bank holds (a9 — bank-stock fast path when the bank already holds enough); for the remaining deficit, open GE (a2), buy the deficit (a10, live low + 5%), collect it (a11) — never withdraw a batch that was never bought. Restocking never resets the cast counter or the break counter; the break cadence (550/1,100/1,650, ...) is independent of the restock cadence (901/1,802, ...)"
|
||||
next: [casting, bank_supplies, failed]
|
||||
complete:
|
||||
goal: "reached the run's XP/level target (default 54, hand off to HLA at 55)"
|
||||
goal: "reached the run's XP/level target (default 54, hand off to HLA at 55) or the 2 h session horizon"
|
||||
failed:
|
||||
goal: "hard failure — stop, log ERROR sentinel, alert"
|
||||
transitions:
|
||||
# Handoff (verified 2026-08-27 from the OSRS wiki): the LA page states
|
||||
# "Players with level 55 Magic are generally recommended to use High
|
||||
# Level Alchemy instead" (65 vs 31 xp/cast, 60% vs 40% of item value).
|
||||
# Superheat Item unlocks at 43 (53 xp/cast) — an optional window if
|
||||
# ore-smelting margins justify it. Handoffs are soft efficiency choices,
|
||||
# not hard caps (all spells stay castable); see
|
||||
# codex/training/magic-alch-handoff.yaml.
|
||||
- from: idle
|
||||
when: "staff equipped AND candidate == carried_batch AND nature == carried_batch (1,000 and 1,000)"
|
||||
to: casting
|
||||
when: "staff + candidate + nature offers all placed (a3/a4/a5)"
|
||||
to: wait_offers
|
||||
- from: idle
|
||||
when: "magic level >= target (default 54, hand off to HLA at 55)"
|
||||
when: "magic level >= target (54, hand off to HLA at 55) at session start"
|
||||
to: complete
|
||||
- from: idle
|
||||
when: "3 consecutive purchase/equip failures with no fallback available (recovery block)"
|
||||
when: "3 consecutive offer-place failures at session start (no fallback candidate)"
|
||||
to: failed
|
||||
- from: wait_offers
|
||||
when: "ge_offers_done AND all purchases collected (a6)"
|
||||
to: ready
|
||||
- from: wait_offers
|
||||
when: "partial fill: filled portion collected (a6), remainder re-placed at the current edge (a4/a5) — still waiting"
|
||||
to: wait_offers
|
||||
- from: wait_offers
|
||||
when: "3 consecutive no-fill rounds (offer cannot fill at the accepted edge)"
|
||||
to: failed
|
||||
- from: ready
|
||||
when: "staff equipped (a7) AND loadout at carried_batch 1,000/1,000 (a8)"
|
||||
to: casting
|
||||
- from: ready
|
||||
when: "staff equip failed after bounded retries — FAIL CLOSED"
|
||||
to: failed
|
||||
- from: casting
|
||||
when: "candidate count < restock_threshold (100) OR nature rune count < restock_threshold (100) — deterministic restock trigger, pinned numbers"
|
||||
when: "candidate count < restock_threshold (100) OR nature count < restock_threshold (100) — deterministic restock trigger, pinned numbers"
|
||||
to: bank_supplies
|
||||
- from: casting
|
||||
when: "magic level >= target (default 54, hand off to HLA at 55)"
|
||||
when: "magic level >= target (54, hand off to HLA at 55)"
|
||||
to: complete
|
||||
- from: casting
|
||||
when: "3 consecutive failed casts (client issue — do not loop)"
|
||||
to: failed
|
||||
- from: bank_supplies
|
||||
when: "restocked to carried_batch (1,000 candidate + 1,000 nature)"
|
||||
when: "restocked to carried_batch (1,000 candidate + 1,000 nature) — bank fast path or re-bought deficit collected"
|
||||
to: casting
|
||||
- from: bank_supplies
|
||||
when: "restock partial: deficit re-bought but offers still filling (ge_offers_done false) — loop the restock phase"
|
||||
to: bank_supplies
|
||||
- from: bank_supplies
|
||||
when: "3 consecutive no-fill restock rounds OR a capital stop fires (assets / loss-reserve checks in stop_conditions)"
|
||||
to: failed
|
||||
|
||||
actions:
|
||||
- id: a1
|
||||
@ -141,18 +189,76 @@ actions:
|
||||
delay: [400, 1600]
|
||||
|
||||
- id: a2
|
||||
step: open-ge
|
||||
verb: ge_open
|
||||
target: grand-exchange
|
||||
params:
|
||||
note: "open the GE interface at the booth. The entry NEVER buys (a3/a4/a5/a10) or collects (a6/a11) without an open GE interface — the action grammar defines ge_open, ge_buy, ge_collect as separate verbs, so the open step is explicit"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1400]
|
||||
|
||||
- id: a3
|
||||
step: buy-staff
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "staff of fire"
|
||||
qty: 1
|
||||
max_price: "live low + 25 % (wiki value 1,500; hard cap 5,000)"
|
||||
note: "offer rests until filled or the run ends; on a partial/no fill, collect what filled (a6) and re-place the remainder at the current edge — bounded retries, then the run fails closed (no fire-rune fallback exists)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a4
|
||||
step: buy-supplies
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "current least-loss live candidate (default: gold necklace)"
|
||||
qty: "carried_batch = 1,000 (fits the 4h GE limit window of 18,000)"
|
||||
max_price: "live low + 5%"
|
||||
on_fail: retry # re-place at live low +5% with paced offers; 3 consecutive failures -> failed (recovery block)
|
||||
max_price: "live low + 5 %"
|
||||
note: "offer rests until filled or the run ends; on a partial fill, collect the filled portion (a6) and re-place the remainder at the current edge"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a3
|
||||
- id: a5
|
||||
step: buy-natures
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "nature rune"
|
||||
qty: "carried_batch = 1,000 (4h GE limit 18,000)"
|
||||
max_price: "live low + 5 %"
|
||||
note: "offer rests until filled or the run ends; partial fills handled like a4"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a6
|
||||
step: collect-purchases
|
||||
verb: ge_collect
|
||||
target: grand-exchange
|
||||
params:
|
||||
note: "collect every filled purchase and refund from the GE collection box into the inventory — staff, candidate, natures (and the equivalent on every restock). Partial offers: the collected portion is added to the ledger; nothing is left in the box at session start"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1400]
|
||||
|
||||
- id: a7
|
||||
step: equip-staff
|
||||
verb: menu_option
|
||||
target: "staff of fire in inventory"
|
||||
params:
|
||||
option: "Wield"
|
||||
note: "Equip requirement is NONE documented on the OSRS wiki Staff of fire page (no requirements row in the infobox, none in the prose; compare Battlestaff 'requires 30 Attack and 30 Magic'; verified 2026-08-28). FAIL-CLOSED path: bounded retries (3), then log ERROR + alert + stop — no fire-rune fallback is modeled (never loop on equip attempts)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1800]
|
||||
|
||||
- id: a8
|
||||
step: cast-alch
|
||||
verb: use_item_on
|
||||
target: "candidate item in inventory"
|
||||
@ -163,102 +269,144 @@ actions:
|
||||
consumption: "1 candidate + 1 nature per cast (staff of fire default); pays 180 gp (lowalch) + 31 xp, net -112 gp"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 3.4-5.2s, mid 4.3s -> realized ~791 casts/hr incl. restock + breaks (pinned in economy)
|
||||
delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 1.8s + 1.6-3.4s = 3.4-5.2s, midpoint 4.3s -> session-realized ~742 casts/hr incl. startup + restock + breaks (rate_model)
|
||||
|
||||
- id: a4
|
||||
step: bank-loop
|
||||
- id: a9
|
||||
step: restock-deposit
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
deposit: [all coins, all products]
|
||||
withdraw: "candidate + nature runes up to carried_batch (1,000/1,000)"
|
||||
withdraw: "ALL candidate + nature runes currently in the bank, up to carried_batch (1,000/1,000) — the bank-stock FAST PATH applies only when the bank actually holds enough stock; the remaining deficit is bought at the GE (a10) and collected (a11)"
|
||||
on_fail: alert
|
||||
anti_ban:
|
||||
delay: [500, 1100]
|
||||
|
||||
- id: a5
|
||||
- id: a10
|
||||
step: restock-buy
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "current least-loss live candidate + nature runes"
|
||||
qty: "deficit to carried_batch = carried_batch - inventory count after the a9 fast path (e.g. 901 candidate + 901 nature after a 901-cast batch with an empty bank)"
|
||||
max_price: "live low + 5 %"
|
||||
note: "the deficit is RE-BOUGHT at the GE on every restock — the entry never withdraws a batch that was never purchased; partial/failed fills use the same collect + re-place recovery as the initial acquisition"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a11
|
||||
step: restock-collect
|
||||
verb: ge_collect
|
||||
target: grand-exchange
|
||||
params:
|
||||
note: "collect the re-bought deficit into the inventory — inventory returns to carried_batch 1,000/1,000 before casting resumes"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1400]
|
||||
|
||||
- id: a12
|
||||
step: take-break
|
||||
verb: take_break
|
||||
target: "current location"
|
||||
params:
|
||||
duration: "[45, 120] seconds of idle AFK per break (matches run_control.break.for)"
|
||||
note: "executed by the translator when the completed-cast counter reaches run_control.break.every — this IS run_control.break's executor, fired on CAST COUNTS, not on bank visits; the cast counter is not reset by restocking"
|
||||
note: "executed by the translator when the completed-cast counter reaches run_control.break.every — this IS run_control.break's executor, fired on CAST COUNTS, not on bank visits; the cast counter is NOT reset by restocking (breaks land at 550/1,100/1,650 in the midpoint example, independent of the 901-cast restock cadence)"
|
||||
on_fail: skip
|
||||
|
||||
run_control:
|
||||
iteration: "one cast" # schema.md: break.every counts iterations, so every
|
||||
# value below is in completed casts, NOT bank trips
|
||||
# (owner decision 2026-08-28)
|
||||
max_iterations: null
|
||||
iteration: "one cast" # = one Low Level Alchemy cast (+31 xp, -112 gp at the anchor). Per docs/schema.md run_control, break.every counts ITERATIONS — so it counts CASTS here, NOT bank trips (owner decision 2026-08-28)
|
||||
max_iterations: null # null = until complete condition
|
||||
break:
|
||||
every: [400, 700] # COMPLETED CASTS between AFK breaks (midpoint 550 = ~39
|
||||
# min at the 4.3s midpoint cycle). Cast-count-based and
|
||||
# independent of bank visits; a restock trip NEVER counts
|
||||
# as a break and does not reset this counter
|
||||
for: [45, 120] # seconds of idle AFK
|
||||
every: [400, 700] # COMPLETED CASTS between AFK breaks (midpoint 550 = ~39 min at the 4.3s midpoint cycle). Cast-count-based and independent of bank visits; a restock trip NEVER counts as a break and does not reset this counter
|
||||
for: [45, 120] # seconds of idle AFK per break
|
||||
stop_conditions:
|
||||
- "magic level >= target (default 54, hand off to HLA at 55)"
|
||||
- "gp under floor (working capital below one carried_batch = 292,000 gp)"
|
||||
- "magic level >= target (54 -> hand off to HLA at 55)"
|
||||
- "session elapsed >= 2 h (session horizon — startup is amortised over this window in the rate model)"
|
||||
- "total assets (cash + supplies at current buy cost) < 311,600 — cannot fund the next batch at max fill (306,600) plus staff replacement (5,000); assets, NOT raw cash: cash that became supplies is not a loss"
|
||||
- "session net loss (total assets - initial capital) > 684,400 — the 6-batch worst-case loss reserve is exhausted"
|
||||
- "3 consecutive failed casts (client issue — do not loop)"
|
||||
- "3 consecutive failed GE purchases with no fallback candidate available"
|
||||
- "3 consecutive no-fill GE rounds (initial acquisition or restock) with no fallback candidate available"
|
||||
- "staff of fire equip failed after bounded retries — FAIL CLOSED (log ERROR, alert, stop)"
|
||||
|
||||
# ================= WORKED EXAMPLE (pinned, deterministic — owner guidance 2026-08-28) =================
|
||||
# Constants: carried_batch = 1,000 candidate + 1,000 nature; restock_threshold = 100;
|
||||
# break.every = [400, 700] casts (midpoint 550); break.for = [45, 120] s;
|
||||
# default staff-of-fire path consumes 1 candidate + 1 nature per cast (worst case 1:1).
|
||||
# break.every = [400, 700] casts — the example pins the MIDPOINT 550;
|
||||
# break.for = [45, 120] s; restock cadence = 901 casts (1,000 - 99); the
|
||||
# staff-of-fire path consumes 1 candidate + 1 nature per cast (worst case 1:1).
|
||||
# The two counters are MONOTONIC and INDEPENDENT: breaks at 550, 1,100, 1,650,
|
||||
# 2,200... (multiples of 550) and restocks at 901, 1,802, 2,703... (multiples
|
||||
# of 901) — gcd(550, 901) = 1, so they interleave forever and restocking NEVER
|
||||
# resets or shifts the break counter.
|
||||
#
|
||||
# Step 0 — before the first inventory: account holds >= 292,000 gp working capital
|
||||
# (1,000 x 148 + 1,000 x 144). At the GE bank: GE-buy staff of fire, Wield it
|
||||
# (no documented requirement), GE-buy 1,000 gold necklaces (noted, stacked) +
|
||||
# 1,000 nature runes, withdraw both -> loadout = carried_batch 1,000/1,000.
|
||||
# Cast counter := 0.
|
||||
# Step 1 — casts #1..#550: each cast consumes 1 candidate + 1 nature (-292 gp) and
|
||||
# pays 180 gp (lowalch) + 31 xp -> net -112 gp. Cast counter 0 -> 550 (~39 min at
|
||||
# midpoint cadence).
|
||||
# Step 2 — FIRST SCHEDULED BREAK at cast 550 (midpoint of break.every [400, 700]):
|
||||
# take-break for 45-120 s. Resume casting at cast 551. The break fired on the CAST
|
||||
# COUNT, not on any bank visit; the restock counter is untouched.
|
||||
# Step 3 — casts #551..#901: candidate 1000-901 = 99 < 100 (restock_threshold) and
|
||||
# nature 1000-901 = 99 < 100 simultaneously (worst case 1:1) -> the transition
|
||||
# fires at the end of cast #901: FIRST RESTOCK/BANK EVENT at cast 901.
|
||||
# Coins deposited at this restock: 901 x 180 = 162,180 gp; batch net -100,912 gp;
|
||||
# batch xp 901 x 31 = 27,931.
|
||||
# Step 4 — restock: deposit coins + products, withdraw 901 candidate + 901 nature ->
|
||||
# carried_batch restored to 1,000/1,000. Resume casting at cast 902 (the cast
|
||||
# counter continues; the break counter also continues — restocking never resets either).
|
||||
# Step 5 — SECOND BREAK when the cast-count interval since break #1 completes
|
||||
# ([400, 700], midpoint: cast 902 + 550 = 1,452): take-break, resume at 1,453.
|
||||
# Step 6 — restock #2 fires at cast 1,802 (901-cast batch cadence), and so on, until a
|
||||
# stop_condition fires (Magic 54+ -> hand off to HLA at 55; gp under floor; 3 failed casts).
|
||||
# Bank visits and breaks are INDEPENDENT by construction: restock cadence = 901 casts;
|
||||
# break cadence = 400-700 casts, and the two counters do not interact.
|
||||
# Step 0 — preparation (one-time startup, ~300-600 s): account holds >= ~1.30M
|
||||
# gp cash (staff cap 5,000 + first batch max 306,600 + reserve 684,400 +
|
||||
# float 306,600). At the GE bank: open GE (a2); place offers — staff (a3),
|
||||
# 1,000 gold necklaces (a4), 1,000 nature runes (a5); wait for fills
|
||||
# (ge_offers_done) and collect everything (a6); equip the staff (a7); loadout
|
||||
# = carried_batch 1,000/1,000 (a8). Cast counter := 0; cash ledger opened.
|
||||
# Step 1 — casts #1..#550: each cast consumes 1 candidate + 1 nature (-292 gp)
|
||||
# and pays 180 gp (lowalch) + 31 xp -> net -112 gp. Cast counter 0 -> 550
|
||||
# (~39 min at midpoint).
|
||||
# Step 2 — FIRST SCHEDULED BREAK at cast 550 (the interval since cast 0):
|
||||
# take-break 45-120 s (a12). Resume casting at cast 551. The break fired on
|
||||
# the CAST COUNT, not on any bank visit.
|
||||
# Step 3 — casts #551..#901: countdown candidate 1,000-901 = 99 < 100
|
||||
# (restock_threshold) and nature 99 < 100 simultaneously (worst case 1:1) ->
|
||||
# the transition fires at the end of cast #901: FIRST RESTOCK at cast 901.
|
||||
# Deposit 901 x 180 = 162,180 gp (a9); the bank holds 0 supplies (all sales
|
||||
# were GE buys), so the fast path yields nothing -> open GE (a2), RE-BUY the
|
||||
# deficit 901 candidate + 901 nature (a10, live low + 5 %), collect (a11) ->
|
||||
# inventory back to 1,000/1,000. Batch net: -100,912 gp (live-low fills) or
|
||||
# -114,067 gp (max fills). The cast counter (901) and break counter continue.
|
||||
# Step 4 — SECOND BREAK at cast 1,100: the interval since break #1 is exactly
|
||||
# 550 casts (1,100 - 550); the cast-901 restock did NOT reset or shift it.
|
||||
# Step 5 — THIRD BREAK at cast 1,650 (1,100 + 550); SECOND RESTOCK at cast
|
||||
# 1,802 (901-cadence, 1,802 = 901 x 2). The sequences 550/1,100/1,650/2,200
|
||||
# and 901/1,802/2,703 interleave independently.
|
||||
# Step 6 — continue until a stop_condition fires: Magic 54+ (HLA handoff at
|
||||
# 55), the 2 h session horizon, the capital stops (assets floor 311,600 or
|
||||
# net loss > 684,400), 3 failed casts, or 3 consecutive no-fill GE rounds.
|
||||
# ============================================================================================
|
||||
|
||||
risk:
|
||||
ban_level: low
|
||||
notes:
|
||||
- "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4]s; realized ~791 casts/hr at the 4.3s midpoint cycle incl. restock + breaks; loop-only 837)."
|
||||
- "Breaks are CAST-COUNT-based (every [400, 700] completed casts, 45-120s AFK) and independent of bank visits — never treat a restock trip as a break."
|
||||
- "GE buy bursts: pace offers, respect the 4h limit window (18,000 necklaces / 18,000 natures)."
|
||||
- "High cast cadence is the main detection vector; keep the mandatory anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4]s; session-realized ~742 casts/hr midpoint incl. startup + restock + breaks over a 2 h session; steady-state ~791; loop-only 837 is a ceiling reference, never the output)."
|
||||
- "Breaks are CAST-COUNT-based (every [400, 700] completed casts, 45-120s AFK) and independent of bank visits — never treat a restock trip as a break, and never reset the break counter on restock (gcd(550, 901) = 1)."
|
||||
- "GE buy bursts: pace offers, respect the 4h limit window (18,000 necklaces / 18,000 natures); offers persist until filled or aborted and a 4h limit reset is NOT an offer expiry."
|
||||
|
||||
# --- recovery block (owner guidance 2026-08-28: every activity must cover its complete runnable lifecycle) ---
|
||||
# - Missing materials / depleted supplies: the restock_threshold transition (casting ->
|
||||
# bank_supplies at candidate OR nature < 100) catches both mid-run depletions; restock
|
||||
# action a4 rebuilds to carried_batch and the loop resumes (worked example steps 3-4).
|
||||
# - Failed GE purchases (offer unfilled): re-place at live low + 5% with paced offers (a2);
|
||||
# after 3 consecutive failures, alert + stop (failed) unless a fallback candidate is
|
||||
# switched in — never sit in an unfillable offer loop.
|
||||
# - Staff of fire cannot be equipped: fall back to fire runes (3 fire + 1 nature = +15 gp
|
||||
# per cast rune cost), log WARN, continue; never loop on equip attempts.
|
||||
# - Invalid loadout (wrong item in hand / spell unavailable): re-run build-loadout (a2/a4);
|
||||
# 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert).
|
||||
# - Explicit completion/stop: stop_conditions above (Magic 54+ HLA handoff, gp floor,
|
||||
# repeated failures).
|
||||
# bank_supplies at candidate OR nature < 100) catches both mid-run depletions; the
|
||||
# restock phase (a9 -> a2 -> a10 -> a11) rebuilds to carried_batch and the loop
|
||||
# resumes (worked example steps 3-4). The entry NEVER withdraws a batch that was
|
||||
# never purchased — every restock re-buys the deficit at the GE.
|
||||
# - GE offers: partial fills are collected (a6/a11) and the remainder is re-placed at
|
||||
# the current edge (a3/a4/a5/a10); after 3 consecutive no-fill rounds (initial or
|
||||
# restock) the run stops (failed) — never sit in an unfillable offer loop. Unfilled
|
||||
# offers at session end are left resting by design (float reserve; coins return when
|
||||
# they fill or the operator aborts them — the single operator-touch point).
|
||||
# - Staff of fire cannot be equipped (a7): bounded retries (3), then FAIL CLOSED —
|
||||
# log ERROR + alert + stop. No fire-rune fallback is modeled (it would need its own
|
||||
# purchase/supply/rate path; the owner decision rejects a prose-only fallback).
|
||||
# - Invalid loadout (wrong item in hand / spell unavailable): re-run build-loadout
|
||||
# (a8); 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert).
|
||||
# - Capital: the stop checks TOTAL ASSETS (cash + supplies at current buy cost), and
|
||||
# both capital stops are assets-based (see stop_conditions) — cash converted into
|
||||
# supplies is not a loss and never fires a stop by itself.
|
||||
# - Explicit completion/stop: stop_conditions above (Magic 54+ HLA handoff at 55,
|
||||
# 2 h session horizon, assets floor 311,600, loss reserve 684,400, repeated
|
||||
# failures, fail-closed staff equip).
|
||||
|
||||
sources:
|
||||
- "OSRS wiki — Low Level Alchemy (level 21, 31 xp/cast, 3-tick, F2P; infobox 2026-08-27)"
|
||||
- "prices.runescape.wiki mapping (id 1654 gold necklace: members=false, limit 18000, lowalch 180, highalch 270, value 450; id 561 nature rune; id 554 fire rune)"
|
||||
- "prices.runescape.wiki latest 2026-08-27 (necklace buy 148, nature buy 144, fire buy 5)"
|
||||
- "OSRS wiki — Grand Exchange (offers persist until filled or aborted; filled items and coin refunds are collected from the booth/collection box; partial fills are separately collectable) — verified 2026-08-28"
|
||||
- "OSRS wiki — Grand Exchange/Buying limits (4-hour window; the limit resets 4 hours after the FIRST unit is bought; outstanding offers remain active and a limit reset is NOT an offer expiry) — verified 2026-08-28"
|
||||
- "OSRS wiki — Staff of fire (page verified 2026-08-28 for the equip requirement: id 1387, F2P item, tradeable, options Wield — NO requirements row in the Infobox Item and NO requirement statement in the prose; contrast Battlestaff, whose prose states 'requires 30 Attack and 30 Magic to wield')"
|
||||
- "docs/schema.md run_control (iteration unit; break.every counts iterations) + docs/action-grammar.md:135 (anti_ban.delay = additive post-action sleep) + verb take_break — the cadence + break + lifecycle model; owner guidance 2026-08-28 (realized rates incl. breaks + banking, worked example)"
|
||||
- "docs/schema.md run_control (iteration unit; break.every counts iterations) + docs/action-grammar.md:135 (anti_ban.delay = additive post-action sleep) + verbs ge_open/ge_buy/ge_collect/ge_offers_done (separate GE primitives) — the cadence + break + GE + lifecycle model; owner guidance 2026-08-28 (rate_model, fail-closed staff, monotonic counters)"
|
||||
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27
|
||||
|
||||
related:
|
||||
|
||||
@ -3,7 +3,7 @@ title: Magic training via alchemy (LA → SII → HLA handoff)
|
||||
domain: training
|
||||
tags: [magic-training, alching, f2p, xp-training]
|
||||
status: verified
|
||||
version: 2 # 2: bounce-fix 3 2026-08-28 (owner guidance) — dynamic recipe selection (highest-unlocked, target_input, fallback), realized rates incl. mandatory breaks + banking, full-lifecycle actions, worked example pinned
|
||||
version: 3 # 3: bounce-fix 4 2026-08-28 (owner guidance 10331) — explicit rate_model discriminator (derived per-entry lifecycle rates), per-recipe inventory capacity floor(27/qty) with derived min/mid/max rates pinned in recipes.rates, complete GE acquisition path (open -> fill -> collect) for initial buy AND every restock (bank-stock fast path), fail-closed staff equip (bounded retries, no fire-rune fallback), configurable session target (55 = LA/SII->HLA handoff completion; >55 = executable HLA tier to the target; LA stays the 43-54 fallback), capital model (cash vs supplies, max-fill costs, reserve, assets-based stops)
|
||||
updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
|
||||
|
||||
summary: >
|
||||
@ -12,44 +12,59 @@ summary: >
|
||||
ore into bars), then High Level Alchemy from 55 (65 xp/cast, the wiki's
|
||||
recommended end-state). All three spells are F2P and stay castable after
|
||||
their unlock levels, so the handoffs are efficiency recommendations, not
|
||||
hard caps. SII model (owner guidance 2026-08-28): the recipe table is
|
||||
dynamic and governed by the account's Smithing level — the highest UNLOCKED
|
||||
recipe with a positive complete-cost margin is selected, its primary ore
|
||||
(target_input, never coal) is the spell target, and fallback is to the next
|
||||
lower unlocked recipe, else skip SII and stay on LA. Rates are REALIZED:
|
||||
mandatory additive delays PLUS breaks (every 400-700 completed casts) PLUS
|
||||
banking (901-cast alch batches; 24-cast SII bank cycles) — pinned below.
|
||||
hard caps. The SESSION TARGET is configurable: the default 55 completes at
|
||||
the LA/SII -> HLA handoff; a target above 55 transitions into the
|
||||
executable HLA tier and continues until the target — HLA is never
|
||||
declared-but-unreachable, and Low Alchemy remains the executable fallback
|
||||
throughout Magic 43-54 whenever SII is skipped. SII model (owner guidance
|
||||
2026-08-28): a dynamic recipe table governed by the account's Smithing
|
||||
level — the highest UNLOCKED recipe whose complete recipe cost (ALL inputs
|
||||
at ALLOWED fill prices, live low + 5%) plus one nature rune stays under the
|
||||
bar's live GE price is selected; its primary ore (target_input, never coal)
|
||||
is the spell target; materials unobtainable -> next lower unlocked recipe,
|
||||
else skip SII and stay on LA. SII inventory cycles are CAPACITY-DERIVED:
|
||||
floor(27 / total input qty) casts per bank cycle (bronze 13, iron/silver/
|
||||
gold 27, steel 9, mithril 5, adamantite 3, runite 3) — never a fixed pack.
|
||||
Rates are SESSION-REALIZED under the declared rate_model: cast cycle +
|
||||
mandatory additive delays + breaks + banking + startup over a 2 h horizon
|
||||
(anchors: LA 23,000 / SII steel 12,400 / HLA 48,200 Magic XP/hr; per-recipe
|
||||
min/mid/max rates pinned in the recipes table and enforced by the
|
||||
validator). Every purchase follows the complete GE path open -> place ->
|
||||
fill/wait -> collect, for the initial acquisition and every restock (bank
|
||||
fast path only when the bank holds stock). Staff-of-fire equip failure is
|
||||
FAIL-CLOSED (bounded retries, then log ERROR + alert + stop).
|
||||
|
||||
accounts:
|
||||
membership: f2p # LA, SII and HLA are all members=No spells
|
||||
min_level: 21 # Magic 21 — Low Level Alchemy unlock
|
||||
requires_quest: []
|
||||
requires_other:
|
||||
- "Staff of Fire equipped (drops LA/SII/HLA rune cost to 1 nature rune per cast; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources)"
|
||||
- "Staff of Fire equipped (drops LA/SII/HLA rune cost to 1 nature rune per cast; NO level requirement documented on the OSRS wiki — verified 2026-08-28, see sources). Equip failure is FAIL-CLOSED: bounded retries (3), then log ERROR + alert + stop — there is no fire-rune fallback in this model"
|
||||
- "SII tier requires the selected recipe's Smithing level (recipes table: bronze 1 / iron 15 / silver 20 / steel 30 / gold 40 / mithril 50 / adamantite 70 / runite 85) — the highest UNLOCKED recipe is selected and an unmet gate is NEVER selected (selection.mode)"
|
||||
- "Working capital >= 150,000 gp for 1,000 nature runes plus the selected recipe's exact inputs (live GE buy; steel example: 1 iron ore 100 + 2 coal 150 each per cast at the 2026-08-27 snapshot)"
|
||||
- "A bank with GE access within reach (alch spot = the bank booth; restock trips 60-120s)"
|
||||
- "Starting capital >= ~1.31M gp CASH for the LA/HLA tier path (staff hard cap 5,000 + first batch 1,000/1,000 at MAX allowed fill 307,650 + session loss reserve 690,075 = 6 worst-case 901-cast batches at live-low+5% fills (covers Magic 21 -> 55) + resting-offer float 307,650). The SII window uses capacity-sized packs (steel example: 9 x (1 iron 105 + 2 coal 157.5 + 1 nature 152.25) = 5,150 gp per pack at max fill). The capital stop checks TOTAL ASSETS (cash + supplies at current buy cost), never raw cash — cash legitimately converts into supplies"
|
||||
- "Session target Magic level configured at session start: default 55 (completes at the LA/SII -> HLA handoff); a target above 55 runs the HLA tier until the target; LA remains the executable fallback throughout 43-54 when SII is skipped"
|
||||
- "A bank with GE access within reach (alch spot = the bank booth; restock trips [60, 120] s)"
|
||||
|
||||
best_practice:
|
||||
equipment:
|
||||
- item: staff of fire
|
||||
why: "unlimited fire runes — the rune cost of every alchemy spell drops to a single nature rune per cast; equip requirement: none documented on the OSRS wiki (verified 2026-08-28)"
|
||||
why: "unlimited fire runes — the rune cost of every alchemy spell drops to a single nature rune per cast; equip requirement: none documented on the OSRS wiki (verified 2026-08-28); if it cannot be equipped after bounded retries the run FAILS CLOSED (no fallback modeled)"
|
||||
source: "OSRS wiki — High Level Alchemy (rune cost table); OSRS wiki — Staff of fire (id 1387: no requirements row in the infobox, none in the prose — contrast Battlestaff 'requires 30 Attack and 30 Magic')"
|
||||
techniques:
|
||||
- name: hand off LA -> HLA at level 55
|
||||
why: "LA page: 'Players with level 55 Magic are generally recommended to use High Level Alchemy instead, as it provides more coins and experience per cast' — 65 vs 31 xp/cast, 60% vs 40% of item value"
|
||||
- name: hand off LA -> HLA at level 55 (configurable target)
|
||||
why: "LA page: 'Players with level 55 Magic are generally recommended to use High Level Alchemy instead, as it provides more coins and experience per cast' — 65 vs 31 xp/cast, 60% vs 40% of item value. The default session target is 55; a target above 55 keeps casting HLA (65 xp/cast) until the configured level — the HLA tier is never declared but unreachable"
|
||||
source: "OSRS wiki — Low Level Alchemy (Comparison with High Level Alchemy); High Level Alchemy (infobox: level 55, 65 xp)"
|
||||
- name: optional Superheat Item window 43-55
|
||||
why: "SII unlocks at 43 (53 xp/cast) and smelts ore into bars without a furnace — a training window that can profit when ore/bar margins are positive; it cannot be cast on noted ore, so it needs the exact recipe inputs in inventory rather than a note stack"
|
||||
why: "SII unlocks at 43 (53 xp/cast) and smelts ore into bars without a furnace — a training window that can profit when ore/bar margins are positive; it cannot be cast on noted ore, so it needs the exact recipe inputs in inventory rather than a note stack. The inventory holds floor(27 / total input qty) casts per bank cycle (steel = 9) — never a fixed 24-cast pack"
|
||||
source: "OSRS wiki — Superheat Item (43 Magic, 53 xp, F2P; 'smelt ore into bars without a furnace'; 'cannot be cast on noted ore')"
|
||||
- name: select the highest UNLOCKED SII recipe, never an unmet gate
|
||||
why: "the recipe table is dynamic: at each cast the active recipe = the recipes-table entry with the highest smithing_level <= current Smithing whose complete input cost (ALL inputs at live GE buy + one nature rune) stays under the bar's GE price; the spell targets that recipe's declared target_input (the primary ore — never coal); when the selected recipe's materials cannot be obtained, fall back to the next lower unlocked recipe; when none has a positive margin, skip SII and stay on LA"
|
||||
source: "OSRS wiki — Superheat Item (requires the Smithing level and materials of the bar); Smithing (bar smelting table); prices.runescape.wiki (live GE prices); owner guidance 2026-08-28 (selection.mode)"
|
||||
why: "at each cast the active recipe = the recipes-table entry with the highest smithing_level <= current Smithing whose complete input cost (ALL inputs at ALLOWED fill prices = live low + 5%) + one nature rune at allowed fill stays under the bar's live GE price; the spell targets that recipe's declared target_input (the primary ore — never coal); when the selected recipe's materials cannot be obtained, fall back to the next lower unlocked recipe; when none has a positive margin, skip SII and stay on LA (LA is executable throughout 43-54)"
|
||||
source: "OSRS wiki — Superheat Item (requires the Smithing level and materials of the bar); Smithing (bar smelting table); prices.runescape.wiki (live GE prices); owner guidance 2026-08-28 (selection.mode, allowed-fill eligibility)"
|
||||
- name: buy GE items below alch value, cast on noted items at a bank
|
||||
why: "HLA page: 'Players who train this way can break even on the cost of runes, or even turn a profit, by buying items on the Grand Exchange for a lower cost than the item's High Alchemy value'; all alchemy spells work on noted items, saving bank withdrawals per cast"
|
||||
source: "OSRS wiki — High Level Alchemy (GE buying for break-even/profit; noted-item casting)"
|
||||
- name: treat the handoffs as soft efficiency choices
|
||||
why: "all three spells remain castable past 21/43/55 and stay on the standard F2P spellbook, so the tier switches optimise xp/cast and cost rather than gate the path"
|
||||
why: "all three spells remain castable past 21/43/55 and stay on the standard F2P spellbook, so the tier switches optimise xp/cast and cost rather than gate the path; LA is the executable fallback whenever the SII window is skipped"
|
||||
source: "OSRS wiki — Low Level Alchemy; Superheat Item; High Level Alchemy (level + experience infoboxes, members=No)"
|
||||
|
||||
economy:
|
||||
@ -57,34 +72,37 @@ economy:
|
||||
# staff of fire -> 1 nature rune per cast. Nature rune id 561 buy 145
|
||||
# (mapping limit 18000, cross-checked against Module:Exchange/Nature_rune,
|
||||
# members=false, lowalch 72 = 40% of value 180 — consistency assertion).
|
||||
# Income/net per cast depend on the alched item, so they live in the
|
||||
# per-activity entries (low-alch-f2p, f2p-alch-catalog): this entry only
|
||||
# prices the shared consumable. SII recipe economics live in the recipes
|
||||
# table below.
|
||||
# Income/net per cast depend on the alched item or the recipe bar, so they
|
||||
# live in the per-activity entries and in the recipes table (this entry
|
||||
# prices the shared consumable + the rate model).
|
||||
# xp_per_hour pins the rate_model ANCHOR tier (sii, steel-bar): the entry's
|
||||
# own declared lifecycle — no hard-coded per-activity constants.
|
||||
item: nature rune
|
||||
item_id: 561
|
||||
cost_per_cycle: 145 # one nature rune per cast (staff of fire), buy price 2026-08-27
|
||||
xp_per_hour: 51400 # HLA tier, REALIZED (breaks + banking included), pinned below
|
||||
# ---- realized-rate derivation (owner guidance 2026-08-28: rates must
|
||||
# include the mandatory breaks and banking, not just the cast loop) ----
|
||||
# Cast: 1.8s (3 ticks x 0.6s) + anti_ban.delay [1.6, 3.4]s ADDITIVE after
|
||||
# the action (docs/action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s.
|
||||
xp_per_hour: 12400 # SII anchor, SESSION-REALIZED: steel mid 249.1 casts/hr x 53 x startup factor 0.9375 = 12,379 (pin 12,400)
|
||||
# ---- rate model (derived from rate_model + actions + run_control + recipes;
|
||||
# enforced by the validator's rate_model_errors gate) ----
|
||||
# Cast: 1.8s (3 ticks x 0.6s, rate_model.cast_ticks) + anti_ban.delay
|
||||
# [1.6, 3.4]s ADDITIVE (action-grammar.md:135) = 3.4-5.2s, midpoint 4.3s.
|
||||
# Restock trip [60, 120]s (rate_model.restock_trip_s); breaks every
|
||||
# [400, 700] casts for [45, 120]s (run_control.break) -> amortised break
|
||||
# share 45/700 fast, 82.5/550 mid, 120/400 slow.
|
||||
# ALCH tiers (LA/HLA): 901-cast restock batch (carried_batch 1,000 -
|
||||
# restock_threshold 100) + 225s overhead per batch (90s restock trip at a
|
||||
# GE-adjacent booth + 135s breaks every [400, 700] casts, mid 550, for
|
||||
# [45, 120]s): realized casts/hr = 901 / ((901 x 4.3 + 225) / 3600) = 791.3.
|
||||
# LA 24,500 xp/hr = 31 x 791.3 (pin 24,530 -> 24,500)
|
||||
# HLA 51,400 xp/hr = 65 x 791.3 (pin 51,435 -> 51,400)
|
||||
# SII tier: bank cycle is INVENTORY-bound (bars are non-stackable): restock
|
||||
# when bars_in_inventory >= 24, i.e. 24-cast cycles with a ~60s trip. Per
|
||||
# 550-cast break window: 550 x 4.3 = 2,365s casting + 22.9 trips x 60s +
|
||||
# 82.5s break = 3,822.5s -> realized casts/hr = 550 / (3,822.5/3600) = 518.0.
|
||||
# SII 27,500 xp/hr = 53 x 518.0 (pin 27,454 -> 27,500; range 24,300 by
|
||||
# slow cycle 458.6/hr .. 31,500 by fast cycle 595.0/hr)
|
||||
# Superseded figures (fail the realized gate on purpose): loop-only 837
|
||||
# casts/hr (54,419 HLA) ignored breaks+banking; the 8-14-cast break regime
|
||||
# (~305 casts/hr, 19,830 HLA) and the 1,400/1,800-cast regimes ignored the
|
||||
# additive delay.
|
||||
# restock_threshold 100), steady-state 1,019.7 / 791.3 / 639.2 casts/hr;
|
||||
# SESSION-REALIZED over the 2 h horizon with startup 300-600s (factor
|
||||
# 0.9583/0.9375/0.9167): 977.1 / 741.8 / 585.9 casts/hr ->
|
||||
# LA 23,000 xp/hr = 31 x 741.8 (22,995)
|
||||
# HLA 48,200 xp/hr = 65 x 741.8 (48,217)
|
||||
# SII tiers: per-recipe capacity = floor(27 / total input qty) (steel 9,
|
||||
# bronze 13, iron/silver/gold 27, mithril 5, adamantite/runite 3); full
|
||||
# derived min/mid/max rates live in each recipe's rates block (validator-
|
||||
# checked): steel steady mid 249.1 casts/hr = 13,204 Magic XP/hr; the
|
||||
# xp_per_hour anchor adds the startup factor (0.9375 mid) -> 12,400.
|
||||
# Superseded figures (all fail the derived gate on purpose): loop-only
|
||||
# 837 casts/hr; the 8-14-cast-break regime (~305 casts/hr); the 1,400/
|
||||
# 1,800-cast regimes; the impossible 24-cast SII pack (518.0 casts/hr) —
|
||||
# capacity is floor(27/qty), never a fixed pack.
|
||||
supplies:
|
||||
- item: nature rune
|
||||
qty_per_cycle: 1
|
||||
@ -93,15 +111,18 @@ economy:
|
||||
profit_drivers:
|
||||
- "XP per cast by tier (wiki infoboxes): LA 31 (level 21) / SII 53 (level 43) / HLA 65 (level 55); SII also grants the bar's Smithing XP (recipes table)"
|
||||
- "HLA can break even or profit on GE-bought items priced below their High Alchemy value (HLA page); LA is a loss at current prices (see low-alch-f2p)"
|
||||
- "SII turns a profit only when the complete recipe cost (ALL inputs + one nature rune) stays under the resulting bar's GE price — re-check margins per window and select the highest UNLOCKED recipe with a positive margin (selection.mode); never a steel-only default"
|
||||
- "SII turns a profit only when the complete recipe cost (ALL inputs at ALLOWED fill prices + one nature rune at allowed fill) stays under the resulting bar's GE price — re-check margins per window and select the highest UNLOCKED recipe with a positive margin; never a steel-only default, never an unmet Smithing gate"
|
||||
|
||||
# Selection model (owner guidance 2026-08-28): the Superheat recipe is chosen
|
||||
# at runtime from the recipes table — never hardcoded to one bar, never above
|
||||
# the account's Smithing level, with a deterministic fallback chain.
|
||||
# the account's Smithing level, with a deterministic fallback chain. The
|
||||
# validator proves eligibility (smithing gate + complete-recipe margin),
|
||||
# target_input (declared input, never coal), unique gates, and the fallback
|
||||
# path via its fixtures.
|
||||
selection:
|
||||
mode: highest-unlocked
|
||||
eligible_rule: "at each cast, the active recipe = the recipes-table entry with the highest smithing_level <= current Smithing level whose complete input cost (ALL inputs at live GE buy) + one nature rune 145 < the bar's live GE price"
|
||||
fallback: "when the selected recipe's materials cannot be obtained (3 consecutive failed GE purchases / unfillable offers), drop to the next lower unlocked recipe and re-check its margin; when no unlocked recipe has a positive margin, skip SII for this window and stay on LA — never select a recipe whose Smithing requirement is unmet, never cast on coal"
|
||||
eligible_rule: "at each cast, the active recipe = the recipes-table entry with the highest smithing_level <= current Smithing level whose complete input cost at ALLOWED fill prices (every input at live GE low + 5%, the a4 max_price cap) + one nature rune at allowed fill < the bar's live GE price — the full actual/allowed recipe cost, not a headline ore price"
|
||||
fallback: "when the selected recipe's materials cannot be obtained (3 consecutive failed GE purchases / unfillable offers), drop to the next lower unlocked recipe and re-check its allowed-fill margin; when no unlocked recipe has a positive margin, skip SII for this window and stay on LA throughout 43-54 — LA remains executable (a8 handles level < 43 AND the 43-54 SII-skip window); never select a recipe whose Smithing requirement is unmet, never cast on coal"
|
||||
|
||||
# Concrete F2P bar recipes for Superheat Item. SII requires the bar's
|
||||
# Smithing level and its exact materials, so every selectable bar is modelled
|
||||
@ -110,8 +131,14 @@ selection:
|
||||
# smelting table; Superheat Item grants the bar's Smithing XP per cast).
|
||||
# target_input = the PRIMARY ORE the spell is cast on (a declared input,
|
||||
# never coal — the validator enforces both).
|
||||
# econ 2026-08-27: bar margin = bar GE price - (all inputs at live GE buy +
|
||||
# one nature rune 145); the client selects via selection.mode.
|
||||
# rates (validator-checked, derived from the entry's rate_model): capacity =
|
||||
# floor(27 / total input qty) casts per bank cycle (28-slot inventory minus
|
||||
# the nature stack); casts_per_hour and magic_xp_per_hour are the STEADY-
|
||||
# STATE min/mid/max (cycle endpoints 3.4/4.3/5.2s, trip 60/90/120s,
|
||||
# break share 45/700, 82.5/550, 120/400). The economy.xp_per_hour anchor
|
||||
# adds the startup factor (see rate_model).
|
||||
# econ 2026-08-27: bar margin = bar GE price - (all inputs at allowed fill +
|
||||
# one nature rune at allowed fill); the client selects via selection.mode.
|
||||
recipes:
|
||||
bronze-bar:
|
||||
bar: Bronze bar
|
||||
@ -128,6 +155,10 @@ recipes:
|
||||
item_id: 438
|
||||
qty: 1
|
||||
source: ge
|
||||
rates:
|
||||
capacity: 13
|
||||
casts_per_hour: {min: 445.6, mid: 316.5, max: 244.4}
|
||||
magic_xp_per_hour: {min: 23617, mid: 16776, max: 12952}
|
||||
iron-bar:
|
||||
bar: Iron bar
|
||||
bar_item_id: 2351
|
||||
@ -139,6 +170,10 @@ recipes:
|
||||
item_id: 440
|
||||
qty: 1
|
||||
source: ge
|
||||
rates:
|
||||
capacity: 27
|
||||
casts_per_hour: {min: 633.1, mid: 462.5, max: 362.0}
|
||||
magic_xp_per_hour: {min: 33554, mid: 24514, max: 19187}
|
||||
silver-bar:
|
||||
bar: Silver bar
|
||||
bar_item_id: 2355
|
||||
@ -150,6 +185,10 @@ recipes:
|
||||
item_id: 442
|
||||
qty: 1
|
||||
source: ge
|
||||
rates:
|
||||
capacity: 27
|
||||
casts_per_hour: {min: 633.1, mid: 462.5, max: 362.0}
|
||||
magic_xp_per_hour: {min: 33554, mid: 24514, max: 19187}
|
||||
steel-bar:
|
||||
bar: Steel bar
|
||||
bar_item_id: 2353
|
||||
@ -165,6 +204,10 @@ recipes:
|
||||
item_id: 453
|
||||
qty: 2
|
||||
source: ge
|
||||
rates:
|
||||
capacity: 9
|
||||
casts_per_hour: {min: 355.3, mid: 249.1, max: 191.2}
|
||||
magic_xp_per_hour: {min: 18833, mid: 13204, max: 10131}
|
||||
gold-bar:
|
||||
bar: Gold bar
|
||||
bar_item_id: 2357
|
||||
@ -176,6 +219,10 @@ recipes:
|
||||
item_id: 444
|
||||
qty: 1
|
||||
source: ge
|
||||
rates:
|
||||
capacity: 27
|
||||
casts_per_hour: {min: 633.1, mid: 462.5, max: 362.0}
|
||||
magic_xp_per_hour: {min: 33554, mid: 24514, max: 19187}
|
||||
mithril-bar:
|
||||
bar: Mithril bar
|
||||
bar_item_id: 2359
|
||||
@ -191,6 +238,10 @@ recipes:
|
||||
item_id: 453
|
||||
qty: 4
|
||||
source: ge
|
||||
rates:
|
||||
capacity: 5
|
||||
casts_per_hour: {min: 232.8, mid: 160.4, max: 122.0}
|
||||
magic_xp_per_hour: {min: 12338, mid: 8499, max: 6468}
|
||||
adamantite-bar:
|
||||
bar: Adamantite bar
|
||||
bar_item_id: 2361
|
||||
@ -206,6 +257,10 @@ recipes:
|
||||
item_id: 453
|
||||
qty: 6
|
||||
source: ge
|
||||
rates:
|
||||
capacity: 3
|
||||
casts_per_hour: {min: 153.4, mid: 104.5, max: 79.1}
|
||||
magic_xp_per_hour: {min: 8131, mid: 5538, max: 4194}
|
||||
runite-bar:
|
||||
bar: Runite bar
|
||||
bar_item_id: 2363
|
||||
@ -221,42 +276,78 @@ recipes:
|
||||
item_id: 453
|
||||
qty: 8
|
||||
source: ge
|
||||
rates:
|
||||
capacity: 3
|
||||
casts_per_hour: {min: 153.4, mid: 104.5, max: 79.1}
|
||||
magic_xp_per_hour: {min: 8131, mid: 5538, max: 4194}
|
||||
|
||||
# Explicit activity/rate discriminator (owner guidance 2026-08-28, bounce 4):
|
||||
# this entry OPTS IN to the derived-rate gate; every rate below derives from
|
||||
# the entry's own declared lifecycle, and the validator re-derives it.
|
||||
rate_model:
|
||||
anchor_tier: sii # xp_per_hour pins the SII tier's realized rate
|
||||
anchor_recipe: steel-bar # the anchor recipe (must be a declared recipe)
|
||||
cast_ticks: 3 # LA/SII/HLA are 3-tick casts -> 1.8 s (0.6 s/tick)
|
||||
restock_trip_s: [60, 120] # GE-adjacent booth trip per restock
|
||||
session_horizon_s: 7200 # 2 h session; startup is amortised over it
|
||||
startup_s: [300, 600] # one-time GE acquisition + loadout
|
||||
alch_carry_batch: 1000 # LA/HLA tier: stacked candidates + natures
|
||||
alch_restock_threshold: 100 # -> 901-cast LA/HLA restock cadence
|
||||
sii_capacity_slots: 27 # 28-slot inventory minus the nature stack
|
||||
|
||||
state_machine:
|
||||
initial: idle
|
||||
terminal: [complete, failed]
|
||||
states:
|
||||
idle:
|
||||
goal: "acquire + equip the staff of fire, GE-buy nature runes and the tier supplies (alch candidates, or the selected recipe's exact inputs), build the starting inventory to its carried_batch"
|
||||
next: [cast, complete, failed]
|
||||
goal: "session start at the GE bank booth; choose the session Magic target (default 55 — completes at the LA/SII -> HLA handoff; above 55 the HLA tier runs to the target). Open GE (a2), place the initial offers (staff a3, tier supplies a4), record initial capital in the cash ledger (total assets = cash + supplies at current buy cost). Nothing is bought or collected without an open GE interface"
|
||||
next: [wait_offers, complete, failed]
|
||||
wait_offers:
|
||||
goal: "wait for the pending offers (ge_offers_done); when filled, collect every purchase + refund from the collection box (a5 — never leave purchases in the box). PARTIAL FILLS: collect the filled portion, re-place the remainder at the current edge (a3/a4); 3 consecutive no-fill rounds -> failed. An offer still unfilled at session end is left resting by design — its coins are tied up (float reserve) and return when it fills or the operator aborts it (the single operator-touch point)"
|
||||
next: [ready, wait_offers, failed]
|
||||
ready:
|
||||
goal: "equip the staff of fire (a6 — bounded retries, then FAIL CLOSED) and build the loadout to the tier's carried amounts (a7; purchases from a5 are already in the inventory — deposit overflow, withdraw only the shortfall)"
|
||||
next: [cast, failed]
|
||||
cast:
|
||||
goal: "cast the active tier spell with the tier-correct target — LA (< 43) or HLA (>= 55) on a noted alch candidate; SII (43-55) on the selected recipe's declared target_input (primary ore, never coal; exact inputs withdrawn, margin positive); a break fires on the completed-cast counter, NOT on bank visits"
|
||||
goal: "cast the active tier spell with the tier-correct target — LA (< 43, or 43-54 when SII is skipped) or HLA (>= 55, only when the session target is above 55) on a noted alch candidate (a8); SII (43-55, selection.mode) on the selected recipe's declared target_input (primary ore, never coal) with its exact allowed-fill inputs withdrawn (a9). A break fires on the completed-cast counter (a13), NEVER on bank visits, and restocking never resets it"
|
||||
next: [cast, bank_supplies, complete, failed]
|
||||
bank_supplies:
|
||||
goal: "deposit coins and smelted bars; restock to the tier's carried_batch (LA/HLA: candidate + natures to 1,000/1,000; SII: one 24-cast input pack of the selected recipe) and resume casting"
|
||||
next: [cast, failed]
|
||||
goal: "restock to the tier's carried amounts: deposit coins + smelted bars and withdraw ALL tier supplies the bank holds (a10 — bank-stock fast path only when the bank actually holds enough); for the remaining deficit, open GE (a2), buy the deficit (a11, live low + 5%), collect it (a12) — never withdraw a batch that was never purchased. Restocking never resets the cast or break counters"
|
||||
next: [cast, bank_supplies, failed]
|
||||
complete:
|
||||
goal: "magic level target reached (default 55 -> HLA is the end-state)"
|
||||
goal: "magic level >= the configured session target (default 55 — completes at the LA/SII -> HLA handoff; a target above 55 finishes only after the executable HLA tier reaches it)"
|
||||
failed:
|
||||
goal: "hard failure — stop, log ERROR sentinel, alert"
|
||||
transitions:
|
||||
- from: idle
|
||||
when: "staff equipped AND tier supplies at carried_batch (LA/HLA: 1,000 candidate + 1,000 natures; SII: 24 iron-or-primary + 48 coal + 24 natures for the selected recipe)"
|
||||
to: cast
|
||||
when: "staff + tier supply offers all placed (a3/a4)"
|
||||
to: wait_offers
|
||||
- from: idle
|
||||
when: "magic level >= target (55 -> HLA end-state)"
|
||||
when: "magic level >= session target at session start"
|
||||
to: complete
|
||||
- from: idle
|
||||
when: "3 consecutive purchase/equip failures with no fallback available (recovery block)"
|
||||
when: "3 consecutive offer-place failures at session start (no fallback candidate/recipe)"
|
||||
to: failed
|
||||
- from: wait_offers
|
||||
when: "ge_offers_done AND all purchases collected (a5)"
|
||||
to: ready
|
||||
- from: wait_offers
|
||||
when: "partial fill: filled portion collected (a5), remainder re-placed at the current edge (a3/a4) — still waiting"
|
||||
to: wait_offers
|
||||
- from: wait_offers
|
||||
when: "3 consecutive no-fill rounds (offer cannot fill at the accepted edge)"
|
||||
to: failed
|
||||
- from: ready
|
||||
when: "staff equipped (a6) AND loadout at the tier's carried amounts (a7)"
|
||||
to: cast
|
||||
- from: ready
|
||||
when: "staff equip failed after bounded retries — FAIL CLOSED"
|
||||
to: failed
|
||||
- from: cast
|
||||
when: "LA/HLA tier: nature count < restock_threshold (100) OR candidate count < restock_threshold (100); SII tier: bars_in_inventory >= 24 (non-stackable bars fill the inventory) OR any selected-recipe input count < 1 — deterministic restock triggers, pinned numbers"
|
||||
to: bank_supplies
|
||||
- from: cast
|
||||
when: "level < 43 and LA tier active (cast on the cheapest alch candidate)"
|
||||
when: "level < 43, or level 43-55 with SII skipped — LA tier: cast on the cheapest alch candidate (LA is executable throughout the 43-54 window when no recipe has a positive margin)"
|
||||
to: cast
|
||||
- from: cast
|
||||
when: "level 43-55 and the SII window is chosen — selection.mode picks the recipes-table entry with the highest smithing_level <= current Smithing and a positive complete-cost margin; cast on its target_input"
|
||||
when: "level 43-55 and the SII window is chosen — selection.mode picks the recipes-table entry with the highest smithing_level <= current Smithing and a positive allowed-fill complete-cost margin; cast on its target_input"
|
||||
to: cast
|
||||
- from: cast
|
||||
when: "level 43-55 — SII fallback: the selected recipe's materials cannot be obtained, so drop to the next lower unlocked recipe (never an unmet Smithing gate) and continue SII if its margin is positive"
|
||||
@ -265,17 +356,26 @@ state_machine:
|
||||
when: "level 43-55 and no unlocked recipe has a positive margin this window — skip SII and stay on LA"
|
||||
to: cast
|
||||
- from: cast
|
||||
when: "level >= 55 and HLA tier active (cast on the cheapest alch candidate)"
|
||||
when: "level >= 55 and session target > 55 — the HLA tier executes (a8) until magic reaches the target"
|
||||
to: cast
|
||||
- from: cast
|
||||
when: "magic level >= target (55 -> HLA end-state)"
|
||||
when: "LA/HLA tier: candidate count < restock_threshold (100) OR nature count < restock_threshold (100); SII tier: bars_in_inventory >= the selected recipe's capacity (floor(27 / total input qty): steel 9, bronze 13, mithril 5, ...) OR any selected-recipe input < 1 — deterministic restock triggers, pinned numbers"
|
||||
to: bank_supplies
|
||||
- from: cast
|
||||
when: "magic level >= session target (default 55 — completes at the LA/SII -> HLA handoff; targets above 55 require the executable HLA tier)"
|
||||
to: complete
|
||||
- from: cast
|
||||
when: "3 consecutive failed casts (client issue — do not loop)"
|
||||
to: failed
|
||||
- from: bank_supplies
|
||||
when: "restocked to the tier's carried_batch (see bank_supplies goal)"
|
||||
when: "restocked to the tier's carried amounts (bank fast path or re-bought deficit collected)"
|
||||
to: cast
|
||||
- from: bank_supplies
|
||||
when: "restock partial: deficit re-bought but offers still filling (ge_offers_done false) — loop the restock phase"
|
||||
to: bank_supplies
|
||||
- from: bank_supplies
|
||||
when: "3 consecutive no-fill restock rounds OR a capital stop fires (assets / loss-reserve checks in stop_conditions)"
|
||||
to: failed
|
||||
|
||||
actions:
|
||||
- id: a1
|
||||
@ -290,81 +390,140 @@ actions:
|
||||
delay: [400, 1600]
|
||||
|
||||
- id: a2
|
||||
step: open-ge
|
||||
verb: ge_open
|
||||
target: grand-exchange
|
||||
params:
|
||||
note: "open the GE interface at the booth. The entry NEVER buys (a3/a4/a11) or collects (a5/a12) without an open GE interface — the action grammar defines ge_open, ge_buy, ge_collect as separate verbs, so the open step is explicit"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1400]
|
||||
|
||||
- id: a3
|
||||
step: buy-staff
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "staff of fire"
|
||||
qty: 1
|
||||
max_price: "live low + 25% (wiki value 1,500; hard cap 5,000)"
|
||||
on_fail: retry # retry once; on a second failure use the fire-rune fallback (see a3 note / recovery block)
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a3
|
||||
step: equip-staff
|
||||
verb: menu_option
|
||||
target: "staff of fire in inventory"
|
||||
params:
|
||||
option: "Wield"
|
||||
note: "Equip requirement is NONE documented on the OSRS wiki Staff of fire page (no requirements row in the infobox, none in the prose; compare Battlestaff 'requires 30 Attack and 30 Magic'; verified 2026-08-28). FAILURE PATH — if Wield is unavailable or the staff cannot be equipped, fall back to buying fire runes per cast (LA/HLA: 3 fire + 1 nature = 155 gp vs 140; SII: 4 fire + 1 nature = 160 gp vs 140 — the recipe margin rule compares against the FULL rune cost), log WARN and continue; never loop on equip attempts."
|
||||
max_price: "live low + 25 % (wiki value 1,500; hard cap 5,000)"
|
||||
note: "offer rests until filled or the run ends; on a partial/no fill, collect what filled (a5) and re-place the remainder at the current edge — bounded retries, then the run fails closed (no fire-rune fallback exists)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1800]
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a4
|
||||
step: buy-supplies
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "nature rune + the tier's supplies — LA/HLA: the current least-loss alch candidate; SII: the exact unnoted inputs of the SELECTED recipe (every input and quantity from the recipes table, e.g. steel-bar = 1 iron ore + 2 coal), only while its complete-cost margin is positive"
|
||||
qty: "LA/HLA: carried_batch 1,000 (4h GE limits: natures 18,000, candidates per mapping); SII: one 24-cast input pack (24 x each input quantity + 24 natures) per bank cycle"
|
||||
max_price: "live low + 5%"
|
||||
on_fail: retry # re-place at live low +5% with paced offers; 3 consecutive failures -> fallback to next unlocked recipe (SII) or -> failed (recovery block)
|
||||
item: "nature runes + the tier's supplies — LA/HLA: the current least-loss alch candidate; SII: the exact unnoted inputs of the SELECTED recipe (every input and quantity from the recipes table, e.g. steel-bar = 1 iron ore + 2 coal), only while the allowed-fill complete-cost margin is positive"
|
||||
qty: "LA/HLA: carried_batch 1,000 (4h GE limits: natures 18,000, candidates per mapping); SII: one capacity-sized pack of the selected recipe — capacity = floor(27 / total input qty) casts (steel = 9, so 9 x each input quantity + 9 natures per pack)"
|
||||
max_price: "live low + 5 %"
|
||||
note: "offer rests until filled or the run ends; partial fills are collected (a5) and the remainder re-placed at the current edge; 3 consecutive no-fills -> SII recipe fallback, else failed"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a5
|
||||
step: collect-purchases
|
||||
verb: ge_collect
|
||||
target: grand-exchange
|
||||
params:
|
||||
note: "collect every filled purchase and refund from the GE collection box into the inventory — staff, candidates/natures, or the recipe's exact inputs (and the equivalent on every restock). Partial offers: the collected portion is added to the ledger; nothing is left in the box at session start"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1400]
|
||||
|
||||
- id: a6
|
||||
step: equip-staff
|
||||
verb: menu_option
|
||||
target: "staff of fire in inventory"
|
||||
params:
|
||||
option: "Wield"
|
||||
note: "Equip requirement is NONE documented on the OSRS wiki Staff of fire page (no requirements row in the infobox, none in the prose; compare Battlestaff 'requires 30 Attack and 30 Magic'; verified 2026-08-28). FAIL-CLOSED path: bounded retries (3), then log ERROR + alert + stop — no fire-rune fallback is modeled (never loop on equip attempts)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1800]
|
||||
|
||||
- id: a7
|
||||
step: build-loadout
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
deposit: [overflow products]
|
||||
withdraw: "to the tier's carried amounts (only the shortfall — collected purchases from a5 are already in the inventory): LA/HLA 1,000 candidate + 1,000 nature; SII one capacity-sized pack of the selected recipe (steel: 9 iron + 18 coal + 9 natures)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [500, 1100]
|
||||
|
||||
- id: a8
|
||||
step: cast-alch
|
||||
verb: use_item_on
|
||||
target: "noted alch candidate in inventory (LA and HLA tiers)"
|
||||
params:
|
||||
spell: "Low Level Alchemy (level < 43) | High Level Alchemy (level >= 55)"
|
||||
spell: "Low Level Alchemy (level < 43, or 43-54 when SII is skipped) | High Level Alchemy (level >= 55 — only when the session target is above 55; HLA is never declared-but-unreachable)"
|
||||
item: "cheapest live alch candidate in inventory — LA/HLA are alchemy tiers, so the selection rule is the alch-candidate rule"
|
||||
on: "itself"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 3.4-5.2s, mid 4.3s -> realized 791.3 casts/hr incl. breaks + banking (pinned in economy)
|
||||
delay: [1600, 3400] # MANDATORY post-action sleep, ADDITIVE to the 1.8s cast (3 ticks x 0.6s; action-grammar.md:135): cycle = 3.4-5.2s, mid 4.3s -> LA/HLA session-realized ~742 casts/hr incl. startup + restock + breaks (rate_model)
|
||||
|
||||
- id: a6
|
||||
- id: a9
|
||||
step: cast-superheat
|
||||
verb: use_item_on
|
||||
target: "the selected recipe's declared target_input in inventory (the primary ore — never coal; SII tier only, 43-55)"
|
||||
params:
|
||||
spell: "Superheat Item"
|
||||
selected_recipe: "runtime selection per selection.mode: the recipes-table entry with the highest smithing_level <= current Smithing whose complete input cost (ALL inputs, live GE buy) + one nature rune < the bar's live GE price; fall back to the next lower unlocked recipe when its materials cannot be obtained"
|
||||
selected_recipe: "runtime selection per selection.mode: the recipes-table entry with the highest smithing_level <= current Smithing whose complete input cost at ALLOWED fill prices + one nature rune at allowed fill < the bar's live GE price; fall back to the next lower unlocked recipe when its materials cannot be obtained"
|
||||
target_input: "the selected recipe's declared target_input field (primary ore; coal can never be the spell target)"
|
||||
on: "itself"
|
||||
consumption: "1 x each recipe input (e.g. steel = 1 iron ore + 2 coal) + 1 nature per cast -> 1 bar, +53 Magic xp + the bar's Smithing xp; the pack holds capacity = floor(27 / total input qty) casts (steel = 9)"
|
||||
on_fail: skip # no unlocked positive-margin recipe -> skip SII, stay on LA (selection.fallback)
|
||||
anti_ban:
|
||||
delay: [1600, 3400] # post-action sleep (additive): same 3.4-5.2s/cast as a5; SII realized 518.0 casts/hr incl. 24-cast bank cycles + breaks (pinned in economy)
|
||||
delay: [1600, 3400] # post-action sleep (additive): same 3.4-5.2s/cast as a8; per-recipe realized rates in recipes.rates (steel mid 249.1 casts/hr steady, session anchor 12,400 xp/hr)
|
||||
|
||||
- id: a7
|
||||
step: bank-loop
|
||||
- id: a10
|
||||
step: restock-deposit
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
deposit: [all coins, all products] # products incl. smelted bars during SII
|
||||
withdraw: "LA/HLA: candidate + nature runes up to carried_batch (1,000/1,000); SII: one 24-cast input pack of the selected recipe (24 x each input quantity + 24 natures)"
|
||||
withdraw: "ALL tier supplies currently in the bank, up to the carried amounts (LA/HLA 1,000/1,000; SII one capacity-sized pack of the selected recipe) — the bank-stock FAST PATH applies only when the bank actually holds enough; the remaining deficit is bought at the GE (a11) and collected (a12)"
|
||||
on_fail: alert
|
||||
anti_ban:
|
||||
delay: [500, 1100]
|
||||
|
||||
- id: a8
|
||||
- id: a11
|
||||
step: restock-buy
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "the tier's supplies to top up to the carried amounts"
|
||||
qty: "deficit = carried amount - inventory count after the a10 fast path (LA/HLA: 901 candidate + 901 natures after a 901-cast batch with an empty bank; SII: the selected recipe's capacity-sized pack minus whatever the bank held)"
|
||||
max_price: "live low + 5 %"
|
||||
note: "the deficit is RE-BOUGHT at the GE on every restock — the entry never withdraws a batch that was never purchased; partial/failed fills use the same collect + re-place recovery as the initial acquisition"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a12
|
||||
step: restock-collect
|
||||
verb: ge_collect
|
||||
target: grand-exchange
|
||||
params:
|
||||
note: "collect the re-bought deficit into the inventory — carried amounts are restored before casting resumes"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1400]
|
||||
|
||||
- id: a13
|
||||
step: take-break
|
||||
verb: take_break
|
||||
target: "current location"
|
||||
params:
|
||||
duration: "[45, 120] seconds of idle AFK per break (matches run_control.break.for)"
|
||||
note: "executed by the translator when the completed-cast counter reaches run_control.break.every [400, 700] casts — this IS run_control.break's executor, fired on CAST COUNTS, not on bank visits; the cast counter is not reset by restocking"
|
||||
note: "executed by the translator when the completed-cast counter reaches run_control.break.every [400, 700] casts — this IS run_control.break's executor, fired on CAST COUNTS, not on bank visits; the cast counter is NOT reset by restocking (breaks land at 550/1,100/1,650 in the midpoint example, independent of the 901-cast alch / capacity-sized SII restock cadences)"
|
||||
on_fail: skip
|
||||
|
||||
run_control:
|
||||
@ -378,79 +537,105 @@ run_control:
|
||||
# reset this counter (owner decision 2026-08-28)
|
||||
for: [45, 120] # seconds of idle AFK
|
||||
stop_conditions:
|
||||
- "magic level >= target (default 55 -> HLA is the end-state)"
|
||||
- "gp under floor (below one full carried_batch of natures + tier supplies)"
|
||||
- "magic level >= session target (default 55: completes at the LA/SII -> HLA handoff; when the configured target is above 55, the HLA tier executes from 55 until the target — HLA is never declared-but-unreachable)"
|
||||
- "session elapsed >= 2 h (session horizon — the rate model amortises startup over it)"
|
||||
- "total assets (cash + supplies at current buy cost) < the next tier batch at max fill plus staff replacement (LA/HLA: 312,650 = 307,650 + 5,000; SII steel: 10,150 = 5,150 + 5,000) — assets, NOT raw cash: cash that became supplies is not a loss"
|
||||
- "session net loss (total assets - initial capital) > 690,075 — the 6-batch worst-case LA reserve (Magic 21 -> 55 at max fills) is exhausted"
|
||||
- "3 consecutive failed casts (client issue — do not loop)"
|
||||
- "3 consecutive failed GE purchases with no fallback recipe/candidate available"
|
||||
- "3 consecutive no-fill GE rounds (initial acquisition or restock) with no fallback recipe/candidate available"
|
||||
- "staff of fire equip failed after bounded retries — FAIL CLOSED (log ERROR, alert, stop)"
|
||||
|
||||
# ================= WORKED EXAMPLE (pinned, deterministic — owner guidance 2026-08-28) =================
|
||||
# SII tier, steel-bar path (Magic 43-54, Smithing 30). Constants: break.every
|
||||
# [400, 700] casts (midpoint 550), break.for [45, 120] s; SII bank cycle = 24
|
||||
# casts (bars_in_inventory >= 24 or any input < 1 -> bank_supplies); LA/HLA use
|
||||
# the 901-cast batch model pinned in low-alch-f2p (carried_batch 1,000/1,000,
|
||||
# restock_threshold 100).
|
||||
# SII tier, steel-bar path (Magic 43-54, Smithing 30, session target 55).
|
||||
# Constants: break.every [400, 700] casts (the example pins the midpoint
|
||||
# 550), break.for [45, 120] s; steel capacity = floor(27 / (1 iron + 2 coal))
|
||||
# = 9 casts per bank cycle — restocks every 9 casts; the counter sequences
|
||||
# are MONOTONIC and INDEPENDENT (restocking never resets the break counter).
|
||||
#
|
||||
# Step 0 — prep, before the first inventory: account has 30 Smithing + 43-54
|
||||
# Magic; GE-buy staff of fire and Wield it (no documented requirement); buy
|
||||
# 1,000 nature runes and steel inputs (1 iron ore + 2 coal per cast, live GE).
|
||||
# Build the starting inventory: 24 iron ore + 48 coal + 24 natures (one
|
||||
# 24-cast pack). Cast counter := 0. Steel is the highest UNLOCKED recipe
|
||||
# (Smithing 30); bronze (1), iron (15) and silver (20) are unlocked too but
|
||||
# LOWER, so steel is selected. Smithing 20 accounts select silver-bar and
|
||||
# steel stays locked (never an unmet gate).
|
||||
# Step 1 — casts #1..#24: each cast consumes 1 iron ore + 2 coal + 1 nature
|
||||
# and produces 1 steel bar (+53 Magic xp, +17.5 Smithing xp). After cast #24
|
||||
# the inventory holds 24 bars -> FIRST RESTOCK/BANK EVENT at cast 24: deposit
|
||||
# the 24 bars, withdraw the next 24 iron + 48 coal + 24 natures.
|
||||
# Step 2 — resume at cast #25; the 24-cast cycle repeats: restocks at casts
|
||||
# 48, 72, 96, ... (every 24 casts, deterministic).
|
||||
# Step 3 — FIRST SCHEDULED BREAK at cast 550 (midpoint of break.every
|
||||
# [400, 700]): the 23rd cycle (casts 529-552) is mid-flight — take-break for
|
||||
# 45-120 s, resume at cast #551, finish the cycle (restock at 552).
|
||||
# The break fired on the CAST COUNTER while a bank cycle was still running:
|
||||
# breaks and banking are independent by construction.
|
||||
# Step 4 — break #2 at cast 1,100 (550 after #1); restocks continue every 24
|
||||
# casts; the SII window ends at Magic 55 -> HLA tier (901-cast batch model,
|
||||
# 65 xp/cast). Stop at the target level, gp floor, or 3 failed casts.
|
||||
# Step 0 — prep (one-time startup ~300-600 s): account has 30 Smithing +
|
||||
# 43-54 Magic and the LA/HLA-tier capital (>= ~1.31M gp). At the GE bank:
|
||||
# choose the session target (55); open GE (a2); place offers — staff (a3),
|
||||
# 1,000 nature runes + LA candidate OR - for the SII window - the selected
|
||||
# recipe's first pack (steel: 9 iron ore + 18 coal + 9 natures, a4); wait
|
||||
# for fills (ge_offers_done) and collect everything (a5); equip the staff
|
||||
# (a6); loadout = 9 iron + 18 coal + 9 natures (a7). Cast counter := 0.
|
||||
# Steel is the highest UNLOCKED recipe (Smithing 30); bronze (1), iron (15)
|
||||
# and silver (20) are unlocked but LOWER, so steel is selected. Smithing 20
|
||||
# accounts select silver-bar (capacity 27) and steel stays locked (never an
|
||||
# unmet gate).
|
||||
# Step 1 — casts #1..#9: each cast consumes 1 iron ore + 2 coal + 1 nature
|
||||
# and produces 1 steel bar (+53 Magic xp, +17.5 Smithing xp). After cast #9
|
||||
# the inventory holds 9 bars (capacity reached) -> FIRST RESTOCK at cast 9:
|
||||
# deposit the 9 bars (a10); the bank holds 0 steel inputs (all sales were
|
||||
# GE buys), so the fast path yields nothing -> open GE (a2), RE-BUY the
|
||||
# pack deficit 9 iron + 18 coal + 9 natures (a11, live low + 5 %), collect
|
||||
# (a12) -> inventory rebuilt. Restocks repeat every 9 casts (9, 18, 27, ...,
|
||||
# and at 558 after the first break below) — capacity-derived, deterministic.
|
||||
# Step 2 — FIRST SCHEDULED BREAK at cast 550 (midpoint of break.every
|
||||
# [400, 700]): the 61st cycle is mid-flight — take-break 45-120 s (a13),
|
||||
# resume at cast 551, finish the cycle (restock at 558). The break fired on
|
||||
# the CAST COUNTER while a bank cycle was still running: breaks and banking
|
||||
# are independent by construction, and restocking did NOT shift the break.
|
||||
# Step 3 — break #2 at cast 1,100 (exactly 550 after #1 — monotonic,
|
||||
# unaffected by the 9-cast restock cadence); break #3 at 1,650; restocks
|
||||
# continue at 1,107, 1,116, ... (every 9). The SII window ends at Magic 55:
|
||||
# with the default target 55 the session completes at the LA/SII -> HLA
|
||||
# handoff; with a target above 55 the HLA tier (a8, 65 xp/cast, 901-cast
|
||||
# batch model) executes until the target.
|
||||
# Fallback chain pinned: steel inputs unfillable at the GE (3 failed offers)
|
||||
# -> drop to silver-bar (20, next lower unlocked) -> if silver is also
|
||||
# unfillable or margin-negative -> skip SII this window and stay on LA.
|
||||
# unfillable or margin-negative at allowed fills -> skip SII this window and
|
||||
# stay on LA throughout 43-54 (a8 handles the SII-skip window explicitly).
|
||||
# =====================================================================================================
|
||||
|
||||
risk:
|
||||
ban_level: low
|
||||
notes:
|
||||
- "3-tick cast cadence is the main detection vector; keep anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4]s; realized ~791 casts/hr alch tiers and ~518 casts/hr SII incl. breaks + banking)."
|
||||
- "Breaks are CAST-COUNT-based (every [400, 700] completed casts, 45-120s AFK) and independent of bank visits — never treat a restock trip as a break."
|
||||
- "3-tick cast cadence is the main detection vector; keep anti_ban delays (cadence pin: additive post-cast sleep [1.6, 3.4]s; session-realized ~742 casts/hr on the alch tiers and ~234 casts/hr on the steel SII anchor, incl. startup + restock + breaks over a 2 h session — loop-only 837 is a ceiling reference, never the output)."
|
||||
- "Breaks are CAST-COUNT-based (every [400, 700] completed casts, 45-120s AFK) and independent of bank visits — never treat a restock trip as a break, and never reset the break counter on restock (break interval 550 and restock cadences 901/9 are coprime by construction)."
|
||||
- "The valuable-item warning fires on item GE value (not alch value) — the script must handle the warning widget when alching high-value items (HLA page)."
|
||||
- "SII: withdraw the exact recipe inputs (unnoted, per the recipes table) and cast on the declared target_input (primary ore) — never on coal, never on a noted stack; bank the produced bars when bars_in_inventory >= 24."
|
||||
- "SII ore-buy bursts are pace-sensitive like any GE buy; respect the 4h limit window; re-check the complete-cost margin per window because ore/bar prices move independently."
|
||||
- "SII: withdraw the EXACT recipe inputs (unnoted, per the recipes table) sized to the derived capacity (floor(27 / input qty)) and cast on the declared target_input (primary ore) — never on coal, never on a noted stack; bank the produced bars when bars_in_inventory >= capacity."
|
||||
- "SII ore-buy bursts are pace-sensitive like any GE buy; respect the 4h limit window; re-check the allowed-fill complete-cost margin per window because ore/bar prices move independently."
|
||||
|
||||
# --- recovery block (owner guidance 2026-08-28: every activity must cover its complete runnable lifecycle) ---
|
||||
# - Missing materials / depleted supplies: LA/HLA restock at candidate OR nature
|
||||
# < 100 (carried_batch 1,000/1,000); SII restocks at bars >= 24 or any selected-
|
||||
# recipe input < 1 — both are deterministic threshold transitions to bank_supplies.
|
||||
# - Failed GE purchases (offer unfilled): re-place at live low + 5% with paced
|
||||
# offers; after 3 consecutive failures in the SII window, fall back to the next
|
||||
# lower unlocked recipe (selection.fallback); with no fallback recipe left,
|
||||
# skip SII and stay on LA — never sit in an unfillable offer loop.
|
||||
# - Staff of fire cannot be equipped (a3): fall back to fire runes (LA/HLA: +15
|
||||
# gp/cast; SII: +20 gp/cast rune cost), log WARN, continue; never loop on equip
|
||||
# attempts.
|
||||
# - Invalid loadout (wrong item in hand / spell unavailable): re-run build-loadout
|
||||
# (a4/a7); 3 consecutive failed casts -> failed (stop, log ERROR sentinel, alert).
|
||||
# - Explicit completion/stop: stop_conditions above (Magic 55 HLA end-state, gp
|
||||
# floor, repeated failures).
|
||||
# < 100 (carried_batch 1,000/1,000); SII restocks at bars >= the selected
|
||||
# recipe's capacity (floor(27 / input qty)) or any selected-recipe input < 1 —
|
||||
# deterministic threshold transitions to bank_supplies; the restock phase
|
||||
# (a10 -> a2 -> a11 -> a12) RE-BUYS the deficit at the GE — never withdraw a
|
||||
# batch that was never purchased.
|
||||
# - GE offers: partial fills are collected (a5/a12) and the remainder re-placed
|
||||
# at the current edge (a3/a4/a11); after 3 consecutive no-fill rounds (initial
|
||||
# or restock) the SII recipe falls back to the next lower unlocked recipe;
|
||||
# with no fallback recipe left, SII is skipped and LA takes over; with no
|
||||
# fallback candidate, the run stops (failed). Unfilled offers at session end
|
||||
# are left resting by design (float reserve; coins return when they fill or
|
||||
# the operator aborts them — the single operator-touch point).
|
||||
# - Staff of fire cannot be equipped (a6): bounded retries (3), then FAIL
|
||||
# CLOSED — log ERROR + alert + stop. No fire-rune fallback is modeled (it
|
||||
# would need its own purchase/supply/rate path; the owner decision rejects a
|
||||
# prose-only fallback).
|
||||
# - Invalid loadout (wrong item in hand / spell unavailable): re-run
|
||||
# build-loadout (a7); 3 consecutive failed casts -> failed (stop, log ERROR
|
||||
# sentinel, alert).
|
||||
# - Capital: both capital stops are assets-based (total assets = cash +
|
||||
# supplies at current buy cost) — cash converted into supplies is not a loss
|
||||
# and never fires a stop by itself (see stop_conditions).
|
||||
# - Explicit completion/stop: stop_conditions above (session target 55 or the
|
||||
# >55 HLA tier, 2 h session horizon, assets floor 312,650 / 10,150, loss
|
||||
# reserve 690,075, repeated failures, fail-closed staff equip).
|
||||
|
||||
sources:
|
||||
- "OSRS wiki — Low Level Alchemy (21 Magic, 31 xp/cast, members=No, 3-tick, 40% of item value; 'recommended to use High Level Alchemy instead' at 55 Magic)"
|
||||
- "OSRS wiki — Superheat Item (43 Magic, 53 xp/cast, members=No; smelts ore into bars without a furnace; cannot be cast on noted ore; grants the bar's Smithing XP; requires the bar's Smithing level and materials)"
|
||||
- "OSRS wiki — High Level Alchemy (55 Magic, 65 xp/cast, members=No, 60% of item value; GE buying for break-even/profit; noted-item casting; valuable-item warning)"
|
||||
- "OSRS wiki — Smithing (bar smelting table: smithing levels, per-bar XP and ore/coal quantities for bronze/iron/silver/steel/gold/mithril/adamantite/runite)"
|
||||
- "OSRS wiki — Grand Exchange (offers persist until filled or aborted; filled items and coin refunds are collected from the booth/collection box; partial fills are separately collectable) — verified 2026-08-28"
|
||||
- "OSRS wiki — Grand Exchange/Buying limits (4-hour window; the limit resets 4 hours after the FIRST unit is bought; outstanding offers remain active and a limit reset is NOT an offer expiry) — verified 2026-08-28"
|
||||
- "prices.runescape.wiki/api/v1/osrs/mapping (id 561 nature rune: members=false, limit 18000, value 180, lowalch 72; limit cross-checked against Module:Exchange/Nature_rune — agree)"
|
||||
- "prices.runescape.wiki/api/v1/osrs/latest (nature rune buy 145, snapshot 2026-08-27; steel-bar fixture prices: iron ore 100, coal 150, steel bar 600)"
|
||||
- "OSRS wiki — Staff of fire (page verified 2026-08-28 for the equip requirement: id 1387, F2P item, tradeable, options Wield — NO requirements row in the Infobox Item and NO requirement statement in the prose; contrast Battlestaff, whose prose states 'requires 30 Attack and 30 Magic to wield')"
|
||||
- "docs/schema.md run_control (iteration unit; break.every counts iterations) + docs/action-grammar.md:135 (anti_ban.delay = additive post-action sleep) + verb take_break — the cadence + break + lifecycle model; owner guidance 2026-08-28 (dynamic recipe selection, realized rates, worked example)"
|
||||
- "docs/schema.md run_control (iteration unit; break.every counts iterations) + docs/action-grammar.md:135 (anti_ban.delay = additive post-action sleep) + verbs ge_open/ge_buy/ge_collect/ge_offers_done (separate GE primitives) — the cadence + break + GE + lifecycle model; owner guidance 2026-08-28 (capacity-derived SII, rate_model, configurable target, fail-closed staff)"
|
||||
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27
|
||||
|
||||
related:
|
||||
|
||||
@ -101,11 +101,66 @@
|
||||
"source": { "enum": ["ge", "p2p", "drops", "craft", "bank", "npc-shop"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
"rates": {
|
||||
"type": "object",
|
||||
"required": ["capacity", "casts_per_hour", "magic_xp_per_hour"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"capacity": { "type": "integer", "minimum": 1 },
|
||||
"casts_per_hour": {
|
||||
"type": "object",
|
||||
"required": ["min", "mid", "max"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"min": { "type": "number", "minimum": 0 },
|
||||
"mid": { "type": "number", "minimum": 0 },
|
||||
"max": { "type": "number", "minimum": 0 }
|
||||
}
|
||||
},
|
||||
"magic_xp_per_hour": {
|
||||
"type": "object",
|
||||
"required": ["min", "mid", "max"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"min": { "type": "number", "minimum": 0 },
|
||||
"mid": { "type": "number", "minimum": 0 },
|
||||
"max": { "type": "number", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"rate_model": {
|
||||
"type": "object",
|
||||
"required": ["anchor_tier", "cast_ticks", "restock_trip_s", "session_horizon_s"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"anchor_tier": { "enum": ["la", "sii", "hla"] },
|
||||
"anchor_recipe": { "type": "string" },
|
||||
"cast_ticks": { "type": "integer", "minimum": 1 },
|
||||
"restock_trip_s": {
|
||||
"type": "array",
|
||||
"items": { "type": "number", "minimum": 0 },
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"session_horizon_s": { "type": "integer", "minimum": 1 },
|
||||
"startup_s": {
|
||||
"type": "array",
|
||||
"items": { "type": "number", "minimum": 0 },
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"alch_carry_batch": { "type": "integer", "minimum": 1 },
|
||||
"alch_restock_threshold": { "type": "integer", "minimum": 0 },
|
||||
"sii_capacity_slots": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
},
|
||||
|
||||
"best_practice": {
|
||||
"type": "object",
|
||||
"required": ["equipment"],
|
||||
|
||||
@ -23,10 +23,17 @@ Runs:
|
||||
`target_input` that is one of its declared inputs and never coal
|
||||
(the spell targets the primary ore), and smithing_level gates must
|
||||
be unique so "highest unlocked" selection is deterministic.
|
||||
- Realized-rate consistency: xp_per_hour must derive from the pinned
|
||||
realized cadence (mandatory additive delays PLUS breaks PLUS
|
||||
banking), not from the loop-only 837 casts/hr or the superseded
|
||||
8-14-cast-break regime (~305 casts/hr).
|
||||
- Realized-rate consistency (owner guidance 2026-08-28, bounce 4):
|
||||
entries that DECLARE a `rate_model` must have xp_per_hour derived
|
||||
from their OWN declared lifecycle — cast ticks + the casting
|
||||
action's mandatory anti_ban.delay (additive), run_control breaks,
|
||||
per-recipe inventory capacity (floor(slots / total input qty)),
|
||||
restock trips, and startup amortised over the session horizon.
|
||||
The gate is behind an explicit discriminator: entries without
|
||||
`rate_model` (e.g. mining) are never treated as alchemy. Every
|
||||
declared SII recipe must pin `rates` (capacity + min/mid/max
|
||||
casts/hr + Magic xp/hr) matching the derived model, and capacity
|
||||
must fit a 28-slot inventory (1 nature stack -> 27 input slots).
|
||||
3. Global id uniqueness across the whole codex tree.
|
||||
|
||||
Malformed/scalar YAML documents (an empty doc, a bare string, a list)
|
||||
@ -42,6 +49,7 @@ Exit codes: 0 = clean (warnings ok), 1 = validation errors.
|
||||
Usage: python3 tools/validate.py [--stale-days 30] [--selftest]
|
||||
--selftest: run the regression fixtures only
|
||||
"""
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
@ -143,21 +151,240 @@ def recipe_margin(recipe, bar_price, input_prices, nature_price):
|
||||
return bar_price - (cost + nature_price)
|
||||
|
||||
|
||||
# --- realized-cadence constants (pinned model; owner guidance 2026-08-28) ---
|
||||
# docs/action-grammar.md:135: anti_ban.delay is an ADDITIVE post-action sleep,
|
||||
# so a cast = 1.8s (3 ticks x 0.6s) + [1.6, 3.4]s = 3.4-5.2s (mid 4.3s).
|
||||
# LA/HLA realized casts/hr: 901-cast restock batch (carried_batch 1,000 -
|
||||
# restock_threshold 100) with 225s overhead per batch (90s restock trip +
|
||||
# 135s breaks at every [400,700] casts, mid 550) -> 791.3 casts/hr midpoint.
|
||||
# SII realized casts/hr: 24-cast bank cycle (non-stackable bars fill the
|
||||
# inventory) -> 518.0 casts/hr midpoint (550-cast break window: 2,365s
|
||||
# casting + 22.9 trips x 60s + 82.5s break).
|
||||
REALIZED_CastsPerHr_ALCH = 791.3
|
||||
REALIZED_CastsPerHr_SII = 518.0
|
||||
# --- rate-model constants (owner guidance 2026-08-28, bounce 4) ---
|
||||
# The cadence is DERIVED from each entry's declared rate_model + actions +
|
||||
# run_control + recipes — nothing is hard-coded per activity class. These
|
||||
# are the only universal facts:
|
||||
# * one game tick = 0.6 s (docs/action-grammar.md);
|
||||
# * anti_ban.delay is an ADDITIVE post-action sleep
|
||||
# (docs/action-grammar.md:135), so cycle = cast_ticks*0.6 + delay;
|
||||
# * LA/SII/HLA Magic XP per cast (wiki infoboxes): 31 / 53 / 65.
|
||||
XP_LA, XP_SII, XP_HLA = 31, 53, 65
|
||||
RATE_TOLERANCE = 0.02 # pinned values may round to 3 significant figures
|
||||
|
||||
|
||||
def _mean(pair):
|
||||
return (pair[0] + pair[1]) / 2.0 if pair else 0.0
|
||||
|
||||
|
||||
def _casting_delay(data):
|
||||
"""The mandatory post-action delay of the entry's casting action(s),
|
||||
converted from ms (anti_ban.delay units) to seconds.
|
||||
|
||||
Derived from the entry's OWN actions: the first action whose step
|
||||
starts with 'cast' and carries an anti_ban.delay pair. Returns
|
||||
(delay_min_s, delay_max_s) or None.
|
||||
"""
|
||||
for a in data.get("actions", []):
|
||||
if not isinstance(a, dict):
|
||||
continue
|
||||
if not str(a.get("step", "")).startswith("cast"):
|
||||
continue
|
||||
delay = (a.get("anti_ban") or {}).get("delay")
|
||||
if isinstance(delay, list) and len(delay) == 2:
|
||||
return float(delay[0]) / 1000.0, float(delay[1]) / 1000.0
|
||||
return None
|
||||
|
||||
|
||||
def _break_amort(data):
|
||||
"""Break time per cast from the entry's OWN run_control.break.
|
||||
|
||||
break.every counts completed casts (one-cast iteration model), so the
|
||||
amortised break share = mean(break.for) / mean(break.every). Fast/slow
|
||||
ends use for_min/every_max and for_max/every_min respectively.
|
||||
"""
|
||||
brk = (data.get("run_control") or {}).get("break") or {}
|
||||
every, for_ = brk.get("every"), brk.get("for")
|
||||
if not (isinstance(every, list) and len(every) == 2
|
||||
and isinstance(for_, list) and len(for_) == 2):
|
||||
return None
|
||||
e_min, e_max = float(every[0]), float(every[1])
|
||||
f_min, f_max = float(for_[0]), float(for_[1])
|
||||
return {
|
||||
"fast": f_min / e_max, "mid": _mean(for_) / _mean(every),
|
||||
"slow": f_max / e_min}
|
||||
|
||||
|
||||
def _session_factor(rm, pace):
|
||||
"""Startup amortisation factor = 1 - startup_s / session_horizon_s.
|
||||
|
||||
pace selects the matching end of the startup range: fast cast pace
|
||||
pairs with the fast (short) startup, slow with the slow one, mid with
|
||||
the midpoint. Returns None when the horizon is missing or startup
|
||||
exceeds the horizon (impossible session)."""
|
||||
startup = rm.get("startup_s") or [300, 600]
|
||||
horizon = rm.get("session_horizon_s")
|
||||
if not isinstance(horizon, (int, float)) or horizon <= 0:
|
||||
return None
|
||||
if not (isinstance(startup, list) and len(startup) == 2):
|
||||
return None
|
||||
s_min, s_max = float(startup[0]), float(startup[1])
|
||||
pick = {"fast": s_min, "mid": _mean(startup), "slow": s_max}[pace]
|
||||
if pick >= horizon:
|
||||
return None
|
||||
return 1.0 - pick / horizon
|
||||
|
||||
|
||||
def recipe_capacity(recipe, slots):
|
||||
"""Inventory capacity of a recipe: floor(slots / total input qty).
|
||||
|
||||
SII bars are non-stackable and one slot holds the nature rune stack,
|
||||
so a 28-slot inventory leaves `slots` (=27) for recipe inputs. A
|
||||
recipe whose inputs exceed the slots cannot exist (capacity 0)."""
|
||||
qty = recipe_total_input_qty(recipe)
|
||||
if qty <= 0:
|
||||
return 0
|
||||
return int(slots // qty)
|
||||
|
||||
|
||||
def _alch_pace_casts_hr(cycle, trip, break_share, batch_casts):
|
||||
"""Steady-state realized casts/hr for an alch tier (LA/HLA), derived
|
||||
from the batch model (carried_batch - restock_threshold casts per
|
||||
restock trip) plus breaks."""
|
||||
batch_s = batch_casts * cycle + trip + break_share * batch_casts
|
||||
if batch_s <= 0:
|
||||
return 0.0
|
||||
return batch_casts * 3600.0 / batch_s
|
||||
|
||||
|
||||
def rate_model_errors(rel, data):
|
||||
"""xp_per_hour and per-recipe rates must derive from the entry's OWN
|
||||
declared lifecycle (owner guidance 2026-08-28, bounce 4).
|
||||
|
||||
The gate is OPT-IN behind an explicit `rate_model` discriminator:
|
||||
entries without one (e.g. a mining entry) are never treated as
|
||||
alchemy. For entries that declare it, the model is derived from the
|
||||
entry's own fields — cast_ticks + the casting action's mandatory
|
||||
anti_ban.delay, run_control.break, restock trips, the recipes table
|
||||
(capacity = floor(slots / total input qty)), and startup amortised
|
||||
over session_horizon_s. Regression coverage (--selftest):
|
||||
non-alchemy pass, loop-only/old-regime failures, cadence-mutation
|
||||
failure (one-hour breaks), per-recipe capacity and rate pins."""
|
||||
|
||||
def err(msg):
|
||||
return f"{rel}: {msg}"
|
||||
|
||||
rm = data.get("rate_model")
|
||||
if not isinstance(rm, dict):
|
||||
return [] # explicit discriminator — unrelated entries pass
|
||||
errors = []
|
||||
tick_s = 0.6
|
||||
cast_ticks = rm.get("cast_ticks")
|
||||
if not isinstance(cast_ticks, (int, float)) or cast_ticks < 1:
|
||||
errors.append(err("rate_model.cast_ticks must be >= 1"))
|
||||
return errors
|
||||
cast_s = cast_ticks * tick_s
|
||||
delay = _casting_delay(data)
|
||||
if delay is None:
|
||||
errors.append(err("rate_model requires a casting action (step 'cast-*') "
|
||||
"with anti_ban.delay [min, max] — the rate derives from "
|
||||
"the entry's own mandatory pacing"))
|
||||
return errors
|
||||
cycle = {"fast": cast_s + delay[0], "mid": cast_s + _mean(delay),
|
||||
"slow": cast_s + delay[1]}
|
||||
break_amort = _break_amort(data)
|
||||
if break_amort is None:
|
||||
errors.append(err("rate_model requires run_control.break with every + for "
|
||||
"ranges (cast-count iteration model)"))
|
||||
return errors
|
||||
trip = rm.get("restock_trip_s")
|
||||
if not (isinstance(trip, list) and len(trip) == 2):
|
||||
errors.append(err("rate_model.restock_trip_s must be [min, max] seconds"))
|
||||
return errors
|
||||
trip = {"fast": float(trip[0]), "mid": _mean(trip), "slow": float(trip[1])}
|
||||
slots = rm.get("sii_capacity_slots", 27)
|
||||
|
||||
# --- per-recipe SII rates (capacity + min/mid/max), fully derived ---
|
||||
recipes = data.get("recipes")
|
||||
if isinstance(recipes, dict):
|
||||
for rid, recipe in recipes.items():
|
||||
if not isinstance(recipe, dict):
|
||||
continue
|
||||
cap = recipe_capacity(recipe, slots)
|
||||
rates = recipe.get("rates")
|
||||
if not isinstance(rates, dict):
|
||||
errors.append(err(f"recipe '{rid}' must pin a rates block "
|
||||
f"(capacity, casts_per_hour, magic_xp_per_hour)"))
|
||||
continue
|
||||
if cap < 1:
|
||||
errors.append(err(f"recipe '{rid}' cannot fit the inventory: "
|
||||
f"total input qty {recipe_total_input_qty(recipe)} "
|
||||
f"> {slots} input slots (capacity 0)"))
|
||||
continue
|
||||
pinned_cap = rates.get("capacity")
|
||||
if pinned_cap != cap:
|
||||
errors.append(err(f"recipe '{rid}' rates.capacity {pinned_cap} != "
|
||||
f"derived floor({slots} / {recipe_total_input_qty(recipe)}) = {cap}"))
|
||||
casts = {}
|
||||
for pace in ("min", "mid", "max"):
|
||||
p = {"min": "fast", "mid": "mid", "max": "slow"}[pace]
|
||||
per_cast = cycle[p] + trip[p] / cap + break_amort[p]
|
||||
casts[pace] = 3600.0 / per_cast if per_cast > 0 else 0.0
|
||||
pin_cph = (rates.get("casts_per_hour") or {})
|
||||
pin_xp = (rates.get("magic_xp_per_hour") or {})
|
||||
for pace, computed in casts.items():
|
||||
expect_xp = computed * XP_SII
|
||||
pc, px = pin_cph.get(pace), pin_xp.get(pace)
|
||||
for label, pinned, expected in (("casts_per_hour", pc, computed),
|
||||
("magic_xp_per_hour", px, expect_xp)):
|
||||
if not isinstance(pinned, (int, float)):
|
||||
errors.append(err(f"recipe '{rid}' rates.{label}.{pace} missing"))
|
||||
elif abs(pinned - expected) / expected > RATE_TOLERANCE:
|
||||
errors.append(err(
|
||||
f"recipe '{rid}' rates.{label}.{pace} {pinned} is not the "
|
||||
f"derived rate ({expected:.1f} from cycle {cycle[p]:.2f}s + "
|
||||
f"trip {trip[p]:.1f}s/{cap} capacity + break {break_amort[p]:.3f}s; "
|
||||
f"tolerance {RATE_TOLERANCE:.0%})"))
|
||||
|
||||
# --- xp_per_hour anchor: the declared anchor tier, same lifecycle ---
|
||||
eco = data.get("economy")
|
||||
xph = eco.get("xp_per_hour") if isinstance(eco, dict) else None
|
||||
if not isinstance(xph, (int, float)):
|
||||
errors.append(err("rate_model requires economy.xp_per_hour to pin the "
|
||||
"anchor tier's realized rate"))
|
||||
return errors
|
||||
anchor = rm.get("anchor_tier")
|
||||
per_cast = {"la": XP_LA, "sii": XP_SII, "hla": XP_HLA}.get(anchor)
|
||||
if per_cast is None:
|
||||
errors.append(err("rate_model.anchor_tier must be la, sii or hla"))
|
||||
return errors
|
||||
if anchor == "sii":
|
||||
rid = rm.get("anchor_recipe")
|
||||
recipe = (recipes or {}).get(rid) if isinstance(recipes, dict) else None
|
||||
if not isinstance(recipe, dict):
|
||||
errors.append(err(f"rate_model.anchor_recipe '{rid}' is not a declared "
|
||||
f"recipe — the anchor must be one of the recipes"))
|
||||
return errors
|
||||
cap = recipe_capacity(recipe, slots)
|
||||
if cap < 1:
|
||||
return errors # already reported as capacity 0
|
||||
casts_mid = 3600.0 / (cycle["mid"] + trip["mid"] / cap + break_amort["mid"])
|
||||
else:
|
||||
batch = rm.get("alch_carry_batch")
|
||||
threshold = rm.get("alch_restock_threshold")
|
||||
if not (isinstance(batch, (int, float)) and batch > 1
|
||||
and isinstance(threshold, (int, float)) and 0 <= threshold < batch):
|
||||
errors.append(err("rate_model.alch_carry_batch and alch_restock_threshold "
|
||||
"are required for la/hla anchors (threshold < batch)"))
|
||||
return errors
|
||||
batch_casts = batch - threshold
|
||||
casts_mid = _alch_pace_casts_hr(cycle["mid"], trip["mid"],
|
||||
break_amort["mid"], batch_casts)
|
||||
factor = _session_factor(rm, "mid")
|
||||
if factor is None:
|
||||
errors.append(err("rate_model startup_s must be < session_horizon_s "
|
||||
"(startup cannot exceed the session)"))
|
||||
return errors
|
||||
expected = per_cast * casts_mid * factor
|
||||
if abs(xph - expected) / expected > RATE_TOLERANCE:
|
||||
errors.append(err(
|
||||
f"xp_per_hour {xph} is not the anchor tier's derived realized rate "
|
||||
f"(expected ~{expected:.0f} = {per_cast} xp/cast x {casts_mid:.1f} "
|
||||
f"steady-state casts/hr x startup factor {factor:.3f} over "
|
||||
f"{rm.get('session_horizon_s')}s; tolerance {RATE_TOLERANCE:.0%})"))
|
||||
return errors
|
||||
|
||||
|
||||
def highest_unlocked_recipe(recipes, smithing_level):
|
||||
"""Deterministic Superheat selection: the declared recipe with the
|
||||
highest smithing_level <= the account's Smithing level, else None.
|
||||
@ -174,6 +401,37 @@ def highest_unlocked_recipe(recipes, smithing_level):
|
||||
return best
|
||||
|
||||
|
||||
def select_recipe(recipes, smithing_level, bar_prices, input_prices,
|
||||
nature_price, fill_factor=1.0):
|
||||
"""Deterministic full selection (owner guidance 2026-08-28, bounce 4):
|
||||
among the recipes whose Smithing gate is unlocked, pick the HIGHEST-gate
|
||||
one whose complete-recipe margin is positive at the given prices.
|
||||
|
||||
fill_factor scales every input and the nature rune price to the ALLOWED
|
||||
fill cost (live low + 5% -> 1.05), so eligibility uses the FULL
|
||||
actual/allowed recipe cost. Returns None when no unlocked recipe has a
|
||||
positive margin — the caller then skips SII and stays on LA (the
|
||||
fallback chain: this helper already drops to the next lower unlocked
|
||||
recipe whenever the highest one is margin-negative)."""
|
||||
unlocked = []
|
||||
for rid, recipe in (recipes or {}).items():
|
||||
if not isinstance(recipe, dict):
|
||||
continue
|
||||
lvl = recipe.get("smithing_level")
|
||||
if isinstance(lvl, int) and lvl <= smithing_level:
|
||||
cost = sum(
|
||||
int(inp.get("qty", 0)) * int(input_prices.get(inp.get("item"), 0))
|
||||
for inp in recipe.get("inputs", []))
|
||||
margin = (bar_prices.get(recipe.get("bar"), 0)
|
||||
- (cost * fill_factor + nature_price * fill_factor))
|
||||
unlocked.append((lvl, rid, margin))
|
||||
unlocked.sort(key=lambda t: t[0], reverse=True)
|
||||
for _lvl, rid, margin in unlocked:
|
||||
if margin > 0:
|
||||
return rid
|
||||
return None
|
||||
|
||||
|
||||
def recipe_model_errors(rel, data):
|
||||
"""SII recipe-model + selection gates (owner guidance 2026-08-28).
|
||||
|
||||
@ -261,37 +519,6 @@ def recipe_model_errors(rel, data):
|
||||
return errors
|
||||
|
||||
|
||||
def rate_errors(rel, data):
|
||||
"""xp_per_hour must derive from the REALIZED cadence: mandatory
|
||||
additive delays PLUS the pinned mandatory breaks PLUS banking.
|
||||
|
||||
Regression guard: the loop-only figure (837 casts/hr) and the
|
||||
superseded 8-14-cast-break regime (~305 casts/hr) both sit outside
|
||||
the tolerance band around the realized rate.
|
||||
"""
|
||||
errors = []
|
||||
eco = data.get("economy")
|
||||
if not isinstance(eco, dict):
|
||||
return errors
|
||||
xph = eco.get("xp_per_hour")
|
||||
if not isinstance(xph, (int, float)):
|
||||
return errors
|
||||
has_sii = any(
|
||||
"superheat" in (str(a.get("params") or {}).lower()
|
||||
+ str(a.get("target") or "").lower()
|
||||
+ str(a.get("step") or "").lower())
|
||||
for a in data.get("actions", []))
|
||||
per_cast = XP_HLA if has_sii else XP_LA
|
||||
expected = per_cast * REALIZED_CastsPerHr_ALCH
|
||||
if abs(xph - expected) / expected > RATE_TOLERANCE:
|
||||
errors.append(
|
||||
f"{rel}: xp_per_hour {xph} is not the realized cadence rate "
|
||||
f"(expected ~{expected:.0f} = {per_cast} xp/cast x "
|
||||
f"{REALIZED_CastsPerHr_ALCH} casts/hr including mandatory breaks "
|
||||
f"and banking; tolerance {RATE_TOLERANCE:.0%})")
|
||||
return errors
|
||||
|
||||
|
||||
def run_selftest(verbose=False):
|
||||
"""Regression fixtures for the cross-field gates; error strings."""
|
||||
errors = []
|
||||
@ -387,6 +614,27 @@ def run_selftest(verbose=False):
|
||||
highest_unlocked_recipe(f2p_bars, 85) == "runite-bar")
|
||||
check("smithing 0 -> None (no recipe selected below the lowest gate)",
|
||||
highest_unlocked_recipe(f2p_bars, 0) is None)
|
||||
# Full selection: Smithing gate + complete-recipe margin at actual and
|
||||
# ALLOWED fill prices, falling back to the next lower unlocked recipe
|
||||
bar_prices = {"Bronze bar": 200, "Iron bar": 380, "Silver bar": 450,
|
||||
"Steel bar": 600, "Runite bar": 12500}
|
||||
input_prices = {"Copper ore": 50, "Tin ore": 30, "Iron ore": 100,
|
||||
"Silver ore": 150, "Coal": 150, "Runite ore": 11000}
|
||||
nature = 145
|
||||
check("full selection at live prices: smithing 30 + steel margin +55 -> steel-bar",
|
||||
select_recipe(f2p_bars, 30, bar_prices, input_prices, nature) == "steel-bar")
|
||||
check("allowed-fill eligibility (x1.05): steel margin +27.75 -> steel-bar still selected",
|
||||
select_recipe(f2p_bars, 30, bar_prices, input_prices, nature,
|
||||
fill_factor=1.05) == "steel-bar")
|
||||
check("material availability: steel margin-negative at allowed fills -> next lower unlocked (silver-bar)",
|
||||
select_recipe(f2p_bars, 30, {**bar_prices, "Steel bar": 570}, input_prices,
|
||||
nature, fill_factor=1.05) == "silver-bar")
|
||||
check("no unlocked recipe with a positive margin -> None (skip SII, stay on LA)",
|
||||
select_recipe(f2p_bars, 30, {"Bronze bar": 10, "Iron bar": 10,
|
||||
"Silver bar": 10, "Steel bar": 10},
|
||||
input_prices, nature) is None)
|
||||
check("an unmet Smithing gate is never selected (smithing 20 -> silver-bar, steel stays locked)",
|
||||
select_recipe(f2p_bars, 20, bar_prices, input_prices, nature) == "silver-bar")
|
||||
|
||||
superheat_action = {
|
||||
"id": "a6", "step": "cast-superheat", "verb": "use_item_on",
|
||||
@ -445,22 +693,87 @@ def run_selftest(verbose=False):
|
||||
check("Superheat entry without an SII fallback transition is rejected",
|
||||
any("SII fallback" in e for e in no_fb))
|
||||
|
||||
# --- realized-rate fixtures: breaks + banking included ---
|
||||
check("LA-only xp_per_hour 24,500 matches the realized cadence (31 x 791.3)",
|
||||
rate_errors("ok.yaml", {"economy": {"xp_per_hour": 24500}}) == [])
|
||||
check("loop-only LA xp_per_hour 25,953 fails the realized-cadence gate",
|
||||
any("realized cadence" in e
|
||||
for e in rate_errors("bad.yaml", {"economy": {"xp_per_hour": 25953}})))
|
||||
check("8-14-cast-break regime LA rate (31 x ~305 = 9,455) fails the realized-cadence gate",
|
||||
any("realized cadence" in e
|
||||
for e in rate_errors("bad.yaml", {"economy": {"xp_per_hour": 9455}})))
|
||||
check("SII entry xp_per_hour 51,400 matches the HLA-tier realized cadence (65 x 791.3)",
|
||||
rate_errors("ok2.yaml", {"actions": [superheat_action],
|
||||
"economy": {"xp_per_hour": 51400}}) == [])
|
||||
check("SII entry with the old loop-only HLA rate 54,419 fails the realized-cadence gate",
|
||||
any("realized cadence" in e
|
||||
for e in rate_errors("bad2.yaml", {"actions": [superheat_action],
|
||||
"economy": {"xp_per_hour": 54419}})))
|
||||
# --- rate_model fixtures (owner guidance 2026-08-28, bounce 4) ---
|
||||
# The gate is OPT-IN: entries without rate_model are never treated as
|
||||
# alchemy, and the rate is DERIVED from the entry's own declared
|
||||
# lifecycle (cast action delay, run_control.break, recipes, startup).
|
||||
cast_action = {"id": "a3", "step": "cast-alch", "verb": "use_item_on",
|
||||
"params": {"spell": "Low Level Alchemy"},
|
||||
"on_fail": "retry",
|
||||
"anti_ban": {"delay": [1600, 3400]}}
|
||||
base = {
|
||||
"rate_model": {"anchor_tier": "la", "cast_ticks": 3,
|
||||
"restock_trip_s": [60, 120], "session_horizon_s": 7200,
|
||||
"startup_s": [300, 600],
|
||||
"alch_carry_batch": 1000, "alch_restock_threshold": 100},
|
||||
"actions": [cast_action],
|
||||
"run_control": {"break": {"every": [400, 700], "for": [45, 120]}},
|
||||
"economy": {"xp_per_hour": 23000}}
|
||||
check("non-alchemy entry without rate_model passes (mining xp 52,500 untouched)",
|
||||
rate_model_errors("mining.yaml", {"economy": {"xp_per_hour": 52500}}) == [])
|
||||
check("LA entry declaring rate_model with the derived 23,000 xp/hr passes",
|
||||
rate_model_errors("ok.yaml", dict(base)) == [])
|
||||
check("loop-only LA 25,953 fails the derived realized-rate gate",
|
||||
any("derived realized rate" in e
|
||||
for e in rate_model_errors("bad.yaml", dict(base, **{"economy": {"xp_per_hour": 25953}}))))
|
||||
check("pre-startup realized 24,500 fails too (startup amortisation is part of the model)",
|
||||
any("derived realized rate" in e
|
||||
for e in rate_model_errors("bad.yaml", dict(base, **{"economy": {"xp_per_hour": 24500}}))))
|
||||
check("8-14-cast-break regime 9,455 fails the derived rate gate",
|
||||
any("derived realized rate" in e
|
||||
for e in rate_model_errors("bad.yaml", dict(base, **{"economy": {"xp_per_hour": 9455}}))))
|
||||
mutated = copy.deepcopy(base)
|
||||
mutated["run_control"]["break"]["for"] = [3600, 3600] # one-hour break per cast
|
||||
check("cadence mutation caught: one-hour break per cast fails the derived rate gate",
|
||||
any("derived realized rate" in e
|
||||
for e in rate_model_errors("bad.yaml", mutated)))
|
||||
# SII: per-recipe capacity (floor(27 / total input qty)) + derived rates
|
||||
def recipe(rid, bar, bar_id, lvl, ti, inputs, cap, cph, xp):
|
||||
return {"bar": bar, "bar_item_id": bar_id, "smithing_level": lvl,
|
||||
"target_input": ti,
|
||||
"inputs": [{"item": it, "item_id": iid, "qty": q, "source": "ge"}
|
||||
for it, iid, q in inputs],
|
||||
"rates": {"capacity": cap,
|
||||
"casts_per_hour": {"min": cph[0], "mid": cph[1], "max": cph[2]},
|
||||
"magic_xp_per_hour": {"min": xp[0], "mid": xp[1], "max": xp[2]}}}
|
||||
steel_recipe = recipe("steel-bar", "Steel bar", 2353, 30, "Iron ore",
|
||||
[("Iron ore", 440, 1), ("Coal", 453, 2)],
|
||||
9, (355.3, 249.1, 191.2), (18833, 13204, 10131))
|
||||
bronze_recipe = recipe("bronze-bar", "Bronze bar", 2349, 1, "Copper ore",
|
||||
[("Copper ore", 436, 1), ("Tin ore", 438, 1)],
|
||||
13, (445.6, 316.5, 244.4), (23617, 16776, 12952))
|
||||
runite_recipe = recipe("runite-bar", "Runite bar", 2363, 85, "Runite ore",
|
||||
[("Runite ore", 451, 1), ("Coal", 453, 8)],
|
||||
3, (153.4, 104.5, 79.1), (8131, 5538, 4194))
|
||||
sii_base = dict(base)
|
||||
sii_base["rate_model"] = {"anchor_tier": "sii", "anchor_recipe": "steel-bar",
|
||||
"cast_ticks": 3, "restock_trip_s": [60, 120],
|
||||
"session_horizon_s": 7200, "startup_s": [300, 600]}
|
||||
sii_base["economy"] = {"xp_per_hour": 12400}
|
||||
sii_base["recipes"] = {"steel-bar": steel_recipe, "bronze-bar": bronze_recipe,
|
||||
"runite-bar": runite_recipe}
|
||||
check("SII entry passes: capacities floor(27/qty) = steel 9, bronze 13, runite 3, all rate pins derived",
|
||||
rate_model_errors("ok.yaml", dict(sii_base)) == [])
|
||||
check("steel anchor xp 12,400 (249.1 x 53 x startup 0.9375) passes the anchor gate",
|
||||
rate_model_errors("ok2.yaml", dict(sii_base)) == [])
|
||||
check("old 24-pack SII rate 27,500 fails the derived per-recipe rate",
|
||||
any("derived realized rate" in e
|
||||
for e in rate_model_errors("bad.yaml", dict(sii_base, **{"economy": {"xp_per_hour": 27500}}))))
|
||||
bad_cap = copy.deepcopy(sii_base)
|
||||
bad_cap["recipes"]["steel-bar"]["rates"]["capacity"] = 8
|
||||
check("capacity pin mismatch (8 vs floor(27/3)=9) is rejected",
|
||||
any("rates.capacity 8" in e for e in rate_model_errors("bad.yaml", bad_cap)))
|
||||
bad_xp = copy.deepcopy(sii_base)
|
||||
bad_xp["recipes"]["steel-bar"]["rates"]["magic_xp_per_hour"]["mid"] = 20000
|
||||
check("recipe magic_xp pin far from the derived 13,204 is rejected",
|
||||
any("magic_xp_per_hour.mid" in e for e in rate_model_errors("bad.yaml", bad_xp)))
|
||||
oversized = copy.deepcopy(sii_base)
|
||||
oversized["recipes"]["oversized-bar"] = recipe("oversized-bar", "Oversized bar", 9999, 90,
|
||||
"Iron ore", [("Iron ore", 440, 28)],
|
||||
1, (1, 1, 1), (53, 53, 53))
|
||||
check("recipe needing more input slots than the inventory (qty 28 > 27) is rejected",
|
||||
any("cannot fit the inventory" in e
|
||||
for e in rate_model_errors("bad.yaml", oversized)))
|
||||
|
||||
return errors
|
||||
|
||||
@ -572,7 +885,7 @@ def main():
|
||||
|
||||
# SII recipe-model + selection gates + realized-rate consistency
|
||||
errors += recipe_model_errors(rel, data)
|
||||
errors += rate_errors(rel, data)
|
||||
errors += rate_model_errors(rel, data)
|
||||
|
||||
# 3. staleness / research TODOs
|
||||
if data.get("status") == "verified":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user