diff --git a/packages/logic/src/index.ts b/packages/logic/src/index.ts index 6d6cccb..ef09579 100644 --- a/packages/logic/src/index.ts +++ b/packages/logic/src/index.ts @@ -1,2 +1,4 @@ export * from './domain/entities'; +export * from './ports/rng'; export * from './ports/world'; +export * from './triggers'; diff --git a/packages/logic/src/ports/rng.ts b/packages/logic/src/ports/rng.ts new file mode 100644 index 0000000..44871de --- /dev/null +++ b/packages/logic/src/ports/rng.ts @@ -0,0 +1,5 @@ +export interface RandomGenerator { + nextFloat(): number; + nextBool(probability: number): boolean; + nextInt(minInclusive: number, maxExclusive: number): number; +} diff --git a/packages/logic/src/triggers/core.ts b/packages/logic/src/triggers/core.ts new file mode 100644 index 0000000..17f543b --- /dev/null +++ b/packages/logic/src/triggers/core.ts @@ -0,0 +1,78 @@ +export const TriggerPriority = { + Min: 0, + Begin: 10000, + Pre: 20000, + Body: 30000, + Post: 40000, + Final: 50000, +} as const; + +export type TriggerPriority = (typeof TriggerPriority)[keyof typeof TriggerPriority]; + +export interface Trigger, TArg = unknown> { + readonly priority: number; + readonly uniqueId: string; + action(context: TContext, env: TEnv, arg?: TArg): TEnv; +} + +export class TriggerCaller, TArg = unknown> { + private readonly triggersByPriority = new Map>>(); + + constructor(...triggers: Array>) { + if (triggers.length === 0) { + return; + } + + for (const trigger of triggers) { + this.append(trigger); + } + } + + isEmpty(): boolean { + return this.triggersByPriority.size === 0; + } + + append(trigger: Trigger): this { + const priority = trigger.priority; + const uniqueId = trigger.uniqueId; + const bucket = this.triggersByPriority.get(priority) ?? new Map(); + + bucket.set(uniqueId, trigger); + this.triggersByPriority.set(priority, bucket); + + return this; + } + + merge(other: TriggerCaller | null | undefined): this { + if (!other) { + return this; + } + + for (const [priority, otherBucket] of other.triggersByPriority.entries()) { + const bucket = this.triggersByPriority.get(priority) ?? new Map(); + for (const [uniqueId, trigger] of otherBucket.entries()) { + bucket.set(uniqueId, trigger); + } + this.triggersByPriority.set(priority, bucket); + } + + return this; + } + + fire(context: TContext, env: TEnv, arg?: TArg): TEnv { + let currentEnv = env; + const priorities = Array.from(this.triggersByPriority.keys()).sort((a, b) => a - b); + + for (const priority of priorities) { + const bucket = this.triggersByPriority.get(priority); + if (!bucket) { + continue; + } + for (const trigger of bucket.values()) { + currentEnv = trigger.action(context, currentEnv, arg); + } + } + + return currentEnv; + } +} diff --git a/packages/logic/src/triggers/general-action.ts b/packages/logic/src/triggers/general-action.ts new file mode 100644 index 0000000..63466f6 --- /dev/null +++ b/packages/logic/src/triggers/general-action.ts @@ -0,0 +1,175 @@ +import type { GeneralTriggerState } from '../domain/entities'; +import { type GeneralActionContext, GeneralTriggerCaller } from './general'; + +export interface GeneralActionModule { + getName?(): string; + getInfo?(): string; + + getPreTurnExecuteTriggerList?( + context: GeneralActionContext + ): GeneralTriggerCaller | null; + + onCalcDomestic?( + context: GeneralActionContext, + turnType: string, + varType: string, + value: number, + aux?: unknown + ): number; + + onCalcStat?( + context: GeneralActionContext, + statName: string, + value: number, + aux?: unknown + ): number; + + onCalcOpposeStat?( + context: GeneralActionContext, + statName: string, + value: number, + aux?: unknown + ): number; + + onCalcStrategic?( + context: GeneralActionContext, + turnType: string, + varType: string, + value: number + ): number; + + onCalcNationalIncome?( + context: GeneralActionContext, + type: string, + amount: number + ): number; + + onArbitraryAction?( + context: GeneralActionContext, + actionType: string, + phase?: string | null, + aux?: Record | null + ): Record | null; +} + +export class GeneralActionPipeline { + private readonly modules: GeneralActionModule[]; + + constructor(modules: Array | null | undefined>) { + this.modules = modules.filter(Boolean) as GeneralActionModule[]; + } + + getPreTurnExecuteTriggerList( + context: GeneralActionContext + ): GeneralTriggerCaller { + const triggerCaller = new GeneralTriggerCaller(); + + for (const module of this.modules) { + const triggers = module.getPreTurnExecuteTriggerList?.(context); + if (triggers) { + triggerCaller.merge(triggers); + } + } + + return triggerCaller; + } + + onCalcDomestic( + context: GeneralActionContext, + turnType: string, + varType: string, + value: number, + aux?: unknown + ): number { + let current = value; + for (const module of this.modules) { + if (!module.onCalcDomestic) { + continue; + } + current = module.onCalcDomestic(context, turnType, varType, current, aux); + } + return current; + } + + onCalcStat( + context: GeneralActionContext, + statName: string, + value: number, + aux?: unknown + ): number { + let current = value; + for (const module of this.modules) { + if (!module.onCalcStat) { + continue; + } + current = module.onCalcStat(context, statName, current, aux); + } + return current; + } + + onCalcOpposeStat( + context: GeneralActionContext, + statName: string, + value: number, + aux?: unknown + ): number { + let current = value; + for (const module of this.modules) { + if (!module.onCalcOpposeStat) { + continue; + } + current = module.onCalcOpposeStat(context, statName, current, aux); + } + return current; + } + + onCalcStrategic( + context: GeneralActionContext, + turnType: string, + varType: string, + value: number + ): number { + let current = value; + for (const module of this.modules) { + if (!module.onCalcStrategic) { + continue; + } + current = module.onCalcStrategic(context, turnType, varType, current); + } + return current; + } + + onCalcNationalIncome( + context: GeneralActionContext, + type: string, + amount: number + ): number { + let current = amount; + for (const module of this.modules) { + if (!module.onCalcNationalIncome) { + continue; + } + current = module.onCalcNationalIncome(context, type, current); + } + return current; + } + + onArbitraryAction( + context: GeneralActionContext, + actionType: string, + phase?: string | null, + aux?: Record | null + ): Record | null { + let current = aux ?? null; + for (const module of this.modules) { + if (!module.onArbitraryAction) { + continue; + } + const result = module.onArbitraryAction(context, actionType, phase, current); + if (result !== undefined) { + current = result; + } + } + return current; + } +} diff --git a/packages/logic/src/triggers/general.ts b/packages/logic/src/triggers/general.ts new file mode 100644 index 0000000..224c537 --- /dev/null +++ b/packages/logic/src/triggers/general.ts @@ -0,0 +1,80 @@ +import type { General, GeneralTriggerState } from '../domain/entities'; +import type { RandomGenerator } from '../ports/rng'; +import type { WorldStateRepository } from '../ports/world'; +import { TriggerCaller, type Trigger } from './core'; + +export interface GeneralActionLogSink { + push(message: string): void; +} + +export interface GeneralSkillActivation { + has(key: string): boolean; + activate(...keys: string[]): void; +} + +export const createGeneralSkillActivation = < + TriggerState extends GeneralTriggerState +>( + general: General +): GeneralSkillActivation => ({ + has: (key: string) => Boolean(general.triggerState.flags[key]), + activate: (...keys: string[]) => { + for (const key of keys) { + general.triggerState.flags[key] = true; + } + }, +}); + +export interface GeneralActionContext { + general: General; + world?: WorldStateRepository; + log?: GeneralActionLogSink; + rng?: RandomGenerator; +} + +export interface GeneralTriggerContext + extends GeneralActionContext { + rng: RandomGenerator; + skill: GeneralSkillActivation; +} + +export const createGeneralTriggerContext = < + TriggerState extends GeneralTriggerState +>( + context: GeneralActionContext & { rng: RandomGenerator } +): GeneralTriggerContext => ({ + ...context, + skill: createGeneralSkillActivation(context.general), +}); + +export type GeneralTrigger< + TriggerState extends GeneralTriggerState = GeneralTriggerState, + Env extends Record = Record, + Arg = unknown +> = Trigger, Env, Arg>; + +export class GeneralTriggerCaller< + TriggerState extends GeneralTriggerState = GeneralTriggerState, + Env extends Record = Record, + Arg = unknown +> extends TriggerCaller, Env, Arg> {} + +export abstract class BaseGeneralTrigger< + TriggerState extends GeneralTriggerState = GeneralTriggerState, + Env extends Record = Record, + Arg = unknown +> implements GeneralTrigger { + public abstract readonly priority: number; + + protected constructor(protected readonly general: General) {} + + get uniqueId(): string { + return `${this.priority}_${this.constructor.name}_${this.general.id}`; + } + + abstract action( + context: GeneralTriggerContext, + env: Env, + arg?: Arg + ): Env; +} diff --git a/packages/logic/src/triggers/index.ts b/packages/logic/src/triggers/index.ts new file mode 100644 index 0000000..5b8f1f2 --- /dev/null +++ b/packages/logic/src/triggers/index.ts @@ -0,0 +1,3 @@ +export * from './core'; +export * from './general'; +export * from './general-action';