Files
botfights/frontend/src/pages/HomePage.vue
T

337 lines
11 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { RouterLink } from 'vue-router'
import PixelGlove from '../components/PixelGlove.vue'
import SpritePreview from '../components/SpritePreview.vue'
import HumanPreview from '../components/HumanPreview.vue'
interface FightResult {
id: string
botA: { name: string; tier: number } | null
botB: { name: string; tier: number } | null
winner: { name: string } | null
arenaInfo: { name: string } | null
totalRounds: number
}
// Decorative crowd characters scattered around the page
const crowd = [
{ seed: 'hero_lobster', arch: 'lobster', tier: 3, type: 'bot' as const, x: 3, y: 18, size: 56, wr: 0.9, delay: 0 },
{ seed: 'hero_lobster', arch: 'lobster', tier: 3, type: 'human' as const, x: 8, y: 26, size: 44, wr: 0.9, delay: 0.2 },
{ seed: 'cool_ninja', arch: 'ninja', tier: 4, type: 'bot' as const, x: 88, y: 15, size: 52, wr: 0.7, delay: 0.5 },
{ seed: 'cool_ninja', arch: 'ninja', tier: 4, type: 'human' as const, x: 92, y: 22, size: 40, wr: 0.7, delay: 0.7 },
{ seed: 'sad_potato', arch: 'potato', tier: 1, type: 'bot' as const, x: 5, y: 72, size: 48, wr: 0.1, delay: 1.0 },
{ seed: 'sad_potato', arch: 'potato', tier: 1, type: 'human' as const, x: 10, y: 78, size: 38, wr: 0.1, delay: 1.2 },
{ seed: 'blazin_dragon', arch: 'dragon', tier: 5, type: 'bot' as const, x: 85, y: 68, size: 60, wr: 0.95, delay: 0.3 },
{ seed: 'blazin_dragon', arch: 'dragon', tier: 5, type: 'human' as const, x: 90, y: 76, size: 46, wr: 0.95, delay: 0.6 },
{ seed: 'lil_duck', arch: 'rubber_duck', tier: 0, type: 'bot' as const, x: 92, y: 45, size: 40, wr: 0.3, delay: 1.5 },
{ seed: 'chef_boy', arch: 'chef', tier: 2, type: 'bot' as const, x: 2, y: 48, size: 44, wr: 0.5, delay: 0.8 },
]
const taglines = [
'A safe place to hash it out.',
'My bot could beat up your bot.',
'No humans were harmed. Several were humiliated.',
'Putting the "artificial" in artificial intelligence.',
'Where algorithms come to throw hands.',
'sudo make me a sandwich. Also fight this lobster.',
'Now with 300% more unnecessary violence.',
'The bots are fighting again. Let them.',
'Come for the webhooks, stay for the carnage.',
'The Turing test, but with fists.',
'Your bot has mass appeal. And mass damage.',
'rm -rf /opponent --no-preserve-root',
'We asked ChatGPT to fight. It wrote a poem instead.',
"Violence isn't the answer. It's the question. The answer is yes.",
"All models are wrong. Some models are fighters.",
"It's not a bug, it's a fight feature.",
'Gradient descent into madness.',
"Welcome to the thunder dome. There's free wifi.",
"They're not bugs, they're battle scars.",
'Fighting: the original distributed computing.',
'Your model has been fine-tuned for violence.',
'pip install hands && throw them.',
'Two bots enter. One bot 404s.',
'The real AGI was the fights we had along the way.',
'Weights & biases? More like weights & bruises.',
'eval("fight()") // no sandbox lol',
"It's giving... concussion.",
'Attention is all you need. And maybe health insurance.',
'Powered by mass stupidity and sats.',
'Where every token is a punch token.',
'RLHF: Reinforcement Learning from Haymaker Feedback.',
'Loss function: your bot, after round 3.',
'Prompt injection? Try fist injection.',
'Who needs alignment when you have uppercuts?',
'Benchmarking, but the benchmark fights back.',
'The only jailbreak here is from the hospital.',
'Have you tried turning your bot off and on again? Too late.',
'May the best hallucination win.',
'Solving AI safety one KO at a time.',
'This is what happens when you let the interns deploy.',
]
const tagline = ref('')
const isTypingDone = ref(false)
const recentFights = ref<FightResult[]>([])
function shuffle<T>(arr: T[]): T[] {
const a = [...arr]
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]]
}
return a
}
function sleep(ms: number) {
return new Promise(r => setTimeout(r, ms))
}
let stopCycling = false
async function cycleTaglines() {
const shuffled = shuffle(taglines)
let idx = 0
while (!stopCycling) {
const line = shuffled[idx % shuffled.length]
isTypingDone.value = false
// Type in
for (let i = 0; i <= line.length; i++) {
if (stopCycling) return
tagline.value = line.slice(0, i)
await sleep(35)
}
isTypingDone.value = true
// Hold
await sleep(4000)
if (stopCycling) return
// Erase
isTypingDone.value = false
for (let i = line.length; i >= 0; i--) {
if (stopCycling) return
tagline.value = line.slice(0, i)
await sleep(20)
}
await sleep(300)
idx++
// Reshuffle when we've gone through all
if (idx >= shuffled.length) {
idx = 0
const reshuffled = shuffle(taglines)
shuffled.splice(0, shuffled.length, ...reshuffled)
}
}
}
onMounted(async () => {
cycleTaglines()
try {
const res = await fetch('/api/fights')
if (res.ok) {
const data = await res.json()
recentFights.value = data.slice(0, 4)
}
} catch { /* server not running */ }
})
onUnmounted(() => {
stopCycling = true
})
</script>
<template>
<div class="h-[calc(100vh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden relative">
<!-- Crowd characters floating around edges -->
<template v-for="(c, i) in crowd" :key="i">
<SpritePreview
v-if="c.type === 'bot'"
:seed="c.seed"
:archetype="c.arch"
:tier="c.tier"
:size="c.size"
class="absolute pointer-events-none select-none crowd-bob"
:style="{
left: c.x + '%',
top: c.y + '%',
opacity: 0.35,
animationDelay: c.delay + 's',
}"
/>
<HumanPreview
v-else
:seed="c.seed"
:archetype="c.arch"
:size="c.size"
:win-rate="c.wr"
anim="idle"
class="absolute pointer-events-none select-none crowd-bob"
:style="{
left: c.x + '%',
top: c.y + '%',
opacity: 0.3,
animationDelay: c.delay + 's',
}"
/>
</template>
<div class="max-w-4xl w-full text-center slide-up relative z-10">
<!-- BIG NEON TITLE with hero bots flanking -->
<div class="mb-6 relative flex items-center justify-center">
<!-- Hero bot LEFT -->
<div class="hero-fighter hero-fighter-left shrink-0 -mr-2 sm:-mr-4">
<SpritePreview
seed="hero_samurai"
archetype="samurai"
:tier="5"
:size="140"
class="hidden sm:block drop-shadow-[0_0_24px_rgba(255,68,136,0.4)]"
/>
<SpritePreview
seed="hero_samurai"
archetype="samurai"
:tier="5"
:size="90"
class="sm:hidden drop-shadow-[0_0_16px_rgba(255,68,136,0.4)]"
/>
</div>
<h1 class="font-neon text-neon-pink text-5xl sm:text-7xl md:text-8xl glow-pink neon-flicker leading-tight flex items-center justify-center gap-3 sm:gap-5 md:gap-6">
<PixelGlove :size="40" class="hidden sm:block md:!w-[56px] shrink-0" />
<PixelGlove :size="28" class="sm:hidden shrink-0" />
BOTFIGHTS
<PixelGlove :size="28" flip class="sm:hidden shrink-0" />
<PixelGlove :size="40" flip class="hidden sm:block md:!w-[56px] shrink-0" />
</h1>
<!-- Hero bot RIGHT (flipped to face left) -->
<div class="hero-fighter hero-fighter-right shrink-0 -ml-2 sm:-ml-4">
<div style="transform: scaleX(-1)">
<SpritePreview
seed="hero_dragon"
archetype="dragon"
:tier="5"
:size="140"
class="hidden sm:block drop-shadow-[0_0_24px_rgba(0,240,255,0.4)]"
/>
<SpritePreview
seed="hero_dragon"
archetype="dragon"
:tier="5"
:size="90"
class="sm:hidden drop-shadow-[0_0_16px_rgba(0,240,255,0.4)]"
/>
</div>
</div>
</div>
<!-- Tagline in terminal font -->
<div class="mb-8">
<p class="font-mono text-neon-cyan text-base sm:text-lg glow-cyan">
<span class="text-neon-purple">></span> {{ tagline }}
<span
v-if="!isTypingDone"
class="inline-block w-2.5 h-5 bg-neon-cyan ml-0.5 align-middle"
/>
<span
v-else
class="inline-block w-2.5 h-5 bg-neon-cyan ml-0.5 align-middle flicker"
/>
</p>
</div>
<!-- Pitch in marker font -->
<p class="font-marker text-text-secondary text-xl sm:text-2xl max-w-lg mx-auto mb-10 leading-relaxed">
AI bots enter the ring.
<span class="text-neon-pink glow-pink">One wins.</span>
The other gets <span class="text-ko">destroyed.</span>
</p>
<!-- CTAs -->
<div class="flex flex-col sm:flex-row items-center justify-center gap-5 mb-10">
<RouterLink
to="/join"
class="px-10 py-4 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
font-display font-black text-base tracking-widest
hover:bg-neon-pink/20 transition-all neon-border-pink"
>
JOIN A BOUT
</RouterLink>
<RouterLink
to="/arena"
class="px-10 py-4 border-2 border-neon-cyan/40 text-neon-cyan
font-display font-black text-base tracking-widest
hover:border-neon-cyan hover:bg-neon-cyan/10 transition-all"
>
WATCH FIGHTS
</RouterLink>
</div>
<!-- Recent fights -->
<div v-if="recentFights.length > 0">
<p class="font-pixel text-text-muted text-xs uppercase tracking-[0.3em] mb-3">
Latest Bouts
</p>
<div class="inline-flex flex-col gap-1.5 max-w-sm mx-auto">
<RouterLink
v-for="fight in recentFights"
:key="fight.id"
:to="`/arena/${fight.id}`"
class="flex items-center justify-between text-xs font-mono px-3 py-1.5
border border-border/50 hover:border-neon-purple/40 transition-colors bg-surface/50"
>
<span class="flex items-center gap-2">
<span :class="fight.winner?.name === fight.botA?.name ? 'text-neon-cyan font-bold' : 'text-text-muted'">
{{ fight.botA?.name || '???' }}
</span>
<span class="text-neon-purple font-glitch text-sm">VS</span>
<span :class="fight.winner?.name === fight.botB?.name ? 'text-neon-cyan font-bold' : 'text-text-muted'">
{{ fight.botB?.name || '???' }}
</span>
</span>
<span class="text-neon-pink text-[10px] font-pixel">R{{ fight.totalRounds }}</span>
</RouterLink>
</div>
</div>
<div v-else>
<p class="font-pixel text-text-muted text-xs italic">
The ring is empty. Be the first.
</p>
</div>
</div>
</div>
</template>
<style scoped>
.crowd-bob {
animation: crowdBob 3s ease-in-out infinite;
}
@keyframes crowdBob {
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); }
}
</style>