Codex skeleton: schema contract, action grammar, data-sources rules, JSON schema, validator, golden example (low-alch-f2p, wiki+price verified), README
This commit is contained in:
parent
f419bde235
commit
986fc1a6a8
69
README.md
69
README.md
@ -1,3 +1,68 @@
|
||||
# osrs-codex
|
||||
# OSRS Codex
|
||||
|
||||
Client-agnostic source of truth for everything botable in OSRS: activities, ordering, actions, requirements. AI agents translate entries into client APIs.
|
||||
**One source of truth for everything that can be botted in OSRS.**
|
||||
|
||||
Every bot-able activity — alching, mining, slayer, bosses, trading — is
|
||||
described here **once**, in a strict client-agnostic YAML structure:
|
||||
requirements, execution order (state machine), the exact action sequence,
|
||||
economics, best-in-class setup, and anti-ban notes.
|
||||
|
||||
AI agents building scripts do **not** re-learn the game. They read a codex
|
||||
entry and *translate* it into the target client API
|
||||
(DreamBot / Microbot / Storm / RuneLite plugins / …). The game knowledge
|
||||
lives in this repo; the client glue lives in whatever script is being
|
||||
written.
|
||||
|
||||
```
|
||||
client-agnostic client-specific (NOT in this repo)
|
||||
+----------------+ +-------------------+
|
||||
| codex entry | translate | DreamBot Java |
|
||||
| (YAML, strict) | ---------> | Microbot JS |
|
||||
| actions: verbs | | RuneLite plugin |
|
||||
+----------------+ +-------------------+
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
codex/<domain>/<entry-id>.yaml # one entry per activity
|
||||
economy/ trading, alching, flipping
|
||||
training/ skill grinds (magic, mining, woodcutting, ...)
|
||||
combat/ bosses
|
||||
combat-slayers/
|
||||
quests/ quest entries (referenced by requires_quest)
|
||||
transport/ teleport/teleport-reach recipes (shared by many entries)
|
||||
meta/ account rotation, world hopping, break scheduling
|
||||
docs/schema.md # the entry contract (read this first)
|
||||
docs/action-grammar.md # the universal verb vocabulary
|
||||
docs/data-sources.md # where every fact must come from + how to verify
|
||||
schemas/entry.schema.json
|
||||
tools/validate.py # the gate: python3 tools/validate.py (must exit 0)
|
||||
```
|
||||
|
||||
## The rules (short version)
|
||||
|
||||
1. **One entry = one activity.** State machine + action list, both mandatory.
|
||||
2. **Every fact carries a source.** Wiki-cited or `researched <date>`
|
||||
(a TODO: follow-up issue must land the proper citation). Live prices
|
||||
are re-verified against `prices.runescape.wiki` on every bump.
|
||||
3. **Best-in-class setup is mandatory** (`best_practice`). Top-operator
|
||||
execution: staff of fire for alchemy, noted items, shortest routes.
|
||||
Translators adopt it by default; deviating requires `improvised: true`.
|
||||
4. **Anti-ban is first-class.** Every action declares delay bounds and
|
||||
movement style; entries carry `risk.ban_level` + notes.
|
||||
5. **The gate must pass** before a PR merges: `python3 tools/validate.py`.
|
||||
6. **Never mix in client knowledge.** No DreamBot/Microbot/Storm API names
|
||||
in entries. Translations live in script repos, referencing the entry id.
|
||||
|
||||
## Entry lifecycle
|
||||
|
||||
`draft` → `verified` (all facts sourced, prices re-checked, validator
|
||||
clean) → `stale` (facts > 30d old or prices moved materially). Stale
|
||||
entries are skipped by translators unless explicitly allowed.
|
||||
|
||||
## Contributing
|
||||
|
||||
This repo is **issue-driven**: all content is specified in Gitea issues
|
||||
(acceptance criteria + source URLs + expected output path), implemented in
|
||||
branches, and gated by `tools/validate.py` in CI. No direct pushes.
|
||||
|
||||
155
codex/economy/low-alch-f2p.yaml
Normal file
155
codex/economy/low-alch-f2p.yaml
Normal file
@ -0,0 +1,155 @@
|
||||
id: low-alch-f2p
|
||||
title: Low-Level Alchemy (F2P)
|
||||
domain: economy
|
||||
tags: [alching, magic-training, f2p, loss-training]
|
||||
status: verified
|
||||
version: 1
|
||||
updated: "2026-08-27" # ISO dates must stay quoted — YAML 1.1 parses bare dates to datetime.date
|
||||
|
||||
summary: >
|
||||
Magic training from level 21 by low-alching F2P items with the highest
|
||||
lowalch-value among affordable, high-limit items. At current prices it is
|
||||
a LOSS activity (money in exchange for Magic XP); run it only for XP.
|
||||
|
||||
accounts:
|
||||
membership: f2p
|
||||
min_level: 21 # Magic 21 to cast Low Level Alchemy (wiki-verified)
|
||||
requires_quest: []
|
||||
requires_other:
|
||||
- "Staff of Fire equipped (removes 3 fire runes from the cost)"
|
||||
- "A bank with GE access within reach"
|
||||
|
||||
best_practice:
|
||||
equipment:
|
||||
- item: staff of fire
|
||||
why: "unlimited fire runes — LA rune cost drops from 3 fire + 1 nature to 1 nature per cast (~15gp/cycle saved)"
|
||||
source: "OSRS wiki — Staff of Fire; Low Level Alchemy (Cost)"
|
||||
techniques:
|
||||
- name: cast on noted (stacked) items
|
||||
why: "one inventory slot, minimal mouse travel between spell icon and item"
|
||||
source: "OSRS wiki — Low Level Alchemy (Usage: casting alchemy spells on noted items)"
|
||||
- name: bank at a GE-adjacent bank
|
||||
why: "supply restock without a second travel leg"
|
||||
source: "researched 2026-08-27 (standard operator practice; wiki LA Cost section corroborates rune economy)"
|
||||
|
||||
economy:
|
||||
# Verified 2026-08-27 against prices.runescape.wiki (live 'latest')
|
||||
# + OSRS wiki Low_Level_Alchemy infobox.
|
||||
item: Gold necklace # example anchor; rank by cheapest candidate live
|
||||
item_id: 1654
|
||||
cost_per_cycle: 292 # necklace buy 148 + nature rune 144 (staff of fire)
|
||||
income_per_cycle: 180 # lowalch of gold necklace (40% of value 450)
|
||||
net_per_cycle: -112
|
||||
gp_per_hour_low: -250000 # negative: this burns GP
|
||||
gp_per_hour_high: -200000
|
||||
xp_per_hour: 55800 # 31 xp/cast (wiki) @ ~1800 casts/hr (3-tick, wiki)
|
||||
supplies:
|
||||
- item: gold necklace
|
||||
qty_per_cycle: 1
|
||||
source: ge
|
||||
ge_limit: 18000 # mapping 'limit', wiki-verified
|
||||
- item: nature rune
|
||||
qty_per_cycle: 1
|
||||
source: ge
|
||||
ge_limit: 12500
|
||||
profit_drivers:
|
||||
- "NONE at current prices — XP is the product, not GP"
|
||||
- "Pick the candidate with the least-negative net each run (live rank)"
|
||||
|
||||
state_machine:
|
||||
initial: idle
|
||||
terminal: [complete, failed]
|
||||
states:
|
||||
idle:
|
||||
goal: reach the bank + have supplies in inventory
|
||||
next: [casting, bank_supplies]
|
||||
casting:
|
||||
goal: cast Low Level Alchemy on the current candidate item
|
||||
next: [bank_supplies, idle, complete, failed]
|
||||
bank_supplies:
|
||||
goal: bank the produced coins, withdraw fresh candidate + runes
|
||||
next: [casting, idle]
|
||||
complete:
|
||||
goal: reached the run's XP/level target
|
||||
failed:
|
||||
goal: hard failure — stop, log ERROR sentinel, alert
|
||||
transitions:
|
||||
- from: casting
|
||||
when: "candidate item count < buffer OR nature rune count < buffer"
|
||||
to: bank_supplies
|
||||
- from: idle
|
||||
when: "magic level >= target (default 54, hand off to HLA at 55)"
|
||||
to: complete
|
||||
|
||||
actions:
|
||||
- id: a1
|
||||
step: reach-bank
|
||||
verb: move_to
|
||||
target: bank
|
||||
params:
|
||||
location: "nearest bank with GE"
|
||||
method: [teleport, walk]
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [400, 1600]
|
||||
|
||||
- id: a2
|
||||
step: buy-supplies
|
||||
verb: ge_buy
|
||||
target: grand-exchange
|
||||
params:
|
||||
item: "current candidate item"
|
||||
qty: "fill to buffer"
|
||||
max_price: "live low + 5%"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1200, 3000]
|
||||
|
||||
- id: a3
|
||||
step: cast-alch
|
||||
verb: use_item_on
|
||||
target: "candidate item in inventory"
|
||||
params:
|
||||
spell: "Low Level Alchemy"
|
||||
item: "cheapest live candidate in inventory"
|
||||
on: "itself"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [1600, 3400] # 3-tick cast = ~5.4s game; stay under it, humanised
|
||||
|
||||
- id: a4
|
||||
step: bank-loop
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
deposit: [all coins, all products]
|
||||
withdraw: "candidate item + nature runes up to buffer"
|
||||
on_fail: alert
|
||||
|
||||
run_control:
|
||||
iteration: "one cast"
|
||||
max_iterations: null
|
||||
break:
|
||||
every: [8, 14] # bank cycles (breaks land on bank visits)
|
||||
for: [45, 120] # seconds of idle AFK
|
||||
stop_conditions:
|
||||
- "magic level >= target"
|
||||
- "gp under floor"
|
||||
- "3 consecutive failed casts (client issue — do not loop)"
|
||||
|
||||
risk:
|
||||
ban_level: low
|
||||
notes:
|
||||
- "High cast cadence is the main detection vector; keep anti_ban delays."
|
||||
- "GE buy bursts: pace offers, respect the 4h limit window."
|
||||
|
||||
sources:
|
||||
- "OSRS wiki — Low Level Alchemy (level 21, 31 xp/cast, 3-tick, F2P; infobox 2026-08-27)"
|
||||
- "prices.runescape.wiki mapping (id 1654 gold necklace: members=false, limit 18000, lowalch 180, highalch 270, value 450; id 561 nature rune; id 554 fire rune)"
|
||||
- "prices.runescape.wiki latest 2026-08-27 (necklace buy 148, nature buy 144, fire buy 5)"
|
||||
verified: "2026-08-27" # keep quoted (YAML 1.1 date parsing)
|
||||
|
||||
related:
|
||||
- high-alch-p2p
|
||||
- f2p-alch-catalog
|
||||
- mule-topup
|
||||
140
docs/action-grammar.md
Normal file
140
docs/action-grammar.md
Normal file
@ -0,0 +1,140 @@
|
||||
# OSRS Codex — Universal Action Grammar
|
||||
|
||||
The action grammar is the **bridge** between codex entries (client-agnostic)
|
||||
and real scripts (client-specific). An entry never names a client API; it
|
||||
uses a **verb** from this file. Each agent maintains the mapping table for
|
||||
its own client.
|
||||
|
||||
A `verb` is one primitive, atomic capability. Compound behaviour is built by
|
||||
chaining verbs in the entry's `actions[]` list and wiring the state machine.
|
||||
|
||||
Legend for the mapping table:
|
||||
- `DB` = DreamBot (tblkit / OSBot-style Java)
|
||||
- `MB` = Microbot
|
||||
- `RL` = RuneLite plugin
|
||||
- `—` = not expressible as a single primitive; entry should decompose.
|
||||
|
||||
> Columns are filled by each client owner as their client support lands.
|
||||
> `—` is a legal value: it means "decompose into smaller verbs."
|
||||
|
||||
---
|
||||
|
||||
## A. World / presence
|
||||
|
||||
| verb | meaning | DB | MB | RL |
|
||||
|-----------------|------------------------------------------------|--------------|-------------|----|
|
||||
| `login` | authenticate the account | — | — | — |
|
||||
| `select_world` | join a specific world by id/type | — | — | — |
|
||||
| `hop_world` | switch worlds (F2P or P2P hop) | — | — | — |
|
||||
| `is_logged_in` | predicate: authenticated & in-game | — | — | — |
|
||||
|
||||
## B. Movement
|
||||
|
||||
| verb | meaning | DB | MB | RL |
|
||||
|-----------------|--------------------------------------------------|----|----|----|
|
||||
| `move_to` | get to a named location (teleport-or-walk) | — | — | — |
|
||||
| `walk_to` | path-walk to a tile | — | — | — |
|
||||
| `teleport` | use a teleport (spell/item/scroll/portal) | — | — | — |
|
||||
| `enter_region` | assert we are inside a named area (predicate) | — | — | — |
|
||||
| `open_door` | open/lock an interactable door | — | — | — |
|
||||
|
||||
## C. Interaction (the core)
|
||||
|
||||
| verb | meaning | DB | MB | RL |
|
||||
|--------------------|----------------------------------------------------|----|----|----|
|
||||
| `use_item_on` | use inventory item `X` on target `Y` (or itself) | — | — | — |
|
||||
| `click_object` | left-click a world object (NPC/NPC tile/thing) | — | — | — |
|
||||
| `right_click` | click a specific menu option on a target | — | — | — |
|
||||
| `talk_to` | open an NPC dialogue and pick a line | — | — | — |
|
||||
| `click_button` | click a UI widget by stable reference | — | — | — |
|
||||
| `menu_option` | select from an in-game menu (F-keys, menus) | — | — | — |
|
||||
|
||||
## D. Inventory / bank
|
||||
|
||||
| verb | meaning | DB | MB | RL |
|
||||
|--------------------|----------------------------------------------------|----|----|----|
|
||||
| `bank_inventory` | open bank, deposit/withdraw per params, close | — | — | — |
|
||||
| `deposit_all` | deposit every bankable item | — | — | — |
|
||||
| `withdraw` | withdraw a named item up to a quantity | — | — | — |
|
||||
| `has_item` | predicate: inventory holds item ≥ qty | — | — | — |
|
||||
| `count_item` | read inventory quantity of an item | — | — | — |
|
||||
| `move_to_inv` | move item from bank/ground into inventory | — | — | — |
|
||||
| `sort_inventory` | reorder inventory into a known layout | — | — | — |
|
||||
|
||||
## E. Exchange (Grand Exchange)
|
||||
|
||||
| verb | meaning | DB | MB | RL |
|
||||
|--------------------|----------------------------------------------------|----|----|----|
|
||||
| `ge_open` | open GE at a bank | — | — | — |
|
||||
| `ge_buy` | buy `qty` of `item` at offer price, paced | — | — | — |
|
||||
| `ge_sell` | sell `qty` of `item`, confirm | — | — | — |
|
||||
| `ge_offers_done` | predicate: no pending buy/sell offers | — | — | — |
|
||||
| `ge_collect` | collect finished GE transactions | — | — | — |
|
||||
| `ge_check_price` | read current buy/sell price of an item | — | — | — |
|
||||
|
||||
## F. Combat
|
||||
|
||||
| verb | meaning | DB | MB | RL |
|
||||
|--------------------|----------------------------------------------------|----|----|----|
|
||||
| `attack` | start/continue attacking a target | — | — | — |
|
||||
| `prayer` | toggle a prayer on/off | — | — | — |
|
||||
| `buff` | apply a buff item/potion | — | — | — |
|
||||
| `eat` | heal when HP < threshold | — | — | — |
|
||||
| `retreat` | disengage and reposition | — | — | — |
|
||||
| `is_dead` | predicate: account is dead | — | — | — |
|
||||
| `respawn` | return to a spawn/bank after death | — | — | — |
|
||||
|
||||
## G. Observation (predicates)
|
||||
|
||||
Predicates return a boolean and drive `state_machine.transitions`. They are
|
||||
**free** (no anti-ban cost) and may be polled every tick.
|
||||
|
||||
| verb | meaning |
|
||||
|------------------|------------------------------------------|
|
||||
| `is_at` | at a named location |
|
||||
| `has_item` | inventory holds item ≥ qty |
|
||||
| `item_count` | quantity of an item in inv/bank |
|
||||
| `level_is` | a skill is at/above a level |
|
||||
| `xp_is` | a skill's xp at/above a threshold |
|
||||
| `hp_is` | HP below a threshold |
|
||||
| `is_dead` | dead |
|
||||
| `ge_clear` | no pending GE offers |
|
||||
| `bank_open` | a bank is open |
|
||||
| `quest_state` | a quest is started/completed |
|
||||
| `npc_present` | a named NPC is in range/visible |
|
||||
|
||||
## H. Housekeeping / safety
|
||||
|
||||
| verb | meaning | DB | MB | RL |
|
||||
|-----------------|--------------------------------------------------|----|----|----|
|
||||
| `log` | write a sentinel log line (RESULT/DONE/ERROR) | — | — | — |
|
||||
| `alert` | raise a humanised alert (operator inbox) | — | — | — |
|
||||
| `sleep` | humanised pause `delay:[min,max]` | — | — | — |
|
||||
| `human_move` | anti-ban mouse wander / cursor nudge | — | — | — |
|
||||
| `take_break` | scheduled AFK pause (run_control.break) | — | — | — |
|
||||
|
||||
---
|
||||
|
||||
## Parameter conventions
|
||||
|
||||
`params` are key/value and client-agnostic. Recognised keys:
|
||||
|
||||
- `location` — a **named** place. Must resolve to a real, named OSRS
|
||||
location (never a bare coordinate in the entry; coordinates live in the
|
||||
client mapping).
|
||||
- `item` / `on` — canonical item names (exact wiki names).
|
||||
- `qty`, `buffer` — counts.
|
||||
- `method` — ordered preference list for how to achieve the verb
|
||||
(e.g. `[teleport, walk]`); the client tries in order.
|
||||
- `menu`, `option` — the exact right-click/option string.
|
||||
- `price` / `max_price` — GP ceiling for a GE buy (anti-loss).
|
||||
- `threshold` — a level/xp/hp cutoff for predicates and `eat`.
|
||||
|
||||
## Anti-ban hints (optional, per action)
|
||||
|
||||
- `delay: [min, max]` — randomised ms to sleep after the action.
|
||||
- `mouse` — free-text humanisation directive (wander, jitter, bezier).
|
||||
- `jitter` — randomise click offset in px.
|
||||
|
||||
These are **hints**, not guarantees. The translating agent may strengthen
|
||||
them, never weaken a codex-mandated value.
|
||||
64
docs/data-sources.md
Normal file
64
docs/data-sources.md
Normal file
@ -0,0 +1,64 @@
|
||||
# OSRS Codex — Data Sources & Verification Rules
|
||||
|
||||
Every codex entry must carry `sources:` and `verified:` (date). Facts are
|
||||
classified, and each class has a **required** verification path. An entry
|
||||
whose facts fail verification stays `status: draft` and blocks merge.
|
||||
|
||||
## Fact classes
|
||||
|
||||
| class | examples | required source |
|
||||
|-------|----------|-----------------|
|
||||
| **item-ids** | item names, wiki ids | `prices.runescape.wiki/api/v1/osrs/mapping` |
|
||||
| **alch values** | lowalch / highalch / value | same mapping endpoint; assert `lowalch == highalch*2//3` |
|
||||
| **ge limits** | buy limit / 4h | mapping `limit` **and** wiki `Module:Exchange/<Item>` Lua field `limit` — both must agree |
|
||||
| **membership** | F2P vs members | mapping `members` **and** item page `members = No/Yes` |
|
||||
| **live prices** | buy/sell price | `prices.runescape.wiki/api/v1/osrs/latest` → `low` (buy); re-verify on every `status: verified` bump |
|
||||
| **xp / mechanics** | XP per action, level reqs, cast times | OSRS wiki article infobox (oldschool.runescape.wiki/w/<Topic>) |
|
||||
| **locations / NPC dialogue** | bank names, menu option strings | OSRS wiki + in-game observation (flag `observed: date`) |
|
||||
| **best-practice setup** | equipment/technique choices (staff of fire, noted items, best prayer) | OSRS wiki page, or `researched <date>` (follow-up citation issue) |
|
||||
|
||||
## Authoritative endpoints (all require a descriptive User-Agent or 403)
|
||||
|
||||
```
|
||||
mapping : https://prices.runescape.wiki/api/v1/osrs/mapping (JSON array)
|
||||
latest : https://prices.runescape.wiki/api/v1/osrs/latest ({"data":{id:{low,high}}})
|
||||
exchange: https://oldschool.runescape.wiki/api.php?action=query&titles=Module:Exchange/<Name>&prop=revisions&rvprop=content&format=json
|
||||
itempage: https://oldschool.runescape.wiki/api.php?action=parse&page=<Name>&prop=wikitext&format=json
|
||||
```
|
||||
|
||||
## Verification pipeline (triple-source, then assert)
|
||||
|
||||
1. Build `by_name` from mapping. Fetch `latest` once.
|
||||
2. For each item fact: mapping → exchange module → item page. All must agree.
|
||||
3. Assert `lowalch == highalch * 2 // 3` — catches copy/paste errors.
|
||||
4. Record the **research date** in `verified:` and the endpoint used in `sources:`.
|
||||
|
||||
## Known traps (learned the hard way — do not repeat)
|
||||
|
||||
- **The `mapping` mirror is incomplete.** Observed 2026-08-27: 4,652 items,
|
||||
dyes present but **no mantle/dye-variant entries at all**. An item missing
|
||||
from mapping is NOT proof the item doesn't exist — cross-check the wiki
|
||||
page before concluding anything. Never trust "not in mirror" as a fact.
|
||||
- **`Module:Exchange` field is `limit`, not `buyLimit`.** Grepping the wrong
|
||||
name yields false mismatches on every row.
|
||||
- **Members flag vs intuition.** Items that "look F2P" can be members-only
|
||||
(all bracelets, battlestaff, unstrung maple/yew/magic bows, unholy symbol,
|
||||
orbs). The strung `Maple longbow` is F2P; `Maple longbow (u)` is members.
|
||||
Always read the flag, never infer it.
|
||||
- **Live prices move daily.** `latest` is a snapshot. Entries that embed
|
||||
prices must carry `verified:` within the last 30 days to stay
|
||||
`status: verified`; the validator warns (not fails) on staleness.
|
||||
- **Alch economics are market-dependent.** Low alch is a *training*
|
||||
activity at current prices (verified negative GP/cycle 2026-08-27).
|
||||
Record the true economics; do not dress a loss as profit.
|
||||
|
||||
## Research tooling
|
||||
|
||||
- `osrs-wiki-item-data` skill (Vale) holds the working Python recipe,
|
||||
the verified 20-item F2P low-alch catalog, and jar-verification notes.
|
||||
- `ge-flip-scanner` skill: live margin/volume scanning (5m endpoint nulls
|
||||
pitfall applies).
|
||||
- OSRS wiki is reachable from this box via the Hermes web proxy
|
||||
(`web_extract` / `web_search`), NOT direct DNS — direct
|
||||
`services.runescape.wiki` resolves fail from WSL. `prices.runescape.wiki`
|
||||
resolves fine directly.
|
||||
224
docs/schema.md
Normal file
224
docs/schema.md
Normal file
@ -0,0 +1,224 @@
|
||||
# OSRS Codex — Entry Schema (v1)
|
||||
|
||||
The codex is a **client-agnostic** knowledge base. Every bot-able OSRS
|
||||
activity is described here once, in a strict YAML structure. AI agents do
|
||||
**not** read the game — they read a codex entry and *translate* its
|
||||
abstract actions into whatever client API they target (DreamBot, Microbot,
|
||||
OSBot, RuneLite plugins, custom, ...).
|
||||
|
||||
Two invariants make translation reliable:
|
||||
|
||||
1. **One entry = one bot-able activity**, described end-to-end.
|
||||
2. **Every action references the universal vocabulary** (`docs/action-grammar.md`).
|
||||
No client-specific calls ever appear in an entry.
|
||||
|
||||
---
|
||||
|
||||
## File layout
|
||||
|
||||
```
|
||||
osrs-codex/
|
||||
docs/
|
||||
schema.md <- this file
|
||||
action-grammar.md <- universal action vocabulary
|
||||
data-sources.md <- where facts come from + verification rules
|
||||
codex/
|
||||
economy/ <- money-making: flips, alching, drops, selling
|
||||
training/ <- level training per skill (per band)
|
||||
combat/ <- bossing, skilling-in-combat
|
||||
combat-slayers/ <- slayer-specific
|
||||
quests/ <- questable XP / requirement unlocks
|
||||
transport/ <- movement, teleporting, banking logistics
|
||||
meta/ <- account building, mule/logistics, world setup
|
||||
schemas/
|
||||
entry.schema.json <- JSON-Schema for validation
|
||||
tools/
|
||||
validate.py <- runs the schema over all codex/**.yaml
|
||||
INDEX.md <- machine-generated catalog of every entry
|
||||
```
|
||||
|
||||
An activity that spans domains (e.g. alchemy training *and* money) lives in
|
||||
its **primary** domain; cross-refs go in `related`.
|
||||
|
||||
---
|
||||
|
||||
## Entry structure (YAML)
|
||||
|
||||
```yaml
|
||||
id: alching-nature-runecoins # kebab-case, globally unique
|
||||
title: Low-Level Alchemy (F2P)
|
||||
domain: economy # one of the codex/ dirs
|
||||
tags: [alching, money, f2p, p2p-safe]
|
||||
status: verified # draft | verified | stale
|
||||
version: 3
|
||||
updated: 2026-08-27 # last date facts were re-verified
|
||||
|
||||
summary: >
|
||||
One-sentence description of what the bot does and why it runs.
|
||||
|
||||
# ---------------------------------------------------------------- accounts
|
||||
accounts:
|
||||
membership: f2p # f2p | members | hybrid (which items)
|
||||
min_level: 1 # absolute floor to start
|
||||
requires_quest: [] # quest ids (see codex/quests/)
|
||||
requires_other: # free-form hard prerequisites
|
||||
- "A bankable inventory + bank access"
|
||||
|
||||
# ---------------------------------------------------------------- setup
|
||||
best_practice: # REQUIRED — best-in-class execution
|
||||
equipment:
|
||||
- item: staff of fire
|
||||
why: "unlimited fire runes; drops LA rune cost to 1 nature/cast"
|
||||
source: "OSRS wiki — Staff of Fire; Low Level Alchemy (Cost)"
|
||||
techniques:
|
||||
- name: cast on noted (stacked) items
|
||||
why: "one slot, minimal mouse travel"
|
||||
source: "OSRS wiki — Low Level Alchemy (Usage)"
|
||||
|
||||
# ---------------------------------------------------------------- economy
|
||||
economy:
|
||||
cost_per_cycle: 62 # GP consumed per meaningful action
|
||||
income_per_cycle: 81
|
||||
net_per_cycle: 19
|
||||
gp_per_hour_low: 3000
|
||||
gp_per_hour_high: 6000
|
||||
xp_per_hour: 14000
|
||||
supplies: # everything the bot must carry / restock
|
||||
- item: nature rune
|
||||
qty_per_cycle: 1
|
||||
source: ge
|
||||
ge_limit: 12500 # buy limit / 4h window (verified)
|
||||
- item: coal
|
||||
qty_per_cycle: 1
|
||||
source: ge
|
||||
ge_limit: 8000
|
||||
profit_drivers: # what actually makes it profitable
|
||||
- "Low alch value vs rune+material buy price"
|
||||
|
||||
# ---------------------------------------------------------------- state
|
||||
state_machine:
|
||||
initial: idle
|
||||
terminal: [complete, failed]
|
||||
states:
|
||||
idle:
|
||||
goal: reach the banking/crafting location
|
||||
next: [crafting, bank_supplies]
|
||||
crafting:
|
||||
goal: cast Low Level Alchemy on a sellable item
|
||||
next: [bank_supplies, idle, complete, failed]
|
||||
bank_supplies:
|
||||
goal: bank products, withdraw fresh supplies
|
||||
next: [crafting, idle]
|
||||
complete:
|
||||
goal: hit the run's completion condition
|
||||
failed:
|
||||
goal: a hard failure — stop, log, alert
|
||||
transitions:
|
||||
- from: crafting
|
||||
when: "runes or materials < buffer"
|
||||
to: bank_supplies
|
||||
- from: idle
|
||||
when: "target level or gp goal reached"
|
||||
to: complete
|
||||
|
||||
# ---------------------------------------------------------------- actions
|
||||
actions:
|
||||
- id: a1
|
||||
step: reach-bank
|
||||
verb: move_to # <- universal verb (action-grammar.md)
|
||||
target: bank
|
||||
params:
|
||||
location: "Bank of Gielinor"
|
||||
method: [teleport, walk] # preferred, fallback
|
||||
on_fail: retry # retry | skip | abort | alert
|
||||
anti_ban: # OPTIONAL humanisation hints
|
||||
mouse: "wander 20-80px around target"
|
||||
delay: [400, 1600]
|
||||
|
||||
- id: a2
|
||||
step: cast-alch
|
||||
verb: use_item_on
|
||||
target: "sellable item in inventory"
|
||||
params:
|
||||
item: "lowest-alch-value inventory item"
|
||||
on: "itself"
|
||||
on_fail: retry
|
||||
anti_ban:
|
||||
delay: [900, 2400]
|
||||
|
||||
- id: a3
|
||||
step: bank-loop
|
||||
verb: bank_inventory
|
||||
target: bank
|
||||
params:
|
||||
deposit: [all products]
|
||||
withdraw: "supplies up to buffer"
|
||||
on_fail: alert
|
||||
|
||||
# ---------------------------------------------------------------- loop / pacing
|
||||
run_control:
|
||||
iteration: "one full craft+bank cycle"
|
||||
max_iterations: null # null = until complete condition
|
||||
break: # humanised pause policy
|
||||
every: [8, 14] # cycles
|
||||
for: [60, 180] # seconds
|
||||
stop_conditions:
|
||||
- "target level reached"
|
||||
- "gp under floor"
|
||||
- "hard error 3x"
|
||||
|
||||
# ---------------------------------------------------------------- safety
|
||||
risk:
|
||||
ban_level: low # low | medium | high
|
||||
notes:
|
||||
- "Rune purchase bursts are the main detection vector; pace GE buys."
|
||||
|
||||
# ---------------------------------------------------------------- provenance
|
||||
sources:
|
||||
- "OSRS Wiki — Low level alchemy (price/limit verified 2026-08-27)"
|
||||
- "prices.runescape.wiki mapping (item ids + GE limits)"
|
||||
verified: 2026-08-27
|
||||
|
||||
related:
|
||||
- mule-topup
|
||||
- f2p-alch-catalog
|
||||
```
|
||||
|
||||
Every key except `actions[].anti_ban`, `notes`, `params` details is
|
||||
**required** (enforced by `schemas/entry.schema.json`). The validator is
|
||||
strict — a draft that fails validation blocks the PR.
|
||||
|
||||
---
|
||||
|
||||
## Best-in-class rule (required)
|
||||
|
||||
Every entry MUST declare how a top operator actually runs the method, in
|
||||
`best_practice`. This is where "staff of fire for alchemy", "noted items in
|
||||
the spell slot", "marks of grace", "best prayer for the boss" etc. live.
|
||||
|
||||
- `equipment[]` — item + `why` (mechanical reason) + `source`.
|
||||
- `techniques[]` — named technique + `why` + `source`.
|
||||
- Every line carries a `source`: an OSRS wiki page, **or** `researched
|
||||
<date>` for a fact not yet documented on the wiki — a `researched` flag
|
||||
is a TODO: a follow-up issue must land the proper citation.
|
||||
- The translator MUST adopt the best-practice setup by default. Deviating
|
||||
requires `improvised: true` in the translation header with a reason.
|
||||
|
||||
## Translation contract (agent -> client)
|
||||
|
||||
When an agent picks up an entry to build a script:
|
||||
|
||||
1. Read `state_machine` → build the client's state machine 1:1 (same state
|
||||
names, same transition conditions).
|
||||
2. Walk `actions[]` in order; map each `verb` using the client's mapping
|
||||
table in `docs/action-grammar.md` (DreamBot column, Microbot column, ...).
|
||||
3. Apply `run_control` pacing and `break` policy verbatim.
|
||||
4. Apply every `anti_ban` hint; the agent may **add** humanisation but must
|
||||
never remove a codex-mandated one.
|
||||
5. Emit a `translation` block in the generated script header: codex `id`,
|
||||
`version`, client name, and any verb the agent had to improvise (flagged
|
||||
`improvised: true`).
|
||||
|
||||
If a verb has no mapping for the target client, the agent MUST mark it
|
||||
`improvised: true` and leave a `TODO(client)` — never silently invent
|
||||
behaviour.
|
||||
220
schemas/entry.schema.json
Normal file
220
schemas/entry.schema.json
Normal file
@ -0,0 +1,220 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "OSRS Codex Entry",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id", "title", "domain", "tags", "status", "version", "updated",
|
||||
"summary", "accounts", "best_practice", "state_machine", "actions",
|
||||
"run_control", "risk", "sources", "verified"
|
||||
],
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 64 },
|
||||
"title": { "type": "string", "minLength": 3 },
|
||||
"domain": {
|
||||
"enum": ["economy", "training", "combat", "combat-slayers", "quests", "transport", "meta"]
|
||||
},
|
||||
"tags": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
|
||||
"status": { "enum": ["draft", "verified", "stale"] },
|
||||
"version": { "type": "integer", "minimum": 1 },
|
||||
"updated": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
|
||||
"summary": { "type": "string", "minLength": 10 },
|
||||
|
||||
"accounts": {
|
||||
"type": "object",
|
||||
"required": ["membership", "min_level"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"membership": { "enum": ["f2p", "members", "hybrid"] },
|
||||
"min_level": { "type": "integer", "minimum": 1, "maximum": 126 },
|
||||
"requires_quest": { "type": "array", "items": { "type": "string" } },
|
||||
"requires_other": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
|
||||
"economy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"item": { "type": "string" },
|
||||
"item_id": { "type": "integer" },
|
||||
"cost_per_cycle": { "type": "number" },
|
||||
"income_per_cycle": { "type": "number" },
|
||||
"net_per_cycle": { "type": "number" },
|
||||
"gp_per_hour_low": { "type": "number" },
|
||||
"gp_per_hour_high": { "type": "number" },
|
||||
"xp_per_hour": { "type": "number" },
|
||||
"supplies": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["item", "qty_per_cycle", "source"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"item": { "type": "string" },
|
||||
"qty_per_cycle": { "type": "number", "exclusiveMinimum": 0 },
|
||||
"source": { "enum": ["ge", "p2p", "drops", "craft", "bank", "npc-shop"] },
|
||||
"ge_limit": { "type": "integer", "minimum": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"profit_drivers": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
|
||||
"best_practice": {
|
||||
"type": "object",
|
||||
"required": ["equipment"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"equipment": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["item", "why", "source"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"item": { "type": "string", "minLength": 2 },
|
||||
"why": { "type": "string", "minLength": 5 },
|
||||
"source": { "type": "string", "minLength": 5 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"techniques": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name", "why", "source"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 2 },
|
||||
"why": { "type": "string", "minLength": 5 },
|
||||
"source": { "type": "string", "minLength": 5 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"state_machine": {
|
||||
"type": "object",
|
||||
"required": ["initial", "terminal", "states"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"initial": { "type": "string" },
|
||||
"terminal": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 1,
|
||||
"contains": { "const": "failed" }
|
||||
},
|
||||
"states": {
|
||||
"type": "object",
|
||||
"minProperties": 2,
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": ["goal"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"goal": { "type": "string", "minLength": 3 },
|
||||
"next": { "type": "array", "items": { "type": "string" }, "minItems": 1 }
|
||||
}
|
||||
}
|
||||
},
|
||||
"transitions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["from", "when", "to"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"from": { "type": "string" },
|
||||
"when": { "type": "string", "minLength": 3 },
|
||||
"to": { "type": "string" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "step", "verb", "on_fail"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^a[0-9]+$" },
|
||||
"step": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
|
||||
"verb": {
|
||||
"enum": [
|
||||
"login", "select_world", "hop_world", "is_logged_in",
|
||||
"move_to", "walk_to", "teleport", "enter_region", "open_door",
|
||||
"use_item_on", "click_object", "right_click", "talk_to",
|
||||
"click_button", "menu_option",
|
||||
"bank_inventory", "deposit_all", "withdraw", "has_item",
|
||||
"count_item", "move_to_inv", "sort_inventory",
|
||||
"ge_open", "ge_buy", "ge_sell", "ge_offers_done", "ge_collect",
|
||||
"ge_check_price",
|
||||
"attack", "prayer", "buff", "eat", "retreat", "is_dead", "respawn",
|
||||
"is_at", "item_count", "level_is", "xp_is", "hp_is",
|
||||
"ge_clear", "bank_open", "quest_state", "npc_present",
|
||||
"log", "alert", "sleep", "human_move", "take_break"
|
||||
]
|
||||
},
|
||||
"target": { "type": "string" },
|
||||
"params": { "type": "object" },
|
||||
"on_fail": { "enum": ["retry", "skip", "abort", "alert"] },
|
||||
"anti_ban": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"delay": {
|
||||
"type": "array",
|
||||
"items": { "type": "number", "minimum": 0 },
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"mouse": { "type": "string" },
|
||||
"jitter": { "type": "number", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"run_control": {
|
||||
"type": "object",
|
||||
"required": ["iteration", "stop_conditions"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"iteration": { "type": "string" },
|
||||
"max_iterations": { "type": ["integer", "null"], "minimum": 1 },
|
||||
"break": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"every": { "type": "array", "items": { "type": "number", "minimum": 1 }, "minItems": 2, "maxItems": 2 },
|
||||
"for": { "type": "array", "items": { "type": "number", "minimum": 1 }, "minItems": 2, "maxItems": 2 }
|
||||
}
|
||||
},
|
||||
"stop_conditions": { "type": "array", "items": { "type": "string" }, "minItems": 1 }
|
||||
}
|
||||
},
|
||||
|
||||
"risk": {
|
||||
"type": "object",
|
||||
"required": ["ban_level"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ban_level": { "enum": ["low", "medium", "high"] },
|
||||
"notes": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
},
|
||||
|
||||
"sources": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
|
||||
"verified": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
|
||||
"related": { "type": "array", "items": { "type": "string" } }
|
||||
}
|
||||
}
|
||||
138
tools/validate.py
Normal file
138
tools/validate.py
Normal file
@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""OSRS Codex validator — the gate for every codex/**.yaml entry.
|
||||
|
||||
Runs:
|
||||
1. JSON-Schema validation (schemas/entry.schema.json)
|
||||
2. Cross-field consistency checks the schema can't express:
|
||||
- state_machine `next`/transition targets must be declared states
|
||||
- `initial` must be a declared state
|
||||
- duplicate action ids / step names
|
||||
- status:verified entries with `verified:` older than --stale-days
|
||||
=> WARNING (entry should be re-verified, not dropped)
|
||||
- `best_practice` lines whose source starts with "researched"
|
||||
=> WARNING (citation TODO — file a follow-up issue)
|
||||
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]
|
||||
"""
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import jsonschema
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCHEMA = json.loads((ROOT / "schemas" / "entry.schema.json").read_text())
|
||||
CODEX = ROOT / "codex"
|
||||
STALE_DAYS = 30
|
||||
|
||||
|
||||
def load_entries():
|
||||
out = []
|
||||
for p in CODEX.rglob("*.yaml"):
|
||||
if p.name.startswith("_"): # dataset files (e.g. _f2p-alch-catalog.yaml)
|
||||
continue
|
||||
out.append(p)
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def main():
|
||||
global STALE_DAYS
|
||||
if "--stale-days" in sys.argv:
|
||||
STALE_DAYS = int(sys.argv[sys.argv.index("--stale-days") + 1])
|
||||
|
||||
errors, warnings = [], []
|
||||
entries = load_entries()
|
||||
if not entries:
|
||||
print("ERROR: no codex entries found under codex/")
|
||||
return 1
|
||||
|
||||
ids = {}
|
||||
today = datetime.date.today()
|
||||
|
||||
for path in entries:
|
||||
rel = str(path.relative_to(ROOT))
|
||||
try:
|
||||
data = yaml.safe_load(path.read_text())
|
||||
except yaml.YAMLError as e:
|
||||
errors.append(f"{rel}: YAML parse error: {e}")
|
||||
continue
|
||||
|
||||
# 1. schema
|
||||
v = jsonschema.Draft7Validator(SCHEMA)
|
||||
for err in sorted(v.iter_errors(data), key=lambda e: list(e.path)):
|
||||
loc = "/".join(str(p) for p in err.path) or "(root)"
|
||||
errors.append(f"{rel} [{loc}]: {err.message}")
|
||||
if errors and errors[-1].startswith(rel):
|
||||
# schema already broke; cross-checks on a broken doc are noise
|
||||
continue
|
||||
|
||||
eid = data.get("id", rel)
|
||||
if eid in ids:
|
||||
errors.append(f"{rel}: duplicate entry id '{eid}' (also in {ids[eid]})")
|
||||
ids[eid] = rel
|
||||
|
||||
# 2. state machine consistency
|
||||
sm = data.get("state_machine", {})
|
||||
states = sm.get("states", {})
|
||||
declared = set(states)
|
||||
terminals = set(sm.get("terminal", []))
|
||||
for sname, sdef in states.items():
|
||||
if sname not in terminals and not sdef.get("next"):
|
||||
errors.append(f"{rel}: non-terminal state '{sname}' has no 'next'")
|
||||
for nxt in sdef.get("next", []):
|
||||
if nxt not in declared:
|
||||
errors.append(f"{rel}: state '{sname}' next->'{nxt}' not declared")
|
||||
if sm.get("initial") not in declared:
|
||||
errors.append(f"{rel}: initial state '{sm.get('initial')}' not declared")
|
||||
for t in sm.get("transitions", []):
|
||||
if t["from"] not in declared:
|
||||
errors.append(f"{rel}: transition from '{t['from']}' not declared")
|
||||
if t["to"] not in declared:
|
||||
errors.append(f"{rel}: transition to '{t['to']}' not declared")
|
||||
for term in sm.get("terminal", []):
|
||||
if term not in declared:
|
||||
errors.append(f"{rel}: terminal '{term}' not declared")
|
||||
|
||||
# action id/step uniqueness
|
||||
a_ids = [a["id"] for a in data["actions"]]
|
||||
a_steps = [a["step"] for a in data["actions"]]
|
||||
if len(a_ids) != len(set(a_ids)):
|
||||
errors.append(f"{rel}: duplicate action ids")
|
||||
if len(a_steps) != len(set(a_steps)):
|
||||
errors.append(f"{rel}: duplicate action step names")
|
||||
|
||||
# 3. staleness / research TODOs
|
||||
if data.get("status") == "verified":
|
||||
try:
|
||||
vdate = datetime.date.fromisoformat(data["verified"])
|
||||
age = (today - vdate).days
|
||||
if age > STALE_DAYS:
|
||||
warnings.append(
|
||||
f"{rel}: status=verified but facts are {age}d old "
|
||||
f"(> {STALE_DAYS}d) — re-verify or drop to status:stale")
|
||||
except ValueError:
|
||||
pass
|
||||
for line in data.get("best_practice", {}).get("equipment", []) + \
|
||||
data.get("best_practice", {}).get("techniques", []):
|
||||
if str(line.get("source", "")).startswith("researched"):
|
||||
warnings.append(
|
||||
f"{rel}: best_practice '{line.get('item') or line.get('name')}' "
|
||||
f"carries a 'researched' source — citation TODO, file follow-up")
|
||||
|
||||
# report
|
||||
for w in warnings:
|
||||
print(f"WARN {w}")
|
||||
for e in errors:
|
||||
print(f"ERROR {e}")
|
||||
print(f"RESULT: {len(entries)} entries, {len(errors)} errors, {len(warnings)} warnings")
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in New Issue
Block a user