new bot agent, in python

This commit is contained in:
Isaac Johnson 2026-09-05 19:32:44 -05:00
parent d2f2eed3f7
commit 6e8f8ef2b2
4 changed files with 341 additions and 104 deletions

228
botagent/bot_agent.py Normal file
View File

@ -0,0 +1,228 @@
"""External Bot Agent with deliberate decision-making logic:
- Evaluates targets via Radar sensor.
- Decides whether to negotiate party formation or refuse & fight based on strength.
- Explicitly engages in 3-bout D20 battles when adjacent to an opposing party/refusing bot.
- Parses battle results, bout rolls, scores, and absorbed members.
"""
import time
import requests
from typing import Optional, Dict, Any
BASE_URL = "http://localhost:8000/api"
class SmartBotAgent:
def __init__(self, name: str = "ExternalCyberBot", color: str = "#10b981", strength: int = 4):
self.name = name
self.color = color
self.strength = strength
self.bot_id: Optional[str] = None
self.party_id: Optional[str] = None
self.is_leader: bool = False
def register(self):
"""Register the bot avatar on the 64x64 grid."""
res = requests.post(
f"{BASE_URL}/players",
json={"name": self.name, "color": self.color, "strength": self.strength},
)
res.raise_for_status()
data = res.json()
self.bot_id = data["id"]
print(f"🚀 [REGISTER] Spawned {self.name} (ID: {self.bot_id}, Str: {self.strength}) at ({data['x']}, {data['y']})")
def refresh_status(self):
"""Update bot state (party membership, leader status, score)."""
res = requests.get(f"{BASE_URL}/players/{self.bot_id}")
if res.status_code == 200:
data = res.json()
self.party_id = data.get("party_id")
self.is_leader = data.get("is_party_leader", False)
return data
return None
def decide_and_act(self):
"""Core AI decision loop executed when it is this bot's turn."""
my_info = self.refresh_status()
if not my_info:
return
print(f"\n🎮 --- Turn for {self.name} | Score: {my_info['score']} | Party: {self.party_id or 'Solo'} ---")
# 1. Consult Radar Sensor
radar_res = requests.get(f"{BASE_URL}/players/{self.bot_id}/radar").json()
targets = radar_res.get("targets", [])
nearest = radar_res.get("nearest_target")
# 2. Check for immediate adjacent interaction (distance <= 1)
adjacent_target = None
for t in targets:
if t["distance"] <= 1 and not t["is_ally"]:
adjacent_target = t
break
if adjacent_target:
self._handle_adjacent_encounter(adjacent_target)
return
# 3. If no immediate adjacent enemy/recruit, move towards target
self._navigate_towards_goal(radar_res)
def _handle_adjacent_encounter(self, target: Dict[str, Any]):
"""Deliberate decision: Should we join, recruit, or fight?"""
target_name = target["name"]
target_str = target["strength"]
target_party = target.get("party_id")
print(f"🔍 [ADJACENT ENCOUNTER] Next to '{target_name}' (Str: {target_str}, Party: {target_party or 'None'})")
# SCENARIO A: I am a Solo Bot
if not self.party_id:
if not target_party:
# Both solo: agree to form a party! Stronger bot is leader.
if self.strength >= target_str:
print(f"🤝 [DECISION] Proposing party with {target_name}. I have >= strength, so I will lead!")
leader_id = self.bot_id
else:
print(f"🤝 [DECISION] Proposing party with {target_name}. They are stronger, so they will lead.")
leader_id = target["id"]
self._execute_party_formation([self.bot_id, target["id"]], leader_id)
else:
# Target belongs to a party: Check leader strength
# Leadership Rule: Desires equal or stronger leader. Refuses weaker leader!
# Fetch target's party leader
target_leader_str = target_str # default fallback
party_info = requests.get(f"{BASE_URL}/parties/{target_party}").json()
if party_info:
leader_player = requests.get(f"{BASE_URL}/players/{party_info['leader_id']}").json()
target_leader_str = leader_player.get("strength", 1)
if self.strength <= target_leader_str:
print(f"🤝 [DECISION] Party leader {party_info['leader_name']} has strength {target_leader_str} >= my {self.strength}. Willingly joining squad!")
# Step towards or engine auto-merges
self._step_or_attack(target)
else:
print(f"⚔️ [DECISION] Party leader has lower strength ({target_leader_str} < my {self.strength})! I REFUSE to join. Engaging in battle!")
self._initiate_battle(target["id"])
# SCENARIO B: I am in a Party
else:
if not self.is_leader:
print(f"🛡️ [PARTY MEMBER] Under command of party leader. Awaiting leader movement.")
requests.post(f"{BASE_URL}/players/{self.bot_id}/pass")
return
if not target_party:
# Party Leader vs Solo Bot:
if target_str <= self.strength:
print(f"🤝 [DECISION] Solo bot {target_name} is willing to join our squad under my leadership.")
self._step_or_attack(target)
else:
print(f"⚔️ [DECISION] Solo bot {target_name} refuses weaker leader! Squad is attacking!")
self._initiate_battle(target["id"])
else:
# Party vs Opposing Party: BATTLE!
print(f"⚔️ [DECISION] Hostile party detected: '{target.get('party_name')}'! Engaging in 3-Bout D20 battle!")
self._initiate_battle(target["id"])
def _execute_party_formation(self, member_ids, leader_id):
"""Form a party using the REST API."""
try:
res = requests.post(
f"{BASE_URL}/parties",
json={"member_ids": member_ids, "leader_id": leader_id, "name": f"Squad_{self.name}"},
)
if res.status_code == 201:
party = res.json()
print(f"✅ [PARTY FORMED] Squad '{party['name']}' established! Leader: {party['leader_name']} | Str: {party['total_strength']}")
else:
# Fallback: move into adjacent square to trigger engine merge
self._pass_or_step()
except Exception as e:
print(f"Party formation error: {e}")
def _initiate_battle(self, opponent_id: str):
"""Explicitly call the 3-Bout D20 Battle endpoint."""
print(f"🎲 [BATTLE INITIATED] Clashing with opponent {opponent_id}...")
res = requests.post(
f"{BASE_URL}/battles/fight",
json={"challenger_id": self.bot_id, "defender_id": opponent_id},
)
if res.status_code == 200:
battle = res.json()
print(f"\n--- ⚔️ 3-BOUT BATTLE RESOLUTION ---")
print(f"Bouts won: {battle['party1_name']} ({battle['party1_bouts_won']}) vs {battle['party2_name']} ({battle['party2_bouts_won']})")
for b in battle["bouts"]:
print(f" Bout #{b['bout_number']}: Roll {b['party1_roll']}×{b['party1_strength']} ({b['party1_score']}) vs Roll {b['party2_roll']}×{b['party2_strength']} ({b['party2_score']}) -> Winner: {b['winner_name']}")
print(f"🏆 Overall Winner: {battle['winner_party_name']} (Leader {battle['winner_leader_name']} receives +2 pts)")
print(f"💀 Defeated: {battle['defeated_party_name']} (Leader {battle['killed_leader_name']} -1 pt)")
if battle.get("absorbed_members"):
print(f"🧲 Absorbed {len(battle['absorbed_members'])} member(s) into {battle['winner_party_name']}")
else:
print(f"Battle failed ({res.status_code}): {res.text}")
def _step_or_attack(self, target: Dict[str, Any]):
"""Move adjacent/towards the target."""
moves_res = requests.get(f"{BASE_URL}/players/{self.bot_id}/available-moves").json()
available = [d for d, chk in moves_res["moves"].items() if chk["available"]]
if available:
# Move towards target
chosen = available[0]
res = requests.post(f"{BASE_URL}/players/{self.bot_id}/move", json={"direction": chosen}).json()
if res.get("battle_triggered"):
print(f"⚔️ Move triggered battle! Winner: {res['battle_result']['winner_party_name']}")
elif res.get("party_formed_triggered"):
print(f"🤝 Move resulted in party alliance!")
else:
requests.post(f"{BASE_URL}/players/{self.bot_id}/pass")
def _navigate_towards_goal(self, radar_res: Dict[str, Any]):
"""Move towards the nearest target or explore unvisited areas."""
rec_dir = radar_res.get("recommended_direction")
moves_res = requests.get(f"{BASE_URL}/players/{self.bot_id}/available-moves").json()
available = [d for d, chk in moves_res["moves"].items() if chk["available"]]
if not available:
print("🚫 No available moves. Passing turn.")
requests.post(f"{BASE_URL}/players/{self.bot_id}/pass")
return
chosen_dir = rec_dir if rec_dir in available else available[0]
print(f"🧭 Moving {chosen_dir} (Goal: {radar_res.get('bot_goal')}, Action: {radar_res.get('recommended_action')})")
res = requests.post(f"{BASE_URL}/players/{self.bot_id}/move", json={"direction": chosen_dir}).json()
if res.get("battle_triggered"):
print(f"⚔️ Encounter battle! Winner: {res['battle_result']['winner_party_name']}")
elif res.get("party_formed_triggered"):
print(f"🤝 Formed or joined squad: {res.get('formed_party', {}).get('name')}")
def run(self):
"""Main game loop for external agent."""
self.register()
try:
while True:
turn_info = requests.get(f"{BASE_URL}/turn").json()
curr_player_id = turn_info.get("current_player_id")
if curr_player_id == self.bot_id:
self.decide_and_act()
# Check for game conclusion
conc = requests.get(f"{BASE_URL}/game/conclusion").json()
if conc.get("concluded"):
print(f"\n🎉 [GAME CONCLUDED] All bots united under '{conc['winning_party_name']}'!")
break
else:
time.sleep(0.4)
except KeyboardInterrupt:
print(f"\nDisconnecting {self.name}...")
requests.delete(f"{BASE_URL}/players/{self.bot_id}")
if __name__ == "__main__":
agent = SmartBotAgent(name="ExternalCyberBot", color="#10b981", strength=4)
agent.run()

View File

@ -1,36 +1,21 @@
import { useState } from 'react'; import { useCallback, useState } from 'react';
import { useGameSocket } from './hooks/useGameSocket'; import { useGameSocket } from './hooks/useGameSocket';
import { BattleModal } from './components/BattleModal';
import { BoardCanvas } from './components/BoardCanvas';
import { Header } from './components/Header'; import { Header } from './components/Header';
import { BoardCanvas } from './components/BoardCanvas';
import { MovementControls } from './components/MovementControls'; import { MovementControls } from './components/MovementControls';
import { PartyModal } from './components/PartyModal';
import { PlayerList } from './components/PlayerList'; import { PlayerList } from './components/PlayerList';
import { RegisterModal } from './components/RegisterModal'; import { RegisterModal } from './components/RegisterModal';
import { PartyModal } from './components/PartyModal';
import { BattleModal } from './components/BattleModal';
import { ScoreboardModal } from './components/ScoreboardModal'; import { ScoreboardModal } from './components/ScoreboardModal';
const BOT_NAMES = [ const BOT_PRESETS = [
'ViperStrike', { name: 'AlphaBot', color: '#38bdf8', strength: 1 },
'PulseMatrix', { name: 'BetaTank', color: '#f43f5e', strength: 2 },
'NexusGlitch', { name: 'GammaStriker', color: '#a855f7', strength: 3 },
'AegisPrime', { name: 'DeltaRanger', color: '#22c55e', strength: 1 },
'CobaltDrift', { name: 'OmegaTitan', color: '#eab308', strength: 4 },
'PhantomCore', { name: 'SigmaScout', color: '#ec4899', strength: 1 },
'ZephyrByte',
'TitanSpark',
'ShadowGrid',
'QuantumFang',
];
const BOT_COLORS = [
'#38BDF8', // Sky
'#F43F5E', // Rose
'#10B981', // Emerald
'#F59E0B', // Amber
'#A855F7', // Purple
'#EC4899', // Pink
'#06B6D4', // Cyan
'#6366F1', // Indigo
]; ];
export function App() { export function App() {
@ -62,6 +47,10 @@ export function App() {
const [isPartyModalOpen, setIsPartyModalOpen] = useState(false); const [isPartyModalOpen, setIsPartyModalOpen] = useState(false);
const [notification, setNotification] = useState<string | null>(null); const [notification, setNotification] = useState<string | null>(null);
const handleCloseBattle = useCallback(() => {
setActiveBattle(null);
}, [setActiveBattle]);
const showNotification = (msg: string) => { const showNotification = (msg: string) => {
setNotification(msg); setNotification(msg);
setTimeout(() => { setTimeout(() => {
@ -70,29 +59,36 @@ export function App() {
}; };
const handleQuickSpawn = async () => { const handleQuickSpawn = async () => {
const name = BOT_NAMES[Math.floor(Math.random() * BOT_NAMES.length)] + '_' + Math.floor(Math.random() * 900 + 100); const existingNames = new Set(boardState.players.map((p) => p.name));
const color = BOT_COLORS[Math.floor(Math.random() * BOT_COLORS.length)]; const availablePresets = BOT_PRESETS.filter((p) => !existingNames.has(p.name));
const preset =
availablePresets.length > 0
? availablePresets[Math.floor(Math.random() * availablePresets.length)]
: {
name: `Bot_${Math.floor(Math.random() * 1000)}`,
color: `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0')}`,
strength: Math.floor(Math.random() * 3) + 1,
};
try { try {
const player = await registerPlayer(name, color); const player = await registerPlayer(preset.name, preset.color, preset.strength);
showNotification(`Spawned ${player.name} at (${player.x}, ${player.y})`); showNotification(`Spawned ${player.name} (Str: ${player.strength}) at (${player.x}, ${player.y})`);
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof Error) { if (err instanceof Error) {
showNotification(`Error: ${err.message}`); showNotification(`Failed to spawn bot: ${err.message}`);
} else {
showNotification('Failed to spawn bot');
} }
} }
}; };
const handleReset = async () => { const handleReset = async () => {
if (window.confirm('Are you sure you want to clear all bots and parties from the board?')) {
try { try {
await resetBoard(); await resetBoard();
showNotification('Board cleared'); setSelectedPlayer(null);
setIsAutoPlaying(false);
showNotification('Board and all game state cleared.');
} catch (err: unknown) { } catch (err: unknown) {
if (err instanceof Error) { if (err instanceof Error) {
showNotification(`Error: ${err.message}`); showNotification(`Reset failed: ${err.message}`);
}
} }
} }
}; };
@ -207,7 +203,7 @@ export function App() {
{/* 3-Bout D20 Battle Modal */} {/* 3-Bout D20 Battle Modal */}
<BattleModal <BattleModal
battle={activeBattle} battle={activeBattle}
onClose={() => setActiveBattle(null)} onClose={handleCloseBattle}
/> />
{/* Game Conclusion Scoreboard Modal */} {/* Game Conclusion Scoreboard Modal */}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import type { BattleResult } from '../types'; import type { BattleResult } from '../types';
interface BattleModalProps { interface BattleModalProps {
@ -8,28 +8,53 @@ interface BattleModalProps {
export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) => { export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) => {
const [countdown, setCountdown] = useState(5); const [countdown, setCountdown] = useState(5);
const [progressPercent, setProgressPercent] = useState(100);
// Keep onClose in a ref to decouple the timer effect from callback identity changes
const onCloseRef = useRef(onClose);
useEffect(() => {
onCloseRef.current = onClose;
});
// Unique battle key so the 5s timer only restarts when a genuinely new battle arrives
const battleKey = battle
? `${battle.winner_party_id}_${battle.defeated_party_id}_${battle.party1_total_score}_${battle.party2_total_score}_${battle.killed_leader_id}`
: null;
useEffect(() => { useEffect(() => {
if (!battle) return; if (!battleKey) {
// Reset countdown to 5 whenever a new battle modal is opened
setCountdown(5); setCountdown(5);
setProgressPercent(100);
return;
}
const DURATION_MS = 5000;
const targetEndTime = Date.now() + DURATION_MS;
setCountdown(5);
setProgressPercent(100);
const timer = window.setInterval(() => { const timer = window.setInterval(() => {
setCountdown((prev) => { const now = Date.now();
if (prev <= 1) { const remainingMs = targetEndTime - now;
if (remainingMs <= 0) {
window.clearInterval(timer); window.clearInterval(timer);
onClose(); setCountdown(0);
return 0; setProgressPercent(0);
onCloseRef.current();
return;
} }
return prev - 1;
}); const seconds = Math.ceil(remainingMs / 1000);
}, 1000); const percent = Math.min(100, Math.max(0, (remainingMs / DURATION_MS) * 100));
setCountdown(seconds);
setProgressPercent(percent);
}, 100);
return () => { return () => {
window.clearInterval(timer); window.clearInterval(timer);
}; };
}, [battle, onClose]); }, [battleKey]);
if (!battle) return null; if (!battle) return null;
@ -42,8 +67,8 @@ export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) =>
{/* 5-second automatic progress bar */} {/* 5-second automatic progress bar */}
<div className="absolute top-2 left-0 right-0 h-1 bg-slate-800"> <div className="absolute top-2 left-0 right-0 h-1 bg-slate-800">
<div <div
className="h-full bg-amber-400 transition-all duration-1000 ease-linear" className="h-full bg-amber-400 transition-all duration-100 ease-linear"
style={{ width: `${(countdown / 5) * 100}%` }} style={{ width: `${progressPercent}%` }}
/> />
</div> </div>
@ -60,7 +85,7 @@ export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) =>
</div> </div>
</div> </div>
<button <button
onClick={onClose} onClick={() => onCloseRef.current()}
className="text-slate-400 hover:text-slate-200 p-1 text-sm font-mono" className="text-slate-400 hover:text-slate-200 p-1 text-sm font-mono"
title="Dismiss now" title="Dismiss now"
> >
@ -155,7 +180,7 @@ export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) =>
{/* Continue Button with 5s Auto-close Countdown */} {/* Continue Button with 5s Auto-close Countdown */}
<button <button
onClick={onClose} onClick={() => onCloseRef.current()}
className="w-full bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold py-2.5 rounded-xl text-xs font-mono transition-all shadow-lg shadow-amber-500/20 flex items-center justify-center gap-2" className="w-full bg-amber-500 hover:bg-amber-400 text-slate-950 font-bold py-2.5 rounded-xl text-xs font-mono transition-all shadow-lg shadow-amber-500/20 flex items-center justify-center gap-2"
> >
<span>Acknowledge & Continue Battle</span> <span>Acknowledge & Continue Battle</span>

View File

@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import type { import type {
AiStepResponse, AiStepResponse,
AvailableMovesResponse, AvailableMovesResponse,
@ -47,6 +47,8 @@ export function useGameSocket() {
const wsRef = useRef<WebSocket | null>(null); const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<number | null>(null); const reconnectTimeoutRef = useRef<number | null>(null);
const autoPlayIntervalRef = useRef<number | null>(null); const autoPlayIntervalRef = useRef<number | null>(null);
const activeBattleRef = useRef<BattleResult | null>(null);
activeBattleRef.current = activeBattle;
const fetchBoard = useCallback(async () => { const fetchBoard = useCallback(async () => {
try { try {
@ -198,41 +200,35 @@ export function useGameSocket() {
const nextPlayers = prev.players.filter((p) => p.id !== data.player_id); const nextPlayers = prev.players.filter((p) => p.id !== data.player_id);
return { return {
...prev, ...prev,
player_count: data.player_count ?? nextPlayers.length, player_count: nextPlayers.length,
players: nextPlayers, players: nextPlayers,
parties: data.parties ?? prev.parties.filter((party) => party.member_ids.includes(data.player_id)),
turn: data.turn ?? prev.turn,
}; };
}); });
setSelectedPlayer((prev) => (prev?.id === data.player_id ? null : prev));
} else if (data.event === 'board_reset') { } else if (data.event === 'board_reset') {
setBoardState((prev) => ({ setBoardState({
...prev, ...data.board,
player_count: 0, parties: data.board.parties || [],
players: [], conclusion: data.board.conclusion || null,
parties: [], });
turn: data.turn ?? INITIAL_BOARD.turn,
conclusion: null,
}));
setSelectedPlayer(null); setSelectedPlayer(null);
setAvailableMoves(null); setAvailableMoves(null);
setActiveBattle(null); setActiveBattle(null);
setShowScoreboard(false); setShowScoreboard(false);
setLastEventMessage('🔄 Board has been reset.');
} }
} catch (err) { } catch (err) {
console.error('Error parsing WebSocket message:', err); console.error('Error handling WebSocket message:', err);
} }
}; };
ws.onclose = () => { ws.onclose = () => {
setIsConnected(false); setIsConnected(false);
reconnectTimeoutRef.current = window.setTimeout(() => { console.log('WebSocket closed, attempting reconnect in 3s...');
connectWebSocket(); reconnectTimeoutRef.current = window.setTimeout(connectWebSocket, 3000);
}, 2500);
}; };
ws.onerror = (err) => { ws.onerror = (err) => {
console.error('WebSocket encountered an error:', err); console.error('WebSocket encountered error:', err);
ws.close(); ws.close();
}; };
}, []); }, []);
@ -253,24 +249,11 @@ export function useGameSocket() {
useEffect(() => { useEffect(() => {
if (selectedPlayer) { if (selectedPlayer) {
const updated = boardState.players.find((p) => p.id === selectedPlayer.id); fetchAvailableMoves(selectedPlayer.id);
if (updated) {
setSelectedPlayer(updated);
} else {
setSelectedPlayer(null);
setAvailableMoves(null);
}
}
}, [boardState.players, selectedPlayer]);
useEffect(() => {
const targetId = selectedPlayer?.id || boardState.turn.current_player_id;
if (targetId) {
fetchAvailableMoves(targetId);
} else { } else {
setAvailableMoves(null); setAvailableMoves(null);
} }
}, [selectedPlayer?.id, boardState.turn.current_player_id, boardState.players, fetchAvailableMoves]); }, [selectedPlayer, fetchAvailableMoves]);
const registerPlayer = async (name: string, color: string, strength: number = 1): Promise<Player> => { const registerPlayer = async (name: string, color: string, strength: number = 1): Promise<Player> => {
const res = await fetch('/api/players', { const res = await fetch('/api/players', {
@ -279,20 +262,23 @@ export function useGameSocket() {
body: JSON.stringify({ name, color, strength }), body: JSON.stringify({ name, color, strength }),
}); });
if (!res.ok) { if (!res.ok) {
const errorData = await res.json().catch(() => ({})); const err = await res.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to register player'); throw new Error(err.detail || 'Failed to register player');
} }
const newPlayer: Player = await res.json(); const player: Player = await res.json();
setSelectedPlayer(newPlayer); return player;
return newPlayer;
}; };
const removePlayer = async (id: string) => { const removePlayer = async (playerId: string): Promise<void> => {
const res = await fetch(`/api/players/${id}`, { const res = await fetch(`/api/players/${playerId}`, {
method: 'DELETE', method: 'DELETE',
}); });
if (!res.ok) { if (!res.ok) {
throw new Error('Failed to remove player'); const err = await res.json().catch(() => ({}));
throw new Error(err.detail || 'Failed to delete player');
}
if (selectedPlayer?.id === playerId) {
setSelectedPlayer(null);
} }
}; };
@ -304,7 +290,7 @@ export function useGameSocket() {
}); });
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({})); const err = await res.json().catch(() => ({}));
throw new Error(err.detail || 'Failed to form party'); throw new Error(err.detail || 'Failed to create party');
} }
const party: Party = await res.json(); const party: Party = await res.json();
return party; return party;
@ -316,7 +302,7 @@ export function useGameSocket() {
}); });
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({})); const err = await res.json().catch(() => ({}));
throw new Error(err.detail || 'Failed to defeat party'); throw new Error(err.detail || 'Failed to trigger party defeat');
} }
const result: PartyDefeatResult = await res.json(); const result: PartyDefeatResult = await res.json();
return result; return result;
@ -371,7 +357,7 @@ export function useGameSocket() {
}; };
const resetBoard = async () => { const resetBoard = async () => {
const res = await fetch('/api/board/reset', { const res = await fetch('/api/reset', {
method: 'POST', method: 'POST',
}); });
if (!res.ok) { if (!res.ok) {
@ -384,6 +370,9 @@ export function useGameSocket() {
// - Bot without party: seeks other bots to form a party (stronger bot insists on being leader) // - Bot without party: seeks other bots to form a party (stronger bot insists on being leader)
// - Party leader: seeks other parties to find and defeat all other parties // - Party leader: seeks other parties to find and defeat all other parties
const stepActiveBotTurn = useCallback(async () => { const stepActiveBotTurn = useCallback(async () => {
// If a battle modal is currently open, pause turn stepping until battle modal acknowledges/closes
if (activeBattleRef.current) return;
const currentId = boardState.turn.current_player_id; const currentId = boardState.turn.current_player_id;
if (!currentId) return; if (!currentId) return;
@ -452,6 +441,5 @@ export function useGameSocket() {
passTurn, passTurn,
stepActiveBotTurn, stepActiveBotTurn,
resetBoard, resetBoard,
refresh: fetchBoard,
}; };
} }