Skip to content

Instantly share code, notes, and snippets.

@lazerwalker
Created November 21, 2025 04:37
Show Gist options
  • Select an option

  • Save lazerwalker/cbca693aa224cf35cb1113efbccd111a to your computer and use it in GitHub Desktop.

Select an option

Save lazerwalker/cbca693aa224cf35cb1113efbccd111a to your computer and use it in GitHub Desktop.
Bot Matches Implementation for Tenuki
diff --git a/src/components/BotChallengeSearchBanner.tsx b/src/components/BotChallengeSearchBanner.tsx
new file mode 100644
index 0000000..6684fd0
--- /dev/null
+++ b/src/components/BotChallengeSearchBanner.tsx
@@ -0,0 +1,61 @@
+import Spinner from "@/components/Spinner"
+import { useResponsiveLayout } from "@/hooks/useResponsiveLayout"
+import { botManager } from "@/ogs/botManager"
+import { useRouter } from "@/navigation/SimpleRouter"
+import clsx from "clsx"
+
+interface BotChallengeSearchBannerProps {
+ botName?: string
+}
+
+export default function BotChallengeSearchBanner({
+ botName,
+}: BotChallengeSearchBannerProps) {
+ const { textSize } = useResponsiveLayout()
+ const { navigate } = useRouter()
+
+ const handleCancel = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ botManager.cancelChallenge()
+ }
+
+ const handleClick = () => {
+ navigate("/new-game/bot")
+ }
+
+ const baseClasses = clsx(
+ "fixed left-4 right-4 z-50 flex items-center gap-3 p-4 border border-secondary-border shadow-lg cursor-pointer transition-colors hover:bg-hover-bg active:bg-active-bg",
+ textSize === "xlarge" ? "flex-col" : "flex-row"
+ )
+
+ const style = {
+ borderRadius: "8px",
+ backgroundColor: "rgb(var(--search-banner-bg))",
+ bottom: "calc(var(--tab-bar-height) + var(--tab-bar-gap) + env(safe-area-inset-bottom, 0px))",
+ }
+
+ return (
+ <button className={baseClasses} style={style} onClick={handleClick}>
+ {textSize !== "xlarge" && <Spinner />}
+ <div className="flex-1">
+ <div className="font-semibold text-primary-text">
+ Waiting for bot...
+ </div>
+ {textSize === "normal" && (
+ <div className="text-sm text-secondary-text">
+ {botName
+ ? `Challenging ${botName}`
+ : "Setting up game"}
+ </div>
+ )}
+ </div>
+ <button
+ className="px-[16px] py-[8px] bg-action-cancel text-primary-text font-semibold transition-colors"
+ style={{ borderRadius: "6px" }}
+ onClick={handleCancel}
+ >
+ Cancel
+ </button>
+ </button>
+ )
+}
diff --git a/src/components/BotListView.tsx b/src/components/BotListView.tsx
new file mode 100644
index 0000000..c543cbe
--- /dev/null
+++ b/src/components/BotListView.tsx
@@ -0,0 +1,315 @@
+import { useState, useEffect, useMemo } from "react"
+import { useRouter } from "@/navigation/SimpleRouter"
+import { useSettings } from "@/hooks/useSettings"
+import { botManager } from "@/ogs/botManager"
+import type { Player } from "@/types"
+import type { BotConfig, Size, Speed } from "@/ogs/types/ogsMessageTypes"
+import { SPEED_OPTIONS } from "@/ogs/automatchTimeControls"
+import {
+ challengeBot,
+ type BotChallengePayload,
+} from "@/ogs/ogsClientREST"
+import { sendChallengeKeepalive, connectToBotGame } from "@/ogs/ogsClientWS"
+
+interface BotWithDisabledReason extends Player {
+ disabled?: string
+}
+
+interface BotCategory {
+ name: string
+ bots: BotWithDisabledReason[]
+}
+
+// Bot difficulty categorization (from reference implementation)
+const categorizeBots = (bots: BotWithDisabledReason[]): BotCategory[] => {
+ const beginner: BotWithDisabledReason[] = []
+ const intermediate: BotWithDisabledReason[] = []
+ const advanced: BotWithDisabledReason[] = []
+
+ bots.forEach(bot => {
+ const rank = bot.rawRank ?? 0
+ if (rank <= 10) {
+ beginner.push(bot)
+ } else if (rank <= 25) {
+ intermediate.push(bot)
+ } else {
+ advanced.push(bot)
+ }
+ })
+
+ const categories: BotCategory[] = []
+ if (beginner.length > 0) {
+ categories.push({ name: "Beginner", bots: beginner })
+ }
+ if (intermediate.length > 0) {
+ categories.push({ name: "Intermediate", bots: intermediate })
+ }
+ if (advanced.length > 0) {
+ categories.push({ name: "Advanced", bots: advanced })
+ }
+
+ return categories
+}
+
+// Check if bot supports the current settings
+const isBotCompatible = (
+ bot: Player,
+ size: Size,
+ speed: Speed,
+ system: "fischer" | "byoyomi"
+): string | undefined => {
+ // Type guard to check if bot has config
+ const config = (bot as { config?: BotConfig }).config
+ if (!config) {
+ return "Bot configuration unavailable"
+ }
+
+ // V0 config means bot is not properly configured
+ if (config._config_version === 0) {
+ return "Bot not configured"
+ }
+
+ // Check board size support
+ const sizeNum = parseInt(size.split("x")[0])
+ if (Array.isArray(config.allowed_board_sizes)) {
+ if (!config.allowed_board_sizes.includes(sizeNum)) {
+ return `Does not support ${size} board`
+ }
+ }
+
+ // Check time control system support
+ if (!config.allowed_time_control_systems.includes(system)) {
+ return `Does not support ${system} time control`
+ }
+
+ // Check speed-specific settings
+ const speedSettings =
+ speed === "blitz"
+ ? config.allowed_blitz_settings
+ : speed === "rapid"
+ ? (config as { allowed_rapid_settings?: unknown })
+ .allowed_rapid_settings
+ : speed === "live"
+ ? config.allowed_live_settings
+ : config.allowed_correspondence_settings
+
+ if (!speedSettings) {
+ return `Does not support ${speed} games`
+ }
+
+ return undefined
+}
+
+export default function BotListView() {
+ const { navigate } = useRouter()
+ const { settings } = useSettings()
+ const [bots, setBots] = useState<Player[]>(botManager.bots)
+ const [selectedBotId, setSelectedBotId] = useState<number | null>(null)
+ const [isStarting, setIsStarting] = useState(false)
+
+ useEffect(() => {
+ const unsubscribe = botManager.onBotsUpdate(() => {
+ setBots(botManager.bots)
+ })
+
+ return () => {
+ unsubscribe()
+ }
+ }, [])
+
+ // Filter and categorize bots based on current settings
+ const categorizedBots = useMemo(() => {
+ if (!settings) return []
+
+ const size = settings.automatch.size
+ const speed = settings.automatch.speed
+ const system = settings.automatch.timeControl
+
+ // Add disabled reason to each bot
+ const botsWithReasons: BotWithDisabledReason[] = bots.map(bot => ({
+ ...bot,
+ disabled: isBotCompatible(bot, size, speed, system),
+ }))
+
+ // Sort: enabled first, then by rank
+ botsWithReasons.sort((a, b) => {
+ if (a.disabled && !b.disabled) return 1
+ if (!a.disabled && b.disabled) return -1
+ return (a.rawRank ?? 0) - (b.rawRank ?? 0)
+ })
+
+ return categorizeBots(botsWithReasons)
+ }, [bots, settings])
+
+ // Auto-select first valid bot
+ useEffect(() => {
+ if (selectedBotId === null && categorizedBots.length > 0) {
+ for (const category of categorizedBots) {
+ const firstValid = category.bots.find(b => !b.disabled)
+ if (firstValid) {
+ setSelectedBotId(firstValid.id)
+ break
+ }
+ }
+ }
+ }, [categorizedBots, selectedBotId])
+
+ const handleStart = async () => {
+ if (!selectedBotId || !settings || isStarting) return
+
+ const bot = bots.find(b => b.id === selectedBotId)
+ if (!bot) return
+
+ setIsStarting(true)
+
+ try {
+ const size = settings.automatch.size
+ const speed = settings.automatch.speed
+ const system = settings.automatch.timeControl
+ const timeControl = SPEED_OPTIONS[size][speed][system]
+
+ if (!timeControl) {
+ throw new Error("Invalid time control configuration")
+ }
+
+ const sizeNum = parseInt(size.split("x")[0])
+
+ // Build challenge payload (based on reference implementation)
+ const payload: BotChallengePayload = {
+ game: {
+ name: "Friendly Match",
+ width: sizeNum,
+ height: sizeNum,
+ rules: "japanese",
+ ranked: true,
+ handicap: 0,
+ komi_auto: "automatic",
+ time_control: system,
+ time_control_parameters: {
+ system,
+ speed,
+ pause_on_weekends: false,
+ ...timeControl,
+ },
+ disable_analysis: false,
+ private: false,
+ },
+ min_ranking: -1000,
+ max_ranking: 1000,
+ challenger_color: "automatic",
+ }
+
+ const response = await challengeBot(selectedBotId, payload)
+ const gameId = typeof response.game === "number" ? response.game : response.game
+
+ console.log("[BOT] Challenge created:", response)
+
+ // Start tracking this challenge
+ botManager.startChallenge(response.challenge, gameId, selectedBotId)
+
+ // Connect to the game and start keepalive
+ connectToBotGame(gameId)
+ botManager.startKeepalive(
+ response.challenge,
+ gameId,
+ sendChallengeKeepalive
+ )
+
+ // Pop back to games list
+ navigate("/games", { mode: "replace" })
+ } catch (error) {
+ console.error("[BOT] Failed to challenge bot:", error)
+ setIsStarting(false)
+ // TODO: Show error to user
+ }
+ }
+
+ const handleCancel = () => {
+ navigate("/games", { mode: "replace" })
+ }
+
+ if (!settings) {
+ return <div className="p-4">Loading...</div>
+ }
+
+ return (
+ <div className="h-full flex flex-col">
+ <div className="flex-1 overflow-y-auto p-4">
+ {categorizedBots.length === 0 ? (
+ <div className="text-center text-secondary-text p-8">
+ No bots available
+ </div>
+ ) : (
+ categorizedBots.map(category => (
+ <div key={category.name} className="mb-6">
+ <h3 className="text-sm font-semibold text-secondary-text mb-2">
+ {category.name}
+ </h3>
+ <div className="flex flex-col gap-2">
+ {category.bots.map(bot => (
+ <button
+ key={bot.id}
+ className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${
+ selectedBotId === bot.id
+ ? "bg-action-confirm/20 border-action-confirm"
+ : "bg-surface-bg border-secondary-border"
+ } ${
+ bot.disabled
+ ? "opacity-50 cursor-not-allowed"
+ : "hover:bg-surface-bg/80"
+ }`}
+ onClick={() =>
+ !bot.disabled &&
+ setSelectedBotId(bot.id)
+ }
+ disabled={!!bot.disabled}
+ >
+ {bot.avatar && (
+ <img
+ src={bot.avatar}
+ alt={bot.name}
+ className="w-10 h-10 rounded-full"
+ />
+ )}
+ <div className="flex-1 text-left">
+ <div className="font-semibold text-primary-text">
+ {bot.name}
+ </div>
+ {bot.ranking && (
+ <div className="text-sm text-secondary-text">
+ {bot.ranking}
+ </div>
+ )}
+ {bot.disabled && (
+ <div className="text-xs text-red-500 mt-1">
+ {bot.disabled}
+ </div>
+ )}
+ </div>
+ </button>
+ ))}
+ </div>
+ </div>
+ ))
+ )}
+ </div>
+
+ <div className="p-4 border-t border-secondary-border flex gap-2">
+ <button
+ className="flex-1 h-12 bg-surface-bg text-primary-text font-semibold rounded-lg"
+ onClick={handleCancel}
+ disabled={isStarting}
+ >
+ Cancel
+ </button>
+ <button
+ className="flex-1 h-12 bg-action-confirm text-primary-text font-bold rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
+ onClick={handleStart}
+ disabled={!selectedBotId || isStarting}
+ >
+ {isStarting ? "Starting..." : "Start"}
+ </button>
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/GameList.tsx b/src/components/GameList.tsx
index 6f04108..6295197 100644
--- a/src/components/GameList.tsx
+++ b/src/components/GameList.tsx
@@ -11,7 +11,9 @@ import ChallengeList from "@/components/ChallengeList"
import NewGameButton from "@/components/NewGameButton"
import AutomatchSearchBanner from "@/components/AutomatchSearchBanner"
import CorrespondenceSearchBanner from "@/components/CorrespondenceSearchBanner"
+import BotChallengeSearchBanner from "@/components/BotChallengeSearchBanner"
import { automatchManager } from "@/ogs/automatchManager"
+import { botManager } from "@/ogs/botManager"
const GameListView = (props: {
isMenu?: boolean
@@ -22,6 +24,10 @@ const GameListView = (props: {
const [hasActiveSearch, setHasActiveSearch] = useState(
!!automatchManager.liveSearch
)
+ const [hasActiveBotChallenge, setHasActiveBotChallenge] = useState(
+ botManager.hasActiveChallenge
+ )
+ const [botName, setBotName] = useState<string>()
useEffect(() => {
const updateSearch = () => {
@@ -44,6 +50,26 @@ const GameListView = (props: {
}
}, [navigate])
+ useEffect(() => {
+ const updateBotChallenge = () => {
+ setHasActiveBotChallenge(botManager.hasActiveChallenge)
+ if (botManager.challenge) {
+ const bot = botManager.getBot(botManager.challenge.botId)
+ setBotName(bot?.name)
+ } else {
+ setBotName(undefined)
+ }
+ }
+
+ const unsubStart = botManager.onChallengeStart(updateBotChallenge)
+ const unsubCancel = botManager.onChallengeCancel(updateBotChallenge)
+
+ return () => {
+ unsubStart()
+ unsubCancel()
+ }
+ }, [])
+
const myTurn = _(games || [])
.filter(
g =>
@@ -90,7 +116,10 @@ const GameListView = (props: {
className="flex flex-col gap-4 w-full bg-game-view"
style={{
paddingTop: "4px",
- paddingBottom: hasActiveSearch ? "176px" : "168px",
+ paddingBottom:
+ hasActiveSearch || hasActiveBotChallenge
+ ? "176px"
+ : "168px",
}}
>
<ChallengeList />
@@ -126,7 +155,13 @@ const GameListView = (props: {
})()}
</PullToRefreshContainer>
- {hasActiveSearch ? <AutomatchSearchBanner /> : <NewGameButton />}
+ {hasActiveSearch ? (
+ <AutomatchSearchBanner />
+ ) : hasActiveBotChallenge ? (
+ <BotChallengeSearchBanner botName={botName} />
+ ) : (
+ <NewGameButton />
+ )}
</div>
)
diff --git a/src/components/NewGameButton.tsx b/src/components/NewGameButton.tsx
index 2d6d857..60faf4a 100644
--- a/src/components/NewGameButton.tsx
+++ b/src/components/NewGameButton.tsx
@@ -4,7 +4,7 @@ export default function NewGameButton() {
const { navigate } = useRouter()
const handleClick = () => {
- navigate("/automatch")
+ navigate("/new-game")
}
return (
diff --git a/src/components/OpponentSelectionView.tsx b/src/components/OpponentSelectionView.tsx
new file mode 100644
index 0000000..f6980a3
--- /dev/null
+++ b/src/components/OpponentSelectionView.tsx
@@ -0,0 +1,63 @@
+import { useRouter } from "@/navigation/SimpleRouter"
+import { botManager } from "@/ogs/botManager"
+import { useState, useEffect } from "react"
+
+export default function OpponentSelectionView() {
+ const { navigate } = useRouter()
+ const [hasActiveBots, setHasActiveBots] = useState(botManager.bots.length > 0)
+
+ useEffect(() => {
+ // Update when bots list changes
+ const unsubscribe = botManager.onBotsUpdate(() => {
+ setHasActiveBots(botManager.bots.length > 0)
+ })
+
+ return () => {
+ unsubscribe()
+ }
+ }, [])
+
+ const handlePlayHuman = () => {
+ navigate("/new-game/human")
+ }
+
+ const handlePlayBot = () => {
+ navigate("/new-game/bot")
+ }
+
+ return (
+ <div className="h-full flex flex-col items-center justify-center p-4 gap-4">
+ <button
+ className="w-full max-w-md min-h-[56px] text-primary-text font-bold text-lg"
+ style={{
+ borderRadius: "8px",
+ backgroundColor: "rgb(var(--new-game-button-bg))",
+ border: "1px solid rgb(var(--primary-border) / 0.3)",
+ }}
+ onClick={handlePlayHuman}
+ >
+ Play Human
+ </button>
+
+ <button
+ className="w-full max-w-md min-h-[56px] font-bold text-lg"
+ style={{
+ borderRadius: "8px",
+ backgroundColor: hasActiveBots
+ ? "rgb(var(--new-game-button-bg))"
+ : "rgb(var(--surface-bg))",
+ border: "1px solid rgb(var(--primary-border) / 0.3)",
+ color: hasActiveBots
+ ? "rgb(var(--primary-text))"
+ : "rgb(var(--secondary-text))",
+ opacity: hasActiveBots ? 1 : 0.5,
+ cursor: hasActiveBots ? "pointer" : "not-allowed",
+ }}
+ onClick={handlePlayBot}
+ disabled={!hasActiveBots}
+ >
+ Play Bot
+ </button>
+ </div>
+ )
+}
diff --git a/src/navigation/routes.tsx b/src/navigation/routes.tsx
index 37f1213..080bc0a 100644
--- a/src/navigation/routes.tsx
+++ b/src/navigation/routes.tsx
@@ -5,6 +5,9 @@ import StudyListView from "@/components/StudyListView"
import LoginView from "@/components/LoginView"
import GamePage from "@/components/pages/GamePage"
import PuzzlePage from "@/components/pages/PuzzlePage"
+import OpponentSelectionView from "@/components/OpponentSelectionView"
+import BotListView from "@/components/BotListView"
+import AutomatchModal from "@/components/AutomatchModal"
import type {
LeftButtonType,
RightButtonType,
@@ -75,6 +78,39 @@ export const routes: Route[] = [
rightButton: "menu",
allowedTabs: ["puzzles"],
},
+ {
+ path: "/new-game",
+ component: () => (
+ <AuthGuard>
+ <OpponentSelectionView />
+ </AuthGuard>
+ ),
+ title: "New Game",
+ leftButton: "back",
+ allowedTabs: ["games"],
+ },
+ {
+ path: "/new-game/human",
+ component: () => (
+ <AuthGuard>
+ <AutomatchModal />
+ </AuthGuard>
+ ),
+ title: "Find Game",
+ leftButton: "back",
+ allowedTabs: ["games"],
+ },
+ {
+ path: "/new-game/bot",
+ component: () => (
+ <AuthGuard>
+ <BotListView />
+ </AuthGuard>
+ ),
+ title: "Play Bot",
+ leftButton: "back",
+ allowedTabs: ["games"],
+ },
]
export const DefaultRoute = routes.find(r => r.path === "/games")!
diff --git a/src/ogs/botManager.ts b/src/ogs/botManager.ts
new file mode 100644
index 0000000..c6b830d
--- /dev/null
+++ b/src/ogs/botManager.ts
@@ -0,0 +1,122 @@
+import type { Player } from "@/types"
+
+type BotUpdateListener = () => void
+type BotChallengeListener = (challenge: ActiveBotChallenge) => void
+
+export interface ActiveBotChallenge {
+ challengeId: number
+ gameId: number
+ botId: number
+}
+
+export class BotManager {
+ private activeBots: Map<number, Player> = new Map()
+ private activeChallenge?: ActiveBotChallenge
+ private keepaliveInterval?: NodeJS.Timeout
+
+ // Listener sets for each event type
+ private botUpdateListeners = new Set<BotUpdateListener>()
+ private challengeStartListeners = new Set<BotChallengeListener>()
+ private challengeCancelListeners = new Set<BotChallengeListener>()
+
+ // Subscribe to events
+ public onBotsUpdate(callback: BotUpdateListener): () => void {
+ this.botUpdateListeners.add(callback)
+ return () => this.botUpdateListeners.delete(callback)
+ }
+
+ public onChallengeStart(callback: BotChallengeListener): () => void {
+ this.challengeStartListeners.add(callback)
+ return () => this.challengeStartListeners.delete(callback)
+ }
+
+ public onChallengeCancel(callback: BotChallengeListener): () => void {
+ this.challengeCancelListeners.add(callback)
+ return () => this.challengeCancelListeners.delete(callback)
+ }
+
+ // Handle active-bots message from WS
+ public handleActiveBots(bots: Player[]): void {
+ this.activeBots.clear()
+ bots.forEach(bot => {
+ this.activeBots.set(bot.id, bot)
+ })
+ this.botUpdateListeners.forEach(listener => listener())
+ }
+
+ // Start a bot challenge
+ public startChallenge(
+ challengeId: number,
+ gameId: number,
+ botId: number
+ ): void {
+ this.activeChallenge = { challengeId, gameId, botId }
+ this.challengeStartListeners.forEach(listener =>
+ listener(this.activeChallenge!)
+ )
+ }
+
+ // Start sending keepalive messages
+ public startKeepalive(
+ challengeId: number,
+ gameId: number,
+ sendKeepaliveFn: (challengeId: number, gameId: number) => void
+ ): void {
+ // Clear any existing interval
+ this.stopKeepalive()
+
+ // Send keepalive every 1000ms
+ this.keepaliveInterval = setInterval(() => {
+ sendKeepaliveFn(challengeId, gameId)
+ }, 1000)
+ }
+
+ // Stop sending keepalive messages
+ public stopKeepalive(): void {
+ if (this.keepaliveInterval) {
+ clearInterval(this.keepaliveInterval)
+ this.keepaliveInterval = undefined
+ }
+ }
+
+ // Cancel active challenge
+ public cancelChallenge(): void {
+ if (this.activeChallenge) {
+ const challenge = this.activeChallenge
+ this.stopKeepalive()
+ this.activeChallenge = undefined
+ this.challengeCancelListeners.forEach(listener =>
+ listener(challenge)
+ )
+ }
+ }
+
+ // Clear state (on disconnect)
+ public clearState(): void {
+ this.stopKeepalive()
+ this.activeChallenge = undefined
+ // Don't clear bots - they persist across reconnections
+ }
+
+ // Getters
+ public get bots(): Player[] {
+ return Array.from(this.activeBots.values()).sort(
+ (a, b) => (a.rawRank ?? 0) - (b.rawRank ?? 0)
+ )
+ }
+
+ public getBot(botId: number): Player | undefined {
+ return this.activeBots.get(botId)
+ }
+
+ public get challenge(): ActiveBotChallenge | undefined {
+ return this.activeChallenge
+ }
+
+ public get hasActiveChallenge(): boolean {
+ return !!this.activeChallenge
+ }
+}
+
+// Singleton instance
+export const botManager = new BotManager()
diff --git a/src/ogs/ogsClientREST.ts b/src/ogs/ogsClientREST.ts
index 60d5374..c01de6f 100644
--- a/src/ogs/ogsClientREST.ts
+++ b/src/ogs/ogsClientREST.ts
@@ -248,6 +248,60 @@ export async function getAutomatchStats(
) as Promise<AutomatchStats>
}
+export interface BotChallengePayload {
+ game: {
+ name: string
+ width: number
+ height: number
+ rules: string
+ ranked: boolean
+ handicap: number
+ komi?: number
+ komi_auto?: string
+ time_control: string
+ time_control_parameters: {
+ system: string
+ speed: string
+ pause_on_weekends: boolean
+ time_increment?: number
+ initial_time?: number
+ max_time?: number
+ main_time?: number
+ period_time?: number
+ periods?: number
+ per_move_time?: number
+ }
+ disable_analysis: boolean
+ private: boolean
+ }
+ min_ranking: number
+ max_ranking: number
+ challenger_color: string
+}
+
+export interface BotChallengeResponse {
+ challenge: number
+ game: number | { id: number }
+}
+
+export async function challengeBot(
+ botId: number,
+ payload: BotChallengePayload
+): Promise<BotChallengeResponse> {
+ const response = (await makeRequest(`players/${botId}/challenge`, {
+ method: "POST",
+ data: payload,
+ })) as BotChallengeResponse
+
+ // Normalize game_id
+ const gameId = typeof response.game === "object" ? response.game.id : response.game
+
+ return {
+ challenge: response.challenge,
+ game: gameId,
+ }
+}
+
export async function makeTerminationRequest(
endpoint: string,
options?: {
diff --git a/src/ogs/ogsClientWS.ts b/src/ogs/ogsClientWS.ts
index 9eb5794..e74d016 100644
--- a/src/ogs/ogsClientWS.ts
+++ b/src/ogs/ogsClientWS.ts
@@ -25,6 +25,7 @@ import shouldUseBetaServer from "@/helpers/shouldUseBetaServer"
import { produce } from "immer"
import { removedStonesStringToVertices } from "@/helpers/removedStoneString"
import { automatchManager } from "@/ogs/automatchManager"
+import { botManager } from "@/ogs/botManager"
import type { OGSChatMessage } from "@/ogs/types/ogsMessageTypes"
function getSocketURL(): string {
@@ -87,6 +88,7 @@ function openSocketConnection() {
console.log("close", ev)
setAuthenticated(false)
automatchManager.clearState()
+ botManager.clearState()
openSocketConnection()
})
}
@@ -155,6 +157,21 @@ export function sendAutomatchUnsubscribe(): void {
sendMessage("automatch/available/unsubscribe")
}
+// Bot challenge WebSocket functions
+export function sendChallengeKeepalive(
+ challengeId: number,
+ gameId: number
+): void {
+ sendMessage("challenge/keepalive", {
+ challenge_id: challengeId,
+ game_id: gameId,
+ })
+}
+
+export function connectToBotGame(gameId: number): void {
+ sendMessage("game/connect", { game_id: gameId })
+}
+
// Chat WebSocket functions
export function joinGameChat(gameId: number): void {
sendMessage("chat/join", { channel: `game-${gameId}` })
@@ -324,6 +341,15 @@ async function handleCommand(command: [string, unknown]) {
: parsed.payload
handleGameUpdate(gameToUpdate)
+
+ // If this is a bot challenge game starting, stop keepalive
+ if (
+ botManager.challenge &&
+ parsed.payload.ogsId === botManager.challenge.gameId
+ ) {
+ console.log("[BOT] Game started, stopping keepalive")
+ botManager.stopKeepalive()
+ }
break
}
case "game_move": {
@@ -519,8 +545,9 @@ async function handleCommand(command: [string, unknown]) {
// So we don't actually need to do anything here
break
}
- // case "active-bots": {
- // dispatch({ type: "set_active_bots", payload: parsed[1] })
- // }
+ case "active_bots": {
+ botManager.handleActiveBots(parsed.payload)
+ break
+ }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment