Commit c8dabe57 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: integrate Stockfish Bot API for singleplayer chess

Add bot selection page with 7 personality bots, wire up game page
to play against bots via the Stockfish API, add routes and navigation.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 75adde37
...@@ -18,6 +18,7 @@ import { NotificationsPage } from './pages/NotificationsPage' ...@@ -18,6 +18,7 @@ import { NotificationsPage } from './pages/NotificationsPage'
import { ShopPage } from './pages/ShopPage' import { ShopPage } from './pages/ShopPage'
import { SettingsPage } from './pages/SettingsPage' import { SettingsPage } from './pages/SettingsPage'
import { GamePage } from './pages/GamePage' import { GamePage } from './pages/GamePage'
import { BotSelectPage } from './pages/BotSelectPage'
import { NotFoundPage } from './pages/NotFoundPage' import { NotFoundPage } from './pages/NotFoundPage'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
...@@ -72,7 +73,9 @@ export default function App() { ...@@ -72,7 +73,9 @@ export default function App() {
<Route path="/splash" element={<SplashPage />} /> <Route path="/splash" element={<SplashPage />} />
<Route path="/game" element={<ProtectedRoute><GamePage /></ProtectedRoute>} /> <Route path="/game" element={<ProtectedRoute><GamePage /></ProtectedRoute>} />
<Route path="/game/bot/:id" element={<ProtectedRoute><GamePage /></ProtectedRoute>} />
<Route path="/game/:id" element={<ProtectedRoute><GamePage /></ProtectedRoute>} /> <Route path="/game/:id" element={<ProtectedRoute><GamePage /></ProtectedRoute>} />
<Route path="/bot-select" element={<ProtectedRoute><BotSelectPage /></ProtectedRoute>} />
<Route element={<ProtectedRoute><AppShell /></ProtectedRoute>}> <Route element={<ProtectedRoute><AppShell /></ProtectedRoute>}>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
......
const STOCKFISH_URL = 'https://stockfishapi.caprover.al-arcade.com'
export interface Bot {
id: string
name: string
name_ar: string
style: string
style_ar: string
bio: string
bio_ar: string
elo_min: number
elo_max: number
skill_level: number
depth: number
blunder_chance: number
think_time_min_ms: number
think_time_max_ms: number
portrait_url: string
}
export interface MoveResponse {
best_move: string
evaluation: number
depth: number
nodes: number
think_time_ms: number
pv: string
}
export async function fetchBots(): Promise<Bot[]> {
const res = await fetch(`${STOCKFISH_URL}/api/chess/bots`)
if (!res.ok) throw new Error('Failed to fetch bots')
const data = await res.json()
return data.bots
}
export async function getBotMove(fen: string, botId: string): Promise<MoveResponse> {
const res = await fetch(`${STOCKFISH_URL}/api/chess/move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fen, bot_id: botId }),
})
if (!res.ok) {
const err = await res.json().catch(() => ({ error: 'Unknown error' }))
throw new Error(err.error || 'Failed to get bot move')
}
return res.json()
}
export function getBotPortraitUrl(botId: string): string {
return `${STOCKFISH_URL}/portraits/${botId}.png`
}
export function uciToMove(uci: string): { from: string; to: string; promotion?: string } {
return {
from: uci.slice(0, 2),
to: uci.slice(2, 4),
promotion: uci.length > 4 ? uci[4] : undefined,
}
}
import { motion } from 'framer-motion'
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { PageTransition } from '../components/layout/PageTransition'
import { Card } from '../components/ui/Card'
import { Button } from '../components/ui/Button'
import { fetchBots, getBotPortraitUrl, type Bot } from '../lib/stockfish'
import { Cpu, Swords } from 'lucide-react'
const DIFFICULTY_COLORS: Record<string, string> = {
beginner: '#4ECDC4',
defensive: '#5B7FD6',
aggressive: '#E06B4A',
positional: '#7B5EA7',
creative: '#D4A843',
solid: '#6B6B80',
near_perfect: '#FFD700',
}
export function BotSelectPage() {
const navigate = useNavigate()
const [bots, setBots] = useState<Bot[]>([])
const [loading, setLoading] = useState(true)
const [selectedBot, setSelectedBot] = useState<string | null>(null)
useEffect(() => {
fetchBots()
.then(setBots)
.catch(() => {})
.finally(() => setLoading(false))
}, [])
function startGame() {
if (!selectedBot) return
navigate(`/game/bot/${selectedBot}`)
}
if (loading) {
return (
<PageTransition className="px-4 py-6 flex flex-col items-center justify-center min-h-[60vh]">
<div className="w-8 h-8 border-2 border-gold/30 border-t-gold rounded-full animate-spin" />
<p className="mt-3 text-sm text-text-muted">جاري تحميل الروبوتات...</p>
</PageTransition>
)
}
return (
<PageTransition className="px-4 py-6 flex flex-col gap-5">
<div className="flex items-center gap-2">
<Cpu size={20} className="text-gold" />
<h1 className="text-xl font-bold">العب ضد الروبوت</h1>
</div>
<p className="text-sm text-text-secondary">اختر خصمك حسب مستواك</p>
<div className="flex flex-col gap-3">
{bots.map((bot, i) => {
const isSelected = selectedBot === bot.id
const diffColor = DIFFICULTY_COLORS[bot.style] || '#6B6B80'
return (
<motion.div
key={bot.id}
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.06, type: 'spring', stiffness: 400, damping: 25 }}
>
<Card
glow={isSelected}
onClick={() => setSelectedBot(bot.id)}
className={`flex items-center gap-3 transition-all ${isSelected ? 'border-gold/60' : ''}`}
>
<div className="relative w-12 h-12 rounded-full overflow-hidden bg-surface-3 flex-shrink-0">
<img
src={getBotPortraitUrl(bot.id)}
alt={bot.name}
className="w-full h-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none'
}}
/>
<div className="absolute inset-0 flex items-center justify-center">
<Cpu size={18} className="text-text-muted" />
</div>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-sm font-bold truncate">{bot.name_ar}</h3>
<span
className="px-1.5 py-0.5 rounded text-[9px] font-bold"
style={{ backgroundColor: `${diffColor}20`, color: diffColor }}
>
{bot.style_ar}
</span>
</div>
<p className="text-[11px] text-text-muted truncate mt-0.5">{bot.bio_ar}</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-[10px] text-text-muted">
{bot.elo_min}-{bot.elo_max} تقييم
</span>
</div>
</div>
{isSelected && (
<motion.div
className="w-5 h-5 rounded-full bg-gold flex items-center justify-center flex-shrink-0"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 500, damping: 20 }}
>
<Swords size={10} className="text-background" />
</motion.div>
)}
</Card>
</motion.div>
)
})}
</div>
{selectedBot && (
<motion.div
className="sticky bottom-20 pt-3"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
<Button onClick={startGame} className="w-full" size="lg">
ابدا المباراة
</Button>
</motion.div>
)}
</PageTransition>
)
}
This diff is collapsed.
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { Lock, Zap, Timer, Clock, Hourglass } from 'lucide-react' import { Lock, Zap, Timer, Clock, Hourglass, Cpu } from 'lucide-react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { PageTransition } from '../components/layout/PageTransition' import { PageTransition } from '../components/layout/PageTransition'
import { GAMES, TIME_CONTROLS } from '../lib/constants' import { GAMES, TIME_CONTROLS } from '../lib/constants'
...@@ -113,6 +113,11 @@ export function PlayPage() { ...@@ -113,6 +113,11 @@ export function PlayPage() {
البحث عن خصم البحث عن خصم
</Button> </Button>
<Button onClick={() => navigate('/bot-select')} variant="ghost" className="w-full" size="md">
<Cpu size={16} />
العب ضد الروبوت
</Button>
<Button onClick={() => navigate('/game')} variant="ghost" className="w-full" size="md"> <Button onClick={() => navigate('/game')} variant="ghost" className="w-full" size="md">
لعب محلي (تجربة) لعب محلي (تجربة)
</Button> </Button>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment