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