import type { FactionId } from './catalog'; import { gameTerritories, splitLocation, type MobileBoard } from './board'; import { presenceAt, type ForcePresence } from './force-presence'; import { stormExposesTerritory, stormSectorAfter } from './disaster-rules'; import { validateSpiritualAdvisorSource, type SpiritualAdvisorSource, type SpiritualAdvisorContext } from './spiritual-advisor'; export type AdvisorStance = { lockedTurn?: number; aloneFlipHold?: string; }; export type AutomaticAdvisorTransport = { event: string; player: string; origin: string; to: string; sector: number; amount: number; elite: number; group: Record; }; export type AutomaticAdvisorHomeworldTransport = { event: string; kind: 'native' | 'junction'; player: string; origin: string; destination: string; amount: number; elite: number; group: Record; }; export type AutomaticAdvisorGamont = { event: string; player: string; target: string; location: string; elite: number; returned: boolean; mentat: boolean; }; export type AutomaticAdvisorBattle = { event: string; turn: number; territory: string; combatants: string[]; winner: string | null; result: 'normal' | 'traitor' | 'mutualTraitors' | 'explosion'; }; export type AutomaticAdvisorFaceDance = { event: string; battle: AutomaticAdvisorBattle; player: string; territory: string; sector: number; sources: Record; amount: number; returned: Record; }; export type AutomaticAdvisorStorm = { event: string; from: number; distance: number; traversed: number; sector: number; }; export type AutomaticAdvisorRelocation = { event: string; kind: 'ambassador' | 'discovery'; player: string; origin: string; destination: string; sector: number; }; /** Completed native Fremen ride, retained while its original arrival waits. */ export type AutomaticAdvisorWormRide = { event: string; player: string; origin: string; destination: string; sector: number; amount: number; elite: number; }; export type AutomaticAdvisorFlipSource = { event: string; owner: string; territory: string; turn: number; phase: number; forces: Record; } & ( | { producer: 'groundMovement'; mover: string; move: number } | { producer: 'guildTransport'; transport: AutomaticAdvisorTransport } | { producer: 'homeworldTransport'; transport: AutomaticAdvisorHomeworldTransport } | { producer: 'gamont'; gamont: AutomaticAdvisorGamont } | { producer: 'battleLosses'; battle: AutomaticAdvisorBattle } | { producer: 'faceDance'; faceDance: AutomaticAdvisorFaceDance } | { producer: 'storm'; storm: AutomaticAdvisorStorm } | { producer: 'specialRelocation'; relocation: AutomaticAdvisorRelocation } | { producer: 'wormRide'; wormRide: AutomaticAdvisorWormRide } | { producer: 'spiritualArrival'; spiritual: SpiritualAdvisorSource } ); export type AutomaticAdvisorFlipPlayer = ForcePresence & { id: string; faction: FactionId; advisors?: Readonly>; moved?: number; shipped?: boolean; reserves?: number; elites?: { forces: Readonly> }; }; export type AdvisorSettlementContext = { advanced: boolean; players: readonly AutomaticAdvisorFlipPlayer[]; }; export type AutomaticAdvisorFlipContext = AdvisorSettlementContext & MobileBoard & { status: string; turn: number; phase: number; homeworldRevivalReturn?: SpiritualAdvisorContext['homeworldRevivalReturn']; homeworldVictoryReinforcement?: SpiritualAdvisorContext['homeworldVictoryReinforcement']; pendingSpiritualAdvisor?: SpiritualAdvisorContext['pendingSpiritualAdvisor']; pendingAmbassador?: SpiritualAdvisorContext['pendingAmbassador']; choamMentatPending?: boolean; lastBattleContext?: { event: string; turn: number; territory: string; combatants: readonly string[]; winner: string | null; result: string; } | null; battle?: unknown; homeworlds?: unknown; storm?: number; shieldWallDestroyed?: boolean; stormResolution?: { from: number; distance: number; traversed: number; pending: readonly string[]; } | null; }; export class AutomaticAdvisorFlipError extends Error { constructor(message: string) { super(message); this.name = 'AutomaticAdvisorFlipError'; } } const identifier = (value: unknown): value is string => typeof value === 'string' && value.trim().length > 0; const positive = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) > 0; const nonnegative = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0; const record = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); function requireFlip(value: unknown, message: string): asserts value { if (!value) throw new AutomaticAdvisorFlipError(message); } function physicalGroup(player: AutomaticAdvisorFlipPlayer, territory: string): Record { return Object.fromEntries(Object.entries(player.forces).filter( ([key, count]) => splitLocation(key).territory === territory && count > 0, )); } function otherPresence(ctx: AdvisorSettlementContext, player: AutomaticAdvisorFlipPlayer, territory: string) { return ctx.players.some((other) => other.id !== player.id && presenceAt(other, territory) > 0); } function currentWindow(ctx: AutomaticAdvisorFlipContext) { return ctx.advanced === true && ctx.status === 'playing' && [0, 1, 3, 4, 5, 6, 8].includes(ctx.phase) && positive(ctx.turn); } /** A supported committed producer opens a response for these current groups. * No cards, resources, leaders or hidden marker values determine eligibility. */ export function quoteAutomaticAdvisorFlips(ctx: AutomaticAdvisorFlipContext): { player: string; territory: string; forces: Record; }[] { if (!currentWindow(ctx)) return []; const flips: { player: string; territory: string; forces: Record }[] = []; for (const player of ctx.players) { if (player.faction !== 'beneGesserit') continue; for (const [territory, stance] of Object.entries(player.advisors ?? {})) { if (stance.aloneFlipHold !== undefined || otherPresence(ctx, player, territory)) continue; const forces = physicalGroup(player, territory); if (Object.keys(forces).length) flips.push({ player: player.id, territory, forces }); } } return flips; } /** Keep a prevented alone occurrence until physical emptiness or foreign entry. * With no occurrence hold, retain the legacy automatic conversion, including setup. */ export function advisorStanceAfterSettlement( ctx: AdvisorSettlementContext, player: AutomaticAdvisorFlipPlayer | string, territory: string, ): AdvisorStance | undefined { const current = ctx.players.find((candidate) => candidate.id === (typeof player === 'string' ? player : player.id)); const stance = current?.advisors?.[territory]; if (!current || current.faction !== 'beneGesserit' || !stance) return undefined; if (!ctx.advanced) return { ...stance }; if (!Object.keys(physicalGroup(current, territory)).length) return undefined; if (otherPresence(ctx, current, territory)) { const remaining = { ...stance }; delete remaining.aloneFlipHold; return remaining; } return stance.aloneFlipHold !== undefined ? { ...stance } : undefined; } function validateTransportSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { const transport = source.transport; requireFlip(record(transport) && Object.keys(transport).sort().join(',') === 'amount,elite,event,group,origin,player,sector,to' && identifier(transport.event) && identifier(transport.player) && identifier(transport.origin) && identifier(transport.to) && nonnegative(transport.sector) && transport.sector <= 18 && positive(transport.amount) && nonnegative(transport.elite) && transport.elite <= transport.amount && record(transport.group) && Object.values(transport.group).every(positive) && Object.values(transport.group).reduce((total, count) => total + count, 0) === transport.amount, 'Automatic advisor conversion lost its exact committed Guild transport metadata.'); const places = gameTerritories(ctx); const origin = places.find((place) => place.id === transport.origin); const destination = places.find((place) => place.id === transport.to); requireFlip(origin && (transport.to === 'reserves' || (destination && destination.sectors.includes(transport.sector))) && Object.keys(transport.group).every((key) => { const point = splitLocation(key); return key === `${point.territory}:${point.sector}` && point.territory === transport.origin && origin.sectors.includes(point.sector); }) && (source.territory === transport.origin || (transport.to !== 'reserves' && source.territory === transport.to)), 'Automatic advisor conversion must belong to the original transport origin or destination.'); const actors = ctx.players.filter((player) => player.id === transport.player); const actor = actors[0]; requireFlip(actors.length === 1 && actor.shipped === true, 'Automatic advisor conversion lost its completed native transport slot.'); const deposited = transport.to === 'reserves' ? actor.reserves : actor.forces[`${transport.to}:${transport.sector}`]; requireFlip(nonnegative(deposited) && deposited >= transport.amount, 'Automatic advisor conversion lost the original transported physical counters.'); } function validateGamontSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { const receipt = source.gamont; requireFlip(record(receipt) && Object.keys(receipt).sort().join(',') === 'elite,event,location,mentat,player,returned,target' && identifier(receipt.event) && identifier(receipt.player) && identifier(receipt.target) && identifier(receipt.location) && (receipt.elite === 0 || receipt.elite === 1) && typeof receipt.returned === 'boolean' && typeof receipt.mentat === 'boolean', 'Automatic advisor conversion lost its exact public Trip to Gamont receipt.'); const point = splitLocation(receipt.location); requireFlip(receipt.location === `${point.territory}:${point.sector}` && gameTerritories(ctx).some((place) => place.id === point.territory && place.sectors.includes(point.sector)) && source.territory === point.territory, 'Automatic advisor conversion must use the original current Gamont location.'); const actors = ctx.players.filter((player) => player.id === receipt.player); const targets = ctx.players.filter((player) => player.id === receipt.target); requireFlip(actors.length === 1 && actors[0].faction === 'choam' && targets.length === 1 && receipt.target !== receipt.player, 'Automatic advisor conversion needs the original native CHOAM actor and distinct Gamont target.'); if (receipt.returned) requireFlip(nonnegative(targets[0].reserves) && targets[0].reserves >= 1, 'Automatic advisor conversion lost the returned Gamont counter.'); requireFlip(!receipt.mentat || ctx.choamMentatPending === true, 'Automatic advisor conversion lost its final CHOAM Mentat opportunity.'); } function validateHomeworldTransportSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { const transport = source.transport; requireFlip(record(ctx.homeworlds) && record(ctx.homeworlds.custody) && record(transport) && Object.keys(transport).sort().join(',') === 'amount,destination,elite,event,group,kind,origin,player' && identifier(transport.event) && (transport.kind === 'native' || transport.kind === 'junction') && identifier(transport.player) && identifier(transport.origin) && identifier(transport.destination) && positive(transport.amount) && nonnegative(transport.elite) && transport.elite <= transport.amount && record(transport.group) && Object.values(transport.group).every(positive) && Object.values(transport.group).reduce((total, count) => total + count, 0) === transport.amount, 'Automatic advisor conversion lost its public Homeworld transport receipt or module.'); const actors = ctx.players.filter((player) => player.id === transport.player); const actor = actors[0]; requireFlip(actors.length === 1 && actor.shipped === true, 'Automatic advisor conversion lost its completed Homeworld transport slot.'); const places = gameTerritories(ctx); const origin = places.find((place) => place.id === transport.origin); const point = splitLocation(transport.destination); const boardDestination = places.find((place) => transport.destination === `${point.territory}:${point.sector}` && place.id === point.territory && place.sectors.includes(point.sector)); const worldDestination = ctx.players.some((player) => transport.destination === `homeworld:${player.faction}` || (player.faction === 'emperor' && transport.destination === 'homeworld:emperor:salusa')); requireFlip(origin && Object.keys(transport.group).every((key) => { const selected = splitLocation(key); return key === `${selected.territory}:${selected.sector}` && selected.territory === transport.origin && origin.sectors.includes(selected.sector); }) && (source.territory === transport.origin || (boardDestination && source.territory === boardDestination.id)) && (transport.kind === 'native' ? actor.faction === 'guild' && worldDestination : ctx.players.filter((player) => player.faction === 'guild').length === 1 && (worldDestination || !!boardDestination)), 'Automatic advisor conversion needs the original native or Junction Homeworld transport locations.'); if (boardDestination) { const deposited = actor.forces[transport.destination]; requireFlip(nonnegative(deposited) && deposited >= transport.amount, 'Automatic advisor conversion lost the Homeworld transport board deposit.'); } } function validatePublicBattleIdentity( ctx: AutomaticAdvisorFlipContext, receipt: AutomaticAdvisorBattle, turn: number, ) { requireFlip(record(receipt) && Object.keys(receipt).sort().join(',') === 'combatants,event,result,territory,turn,winner' && identifier(receipt.event) && positive(receipt.turn) && receipt.turn === turn && identifier(receipt.territory) && Array.isArray(receipt.combatants) && receipt.combatants.length === 2 && receipt.combatants.every(identifier) && new Set(receipt.combatants).size === 2 && receipt.combatants.every((id) => ctx.players.filter((player) => player.id === id).length === 1) && ['normal', 'traitor', 'mutualTraitors', 'explosion'].includes(receipt.result) && (receipt.result === 'normal' || receipt.result === 'traitor' ? identifier(receipt.winner) && receipt.combatants.includes(receipt.winner) : receipt.winner === null), 'Automatic advisor conversion needs the original public battle result.'); const current = ctx.lastBattleContext; requireFlip(!ctx.battle && record(current) && current.event === receipt.event && current.turn === receipt.turn && current.territory === receipt.territory && current.winner === receipt.winner && current.result === receipt.result && Array.isArray(current.combatants) && current.combatants.length === receipt.combatants.length && current.combatants.every((id, index) => id === receipt.combatants[index]), 'Automatic advisor conversion lost its original completed battle context.'); } function validateBattleSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { validatePublicBattleIdentity(ctx, source.battle, source.turn); requireFlip(source.battle.territory === source.territory && !source.battle.combatants.includes(source.owner) && gameTerritories(ctx).some((place) => place.id === source.battle.territory), 'Automatic advisor conversion needs the original public battle result and bystander group.'); } function validateFaceDanceSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { const receipt = source.faceDance; requireFlip(record(receipt) && Object.keys(receipt).sort().join(',') === 'amount,battle,event,player,returned,sector,sources,territory' && identifier(receipt.event) && identifier(receipt.player) && identifier(receipt.territory) && nonnegative(receipt.sector) && nonnegative(receipt.amount) && record(receipt.sources) && Object.values(receipt.sources).every(positive) && Object.values(receipt.sources).reduce((total, count) => total + count, 0) === receipt.amount && record(receipt.returned) && Object.values(receipt.returned).every(positive) && Object.keys(receipt.returned).every((id) => identifier(id) && ctx.players.filter((player) => player.id === id).length === 1), 'Automatic advisor conversion lost its exact public Face Dance receipt.'); validatePublicBattleIdentity(ctx, receipt.battle, source.turn); const actors = ctx.players.filter((player) => player.id === receipt.player); const actor = actors[0]; requireFlip(actors.length === 1 && actor.faction === 'tleilaxu' && receipt.battle.winner !== null && receipt.player !== receipt.battle.winner && (receipt.battle.result === 'normal' || receipt.battle.result === 'traitor') && receipt.territory === receipt.battle.territory, 'Automatic advisor conversion needs the original other winner and native Face Dance actor.'); const places = gameTerritories(ctx); const destination = places.find((place) => place.id === receipt.territory && place.sectors.includes(receipt.sector)); const worldDestination = receipt.territory === 'homeworld:tleilaxu' && receipt.sector === 0 && record(ctx.homeworlds) && record(ctx.homeworlds.custody); requireFlip((!!destination || worldDestination) && Object.keys(receipt.sources).every((key) => { if (key === 'reserves') return true; const point = splitLocation(key); return key === `${point.territory}:${point.sector}` && places.some((place) => place.id === point.territory && place.sectors.includes(point.sector)); }) && ((!!destination && source.territory === receipt.territory) || Object.keys(receipt.sources).some((key) => key !== 'reserves' && splitLocation(key).territory === source.territory)), 'Automatic advisor conversion must use the Face Dance destination or a positive board source.'); if (destination) { const current = actor.forces[`${receipt.territory}:${receipt.sector}`]; const deposited = current === undefined ? 0 : current; requireFlip(nonnegative(deposited) && deposited >= receipt.amount, 'Automatic advisor conversion lost the Face Dance board replacement.'); } } function validateStormSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { const receipt = source.storm; requireFlip(record(receipt) && Object.keys(receipt).sort().join(',') === 'distance,event,from,sector,traversed' && identifier(receipt.event) && nonnegative(receipt.from) && receipt.from <= 18 && ctx.storm === receipt.from && positive(receipt.distance) && receipt.distance <= 40 && positive(receipt.traversed) && receipt.traversed <= receipt.distance && positive(receipt.sector) && receipt.sector <= 18 && receipt.sector === stormSectorAfter(receipt.from, receipt.traversed), 'Automatic advisor conversion lost its exact public current-sector storm receipt.'); const current = ctx.stormResolution; requireFlip(record(current) && current.from === receipt.from && current.distance === receipt.distance && current.traversed === receipt.traversed && Array.isArray(current.pending) && current.pending.length === 0, 'Automatic advisor conversion must finish the original current-sector storm casualties first.'); const place = gameTerritories(ctx).find((candidate) => candidate.id === source.territory); requireFlip(place && place.sectors.includes(receipt.sector) && stormExposesTerritory(place.id, ctx.shieldWallDestroyed === true), 'Automatic advisor conversion needs its current exposed storm territory.'); } function validateRelocationSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { const receipt = source.relocation; requireFlip(record(receipt) && Object.keys(receipt).sort().join(',') === 'destination,event,kind,origin,player,sector' && identifier(receipt.event) && (receipt.kind === 'ambassador' || receipt.kind === 'discovery') && identifier(receipt.player) && identifier(receipt.origin) && identifier(receipt.destination) && nonnegative(receipt.sector), 'Automatic advisor conversion lost its exact public special-relocation receipt.'); const places = gameTerritories(ctx); requireFlip(places.some((place) => place.id === receipt.origin) && places.some((place) => place.id === receipt.destination && place.sectors.includes(receipt.sector)) && (source.territory === receipt.origin || source.territory === receipt.destination), 'Automatic advisor conversion needs the current special-relocation origin or destination.'); const actors = ctx.players.filter((player) => player.id === receipt.player); requireFlip(actors.length === 1 && presenceAt(actors[0], receipt.destination) > 0, 'Automatic advisor conversion lost the original relocated actor at its destination.'); } function validateWormRideSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { const ride = source.wormRide; requireFlip(record(ride) && Object.keys(ride).sort().join(',') === 'amount,destination,elite,event,origin,player,sector' && identifier(ride.event) && identifier(ride.player) && identifier(ride.origin) && identifier(ride.destination) && ride.origin !== ride.destination && nonnegative(ride.sector) && positive(ride.amount) && nonnegative(ride.elite) && ride.elite <= ride.amount, 'Automatic advisor conversion lost its exact completed worm ride.'); const places = gameTerritories(ctx); requireFlip(places.some(place => place.id === ride.origin) && places.some(place => place.id === ride.destination && place.sectors.includes(ride.sector)) && (source.territory === ride.origin || source.territory === ride.destination), 'Automatic advisor conversion must belong to the original worm ride origin or destination.'); const riders = ctx.players.filter(player => player.id === ride.player); const rider = riders[0], location = `${ride.destination}:${ride.sector}`; const deposited = rider?.forces[location], elite = rider?.elites?.forces[location] ?? 0; requireFlip(riders.length === 1 && rider.faction === 'fremen' && nonnegative(deposited) && nonnegative(elite) && elite <= deposited && elite >= ride.elite && deposited - elite >= ride.amount - ride.elite, 'Automatic advisor conversion lost the original native Fremen ride and deposited forces.'); } function validateSpiritualArrivalSource( ctx: AutomaticAdvisorFlipContext, source: Extract, ) { try { validateSpiritualAdvisorSource(ctx, source.spiritual); } catch (cause) { throw new AutomaticAdvisorFlipError(cause instanceof Error ? cause.message : 'The public Spiritual Advisor source changed.'); } const receipt = source.spiritual; requireFlip(receipt.owner === source.owner && receipt.territory === source.territory, 'Automatic advisor conversion must use the original Spiritual Advisor owner and target.'); const owner = ctx.players.find((player) => player.id === source.owner)!; const deposited = owner.forces[`${receipt.territory}:${receipt.sector}`]; requireFlip(nonnegative(deposited) && deposited >= receipt.amount, 'Automatic advisor conversion lost its completed Spiritual Advisor placement.'); } /** Validate the same committed producer and exact held group before either response outcome. */ export function validateAutomaticAdvisorFlip( ctx: AutomaticAdvisorFlipContext, source: AutomaticAdvisorFlipSource, ): void { requireFlip(record(source) && ((source.producer === 'groundMovement' && identifier(source.mover) && Object.keys(source).sort().join(',') === 'event,forces,move,mover,owner,phase,producer,territory,turn') || (source.producer === 'guildTransport' && Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,territory,transport,turn') || (source.producer === 'homeworldTransport' && Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,territory,transport,turn') || (source.producer === 'gamont' && Object.keys(source).sort().join(',') === 'event,forces,gamont,owner,phase,producer,territory,turn') || (source.producer === 'battleLosses' && Object.keys(source).sort().join(',') === 'battle,event,forces,owner,phase,producer,territory,turn') || (source.producer === 'faceDance' && Object.keys(source).sort().join(',') === 'event,faceDance,forces,owner,phase,producer,territory,turn') || (source.producer === 'storm' && Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,storm,territory,turn') || (source.producer === 'specialRelocation' && Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,relocation,territory,turn') || (source.producer === 'wormRide' && Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,territory,turn,wormRide') || (source.producer === 'spiritualArrival' && Object.keys(source).sort().join(',') === 'event,forces,owner,phase,producer,spiritual,territory,turn')) && identifier(source.event) && identifier(source.owner) && identifier(source.territory) && positive(source.turn) && source.turn === ctx.turn && (source.producer === 'specialRelocation' ? record(source.relocation) && (source.relocation.kind === 'discovery' ? source.phase === 0 : source.relocation.kind === 'ambassador' && [1, 3, 5, 8].includes(source.phase)) : source.producer === 'spiritualArrival' ? [1, 3, 4, 5, 6, 8].includes(source.phase) : source.phase === (source.producer === 'storm' ? 0 : source.producer === 'wormRide' ? 1 : source.producer === 'gamont' ? 8 : source.producer === 'battleLosses' || source.producer === 'faceDance' ? 6 : 5)) && currentWindow(ctx) && source.phase === ctx.phase, 'Automatic advisor conversion lost its original current-turn committed producer.'); const owners = ctx.players.filter((player) => player.id === source.owner); const owner = owners[0]; requireFlip(owners.length === 1 && owner?.faction === 'beneGesserit' && owner.advisors?.[source.territory]?.aloneFlipHold === source.event, 'Automatic advisor conversion needs its original native owner and held occurrence.'); requireFlip(!otherPresence(ctx, owner, source.territory), 'Automatic advisor conversion requires the original group to remain alone.'); if (source.producer === 'groundMovement') { const movers = ctx.players.filter((player) => player.id === source.mover); requireFlip(movers.length === 1 && positive(source.move) && movers[0].moved === source.move, 'Automatic advisor conversion lost its original completed movement.'); } else if (source.producer === 'guildTransport') validateTransportSource(ctx, source); else if (source.producer === 'homeworldTransport') validateHomeworldTransportSource(ctx, source); else if (source.producer === 'gamont') validateGamontSource(ctx, source); else if (source.producer === 'battleLosses') validateBattleSource(ctx, source); else if (source.producer === 'faceDance') validateFaceDanceSource(ctx, source); else if (source.producer === 'storm') validateStormSource(ctx, source); else if (source.producer === 'specialRelocation') validateRelocationSource(ctx, source); else if (source.producer === 'wormRide') validateWormRideSource(ctx, source); else validateSpiritualArrivalSource(ctx, source); const forces = physicalGroup(owner, source.territory); requireFlip(record(source.forces) && Object.keys(forces).length > 0 && Object.values(forces).every(positive) && Object.values(source.forces).every(positive) && Object.keys(forces).sort().join(',') === Object.keys(source.forces).sort().join(',') && Object.entries(forces).every(([key, count]) => source.forces[key] === count), 'Automatic advisor conversion needs the same complete physical advisor group.'); }