quarkshologridledger/reference/ENGINE.md

39 KiB
Raw Blame History

Simulation Engine Architecture: Quark's Holo-Grid Ledger

Status: Design Analysis — Pre-Implementation
Author: Engine Architecture Team
Dependencies: data-source/taxonomy.yaml, Go 1.22+, Valkey 8.x


1. Engine Overview

The simulation engine is a Go-based daemon that reads the static taxonomy, generates live match events on a rolling schedule, simulates tick-by-tick combat, calculates real-time probabilities for all market types, and pushes continuous state updates into Valkey for the TanStack Start frontend to consume.

┌─────────────────────────────────────────────────────────────┐
│                    SIMULATION ENGINE                        │
│                                                             │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌───────────┐  │
│  │SCHEDULER │→ │MATCHMAKER│→ │SIMULATOR │→ │PROBABILITY│  │
│  │          │  │          │  │  (tick)   │  │  ENGINE   │  │
│  └──────────┘  └──────────┘  └────┬─────┘  └─────┬─────┘  │
│                                   │               │        │
│                              ┌────▼─────┐   ┌────▼─────┐  │
│                              │  EVENT   │   │  MARKET   │  │
│                              │GENERATOR │   │  UPDATER  │  │
│                              └──────────┘   └────┬─────┘  │
│                                                  │        │
│                                            ┌─────▼──────┐ │
│                                            │   VALKEY   │ │
│                                            │   WRITER   │ │
│                                            └────────────┘ │
└─────────────────────────────────────────────────────────────┘

Engine Modules

Module Responsibility Frequency
Scheduler Manages match lifecycle: create, run, resolve, retire Every 30s check
Matchmaker Pairs factions, picks venues, generates initial markets On match creation
Simulator Runs one combat tick per active match Every 2s per match
Event Generator Rolls for random in-match events Every tick
Probability Engine Recalculates all market odds from current state Every tick
Market Updater Applies probability → price conversion + margin Every tick
Valkey Writer Serializes & pushes atomic state updates to cache Every tick

2. Match Lifecycle

Every match (sector engagement) follows a deterministic state machine:

    SCHEDULED ──────► PRE-MATCH ──────► LIVE ──────► RESOLVING ──────► SETTLED
       │                  │               │              │                │
       │ T-60s before     │ Markets open  │ Combat ticks │ Winner found   │ Markets paid
       │ match start      │ initial odds  │ odds moving  │ final odds     │ match archived
       │                  │ set from      │ events fire  │ locked         │
       │                  │ faction stats │              │                │
       ▼                  ▼               ▼              ▼                ▼
    Announced to       Betslip accepts  Live telemetry  3-tick cooldown  Removed from
    frontend index     wagers           streaming       no new bets      active index

Lifecycle Timing

Phase Duration Markets Description
SCHEDULED 3090s Closed Match announced, factions & venue visible, no betting
PRE_MATCH 60s Open (pre-match odds) Initial odds calculated, pre-match wagers accepted
LIVE 60150 ticks (25 min) Open (live odds) Active combat simulation, odds shift each tick
RESOLVING 3 ticks (6s) Suspended Winner determined, final state locked, dramatic pause
SETTLED 10s Closed (final) Results committed, markets graded, match archived

Concurrent Match Capacity

The engine should maintain a rolling window of active matches:

const (
    TargetActiveMatches  = 8    // Aim for 8 concurrent live matches
    MaxActiveMatches     = 12   // Hard cap
    MinActiveMatches     = 4    // Trigger new match creation if below
    MatchCreationCooldown = 15  // Seconds between new match spawns
)

The scheduler continuously checks: if active < MinActiveMatches, create new matches until TargetActiveMatches is reached. This guarantees the frontend always has events to display across all quadrants.


3. Matchmaker — Pairing Algorithm

The matchmaker selects two factions and a venue for each new engagement.

3.1 Faction Pairing Rules

┌─────────────────────────────────────────────────┐
│             PAIRING PROBABILITY WEIGHTS          │
├─────────────────────────────────────────────────┤
│  Same-quadrant matchup        │ 60% chance      │
│  Cross-quadrant matchup       │ 30% chance      │
│  Cross-quadrant (distant)     │ 10% chance      │
│    (Alpha↔Delta, Beta↔Gamma)  │                 │
└─────────────────────────────────────────────────┘

Constraints:

  • A faction cannot appear in more than 2 simultaneous active matches
  • Repeat matchups (same two factions) require a 3-match cooldown
  • Rivalry matchups (high rivalry_modifier) get a 2x selection weight boost — they're marquee events

3.2 Venue Selection

func SelectVenue(home, away FactionDef, taxonomy Taxonomy) SectorPoolItem {
    // 1. Prefer venues in the home faction's quadrant (60%)
    // 2. Neutral venues from either faction's quadrant (30%)  
    // 3. Random venue from any quadrant (10%)
    
    // Weight boost for thematic venues:
    // - If home faction is Klingon and venue is "qonos-arena" → 3x weight
    // - If matchup has rivalry_modifier > 1.20 → prefer "high-profile" tagged venues
    
    // Environmental modifiers from the venue apply to the entire match
}

3.3 Initial Odds Calculation

Pre-match odds derive from faction combat stats + venue modifiers + rivalry premium:

func CalculatePreMatchOdds(home, away FactionDef, venue SectorPoolItem) float64 {
    // 1. Calculate effective power ratings
    homePower := (float64(home.BaseShield) * venue.ShieldModifier * 0.4) +
                 (float64(home.BaseAttack) * venue.DamageModifier * 0.4) +
                 (float64(home.BaseEvasion) * venue.EvasionModifier * 0.2)
    
    awayPower := (float64(away.BaseShield) * venue.ShieldModifier * 0.4) +
                 (float64(away.BaseAttack) * venue.DamageModifier * 0.4) +
                 (float64(away.BaseEvasion) * venue.EvasionModifier * 0.2)
    
    // 2. Convert power differential to probability via logistic function
    diff := homePower - awayPower
    homeProb := 1.0 / (1.0 + math.Exp(-diff / ScalingFactor))
    
    // 3. Apply rivalry modifier (inflates drama, widens spread)
    rivalryMod := home.RivalryModifiers[away.ID]  // e.g., 1.35
    // Rivalry pushes odds away from 50/50 — makes favorites more favored
    
    // 4. Convert probability to decimal odds
    homeOdds := 1.0 / homeProb
    awayOdds := 1.0 / (1.0 - homeProb)
    
    // 5. Apply house margin (overround)
    // Target: 105-108% overround (realistic sportsbook margin)
    margin := 1.06
    homeOdds = homeOdds / margin
    awayOdds = awayOdds / margin
    
    return homeOdds // (and awayOdds)
}

4. Combat Simulator — The Tick Loop

Every 2 seconds, each active match processes one combat tick.

4.1 Tick Sequence

FOR each active match:
  │
  ├─ 1. RESOLVE ACTIVE EFFECTS
  │     └─ Process lingering buffs/debuffs from prior events
  │
  ├─ 2. CALCULATE EFFECTIVE STATS
  │     ├─ Base stats × venue modifiers × active effects
  │     └─ Factor in type advantages (aggressive vs defensive, etc.)
  │
  ├─ 3. COMBAT EXCHANGE
  │     ├─ Both factions attack simultaneously
  │     ├─ Roll hit/miss for each (attack_power vs opponent evasion)
  │     ├─ Calculate damage dealt (if hit)
  │     ├─ Apply damage to opponent shields
  │     └─ Accumulate kinetic_yield (total energy released)
  │
  ├─ 4. EVENT ROLL
  │     ├─ Roll against venue.special_event_chance
  │     ├─ Roll against faction-specific event triggers
  │     └─ If event fires → apply effects, log to event stream
  │
  ├─ 5. CHECK WIN CONDITIONS
  │     ├─ Shield collapse (one faction hits 0%)
  │     ├─ Mutual destruction (both hit 0% same tick)
  │     └─ Time limit (max ticks reached → highest shields wins)
  │
  ├─ 6. RECALCULATE ALL MARKET PROBABILITIES
  │     └─ Feed current state to probability engine
  │
  └─ 7. PUSH TO VALKEY
        └─ Atomic JSON state update

4.2 Combat Resolution Math

type TickResult struct {
    HomeDamageDealt   float64
    AwayDamageDealt   float64
    HomeShieldsAfter  float64
    AwayShieldsAfter  float64
    KineticYieldDelta float64
    EventFired        *MatchEvent // nil if no event
}

func ResolveCombatTick(match *LiveMatch) TickResult {
    home := &match.Factions.Home
    away := &match.Factions.Away
    venue := match.Venue
    
    // Effective stats = base × venue modifier × active effects
    homeAttack := float64(home.EffectiveAttack) * venue.DamageModifier
    awayAttack := float64(away.EffectiveAttack) * venue.DamageModifier
    homeEvasion := float64(home.EffectiveEvasion) * venue.EvasionModifier
    awayEvasion := float64(away.EffectiveEvasion) * venue.EvasionModifier
    
    // Hit chance = attacker_power / (attacker_power + defender_evasion)
    // This creates a natural 0-1 probability range
    homeHitChance := homeAttack / (homeAttack + awayEvasion)
    awayHitChance := awayAttack / (awayAttack + homeEvasion)
    
    // Roll for hits
    var homeDmg, awayDmg float64
    if rand.Float64() < homeHitChance {
        // Home hits away — damage = base * random(0.6, 1.4) variance
        homeDmg = homeAttack * (0.6 + rand.Float64()*0.8) * 0.1 // Scale to shield %
    }
    if rand.Float64() < awayHitChance {
        awayDmg = awayAttack * (0.6 + rand.Float64()*0.8) * 0.1
    }
    
    // Apply type advantage multipliers
    homeDmg *= TypeAdvantage(home.Type, away.Type)
    awayDmg *= TypeAdvantage(away.Type, home.Type)
    
    // Apply damage to shields (clamped to 0-100)
    away.ShieldCapacity = clamp(away.ShieldCapacity - homeDmg, 0, 100)
    home.ShieldCapacity = clamp(home.ShieldCapacity - awayDmg, 0, 100)
    
    // Accumulate kinetic yield (total energy released = total damage dealt)
    kineticDelta := homeDmg + awayDmg
    match.KineticYield += kineticDelta
    
    return TickResult{
        HomeDamageDealt:   homeDmg,
        AwayDamageDealt:   awayDmg,
        HomeShieldsAfter:  home.ShieldCapacity,
        AwayShieldsAfter:  away.ShieldCapacity,
        KineticYieldDelta: kineticDelta,
    }
}

4.3 Type Advantage Matrix

Combat types create a rock-paper-scissors dynamic:

              DEFENDER
              aggressive  defensive  tactical  infiltrator  swarm
ATTACKER  ┌──────────────────────────────────────────────────────┐
aggressive│    1.00       0.80       1.20       1.30        0.90 │
defensive │    1.20       1.00       0.90       0.80        1.10 │
tactical  │    0.80       1.10       1.00       1.20        1.00 │
infiltratr│    0.70       1.20       0.80       1.00        1.30 │
swarm     │    1.10       0.90       1.00       0.70        1.00 │
          └──────────────────────────────────────────────────────┘

Key interactions:
  aggressive → infiltrator: 1.30 (brute force overwhelms trickery)
  defensive  → aggressive:  1.20 (shields absorb the charge)
  infiltrator→ defensive:   1.20 (bypasses shield walls)
  tactical   → aggressive:  counterplay
  swarm      → infiltrator: 0.70 (can't find them all)
  infiltrator→ swarm:       1.30 (surgical strikes on hive nodes)

5. Random Event System

Events inject narrative drama and create sudden odds shifts — the equivalent of a red card, injury, or weather delay in real sports.

5.1 Event Architecture

type MatchEvent struct {
    ID          string         // Unique event instance ID
    TemplateID  string         // Reference to event template
    Name        string         // Display name
    Description string         // Narrative text for UI feed
    Tick        int            // When it fired
    Category    EventCategory  // combat | environmental | faction | market
    Effects     []EventEffect  // Stat modifications
    Duration    int            // Ticks the effect lasts (0 = instant)
    Severity    string         // minor | major | critical
}

type EventEffect struct {
    Target     string  // "home" | "away" | "both" | "market"
    Stat       string  // "shield" | "attack" | "evasion" | "price"
    Operation  string  // "add" | "multiply" | "set"
    Value      float64
}

5.2 Event Categories & Templates

COMBAT EVENTS (triggered by combat state)

Event Trigger Effect Duration Severity
Critical Hit 5% per hit 3× damage on this hit Instant minor
Shield Harmonics Failure When shields < 30% Shields drop additional 10% Instant major
Weapons Overload 3% per tick Attacker deals 2× but takes 5% self-damage Instant major
Emergency Shield Reroute When shields < 15% Restore 20% shields, disable attack for 3 ticks 3 ticks critical
Hull Breach When shields hit 0% for first time Shields cannot regenerate above 25% Permanent critical

ENVIRONMENTAL EVENTS (triggered by venue)

Event Base Chance Venue Tags Effect Duration
Plasma Storm Surge venue.special_event_chance plasma-storms Both shields 15% Instant
Gravitational Anomaly venue.special_event_chance × 0.5 any Both evasion ×0.5 5 ticks
Subspace Rift venue.special_event_chance × 0.3 temporal-anomaly Random shield reset (both set to random 40-80%) Instant
Sensor Blackout venue.special_event_chance × 0.4 sensor-interference All markets SUSPENDED 3 ticks
Debris Impact venue.special_event_chance × 0.6 debris-field, debris-dense Random faction takes 8-15% shield damage Instant
Energy Dampening Field venue.special_event_chance × 0.2 energy-dampening Both attack ×0.6 4 ticks

FACTION-SPECIFIC EVENTS (triggered by faction type)

Event Faction(s) Trigger Effect
Borg Adaptation borg After taking 3 consecutive hits Incoming damage ×0.5 for 5 ticks
Cloaking Ambush romulans 8% per tick when shields > 60% Next attack deals 2.5× damage, market suspends 1 tick
Honor Duel Challenge klingons When losing by 20+ shield points Attack ×1.5, shields stop regenerating
Prophet Intervention bajorans 3% per tick Shields restored to 70%, all markets suspended 2 ticks
Energy Dampening Weapon breen Once per match, 15% trigger Opponent shields instantly halved
Tholian Web Deployment tholians When shields > 50% Opponent evasion set to 0 for 4 ticks
Ketracel Supply Drop dominion When shields < 40% All stats ×1.3 for 6 ticks
Ferengi Tactical Retreat ferengi When shields < 25% Evasion ×3 for 3 ticks (hard to finish off)
Species 8472 Bio-Pulse species8472 10% per tick Ignores shields, deals 20% direct damage
Changeling Sabotage dominion (away only) 5% per tick Home faction attack ×0.7 for 3 ticks

MARKET EVENTS (triggered by odds state)

Event Trigger Effect
Subspace Interruption Shield swing > 25% in single tick ALL markets suspended for 2 ticks
Odds Avalanche One faction's moneyline crosses 5.00 Exotic markets (comeback, mutual destruction) repriced with tighter odds
Dead Heat Protocol Both shields within 5% and tick > 60% of max Duration market prices collapse; match-winner market widens

5.3 Event Execution Flow

func (engine *Engine) RollForEvents(match *LiveMatch, tickResult TickResult) []MatchEvent {
    var events []MatchEvent
    
    // 1. Environmental event roll
    if rand.Float64() < match.Venue.SpecialEventChance {
        event := engine.pickEnvironmentalEvent(match.Venue)
        events = append(events, event)
    }
    
    // 2. Faction-specific event checks
    for _, side := range []string{"home", "away"} {
        faction := match.GetFaction(side)
        for _, template := range engine.factionEvents[faction.Type] {
            if template.CheckTrigger(match, side, tickResult) {
                if rand.Float64() < template.Probability {
                    event := template.Instantiate(match, side)
                    events = append(events, event)
                }
            }
        }
    }
    
    // 3. Combat state events
    if tickResult.HomeDamageDealt > 0 && rand.Float64() < 0.05 {
        events = append(events, CriticalHitEvent("home"))
    }
    
    // 4. Market disruption checks
    shieldSwing := math.Abs(tickResult.HomeShieldsAfter - tickResult.AwayShieldsAfter)
    if shieldSwing > 25 {
        events = append(events, SubspaceInterruptionEvent())
    }
    
    // Apply all event effects
    for _, event := range events {
        engine.applyEventEffects(match, event)
    }
    
    return events
}

6. Probability Engine — The Core Odds Machine

This is the heart of the sportsbook. Every tick, after combat resolution and event processing, all market probabilities must be recalculated from the current match state.

6.1 Design Principles

  1. Probabilities are derived, not stored — odds are always computed from the current state vector, never carried forward from previous ticks
  2. Overround is applied last — compute fair probability first, then apply house margin
  3. Suspension trumps calculation — if a market event has suspended a market, skip recalculation until the suspension lifts
  4. Odds floor/ceiling — No market selection can go below 1.01 or above 51.00

6.2 Match State Vector

Every probability calculation takes this state as input:

type MatchState struct {
    HomeShields      float64   // 0-100
    AwayShields      float64   // 0-100
    HomeFactionType  string    // aggressive, defensive, etc.
    AwayFactionType  string    
    HomeBaseStats    [3]int    // shield, attack, evasion
    AwayBaseStats    [3]int    
    KineticYield     float64   // Cumulative energy released
    CurrentTick      int       // How far into the match
    MaxTicks         int       // Match duration limit
    ActiveEffects    []Effect  // Lingering buffs/debuffs
    VenueModifiers   Venue     // Environmental modifiers
    EventLog         []Event   // History (for conditional markets)
}

6.3 Market-Specific Probability Models

MODEL A: Match Winner (Moneyline)

The most important market. Uses a logistic regression model on shield differential, weighted by time remaining.

func ProbabilityMatchWinner(state MatchState) (homeProb, awayProb float64) {
    // Shield differential (positive = home advantage)
    shieldDiff := state.HomeShields - state.AwayShields
    
    // Time factor: early ticks → differential matters less
    // Late ticks → differential matters more (less time to recover)
    tickProgress := float64(state.CurrentTick) / float64(state.MaxTicks)
    timeFactor := 0.5 + (tickProgress * 1.5) // Ranges 0.5 → 2.0
    
    // Stat advantage: inherent faction strength difference
    homePower := effectivePower(state.HomeBaseStats, state.VenueModifiers)
    awayPower := effectivePower(state.AwayBaseStats, state.VenueModifiers)
    statDiff := (homePower - awayPower) * 0.1 // Scaled down
    
    // Combined signal
    signal := (shieldDiff * timeFactor * 0.05) + statDiff
    
    // Logistic function → probability
    homeProb = 1.0 / (1.0 + math.Exp(-signal))
    
    // Edge case: if one side is at 0 shields
    if state.HomeShields <= 0 { homeProb = 0.0 }
    if state.AwayShields <= 0 { homeProb = 1.0 }
    
    // Clamp to avoid absurd odds
    homeProb = clamp(homeProb, 0.02, 0.98)
    awayProb = 1.0 - homeProb
    
    return homeProb, awayProb
}

Behavior examples:

State Home P Away P Home Odds Away Odds
Start (both 100%) 0.50 0.50 2.00 2.00
Home 80%, Away 60%, tick 25% 0.58 0.42 1.72 2.38
Home 45%, Away 70%, tick 50% 0.30 0.70 3.33 1.43
Home 20%, Away 65%, tick 80% 0.08 0.92 12.50 1.09
Home 5%, Away 50%, tick 90% 0.03 0.97 33.33 1.03

MODEL B: Total Kinetic Yield (Over/Under)

Projects final total kinetic yield from current rate and remaining time.

func ProbabilityKineticYield(state MatchState, line float64) (overProb, underProb float64) {
    // Current rate of energy release per tick
    ticksElapsed := max(state.CurrentTick, 1)
    currentRate := state.KineticYield / float64(ticksElapsed)
    
    // Project remaining yield
    remainingTicks := float64(state.MaxTicks - state.CurrentTick)
    projectedTotal := state.KineticYield + (currentRate * remainingTicks)
    
    // Variance decreases as match progresses (more certainty)
    variance := (1.0 - float64(state.CurrentTick)/float64(state.MaxTicks)) * 3.0
    variance = max(variance, 0.3) // Minimum uncertainty
    
    // Normal CDF approximation
    z := (line - projectedTotal) / variance
    underProb = normalCDF(z)
    overProb = 1.0 - underProb
    
    // Clamp
    overProb = clamp(overProb, 0.02, 0.98)
    underProb = 1.0 - overProb
    
    return overProb, underProb
}

MODEL C: First Shield Breach

Predicts which faction's shields hit zero first based on damage rate trajectories.

func ProbabilityFirstBreach(state MatchState) (homeFirstProb, awayFirstProb float64) {
    // If one already breached
    if state.HomeShields <= 0 && state.AwayShields > 0 { return 1.0, 0.0 }
    if state.AwayShields <= 0 && state.HomeShields > 0 { return 0.0, 1.0 }
    if state.HomeShields <= 0 && state.AwayShields <= 0 { return 0.5, 0.5 }
    
    // Estimate ticks to breach for each
    homeDamageRate := estimateDamageRate("away_to_home", state)  // damage per tick to home
    awayDamageRate := estimateDamageRate("home_to_away", state)  // damage per tick to away
    
    homeTicksToBreach := state.HomeShields / max(homeDamageRate, 0.01)
    awayTicksToBreach := state.AwayShields / max(awayDamageRate, 0.01)
    
    // Faction with fewer ticks to breach is more likely to breach first
    total := homeTicksToBreach + awayTicksToBreach
    homeFirstProb = awayTicksToBreach / total  // Inverted: if away takes longer, home breaches first more likely... wait
    // Actually: if home has fewer ticks to breach, home breaches first
    homeFirstProb = 1.0 - (homeTicksToBreach / total)
    awayFirstProb = 1.0 - homeFirstProb
    
    return clamp(homeFirstProb, 0.02, 0.98), clamp(awayFirstProb, 0.02, 0.98)
}

MODEL D: Engagement Duration (Over/Under Cycles)

func ProbabilityDuration(state MatchState, line float64) (overProb, underProb float64) {
    // Estimate remaining ticks based on shield drain rates
    avgDrainRate := estimateAverageDrainRate(state) // Combined shield loss per tick
    avgShieldsRemaining := (state.HomeShields + state.AwayShields) / 2.0
    
    estimatedRemainingTicks := avgShieldsRemaining / max(avgDrainRate, 0.1)
    estimatedTotalTicks := float64(state.CurrentTick) + estimatedRemainingTicks
    
    // If already past the line, it's definitely over
    if float64(state.CurrentTick) >= line {
        return 1.0, 0.0
    }
    
    // Normal CDF on projected total vs line
    variance := estimatedRemainingTicks * 0.3
    z := (line - estimatedTotalTicks) / max(variance, 0.1)
    underProb = normalCDF(z)
    overProb = 1.0 - underProb
    
    return clamp(overProb, 0.02, 0.98), clamp(underProb, 0.02, 0.98)
}

MODEL E: Comeback Victory

func ProbabilityComeback(state MatchState, deficitThreshold float64) (homeComeback, awayComeback float64) {
    // Check if the deficit condition has been met historically
    homeMaxDeficit := trackMaxDeficit(state.EventLog, "home")
    awayMaxDeficit := trackMaxDeficit(state.EventLog, "away")
    
    homeDeficitMet := homeMaxDeficit >= deficitThreshold
    awayDeficitMet := awayMaxDeficit >= deficitThreshold
    
    if !homeDeficitMet && !awayDeficitMet {
        // Need both: P(reach deficit) × P(win from deficit)
        // This is very rare → long odds
        homeComeback = 0.05  // ~5% base, adjusted by state
        awayComeback = 0.05
    } else {
        // Deficit was met — now it's "just" a win probability from behind
        if homeDeficitMet {
            homeWinProb, _ := ProbabilityMatchWinner(state)
            homeComeback = homeWinProb * 0.8 // Discount for comeback difficulty
        }
        if awayDeficitMet {
            _, awayWinProb := ProbabilityMatchWinner(state)
            awayComeback = awayWinProb * 0.8
        }
    }
    
    return clamp(homeComeback, 0.02, 0.50), clamp(awayComeback, 0.02, 0.50)
}

MODEL F: Mutual Destruction

func ProbabilityMutualDestruction(state MatchState) float64 {
    // Both shields need to reach 0 on the same tick
    // More likely when shields are close together and both low
    shieldDiff := math.Abs(state.HomeShields - state.AwayShields)
    avgShields := (state.HomeShields + state.AwayShields) / 2.0
    
    // Base probability is very low
    baseProb := 0.01
    
    // Increases when shields are similar (small diff)
    similarityBoost := 1.0 + (20.0 - min(shieldDiff, 20.0)) / 20.0 * 3.0
    
    // Increases when both shields are low
    lowShieldBoost := 1.0
    if avgShields < 30 {
        lowShieldBoost = 1.0 + (30.0 - avgShields) / 30.0 * 5.0
    }
    
    prob := baseProb * similarityBoost * lowShieldBoost
    return clamp(prob, 0.005, 0.30)
}

MODEL G: Subspace Anomaly

func ProbabilitySubspaceAnomaly(state MatchState) (anomalyProb, cleanProb float64) {
    // Geometric distribution: P(at least one event in remaining ticks)
    remainingTicks := state.MaxTicks - state.CurrentTick
    perTickChance := state.VenueModifiers.SpecialEventChance
    
    // Has an anomaly already occurred?
    alreadyOccurred := hasAnomalyOccurred(state.EventLog)
    if alreadyOccurred {
        return 1.0, 0.0  // Already happened → "yes" is certain
    }
    
    // P(no anomaly in remaining ticks) = (1 - p)^n
    cleanProb = math.Pow(1.0 - perTickChance, float64(remainingTicks))
    anomalyProb = 1.0 - cleanProb
    
    return clamp(anomalyProb, 0.02, 0.98), clamp(cleanProb, 0.02, 0.98)
}

6.4 Probability → Price Conversion

func ProbabilityToPrice(prob float64, margin float64) float64 {
    // Fair odds
    fairOdds := 1.0 / prob
    
    // Apply house margin (overround)
    // margin = 1.06 means 106% book (6% house edge)
    adjustedOdds := fairOdds / margin
    
    // Apply floor/ceiling
    adjustedOdds = clamp(adjustedOdds, 1.01, 51.00)
    
    // Round to 2 decimal places
    return math.Round(adjustedOdds * 100) / 100
}

6.5 Overround Strategy

The house margin varies by market category:

Category Target Overround Rationale
Primary (moneyline, O/U) 105106% High liquidity, competitive odds
Secondary (handicap, duration) 107108% Moderate liquidity
Exotic (comeback, mutual dest.) 110115% Low liquidity, high variance, more house edge

7. Valkey Data Schema

7.1 Key Structure

sector:telemetry:{match_id}     → Full match JSON state (HSET)
sector:events:{match_id}        → Capped event log, last 20 events (LIST)
sector:index:active             → Set of active match IDs (SET)  
sector:index:quadrant:{q_id}    → Active match IDs in quadrant (SET)
sector:schedule:upcoming        → Upcoming match announcements (SORTED SET by start time)
global:stats                    → Engine statistics (HASH)

7.2 Match Telemetry Payload (JSON pushed to Valkey)

This is the exact JSON the TanStack Start frontend receives:

{
  "id": "wolf-359-a7f3",
  "name": "Wolf 359 — Memorial Perimeter",
  "quadrant": "alpha",
  "status": "LIVE",
  "tick": 42,
  "maxTicks": 90,
  "kinetic_yield": 14.7,
  "venue": {
    "id": "wolf-359",
    "name": "Wolf 359 — Memorial Perimeter",
    "tags": ["memorial", "debris-field", "high-anomaly"]
  },
  "factions": {
    "home": {
      "id": "starfleet",
      "name": "Starfleet Command",
      "abbreviation": "SFC",
      "type": "defensive",
      "glyph": "◆",
      "colorCode": "#34d399",
      "shieldCapacity": 62,
      "effectiveAttack": 58,
      "effectiveEvasion": 40,
      "activeEffects": []
    },
    "away": {
      "id": "borg",
      "name": "The Borg Collective",
      "abbreviation": "BRG",
      "type": "swarm",
      "glyph": "█",
      "colorCode": "#38bdf8",
      "shieldCapacity": 71,
      "effectiveAttack": 99,
      "effectiveEvasion": 12,
      "activeEffects": [
        { "name": "Borg Adaptation", "remainingTicks": 3, "effect": "damage_taken_x0.5" }
      ]
    }
  },
  "markets": [
    {
      "id": "wolf-359-a7f3-moneyline-home",
      "templateId": "match-winner",
      "name": "Starfleet Command Decisive Victory",
      "category": "primary",
      "price": 2.85,
      "previousPrice": 2.70,
      "trend": "up",
      "status": "OPEN",
      "impliedProbability": 0.351
    },
    {
      "id": "wolf-359-a7f3-moneyline-away",
      "templateId": "match-winner",
      "name": "The Borg Collective Decisive Victory",
      "category": "primary",
      "price": 1.48,
      "previousPrice": 1.52,
      "trend": "down",
      "status": "OPEN",
      "impliedProbability": 0.676
    },
    {
      "id": "wolf-359-a7f3-ky-over",
      "templateId": "kinetic-over-under",
      "name": "Over 8.5 Terawatts",
      "category": "primary",
      "price": 1.35,
      "previousPrice": 1.40,
      "trend": "down",
      "status": "OPEN",
      "impliedProbability": 0.741
    }
  ],
  "recentEvents": [
    {
      "tick": 38,
      "name": "Borg Adaptation",
      "description": "The Collective adapts to Starfleet phaser frequencies. Incoming damage reduced by 50%.",
      "severity": "major"
    },
    {
      "tick": 31,
      "name": "Debris Impact",
      "description": "Wolf 359 wreckage collides with Borg cube. Shields -12%.",
      "severity": "minor"
    }
  ]
}

7.3 Event Log Entry (LPUSH to sector:events:{match_id})

{
  "tick": 38,
  "timestamp": "2024-01-15T10:30:42Z",
  "name": "Borg Adaptation",
  "category": "faction",
  "severity": "major",
  "description": "The Collective adapts to Starfleet phaser frequencies. Incoming damage reduced by 50%.",
  "effects": [
    { "target": "away", "stat": "damage_taken", "operation": "multiply", "value": 0.5, "duration": 5 }
  ],
  "marketImpact": {
    "suspendedMarkets": [],
    "significantPriceChanges": ["wolf-359-a7f3-moneyline-home", "wolf-359-a7f3-moneyline-away"]
  }
}

8. Go Engine File Structure

ingestion-engine/
├── main.go                     # Entry point: init, taxonomy load, main loop
├── go.mod
├── go.sum
├── config/
│   ├── taxonomy.go             # YAML parsing & taxonomy types
│   └── constants.go            # Tuning knobs (tick rate, max matches, etc.)
├── engine/
│   ├── scheduler.go            # Match lifecycle state machine
│   ├── matchmaker.go           # Faction pairing & venue selection
│   ├── simulator.go            # Combat tick resolution
│   ├── events.go               # Random event generation & application
│   ├── probability.go          # All probability models (A through G)
│   ├── markets.go              # Market hydration, pricing, suspension
│   └── type_advantage.go       # Combat type interaction matrix
├── models/
│   ├── match.go                # LiveMatch, MatchState structs
│   ├── faction.go              # FactionState, ActiveEffect structs
│   ├── market.go               # Market, MarketSelection structs
│   └── event.go                # MatchEvent, EventEffect structs
└── cache/
    ├── valkey.go                # Valkey client wrapper
    └── serializer.go           # JSON payload assembly

9. Main Loop Pseudocode

func main() {
    // 1. Load taxonomy
    taxonomy := config.LoadTaxonomy("../data-source/taxonomy.yaml")
    
    // 2. Connect to Valkey
    valkeyClient := cache.NewValkeyClient("localhost:6379")
    
    // 3. Initialize engine
    engine := engine.NewEngine(taxonomy, valkeyClient)
    
    // 4. Seed initial matches
    engine.Scheduler.SeedMatches(TargetActiveMatches)
    
    // 5. Main tick loop (every 2 seconds)
    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        // A. Scheduler: check for matches to create/retire
        engine.Scheduler.Tick()
        
        // B. For each active match, run one combat tick
        for _, match := range engine.Scheduler.ActiveMatches() {
            switch match.Status {
            case "PRE_MATCH":
                // Just update countdown timer, no combat
                engine.UpdatePreMatchCountdown(match)
                
            case "LIVE":
                // Run full tick cycle
                tickResult := engine.Simulator.ResolveTick(match)
                events := engine.EventGenerator.Roll(match, tickResult)
                engine.Probability.RecalculateAll(match)
                engine.Markets.UpdatePrices(match)
                
            case "RESOLVING":
                // Cooldown ticks — no combat, markets suspended
                engine.Simulator.ResolveCountdown(match)
                
            case "SETTLED":
                // Grade markets, archive, remove from active
                engine.Scheduler.SettleMatch(match)
                continue
            }
            
            // Push state to Valkey
            engine.Cache.PushMatchState(match)
            engine.Cache.PushEventLog(match)
        }
        
        // C. Update active index
        engine.Cache.UpdateActiveIndex(engine.Scheduler.ActiveMatches())
        
        log.Println(">> Tick complete. Active matches:", len(engine.Scheduler.ActiveMatches()))
    }
}

10. Tuning Knobs & Configuration

All magic numbers should be extractable constants:

// config/constants.go

// Tick timing
const TickInterval = 2 * time.Second

// Match lifecycle  
const TargetActiveMatches = 8
const MaxActiveMatches = 12
const MinActiveMatches = 4
const PreMatchDuration = 30  // ticks (60s)
const MinMatchDuration = 60  // ticks (2 min)
const MaxMatchDuration = 150 // ticks (5 min)
const ResolvingDuration = 3  // ticks (6s)

// Combat
const DamageScalingFactor = 0.1   // Converts raw attack to shield %
const DamageVarianceLow = 0.6     // Min damage multiplier
const DamageVarianceHigh = 1.4    // Max damage multiplier
const CriticalHitChance = 0.05    // 5% per hit
const CriticalHitMultiplier = 3.0

// Probability
const LogisticScalingFactor = 20.0  // Controls sigmoid steepness
const MinProbability = 0.02         // Floor (max odds ~50.00)
const MaxProbability = 0.98         // Ceiling (min odds ~1.02)
const MinPrice = 1.01
const MaxPrice = 51.00

// Margins
const PrimaryMarketMargin = 1.06    // 106% overround
const SecondaryMarketMargin = 1.075
const ExoticMarketMargin = 1.12

// Matchmaking
const SameQuadrantWeight = 0.60
const CrossQuadrantWeight = 0.30
const DistantQuadrantWeight = 0.10
const MaxSimultaneousAppearances = 2
const RepeatMatchupCooldown = 3
const RivalrySelectionBoost = 2.0

11. Edge Cases & Failure Modes

Scenario Handling
Both shields hit 0 on same tick Mutual Destruction — market graded as "yes", match winner is a draw (both moneylines voided/refunded)
Match hits max tick limit Higher shields wins. If tied, compare cumulative damage dealt. If still tied, draw.
Valkey connection lost Engine continues simulating in memory, buffers state, reconnects with exponential backoff. On reconnect, flush all buffered states.
All factions in cooldown Reduce cooldown timers by 1, create cross-quadrant match with least-recently-used factions
Event chain causes instant kill Shield floor of 1% for first 10 ticks (grace period). After that, instant kills are valid.
Price oscillation (rapid up/down) Apply 3-tick moving average smoothing to price output. Raw probability still updates instantly.
Probability model disagrees with state State always wins. If shields are 0, P(win) = 0 regardless of model output. Hard overrides > model.

12. Future Extensibility

Feature Approach
Accumulator / Parlay bets Multiply implied probabilities across selections from different matches. Apply accumulator margin premium.
Match history / form Track last N results per faction in Valkey sorted set. Feed into pre-match odds as a "form" modifier.
Tournament mode Bracket of 8/16 factions. Winners advance. Outright winner market across the tournament.
Player props (crew members) Add crew member sub-entities to factions with individual stats. Create crew-level markets.
Live commentary feed Event log already structured. Add a natural language template engine to generate commentary strings.
Seasonal rankings Track faction W/L/D records. Create league table. End-of-season markets (champion, relegation).