What POE Crafter Does
From game item to validation result in 50 milliseconds
What Is POE Crafter?
Path of Exile is a game built around one idea: every piece of equipment is unique because it carries random bonuses called mods. Each item can hold up to six mods, split into two groups — prefixes and suffixes — with a maximum of three of each.
To change or improve those mods, players spend currency orbs — which are valuable and scarce. A crafting mistake can waste hours of in-game work. POE Crafter lets you check whether your plan is legal before you spend anything.
Path of Exile has hundreds of hidden rules about which mods can coexist on an item. Violating any one of them means your planned item simply cannot exist — and there is no in-game tool to check ahead of time.
What You Get
Reads both in-game copy-paste format and Path of Building export format — you do not need to do anything special, just paste.
Checks four rules automatically: affix limits, mod group exclusivity, item type compatibility, and item level gating.
Coming soon — runs thousands of simulated orb rolls to estimate how much currency a crafting strategy will cost on average.
What Happens When You Paste an Item
Here is what happens in the ~50 milliseconds between you clicking Paste and seeing the result. Four components spring into action in a precise sequence — each one handing off work to the next.
The browser sends a POST request to an endpoint on the server. The HTTP response comes back as JSON — a simple text format that the browser can read and display.
The Tools POE Crafter Is Built With
Every software project is assembled from existing building blocks. Here are the four core tools POE Crafter relies on — and what each one actually does.
Python
The programming language. All of the logic — parsing, validating, simulating — is written in Python. It reads almost like English, which makes it a good choice for complex rule systems.
FastAPI
A Python
framework
that turns Python functions into
API endpoints
you can call over the internet.
It also auto-generates interactive documentation at /docs.
SQLite
A tiny embedded database stored as a single file on disk. It holds all Path of Exile game data — thousands of mod definitions, spawn weights, and item types — ready to query in microseconds.
Pydantic
A data library that ensures incoming requests have exactly the right shape before any validation logic runs. If a request is malformed, Pydantic rejects it immediately with a clear error.
Where Everything Lives
The repository is organized so each folder has exactly one job. Once you know the layout, you will always know where to look — or where to add something new.
In the next module, we will meet the six main components that make all of this work — and see exactly how they connect to each other.
Check Your Understanding
Three quick questions to make sure the key ideas landed.
You want to add a new validation rule to POE Crafter. Which folder would you tell an AI coding assistant to look in?
A user pastes an item and gets back an invalid result with an error. Where in the journey did that decision happen?
You want POE Crafter to support flasks as a new item type. Based on what you now know, what would need updating?
The Cast of Characters
Six components, six jobs — and how they talk to each other
Six Components, Six Jobs
POE Crafter has six main components. When you paste an item, all six spring into action — like a stage crew that has rehearsed their moves so many times it looks effortless.
The front door. Routes HTTP requests to the right handler and returns responses to the browser.
The memory palace. Holds all game data as fast in-memory Python dictionaries — bases, mods, spawn weights — all loaded once at startup.
The translator. Converts raw item text (from the game or Path of Building) into a structured Python object the rest of the app can reason about.
The referee. Runs 4 rules against a parsed item and returns a clear pass/fail with reasons — affix limits, mod group exclusivity, tag requirements, and item-level gating.
The dice roller. Simulates currency orb effects using weighted RNG — the same pure-function logic the actual game uses.
The arena. Wraps the engine in a game episode — handles reset, step, and done so strategies never need to touch the engine directly.
Stateless vs Stateful
Not all components are created equal. Some remember things between calls; others forget everything the moment they finish. This distinction shapes how the whole system is tested, scaled, and debugged.
Stateless (No Memory)
CraftingEngine, ValidationEngine, PoB Parser — give them the same item twice and you get the same answer. No side effects, no surprises. Easy to test, safe to run in parallel.
Stateful (Has Memory)
DataCache holds 10,000+ mods in RAM between calls. CraftingEnvironment tracks step count and current episode. They remember things so you do not have to reload from disk every time.
Stateless functions are the honest parts of software — they can be tested in isolation, run in parallel, and debugged easily. Engineers deliberately push as much logic as possible into stateless functions.
The DataCache Singleton
Think of a hotel concierge who memorizes every guest preference once — ask twice and you get the same instant answer. The DataCache loads all 10,000+ game items once at startup, then answers lookups in microseconds. Below is the exact class definition from loader.py, translated line by line.
class DataCache:
"""
In-memory cache for POE game data.
Each instance holds independent state — no shared
class-level mutable dicts.
Use the module-level singleton `cache` for normal
application code.
Instantiate a fresh DataCache() in tests that need
isolation.
"""
def __init__(self) -> None:
self.bases: Dict[str, Dict[str, Any]] = {}
self.mods: Dict[str, Dict[str, Any]] = {}
self._eligible_mods: Dict[str, Dict[int, List[...]]] = {}
self._mod_weights: Dict[str, Dict[int, Dict[str, int]]] = {}
self._initialized: bool = False
self._engine: Optional[Engine] = None
Define the DataCache class — the blueprint for the in-memory game data store...
The docstring says: use the module-level singleton in app code, but create fresh instances in tests...
When a new DataCache is created, all data dictionaries start empty...
bases{} will hold all 3,200+ item base definitions, keyed by their ID...
mods{} will hold all 50,000+ modifier definitions...
The _initialized flag prevents reloading data if initialize() is called more than once — a form of lazy initialization ...
The _engine reference lets the cache make its own database queries later via dependency injection ...
The module-level cache = DataCache() line at the very bottom of loader.py creates THE single instance. Python modules load once per process, so this line runs exactly once — creating the one true cache for the entire application.
Who Calls Whom
When you POST an item to /api/parse-pob, here is the conversation that plays out between the components. Click Next Message to step through it one exchange at a time.
The Full Picture
Every component has a home in the file system and a role in the request lifecycle. Click any component below to see exactly what it does and where it lives.
Backend (Python)
Storage
Check Your Understanding
You want to change how the app decides if a mod tier is too low. Which component owns that decision?
A new engineer asks why there is only one DataCache instead of creating a new one for each request. What is the best explanation?
You add a new item type to the game data. Which components would likely need updating?
The Data Pipeline
From 200MB of raw game files to instant in-memory lookups
Where Does the Game Data Come From?
Path of Exile does not publish a public API for its game data. A community project called RePoE extracts the game files and exports them as JSON. POE Crafter downloads those files and loads them into a local database.
base_items.json
Item Bases (~3,200 entries). Every craftable base item: Runic Gauntlets, Steel Ring, Hubris Circlet. Each has a name, item class, drop level, and a set of tags that control which mods can spawn on it.
mods.json
Modifier Definitions (~50,000 entries). Every possible mod: its display text, stat ranges, spawn weight list, tier, mod group, and minimum item level. This is the game rule book.
Spawn Weight Lists
Inside each mod definition is a priority list of (tag, weight) pairs. Walk the list; return the weight for the first tag that matches the item base. Zero means the mod cannot spawn there.
Game companies rarely publish machine-readable exports of their internal rule data. RePoE fills that gap by reading the raw game client files and converting them to JSON that developers can use. Without it, building a tool like POE Crafter would require manually encoding thousands of rules by hand.
Three Steps: Extract, Transform, Load
ETL is one of the most universal patterns in software: Extract raw data from its source, Transform it into a clean and useful shape, then Load it into a destination system. The POE Crafter pipeline follows this pattern exactly.
Extract, Transform, Load is one of the most common patterns in software. Any time you move data from one system to another — a CSV into a database, an API response into a cache — you are doing ETL. The specific tools change; the three steps stay the same.
Read base_items.json and mods.json from disk. At this point, the data is raw and unfiltered — about 200MB total.
Keep only craftable equipment bases and prefix/suffix mods. Cross-reference every base against every mod to compute the spawn weight table. This is the expensive step.
Write all rows to SQLite in a single transaction using a bulk insert. Then DataCache reads it all into RAM.
The Idempotency Guard
Imagine a printer that checks whether the document is already in the tray before starting — safe to press Print again without getting a duplicate copy. The data loader works the same way: before inserting anything, it checks whether data is already present and returns immediately if it is.
def load_poe_data(
engine: Optional[Engine] = None,
data_dir: Optional[Path] = None,
) -> None:
...
with Session(eng) as session:
# Idempotency guard — skip if already loaded
if session.query(ItemBase).count() > 0:
log.debug("Database already populated — skipping load")
return
Define the function that loads game data into the database...
engine and data_dir are optional — tests can provide their own...
Optional — tests can provide their own isolated database...
...
Open a database session — a temporary connection for reading and writing...
Check if any item base rows already exist — this is the guard...
If the count query returns more than zero, data was already loaded before...
Log a message and return immediately — no duplicate inserts...
An
idempotent
operation produces the same result no matter how many times you call it. load_poe_data() is idempotent — run it once, run it ten times, the database ends up in the same state. This is a cornerstone of reliable data pipelines.
The test suite runs hundreds of times per day. Without idempotency, each test run would try to re-insert all game data and fail with duplicate-key errors. The guard lets the ORM call the function freely without worrying about state.
The Pre-computation Trick
To know which mods can roll on a specific item, you would need to cross-reference every mod against every item base. With 3,200 bases and 50,000 mods, that is O(N times M) — 160 million combinations too slow to compute on every request.
The solution: run this loop once at load time, store every index-able (base, mod, weight) triplet in SQLite, and never repeat the work again.
spawn_weight_rows = []
for base_path, base_data in accepted_bases.items():
item_tags = set(base_data.get("tags", []))
for mod_id, mod_data in accepted_mods.items():
weight = _resolve_spawn_weight(
mod_data.get("spawn_weights", []), item_tags
)
if weight > 0:
spawn_weight_rows.append({
"item_base_id": base_path,
"mod_id": mod_id,
"spawn_weight": weight,
})
Create a list to collect all (base, mod, weight) rows...
Loop over every item base — about 3,200 bases in total...
Get this base item tags — e.g., belt, str_armour, has_str...
For each mod, walk its spawn weight priority list...
Return the weight of the first tag that matches — zero if none match...
If weight is positive, this mod can spawn on this base — save the triplet...
This is a classic time-space trade-off: spend extra time at load (a few seconds) to save time at runtime (microseconds per lookup). The DataCache earns its name — it caches expensive computations so you never pay the cost twice.
Every API request would need to cross-reference 3,200 bases against 50,000 mods. That is 160 million comparisons per request — several seconds of CPU work.
The loader runs the O(N times M) loop once and stores the results. Every runtime query becomes a fast, indexed database join — microseconds instead of seconds.
Check Your Understanding
Three questions to confirm the key ideas from the data pipeline landed.
The POE Crafter server starts up and the database already has data from a previous run.
What happens when load_poe_data() is called?
A new expansion adds 500 new mods to Path of Exile. How do you get them into POE Crafter?
Why are spawn weights stored as a pre-computed table instead of computed on the fly for each request?
Parse & Validate
From raw item text to legal or illegal in two precise steps
Two Formats, One Result
POE Crafter accepts item text in two formats. Both are auto-detected — you just paste and go.
Item Class: Gloves Rarity: Rare Havoc Claw Runic Gauntlets -------- Item Level: 84 -------- +110 to maximum Life +35% to Cold Resistance
Rarity: RARE
Eldritch Gloves
Runic Gauntlets
ItemLvl: 85
Prefix: {range:0.525}IncreasedLife8
Suffix: {range:0.803}FireResist6
The format detection logic is just a few lines. Here is exactly how Path of Building (PoB) exports are told apart from in-game pastes.
def parse_item(text: str, cache) -> ParseResult:
lines = text.splitlines()
is_pob = any(
l.startswith("Prefix: ") or l.startswith("Suffix: ") for l in lines
)
if is_pob:
log.debug("Detected PoB format")
return _parse_pob(text, cache)
log.debug("Detected in-game format")
return _parse_ingame(text, cache)
The main entry point: accepts raw text and the game data cache and promises to return a ParseResult...
Split the text into individual lines so we can examine each one separately...
Scan every line — if ANY starts with Prefix: or Suffix: it is a PoB export. That pattern never appears in in-game copy-paste...
PoB format detection confirmed: hand off to the PoB-specific parser...
No Prefix:/Suffix: lines found: fall through to the in-game format parser...
How a Stat Line Becomes a Mod ID
The game shows +110 to maximum Life, but the code needs IncreasedLife8. These two things mean the same thing in different languages. The parser builds a bridge.
The Problem
The mod database stores stat text as ranges: +(115-129) to maximum Life. A player pastes a rolled value: +110 to maximum Life. They look different — the parser must recognize them as the same mod.
The Solution
Convert every range like (115-129) into a
regex
capture group
like (\d+). Do this once at startup for all 10,000+ mods. Then test every pasted line against every pattern.
The function below converts one line from the mod database into a compiled pre-compiled regex pattern. It is called once per mod during server startup.
def _text_line_to_pattern(line: str) -> Tuple[re.Pattern, List[Tuple[float, float]]]:
ranges: List[Tuple[float, float]] = []
parts: List[str] = []
last_end = 0
for m in _RANGE_RE.finditer(line):
parts.append(re.escape(line[last_end:m.start()]))
lo = float(m.group(1))
hi = float(m.group(2))
ranges.append((lo, hi))
is_float = ("." in m.group(1)) or ("." in m.group(2))
parts.append(r"([\d.]+)" if is_float else r"(\d+)")
last_end = m.end()
parts.append(re.escape(line[last_end:]))
pattern_str = "".join(parts)
return re.compile(r"^" + pattern_str + r"$"), ranges
Convert one line of mod database text into a regex pattern we can match against pasted item text...
Find each (min-max) range token in the text using the pre-built _RANGE_RE scanner, e.g. (115-129)...
Escape the literal text between range tokens so it matches exactly character-for-character...
Record the min and max values — we need them later to verify the rolled number actually falls inside the valid range...
Replace the (115-129) range token with a regex capture group that matches any number the player could have rolled...
Combine all the escaped literals and capture groups into a single regex string...
Compile the regex for speed and return it alongside the range bounds — both pieces are needed to validate a match...
This pattern — compile rules into fast lookup structures at startup, then query them cheaply at runtime — is called pre-compilation. It is used in search engines, compilers, and parsers everywhere. The 10,000 patterns are built once when the server starts, then reused for every request.
The Four Validation Rules
Once the parser produces an
ItemState,
the ValidationEngine runs it through four rules. Any rule can return errors. If all return None, the item is valid.
AffixLimitRule
Maximum 3 prefixes and 3 suffixes. If your target has 4 life mods, this rule fires immediately — items only have 3 prefix slots total.
ModGroupExclusivityRule
No two mods from the same
mod group.
IncreasedLife5 and IncreasedLife3 are both in the IncreasedLife group — you cannot have both on one item.
TagRequirementRule
Each mod must be able to spawn on this base item type. An Elder influence mod cannot roll on a non-Elder item. The rule checks the pre-computed spawn weight table.
ItemLevelGatingRule
Item level must meet each mod minimum. Tier 1 life requires item level 86 — a level 60 item cannot roll it no matter how many orbs you use.
Inside AffixLimitRule
Every rule follows the same contract: receive a dictionary describing the item, return None on success or an error dictionary on failure. Here is AffixLimitRule in full.
class AffixLimitRule(ValidationRule):
MAX_AFFIXES = 3
def validate(self, item_state: Dict) -> Optional[Dict]:
prefixes: List[str] = []
suffixes: List[str] = []
for mod_entry in item_state.get("target_mods", []):
mod_id = mod_entry.get("mod_id") if isinstance(mod_entry, dict) else mod_entry
mod_def = self.cache.mods.get(mod_id)
if mod_def is None:
continue
if mod_def["mod_type"] == "prefix":
prefixes.append(mod_id)
else:
suffixes.append(mod_id)
errors: List[Dict] = []
suggestions: List[str] = []
if len(prefixes) > self.MAX_AFFIXES:
excess = len(prefixes) - self.MAX_AFFIXES
errors.append({
"rule": "affix_limit",
"type": "too_many_prefixes",
"message": f"Too many prefixes: {len(prefixes)} (max {self.MAX_AFFIXES})",
})
Define this rule class. It extends ValidationRule which gives it the cache reference...
Maximum is 3 — this constant comes directly from the Path of Exile game rules...
The validate method runs the check. Returns None if valid, an error dict if not...
Create empty lists to track which mods are prefixes vs suffixes as we sort through them...
For each mod in the target list, look it up in the game data cache by its ID...
If not found (unknown mod ID), skip it — another part of the engine handles unknown mods separately...
Sort each mod into the prefix or suffix list based on its mod_type field from the game database...
If we have more than 3 prefixes, build an error message explaining exactly how many are over the limit...
Returning None to signal success is a Python convention for optional results. The ValidationEngine collects all non-None results from all 4 rules and merges them into the final response.
The Full Parse-Validate Flow
Every item paste travels the same seven-step path from raw text to a structured response. Each step has a single job and hands its output to the next.
/api/parse-pob
The raw item text arrives as the body of an HTTP POST request. No processing has happened yet — it is just a string.
parse_item() auto-detects format
The entry point scans every line. If any starts with Prefix: or Suffix: it routes to the PoB parser. Otherwise it routes to the in-game parser.
Each explicit stat line is tested against 10,000+ compiled regex patterns. Matched lines become mod IDs; unmatched lines become parse warnings.
ItemState object is built
The parser assembles base_id, ilvl, prefixes list, suffixes list, and influences into a single structured object.
engine.validate(item_state.to_validation_dict()) runs all 4 rules
The ItemState is serialized to a plain dictionary and passed through AffixLimitRule, ModGroupExclusivityRule, TagRequirementRule, and ItemLevelGatingRule in sequence.
Each rule either returns None (pass) or an error dictionary. The engine merges all non-None results into a combined errors and suggestions list.
ParsePobResponseSchema is returned
The final response contains three things: the parsed item_state, the complete validation result, and any warnings from lines the parser could not match.
If the parser cannot read the item (unknown base, unrecognized format), it returns parse_errors and stops before validation runs. Parsing and validating fail in different ways — which is exactly why they are separate steps.
Check Your Understanding
A user pastes an item and gets "Too many prefixes: 4 (max 3)" as an error. Which rule caught this?
Why does the parser build regex patterns from the mod database at startup rather than on each request?
An item from a new expansion has a mod not yet in the game database. What happens when someone pastes it?
The Crafting Engine
Weighted lotteries, pure functions, and the math behind each roll
The Currency Operations
Every Path of Exile crafting method boils down to one of five operations. POE Crafter simulates all five. Each currency orb targets a specific rarity and performs exactly one kind of change to the mod pool.
Chaos Orb
Re-rolls ALL mods on a rare item. The nuclear option — wipe everything and roll fresh. 66% chance of 4 mods, 25% chance of 5, 8% chance of 6.
Orb of Alteration
Re-rolls mods on a magic item (max 2 mods). Much cheaper than Chaos. Used when you need one or two specific mods and are willing to roll many times.
Orb of Augmentation
ADDS one mod to a magic item that only has one mod. Completes the pair before you use a Regal Orb.
Regal Orb
Upgrades a MAGIC item to RARE and adds one extra random mod. The bridge between the cheap Alteration path and rare item crafting.
Essence
Forces ONE guaranteed specific mod and fills remaining slots randomly. The only way to guarantee a particular stat while leaving others to chance.
The Weighted Lottery
Not all mods are equally likely to roll. Each mod is assigned a spawn weight — common mods have weight 1000, rare mods might have 100 or even 10. When an orb rolls, the engine draws from this weighted pool. Higher weight = higher probability.
Reading the Weighted Selection
Here is the inner loop of generate_mods() in
crafting_engine.py. Each iteration fills one mod slot using the
weighted random
approach described above.
for _ in range(num_mods):
can_prefix = prefix_count < 3 and mod_type in ("all", "prefix")
can_suffix = suffix_count < 3 and mod_type in ("all", "suffix")
ids: List[str] = []
weights: List[int] = []
if can_prefix:
p_ids, p_weights = self._select_eligible_mods(base_id, ilvl, "prefix", excluded_groups)
ids.extend(p_ids)
weights.extend(p_weights)
if can_suffix:
s_ids, s_weights = self._select_eligible_mods(base_id, ilvl, "suffix", excluded_groups)
ids.extend(s_ids)
weights.extend(s_weights)
if not ids:
break
total_weight = sum(weights)
probs = [w / total_weight for w in weights]
chosen_id = str(self.rng.choice(ids, p=probs))
mod_def = self.cache.mods[chosen_id]
chosen_type = mod_def["mod_type"]
result.append({"mod_id": chosen_id, "mod_type": chosen_type})
excluded_groups.add(mod_def["mod_group"])
- For each mod slot to fill (4, 5, or 6 total)...
- Check if prefix slots and suffix slots are still open (max 3 each)...
- Build two parallel lists: eligible mod IDs and their spawn weights...
- Get all eligible prefix mods and their weights...
- Get all eligible suffix mods and add them to the same pool...
- If no eligible mods remain at all, stop early...
- Sum all weights to get the denominator...
- Divide each weight by the total — now they are probabilities that add up to 1.0...
- numpy draws one mod ID using those probabilities...
- Add the chosen mod group to the exclusion set — prevents two life mods from rolling...
The Engine and the Arena
CraftingEngine and CraftingEnvironment look similar —
both deal with items and crafting. But they play very different roles.
The Engine is a stateless physics layer; the Environment is a stateful
episode
wrapper.
🎲 CraftingEngine (Pure Functions)
- • Takes an ItemState in, returns a NEW ItemState out
- • Never modifies anything — always creates fresh objects
- • No episode tracking, no step count, no memory
- • Same item + same RNG seed = same result every time
- • Safe to run 10,000 simulations in parallel — no shared mutable data
🏟️ CraftingEnvironment (Stateful Wrapper)
- • Tracks current_state, step_count, target_mods
- • Has reset() / step() / done lifecycle — like a board game
- • Strategies call env.step(action), never the engine directly
- • Each simulation episode gets its own Environment instance
apply_chaos_orb: a pure function in practice
def apply_chaos_orb(self, item_state: ItemState) -> ItemState:
if item_state.rarity != "rare":
raise ValueError(f"Chaos Orb requires a rare item, got '{item_state.rarity}'")
count = self._roll_mod_count()
new_mods = self.generate_mods(item_state.base_id, item_state.ilvl, count)
return self._build_item(item_state, new_mods, rarity="rare")
- Apply a Chaos Orb to an item. Takes the current item in, returns a new item out...
- Guard: Chaos Orbs only work on rare items — raise ValueError for anything else...
- Roll how many mods to generate: 4 (66%), 5 (25%), or 6 (8%)...
- Generate that many random mods using weighted selection from the pool...
- Return a BRAND NEW ItemState — the original item is never modified...
apply_chaos_orb does not modify the item in place — it returns a new one.
This is
immutability.
It means you can run a thousand parallel simulations starting from the same item
without any risk of one corrupting another. A
pure function
is the safest kind of function.
66% Fours, 25% Fives, 8% Sixes
Before rolling which mods appear, the engine first decides how many mods to roll. This is a discrete distribution with three possible outcomes. The weights come directly from the observed Path of Exile game data.
def _roll_mod_count(self) -> int:
"""Roll number of mods for a rare item: 4 (8/12), 5 (3/12), 6 (1/12)."""
return int(self.rng.choice([4, 5, 6], p=[8 / 12, 3 / 12, 1 / 12]))
- Roll how many mods a Chaos Orb will generate on the new item...
- Weighted choice: 4 mods 8 out of 12 times, 5 mods 3 out of 12, 6 mods 1 out of 12...
- Convert to int because numpy returns a numpy integer type...
This matches the actual Path of Exile probability distribution, reverse-engineered by the community from game observations.
Check Your Understanding
You want a guaranteed +1 to Level of Socketed Gems mod on your item with other mods randomized. Which operation does this?
Two runs of apply_chaos_orb with the same ItemState and the same RNG seed
produce what result?
You want to run 10,000 Chaos Orb simulations in parallel to estimate costs. What architectural property makes this safe?
Smart Patterns
The four reusable patterns behind POE Crafter — and how to ask AI for them by name
Pattern 1 — The Singleton
One Instance to Rule Them All
Imagine a city library with one central card catalog. Every librarian — no matter which desk they sit at — consults the same catalog. Nobody gets their own private copy. The DataCache is that catalog.
A
singleton
is a class where only ONE
instance
is ever created and shared by the entire application. The
module-level
cache = DataCache() at the bottom of loader.py is the textbook example. When the application starts, Python imports the module, runs that one line, and that single object is reused everywhere for as long as the server runs.
class DataCache:
"""
In-memory cache for POE game data.
Each instance holds independent state — no shared class-level mutable dicts.
Use the module-level singleton `cache` for normal application code.
Instantiate a fresh DataCache() in tests that need isolation.
"""
def __init__(self) -> None:
self.bases: Dict[str, Dict[str, Any]] = {}
self.mods: Dict[str, Dict[str, Any]] = {}
self._eligible_mods: Dict[str, Dict[int, List[Dict[str, Any]]]] = {}
self._mod_weights: Dict[str, Dict[int, Dict[str, int]]] = {}
self._initialized: bool = False
self._engine: Optional[Engine] = None
Define the DataCache class — the blueprint for the in-memory game data store...
The docstring is the contract: use the singleton in app code, fresh instances in tests...
When created, all data dictionaries start empty...
bases will hold all 3,200+ item base definitions...
mods will hold all 50,000+ modifier definitions...
The
_initialized
flag prevents reloading if initialize() is called more than once...
The _engine reference lets the cache make its own database queries later...
DataCache() instances per test.
Pattern 2 — The Strategy Pattern
Plug-In Behaviors with the Strategy Pattern
Think of a power drill with interchangeable bits. The drill (CraftingEnvironment) is the same every time. The bit (the strategy) is swappable. You can design a new bit without touching the drill at all. Python's
ABC
module lets you define a
contract
— any class that wants to be a strategy MUST implement select_action. The environment calls that method without caring which strategy it holds.
Keep applying Chaos Orbs until the target mods appear. Brute force, expensive, simple.
Use cheaper Alteration plus Regal when targeting one or two specific mods. More efficient for simple targets.
Use Essences to lock in one guaranteed mod each roll. Reduces variance when one mod is critical.
Watch how the environment and a strategy talk to each other during an episode:
Pattern 3 — The Gym Contract
Reset, Step, Done — The Universal Game Loop
CraftingEnvironment mimics the
OpenAI Gym
interface — a standard contract used in machine learning for training
AI agents.
reset() starts an
episode,
step() applies one action, and done is True when the target is met or time runs out. Any
RL
library can plug into this loop without knowing anything about Path of Exile.
Start a new episode with a blank item and zero step count. Every simulation run begins here.
Apply one orb to the current item. Returns the updated item, the cost of 1 orb, and whether the episode has ended.
Did all target mod IDs appear on the item? If yes, the episode succeeds and stops immediately.
def step(self, action: CraftingAction) -> Tuple[ItemState, int, bool]:
if self._state is None:
raise RuntimeError("Call reset() before step()")
self._state = self._apply(action, self._state)
self._step_count += 1
done = self.is_target_met() or self._step_count >= self.max_steps
return self._state, 1, done
Apply one crafting action to the current item. Returns 3 values as a tuple return: new state, cost, and done flag...
Guard: step() cannot run before reset() has been called — _state would be None and nothing makes sense...
Apply the action using the engine — get a new ItemState back. The old state is discarded...
Increment the step counter — one orb has been used...
Episode is done if target mods are all present OR we hit the step limit (default 1000)...
Return the new state, a cost of 1 orb, and whether the episode ended...
What Is Built and What Comes Next
The Roadmap
POE Crafter is production-ready for parsing and validation. The simulation layer has the architecture in place but needs the strategy logic filled in.
✅ Working Today
load_poe_data()
Loads all game data from RePoE JSON into SQLite
DataCache
In-memory store for all bases and mods — instant lookups
parse_item()
Dual-format parser — accepts in-game and PoB text
ValidationEngine
All 4 rules: affix limit, mod group, tag requirement, item level
CraftingEngine
All 5 orb operations with correct weighted random selection
CraftingEnvironment
Full gym-style episode contract: reset / step / done
🔲 Coming Next
ChaosSpamStrategy
Stub
— select_action raises
NotImplementedError
MonteCarloSimulator
Stub — needs to run N episodes and aggregate statistics
/simulate endpoint
Returns pass today — needs
Monte Carlo
backend
/recommend endpoint
Returns pass today — needs strategy comparison logic
The Patterns You Now Know
A Language for Talking to AI
You have now seen four patterns that appear in production software everywhere. Knowing their names means you can describe exactly what you want to an AI coding assistant — instead of hoping it guesses the right structure.
Singleton
One shared instance. Tell AI: "the DataCache should be a module-level singleton — create once at import time, never create per-request."
Strategy Pattern (ABC)
Pluggable behaviors with a defined contract. Tell AI: "add a new strategy class that implements the CraftingStrategy ABC — just the select_action method."
Pure Functions
No side effects, same inputs always give the same outputs. Tell AI: "keep all engine methods pure — each takes state in and returns a new state, never modifies in place."
ETL Pipeline
Extract, Transform, Load. Tell AI: "write an ETL pipeline that reads from X, filters and cleans the data, and bulk-inserts into Y in a single transaction."
These are not advanced concepts — they are the vocabulary of working software. Every codebase you encounter will use variations of these four patterns. Now you can name them.