All skills
thedivergentai avatar

/godot-monte-carlo-balancer

@9d6e91e

Use when auditing or recalibrating game balance: build a source-driven Monte Carlo balance lab (Rust + rayon) that extracts live game data, simulates human playstyles (AFK→pro), emits win-rate/economy verdicts with confidence intervals, and bruteforce-tunes parameters. Trigger on unfair levels, unreachable shops, farm exploits, interest-curve cliffs, post-content recalibration, or CI balance JSON diffs. Keywords: balance lab, Monte Carlo, win rate, difficulty curve, economy career, playstyle simulation, Resource extraction, GDScript parser, bruteforce tuning.

Use this Skill: https://skilld.dev/gh/thedivergentai/gd-agentic-skills/godot-monte-carlo-balancer

This session only. Nothing lands on disk.

references01-source-extraction.md

≈1.9k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Phase 1 — Source Extraction Layer (extract.rs)

Goal: Turn live game source into typed GameData at every startup. No hand-copied config. When a designer edits a Resource or coefficient, the next run uses the new values.

Decision tree (Resource-first)

1. Does balance data live in .tres / Resource / CSV→.tres?
   YES → deserialize (preferred). Skip regex for those fields.
2. Does it live in GDScript const / @export on a Resource script?
   YES → parse typed assignments; still prefer loading .tres instances.
3. Are coefficients embedded only in gameplay formulas?
   YES → targeted regex for coefficients; reimplement formula once in sim.
4. Still no data layer?
   STOP → apply godot-resource-data-patterns (+ economy/combat Resources), then return to 1.

Anti-pattern: Building a regex farm for a project that already has WeaponProfile / EnemyStats Resources. That fights the GDSkills data culture and rot-proofing.

Architecture

pub fn extract_game_data(root: &Path) -> Result<GameData> {
    Ok(GameData {
        weapons: extract_weapons(root)?,
        enemies: extract_enemies(root)?,
        traps: extract_traps(root)?,
        levels: extract_levels(root)?,
        modes: extract_modes(root)?,
        constants: extract_constants(root)?, // engine constants + embedded formulas
    })
}

One pure extractor per Phase 0 content class: &Path -> Result<T>. Assemble into GameData. No on-disk GameData cache between runs.

Preferred path — .tres / Resources

Godot .tres is INI-like (sections + key = value). Parse sections into typed structs, or:

  1. Add a tiny Godot --script / editor tool that dumps Resources to JSON (stable field names).
  2. Have Rust serde_json load that dump via balance-lab extract / startup.

Prefer JSON dump when Resource graphs are deep (nested subresources, PackedArrays). Keep the dump generated at extract time, never hand-edited.

CSV→.tres pipelines (godot-genre-simulation) are ideal: extract from the same CSV designers already balance, or from baked .tres.

Core helpers (reuse these patterns)

fn read(root: &Path, rel: &str) -> Result<String>            // read a game file, error with path context
fn capture_f64(src: &str, pattern: &str, default: f64) -> f64 // single-value regex capture
fn capture_i32(src: &str, pattern: &str, default: i32) -> i32
fn get_s / get_i / get_f64(fields, key, default)              // typed access into a parsed block

Rule: every default that actually gets used must be surfaced in inspect output (e.g. suffix the value with (default!)). Silent defaults are how balancers lie.

Fallback — GDScript constants / factory blocks

Use only when Phase 0 marked no Resource truth.

Pattern 1 — Factory/profile blocks

Godot data-driven games typically define content as static factory functions assigning to a profile object:

static func create_stapler() -> WeaponProfile:
    var profile := WeaponProfile.new()
    profile.damage = 12
    profile.fire_interval = 0.45
    profile.magazine = 30
    return profile

Extract with a header regex per class plus a generic key-value regex:

fn factory_blocks(src: &str, class_name: &str) -> Vec<(String, HashMap<String, String>)> {
    // header: static func create_([a-z_0-9]+) ... -> ClassName
    // fields: (?m)^\s*profile\.([a-z_]+)\s*=\s*(.+?)\s*$
}

This makes the parser resilient to new fields and new instances: adding a weapon or property in GDScript requires zero parser changes unless a new shape of definition appears.

Pattern 2 — Constants and typed vars

let const_re = Regex::new(r"(?m)^const (\w+):\s*float\s*=\s*([\d.]+)").unwrap();
let var_re   = Regex::new(r"^var (\w+)(?::\s*[\w.]+)?\s*=\s*(.+)$").unwrap();
let array_re = Regex::new(r"TRAP_SLOT_UNLOCK_LEVELS: Array\[int\] = \[([\d,\s]+)\]").unwrap();

Resolve symbolic references: parse scalars that may be numbers, bools, or names of previously-captured constants (parse_scalar(raw, &constants)).

Pattern 3 — Embedded formulas (critical!)

Difficulty and reward curves usually live inline in gameplay code, not in data:

var hp_scale := 1.0 + (0.08 + 0.03 * _definition.id) * wave
cash = 40 + level_data.id * 15

Extract the coefficients with formula-shaped regexes and store them as named constants:

let hp_re   = Regex::new(r"1\.0 \+ \(([\d.]+) \+ ([\d.]+) \* _definition\.id\)").unwrap();
let cash_re = Regex::new(r"cash = (\d+) \+ level_data\.id \* (\d+)").unwrap();

Then reimplement the formula once in model.rs/sim.rs using the extracted coefficients. If the game formula changes shape (not just coefficients), the regex fails → the tool errors loudly → you update both together. That is the correct failure mode.

Pattern 4 — Structured code sections (modes, level definitions)

For match-based rule blocks:

let kind_re = Regex::new(r"^\s*Kind\.(\w+):\s*$").unwrap();   // section header
let rule_re = Regex::new(r"^\s*rules\.(\w+)\s*=\s*(.+)$").unwrap(); // assignments inside

Walk lines, track the current section, apply assignments via a single apply_mode(&mut mode, name, value) dispatcher so new rule fields need one match arm, not a new parser.

For level definitions built in code (var lvl := _base_level(3, "Server Room", "...") followed by lvl.spawn_interval = 1.2), capture the constructor call and then generic ident.field = number assignments, routed through set_level_field(&mut level, name, raw).

Project root discovery

pub fn find_project_root() -> Result<PathBuf>
  • Walk up from the executable/cwd looking for the engine marker (project.godot, *.uproject, package.json, …).
  • Support an env override (BALANCE_LAB_PROJECT_ROOT) for running a copied binary outside the tree.
  • Error with a clear message listing what was searched.

inspect (mandatory)

Pretty-print everything extracted: every profile with every field, every constant, every formula coefficient, every mode's effective rules (GameMode::describe()), every level's curve values, and flags any defaulted field. Also provide --json (GameData::inspect_json()).

Workflow rule: after writing or modifying any extractor, run inspect and diff it against the source by eye (or have the agent verify field-by-field) before running a single simulation.

Honesty over time

  • Smoke test: non-empty catalogs + a few stable invariants (damage > 0, …).
  • On game refactor parse failure: fix extractor first, re-inspect, then resume balancing.
  • Never cache GameData across game-source edits.

Non-Godot / other engines

Prefer real deserialization (JSON/YAML/CSV) when available. Regex only for values embedded in code. See 06-genre-adaptation.md.

<!-- GDSkills research links (agents) Official docs: - https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html - https://docs.godotengine.org/en/stable/classes/class_json.html - https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html Related skills: - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-resource-data-patterns/SKILL.md — Resource-first extract - https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-testing-patterns/SKILL.md — Phase 7 headless calibration Parent skill: https://github.com/thedivergentai/gd-agentic-skills/blob/main/skills/godot-monte-carlo-balancer/SKILL.md -->

Source: SKILL.md on GitHub

No alerts1mo3 checks · Risk SAFE
  • Gen Agent Trust Hub1mo

    This skill provides a robust framework and Rust-based templates for creating Monte Carlo game balance simulations in Godot 4.7. It includes automated build scripts, source-driven data extraction patterns, and statistical analysis tools. Security analysis confirms the skill follows safe practices, utilizing trusted external resources and standard development workflows.

  • Socket1mo

    No alerts

  • Snyk1mo

    Risk: LOW · No issues

Signed by skilld at 9d6e91e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 2 months ago

README badge

README badge for thedivergentai/gd-agentic-skills/godot-monte-carlo-balancer