From 9b92d97717eb971f2349ac329c2a479d27c4b258 Mon Sep 17 00:00:00 2001 From: Fen Dweller Date: Tue, 11 Aug 2020 08:28:04 -0400 Subject: [PATCH] Start working on a new AI system The AI uses a collection of Decider objects to assign weights to each possible action. --- src/game/ai.ts | 87 ++++++++++++++++++++++++++++++++--------- src/game/ai/deciders.ts | 39 ++++++++++++++++++ 2 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 src/game/ai/deciders.ts diff --git a/src/game/ai.ts b/src/game/ai.ts index d9e63a2..cc921a4 100644 --- a/src/game/ai.ts +++ b/src/game/ai.ts @@ -1,7 +1,8 @@ import { Creature } from './creature' -import { Encounter } from './combat' -import { LogEntry } from './interface' +import { Encounter, Action } from './combat' +import { LogEntry, nilLog } from './interface' import { ReleaseAction, TransferAction, PassAction } from './combat/actions' +import { NoPassDecider, NoReleaseDecider, ChanceDecider } from './ai/deciders' export interface AI { name: string; @@ -15,6 +16,62 @@ export class NoAI implements AI { } } +/** + * A Decider determines how favorable an action is to perform. + */ +export interface Decider { + decide: (encounter: Encounter, user: Creature, target: Creature, action: Action) => number +} + +export class DeciderAI implements AI { + constructor (public name: string, private deciders: Decider[]) { + + } + + decide(actor: Creature, encounter: Encounter): LogEntry { + const options = encounter.combatants.flatMap(enemy => actor.validActions(enemy).map(action => ({ + target: enemy, + action: action, + weight: 1 + }))) + + console.log(options) + + this.deciders.forEach( + decider => options.forEach( + option => option.weight *= decider.decide(encounter, actor, option.target, option.action) + ) + ) + + let total = options.reduce( + (sum: number, option) => sum + option.weight, + 0 + ) + + total *= Math.random() + console.log(total) + + const chosen = options.find ( + option => { + if (total < option.weight) { + return true + } else { + total -= option.weight + return false + } + } + ) + + if (chosen !== undefined) { + return chosen.action.try(actor, chosen.target) + } + + // should never reach this point! + + throw new Error("Couldn't pick an action") + } +} + /** * The RandomAI is **COMPLETELY** random. Good luck. */ @@ -33,21 +90,15 @@ export class RandomAI implements AI { /** * The VoreAI tries to eat opponents, but only if the odds are good enough */ -export class VoreAI extends RandomAI { - name = "Vore AI" - decide (actor: Creature, encounter: Encounter): LogEntry { - const actions = encounter.combatants.flatMap(enemy => actor.validActions(enemy).map(action => ({ - target: enemy, - action: action - }))) - const voreActions = actions.filter(action => actor.otherContainers.concat(actor.containers).some(container => container.actions.includes(action.action) || action instanceof TransferAction)) - const aggressiveActions = voreActions.filter(action => !(action.action instanceof ReleaseAction) && !(action.action instanceof PassAction)) - const likelyActions = aggressiveActions.filter(action => action.action.odds(actor, action.target) > 0.4) - likelyActions.forEach(action => console.log(action.action.odds(actor, action.target))) - const chosen = likelyActions[Math.floor(Math.random() * likelyActions.length)] - if (chosen === undefined) { - return super.decide(actor, encounter) - } - return chosen.action.try(actor, chosen.target) +export class VoreAI extends DeciderAI { + constructor () { + super ( + "Vore AI", + [ + new NoPassDecider(), + new NoReleaseDecider(), + new ChanceDecider() + ] + ) } } diff --git a/src/game/ai/deciders.ts b/src/game/ai/deciders.ts new file mode 100644 index 0000000..02553fc --- /dev/null +++ b/src/game/ai/deciders.ts @@ -0,0 +1,39 @@ +import { Decider } from '../ai' +import { Encounter, Action } from '../combat' +import { Creature } from '../creature' +import { PassAction, ReleaseAction } from '../combat/actions' + +/** + * Specifically avoids using a [[PassAction]] + */ +export class NoPassDecider implements Decider { + decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { + if (action instanceof PassAction) { + return 0 + } else { + return 1 + } + } +} + +/** + * Specifically avoids using a [[ReleaseAction]] + */ +export class NoReleaseDecider implements Decider { + decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { + if (action instanceof ReleaseAction) { + return 0 + } else { + return 1 + } + } +} + +/** + * Weights actions based on how likely they are to succeed + */ +export class ChanceDecider implements Decider { + decide (encounter: Encounter, user: Creature, target: Creature, action: Action): number { + return action.odds(user, target) + } +}