Add bot to play against.

This commit is contained in:
Greyson Parrelli
2026-07-23 15:58:12 -04:00
parent 3825dbacec
commit db1a6ef290
23 changed files with 1852 additions and 23 deletions
+4 -2
View File
@@ -13,8 +13,10 @@ async function post(path: string, body: unknown): Promise<Session> {
return data as Session
}
export function createGame(name: string): Promise<Session> {
return post('/api/games', { name })
export type BotDifficulty = 'easy' | 'medium' | 'hard'
export function createGame(name: string, bot?: BotDifficulty): Promise<Session> {
return post('/api/games', bot ? { name, bot } : { name })
}
export function joinGame(code: string, name: string): Promise<Session> {
+25
View File
@@ -1,7 +1,14 @@
import { useState } from 'react'
import { createGame, joinGame } from '../api'
import type { BotDifficulty } from '../api'
import type { Session } from '../types'
const BOT_LEVELS: { value: BotDifficulty; label: string; blurb: string }[] = [
{ value: 'easy', label: '🐣 Easy', blurb: 'Learns you the ropes' },
{ value: 'medium', label: '🐺 Medium', blurb: 'Puts up a fight' },
{ value: 'hard', label: '🦁 Hard', blurb: 'Shows no mercy' },
]
// Home is the create/join screen shown when there's no active session.
export function Home({ onSession }: { onSession: (s: Session) => void }) {
const [name, setName] = useState('')
@@ -50,6 +57,24 @@ export function Home({ onSession }: { onSession: (s: Session) => void }) {
Host a new game
</button>
<div className="home-divider">
<span>or challenge the computer</span>
</div>
<div className="home-bots">
{BOT_LEVELS.map((b) => (
<button
key={b.value}
className="btn btn-secondary"
disabled={busy}
title={b.blurb}
onClick={() => run(() => createGame(name, b.value))}
>
{b.label}
</button>
))}
</div>
<div className="home-divider">
<span>or join a friend</span>
</div>
+8 -2
View File
@@ -93,7 +93,13 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
key={p.id}
className={`topbar-player ${p.seat === view.youSeat ? 'is-you' : ''}`}
>
<span className={`conn-dot ${p.connected ? 'on' : 'off'}`} />
{p.isBot ? (
<span className="bot-dot" title="Computer player">
🤖
</span>
) : (
<span className={`conn-dot ${p.connected ? 'on' : 'off'}`} />
)}
<span className="topbar-name">{p.name}</span>
<span className="chip">🏆 {p.trophies}</span>
{(view.phase === 'shop' || view.phase === 'cleanup') && (
@@ -127,7 +133,7 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
)}
</div>
{opponents.some((p) => !p.connected) && view.phase !== 'lobby' && (
{opponents.some((p) => !p.connected && !p.isBot) && view.phase !== 'lobby' && (
<div className="banner banner-warn">An opponent is disconnected</div>
)}
{error && <div className="toast">{error}</div>}
+17
View File
@@ -371,6 +371,17 @@ h3 {
gap: 10px;
}
/* The three computer-opponent difficulty buttons share the row evenly. */
.home-bots {
display: flex;
gap: 10px;
}
.home-bots .btn {
flex: 1;
white-space: nowrap;
}
.home-error {
color: var(--red-soft);
font-weight: 700;
@@ -479,6 +490,12 @@ h3 {
background: var(--red);
}
/* Bots swap the connection dot for a little robot face. */
.bot-dot {
font-size: 0.85rem;
line-height: 1;
}
/* The room code, stamped like a label on a card tray. */
.topbar-code {
font-family: var(--font-display);
+1
View File
@@ -26,6 +26,7 @@ export interface PlayerView {
trophies: number
ready: boolean
connected: boolean
isBot?: boolean
deckSize: number
petCount: number
deck?: Card[]