codex: dynamic SII recipe selection + realized rates + validator crash fix (bounce fix attempt 3, owner guidance)
Owner decision (2026-08-28): Superheat Item uses a dynamic recipe table
governed by Smithing level — highest UNLOCKED recipe selected, primary ore
(target_input, never coal) is the spell target, fallback to the next lower
unlocked recipe, else skip SII and stay on LA.
- magic-alch-handoff: per-recipe target_input; selection block
(mode highest-unlocked / eligible_rule / fallback); full-lifecycle
actions (buy-staff, equip-staff w/ fire-rune failure path, buy-supplies,
cast-alch, cast-superheat, bank-loop, take-break); deterministic restock
(LA/HLA carried_batch 1,000 + threshold 100 -> 901-cast batches; SII
24-cast bank cycles, bars >= 24); breaks [400,700] COMPLETED casts,
independent of banking; worked example pinned (prep -> first restock @24
-> break @550 -> resume -> restock @552); stop conditions + recovery block
- rates REALIZED incl. mandatory breaks + banking: LA 24,500 / SII 27,500 /
HLA 51,400 xp/hr (formulas pinned; loop-only 837/hr and 305/hr regimes
superseded); low-alch-f2p also switched (xp 24,500, gp -110,500..-74,000)
- validate.py: rows now collected ONLY after schema validation (schema-
invalid MAPPINGS incl. 'economy: not-a-mapping' produce normal ERRORs,
never AttributeError tracebacks — end-to-end subprocess regression added);
new gates: recipe_model_errors (target_input in declared inputs, coal
never targetable, unique smithing gates, selection block + SII/fallback
transitions) and rate_errors (xp_per_hour must match the realized cadence,
tolerance 2%); highest_unlocked_recipe helper + 6 eligibility fixtures
- entry.schema.json: optional selection block + per-recipe target_input
- Evidence: validate 2 entries 0 errors 0 warnings; --selftest 31 PASS;
governor's exact direct repro supply_limit_conflicts([('bad.yaml',
{'economy':'not-a-mapping'})]) -> [] no crash; synthetic 3-way merge of
main 79cab78 (incl. issue-6 catalog) + head: clean, gate green; git
diff --check clean
This commit is contained in:
parent
75a9965c9f
commit
34f36d2275
@ -3,58 +3,74 @@ title: Low-Level Alchemy (F2P)
|
||||
domain: economy
|
||||
tags: [alching, magic-training, f2p, loss-training]
|
||||
status: verified
|
||||
version: 1
|
||||
updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
|
||||
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
|
||||
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
|
||||
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).
|
||||
|
||||
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)"
|
||||
- "A bank with GE access within reach"
|
||||
- "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)"
|
||||
|
||||
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)"
|
||||
source: "OSRS wiki — Staff of Fire; Low Level Alchemy (Cost)"
|
||||
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)"
|
||||
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
|
||||
why: "one inventory slot, minimal mouse travel between spell icon and item"
|
||||
why: "one inventory slot, minimal mouse travel between spell icon and item; means an inventory does NOT define a batch size — batch sizes are the explicit numeric carried_batch below"
|
||||
source: "OSRS wiki — Low Level Alchemy (Usage: casting alchemy spells on noted items)"
|
||||
- name: bank at a GE-adjacent bank
|
||||
why: "supply restock without a second travel leg — runes and alch items both come from the GE"
|
||||
why: "supply restock without a second travel leg — runes and alch items both come from the GE; restock trip 60-120s"
|
||||
source: "OSRS wiki — Money making guide/High alching profitable free-to-play items (bulk alching via noted items; West Varrock Bank is the common F2P alch/trade hub); OSRS wiki — High Level Alchemy (buying GE items below alch value for break-even/profit; noted-item casting saves bank withdrawals)"
|
||||
|
||||
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.
|
||||
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)
|
||||
income_per_cycle: 180 # lowalch of gold necklace (40% of value 450)
|
||||
net_per_cycle: -112
|
||||
gp_per_hour_low: -119000 # -112 net/cast x rate endpoints: max burn at 1.6s delay
|
||||
# (3.6/3.4s = 1,058 casts/hr) ~ -118,500; mid 4.3s/cast
|
||||
# (837 casts/hr) ~ -93,800
|
||||
gp_per_hour_high: -78000 # min burn at 3.4s delay (3.6/5.2s = 692 casts/hr) ~ -77,500
|
||||
xp_per_hour: 25953 # 31 xp/cast (wiki) @ ~837 casts/hr — additive wall time:
|
||||
# 3-tick cast = 1.8s PLUS anti_ban.delay 1.6-3.4s which is
|
||||
# sleep AFTER the action (action-grammar.md) = 3.4-5.2s/cast,
|
||||
# mid 4.3s -> 3,600/4.3 = ~837 casts/hr (midpoint rate).
|
||||
# The old 1,800 casts/hr (55,800 xp/hr) ignored the additive
|
||||
# delay and was not reachable under the entry's own pacing.
|
||||
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.
|
||||
# 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.
|
||||
supplies:
|
||||
- item: gold necklace
|
||||
qty_per_cycle: 1
|
||||
source: ge
|
||||
ge_limit: 18000 # mapping 'limit', wiki-verified
|
||||
ge_limit: 18000 # mapping 'limit', wiki-verified; one carried_batch of 1,000 fits the 4h window
|
||||
- item: nature rune
|
||||
qty_per_cycle: 1
|
||||
source: ge
|
||||
@ -63,24 +79,25 @@ 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"
|
||||
|
||||
state_machine:
|
||||
initial: idle
|
||||
terminal: [complete, failed]
|
||||
states:
|
||||
idle:
|
||||
goal: reach the bank + have supplies in inventory
|
||||
next: [casting, bank_supplies]
|
||||
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]
|
||||
casting:
|
||||
goal: cast Low Level Alchemy on the current candidate item
|
||||
next: [bank_supplies, idle, complete, failed]
|
||||
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)"
|
||||
next: [bank_supplies, complete, failed]
|
||||
bank_supplies:
|
||||
goal: bank the produced coins, withdraw fresh candidate + runes
|
||||
next: [casting, idle]
|
||||
goal: "deposit coins, withdraw candidate + natures back to carried_batch (1,000/1,000), resume casting"
|
||||
next: [casting, failed]
|
||||
complete:
|
||||
goal: reached the run's XP/level target
|
||||
goal: "reached the run's XP/level target (default 54, hand off to HLA at 55)"
|
||||
failed:
|
||||
goal: hard failure — stop, log ERROR sentinel, alert
|
||||
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
|
||||
@ -89,12 +106,27 @@ state_machine:
|
||||
# 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: casting
|
||||
when: "candidate item count < buffer OR nature rune count < buffer"
|
||||
to: bank_supplies
|
||||
- from: idle
|
||||
when: "staff equipped AND candidate == carried_batch AND nature == carried_batch (1,000 and 1,000)"
|
||||
to: casting
|
||||
- from: idle
|
||||
when: "magic level >= target (default 54, hand off to HLA at 55)"
|
||||
to: complete
|
||||
- from: idle
|
||||
when: "3 consecutive purchase/equip failures with no fallback available (recovery block)"
|
||||
to: failed
|
||||
- from: casting
|
||||
when: "candidate count < restock_threshold (100) OR nature rune 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)"
|
||||
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)"
|
||||
to: casting
|
||||
|
||||
actions:
|
||||
- id: a1
|
||||
@ -102,7 +134,7 @@ actions:
|
||||
verb: move_to
|
||||
target: bank
|
||||
params:
|
||||
location: "nearest bank with GE"
|
||||
location: "GE-adjacent bank (alch spot = the bank booth, so restock is a single short leg)"
|
||||
method: [teleport, walk]
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
@ -113,10 +145,10 @@ actions:
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "current candidate item"
|
||||
qty: "fill to buffer"
|
||||
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
|
||||
on_fail: retry # re-place at live low +5% with paced offers; 3 consecutive failures -> failed (recovery block)
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
@ -128,11 +160,10 @@ actions:
|
||||
spell: "Low Level Alchemy"
|
||||
item: "cheapest live candidate in inventory"
|
||||
on: "itself"
|
||||
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] # sleep AFTER the action (action-grammar.md): 1.8s cast +
|
||||
# 1.6-3.4s delay = 3.4-5.2s/cast, mid 4.3s -> ~837 casts/hr
|
||||
# mid; xp_per_hour pinned at that additive rate, humanised
|
||||
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)
|
||||
|
||||
- id: a4
|
||||
step: bank-loop
|
||||
@ -140,32 +171,95 @@ actions:
|
||||
target: bank
|
||||
params:
|
||||
deposit: [all coins, all products]
|
||||
withdraw: "candidate item + nature runes up to buffer"
|
||||
withdraw: "candidate + nature runes up to carried_batch (1,000/1,000)"
|
||||
on_fail: alert
|
||||
|
||||
- id: a5
|
||||
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"
|
||||
on_fail: skip
|
||||
|
||||
run_control:
|
||||
iteration: "one cast" # schema.md: break.every counts iterations, so every
|
||||
# value below is in casts (one iteration = one cast)
|
||||
# value below is in completed casts, NOT bank trips
|
||||
# (owner decision 2026-08-28)
|
||||
max_iterations: null
|
||||
break:
|
||||
every: [8, 14] # casts (one iteration = one cast); no bank-visit alignment assumed
|
||||
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
|
||||
stop_conditions:
|
||||
- "magic level >= target"
|
||||
- "gp under floor"
|
||||
- "magic level >= target (default 54, hand off to HLA at 55)"
|
||||
- "gp under floor (working capital below one carried_batch = 292,000 gp)"
|
||||
- "3 consecutive failed casts (client issue — do not loop)"
|
||||
- "3 consecutive failed GE purchases with no fallback candidate available"
|
||||
|
||||
# ================= 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).
|
||||
#
|
||||
# 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.
|
||||
# ============================================================================================
|
||||
|
||||
risk:
|
||||
ban_level: low
|
||||
notes:
|
||||
- "High cast cadence is the main detection vector; keep anti_ban delays."
|
||||
- "GE buy bursts: pace offers, respect the 4h limit window."
|
||||
- "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)."
|
||||
|
||||
# --- 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).
|
||||
|
||||
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)"
|
||||
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing)
|
||||
- "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)"
|
||||
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27
|
||||
|
||||
related:
|
||||
- magic-alch-handoff
|
||||
|
||||
@ -3,8 +3,8 @@ title: Magic training via alchemy (LA → SII → HLA handoff)
|
||||
domain: training
|
||||
tags: [magic-training, alching, f2p, xp-training]
|
||||
status: verified
|
||||
version: 1
|
||||
updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
|
||||
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
|
||||
updated: "2026-08-28" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
|
||||
|
||||
summary: >
|
||||
The standard F2P alchemy training path: Low Level Alchemy from level 21
|
||||
@ -12,21 +12,29 @@ 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.
|
||||
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.
|
||||
|
||||
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)"
|
||||
- "A bank with GE access within reach (runes + alch items come from the GE)"
|
||||
- "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)"
|
||||
- "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)"
|
||||
|
||||
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"
|
||||
source: "OSRS wiki — High Level Alchemy (rune cost table; 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)"
|
||||
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"
|
||||
@ -34,9 +42,9 @@ best_practice:
|
||||
- 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"
|
||||
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 SII bars by complete recipe margin
|
||||
why: "SII smelts a concrete bar from its exact inputs (see the recipes table below) — the bar's Smithing gate must be met and the complete input cost (all ores + coal + one nature rune, live GE buy) must stay under the bar's GE price; withdraw those exact inputs and bank the produced bar"
|
||||
source: "OSRS wiki — Superheat Item (requires the Smithing level and materials of the bar); Smithing (bar smelting table); prices.runescape.wiki (live GE prices)"
|
||||
- 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)"
|
||||
- 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)"
|
||||
@ -56,38 +64,61 @@ economy:
|
||||
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: 54419 # HLA tier: 65 xp/cast @ ~837 casts/hr — additive wall
|
||||
# time: 3-tick cast = 1.8s PLUS anti_ban.delay 1.6-3.4s
|
||||
# after the action (action-grammar.md) = 3.4-5.2s/cast,
|
||||
# mid 4.3s -> 3,600/4.3 = ~837 casts/hr. Tiers pinned at
|
||||
# the midpoint rate: LA 25,953 (31 x 837)
|
||||
# / SII 44,372 (53 x 837) / HLA 54,419 (65 x 837).
|
||||
# Old 1,400-1,800 casts/hr figures (43,400 / 74,200 /
|
||||
# 91,000) ignored the additive delay — unreachable.
|
||||
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.
|
||||
# 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.
|
||||
supplies:
|
||||
- item: nature rune
|
||||
qty_per_cycle: 1
|
||||
source: ge
|
||||
ge_limit: 18000 # mapping + Module:Exchange agree
|
||||
profit_drivers:
|
||||
- "XP per cast by tier (wiki infoboxes): LA 31 (level 21) / SII 53 (level 43) / HLA 65 (level 55)"
|
||||
- "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 + nature rune) stays under the resulting bar's GE price — re-check margins per window (SII page; the recipes table)"
|
||||
- "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"
|
||||
|
||||
# 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.
|
||||
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"
|
||||
|
||||
# Concrete F2P bar recipes for Superheat Item. SII requires the bar's
|
||||
# Smithing level and its exact materials, so every selectable bar is modelled
|
||||
# with ALL inputs and quantities (id 453 coal appears in steel/mithril/
|
||||
# adamantite/runite). Levels, xp and item ids from the OSRS wiki (Smithing
|
||||
# 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 the highest-margin recipe whose
|
||||
# smithing_level <= current Smithing level and withdraws its exact inputs.
|
||||
# one nature rune 145); the client selects via selection.mode.
|
||||
recipes:
|
||||
bronze-bar:
|
||||
bar: Bronze bar
|
||||
bar_item_id: 2349
|
||||
smithing_level: 1
|
||||
smithing_xp: 6.2
|
||||
target_input: Copper ore
|
||||
inputs:
|
||||
- item: Copper ore
|
||||
item_id: 436
|
||||
@ -102,6 +133,7 @@ recipes:
|
||||
bar_item_id: 2351
|
||||
smithing_level: 15
|
||||
smithing_xp: 12.5
|
||||
target_input: Iron ore
|
||||
inputs:
|
||||
- item: Iron ore
|
||||
item_id: 440
|
||||
@ -112,6 +144,7 @@ recipes:
|
||||
bar_item_id: 2355
|
||||
smithing_level: 20
|
||||
smithing_xp: 13.7
|
||||
target_input: Silver ore
|
||||
inputs:
|
||||
- item: Silver ore
|
||||
item_id: 442
|
||||
@ -122,6 +155,7 @@ recipes:
|
||||
bar_item_id: 2353
|
||||
smithing_level: 30
|
||||
smithing_xp: 17.5
|
||||
target_input: Iron ore
|
||||
inputs:
|
||||
- item: Iron ore
|
||||
item_id: 440
|
||||
@ -136,6 +170,7 @@ recipes:
|
||||
bar_item_id: 2357
|
||||
smithing_level: 40
|
||||
smithing_xp: 22.5
|
||||
target_input: Gold ore
|
||||
inputs:
|
||||
- item: Gold ore
|
||||
item_id: 444
|
||||
@ -146,6 +181,7 @@ recipes:
|
||||
bar_item_id: 2359
|
||||
smithing_level: 50
|
||||
smithing_xp: 30
|
||||
target_input: Mithril ore
|
||||
inputs:
|
||||
- item: Mithril ore
|
||||
item_id: 447
|
||||
@ -160,6 +196,7 @@ recipes:
|
||||
bar_item_id: 2361
|
||||
smithing_level: 70
|
||||
smithing_xp: 37.5
|
||||
target_input: Adamantite ore
|
||||
inputs:
|
||||
- item: Adamantite ore
|
||||
item_id: 449
|
||||
@ -174,6 +211,7 @@ recipes:
|
||||
bar_item_id: 2363
|
||||
smithing_level: 85
|
||||
smithing_xp: 50
|
||||
target_input: Runite ore
|
||||
inputs:
|
||||
- item: Runite ore
|
||||
item_id: 451
|
||||
@ -189,40 +227,55 @@ state_machine:
|
||||
terminal: [complete, failed]
|
||||
states:
|
||||
idle:
|
||||
goal: be at a bank with GE access, staff of fire equipped, nature runes in stock
|
||||
next: [cast, bank_supplies, complete, failed]
|
||||
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]
|
||||
cast:
|
||||
goal: cast the active tier spell with the tier-correct target — LA < 43 on a
|
||||
noted alch candidate; SII 43-55 on an unnoted input of the selected
|
||||
recipe (all exact inputs withdrawn; see the recipes table); HLA >= 55
|
||||
on a noted alch candidate
|
||||
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"
|
||||
next: [cast, bank_supplies, complete, failed]
|
||||
bank_supplies:
|
||||
goal: deposit coins and smelted bars, restock nature runes (+ the exact recipe inputs during the SII window)
|
||||
next: [cast, idle]
|
||||
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]
|
||||
complete:
|
||||
goal: magic level target reached
|
||||
goal: "magic level target reached (default 55 -> HLA is the end-state)"
|
||||
failed:
|
||||
goal: hard failure — stop, log ERROR sentinel, alert
|
||||
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
|
||||
- from: idle
|
||||
when: "magic level >= target (55 -> HLA end-state)"
|
||||
to: complete
|
||||
- from: idle
|
||||
when: "3 consecutive purchase/equip failures with no fallback available (recovery block)"
|
||||
to: failed
|
||||
- from: cast
|
||||
when: "nature rune count < buffer (or a selected recipe's inputs < buffer in SII window)"
|
||||
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)"
|
||||
to: cast
|
||||
- from: cast
|
||||
when: "level 43-55 and the SII window is chosen (a recipes-table entry has smithing_level <= current Smithing and a positive complete-cost margin)"
|
||||
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"
|
||||
to: cast
|
||||
- from: cast
|
||||
when: "level 43-55 and the SII window is skipped (no recipes-table entry has a positive margin this window) — stay on LA"
|
||||
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"
|
||||
to: cast
|
||||
- from: cast
|
||||
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)"
|
||||
to: cast
|
||||
- from: idle
|
||||
when: "magic level >= target"
|
||||
- from: cast
|
||||
when: "magic level >= target (55 -> HLA end-state)"
|
||||
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)"
|
||||
to: cast
|
||||
|
||||
actions:
|
||||
- id: a1
|
||||
@ -230,25 +283,48 @@ actions:
|
||||
verb: move_to
|
||||
target: bank
|
||||
params:
|
||||
location: "nearest bank with GE"
|
||||
location: "GE-adjacent bank (alch spot = the bank booth, so restock is a single short leg, 60-120s)"
|
||||
method: [teleport, walk]
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [400, 1600]
|
||||
|
||||
- id: a2
|
||||
step: buy-supplies
|
||||
step: buy-staff
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "nature rune + the exact SII recipe inputs (unnoted) 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: "fill to buffer"
|
||||
max_price: "live low + 5%"
|
||||
on_fail: retry
|
||||
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."
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1800]
|
||||
|
||||
- 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)
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a5
|
||||
step: cast-alch
|
||||
verb: use_item_on
|
||||
target: "noted alch candidate in inventory (LA and HLA tiers)"
|
||||
@ -258,64 +334,124 @@ actions:
|
||||
on: "itself"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1600, 3400] # sleep AFTER the action (action-grammar.md): 1.8s cast +
|
||||
# 1.6-3.4s delay = 3.4-5.2s/cast, mid 4.3s -> ~837 casts/hr
|
||||
# mid; xp_per_hour pinned at that additive rate, humanised
|
||||
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)
|
||||
|
||||
- id: a4
|
||||
- id: a6
|
||||
step: cast-superheat
|
||||
verb: use_item_on
|
||||
target: "an unnoted input of the selected recipe in inventory (SII tier only, 43-55)"
|
||||
target: "the selected recipe's declared target_input in inventory (the primary ore — never coal; SII tier only, 43-55)"
|
||||
params:
|
||||
spell: "Superheat Item"
|
||||
recipe: "steel-bar" # concrete recipes-table entry — every input and quantity,
|
||||
# smithing gate, bar output (steel-bar = 1 iron ore +
|
||||
# 2 coal, Smithing 30). Any recipes-table bar may be
|
||||
# selected live; steel-bar is the fixture anchor.
|
||||
recipe_rule: "select a recipe from the recipes table whose smithing_level <= current Smithing level and whose complete input cost (ALL inputs, live GE buy) + one nature rune < the bar's live GE price; withdraw those exact inputs (unnoted) and cast on one of them; never a noted stack, never a price-only pick"
|
||||
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"
|
||||
target_input: "the selected recipe's declared target_input field (primary ore; coal can never be the spell target)"
|
||||
on: "itself"
|
||||
on_fail: skip # no positive-margin recipe -> skip SII, stay on LA
|
||||
on_fail: skip # no unlocked positive-margin recipe -> skip SII, stay on LA (selection.fallback)
|
||||
anti_ban:
|
||||
delay: [1600, 3400] # post-action sleep (additive): 1.8s cast + delay (mid 2.5s)
|
||||
# = 4.3s/cast mid -> ~837 casts/hr, same pacing as a3
|
||||
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)
|
||||
|
||||
- id: a5
|
||||
- id: a7
|
||||
step: bank-loop
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
deposit: [all coins, all products] # products incl. smelted bars during SII
|
||||
withdraw: "nature runes up to buffer + the exact unnoted inputs of the selected SII recipe (per the recipes table)"
|
||||
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)"
|
||||
on_fail: alert
|
||||
|
||||
- id: a8
|
||||
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"
|
||||
on_fail: skip
|
||||
|
||||
run_control:
|
||||
iteration: "one cast" # schema.md: break.every counts iterations, so every
|
||||
# value below is in casts (one iteration = one cast)
|
||||
# value below is in completed casts, NOT bank trips
|
||||
max_iterations: null
|
||||
break:
|
||||
every: [8, 14] # casts (one iteration = one cast); no bank-visit alignment assumed
|
||||
every: [400, 700] # COMPLETED CASTS between AFK breaks (midpoint 550 = ~39 min
|
||||
# at the 4.3s midpoint cycle). Cast-count-based, independent of
|
||||
# banking; a restock trip NEVER counts as a break and does not
|
||||
# reset this counter (owner decision 2026-08-28)
|
||||
for: [45, 120] # seconds of idle AFK
|
||||
stop_conditions:
|
||||
- "magic level >= target"
|
||||
- "gp under floor"
|
||||
- "magic level >= target (default 55 -> HLA is the end-state)"
|
||||
- "gp under floor (below one full carried_batch of natures + tier supplies)"
|
||||
- "3 consecutive failed casts (client issue — do not loop)"
|
||||
- "3 consecutive failed GE purchases with no fallback recipe/candidate available"
|
||||
|
||||
# ================= 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).
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
# =====================================================================================================
|
||||
|
||||
risk:
|
||||
ban_level: low
|
||||
notes:
|
||||
- "3-tick cast cadence is the main detection vector; keep anti_ban delays."
|
||||
- "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."
|
||||
- "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) before casting — every cast consumes one of each; bank the produced bar after each cast; never cast on noted ores."
|
||||
- "SII ore-buy bursts are pace-sensitive like any GE buy; respect the 4h limit window."
|
||||
- "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."
|
||||
|
||||
# --- 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).
|
||||
|
||||
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)"
|
||||
- "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)"
|
||||
- "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)"
|
||||
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing)
|
||||
- "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)"
|
||||
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing); prices last re-fetched live 2026-08-27
|
||||
|
||||
related:
|
||||
- low-alch-f2p
|
||||
|
||||
@ -62,19 +62,31 @@
|
||||
}
|
||||
},
|
||||
|
||||
"selection": {
|
||||
"type": "object",
|
||||
"required": ["mode", "eligible_rule", "fallback"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"mode": { "enum": ["highest-unlocked"] },
|
||||
"eligible_rule": { "type": "string", "minLength": 10 },
|
||||
"fallback": { "type": "string", "minLength": 10 }
|
||||
}
|
||||
},
|
||||
|
||||
"recipes": {
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"propertyNames": { "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": ["bar", "smithing_level", "inputs"],
|
||||
"required": ["bar", "smithing_level", "target_input", "inputs"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"bar": { "type": "string", "minLength": 2 },
|
||||
"bar_item_id": { "type": "integer", "minimum": 1 },
|
||||
"smithing_level": { "type": "integer", "minimum": 1, "maximum": 126 },
|
||||
"smithing_xp": { "type": "number", "exclusiveMinimum": 0 },
|
||||
"target_input": { "type": "string", "minLength": 2 },
|
||||
"inputs": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
|
||||
@ -17,15 +17,25 @@ Runs:
|
||||
another) is a silent fact error, not a per-entry choice — GE
|
||||
buy limits are item-global.
|
||||
- SII translatability: an action that casts Superheat Item must
|
||||
name a concrete recipe id declared in the entry's `recipes:`
|
||||
table (all inputs + quantities, smithing gate, bar output). The
|
||||
client translates a bar from its exact recipe, not from a vague
|
||||
"positive margin ore" rule.
|
||||
declare a `selection:` block (mode highest-unlocked, eligible rule,
|
||||
fallback) and the state machine must cover both the SII window and
|
||||
the stay-on-LA fallback. Every declared recipe must pin a
|
||||
`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).
|
||||
3. Global id uniqueness across the whole codex tree.
|
||||
|
||||
Malformed/scalar YAML documents (an empty doc, a bare string, a list)
|
||||
are rejected with a normal validator ERROR — they never reach the
|
||||
cross-entry gates and never crash the tool with a traceback.
|
||||
cross-entry gates and never crash the tool with a traceback. The same
|
||||
holds for schema-INVALID MAPPINGS (e.g. `economy: not-a-mapping`):
|
||||
shared-gate rows are only collected after schema validation succeeds
|
||||
(regression: a malformed nested field used to crash
|
||||
`supply_limit_conflicts` with an AttributeError).
|
||||
|
||||
Exit codes: 0 = clean (warnings ok), 1 = validation errors.
|
||||
|
||||
@ -88,8 +98,12 @@ def supply_limit_conflicts(rows):
|
||||
for rel, data in rows:
|
||||
if not isinstance(data, dict): # never crash on malformed rows
|
||||
continue
|
||||
eco = data.get("economy") or {}
|
||||
eco = data.get("economy")
|
||||
if not isinstance(eco, dict): # schema-invalid nested mapping (e.g. economy: "x")
|
||||
continue
|
||||
for sup in eco.get("supplies", []):
|
||||
if not isinstance(sup, dict):
|
||||
continue
|
||||
lim = sup.get("ge_limit")
|
||||
if lim is None:
|
||||
continue
|
||||
@ -129,28 +143,152 @@ def recipe_margin(recipe, bar_price, input_prices, nature_price):
|
||||
return bar_price - (cost + nature_price)
|
||||
|
||||
|
||||
def recipe_errors(rel, data):
|
||||
"""Entry-level SII translatability checks. Returns error strings."""
|
||||
# --- 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
|
||||
XP_LA, XP_SII, XP_HLA = 31, 53, 65
|
||||
RATE_TOLERANCE = 0.02 # pinned values may round to 3 significant figures
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Uniqueness of the gates is enforced by recipe_model_errors, so this
|
||||
helper's ordering cannot be ambiguous. An account can NEVER be
|
||||
offered a recipe above its Smithing level.
|
||||
"""
|
||||
best, best_lvl = None, -1
|
||||
for rid, recipe in (recipes or {}).items():
|
||||
lvl = recipe.get("smithing_level") if isinstance(recipe, dict) else None
|
||||
if isinstance(lvl, int) and lvl <= smithing_level and lvl > best_lvl:
|
||||
best, best_lvl = rid, lvl
|
||||
return best
|
||||
|
||||
|
||||
def recipe_model_errors(rel, data):
|
||||
"""SII recipe-model + selection gates (owner guidance 2026-08-28).
|
||||
|
||||
- every declared recipe pins a `target_input` that is one of its
|
||||
declared inputs and is never coal (the spell targets the primary
|
||||
ore — Superheat cannot be cast on coal);
|
||||
- smithing_level gates are unique, so "highest unlocked" selection
|
||||
is deterministic;
|
||||
- an entry casting Superheat Item must declare a `selection:` block
|
||||
(mode highest-unlocked + eligible_rule + fallback) and its state
|
||||
machine must cover both the SII window (43-55) and the fallback
|
||||
(skip SII / stay on LA / next unlocked recipe).
|
||||
"""
|
||||
errors = []
|
||||
recipes = data.get("recipes")
|
||||
if recipes is not None and not isinstance(recipes, dict):
|
||||
return [
|
||||
f"{rel}: 'recipes' must be a mapping of recipe id -> recipe definition"]
|
||||
for action in data.get("actions", []):
|
||||
params = action.get("params") or {}
|
||||
spell = str(params.get("spell", ""))
|
||||
if "superheat" not in spell.lower():
|
||||
continue
|
||||
rid = params.get("recipe")
|
||||
if not isinstance(rid, str) or not rid:
|
||||
if isinstance(recipes, dict):
|
||||
seen_levels = {}
|
||||
for rid, recipe in recipes.items():
|
||||
if not isinstance(recipe, dict):
|
||||
continue # schema reports the shape
|
||||
inputs = recipe.get("inputs") or []
|
||||
in_names = [str(i.get("item", "")).lower()
|
||||
for i in inputs if isinstance(i, dict)]
|
||||
ti = recipe.get("target_input")
|
||||
if not isinstance(ti, str) or not ti:
|
||||
errors.append(
|
||||
f"{rel}: recipe '{rid}' has no target_input — the spell "
|
||||
f"targets the primary ore and it must be pinned per recipe")
|
||||
elif ti.lower() not in in_names:
|
||||
errors.append(
|
||||
f"{rel}: recipe '{rid}' target_input '{ti}' is not one of "
|
||||
f"its declared inputs")
|
||||
if ti and (ti.lower() == "coal" or ti.strip().isdigit()
|
||||
and int(ti) in {453}):
|
||||
errors.append(
|
||||
f"{rel}: recipe '{rid}' target_input '{ti}' is coal — coal "
|
||||
f"can never be the Superheat spell target")
|
||||
lvl = recipe.get("smithing_level")
|
||||
if isinstance(lvl, int):
|
||||
if lvl in seen_levels:
|
||||
errors.append(
|
||||
f"{rel}: recipes '{seen_levels[lvl]}' and '{rid}' share "
|
||||
f"smithing_level {lvl} — highest-unlocked selection "
|
||||
f"would be ambiguous")
|
||||
seen_levels[lvl] = rid
|
||||
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", []))
|
||||
if has_sii:
|
||||
sel = data.get("selection")
|
||||
if not isinstance(sel, dict):
|
||||
errors.append(
|
||||
f"{rel}: action {action.get('id')} casts Superheat Item but has no "
|
||||
f"params.recipe naming a concrete recipes: entry (the client cannot "
|
||||
f"translate an ore choice into a bar without the full recipe)")
|
||||
elif recipes is None or rid not in recipes:
|
||||
f"{rel}: a Superheat Item action requires a 'selection' block "
|
||||
f"(mode, eligible_rule, fallback)")
|
||||
else:
|
||||
if sel.get("mode") != "highest-unlocked":
|
||||
errors.append(
|
||||
f"{rel}: selection.mode must be 'highest-unlocked'")
|
||||
if not (isinstance(sel.get("eligible_rule"), str)
|
||||
and len(sel["eligible_rule"]) >= 10):
|
||||
errors.append(
|
||||
f"{rel}: selection.eligible_rule must state the eligibility "
|
||||
f"rule (smithing gate + complete-recipe margin)")
|
||||
if not (isinstance(sel.get("fallback"), str)
|
||||
and len(sel["fallback"]) >= 10):
|
||||
errors.append(
|
||||
f"{rel}: selection.fallback must state the fallback path "
|
||||
f"(next unlocked recipe; else skip SII / stay on LA)")
|
||||
whens = [str(t.get("when", ""))
|
||||
for t in data.get("state_machine", {}).get("transitions", [])]
|
||||
if not any("43-55" in w for w in whens):
|
||||
errors.append(
|
||||
f"{rel}: action {action.get('id')} references recipe '{rid}' which "
|
||||
f"is not declared in recipes:")
|
||||
f"{rel}: no transition covers the SII window (43-55) — the "
|
||||
f"selected recipe tier is unreachable")
|
||||
if not any(("stay on la" in w or "skip" in w or "fallback" in w)
|
||||
for w in whens):
|
||||
errors.append(
|
||||
f"{rel}: no transition covers the SII fallback (skip SII / "
|
||||
f"stay on LA) — selection is not deterministic")
|
||||
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
|
||||
|
||||
|
||||
@ -188,6 +326,8 @@ def run_selftest(verbose=False):
|
||||
# --- malformed-document regression: no traceback, normal errors ---
|
||||
check("supply_limit_conflicts handles (bad.yaml, None) rows without crashing",
|
||||
supply_limit_conflicts([("bad.yaml", None)]) == [])
|
||||
check("supply_limit_conflicts ignores schema-invalid nested mappings (economy: not-a-mapping) without raising",
|
||||
supply_limit_conflicts([("bad.yaml", {"economy": "not-a-mapping"})]) == [])
|
||||
for val in (None, "just a scalar", [1, 2, 3]):
|
||||
msg = document_error("bad.yaml", val)
|
||||
check(
|
||||
@ -195,9 +335,12 @@ def run_selftest(verbose=False):
|
||||
isinstance(msg, str) and "not a YAML mapping" in msg)
|
||||
check("document_error accepts mapping documents",
|
||||
document_error("ok.yaml", {"id": "x"}) is None)
|
||||
check("end-to-end: schema-invalid MAPPING -> exit 1, clean ERROR, no Traceback",
|
||||
_end_to_end_bad_mapping())
|
||||
|
||||
# --- deterministic multi-input recipe fixture: steel-bar (1 iron + 2 coal) ---
|
||||
steel = {"bar": "Steel bar", "bar_item_id": 2353, "smithing_level": 30,
|
||||
"target_input": "Iron ore",
|
||||
"inputs": [
|
||||
{"item": "Iron ore", "item_id": 440, "qty": 1, "source": "ge"},
|
||||
{"item": "Coal", "item_id": 453, "qty": 2, "source": "ge"}]}
|
||||
@ -213,32 +356,138 @@ def run_selftest(verbose=False):
|
||||
input_prices={"Iron ore": 100, "Coal": 150},
|
||||
nature_price=145) < 0)
|
||||
|
||||
# --- SII translatability fixtures ---
|
||||
entry_ok = {"actions": [
|
||||
{"id": "a4", "step": "cast-superheat", "verb": "use_item_on",
|
||||
"params": {"spell": "Superheat Item", "recipe": "steel-bar"},
|
||||
"on_fail": "skip"}],
|
||||
"recipes": {"steel-bar": steel}}
|
||||
entry_undeclared = {"actions": [
|
||||
{"id": "a4", "step": "cast-superheat", "verb": "use_item_on",
|
||||
"params": {"spell": "Superheat Item", "recipe": "steel-bar"},
|
||||
"on_fail": "skip"}]}
|
||||
entry_unpinned = {"actions": [
|
||||
{"id": "a4", "step": "cast-superheat", "verb": "use_item_on",
|
||||
"params": {"spell": "Superheat Item"}, "on_fail": "skip"}],
|
||||
"recipes": {"steel-bar": steel}}
|
||||
check("Superheat action naming a declared recipes: id passes",
|
||||
recipe_errors("ok.yaml", entry_ok) == [])
|
||||
undeclared = recipe_errors("bad.yaml", entry_undeclared)
|
||||
check("Superheat action naming an undeclared recipe id errors",
|
||||
any("not declared in recipes:" in e for e in undeclared))
|
||||
unpinned = recipe_errors("bad.yaml", entry_unpinned)
|
||||
check("Superheat action without params.recipe errors",
|
||||
any("no params.recipe" in e for e in unpinned))
|
||||
# --- recipe-model fixtures: target_input, unique gates, selection block ---
|
||||
f2p_bars = {
|
||||
"bronze-bar": {"bar": "Bronze bar", "bar_item_id": 2349, "smithing_level": 1,
|
||||
"smithing_xp": 6.2, "target_input": "Copper ore",
|
||||
"inputs": [{"item": "Copper ore", "item_id": 436, "qty": 1, "source": "ge"},
|
||||
{"item": "Tin ore", "item_id": 438, "qty": 1, "source": "ge"}]},
|
||||
"iron-bar": {"bar": "Iron bar", "bar_item_id": 2351, "smithing_level": 15,
|
||||
"smithing_xp": 12.5, "target_input": "Iron ore",
|
||||
"inputs": [{"item": "Iron ore", "item_id": 440, "qty": 1, "source": "ge"}]},
|
||||
"silver-bar": {"bar": "Silver bar", "bar_item_id": 2355, "smithing_level": 20,
|
||||
"smithing_xp": 13.7, "target_input": "Silver ore",
|
||||
"inputs": [{"item": "Silver ore", "item_id": 442, "qty": 1, "source": "ge"}]},
|
||||
"steel-bar": steel,
|
||||
"runite-bar": {"bar": "Runite bar", "bar_item_id": 2363, "smithing_level": 85,
|
||||
"smithing_xp": 50, "target_input": "Runite ore",
|
||||
"inputs": [{"item": "Runite ore", "item_id": 451, "qty": 1, "source": "ge"},
|
||||
{"item": "Coal", "item_id": 453, "qty": 8, "source": "ge"}]},
|
||||
}
|
||||
# Smithing-level eligibility: highest unlocked, never an unmet gate
|
||||
check("smithing 1 -> bronze-bar (highest unlocked)",
|
||||
highest_unlocked_recipe(f2p_bars, 1) == "bronze-bar")
|
||||
check("smithing 15 -> iron-bar",
|
||||
highest_unlocked_recipe(f2p_bars, 15) == "iron-bar")
|
||||
check("smithing 20 -> silver-bar (steel at 30 stays locked)",
|
||||
highest_unlocked_recipe(f2p_bars, 20) == "silver-bar")
|
||||
check("smithing 30 -> steel-bar",
|
||||
highest_unlocked_recipe(f2p_bars, 30) == "steel-bar")
|
||||
check("smithing 85 -> runite-bar",
|
||||
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)
|
||||
|
||||
superheat_action = {
|
||||
"id": "a6", "step": "cast-superheat", "verb": "use_item_on",
|
||||
"target": "the selected recipe's declared target_input in inventory (primary ore — never coal)",
|
||||
"params": {"spell": "Superheat Item",
|
||||
"selected_recipe": "runtime selection per selection.mode: highest-unlocked recipe with a positive complete-cost margin",
|
||||
"target_input": "the selected recipe's declared target_input field"},
|
||||
"on_fail": "skip"}
|
||||
selection_ok = {"mode": "highest-unlocked",
|
||||
"eligible_rule": "recipes entries with smithing_level <= current Smithing and a positive complete-cost margin",
|
||||
"fallback": "next lower unlocked recipe when materials cannot be obtained; else skip SII and stay on LA"}
|
||||
transitions_ok = [
|
||||
{"from": "cast", "when": "level 43-55 and the SII window is chosen (selection.mode picks the highest unlocked recipe with positive margin)", "to": "cast"},
|
||||
{"from": "cast", "when": "no unlocked recipe has a positive margin this window — skip SII and stay on LA", "to": "cast"}]
|
||||
entry_model_ok = {"actions": [superheat_action], "recipes": f2p_bars,
|
||||
"selection": selection_ok,
|
||||
"state_machine": {"transitions": transitions_ok}}
|
||||
check("recipe model with declared target_inputs + selection block passes",
|
||||
recipe_model_errors("ok.yaml", entry_model_ok) == [])
|
||||
# coal can never be the spell target
|
||||
entry_coal = {"actions": [superheat_action],
|
||||
"recipes": {"steel-bar": dict(steel, target_input="Coal")},
|
||||
"selection": selection_ok,
|
||||
"state_machine": {"transitions": transitions_ok}}
|
||||
coal_errs = recipe_model_errors("bad.yaml", entry_coal)
|
||||
check("steel-bar target_input 'Coal' is rejected (coal is never the spell target)",
|
||||
any("is coal" in e for e in coal_errs))
|
||||
# target_input must be a declared input
|
||||
entry_bad_ti = {"actions": [superheat_action],
|
||||
"recipes": {"steel-bar": dict(steel, target_input="Runite ore")},
|
||||
"selection": selection_ok,
|
||||
"state_machine": {"transitions": transitions_ok}}
|
||||
bad_ti = recipe_model_errors("bad.yaml", entry_bad_ti)
|
||||
check("target_input not among the recipe's declared inputs is rejected",
|
||||
any("not one of" in e for e in bad_ti))
|
||||
# duplicate smithing gates would make highest-unlocked ambiguous
|
||||
entry_dup = {"actions": [superheat_action],
|
||||
"recipes": {"steel-bar": steel,
|
||||
"steel-bar-2": dict(steel, bar="Steel bar 2", bar_item_id=2353)},
|
||||
"selection": selection_ok,
|
||||
"state_machine": {"transitions": transitions_ok}}
|
||||
dup_errs = recipe_model_errors("bad.yaml", entry_dup)
|
||||
check("duplicate smithing_level across recipes is rejected (ambiguous highest-unlocked)",
|
||||
any("share smithing_level" in e for e in dup_errs))
|
||||
# Superheat action requires the selection block + fallback transitions
|
||||
entry_no_sel = {"actions": [superheat_action], "recipes": f2p_bars,
|
||||
"state_machine": {"transitions": transitions_ok}}
|
||||
no_sel = recipe_model_errors("bad.yaml", entry_no_sel)
|
||||
check("Superheat action without a selection block is rejected",
|
||||
any("'selection' block" in e for e in no_sel))
|
||||
entry_no_fb = {"actions": [superheat_action], "recipes": f2p_bars,
|
||||
"selection": selection_ok,
|
||||
"state_machine": {"transitions": [
|
||||
{"from": "cast", "when": "level 43-55 and the SII window is chosen", "to": "cast"}]}}
|
||||
no_fb = recipe_model_errors("bad.yaml", entry_no_fb)
|
||||
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}})))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def _end_to_end_bad_mapping():
|
||||
"""Regression (governor bounce 3, finding 1): a schema-invalid MAPPING
|
||||
with a malformed nested field — `economy: not-a-mapping` — must yield
|
||||
exit 1 with a normal ERROR line and NO traceback.
|
||||
|
||||
Writes the fixture into the real codex tree and shells out to the real
|
||||
validator so the full main() path is exercised, then removes the file.
|
||||
Returns True when the gate behaves.
|
||||
"""
|
||||
import subprocess
|
||||
bad = ROOT / "codex" / "selftest-bad-mapping.yaml"
|
||||
try:
|
||||
bad.write_text("id: selftest-bad-mapping\neconomy: not-a-mapping\n")
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(Path(__file__).resolve()), "--stale-days", "9999"],
|
||||
capture_output=True, text=True, cwd=str(ROOT), timeout=120)
|
||||
out = (proc.stdout or "") + (proc.stderr or "")
|
||||
return (proc.returncode == 1 and "selftest-bad-mapping" in out
|
||||
and "ERROR" in out and "Traceback" not in out)
|
||||
finally:
|
||||
bad.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main():
|
||||
global STALE_DAYS
|
||||
if "--stale-days" in sys.argv:
|
||||
@ -269,9 +518,12 @@ def main():
|
||||
if shape_err:
|
||||
errors.append(shape_err)
|
||||
continue
|
||||
rows.append((rel, data))
|
||||
|
||||
# 1. schema
|
||||
# 1. schema — BEFORE the shared row set. Schema-invalid documents
|
||||
# (including mappings with malformed nested fields like
|
||||
# `economy: not-a-mapping`) must never reach the cross-entry gates:
|
||||
# regression — rows.append() used to run first, and
|
||||
# supply_limit_conflicts() then crashed with an AttributeError.
|
||||
v = jsonschema.Draft7Validator(SCHEMA)
|
||||
schema_errors = sorted(v.iter_errors(data), key=lambda e: list(e.path))
|
||||
for err in schema_errors:
|
||||
@ -281,6 +533,8 @@ def main():
|
||||
# cross-checks on a schema-invalid doc are noise
|
||||
continue
|
||||
|
||||
rows.append((rel, data))
|
||||
|
||||
eid = data.get("id", rel)
|
||||
if eid in ids:
|
||||
errors.append(f"{rel}: duplicate entry id '{eid}' (also in {ids[eid]})")
|
||||
@ -316,8 +570,9 @@ def main():
|
||||
if len(a_steps) != len(set(a_steps)):
|
||||
errors.append(f"{rel}: duplicate action step names")
|
||||
|
||||
# SII translatability (concrete recipes: entry per Superheat action)
|
||||
errors += recipe_errors(rel, data)
|
||||
# SII recipe-model + selection gates + realized-rate consistency
|
||||
errors += recipe_model_errors(rel, data)
|
||||
errors += rate_errors(rel, data)
|
||||
|
||||
# 3. staleness / research TODOs
|
||||
if data.get("status") == "verified":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user