113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import type { Action, GameView } from './engine';
|
|
import type {
|
|
DiscoveryEntryOffer,
|
|
DiscoveryEntryNoField,
|
|
DiscoveryEntrySource,
|
|
} from './discovery-entry';
|
|
import { splitLocation } from './board';
|
|
|
|
function entryDecision(game: GameView) {
|
|
const decision = game.decision;
|
|
return decision?.kind === 'discoveryEntry' ? decision : null;
|
|
}
|
|
|
|
function currentEntry(game: GameView) {
|
|
const decision = entryDecision(game),
|
|
offer = game.discoveryEntry;
|
|
if (
|
|
game.status !== 'playing' ||
|
|
!decision ||
|
|
decision.player !== game.me ||
|
|
!offer ||
|
|
offer.owner !== game.me ||
|
|
offer.event !== decision.event ||
|
|
offer.blocked
|
|
)
|
|
return null;
|
|
return { decision, offer };
|
|
}
|
|
|
|
function whole(value: unknown): value is number {
|
|
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
}
|
|
|
|
function selectedGroups(
|
|
offer: DiscoveryEntryOffer,
|
|
groups: readonly DiscoveryEntrySource[],
|
|
allowEmpty = false,
|
|
): DiscoveryEntrySource[] | null {
|
|
if (!Array.isArray(groups) || (!groups.length && !allowEmpty)) return null;
|
|
const selected = groups
|
|
.map((group) => ({ ...group }))
|
|
.sort((a, b) => a.source.localeCompare(b.source));
|
|
if (
|
|
new Set(selected.map((group) => group.source)).size !== selected.length ||
|
|
selected.some((group) => {
|
|
const available = offer.sources.find(
|
|
(source) => source.source === group.source,
|
|
);
|
|
return (
|
|
Object.keys(group).sort().join(',') !== 'elite,normal,source' ||
|
|
!available ||
|
|
!whole(group.normal) ||
|
|
!whole(group.elite) ||
|
|
group.normal + group.elite <= 0 ||
|
|
group.normal > available.normal ||
|
|
group.elite > available.elite
|
|
);
|
|
})
|
|
)
|
|
return null;
|
|
return selected;
|
|
}
|
|
|
|
export function discoveryEntryMoveAction(
|
|
game: GameView,
|
|
groups: readonly DiscoveryEntrySource[],
|
|
noField?: DiscoveryEntryNoField,
|
|
): Action | null {
|
|
const entry = currentEntry(game);
|
|
if (!entry) return null;
|
|
if (noField !== undefined) {
|
|
const offered = entry.offer.noField;
|
|
if (!noField || typeof noField !== 'object' || Array.isArray(noField) ||
|
|
Object.keys(noField).sort().join(',') !== 'event,source,tokenId' ||
|
|
typeof noField.tokenId !== 'string' || !noField.tokenId.trim() ||
|
|
typeof noField.event !== 'string' || !noField.event.trim() ||
|
|
typeof noField.source !== 'string' ||
|
|
!offered || noField.tokenId !== offered.tokenId || noField.event !== offered.event ||
|
|
noField.source !== offered.source) return null;
|
|
const source = splitLocation(noField.source);
|
|
if (source.territory !== entry.offer.parent || noField.source !== `${source.territory}:${source.sector}`)
|
|
return null;
|
|
}
|
|
const selected = selectedGroups(entry.offer, groups, noField !== undefined);
|
|
return selected
|
|
? {
|
|
type: 'decision',
|
|
event: entry.decision.event,
|
|
accept: true,
|
|
groups: selected,
|
|
...(noField ? { noField: noField.tokenId, noFieldEvent: noField.event } : {}),
|
|
}
|
|
: null;
|
|
}
|
|
|
|
export function discoveryEntryDeclineAction(
|
|
game: GameView,
|
|
): Action | null {
|
|
const entry = currentEntry(game);
|
|
return entry
|
|
? { type: 'decision', event: entry.decision.event, accept: false }
|
|
: null;
|
|
}
|
|
|
|
/** Uses only the current player's projected groups and offered owned marker. */
|
|
export function discoveryEntryBotActions(game: GameView): Action[] {
|
|
const offer = game.discoveryEntry;
|
|
if (!currentEntry(game) || !offer) return [];
|
|
const move = discoveryEntryMoveAction(game, offer.sources, offer.noField),
|
|
decline = discoveryEntryDeclineAction(game);
|
|
return [move, decline].filter((action): action is Action => action !== null);
|
|
}
|