39 KiB
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:
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
- Probabilities are derived, not stored — odds are always computed from the current state vector, never carried forward from previous ticks
- Overround is applied last — compute fair probability first, then apply house margin
- Suspension trumps calculation — if a market event has suspended a market, skip recalculation until the suspension lifts
- 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) | 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:
{
"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). |