initial import

This commit is contained in:
Pedro Faria da Costa 2026-07-05 21:27:12 +01:00
commit caa0e4225f
No known key found for this signature in database
91 changed files with 14952 additions and 0 deletions

19
.cta.json Normal file
View file

@ -0,0 +1,19 @@
{
"projectName": "quarkshologridledger",
"mode": "file-router",
"typescript": true,
"packageManager": "npm",
"includeExamples": false,
"tailwind": true,
"addOnOptions": {},
"envVarValues": {},
"git": true,
"install": true,
"routerOnly": false,
"version": 1,
"framework": "react",
"chosenAddOns": [
"eslint",
"nitro"
]
}

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.env
.nitro
.tanstack
.wrangler
.output
.vinxi
__unconfig*
todos.json

11
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}

35
Makefile Normal file
View file

@ -0,0 +1,35 @@
.PHONY: build run clean help infra-up infra-down infra-logs run-engine run-web
# Command Variables
DOCKER_COMPOSE=docker compose
GO=go
NPM=npm
## infra-up: Start Valkey and Redis Insight containers in the background
infra-up:
$(DOCKER_COMPOSE) up -d
## infra-down: Stop Valkey and Redis Insight containers
infra-down:
$(DOCKER_COMPOSE) down
## infra-logs: Follow logs of the infrastructure containers
infra-logs:
$(DOCKER_COMPOSE) logs -f
## run-engine: Build and run the Tongo Core backend engine daemon
run-engine:
$(MAKE) -C tongo-core run
## run-web: Run the TanStack Start frontend web terminal dev server
run-web:
cd web-terminal && $(NPM) run dev
## clean: Stop containers and clean all build artifacts and volumes
clean: infra-down
$(MAKE) -C tongo-core clean
$(DOCKER_COMPOSE) down -v
## help: Show this help
help:
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'

55
README.md Normal file
View file

@ -0,0 +1,55 @@
# 🌌 Quark's Holo-Grid Ledger
Welcome to the **Holo-Grid Ledger** — an authentic, plaintext terminal-style sportsbook application built for the Star Trek-inspired galactic economy.
---
## 🏛 Project Architecture
This repository is organized as a clean monorepo:
- **[`web-terminal/`](file:///Users/pedro/code/github.com/costap/quarkshologridledger/web-terminal)**: The frontend user interface built with **TanStack Start**, **Vite**, **Valkey**, and **Tailwind CSS**. It renders high-density VT100-style console dashboards.
- **[`tongo-core/`](file:///Users/pedro/code/github.com/costap/quarkshologridledger/tongo-core)**: The high-performance Go simulation and probability engine. It runs as a background daemon, streaming active match telemetry and live odds directly to the Valkey cache.
- **[`data-source/`](file:///Users/pedro/code/github.com/costap/quarkshologridledger/data-source)**: Static definition registry ([`taxonomy.yaml`](file:///Users/pedro/code/github.com/costap/quarkshologridledger/data-source/taxonomy.yaml)) containing quadrants, factions, venues, and market templates.
- **[`reference/`](file:///Users/pedro/code/github.com/costap/quarkshologridledger/reference)**: Architectural references and blueprint documents for components and the simulation engine.
---
## 🛠 Getting Started
### 1. Start Infrastructure (Valkey & Redis Insight)
The sportsbook relies on Valkey as a shared memory layer, and Redis Insight provides a GUI for database inspection.
```bash
# Start Valkey and Redis Insight in the background
make infra-up
# View logs
make infra-logs
# Stop infrastructure
make infra-down
```
Valkey will run on `localhost:6379`, and Redis Insight will be accessible at `http://localhost:5540`.
### 2. Start the Simulation Engine
Go to the Go backend folder and start the engine daemon:
```bash
make run-engine
```
### 3. Start the Web Terminal UI
Go to the frontend folder, install dependencies, and start the Vite dev server:
```bash
make run-web
```
Visit the console at `http://localhost:3000`.
---
## 📜 Ferengi Code of Conduct
> "The riskier the road, the greater the profit." — Rule of Acquisition #62

1056
data-source/taxonomy.yaml Normal file

File diff suppressed because it is too large Load diff

26
docker-compose.yml Normal file
View file

@ -0,0 +1,26 @@
version: '3.8'
services:
valkey:
image: valkey/valkey:8.0
container_name: tongo-valkey
ports:
- "6379:6379"
volumes:
- valkey_data:/data
restart: always
redisinsight:
image: redis/redisinsight:latest
container_name: tongo-redisinsight
ports:
- "5540:5540"
volumes:
- redisinsight_data:/db
depends_on:
- valkey
restart: always
volumes:
valkey_data:
redisinsight_data:

2
pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,2 @@
allowBuilds:
unrs-resolver: set this to true or false

989
reference/ENGINE.md Normal file
View file

@ -0,0 +1,989 @@
# Simulation Engine Architecture: Quark's Holo-Grid Ledger
> **Status**: Design Analysis — Pre-Implementation
> **Author**: Engine Architecture Team
> **Dependencies**: `data-source/taxonomy.yaml`, Go 1.22+, Valkey 8.x
---
## 1. Engine Overview
The simulation engine is a **Go-based daemon** that reads the static taxonomy, generates live match events on a rolling schedule, simulates tick-by-tick combat, calculates real-time probabilities for all market types, and pushes continuous state updates into Valkey for the TanStack Start frontend to consume.
```
┌─────────────────────────────────────────────────────────────┐
│ SIMULATION ENGINE │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │SCHEDULER │→ │MATCHMAKER│→ │SIMULATOR │→ │PROBABILITY│ │
│ │ │ │ │ │ (tick) │ │ ENGINE │ │
│ └──────────┘ └──────────┘ └────┬─────┘ └─────┬─────┘ │
│ │ │ │
│ ┌────▼─────┐ ┌────▼─────┐ │
│ │ EVENT │ │ MARKET │ │
│ │GENERATOR │ │ UPDATER │ │
│ └──────────┘ └────┬─────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ VALKEY │ │
│ │ WRITER │ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Engine Modules
| Module | Responsibility | Frequency |
|--------|---------------|-----------|
| **Scheduler** | Manages match lifecycle: create, run, resolve, retire | Every 30s check |
| **Matchmaker** | Pairs factions, picks venues, generates initial markets | On match creation |
| **Simulator** | Runs one combat tick per active match | Every 2s per match |
| **Event Generator** | Rolls for random in-match events | Every tick |
| **Probability Engine** | Recalculates all market odds from current state | Every tick |
| **Market Updater** | Applies probability → price conversion + margin | Every tick |
| **Valkey Writer** | Serializes & pushes atomic state updates to cache | Every tick |
---
## 2. Match Lifecycle
Every match (sector engagement) follows a deterministic state machine:
```
SCHEDULED ──────► PRE-MATCH ──────► LIVE ──────► RESOLVING ──────► SETTLED
│ │ │ │ │
│ T-60s before │ Markets open │ Combat ticks │ Winner found │ Markets paid
│ match start │ initial odds │ odds moving │ final odds │ match archived
│ │ set from │ events fire │ locked │
│ │ faction stats │ │ │
▼ ▼ ▼ ▼ ▼
Announced to Betslip accepts Live telemetry 3-tick cooldown Removed from
frontend index wagers streaming no new bets active index
```
### Lifecycle Timing
| Phase | Duration | Markets | Description |
|-------|----------|---------|-------------|
| `SCHEDULED` | 3090s | Closed | Match announced, factions & venue visible, no betting |
| `PRE_MATCH` | 60s | Open (pre-match odds) | Initial odds calculated, pre-match wagers accepted |
| `LIVE` | 60150 ticks (25 min) | Open (live odds) | Active combat simulation, odds shift each tick |
| `RESOLVING` | 3 ticks (6s) | **Suspended** | Winner determined, final state locked, dramatic pause |
| `SETTLED` | 10s | Closed (final) | Results committed, markets graded, match archived |
### Concurrent Match Capacity
The engine should maintain a **rolling window** of active matches:
```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) | 105106% | High liquidity, competitive odds |
| Secondary (handicap, duration) | 107108% | Moderate liquidity |
| Exotic (comeback, mutual dest.) | 110115% | Low liquidity, high variance, more house edge |
---
## 7. Valkey Data Schema
### 7.1 Key Structure
```
sector:telemetry:{match_id} → Full match JSON state (HSET)
sector:events:{match_id} → Capped event log, last 20 events (LIST)
sector:index:active → Set of active match IDs (SET)
sector:index:quadrant:{q_id} → Active match IDs in quadrant (SET)
sector:schedule:upcoming → Upcoming match announcements (SORTED SET by start time)
global:stats → Engine statistics (HASH)
```
### 7.2 Match Telemetry Payload (JSON pushed to Valkey)
This is the exact JSON the TanStack Start frontend receives:
```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). |

765
reference/FRONTEND.md Normal file
View file

@ -0,0 +1,765 @@
# Frontend Architecture: Quark's Holo-Grid Ledger
> **Status**: Design Analysis — Pre-Implementation
> **Dependencies**: `reference/ENGINE.md`, `data-source/taxonomy.yaml`, `AGENTS.md`
> **Framework**: TanStack Start + TanStack Router (file-based routing)
---
## 1. Layout Topology
The application uses a fixed-viewport, three-column terminal layout defined in `__root.tsx`. The root shell is persistent — only the center column swaps content via `<Outlet />`.
```
┌────────────────────────────────────────────────────────────────────────────┐
│ [=] QUARK'S HOLO-GRID LEDGER // SUB-NET RECEPTOR STARDATE: 54201.3 │
│ BALANCE: 4,250.00 GPL │
├──────────────┬────────────────────────────────┬────────────────────────────┤
│ │ │ │
│ LEFT PANEL │ CENTER PANEL │ RIGHT PANEL │
│ col-span-3 │ col-span-6 │ col-span-3 │
│ │ │ │
│ Navigation │ <Outlet /> │ Betslip │
│ Sidebar │ (route content) │ Sidecar │
│ │ │ │
│ 240px-ish │ flex-1 │ 280px-ish │
│ │ │ │
├──────────────┴────────────────────────────────┴────────────────────────────┤
│ SYSTEM RUNTIME: OK // CORE MODULES ACTIVE TANSTACK + VITE │
└────────────────────────────────────────────────────────────────────────────┘
```
### Responsive Breakpoints
```
DESKTOP (≥1280px): [col-3 Sidebar] [col-6 Main] [col-3 Betslip]
TABLET (768-1279): [col-2 Sidebar] [col-7 Main] [col-3 Betslip] (sidebar compressed)
MOBILE (<768): [Full-width Main] + bottom tab bar + slide-up betslip
```
On mobile, the sidebar collapses into a **bottom navigation bar** with 5 terminal command buttons, and the betslip becomes a pull-up sheet triggered from the bar.
```
┌────────────────────────────────────────┐
│ QUARK'S HOLO-GRID LEDGER 4,250 GPL │
├────────────────────────────────────────┤
│ │
│ MAIN CONTENT │
│ (full-width) │
│ │
│ │
├────────────────────────────────────────┤
│ GRID │ ALPHA │ BETA │ SLIP │ LEDGER │
│ ◈ │ α │ β │ ₲ │ ▤ │
└────────────────────────────────────────┘
```
---
## 2. Navigation Sidebar (Left Panel)
The sidebar is the primary navigation interface. It must feel like a hardware command console — numbered entries, hierarchical quadrant trees, and live status indicators.
### 2.1 Sidebar Sections
```
╔══════════════════════════════════╗
║ [ SYSTEM ARRAY ] Quadrants ║
╠══════════════════════════════════╣
║ ║
║ > GRID OVERVIEW [00] ║ ← Link to /
║ ║
║ ═══════════════════════════════ ║
║ > LIVE ENGAGEMENTS ║ ← Section header
║ ║
║ ▾ ALPHA QUADRANT [α] ║ ← Collapsible quadrant group
║ ├─ ◆SFC vs █BRG ●LIVE ║ ← Active match (link to /quadrants/{id})
║ │ 62% ████░░ 71% ║ ← Inline shield bars
║ ├─ ▼CRD vs ☼BAJ ●LIVE ║
║ │ 48% ███░░░ 55% ║
║ └─ ⚔KLG vs ◈ROM ○PRE ║ ← Pre-match (different indicator)
║ ║
║ ▸ BETA QUADRANT [β] ║ ← Collapsed quadrant
║ 2 active ║
║ ║
║ ▸ GAMMA QUADRANT [γ] ║
║ 1 active ║
║ ║
║ ▸ DELTA QUADRANT [δ] ║
║ 3 active ║
║ ║
║ ═══════════════════════════════ ║
║ > STATION ARCHIVES ║ ← Section header
║ ║
║ [01] Wager Ledger ║ ← Link to /ledger
║ [02] Faction Registry ║ ← Link to /registry/factions
║ [03] Venue Database ║ ← Link to /registry/venues
║ [04] Transmission Schedule ║ ← Link to /schedule
║ ║
╚══════════════════════════════════╝
```
### 2.2 Live Match Items
Each match in the sidebar should show at a glance:
- Faction glyphs and abbreviations (e.g., `◆SFC vs █BRG`)
- Match status indicator: `●LIVE` (pulsing green) / `○PRE` (amber) / `◉RES` (red)
- Compact shield capacity bars (optional, space permitting)
- Clicking navigates to `/quadrants/{sectorId}?quadrant=Alpha&currency=Latinum`
### 2.3 Sidebar Data Source
The sidebar reads from the `sector:index:active` Valkey set via a **server function** that returns all active match summaries. It polls at the same 2s interval as match telemetry.
```tsx
// Server function: returns lightweight match summaries for sidebar
const getActiveMatchIndex = createServerFn({ method: 'GET' })
.handler(async () => {
const matchIds = await valkey.smembers('sector:index:active')
// For each match, read just the fields needed for sidebar rendering
// (id, name, quadrant, status, factions.home.abbreviation, factions.away.abbreviation,
// factions.home.shieldCapacity, factions.away.shieldCapacity)
return summaries
})
```
---
## 3. Route Map
### 3.1 File-Based Route Structure
```
src/routes/
├── __root.tsx # Shell layout (header, sidebar, betslip, footer)
├── index.tsx # Grid Overview / Dashboard
├── quadrants/
│ ├── index.tsx # Quadrant listing (all matches, filterable)
│ └── $sectorId.tsx # Live match detail (the main event page)
├── ledger/
│ ├── index.tsx # Wager Ledger overview (active + recent settled)
│ ├── active.tsx # Active/open wagers only
│ └── settled.tsx # Settled/graded wagers history
├── registry/
│ ├── factions.tsx # Faction directory (all teams)
│ ├── factions.$factionId.tsx # Faction detail (stats, form, history)
│ ├── venues.tsx # Venue directory (all sectors)
│ └── venues.$venueId.tsx # Venue detail (modifiers, match history)
└── schedule.tsx # Upcoming match schedule
```
### 3.2 Route Definitions with Validated Search Params
Every route enforces typed search params via Zod validators per AGENTS.md Rule A.
```tsx
// ─── / (Grid Overview Dashboard) ───
export const Route = createFileRoute('/')({
validateSearch: z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']).optional(),
currency: z.enum(['Latinum', 'Credits']).default('Latinum'),
}),
})
// ─── /quadrants (Quadrant Match Listing) ───
export const Route = createFileRoute('/quadrants/')({
validateSearch: z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']).optional(),
status: z.enum(['LIVE', 'PRE_MATCH', 'RESOLVING', 'all']).default('all'),
currency: z.enum(['Latinum', 'Credits']).default('Latinum'),
sort: z.enum(['shields', 'yield', 'newest', 'ending']).default('newest'),
}),
})
// ─── /quadrants/$sectorId (Live Match Detail) ───
export const Route = createFileRoute('/quadrants/$sectorId')({
validateSearch: z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']),
currency: z.enum(['Latinum', 'Credits']).default('Latinum'),
marketCategory: z.enum(['primary', 'secondary', 'exotic', 'all']).default('all'),
}),
})
// ─── /ledger (Wager Ledger) ───
export const Route = createFileRoute('/ledger/')({
validateSearch: z.object({
status: z.enum(['active', 'settled', 'all']).default('all'),
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']).optional(),
currency: z.enum(['Latinum', 'Credits']).default('Latinum'),
page: z.number().default(1),
}),
})
// ─── /registry/factions (Faction Directory) ───
export const Route = createFileRoute('/registry/factions')({
validateSearch: z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']).optional(),
type: z.enum(['aggressive', 'defensive', 'tactical', 'infiltrator', 'swarm']).optional(),
sort: z.enum(['name', 'attack', 'shield', 'evasion', 'winrate']).default('name'),
}),
})
// ─── /registry/venues (Venue Directory) ───
export const Route = createFileRoute('/registry/venues')({
validateSearch: z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']).optional(),
sort: z.enum(['name', 'shield_mod', 'damage_mod', 'chaos']).default('name'),
}),
})
// ─── /schedule (Upcoming Matches) ───
export const Route = createFileRoute('/schedule')({
validateSearch: z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']).optional(),
currency: z.enum(['Latinum', 'Credits']).default('Latinum'),
}),
})
```
---
## 4. Page Designs
### 4.1 Grid Overview Dashboard (`/`)
The landing page — a high-density terminal dashboard showing all active engagements at a glance.
```
┌──────────────────────────────────────────────────────────────────┐
│ [ CONSOLE TERMINAL ] Active Feeds │
├──────────────────────────────────────────────────────────────────┤
│ │
│ > SUBSPACE BROADCAST — LIVE ENGAGEMENTS ACROSS ALL QUADRANTS │
│ ════════════════════════════════════════════════════════════ │
│ │
│ ┌─── FILTER ─────────────────────────────────────────────────┐ │
│ │ [ALL] [α ALPHA] [β BETA] [γ GAMMA] [δ DELTA] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SECTOR 001 — EARTH ORBITAL ARRAY ●LIVE α │ │
│ │ ◆ Starfleet Command 62% ████████░░░ VS │ │
│ │ █ Borg Collective 71% █████████░░ │ │
│ │ ────────────────────────────────────────────────── │ │
│ │ KY: 14.7 TW TICK: 42/90 ODDS: SFC 2.85 | BRG 1.48 │
│ │ ▸ 6 MARKETS OPEN [VIEW] → │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ THE BADLANDS — PLASMA STORM ZONE ●LIVE α │ │
│ │ ▼ Cardassian Union 48% ██████░░░░ VS │ │
│ │ ☼ Bajoran Militia 55% ███████░░░ │ │
│ │ ────────────────────────────────────────────────── │ │
│ │ KY: 8.2 TW TICK: 31/75 ODDS: CRD 2.10 | BAJ 1.72 │
│ │ ▸ 8 MARKETS OPEN [VIEW] → │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ NARENDRA III — HONOR'S EDGE ○PRE β │ │
│ │ ⚔ Klingon Empire VS │ │
│ │ ◈ Romulan Star Empire │ │
│ │ ────────────────────────────────────────────────── │ │
│ │ COMMENCING IN: 00:47 PRE-MATCH ODDS AVAILABLE │ │
│ │ ▸ 4 MARKETS OPEN [VIEW] → │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ LIVE TICKER ──────────────────────────────────────────┐ │
│ │ 14:32 Borg Adaptation triggered — Damage reduced 50% │ │
│ │ 14:30 Critical Hit! Starfleet deals 3× damage │ │
│ │ 14:28 Bajor: Prophet Intervention — Markets SUSPENDED │ │
│ │ 14:25 New match: Klingons vs Romulans @ Narendra III │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
**Key features:**
- Match summary cards with inline shield bars and headline odds
- Quadrant filter tabs (search param `?quadrant=alpha`)
- Live event ticker scrolling recent events from ALL active matches
- Pre-match cards show countdown timer instead of combat data
- Each card links to `/quadrants/$sectorId`
### 4.2 Match Detail Page (`/quadrants/$sectorId`)
The most important page — full live match view with all markets.
```
┌──────────────────────────────────────────────────────────────────┐
│ [ ENGAGEMENT MATRIX ] Wolf 359 — Memorial Perimeter │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ VERSUS ARENA ────────────────────────────────────────────┐ │
│ │ │ │
│ │ ◆ STARFLEET COMMAND █ BORG COLLECTIVE │ │
│ │ [SFC] [BRG] │ │
│ │ │ │
│ │ SHIELDS 62% ████████░░░ 71% █████████░░ SHIELDS │ │
│ │ ATTACK 58 99 ATTACK │ │
│ │ EVASION 40 12 EVASION │ │
│ │ │ │
│ │ ─────────── ✦ VS ✦ ────────── │ │
│ │ │ │
│ │ KINETIC YIELD: 14.7 TW TICK: 42 / 90 │ │
│ │ VENUE: Wolf 359 [debris-field, high-anomaly] │ │
│ │ SHIELD MOD: 0.95 DMG MOD: 1.10 EVA MOD: 0.80 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ ACTIVE EFFECTS ──────────────────────────────────────────┐ │
│ │ ⚡ BORG ADAPTATION (BRG) — Incoming damage ×0.5 [3/5] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ FILTER ───────────────────────────────────────────────────┐ │
│ │ [ALL] [PRIMARY] [SECONDARY] [EXOTIC] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ MARKETS ─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ COMBAT RESOLUTION — MONEYLINE [primary] │ │
│ │ ┌──────────────────────────┬───────────────────────────┐ │ │
│ │ │ ◆ Starfleet Victory │ █ Borg Assimilation │ │ │
│ │ │ [2.85] ▲ │ [1.48] ▼ │ │ │
│ │ └──────────────────────────┴───────────────────────────┘ │ │
│ │ │ │
│ │ FIRST SHIELD BREACH [primary] │ │
│ │ ┌──────────────────────────┬───────────────────────────┐ │ │
│ │ │ ◆ SFC Shields First │ █ BRG Shields First │ │ │
│ │ │ [1.65] ▼ │ [2.20] ▲ │ │ │
│ │ └──────────────────────────┴───────────────────────────┘ │ │
│ │ │ │
│ │ TOTAL KINETIC YIELD [primary] │ │
│ │ ┌──────────────────────────┬───────────────────────────┐ │ │
│ │ │ Over 8.5 TW │ Under 8.5 TW │ │ │
│ │ │ [1.35] ▼ │ [3.10] ▲ │ │ │
│ │ └──────────────────────────┴───────────────────────────┘ │ │
│ │ │ │
│ │ COMEBACK VICTORY [exotic] │ │
│ │ ┌──────────────────────────┬───────────────────────────┐ │ │
│ │ │ ◆ SFC 30+ Comeback │ █ BRG 30+ Comeback │ │ │
│ │ │ [5.50] │ [8.20] ▲ │ │ │
│ │ └──────────────────────────┴───────────────────────────┘ │ │
│ │ │ │
│ │ MUTUAL DESTRUCTION [exotic] │ │
│ │ ┌──────────────────────────┬───────────────────────────┐ │ │
│ │ │ Both Shields 0% │ Survivor │ │ │
│ │ │ [12.00] │ [1.05] │ │ │
│ │ └──────────────────────────┴───────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ ENGAGEMENT LOG ──────────────────────────────────────────┐ │
│ │ [T42] ◆ Starfleet fires — HIT — 4.2% shield damage │ │
│ │ [T42] █ Borg fires — MISS — Evasion successful │ │
│ │ [T38] ⚡ EVENT: Borg Adaptation — Incoming dmg ×0.5 │ │
│ │ [T35] ◆ Starfleet fires — CRIT — 12.6% shield damage │ │
│ │ [T31] 🌀 EVENT: Debris Impact — BRG shields -12% │ │
│ │ [T28] █ Borg fires — HIT — 6.1% shield damage │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
**Key features:**
- Versus arena header with full faction stats, shield bars, and active effects
- Venue modifier display (so bettors understand the environment)
- Market category filter tabs (search param `?marketCategory=primary`)
- Each market selection is a clickable button that adds to betslip
- Price trend indicators (▲ up / ▼ down) with color coding
- Engagement log showing tick-by-tick combat events
- Market suspension overlay when `SUBSPACE INTERRUPTION` event fires
### 4.3 Betslip Sidecar (Right Panel — always visible)
The betslip is NOT a route — it's a persistent panel in the root layout, managed via React context or a lightweight client store.
```
╔══════════════════════════════════╗
║ [ WAGER SLIP ] Secure Link ║
╠══════════════════════════════════╣
║ ║
║ SELECTIONS (2) ║
║ ════════════════════════════ ║
║ ║
║ ┌────────────────────────────┐ ║
║ │ Wolf 359 — SFC vs BRG │ ║
║ │ ◆ Starfleet Victory │ ║
║ │ [2.85] [×] │ ║
║ │ STAKE: [___25.00___] GPL │ ║
║ │ RETURN: 71.25 GPL │ ║
║ └────────────────────────────┘ ║
║ ║
║ ┌────────────────────────────┐ ║
║ │ Badlands — CRD vs BAJ │ ║
║ │ Over 8.5 Terawatts │ ║
║ │ [1.85] [×] │ ║
║ │ STAKE: [___10.00___] GPL │ ║
║ │ RETURN: 18.50 GPL │ ║
║ └────────────────────────────┘ ║
║ ║
║ ═══════════════════════════════ ║
║ SINGLES TOTAL STAKE: 35.00 ║
║ POTENTIAL RETURN: 89.75 ║
║ ║
║ ─── ACCUMULATOR ────────────── ║
║ COMBINED ODDS: [5.27] ║
║ ACCA STAKE: [________] GPL ║
║ ACCA RETURN: 0.00 GPL ║
║ ║
║ ═══════════════════════════════ ║
║ ║
║ ┌────────────────────────────┐ ║
║ │ ▶ TRANSMIT WAGER ◀ │ ║
║ │ Rule of Acquisition #62 │ ║
║ └────────────────────────────┘ ║
║ ║
║ ─── RECENT TRANSMISSIONS ───── ║
║ ✓ SFC Victory @ 2.40 +36.00 ║
║ ✗ Over 8.5 TW @ 1.85 -10.00 ║
║ ○ BRG Victory @ 1.48 PENDING ║
║ ║
╚══════════════════════════════════╝
```
**Key features:**
- Selections added by clicking market buttons on any match page
- Individual stake inputs per selection (singles)
- Accumulator option with combined odds
- Live price updates — if odds change while in betslip, show old→new with flash
- Odds lock warning if price moves significantly
- Recent bet results at the bottom
- Persistent across route changes (React context, not URL state)
### 4.4 Wager Ledger (`/ledger`)
```
┌──────────────────────────────────────────────────────────────────┐
│ [ WAGER LEDGER ] Transaction Archive │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ FILTER ───────────────────────────────────────────────────┐ │
│ │ [ALL] [ACTIVE] [SETTLED] Quadrant: [ALL ▾] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ ACCOUNT SUMMARY ─────────────────────────────────────────┐ │
│ │ BALANCE: 4,250.00 GPL ACTIVE RISK: 285.00 GPL │ │
│ │ SESSION P/L: +127.50 GPL ▲ │ │
│ │ WIN RATE: 58% (14/24) AVG ODDS: 2.15 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ─── ACTIVE WAGERS ──────────────────────────────────────────── │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ #W-0042 Wolf 359 — SFC vs BRG ●LIVE α │ │
│ │ Selection: ◆ Starfleet Victory │ │
│ │ Odds Taken: 2.85 Stake: 25.00 GPL │ │
│ │ Current Odds: 3.10 Potential: 71.25 GPL │ │
│ │ Status: OPEN — Shields: SFC 62% | BRG 71% │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ─── SETTLED WAGERS ─────────────────────────────────────────── │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ #W-0039 Qo'noS Arena — KLG vs GRN ✓ WON β │ │
│ │ Selection: ⚔ Klingon Victory │ │
│ │ Odds: 1.65 Stake: 30.00 Return: 49.50 GPL +19.50 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ #W-0037 Badlands — CRD vs BAJ ✗ LOST α │ │
│ │ Selection: Over 8.5 TW │ │
│ │ Odds: 1.85 Stake: 10.00 Return: 0.00 GPL -10.00 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.5 Faction Registry (`/registry/factions`)
```
┌──────────────────────────────────────────────────────────────────┐
│ [ FACTION REGISTRY ] Combatant Database │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ FILTER ───────────────────────────────────────────────────┐ │
│ │ Quadrant: [ALL ▾] Type: [ALL ▾] Sort: [NAME ▾] │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ GLYPH FACTION QDR TYPE SHD ATK EVA │
│ ═════ ═══════════════════ ═══ ═══════════ ═══ ═══ ═══ │
│ ◆ Starfleet Command α defensive 92 65 50 │
│ █ Borg Collective δ swarm 95 90 15 │
│ ⚔ Klingon Empire β aggressive 60 95 35 │
│ ◈ Romulan Star Empire β infiltrator 72 70 85 │
│ ◉ The Dominion γ swarm 75 88 40 │
│ ∞ Species 8472 δ aggressive 99 99 70 │
│ ... │
│ │
│ Click a faction row to view detailed profile → │
│ │
└──────────────────────────────────────────────────────────────────┘
```
Clicking a faction row navigates to `/registry/factions/$factionId`, which shows:
- Full tactical profile text
- Rivalry modifiers table
- Active matches this faction is in
- Recent match history (W/L record)
- Type advantage breakdown
### 4.6 Venue Database (`/registry/venues`)
```
┌──────────────────────────────────────────────────────────────────┐
│ [ VENUE DATABASE ] Sector Pool Registry │
├──────────────────────────────────────────────────────────────────┤
│ │
│ VENUE QDR SHD× DMG× EVA× ANOMALY │
│ ═══════════════════════ ════ ═════ ═════ ═════ ════════ │
│ Fluidic Space Rift δ 0.60 1.50 0.80 35% !!! │
│ The Badlands α 0.70 1.30 0.60 35% !!! │
│ Founders' Approach γ 0.85 1.10 0.60 40% !!! │
│ Qo'noS Arena β 0.80 1.25 0.95 8% │
│ Vulcan Defense Grid α 1.20 0.85 1.00 3% │
│ Nekrit Expanse δ 1.00 1.00 1.00 1% │
│ ... │
│ │
└──────────────────────────────────────────────────────────────────┘
```
### 4.7 Match Schedule (`/schedule`)
```
┌──────────────────────────────────────────────────────────────────┐
│ [ TRANSMISSION SCHEDULE ] Upcoming Engagements │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ─── SCHEDULED (starting soon) ──────────────────────────────── │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ T-47s Narendra III — Honor's Edge [β] │ │
│ │ ⚔ Klingon Empire vs ◈ Romulan Star Empire │ │
│ │ Rivalry: 1.30 — MARQUEE MATCHUP │ │
│ │ Pre-match odds available at T-60s │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ T-2m Gornar Orbit — Hegemony Arena [β] │ │
│ │ ▲ Gorn Hegemony vs ⊘ Orion Syndicate │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ─── RECENTLY SETTLED ───────────────────────────────────────── │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ SETTLED Vulcan Grid — T'Khasi Defense Grid [α] │ │
│ │ ◆ Starfleet Command 68% vs ₲ Ferengi 0% │ │
│ │ RESULT: Starfleet Victory — 72 ticks │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
```
---
## 5. Component Architecture
Per AGENTS.md, route files contain only configuration. All visual markup lives in components.
### 5.1 Component Map
```
src/components/
├── ascii/ # Low-level visual primitives
│ ├── AsciiPanel.tsx # Bordered panel container (EXISTS)
│ ├── ShieldBar.tsx # Horizontal shield capacity bar
│ ├── PriceTag.tsx # Odds display with trend indicator
│ ├── StatusBadge.tsx # LIVE/PRE/RESOLVING/SETTLED indicator
│ ├── TerminalLine.tsx # Decorative separator line
│ ├── BlinkingCursor.tsx # Animated cursor for loading states
│ ├── TickProgress.tsx # Match progress bar (tick X / maxTicks)
│ └── GlyphIcon.tsx # Faction glyph renderer with color
├── panels/ # Domain feature views (large composites)
│ ├── NavigationMenu.tsx # Left sidebar navigation (EXISTS)
│ ├── BetslipSidecar.tsx # Right sidebar betslip
│ ├── MatchCard.tsx # Summary card for dashboard grid
│ ├── VersusArena.tsx # Full faction-vs-faction header
│ ├── MarketGrid.tsx # All markets for a match
│ ├── MarketSelection.tsx # Single clickable odds button
│ ├── EngagementLog.tsx # Scrolling event log for a match
│ ├── LiveTicker.tsx # Global event feed (dashboard)
│ ├── FactionTable.tsx # Sortable faction directory table
│ ├── VenueTable.tsx # Sortable venue directory table
│ ├── FactionProfile.tsx # Detailed faction stats view
│ ├── WagerCard.tsx # Single bet display (active or settled)
│ ├── AccountSummary.tsx # Balance, P/L, win rate stats
│ ├── ScheduleCard.tsx # Upcoming/settled match card
│ └── QuadrantFilterBar.tsx # Reusable quadrant filter tabs
└── providers/ # Client state providers
├── BetslipProvider.tsx # Betslip selection state (React Context)
└── CurrencyProvider.tsx # Active currency preference
```
### 5.2 Route → Component Mapping
Routes stay thin (AGENTS.md compliance). Heavy rendering delegates to panel components.
```tsx
// src/routes/quadrants/$sectorId.tsx — THIN ROUTE FILE
export const Route = createFileRoute('/quadrants/$sectorId')({
validateSearch: z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']),
currency: z.enum(['Latinum', 'Credits']).default('Latinum'),
marketCategory: z.enum(['primary', 'secondary', 'exotic', 'all']).default('all'),
}),
loader: async ({ params }) => {
const snapshot = await getCachedSectorData({ input: params.sectorId })
return { snapshot }
},
component: MatchDetailView, // ← delegates to panel
})
// The actual MatchDetailView component lives in src/components/panels/
// It composes: VersusArena + MarketGrid + EngagementLog
```
---
## 6. Data Flow & Server Functions
### 6.1 Server Function Registry
```
src/server/
├── db/
│ └── valkeyConnector.ts # Valkey client instance
└── functions/
├── getActiveMatchIndex.ts # All active match summaries (sidebar)
├── getMatchTelemetry.ts # Full match state for detail page
├── getUpcomingSchedule.ts # Scheduled matches
├── getSettledMatches.ts # Recently settled results
├── getFactionRegistry.ts # Static faction data from taxonomy
├── getVenueRegistry.ts # Static venue data from taxonomy
└── submitWager.ts # Place a bet (write to Valkey)
```
### 6.2 Polling Strategy
| Data | Interval | Source | Route |
|------|----------|--------|-------|
| Active match index (sidebar) | 2s | `sector:index:active` | `__root.tsx` (always) |
| Match telemetry (detail page) | 2s | `sector:telemetry:{id}` | `/quadrants/$sectorId` |
| Match events (detail page) | 2s | `sector:events:{id}` | `/quadrants/$sectorId` |
| Dashboard match cards | 2s | All active telemetry | `/` |
| Schedule | 10s | `sector:schedule:upcoming` | `/schedule` |
| Faction/venue registry | Static | `taxonomy.yaml` (cached) | `/registry/*` |
| Betslip price updates | 2s | Individual market keys | `BetslipSidecar` |
### 6.3 SSR + Client Hydration Pattern
```tsx
// Pattern for all live data routes:
// 1. Loader runs on server — fetches initial snapshot from Valkey
loader: async ({ params }) => {
return await getCachedSectorData({ input: params.sectorId })
},
// 2. Component hydrates with loader data, then starts client polling
function MatchDetailView() {
const initialData = Route.useLoaderData()
const { data } = useQuery({
queryKey: ['telemetry', sectorId],
queryFn: () => getCachedSectorData({ input: sectorId }),
initialData, // SSR data used immediately
refetchInterval: 2000, // Client polls every 2s after hydration
})
}
```
---
## 7. Client State Management
Only two pieces of state live on the client (not URL, not server):
### 7.1 Betslip State (React Context)
```tsx
interface BetslipSelection {
matchId: string
matchName: string
marketId: string
selectionName: string
price: number // Price when added
currentPrice: number // Live price (updated by polling)
stake: number // User-entered stake
factionGlyph: string
}
interface BetslipState {
selections: BetslipSelection[]
addSelection: (sel: BetslipSelection) => void
removeSelection: (marketId: string) => void
updateStake: (marketId: string, stake: number) => void
clearAll: () => void
accumulatorStake: number
setAccumulatorStake: (stake: number) => void
}
```
### 7.2 Currency Preference
Currency is tracked in URL search params (`?currency=Latinum`) per AGENTS.md deterministic URL state rule — NOT in client state. The `CurrencyProvider` simply reads from the URL.
---
## 8. Route Transition Map
```mermaid
graph TD
ROOT["__root.tsx<br/>(always rendered)"]
DASH["/ Dashboard<br/>Grid Overview"]
QUAD_LIST["/quadrants<br/>Filtered Match List"]
MATCH["/quadrants/$sectorId<br/>Live Match Detail"]
LEDGER["/ledger<br/>Wager Overview"]
ACTIVE["/ledger/active<br/>Open Wagers"]
SETTLED["/ledger/settled<br/>Settled History"]
FACTIONS["/registry/factions<br/>All Factions"]
FACTION_D["/registry/factions/$id<br/>Faction Profile"]
VENUES["/registry/venues<br/>All Venues"]
VENUE_D["/registry/venues/$id<br/>Venue Profile"]
SCHED["/schedule<br/>Upcoming Matches"]
ROOT --> DASH
ROOT --> QUAD_LIST
ROOT --> MATCH
ROOT --> LEDGER
ROOT --> SCHED
ROOT --> FACTIONS
ROOT --> VENUES
DASH -->|"click match card"| MATCH
QUAD_LIST -->|"click match"| MATCH
SCHED -->|"click scheduled"| MATCH
LEDGER --> ACTIVE
LEDGER --> SETTLED
ACTIVE -->|"click match ref"| MATCH
FACTIONS --> FACTION_D
FACTION_D -->|"click active match"| MATCH
VENUES --> VENUE_D
VENUE_D -->|"click active match"| MATCH
MATCH -->|"click market selection"| BETSLIP["Betslip (context, not route)"]
style MATCH fill:#34d399,color:#000
style BETSLIP fill:#fbbf24,color:#000
style DASH fill:#38bdf8,color:#000
```

584
reference/PROJECT.md Normal file
View file

@ -0,0 +1,584 @@
Here is a complete, self-contained `REFERENCE.md` file that captures the entire system architecture. It bridges your plaintext TanStack Start frontend with a high-performance **Go-based data loader** streaming raw **YAML telemetry** into a **Valkey** cache (the open-source, high-performance successor to Redis).
You can save this file locally to use as your architectural blueprint.
### `REFERENCE.md`
Markdown
````
# Architectural Reference: Quark's Holo-Grid Ledger
This document serves as the absolute technical reference for the intergalactic terminal sportsbook. The architecture uses a three-tier design to handle high-frequency data streams safely and efficiently:
1. **Data Layer**: Flat YAML configuration files mimicking raw sub-atomic telemetry.
2. **Ingestion Layer (Go)**: A high-performance Go routine that parses YAML files and pushes records continuously into a Valkey cache instance.
3. **Application Layer (TanStack Start)**: A type-safe, server-rendered frontend pulling directly from Valkey via server functions and background synchronization hooks.
---
## 1. System Topology
```mermaid
graph LR
subgraph Data Layer
A[(telemetry.yaml)]
</nav>
subgraph Ingestion Layer
B[Go Ingestion Worker]
end
subgraph Cache Layer
C[(Valkey Cache Instance)]
end
subgraph Presentation Layer
D[TanStack Start SSR Server] --> E[Browser Terminal UI]
end
A --> B
B -- HSET / LPUSH --> C
C -- READ --> D
````
## 2. Data Layer: Telemetry YAML Layout
Place these static definition assets under the `./data-source/` directory root.
### File: `./data-source/sectors.yaml`
YAML
```
sectors:
- id: "sector-001"
name: "Sector 001 (Earth Array)"
quadrant: "Alpha"
initial_shield_capacity: 100
kinetic_yield: 4.2
status: "LIVE"
markets:
- id: "s1-over"
name: "Kinetic Yield Over 8.5 TW"
initial_price: 1.85
- id: "s1-under"
name: "Kinetic Yield Under 8.5 TW"
initial_price: 1.95
- id: "wolf-359"
name: "Wolf 359 Outpost"
quadrant: "Alpha"
initial_shield_capacity: 34
kinetic_yield: 12.8
status: "LIVE"
markets:
- id: "w359-fail"
name: "Total Shield Failure"
initial_price: 1.40
- id: "w359-hold"
name: "Defenses Hold Perimeter"
initial_price: 2.80
```
## 3. Ingestion Layer: Go Worker Utility
This Go engine handles parsing the local YAML file, calculating artificial odds fluctuations, and writing live JSON payloads directly into **Valkey** using standard Redis protocol definitions via the `go-redis/v9` driver.
### File: `./ingestion-engine/main.go`
Go
```
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"math/rand"
"os"
"time"
"[github.com/redis/go-redis/v9](https://github.com/redis/go-redis/v9)"
"gopkg.in/yaml.v3"
)
type Market struct {
ID string `yaml:"id" json:"id"`
Name string `yaml:"name" json:"name"`
Price float64 `yaml:"initial_price" json:"price"`
Trend string `json:"trend"`
}
type Sector struct {
ID string `yaml:"id" json:"id"`
Name string `yaml:"name" json:"name"`
Quadrant string `yaml:"quadrant" json:"quadrant"`
ShieldCapacity int `yaml:"initial_shield_capacity" json:"shieldCapacity"`
KineticYield float64 `yaml:"kinetic_yield" json:"kinetic_yield"`
Status string `yaml:"status" json:"status"`
Markets []Market `yaml:"markets" json:"markets"`
}
type Config struct {
Sectors []Sector `yaml:"sectors"`
}
var ctx = context.Background()
func main() {
// 1. Connect to Valkey (Using standard Redis protocol port 6379)
valkeyClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // Default empty
DB: 0, // Default DB
})
// Test connection
if err := valkeyClient.Ping(ctx).Err(); err != nil {
log.Fatalf("Critical: Could not resolve Valkey connection: %v", err)
}
fmt.Println("> Subspace Comms Link to Valkey established.")
// 2. Read and parse the YAML file
yamlFile, err := os.ReadFile("../data-source/sectors.yaml")
if err != nil {
log.Fatalf("Error reading target file definitions: %v", err)
}
var config Config
if err := yaml.Unmarshal(yamlFile, &config); err != nil {
log.Fatalf("Error parsing configuration details: %v", err)
}
// 3. Fire the Ingestion Loop Simulation
ticker := time.NewTicker(2000 * time.Millisecond)
defer ticker.Stop()
fmt.Println("> Telemetry engine loop initiated. Streaming telemetry logs...")
for range ticker.C {
for i := range config.Sectors {
sector := &config.Sectors[i]
// Fluctuate shield capacities artificially
shieldChange := rand.Intn(5) - 2 // -2 to +2
sector.ShieldCapacity += shieldChange
if sector.ShieldCapacity > 100 {
sector.ShieldCapacity = 100
} else if sector.ShieldCapacity < 0 {
sector.ShieldCapacity = 0
}
// Modulate marker coefficients
for j := range sector.Markets {
market := &sector.Markets[j]
priceShift := (rand.Float64() * 0.2) - 0.1 // -0.10 to +0.10
oldPrice := market.Price
market.Price = oldPrice + priceShift
if market.Price < 1.05 {
market.Price = 1.05
}
if market.Price > oldPrice {
market.Trend = "up"
} else if market.Price < oldPrice {
market.Trend = "down"
} else {
market.Trend = "stable"
}
}
// Marshall state to absolute JSON for application caching efficiency
jsonPayload, err := json.Marshal(sector)
if err != nil {
log.Printf("Error marshalling schema object: %v", err)
continue
}
// Push atomic updates directly into Valkey hash set
key := fmt.Sprintf("sector:telemetry:%s", sector.ID)
err = valkeyClient.Set(ctx, key, jsonPayload, 0).Err()
if err != nil {
log.Printf("Valkey stream pipeline transmission error: %v", err)
}
}
log.Println(">> Atomic telemetry matrix block sent to Valkey pipeline cluster.")
}
}
```
## 4. Application Layer: Reading from Valkey in TanStack Start
Your frontend reads directly from the cache pool inside server execution limits. This guarantees sub-millisecond data delivery times to the rendering layer.
First, make sure you have standard client integration parameters ready (`npm i ioredis`).
### File: `./src/server/db/valkeyConnector.ts`
TypeScript
```
import Redis from 'ioredis'
// Connect to local Valkey instance over port 6379
export const valkey = new Redis({
host: '127.0.0.1',
port: 6379,
})
export async function fetchLiveSectorFromCache(sectorId: string) {
const data = await valkey.get(`sector:telemetry:${sectorId}`)
if (!data) return null
return JSON.parse(data)
}
```
### File: `./src/routes/quadrants/$sectorId.tsx`
TypeScript
```
import { createFileRoute } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { fetchLiveSectorFromCache } from '../../server/db/valkeyConnector'
import { createServerFn } from '@tanstack/react-start'
// Server RPC definition boundary
const getCachedSectorData = createServerFn({ method: 'GET' })
.input(String)
.handler(async ({ input: sectorId }) => {
return await fetchLiveSectorFromCache(sectorId)
})
export const Route = createFileRoute('/quadrants/$sectorId')({
loader: async ({ params }) => {
const initialSnapshot = await getCachedSectorData({ input: params.sectorId })
return { initialSnapshot }
},
component: TerminalGridPane,
})
function TerminalGridPane() {
const { initialSnapshot } = Route.useLoaderData()
const { sectorId } = Route.useParams()
// High-frequency client polling strategy linking right back to Valkey keyspaces
const { data: sector } = useQuery({
queryKey: ['live-telemetry', sectorId],
queryFn: () => getCachedSectorData({ input: sectorId }),
initialData: initialSnapshot,
refetchInterval: 2000,
})
if (!sector) return <pre className="text-rose-500">&gt; ERROR: MATRIX SYNC COMPROMISED</pre>
return (
<div className="border border-emerald-500/30 p-4 bg-black font-mono">
<h3 className="text-emerald-400 font-bold mb-2">&gt; COGNITIVE STREAM: {sector.name}</h3>
<div className="space-y-1 text-xs">
<p>QUADRANT TELEMETRY: {sector.quadrant}</p>
<p>SHIELD EFFICIENCY: <span className="text-amber-500">{sector.shieldCapacity}%</span></p>
</div>
{/* Dynamic Betting Selections Render */}
<div className="mt-4 grid grid-cols-1 gap-2">
{sector.markets.map((m: any) => (
<button key={m.id} className="p-2 border border-slate-800 text-left hover:border-emerald-500 flex justify-between">
<span>{m.name}</span>
<span className={m.trend === 'up' ? 'text-emerald-400' : m.trend === 'down' ? 'text-rose-400' : 'text-slate-400'}>
[{m.price.toFixed(2)}]
</span>
</button>
))}
</div>
</div>
)
}
```
To bring factions into your data, you just need to explicitly name the two competing forces inside your **Sectors**. In a sportsbook, these are your **Home Team** and **Away Team** (or Competitor A and Competitor B).
By adding a `factions` object to your data model, your Go engine can calculate specific tactical advantages, and your TanStack Start frontend can render a true _"Versus"_ scoreboard layout.
Here is how you inject factions across your entire architecture.
## 1. Update the Data Layer (`data-source/sectors.yaml`)
Add a `factions` object to each sector inside your YAML file. Give each faction a clear name and track their individual shield capacities separately instead of having just one global shield score.
YAML
```
sectors:
- id: "sector-001"
name: "Sector 001 (Earth Array)"
quadrant: "Alpha"
kinetic_yield: 4.2
status: "LIVE"
factions:
home:
name: "Starfleet Command"
shield_capacity: 100
away:
name: "The Borg Collective"
shield_capacity: 100
markets:
- id: "s1-home-win"
name: "Starfleet Victory"
initial_price: 2.10
- id: "s1-away-win"
name: "Borg Assimilation"
initial_price: 1.65
```
## 2. Update the Go Ingestion Engine (`main.go`)
Now, update your Go structs to parse these factions, and modify the engine loop so the factions actively damage each other's shields.
Go
```
type Faction struct {
Name string `yaml:"name" json:"name"`
ShieldCapacity int `yaml:"shield_capacity" json:"shieldCapacity"`
}
type FactionsSchema struct {
Home Faction `yaml:"home" json:"home"`
Away Faction `yaml:"away" json:"away"`
}
type Sector struct {
ID string `yaml:"id" json:"id"`
Name string `yaml:"name" json:"name"`
Quadrant string `yaml:"quadrant" json:"quadrant"`
KineticYield float64 `yaml:"kinetic_yield" json:"kinetic_yield"`
Status string `yaml:"status" json:"status"`
Factions FactionsSchema `yaml:"factions" json:"factions"`
Markets []Market `yaml:"markets" json:"markets"`
}
// Inside the background loop, simulate combat between the factions:
for i := range config.Sectors {
sector := &config.Sectors[i]
// Randomly choose which faction takes a hit this tick
if rand.Float64() > 0.5 {
sector.Factions.Home.ShieldCapacity -= rand.Intn(8) // Borg fires on Starfleet
} else {
sector.Factions.Away.ShieldCapacity -= rand.Intn(5) // Starfleet fires on Borg
}
// Clamp values between 0 and 100
if sector.Factions.Home.ShieldCapacity < 0 { sector.Factions.Home.ShieldCapacity = 0 }
if sector.Factions.Away.ShieldCapacity < 0 { sector.Factions.Away.ShieldCapacity = 0 }
// Dynamically shift odds based on who has higher shields
for j := range sector.Markets {
market := &sector.Markets[j]
// If Home shields drop below Away shields, increase Home odds multiplier (underdog status)
if sector.Factions.Home.ShieldCapacity < sector.Factions.Away.ShieldCapacity {
if market.ID == "s1-home-win" { market.Price += 0.05 }
}
}
}
```
## 3. Render the Confrontation in the UI (`$sectorId.tsx`)
Now that Valkey is serving the detailed faction data, you can build a plaintext split-panel scoreboard at the top of your main grid layout.
TypeScript
```
function SectorConsoleView() {
const { sectorId } = Route.useParams()
const { data: sector } = useQuery({
queryKey: ['live-telemetry', sectorId],
queryFn: () => getCachedSectorData({ input: sectorId }),
refetchInterval: 2000,
})
if (!sector) return <pre>&gt; ACCESSING SYSTEM NET...</pre>
return (
<div className="space-y-4">
{/* Dynamic Versus Arena Header */}
<div className="border border-slate-800 bg-slate-950 p-4 font-mono text-center">
<div className="text-[10px] text-slate-500 mb-2">ENGAGEMENT MATRIX // {sector.name}</div>
<div className="grid grid-cols-7 items-center text-sm font-bold">
{/* Home Faction */}
<div className="col-span-3 text-right">
<span className="text-amber-400 block">{sector.factions.home.name}</span>
<span className="text-xs text-slate-400">SHD: {sector.factions.home.shieldCapacity}%</span>
</div>
{/* VS Divider */}
<div className="col-span-1 text-slate-600 tracking-widest animate-pulse text-xs">VS</div>
{/* Away Faction */}
<div className="col-span-3 text-left">
<span className="text-rose-400 block">{sector.factions.away.name}</span>
<span className="text-xs text-slate-400">SHD: {sector.factions.away.shieldCapacity}%</span>
</div>
</div>
{/* Global Match Clock / Energy Tracker */}
<div className="mt-3 border-t border-slate-900 pt-2 text-xs text-slate-500 flex justify-between px-4">
<span>TOTAL ENERGY RELEASED:</span>
<span className="text-emerald-400 font-bold">{sector.kinetic_yield.toFixed(1)} TW</span>
</div>
</div>
{/* Markets Selection Grid sits underneath this header view */}
</div>
)
}
```
Here is your consolidated, terminal-ready command dictionary mapping our **Star Trek Telemetry** to standard **Sportsbook Architecture**:
| **Space-Grid Term** | **Real-World Sportsbook Translation** | **Practical Example** |
| ------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **Quadrant** | **Sport Category / League** | _Alpha Quadrant_ (Matches your top-level category menu, like filtering for Premier League or NBA). |
| **Sector** | **Individual Match / Fixture** | _Sector 001_ or _Wolf 359_ (The active "game page" where an event is currently happening). |
| **Factions** | **Teams / Contenders** | _Starfleet Command (Home)_ vs. _The Borg Collective (Away)_ (The competitors squaring off inside the arena). |
| **Shield Capacity** | **Live Defensive Metric / Health** | _Starfleet Shields: 74%_ (Acts like an active match timeline indicator, showing which side is taking damage or losing control). |
| **Kinetic Yield** | **Live In-Play Match Score** | _14.2 Terawatts released_ (The cumulative scoreboard counter that grows as action happens, like total points scored). |
| **Market** | **Betting Line / Wager Type** | _Faction Victory_ or _Total Kinetic Yield Over/Under_ (The explicit proposal lines a user risks their Latinum on). |
| **Latinum** | **Account Balance Base Currency** | _Gold-Pressed Latinum_ (The account balance backing your betslip tokens). |
| **Subspace Interruption** | **Odds Change Suspended Lockout** | _Market Suspended_ (The background Valkey cache triggers an automatic input lockout when a sudden scoring event happens). |
To generate a continuous, realistic stream of sports matches (Sectors) and betting options (Markets), you shouldn't hardcode individual events. Instead, you need a stable, static **Taxonomy Base Data** file.
Think of this base taxonomy file as a template library. Your Go ingestion engine will read this library, pick two opposing factions, assign them a sector arena, and generate the live match data dynamically.
Here is the structured YAML blueprint for your taxonomy database and how the generation engine maps it out.
## 1. The Core Taxonomy Configuration (`data-source/taxonomy.yaml`)
This file holds your static master rules. It defines what factions exist, which quadrants they belong to, their natural rivalries, and standard betting templates.
YAML
```
# data-source/taxonomy.yaml
quadrants:
- id: "alpha"
name: "Alpha Quadrant"
factions:
- id: "starfleet"
name: "Starfleet Command"
type: "defensive"
- id: "borg"
name: "The Borg Collective"
type: "aggressive"
- id: "cardassians"
name: "Cardassian Union"
type: "tactical"
- id: "beta"
name: "Beta Quadrant"
factions:
- id: "klingons"
name: "Klingon Empire"
type: "aggressive"
- id: "romulans"
name: "Romulan Star Empire"
type: "tactical"
# Standard templates used by the generator to instantiate structural betting lines
market_templates:
- id: "match-outcomes"
name: "Combat Resolution"
selections:
- type: "home-win"
suffix: "Decisive Victory"
- type: "away-win"
suffix: "Decisive Victory"
- id: "kinetic-over-under"
name: "Total Kinetic Yield"
selections:
- type: "over"
name: "Over 8.5 Terawatts"
base_price: 1.85
- type: "under"
name: "Under 8.5 Terawatts"
base_price: 1.85
# A list of location names the engine can pick to host matches
sector_pool:
- id: "wolf-359"
name: "Wolf 359 Outpost"
quadrant: "alpha"
- id: "sector-001"
name: "Sector 001 (Earth Core)"
quadrant: "alpha"
- id: "narendra-3"
name: "Narendra III Outpost"
quadrant: "beta"
```
## 2. The Generation Blueprint: From Rules to Live Data
When your Go ingestion engine spins up, it loops through this taxonomy file to generate actual live events using an instantiation pattern:
Plaintext
```
[ TAXONOMY DEFINITION ]
▼ (Go Generator Engine)
1. Pick Location: "Wolf 359" (Alpha)
2. Pick Home Faction: "Starfleet Command"
3. Pick Away Faction: "The Borg Collective"
4. Hydrate Market Templates: Substitute Faction names into strings
[ LIVE VALKEY DATA STRUCTURE ]
```
## 3. The Go Generation Logic (Conceptual Snippet)
This is how your Go ingestion worker takes the abstract templates from `taxonomy.yaml` and handles generating real live event instances:
Go
```
// Psuedocode for the Go template instantiation loop
func GenerateLiveEvent(location SectorPoolItem, home Faction, away Faction, templates []MarketTemplate) Sector {
var generatedMarkets []Market
for _, tmpl := range templates {
if tmpl.ID == "match-outcomes" {
generatedMarkets = append(generatedMarkets, Market{
ID: location.ID + "-moneyline-home",
Name: home.Name + " " + tmpl.Selections[0].Suffix, // "Starfleet Command Decisive Victory"
Price: 2.00, // Balanced starting base odds
})
generatedMarkets = append(generatedMarkets, Market{
ID: location.ID + "-moneyline-away",
Name: away.Name + " " + tmpl.Selections[1].Suffix, // "The Borg Collective Decisive Victory"
Price: 1.80, // Borg slight favorite based on aggressive profile
})
}
}
return Sector{
ID: location.ID,
Name: location.Name,
Quadrant: location.Quadrant,
Status: "LIVE",
Factions: FactionsSchema{
Home: FactionState{Name: home.Name, ShieldCapacity: 100},
Away: FactionState{Name: away.Name, ShieldCapacity: 100},
},
Markets: generatedMarkets,
}
}
```
By organizing your database logic this way, you can add new teams (factions) or new stadiums (sectors) directly to your `taxonomy.yaml` file without altering a single line of your Go simulation logic or your TanStack Start React components. Everything down the wire becomes purely data-driven!

36
tongo-core/Makefile Normal file
View file

@ -0,0 +1,36 @@
.PHONY: build run validate test clean fmt lint
BINARY_NAME=tongo
GO=go
## build: Compile the Tongo Core binary
build:
$(GO) build -o bin/$(BINARY_NAME) ./cmd/tongo
## run: Build and run the simulation daemon
run: build
./bin/$(BINARY_NAME) serve
## validate: Validate the taxonomy configuration
validate: build
./bin/$(BINARY_NAME) validate
## test: Run all tests
test:
$(GO) test ./... -v
## clean: Remove build artifacts
clean:
rm -rf bin/
## fmt: Format all Go source files
fmt:
$(GO) fmt ./...
## lint: Run go vet
lint:
$(GO) vet ./...
## help: Show this help
help:
@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'

47
tongo-core/README.md Normal file
View file

@ -0,0 +1,47 @@
# 🎰 TONGO CORE
Tongo Core is the Star Trek-inspired live simulation and probability engine powering **Quark's Holo-Grid Ledger**. It operates as a background daemon, simulating active space-grid combat, generating random in-play events, recalculating live market odds, and writing JSON telemetry directly to Valkey for client-side consumption.
---
## 🏛 Architecture: Onion Clean Architecture
The engine uses a strict clean architecture (onion pattern) with clean separation between layers:
- **Domain Layer (`internal/domain/`)**: Pure domain models (`entity/`) and output/input interface definitions (`port/`). Free of external frameworks or dependencies.
- **Application Layer (`internal/application/`)**: Application services coordinating domain logic (match scheduling, combat resolution, probability calculation, markets).
- **Infrastructure Layer (`internal/infrastructure/`)**: Concrete implementations of ports (Valkey adapter, YAML loader, Viper config).
---
## 🚀 Getting Started
### Prerequisites
- Go 1.23+
- Valkey or Redis running on port 6379
### Commands
Run using the `Makefile`:
```bash
# Build the binary
make build
# Run the simulation daemon
make run
# Validate taxonomy config
make validate
# Run tests
make test
```
### CLI Interface
Direct CLI commands:
- `tongo serve`: Start the simulation engine daemon.
- `tongo validate`: Parse and check structure of `taxonomy.yaml`.
- `tongo version`: Display current version.

BIN
tongo-core/bin/tongo Executable file

Binary file not shown.

View file

@ -0,0 +1,24 @@
package cli
import (
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "tongo",
Short: "Tongo Core — Quark's Holo-Grid Ledger Simulation Engine",
Long: `Tongo Core is the simulation engine powering Quark's Holo-Grid Ledger.
It generates live match events, simulates combat, calculates real-time
odds, and streams telemetry data into Valkey for the frontend to consume.
"The riskier the road, the greater the profit." Rule of Acquisition #62`,
}
// Execute runs the root cobra command.
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}

View file

@ -0,0 +1,28 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
)
var serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the Tongo Core simulation daemon",
Long: `Starts the main simulation loop. Reads taxonomy data, connects to Valkey,
and begins generating live match events on a 2-second tick cycle.
The daemon runs until interrupted (SIGINT/SIGTERM).`,
RunE: runServe,
}
func init() {
rootCmd.AddCommand(serveCmd)
}
func runServe(cmd *cobra.Command, args []string) error {
fmt.Println("> Subspace Comms Link initializing...")
fmt.Println("> Tongo Core — Simulation Engine v0.1.0")
fmt.Println("> Awaiting implementation. All onion modules scaffolded.")
return nil
}

View file

@ -0,0 +1,25 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
)
var validateCmd = &cobra.Command{
Use: "validate",
Short: "Validate the taxonomy YAML file",
Long: `Parses the taxonomy YAML file and reports any structural errors
without starting the simulation engine.`,
RunE: runValidate,
}
func init() {
rootCmd.AddCommand(validateCmd)
}
func runValidate(cmd *cobra.Command, args []string) error {
fmt.Println("> Validating taxonomy configuration...")
fmt.Println("> Validation not yet implemented.")
return nil
}

View file

@ -0,0 +1,22 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
)
// Version of the tongo application.
const Version = "0.1.0"
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the Tongo Core version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Tongo Core v%s\n", Version)
},
}
func init() {
rootCmd.AddCommand(versionCmd)
}

View file

@ -0,0 +1,7 @@
package main
import "github.com/costap/quarkshologridledger/tongo-core/cmd/tongo/cli"
func main() {
cli.Execute()
}

View file

@ -0,0 +1,41 @@
# Tongo Core Configuration
# Quark's Holo-Grid Ledger — Simulation Engine
#
# Override any value with TONGO_ prefixed environment variables.
# Example: TONGO_VALKEY_HOST=redis.example.com
daemon:
tick_interval_ms: 2000
log_level: "info"
valkey:
host: "localhost"
port: 6379
password: ""
db: 0
engine:
target_active_matches: 8
max_active_matches: 12
min_active_matches: 4
pre_match_duration_ticks: 30
min_match_duration_ticks: 60
max_match_duration_ticks: 150
resolving_duration_ticks: 3
taxonomy_path: "../data-source/taxonomy.yaml"
combat:
damage_scaling_factor: 0.1
damage_variance_low: 0.6
damage_variance_high: 1.4
critical_hit_chance: 0.05
critical_hit_multiplier: 3.0
markets:
primary_margin: 1.06
secondary_margin: 1.075
exotic_margin: 1.12
min_price: 1.01
max_price: 51.00
min_probability: 0.02
max_probability: 0.98

29
tongo-core/go.mod Normal file
View file

@ -0,0 +1,29 @@
module github.com/costap/quarkshologridledger/tongo-core
go 1.23
require (
github.com/redis/go-redis/v9 v9.7.3
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

68
tongo-core/go.sum Normal file
View file

@ -0,0 +1,68 @@
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,65 @@
package application
import (
"context"
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/port"
)
// Engine is the top-level orchestrator for the Tongo Core simulation.
// It coordinates the scheduler, simulator, event generator, probability engine, and cache.
type Engine struct {
scheduler port.SchedulerService
matchmaker port.MatchmakerService
simulator port.SimulatorService
events port.EventGeneratorService
probability port.ProbabilityService
markets port.MarketService
cache port.CachePort
taxonomy port.TaxonomyPort
logger *slog.Logger
}
// EngineDeps holds all dependencies for engine construction.
type EngineDeps struct {
Scheduler port.SchedulerService
Matchmaker port.MatchmakerService
Simulator port.SimulatorService
Events port.EventGeneratorService
Probability port.ProbabilityService
Markets port.MarketService
Cache port.CachePort
Taxonomy port.TaxonomyPort
Logger *slog.Logger
}
// NewEngine creates a new Engine with all service dependencies.
func NewEngine(deps EngineDeps) *Engine {
return &Engine{
scheduler: deps.Scheduler,
matchmaker: deps.Matchmaker,
simulator: deps.Simulator,
events: deps.Events,
probability: deps.Probability,
markets: deps.Markets,
cache: deps.Cache,
taxonomy: deps.Taxonomy,
logger: deps.Logger,
}
}
// Run starts the main simulation loop. It blocks until ctx is cancelled.
func (e *Engine) Run(ctx context.Context) error {
e.logger.Info("Starting Tongo Core simulation loop")
// TODO: implement actual tick logic
<-ctx.Done()
return nil
}
// Shutdown gracefully stops the engine.
func (e *Engine) Shutdown() error {
e.logger.Info("Stopping Tongo Core simulation engine")
return nil
}

View file

@ -0,0 +1,25 @@
package events
import (
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
)
// Generator produces random in-match events.
type Generator struct {
logger *slog.Logger
}
// New creates a new Generator instance.
func New(logger *slog.Logger) *Generator {
return &Generator{
logger: logger,
}
}
// Roll checks for and generates any random events this tick.
func (g *Generator) Roll(match *entity.Match, tickResult entity.TickResult) []entity.MatchEvent {
g.logger.Debug("Rolling for random events", "id", match.ID)
return nil
}

View file

@ -0,0 +1,34 @@
package markets
import (
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
)
// Service manages market hydration, pricing, and suspension.
type Service struct {
logger *slog.Logger
}
// New creates a new Service instance.
func New(logger *slog.Logger) *Service {
return &Service{
logger: logger,
}
}
// HydrateMarkets generates markets for a new match from templates.
func (s *Service) HydrateMarkets(match *entity.Match, templates []entity.MarketTemplate) {
s.logger.Debug("Hydrating markets from templates for match", "id", match.ID)
}
// UpdatePrices converts probabilities to prices with margin.
func (s *Service) UpdatePrices(match *entity.Match) {
s.logger.Debug("Updating market prices for match", "id", match.ID)
}
// SuspendAll suspends all markets in a match.
func (s *Service) SuspendAll(match *entity.Match, durationTicks int) {
s.logger.Info("Suspending all markets for match", "id", match.ID, "duration", durationTicks)
}

View file

@ -0,0 +1,41 @@
package matchmaker
import (
"context"
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/port"
)
// Matchmaker pairs factions and selects venues to create new matches.
type Matchmaker struct {
quadrants []entity.Quadrant
venues []entity.Venue
templates []entity.MarketTemplate
markets port.MarketService
logger *slog.Logger
}
// New creates a new Matchmaker instance.
func New(
quadrants []entity.Quadrant,
venues []entity.Venue,
templates []entity.MarketTemplate,
markets port.MarketService,
logger *slog.Logger,
) *Matchmaker {
return &Matchmaker{
quadrants: quadrants,
venues: venues,
templates: templates,
markets: markets,
logger: logger,
}
}
// CreateMatch generates a new match by selecting factions and a venue.
func (m *Matchmaker) CreateMatch(ctx context.Context) (*entity.Match, error) {
m.logger.Debug("Creating new match")
return &entity.Match{}, nil
}

View file

@ -0,0 +1,59 @@
package probability
import (
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
)
// Calculator recalculates market probabilities from match state.
type Calculator struct {
logger *slog.Logger
}
// New creates a new Calculator instance.
func New(logger *slog.Logger) *Calculator {
return &Calculator{
logger: logger,
}
}
// RecalculateAll recomputes probabilities for all markets in a match.
func (c *Calculator) RecalculateAll(match *entity.Match) {
c.logger.Debug("Recalculating all market probabilities for match", "id", match.ID)
}
// matchWinner calculates Model A (Match Winner).
func (c *Calculator) matchWinner(match *entity.Match) (homeProb, awayProb float64) {
return 0.5, 0.5
}
// kineticYield calculates Model B (Total Kinetic Yield).
func (c *Calculator) kineticYield(match *entity.Match, line float64) (overProb, underProb float64) {
return 0.5, 0.5
}
// firstBreach calculates Model C (First Shield Breach).
func (c *Calculator) firstBreach(match *entity.Match) (homeFirstProb, awayFirstProb float64) {
return 0.5, 0.5
}
// duration calculates Model D (Engagement Duration).
func (c *Calculator) duration(match *entity.Match, line float64) (overProb, underProb float64) {
return 0.5, 0.5
}
// comeback calculates Model E (Comeback Victory).
func (c *Calculator) comeback(match *entity.Match) (homeProb, awayProb float64) {
return 0.05, 0.05
}
// mutualDestruction calculates Model F (Mutual Destruction).
func (c *Calculator) mutualDestruction(match *entity.Match) float64 {
return 0.01
}
// subspaceAnomaly calculates Model G (Subspace Anomaly).
func (c *Calculator) subspaceAnomaly(match *entity.Match) (anomalyProb, cleanProb float64) {
return 0.1, 0.9
}

View file

@ -0,0 +1,42 @@
package scheduler
import (
"context"
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/port"
)
// Scheduler manages the lifecycle of all active matches.
type Scheduler struct {
matchmaker port.MatchmakerService
matches []*entity.Match
logger *slog.Logger
}
// New creates a new Scheduler instance.
func New(matchmaker port.MatchmakerService, logger *slog.Logger) *Scheduler {
return &Scheduler{
matchmaker: matchmaker,
matches: make([]*entity.Match, 0),
logger: logger,
}
}
// Tick advances the scheduler by one cycle, creating or retiring matches as needed.
func (s *Scheduler) Tick(ctx context.Context) error {
s.logger.Debug("Scheduler tick")
return nil
}
// ActiveMatches returns all currently active matches.
func (s *Scheduler) ActiveMatches() []*entity.Match {
return s.matches
}
// SeedMatches creates initial matches up to the target count.
func (s *Scheduler) SeedMatches(ctx context.Context, count int) error {
s.logger.Info("Seeding initial matches", "count", count)
return nil
}

View file

@ -0,0 +1,27 @@
package simulator
import (
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
)
// Simulator resolves combat ticks between factions.
type Simulator struct {
typeAdvantage TypeAdvantageMatrix
logger *slog.Logger
}
// New creates a new Simulator instance.
func New(logger *slog.Logger) *Simulator {
return &Simulator{
typeAdvantage: NewTypeAdvantageMatrix(),
logger: logger,
}
}
// ResolveTick processes one combat tick for a match.
func (s *Simulator) ResolveTick(match *entity.Match) entity.TickResult {
s.logger.Debug("Simulating combat tick for match", "id", match.ID)
return entity.TickResult{}
}

View file

@ -0,0 +1,22 @@
package simulator
import "github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
// TypeAdvantageMatrix maps attacker type × defender type to a damage multiplier.
type TypeAdvantageMatrix map[entity.FactionType]map[entity.FactionType]float64
// NewTypeAdvantageMatrix returns the default type advantage matrix.
func NewTypeAdvantageMatrix() TypeAdvantageMatrix {
// The real implementation will hydrate this with the rock-paper-scissors values
return make(TypeAdvantageMatrix)
}
// Get returns the advantage multiplier for the given attacker/defender pair.
func (m TypeAdvantageMatrix) Get(attacker, defender entity.FactionType) float64 {
if attackerMap, ok := m[attacker]; ok {
if val, ok := attackerMap[defender]; ok {
return val
}
}
return 1.0
}

View file

@ -0,0 +1,41 @@
package entity
// EventCategory classifies the type of random event.
type EventCategory string
const (
EventCategoryCombat EventCategory = "combat"
EventCategoryEnvironmental EventCategory = "environmental"
EventCategoryFaction EventCategory = "faction"
EventCategoryMarket EventCategory = "market"
)
// EventSeverity indicates the impact level.
type EventSeverity string
const (
EventSeverityMinor EventSeverity = "minor"
EventSeverityMajor EventSeverity = "major"
EventSeverityCritical EventSeverity = "critical"
)
// MatchEvent represents something that happened during a match.
type MatchEvent struct {
ID string `json:"id"`
TemplateID string `json:"templateId"`
Name string `json:"name"`
Description string `json:"description"`
Tick int `json:"tick"`
Category EventCategory `json:"category"`
Severity EventSeverity `json:"severity"`
Effects []EventEffect `json:"effects"`
Duration int `json:"duration"`
}
// EventEffect describes a single modification caused by an event.
type EventEffect struct {
Target string `json:"target"`
Stat string `json:"stat"`
Operation string `json:"operation"`
Value float64 `json:"value"`
}

View file

@ -0,0 +1,50 @@
package entity
// Faction represents a team/contender in the sportsbook.
type Faction struct {
ID string `json:"id"`
Name string `json:"name"`
Abbreviation string `json:"abbreviation"`
Type FactionType `json:"type"`
Homeworld string `json:"homeworld"`
ColorCode string `json:"colorCode"`
EmblemGlyph string `json:"emblemGlyph"`
BaseShieldStrength int `json:"baseShieldStrength"`
BaseAttackPower int `json:"baseAttackPower"`
BaseEvasion int `json:"baseEvasion"`
RivalryModifiers map[string]float64 `json:"rivalryModifiers"`
}
// FactionType defines the combat archetype.
type FactionType string
const (
FactionTypeAggressive FactionType = "aggressive"
FactionTypeDefensive FactionType = "defensive"
FactionTypeTactical FactionType = "tactical"
FactionTypeInfiltrator FactionType = "infiltrator"
FactionTypeSwarm FactionType = "swarm"
)
// FactionState represents a faction's live combat state within a match.
type FactionState struct {
ID string `json:"id"`
Name string `json:"name"`
Abbreviation string `json:"abbreviation"`
Type FactionType `json:"type"`
Glyph string `json:"glyph"`
ColorCode string `json:"colorCode"`
ShieldCapacity float64 `json:"shieldCapacity"`
EffectiveAttack float64 `json:"effectiveAttack"`
EffectiveEvasion float64 `json:"effectiveEvasion"`
ActiveEffects []ActiveEffect `json:"activeEffects"`
}
// ActiveEffect represents a temporary buff/debuff on a faction.
type ActiveEffect struct {
Name string `json:"name"`
RemainingTicks int `json:"remainingTicks"`
Stat string `json:"stat"`
Operation string `json:"operation"`
Value float64 `json:"value"`
}

View file

@ -0,0 +1,49 @@
package entity
// MarketCategory classifies a market by risk tier.
type MarketCategory string
const (
MarketCategoryPrimary MarketCategory = "primary"
MarketCategorySecondary MarketCategory = "secondary"
MarketCategoryExotic MarketCategory = "exotic"
)
// MarketStatus tracks whether a market is tradeable.
type MarketStatus string
const (
MarketStatusOpen MarketStatus = "OPEN"
MarketStatusSuspended MarketStatus = "SUSPENDED"
MarketStatusClosed MarketStatus = "CLOSED"
)
// Market represents a single betting market within a match.
type Market struct {
ID string `json:"id"`
TemplateID string `json:"templateId"`
Name string `json:"name"`
Category MarketCategory `json:"category"`
Price float64 `json:"price"`
PreviousPrice float64 `json:"previousPrice"`
Trend string `json:"trend"`
Status MarketStatus `json:"status"`
ImpliedProbability float64 `json:"impliedProbability"`
}
// MarketTemplate defines the blueprint for generating markets.
type MarketTemplate struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Category MarketCategory `json:"category"`
Selections []SelectionTemplate `json:"selections"`
}
// SelectionTemplate is a single outcome option within a market template.
type SelectionTemplate struct {
Type string `json:"type"`
Name string `json:"name,omitempty"`
Suffix string `json:"suffix,omitempty"`
BasePrice float64 `json:"basePrice"`
}

View file

@ -0,0 +1,43 @@
package entity
// MatchStatus represents the lifecycle phase of a match.
type MatchStatus string
const (
MatchStatusScheduled MatchStatus = "SCHEDULED"
MatchStatusPreMatch MatchStatus = "PRE_MATCH"
MatchStatusLive MatchStatus = "LIVE"
MatchStatusResolving MatchStatus = "RESOLVING"
MatchStatusSettled MatchStatus = "SETTLED"
)
// Match represents a live engagement between two factions at a venue.
type Match struct {
ID string `json:"id"`
Name string `json:"name"`
Quadrant string `json:"quadrant"`
Status MatchStatus `json:"status"`
Tick int `json:"tick"`
MaxTicks int `json:"maxTicks"`
KineticYield float64 `json:"kinetic_yield"`
Venue Venue `json:"venue"`
Factions MatchFactions `json:"factions"`
Markets []Market `json:"markets"`
RecentEvents []MatchEvent `json:"recentEvents"`
}
// MatchFactions holds the two competing factions.
type MatchFactions struct {
Home FactionState `json:"home"`
Away FactionState `json:"away"`
}
// TickResult captures the outcome of a single combat tick.
type TickResult struct {
HomeDamageDealt float64
AwayDamageDealt float64
HomeShieldsAfter float64
AwayShieldsAfter float64
KineticYieldDelta float64
EventsFired []MatchEvent
}

View file

@ -0,0 +1,11 @@
package entity
// Quadrant represents a galactic league division.
type Quadrant struct {
ID string `json:"id"`
Name string `json:"name"`
Designation string `json:"designation"`
ColorCode string `json:"colorCode"`
BaseVolatility float64 `json:"baseVolatility"`
Factions []Faction `json:"factions"`
}

View file

@ -0,0 +1,15 @@
package entity
// Venue represents a match location from the sector pool.
type Venue struct {
ID string `json:"id"`
Name string `json:"name"`
Quadrant string `json:"quadrant"`
Coordinates string `json:"coordinates"`
ShieldModifier float64 `json:"shieldModifier"`
DamageModifier float64 `json:"damageModifier"`
EvasionModifier float64 `json:"evasionModifier"`
SpecialEventChance float64 `json:"specialEventChance"`
Status string `json:"status"`
Tags []string `json:"tags"`
}

View file

@ -0,0 +1,28 @@
package port
import (
"context"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
)
// CachePort defines the interface for the cache layer (Valkey/Redis).
type CachePort interface {
// PushMatchState writes the full match telemetry to the cache.
PushMatchState(ctx context.Context, match *entity.Match) error
// PushEvent appends an event to the match's event log.
PushEvent(ctx context.Context, matchID string, event entity.MatchEvent) error
// UpdateActiveIndex updates the set of currently active match IDs.
UpdateActiveIndex(ctx context.Context, matchIDs []string) error
// UpdateQuadrantIndex updates the set of active matches for a specific quadrant.
UpdateQuadrantIndex(ctx context.Context, quadrantID string, matchIDs []string) error
// Ping checks that the cache connection is alive.
Ping(ctx context.Context) error
// Close gracefully shuts down the cache connection.
Close() error
}

View file

@ -0,0 +1,55 @@
package port
import (
"context"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
)
// SchedulerService defines the match lifecycle management interface.
type SchedulerService interface {
// Tick advances the scheduler by one cycle, creating or retiring matches as needed.
Tick(ctx context.Context) error
// ActiveMatches returns all currently active matches.
ActiveMatches() []*entity.Match
// SeedMatches creates initial matches up to the target count.
SeedMatches(ctx context.Context, count int) error
}
// MatchmakerService defines the faction pairing and venue selection interface.
type MatchmakerService interface {
// CreateMatch generates a new match by selecting factions and a venue.
CreateMatch(ctx context.Context) (*entity.Match, error)
}
// SimulatorService defines the combat tick resolution interface.
type SimulatorService interface {
// ResolveTick processes one combat tick for a match.
ResolveTick(match *entity.Match) entity.TickResult
}
// EventGeneratorService defines the random event system interface.
type EventGeneratorService interface {
// Roll checks for and generates any random events this tick.
Roll(match *entity.Match, tickResult entity.TickResult) []entity.MatchEvent
}
// ProbabilityService defines the odds calculation interface.
type ProbabilityService interface {
// RecalculateAll recomputes probabilities for all markets in a match.
RecalculateAll(match *entity.Match)
}
// MarketService defines the market management interface.
type MarketService interface {
// HydrateMarkets generates markets for a new match from templates.
HydrateMarkets(match *entity.Match, templates []entity.MarketTemplate)
// UpdatePrices converts probabilities to prices with margin.
UpdatePrices(match *entity.Match)
// SuspendAll suspends all markets in a match.
SuspendAll(match *entity.Match, durationTicks int)
}

View file

@ -0,0 +1,15 @@
package port
import "github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
// TaxonomyPort defines the interface for loading taxonomy configuration data.
type TaxonomyPort interface {
// LoadQuadrants returns all quadrant definitions including their factions.
LoadQuadrants() ([]entity.Quadrant, error)
// LoadVenues returns all sector pool venue definitions.
LoadVenues() ([]entity.Venue, error)
// LoadMarketTemplates returns all market template definitions.
LoadMarketTemplates() ([]entity.MarketTemplate, error)
}

View file

@ -0,0 +1,66 @@
package cache
import (
"context"
"fmt"
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
"github.com/costap/quarkshologridledger/tongo-core/internal/infrastructure/config"
"github.com/redis/go-redis/v9"
)
// ValkeyAdapter implements port.CachePort using a Valkey (Redis-compatible) client.
type ValkeyAdapter struct {
client *redis.Client
logger *slog.Logger
}
// New creates a new ValkeyAdapter and tests the connection.
func New(cfg config.ValkeyConfig, logger *slog.Logger) (*ValkeyAdapter, error) {
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
Password: cfg.Password,
DB: cfg.DB,
})
return &ValkeyAdapter{
client: client,
logger: logger,
}, nil
}
// PushMatchState writes the full match telemetry to the cache.
func (v *ValkeyAdapter) PushMatchState(ctx context.Context, match *entity.Match) error {
v.logger.Debug("Pushing match state to Valkey", "id", match.ID)
return nil
}
// PushEvent appends an event to the match's event log.
func (v *ValkeyAdapter) PushEvent(ctx context.Context, matchID string, event entity.MatchEvent) error {
v.logger.Debug("Pushing event to Valkey", "matchId", matchID, "eventId", event.ID)
return nil
}
// UpdateActiveIndex updates the set of currently active match IDs.
func (v *ValkeyAdapter) UpdateActiveIndex(ctx context.Context, matchIDs []string) error {
v.logger.Debug("Updating active match index in Valkey", "count", len(matchIDs))
return nil
}
// UpdateQuadrantIndex updates the set of active matches for a specific quadrant.
func (v *ValkeyAdapter) UpdateQuadrantIndex(ctx context.Context, quadrantID string, matchIDs []string) error {
v.logger.Debug("Updating quadrant index in Valkey", "quadrant", quadrantID, "count", len(matchIDs))
return nil
}
// Ping checks that the cache connection is alive.
func (v *ValkeyAdapter) Ping(ctx context.Context) error {
return v.client.Ping(ctx).Err()
}
// Close gracefully shuts down the cache connection.
func (v *ValkeyAdapter) Close() error {
v.logger.Info("Closing Valkey connection")
return v.client.Close()
}

View file

@ -0,0 +1,117 @@
package config
import (
"fmt"
"github.com/spf13/viper"
)
// Config holds all Tongo Core configuration values.
type Config struct {
Daemon DaemonConfig `mapstructure:"daemon"`
Valkey ValkeyConfig `mapstructure:"valkey"`
Engine EngineConfig `mapstructure:"engine"`
Combat CombatConfig `mapstructure:"combat"`
Markets MarketsConfig `mapstructure:"markets"`
}
// DaemonConfig holds parameters for the daemon runtime.
type DaemonConfig struct {
TickIntervalMs int `mapstructure:"tick_interval_ms"`
LogLevel string `mapstructure:"log_level"`
}
// ValkeyConfig holds connections settings for Valkey/Redis.
type ValkeyConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
// EngineConfig holds high-level simulation settings.
type EngineConfig struct {
TargetActiveMatches int `mapstructure:"target_active_matches"`
MaxActiveMatches int `mapstructure:"max_active_matches"`
MinActiveMatches int `mapstructure:"min_active_matches"`
PreMatchDurationTicks int `mapstructure:"pre_match_duration_ticks"`
MinMatchDurationTicks int `mapstructure:"min_match_duration_ticks"`
MaxMatchDurationTicks int `mapstructure:"max_match_duration_ticks"`
ResolvingDurationTicks int `mapstructure:"resolving_duration_ticks"`
TaxonomyPath string `mapstructure:"taxonomy_path"`
}
// CombatConfig holds modifiers for combat simulation.
type CombatConfig struct {
DamageScalingFactor float64 `mapstructure:"damage_scaling_factor"`
DamageVarianceLow float64 `mapstructure:"damage_variance_low"`
DamageVarianceHigh float64 `mapstructure:"damage_variance_high"`
CriticalHitChance float64 `mapstructure:"critical_hit_chance"`
CriticalHitMultiplier float64 `mapstructure:"critical_hit_multiplier"`
}
// MarketsConfig holds parameters for overround margin and price limits.
type MarketsConfig struct {
PrimaryMargin float64 `mapstructure:"primary_margin"`
SecondaryMargin float64 `mapstructure:"secondary_margin"`
ExoticMargin float64 `mapstructure:"exotic_margin"`
MinPrice float64 `mapstructure:"min_price"`
MaxPrice float64 `mapstructure:"max_price"`
MinProbability float64 `mapstructure:"min_probability"`
MaxProbability float64 `mapstructure:"max_probability"`
}
// Load reads configuration from the config file and environment variables.
func Load() (*Config, error) {
viper.SetConfigName("tongo")
viper.SetConfigType("yaml")
viper.AddConfigPath("./config")
viper.AddConfigPath(".")
viper.SetEnvPrefix("TONGO")
viper.AutomaticEnv()
// Defaults
viper.SetDefault("daemon.tick_interval_ms", 2000)
viper.SetDefault("daemon.log_level", "info")
viper.SetDefault("valkey.host", "localhost")
viper.SetDefault("valkey.port", 6379)
viper.SetDefault("valkey.password", "")
viper.SetDefault("valkey.db", 0)
viper.SetDefault("engine.target_active_matches", 8)
viper.SetDefault("engine.max_active_matches", 12)
viper.SetDefault("engine.min_active_matches", 4)
viper.SetDefault("engine.pre_match_duration_ticks", 30)
viper.SetDefault("engine.min_match_duration_ticks", 60)
viper.SetDefault("engine.max_match_duration_ticks", 150)
viper.SetDefault("engine.resolving_duration_ticks", 3)
viper.SetDefault("engine.taxonomy_path", "../data-source/taxonomy.yaml")
viper.SetDefault("combat.damage_scaling_factor", 0.1)
viper.SetDefault("combat.damage_variance_low", 0.6)
viper.SetDefault("combat.damage_variance_high", 1.4)
viper.SetDefault("combat.critical_hit_chance", 0.05)
viper.SetDefault("combat.critical_hit_multiplier", 3.0)
viper.SetDefault("markets.primary_margin", 1.06)
viper.SetDefault("markets.secondary_margin", 1.075)
viper.SetDefault("markets.exotic_margin", 1.12)
viper.SetDefault("markets.min_price", 1.01)
viper.SetDefault("markets.max_price", 51.00)
viper.SetDefault("markets.min_probability", 0.02)
viper.SetDefault("markets.max_probability", 0.98)
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
return nil, fmt.Errorf("reading config: %w", err)
}
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unmarshaling config: %w", err)
}
return &cfg, nil
}

View file

@ -0,0 +1,101 @@
package taxonomy
import (
"log/slog"
"github.com/costap/quarkshologridledger/tongo-core/internal/domain/entity"
)
// YAMLLoader implements port.TaxonomyPort by reading from a YAML file.
type YAMLLoader struct {
path string
logger *slog.Logger
}
// New creates a new YAMLLoader instance.
func New(path string, logger *slog.Logger) *YAMLLoader {
return &YAMLLoader{
path: path,
logger: logger,
}
}
// Intermediary YAML file schema types
type taxonomyFile struct {
Quadrants []yamlQuadrant `yaml:"quadrants"`
MarketTemplates []yamlMarketTemplate `yaml:"market_templates"`
Venues []yamlVenue `yaml:"sector_pool"`
}
type yamlQuadrant struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Designation string `yaml:"designation"`
ColorCode string `yaml:"color_code"`
BaseVolatility float64 `yaml:"base_volatility"`
Factions []yamlFaction `yaml:"factions"`
}
type yamlFaction struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Abbreviation string `yaml:"abbreviation"`
Type string `yaml:"type"`
Homeworld string `yaml:"homeworld"`
ColorCode string `yaml:"color_code"`
EmblemGlyph string `yaml:"emblem_glyph"`
BaseShieldStrength int `yaml:"base_shield_strength"`
BaseAttackPower int `yaml:"base_attack_power"`
BaseEvasion int `yaml:"base_evasion"`
RivalryModifiers map[string]float64 `yaml:"rivalry_modifiers"`
}
type yamlVenue struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Quadrant string `yaml:"quadrant"`
Coordinates string `yaml:"coordinates"`
ShieldModifier float64 `yaml:"shield_modifier"`
DamageModifier float64 `yaml:"damage_modifier"`
EvasionModifier float64 `yaml:"evasion_modifier"`
SpecialEventChance float64 `yaml:"special_event_chance"`
Status string `yaml:"status"`
Tags []string `yaml:"tags"`
}
type yamlMarketTemplate struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Category string `yaml:"category"`
Selections []yamlSelectionTemplate `yaml:"selections"`
}
type yamlSelectionTemplate struct {
Type string `yaml:"type"`
Name string `yaml:"name,omitempty"`
Suffix string `yaml:"suffix,omitempty"`
BasePrice float64 `yaml:"base_price"`
}
// LoadQuadrants returns all quadrant definitions including their factions.
func (l *YAMLLoader) LoadQuadrants() ([]entity.Quadrant, error) {
l.logger.Debug("Loading quadrants from taxonomy")
return nil, nil
}
// LoadVenues returns all sector pool venue definitions.
func (l *YAMLLoader) LoadVenues() ([]entity.Venue, error) {
l.logger.Debug("Loading venues from taxonomy")
return nil, nil
}
// LoadMarketTemplates returns all market template definitions.
func (l *YAMLLoader) LoadMarketTemplates() ([]entity.MarketTemplate, error) {
l.logger.Debug("Loading market templates from taxonomy")
return nil, nil
}
func (l *YAMLLoader) loadFile() (*taxonomyFile, error) {
return nil, nil
}

View file

@ -0,0 +1,3 @@
package-lock.json
pnpm-lock.yaml
yarn.lock

View file

@ -0,0 +1,20 @@
// @ts-check
import { tanstackConfig } from '@tanstack/eslint-config'
export default [
...tanstackConfig,
{
rules: {
'import/no-cycle': 'off',
'import/order': 'off',
'sort-imports': 'off',
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/require-await': 'off',
'pnpm/json-enforce-catalog': 'off',
},
},
{
ignores: ['eslint.config.js', 'prettier.config.js', '.output/**'],
},
]

7352
web-terminal/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

56
web-terminal/package.json Normal file
View file

@ -0,0 +1,56 @@
{
"name": "quarkshologridledger",
"private": true,
"type": "module",
"imports": {
"#/*": "./src/*"
},
"scripts": {
"dev": "vite dev --port 3000",
"generate-routes": "tsr generate",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"lint": "eslint",
"format": "prettier --write . && eslint --fix",
"check": "prettier --check ."
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "latest",
"@tanstack/react-router": "latest",
"@tanstack/react-router-devtools": "latest",
"@tanstack/react-router-ssr-query": "latest",
"@tanstack/react-start": "latest",
"@tanstack/router-plugin": "^1.132.0",
"lucide-react": "^0.545.0",
"nitro": "npm:nitro-nightly@latest",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.1.18"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@tanstack/devtools-vite": "latest",
"@tanstack/eslint-config": "latest",
"@tanstack/router-cli": "^1.132.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.0",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.20.0",
"jsdom": "^28.1.0",
"prettier": "^3.8.1",
"typescript": "^6.0.2",
"vite": "^8.0.0",
"vitest": "^4.1.5"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"lightningcss"
]
}
}

View file

@ -0,0 +1,10 @@
// @ts-check
/** @type {import('prettier').Config} */
const config = {
semi: false,
singleQuote: true,
trailingComma: 'all',
}
export default config

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -0,0 +1,25 @@
{
"short_name": "TanStack App",
"name": "Create TanStack App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View file

@ -0,0 +1,38 @@
import React from 'react'
interface AsciiPanelProps {
title: string
subtitle?: string
children: React.ReactNode
variant: 'emerald' | 'amber' | 'slate'
}
export function AsciiPanel({
title,
subtitle,
children,
variant = 'slate',
}: AsciiPanelProps) {
const colorMap = {
emerald:
'border-emerald-500/40 text-emerald-400 shadow-[0_0_10px_rgba(16,185,129,0.05)]',
amber:
'border-amber-500/40 text-amber-400 shadow-[0_0_10px_rgba(245,158,11,0.05)]',
slate: 'border-slate-800 text-slate-400',
}
return (
<div
className={`border bg-black font-mono flex flex-col h-full rounded-sm ${colorMap[variant]}`}
>
{/* Dynamic Header Band */}
<div className="flex justify-between items-center bg-slate-900/40 px-3 py-1.5 border-b border-inherit text-xs uppercase tracking-wider font-bold select-none">
<span>[ {title} ]</span>
{subtitle && <span className="opacity-60">{subtitle}</span>}
</div>
{/* Panel Inner Body */}
<div className="p-4 flex-1 text-slate-300 text-sm overflow-y-auto">
{children}
</div>
</div>
)
}

View file

@ -0,0 +1,5 @@
export function BlinkingCursor() {
return (
<span className="inline-block w-1.5 h-3 bg-current animate-[pulse_1s_infinite] align-middle ml-0.5" />
)
}

View file

@ -0,0 +1,26 @@
interface GlyphIconProps {
glyph: string
colorCode?: string
size?: 'sm' | 'md' | 'lg'
}
export function GlyphIcon({
glyph,
colorCode = '#94a3b8',
size = 'sm',
}: GlyphIconProps) {
const sizeClass = {
sm: 'text-xs w-4 h-4',
md: 'text-sm w-5 h-5',
lg: 'text-base w-6 h-6',
}[size]
return (
<span
className={`inline-flex items-center justify-center font-mono font-bold select-none border border-slate-900 bg-slate-950/60 leading-none ${sizeClass}`}
style={{ color: colorCode, borderColor: `${colorCode}20` }}
>
{glyph}
</span>
)
}

View file

@ -0,0 +1,25 @@
interface PriceTagProps {
price: number
previousPrice?: number
trend?: string
}
export function PriceTag({ price, previousPrice, trend }: PriceTagProps) {
const showTrend = trend && trend !== 'stable' && previousPrice !== undefined
const isUp = trend === 'up'
return (
<span className="inline-flex items-center gap-1 font-mono text-xs select-none">
<span className="font-bold bg-slate-900 px-1.5 py-0.5 border border-slate-800 text-slate-300">
[{price.toFixed(2)}]
</span>
{showTrend && (
<span
className={`text-[10px] ${isUp ? 'text-emerald-400' : 'text-rose-500'}`}
>
{isUp ? '▲' : '▼'}
</span>
)}
</span>
)
}

View file

@ -0,0 +1,24 @@
interface ShieldBarProps {
homeShield: number
awayShield: number
}
// ShieldBar is a low-level plaintext visual decorator showing combatant health
export function ShieldBar({ homeShield, awayShield }: ShieldBarProps) {
return (
<div className="flex items-center gap-1 text-[8px] font-mono text-slate-600 w-full select-none">
<span className="w-6 text-right">{homeShield}%</span>
<div className="flex-1 h-1 bg-slate-900 overflow-hidden flex gap-px border border-slate-900">
<div
className="bg-amber-500/60 h-full"
style={{ width: `${homeShield}%` }}
/>
<div
className="bg-rose-500/60 h-full"
style={{ width: `${awayShield}%` }}
/>
</div>
<span className="w-6 text-left">{awayShield}%</span>
</div>
)
}

View file

@ -0,0 +1,28 @@
interface StatusBadgeProps {
status: 'LIVE' | 'PRE' | 'RES' | 'SETTLED'
}
// StatusBadge is a low-level visual indicator for match states
export function StatusBadge({ status }: StatusBadgeProps) {
const colorMap = {
LIVE: 'text-emerald-400 animate-pulse',
PRE: 'text-amber-500',
RES: 'text-rose-500',
SETTLED: 'text-slate-500',
}
const labelMap = {
LIVE: '● LIVE',
PRE: '○ PRE',
RES: '◉ RES',
SETTLED: '■ SETTLED',
}
return (
<span
className={`text-[9px] px-1 font-bold rounded-none select-none ${colorMap[status]}`}
>
{labelMap[status]}
</span>
)
}

View file

@ -0,0 +1,31 @@
interface TerminalLineProps {
variant?: 'solid' | 'dashed' | 'dotted' | 'double'
label?: string
}
export function TerminalLine({ variant = 'solid', label }: TerminalLineProps) {
const borderStyle = {
solid: 'border-solid',
dashed: 'border-dashed',
dotted: 'border-dotted',
double: 'border-double border-b-2',
}[variant]
if (label) {
return (
<div className="flex items-center gap-2 my-2 select-none w-full">
<div className={`flex-1 border-t border-slate-800 ${borderStyle}`} />
<span className="text-[10px] text-slate-500 uppercase tracking-widest font-bold whitespace-nowrap">
{label}
</span>
<div className={`flex-1 border-t border-slate-800 ${borderStyle}`} />
</div>
)
}
return (
<div
className={`w-full border-t border-slate-800 my-2 ${borderStyle} select-none`}
/>
)
}

View file

@ -0,0 +1,28 @@
interface TickProgressProps {
current: number
max: number
}
export function TickProgress({ current, max }: TickProgressProps) {
const percent = Math.min(Math.max((current / max) * 100, 0), 100)
const barLength = 20
const filledLength = Math.round((percent / 100) * barLength)
const emptyLength = barLength - filledLength
const filledBar = '█'.repeat(filledLength)
const emptyBar = '░'.repeat(emptyLength)
return (
<div className="font-mono text-xs select-none flex items-center gap-2">
<span className="text-slate-500">CYCLE:</span>
<span className="text-slate-300 font-bold">
{current}/{max}
</span>
<span className="text-slate-600 tracking-tighter">
[{filledBar}
{emptyBar}]
</span>
<span className="text-slate-400">{percent.toFixed(0)}%</span>
</div>
)
}

View file

@ -0,0 +1,7 @@
export function AccountSummary() {
return (
<div className="border border-slate-900 bg-slate-950/40 p-4 font-mono text-xs select-none">
<div>Account Statistics Summary</div>
</div>
)
}

View file

@ -0,0 +1,103 @@
import { useBetslip } from '#/components/providers/BetslipProvider'
export function BetslipSidecar() {
const { selections, removeSelection, clearAll, updateStake } = useBetslip()
const totalStake = selections.reduce((sum, s) => sum + (s.stake || 0), 0)
const totalReturn = selections.reduce(
(sum, s) => sum + (s.stake || 0) * s.price,
0,
)
return (
<div className="space-y-4 font-mono select-none text-xs text-slate-400">
<div className="flex justify-between items-center text-slate-500 font-bold border-b border-slate-900 pb-2">
<span>SELECTIONS ({selections.length})</span>
{selections.length > 0 && (
<button
onClick={clearAll}
className="text-[10px] text-rose-500 hover:text-rose-400 cursor-pointer border-none bg-transparent"
>
[CLEAR ALL]
</button>
)}
</div>
{selections.length === 0 ? (
<div className="p-4 border border-dashed border-slate-900 text-center italic text-slate-600">
Wager slip standby. Choose active odds matrices to link a prediction.
</div>
) : (
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{selections.map((sel) => (
<div
key={sel.marketId}
className="border border-slate-900 p-2 bg-slate-950/40 space-y-1.5"
>
<div className="flex justify-between items-center text-[10px]">
<span className="font-bold truncate max-w-[150px]">
{sel.matchName}
</span>
<button
onClick={() => removeSelection(sel.marketId)}
className="text-rose-500 hover:text-rose-400 font-bold border-none bg-transparent cursor-pointer"
>
[×]
</button>
</div>
<div className="flex justify-between items-center text-slate-300">
<span className="flex items-center gap-1 font-bold">
<span className="text-slate-500">{sel.factionGlyph}</span>
{sel.selectionName}
</span>
<span className="bg-slate-900 px-1 border border-slate-800 text-amber-400 font-bold">
{sel.price.toFixed(2)}
</span>
</div>
<div className="flex items-center justify-between gap-2 border-t border-slate-950 pt-1.5 mt-1">
<span>STAKE:</span>
<div className="flex items-center gap-1">
<input
type="number"
min="0"
step="5"
value={sel.stake || ''}
onChange={(e) =>
updateStake(sel.marketId, parseFloat(e.target.value) || 0)
}
className="w-16 bg-slate-950 border border-slate-800 text-right px-1 text-slate-300 font-mono text-[10px] outline-none"
placeholder="0.00"
/>
<span>GPL</span>
</div>
</div>
</div>
))}
</div>
)}
{selections.length > 0 && (
<div className="border-t border-slate-900 pt-3 space-y-2">
<div className="flex justify-between text-slate-500 font-bold">
<span>TOTAL STAKE:</span>
<span className="text-slate-300">{totalStake.toFixed(2)} GPL</span>
</div>
<div className="flex justify-between text-slate-500 font-bold">
<span>EST. RETURN:</span>
<span className="text-emerald-400 font-bold">
{totalReturn.toFixed(2)} GPL
</span>
</div>
<button className="w-full mt-2 p-2 border border-amber-500/50 bg-amber-950/20 text-amber-400 font-bold text-center uppercase hover:bg-amber-950/40 hover:border-amber-500 cursor-pointer transition-all">
TRANSMIT WAGER
</button>
<div className="text-[10px] text-slate-600 italic text-center leading-normal">
"The riskier the road, the greater the profit."
<br /> Rule of Acquisition #62
</div>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,78 @@
export interface CombatLogEntry {
id: string
tick: number
message: string
type: 'combat' | 'event' | 'system'
}
interface EngagementLogProps {
logs?: CombatLogEntry[]
}
export function EngagementLog({ logs }: EngagementLogProps) {
// Default mock logs for display
const defaultLogs: CombatLogEntry[] = [
{
id: '1',
tick: 42,
message: '◆ Starfleet Command fires — HIT — 4.2% shield damage',
type: 'combat',
},
{
id: '2',
tick: 42,
message: '█ Borg Collective fires — MISS — Evasion successful',
type: 'combat',
},
{
id: '3',
tick: 38,
message:
'⚡ EVENT: Borg Adaptation active — Incoming damage multiplier ×0.5',
type: 'event',
},
{
id: '4',
tick: 35,
message: '◆ Starfleet Command fires — CRIT — 12.6% shield damage',
type: 'combat',
},
{
id: '5',
tick: 31,
message:
'🌀 EVENT: Debris Impact — Borg Collective shields reduced by 12%',
type: 'event',
},
{
id: '6',
tick: 28,
message: '█ Borg Collective fires — HIT — 6.1% shield damage',
type: 'combat',
},
]
const displayLogs = logs || defaultLogs
const logColor = {
combat: 'text-slate-400',
event: 'text-amber-400',
system: 'text-sky-400',
}
return (
<div className="border border-slate-900 bg-slate-950/20 p-2 font-mono text-xs select-none space-y-1 w-full max-h-[160px] overflow-y-auto pr-1">
{displayLogs.map((log) => (
<div
key={log.id}
className="flex gap-2 items-start leading-relaxed text-[11px]"
>
<span className="text-slate-600 font-bold shrink-0">
[T{log.tick}]
</span>
<span className={`${logColor[log.type]}`}>{log.message}</span>
</div>
))}
</div>
)
}

View file

@ -0,0 +1,7 @@
export function FactionProfile() {
return (
<div className="border border-slate-900 bg-slate-950/40 p-4 font-mono text-xs select-none">
<div>Faction Tactical Profile View</div>
</div>
)
}

View file

@ -0,0 +1,7 @@
export function FactionTable() {
return (
<div className="border border-slate-900 bg-slate-950/40 p-4 font-mono text-xs select-none">
<div>Faction Registry Database Table</div>
</div>
)
}

View file

@ -0,0 +1,64 @@
export interface TickerEvent {
id: string
time: string
message: string
type: 'combat' | 'event' | 'system'
}
interface LiveTickerProps {
events?: TickerEvent[]
}
export function LiveTicker({ events }: LiveTickerProps) {
// Default mock ticker events
const defaultEvents: TickerEvent[] = [
{
id: '1',
time: '14:32',
message: 'Borg Adaptation triggered — Damage reduced 50%',
type: 'event',
},
{
id: '2',
time: '14:30',
message: 'Critical Hit! Starfleet Command deals 3× damage',
type: 'combat',
},
{
id: '3',
time: '14:28',
message: 'Bajor Sector: Prophet Intervention — Markets SUSPENDED',
type: 'event',
},
{
id: '4',
time: '14:25',
message: 'New match scheduled: Klingons vs Romulans @ Narendra III',
type: 'system',
},
]
const displayEvents = events || defaultEvents
const typeColor = {
combat: 'text-rose-400',
event: 'text-amber-400',
system: 'text-sky-400',
}
return (
<div className="border border-slate-900 bg-slate-950/20 p-2 font-mono text-xs select-none space-y-1 w-full">
{displayEvents.map((evt) => (
<div key={evt.id} className="flex gap-2 items-start leading-relaxed">
<span className="text-slate-600 font-bold shrink-0">
[{evt.time}]
</span>
<span className={`${typeColor[evt.type]} font-semibold shrink-0`}>
{evt.type.toUpperCase()}:
</span>
<span className="text-slate-400 truncate">{evt.message}</span>
</div>
))}
</div>
)
}

View file

@ -0,0 +1,103 @@
import { Link } from '@tanstack/react-router'
import { MarketSelection } from '#/components/panels/MarketSelection'
export interface SelectionData {
id: string
name: string
price: number
factionGlyph: string
}
export interface MarketItem {
id: string
templateId: string
name: string
category: 'primary' | 'secondary' | 'exotic'
selections: SelectionData[]
}
interface MarketGridProps {
matchId: string
matchName: string
markets: MarketItem[]
currentCategory: 'primary' | 'secondary' | 'exotic' | 'all'
}
export function MarketGrid({
matchId,
matchName,
markets,
currentCategory,
}: MarketGridProps) {
// Filter tabs config
const tabs = [
{ id: 'all', label: 'ALL MARKETS' },
{ id: 'primary', label: 'PRIMARY' },
{ id: 'secondary', label: 'SECONDARY' },
{ id: 'exotic', label: 'EXOTIC' },
] as const
// Filter markets by current category
const filteredMarkets =
currentCategory === 'all'
? markets
: markets.filter((m) => m.category === currentCategory)
return (
<div className="space-y-4 font-mono text-xs select-none w-full">
{/* Category Tabs */}
<div className="flex gap-1.5 border border-slate-900 p-1.5 bg-slate-950/20">
{tabs.map((tab) => (
<Link
key={tab.id}
to="."
search={(prev: any) => ({ ...prev, marketCategory: tab.id })}
className={`flex-1 text-center py-1.5 border font-bold uppercase transition-all cursor-pointer ${
currentCategory === tab.id
? 'border-emerald-500/50 text-emerald-400 bg-emerald-950/20'
: 'border-slate-950 text-slate-500 hover:text-slate-400 hover:border-slate-800 bg-black/40'
}`}
>
{tab.label}
</Link>
))}
</div>
{/* Markets List */}
<div className="space-y-3.5 max-h-[420px] overflow-y-auto pr-1">
{filteredMarkets.length === 0 ? (
<div className="p-8 border border-dashed border-slate-900 text-center italic text-slate-600">
No active markets available in this category sector.
</div>
) : (
filteredMarkets.map((market) => (
<div key={market.id} className="space-y-2">
{/* Market Header: Title + Category Tag */}
<div className="flex justify-between items-center text-[10px] text-slate-500 font-bold px-0.5">
<span className="uppercase tracking-wide">{market.name}</span>
<span className="bg-slate-900 px-1 border border-slate-800 text-slate-500">
[{market.category}]
</span>
</div>
{/* Selections Selection Grid (moneyline has 2 columns, others can wrap) */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{market.selections.map((sel) => (
<MarketSelection
key={sel.id}
matchId={matchId}
matchName={matchName}
marketId={sel.id}
selectionName={sel.name}
price={sel.price}
factionGlyph={sel.factionGlyph}
/>
))}
</div>
</div>
))
)}
</div>
</div>
)
}

View file

@ -0,0 +1,58 @@
import { useBetslip } from '#/components/providers/BetslipProvider'
import { PriceTag } from '#/components/ascii/PriceTag'
interface MarketSelectionProps {
matchId: string
matchName: string
marketId: string
selectionName: string
price: number
factionGlyph: string
}
export function MarketSelection({
matchId,
matchName,
marketId,
selectionName,
price,
factionGlyph,
}: MarketSelectionProps) {
const { selections, addSelection, removeSelection } = useBetslip()
const isSelected = selections.some((s) => s.marketId === marketId)
const handleToggle = () => {
if (isSelected) {
removeSelection(marketId)
} else {
addSelection({
matchId,
matchName,
marketId,
selectionName,
price,
factionGlyph,
})
}
}
return (
<button
onClick={handleToggle}
className={`w-full flex justify-between items-center p-2.5 border transition-all cursor-pointer font-mono text-xs select-none ${
isSelected
? 'border-emerald-500/50 bg-emerald-950/20 text-emerald-400 font-bold'
: 'border-slate-900 hover:border-slate-800 bg-black/60 text-slate-400 hover:text-slate-300'
}`}
>
<span className="flex items-center gap-1">
<span className={isSelected ? 'text-emerald-500' : 'text-slate-600'}>
{factionGlyph || '◇'}
</span>
{selectionName}
</span>
<PriceTag price={price} />
</button>
)
}

View file

@ -0,0 +1,176 @@
import { Link } from '@tanstack/react-router'
import { GlyphIcon } from '#/components/ascii/GlyphIcon'
import { StatusBadge } from '#/components/ascii/StatusBadge'
import { ShieldBar } from '#/components/ascii/ShieldBar'
import { TerminalLine } from '#/components/ascii/TerminalLine'
import { PriceTag } from '#/components/ascii/PriceTag'
interface FactionDetail {
abbreviation: string
name: string
glyph: string
colorCode: string
shieldCapacity: number
oddsPrice: number
}
export interface MatchData {
id: string
name: string
quadrant: 'alpha' | 'beta' | 'gamma' | 'delta'
status: 'LIVE' | 'PRE' | 'RES'
tick: number
maxTicks: number
kineticYield: number
homeFaction: FactionDetail
awayFaction: FactionDetail
marketsOpenCount: number
commencingIn?: string
}
interface MatchCardProps {
match: MatchData
}
export function MatchCard({ match }: MatchCardProps) {
const isLive = match.status === 'LIVE'
const isPre = match.status === 'PRE'
// Format quadrant letter glyph
const quadGlyph = {
alpha: 'α',
beta: 'β',
gamma: 'γ',
delta: 'δ',
}[match.quadrant]
return (
<div className="border border-slate-900 bg-slate-950/40 p-3 font-mono text-xs select-none hover:border-slate-800 transition-all flex flex-col justify-between">
<div>
{/* Header: Venue + Status + Quadrant */}
<div className="flex justify-between items-center mb-2">
<span className="font-bold text-slate-300 truncate max-w-[260px] uppercase">
{match.name}
</span>
<div className="flex items-center gap-2 shrink-0">
<StatusBadge status={match.status} />
<span className="text-[10px] text-slate-500 bg-slate-900 px-1 border border-slate-800 uppercase font-bold">
{quadGlyph}
</span>
</div>
</div>
{/* Combat Factions Layout */}
<div className="space-y-2 my-3">
{/* Home Faction */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 min-w-0">
<GlyphIcon
glyph={match.homeFaction.glyph}
colorCode={match.homeFaction.colorCode}
/>
<span className="font-bold text-slate-400 truncate">
{match.homeFaction.name}
</span>
</div>
{isLive && (
<span className="text-slate-500 font-bold shrink-0">
{match.homeFaction.shieldCapacity}%
</span>
)}
</div>
{/* Away Faction */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5 min-w-0">
<GlyphIcon
glyph={match.awayFaction.glyph}
colorCode={match.awayFaction.colorCode}
/>
<span className="font-bold text-slate-400 truncate">
{match.awayFaction.name}
</span>
</div>
{isLive && (
<span className="text-slate-500 font-bold shrink-0">
{match.awayFaction.shieldCapacity}%
</span>
)}
</div>
{/* Dual Shield Bar for Live Match */}
{isLive && (
<div className="mt-1">
<ShieldBar
homeShield={match.homeFaction.shieldCapacity}
awayShield={match.awayFaction.shieldCapacity}
/>
</div>
)}
</div>
</div>
<div>
<TerminalLine variant="dashed" />
{/* Footer info: Telemetry details + Action Button */}
<div className="flex justify-between items-center text-[10px] text-slate-500">
<div>
{isLive ? (
<div className="space-y-0.5">
<div>
KY:{' '}
<span className="text-slate-400">
{match.kineticYield.toFixed(1)} TW
</span>{' '}
| TICK:{' '}
<span className="text-slate-400">
{match.tick}/{match.maxTicks}
</span>
</div>
<div className="flex items-center gap-1">
<span>ODDS:</span>
<span className="text-slate-400">
{match.homeFaction.abbreviation}
</span>
<PriceTag price={match.homeFaction.oddsPrice} />
<span>|</span>
<span className="text-slate-400">
{match.awayFaction.abbreviation}
</span>
<PriceTag price={match.awayFaction.oddsPrice} />
</div>
</div>
) : isPre ? (
<div className="space-y-0.5">
<div className="text-amber-500/80 font-bold">
COMMENCING IN: {match.commencingIn}
</div>
<div>PRE-MATCH ODDS AVAILABLE</div>
</div>
) : (
<div>MATCH IN RESOLUTION PHASE</div>
)}
</div>
<Link
to="/quadrants/$sectorId"
params={{ sectorId: match.id }}
search={(prev: any) => ({
...prev,
quadrant: match.quadrant,
currency: prev.currency || 'Latinum',
})}
className="border border-slate-900 hover:border-amber-500/50 bg-slate-950 px-2 py-1 text-slate-400 hover:text-amber-400 font-bold tracking-wider shrink-0 transition-all cursor-pointer"
>
[VIEW]
</Link>
</div>
<div className="text-[9px] text-slate-600 mt-2">
{match.marketsOpenCount} MARKETS OPEN
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,392 @@
import { useState } from 'react'
import { Link } from '@tanstack/react-router'
import { ShieldBar } from '#/components/ascii/ShieldBar'
import { StatusBadge } from '#/components/ascii/StatusBadge'
import { GlyphIcon } from '#/components/ascii/GlyphIcon'
// ─── TYPES ──────────────────────────────────────────────────────────────
interface ActiveMatchSummary {
id: string
name: string
quadrant: 'alpha' | 'beta' | 'gamma' | 'delta'
status: 'LIVE' | 'PRE' | 'RES'
homeFaction: {
abbreviation: string
glyph: string
colorCode: string
shieldCapacity: number
}
awayFaction: {
abbreviation: string
glyph: string
colorCode: string
shieldCapacity: number
}
}
interface QuadrantConfig {
id: string
name: string
glyph: string
}
// ─── MAIN COMPONENT ─────────────────────────────────────────────────────
export function NavigationMenu() {
// Collapsible quadrant groups state
const [openQuadrants, setOpenQuadrants] = useState<Record<string, boolean>>({
alpha: true,
beta: true,
gamma: false,
delta: false,
})
const toggleQuadrant = (q: string) => {
setOpenQuadrants((prev) => ({
...prev,
[q]: !prev[q],
}))
}
// Mock Active Matches aligned with taxonomy.yaml and color codes
const matches: ActiveMatchSummary[] = [
{
id: 'sector-001',
name: 'Sector 001 — Sol Array',
quadrant: 'alpha',
status: 'LIVE',
homeFaction: {
abbreviation: 'SFC',
glyph: '◆',
colorCode: '#34d399',
shieldCapacity: 62,
},
awayFaction: {
abbreviation: 'BRG',
glyph: '█',
colorCode: '#38bdf8',
shieldCapacity: 71,
},
},
{
id: 'bajor-wormhole',
name: 'Bajor Sector — DS9',
quadrant: 'alpha',
status: 'LIVE',
homeFaction: {
abbreviation: 'BAJ',
glyph: '☼',
colorCode: '#fb923c',
shieldCapacity: 48,
},
awayFaction: {
abbreviation: 'DOM',
glyph: '◉',
colorCode: '#fb7185',
shieldCapacity: 55,
},
},
{
id: 'wolf-359',
name: 'Wolf 359 Outpost',
quadrant: 'alpha',
status: 'PRE',
homeFaction: {
abbreviation: 'SFC',
glyph: '◆',
colorCode: '#34d399',
shieldCapacity: 100,
},
awayFaction: {
abbreviation: 'BRG',
glyph: '█',
colorCode: '#38bdf8',
shieldCapacity: 100,
},
},
{
id: 'qonos-arena',
name: "Qo'noS Sector — Warrior's Ring",
quadrant: 'beta',
status: 'LIVE',
homeFaction: {
abbreviation: 'KLG',
glyph: '⚔',
colorCode: '#f87171',
shieldCapacity: 88,
},
awayFaction: {
abbreviation: 'ROM',
glyph: '◈',
colorCode: '#a78bfa',
shieldCapacity: 72,
},
},
{
id: 'neutral-zone',
name: 'The Neutral Zone',
quadrant: 'beta',
status: 'LIVE',
homeFaction: {
abbreviation: 'ROM',
glyph: '◈',
colorCode: '#a78bfa',
shieldCapacity: 90,
},
awayFaction: {
abbreviation: 'SFC',
glyph: '◆',
colorCode: '#34d399',
shieldCapacity: 95,
},
},
{
id: 'karemma-trade-corridor',
name: 'Karemma Trade Corridor',
quadrant: 'gamma',
status: 'LIVE',
homeFaction: {
abbreviation: 'KRM',
glyph: '◊',
colorCode: '#fcd34d',
shieldCapacity: 65,
},
awayFaction: {
abbreviation: 'DOS',
glyph: '▬',
colorCode: '#c084fc',
shieldCapacity: 80,
},
},
{
id: 'grid-325',
name: 'Grid 325 — Borg Corridor',
quadrant: 'delta',
status: 'LIVE',
homeFaction: {
abbreviation: 'BRG',
glyph: '█',
colorCode: '#38bdf8',
shieldCapacity: 15,
},
awayFaction: {
abbreviation: 'S84',
glyph: '∞',
colorCode: '#ec4899',
shieldCapacity: 99,
},
},
{
id: 'hirogen-relay',
name: 'Hirogen Relay Nexus',
quadrant: 'delta',
status: 'LIVE',
homeFaction: {
abbreviation: 'HRG',
glyph: '⊗',
colorCode: '#fbbf24',
shieldCapacity: 75,
},
awayFaction: {
abbreviation: 'KZN',
glyph: '',
colorCode: '#f97316',
shieldCapacity: 40,
},
},
]
const quadrants: QuadrantConfig[] = [
{ id: 'alpha', name: 'ALPHA QUADRANT', glyph: 'α' },
{ id: 'beta', name: 'BETA QUADRANT', glyph: 'β' },
{ id: 'gamma', name: 'GAMMA QUADRANT', glyph: 'γ' },
{ id: 'delta', name: 'DELTA QUADRANT', glyph: 'δ' },
]
return (
<div className="space-y-4 font-mono select-none">
{/* 00 Grid Overview Dashboard */}
<div>
<Link
to="/"
className="group w-full flex justify-between items-center p-2 border border-slate-900 bg-slate-950/40 text-slate-400 hover:border-amber-500/40 hover:bg-slate-900/40 transition-all text-xs"
activeProps={{
className:
'!border-emerald-500/50 !text-emerald-400 !bg-emerald-950/20',
}}
activeOptions={{ exact: true }}
>
<span className="font-bold tracking-wider group-hover:translate-x-1 transition-transform">
&gt; GRID OVERVIEW
</span>
<span className="opacity-60">[00]</span>
</Link>
</div>
<div className="border-t border-slate-900 pt-2">
<div className="text-slate-600 text-[10px] font-bold tracking-widest mb-2 px-1">
&gt; LIVE ENGAGEMENTS
</div>
<div className="space-y-1.5">
{quadrants.map((quad) => (
<QuadrantGroup
key={quad.id}
quadrant={quad}
isOpen={openQuadrants[quad.id]}
onToggle={() => toggleQuadrant(quad.id)}
matches={matches.filter((m) => m.quadrant === quad.id)}
/>
))}
</div>
</div>
{/* Archives Section */}
<div className="border-t border-slate-900 pt-2">
<div className="text-slate-600 text-[10px] font-bold tracking-widest mb-2 px-1">
&gt; STATION ARCHIVES
</div>
<div className="flex flex-col gap-1">
<ArchiveLink to="/ledger" label="Wager Ledger" num="01" glyph="▤" />
<ArchiveLink
to="/registry/factions"
label="Faction Registry"
num="02"
glyph="◆"
/>
<ArchiveLink
to="/registry/venues"
label="Venue Database"
num="03"
glyph="◇"
/>
<ArchiveLink
to="/schedule"
label="Transmission Schedule"
num="04"
glyph="☼"
/>
</div>
</div>
</div>
)
}
// ─── SUB-COMPONENTS ─────────────────────────────────────────────────────
interface QuadrantGroupProps {
quadrant: QuadrantConfig
isOpen: boolean
onToggle: () => void
matches: ActiveMatchSummary[]
}
function QuadrantGroup({
quadrant,
isOpen,
onToggle,
matches,
}: QuadrantGroupProps) {
return (
<div className="border border-slate-900/60 bg-black/20">
<button
onClick={onToggle}
className="w-full text-left p-2 bg-slate-950/20 hover:bg-slate-900/20 flex justify-between items-center text-xs font-bold text-slate-400 cursor-pointer border-none"
>
<span className="flex items-center gap-1.5">
<span className="text-[10px] text-slate-600">
{isOpen ? '▼' : '▸'}
</span>
{quadrant.name}
</span>
<span className="text-[10px] text-slate-500 bg-slate-900/60 px-1 border border-slate-800">
[{quadrant.glyph}]
</span>
</button>
{isOpen && (
<div className="border-t border-slate-950/80 p-1 flex flex-col gap-1">
{matches.length === 0 ? (
<div className="p-2 text-[10px] text-slate-600 italic">
No active engagements
</div>
) : (
matches.map((match) => (
<MatchNavItem key={match.id} match={match} />
))
)}
</div>
)}
</div>
)
}
interface MatchNavItemProps {
match: ActiveMatchSummary
}
function MatchNavItem({ match }: MatchNavItemProps) {
return (
<Link
to="/quadrants/$sectorId"
params={{ sectorId: match.id }}
search={{ quadrant: match.quadrant, currency: 'Latinum' }}
className="group p-1.5 bg-black border border-slate-950 hover:border-amber-500/30 text-slate-400 flex flex-col gap-1 transition-all"
activeProps={{
className:
'!border-emerald-500/50 !text-emerald-400 !bg-emerald-950/20',
}}
>
<div className="flex justify-between items-center text-[10px]">
<span className="truncate group-hover:translate-x-0.5 transition-transform flex items-center gap-1">
<GlyphIcon
glyph={match.homeFaction.glyph}
colorCode={match.homeFaction.colorCode}
/>
<span className="font-bold">{match.homeFaction.abbreviation}</span>
<span className="text-slate-600 text-[8px]">VS</span>
<GlyphIcon
glyph={match.awayFaction.glyph}
colorCode={match.awayFaction.colorCode}
/>
<span className="font-bold">{match.awayFaction.abbreviation}</span>
</span>
<StatusBadge status={match.status} />
</div>
{match.status === 'LIVE' && (
<ShieldBar
homeShield={match.homeFaction.shieldCapacity}
awayShield={match.awayFaction.shieldCapacity}
/>
)}
</Link>
)
}
interface ArchiveLinkProps {
to: string
label: string
num: string
glyph: string
}
function ArchiveLink({ to, label, num, glyph }: ArchiveLinkProps) {
return (
<Link
to={to}
className="w-full text-left p-2 border border-slate-900 text-slate-500 hover:text-slate-300 transition-all text-xs flex justify-between"
activeProps={{
className:
'!text-emerald-400 !border-emerald-500/30 !bg-emerald-950/10',
}}
>
<span>
[{num}] {label}
</span>
<span className="opacity-40">{glyph}</span>
</Link>
)
}

View file

@ -0,0 +1,34 @@
import { Link } from '@tanstack/react-router'
interface QuadrantFilterBarProps {
currentQuadrant: 'alpha' | 'beta' | 'gamma' | 'delta' | 'all'
}
export function QuadrantFilterBar({ currentQuadrant }: QuadrantFilterBarProps) {
const tabs = [
{ id: 'all', label: 'ALL FEEDS' },
{ id: 'alpha', label: 'α ALPHA' },
{ id: 'beta', label: 'β BETA' },
{ id: 'gamma', label: 'γ GAMMA' },
{ id: 'delta', label: 'δ DELTA' },
] as const
return (
<div className="flex gap-1.5 border border-slate-900 p-1.5 bg-slate-950/20 font-mono text-xs select-none w-full">
{tabs.map((tab) => (
<Link
key={tab.id}
to="."
search={(prev: any) => ({ ...prev, quadrant: tab.id })}
className={`flex-1 text-center py-1.5 border font-bold uppercase transition-all cursor-pointer ${
currentQuadrant === tab.id
? 'border-emerald-500/50 text-emerald-400 bg-emerald-950/20'
: 'border-slate-950 text-slate-500 hover:text-slate-400 hover:border-slate-800 bg-black/40'
}`}
>
{tab.label}
</Link>
))}
</div>
)
}

View file

@ -0,0 +1,7 @@
export function ScheduleCard() {
return (
<div className="border border-slate-900 bg-slate-950/40 p-4 font-mono text-xs select-none">
<div>Scheduled Transmission Info</div>
</div>
)
}

View file

@ -0,0 +1,7 @@
export function VenueTable() {
return (
<div className="border border-slate-900 bg-slate-950/40 p-4 font-mono text-xs select-none">
<div>Sector Pool Venue Database Table</div>
</div>
)
}

View file

@ -0,0 +1,187 @@
import { ShieldBar } from '#/components/ascii/ShieldBar'
import { GlyphIcon } from '#/components/ascii/GlyphIcon'
import { TerminalLine } from '#/components/ascii/TerminalLine'
interface FactionState {
name: string
abbreviation: string
glyph: string
colorCode: string
shieldCapacity: number
attack: number
evasion: number
activeEffects: Array<{
name: string
description: string
remainingTicks: number
}>
}
interface VenueDetail {
name: string
coordinates: string
shieldModifier: number
damageModifier: number
evasionModifier: number
tags: string[]
}
interface VersusArenaProps {
home: FactionState
away: FactionState
kineticYield: number
tick: number
maxTicks: number
venue: VenueDetail
}
export function VersusArena({
home,
away,
kineticYield,
tick,
maxTicks,
venue,
}: VersusArenaProps) {
// Combine all active effects for display
const allEffects = [
...home.activeEffects.map((e) => ({ ...e, faction: home.abbreviation })),
...away.activeEffects.map((e) => ({ ...e, faction: away.abbreviation })),
]
return (
<div className="border border-slate-900 bg-slate-950/20 p-4 font-mono text-xs select-none space-y-4 w-full">
{/* Factions Headers */}
<div className="flex justify-between items-center text-center">
<div className="flex-1 flex flex-col items-center">
<GlyphIcon glyph={home.glyph} colorCode={home.colorCode} size="lg" />
<span className="font-bold text-slate-300 mt-1 uppercase text-sm">
{home.name}
</span>
<span className="text-slate-500 font-bold">
[{home.abbreviation}]
</span>
</div>
<div className="px-4 text-slate-600 font-bold text-base shrink-0">
VS
</div>
<div className="flex-1 flex flex-col items-center">
<GlyphIcon glyph={away.glyph} colorCode={away.colorCode} size="lg" />
<span className="font-bold text-slate-300 mt-1 uppercase text-sm">
{away.name}
</span>
<span className="text-slate-500 font-bold">
[{away.abbreviation}]
</span>
</div>
</div>
<TerminalLine variant="dashed" />
{/* Symmetrical Stats Grid */}
<div className="grid grid-cols-3 gap-2 text-center items-center">
{/* Home Stats */}
<div className="space-y-1.5 text-right pr-4">
<div className="text-slate-300 font-bold">{home.shieldCapacity}%</div>
<div className="text-slate-400">{home.attack}</div>
<div className="text-slate-400">{home.evasion}</div>
</div>
{/* Labels */}
<div className="space-y-1.5 text-slate-500 text-[10px] font-bold">
<div>SHIELDS</div>
<div>ATTACK POWER</div>
<div>EVASION CAP.</div>
</div>
{/* Away Stats */}
<div className="space-y-1.5 text-left pl-4">
<div className="text-slate-300 font-bold">{away.shieldCapacity}%</div>
<div className="text-slate-400">{away.attack}</div>
<div className="text-slate-400">{away.evasion}</div>
</div>
</div>
{/* Symmetrical Shield Bar */}
<div className="px-2">
<ShieldBar
homeShield={home.shieldCapacity}
awayShield={away.shieldCapacity}
/>
</div>
<TerminalLine variant="dashed" />
{/* Venue & Environment Telemetry */}
<div className="grid grid-cols-2 gap-4 text-[10px] text-slate-500 leading-relaxed">
<div className="space-y-1">
<div>
KINETIC YIELD:{' '}
<span className="text-slate-300 font-bold">
{kineticYield.toFixed(1)} TW
</span>
</div>
<div>
CYCLE TICK:{' '}
<span className="text-slate-300 font-bold">
{tick} / {maxTicks}
</span>
</div>
<div>
VENUE:{' '}
<span className="text-slate-300 uppercase">{venue.name}</span>
</div>
</div>
<div className="space-y-1 text-right">
<div>
COORD: <span className="text-slate-400">{venue.coordinates}</span>
</div>
<div>
TAGS:{' '}
<span className="text-slate-400 uppercase">
[{venue.tags.join(', ')}]
</span>
</div>
<div>
SHD×:{' '}
<span className="text-slate-400">
{venue.shieldModifier.toFixed(2)}
</span>{' '}
| DMG×:{' '}
<span className="text-slate-400">
{venue.damageModifier.toFixed(2)}
</span>{' '}
| EVA×:{' '}
<span className="text-slate-400">
{venue.evasionModifier.toFixed(2)}
</span>
</div>
</div>
</div>
{/* Active Effects Section */}
{allEffects.length > 0 && (
<div className="border border-slate-900 bg-slate-950/40 p-2 space-y-1 mt-3">
<div className="text-[10px] text-slate-600 font-bold uppercase tracking-wider">
&gt; ACTIVE EFFECTS IN SECTOR ARRAY
</div>
{allEffects.map((eff, idx) => (
<div
key={idx}
className="text-[10px] text-amber-500 flex justify-between items-center"
>
<span>
{eff.name} ({eff.faction}) {eff.description}
</span>
<span className="text-[9px] bg-slate-900 px-1 border border-slate-800 text-slate-500">
[{eff.remainingTicks} TICKS REMAINING]
</span>
</div>
))}
</div>
)}
</div>
)
}

View file

@ -0,0 +1,7 @@
export function WagerCard() {
return (
<div className="border border-slate-900 bg-slate-950/40 p-4 font-mono text-xs select-none">
<div>Wager Details</div>
</div>
)
}

View file

@ -0,0 +1,87 @@
import React, { createContext, useContext, useState } from 'react'
export interface BetslipSelection {
matchId: string
matchName: string
marketId: string
selectionName: string
price: number
currentPrice: number
stake: number
factionGlyph: string
}
interface BetslipContextType {
selections: BetslipSelection[]
addSelection: (sel: Omit<BetslipSelection, 'stake' | 'currentPrice'>) => void
removeSelection: (marketId: string) => void
updateStake: (marketId: string, stake: number) => void
clearAll: () => void
accumulatorStake: number
setAccumulatorStake: (stake: number) => void
}
const BetslipContext = createContext<BetslipContextType | undefined>(undefined)
export function BetslipProvider({ children }: { children: React.ReactNode }) {
const [selections, setSelections] = useState<BetslipSelection[]>([])
const [accumulatorStake, setAccumulatorStake] = useState<number>(0)
const addSelection = (
sel: Omit<BetslipSelection, 'stake' | 'currentPrice'>,
) => {
setSelections((prev) => {
// Check if selection already exists
if (prev.some((s) => s.marketId === sel.marketId)) {
return prev
}
return [
...prev,
{
...sel,
currentPrice: sel.price,
stake: 0,
},
]
})
}
const removeSelection = (marketId: string) => {
setSelections((prev) => prev.filter((s) => s.marketId !== marketId))
}
const updateStake = (marketId: string, stake: number) => {
setSelections((prev) =>
prev.map((s) => (s.marketId === marketId ? { ...s, stake } : s)),
)
}
const clearAll = () => {
setSelections([])
setAccumulatorStake(0)
}
return (
<BetslipContext.Provider
value={{
selections,
addSelection,
removeSelection,
updateStake,
clearAll,
accumulatorStake,
setAccumulatorStake,
}}
>
{children}
</BetslipContext.Provider>
)
}
export function useBetslip() {
const context = useContext(BetslipContext)
if (!context) {
throw new Error('useBetslip must be used within a BetslipProvider')
}
return context
}

View file

@ -0,0 +1,31 @@
import React, { createContext, useContext } from 'react'
import { useRouterState } from '@tanstack/react-router'
type Currency = 'Latinum' | 'Credits'
interface CurrencyContextType {
currency: Currency
}
const CurrencyContext = createContext<CurrencyContextType>({
currency: 'Latinum',
})
export function CurrencyProvider({ children }: { children: React.ReactNode }) {
const routerState = useRouterState()
// Safe search param extraction across any active route
const searchParams = routerState.location.search as Record<string, any>
const currency: Currency =
searchParams.currency === 'Credits' ? 'Credits' : 'Latinum'
return (
<CurrencyContext.Provider value={{ currency }}>
{children}
</CurrencyContext.Provider>
)
}
export function useCurrency() {
return useContext(CurrencyContext)
}

View file

@ -0,0 +1,198 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as ScheduleRouteImport } from './routes/schedule'
import { Route as IndexRouteImport } from './routes/index'
import { Route as QuadrantsIndexRouteImport } from './routes/quadrants/index'
import { Route as LedgerIndexRouteImport } from './routes/ledger/index'
import { Route as RegistryVenuesRouteImport } from './routes/registry/venues'
import { Route as RegistryFactionsRouteImport } from './routes/registry/factions'
import { Route as QuadrantsSectorIdRouteImport } from './routes/quadrants/$sectorId'
const ScheduleRoute = ScheduleRouteImport.update({
id: '/schedule',
path: '/schedule',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const QuadrantsIndexRoute = QuadrantsIndexRouteImport.update({
id: '/quadrants/',
path: '/quadrants/',
getParentRoute: () => rootRouteImport,
} as any)
const LedgerIndexRoute = LedgerIndexRouteImport.update({
id: '/ledger/',
path: '/ledger/',
getParentRoute: () => rootRouteImport,
} as any)
const RegistryVenuesRoute = RegistryVenuesRouteImport.update({
id: '/registry/venues',
path: '/registry/venues',
getParentRoute: () => rootRouteImport,
} as any)
const RegistryFactionsRoute = RegistryFactionsRouteImport.update({
id: '/registry/factions',
path: '/registry/factions',
getParentRoute: () => rootRouteImport,
} as any)
const QuadrantsSectorIdRoute = QuadrantsSectorIdRouteImport.update({
id: '/quadrants/$sectorId',
path: '/quadrants/$sectorId',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/schedule': typeof ScheduleRoute
'/quadrants/$sectorId': typeof QuadrantsSectorIdRoute
'/registry/factions': typeof RegistryFactionsRoute
'/registry/venues': typeof RegistryVenuesRoute
'/ledger/': typeof LedgerIndexRoute
'/quadrants/': typeof QuadrantsIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/schedule': typeof ScheduleRoute
'/quadrants/$sectorId': typeof QuadrantsSectorIdRoute
'/registry/factions': typeof RegistryFactionsRoute
'/registry/venues': typeof RegistryVenuesRoute
'/ledger': typeof LedgerIndexRoute
'/quadrants': typeof QuadrantsIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/schedule': typeof ScheduleRoute
'/quadrants/$sectorId': typeof QuadrantsSectorIdRoute
'/registry/factions': typeof RegistryFactionsRoute
'/registry/venues': typeof RegistryVenuesRoute
'/ledger/': typeof LedgerIndexRoute
'/quadrants/': typeof QuadrantsIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/schedule'
| '/quadrants/$sectorId'
| '/registry/factions'
| '/registry/venues'
| '/ledger/'
| '/quadrants/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/schedule'
| '/quadrants/$sectorId'
| '/registry/factions'
| '/registry/venues'
| '/ledger'
| '/quadrants'
id:
| '__root__'
| '/'
| '/schedule'
| '/quadrants/$sectorId'
| '/registry/factions'
| '/registry/venues'
| '/ledger/'
| '/quadrants/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ScheduleRoute: typeof ScheduleRoute
QuadrantsSectorIdRoute: typeof QuadrantsSectorIdRoute
RegistryFactionsRoute: typeof RegistryFactionsRoute
RegistryVenuesRoute: typeof RegistryVenuesRoute
LedgerIndexRoute: typeof LedgerIndexRoute
QuadrantsIndexRoute: typeof QuadrantsIndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/schedule': {
id: '/schedule'
path: '/schedule'
fullPath: '/schedule'
preLoaderRoute: typeof ScheduleRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/quadrants/': {
id: '/quadrants/'
path: '/quadrants'
fullPath: '/quadrants/'
preLoaderRoute: typeof QuadrantsIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/ledger/': {
id: '/ledger/'
path: '/ledger'
fullPath: '/ledger/'
preLoaderRoute: typeof LedgerIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/registry/venues': {
id: '/registry/venues'
path: '/registry/venues'
fullPath: '/registry/venues'
preLoaderRoute: typeof RegistryVenuesRouteImport
parentRoute: typeof rootRouteImport
}
'/registry/factions': {
id: '/registry/factions'
path: '/registry/factions'
fullPath: '/registry/factions'
preLoaderRoute: typeof RegistryFactionsRouteImport
parentRoute: typeof rootRouteImport
}
'/quadrants/$sectorId': {
id: '/quadrants/$sectorId'
path: '/quadrants/$sectorId'
fullPath: '/quadrants/$sectorId'
preLoaderRoute: typeof QuadrantsSectorIdRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ScheduleRoute: ScheduleRoute,
QuadrantsSectorIdRoute: QuadrantsSectorIdRoute,
RegistryFactionsRoute: RegistryFactionsRoute,
RegistryVenuesRoute: RegistryVenuesRoute,
LedgerIndexRoute: LedgerIndexRoute,
QuadrantsIndexRoute: QuadrantsIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}

View file

@ -0,0 +1,19 @@
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function getRouter() {
const router = createTanStackRouter({
routeTree,
scrollRestoration: true,
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}

View file

@ -0,0 +1,130 @@
import {
HeadContent,
Outlet,
Scripts,
createRootRoute,
} from '@tanstack/react-router'
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools'
import { TanStackDevtools } from '@tanstack/react-devtools'
import appCss from '../styles.css?url'
import { AsciiPanel } from '#/components/ascii/AsciiPanel'
import { NavigationMenu } from '#/components/panels/NavigationMenu'
import { BetslipProvider } from '#/components/providers/BetslipProvider'
import { CurrencyProvider } from '#/components/providers/CurrencyProvider'
import { BetslipSidecar } from '#/components/panels/BetslipSidecar'
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: "Quark's Holo-Grid Ledger",
},
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
}),
shellComponent: RootDocument,
component: RootComponent,
})
// RootDocument renders the outer HTML envelope
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html
lang="en"
className="bg-black text-slate-300 font-mono select-none antialiased h-screen overflow-hidden"
>
<head>
<HeadContent />
</head>
<body className="h-screen max-h-screen p-3 flex flex-col gap-3 box-border bg-black">
{children}
<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
]}
/>
<Scripts />
</body>
</html>
)
}
// RootComponent renders the actual terminal UI workspace layout
function RootComponent() {
return (
<BetslipProvider>
<CurrencyProvider>
<div className="flex flex-col gap-3 h-full w-full">
{/* ================= HEADER SECTION ================= */}
<header className="flex justify-between items-center p-3 border border-slate-800 bg-slate-950 text-xs tracking-widest text-slate-400">
<div className="font-bold text-amber-500">
[=] QUARK'S HOLO-GRID LEDGER // SUB-NET RECEPTOR
</div>
<div className="flex gap-6">
<span>STARDATE: 54201.3</span>
<span className="text-emerald-400">
BALANCE: 4,250.00 LATINUM
</span>
</div>
</header>
{/* ================= WORKSPACE INTERFACE GRID ================= */}
<main className="flex-1 grid grid-cols-12 gap-3 min-h-0">
{/* COLUMN 1: Subspace Navigation Sidecar (Left) */}
<section className="col-span-3 h-full min-h-0">
<AsciiPanel
title="System Array"
subtitle="Quadrants"
variant="slate"
>
<NavigationMenu />
</AsciiPanel>
</section>
{/* COLUMN 2: Target Matrix / Main Display (Center) */}
<section className="col-span-6 h-full min-h-0">
{/* The Outlet element renders the individual nested route content dynamically */}
<Outlet />
</section>
{/* COLUMN 3: Transaction Ledger Ticket (Right) */}
<section className="col-span-3 h-full min-h-0">
<AsciiPanel
title="Wager Slip"
subtitle="Secure Link"
variant="amber"
>
<BetslipSidecar />
</AsciiPanel>
</section>
</main>
{/* ================= DIAGNOSTIC BASE FOOTER ================= */}
<footer className="p-2 border border-slate-900 bg-black/60 text-[10px] text-slate-600 flex justify-between tracking-wide">
<span>SYSTEM RUNTIME: OK // CORE MODULES ACTIVE</span>
<span>POWERED BY TANSTACK START + VITE</span>
</footer>
</div>
</CurrencyProvider>
</BetslipProvider>
)
}

View file

@ -0,0 +1,282 @@
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { AsciiPanel } from '#/components/ascii/AsciiPanel'
import { QuadrantFilterBar } from '#/components/panels/QuadrantFilterBar'
import type { MatchData } from '#/components/panels/MatchCard'
import { MatchCard } from '#/components/panels/MatchCard'
import { LiveTicker } from '#/components/panels/LiveTicker'
// Define schema for deterministic URL state filtering (AGENTS.md Rule A compliance)
const dashboardSearchSchema = z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta', 'all']).default('all'),
currency: z.enum(['Latinum', 'Credits']).default('Latinum'),
})
export const Route = createFileRoute('/')({
validateSearch: dashboardSearchSchema,
component: DashboardView,
})
function DashboardView() {
const { quadrant } = Route.useSearch()
// Full active matches pool matching taxonomy.yaml configuration
const matches: MatchData[] = [
{
id: 'sector-001',
name: 'Sector 001 — Sol Array',
quadrant: 'alpha',
status: 'LIVE',
tick: 42,
maxTicks: 90,
kineticYield: 14.7,
homeFaction: {
abbreviation: 'SFC',
name: 'Starfleet Command',
glyph: '◆',
colorCode: '#34d399',
shieldCapacity: 62,
oddsPrice: 2.85,
},
awayFaction: {
abbreviation: 'BRG',
name: 'Borg Collective',
glyph: '█',
colorCode: '#38bdf8',
shieldCapacity: 71,
oddsPrice: 1.48,
},
marketsOpenCount: 6,
},
{
id: 'bajor-wormhole',
name: 'Bajor Sector — DS9',
quadrant: 'alpha',
status: 'LIVE',
tick: 31,
maxTicks: 75,
kineticYield: 8.2,
homeFaction: {
abbreviation: 'BAJ',
name: 'Bajoran Militia',
glyph: '☼',
colorCode: '#fb923c',
shieldCapacity: 48,
oddsPrice: 2.1,
},
awayFaction: {
abbreviation: 'DOM',
name: 'The Dominion',
glyph: '◉',
colorCode: '#fb7185',
shieldCapacity: 55,
oddsPrice: 1.72,
},
marketsOpenCount: 8,
},
{
id: 'wolf-359',
name: 'Wolf 359 Outpost',
quadrant: 'alpha',
status: 'PRE',
tick: 0,
maxTicks: 90,
kineticYield: 0.0,
homeFaction: {
abbreviation: 'SFC',
name: 'Starfleet Command',
glyph: '◆',
colorCode: '#34d399',
shieldCapacity: 100,
oddsPrice: 2.4,
},
awayFaction: {
abbreviation: 'BRG',
name: 'Borg Collective',
glyph: '█',
colorCode: '#38bdf8',
shieldCapacity: 100,
oddsPrice: 1.62,
},
marketsOpenCount: 4,
commencingIn: '00:47',
},
{
id: 'qonos-arena',
name: "Qo'noS Sector — Warrior's Ring",
quadrant: 'beta',
status: 'LIVE',
tick: 58,
maxTicks: 120,
kineticYield: 12.4,
homeFaction: {
abbreviation: 'KLG',
name: 'Klingon Empire',
glyph: '⚔',
colorCode: '#f87171',
shieldCapacity: 88,
oddsPrice: 1.75,
},
awayFaction: {
abbreviation: 'ROM',
name: 'Romulan Star Empire',
glyph: '◈',
colorCode: '#a78bfa',
shieldCapacity: 72,
oddsPrice: 2.15,
},
marketsOpenCount: 6,
},
{
id: 'neutral-zone',
name: 'The Neutral Zone',
quadrant: 'beta',
status: 'LIVE',
tick: 15,
maxTicks: 80,
kineticYield: 4.2,
homeFaction: {
abbreviation: 'ROM',
name: 'Romulan Star Empire',
glyph: '◈',
colorCode: '#a78bfa',
shieldCapacity: 90,
oddsPrice: 1.95,
},
awayFaction: {
abbreviation: 'SFC',
name: 'Starfleet Command',
glyph: '◆',
colorCode: '#34d399',
shieldCapacity: 95,
oddsPrice: 1.85,
},
marketsOpenCount: 8,
},
{
id: 'karemma-trade-corridor',
name: 'Karemma Trade Corridor',
quadrant: 'gamma',
status: 'LIVE',
tick: 73,
maxTicks: 100,
kineticYield: 18.1,
homeFaction: {
abbreviation: 'KRM',
name: 'Karemma Trade Alliance',
glyph: '◊',
colorCode: '#fcd34d',
shieldCapacity: 65,
oddsPrice: 2.3,
},
awayFaction: {
abbreviation: 'DOS',
name: 'Dominion Outpost 6',
glyph: '▬',
colorCode: '#c084fc',
shieldCapacity: 80,
oddsPrice: 1.6,
},
marketsOpenCount: 6,
},
{
id: 'grid-325',
name: 'Grid 325 — Borg Corridor',
quadrant: 'delta',
status: 'LIVE',
tick: 82,
maxTicks: 90,
kineticYield: 24.5,
homeFaction: {
abbreviation: 'BRG',
name: 'Borg Collective',
glyph: '█',
colorCode: '#38bdf8',
shieldCapacity: 15,
oddsPrice: 9.5,
},
awayFaction: {
abbreviation: 'S84',
name: 'Species 8472',
glyph: '∞',
colorCode: '#ec4899',
shieldCapacity: 99,
oddsPrice: 1.08,
},
marketsOpenCount: 8,
},
{
id: 'hirogen-relay',
name: 'Hirogen Relay Nexus',
quadrant: 'delta',
status: 'LIVE',
tick: 49,
maxTicks: 100,
kineticYield: 9.3,
homeFaction: {
abbreviation: 'HRG',
name: 'Hirogen Hunt Pack',
glyph: '⊗',
colorCode: '#fbbf24',
shieldCapacity: 75,
oddsPrice: 1.55,
},
awayFaction: {
abbreviation: 'KZN',
name: 'Kazon Sects',
glyph: '',
colorCode: '#f97316',
shieldCapacity: 40,
oddsPrice: 2.45,
},
marketsOpenCount: 6,
},
]
// Filter matches based on the URL query param state
const filteredMatches =
quadrant === 'all'
? matches
: matches.filter((m) => m.quadrant === quadrant)
return (
<AsciiPanel
title="Console Terminal"
subtitle="Active Feeds"
variant="emerald"
>
<div className="space-y-4 font-mono select-none h-full flex flex-col justify-between">
{/* Broadcast Banner */}
<div>
<div className="text-emerald-400 font-bold text-xs uppercase tracking-wider mb-2">
&gt; SUBSPACE BROADCAST LIVE ENGAGEMENTS ACROSS ALL QUADRANTS
</div>
<QuadrantFilterBar currentQuadrant={quadrant} />
</div>
{/* Matches Grid Area */}
<div className="flex-1 overflow-y-auto min-h-0 my-3 pr-1">
{filteredMatches.length === 0 ? (
<div className="p-8 border border-dashed border-slate-900 text-center italic text-slate-600">
No active engagements detected in this quadrant division array.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{filteredMatches.map((match) => (
<MatchCard key={match.id} match={match} />
))}
</div>
)}
</div>
{/* Live Feed Event Ticker */}
<div>
<div className="text-slate-600 text-[10px] font-bold tracking-wider mb-1.5 px-0.5">
&gt; LIVE GLOBAL TELEMETRY FEED TICKER
</div>
<LiveTicker />
</div>
</div>
</AsciiPanel>
)
}

View file

@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/ledger/')({
component: () => <div>Wager Ledger Archives</div>,
})

View file

@ -0,0 +1,403 @@
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { AsciiPanel } from '#/components/ascii/AsciiPanel'
import { VersusArena } from '#/components/panels/VersusArena'
import type { MarketItem } from '#/components/panels/MarketGrid'
import { MarketGrid } from '#/components/panels/MarketGrid'
import type { CombatLogEntry } from '#/components/panels/EngagementLog'
import { EngagementLog } from '#/components/panels/EngagementLog'
// Search parameters validator matching AGENTS.md Rule A compliance
const searchSchema = z.object({
quadrant: z.enum(['alpha', 'beta', 'gamma', 'delta']),
currency: z.enum(['Latinum', 'Credits']).default('Latinum'),
marketCategory: z
.enum(['primary', 'secondary', 'exotic', 'all'])
.default('all'),
})
export const Route = createFileRoute('/quadrants/$sectorId')({
validateSearch: searchSchema,
component: MatchDetailView,
})
function MatchDetailView() {
const { sectorId } = Route.useParams()
const { marketCategory } = Route.useSearch()
// Full detailed mock matches database matching taxonomy.yaml
const matchesDetails:
| Record<
string,
{
name: string
status: 'LIVE' | 'PRE' | 'RES'
kineticYield: number
tick: number
maxTicks: number
venue: {
name: string
coordinates: string
shieldModifier: number
damageModifier: number
evasionModifier: number
tags: string[]
}
homeFaction: {
name: string
abbreviation: string
glyph: string
colorCode: string
shieldCapacity: number
attack: number
evasion: number
activeEffects: Array<{
name: string
description: string
remainingTicks: number
}>
}
awayFaction: {
name: string
abbreviation: string
glyph: string
colorCode: string
shieldCapacity: number
attack: number
evasion: number
activeEffects: Array<{
name: string
description: string
remainingTicks: number
}>
}
markets: MarketItem[]
logs: CombatLogEntry[]
}
>
| undefined = {
'sector-001': {
name: 'Sector 001 — Sol Array',
status: 'LIVE',
kineticYield: 14.7,
tick: 42,
maxTicks: 90,
venue: {
name: 'Sol System Orbit',
coordinates: '001-ALPHA-0',
shieldModifier: 1.0,
damageModifier: 1.0,
evasionModifier: 1.0,
tags: ['orbital-grid', 'starfleet-array'],
},
homeFaction: {
name: 'Starfleet Command',
abbreviation: 'SFC',
glyph: '◆',
colorCode: '#34d399',
shieldCapacity: 62,
attack: 75,
evasion: 55,
activeEffects: [],
},
awayFaction: {
name: 'Borg Collective',
abbreviation: 'BRG',
glyph: '█',
colorCode: '#38bdf8',
shieldCapacity: 71,
attack: 90,
evasion: 15,
activeEffects: [
{
name: 'BORG ADAPTATION',
description: 'Incoming damage multiplier ×0.5',
remainingTicks: 3,
},
],
},
markets: [
{
id: 's1-m1',
templateId: 'moneyline',
name: 'COMBAT RESOLUTION — MONEYLINE',
category: 'primary',
selections: [
{
id: 's1-m1-o1',
name: 'Starfleet Victory',
price: 2.85,
factionGlyph: '◆',
},
{
id: 's1-m1-o2',
name: 'Borg Assimilation',
price: 1.48,
factionGlyph: '█',
},
],
},
{
id: 's1-m2',
templateId: 'first-breach',
name: 'FIRST SHIELD BREACH',
category: 'primary',
selections: [
{
id: 's1-m2-o1',
name: 'SFC Shields First',
price: 1.65,
factionGlyph: '◆',
},
{
id: 's1-m2-o2',
name: 'BRG Shields First',
price: 2.2,
factionGlyph: '█',
},
],
},
{
id: 's1-m3',
templateId: 'kinetic-yield',
name: 'TOTAL KINETIC YIELD',
category: 'secondary',
selections: [
{
id: 's1-m3-o1',
name: 'Over 18.5 TW',
price: 1.85,
factionGlyph: '◇',
},
{
id: 's1-m3-o2',
name: 'Under 18.5 TW',
price: 1.85,
factionGlyph: '◇',
},
],
},
],
logs: [
{
id: 'l1',
tick: 42,
message: '◆ Starfleet Command fires — HIT — 4.2% shield damage',
type: 'combat',
},
{
id: 'l2',
tick: 42,
message: '█ Borg Collective fires — MISS — Evasion successful',
type: 'combat',
},
{
id: 'l3',
tick: 38,
message:
'⚡ EVENT: Borg Adaptation active — Incoming damage multiplier ×0.5',
type: 'event',
},
],
},
'bajor-wormhole': {
name: 'Bajor Sector — DS9',
status: 'LIVE',
kineticYield: 8.2,
tick: 31,
maxTicks: 75,
venue: {
name: 'Denorios Belt — Wormhole',
coordinates: 'BAJ-WORM-1',
shieldModifier: 0.85,
damageModifier: 1.15,
evasionModifier: 0.9,
tags: ['high-gravitational', 'plasma-clouds'],
},
homeFaction: {
name: 'Bajoran Militia',
abbreviation: 'BAJ',
glyph: '☼',
colorCode: '#fb923c',
shieldCapacity: 48,
attack: 60,
evasion: 75,
activeEffects: [],
},
awayFaction: {
name: 'The Dominion',
abbreviation: 'DOM',
glyph: '◉',
colorCode: '#fb7185',
shieldCapacity: 55,
attack: 85,
evasion: 40,
activeEffects: [],
},
markets: [
{
id: 's2-m1',
templateId: 'moneyline',
name: 'COMBAT RESOLUTION — MONEYLINE',
category: 'primary',
selections: [
{
id: 's2-m1-o1',
name: 'Bajoran Defiance',
price: 2.1,
factionGlyph: '☼',
},
{
id: 's2-m1-o2',
name: 'Dominion Conquest',
price: 1.72,
factionGlyph: '◉',
},
],
},
],
logs: [
{
id: 'l2-1',
tick: 31,
message: '☼ Bajoran Militia fires — HIT — 3.1% shield damage',
type: 'combat',
},
{
id: 'l2-2',
tick: 30,
message: '◉ The Dominion fires — HIT — 5.4% shield damage',
type: 'combat',
},
],
},
'wolf-359': {
name: 'Wolf 359 Outpost',
status: 'PRE',
kineticYield: 0.0,
tick: 0,
maxTicks: 90,
venue: {
name: 'Wolf 359 Debris Field',
coordinates: '359-DEBRIS-2',
shieldModifier: 0.95,
damageModifier: 1.1,
evasionModifier: 0.8,
tags: ['debris-field', 'high-anomaly'],
},
homeFaction: {
name: 'Starfleet Command',
abbreviation: 'SFC',
glyph: '◆',
colorCode: '#34d399',
shieldCapacity: 100,
attack: 75,
evasion: 55,
activeEffects: [],
},
awayFaction: {
name: 'Borg Collective',
abbreviation: 'BRG',
glyph: '█',
colorCode: '#38bdf8',
shieldCapacity: 100,
attack: 90,
evasion: 15,
activeEffects: [],
},
markets: [
{
id: 's3-m1',
templateId: 'moneyline',
name: 'COMBAT RESOLUTION — MONEYLINE',
category: 'primary',
selections: [
{
id: 's3-m1-o1',
name: 'Starfleet Victory',
price: 2.4,
factionGlyph: '◆',
},
{
id: 's3-m1-o2',
name: 'Borg Assimilation',
price: 1.62,
factionGlyph: '█',
},
],
},
],
logs: [
{
id: 'l3-1',
tick: 0,
message:
'Subspace communications linked. Awaiting combat deployment.',
type: 'system',
},
],
},
}
// Get current match detail
const match = (matchesDetails as Record<string, any>)[sectorId]
if (!match) {
return (
<AsciiPanel
title="Engagement Matrix"
subtitle="Telemetry Lost"
variant="amber"
>
<div className="p-8 text-center font-mono text-xs text-rose-500 italic space-y-4">
<div>&gt; ERROR: SECTOR ARRAY COORDINATES OFFLINE</div>
<div className="text-slate-400">
Link signals for sector [{sectorId}] cannot be parsed. Select an
active fleet connection link from the left array.
</div>
</div>
</AsciiPanel>
)
}
return (
<AsciiPanel
title="Engagement Matrix"
subtitle={match.name}
variant="emerald"
>
<div className="space-y-4 font-mono select-none h-full flex flex-col justify-between">
{/* Versus Arena header and stats */}
<VersusArena
home={match.homeFaction}
away={match.awayFaction}
kineticYield={match.kineticYield}
tick={match.tick}
maxTicks={match.maxTicks}
venue={match.venue}
/>
{/* Markets configuration grid */}
<div>
<div className="text-emerald-400 font-bold text-xs uppercase tracking-wider mb-2">
&gt; AVAILABLE ODDS MATRICES
</div>
<MarketGrid
matchId={sectorId}
matchName={match.name}
markets={match.markets}
currentCategory={marketCategory}
/>
</div>
{/* Live log feed specific to match */}
<div>
<div className="text-slate-600 text-[10px] font-bold tracking-wider mb-1.5 px-0.5">
&gt; TACTICAL ENGAGEMENT LOG
</div>
<EngagementLog logs={match.logs} />
</div>
</div>
</AsciiPanel>
)
}

View file

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/quadrants/')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/quadrants/"!</div>
}

View file

@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/registry/factions')({
component: () => <div>Faction Registry Database</div>,
})

View file

@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/registry/venues')({
component: () => <div>Venue Sector Database</div>,
})

View file

@ -0,0 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/schedule')({
component: () => <div>Transmission Schedule</div>,
})

View file

@ -0,0 +1,15 @@
@import 'tailwindcss';
* {
box-sizing: border-box;
}
html,
body,
#app {
min-height: 100%;
}
body {
margin: 0;
}

View file

@ -0,0 +1,35 @@
{
"include": [
"**/*.ts",
"**/*.tsx",
"eslint.config.js",
"prettier.config.js",
"vite.config.js"
],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"paths": {
"#/*": ["./src/*"],
"@/*": ["./src/*"]
},
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}

View file

@ -0,0 +1,3 @@
{
"target": "react"
}

View file

@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { nitro } from 'nitro/vite'
const config = defineConfig({
resolve: { tsconfigPaths: true },
plugins: [
devtools(),
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
tailwindcss(),
tanstackStart(),
viteReact(),
],
})
export default config