import React, { useState } from 'react'; import type { BoardState, Player } from '../types'; interface PartyModalProps { isOpen: boolean; boardState: BoardState; onClose: () => void; onFormParty: (memberIds: string[], leaderId: string, name?: string) => Promise; } export const PartyModal: React.FC = ({ isOpen, boardState, onClose, onFormParty, }) => { const [selectedIds, setSelectedIds] = useState([]); const [leaderId, setLeaderId] = useState(''); const [partyName, setPartyName] = useState(''); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); if (!isOpen) return null; const { players } = boardState; // Check if two players are adjacent (Chebyshev distance <= 1) const areAdjacent = (p1: Player, p2: Player) => { return Math.max(Math.abs(p1.x - p2.x), Math.abs(p1.y - p2.y)) <= 1; }; // Find any adjacent pairs in the game for quick selection const findAdjacentPairs = () => { for (let i = 0; i < players.length; i++) { for (let j = i + 1; j < players.length; j++) { if (areAdjacent(players[i], players[j])) { return [players[i], players[j]]; } } } return null; }; const pickStrongestLeader = (ids: string[]) => { if (ids.length === 0) return ''; const selectedBots = players.filter((p) => ids.includes(p.id)); selectedBots.sort((a, b) => (b.strength || 1) - (a.strength || 1)); return selectedBots[0]?.id || ids[0]; }; const handleQuickPair = () => { const pair = findAdjacentPairs(); if (pair) { const ids = [pair[0].id, pair[1].id]; setSelectedIds(ids); const chosenLeader = pickStrongestLeader(ids); setLeaderId(chosenLeader); const leadBot = players.find((p) => p.id === chosenLeader) || pair[0]; setPartyName(`Squad ${leadBot.name}`); setError(null); } else { setError('No adjacent bots found! Move bots within 1 distance of each other to link them.'); } }; const toggleSelect = (id: string) => { setError(null); setSelectedIds((prev) => { const next = prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]; setLeaderId(pickStrongestLeader(next)); return next; }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (selectedIds.length < 2) { setError('Select at least 2 bots to form a party'); return; } if (!leaderId) { setError('You must agree on a party leader'); return; } setLoading(true); setError(null); try { await onFormParty(selectedIds, leaderId, partyName.trim() || undefined); onClose(); } catch (err: unknown) { if (err instanceof Error) { setError(err.message); } else { setError('Failed to form party'); } } finally { setLoading(false); } }; return (
🤝

Form a Bot Party

The goal of a bot without a party is to form a party! Bots insist on being leader if they consider the other bot less than them (higher strength), and desire to join a bot that is equal or stronger.
{/* Party Name */}
setPartyName(e.target.value)} placeholder="e.g. NeonVipers" className="w-full bg-slate-950 border border-slate-700 rounded-lg px-3 py-2 text-xs font-mono text-slate-100 focus:outline-none focus:border-sky-500" />
{/* Member Selection */}
{players.map((p) => { const isChecked = selectedIds.includes(p.id); return (
toggleSelect(p.id)} className={`flex items-center justify-between p-2 rounded-lg cursor-pointer border text-xs transition-colors ${ isChecked ? 'bg-sky-950/60 border-sky-500/70 text-sky-200' : 'bg-slate-900/60 border-slate-800 text-slate-400 hover:border-slate-700' }`} >
{p.name} ⚡{p.strength || 1} ({p.x}, {p.y})
{}} className="rounded border-slate-700 text-sky-500" />
); })}
{/* Agreed Leader Selection */} {selectedIds.length > 0 && (
(Stronger bots insist on leading)
{selectedIds.map((id) => { const bot = players.find((p) => p.id === id); if (!bot) return null; const isLeader = leaderId === id; return ( ); })}
)} {error && (
{error}
)}
); };