# 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` | 30–90s | Closed | Match announced, factions & venue visible, no betting |
| `PRE_MATCH` | 60s | Open (pre-match odds) | Initial odds calculated, pre-match wagers accepted |
| `LIVE` | 60–150 ticks (2–5 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:

```go
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

```go
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**:

```go
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

```go
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

```go
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

```go
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:

```go
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.

```go
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.

```go
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.

```go
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)

```go
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

```go
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

```go
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

```go
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

```go
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) | 105–106% | High liquidity, competitive odds |
| Secondary (handicap, duration) | 107–108% | Moderate liquidity |
| Exotic (comeback, mutual dest.) | 110–115% | 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:

```json
{
  "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})

```json
{
  "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

```go
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:

```go
// 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). |
