# 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). |