initial import
This commit is contained in:
commit
caa0e4225f
91 changed files with 14952 additions and 0 deletions
19
.cta.json
Normal file
19
.cta.json
Normal 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
13
.gitignore
vendored
Normal 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
11
.vscode/settings.json
vendored
Normal 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
35
Makefile
Normal 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
55
README.md
Normal 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
1056
data-source/taxonomy.yaml
Normal file
File diff suppressed because it is too large
Load diff
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal 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
2
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
allowBuilds:
|
||||
unrs-resolver: set this to true or false
|
||||
989
reference/ENGINE.md
Normal file
989
reference/ENGINE.md
Normal 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` | 30–90s | Closed | Match announced, factions & venue visible, no betting |
|
||||
| `PRE_MATCH` | 60s | Open (pre-match odds) | Initial odds calculated, pre-match wagers accepted |
|
||||
| `LIVE` | 60–150 ticks (2–5 min) | Open (live odds) | Active combat simulation, odds shift each tick |
|
||||
| `RESOLVING` | 3 ticks (6s) | **Suspended** | Winner determined, final state locked, dramatic pause |
|
||||
| `SETTLED` | 10s | Closed (final) | Results committed, markets graded, match archived |
|
||||
|
||||
### Concurrent Match Capacity
|
||||
|
||||
The engine should maintain a **rolling window** of active matches:
|
||||
|
||||
```go
|
||||
const (
|
||||
TargetActiveMatches = 8 // Aim for 8 concurrent live matches
|
||||
MaxActiveMatches = 12 // Hard cap
|
||||
MinActiveMatches = 4 // Trigger new match creation if below
|
||||
MatchCreationCooldown = 15 // Seconds between new match spawns
|
||||
)
|
||||
```
|
||||
|
||||
The scheduler continuously checks: if `active < MinActiveMatches`, create new matches until `TargetActiveMatches` is reached. This guarantees the frontend always has events to display across all quadrants.
|
||||
|
||||
---
|
||||
|
||||
## 3. Matchmaker — Pairing Algorithm
|
||||
|
||||
The matchmaker selects two factions and a venue for each new engagement.
|
||||
|
||||
### 3.1 Faction Pairing Rules
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ PAIRING PROBABILITY WEIGHTS │
|
||||
├─────────────────────────────────────────────────┤
|
||||
│ Same-quadrant matchup │ 60% chance │
|
||||
│ Cross-quadrant matchup │ 30% chance │
|
||||
│ Cross-quadrant (distant) │ 10% chance │
|
||||
│ (Alpha↔Delta, Beta↔Gamma) │ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- A faction cannot appear in more than 2 simultaneous active matches
|
||||
- Repeat matchups (same two factions) require a 3-match cooldown
|
||||
- Rivalry matchups (high rivalry_modifier) get a 2x selection weight boost — they're marquee events
|
||||
|
||||
### 3.2 Venue Selection
|
||||
|
||||
```go
|
||||
func SelectVenue(home, away FactionDef, taxonomy Taxonomy) SectorPoolItem {
|
||||
// 1. Prefer venues in the home faction's quadrant (60%)
|
||||
// 2. Neutral venues from either faction's quadrant (30%)
|
||||
// 3. Random venue from any quadrant (10%)
|
||||
|
||||
// Weight boost for thematic venues:
|
||||
// - If home faction is Klingon and venue is "qonos-arena" → 3x weight
|
||||
// - If matchup has rivalry_modifier > 1.20 → prefer "high-profile" tagged venues
|
||||
|
||||
// Environmental modifiers from the venue apply to the entire match
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Initial Odds Calculation
|
||||
|
||||
Pre-match odds derive from **faction combat stats + venue modifiers + rivalry premium**:
|
||||
|
||||
```go
|
||||
func CalculatePreMatchOdds(home, away FactionDef, venue SectorPoolItem) float64 {
|
||||
// 1. Calculate effective power ratings
|
||||
homePower := (float64(home.BaseShield) * venue.ShieldModifier * 0.4) +
|
||||
(float64(home.BaseAttack) * venue.DamageModifier * 0.4) +
|
||||
(float64(home.BaseEvasion) * venue.EvasionModifier * 0.2)
|
||||
|
||||
awayPower := (float64(away.BaseShield) * venue.ShieldModifier * 0.4) +
|
||||
(float64(away.BaseAttack) * venue.DamageModifier * 0.4) +
|
||||
(float64(away.BaseEvasion) * venue.EvasionModifier * 0.2)
|
||||
|
||||
// 2. Convert power differential to probability via logistic function
|
||||
diff := homePower - awayPower
|
||||
homeProb := 1.0 / (1.0 + math.Exp(-diff / ScalingFactor))
|
||||
|
||||
// 3. Apply rivalry modifier (inflates drama, widens spread)
|
||||
rivalryMod := home.RivalryModifiers[away.ID] // e.g., 1.35
|
||||
// Rivalry pushes odds away from 50/50 — makes favorites more favored
|
||||
|
||||
// 4. Convert probability to decimal odds
|
||||
homeOdds := 1.0 / homeProb
|
||||
awayOdds := 1.0 / (1.0 - homeProb)
|
||||
|
||||
// 5. Apply house margin (overround)
|
||||
// Target: 105-108% overround (realistic sportsbook margin)
|
||||
margin := 1.06
|
||||
homeOdds = homeOdds / margin
|
||||
awayOdds = awayOdds / margin
|
||||
|
||||
return homeOdds // (and awayOdds)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Combat Simulator — The Tick Loop
|
||||
|
||||
Every 2 seconds, each active match processes one **combat tick**.
|
||||
|
||||
### 4.1 Tick Sequence
|
||||
|
||||
```
|
||||
FOR each active match:
|
||||
│
|
||||
├─ 1. RESOLVE ACTIVE EFFECTS
|
||||
│ └─ Process lingering buffs/debuffs from prior events
|
||||
│
|
||||
├─ 2. CALCULATE EFFECTIVE STATS
|
||||
│ ├─ Base stats × venue modifiers × active effects
|
||||
│ └─ Factor in type advantages (aggressive vs defensive, etc.)
|
||||
│
|
||||
├─ 3. COMBAT EXCHANGE
|
||||
│ ├─ Both factions attack simultaneously
|
||||
│ ├─ Roll hit/miss for each (attack_power vs opponent evasion)
|
||||
│ ├─ Calculate damage dealt (if hit)
|
||||
│ ├─ Apply damage to opponent shields
|
||||
│ └─ Accumulate kinetic_yield (total energy released)
|
||||
│
|
||||
├─ 4. EVENT ROLL
|
||||
│ ├─ Roll against venue.special_event_chance
|
||||
│ ├─ Roll against faction-specific event triggers
|
||||
│ └─ If event fires → apply effects, log to event stream
|
||||
│
|
||||
├─ 5. CHECK WIN CONDITIONS
|
||||
│ ├─ Shield collapse (one faction hits 0%)
|
||||
│ ├─ Mutual destruction (both hit 0% same tick)
|
||||
│ └─ Time limit (max ticks reached → highest shields wins)
|
||||
│
|
||||
├─ 6. RECALCULATE ALL MARKET PROBABILITIES
|
||||
│ └─ Feed current state to probability engine
|
||||
│
|
||||
└─ 7. PUSH TO VALKEY
|
||||
└─ Atomic JSON state update
|
||||
```
|
||||
|
||||
### 4.2 Combat Resolution Math
|
||||
|
||||
```go
|
||||
type TickResult struct {
|
||||
HomeDamageDealt float64
|
||||
AwayDamageDealt float64
|
||||
HomeShieldsAfter float64
|
||||
AwayShieldsAfter float64
|
||||
KineticYieldDelta float64
|
||||
EventFired *MatchEvent // nil if no event
|
||||
}
|
||||
|
||||
func ResolveCombatTick(match *LiveMatch) TickResult {
|
||||
home := &match.Factions.Home
|
||||
away := &match.Factions.Away
|
||||
venue := match.Venue
|
||||
|
||||
// Effective stats = base × venue modifier × active effects
|
||||
homeAttack := float64(home.EffectiveAttack) * venue.DamageModifier
|
||||
awayAttack := float64(away.EffectiveAttack) * venue.DamageModifier
|
||||
homeEvasion := float64(home.EffectiveEvasion) * venue.EvasionModifier
|
||||
awayEvasion := float64(away.EffectiveEvasion) * venue.EvasionModifier
|
||||
|
||||
// Hit chance = attacker_power / (attacker_power + defender_evasion)
|
||||
// This creates a natural 0-1 probability range
|
||||
homeHitChance := homeAttack / (homeAttack + awayEvasion)
|
||||
awayHitChance := awayAttack / (awayAttack + homeEvasion)
|
||||
|
||||
// Roll for hits
|
||||
var homeDmg, awayDmg float64
|
||||
if rand.Float64() < homeHitChance {
|
||||
// Home hits away — damage = base * random(0.6, 1.4) variance
|
||||
homeDmg = homeAttack * (0.6 + rand.Float64()*0.8) * 0.1 // Scale to shield %
|
||||
}
|
||||
if rand.Float64() < awayHitChance {
|
||||
awayDmg = awayAttack * (0.6 + rand.Float64()*0.8) * 0.1
|
||||
}
|
||||
|
||||
// Apply type advantage multipliers
|
||||
homeDmg *= TypeAdvantage(home.Type, away.Type)
|
||||
awayDmg *= TypeAdvantage(away.Type, home.Type)
|
||||
|
||||
// Apply damage to shields (clamped to 0-100)
|
||||
away.ShieldCapacity = clamp(away.ShieldCapacity - homeDmg, 0, 100)
|
||||
home.ShieldCapacity = clamp(home.ShieldCapacity - awayDmg, 0, 100)
|
||||
|
||||
// Accumulate kinetic yield (total energy released = total damage dealt)
|
||||
kineticDelta := homeDmg + awayDmg
|
||||
match.KineticYield += kineticDelta
|
||||
|
||||
return TickResult{
|
||||
HomeDamageDealt: homeDmg,
|
||||
AwayDamageDealt: awayDmg,
|
||||
HomeShieldsAfter: home.ShieldCapacity,
|
||||
AwayShieldsAfter: away.ShieldCapacity,
|
||||
KineticYieldDelta: kineticDelta,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Type Advantage Matrix
|
||||
|
||||
Combat types create a rock-paper-scissors dynamic:
|
||||
|
||||
```
|
||||
DEFENDER
|
||||
aggressive defensive tactical infiltrator swarm
|
||||
ATTACKER ┌──────────────────────────────────────────────────────┐
|
||||
aggressive│ 1.00 0.80 1.20 1.30 0.90 │
|
||||
defensive │ 1.20 1.00 0.90 0.80 1.10 │
|
||||
tactical │ 0.80 1.10 1.00 1.20 1.00 │
|
||||
infiltratr│ 0.70 1.20 0.80 1.00 1.30 │
|
||||
swarm │ 1.10 0.90 1.00 0.70 1.00 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
|
||||
Key interactions:
|
||||
aggressive → infiltrator: 1.30 (brute force overwhelms trickery)
|
||||
defensive → aggressive: 1.20 (shields absorb the charge)
|
||||
infiltrator→ defensive: 1.20 (bypasses shield walls)
|
||||
tactical → aggressive: counterplay
|
||||
swarm → infiltrator: 0.70 (can't find them all)
|
||||
infiltrator→ swarm: 1.30 (surgical strikes on hive nodes)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Random Event System
|
||||
|
||||
Events inject narrative drama and create sudden odds shifts — the equivalent of a red card, injury, or weather delay in real sports.
|
||||
|
||||
### 5.1 Event Architecture
|
||||
|
||||
```go
|
||||
type MatchEvent struct {
|
||||
ID string // Unique event instance ID
|
||||
TemplateID string // Reference to event template
|
||||
Name string // Display name
|
||||
Description string // Narrative text for UI feed
|
||||
Tick int // When it fired
|
||||
Category EventCategory // combat | environmental | faction | market
|
||||
Effects []EventEffect // Stat modifications
|
||||
Duration int // Ticks the effect lasts (0 = instant)
|
||||
Severity string // minor | major | critical
|
||||
}
|
||||
|
||||
type EventEffect struct {
|
||||
Target string // "home" | "away" | "both" | "market"
|
||||
Stat string // "shield" | "attack" | "evasion" | "price"
|
||||
Operation string // "add" | "multiply" | "set"
|
||||
Value float64
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Event Categories & Templates
|
||||
|
||||
#### COMBAT EVENTS (triggered by combat state)
|
||||
|
||||
| Event | Trigger | Effect | Duration | Severity |
|
||||
|-------|---------|--------|----------|----------|
|
||||
| **Critical Hit** | 5% per hit | 3× damage on this hit | Instant | minor |
|
||||
| **Shield Harmonics Failure** | When shields < 30% | Shields drop additional 10% | Instant | major |
|
||||
| **Weapons Overload** | 3% per tick | Attacker deals 2× but takes 5% self-damage | Instant | major |
|
||||
| **Emergency Shield Reroute** | When shields < 15% | Restore 20% shields, disable attack for 3 ticks | 3 ticks | critical |
|
||||
| **Hull Breach** | When shields hit 0% for first time | Shields cannot regenerate above 25% | Permanent | critical |
|
||||
|
||||
#### ENVIRONMENTAL EVENTS (triggered by venue)
|
||||
|
||||
| Event | Base Chance | Venue Tags | Effect | Duration |
|
||||
|-------|-------------|------------|--------|----------|
|
||||
| **Plasma Storm Surge** | venue.special_event_chance | plasma-storms | Both shields −15% | Instant |
|
||||
| **Gravitational Anomaly** | venue.special_event_chance × 0.5 | any | Both evasion ×0.5 | 5 ticks |
|
||||
| **Subspace Rift** | venue.special_event_chance × 0.3 | temporal-anomaly | Random shield reset (both set to random 40-80%) | Instant |
|
||||
| **Sensor Blackout** | venue.special_event_chance × 0.4 | sensor-interference | All markets SUSPENDED | 3 ticks |
|
||||
| **Debris Impact** | venue.special_event_chance × 0.6 | debris-field, debris-dense | Random faction takes 8-15% shield damage | Instant |
|
||||
| **Energy Dampening Field** | venue.special_event_chance × 0.2 | energy-dampening | Both attack ×0.6 | 4 ticks |
|
||||
|
||||
#### FACTION-SPECIFIC EVENTS (triggered by faction type)
|
||||
|
||||
| Event | Faction(s) | Trigger | Effect |
|
||||
|-------|-----------|---------|--------|
|
||||
| **Borg Adaptation** | `borg` | After taking 3 consecutive hits | Incoming damage ×0.5 for 5 ticks |
|
||||
| **Cloaking Ambush** | `romulans` | 8% per tick when shields > 60% | Next attack deals 2.5× damage, market suspends 1 tick |
|
||||
| **Honor Duel Challenge** | `klingons` | When losing by 20+ shield points | Attack ×1.5, shields stop regenerating |
|
||||
| **Prophet Intervention** | `bajorans` | 3% per tick | Shields restored to 70%, all markets suspended 2 ticks |
|
||||
| **Energy Dampening Weapon** | `breen` | Once per match, 15% trigger | Opponent shields instantly halved |
|
||||
| **Tholian Web Deployment** | `tholians` | When shields > 50% | Opponent evasion set to 0 for 4 ticks |
|
||||
| **Ketracel Supply Drop** | `dominion` | When shields < 40% | All stats ×1.3 for 6 ticks |
|
||||
| **Ferengi Tactical Retreat** | `ferengi` | When shields < 25% | Evasion ×3 for 3 ticks (hard to finish off) |
|
||||
| **Species 8472 Bio-Pulse** | `species8472` | 10% per tick | Ignores shields, deals 20% direct damage |
|
||||
| **Changeling Sabotage** | `dominion` (away only) | 5% per tick | Home faction attack ×0.7 for 3 ticks |
|
||||
|
||||
#### MARKET EVENTS (triggered by odds state)
|
||||
|
||||
| Event | Trigger | Effect |
|
||||
|-------|---------|--------|
|
||||
| **Subspace Interruption** | Shield swing > 25% in single tick | ALL markets suspended for 2 ticks |
|
||||
| **Odds Avalanche** | One faction's moneyline crosses 5.00 | Exotic markets (comeback, mutual destruction) repriced with tighter odds |
|
||||
| **Dead Heat Protocol** | Both shields within 5% and tick > 60% of max | Duration market prices collapse; match-winner market widens |
|
||||
|
||||
### 5.3 Event Execution Flow
|
||||
|
||||
```go
|
||||
func (engine *Engine) RollForEvents(match *LiveMatch, tickResult TickResult) []MatchEvent {
|
||||
var events []MatchEvent
|
||||
|
||||
// 1. Environmental event roll
|
||||
if rand.Float64() < match.Venue.SpecialEventChance {
|
||||
event := engine.pickEnvironmentalEvent(match.Venue)
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
// 2. Faction-specific event checks
|
||||
for _, side := range []string{"home", "away"} {
|
||||
faction := match.GetFaction(side)
|
||||
for _, template := range engine.factionEvents[faction.Type] {
|
||||
if template.CheckTrigger(match, side, tickResult) {
|
||||
if rand.Float64() < template.Probability {
|
||||
event := template.Instantiate(match, side)
|
||||
events = append(events, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Combat state events
|
||||
if tickResult.HomeDamageDealt > 0 && rand.Float64() < 0.05 {
|
||||
events = append(events, CriticalHitEvent("home"))
|
||||
}
|
||||
|
||||
// 4. Market disruption checks
|
||||
shieldSwing := math.Abs(tickResult.HomeShieldsAfter - tickResult.AwayShieldsAfter)
|
||||
if shieldSwing > 25 {
|
||||
events = append(events, SubspaceInterruptionEvent())
|
||||
}
|
||||
|
||||
// Apply all event effects
|
||||
for _, event := range events {
|
||||
engine.applyEventEffects(match, event)
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Probability Engine — The Core Odds Machine
|
||||
|
||||
This is the heart of the sportsbook. Every tick, after combat resolution and event processing, all market probabilities must be recalculated from the current match state.
|
||||
|
||||
### 6.1 Design Principles
|
||||
|
||||
1. **Probabilities are derived, not stored** — odds are always computed from the current state vector, never carried forward from previous ticks
|
||||
2. **Overround is applied last** — compute fair probability first, then apply house margin
|
||||
3. **Suspension trumps calculation** — if a market event has suspended a market, skip recalculation until the suspension lifts
|
||||
4. **Odds floor/ceiling** — No market selection can go below 1.01 or above 51.00
|
||||
|
||||
### 6.2 Match State Vector
|
||||
|
||||
Every probability calculation takes this state as input:
|
||||
|
||||
```go
|
||||
type MatchState struct {
|
||||
HomeShields float64 // 0-100
|
||||
AwayShields float64 // 0-100
|
||||
HomeFactionType string // aggressive, defensive, etc.
|
||||
AwayFactionType string
|
||||
HomeBaseStats [3]int // shield, attack, evasion
|
||||
AwayBaseStats [3]int
|
||||
KineticYield float64 // Cumulative energy released
|
||||
CurrentTick int // How far into the match
|
||||
MaxTicks int // Match duration limit
|
||||
ActiveEffects []Effect // Lingering buffs/debuffs
|
||||
VenueModifiers Venue // Environmental modifiers
|
||||
EventLog []Event // History (for conditional markets)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 Market-Specific Probability Models
|
||||
|
||||
#### MODEL A: Match Winner (Moneyline)
|
||||
|
||||
The most important market. Uses a **logistic regression model** on shield differential, weighted by time remaining.
|
||||
|
||||
```go
|
||||
func ProbabilityMatchWinner(state MatchState) (homeProb, awayProb float64) {
|
||||
// Shield differential (positive = home advantage)
|
||||
shieldDiff := state.HomeShields - state.AwayShields
|
||||
|
||||
// Time factor: early ticks → differential matters less
|
||||
// Late ticks → differential matters more (less time to recover)
|
||||
tickProgress := float64(state.CurrentTick) / float64(state.MaxTicks)
|
||||
timeFactor := 0.5 + (tickProgress * 1.5) // Ranges 0.5 → 2.0
|
||||
|
||||
// Stat advantage: inherent faction strength difference
|
||||
homePower := effectivePower(state.HomeBaseStats, state.VenueModifiers)
|
||||
awayPower := effectivePower(state.AwayBaseStats, state.VenueModifiers)
|
||||
statDiff := (homePower - awayPower) * 0.1 // Scaled down
|
||||
|
||||
// Combined signal
|
||||
signal := (shieldDiff * timeFactor * 0.05) + statDiff
|
||||
|
||||
// Logistic function → probability
|
||||
homeProb = 1.0 / (1.0 + math.Exp(-signal))
|
||||
|
||||
// Edge case: if one side is at 0 shields
|
||||
if state.HomeShields <= 0 { homeProb = 0.0 }
|
||||
if state.AwayShields <= 0 { homeProb = 1.0 }
|
||||
|
||||
// Clamp to avoid absurd odds
|
||||
homeProb = clamp(homeProb, 0.02, 0.98)
|
||||
awayProb = 1.0 - homeProb
|
||||
|
||||
return homeProb, awayProb
|
||||
}
|
||||
```
|
||||
|
||||
**Behavior examples:**
|
||||
| State | Home P | Away P | Home Odds | Away Odds |
|
||||
|-------|--------|--------|-----------|-----------|
|
||||
| Start (both 100%) | 0.50 | 0.50 | 2.00 | 2.00 |
|
||||
| Home 80%, Away 60%, tick 25% | 0.58 | 0.42 | 1.72 | 2.38 |
|
||||
| Home 45%, Away 70%, tick 50% | 0.30 | 0.70 | 3.33 | 1.43 |
|
||||
| Home 20%, Away 65%, tick 80% | 0.08 | 0.92 | 12.50 | 1.09 |
|
||||
| Home 5%, Away 50%, tick 90% | 0.03 | 0.97 | 33.33 | 1.03 |
|
||||
|
||||
#### MODEL B: Total Kinetic Yield (Over/Under)
|
||||
|
||||
Projects final total kinetic yield from current rate and remaining time.
|
||||
|
||||
```go
|
||||
func ProbabilityKineticYield(state MatchState, line float64) (overProb, underProb float64) {
|
||||
// Current rate of energy release per tick
|
||||
ticksElapsed := max(state.CurrentTick, 1)
|
||||
currentRate := state.KineticYield / float64(ticksElapsed)
|
||||
|
||||
// Project remaining yield
|
||||
remainingTicks := float64(state.MaxTicks - state.CurrentTick)
|
||||
projectedTotal := state.KineticYield + (currentRate * remainingTicks)
|
||||
|
||||
// Variance decreases as match progresses (more certainty)
|
||||
variance := (1.0 - float64(state.CurrentTick)/float64(state.MaxTicks)) * 3.0
|
||||
variance = max(variance, 0.3) // Minimum uncertainty
|
||||
|
||||
// Normal CDF approximation
|
||||
z := (line - projectedTotal) / variance
|
||||
underProb = normalCDF(z)
|
||||
overProb = 1.0 - underProb
|
||||
|
||||
// Clamp
|
||||
overProb = clamp(overProb, 0.02, 0.98)
|
||||
underProb = 1.0 - overProb
|
||||
|
||||
return overProb, underProb
|
||||
}
|
||||
```
|
||||
|
||||
#### MODEL C: First Shield Breach
|
||||
|
||||
Predicts which faction's shields hit zero first based on damage rate trajectories.
|
||||
|
||||
```go
|
||||
func ProbabilityFirstBreach(state MatchState) (homeFirstProb, awayFirstProb float64) {
|
||||
// If one already breached
|
||||
if state.HomeShields <= 0 && state.AwayShields > 0 { return 1.0, 0.0 }
|
||||
if state.AwayShields <= 0 && state.HomeShields > 0 { return 0.0, 1.0 }
|
||||
if state.HomeShields <= 0 && state.AwayShields <= 0 { return 0.5, 0.5 }
|
||||
|
||||
// Estimate ticks to breach for each
|
||||
homeDamageRate := estimateDamageRate("away_to_home", state) // damage per tick to home
|
||||
awayDamageRate := estimateDamageRate("home_to_away", state) // damage per tick to away
|
||||
|
||||
homeTicksToBreach := state.HomeShields / max(homeDamageRate, 0.01)
|
||||
awayTicksToBreach := state.AwayShields / max(awayDamageRate, 0.01)
|
||||
|
||||
// Faction with fewer ticks to breach is more likely to breach first
|
||||
total := homeTicksToBreach + awayTicksToBreach
|
||||
homeFirstProb = awayTicksToBreach / total // Inverted: if away takes longer, home breaches first more likely... wait
|
||||
// Actually: if home has fewer ticks to breach, home breaches first
|
||||
homeFirstProb = 1.0 - (homeTicksToBreach / total)
|
||||
awayFirstProb = 1.0 - homeFirstProb
|
||||
|
||||
return clamp(homeFirstProb, 0.02, 0.98), clamp(awayFirstProb, 0.02, 0.98)
|
||||
}
|
||||
```
|
||||
|
||||
#### MODEL D: Engagement Duration (Over/Under Cycles)
|
||||
|
||||
```go
|
||||
func ProbabilityDuration(state MatchState, line float64) (overProb, underProb float64) {
|
||||
// Estimate remaining ticks based on shield drain rates
|
||||
avgDrainRate := estimateAverageDrainRate(state) // Combined shield loss per tick
|
||||
avgShieldsRemaining := (state.HomeShields + state.AwayShields) / 2.0
|
||||
|
||||
estimatedRemainingTicks := avgShieldsRemaining / max(avgDrainRate, 0.1)
|
||||
estimatedTotalTicks := float64(state.CurrentTick) + estimatedRemainingTicks
|
||||
|
||||
// If already past the line, it's definitely over
|
||||
if float64(state.CurrentTick) >= line {
|
||||
return 1.0, 0.0
|
||||
}
|
||||
|
||||
// Normal CDF on projected total vs line
|
||||
variance := estimatedRemainingTicks * 0.3
|
||||
z := (line - estimatedTotalTicks) / max(variance, 0.1)
|
||||
underProb = normalCDF(z)
|
||||
overProb = 1.0 - underProb
|
||||
|
||||
return clamp(overProb, 0.02, 0.98), clamp(underProb, 0.02, 0.98)
|
||||
}
|
||||
```
|
||||
|
||||
#### MODEL E: Comeback Victory
|
||||
|
||||
```go
|
||||
func ProbabilityComeback(state MatchState, deficitThreshold float64) (homeComeback, awayComeback float64) {
|
||||
// Check if the deficit condition has been met historically
|
||||
homeMaxDeficit := trackMaxDeficit(state.EventLog, "home")
|
||||
awayMaxDeficit := trackMaxDeficit(state.EventLog, "away")
|
||||
|
||||
homeDeficitMet := homeMaxDeficit >= deficitThreshold
|
||||
awayDeficitMet := awayMaxDeficit >= deficitThreshold
|
||||
|
||||
if !homeDeficitMet && !awayDeficitMet {
|
||||
// Need both: P(reach deficit) × P(win from deficit)
|
||||
// This is very rare → long odds
|
||||
homeComeback = 0.05 // ~5% base, adjusted by state
|
||||
awayComeback = 0.05
|
||||
} else {
|
||||
// Deficit was met — now it's "just" a win probability from behind
|
||||
if homeDeficitMet {
|
||||
homeWinProb, _ := ProbabilityMatchWinner(state)
|
||||
homeComeback = homeWinProb * 0.8 // Discount for comeback difficulty
|
||||
}
|
||||
if awayDeficitMet {
|
||||
_, awayWinProb := ProbabilityMatchWinner(state)
|
||||
awayComeback = awayWinProb * 0.8
|
||||
}
|
||||
}
|
||||
|
||||
return clamp(homeComeback, 0.02, 0.50), clamp(awayComeback, 0.02, 0.50)
|
||||
}
|
||||
```
|
||||
|
||||
#### MODEL F: Mutual Destruction
|
||||
|
||||
```go
|
||||
func ProbabilityMutualDestruction(state MatchState) float64 {
|
||||
// Both shields need to reach 0 on the same tick
|
||||
// More likely when shields are close together and both low
|
||||
shieldDiff := math.Abs(state.HomeShields - state.AwayShields)
|
||||
avgShields := (state.HomeShields + state.AwayShields) / 2.0
|
||||
|
||||
// Base probability is very low
|
||||
baseProb := 0.01
|
||||
|
||||
// Increases when shields are similar (small diff)
|
||||
similarityBoost := 1.0 + (20.0 - min(shieldDiff, 20.0)) / 20.0 * 3.0
|
||||
|
||||
// Increases when both shields are low
|
||||
lowShieldBoost := 1.0
|
||||
if avgShields < 30 {
|
||||
lowShieldBoost = 1.0 + (30.0 - avgShields) / 30.0 * 5.0
|
||||
}
|
||||
|
||||
prob := baseProb * similarityBoost * lowShieldBoost
|
||||
return clamp(prob, 0.005, 0.30)
|
||||
}
|
||||
```
|
||||
|
||||
#### MODEL G: Subspace Anomaly
|
||||
|
||||
```go
|
||||
func ProbabilitySubspaceAnomaly(state MatchState) (anomalyProb, cleanProb float64) {
|
||||
// Geometric distribution: P(at least one event in remaining ticks)
|
||||
remainingTicks := state.MaxTicks - state.CurrentTick
|
||||
perTickChance := state.VenueModifiers.SpecialEventChance
|
||||
|
||||
// Has an anomaly already occurred?
|
||||
alreadyOccurred := hasAnomalyOccurred(state.EventLog)
|
||||
if alreadyOccurred {
|
||||
return 1.0, 0.0 // Already happened → "yes" is certain
|
||||
}
|
||||
|
||||
// P(no anomaly in remaining ticks) = (1 - p)^n
|
||||
cleanProb = math.Pow(1.0 - perTickChance, float64(remainingTicks))
|
||||
anomalyProb = 1.0 - cleanProb
|
||||
|
||||
return clamp(anomalyProb, 0.02, 0.98), clamp(cleanProb, 0.02, 0.98)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.4 Probability → Price Conversion
|
||||
|
||||
```go
|
||||
func ProbabilityToPrice(prob float64, margin float64) float64 {
|
||||
// Fair odds
|
||||
fairOdds := 1.0 / prob
|
||||
|
||||
// Apply house margin (overround)
|
||||
// margin = 1.06 means 106% book (6% house edge)
|
||||
adjustedOdds := fairOdds / margin
|
||||
|
||||
// Apply floor/ceiling
|
||||
adjustedOdds = clamp(adjustedOdds, 1.01, 51.00)
|
||||
|
||||
// Round to 2 decimal places
|
||||
return math.Round(adjustedOdds * 100) / 100
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 Overround Strategy
|
||||
|
||||
The house margin varies by market category:
|
||||
|
||||
| Category | Target Overround | Rationale |
|
||||
|----------|-----------------|-----------|
|
||||
| Primary (moneyline, O/U) | 105–106% | High liquidity, competitive odds |
|
||||
| Secondary (handicap, duration) | 107–108% | Moderate liquidity |
|
||||
| Exotic (comeback, mutual dest.) | 110–115% | Low liquidity, high variance, more house edge |
|
||||
|
||||
---
|
||||
|
||||
## 7. Valkey Data Schema
|
||||
|
||||
### 7.1 Key Structure
|
||||
|
||||
```
|
||||
sector:telemetry:{match_id} → Full match JSON state (HSET)
|
||||
sector:events:{match_id} → Capped event log, last 20 events (LIST)
|
||||
sector:index:active → Set of active match IDs (SET)
|
||||
sector:index:quadrant:{q_id} → Active match IDs in quadrant (SET)
|
||||
sector:schedule:upcoming → Upcoming match announcements (SORTED SET by start time)
|
||||
global:stats → Engine statistics (HASH)
|
||||
```
|
||||
|
||||
### 7.2 Match Telemetry Payload (JSON pushed to Valkey)
|
||||
|
||||
This is the exact JSON the TanStack Start frontend receives:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "wolf-359-a7f3",
|
||||
"name": "Wolf 359 — Memorial Perimeter",
|
||||
"quadrant": "alpha",
|
||||
"status": "LIVE",
|
||||
"tick": 42,
|
||||
"maxTicks": 90,
|
||||
"kinetic_yield": 14.7,
|
||||
"venue": {
|
||||
"id": "wolf-359",
|
||||
"name": "Wolf 359 — Memorial Perimeter",
|
||||
"tags": ["memorial", "debris-field", "high-anomaly"]
|
||||
},
|
||||
"factions": {
|
||||
"home": {
|
||||
"id": "starfleet",
|
||||
"name": "Starfleet Command",
|
||||
"abbreviation": "SFC",
|
||||
"type": "defensive",
|
||||
"glyph": "◆",
|
||||
"colorCode": "#34d399",
|
||||
"shieldCapacity": 62,
|
||||
"effectiveAttack": 58,
|
||||
"effectiveEvasion": 40,
|
||||
"activeEffects": []
|
||||
},
|
||||
"away": {
|
||||
"id": "borg",
|
||||
"name": "The Borg Collective",
|
||||
"abbreviation": "BRG",
|
||||
"type": "swarm",
|
||||
"glyph": "█",
|
||||
"colorCode": "#38bdf8",
|
||||
"shieldCapacity": 71,
|
||||
"effectiveAttack": 99,
|
||||
"effectiveEvasion": 12,
|
||||
"activeEffects": [
|
||||
{ "name": "Borg Adaptation", "remainingTicks": 3, "effect": "damage_taken_x0.5" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"markets": [
|
||||
{
|
||||
"id": "wolf-359-a7f3-moneyline-home",
|
||||
"templateId": "match-winner",
|
||||
"name": "Starfleet Command Decisive Victory",
|
||||
"category": "primary",
|
||||
"price": 2.85,
|
||||
"previousPrice": 2.70,
|
||||
"trend": "up",
|
||||
"status": "OPEN",
|
||||
"impliedProbability": 0.351
|
||||
},
|
||||
{
|
||||
"id": "wolf-359-a7f3-moneyline-away",
|
||||
"templateId": "match-winner",
|
||||
"name": "The Borg Collective Decisive Victory",
|
||||
"category": "primary",
|
||||
"price": 1.48,
|
||||
"previousPrice": 1.52,
|
||||
"trend": "down",
|
||||
"status": "OPEN",
|
||||
"impliedProbability": 0.676
|
||||
},
|
||||
{
|
||||
"id": "wolf-359-a7f3-ky-over",
|
||||
"templateId": "kinetic-over-under",
|
||||
"name": "Over 8.5 Terawatts",
|
||||
"category": "primary",
|
||||
"price": 1.35,
|
||||
"previousPrice": 1.40,
|
||||
"trend": "down",
|
||||
"status": "OPEN",
|
||||
"impliedProbability": 0.741
|
||||
}
|
||||
],
|
||||
"recentEvents": [
|
||||
{
|
||||
"tick": 38,
|
||||
"name": "Borg Adaptation",
|
||||
"description": "The Collective adapts to Starfleet phaser frequencies. Incoming damage reduced by 50%.",
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"tick": 31,
|
||||
"name": "Debris Impact",
|
||||
"description": "Wolf 359 wreckage collides with Borg cube. Shields -12%.",
|
||||
"severity": "minor"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Event Log Entry (LPUSH to sector:events:{match_id})
|
||||
|
||||
```json
|
||||
{
|
||||
"tick": 38,
|
||||
"timestamp": "2024-01-15T10:30:42Z",
|
||||
"name": "Borg Adaptation",
|
||||
"category": "faction",
|
||||
"severity": "major",
|
||||
"description": "The Collective adapts to Starfleet phaser frequencies. Incoming damage reduced by 50%.",
|
||||
"effects": [
|
||||
{ "target": "away", "stat": "damage_taken", "operation": "multiply", "value": 0.5, "duration": 5 }
|
||||
],
|
||||
"marketImpact": {
|
||||
"suspendedMarkets": [],
|
||||
"significantPriceChanges": ["wolf-359-a7f3-moneyline-home", "wolf-359-a7f3-moneyline-away"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Go Engine File Structure
|
||||
|
||||
```
|
||||
ingestion-engine/
|
||||
├── main.go # Entry point: init, taxonomy load, main loop
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
├── config/
|
||||
│ ├── taxonomy.go # YAML parsing & taxonomy types
|
||||
│ └── constants.go # Tuning knobs (tick rate, max matches, etc.)
|
||||
├── engine/
|
||||
│ ├── scheduler.go # Match lifecycle state machine
|
||||
│ ├── matchmaker.go # Faction pairing & venue selection
|
||||
│ ├── simulator.go # Combat tick resolution
|
||||
│ ├── events.go # Random event generation & application
|
||||
│ ├── probability.go # All probability models (A through G)
|
||||
│ ├── markets.go # Market hydration, pricing, suspension
|
||||
│ └── type_advantage.go # Combat type interaction matrix
|
||||
├── models/
|
||||
│ ├── match.go # LiveMatch, MatchState structs
|
||||
│ ├── faction.go # FactionState, ActiveEffect structs
|
||||
│ ├── market.go # Market, MarketSelection structs
|
||||
│ └── event.go # MatchEvent, EventEffect structs
|
||||
└── cache/
|
||||
├── valkey.go # Valkey client wrapper
|
||||
└── serializer.go # JSON payload assembly
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Main Loop Pseudocode
|
||||
|
||||
```go
|
||||
func main() {
|
||||
// 1. Load taxonomy
|
||||
taxonomy := config.LoadTaxonomy("../data-source/taxonomy.yaml")
|
||||
|
||||
// 2. Connect to Valkey
|
||||
valkeyClient := cache.NewValkeyClient("localhost:6379")
|
||||
|
||||
// 3. Initialize engine
|
||||
engine := engine.NewEngine(taxonomy, valkeyClient)
|
||||
|
||||
// 4. Seed initial matches
|
||||
engine.Scheduler.SeedMatches(TargetActiveMatches)
|
||||
|
||||
// 5. Main tick loop (every 2 seconds)
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
// A. Scheduler: check for matches to create/retire
|
||||
engine.Scheduler.Tick()
|
||||
|
||||
// B. For each active match, run one combat tick
|
||||
for _, match := range engine.Scheduler.ActiveMatches() {
|
||||
switch match.Status {
|
||||
case "PRE_MATCH":
|
||||
// Just update countdown timer, no combat
|
||||
engine.UpdatePreMatchCountdown(match)
|
||||
|
||||
case "LIVE":
|
||||
// Run full tick cycle
|
||||
tickResult := engine.Simulator.ResolveTick(match)
|
||||
events := engine.EventGenerator.Roll(match, tickResult)
|
||||
engine.Probability.RecalculateAll(match)
|
||||
engine.Markets.UpdatePrices(match)
|
||||
|
||||
case "RESOLVING":
|
||||
// Cooldown ticks — no combat, markets suspended
|
||||
engine.Simulator.ResolveCountdown(match)
|
||||
|
||||
case "SETTLED":
|
||||
// Grade markets, archive, remove from active
|
||||
engine.Scheduler.SettleMatch(match)
|
||||
continue
|
||||
}
|
||||
|
||||
// Push state to Valkey
|
||||
engine.Cache.PushMatchState(match)
|
||||
engine.Cache.PushEventLog(match)
|
||||
}
|
||||
|
||||
// C. Update active index
|
||||
engine.Cache.UpdateActiveIndex(engine.Scheduler.ActiveMatches())
|
||||
|
||||
log.Println(">> Tick complete. Active matches:", len(engine.Scheduler.ActiveMatches()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Tuning Knobs & Configuration
|
||||
|
||||
All magic numbers should be extractable constants:
|
||||
|
||||
```go
|
||||
// config/constants.go
|
||||
|
||||
// Tick timing
|
||||
const TickInterval = 2 * time.Second
|
||||
|
||||
// Match lifecycle
|
||||
const TargetActiveMatches = 8
|
||||
const MaxActiveMatches = 12
|
||||
const MinActiveMatches = 4
|
||||
const PreMatchDuration = 30 // ticks (60s)
|
||||
const MinMatchDuration = 60 // ticks (2 min)
|
||||
const MaxMatchDuration = 150 // ticks (5 min)
|
||||
const ResolvingDuration = 3 // ticks (6s)
|
||||
|
||||
// Combat
|
||||
const DamageScalingFactor = 0.1 // Converts raw attack to shield %
|
||||
const DamageVarianceLow = 0.6 // Min damage multiplier
|
||||
const DamageVarianceHigh = 1.4 // Max damage multiplier
|
||||
const CriticalHitChance = 0.05 // 5% per hit
|
||||
const CriticalHitMultiplier = 3.0
|
||||
|
||||
// Probability
|
||||
const LogisticScalingFactor = 20.0 // Controls sigmoid steepness
|
||||
const MinProbability = 0.02 // Floor (max odds ~50.00)
|
||||
const MaxProbability = 0.98 // Ceiling (min odds ~1.02)
|
||||
const MinPrice = 1.01
|
||||
const MaxPrice = 51.00
|
||||
|
||||
// Margins
|
||||
const PrimaryMarketMargin = 1.06 // 106% overround
|
||||
const SecondaryMarketMargin = 1.075
|
||||
const ExoticMarketMargin = 1.12
|
||||
|
||||
// Matchmaking
|
||||
const SameQuadrantWeight = 0.60
|
||||
const CrossQuadrantWeight = 0.30
|
||||
const DistantQuadrantWeight = 0.10
|
||||
const MaxSimultaneousAppearances = 2
|
||||
const RepeatMatchupCooldown = 3
|
||||
const RivalrySelectionBoost = 2.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Edge Cases & Failure Modes
|
||||
|
||||
| Scenario | Handling |
|
||||
|----------|----------|
|
||||
| **Both shields hit 0 on same tick** | Mutual Destruction — market graded as "yes", match winner is a draw (both moneylines voided/refunded) |
|
||||
| **Match hits max tick limit** | Higher shields wins. If tied, compare cumulative damage dealt. If still tied, draw. |
|
||||
| **Valkey connection lost** | Engine continues simulating in memory, buffers state, reconnects with exponential backoff. On reconnect, flush all buffered states. |
|
||||
| **All factions in cooldown** | Reduce cooldown timers by 1, create cross-quadrant match with least-recently-used factions |
|
||||
| **Event chain causes instant kill** | Shield floor of 1% for first 10 ticks (grace period). After that, instant kills are valid. |
|
||||
| **Price oscillation (rapid up/down)** | Apply 3-tick moving average smoothing to price output. Raw probability still updates instantly. |
|
||||
| **Probability model disagrees with state** | State always wins. If shields are 0, P(win) = 0 regardless of model output. Hard overrides > model. |
|
||||
|
||||
---
|
||||
|
||||
## 12. Future Extensibility
|
||||
|
||||
| Feature | Approach |
|
||||
|---------|----------|
|
||||
| **Accumulator / Parlay bets** | Multiply implied probabilities across selections from different matches. Apply accumulator margin premium. |
|
||||
| **Match history / form** | Track last N results per faction in Valkey sorted set. Feed into pre-match odds as a "form" modifier. |
|
||||
| **Tournament mode** | Bracket of 8/16 factions. Winners advance. Outright winner market across the tournament. |
|
||||
| **Player props (crew members)** | Add crew member sub-entities to factions with individual stats. Create crew-level markets. |
|
||||
| **Live commentary feed** | Event log already structured. Add a natural language template engine to generate commentary strings. |
|
||||
| **Seasonal rankings** | Track faction W/L/D records. Create league table. End-of-season markets (champion, relegation). |
|
||||
765
reference/FRONTEND.md
Normal file
765
reference/FRONTEND.md
Normal 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¤cy=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
584
reference/PROJECT.md
Normal 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 := §or.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">> 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">> 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 := §or.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>> 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
36
tongo-core/Makefile
Normal 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
47
tongo-core/README.md
Normal 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
BIN
tongo-core/bin/tongo
Executable file
Binary file not shown.
24
tongo-core/cmd/tongo/cli/root.go
Normal file
24
tongo-core/cmd/tongo/cli/root.go
Normal 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)
|
||||
}
|
||||
}
|
||||
28
tongo-core/cmd/tongo/cli/serve.go
Normal file
28
tongo-core/cmd/tongo/cli/serve.go
Normal 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
|
||||
}
|
||||
25
tongo-core/cmd/tongo/cli/validate.go
Normal file
25
tongo-core/cmd/tongo/cli/validate.go
Normal 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
|
||||
}
|
||||
22
tongo-core/cmd/tongo/cli/version.go
Normal file
22
tongo-core/cmd/tongo/cli/version.go
Normal 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)
|
||||
}
|
||||
7
tongo-core/cmd/tongo/main.go
Normal file
7
tongo-core/cmd/tongo/main.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "github.com/costap/quarkshologridledger/tongo-core/cmd/tongo/cli"
|
||||
|
||||
func main() {
|
||||
cli.Execute()
|
||||
}
|
||||
41
tongo-core/config/tongo.yaml
Normal file
41
tongo-core/config/tongo.yaml
Normal 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
29
tongo-core/go.mod
Normal 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
68
tongo-core/go.sum
Normal 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=
|
||||
65
tongo-core/internal/application/engine.go
Normal file
65
tongo-core/internal/application/engine.go
Normal 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
|
||||
}
|
||||
|
||||
25
tongo-core/internal/application/events/generator.go
Normal file
25
tongo-core/internal/application/events/generator.go
Normal 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
|
||||
}
|
||||
34
tongo-core/internal/application/markets/markets.go
Normal file
34
tongo-core/internal/application/markets/markets.go
Normal 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)
|
||||
}
|
||||
41
tongo-core/internal/application/matchmaker/matchmaker.go
Normal file
41
tongo-core/internal/application/matchmaker/matchmaker.go
Normal 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
|
||||
}
|
||||
59
tongo-core/internal/application/probability/probability.go
Normal file
59
tongo-core/internal/application/probability/probability.go
Normal 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
|
||||
}
|
||||
42
tongo-core/internal/application/scheduler/scheduler.go
Normal file
42
tongo-core/internal/application/scheduler/scheduler.go
Normal 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
|
||||
}
|
||||
27
tongo-core/internal/application/simulator/simulator.go
Normal file
27
tongo-core/internal/application/simulator/simulator.go
Normal 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{}
|
||||
}
|
||||
22
tongo-core/internal/application/simulator/typeadvantage.go
Normal file
22
tongo-core/internal/application/simulator/typeadvantage.go
Normal 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
|
||||
}
|
||||
41
tongo-core/internal/domain/entity/event.go
Normal file
41
tongo-core/internal/domain/entity/event.go
Normal 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"`
|
||||
}
|
||||
50
tongo-core/internal/domain/entity/faction.go
Normal file
50
tongo-core/internal/domain/entity/faction.go
Normal 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"`
|
||||
}
|
||||
49
tongo-core/internal/domain/entity/market.go
Normal file
49
tongo-core/internal/domain/entity/market.go
Normal 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"`
|
||||
}
|
||||
43
tongo-core/internal/domain/entity/match.go
Normal file
43
tongo-core/internal/domain/entity/match.go
Normal 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
|
||||
}
|
||||
11
tongo-core/internal/domain/entity/quadrant.go
Normal file
11
tongo-core/internal/domain/entity/quadrant.go
Normal 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"`
|
||||
}
|
||||
15
tongo-core/internal/domain/entity/venue.go
Normal file
15
tongo-core/internal/domain/entity/venue.go
Normal 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"`
|
||||
}
|
||||
28
tongo-core/internal/domain/port/cache.go
Normal file
28
tongo-core/internal/domain/port/cache.go
Normal 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
|
||||
}
|
||||
55
tongo-core/internal/domain/port/engine.go
Normal file
55
tongo-core/internal/domain/port/engine.go
Normal 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)
|
||||
}
|
||||
15
tongo-core/internal/domain/port/taxonomy.go
Normal file
15
tongo-core/internal/domain/port/taxonomy.go
Normal 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)
|
||||
}
|
||||
66
tongo-core/internal/infrastructure/cache/valkey.go
vendored
Normal file
66
tongo-core/internal/infrastructure/cache/valkey.go
vendored
Normal 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()
|
||||
}
|
||||
117
tongo-core/internal/infrastructure/config/config.go
Normal file
117
tongo-core/internal/infrastructure/config/config.go
Normal 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
|
||||
}
|
||||
101
tongo-core/internal/infrastructure/taxonomy/loader.go
Normal file
101
tongo-core/internal/infrastructure/taxonomy/loader.go
Normal 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
|
||||
}
|
||||
3
web-terminal/.prettierignore
Normal file
3
web-terminal/.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
20
web-terminal/eslint.config.js
Normal file
20
web-terminal/eslint.config.js
Normal 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
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
56
web-terminal/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
10
web-terminal/prettier.config.js
Normal file
10
web-terminal/prettier.config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// @ts-check
|
||||
|
||||
/** @type {import('prettier').Config} */
|
||||
const config = {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
}
|
||||
|
||||
export default config
|
||||
BIN
web-terminal/public/favicon.ico
Normal file
BIN
web-terminal/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
web-terminal/public/logo192.png
Normal file
BIN
web-terminal/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
web-terminal/public/logo512.png
Normal file
BIN
web-terminal/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
web-terminal/public/manifest.json
Normal file
25
web-terminal/public/manifest.json
Normal 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"
|
||||
}
|
||||
3
web-terminal/public/robots.txt
Normal file
3
web-terminal/public/robots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
38
web-terminal/src/components/ascii/AsciiPanel.tsx
Normal file
38
web-terminal/src/components/ascii/AsciiPanel.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
5
web-terminal/src/components/ascii/BlinkingCursor.tsx
Normal file
5
web-terminal/src/components/ascii/BlinkingCursor.tsx
Normal 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" />
|
||||
)
|
||||
}
|
||||
26
web-terminal/src/components/ascii/GlyphIcon.tsx
Normal file
26
web-terminal/src/components/ascii/GlyphIcon.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
25
web-terminal/src/components/ascii/PriceTag.tsx
Normal file
25
web-terminal/src/components/ascii/PriceTag.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
24
web-terminal/src/components/ascii/ShieldBar.tsx
Normal file
24
web-terminal/src/components/ascii/ShieldBar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
28
web-terminal/src/components/ascii/StatusBadge.tsx
Normal file
28
web-terminal/src/components/ascii/StatusBadge.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
31
web-terminal/src/components/ascii/TerminalLine.tsx
Normal file
31
web-terminal/src/components/ascii/TerminalLine.tsx
Normal 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`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
28
web-terminal/src/components/ascii/TickProgress.tsx
Normal file
28
web-terminal/src/components/ascii/TickProgress.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
web-terminal/src/components/panels/AccountSummary.tsx
Normal file
7
web-terminal/src/components/panels/AccountSummary.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
103
web-terminal/src/components/panels/BetslipSidecar.tsx
Normal file
103
web-terminal/src/components/panels/BetslipSidecar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
78
web-terminal/src/components/panels/EngagementLog.tsx
Normal file
78
web-terminal/src/components/panels/EngagementLog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
web-terminal/src/components/panels/FactionProfile.tsx
Normal file
7
web-terminal/src/components/panels/FactionProfile.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
web-terminal/src/components/panels/FactionTable.tsx
Normal file
7
web-terminal/src/components/panels/FactionTable.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
64
web-terminal/src/components/panels/LiveTicker.tsx
Normal file
64
web-terminal/src/components/panels/LiveTicker.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
103
web-terminal/src/components/panels/MarketGrid.tsx
Normal file
103
web-terminal/src/components/panels/MarketGrid.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
58
web-terminal/src/components/panels/MarketSelection.tsx
Normal file
58
web-terminal/src/components/panels/MarketSelection.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
176
web-terminal/src/components/panels/MatchCard.tsx
Normal file
176
web-terminal/src/components/panels/MatchCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
392
web-terminal/src/components/panels/NavigationMenu.tsx
Normal file
392
web-terminal/src/components/panels/NavigationMenu.tsx
Normal 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">
|
||||
> 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">
|
||||
> 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">
|
||||
> 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>
|
||||
)
|
||||
}
|
||||
34
web-terminal/src/components/panels/QuadrantFilterBar.tsx
Normal file
34
web-terminal/src/components/panels/QuadrantFilterBar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
web-terminal/src/components/panels/ScheduleCard.tsx
Normal file
7
web-terminal/src/components/panels/ScheduleCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
web-terminal/src/components/panels/VenueTable.tsx
Normal file
7
web-terminal/src/components/panels/VenueTable.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
187
web-terminal/src/components/panels/VersusArena.tsx
Normal file
187
web-terminal/src/components/panels/VersusArena.tsx
Normal 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">
|
||||
> 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>
|
||||
)
|
||||
}
|
||||
7
web-terminal/src/components/panels/WagerCard.tsx
Normal file
7
web-terminal/src/components/panels/WagerCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
87
web-terminal/src/components/providers/BetslipProvider.tsx
Normal file
87
web-terminal/src/components/providers/BetslipProvider.tsx
Normal 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
|
||||
}
|
||||
31
web-terminal/src/components/providers/CurrencyProvider.tsx
Normal file
31
web-terminal/src/components/providers/CurrencyProvider.tsx
Normal 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)
|
||||
}
|
||||
198
web-terminal/src/routeTree.gen.ts
Normal file
198
web-terminal/src/routeTree.gen.ts
Normal 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>>
|
||||
}
|
||||
}
|
||||
19
web-terminal/src/router.tsx
Normal file
19
web-terminal/src/router.tsx
Normal 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>
|
||||
}
|
||||
}
|
||||
130
web-terminal/src/routes/__root.tsx
Normal file
130
web-terminal/src/routes/__root.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
282
web-terminal/src/routes/index.tsx
Normal file
282
web-terminal/src/routes/index.tsx
Normal 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">
|
||||
> 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">
|
||||
> LIVE GLOBAL TELEMETRY FEED TICKER
|
||||
</div>
|
||||
<LiveTicker />
|
||||
</div>
|
||||
</div>
|
||||
</AsciiPanel>
|
||||
)
|
||||
}
|
||||
5
web-terminal/src/routes/ledger/index.tsx
Normal file
5
web-terminal/src/routes/ledger/index.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/ledger/')({
|
||||
component: () => <div>Wager Ledger Archives</div>,
|
||||
})
|
||||
403
web-terminal/src/routes/quadrants/$sectorId.tsx
Normal file
403
web-terminal/src/routes/quadrants/$sectorId.tsx
Normal 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>> 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">
|
||||
> 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">
|
||||
> TACTICAL ENGAGEMENT LOG
|
||||
</div>
|
||||
<EngagementLog logs={match.logs} />
|
||||
</div>
|
||||
</div>
|
||||
</AsciiPanel>
|
||||
)
|
||||
}
|
||||
9
web-terminal/src/routes/quadrants/index.tsx
Normal file
9
web-terminal/src/routes/quadrants/index.tsx
Normal 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>
|
||||
}
|
||||
5
web-terminal/src/routes/registry/factions.tsx
Normal file
5
web-terminal/src/routes/registry/factions.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/registry/factions')({
|
||||
component: () => <div>Faction Registry Database</div>,
|
||||
})
|
||||
5
web-terminal/src/routes/registry/venues.tsx
Normal file
5
web-terminal/src/routes/registry/venues.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/registry/venues')({
|
||||
component: () => <div>Venue Sector Database</div>,
|
||||
})
|
||||
5
web-terminal/src/routes/schedule.tsx
Normal file
5
web-terminal/src/routes/schedule.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/schedule')({
|
||||
component: () => <div>Transmission Schedule</div>,
|
||||
})
|
||||
15
web-terminal/src/styles.css
Normal file
15
web-terminal/src/styles.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
@import 'tailwindcss';
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
35
web-terminal/tsconfig.json
Normal file
35
web-terminal/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
3
web-terminal/tsr.config.json
Normal file
3
web-terminal/tsr.config.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"target": "react"
|
||||
}
|
||||
21
web-terminal/vite.config.ts
Normal file
21
web-terminal/vite.config.ts
Normal 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
|
||||
Loading…
Reference in a new issue