Untitled
unknown
typescript
a year ago
5.4 kB
11
Indexable
import type { Player } from "./player";
import { chance } from "../helpers";
import type { RootState } from "../state";
export interface Consideration<TAction> {
name: string;
weight?: number;
evaluate(player: Player, state: RootState, action: TAction): number;
}
type ScoredAction<TAction> = {
action: TAction;
score: number;
}
export abstract class Decision<TAction> {
protected abstract considerations: Consideration<TAction>[];
protected abstract getActions(player: Player, state: RootState): TAction[];
// During evaluation, any option scored as this will be removed from consideration.
public static IMPOSSIBLE_ACTION_SIGNIFYING_SCORE = -Infinity;
public static NoDecisionAvailableError = class extends Error {
constructor(name: string) {
super(`No decisions remained after ${name} evaluation.`);
}
}
public make(player: Player, state: RootState): TAction {
const [best, runnerUp] = this.consider(player, state);
return this.willMakeSuboptimalDecision(player) ? runnerUp : best;
}
private consider(player: Player, state: RootState): [TAction, TAction] {
const [best, runnerUp] = this.evaluateActions(player, state);;
return [best.action, runnerUp.action];
}
private evaluateActions(player: Player, state: RootState): { action: TAction; score: number }[] {
const actions = this.getActions(player, state);
if (actions.length === 1) {
const action = actions[0];
const scoredAction = {
action,
score: Infinity
};
return [scoredAction, scoredAction];
}
const rawEvaluations = actions
.map(action => {
let isPossible = true;
const score = this.considerations.reduce((prev, next) => {
const evaluation = next.evaluate(player, state, action);
if (evaluation === Decision.IMPOSSIBLE_ACTION_SIGNIFYING_SCORE) {
isPossible = false;
}
const weightedScore = evaluation * (next.weight ?? 1);
return prev + weightedScore;
}, 0);
return isPossible
? { action, score }
: null;
})
.filter(scoredAction => scoredAction && scoredAction.score > 0) as ScoredAction<TAction>[];
if (rawEvaluations.length === 0) {
throw new Decision.NoDecisionAvailableError(this.constructor.name);
} else if (rawEvaluations.length === 1) {
const [onlyAction] = rawEvaluations;
return [onlyAction, onlyAction];
}
// Extract scores for normalization
const scores = rawEvaluations.map(({ score }) => score);
const minScore = Math.min(...scores);
const maxScore = Math.max(...scores);
// Normalize evaluations
const normalized = rawEvaluations.map(({ action, score }) => ({
action,
score: this.normalize(score, minScore, maxScore),
}));
const { bestChoice, remaining } = this.sortBySuboptimalCoefficient(player, normalized);
const { bestChoice: nextBestChoice } = this.sortBySuboptimalCoefficient(player, remaining);
if (!bestChoice) {
throw new Decision.NoDecisionAvailableError(this.constructor.name);
}
return [bestChoice, nextBestChoice ?? bestChoice];
}
private normalize(value: number, min: number, max: number): number {
return min === max ? 0.5 : (value - min) / (max - min);
}
private willMakeSuboptimalDecision(player: Player): boolean {
return chance.bool({ likelihood: player.suboptimalDecisionCoefficient * 100 });
}
private sortBySuboptimalCoefficient(
player: Player,
actions: ScoredAction<TAction>[]
): {
bestChoice: ScoredAction<TAction>;
remaining: ScoredAction<TAction>[];
} {
const coefficient = player.suboptimalDecisionCoefficient;
// First, sort by score.
actions.sort((a, b) => b.score - a.score);
if (coefficient >= 0.5) {
// Fully randomized if suboptimalCoefficient >= 0.5
actions.sort(() => Math.random() - 0.5);
} else {
actions.sort((a, b) => {
if (coefficient <= 0) {
// If suboptimalCoefficient is 0, always select the higher score
return b.score - a.score;
} else if (a.score === b.score) {
// Randomize ties
return Math.random() - 0.5;
}
const distance = Math.abs(a.score - b.score);
const distanceModifier = this.calculateDistanceModifier(distance)
const likelihood = coefficient * distanceModifier * 100;
return chance.bool({ likelihood }) ? -1 : 1;
});
}
return {
bestChoice: actions.shift()!,
remaining: actions
};
}
private calculateDistanceModifier(distance: number): number {
return 1 - Math.pow(1 - distance, 2);
}
}Editor is loading...
Leave a Comment