Codex entry: woodcutting (F2P) (Closes #3) #12
692
codex/training/woodcutting-f2p.yaml
Normal file
692
codex/training/woodcutting-f2p.yaml
Normal file
@ -0,0 +1,692 @@
|
||||
id: woodcutting-f2p
|
||||
title: Woodcutting (F2P)
|
||||
domain: training
|
||||
tags: [woodcutting, gathering, f2p, skill-training]
|
||||
status: verified
|
||||
version: 5 # 5: bounce-fix 4 2026-08-29 — governor 10398 + owner decision 10499: executable axe preparation (a2-a9 check/withdraw/GE-buy/wait/collect/equip-or-carry/loadout), unobtainable-axe -> failed (never complete), drop transition gated to afk-xp, per-SELECTED-row dynamic rates with 27/28 capacity from axe_equipped (woodcutting_fixtures recomputed by the validator), drop-mode realized GP zero
|
||||
updated: "2026-08-29" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
|
||||
|
||||
summary: >-
|
||||
F2P woodcutting on the verified F2P tree ladder — Tree (1/25xp), Oak
|
||||
(15/37.5xp), Willow (30/67.5xp), Maple (45/100xp, optional), Yew
|
||||
(60/175xp) — with THREE explicit, mutually exclusive run modes (owner
|
||||
decision 10288, 2026-08-28): `fast-xp` (XP-oriented progression, willow
|
||||
as the intended fast-XP target), `afk-xp` (quest/location-gated
|
||||
low-interaction progression, maple only when Dragon Slayer I access is
|
||||
satisfied), and `banked-income` (banked logs sold at the GE, yew from 60
|
||||
when eligible). Each mode has a deterministic first-match ladder (no
|
||||
parenthesized "or"), a level-1 fallback, a pinned activity state
|
||||
(mode + tree + spot), a complete start-to-finish lifecycle, and a worked
|
||||
example covering preparation, the first full-inventory boundary, the
|
||||
first scheduled break and the resume. Axe model (corrected per governor
|
||||
10323 / owner 10288): the account uses the best F2P axe whose
|
||||
WOODCUTTING level it meets (rune/gilded = 41); it equips it only when
|
||||
its separate ATTACK level is met (rune/gilded = 40), otherwise it
|
||||
carries that same axe (costing one inventory slot — 27 log slots vs 28
|
||||
when equipped). RATES are realized, not idealized: every mode's
|
||||
logs/hour equals its documented model chain (chop cadence x capacity /
|
||||
cycle incl. walking/depositing or dropping, x respawn/switch duty x
|
||||
break duty), and xp/hour and gp/hour derive from it — committed as
|
||||
structured fixtures in tools/validate.py. Verified 2026-08-28 (wiki
|
||||
axe/tree pages + prices.runescape.wiki latest).
|
||||
|
||||
accounts:
|
||||
membership: f2p
|
||||
min_level: 1 # Woodcutting 1 — normal trees from the start
|
||||
requires_quest: [] # EMPTY for the base entry — Tree/Oak/Willow/Yew need no quest. Dragon Slayer I is NOT an entry-level prerequisite: it gates ONLY the afk-xp maple row (Corsair Cove Resource Area), expressed in woodcutting_modes and the a1 ladder — never in accounts.requires_quest.
|
||||
requires_other:
|
||||
- "An axe. The account USES the best F2P axe whose Woodcutting level it meets (bronze 1 / iron 1 / steel 6 / black 11 / mithril 21 / adamant 31 / rune 41 / gilded 41 — verified wiki table 2026-08-28); it EQUIPS it only when its separate Attack level is met (steel 5 / black 10 / mithril 20 / adamant 30 / rune 40 / gilded 40), otherwise it CARRIES that same axe and chops from the inventory (legal at every level). A carried axe costs ONE log slot (27 log slots), an equipped axe costs none (28 log slots); inventory-full logic derives the count dynamically, never hardcoded."
|
||||
- "Combat 15+ to use the Draynor Village willow spot (two aggressive level-7 Dark wizards attack players under combat 15); accounts below that use the Edgeville willow instead (fast-xp and banked-income ladders both carry the combat split as two deterministic rows)"
|
||||
- "For the afk-xp maple row only: Dragon Slayer I completed (Corsair Cove Resource Area entrance gate); without it the mode's ladder falls back to willow"
|
||||
- "Bank within a short walk of the tier spot (Draynor, Edgeville, Lumbridge, or Corsair Cove bank); rune axe one-time cost ~7.1-7.3k gp (GE live 2026-08-28) if not already owned"
|
||||
|
||||
axe_tiers:
|
||||
# Canonical table (OSRS wiki axe pages, raw wikitext verified 2026-08-28):
|
||||
# woodcutting_level = tool-USE requirement; attack_level = EQUIP requirement.
|
||||
# tools/validate.py enforces this table exactly and recomputes axe_examples.
|
||||
- {axe: bronze, woodcutting_level: 1, attack_level: 1, members: false}
|
||||
- {axe: iron, woodcutting_level: 1, attack_level: 1, members: false}
|
||||
- {axe: steel, woodcutting_level: 6, attack_level: 5, members: false}
|
||||
- {axe: black, woodcutting_level: 11, attack_level: 10, members: false}
|
||||
- {axe: mithril, woodcutting_level: 21, attack_level: 20, members: false}
|
||||
- {axe: adamant, woodcutting_level: 31, attack_level: 30, members: false}
|
||||
- {axe: rune, woodcutting_level: 41, attack_level: 40, members: false}
|
||||
- {axe: gilded, woodcutting_level: 41, attack_level: 40, members: false}
|
||||
- {axe: dragon, woodcutting_level: 61, attack_level: 60, members: true}
|
||||
|
||||
axe_examples:
|
||||
# Committed regression for the equip-vs-carry rule (owner coverage 10288:
|
||||
# "a 1-Attack/41-Woodcutting account carries and uses a rune axe rather
|
||||
# than selecting a lower axe"). tools/validate.py recomputes each row.
|
||||
- {account: "1-Attack / 41-Woodcutting (owner coverage)", woodcutting_level: 41, attack_level: 1, expected_axe: rune, expected_equipped: false}
|
||||
- {account: "40-Attack / 41-Woodcutting", woodcutting_level: 41, attack_level: 40, expected_axe: rune, expected_equipped: true}
|
||||
- {account: "40-Attack / 30-Woodcutting (below adamant 31)", woodcutting_level: 30, attack_level: 40, expected_axe: mithril, expected_equipped: true}
|
||||
- {account: "5-Attack / 31-Woodcutting (adamant usable, not equippable)", woodcutting_level: 31, attack_level: 5, expected_axe: adamant, expected_equipped: false}
|
||||
- {account: "1-Attack / 1-Woodcutting", woodcutting_level: 1, attack_level: 1, expected_axe: iron, expected_equipped: true}
|
||||
- {account: "99-Attack / 99-Woodcutting (F2P top; dragon is members-only)", woodcutting_level: 99, attack_level: 99, expected_axe: rune, expected_equipped: true}
|
||||
|
||||
woodcutting_modes:
|
||||
# Deterministic first-match ladders, ordered min_level DESC. The selected
|
||||
# mode AND tree are pinned in activity state by a1 (never decided ad hoc).
|
||||
# MODEL SPLIT (governor 10398, bounce-fix 4): mode-level `model` holds the
|
||||
# mode-wide constants (drop/deposit/break); each LADDER ROW carries its own
|
||||
# chop_seconds_per_log / walk_seconds_each_way / respawn_duty / log_price
|
||||
# so rates derive from the SELECTED row, not a fixed top tree. The mode
|
||||
# `rates` block is the canonical TOP-ROW reference (ladder[0], axe
|
||||
# equipped = 28 slots, labeled as such); dynamic per-selected-row rates
|
||||
# with 27/28 capacity from axe_equipped are committed in
|
||||
# `woodcutting_fixtures` and recomputed by the validator.
|
||||
- mode: fast-xp
|
||||
intent: >-
|
||||
Maximum Woodcutting XP per hour on the F2P ladder. Willow is the
|
||||
intended fast-XP target from level 30 (67.5 xp/log, close bank loop,
|
||||
high chop cadence); oak covers 15-29 and normal trees below 15. The
|
||||
ladder intentionally does NOT switch to yew at 60 — the F2P guide's
|
||||
XP recommendation is willow (fast-xp stays on willow; yew is the
|
||||
banked-income target).
|
||||
inventory_policy: bank
|
||||
ladder:
|
||||
- {tree: willow, min_level: 30, gate: combat-15-plus, spot: draynor-willows, bank: draynor, chop_seconds_per_log: 4.0, walk_seconds_each_way: 12, respawn_duty: 0.92, log_price: 21}
|
||||
- {tree: willow, min_level: 30, gate: combat-under-15, spot: edgeville-willow, bank: edgeville, chop_seconds_per_log: 4.0, walk_seconds_each_way: 6, respawn_duty: 0.90, log_price: 21}
|
||||
- {tree: oak, min_level: 15, gate: none, spot: draynor-oaks, bank: draynor, chop_seconds_per_log: 3.5, walk_seconds_each_way: 8, respawn_duty: 0.93, log_price: 40}
|
||||
- {tree: tree, min_level: 1, gate: none, spot: lumbridge-west, bank: lumbridge, chop_seconds_per_log: 3.0, walk_seconds_each_way: 10, respawn_duty: 0.95, log_price: 23}
|
||||
model:
|
||||
drop_seconds_per_log: 0 # bank mode — no dropping
|
||||
deposit_seconds: 4
|
||||
break_duty: 0.94 # scheduled breaks land at the bank
|
||||
rates: # CANONICAL TOP-ROW reference ONLY:
|
||||
logs_per_hour: 622 # willow row, axe EQUIPPED (28 slots): 28/(28x4.0+2x12+4=140s)x3600x0.92x0.94 = 622.7; carried-axe/other-row profiles -> woodcutting_fixtures
|
||||
xp_per_hour: 41985 # 622 x 67.5 (willow xp/log, wiki infobox)
|
||||
gp_per_hour: 13062 # 622 x 21 (willow log sell, prices.runescape.wiki latest 2026-08-28)
|
||||
log_price: 21
|
||||
price_verified: "2026-08-28"
|
||||
fallback: "below level 30 -> oak (15+) then normal trees (1+); below combat 15 the Draynor willow row is replaced by the Edgeville willow row (same min_level, different gate)"
|
||||
|
||||
- mode: afk-xp
|
||||
intent: >-
|
||||
Low-interaction, quest/location-gated progression: maple at Corsair
|
||||
Cove Resource Area (100 xp/log, 4 trees, Dragon Slayer I gate) with
|
||||
dropping instead of banking — the fewest interactions per hour of the
|
||||
three modes. Maple is selected ONLY when level 45+ AND the quest gate
|
||||
is satisfied; otherwise the ladder falls back through willow/oak/tree.
|
||||
DROP mode: logs are dropped at the spot, so REALIZED GP is 0; the log
|
||||
sell value is carried as reference/opportunity cost only.
|
||||
inventory_policy: drop
|
||||
ladder:
|
||||
- {tree: maple, min_level: 45, gate: dragon-slayer-i, spot: corsair-cove-resource-area, bank: none-drop, chop_seconds_per_log: 6.0, walk_seconds_each_way: 0, respawn_duty: 0.80, log_price: 12}
|
||||
- {tree: willow, min_level: 30, gate: combat-15-plus, spot: draynor-willows, bank: none-drop, chop_seconds_per_log: 6.0, walk_seconds_each_way: 0, respawn_duty: 0.92, log_price: 21}
|
||||
- {tree: willow, min_level: 30, gate: combat-under-15, spot: edgeville-willow, bank: none-drop, chop_seconds_per_log: 6.0, walk_seconds_each_way: 0, respawn_duty: 0.90, log_price: 21}
|
||||
- {tree: oak, min_level: 15, gate: none, spot: draynor-oaks, bank: none-drop, chop_seconds_per_log: 5.5, walk_seconds_each_way: 0, respawn_duty: 0.93, log_price: 40}
|
||||
- {tree: tree, min_level: 1, gate: none, spot: lumbridge-west, bank: none-drop, chop_seconds_per_log: 5.0, walk_seconds_each_way: 0, respawn_duty: 0.95, log_price: 23}
|
||||
model:
|
||||
drop_seconds_per_log: 1.0 # dropping the whole load at the spot; NO bank legs (far-from-bank spot, per the guide's AFK trade-off)
|
||||
deposit_seconds: 0
|
||||
break_duty: 0.94 # scheduled breaks at the spot (drop mode has no bank)
|
||||
rates: # CANONICAL TOP-ROW reference ONLY:
|
||||
logs_per_hour: 386 # maple row, axe EQUIPPED (28 slots): 28/(28x(6.0+1.0)=196s)x3600x0.80x0.94 = 386.8; carried-axe (27-slot) profile -> woodcutting_fixtures
|
||||
xp_per_hour: 38600 # 386 x 100 (maple xp/log) — deliberately below fast-xp per the guide (maple is AFK, not top XP)
|
||||
gp_per_hour: 0 # DROP MODE: logs are dropped — REALIZED GP IS 0 (validator-enforced); log_price 12 is reference/opportunity cost only, never realized income
|
||||
log_price: 12
|
||||
price_verified: "2026-08-28"
|
||||
fallback: "below 45 or without Dragon Slayer I -> willow (30) then oak (15) then tree (1); never select maple without the quest gate"
|
||||
|
||||
- mode: banked-income
|
||||
intent: >-
|
||||
Banked logs sold at the GE: yew from level 60 (175 xp/log, 111 gp/log
|
||||
live 2026-08-28, Edgeville south spot ~8s from the bank) when
|
||||
eligible, with deterministic lower fallbacks (willow/oak/tree banked)
|
||||
below 60. This is the income mode — the mode split implements the F2P
|
||||
guide's "cut willow/maple for experience, yew for profit".
|
||||
inventory_policy: bank
|
||||
ladder:
|
||||
- {tree: yew, min_level: 60, gate: none, spot: edgeville-south-yews, bank: edgeville, chop_seconds_per_log: 7.5, walk_seconds_each_way: 8, respawn_duty: 0.52, log_price: 111}
|
||||
- {tree: willow, min_level: 30, gate: combat-15-plus, spot: draynor-willows, bank: draynor, chop_seconds_per_log: 4.0, walk_seconds_each_way: 12, respawn_duty: 0.92, log_price: 21}
|
||||
- {tree: willow, min_level: 30, gate: combat-under-15, spot: edgeville-willow, bank: edgeville, chop_seconds_per_log: 4.0, walk_seconds_each_way: 6, respawn_duty: 0.90, log_price: 21}
|
||||
- {tree: oak, min_level: 15, gate: none, spot: draynor-oaks, bank: draynor, chop_seconds_per_log: 3.5, walk_seconds_each_way: 8, respawn_duty: 0.93, log_price: 40}
|
||||
- {tree: tree, min_level: 1, gate: none, spot: lumbridge-west, bank: lumbridge, chop_seconds_per_log: 3.0, walk_seconds_each_way: 10, respawn_duty: 0.95, log_price: 23}
|
||||
model:
|
||||
drop_seconds_per_log: 0
|
||||
deposit_seconds: 4
|
||||
break_duty: 0.94 # scheduled breaks at the bank
|
||||
rates: # CANONICAL TOP-ROW reference ONLY:
|
||||
logs_per_hour: 214 # yew row, axe EQUIPPED (28 slots): 28/(28x7.5+2x8+4=230s)x3600x0.52x0.94 = 214.2; carried-axe/other-row profiles -> woodcutting_fixtures
|
||||
xp_per_hour: 37450 # 214 x 175 (yew xp/log)
|
||||
gp_per_hour: 23754 # 214 x 111 (yew log sell, prices.runescape.wiki latest 2026-08-28)
|
||||
log_price: 111
|
||||
price_verified: "2026-08-28"
|
||||
fallback: "below 60 -> willow (30) then oak (15) then tree (1), all banked; yew never selected below 60"
|
||||
|
||||
# Dynamic per-selected-row rate proof (governor 10398, bounce-fix 4): the
|
||||
# mode `rates` blocks above are the canonical TOP-ROW references only;
|
||||
# these fixtures prove the entry derives logs/xp/gp from the SELECTED
|
||||
# ladder row AND the actual 27/28-slot capacity from axe_equipped. Each
|
||||
# row is recomputed by the validator (select_ladder_row + best_axe +
|
||||
# capacity = 28 - (1 if carried else 0)) and must match within tolerance.
|
||||
woodcutting_fixtures:
|
||||
- account: "level-1 fast-xp"
|
||||
mode: fast-xp
|
||||
woodcutting_level: 1
|
||||
attack_level: 1
|
||||
quest_gate: false
|
||||
combat_level: 3
|
||||
expected_tree: tree
|
||||
expected_axe: iron
|
||||
expected_equipped: true # 1 WC / 1 Attack: iron axe (tie rank) equipped
|
||||
expected_logs_per_hour: 833
|
||||
expected_xp_per_hour: 20836
|
||||
expected_gp_per_hour: 19170
|
||||
- account: "level-50 fast-xp"
|
||||
mode: fast-xp
|
||||
woodcutting_level: 50
|
||||
attack_level: 40
|
||||
quest_gate: false
|
||||
combat_level: 50
|
||||
expected_tree: willow
|
||||
expected_axe: rune
|
||||
expected_equipped: true # 28 log slots — matches the canonical top-row reference
|
||||
expected_logs_per_hour: 622
|
||||
expected_xp_per_hour: 41985
|
||||
expected_gp_per_hour: 13062
|
||||
- account: "level-50 afk-xp with Dragon Slayer I (maple)"
|
||||
mode: afk-xp
|
||||
woodcutting_level: 50
|
||||
attack_level: 1
|
||||
quest_gate: true
|
||||
combat_level: 50
|
||||
expected_tree: maple
|
||||
expected_axe: rune
|
||||
expected_equipped: false # owner coverage: rune axe CARRIED -> 27 log slots
|
||||
expected_logs_per_hour: 387
|
||||
expected_xp_per_hour: 38674
|
||||
expected_gp_per_hour: 0 # drop mode — realized GP is 0
|
||||
- account: "level-50 afk-xp WITHOUT Dragon Slayer I (willow fallback)"
|
||||
mode: afk-xp
|
||||
woodcutting_level: 50
|
||||
attack_level: 1
|
||||
quest_gate: false
|
||||
combat_level: 50
|
||||
expected_tree: willow
|
||||
expected_axe: rune
|
||||
expected_equipped: false # carried rune axe -> 27 log slots
|
||||
expected_logs_per_hour: 445 # willow drop row, rune CARRIED (27 slots): 27/(27x(6.0+1.0)=189s)x3600x0.92x0.94 = 444.8
|
||||
expected_xp_per_hour: 30021 # 444.8 x 67.5 (willow xp/log)
|
||||
expected_gp_per_hour: 0 # drop mode — realized GP is 0
|
||||
- account: "level-70 banked-income (yew)"
|
||||
mode: banked-income
|
||||
woodcutting_level: 70
|
||||
attack_level: 40
|
||||
quest_gate: false
|
||||
combat_level: 60
|
||||
expected_tree: yew
|
||||
expected_axe: rune
|
||||
expected_equipped: true # 28 log slots — matches the canonical top-row reference
|
||||
expected_logs_per_hour: 214
|
||||
expected_xp_per_hour: 37450
|
||||
expected_gp_per_hour: 23754
|
||||
- account: "level-30 fast-xp under combat 15 (Edgeville willow)"
|
||||
mode: fast-xp
|
||||
woodcutting_level: 30
|
||||
attack_level: 1
|
||||
quest_gate: false
|
||||
combat_level: 3
|
||||
expected_tree: willow
|
||||
expected_axe: mithril
|
||||
expected_equipped: false # 30 WC selects mithril (21); Attack 1 < 20 -> CARRY -> 27 log slots
|
||||
expected_logs_per_hour: 663
|
||||
expected_xp_per_hour: 44766
|
||||
expected_gp_per_hour: 13927
|
||||
|
||||
best_practice:
|
||||
equipment:
|
||||
- item: "rune axe (best F2P axe: 41 Woodcutting to USE, 40 Attack to EQUIP)"
|
||||
why: "fastest F2P chop rate; per the wiki axe table Rune axe (41) and Gilded axe (41) are 'the best axes available in Free-to-play' (dragon is members-only). F2P ladder: Bronze(1)/Iron(1)/Steel(6)/Black(11)/Mithril(21)/Adamant(31)/Rune(41)/Gilded(41) — the number is the WOODCUTTING level to use the axe for chopping, NOT an Attack gate. EQUIP is a separate Attack requirement (rune/gilded 40): equip only when the account meets it, otherwise carry that same axe and chop from the inventory — legal at every level, costs one log slot. Never select a lower axe because the Attack level is low."
|
||||
source: "OSRS wiki — Woodcutting (axe table incl. F2P/members flags) + Rune axe / Gilded axe / Black axe item pages raw wikitext 2026-08-28 ('Level 41 Woodcutting is required to use this axe' / 'Attack level of 40 to wield this axe'; gilded identical; black: 11 to use, 10 to equip; dragon: 61/60 members); governor review comment 10248 + owner decision 10288 (carry-and-use rule)"
|
||||
techniques:
|
||||
- name: pick the mode first, then the deterministic ladder
|
||||
why: "Owner decision 10288: run exactly ONE of fast-xp / afk-xp / banked-income; the ladder inside the mode decides the tree by level + quest/combat gates (first match, min_level DESC — no parenthesized 'or'), and the chosen mode + tree are pinned in activity state. fast-xp never silently becomes income; afk-xp never runs maple without the quest gate; banked-income never claims yew below 60."
|
||||
source: "owner-guidance comment 10288 (2026-08-28); governor 10248 (deterministic tier field)"
|
||||
- name: per-tier route table (spots and banks, wiki 2026-08-27/28)
|
||||
why: "Tree (1) — west of Lumbridge Castle, bank Lumbridge; Oak (15) — two east of Draynor Village bank (alt: south-west of Varrock west bank), bank Draynor; Willow (30) — south-west of Draynor Village bank GATED to combat 15+ (two aggressive level-7 Dark wizards attack under combat 15), sub-15 accounts use the single willow East of Edgeville, bank Edgeville; Maple (45) — Corsair Cove Resource Area (4 trees, only F2P maples, requires Dragon Slayer I), far from a bank; Yew (60) — two south of Edgeville bank, bank Edgeville."
|
||||
source: "OSRS wiki — Free-to-play Woodcutting training + Draynor Village + tree location lists (2026-08-27, re-verified 2026-08-28)"
|
||||
- name: drop versus bank is a MODE property, not an ad-hoc choice
|
||||
why: "fast-xp banks (Draynor loop keeps the bank close and income attached); afk-xp DROPS (Corsair Cove is far from a bank — the guide's AFK trade-off; no bank legs at all); banked-income banks and sells (yew 111 gp/log live). Each mode's rate chain includes its own dropping or banking route (model fields + validator chain)."
|
||||
source: "owner-guidance comment 10288 (complete dropping or banking route per mode); OSRS wiki — Free-to-play Woodcutting training (drop-vs-bank trade-off)"
|
||||
- name: bank logs, keep the axe
|
||||
why: "Logs are F2P-tradeable income: live sell prices 2026-08-28 — willow 21, oak 40, maple 12, yew 111 per log (GE limits 15k/15k/15k/12k per 4h — none binding at modeled rates). deposit_logs deposits logs ONLY — the axe is never deposited (equipped or carried)."
|
||||
source: "prices.runescape.wiki latest 2026-08-28"
|
||||
- name: rates are realized, not idealized
|
||||
why: "Every mode's rates come from its documented model chain: capacity / cycle (chop seconds x capacity + walking + depositing, or + dropping) x 3600 x respawn/switch duty x break duty — including travel, tree respawn, dropping/banking and mandatory breaks. The validator recomputes the chain within 1% and derives xp/gp from it; no hidden 'chop loop only' numbers."
|
||||
source: "owner-guidance comment 10288 (validators and rate claims must include travel, respawn, dropping or banking, and mandatory breaks)"
|
||||
|
||||
economy:
|
||||
# Verified 2026-08-28 against prices.runescape.wiki latest + OSRS wiki
|
||||
# tree-page infoboxes. Logs are pure income (no per-cycle cost; axe is
|
||||
# one-time equipment). Anchor = fast-xp mode (Draynor willows, combat 15+).
|
||||
item: Willow logs
|
||||
item_id: 1519
|
||||
cost_per_cycle: 0 # no supplies consumed; axe cost amortised
|
||||
income_per_cycle: 21 # willow log sell price (latest.high 2026-08-28)
|
||||
net_per_cycle: 21
|
||||
gp_per_hour_low: 13100 # fast-xp realized banked income: 622 logs/hr x 21 (model chain, 2026-08-28)
|
||||
gp_per_hour_high: 23800 # banked-income realized: 214 yew logs/hr x 111 (model chain, 2026-08-28)
|
||||
xp_per_hour: 42000 # fast-xp realized: 41,985 (622 willow logs/hr x 67.5; see woodcutting_modes)
|
||||
supplies: []
|
||||
profit_drivers:
|
||||
- "Logs are the income: sell at the GE (willow 21, oak 40, maple 12, yew 111 per log — live 2026-08-28; all F2P-tradeable with 12k-15k/4h buy limits, none binding at modeled rates)"
|
||||
- "THREE explicit modes (owner decision 10288), each with a deterministic ladder and realized rates — fast-xp 622 logs/hr -> 41,985 xp/hr (willow, banked); afk-xp 386 logs/hr -> 38,600 xp/hr (maple @45 + Dragon Slayer I, dropped); banked-income 214 logs/hr -> 37,450 xp/hr and 23,754 gp/hr (yew @60, banked)"
|
||||
- "Mode split implements the F2P guide: willow/maple for experience, yew for profit — fast-xp stays on willow past 60; banked-income takes yew at 60+"
|
||||
- "Maple (45, Dragon Slayer I, Corsair Cove Resource Area) is an optional AFK F2P tier: 100 xp/log, 4 trees, far from a bank — more AFK, lower xp/hr than willows (modeled accordingly)"
|
||||
- "Axe economics: best F2P axe by Woodcutting level (rune from 41); one-time GE cost ~7.1-7.3k gp (2026-08-28); equip only when Attack permits, else carry (one log slot)"
|
||||
|
||||
state_machine:
|
||||
initial: idle
|
||||
terminal: [complete, failed]
|
||||
states:
|
||||
idle:
|
||||
goal: "logged in at a bank with an axe available (bank or inventory); choose run mode (explicit run parameter) and confirm prerequisites"
|
||||
next: [arm_run, complete]
|
||||
arm_run:
|
||||
goal: "PIN the run in activity state: mode (fast-xp | afk-xp | banked-income), tree, spot, bank, axe, axe_equipped (true/false) — decided once by the a1 ladder, never changed mid-run; re-arm only on fallback events"
|
||||
next: [prepare, idle]
|
||||
prepare:
|
||||
goal: >-
|
||||
EXECUTABLE axe preparation (governor 10398, bounce-fix 4), run
|
||||
BEFORE walking to the trees: (1) check inventory and bank for the
|
||||
pinned best axe (a2); (2) owned -> withdraw it (a3); missing ->
|
||||
GE purchase chain a4 open-ge -> a5 buy-axe (ONE offer) -> a6
|
||||
wait-ge-fill (BOUNDED fill window; on no-fill/partial-fill TIMEOUT
|
||||
cancel the resting offer and collect the item or refund) -> a7
|
||||
collect-axe (ge_collect + has_item verification); (3) equip it
|
||||
ONLY when level_is Attack >= the tier's equip level (a8, menu_option
|
||||
Wield) or carry the same axe; (4) build the initial inventory (a9:
|
||||
axe + empty log slots — nothing else is needed). An axe that cannot
|
||||
be obtained after the bounded recovery (3 acquisition loops) is a
|
||||
FAILED run: log ERROR + alert, never a silent lower-axe continue
|
||||
and never a 'complete'.
|
||||
next: [walk_to_trees, prepare, failed]
|
||||
walk_to_trees:
|
||||
goal: "walk the pinned tier route from the bank to the pinned tree spot"
|
||||
next: [chopping, idle]
|
||||
chopping:
|
||||
goal: "chop the pinned tree until the inventory is full or the tree falls; one chop attempt = click + observe (log gained / tree fell / full / fail) — see a11"
|
||||
next: [drop_logs, walk_to_bank, walk_to_trees, complete, failed]
|
||||
drop_logs:
|
||||
goal: "AFK-XP DROP MODE ONLY: drop the whole load at the spot (a13, menu_option per log group); inventory now empty; no bank legs"
|
||||
next: [chopping, walk_to_trees]
|
||||
walk_to_bank:
|
||||
goal: "walk the pinned route from the tree spot to the pinned bank (bank modes only: fast-xp | banked-income)"
|
||||
next: [deposit_logs]
|
||||
deposit_logs:
|
||||
goal: "deposit ALL logs (a15 — logs only; the axe stays equipped or carried, never deposited); short bank trip lands the scheduled break"
|
||||
next: [walk_back]
|
||||
walk_back:
|
||||
goal: "walk back from the bank to the pinned tree spot"
|
||||
next: [chopping, idle]
|
||||
complete:
|
||||
goal: "XP/level target or GP target reached — collect and stop"
|
||||
failed:
|
||||
goal: "hard failure — stop, log ERROR sentinel, alert"
|
||||
transitions:
|
||||
- from: idle
|
||||
when: "mode + prerequisites confirmed and session target not reached"
|
||||
to: arm_run
|
||||
- from: idle
|
||||
when: "session target already reached at session start"
|
||||
to: complete
|
||||
- from: arm_run
|
||||
when: "ladder picked a row (mode, tree, spot, bank pinned in activity state) — axe decided: highest usable by Woodcutting level, equip only when Attack permits else carry"
|
||||
to: prepare
|
||||
- from: arm_run
|
||||
when: "maple row selected but Dragon Slayer I access denied at the gate — re-arm on the next ladder row (fallback) and log the denial"
|
||||
to: idle
|
||||
- from: prepare
|
||||
when: "axe acquired (owned withdraw or GE open/buy/wait/collect verified) AND equipped-or-carried (a8) AND loadout built (a9)"
|
||||
to: walk_to_trees
|
||||
- from: prepare
|
||||
when: "GE buy offer still filling (a6 bounded window polling) or a partial fill re-placed — loop the acquisition chain"
|
||||
to: prepare
|
||||
- from: prepare
|
||||
when: "3 consecutive no-fill GE rounds or the bank also lacks any usable F2P axe — axe unobtainable: log ERROR + alert"
|
||||
to: failed
|
||||
- from: walk_to_trees
|
||||
when: "at the pinned tree spot"
|
||||
to: chopping
|
||||
- from: chopping
|
||||
when: "mode is afk-xp (inventory_policy drop) AND inventory full (28 log slots with the axe equipped, 27 with the axe carried — derived dynamically, never hardcoded)"
|
||||
to: drop_logs
|
||||
- from: chopping
|
||||
when: "mode is fast-xp OR banked-income (inventory_policy bank) AND inventory full — the two full-inventory routes are mutually exclusive by mode"
|
||||
to: walk_to_bank
|
||||
- from: chopping
|
||||
when: "tree fell and no interactable tree at the spot (> 30s empty) — move to the next tree or hop_world (a17)"
|
||||
to: walk_to_trees
|
||||
- from: drop_logs
|
||||
when: "load dropped — inventory empty"
|
||||
to: chopping
|
||||
- from: walk_to_bank
|
||||
when: "at the pinned bank"
|
||||
to: deposit_logs
|
||||
- from: deposit_logs
|
||||
when: "logs deposited (axe still present)"
|
||||
to: walk_back
|
||||
- from: walk_back
|
||||
when: "at the pinned tree spot"
|
||||
to: chopping
|
||||
- from: walk_back
|
||||
when: "spot unavailable on return (crowded > 60s or invalid access) — re-arm on the next ladder row (fallback)"
|
||||
to: idle
|
||||
- from: idle
|
||||
when: "woodcutting level >= target (fast-xp / afk-xp) or GP target banked (banked-income)"
|
||||
to: complete
|
||||
|
||||
actions:
|
||||
- id: a1
|
||||
step: arm-run
|
||||
verb: level_is
|
||||
target: "woodcutting level (mode ladder)"
|
||||
params:
|
||||
mode: "fast-xp | afk-xp | banked-income — explicit run parameter, pinned with the chosen tree/spot/bank in activity state (owner 10288)"
|
||||
ladder: "deterministic first match, min_level DESC (see woodcutting_modes): fast-xp -> willow@30 (combat-15-plus: draynor / combat-under-15: edgeville) -> oak@15 -> tree@1; afk-xp -> maple@45 (dragon-slayer-i gate) -> willow@30 -> oak@15 -> tree@1; banked-income -> yew@60 -> willow@30 -> oak@15 -> tree@1"
|
||||
spot: "per the chosen row (route table in a10)"
|
||||
pin: "{mode, tree, spot, bank, axe, axe_equipped} written to activity state — never re-decided mid-run except by explicit fallback events"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [200, 900]
|
||||
|
||||
- id: a2
|
||||
step: check-axe
|
||||
verb: has_item
|
||||
target: "the pinned best axe tier (by Woodcutting level — see axe_tiers)"
|
||||
params:
|
||||
check: "inventory AND bank (bank_open / item_count); branch: owned -> a3 withdraw-axe; missing -> the GE purchase chain a4 -> a5 -> a6 -> a7"
|
||||
decide: "never a lower axe because the Attack level is low — the WOODCUTTING level selects the tier, Attack only decides equip-or-carry (axe_examples)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [300, 1000]
|
||||
|
||||
- id: a3
|
||||
step: withdraw-axe
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
withdraw: "1x the pinned best axe tier (owned-tool branch — the axe comes from the bank, never bought twice)"
|
||||
verify: "has_item axe == true after withdrawal"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [400, 1200]
|
||||
|
||||
- id: a4
|
||||
step: open-ge
|
||||
verb: ge_open
|
||||
target: grand-exchange
|
||||
params:
|
||||
location: "the GE booth (bank side) — REQUIRED before any ge_buy / ge_collect: the exchange interface must be open before an offer is placed or items are collected"
|
||||
note: "only the purchase branch reaches the GE (a2 missing-tool); owned axes go straight to a3 withdraw-axe"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1600]
|
||||
|
||||
- id: a5
|
||||
step: buy-axe
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "the pinned best axe tier (Woodcutting-gated)"
|
||||
qty: 1
|
||||
max_price: "live high + 10% cap (rune axe ~7.1-7.3k gp live 2026-08-28; GE limit 40/4h — one axe is trivial)"
|
||||
note: "place exactly ONE buy offer; the fill waits at most the BOUNDED a6 window — a resting offer is never retried indefinitely"
|
||||
pair_with: "a4 open-ge (before) then a6 wait-ge-fill (bounded) then a7 collect-axe (collect + verify)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a6
|
||||
step: wait-ge-fill
|
||||
verb: ge_offers_done
|
||||
target: "the axe purchase offer"
|
||||
params:
|
||||
until: "the axe offer FILLS (has_item axe) — poll for exactly ONE bounded fill window (3-6 min); on no-fill / partial-fill TIMEOUT: CANCEL the resting offer, collect the item if it finally filled or the gp refund (a7), then verify — fail CLOSED via prepare recovery (3 loops -> failed); never re-place the same resting offer"
|
||||
on_fail: alert
|
||||
anti_ban:
|
||||
delay: [900, 2400]
|
||||
|
||||
- id: a7
|
||||
step: collect-axe
|
||||
verb: ge_collect
|
||||
target: grand-exchange
|
||||
params:
|
||||
collect: "finished GE transactions — the bought axe if the offer filled, and the gp refund if it was cancelled on timeout; collection closes the offer slot"
|
||||
verify: "has_item axe == true after collection; if still missing (no-fill / cancelled), loop the acquisition chain via prepare (a2 path, 3 loops -> failed) — a failed purchase is diagnosed, never silently re-queued"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [600, 1400]
|
||||
|
||||
- id: a8
|
||||
step: equip-or-carry-axe
|
||||
verb: menu_option
|
||||
target: "best axe in inventory (decided by Woodcutting level)"
|
||||
params:
|
||||
rule: "equip (menu_option 'Wield <axe>') ONLY when Attack >= the axe's equip level (rune/gilded 40, adamant 30, mithril 20, black 10, steel 5); otherwise carry it and chop from the inventory — the same axe, never a lower one because Attack is low (owner coverage: 1-Attack/41-Woodcutting carries and uses rune)"
|
||||
action: "menu_option axe -> 'Wield <axe>'"
|
||||
capacity: "equipped: 28 log slots; carried: 27 (one slot for the axe)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [300, 1100]
|
||||
|
||||
- id: a9
|
||||
step: build-loadout
|
||||
verb: has_item
|
||||
target: "the pinned axe tier in inventory (equipped or carried)"
|
||||
params:
|
||||
verify: "inventory holds exactly ONE axe of the pinned tier and the rest is empty log space — the initial inventory needs nothing else (no supplies); the axe is never deposited mid-run"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [300, 900]
|
||||
|
||||
- id: a10
|
||||
step: walk-to-trees
|
||||
verb: walk_to
|
||||
target: "pinned tier tree spot"
|
||||
params:
|
||||
route: "tree -> west of Lumbridge Castle (bank lumbridge); oak -> two east of Draynor Village bank (bank draynor); willow -> south-west of Draynor Village bank, combat 15+ only (bank draynor) OR the single willow East of Edgeville, combat under 15 (bank edgeville); maple -> Corsair Cove Resource Area, Dragon Slayer I gate (no bank — drop mode); yew -> two south of Edgeville bank (bank edgeville)"
|
||||
method: [walk, teleport]
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [400, 1600]
|
||||
|
||||
- id: a11
|
||||
step: chop-tree
|
||||
verb: click_object
|
||||
target: "tree of the pinned tier"
|
||||
params:
|
||||
object: "tree/oak/willow/maple/yew per the pinned ladder row"
|
||||
attempt: "one chop = click + observe: (a) log gained -> continue; (b) tree fell -> switch to the next interactable tree at the spot; (c) inventory full -> route to drop (afk-xp) or bank (fast-xp / banked-income) by the mode-gated transitions; (d) no interactable tree for > 30s -> walk_to_trees move / hop_world (a17); (e) click failed -> retry up to 3, then alert"
|
||||
rhythm: "humanised cadence; the SELECTED ladder row's chop_seconds_per_log (woodcutting_modes) — never a fixed top-tree cadence"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [900, 2400]
|
||||
|
||||
- id: a12
|
||||
step: check-inventory
|
||||
verb: item_count
|
||||
target: "logs in inventory"
|
||||
params:
|
||||
item: "logs of the pinned tier"
|
||||
op: "inventory full -> drop (a13, afk-xp ONLY) or walk_to_bank (a14, fast-xp / banked-income ONLY); available log slots = 28 with the axe equipped, 27 with the axe carried — never hardcode 28"
|
||||
on_fail: skip
|
||||
anti_ban:
|
||||
delay: [150, 700]
|
||||
|
||||
- id: a13
|
||||
step: drop-load
|
||||
verb: menu_option
|
||||
target: "logs (afk-xp drop mode only)"
|
||||
params:
|
||||
action: "drop all logs of the pinned tier at the spot (grouped menu_option drops)"
|
||||
note: "no bank legs in drop mode — the rate chain uses the drop_seconds_per_log model constant; realized gp from dropped logs is 0 (log value is reference/opportunity cost only)"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [200, 800]
|
||||
|
||||
- id: a14
|
||||
step: walk-to-bank
|
||||
verb: walk_to
|
||||
target: "pinned bank from the tree spot"
|
||||
params:
|
||||
bank: "lumbridge (tree) | draynor (oak / combat-15+ willow) | edgeville (combat-under-15 willow / yew)"
|
||||
method: [walk]
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [400, 1600]
|
||||
|
||||
- id: a15
|
||||
step: deposit-logs
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
deposit: [all logs] # logs ONLY — the axe stays (equipped or carried); never deposit the axe
|
||||
withdraw: []
|
||||
break: "scheduled breaks land here (bank modes) — see a18"
|
||||
on_fail: alert
|
||||
|
||||
- id: a16
|
||||
step: walk-back-to-trees
|
||||
verb: walk_to
|
||||
target: "pinned tree spot from the bank"
|
||||
params:
|
||||
route: "reverse of a14 — back to the same spot"
|
||||
spot_check: "if the spot is unavailable on return (crowded > 60s -> hop_world; invalid access -> re-arm on the next ladder row)"
|
||||
method: [walk]
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [400, 1600]
|
||||
|
||||
- id: a17
|
||||
step: move-or-hop-empty
|
||||
verb: hop_world
|
||||
target: "current spot"
|
||||
params:
|
||||
when: "no interactable tree for > 30s at the pinned spot, or the spot is crowded for > 60s (shared spots: Draynor willows, Edgeville yews)"
|
||||
then: "move to the next tree at the same spot (walk_to), then hop_world if the spot is still empty for > 60s"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [800, 2200]
|
||||
|
||||
- id: a18
|
||||
step: take-break
|
||||
verb: take_break
|
||||
target: "current location"
|
||||
params:
|
||||
duration: "[45, 150] seconds of idle AFK per break (run_control.break.for)"
|
||||
safe: "bank modes: break lands at the bank after deposit_logs (inventory empty, axe safe); drop mode: break at the spot with an empty inventory — logs dropped, axe in hand; NEVER mid-chop (axe in tree) or mid-walk"
|
||||
resume: "continue from the pinned activity state (mode/tree/spot unchanged): bank modes -> walk_back (a16) -> chopping; drop mode -> chopping"
|
||||
on_fail: skip
|
||||
|
||||
run_control:
|
||||
iteration: "one full inventory cycle: chop load -> (drop at the spot) or (bank + walk back) -> resume chopping the pinned tree; cycle length = 28 x chop_seconds_per_log + drop or bank legs per woodcutting_modes.model"
|
||||
max_iterations: null
|
||||
break:
|
||||
every: [8, 14] # inventory cycles (breaks land at the bank in bank modes, at the spot in drop mode — INDEPENDENT of incidental banking)
|
||||
for: [45, 150] # seconds of idle AFK
|
||||
stop_conditions:
|
||||
- "woodcutting level >= target (fast-xp / afk-xp) or GP target banked (banked-income)"
|
||||
- "axe lost and unobtainable at bank + GE (recovery exhausted)"
|
||||
- "3 consecutive empty-tree waits after hop_world (world dead or spot gone — stop and report)"
|
||||
- "3 consecutive failed clicks (client issue — do not loop)"
|
||||
|
||||
# ================= WORKED EXAMPLES (owner 10288: each mode = preparation -> first
|
||||
# full-inventory boundary -> first scheduled break -> resume) =================
|
||||
# Rates below are the model-chain values from woodcutting_modes
|
||||
# (validator-pinned; the SELECTED ladder row's chop/walk/respawn + the
|
||||
# 27/28-slot capacity from axe_equipped — see woodcutting_fixtures).
|
||||
#
|
||||
# --- fast-xp @ level 50 (pinned per governor 10248: level-50 example) ---
|
||||
# Account: 50 Woodcutting, 40+ Attack, combat 15+. Mode fast-xp, ladder
|
||||
# first match = willow@30 combat-15-plus -> Draynor willows, bank Draynor.
|
||||
# a1 pins {mode fast-xp, tree willow, spot draynor-willows, bank draynor,
|
||||
# axe rune, axe_equipped true}. Preparation (prepare state): a2 check-axe
|
||||
# -> owned -> a3 withdraw-axe; a8 rune axe equip gate — 40 Attack >= 40 ->
|
||||
# EQUIPPED (28 log slots); a9 loadout verified.
|
||||
# a11 chop: 28 logs at ~4s each (112s) -> a12 inventory full -> a14 walk to
|
||||
# Draynor bank (~12s) -> a15 deposit (4s) -> a16 walk back (~12s).
|
||||
# FIRST FULL-INVENTORY BOUNDARY crossed at 140s: 28 willow logs banked.
|
||||
# Cycle x respawn 0.92 x break 0.94 = 622 logs/hr model; xp 41,985/hr.
|
||||
# Every 8-14 cycles (mid ~11 = ~25.7 min), a18 take-break 45-150s LANDS AT
|
||||
# THE BANK (post-deposit, inventory empty, axe safe) — a scheduled break,
|
||||
# independent of the incidental per-load banking. Resume: a16 walk back,
|
||||
# chopping continues from the pinned state.
|
||||
# Session target 250k xp: ~6h of cycles + 6-7 breaks -> complete.
|
||||
#
|
||||
# --- afk-xp @ level 50 with Dragon Slayer I (pinned level-50 example) ---
|
||||
# Account: 50 Woodcutting, 1 Attack, Dragon Slayer I done. Mode afk-xp,
|
||||
# ladder first match = maple@45 dragon-slayer-i -> Corsair Cove Resource
|
||||
# Area. a1 pins {mode afk-xp, tree maple, spot corsair-cove-resource-area,
|
||||
# bank none-drop, axe rune, axe_equipped FALSE}. a2/a3: rune axe owned ->
|
||||
# withdrawn; a8: rune axe usable at 41 WC -> selected; 1 Attack < 40 ->
|
||||
# CARRIED (27 log slots) — the owner coverage case: rune axe used from the
|
||||
# inventory, never a lower axe.
|
||||
# a11 chop until 27 logs (~162s at 6s each incl. post-fall reaction); tree
|
||||
# falls ~60s so the spot's 4 trees rotate; a12 full -> a13 DROP the load at
|
||||
# the spot (~27s) — no bank legs at all. FIRST FULL-INVENTORY BOUNDARY at
|
||||
# ~189s (dropped, not banked). Cycle x respawn 0.80 x break 0.94 = 387
|
||||
# logs/hr model; xp ~38,670/hr; REALIZED GP 0 (drop mode — log value 12
|
||||
# gp/log is reference/opportunity cost only, never realized income);
|
||||
# ~387 clicks/hr — the lowest-interaction mode. a18 break 45-150s AT THE
|
||||
# SPOT (inventory empty, axe in hand) every 8-14 cycles, INDEPENDENT of the
|
||||
# per-load dropping. Resume: a11 continues.
|
||||
# WITHOUT Dragon Slayer I the same account falls back to willow@30
|
||||
# (combat-15-plus) — deterministic, logged, never a silent maple
|
||||
# (woodcutting_fixtures level-50 no-quest row).
|
||||
#
|
||||
# --- banked-income @ level 70 (pinned level-70 example) ---
|
||||
# Account: 70 Woodcutting, 40+ Attack, combat 15+. Mode banked-income,
|
||||
# ladder first match = yew@60 -> Edgeville south yews, bank Edgeville.
|
||||
# a1 pins {mode banked-income, tree yew, spot edgeville-south-yews, bank
|
||||
# edgeville, axe rune, axe_equipped true}; a2/a3 withdraw owned rune axe;
|
||||
# a8 equipped (40 Attack). a11 chop: 28 yew logs at ~7.5s each (210s; yew
|
||||
# falls ~114s, respawns ~59.4s — the 2-tree spot rotates, occasionally
|
||||
# both down -> respawn_duty 0.52). a12 full -> a14 walk to Edgeville bank
|
||||
# (~8s) -> a15 deposit (4s) -> a16 walk back (~8s). FIRST FULL-INVENTORY
|
||||
# BOUNDARY at 230s: 28 yew logs banked. Cycle x respawn 0.52 x break 0.94
|
||||
# = 214 logs/hr model; xp 37,450/hr; gp 23,754/hr (111 gp/log). a18 break
|
||||
# 45-150s AT THE BANK every 8-14 cycles (mid ~11 = ~47 min). Session
|
||||
# target: 1M gp banked = ~42h of cycles + breaks.
|
||||
# Below 60 the same mode banks willow@30 (fallback) — yew never below 60.
|
||||
#
|
||||
# All three examples: preparation (a1-a9: arm-run -> check-axe ->
|
||||
# withdraw-or-GE-buy -> collect/verify -> equip-or-carry -> build-loadout)
|
||||
# -> first full-inventory boundary -> first scheduled break -> correct
|
||||
# resume from the pinned activity state; tree depletion -> move/hop (a17);
|
||||
# access denial / missing axe -> explicit recovery (a2-a7 bounded GE loop,
|
||||
# a16 spot_check, re-arm fallback — an unobtainable axe FAILS the run with
|
||||
# ERROR + alert, it never completes).
|
||||
# ======================================================================
|
||||
|
||||
risk:
|
||||
ban_level: low
|
||||
notes:
|
||||
- "Woodcutting is one of the most botted skills; keep chop cadence humanised and bank rhythm varied (anti_ban delays + per-mode chop_seconds_per_log)."
|
||||
- "Draynor willow spot is HOSTILE for accounts under combat 15: two aggressive level-7 Dark wizards attack players below that threshold — the ladder routes sub-15 accounts to the Edgeville willow via a separate deterministic row (combat-under-15). The no-combat claim applies ONLY to the gated routes; treat Draynor as a combat zone for low-level accounts."
|
||||
- "Shared tree spots (Draynor willows, Edgeville yews): crowded > 60s -> hop_world (a11); do not camp a single world for hours."
|
||||
- "Maple route (Corsair Cove Resource Area): quest-gated (Dragon Slayer I) and far from a bank — afk-xp only, drop mode; never select it without the quest gate (ladder + state machine enforce)."
|
||||
- "The axe always stays with the account: deposit_logs deposits logs ONLY; an axe is never deposited, alched, or traded away mid-run. Losing the axe is a stop-and-report event, not a silent downgrade."
|
||||
- "Mode discipline is the anti-detection edge: one pinned mode per run, deterministic ladder, explicit fallbacks — no mid-run mode flips."
|
||||
|
||||
sources:
|
||||
- "OSRS wiki — Free-to-play Woodcutting training (full tier ladder, 2026-08-27 re-verified 2026-08-28): maple = Corsair Cove Resource Area ONLY for F2P, requires Dragon Slayer I; yew 'earn a profit' but 'recommended to cut willow or maple trees for experience'; 'best place to chop yew trees is south of Edgeville bank'; willow spots 'south-west of the bank in Draynor Village, but beware of aggressive dark wizards' + 'one tree East of Edgeville that is very close to a bank'; oak spots incl. south-west of Varrock west bank; drop-vs-bank trade-off"
|
||||
- "OSRS wiki — Draynor Village (two aggressive level-7 Dark Wizards near the willows 'will attack any players under combat level 15')"
|
||||
- "OSRS wiki — Woodcutting (axe table: F2P axes Bronze 1, Iron 1, Steel 6, Black 11, Mithril 21, Adamant 31, Rune 41, Gilded 41; 'best axes available in Free-to-play'; Dragon 61+ members)"
|
||||
- "OSRS wiki axe pages raw wikitext, verified 2026-08-28: Rune axe 'Level 41 Woodcutting is required to use this axe' + 'Attack level of 40 to wield this axe'; Gilded axe 'Attack level of 40 to wield ... Woodcutting level of 41 to use'; Black axe 'requires level 11 Woodcutting. Level 10 Attack is required to equip'; Dragon axe 'requiring 60 Attack to wield' and '61 Woodcutting to use' (members) — the Woodcutting number is the USE requirement, Attack is the EQUIP requirement"
|
||||
- "OSRS wiki — Tree (members=No, level 1, 25 xp), Oak tree (15, 37.5 xp), Willow tree (30, 67.5 xp; falls 50 ticks/30s), Maple tree (45, 100 xp; members1=No Corsair Cove Resource Area — only F2P maples; falls 100 ticks/60s), Yew tree (60, 175 xp; falls 190 ticks/114s, respawn 99 ticks/59.4s) — infoboxes 2026-08-27/28"
|
||||
- "OSRS wiki — Corsair Cove Resource Area ('requires completion of Dragon Slayer I to enter'; 4 maple trees + 3 yew trees; only F2P maple location)"
|
||||
- "OSRS wiki — Pine tree (scenery in Shayzien/Varlamore, CANNOT be chopped — disproves any '45 Pine' tier)"
|
||||
- "prices.runescape.wiki mapping + latest 2026-08-28 (live buy/sell + 4h limits: logs 1511 22/23 limit 15000; oak 1521 39/40 limit 15000; willow 1519 20/21 limit 15000; maple 1517 11/12 limit 15000; yew 1515 107/111 limit 12000; rune axe 1359 7140/7293 limit 40; black 1001/1009; adamant 573/600; mithril 350/350)"
|
||||
- "Governor review comments on PR #12: 10178 (bounce 1 — maple F2P ladder, deterministic tier selection), 10216 (bounce 2 — DS I scoping to the maple tier), 10248 (bounce 3 — axe USE-vs-EQUIP conflation, nondeterministic tier 'or'), 10263 (governor guidance request — mode options A/B/C), 10398 (bounce 4 — executable axe preparation chain, missing-tools -> failed, drop transition gated to afk-xp, per-selected-row dynamic 27/28-slot rates with fixtures, drop-mode realized GP zero)"
|
||||
- "Owner decision comment 10288 (2026-08-28): three modes fast-xp / afk-xp / banked-income; deterministic lower fallbacks + pinned activity state; level-50 and level-70 worked examples; full per-mode lifecycle (prereqs, axe acquisition, initial inventory, travel, depletion/respawn, one chop attempt, inventory-full, complete dropping or banking route, inventory reconstruction, breaks independent of incidental banking, recovery from unavailable trees / invalid access / missing tools / path failure, completion); realized rates incl. travel/respawn/dropping-or-banking/breaks; axe model 41 WC to use / 40 Attack to equip with the 1-Attack/41-WC rune-axe carry coverage; repair the SAME issue-3 branch, no replacement PR"
|
||||
- "Owner decision comment 10499 (2026-08-29): authorize one more repair iteration implementing governor 10398 exactly — executable prepare path (check -> withdraw or GE open/buy/bounded-wait/cancel-refund/collect -> verify -> equip-or-carry -> build inventory -> travel), unobtainable axe -> failed with error/alert (never complete), inventory-full drop transition exclusive to the drop mode, per-selected-profile 27/28-capacity rates with level-1 / level-50 quest+no-quest / level-70 fixtures, realized GP zero for drop modes with log value as reference-only; repair this PR, no replacement"
|
||||
verified: "2026-08-28" # keep quoted (YAML 1.1 date parsing); prices + axe pages re-verified live 2026-08-28
|
||||
|
||||
related:
|
||||
- low-alch-f2p
|
||||
- f2p-alch-catalog
|
||||
- flip-candidates
|
||||
@ -344,6 +344,123 @@
|
||||
}
|
||||
},
|
||||
|
||||
"axe_tiers": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["axe", "woodcutting_level", "attack_level", "members"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"axe": { "type": "string" },
|
||||
"woodcutting_level": { "type": "integer", "minimum": 1, "maximum": 99 },
|
||||
"attack_level": { "type": "integer", "minimum": 1, "maximum": 99 },
|
||||
"members": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"axe_examples": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["account", "woodcutting_level", "attack_level", "expected_axe", "expected_equipped"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": { "type": "string" },
|
||||
"woodcutting_level": { "type": "integer", "minimum": 1, "maximum": 99 },
|
||||
"attack_level": { "type": "integer", "minimum": 1, "maximum": 99 },
|
||||
"expected_axe": { "type": "string" },
|
||||
"expected_equipped": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"woodcutting_modes": {
|
||||
"type": "array",
|
||||
"minItems": 3,
|
||||
"maxItems": 3,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["mode", "intent", "inventory_policy", "ladder", "fallback", "model", "rates"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"mode": { "enum": ["fast-xp", "afk-xp", "banked-income"] },
|
||||
"intent": { "type": "string", "minLength": 10 },
|
||||
"inventory_policy": { "enum": ["bank", "drop"] },
|
||||
"fallback": { "type": "string", "minLength": 3 },
|
||||
"ladder": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["tree", "min_level", "gate", "spot", "bank",
|
||||
"chop_seconds_per_log", "walk_seconds_each_way",
|
||||
"respawn_duty", "log_price"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tree": { "enum": ["tree", "oak", "willow", "maple", "yew"] },
|
||||
"min_level": { "type": "integer", "minimum": 1, "maximum": 99 },
|
||||
"gate": { "enum": ["none", "combat-15-plus", "combat-under-15", "dragon-slayer-i"] },
|
||||
"spot": { "type": "string", "minLength": 3 },
|
||||
"bank": { "type": "string", "minLength": 3 },
|
||||
"chop_seconds_per_log": { "type": "number", "exclusiveMinimum": 0 },
|
||||
"walk_seconds_each_way": { "type": "number", "minimum": 0 },
|
||||
"respawn_duty": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 },
|
||||
"log_price": { "type": "number", "exclusiveMinimum": 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"type": "object",
|
||||
"required": ["drop_seconds_per_log", "deposit_seconds", "break_duty"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"drop_seconds_per_log": { "type": "number", "minimum": 0 },
|
||||
"deposit_seconds": { "type": "number", "minimum": 0 },
|
||||
"break_duty": { "type": "number", "exclusiveMinimum": 0, "maximum": 1 }
|
||||
}
|
||||
},
|
||||
"rates": {
|
||||
"type": "object",
|
||||
"required": ["logs_per_hour", "xp_per_hour", "gp_per_hour", "log_price", "price_verified"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"logs_per_hour": { "type": "integer", "exclusiveMinimum": 0 },
|
||||
"xp_per_hour": { "type": "integer", "minimum": 0 },
|
||||
"gp_per_hour": { "type": "integer", "minimum": 0 },
|
||||
"log_price": { "type": "number", "exclusiveMinimum": 0 },
|
||||
"price_verified": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"woodcutting_fixtures": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["account", "mode", "woodcutting_level", "attack_level",
|
||||
"quest_gate", "combat_level", "expected_tree", "expected_axe",
|
||||
"expected_equipped", "expected_logs_per_hour",
|
||||
"expected_xp_per_hour", "expected_gp_per_hour"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"account": { "type": "string", "minLength": 3 },
|
||||
"mode": { "enum": ["fast-xp", "afk-xp", "banked-income"] },
|
||||
"woodcutting_level": { "type": "integer", "minimum": 1, "maximum": 99 },
|
||||
"attack_level": { "type": "integer", "minimum": 1, "maximum": 99 },
|
||||
"quest_gate": { "type": "boolean" },
|
||||
"combat_level": { "type": "integer", "minimum": 3 },
|
||||
"expected_tree": { "enum": ["tree", "oak", "willow", "maple", "yew"] },
|
||||
"expected_axe": { "type": "string" },
|
||||
"expected_equipped": { "type": "boolean" },
|
||||
"expected_logs_per_hour": { "type": "integer", "exclusiveMinimum": 0 },
|
||||
"expected_xp_per_hour": { "type": "integer", "minimum": 0 },
|
||||
"expected_gp_per_hour": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"state_machine": {
|
||||
"type": "object",
|
||||
"required": ["initial", "terminal", "states"],
|
||||
|
||||
@ -97,11 +97,24 @@ Runs:
|
||||
mutation of the archived record;
|
||||
economy.ceiling_gp_per_hour is the TYPED economic ceiling and
|
||||
0 <= gp_per_hour_low <= gp_per_hour_high <= ceiling.
|
||||
- Woodcutting entries (woodcutting tag, owner 10288 + governor
|
||||
10398): canonical F2P axe table + equip-vs-carry rule (41 WC to
|
||||
USE / 40 Attack to EQUIP, a 1-Attack/41-WC account carries and
|
||||
uses a rune axe), exactly three deterministic modes with
|
||||
per-SELECTED-row rate derivation (27/28-slot capacity from
|
||||
axe_equipped, committed woodcutting_fixtures recomputed by this
|
||||
validator), drop-mode realized GP zero, and state-machine wiring
|
||||
(executable prepare -> walk_to_trees, no axe -> failed, drop
|
||||
transition gated to the afk-xp drop mode).
|
||||
- Regression fixtures (--selftest): a fractional-tax flipping
|
||||
entry must FAIL; an above-cap high-value flip with the
|
||||
correctly capped-tax net must PASS; a non-margin entry carrying
|
||||
only `ge` plus a non-flip tag must PASS; plus per-slot scheduler
|
||||
dispatch, conservation, buy-limit and nesting fixtures.
|
||||
dispatch, conservation, buy-limit and nesting fixtures, the
|
||||
Mining pickaxe suite, the Woodcutting axe/mode/fixture/wiring
|
||||
suite, and the alchemy rate-model/tier/loadout suite — each
|
||||
suite runs exactly once per invocation (no aliases or
|
||||
duplicated executions).
|
||||
|
||||
3. Global id uniqueness across the whole codex tree.
|
||||
|
||||
@ -1803,6 +1816,552 @@ def ge_lifecycle_errors(rel, data):
|
||||
|
||||
|
||||
|
||||
# --- Woodcutting structured gates (owner 10288 + governor 10398 shape) ---
|
||||
# Canonical F2P axe table (OSRS wiki, verified 2026-08-28): the Woodcutting
|
||||
# level is the TOOL-USE requirement (chopping), the Attack level is the
|
||||
# EQUIP requirement. An account uses the highest axe whose Woodcutting level
|
||||
# it meets, and equips it only when its Attack level is also met — otherwise
|
||||
# it CARRIES that same axe (costing one inventory slot). Order is the tier
|
||||
# tiebreak (iron beats bronze at equal requirements; gilded ~ rune).
|
||||
AXE_TIERS = [
|
||||
# (axe, woodcutting_level_to_use, attack_level_to_equip, members_only)
|
||||
("bronze", 1, 1, False),
|
||||
("iron", 1, 1, False),
|
||||
("steel", 6, 5, False),
|
||||
("black", 11, 10, False),
|
||||
("mithril", 21, 20, False),
|
||||
("adamant", 31, 30, False),
|
||||
("rune", 41, 40, False),
|
||||
("gilded", 41, 40, False),
|
||||
("dragon", 61, 60, True),
|
||||
]
|
||||
AXE_NAMES = {row[0] for row in AXE_TIERS}
|
||||
TIER_RANK = {"bronze": 0, "iron": 1, "steel": 2, "black": 3, "mithril": 4,
|
||||
"adamant": 5, "rune": 6, "gilded": 6, "dragon": 7}
|
||||
XP_PER_LOG = {"tree": 25, "oak": 37.5, "willow": 67.5, "maple": 100, "yew": 175}
|
||||
TREE_MIN_LEVEL = {"tree": 1, "oak": 15, "willow": 30, "maple": 45, "yew": 60}
|
||||
|
||||
|
||||
def best_axe(wc_level, attack_level):
|
||||
"""Deterministic axe selection (owner guidance 2026-08-28 / governor 10323):
|
||||
pick the highest tier usable at wc_level (tool-use requirement); equip it
|
||||
only when attack_level meets that tier's equip requirement, else carry it.
|
||||
Returns (axe, equipped_bool). Ties at the same Woodcutting requirement are
|
||||
broken by TIER_RANK (iron beats bronze; rune beats the cosmetic gilded)."""
|
||||
usable = [row for row in AXE_TIERS if row[3] is False and wc_level >= row[1]]
|
||||
if not usable:
|
||||
return None, False
|
||||
best = max(usable, key=lambda row: (row[1], TIER_RANK[row[0]]))
|
||||
return best[0], attack_level >= best[2]
|
||||
|
||||
|
||||
def select_ladder_row(ladder, wc_level, quest_gate, combat_level):
|
||||
"""Deterministic first match over a mode ladder (rows ordered min_level
|
||||
DESC per the schema contract): the first row whose min_level the account
|
||||
meets AND whose gate is satisfied.
|
||||
|
||||
Gates: none -> always; combat-15-plus -> combat_level >= 15;
|
||||
combat-under-15 -> combat_level < 15 (below the Draynor dark-wizard
|
||||
threshold); dragon-slayer-i -> quest_gate (Corsair Cove Resource Area).
|
||||
Returns the matched row dict or None."""
|
||||
for r in ladder:
|
||||
if wc_level < int(r.get("min_level", 0)):
|
||||
continue
|
||||
gate = r.get("gate", "none")
|
||||
if gate == "combat-15-plus" and not (combat_level >= 15):
|
||||
continue
|
||||
if gate == "combat-under-15" and not (combat_level < 15):
|
||||
continue
|
||||
if gate == "dragon-slayer-i" and not quest_gate:
|
||||
continue
|
||||
return r
|
||||
return None
|
||||
|
||||
|
||||
def row_rates(row, model, policy, capacity):
|
||||
"""Per-SELECTED-ROW realized rate derivation (governor 10398 finding 4):
|
||||
the account's ladder row carries its own chop cadence / bank-walk /
|
||||
respawn duty / log price, so rates derive from the row actually chosen
|
||||
(and the actual 27/28 capacity from axe_equipped) — never from a fixed
|
||||
top tree or a hardcoded 28-log cycle.
|
||||
|
||||
Returns (logs_per_hour, xp_per_hour, gp_per_hour) with gp_per_hour = 0
|
||||
for drop modes (realized GP; log value is reference-only)."""
|
||||
chop = float(row.get("chop_seconds_per_log", 0))
|
||||
walk = float(row.get("walk_seconds_each_way", 0))
|
||||
respawn = float(row.get("respawn_duty", 0))
|
||||
price = float(row.get("log_price", 0))
|
||||
drop = float(model.get("drop_seconds_per_log") or 0)
|
||||
deposit = float(model.get("deposit_seconds") or 0)
|
||||
brk = float(model.get("break_duty") or 0)
|
||||
tree = row.get("tree")
|
||||
if tree not in XP_PER_LOG or chop <= 0 or respawn <= 0 or brk <= 0:
|
||||
return None
|
||||
if policy == "drop":
|
||||
cycle = capacity * (chop + drop) + deposit # no bank legs
|
||||
else:
|
||||
cycle = capacity * chop + 2 * walk + deposit
|
||||
if cycle <= 0:
|
||||
return None
|
||||
logs = (capacity / cycle) * 3600.0 * respawn * brk
|
||||
xp = logs * XP_PER_LOG[tree]
|
||||
gp = 0.0 if policy == "drop" else logs * price
|
||||
return logs, xp, gp
|
||||
|
||||
|
||||
def woodcutting_errors(rel, data):
|
||||
"""Structured gates for woodcutting entries (owner guidance 10288 +
|
||||
governor 10398; no phrase-search, exact computed checks):
|
||||
|
||||
- axe_tiers must match the canonical table exactly;
|
||||
- axe_examples are recomputed and must match (incl. the 1-Attack /
|
||||
41-Woodcutting rune-axe carry case);
|
||||
- woodcutting_modes must be exactly [fast-xp, afk-xp, banked-income],
|
||||
each ladder deterministic (rows ordered by min_level desc, first
|
||||
match), with canonical per-tree level/gate constraints;
|
||||
- mode `rates` is the canonical TOP-ROW reference (ladder[0], axe
|
||||
equipped = 28 slots) and must equal that row's model chain within
|
||||
tolerance, with xp/gp derived from the row's tree xp and log price;
|
||||
- every LADDER ROW carries its own chop/walk/respawn/log_price and the
|
||||
committed per-account woodcutting_fixtures rows must match the
|
||||
selected-row derivation (27/28 capacity from axe_equipped) within
|
||||
tolerance — governor 10398 finding 4;
|
||||
- drop-policy modes realize ZERO gp (log value is a reference field,
|
||||
never realized income) — governor 10398 finding 5;
|
||||
- the state machine wires axe preparation BEFORE travel (a `prepare`
|
||||
state with a bounded GE-buy chain leading to walk_to_trees), routes
|
||||
an unobtainable axe to `failed` (never `complete`), and gates the
|
||||
inventory-full drop transition to the drop policy mode only —
|
||||
governor 10398 findings 1-3.
|
||||
"""
|
||||
errors = []
|
||||
tags = {str(t).lower() for t in data.get("tags", [])}
|
||||
if "woodcutting" not in tags:
|
||||
return errors
|
||||
|
||||
# --- canonical axe table ---
|
||||
tiers = data.get("axe_tiers") or []
|
||||
canon = {(row[0], row[1], row[2], bool(row[3])) for row in AXE_TIERS}
|
||||
declared = {(str(t.get("axe")), t.get("woodcutting_level"), t.get("attack_level"),
|
||||
bool(t.get("members"))) for t in tiers}
|
||||
if declared != canon:
|
||||
errors.append(
|
||||
f"{rel}: axe_tiers must match the canonical F2P table exactly "
|
||||
f"(bronze 1/1, iron 1/1, steel 6/5, black 11/10, mithril 21/20, "
|
||||
f"adamant 31/30, rune 41/40, gilded 41/40, dragon 61/60 members); "
|
||||
f"got {sorted(declared)}")
|
||||
return errors
|
||||
|
||||
# --- recompute the committed axe examples ---
|
||||
for ex in data.get("axe_examples") or []:
|
||||
name = ex.get("account", "?")
|
||||
wc, atk = ex.get("woodcutting_level"), ex.get("attack_level")
|
||||
axe, eq = best_axe(wc, atk)
|
||||
if axe != ex.get("expected_axe") or eq != ex.get("expected_equipped"):
|
||||
errors.append(
|
||||
f"{rel}: axe_examples '{name}' (WC {wc}, Attack {atk}) — "
|
||||
f"recomputed {axe}/equipped={eq}, entry declares "
|
||||
f"{ex.get('expected_axe')}/equipped={ex.get('expected_equipped')}")
|
||||
|
||||
# --- modes ---
|
||||
modes = data.get("woodcutting_modes") or []
|
||||
if sorted(str(m.get("mode")) for m in modes) != ["afk-xp", "banked-income", "fast-xp"]:
|
||||
errors.append(f"{rel}: woodcutting_modes must declare exactly "
|
||||
f"fast-xp, afk-xp and banked-income")
|
||||
return errors
|
||||
for m in modes:
|
||||
mode = m["mode"]
|
||||
ladder = m.get("ladder") or []
|
||||
if not ladder:
|
||||
errors.append(f"{rel}: {mode} ladder is empty")
|
||||
continue
|
||||
levels = [int(r["min_level"]) for r in ladder]
|
||||
if levels != sorted(levels, reverse=True):
|
||||
errors.append(f"{rel}: {mode} ladder must be ordered by "
|
||||
f"min_level DESC (deterministic first-match); got {levels}")
|
||||
if any(re.search(r"\bor\b", str(r.get("spot", "")).lower())
|
||||
or re.search(r"\bor\b", str(r.get("bank", "")).lower())
|
||||
for r in ladder):
|
||||
errors.append(f"{rel}: {mode} ladder rows must be unambiguous "
|
||||
f"(no 'or' inside spot/bank fields)")
|
||||
for r in ladder:
|
||||
tree, min_level, gate = r["tree"], int(r["min_level"]), r.get("gate")
|
||||
if min_level != TREE_MIN_LEVEL.get(tree):
|
||||
errors.append(f"{rel}: {mode} {tree} must have min_level "
|
||||
f"{TREE_MIN_LEVEL.get(tree)} (got {min_level})")
|
||||
if tree == "maple" and gate != "dragon-slayer-i":
|
||||
errors.append(f"{rel}: {mode} maple requires gate "
|
||||
f"dragon-slayer-i (Corsair Cove Resource Area)")
|
||||
if tree != "maple" and gate == "dragon-slayer-i":
|
||||
errors.append(f"{rel}: {mode} gate dragon-slayer-i may only "
|
||||
f"apply to the maple row (got {tree})")
|
||||
# governor 10398 finding 4: every row must carry its own rate
|
||||
# constants — rates derive from the SELECTED row, never from a
|
||||
# fixed top-tree model.
|
||||
for field in ("chop_seconds_per_log", "walk_seconds_each_way",
|
||||
"respawn_duty", "log_price"):
|
||||
if not isinstance(r.get(field), (int, float)):
|
||||
errors.append(f"{rel}: {mode} {tree} row must carry "
|
||||
f"numeric {field} (per-selected-row rates, "
|
||||
f"governor 10398 finding 4)")
|
||||
if mode == "fast-xp" and not any(r["tree"] == "willow" and int(r["min_level"]) == 30
|
||||
for r in ladder):
|
||||
errors.append(f"{rel}: fast-xp must include a willow row at "
|
||||
f"min_level 30 (willow is the intended fast-XP target)")
|
||||
if mode == "banked-income" and not any(r["tree"] == "yew" for r in ladder):
|
||||
errors.append(f"{rel}: banked-income must include a yew row "
|
||||
f"(yew from 60 when eligible)")
|
||||
if mode == "afk-xp" and not any(r["tree"] == "maple" for r in ladder):
|
||||
errors.append(f"{rel}: afk-xp must include a maple row (quest-gated "
|
||||
f"AFK target); include quest-gated maple only when "
|
||||
f"access is satisfied — with lower fallbacks below")
|
||||
if not any(int(r["min_level"]) == 1 for r in ladder):
|
||||
errors.append(f"{rel}: {mode} ladder must bottom out at a level-1 "
|
||||
f"fallback (tree) — deterministic lower-level fallback")
|
||||
|
||||
# --- canonical TOP-ROW `rates` reference (ladder[0], 28 slots equipped) ---
|
||||
model = m.get("model") or {}
|
||||
policy = m.get("inventory_policy")
|
||||
top = row_rates(ladder[0], model, policy, 28)
|
||||
if top is None:
|
||||
errors.append(f"{rel}: {mode} top-row rate chain does not compute "
|
||||
f"(row constants/model numeric, cycle positive)")
|
||||
continue
|
||||
t_logs, t_xp, t_gp = top
|
||||
rates = m.get("rates") or {}
|
||||
logs = rates.get("logs_per_hour")
|
||||
if not isinstance(logs, (int, float)) or abs(logs - t_logs) / t_logs > 0.01:
|
||||
errors.append(
|
||||
f"{rel}: {mode} logs_per_hour {logs} must equal the SELECTED-row "
|
||||
f"chain (top row {ladder[0]['tree']}, 28 slots equipped: "
|
||||
f"{t_logs:.1f}) within 1% — rates are per-selected-row, never "
|
||||
f"a fixed 28-log top-tree hardcode (governor 10398 finding 4)")
|
||||
if abs(rates.get("xp_per_hour", -1) - t_xp) / t_xp > 0.02:
|
||||
errors.append(f"{rel}: {mode} xp_per_hour {rates.get('xp_per_hour')} "
|
||||
f"must equal the row chain xp {t_xp:.0f} "
|
||||
f"(logs x {XP_PER_LOG[ladder[0]['tree']]}) within 2%")
|
||||
if policy == "drop":
|
||||
# governor 10398 finding 5: dropped logs realize ZERO gp; the log
|
||||
# sell price is reference/opportunity cost only.
|
||||
if rates.get("gp_per_hour", -1) != 0:
|
||||
errors.append(f"{rel}: {mode} inventory_policy drop must "
|
||||
f"realize gp_per_hour 0 (dropped logs produce no "
|
||||
f"income; log_price is reference only) — got "
|
||||
f"{rates.get('gp_per_hour')}")
|
||||
else:
|
||||
price = rates.get("log_price")
|
||||
if isinstance(logs, (int, float)) and isinstance(price, (int, float)):
|
||||
gp_expect = t_logs * price
|
||||
if abs(rates.get("gp_per_hour", -1) - gp_expect) / gp_expect > 0.03:
|
||||
errors.append(f"{rel}: {mode} gp_per_hour "
|
||||
f"{rates.get('gp_per_hour')} must equal "
|
||||
f"logs_per_hour x declared log_price "
|
||||
f"({gp_expect:.0f}) within 3%")
|
||||
|
||||
# --- per-account fixtures: selected-row + 27/28 capacity derivation ---
|
||||
for fx in data.get("woodcutting_fixtures") or []:
|
||||
name = fx.get("account", "?")
|
||||
mode = fx.get("mode")
|
||||
wc, atk = fx.get("woodcutting_level"), fx.get("attack_level")
|
||||
qgate = bool(fx.get("quest_gate"))
|
||||
combat = fx.get("combat_level", 0)
|
||||
modes_by_name = {str(m.get("mode")): m for m in modes}
|
||||
m = modes_by_name.get(str(mode))
|
||||
if m is None:
|
||||
errors.append(f"{rel}: fixture '{name}' references unknown "
|
||||
f"mode {mode!r}")
|
||||
continue
|
||||
row = select_ladder_row(m.get("ladder") or [], wc, qgate, combat)
|
||||
if row is None:
|
||||
errors.append(f"{rel}: fixture '{name}' (WC {wc}, quest "
|
||||
f"{qgate}, combat {combat}) matches no ladder row")
|
||||
continue
|
||||
if row["tree"] != fx.get("expected_tree"):
|
||||
errors.append(f"{rel}: fixture '{name}' selected row "
|
||||
f"{row['tree']}@{row['min_level']} but declares "
|
||||
f"expected_tree {fx.get('expected_tree')}")
|
||||
axe, eq = best_axe(wc, atk)
|
||||
if axe != fx.get("expected_axe") or eq != fx.get("expected_equipped"):
|
||||
errors.append(f"{rel}: fixture '{name}' — recomputed axe "
|
||||
f"{axe}/equipped={eq}, declares "
|
||||
f"{fx.get('expected_axe')}/equipped="
|
||||
f"{fx.get('expected_equipped')}")
|
||||
capacity = 28 if eq else 27 # governor 10398 finding 4
|
||||
rr = row_rates(row, m.get("model") or {}, m.get("inventory_policy"), capacity)
|
||||
if rr is None:
|
||||
errors.append(f"{rel}: fixture '{name}' rate chain does not "
|
||||
f"compute (capacity {capacity})")
|
||||
continue
|
||||
f_logs, f_xp, f_gp = rr
|
||||
# fixture values are pinned to the recomputed chain within the same
|
||||
# tolerances as the mode rates (logs 1% / xp 2% / gp 3%); drop-mode
|
||||
# fixtures must realize zero gp.
|
||||
expect_logs = fx.get("expected_logs_per_hour")
|
||||
if not isinstance(expect_logs, (int, float)) or abs(expect_logs - f_logs) / f_logs > 0.01:
|
||||
errors.append(f"{rel}: fixture '{name}' expected_logs_per_hour "
|
||||
f"{expect_logs} must equal the selected-row chain "
|
||||
f"({row['tree']}@{row['min_level']}, capacity "
|
||||
f"{capacity}, respawn {row.get('respawn_duty')}, "
|
||||
f"equipped={eq}) = {f_logs:.1f} within 1%")
|
||||
expect_xp = fx.get("expected_xp_per_hour")
|
||||
if not isinstance(expect_xp, (int, float)) or abs(expect_xp - f_xp) / f_xp > 0.02:
|
||||
errors.append(f"{rel}: fixture '{name}' expected_xp_per_hour "
|
||||
f"{expect_xp} != selected-row xp {f_xp:.0f} within 2%")
|
||||
if m.get("inventory_policy") == "drop":
|
||||
if fx.get("expected_gp_per_hour", -1) != 0:
|
||||
errors.append(f"{rel}: fixture '{name}' is drop mode — "
|
||||
f"realized expected_gp_per_hour must be 0")
|
||||
else:
|
||||
expect_gp = fx.get("expected_gp_per_hour")
|
||||
if expect_gp is None or abs(expect_gp - f_gp) / f_gp > 0.03:
|
||||
errors.append(f"{rel}: fixture '{name}' expected_gp_per_hour "
|
||||
f"{expect_gp} != selected-row gp {f_gp:.0f} "
|
||||
f"within 3%")
|
||||
|
||||
# --- state-machine wiring (governor 10398 findings 1-3) ---
|
||||
sm = data.get("state_machine") or {}
|
||||
states = set(sm.get("states", {}))
|
||||
if "prepare" not in states:
|
||||
errors.append(f"{rel}: state_machine must declare a `prepare` state "
|
||||
f"(executable axe acquisition before walking to trees — "
|
||||
f"governor 10398 finding 1)")
|
||||
else:
|
||||
tr = sm.get("transitions") or []
|
||||
from_to = {(t.get("from"), t.get("to")) for t in tr}
|
||||
prep_f = sum(1 for t in tr if t.get("from") == "prepare" and t.get("to") == "walk_to_trees")
|
||||
if not prep_f:
|
||||
errors.append(f"{rel}: `prepare` must transition to walk_to_trees "
|
||||
f"when the axe is acquired (governor 10398 finding 1)")
|
||||
if ("prepare", "failed") not in from_to:
|
||||
errors.append(f"{rel}: `prepare` must transition to `failed` when "
|
||||
f"no axe is obtainable after bounded recovery — a "
|
||||
f"missing tool is a FAILED run, never `complete` "
|
||||
f"(governor 10398 finding 2)")
|
||||
# idle -> complete must not be the no-axe escape hatch
|
||||
for t in tr:
|
||||
if t.get("from") == "idle" and t.get("to") == "complete":
|
||||
if re.search(r"no axe|no-axe|obtainable", str(t.get("when", "")), re.I):
|
||||
errors.append(f"{rel}: idle -> complete must fire only on "
|
||||
f"the session target, never on a missing axe "
|
||||
f"(governor 10398 finding 2)")
|
||||
# inventory-full drop transition gated to the drop policy mode only
|
||||
drop_trans = [t for t in (sm.get("transitions") or [])
|
||||
if t.get("from") == "chopping" and t.get("to") == "drop_logs"]
|
||||
bank_trans = [t for t in (sm.get("transitions") or [])
|
||||
if t.get("from") == "chopping" and t.get("to") == "walk_to_bank"]
|
||||
if drop_trans and "afk-xp" not in str(drop_trans[0].get("when", "")):
|
||||
errors.append(f"{rel}: chopping -> drop_logs must be gated explicitly "
|
||||
f"to the afk-xp drop mode (inventory_policy drop) — "
|
||||
f"governor 10398 finding 3")
|
||||
if not drop_trans and bank_trans:
|
||||
# drop mode exists somewhere in the modes; if the entry declares a
|
||||
# drop policy it must also declare the drop transition
|
||||
if any(str(m.get("inventory_policy")) == "drop" for m in modes):
|
||||
errors.append(f"{rel}: a drop-policy mode exists but chopping has "
|
||||
f"no drop_logs transition (finding 3)")
|
||||
return errors
|
||||
|
||||
|
||||
def run_woodcutting_selftest(verbose=False):
|
||||
"""Regression fixtures for the woodcutting structured gates
|
||||
(canonical axe table, deterministic per-mode ladders,
|
||||
per-selected-row 27/28 dynamic rates, drop-mode realized GP zero,
|
||||
prepare/failed wiring, drop-transition gating); returns error
|
||||
strings."""
|
||||
errors = []
|
||||
|
||||
def check(name, passed):
|
||||
if verbose:
|
||||
print(f"SELFTEST {'PASS' if passed else 'FAIL'} {name}")
|
||||
if not passed:
|
||||
errors.append(f"selftest: {name}")
|
||||
|
||||
# --- woodcutting structured gates (owner 10288 / governor 10398) ---
|
||||
check("axe: 41-Woodcutting/1-Attack account carries and USES a rune axe (does not pick a lower axe)",
|
||||
best_axe(41, 1) == ("rune", False))
|
||||
check("axe: 41-Woodcutting/40-Attack account equips the rune axe",
|
||||
best_axe(41, 40) == ("rune", True))
|
||||
check("axe: 30-Woodcutting account is below adamant (31) and uses mithril (equippable at 20 Attack)",
|
||||
best_axe(30, 40) == ("mithril", True))
|
||||
check("axe: 31-Woodcutting/5-Attack account uses adamant but cannot equip it (carries)",
|
||||
best_axe(31, 5) == ("adamant", False))
|
||||
check("axe: level-1 account uses an iron axe equipped from the start",
|
||||
best_axe(1, 1) == ("iron", True))
|
||||
check("axe: dragon (61/60, members) is never selected for an F2P woodcutting entry; maxed account still picks the top F2P axe (rune)",
|
||||
best_axe(99, 99) == ("rune", True))
|
||||
canon = {(row[0], row[1], row[2]) for row in AXE_TIERS} - {("dragon", 61, 60)}
|
||||
want = {("bronze", 1, 1), ("iron", 1, 1), ("steel", 6, 5), ("black", 11, 10),
|
||||
("mithril", 21, 20), ("adamant", 31, 30), ("rune", 41, 40), ("gilded", 41, 40)}
|
||||
check("axe: canonical F2P axe table matches the wiki (WC to use / Attack to equip)",
|
||||
canon == want)
|
||||
|
||||
def wc_fake_entry(**over):
|
||||
base = {"tags": ["woodcutting"],
|
||||
"axe_tiers": [{"axe": a, "woodcutting_level": w, "attack_level": k, "members": m}
|
||||
for a, w, k, m in AXE_TIERS],
|
||||
"axe_examples": [{"account": "41WC/1Atk", "woodcutting_level": 41, "attack_level": 1,
|
||||
"expected_axe": "rune", "expected_equipped": False}],
|
||||
"state_machine": {"states": {"idle": {"goal": "start", "next": ["arm_run"]},
|
||||
"arm_run": {"goal": "pick", "next": ["prepare"]},
|
||||
"prepare": {"goal": "acquire axe", "next": ["walk_to_trees", "failed"]},
|
||||
"chopping": {"goal": "chop", "next": ["drop_logs", "walk_to_bank"]},
|
||||
"drop_logs": {"goal": "drop", "next": ["chopping"]},
|
||||
"walk_to_bank": {"goal": "bank", "next": ["chopping"]},
|
||||
"failed": {"goal": "fail"}},
|
||||
"initial": "idle", "terminal": ["failed"],
|
||||
"transitions": [
|
||||
{"from": "idle", "when": "session target reached", "to": "complete"},
|
||||
{"from": "arm_run", "when": "row pinned", "to": "prepare"},
|
||||
{"from": "prepare", "when": "axe acquired", "to": "walk_to_trees"},
|
||||
{"from": "prepare", "when": "axe unobtainable", "to": "failed"},
|
||||
{"from": "chopping", "when": "mode is afk-xp (inventory_policy drop) and inventory full", "to": "drop_logs"},
|
||||
{"from": "chopping", "when": "mode is fast-xp or banked-income and inventory full", "to": "walk_to_bank"}]},
|
||||
"woodcutting_fixtures": [
|
||||
{"account": "f1", "mode": "fast-xp", "woodcutting_level": 50, "attack_level": 40,
|
||||
"quest_gate": False, "combat_level": 50, "expected_tree": "willow",
|
||||
"expected_axe": "rune", "expected_equipped": True,
|
||||
"expected_logs_per_hour": 622, "expected_xp_per_hour": 41985,
|
||||
"expected_gp_per_hour": 13062}],
|
||||
"woodcutting_modes": [
|
||||
{"mode": "fast-xp", "intent": "max xp per hour on the F2P ladder",
|
||||
"inventory_policy": "bank",
|
||||
"ladder": [
|
||||
{"tree": "willow", "min_level": 30, "gate": "combat-15-plus",
|
||||
"spot": "draynor willows", "bank": "draynor",
|
||||
"chop_seconds_per_log": 4.0, "walk_seconds_each_way": 12,
|
||||
"respawn_duty": 0.92, "log_price": 21},
|
||||
{"tree": "willow", "min_level": 30, "gate": "combat-under-15",
|
||||
"spot": "edgeville willow", "bank": "edgeville",
|
||||
"chop_seconds_per_log": 4.0, "walk_seconds_each_way": 6,
|
||||
"respawn_duty": 0.90, "log_price": 21},
|
||||
{"tree": "oak", "min_level": 15, "gate": "none",
|
||||
"spot": "draynor oaks", "bank": "draynor",
|
||||
"chop_seconds_per_log": 3.5, "walk_seconds_each_way": 8,
|
||||
"respawn_duty": 0.93, "log_price": 40},
|
||||
{"tree": "tree", "min_level": 1, "gate": "none",
|
||||
"spot": "lumbridge west", "bank": "lumbridge",
|
||||
"chop_seconds_per_log": 3.0, "walk_seconds_each_way": 10,
|
||||
"respawn_duty": 0.95, "log_price": 23}],
|
||||
"model": {"drop_seconds_per_log": 0, "deposit_seconds": 4,
|
||||
"break_duty": 0.94},
|
||||
"rates": {"logs_per_hour": 622, "xp_per_hour": 41985,
|
||||
"gp_per_hour": 13062, "log_price": 21,
|
||||
"price_verified": "2026-08-28"}},
|
||||
{"mode": "afk-xp", "intent": "low-interaction quest-gated progress",
|
||||
"inventory_policy": "drop",
|
||||
"ladder": [
|
||||
{"tree": "maple", "min_level": 45, "gate": "dragon-slayer-i",
|
||||
"spot": "corsair cove resource area", "bank": "none-drop",
|
||||
"chop_seconds_per_log": 6.0, "walk_seconds_each_way": 0,
|
||||
"respawn_duty": 0.80, "log_price": 12},
|
||||
{"tree": "willow", "min_level": 30, "gate": "combat-15-plus",
|
||||
"spot": "draynor willows", "bank": "none-drop",
|
||||
"chop_seconds_per_log": 6.0, "walk_seconds_each_way": 0,
|
||||
"respawn_duty": 0.92, "log_price": 21},
|
||||
{"tree": "oak", "min_level": 15, "gate": "none",
|
||||
"spot": "draynor oaks", "bank": "none-drop",
|
||||
"chop_seconds_per_log": 5.5, "walk_seconds_each_way": 0,
|
||||
"respawn_duty": 0.93, "log_price": 40},
|
||||
{"tree": "tree", "min_level": 1, "gate": "none",
|
||||
"spot": "lumbridge west", "bank": "none-drop",
|
||||
"chop_seconds_per_log": 5.0, "walk_seconds_each_way": 0,
|
||||
"respawn_duty": 0.95, "log_price": 23}],
|
||||
"model": {"drop_seconds_per_log": 1.0, "deposit_seconds": 0,
|
||||
"break_duty": 0.94},
|
||||
"rates": {"logs_per_hour": 386, "xp_per_hour": 38600,
|
||||
"gp_per_hour": 0, "log_price": 12,
|
||||
"price_verified": "2026-08-28"}},
|
||||
{"mode": "banked-income", "intent": "banked logs sold at the GE",
|
||||
"inventory_policy": "bank",
|
||||
"ladder": [
|
||||
{"tree": "yew", "min_level": 60, "gate": "none",
|
||||
"spot": "edgeville south yews", "bank": "edgeville",
|
||||
"chop_seconds_per_log": 7.5, "walk_seconds_each_way": 8,
|
||||
"respawn_duty": 0.52, "log_price": 111},
|
||||
{"tree": "willow", "min_level": 30, "gate": "combat-15-plus",
|
||||
"spot": "draynor willows", "bank": "draynor",
|
||||
"chop_seconds_per_log": 4.0, "walk_seconds_each_way": 12,
|
||||
"respawn_duty": 0.92, "log_price": 21},
|
||||
{"tree": "oak", "min_level": 15, "gate": "none",
|
||||
"spot": "draynor oaks", "bank": "draynor",
|
||||
"chop_seconds_per_log": 3.5, "walk_seconds_each_way": 8,
|
||||
"respawn_duty": 0.93, "log_price": 40},
|
||||
{"tree": "tree", "min_level": 1, "gate": "none",
|
||||
"spot": "lumbridge west", "bank": "lumbridge",
|
||||
"chop_seconds_per_log": 3.0, "walk_seconds_each_way": 10,
|
||||
"respawn_duty": 0.95, "log_price": 23}],
|
||||
"model": {"drop_seconds_per_log": 0, "deposit_seconds": 4,
|
||||
"break_duty": 0.94},
|
||||
"rates": {"logs_per_hour": 214, "xp_per_hour": 37450,
|
||||
"gp_per_hour": 23754, "log_price": 111,
|
||||
"price_verified": "2026-08-28"}}]}
|
||||
base.update(over)
|
||||
return base
|
||||
|
||||
check("modes: a compliant 3-mode entry (per-row rates, drop-GP-zero, prepare wiring) passes every structured gate",
|
||||
woodcutting_errors("selftest/wc-good", wc_fake_entry()) == [])
|
||||
bad = wc_fake_entry(woodcutting_modes=wc_fake_entry()["woodcutting_modes"][1:])
|
||||
check("modes: missing a mode (only 2 declared) is rejected",
|
||||
any("exactly" in e for e in woodcutting_errors("selftest/wc-missing", bad)))
|
||||
bad = wc_fake_entry(axe_examples=[{"account": "41WC/1Atk", "woodcutting_level": 41, "attack_level": 1,
|
||||
"expected_axe": "adamant", "expected_equipped": True}])
|
||||
check("modes: a 41-WC/1-Attack example declaring a lower/equipped axe is rejected",
|
||||
any("recomputed rune" in e for e in woodcutting_errors("selftest/wc-badex", bad)))
|
||||
bad = wc_fake_entry(woodcutting_modes=[dict(m, ladder=[dict(r, min_level=30 if r["tree"] == "maple" else r["min_level"]) for r in m["ladder"]])
|
||||
for m in wc_fake_entry()["woodcutting_modes"]])
|
||||
check("modes: maple below level 45 is rejected",
|
||||
any("maple must have min_level 45" in e for e in woodcutting_errors("selftest/wc-maple45", bad)))
|
||||
bad = wc_fake_entry(woodcutting_modes=[dict(m, ladder=[dict(r, gate="none") for r in m["ladder"]])
|
||||
for m in wc_fake_entry()["woodcutting_modes"]])
|
||||
check("modes: maple without the dragon-slayer-i gate is rejected",
|
||||
any("gate dragon-slayer-i" in e for e in woodcutting_errors("selftest/wc-maplegate", bad)))
|
||||
bad = wc_fake_entry(woodcutting_modes=[dict(m, ladder=[dict(r) for r in m["ladder"][::-1]])
|
||||
for m in wc_fake_entry()["woodcutting_modes"]])
|
||||
check("modes: a ladder not ordered by min_level DESC is rejected (nondeterministic)",
|
||||
any("ordered by min_level DESC" in e for e in woodcutting_errors("selftest/wc-order", bad)))
|
||||
badm = [dict(m, rates=dict(m["rates"], logs_per_hour=9999))
|
||||
for m in wc_fake_entry()["woodcutting_modes"]]
|
||||
check("modes: a rate far above the model chain (9999/hr) is rejected",
|
||||
any("SELECTED-row" in e for e in woodcutting_errors("selftest/wc-rate", wc_fake_entry(woodcutting_modes=badm))))
|
||||
bad = wc_fake_entry(axe_tiers=[{"axe": a, "woodcutting_level": w, "attack_level": k, "members": m}
|
||||
for a, w, k, m in AXE_TIERS if a != "rune"])
|
||||
check("modes: a non-canonical axe table (rune missing) is rejected",
|
||||
any("canonical F2P table" in e for e in woodcutting_errors("selftest/wc-axes", bad)))
|
||||
badm = wc_fake_entry()["woodcutting_modes"]
|
||||
badm[0] = dict(badm[0], rates=dict(badm[0]["rates"], xp_per_hour=40000))
|
||||
check("modes: xp_per_hour not equal to logs x per-log xp is rejected",
|
||||
any("xp_per_hour" in e for e in woodcutting_errors("selftest/wc-xp", wc_fake_entry(woodcutting_modes=badm))))
|
||||
# governor 10398 finding 4: a fixture whose expected logs/XP do not match
|
||||
# the selected-row chain with the 27/28 capacity is rejected.
|
||||
bad = wc_fake_entry(woodcutting_fixtures=[
|
||||
{"account": "carried-rune-27", "mode": "afk-xp", "woodcutting_level": 50, "attack_level": 1,
|
||||
"quest_gate": True, "combat_level": 50, "expected_tree": "maple",
|
||||
"expected_axe": "rune", "expected_equipped": False,
|
||||
"expected_logs_per_hour": 9999, "expected_xp_per_hour": 999999,
|
||||
"expected_gp_per_hour": 0}])
|
||||
check("modes: a fixture whose declared rates do not match the selected-row 27-slot chain is rejected (governor 10398 finding 4)",
|
||||
any("selected-row chain" in e for e in woodcutting_errors("selftest/wc-fxrate", bad)))
|
||||
# governor 10398 finding 5: a drop mode declaring non-zero realized GP.
|
||||
badm = wc_fake_entry()["woodcutting_modes"]
|
||||
badm[1] = dict(badm[1], rates=dict(badm[1]["rates"], gp_per_hour=4632))
|
||||
check("modes: drop mode with non-zero realized gp_per_hour is rejected (dropped logs realize 0 gp — governor 10398 finding 5)",
|
||||
any("realize gp_per_hour 0" in e for e in woodcutting_errors("selftest/wc-dropgp", wc_fake_entry(woodcutting_modes=badm))))
|
||||
# governor 10398 finding 2: idle -> complete must not be the no-axe hatch.
|
||||
bad = wc_fake_entry(state_machine=dict(
|
||||
wc_fake_entry()["state_machine"],
|
||||
transitions=[{"from": "idle", "when": "no axe obtainable", "to": "complete"}]))
|
||||
check("modes: idle -> complete on a missing axe is rejected (unobtainable axe must go to failed — governor 10398 finding 2)",
|
||||
any("never on a missing axe" in e for e in woodcutting_errors("selftest/wc-idlecomplete", bad)))
|
||||
# governor 10398 finding 3: the drop transition must be gated to afk-xp.
|
||||
bad = wc_fake_entry(state_machine=dict(
|
||||
wc_fake_entry()["state_machine"],
|
||||
transitions=[{"from": "chopping", "when": "inventory full", "to": "drop_logs"}]))
|
||||
check("modes: an ungated chopping -> drop_logs transition is rejected (drop route exclusive to the afk-xp drop mode — governor 10398 finding 3)",
|
||||
any("gated explicitly" in e for e in woodcutting_errors("selftest/wc-dropgate", bad)))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def run_selftest(verbose=False):
|
||||
"""Regression fixtures for the cross-field gates; error strings."""
|
||||
errors = []
|
||||
@ -2418,11 +2977,13 @@ def main():
|
||||
errors += run_selftest(verbose=True)
|
||||
errors += run_flip_selftest(verbose=True)
|
||||
errors += run_mining_selftest(verbose=True)
|
||||
errors += run_woodcutting_selftest(verbose=True)
|
||||
print(f"RESULT: selftest, {len(errors)} errors, 0 warnings")
|
||||
return 1 if errors else 0
|
||||
|
||||
# The fixes themselves are gated: all regression suites run on every
|
||||
# invocation (alchemy/citation, mining model, GE flip lifecycle),
|
||||
# invocation (alchemy/citation, mining model, GE flip lifecycle,
|
||||
# woodcutting),
|
||||
# not just --selftest. A nested validator spawned by the
|
||||
# _end_to_end_bad_mapping fixture must not re-run the suites (that
|
||||
# would recurse); the fixture's child scans the tree only.
|
||||
@ -2430,6 +2991,7 @@ def main():
|
||||
errors += run_selftest(verbose=False)
|
||||
errors += run_flip_selftest(verbose=False)
|
||||
errors += run_mining_selftest(verbose=False)
|
||||
errors += run_woodcutting_selftest(verbose=False)
|
||||
entries = load_entries()
|
||||
if not entries:
|
||||
print("ERROR: no codex entries found under codex/")
|
||||
@ -2524,6 +3086,11 @@ def main():
|
||||
# (merged from low-alch-members PR #8)
|
||||
errors += alch_worked_example_regression(rel, data, path.read_text())
|
||||
errors += alch_accounting_regression(rel, data, path.read_text())
|
||||
# Woodcutting structured gates (owner 10288 / governor 10398
|
||||
# shape: canonical axe table, deterministic per-mode ladders,
|
||||
# per-selected-row dynamic rates with 27/28 capacity, drop-mode
|
||||
# realized GP zero, prepare/failed wiring).
|
||||
errors += woodcutting_errors(rel, data)
|
||||
|
||||
# 3. staleness / research TODOs
|
||||
if data.get("status") == "verified":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user