From b1010ae00879edc96089293d92378f2694bf4083 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 6 Jul 2026 23:34:34 +0200 Subject: [PATCH] Frontend: pridavanie botov v lobby Hostitel prida na volne sedadlo bota (+ Bot = heuristika, + AI bot = natrenovana siet) alebo bota odoberie; boti maju vlastnu ikonu a flag is_bot v rosteroch. Surove ucty bot:- sa vsade zobrazuju cez displayName ako "Bot 2" / "AI bot 1" (stol, kopka, tabulky, historia). Co-Authored-By: Claude Fable 5 --- frontend/src/components/Standings.tsx | 3 ++- frontend/src/components/Trick.tsx | 3 ++- frontend/src/lib/names.ts | 22 ++++++++++++++++++++ frontend/src/lib/socket.ts | 4 ++++ frontend/src/pages/GameOver.tsx | 3 ++- frontend/src/pages/GameTable.tsx | 9 +++++---- frontend/src/pages/History.tsx | 5 +++-- frontend/src/pages/Lobby.tsx | 29 +++++++++++++++++++++++++-- frontend/src/types.ts | 1 + 9 files changed, 68 insertions(+), 11 deletions(-) create mode 100644 frontend/src/lib/names.ts diff --git a/frontend/src/components/Standings.tsx b/frontend/src/components/Standings.tsx index 09fe1db..23d894a 100644 --- a/frontend/src/components/Standings.tsx +++ b/frontend/src/components/Standings.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import type { PlayerInfo } from '../types'; import { computeTotal } from '../lib/standings'; +import { displayName } from '../lib/names'; interface Props { standings: number[][][]; @@ -62,7 +63,7 @@ export default function Standings({ standings, guesses = [], players, myOrder, d }`} style={{ fontSize: fz.head }} > - {p.name} + {displayName(p.name)} ))} diff --git a/frontend/src/components/Trick.tsx b/frontend/src/components/Trick.tsx index 26c8e4b..54d2047 100644 --- a/frontend/src/components/Trick.tsx +++ b/frontend/src/components/Trick.tsx @@ -1,5 +1,6 @@ import type { PlayerInfo, StashData } from '../types'; import CardView from './CardView'; +import { displayName } from '../lib/names'; interface Props { stash: StashData | null; @@ -18,7 +19,7 @@ export default function Trick({ stash, players, myOrder }: Props) { : []; const nameFor = (order: number) => - players.find((p) => p.order === order)?.name ?? ''; + displayName(players.find((p) => p.order === order)?.name); const overlap = -16; const slotH = 80; diff --git a/frontend/src/lib/names.ts b/frontend/src/lib/names.ts new file mode 100644 index 0000000..efe4a8b --- /dev/null +++ b/frontend/src/lib/names.ts @@ -0,0 +1,22 @@ +/** Display helpers for player names. Bot accounts follow the server-side + * convention "bot:-" (see api/bots.py) — render them as a short + * friendly label instead of the raw username. */ + +const BOT_PREFIX = 'bot:'; + +export function isBotName(name?: string | null): boolean { + return !!name && name.startsWith(BOT_PREFIX); +} + +/** "bot:heuristic-2" -> "Bot 2", "bot:neural-1" -> "AI bot 1"; other kinds + * keep a suffix ("Bot 1 (random)"); non-bot names pass through unchanged. */ +export function displayName(name?: string | null): string { + if (!name || !isBotName(name)) return name ?? ''; + const body = name.slice(BOT_PREFIX.length); + const dash = body.lastIndexOf('-'); + const kind = dash > 0 ? body.slice(0, dash) : body; + const num = dash > 0 ? body.slice(dash + 1) : ''; + if (kind === 'neural') return num ? `AI bot ${num}` : 'AI bot'; + const label = num ? `Bot ${num}` : 'Bot'; + return kind === 'heuristic' ? label : `${label} (${kind})`; +} diff --git a/frontend/src/lib/socket.ts b/frontend/src/lib/socket.ts index 641eb66..598835c 100644 --- a/frontend/src/lib/socket.ts +++ b/frontend/src/lib/socket.ts @@ -50,6 +50,10 @@ export const emit = { // Reopen a prematurely-ended game from history back into the lobby. restoreGame: (gid: string) => socket.emit('restore_game', gid), leaveGame: () => socket.emit('leave_game'), + // Bots: host-only, before the game starts (seat picked by the server). + // kind: 'heuristic' (default) | 'neural' (trained net) | 'random' + addBot: (gid: string, kind: string = 'heuristic') => socket.emit('add_bot', gid, kind), + removeBot: (gid: string, order: number) => socket.emit('remove_bot', gid, order), endGame: (gid: string) => socket.emit('end_game', gid), startGame: (gid: string) => socket.emit('start_game', gid), reconnectToGame: (gid: string, token: string) => socket.emit('reconnect_to_game', gid, token), diff --git a/frontend/src/pages/GameOver.tsx b/frontend/src/pages/GameOver.tsx index fd7bcfd..63298cb 100644 --- a/frontend/src/pages/GameOver.tsx +++ b/frontend/src/pages/GameOver.tsx @@ -2,6 +2,7 @@ import { useNavigate } from 'react-router-dom'; import type { PlayerInfo } from '../types'; import { computeTotal } from '../lib/standings'; import { leaveGame } from '../lib/leaveGame'; +import { displayName } from '../lib/names'; interface Props { players: PlayerInfo[]; @@ -30,7 +31,7 @@ export default function GameOver({ players, standings }: Props) { >
{medals[i]} - {p.name} + {displayName(p.name)}
{p.total} diff --git a/frontend/src/pages/GameTable.tsx b/frontend/src/pages/GameTable.tsx index a340e27..b947f29 100644 --- a/frontend/src/pages/GameTable.tsx +++ b/frontend/src/pages/GameTable.tsx @@ -5,6 +5,7 @@ import { emit } from '../lib/socket'; import { leaveGame } from '../lib/leaveGame'; import { computePlayable, stashWinner } from '../lib/gameRules'; import { computeTotal } from '../lib/standings'; +import { displayName } from '../lib/names'; import { useIsDesktop } from '../lib/useIsDesktop'; import { useFitScale } from '../lib/useFitScale'; import Hand from '../components/Hand'; @@ -123,7 +124,7 @@ export default function GameTable() { ? computePlayable(hand, active_stash.cards[String(active_stash.first_player)]?.color ?? null) : undefined; - const activePlayerName = players.find((p) => p.order === active_player)?.name ?? ''; + const activePlayerName = displayName(players.find((p) => p.order === active_player)?.name); // Seat mapping relative to "Ty": left / across / right. const seat = (offset: number): PlayerInfo | undefined => @@ -183,7 +184,7 @@ export default function GameTable() { {opponents.map((p) => (
- {p.name} + {displayName(p.name)}
{computeTotal(standings, p.order)} @@ -224,14 +225,14 @@ export default function GameTable() { const topSeat = (
- +
); const sideSeat = (p?: PlayerInfo) => (
- +
); diff --git a/frontend/src/pages/History.tsx b/frontend/src/pages/History.tsx index 8c610c5..efaed16 100644 --- a/frontend/src/pages/History.tsx +++ b/frontend/src/pages/History.tsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { useGameStore } from '../store/gameStore'; import { emit, socket } from '../lib/socket'; import { useIsDesktop } from '../lib/useIsDesktop'; +import { displayName } from '../lib/names'; import type { GameDetail, GameDetailRound } from '../types'; function fmtDate(iso: string | null): string { @@ -62,7 +63,7 @@ export default function History() { className="flex-1 min-w-0 text-left px-4 py-3 hover:bg-white/[.02] transition-colors" >

{g.name || 'Hra'}

-

{g.players.join(', ')}

+

{g.players.map((n) => displayName(n)).join(', ')}

{fmtDate(g.created_at)} · {g.completed ? 'dohraná' : 'predčasne ukončená'}

@@ -158,7 +159,7 @@ function GameDetailView({ detail, onBack }: { detail: GameDetail; onBack: () => }} > - {p.username} + {displayName(p.username)} {totals[c]} diff --git a/frontend/src/pages/Lobby.tsx b/frontend/src/pages/Lobby.tsx index 1ee79f2..1fba774 100644 --- a/frontend/src/pages/Lobby.tsx +++ b/frontend/src/pages/Lobby.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import { useGameStore } from '../store/gameStore'; import { emit } from '../lib/socket'; import { leaveGame } from '../lib/leaveGame'; +import { displayName } from '../lib/names'; export default function Lobby() { const { gid } = useParams<{ gid: string }>(); @@ -65,11 +66,35 @@ export default function Lobby() { return (
- {p ? '✦' : '○'} + {p ? (p.is_bot ? '⚙' : '✦') : '○'} - {p ? `${p.name}${myPlayer?.order === p.order ? ' (ty)' : ''}` : 'Čaká sa…'} + {p ? `${displayName(p.name)}${myPlayer?.order === p.order ? ' (ty)' : ''}` : 'Čaká sa…'} + {isHost && p?.is_bot && ( + + )} + {isHost && !p && ( + + + + + )}
); })} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 527f86a..30415c2 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -11,6 +11,7 @@ export interface PlayerInfo { name: string; connected: boolean; player_id?: number; + is_bot?: boolean; } export interface MyPlayer {