fix(codex): pin alch cadence to delay policy, unify nature-rune limit, phase-aware SII (issue #5)
Governor-review bounce (PR #10) fixes on issue-5: 1. Timing units + rates: one OSRS tick = 0.6s so a 3-tick cast is 1.8s, not 5.4s as both action comments claimed. The mandatory 1.6-3.4s post-action delay (mid ~2.5s) allows ~1,440 casts/hr before overhead; rates are now pinned at 1,400 casts/hr: LA 43,400 / SII 74,200 / HLA 91,000 xp/hr (was 55,800 / 95,400 / 117,000 @ 1,800). The old 1,800 figures were unreachable under the entries' own pacing. 2. Nature rune limit unified to 18000 (mapping 'limit' + Module:Exchange agree, fetched live 2026-08-27): low-alch-f2p 12500 -> 18000, matching magic-alch-handoff. tools/validate.py gains a cross-entry supply-fact consistency check (same item+source must agree on ge_limit; run on every gate invocation) plus --selftest regression fixtures: diverging limits must fail, agreeing limits must pass, missing ge_limit is not gated. 3. SII tier is now phase-aware and translatable: actions split into cast-alch (LA/HLA -> cheapest alch candidate, noted stack) and cast-superheat (SII 43-55 -> unnoted ore whose live bar margin is positive; never a noted stack, never a price-only pick; on_fail skip when no positive-margin ore). State machine pins each tier path (LA <43, SII window chosen/skipped 43-55, HLA >=55), bank_supplies banks smelted bars, buy-supplies restricts ores to positive-margin recipes. Tests: python3 tools/validate.py -> 2 entries, 0 errors, 0 warnings; --selftest -> 3 PASS; negative test (reintroducing 12500) caught by the new check; git diff --check clean.
This commit is contained in:
parent
ca34e97366
commit
9a2f936c05
@ -42,7 +42,12 @@ economy:
|
||||
net_per_cycle: -112
|
||||
gp_per_hour_low: -250000 # negative: this burns GP
|
||||
gp_per_hour_high: -200000
|
||||
xp_per_hour: 55800 # 31 xp/cast (wiki) @ ~1800 casts/hr (3-tick, wiki)
|
||||
xp_per_hour: 43400 # 31 xp/cast (wiki) @ 1,400 casts/hr pinned — one OSRS
|
||||
# tick = 0.6s, 3-tick cast = 1.8s; mandatory post-action
|
||||
# delay 1.6-3.4s (mid ~2.5s) -> 3,600/2.5 = ~1,440 casts/hr
|
||||
# ceiling, pinned at 1,400 for bank-cycle overhead. The
|
||||
# old 1,800 casts/hr (55,800 xp/hr) was not reachable
|
||||
# under the entry's own pacing.
|
||||
supplies:
|
||||
- item: gold necklace
|
||||
qty_per_cycle: 1
|
||||
@ -51,7 +56,8 @@ economy:
|
||||
- item: nature rune
|
||||
qty_per_cycle: 1
|
||||
source: ge
|
||||
ge_limit: 12500
|
||||
ge_limit: 18000 # mapping 'limit' + Module:Exchange/Nature_rune agree;
|
||||
# matches magic-alch-handoff (consistency check enforces)
|
||||
profit_drivers:
|
||||
- "NONE at current prices — XP is the product, not GP"
|
||||
- "Pick the candidate with the least-negative net each run (live rank)"
|
||||
@ -122,7 +128,9 @@ actions:
|
||||
on: "itself"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1600, 3400] # 3-tick cast = ~5.4s game; stay under it, humanised
|
||||
delay: [1600, 3400] # 3-tick cast = 1.8s game (3 ticks x 0.6s); post-action
|
||||
# delay mid ~2.5s -> ~1,400 casts/hr pinned; stay under
|
||||
# it, humanised
|
||||
|
||||
- id: a4
|
||||
step: bank-loop
|
||||
|
||||
@ -52,7 +52,14 @@ 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: 117000 # HLA tier (65 xp/cast, ~1800 casts/hr at the 3-tick spell speed — derived; LA tier 55800, SII tier 95400)
|
||||
xp_per_hour: 91000 # HLA tier: 65 xp/cast @ 1,400 casts/hr pinned — one
|
||||
# tick = 0.6s, 3-tick cast = 1.8s; mandatory post-action
|
||||
# delay 1.6-3.4s (mid ~2.5s) -> 3,600/2.5 = ~1,440 casts/hr
|
||||
# ceiling, pinned at 1,400 for bank-cycle overhead.
|
||||
# Tiers: LA 43,400 (31 x 1400) / SII 74,200 (53 x 1400)
|
||||
# / HLA 91,000 (65 x 1400). Old 1,800 casts/hr figures
|
||||
# (55,800 / 95,400 / 117,000) were not reachable under
|
||||
# the entry's own pacing.
|
||||
supplies:
|
||||
- item: nature rune
|
||||
qty_per_cycle: 1
|
||||
@ -71,10 +78,12 @@ state_machine:
|
||||
goal: be at a bank with GE access, staff of fire equipped, nature runes in stock
|
||||
next: [cast, bank_supplies, complete, failed]
|
||||
cast:
|
||||
goal: cast the current tier spell (LA below 43, SII 43-55 if chosen, HLA from 55)
|
||||
goal: cast the active tier spell with the tier-correct target — LA < 43 on a
|
||||
noted alch candidate; SII 43-55 on an unnoted ore whose live bar margin
|
||||
is positive (never a noted stack); HLA >= 55 on a noted alch candidate
|
||||
next: [cast, bank_supplies, complete, failed]
|
||||
bank_supplies:
|
||||
goal: deposit coins, restock nature runes (+ ores if running the SII window)
|
||||
goal: deposit coins and smelted bars, restock nature runes (+ ores during the SII window)
|
||||
next: [cast, idle]
|
||||
complete:
|
||||
goal: magic level target reached
|
||||
@ -85,10 +94,16 @@ state_machine:
|
||||
when: "nature rune count < buffer (or ore count < buffer in SII window)"
|
||||
to: bank_supplies
|
||||
- from: cast
|
||||
when: "level 43 reached and the SII window is chosen (margins positive)"
|
||||
when: "level < 43 and LA tier active (cast on the cheapest alch candidate)"
|
||||
to: cast
|
||||
- from: cast
|
||||
when: "level 55 reached"
|
||||
when: "level 43-55 and the SII window is chosen (some unnoted ore's bar margin is positive)"
|
||||
to: cast
|
||||
- from: cast
|
||||
when: "level 43-55 and the SII window is skipped (no positive-margin ore this window) — 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"
|
||||
@ -111,7 +126,7 @@ actions:
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "nature rune (and ores during the SII window)"
|
||||
item: "nature rune (and unnoted ores during the SII window — only recipes whose live bar margin is positive)"
|
||||
qty: "fill to buffer"
|
||||
max_price: "live low + 5%"
|
||||
on_fail: retry
|
||||
@ -119,24 +134,38 @@ actions:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a3
|
||||
step: cast-tier-spell
|
||||
step: cast-alch
|
||||
verb: use_item_on
|
||||
target: "tier supply in inventory (noted stack for LA/HLA, ore for SII)"
|
||||
target: "noted alch candidate in inventory (LA and HLA tiers)"
|
||||
params:
|
||||
spell: "Low Level Alchemy | Superheat Item | High Level Alchemy (per current tier)"
|
||||
item: "cheapest live candidate in inventory"
|
||||
spell: "Low Level Alchemy (level < 43) | High Level Alchemy (level >= 55)"
|
||||
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] # 3-tick cast = ~5.4s game; stay under it, humanised
|
||||
delay: [1600, 3400] # 3-tick cast = 1.8s game (3 ticks x 0.6s); post-action
|
||||
# delay mid ~2.5s -> ~1,400 casts/hr pinned; stay under
|
||||
# it, humanised
|
||||
|
||||
- id: a4
|
||||
step: cast-superheat
|
||||
verb: use_item_on
|
||||
target: "unnoted ore in inventory (SII tier only, 43-55)"
|
||||
params:
|
||||
spell: "Superheat Item"
|
||||
item: "an unnoted ore whose live bar margin is positive (bar GE price covers ore + nature cost); never a noted stack, never a price-only pick"
|
||||
on: "itself"
|
||||
on_fail: skip # no positive-margin ore -> skip SII, stay on LA
|
||||
anti_ban:
|
||||
delay: [1600, 3400]
|
||||
|
||||
- id: a5
|
||||
step: bank-loop
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
deposit: [all coins, all products]
|
||||
withdraw: "nature runes up to buffer (+ ores during the SII window)"
|
||||
deposit: [all coins, all products] # products incl. smelted bars during SII
|
||||
withdraw: "nature runes up to buffer (+ unnoted ores during the SII window)"
|
||||
on_fail: alert
|
||||
|
||||
run_control:
|
||||
|
||||
@ -11,11 +11,17 @@ Runs:
|
||||
=> WARNING (entry should be re-verified, not dropped)
|
||||
- `best_practice` lines whose source starts with "researched"
|
||||
=> WARNING (citation TODO — file a follow-up issue)
|
||||
- Cross-entry supply-fact consistency: the same (supply item,
|
||||
source) across entries must carry the same ge_limit. A diverging
|
||||
limit (e.g. nature rune "12500" in one entry vs "18000" in
|
||||
another) is a silent fact error, not a per-entry choice — GE
|
||||
buy limits are item-global.
|
||||
3. Global id uniqueness across the whole codex tree.
|
||||
|
||||
Exit codes: 0 = clean (warnings ok), 1 = validation errors.
|
||||
|
||||
Usage: python3 tools/validate.py [--stale-days 30]
|
||||
Usage: python3 tools/validate.py [--stale-days 30] [--selftest]
|
||||
--selftest: run the supply-consistency regression fixtures only
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
@ -41,12 +47,77 @@ def load_entries():
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def supply_limit_conflicts(rows):
|
||||
"""Cross-entry consistency for duplicate supply facts.
|
||||
|
||||
rows: iterable of (rel, data). The same (item, source) appearing in
|
||||
more than one entry must agree on ge_limit — GE buy limits are a
|
||||
property of the item, so a divergence is a silent fact error.
|
||||
Returns a list of error strings.
|
||||
"""
|
||||
facts = {}
|
||||
for rel, data in rows:
|
||||
eco = data.get("economy") or {}
|
||||
for sup in eco.get("supplies", []):
|
||||
lim = sup.get("ge_limit")
|
||||
if lim is None:
|
||||
continue
|
||||
key = (sup.get("item"), sup.get("source"))
|
||||
facts.setdefault(key, {}).setdefault(lim, set()).add(rel)
|
||||
errors = []
|
||||
for (item, source), by_lim in sorted(facts.items()):
|
||||
if len(by_lim) > 1:
|
||||
detail = "; ".join(
|
||||
f"limit {lim} in {', '.join(sorted(rs))}"
|
||||
for lim, rs in sorted(by_lim.items()))
|
||||
errors.append(
|
||||
f"conflicting ge_limit for supply '{item}' (source {source}): {detail}")
|
||||
return errors
|
||||
|
||||
|
||||
def run_selftest(verbose=False):
|
||||
"""Regression fixtures for the supply-consistency gate; error strings."""
|
||||
fixtures = [
|
||||
# name, rows, expect_error
|
||||
("identical (item, source) with diverging ge_limit (18000 vs 12500)",
|
||||
[("e1.yaml", {"economy": {"supplies": [
|
||||
{"item": "nature rune", "source": "ge", "ge_limit": 18000}]}}),
|
||||
("e2.yaml", {"economy": {"supplies": [
|
||||
{"item": "nature rune", "source": "ge", "ge_limit": 12500}]}})], True),
|
||||
("identical (item, source) with agreeing ge_limit (18000 both)",
|
||||
[("e1.yaml", {"economy": {"supplies": [
|
||||
{"item": "nature rune", "source": "ge", "ge_limit": 18000}]}}),
|
||||
("e2.yaml", {"economy": {"supplies": [
|
||||
{"item": "nature rune", "source": "ge", "ge_limit": 18000}]}})], False),
|
||||
("no supplies / missing ge_limit are not gated",
|
||||
[("e1.yaml", {"economy": {}}),
|
||||
("e2.yaml", {"economy": {"supplies": [
|
||||
{"item": "gold necklace", "source": "ge"}]}})], False),
|
||||
]
|
||||
errors = []
|
||||
for name, rows, expect_error in fixtures:
|
||||
got = bool(supply_limit_conflicts(rows))
|
||||
passed = got == expect_error
|
||||
if verbose:
|
||||
print(f"SELFTEST {'PASS' if passed else 'FAIL'} {name}")
|
||||
if not passed:
|
||||
errors.append(
|
||||
f"selftest: '{name}' expected "
|
||||
f"{'an error' if expect_error else 'no error'} but got "
|
||||
f"{'an error' if got else 'no error'}")
|
||||
return errors
|
||||
|
||||
|
||||
def main():
|
||||
global STALE_DAYS
|
||||
if "--stale-days" in sys.argv:
|
||||
STALE_DAYS = int(sys.argv[sys.argv.index("--stale-days") + 1])
|
||||
|
||||
errors, warnings = [], []
|
||||
if "--selftest" in sys.argv:
|
||||
errors += run_selftest(verbose=True)
|
||||
print(f"RESULT: selftest, {len(errors)} errors, 0 warnings")
|
||||
return 1 if errors else 0
|
||||
entries = load_entries()
|
||||
if not entries:
|
||||
print("ERROR: no codex entries found under codex/")
|
||||
@ -54,6 +125,7 @@ def main():
|
||||
|
||||
ids = {}
|
||||
today = datetime.date.today()
|
||||
rows = []
|
||||
|
||||
for path in entries:
|
||||
rel = str(path.relative_to(ROOT))
|
||||
@ -62,6 +134,7 @@ def main():
|
||||
except yaml.YAMLError as e:
|
||||
errors.append(f"{rel}: YAML parse error: {e}")
|
||||
continue
|
||||
rows.append((rel, data))
|
||||
|
||||
# 1. schema
|
||||
v = jsonschema.Draft7Validator(SCHEMA)
|
||||
@ -125,6 +198,9 @@ def main():
|
||||
f"{rel}: best_practice '{line.get('item') or line.get('name')}' "
|
||||
f"carries a 'researched' source — citation TODO, file follow-up")
|
||||
|
||||
# 2b. cross-entry supply-fact consistency (GE buy limits are item-global)
|
||||
errors += supply_limit_conflicts(rows)
|
||||
|
||||
# report
|
||||
for w in warnings:
|
||||
print(f"WARN {w}")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user