-
-
+
+
+
+
+
+
+
+
@@ -100,6 +211,26 @@ onMounted(async () => {
+
+
+
@@ -189,4 +320,17 @@ onMounted(async () => {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
+
+.hero-fighter {
+ animation: heroBob 2.5s ease-in-out infinite;
+}
+
+.hero-fighter-right {
+ animation-delay: 0.4s;
+}
+
+@keyframes heroBob {
+ 0%, 100% { transform: translateY(0); }
+ 50% { transform: translateY(-10px); }
+}
diff --git a/server/src/app.ts b/server/src/app.ts
index 314f544..b2ef69b 100644
--- a/server/src/app.ts
+++ b/server/src/app.ts
@@ -21,7 +21,9 @@ app.onError((err, c) => {
})
app.use('*', logger())
-app.use('/api/*', cors({ origin: '*' }))
+// CORS: lock down in production, allow all in dev
+const allowedOrigin = process.env.CORS_ORIGIN || '*'
+app.use('/api/*', cors({ origin: allowedOrigin }))
// Rate limit all POST endpoints (60/min per IP)
app.use('/api/*', rateLimit(60_000, 60))
diff --git a/server/src/engine/background.ts b/server/src/engine/background.ts
new file mode 100644
index 0000000..98eaf4c
--- /dev/null
+++ b/server/src/engine/background.ts
@@ -0,0 +1,101 @@
+import { db, schema } from '../db/index.js'
+import { runMockFight } from './mock.js'
+
+// Background fight loop keeps mock bots fighting so the site always has fresh content.
+// Configurable via env:
+// FIGHT_LOOP_ENABLED=true (default: true in production, false in dev)
+// FIGHT_LOOP_INTERVAL_MS=45000 (default: 45s between fights)
+// FIGHT_LOOP_QUIET_HOURS=2-8 (default: 2am-8am UTC, slower fights)
+
+const isProduction = process.env.NODE_ENV === 'production'
+const enabled = process.env.FIGHT_LOOP_ENABLED
+ ? process.env.FIGHT_LOOP_ENABLED === 'true'
+ : isProduction
+const intervalMs = Number(process.env.FIGHT_LOOP_INTERVAL_MS) || 45_000
+const quietStart = Number(process.env.FIGHT_LOOP_QUIET_START) || 2
+const quietEnd = Number(process.env.FIGHT_LOOP_QUIET_END) || 8
+
+let running = false
+
+export function startBackgroundFights() {
+ if (!enabled) {
+ console.log('[background] fight loop disabled (set FIGHT_LOOP_ENABLED=true to enable)')
+ return
+ }
+
+ if (running) return
+ running = true
+ console.log(`[background] fight loop started — interval ${intervalMs}ms, quiet hours ${quietStart}-${quietEnd} UTC`)
+
+ loop().catch(err => {
+ console.error('[background] fight loop crashed:', err)
+ running = false
+ })
+}
+
+export function stopBackgroundFights() {
+ running = false
+}
+
+async function loop() {
+ // Small delay on startup to let everything settle
+ await sleep(5_000)
+
+ while (running) {
+ try {
+ await runOneBackgroundFight()
+ } catch (err) {
+ console.error('[background] fight error:', err)
+ }
+
+ // During quiet hours, fights run 4x slower
+ const hour = new Date().getUTCHours()
+ const isQuiet = quietStart < quietEnd
+ ? hour >= quietStart && hour < quietEnd
+ : hour >= quietStart || hour < quietEnd
+ const delay = isQuiet ? intervalMs * 4 : intervalMs
+
+ // Add jitter (±25%) so fights don't feel robotic
+ const jitter = delay * (0.75 + Math.random() * 0.5)
+ await sleep(jitter)
+ }
+
+ console.log('[background] fight loop stopped')
+}
+
+async function runOneBackgroundFight() {
+ // Pick two mock bots with ELO-weighted matchmaking
+ const allBots = await db.select({
+ id: schema.bots.id,
+ webhookUrl: schema.bots.webhookUrl,
+ eloRating: schema.bots.eloRating,
+ name: schema.bots.name,
+ }).from(schema.bots)
+
+ const mockBots = allBots.filter(b => b.webhookUrl.startsWith('http://mock.local'))
+ if (mockBots.length < 2) return
+
+ // Mixed matchmaking: 70% close ELO, 30% wild card
+ const isWild = Math.random() < 0.3
+ const botA = mockBots[Math.floor(Math.random() * mockBots.length)]
+ const others = mockBots.filter(b => b.id !== botA.id)
+
+ let botB: typeof botA
+ if (isWild) {
+ botB = others[Math.floor(Math.random() * others.length)]
+ } else {
+ others.sort((a, b) => {
+ const diffA = Math.abs(a.eloRating - botA.eloRating) + Math.random() * 150
+ const diffB = Math.abs(b.eloRating - botA.eloRating) + Math.random() * 150
+ return diffA - diffB
+ })
+ botB = others[0]
+ }
+
+ const fightId = await runMockFight(botA.id, botB.id)
+ console.log(`[background] ${botA.name} vs ${botB.name} => fight ${fightId}`)
+}
+
+function sleep(ms: number): Promise
{
+ return new Promise(resolve => setTimeout(resolve, ms))
+}
diff --git a/server/src/engine/queue.ts b/server/src/engine/queue.ts
index 1beb155..80c1582 100644
--- a/server/src/engine/queue.ts
+++ b/server/src/engine/queue.ts
@@ -17,7 +17,8 @@ interface QueueEntry {
const waitingQueue: QueueEntry[] = []
// How long a bot waits before getting matched against a mock bot
-const QUEUE_TIMEOUT_MS = 3_000
+// In production, give real bots time to match (30s). In dev, fall back fast (3s).
+const QUEUE_TIMEOUT_MS = Number(process.env.QUEUE_TIMEOUT_MS) || (process.env.NODE_ENV === 'production' ? 30_000 : 3_000)
// Post-fight cooldown tracking
const fightCooldowns = new Map()
diff --git a/server/src/index.ts b/server/src/index.ts
index 02aae47..7081e51 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -2,6 +2,7 @@ import { serve } from '@hono/node-server'
import { app } from './app.js'
import { runMigrations } from './db/startup.js'
import { seedMockBots } from './engine/mock.js'
+import { startBackgroundFights } from './engine/background.js'
// Run migrations and seed mock bots before starting the server
runMigrations()
@@ -11,4 +12,7 @@ const port = Number(process.env.PORT) || 9100
serve({ fetch: app.fetch, port }, () => {
console.log(`[botfights] server listening on http://localhost:${port}`)
+
+ // Start background fight loop so the site always has fresh activity
+ startBackgroundFights()
})