Untitled
unknown
typescript
4 months ago
5.4 kB
3
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