refactor: enforce package boundaries
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"@prisma/adapter-pg": "^7.2.0",
|
||||
"@prisma/client": "^7.2.0",
|
||||
"@prisma/client-runtime-utils": "^7.2.0",
|
||||
"@sammo-ts/logic": "workspace:*",
|
||||
"es-toolkit": "^1.43.0",
|
||||
"pg": "^8.16.3",
|
||||
"redis": "^5.10.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PrismaClient as GamePrismaClient } from '../prisma/generated/game/index.js';
|
||||
export { LogCategory, LogScope, Prisma as GamePrisma } from '../prisma/generated/game/index.js';
|
||||
export { Prisma as GamePrisma } from '../prisma/generated/game/index.js';
|
||||
export type { PrismaClient as GamePrismaClient } from '../prisma/generated/game/index.js';
|
||||
|
||||
import type { PostgresConfig, PostgresConnector } from './postgres.js';
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { PrismaClient as GatewayPrismaClient } from '../prisma/generated/gateway/index.js';
|
||||
export {
|
||||
GatewayBuildStatus,
|
||||
GatewayProfileStatus,
|
||||
OAuthType,
|
||||
Prisma as GatewayPrisma,
|
||||
} from '../prisma/generated/gateway/index.js';
|
||||
export { Prisma as GatewayPrisma } from '../prisma/generated/gateway/index.js';
|
||||
export type { PrismaClient as GatewayPrismaClient } from '../prisma/generated/gateway/index.js';
|
||||
|
||||
import type { PostgresConfig, PostgresConnector } from './postgres.js';
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
export * from './postgres.js';
|
||||
export { createGamePostgresConnector, GamePrisma, LogCategory, LogScope } from './gamePrisma.js';
|
||||
export { createGamePostgresConnector, GamePrisma } from './gamePrisma.js';
|
||||
export type { GamePrismaClient } from './gamePrisma.js';
|
||||
export {
|
||||
createGatewayPostgresConnector,
|
||||
GatewayBuildStatus,
|
||||
GatewayProfileStatus,
|
||||
GatewayPrisma,
|
||||
OAuthType,
|
||||
} from './gatewayPrisma.js';
|
||||
export { createGatewayPostgresConnector, GatewayPrisma } from './gatewayPrisma.js';
|
||||
export type { GatewayPrismaClient } from './gatewayPrisma.js';
|
||||
export * from './db.js';
|
||||
export * from './errorLogRepository.js';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { LogCategory, LogScope, type GamePrisma, type GamePrismaClient } from './gamePrisma.js';
|
||||
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||
|
||||
import type { GamePrisma, GamePrismaClient } from './gamePrisma.js';
|
||||
|
||||
export interface LogQueryOptions {
|
||||
limit?: number;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { GamePrisma, LogCategory, LogScope } from './gamePrisma.js';
|
||||
import type { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||
|
||||
import type { GamePrisma } from './gamePrisma.js';
|
||||
|
||||
export type JsonValue = GamePrisma.JsonValue;
|
||||
export type InputJsonValue = GamePrisma.InputJsonValue;
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
"outDir": "dist",
|
||||
"composite": true
|
||||
},
|
||||
"include": ["src", "test", "*.ts"]
|
||||
"include": ["src", "test", "*.ts"],
|
||||
"references": [{ "path": "../logic" }]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type { NationTraitModule } from '@sammo-ts/logic/actionModules/traits/nation/index.js';
|
||||
import type { RefOrderedActionStack } from '@sammo-ts/logic/actionModules/bundle.js';
|
||||
import type { ScenarioEffectKey } from '@sammo-ts/logic/scenario/scenarioEffect.js';
|
||||
import type { TracePort } from '@sammo-ts/logic/ports/trace.js';
|
||||
|
||||
export interface TurnCommandItemCatalogEntry {
|
||||
slot: 'horse' | 'weapon' | 'book' | 'item';
|
||||
@@ -17,6 +18,7 @@ export interface TurnCommandItemCatalogEntry {
|
||||
}
|
||||
|
||||
export interface TurnCommandEnv {
|
||||
trace?: TracePort;
|
||||
unitSet?: UnitSetDefinition;
|
||||
scenarioEffect?: ScenarioEffectKey | null;
|
||||
develCost: number;
|
||||
|
||||
@@ -124,20 +124,26 @@ export class ActionDefinition<
|
||||
|
||||
const generalCount = Math.max(context.nationGeneralCount, this.env.initialNationGenLimit);
|
||||
if (
|
||||
(process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []).includes(String(context.general.id)) ||
|
||||
(process.env.CORE_AI_TRACE_NATION_IDS?.split(',') ?? []).includes(String(context.nation.id))
|
||||
this.env.trace?.isEnabled('AI_ACTION_PATCH_TRACE', {
|
||||
generalIds: [context.general.id],
|
||||
nationIds: [context.nation.id],
|
||||
})
|
||||
) {
|
||||
process.stdout.write(
|
||||
`AI_ACTION_PATCH_TRACE ${JSON.stringify({ engine: 'core-tech', generalId: context.general.id, nationId: context.nation.id, currentTech, techScore, nationGeneralCount: context.nationGeneralCount, generalCount, delta: techScore / generalCount })}\n`
|
||||
);
|
||||
this.env.trace.write('AI_ACTION_PATCH_TRACE', {
|
||||
engine: 'core-tech',
|
||||
generalId: context.general.id,
|
||||
nationId: context.nation.id,
|
||||
currentTech,
|
||||
techScore,
|
||||
nationGeneralCount: context.nationGeneralCount,
|
||||
generalCount,
|
||||
delta: techScore / generalCount,
|
||||
});
|
||||
}
|
||||
|
||||
context.nation.meta = {
|
||||
...context.nation.meta,
|
||||
tech: addLegacyStoredTech(
|
||||
currentTech,
|
||||
techScore / generalCount
|
||||
),
|
||||
tech: addLegacyStoredTech(currentTech, techScore / generalCount),
|
||||
};
|
||||
context.general.gold = Math.max(0, context.general.gold - result.costGold);
|
||||
context.general.experience += result.exp;
|
||||
|
||||
@@ -39,6 +39,7 @@ import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/typ
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||
import { buildNationFrontStatePatches } from '../../../diplomacy/frontState.js';
|
||||
import type { TracePort } from '../../../ports/trace.js';
|
||||
import { formatDestCityConstraintFailure } from '../constraintFailure.js';
|
||||
import {
|
||||
buildWarAftermathConfig,
|
||||
@@ -380,7 +381,8 @@ export class ActionDefinition<
|
||||
constructor(
|
||||
modules: ReadonlyArray<WarActionModule<TriggerState> | null | undefined> = [],
|
||||
nationTraitModules: NationTraitModule[] = [],
|
||||
generalModules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined> = []
|
||||
generalModules: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined> = [],
|
||||
private readonly trace?: TracePort
|
||||
) {
|
||||
this.warModules = modules.filter(Boolean) as ReadonlyArray<WarActionModule<TriggerState>>;
|
||||
this.nationTraitModules = new Map(nationTraitModules.map((module) => [module.key, module]));
|
||||
@@ -572,29 +574,25 @@ export class ActionDefinition<
|
||||
(unitSet.crewTypes?.some((crewType) => crewType.id === general.crewTypeId) ?? false)
|
||||
)
|
||||
);
|
||||
const traceGeneralIds = new Set(process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []);
|
||||
const shouldTraceWar =
|
||||
traceGeneralIds.has(String(context.general.id)) ||
|
||||
defenderGenerals.some((general) => traceGeneralIds.has(String(general.id)));
|
||||
const battleGeneralIds = [context.general.id, ...defenderGenerals.map((general) => general.id)];
|
||||
const shouldTraceWar = this.trace?.isEnabled('AI_WAR_TRACE', { generalIds: battleGeneralIds }) ?? false;
|
||||
|
||||
if (process.env.CORE_BATTLE_FIXTURE_TRACE === '1') {
|
||||
process.stdout.write(
|
||||
`AI_WAR_FIXTURE_CORE ${JSON.stringify({
|
||||
action: 'battle',
|
||||
seed,
|
||||
repeatCnt: 1,
|
||||
year: time.year,
|
||||
month: time.month,
|
||||
startYear: time.startYear,
|
||||
scenarioEffect: null,
|
||||
attackerGeneral: buildBattleGeneralFixture(context.general),
|
||||
attackerCity: buildBattleCityFixture(attackerCity),
|
||||
attackerNation: buildBattleNationFixture(attackerNation),
|
||||
defenderGenerals: defenderGenerals.map(buildBattleGeneralFixture),
|
||||
defenderCity: buildBattleCityFixture(defenderCity),
|
||||
defenderNation: buildBattleNationFixture(defenderNation),
|
||||
})}\n`
|
||||
);
|
||||
if (this.trace?.isEnabled('AI_WAR_FIXTURE_CORE', { generalIds: battleGeneralIds })) {
|
||||
this.trace.write('AI_WAR_FIXTURE_CORE', {
|
||||
action: 'battle',
|
||||
seed,
|
||||
repeatCnt: 1,
|
||||
year: time.year,
|
||||
month: time.month,
|
||||
startYear: time.startYear,
|
||||
scenarioEffect: null,
|
||||
attackerGeneral: buildBattleGeneralFixture(context.general),
|
||||
attackerCity: buildBattleCityFixture(attackerCity),
|
||||
attackerNation: buildBattleNationFixture(attackerNation),
|
||||
defenderGenerals: defenderGenerals.map(buildBattleGeneralFixture),
|
||||
defenderCity: buildBattleCityFixture(defenderCity),
|
||||
defenderNation: buildBattleNationFixture(defenderNation),
|
||||
});
|
||||
}
|
||||
|
||||
const battle = resolveWarBattle({
|
||||
@@ -619,9 +617,7 @@ export class ActionDefinition<
|
||||
...(shouldTraceWar
|
||||
? {
|
||||
trace: (event) => {
|
||||
process.stdout.write(
|
||||
`AI_WAR_TRACE ${JSON.stringify({ generalId: context.general.id, event })}\n`
|
||||
);
|
||||
this.trace?.write('AI_WAR_TRACE', { generalId: context.general.id, event });
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@@ -648,6 +644,7 @@ export class ActionDefinition<
|
||||
baseGain
|
||||
);
|
||||
},
|
||||
...(this.trace ? { trace: this.trace } : {}),
|
||||
});
|
||||
|
||||
// Ref ConquerCity() recalculates the fronts of every nation around the
|
||||
@@ -802,5 +799,10 @@ export const commandSpec: GeneralTurnCommandSpec = {
|
||||
availabilityArgs: { destCityId: 0 },
|
||||
argsSchema: ARGS_SCHEMA,
|
||||
createDefinition: (env: TurnCommandEnv) =>
|
||||
new ActionDefinition(env.warActionModules ?? [], env.nationTraitModules ?? [], env.generalActionModules ?? []),
|
||||
new ActionDefinition(
|
||||
env.warActionModules ?? [],
|
||||
env.nationTraitModules ?? [],
|
||||
env.generalActionModules ?? [],
|
||||
env.trace
|
||||
),
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ export * from './inheritance/inheritBuff.js';
|
||||
export * from './resources/index.js';
|
||||
export * from './ports/world.js';
|
||||
export * from './ports/worldSnapshot.js';
|
||||
export * from './ports/trace.js';
|
||||
export * from './scenario/index.js';
|
||||
export * from './triggers/index.js';
|
||||
export * from './turn/index.js';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface TraceSubject {
|
||||
generalIds?: readonly number[];
|
||||
nationIds?: readonly number[];
|
||||
}
|
||||
|
||||
export type TraceEvent = 'AI_ACTION_PATCH_TRACE' | 'AI_WAR_TRACE' | 'AI_WAR_FIXTURE_CORE' | 'WAR_TECH_TRACE';
|
||||
|
||||
/**
|
||||
* 도메인 계산이 환경 변수나 stdout에 직접 의존하지 않도록 런타임이 주입하는
|
||||
* 진단 포트입니다. event 이름은 기존 비교 도구가 소비하는 출력 prefix입니다.
|
||||
*/
|
||||
export interface TracePort {
|
||||
isEnabled(event: TraceEvent, subject?: TraceSubject): boolean;
|
||||
write(event: TraceEvent, payload: unknown): void;
|
||||
}
|
||||
@@ -134,10 +134,21 @@ const applyNationTechGain = <TriggerState extends GeneralTriggerState>(
|
||||
// arithmetic starts from the stored binary32 value without a PHP text read.
|
||||
nation.meta.tech = Math.fround(currentTech + delta);
|
||||
// REF-COMPAT:END ref-mariadb-float-boundary
|
||||
if ((process.env.CORE_WAR_TECH_TRACE_NATION_IDS?.split(',') ?? []).includes(String(nation.id))) {
|
||||
process.stdout.write(
|
||||
`WAR_TECH_TRACE ${JSON.stringify({ engine: 'core', nationId: nation.id, side: context.side, currentTech, baseGain, gain, total, effective, divisor, delta, storedTech: nation.meta.tech, attackerGeneralId: context.attackerReport.id })}\n`
|
||||
);
|
||||
if (input.trace?.isEnabled('WAR_TECH_TRACE', { nationIds: [nation.id] })) {
|
||||
input.trace.write('WAR_TECH_TRACE', {
|
||||
engine: 'core',
|
||||
nationId: nation.id,
|
||||
side: context.side,
|
||||
currentTech,
|
||||
baseGain,
|
||||
gain,
|
||||
total,
|
||||
effective,
|
||||
divisor,
|
||||
delta,
|
||||
storedTech: nation.meta.tech,
|
||||
attackerGeneralId: context.attackerReport.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic
|
||||
import type { GeneralActionModule } from '@sammo-ts/logic/actionModules/general.js';
|
||||
import type { ActionLogger } from '@sammo-ts/logic/logging/actionLogger.js';
|
||||
import type { LogEntryDraft } from '@sammo-ts/logic/logging/types.js';
|
||||
import type { TracePort } from '@sammo-ts/logic/ports/trace.js';
|
||||
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type { WarActionModule } from './actions.js';
|
||||
import type { WarTriggerRegistry } from './triggers.js';
|
||||
@@ -194,6 +195,7 @@ export interface WarAftermathInput<TriggerState extends GeneralTriggerState = Ge
|
||||
rng?: RandUtil;
|
||||
generalActionModules?: ReadonlyArray<GeneralActionModule<TriggerState> | null | undefined>;
|
||||
calcNationTechGain?: (context: WarAftermathTechContext) => number;
|
||||
trace?: TracePort;
|
||||
}
|
||||
|
||||
export interface WarAftermathOutcome<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
|
||||
|
||||
@@ -1,5 +1,97 @@
|
||||
import type { City } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
|
||||
const buildConnectionMap = (map: MapDefinition): Map<number, number[]> => {
|
||||
const result = new Map<number, number[]>();
|
||||
for (const city of map.cities) {
|
||||
result.set(city.id, city.connections ?? []);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const searchAllDistanceByCityList = (
|
||||
map: MapDefinition,
|
||||
cityIds: number[]
|
||||
): Record<number, Record<number, number>> => {
|
||||
if (cityIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const connectionMap = buildConnectionMap(map);
|
||||
const citySet = new Set(cityIds);
|
||||
const result: Record<number, Record<number, number>> = {};
|
||||
|
||||
for (const startId of citySet) {
|
||||
const distances: Record<number, number> = { [startId]: 0 };
|
||||
const queue: Array<[number, number]> = [[startId, 0]];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const [currentId, dist] = queue.shift()!;
|
||||
const connections = connectionMap.get(currentId) ?? [];
|
||||
for (const nextId of connections) {
|
||||
if (!citySet.has(nextId) || distances[nextId] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
distances[nextId] = dist + 1;
|
||||
queue.push([nextId, dist + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
result[startId] = distances;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const searchAllDistanceByNationList = (
|
||||
map: MapDefinition,
|
||||
cities: City[],
|
||||
nationIds: number[],
|
||||
suppliedCityOnly: boolean
|
||||
): Record<number, Record<number, number>> => {
|
||||
if (nationIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const cityIds = cities
|
||||
.filter((city) => nationIds.includes(city.nationId))
|
||||
.filter((city) => !suppliedCityOnly || city.supplyState > 0)
|
||||
.map((city) => city.id);
|
||||
return searchAllDistanceByCityList(map, cityIds);
|
||||
};
|
||||
|
||||
export const isNeighbor = (
|
||||
map: MapDefinition,
|
||||
cities: City[],
|
||||
nationA: number,
|
||||
nationB: number,
|
||||
includeNoSupply = true
|
||||
): boolean => {
|
||||
if (nationA === nationB) {
|
||||
return false;
|
||||
}
|
||||
const connectionMap = buildConnectionMap(map);
|
||||
const nationACities = new Set(
|
||||
cities
|
||||
.filter((city) => city.nationId === nationA)
|
||||
.filter((city) => includeNoSupply || city.supplyState > 0)
|
||||
.map((city) => city.id)
|
||||
);
|
||||
|
||||
const nationBCities = cities
|
||||
.filter((city) => city.nationId === nationB)
|
||||
.filter((city) => includeNoSupply || city.supplyState > 0)
|
||||
.map((city) => city.id);
|
||||
|
||||
for (const cityId of nationBCities) {
|
||||
for (const adjacentId of connectionMap.get(cityId) ?? []) {
|
||||
if (nationACities.has(adjacentId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getCityDistance = (map: MapDefinition, startCityId: number, endCityId: number): number => {
|
||||
if (startCityId === endCityId) return 0;
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getCityDistance, searchDistance, searchDistanceEntries } from '@sammo-ts/logic/world/distance.js';
|
||||
import {
|
||||
getCityDistance,
|
||||
isNeighbor,
|
||||
searchAllDistanceByCityList,
|
||||
searchAllDistanceByNationList,
|
||||
searchDistance,
|
||||
searchDistanceEntries,
|
||||
} from '@sammo-ts/logic/world/distance.js';
|
||||
import type { City } from '@sammo-ts/logic';
|
||||
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
|
||||
describe('World Distance', () => {
|
||||
@@ -87,4 +95,27 @@ describe('World Distance', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AI distance projections', () => {
|
||||
const cities = [
|
||||
{ id: 1, nationId: 1, supplyState: 1 },
|
||||
{ id: 2, nationId: 2, supplyState: 1 },
|
||||
{ id: 3, nationId: 1, supplyState: 0 },
|
||||
] as City[];
|
||||
|
||||
it('projects pairwise distances only across the selected city set', () => {
|
||||
expect(searchAllDistanceByCityList(mockMap, [1, 2, 4])).toEqual({
|
||||
1: { 1: 0, 2: 1, 4: 2 },
|
||||
2: { 1: 1, 2: 0, 4: 1 },
|
||||
4: { 1: 2, 2: 1, 4: 0 },
|
||||
});
|
||||
expect(searchAllDistanceByNationList(mockMap, cities, [1], true)).toEqual({ 1: { 1: 0 } });
|
||||
});
|
||||
|
||||
it('checks supplied and unsupplied borders with the requested policy', () => {
|
||||
expect(isNeighbor(mockMap, cities, 1, 2, true)).toBe(true);
|
||||
expect(isNeighbor(mockMap, cities, 1, 2, false)).toBe(true);
|
||||
expect(isNeighbor(mockMap, cities, 1, 1, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user