Scenario

The root type. A single .toml file maps to one Scenario. All fields are required unless marked optional.

pub struct Scenario {
    pub meta:               ScenarioMeta,
    pub map:                MapConfig,
    pub factions:           Vec<Faction>,
    pub technology:         Vec<TechCard>,
    pub political_climate:  PoliticalClimate,
    pub events:             Vec<EventDefinition>,
    pub simulation:         SimulationConfig,
    pub victory_conditions: Vec<VictoryCondition>,
}

pub struct ScenarioMeta {
    pub name:        String,
    pub description: String,
    pub author:      String,
    pub version:     String,
    /// Freeform tags: "us", "institutional", "2027", "asymmetric"
    pub tags:        Vec<String>,
}

SimulationConfig

pub struct SimulationConfig {
    /// Maximum number of ticks before the simulation is declared inconclusive.
    pub max_ticks:     u32,
    /// RNG seed. Omit to use a random seed (stored in output for reproducibility).
    pub seed:          Option<u64>,
    /// Number of Monte Carlo runs (CLI default: 1000, WASM default: 100).
    pub runs:          u32,
    /// Time resolution: how many real-world hours each tick represents.
    pub hours_per_tick: f64,
}

TOML Example

[meta]
name        = "Tutorial: Symmetric Forces"
description = "Two equal forces on flat terrain. Baseline reference scenario."
author      = "AndrewAltimit"
version     = "1.0.0"
tags        = ["tutorial", "symmetric"]

[simulation]
max_ticks    = 720   # 30 days at 1 hour/tick
seed         = 1337
runs         = 1000
hours_per_tick = 1.0

Faction

Represents one belligerent actor. A scenario must have at least two factions.

pub struct Faction {
    pub id:             FactionId,      // unique string slug
    pub name:           String,
    pub faction_type:   FactionType,
    pub forces:         Vec<ForceUnit>,
    pub objectives:     Vec<Objective>,
    pub tech_cards:     Vec<TechCardId>, // references TechCard.id
    pub supply_sources: Vec<RegionId>,
    pub morale:         f64,            // 0.0–1.0
    pub cohesion:       f64,            // 0.0–1.0
    pub ai_profile:     AiProfile,
}

pub enum FactionType {
    ConventionalMilitary,
    Paramilitary,
    Insurgent,
    StateInstitution, // law enforcement, national guard
    ExternalActor,    // foreign state support
}

pub struct ForceUnit {
    pub id:           UnitId,
    pub name:         String,
    pub unit_type:    UnitType,
    pub strength:     f64,      // abstract combat power (1.0 = baseline company)
    pub quality:      f64,      // training/equipment multiplier (0.5–2.0)
    pub location:     RegionId,
    pub supply_level: f64,      // 0.0–1.0
}

TOML Example

[[factions]]
id           = "blue_force"
name         = "Federal Forces"
faction_type = "ConventionalMilitary"
morale       = 0.75
cohesion     = 0.80
tech_cards   = ["drone_swarm", "sigint"]
supply_sources = ["region_dc", "region_norfolk"]

[[factions.forces]]
id         = "1st_brigade"
name       = "1st Brigade Combat Team"
unit_type  = "Infantry"
strength   = 3.0
quality    = 1.2
location   = "region_northern_virginia"
supply_level = 1.0

TechCard

A named bundle of statistical modifiers. Assigned to factions via faction.tech_cards. Effects stack multiplicatively when multiple cards apply to the same parameter.

pub struct TechCard {
    pub id:          TechCardId,
    pub name:        String,
    pub description: String,
    pub effects:     Vec<TechEffect>,
    /// Card IDs this card partially or fully counters.
    pub counters:    Vec<CounterSpec>,
}

pub struct TechEffect {
    /// Which simulation parameter this effect modifies.
    pub parameter:    TechParameter,
    /// Multiplicative modifier sampled each application.
    /// Expressed as a statistical distribution.
    pub multiplier:   EffectDistribution,
    /// Which phase(s) this effect applies in (None = all phases).
    pub phases:       Option<Vec<TickPhase>>,
}

pub enum EffectDistribution {
    /// Fixed scalar (no variance).
    Fixed(f64),
    /// Normal distribution: mean, std_dev.
    Normal { mean: f64, std_dev: f64 },
    /// Uniform distribution: low, high.
    Uniform { low: f64, high: f64 },
    /// Beta distribution (bounded 0–1, then scaled).
    Beta { alpha: f64, beta: f64, scale: f64 },
}

TOML Example

[[technology]]
id          = "drone_swarm"
name        = "Drone Swarm (Gen 3)"
description = "Autonomous aerial ISR and strike drones operating in coordinated mesh."

[[technology.effects]]
parameter  = "IsrVisibility"
multiplier = { Normal = { mean = 1.35, std_dev = 0.10 } }

[[technology.effects]]
parameter  = "AirAttackModifier"
multiplier = { Normal = { mean = 1.25, std_dev = 0.15 } }
phases     = ["Combat"]

[[technology.counters]]
card       = "c_uas"
reduces_to = { Uniform = { low = 0.15, high = 0.45 } }

PoliticalClimate

Defines the initial political environment and per-faction institutional relationships. Drives defection probability and loyalty shift events in the political tick phase.

pub struct PoliticalClimate {
    /// Global instability index (0.0 = stable, 1.0 = maximal chaos).
    pub instability:           f64,
    /// Per-faction institutional loyalty ratings.
    pub loyalties:             Vec<InstitutionalLoyalty>,
    /// Population sentiment per region.
    pub population_sentiment:  Vec<RegionSentiment>,
    /// External actor influence per faction.
    pub external_support:      Vec<ExternalSupport>,
}

pub struct InstitutionalLoyalty {
    pub faction_id:       FactionId,
    /// Which institutions currently back this faction (military branches,
    /// law enforcement, intelligence community, judiciary, etc.).
    pub institutions:     Vec<InstitutionLoyaltyEntry>,
    /// Base defection probability per tick (modified by instability and events).
    pub defection_rate:   f64,
}

EventDefinition

Conditional events that fire when triggers are satisfied. Events can have immediate effects, modify scenario parameters, or enqueue chained events.

pub struct EventDefinition {
    pub id:          EventId,
    pub name:        String,
    pub description: String,
    pub trigger:     EventTrigger,
    pub effects:     Vec<EventEffect>,
    /// Probability the event fires when the trigger is satisfied (default 1.0).
    pub probability: f64,
    /// Whether the event can fire more than once.
    pub repeatable:  bool,
}

pub enum EventTrigger {
    /// Fires at a specific tick number.
    Tick(u32),
    /// Fires when a faction's control of a region reaches a threshold.
    RegionControl { region: RegionId, faction: FactionId, threshold: f64 },
    /// Fires when a faction's morale drops below a threshold.
    MoraleBelow { faction: FactionId, threshold: f64 },
    /// Fires when an infrastructure node drops below an operational threshold.
    InfrastructureDegraded { node: InfraId, below: f64 },
    /// Compound trigger: all sub-triggers must be true.
    All(Vec<EventTrigger>),
    /// Compound trigger: any sub-trigger must be true.
    Any(Vec<EventTrigger>),
}

VictoryCondition

Terminal conditions that end the simulation. The first condition satisfied in a run determines that run's outcome. Multiple conditions allow modeling negotiated settlements, stalemates, and unconditional wins.

pub struct VictoryCondition {
    pub id:          VictoryId,
    pub name:        String,
    pub winner:      Option<FactionId>, // None = inconclusive / stalemate
    pub condition:   VictoryCheck,
}

pub enum VictoryCheck {
    /// Faction controls at least `min_fraction` of total strategic value.
    StrategicControl { faction: FactionId, min_fraction: f64 },
    /// Opposing faction's combined strength drops below threshold.
    ForceElimination { faction: FactionId, below: f64 },
    /// A specific region is held continuously for N ticks.
    RegionHeld { region: RegionId, faction: FactionId, ticks: u32 },
    /// All of the sub-conditions are met.
    All(Vec<VictoryCheck>),
    /// Any of the sub-conditions are met.
    Any(Vec<VictoryCheck>),
}

CLI Reference

The faultline-cli crate builds a headless binary for batch Monte Carlo evaluation. All flags are available via faultline --help.

Subcommands

SubcommandPurpose
runExecute Monte Carlo simulation batch and write results
validateParse and validate a scenario file without running
inspectPretty-print a parsed scenario as JSON for debugging

faultline run

faultline run [OPTIONS] --scenario <PATH>

OPTIONS:
  -s, --scenario <PATH>     Path to the scenario TOML file [required]
  -r, --runs <N>            Number of Monte Carlo runs [default: 1000]
      --seed <SEED>         Fixed RNG seed for deterministic output
  -o, --output <PATH>       Output file path [default: stdout]
      --format <FMT>        Output format: json | csv [default: json]
      --threads <N>         Parallel worker threads [default: num_cpus]
      --verbose             Emit per-tick trace logs (very noisy)
  -h, --help                Print help

faultline validate

faultline validate --scenario <PATH>

OPTIONS:
  -s, --scenario <PATH>     Path to the scenario TOML file [required]
      --strict              Treat warnings as errors
  -h, --help                Print help

Output Format (JSON)

{
  "scenario":    "Tutorial: Symmetric Forces",
  "runs":        1000,
  "seed":        1337,
  "outcomes": [
    {
      "condition_id":  "blue_wins",
      "probability":   0.612,
      "ci_95":         [0.581, 0.643],
      "median_ticks":  218,
      "p10_ticks":     140,
      "p90_ticks":     380
    }
  ],
  "sensitivity": [
    { "parameter": "blue_force.morale",  "spearman_rho": 0.41 },
    { "parameter": "tech.drone_swarm",   "spearman_rho": 0.28 }
  ]
}

WASM API

The faultline-backend-wasm crate exposes a JavaScript API via wasm-bindgen. All functions are async-compatible and work in a browser Worker context for non-blocking execution.

load_scenario

/**
 * Parse and validate a TOML scenario string.
 * Returns a ScenarioHandle (opaque token) on success.
 * Throws a string error message on parse or validation failure.
 */
async function load_scenario(toml_string: string): Promise<ScenarioHandle>

validate

/**
 * Validate a TOML scenario string without creating a handle.
 * Returns a ValidationResult with ok: bool and errors: string[].
 */
function validate(toml_string: string): ValidationResult

interface ValidationResult {
  ok:      boolean;
  errors:  string[];
  warnings: string[];
}

run_single

/**
 * Execute a single simulation run on the given handle.
 * Returns a full tick-by-tick snapshot array for replay/visualization.
 * seed is optional; omit for random.
 */
async function run_single(
  handle: ScenarioHandle,
  seed?: bigint
): Promise<RunResult>

interface RunResult {
  seed:       bigint;
  ticks:      TickSnapshot[];
  outcome:    string | null;  // winning condition id, or null if inconclusive
  ticks_elapsed: number;
}

run_batch

/**
 * Run N simulations and return aggregated statistics.
 * Progress callbacks fire after each completed run.
 */
async function run_batch(
  handle:   ScenarioHandle,
  runs:     number,
  on_progress?: (completed: number, total: number) => void
): Promise<BatchResult>

interface BatchResult {
  runs:       number;
  outcomes:   OutcomeStats[];
  sensitivity: SensitivityEntry[];
}

Usage Example

import init, { load_scenario, run_batch } from './faultline_wasm.js';

await init();

const toml = await fetch('scenarios/tutorial_symmetric.toml').then(r => r.text());
const handle = await load_scenario(toml);

const results = await run_batch(handle, 500, (done, total) => {
  console.log(`${done}/${total} runs complete`);
});

console.log(results.outcomes);

Simulator UI

The browser app at /app.html exposes the same scenarios and Monte Carlo runner as the CLI, plus an analyst workflow for capturing and comparing results without re-loading TOML.

Pin Results

After a Monte Carlo batch finishes, click Pin next to Run MC to save the result. Pins persist in localStorage (key faultline:pinned-mc:v1) so they survive page reloads. The store caps at 8 pins and drops the oldest when the cap is reached or when the browser quota is hit.

Each pin captures the scenario name, the TOML at the time of pinning, and the trimmed Monte Carlo summary (faction win rates, Wilson CIs, kill-chain feasibility, mean duration). Per-run snapshot arrays are stripped before persistence to stay under the storage quota.

Side-by-side Comparison

Click Set A on one pin and Set B on another, then Compare A vs B. The panel renders deltas computed exactly the same way as --counterfactual / --compare on the CLI — variant minus baseline, with factions or chains present on only one side defaulting the missing side to zero so the asymmetry is surfaced rather than silently dropped.

Surfaced metrics:

  • Per-faction win-rate deltas with both sides' 95% Wilson CIs.
  • Per-kill-chain success and detection rate deltas.
  • Per-kill-chain cost-asymmetry ratio deltas.
  • Mean campaign duration delta.

If the two pinned batches were run with different total_runs, a banner warns that the deltas mix scenario and sampling variance — the analyst should re-pin both at the same run count for a clean comparison.

Scenario Diff Viewer

Click Diff next to Validate in the editor to open a unified-diff modal. The dropdown picks the baseline:

  • The last loaded preset / import — useful for "what have I changed since the scenario was loaded?"
  • Any pinned scenario — useful for "what's different between this run and the one I pinned earlier?"

The diff is line-oriented with three lines of context, computed via an LCS table walk. Adjacent changes merge into a single hunk; distant changes split into separate hunks with their own @@ -a,b +c,d @@ headers. Plain HTML escaping protects against TOML content that happens to contain markup.

CI/CD Pipeline

All CI runs inside Docker containers via docker compose --profile ci on a self-hosted runner.

Pipeline Stages

StageCommandPurpose
1. Formatcargo fmt --all -- --checkConsistent code style (100-char width)
2. Lintcargo clippy --all-targets -- -D warningsStatic analysis (warnings = errors)
3. Testcargo testUnit, integration, and property tests
4. Buildcargo build --releaseRelease binary compilation
5. Auditcargo deny checkLicense and advisory checking
6. JS Testsnode --test tests/integration/*.test.mjsPure-logic frontend modules (pinned store, comparison deltas, diff renderer, share roundtrip, heatmap)

Workflows

  • main-ci.yml — Runs on main push and tags. Full format, lint, test, build, cargo-deny pipeline plus WASM build via wasm-pack and GitHub Pages deployment. Auto-creates GitHub issues on failure.
  • pr-validation.yml — Runs on pull requests. CI stages + Claude Code AI review (security + quality profiles) + OpenRouter / Qwen 3.5 general review + automated agent fix iterations (max 5, extendable with [CONTINUE] comment). Add no-auto-fix label to disable automated fixes.

Advisory Exemptions

One advisory is currently exempted in deny.toml:

  • RUSTSEC-2026-0097 — rand 0.8 unsound only when a custom logger calls rand::rng() and ThreadRng reseeds inside that logger. Faultline uses tracing (not log) and never calls rand from a logging context; upgrading to rand 0.9+ requires coordinated updates across rand_chacha, rand_distr, statrs, and nalgebra and is planned for a future release.

GitHub README

The canonical project documentation lives alongside the source code on GitHub. The README includes full build instructions, scenario authoring guides, and the CHANGELOG.

View on GitHub Example Scenarios