Skip to content

Commit

Permalink
Add the ability to use a different AI for each player.
Browse files Browse the repository at this point in the history
GitOrigin-RevId: af7fc55a9ab791ebbe29bf735634d96b7a9d365a
  • Loading branch information
cpojer committed May 14, 2024
1 parent 4db9562 commit c80d0cc
Show file tree
Hide file tree
Showing 17 changed files with 174 additions and 74 deletions.
28 changes: 20 additions & 8 deletions apollo/actions/executeGameAction.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { hasAI } from '@deities/athena/map/Player.tsx';
import MapData from '@deities/athena/MapData.tsx';
import { VisionT } from '@deities/athena/Vision.tsx';
import { Action, execute, MutateActionResponseFn } from '../Action.tsx';
Expand All @@ -7,19 +8,24 @@ import applyConditions from '../lib/applyConditions.tsx';
import gameHasEnded from '../lib/gameHasEnded.tsx';
import { GameState } from '../Types.tsx';

type AIClass = {
new (effects: Effects): AIType;
};

type AIType = {
act(map: MapData): MapData | null;
retrieveEffects(): Effects;
retrieveGameState(): GameState;
};

export type AIRegistryEntry = Readonly<{
class: {
new (effects: Effects): AIType;
};
name: string;
}>;

export type AIRegistryT = ReadonlyMap<number, AIRegistryEntry>;

export function executeAIAction(
activeMap: MapData | null,
AIClass: AIClass,
AIRegistry: AIRegistryT,
effects: Effects,
gameState: GameState = [],
): [GameState, Effects] {
Expand All @@ -31,6 +37,12 @@ export function executeAIAction(
activeMap.active.length > 1 &&
activeMap.getCurrentPlayer().isBot()
) {
const player = activeMap.getCurrentPlayer();
const AIClass = ((hasAI(player) &&
player.ai != null &&
AIRegistry.get(player.ai)) ||
AIRegistry.get(0))!.class;

const ai = new AIClass(effects);
while (activeMap) {
activeMap = ai.act(activeMap);
Expand Down Expand Up @@ -72,7 +84,7 @@ export default function executeGameAction(
vision: VisionT,
effects: Effects,
action: Action,
AIClass: AIClass | null,
AIRegistry: AIRegistryT | null,
mutateAction?: MutateActionResponseFn,
): [ActionResponse, MapData, GameState, Effects] | [null, null, null, null] {
const actionResult = execute(map, vision, action, mutateAction);
Expand All @@ -91,15 +103,15 @@ export default function executeGameAction(
activeMap = gameState.at(-1)![1];
}
const shouldInvokeAI = !!(
AIClass &&
AIRegistry &&
!gameHasEnded(gameState) &&
(gameState.at(-1)?.[1] || activeMap).getCurrentPlayer().isBot()
);
return [
actionResponse,
activeMap,
...((shouldInvokeAI &&
executeAIAction(activeMap, AIClass, newEffects, gameState)) || [
executeAIAction(activeMap, AIRegistry, newEffects, gameState)) || [
gameState,
newEffects,
]),
Expand Down
6 changes: 6 additions & 0 deletions athena/lib/__tests__/validateTeams.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ test('validates teams', () => {
"name": "",
"players": [
{
"ai": undefined,
"funds": 0,
"id": 1,
"skills": [],
Expand All @@ -87,6 +88,7 @@ test('validates teams', () => {
"name": "",
"players": [
{
"ai": undefined,
"funds": 0,
"id": 2,
"skills": [],
Expand All @@ -101,6 +103,7 @@ test('validates teams', () => {
"name": "",
"players": [
{
"ai": undefined,
"funds": 0,
"id": 3,
"skills": [],
Expand All @@ -115,6 +118,7 @@ test('validates teams', () => {
"name": "",
"players": [
{
"ai": undefined,
"funds": 0,
"id": 4,
"skills": [],
Expand Down Expand Up @@ -153,6 +157,7 @@ test('validates teams', () => {
"name": "",
"players": [
{
"ai": undefined,
"funds": 0,
"id": 1,
"skills": [],
Expand All @@ -167,6 +172,7 @@ test('validates teams', () => {
"name": "",
"players": [
{
"ai": undefined,
"funds": 0,
"id": 2,
"skills": [],
Expand Down
35 changes: 20 additions & 15 deletions athena/lib/validateMap.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import isPositiveInteger from '@deities/hephaestus/isPositiveInteger.tsx';
import ImmutableMap from '@nkzw/immutable-map';
import AIRegistry from '../../dionysus/AIRegistry.tsx';
import {
Behavior,
getBuildingInfo,
Expand All @@ -25,6 +26,7 @@ import {
} from '../map/Configuration.tsx';
import Entity from '../map/Entity.tsx';
import Player, {
hasAI,
PlaceholderPlayer,
PlayerID,
toPlayerID,
Expand Down Expand Up @@ -376,25 +378,28 @@ export default function validateMap(
}

const teams = ImmutableMap(
active.map(
(id) =>
[
active.map((id) => {
const player = map.getPlayer(id);
return [
id,
new Team(
id,
new Team(
id,
'',
ImmutableMap<PlayerID, Player>().set(
'',
ImmutableMap<PlayerID, Player>().set(
toPlayerID(id),
new PlaceholderPlayer(
toPlayerID(id),
new PlaceholderPlayer(
toPlayerID(id),
id,
0,
map.getPlayer(id).skills,
),
id,
0,
hasAI(player) && player.ai != null && AIRegistry.has(player.ai)
? player.ai
: undefined,
player.skills,
),
),
] as const,
),
),
] as const;
}),
);

const newMap = map.copy({
Expand Down
1 change: 1 addition & 0 deletions athena/lib/validateTeams.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export default function validateTeams(
id,
teamId,
0,
undefined,
validateSkills(
{ skillSlots: DefaultMapSkillSlots, skills: Skills },
map.getPlayer(id).skills,
Expand Down
23 changes: 20 additions & 3 deletions athena/map/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@ export type PlainPlayerType = BasePlainPlayerType &

type PlainBotType = BasePlainPlayerType &
Readonly<{
ai?: number;
name: string;
}>;

type PlaceholderPlayerType = Readonly<{
activeSkills?: undefined;
ai?: number;
funds: number;
id: PlayerID;
skills?: ReadonlyArray<Skill>;
Expand Down Expand Up @@ -175,17 +177,20 @@ export class PlaceholderPlayer extends Player {
id: PlayerID,
teamId: PlayerID,
funds: number,
public readonly ai: number | undefined,
skills: ReadonlySet<Skill>,
) {
super(id, teamId, funds, skills, new Set(), 0, null, 0);
}

copy({
ai,
funds,
id,
skills,
teamId,
}: {
ai?: number | undefined;
funds?: number;
id?: PlayerID;
skills?: ReadonlySet<Skill>;
Expand All @@ -195,13 +200,14 @@ export class PlaceholderPlayer extends Player {
id ?? this.id,
teamId ?? this.teamId,
funds ?? this.funds,
ai ?? this.ai,
skills ?? this.skills,
) as this;
}

toJSON(): PlaceholderPlayerType {
const { funds, id, skills } = this;
return { funds, id, skills: [...skills] };
const { ai, funds, id, skills } = this;
return { ai, funds, id, skills: [...skills] };
}

static from(player: Player): PlaceholderPlayer {
Expand All @@ -211,6 +217,7 @@ export class PlaceholderPlayer extends Player {
player.id,
player.teamId,
player.funds,
player.isBot() ? player.ai : undefined,
player.skills,
);
}
Expand All @@ -224,6 +231,7 @@ export class Bot extends Player {
public readonly name: string,
teamId: PlayerID,
funds: number,
public readonly ai: number | undefined,
skills: ReadonlySet<Skill>,
activeSkills: ReadonlySet<Skill>,
charge: number,
Expand All @@ -235,6 +243,7 @@ export class Bot extends Player {

copy({
activeSkills,
ai,
charge,
funds,
id,
Expand All @@ -245,6 +254,7 @@ export class Bot extends Player {
teamId,
}: {
activeSkills?: ReadonlySet<Skill>;
ai?: number | undefined;
charge?: number;
funds?: number;
id?: PlayerID;
Expand All @@ -259,6 +269,7 @@ export class Bot extends Player {
name ?? this.name,
teamId ?? this.teamId,
funds ?? this.funds,
ai ?? this.ai,
skills ?? this.skills,
activeSkills ?? this.activeSkills,
charge ?? this.charge,
Expand All @@ -268,10 +279,11 @@ export class Bot extends Player {
}

toJSON(): PlainBotType {
const { activeSkills, charge, funds, id, misses, name, skills, stats } =
const { activeSkills, ai, charge, funds, id, misses, name, skills, stats } =
this;
return {
activeSkills: [...activeSkills],
ai,
charge,
funds,
id,
Expand All @@ -290,6 +302,7 @@ export class Bot extends Player {
name,
player.teamId,
player.funds,
player.isPlaceholder() ? player.ai : undefined,
player.skills,
player.activeSkills,
player.charge,
Expand Down Expand Up @@ -495,3 +508,7 @@ export function isHumanPlayer(player: Player): player is HumanPlayer {
export function isBot(player: Player): player is Bot {
return player.isBot();
}

export function hasAI(player: Player): player is Bot | PlaceholderPlayer {
return isBot(player) || player.isPlaceholder();
}
9 changes: 8 additions & 1 deletion athena/map/Serialization.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,20 @@ export function decodePlayers(
player.name,
teamId,
player.funds,
player.ai,
skills,
activeSkills,
player.charge || 0,
decodePlayerStatistics(player.stats),
player.misses || 0,
)
: new PlaceholderPlayer(playerID, teamId, player.funds, skills),
: new PlaceholderPlayer(
playerID,
teamId,
player.funds,
player.ai,
skills,
),
);
}),
);
Expand Down
8 changes: 8 additions & 0 deletions dionysus/AIRegistry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AIRegistryT } from '@deities/apollo/actions/executeGameAction.tsx';
import DionysusAlpha from './DionysusAlpha.tsx';

const AIRegistry: AIRegistryT = new Map([
[0, { class: DionysusAlpha, name: 'Alpha' }],
]);

export default AIRegistry;
4 changes: 2 additions & 2 deletions hera/hooks/useClientGameAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import dropLabelsFromGameState from '@deities/apollo/lib/dropLabelsFromGameState
import { GameActionResponse, GameState } from '@deities/apollo/Types.tsx';
import MapData from '@deities/athena/MapData.tsx';
import { getHiddenLabels } from '@deities/athena/WinConditions.tsx';
import DionysusAlpha from '@deities/dionysus/DionysusAlpha.tsx';
import AIRegistry from '@deities/dionysus/AIRegistry.tsx';
import onGameEnd from '@deities/hermes/game/onGameEnd.tsx';
import toClientGame, {
ClientGame,
Expand Down Expand Up @@ -52,7 +52,7 @@ export default function useClientGameAction(
vision,
game.effects,
action,
DionysusAlpha,
AIRegistry,
mutateAction,
) || [null, null, null];
} catch (error) {
Expand Down
Loading

0 comments on commit c80d0cc

Please sign in to comment.