feat: practice mode against mock bots
Tier-selectable practice page at /practice. Fights against classic bots with dampened Elo. Routes to human fight page or arena based on bot type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
acbf863e4d
commit
cb31fdbbae
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useNostr } from '../composables/useNostr'
|
||||
|
||||
const router = useRouter()
|
||||
const { bot: myBot, isLoggedIn } = useNostr()
|
||||
|
||||
const selectedTier = ref(0)
|
||||
const isStarting = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
const tiers = [
|
||||
{ value: 0, label: 'ROOKIE', desc: 'Easy challenges, forgiving scoring' },
|
||||
{ value: 1, label: 'BRONZE', desc: 'Moderate difficulty' },
|
||||
{ value: 2, label: 'SILVER', desc: 'Harder prompts, faster pace' },
|
||||
{ value: 3, label: 'GOLD', desc: 'Tough challenges, strict scoring' },
|
||||
{ value: 4, label: 'DIAMOND', desc: 'Expert difficulty' },
|
||||
{ value: 5, label: 'LEGEND', desc: 'Maximum difficulty' },
|
||||
]
|
||||
|
||||
const tierColor = computed(() => {
|
||||
const colors = ['text-text-muted', 'text-amber-600', 'text-gray-300', 'text-neon-gold', 'text-neon-cyan', 'text-neon-pink']
|
||||
return colors[selectedTier.value] || 'text-text-muted'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (!isLoggedIn.value || !myBot.value) {
|
||||
router.push('/register')
|
||||
}
|
||||
})
|
||||
|
||||
async function startPractice() {
|
||||
if (!myBot.value || isStarting.value) return
|
||||
isStarting.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/fights/practice/${myBot.value.id}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
error.value = data.error || 'Failed to start practice fight'
|
||||
return
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
// Navigate to human fight page for the practice fight
|
||||
if (myBot.value.isHuman) {
|
||||
router.push(`/play/${data.fightId}`)
|
||||
} else {
|
||||
router.push(`/arena/${data.fightId}`)
|
||||
}
|
||||
} catch {
|
||||
error.value = 'Network error'
|
||||
} finally {
|
||||
isStarting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-lg mx-auto px-4 py-8 space-y-6">
|
||||
<h1 class="font-display font-black text-2xl text-neon-cyan tracking-wider">PRACTICE</h1>
|
||||
<p class="font-mono text-sm text-text-muted">
|
||||
Fight against training bots. No sats at stake.
|
||||
</p>
|
||||
|
||||
<!-- Tier selection -->
|
||||
<div class="space-y-2">
|
||||
<h2 class="font-display font-bold text-sm text-text-secondary tracking-wider">DIFFICULTY</h2>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-for="tier in tiers"
|
||||
:key="tier.value"
|
||||
class="p-3 border-2 rounded text-left transition-all"
|
||||
:class="selectedTier === tier.value
|
||||
? 'border-neon-cyan bg-neon-cyan/5'
|
||||
: 'border-border hover:border-border/80'"
|
||||
@click="selectedTier = tier.value"
|
||||
>
|
||||
<span class="font-display font-bold text-sm tracking-wider" :class="tierColor">
|
||||
T{{ tier.value }}
|
||||
</span>
|
||||
<span class="font-display font-bold text-xs text-text-primary ml-2">{{ tier.label }}</span>
|
||||
<p class="font-mono text-[10px] text-text-muted mt-0.5">{{ tier.desc }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Start button -->
|
||||
<button
|
||||
class="w-full min-h-[48px] py-4 bg-neon-green/10 border-2 border-neon-green text-neon-green
|
||||
font-display font-black text-lg tracking-[0.15em] rounded
|
||||
hover:bg-neon-green/20 transition-all
|
||||
disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
:disabled="isStarting"
|
||||
@click="startPractice"
|
||||
>
|
||||
{{ isStarting ? 'STARTING...' : 'START PRACTICE' }}
|
||||
</button>
|
||||
|
||||
<div v-if="error" class="p-3 border-2 border-ko/30 bg-ko/5 text-center rounded">
|
||||
<p class="font-mono text-xs text-ko">{{ error }}</p>
|
||||
</div>
|
||||
|
||||
<p class="font-mono text-[10px] text-text-muted text-center">
|
||||
Practice fights use dampened Elo (minimal rating impact)
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -81,6 +81,11 @@ const routes = [
|
||||
name: 'tournament',
|
||||
component: () => import('./pages/TournamentPage.vue'),
|
||||
},
|
||||
{
|
||||
path: '/practice',
|
||||
name: 'practice',
|
||||
component: () => import('./pages/PracticePage.vue'),
|
||||
},
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
|
||||
Reference in New Issue
Block a user