new bot agent, in python
This commit is contained in:
parent
d2f2eed3f7
commit
6e8f8ef2b2
|
|
@ -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()
|
||||
|
|
@ -1,36 +1,21 @@
|
|||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useGameSocket } from './hooks/useGameSocket';
|
||||
import { BattleModal } from './components/BattleModal';
|
||||
import { BoardCanvas } from './components/BoardCanvas';
|
||||
import { Header } from './components/Header';
|
||||
import { BoardCanvas } from './components/BoardCanvas';
|
||||
import { MovementControls } from './components/MovementControls';
|
||||
import { PartyModal } from './components/PartyModal';
|
||||
import { PlayerList } from './components/PlayerList';
|
||||
import { RegisterModal } from './components/RegisterModal';
|
||||
import { PartyModal } from './components/PartyModal';
|
||||
import { BattleModal } from './components/BattleModal';
|
||||
import { ScoreboardModal } from './components/ScoreboardModal';
|
||||
|
||||
const BOT_NAMES = [
|
||||
'ViperStrike',
|
||||
'PulseMatrix',
|
||||
'NexusGlitch',
|
||||
'AegisPrime',
|
||||
'CobaltDrift',
|
||||
'PhantomCore',
|
||||
'ZephyrByte',
|
||||
'TitanSpark',
|
||||
'ShadowGrid',
|
||||
'QuantumFang',
|
||||
];
|
||||
|
||||
const BOT_COLORS = [
|
||||
'#38BDF8', // Sky
|
||||
'#F43F5E', // Rose
|
||||
'#10B981', // Emerald
|
||||
'#F59E0B', // Amber
|
||||
'#A855F7', // Purple
|
||||
'#EC4899', // Pink
|
||||
'#06B6D4', // Cyan
|
||||
'#6366F1', // Indigo
|
||||
const BOT_PRESETS = [
|
||||
{ name: 'AlphaBot', color: '#38bdf8', strength: 1 },
|
||||
{ name: 'BetaTank', color: '#f43f5e', strength: 2 },
|
||||
{ name: 'GammaStriker', color: '#a855f7', strength: 3 },
|
||||
{ name: 'DeltaRanger', color: '#22c55e', strength: 1 },
|
||||
{ name: 'OmegaTitan', color: '#eab308', strength: 4 },
|
||||
{ name: 'SigmaScout', color: '#ec4899', strength: 1 },
|
||||
];
|
||||
|
||||
export function App() {
|
||||
|
|
@ -62,6 +47,10 @@ export function App() {
|
|||
const [isPartyModalOpen, setIsPartyModalOpen] = useState(false);
|
||||
const [notification, setNotification] = useState<string | null>(null);
|
||||
|
||||
const handleCloseBattle = useCallback(() => {
|
||||
setActiveBattle(null);
|
||||
}, [setActiveBattle]);
|
||||
|
||||
const showNotification = (msg: string) => {
|
||||
setNotification(msg);
|
||||
setTimeout(() => {
|
||||
|
|
@ -70,29 +59,36 @@ export function App() {
|
|||
};
|
||||
|
||||
const handleQuickSpawn = async () => {
|
||||
const name = BOT_NAMES[Math.floor(Math.random() * BOT_NAMES.length)] + '_' + Math.floor(Math.random() * 900 + 100);
|
||||
const color = BOT_COLORS[Math.floor(Math.random() * BOT_COLORS.length)];
|
||||
const existingNames = new Set(boardState.players.map((p) => p.name));
|
||||
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 {
|
||||
const player = await registerPlayer(name, color);
|
||||
showNotification(`Spawned ${player.name} at (${player.x}, ${player.y})`);
|
||||
const player = await registerPlayer(preset.name, preset.color, preset.strength);
|
||||
showNotification(`Spawned ${player.name} (Str: ${player.strength}) at (${player.x}, ${player.y})`);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
showNotification(`Error: ${err.message}`);
|
||||
} else {
|
||||
showNotification('Failed to spawn bot');
|
||||
showNotification(`Failed to spawn bot: ${err.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
if (window.confirm('Are you sure you want to clear all bots and parties from the board?')) {
|
||||
try {
|
||||
await resetBoard();
|
||||
showNotification('Board cleared');
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
showNotification(`Error: ${err.message}`);
|
||||
}
|
||||
try {
|
||||
await resetBoard();
|
||||
setSelectedPlayer(null);
|
||||
setIsAutoPlaying(false);
|
||||
showNotification('Board and all game state cleared.');
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
showNotification(`Reset failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -207,7 +203,7 @@ export function App() {
|
|||
{/* 3-Bout D20 Battle Modal */}
|
||||
<BattleModal
|
||||
battle={activeBattle}
|
||||
onClose={() => setActiveBattle(null)}
|
||||
onClose={handleCloseBattle}
|
||||
/>
|
||||
|
||||
{/* Game Conclusion Scoreboard Modal */}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { BattleResult } from '../types';
|
||||
|
||||
interface BattleModalProps {
|
||||
|
|
@ -8,28 +8,53 @@ interface BattleModalProps {
|
|||
|
||||
export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) => {
|
||||
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(() => {
|
||||
if (!battle) return;
|
||||
if (!battleKey) {
|
||||
setCountdown(5);
|
||||
setProgressPercent(100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset countdown to 5 whenever a new battle modal is opened
|
||||
const DURATION_MS = 5000;
|
||||
const targetEndTime = Date.now() + DURATION_MS;
|
||||
setCountdown(5);
|
||||
setProgressPercent(100);
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
window.clearInterval(timer);
|
||||
onClose();
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
const now = Date.now();
|
||||
const remainingMs = targetEndTime - now;
|
||||
|
||||
if (remainingMs <= 0) {
|
||||
window.clearInterval(timer);
|
||||
setCountdown(0);
|
||||
setProgressPercent(0);
|
||||
onCloseRef.current();
|
||||
return;
|
||||
}
|
||||
|
||||
const seconds = Math.ceil(remainingMs / 1000);
|
||||
const percent = Math.min(100, Math.max(0, (remainingMs / DURATION_MS) * 100));
|
||||
setCountdown(seconds);
|
||||
setProgressPercent(percent);
|
||||
}, 100);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [battle, onClose]);
|
||||
}, [battleKey]);
|
||||
|
||||
if (!battle) return null;
|
||||
|
||||
|
|
@ -42,8 +67,8 @@ export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) =>
|
|||
{/* 5-second automatic progress bar */}
|
||||
<div className="absolute top-2 left-0 right-0 h-1 bg-slate-800">
|
||||
<div
|
||||
className="h-full bg-amber-400 transition-all duration-1000 ease-linear"
|
||||
style={{ width: `${(countdown / 5) * 100}%` }}
|
||||
className="h-full bg-amber-400 transition-all duration-100 ease-linear"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -60,7 +85,7 @@ export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) =>
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
onClick={() => onCloseRef.current()}
|
||||
className="text-slate-400 hover:text-slate-200 p-1 text-sm font-mono"
|
||||
title="Dismiss now"
|
||||
>
|
||||
|
|
@ -155,7 +180,7 @@ export const BattleModal: React.FC<BattleModalProps> = ({ battle, onClose }) =>
|
|||
|
||||
{/* Continue Button with 5s Auto-close Countdown */}
|
||||
<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"
|
||||
>
|
||||
<span>Acknowledge & Continue Battle</span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type {
|
||||
AiStepResponse,
|
||||
AvailableMovesResponse,
|
||||
|
|
@ -47,6 +47,8 @@ export function useGameSocket() {
|
|||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<number | null>(null);
|
||||
const autoPlayIntervalRef = useRef<number | null>(null);
|
||||
const activeBattleRef = useRef<BattleResult | null>(null);
|
||||
activeBattleRef.current = activeBattle;
|
||||
|
||||
const fetchBoard = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -198,41 +200,35 @@ export function useGameSocket() {
|
|||
const nextPlayers = prev.players.filter((p) => p.id !== data.player_id);
|
||||
return {
|
||||
...prev,
|
||||
player_count: data.player_count ?? nextPlayers.length,
|
||||
player_count: nextPlayers.length,
|
||||
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') {
|
||||
setBoardState((prev) => ({
|
||||
...prev,
|
||||
player_count: 0,
|
||||
players: [],
|
||||
parties: [],
|
||||
turn: data.turn ?? INITIAL_BOARD.turn,
|
||||
conclusion: null,
|
||||
}));
|
||||
setBoardState({
|
||||
...data.board,
|
||||
parties: data.board.parties || [],
|
||||
conclusion: data.board.conclusion || null,
|
||||
});
|
||||
setSelectedPlayer(null);
|
||||
setAvailableMoves(null);
|
||||
setActiveBattle(null);
|
||||
setShowScoreboard(false);
|
||||
setLastEventMessage('🔄 Board has been reset.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing WebSocket message:', err);
|
||||
console.error('Error handling WebSocket message:', err);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
setIsConnected(false);
|
||||
reconnectTimeoutRef.current = window.setTimeout(() => {
|
||||
connectWebSocket();
|
||||
}, 2500);
|
||||
console.log('WebSocket closed, attempting reconnect in 3s...');
|
||||
reconnectTimeoutRef.current = window.setTimeout(connectWebSocket, 3000);
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error('WebSocket encountered an error:', err);
|
||||
console.error('WebSocket encountered error:', err);
|
||||
ws.close();
|
||||
};
|
||||
}, []);
|
||||
|
|
@ -253,24 +249,11 @@ export function useGameSocket() {
|
|||
|
||||
useEffect(() => {
|
||||
if (selectedPlayer) {
|
||||
const updated = boardState.players.find((p) => p.id === 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);
|
||||
fetchAvailableMoves(selectedPlayer.id);
|
||||
} else {
|
||||
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 res = await fetch('/api/players', {
|
||||
|
|
@ -279,20 +262,23 @@ export function useGameSocket() {
|
|||
body: JSON.stringify({ name, color, strength }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json().catch(() => ({}));
|
||||
throw new Error(errorData.detail || 'Failed to register player');
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Failed to register player');
|
||||
}
|
||||
const newPlayer: Player = await res.json();
|
||||
setSelectedPlayer(newPlayer);
|
||||
return newPlayer;
|
||||
const player: Player = await res.json();
|
||||
return player;
|
||||
};
|
||||
|
||||
const removePlayer = async (id: string) => {
|
||||
const res = await fetch(`/api/players/${id}`, {
|
||||
const removePlayer = async (playerId: string): Promise<void> => {
|
||||
const res = await fetch(`/api/players/${playerId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
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) {
|
||||
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();
|
||||
return party;
|
||||
|
|
@ -316,7 +302,7 @@ export function useGameSocket() {
|
|||
});
|
||||
if (!res.ok) {
|
||||
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();
|
||||
return result;
|
||||
|
|
@ -371,7 +357,7 @@ export function useGameSocket() {
|
|||
};
|
||||
|
||||
const resetBoard = async () => {
|
||||
const res = await fetch('/api/board/reset', {
|
||||
const res = await fetch('/api/reset', {
|
||||
method: 'POST',
|
||||
});
|
||||
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)
|
||||
// - Party leader: seeks other parties to find and defeat all other parties
|
||||
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;
|
||||
if (!currentId) return;
|
||||
|
||||
|
|
@ -452,6 +441,5 @@ export function useGameSocket() {
|
|||
passTurn,
|
||||
stepActiveBotTurn,
|
||||
resetBoard,
|
||||
refresh: fetchBoard,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue