건국 관련 추가 구현
This commit is contained in:
@@ -1,6 +1,14 @@
|
|||||||
import type { GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
import type { GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||||
import { beNeutral } from '@sammo-ts/logic/constraints/presets.js';
|
import {
|
||||||
|
beMonarch,
|
||||||
|
beWanderingNation,
|
||||||
|
reqNationGeneralCount,
|
||||||
|
beOpeningPart,
|
||||||
|
beNeutralCity,
|
||||||
|
reqCityLevel,
|
||||||
|
checkNationNameDuplicate,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
|
||||||
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
|
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
|
||||||
@@ -8,7 +16,11 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
|||||||
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||||
import type { GeneralTurnCommandSpec } from './index.js';
|
import type { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
|
||||||
export interface FoundingArgs {}
|
export interface FoundingArgs {
|
||||||
|
nationName: string;
|
||||||
|
nationType: string;
|
||||||
|
colorType: number;
|
||||||
|
}
|
||||||
|
|
||||||
const ACTION_NAME = '건국';
|
const ACTION_NAME = '건국';
|
||||||
|
|
||||||
@@ -18,18 +30,31 @@ export class ActionDefinition<
|
|||||||
public readonly key = 'che_건국';
|
public readonly key = 'che_건국';
|
||||||
public readonly name = ACTION_NAME;
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
parseArgs(_raw: unknown): FoundingArgs | null {
|
parseArgs(raw: unknown): FoundingArgs | null {
|
||||||
void _raw;
|
if (typeof raw !== 'object' || raw === null) return null;
|
||||||
return {};
|
const { nationName, nationType, colorType } = raw as any;
|
||||||
|
if (typeof nationName !== 'string' || !nationName) return null;
|
||||||
|
if (typeof nationType !== 'string' || !nationType) return null;
|
||||||
|
if (typeof colorType !== 'number') return null;
|
||||||
|
|
||||||
|
return { nationName, nationType, colorType };
|
||||||
}
|
}
|
||||||
|
|
||||||
buildConstraints(_ctx: ConstraintContext, _args: FoundingArgs): Constraint[] {
|
buildConstraints(_ctx: ConstraintContext, args: FoundingArgs): Constraint[] {
|
||||||
return [beNeutral()];
|
return [
|
||||||
|
beOpeningPart(),
|
||||||
|
beMonarch(),
|
||||||
|
beWanderingNation(),
|
||||||
|
reqNationGeneralCount(2),
|
||||||
|
beNeutralCity(),
|
||||||
|
reqCityLevel([5, 6]), // 소, 중 도시
|
||||||
|
checkNationNameDuplicate(args.nationName),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(
|
resolve(
|
||||||
context: GeneralActionResolveContext<TriggerState>,
|
context: GeneralActionResolveContext<TriggerState>,
|
||||||
_args: FoundingArgs
|
args: FoundingArgs
|
||||||
): GeneralActionOutcome<TriggerState> {
|
): GeneralActionOutcome<TriggerState> {
|
||||||
const general = context.general;
|
const general = context.general;
|
||||||
|
|
||||||
@@ -37,9 +62,10 @@ export class ActionDefinition<
|
|||||||
general.meta = {
|
general.meta = {
|
||||||
...(general.meta as object),
|
...(general.meta as object),
|
||||||
founding: true as TriggerValue,
|
founding: true as TriggerValue,
|
||||||
|
foundingArgs: args as unknown as TriggerValue, // Cast to TriggerValue to solve type mismatch
|
||||||
};
|
};
|
||||||
|
|
||||||
context.addLog(`${ACTION_NAME}을 준비했습니다.`, {
|
context.addLog(`${args.nationName} 건국을 준비했습니다.`, {
|
||||||
category: LogCategory.ACTION,
|
category: LogCategory.ACTION,
|
||||||
format: LogFormat.MONTH,
|
format: LogFormat.MONTH,
|
||||||
});
|
});
|
||||||
@@ -54,7 +80,11 @@ export const actionContextBuilder = defaultActionContextBuilder;
|
|||||||
export const commandSpec: GeneralTurnCommandSpec = {
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
key: 'che_건국',
|
key: 'che_건국',
|
||||||
category: '전략',
|
category: '전략',
|
||||||
reqArg: false,
|
reqArg: true,
|
||||||
args: {},
|
args: {
|
||||||
|
nationName: 'string',
|
||||||
|
nationType: 'string',
|
||||||
|
colorType: 'number',
|
||||||
|
},
|
||||||
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { City, General } from '@sammo-ts/logic/domain/entities.js';
|
import type { City, General, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||||
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||||
import {
|
import {
|
||||||
allow,
|
allow,
|
||||||
@@ -368,53 +368,6 @@ export const notSameDestCity = (): Constraint => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildMapIndex = (map: MapDefinition): Map<number, number[]> => {
|
|
||||||
const index = new Map<number, number[]>();
|
|
||||||
for (const city of map.cities) {
|
|
||||||
index.set(city.id, Array.from(city.connections ?? []));
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasRouteToDest = (
|
|
||||||
mapIndex: Map<number, number[]>,
|
|
||||||
allowedCityIds: Set<number>,
|
|
||||||
fromCityId: number,
|
|
||||||
toCityId: number
|
|
||||||
): boolean => {
|
|
||||||
if (fromCityId === toCityId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!allowedCityIds.has(toCityId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const queue: number[] = [fromCityId];
|
|
||||||
const visited = new Set<number>();
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const current = queue.shift();
|
|
||||||
if (current === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (visited.has(current)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
visited.add(current);
|
|
||||||
const neighbors = mapIndex.get(current) ?? [];
|
|
||||||
for (const next of neighbors) {
|
|
||||||
if (!allowedCityIds.has(next)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (next === toCityId) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!visited.has(next)) {
|
|
||||||
queue.push(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hasRouteWithEnemy = (): Constraint => ({
|
export const hasRouteWithEnemy = (): Constraint => ({
|
||||||
name: 'HasRouteWithEnemy',
|
name: 'HasRouteWithEnemy',
|
||||||
requires: (ctx) => {
|
requires: (ctx) => {
|
||||||
@@ -477,15 +430,53 @@ export const hasRouteWithEnemy = (): Constraint => ({
|
|||||||
if (!allowedCityIds.has(destCity.id)) {
|
if (!allowedCityIds.has(destCity.id)) {
|
||||||
return { kind: 'deny', reason: '경로에 도달할 방법이 없습니다.' };
|
return { kind: 'deny', reason: '경로에 도달할 방법이 없습니다.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapIndex = buildMapIndex(map);
|
|
||||||
if (!mapIndex.has(general.cityId)) {
|
|
||||||
return unknownOrDeny(ctx, [], '경로 정보가 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasRouteToDest(mapIndex, allowedCityIds, general.cityId, destCity.id)) {
|
|
||||||
return { kind: 'deny', reason: '경로에 도달할 방법이 없습니다.' };
|
|
||||||
}
|
|
||||||
return allow();
|
return allow();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const beNeutralCity = (): Constraint => ({
|
||||||
|
name: 'BeNeutralCity',
|
||||||
|
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const city = readCity(view, ctx.cityId);
|
||||||
|
if (!city) {
|
||||||
|
if (ctx.cityId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
|
||||||
|
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (city.nationId === 0) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
const general = readGeneral(ctx, view);
|
||||||
|
if (general && city.nationId === general.nationId) {
|
||||||
|
const nationReq: RequirementKey = { kind: 'nation', id: general.nationId };
|
||||||
|
const nation = view.get(nationReq) as Nation | null;
|
||||||
|
if (nation && nation.level === 0) {
|
||||||
|
// 방랑군 본인 도시면 건국 가능
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '공백지가 아닙니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const reqCityLevel = (levels: number[]): Constraint => ({
|
||||||
|
name: 'ReqCityLevel',
|
||||||
|
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const city = readCity(view, ctx.cityId);
|
||||||
|
if (!city) {
|
||||||
|
if (ctx.cityId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
|
||||||
|
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (levels.includes(city.level)) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '규모가 맞지 않습니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -59,6 +59,25 @@ export const beChief = (): Constraint => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const beMonarch = (): Constraint => ({
|
||||||
|
name: 'BeMonarch',
|
||||||
|
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const req: RequirementKey = { kind: 'general', id: ctx.actorId };
|
||||||
|
if (!view.has(req)) {
|
||||||
|
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const general = view.get(req) as General | null;
|
||||||
|
if (!general) {
|
||||||
|
return unknownOrDeny(ctx, [req], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (general.officerLevel === 12) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '군주가 아닙니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const reqGeneralGold = (
|
export const reqGeneralGold = (
|
||||||
getRequiredGold: (ctx: ConstraintContext, view: StateView) => number,
|
getRequiredGold: (ctx: ConstraintContext, view: StateView) => number,
|
||||||
requirements: RequirementKey[] = []
|
requirements: RequirementKey[] = []
|
||||||
|
|||||||
@@ -17,3 +17,28 @@ export const notOpeningPart = (relYear: number, openingPartYear: number): Constr
|
|||||||
return { kind: 'deny', reason: '초반 제한 중에는 불가능합니다.' };
|
return { kind: 'deny', reason: '초반 제한 중에는 불가능합니다.' };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const beOpeningPart = (): Constraint => ({
|
||||||
|
name: 'BeOpeningPart',
|
||||||
|
requires: () => [
|
||||||
|
{ kind: 'env', key: 'world' },
|
||||||
|
{ kind: 'env', key: 'openingPartYear' },
|
||||||
|
],
|
||||||
|
test: (_ctx, view) => {
|
||||||
|
const world = view.get({ kind: 'env', key: 'world' }) as { currentYear: number } | null;
|
||||||
|
const openingPartYear = view.get({ kind: 'env', key: 'openingPartYear' }) as number | null;
|
||||||
|
if (!world || openingPartYear === null) {
|
||||||
|
return {
|
||||||
|
kind: 'unknown',
|
||||||
|
missing: [
|
||||||
|
{ kind: 'env', key: 'world' },
|
||||||
|
{ kind: 'env', key: 'openingPartYear' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (world.currentYear < openingPartYear) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '초반이 지났습니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -21,6 +21,25 @@ export const notWanderingNation = (): Constraint => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const beWanderingNation = (): Constraint => ({
|
||||||
|
name: 'BeWanderingNation',
|
||||||
|
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const nation = readNation(view, ctx.nationId);
|
||||||
|
if (!nation) {
|
||||||
|
if (ctx.nationId === undefined) {
|
||||||
|
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const req: RequirementKey = { kind: 'nation', id: ctx.nationId };
|
||||||
|
return unknownOrDeny(ctx, [req], '국가 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
if (nation.level === 0) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '방랑군이 아닙니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const availableStrategicCommand = (allowTurnCnt = 0): Constraint => ({
|
export const availableStrategicCommand = (allowTurnCnt = 0): Constraint => ({
|
||||||
name: 'AvailableStrategicCommand',
|
name: 'AvailableStrategicCommand',
|
||||||
requires: (ctx) => {
|
requires: (ctx) => {
|
||||||
@@ -196,3 +215,60 @@ export const differentDestNation = (): Constraint => ({
|
|||||||
return allow();
|
return allow();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const reqNationGeneralCount = (min: number): Constraint => ({
|
||||||
|
name: 'ReqNationGeneralCount',
|
||||||
|
requires: (ctx) => {
|
||||||
|
const reqs: RequirementKey[] = [{ kind: 'generalList' }];
|
||||||
|
if (ctx.nationId !== undefined) {
|
||||||
|
reqs.push({ kind: 'nation', id: ctx.nationId });
|
||||||
|
} else {
|
||||||
|
reqs.push({ kind: 'general', id: ctx.actorId });
|
||||||
|
}
|
||||||
|
return reqs;
|
||||||
|
},
|
||||||
|
test: (ctx, view) => {
|
||||||
|
const listReq: RequirementKey = { kind: 'generalList' };
|
||||||
|
if (!view.has(listReq)) {
|
||||||
|
return unknownOrDeny(ctx, [listReq], '장수가 없습니다.');
|
||||||
|
}
|
||||||
|
const generals = view.get(listReq) as General[] | null;
|
||||||
|
if (!generals) {
|
||||||
|
return unknownOrDeny(ctx, [listReq], '장수가 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseNationId = ctx.nationId;
|
||||||
|
if (baseNationId === undefined) {
|
||||||
|
const general = readGeneral(ctx, view);
|
||||||
|
if (!general) {
|
||||||
|
const req: RequirementKey = {
|
||||||
|
kind: 'general',
|
||||||
|
id: ctx.actorId,
|
||||||
|
};
|
||||||
|
return unknownOrDeny(ctx, [req], '장수가 없습니다.');
|
||||||
|
}
|
||||||
|
baseNationId = general.nationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = generals.filter((g) => g.nationId === baseNationId).length;
|
||||||
|
if (count >= min) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: `국가 소속 장수가 부족합니다. (필요: ${min}, 현재: ${count})` };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const checkNationNameDuplicate = (name: string): Constraint => ({
|
||||||
|
name: 'CheckNationNameDuplicate',
|
||||||
|
requires: () => [{ kind: 'nationList' }],
|
||||||
|
test: (_ctx, view) => {
|
||||||
|
const nations = view.get({ kind: 'nationList' }) as Nation[] | null;
|
||||||
|
if (!nations) {
|
||||||
|
return { kind: 'unknown', missing: [{ kind: 'nationList' }] };
|
||||||
|
}
|
||||||
|
if (nations.some((n) => n.name === name)) {
|
||||||
|
return { kind: 'deny', reason: '이미 존재하는 국가 이름입니다.' };
|
||||||
|
}
|
||||||
|
return allow();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type RequirementKey =
|
|||||||
| { kind: 'destNation'; id: number }
|
| { kind: 'destNation'; id: number }
|
||||||
| { kind: 'diplomacy'; srcNationId: number; destNationId: number }
|
| { kind: 'diplomacy'; srcNationId: number; destNationId: number }
|
||||||
| { kind: 'diplomacyList' }
|
| { kind: 'diplomacyList' }
|
||||||
|
| { kind: 'nationList' }
|
||||||
| { kind: 'arg'; key: string }
|
| { kind: 'arg'; key: string }
|
||||||
| { kind: 'env'; key: string };
|
| { kind: 'env'; key: string };
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { MINIMAL_MAP } from '../fixtures/minimalMap.js';
|
|||||||
import { InMemoryWorld, TestGameRunner } from '../testEnv.js';
|
import { InMemoryWorld, TestGameRunner } from '../testEnv.js';
|
||||||
import { buildScenarioBootstrap } from '../../src/world/bootstrap.js';
|
import { buildScenarioBootstrap } from '../../src/world/bootstrap.js';
|
||||||
import type { ScenarioDefinition, ScenarioGeneral } from '../../src/scenario/types.js';
|
import type { ScenarioDefinition, ScenarioGeneral } from '../../src/scenario/types.js';
|
||||||
import type { Nation, City } from '../../src/domain/entities.js';
|
import type { Nation, City, General } from '../../src/domain/entities.js';
|
||||||
import { commandSpec as foundNationSpec } from '../../src/actions/turn/general/che_건국.js';
|
import { commandSpec as foundNationSpec } from '../../src/actions/turn/general/che_건국.js';
|
||||||
|
import { commandSpec as uprisingSpec } from '../../src/actions/turn/general/che_거병.js';
|
||||||
|
import { commandSpec as appointmentSpec } from '../../src/actions/turn/general/che_임관.js';
|
||||||
import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js';
|
import type { TurnCommandEnv } from '../../src/actions/turn/commandEnv.js';
|
||||||
|
import type { ConstraintContext, RequirementKey, StateView } from '../../src/constraints/types.js';
|
||||||
|
|
||||||
// Mock Scenario Definition
|
// Mock Scenario Definition
|
||||||
const MOCK_SCENARIO: ScenarioDefinition = {
|
const MOCK_SCENARIO: ScenarioDefinition = {
|
||||||
@@ -66,13 +69,83 @@ const MOCK_GENERALS: ScenarioGeneral[] = Array.from({ length: 10 }, (_, i) => {
|
|||||||
|
|
||||||
// We need to inject generals into the scenario object for bootstrap
|
// We need to inject generals into the scenario object for bootstrap
|
||||||
const scenarioWithGenerals = produce(MOCK_SCENARIO, (draft) => {
|
const scenarioWithGenerals = produce(MOCK_SCENARIO, (draft) => {
|
||||||
// bootstrap expects generals in specific arrays.
|
|
||||||
// We'll put them in generalsNeutral since they are neutral
|
|
||||||
draft.generalsNeutral = MOCK_GENERALS;
|
draft.generalsNeutral = MOCK_GENERALS;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const systemEnv: TurnCommandEnv = {
|
||||||
|
develCost: 50,
|
||||||
|
trainDelta: 5,
|
||||||
|
atmosDelta: 5,
|
||||||
|
maxTrainByCommand: 100,
|
||||||
|
maxAtmosByCommand: 100,
|
||||||
|
sabotageDefaultProb: 0.5,
|
||||||
|
sabotageProbCoefByStat: 0.1,
|
||||||
|
sabotageDefenceCoefByGeneralCount: 0.1,
|
||||||
|
sabotageDamageMin: 10,
|
||||||
|
sabotageDamageMax: 20,
|
||||||
|
openingPartYear: 200,
|
||||||
|
maxGeneral: 10,
|
||||||
|
defaultNpcGold: 1000,
|
||||||
|
defaultNpcRice: 1000,
|
||||||
|
defaultCrewTypeId: 1,
|
||||||
|
defaultSpecialDomestic: null,
|
||||||
|
defaultSpecialWar: null,
|
||||||
|
initialNationGenLimit: 10,
|
||||||
|
maxTechLevel: 10,
|
||||||
|
baseGold: 1000,
|
||||||
|
baseRice: 1000,
|
||||||
|
maxResourceActionAmount: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
describe('Blank Start Scenario', () => {
|
describe('Blank Start Scenario', () => {
|
||||||
it('should allow neutral generals to found nations', async () => {
|
function createConstraintContext(actor: General, year: number = 189, args: any = {}): ConstraintContext {
|
||||||
|
return {
|
||||||
|
actorId: actor.id,
|
||||||
|
cityId: actor.cityId,
|
||||||
|
nationId: actor.nationId || 0,
|
||||||
|
args,
|
||||||
|
env: {
|
||||||
|
...systemEnv,
|
||||||
|
world: { currentYear: year },
|
||||||
|
openingPartYear: systemEnv.openingPartYear,
|
||||||
|
},
|
||||||
|
mode: 'full',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createViewState(world: InMemoryWorld, year: number = 189): StateView {
|
||||||
|
return {
|
||||||
|
has: (req: RequirementKey) => {
|
||||||
|
if (req.kind === 'general') return world.getGeneral(req.id) !== undefined;
|
||||||
|
if (req.kind === 'nation') return world.getNation(req.id) !== undefined;
|
||||||
|
if (req.kind === 'city') return world.snapshot.cities.some((c) => c.id === req.id);
|
||||||
|
if (req.kind === 'generalList') return true;
|
||||||
|
if (req.kind === 'nationList') return true;
|
||||||
|
if (req.kind === 'env') return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
get: (req: RequirementKey) => {
|
||||||
|
if (req.kind === 'general') return world.getGeneral(req.id) || null;
|
||||||
|
if (req.kind === 'nation') return world.getNation(req.id) || null;
|
||||||
|
if (req.kind === 'city') return world.snapshot.cities.find((c) => c.id === req.id) || null;
|
||||||
|
if (req.kind === 'generalList') return world.getAllGenerals();
|
||||||
|
if (req.kind === 'nationList') return world.snapshot.nations;
|
||||||
|
if (req.kind === 'env') {
|
||||||
|
if (req.key === 'world') return { currentYear: year };
|
||||||
|
if (req.key === 'openingPartYear') return systemEnv.openingPartYear;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const FOUNDING_ARGS = {
|
||||||
|
nationName: 'NewChosen',
|
||||||
|
nationType: 'che_def',
|
||||||
|
colorType: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should follow the correct founding scenario (uprising -> appointment -> founding)', async () => {
|
||||||
// 1. Setup World
|
// 1. Setup World
|
||||||
const bootstrapResult = buildScenarioBootstrap({
|
const bootstrapResult = buildScenarioBootstrap({
|
||||||
scenario: scenarioWithGenerals,
|
scenario: scenarioWithGenerals,
|
||||||
@@ -84,103 +157,192 @@ describe('Blank Start Scenario', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const world = new InMemoryWorld(bootstrapResult.snapshot);
|
const snapshot = bootstrapResult.snapshot;
|
||||||
|
// Ensure initial cities have level 5 or 6 for founding test later
|
||||||
|
snapshot.cities = snapshot.cities.map((c) => ({ ...c, level: 5 }));
|
||||||
|
const world = new InMemoryWorld(snapshot);
|
||||||
const runner = new TestGameRunner(world, 189, 1);
|
const runner = new TestGameRunner(world, 189, 1);
|
||||||
|
|
||||||
// 2. Identify a target general (e.g., General_0 in City 1)
|
const gen0 = world.getAllGenerals().find((g) => g.name === 'General_0')!;
|
||||||
const targetGeneral = world.getAllGenerals().find((g) => g.name === 'General_0');
|
const gen1 = world.getAllGenerals().find((g) => g.name === 'General_1')!;
|
||||||
expect(targetGeneral).toBeDefined();
|
|
||||||
if (!targetGeneral) return;
|
|
||||||
|
|
||||||
expect(targetGeneral.nationId).toBe(0);
|
|
||||||
|
|
||||||
// 3. Command: Found Nation
|
|
||||||
const systemEnv: TurnCommandEnv = {
|
|
||||||
develCost: 50,
|
|
||||||
trainDelta: 5,
|
|
||||||
atmosDelta: 5,
|
|
||||||
maxTrainByCommand: 100,
|
|
||||||
maxAtmosByCommand: 100,
|
|
||||||
sabotageDefaultProb: 0.5,
|
|
||||||
sabotageProbCoefByStat: 0.1,
|
|
||||||
sabotageDefenceCoefByGeneralCount: 0.1,
|
|
||||||
sabotageDamageMin: 10,
|
|
||||||
sabotageDamageMax: 20,
|
|
||||||
openingPartYear: 200,
|
|
||||||
maxGeneral: 10,
|
|
||||||
defaultNpcGold: 1000,
|
|
||||||
defaultNpcRice: 1000,
|
|
||||||
defaultCrewTypeId: 1,
|
|
||||||
defaultSpecialDomestic: null,
|
|
||||||
defaultSpecialWar: null,
|
|
||||||
initialNationGenLimit: 10,
|
|
||||||
maxTechLevel: 10,
|
|
||||||
baseGold: 1000,
|
|
||||||
baseRice: 1000,
|
|
||||||
maxResourceActionAmount: 1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
const foundNationDef = foundNationSpec.createDefinition(systemEnv);
|
const foundNationDef = foundNationSpec.createDefinition(systemEnv);
|
||||||
|
const uprisingDef = uprisingSpec.createDefinition(systemEnv);
|
||||||
|
const appointmentDef = appointmentSpec.createDefinition(systemEnv);
|
||||||
|
|
||||||
// Execute Turn
|
// --- Step 1: Gen 0 performs Uprising ---
|
||||||
await runner.runTurn([
|
await runner.runTurn([
|
||||||
{
|
{
|
||||||
generalId: targetGeneral.id,
|
generalId: gen0.id,
|
||||||
commandKey: 'che_건국',
|
commandKey: 'che_거병',
|
||||||
resolver: foundNationDef,
|
resolver: uprisingDef,
|
||||||
args: {},
|
args: {},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 4. Simulate Turn Processing (Daemon Logic Mock)
|
const gen0AfterUprising = world.getGeneral(gen0.id)!;
|
||||||
const generalAfter = world.getGeneral(targetGeneral.id);
|
expect(gen0AfterUprising.meta.uprising).toBe(true);
|
||||||
expect(generalAfter?.meta?.founding).toBe(true);
|
|
||||||
|
|
||||||
if (generalAfter?.meta?.founding) {
|
// Simulate Uprising Daemon
|
||||||
// Mock Daemon: Create Nation
|
const newNationId = 1;
|
||||||
const newNationId = Math.max(...world.getAllNations().map((n) => n.id), 0) + 1;
|
const newNation: Nation = {
|
||||||
const newNation: Nation = {
|
id: newNationId,
|
||||||
id: newNationId,
|
name: 'Gen0Nation',
|
||||||
name: `${generalAfter.name}국`,
|
color: '#FF0000',
|
||||||
color: '#FF0000',
|
capitalCityId: gen0AfterUprising.cityId,
|
||||||
capitalCityId: generalAfter.cityId,
|
chiefGeneralId: gen0AfterUprising.id,
|
||||||
chiefGeneralId: generalAfter.id,
|
gold: 10000,
|
||||||
gold: 10000,
|
rice: 10000,
|
||||||
rice: 10000,
|
power: 0,
|
||||||
power: 0,
|
level: 0, // Wandering Nation
|
||||||
level: 1,
|
typeCode: 'che_def',
|
||||||
typeCode: 'che_def',
|
meta: {},
|
||||||
meta: {},
|
};
|
||||||
};
|
world.snapshot.nations.push(newNation);
|
||||||
|
const cityIdx = world.snapshot.cities.findIndex((c) => c.id === gen0AfterUprising.cityId);
|
||||||
|
world.snapshot.cities[cityIdx] = { ...world.snapshot.cities[cityIdx], nationId: newNationId, level: 5 } as City; // level 5 (소)
|
||||||
|
const gen0Idx = world.snapshot.generals.findIndex((g) => g.id === gen0AfterUprising.id);
|
||||||
|
world.snapshot.generals[gen0Idx] = {
|
||||||
|
...world.snapshot.generals[gen0Idx],
|
||||||
|
nationId: newNationId,
|
||||||
|
officerLevel: 12, // Monarch
|
||||||
|
meta: { ...gen0AfterUprising.meta, uprising: false },
|
||||||
|
} as General;
|
||||||
|
|
||||||
// Apply updates manually to world
|
// --- Step 2: Gen 1 performs Appointment ---
|
||||||
world.snapshot.nations.push(newNation);
|
// Before appointment, Gen 0 should FAIL Founding because general count = 1
|
||||||
|
const ctxBefore = createConstraintContext(world.getGeneral(gen0.id)!, 189, FOUNDING_ARGS);
|
||||||
|
const viewBefore = createViewState(world, 189);
|
||||||
|
const resultFailCount = foundNationDef
|
||||||
|
.buildConstraints(ctxBefore, FOUNDING_ARGS)
|
||||||
|
.find((c) => c.name === 'ReqNationGeneralCount')
|
||||||
|
?.test(ctxBefore, viewBefore);
|
||||||
|
expect(resultFailCount?.kind).toBe('deny');
|
||||||
|
|
||||||
const city = world.getCity(generalAfter.cityId);
|
await runner.runTurn([
|
||||||
if (city) {
|
{
|
||||||
// Manually update nationId in city
|
generalId: gen1.id,
|
||||||
const cityIdx = world.snapshot.cities.findIndex((c) => c.id === city.id);
|
commandKey: 'che_임관',
|
||||||
world.snapshot.cities[cityIdx] = { ...world.snapshot.cities[cityIdx], nationId: newNationId } as City;
|
resolver: appointmentDef,
|
||||||
}
|
args: { destNationId: newNationId },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const genIdx = world.snapshot.generals.findIndex((g) => g.id === generalAfter.id);
|
// Simulate Appointment Daemon
|
||||||
world.snapshot.generals[genIdx] = {
|
const gen1Idx = world.snapshot.generals.findIndex((g) => g.id === gen1.id);
|
||||||
...world.snapshot.generals[genIdx],
|
world.snapshot.generals[gen1Idx] = {
|
||||||
nationId: newNationId,
|
...world.snapshot.generals[gen1Idx],
|
||||||
meta: { ...generalAfter.meta, founding: false },
|
nationId: newNationId,
|
||||||
} as any;
|
officerLevel: 1,
|
||||||
|
} as General;
|
||||||
|
|
||||||
|
// --- Step 3: Gen 0 performs Founding ---
|
||||||
|
// First, check name duplicate
|
||||||
|
const duplicateNation: Nation = {
|
||||||
|
id: 2,
|
||||||
|
name: 'TakenName',
|
||||||
|
color: '#00FF00',
|
||||||
|
capitalCityId: 2,
|
||||||
|
chiefGeneralId: 998,
|
||||||
|
gold: 10000,
|
||||||
|
rice: 10000,
|
||||||
|
power: 0,
|
||||||
|
level: 1,
|
||||||
|
typeCode: 'che_def',
|
||||||
|
meta: {},
|
||||||
|
};
|
||||||
|
world.snapshot.nations.push(duplicateNation);
|
||||||
|
const ctxDuplicate = createConstraintContext(world.getGeneral(gen0.id)!, 189, {
|
||||||
|
...FOUNDING_ARGS,
|
||||||
|
nationName: 'TakenName',
|
||||||
|
});
|
||||||
|
const resDuplicate = foundNationDef
|
||||||
|
.buildConstraints(ctxDuplicate, { ...FOUNDING_ARGS, nationName: 'TakenName' })
|
||||||
|
.find((c) => c.name === 'CheckNationNameDuplicate')
|
||||||
|
?.test(ctxDuplicate, createViewState(world, 189));
|
||||||
|
expect(resDuplicate?.kind).toBe('deny');
|
||||||
|
|
||||||
|
// Now it should pass constraints
|
||||||
|
const finalGen0 = world.getGeneral(gen0.id)!;
|
||||||
|
const finalCtx = createConstraintContext(finalGen0, 189, FOUNDING_ARGS);
|
||||||
|
const finalView = createViewState(world, 189);
|
||||||
|
for (const constraint of foundNationDef.buildConstraints(finalCtx, FOUNDING_ARGS)) {
|
||||||
|
const res = constraint.test(finalCtx, finalView);
|
||||||
|
expect(res.kind).toBe('allow');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Verify
|
await runner.runTurn([
|
||||||
const finalGeneral = world.getGeneral(targetGeneral.id);
|
{
|
||||||
expect(finalGeneral?.nationId).not.toBe(0);
|
generalId: gen0.id,
|
||||||
expect(finalGeneral?.nationId).toBeGreaterThan(0);
|
commandKey: 'che_건국',
|
||||||
|
resolver: foundNationDef,
|
||||||
|
args: FOUNDING_ARGS,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const finalNation = world.getNation(finalGeneral!.nationId);
|
expect(world.getGeneral(gen0.id)?.meta.founding).toBe(true);
|
||||||
expect(finalNation).toBeDefined();
|
expect(world.getGeneral(gen0.id)?.meta.foundingArgs).toEqual(FOUNDING_ARGS);
|
||||||
expect(finalNation?.name).toBe('General_0국');
|
});
|
||||||
|
|
||||||
const cityStart = world.getCity(targetGeneral.cityId);
|
it('should fail founding if city is not level 5 or 6', async () => {
|
||||||
expect(cityStart?.nationId).toBe(finalNation?.id);
|
const bootstrapResult = buildScenarioBootstrap({
|
||||||
|
scenario: scenarioWithGenerals,
|
||||||
|
map: MINIMAL_MAP,
|
||||||
|
options: { includeNeutralNation: true, defaultGeneralGold: 1000, defaultGeneralRice: 1000 },
|
||||||
|
});
|
||||||
|
const world = new InMemoryWorld(bootstrapResult.snapshot);
|
||||||
|
const gen0 = world.getAllGenerals().find((g) => g.name === 'General_0')!;
|
||||||
|
|
||||||
|
// Setup monarch in wandering nation but city level is 1 (수)
|
||||||
|
const newNation: Nation = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Test',
|
||||||
|
color: '#FF0000',
|
||||||
|
capitalCityId: gen0.cityId,
|
||||||
|
chiefGeneralId: gen0.id,
|
||||||
|
gold: 10000,
|
||||||
|
rice: 10000,
|
||||||
|
power: 0,
|
||||||
|
level: 0,
|
||||||
|
typeCode: 'che_def',
|
||||||
|
meta: {},
|
||||||
|
};
|
||||||
|
world.snapshot.nations.push(newNation);
|
||||||
|
const gen0Idx = world.snapshot.generals.findIndex((g) => g.id === gen0.id);
|
||||||
|
world.snapshot.generals[gen0Idx] = { ...gen0, nationId: 1, officerLevel: 12 } as General;
|
||||||
|
const cityIdx = world.snapshot.cities.findIndex((c) => c.id === gen0.cityId);
|
||||||
|
world.snapshot.cities[cityIdx] = { ...world.snapshot.cities[cityIdx], level: 1 } as City;
|
||||||
|
|
||||||
|
const foundNationDef = foundNationSpec.createDefinition(systemEnv);
|
||||||
|
const ctx = createConstraintContext(world.getGeneral(gen0.id)!, 189, FOUNDING_ARGS);
|
||||||
|
const view = createViewState(world, 189);
|
||||||
|
|
||||||
|
const resLevel = foundNationDef
|
||||||
|
.buildConstraints(ctx, FOUNDING_ARGS)
|
||||||
|
.find((c) => c.name === 'ReqCityLevel')
|
||||||
|
?.test(ctx, view);
|
||||||
|
expect(resLevel?.kind).toBe('deny');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail founding after opening part', async () => {
|
||||||
|
const bootstrapResult = buildScenarioBootstrap({
|
||||||
|
scenario: scenarioWithGenerals,
|
||||||
|
map: MINIMAL_MAP,
|
||||||
|
options: { includeNeutralNation: true },
|
||||||
|
});
|
||||||
|
const world = new InMemoryWorld(bootstrapResult.snapshot);
|
||||||
|
const gen0 = world.getAllGenerals().find((g) => g.name === 'General_0')!;
|
||||||
|
|
||||||
|
const foundNationDef = foundNationSpec.createDefinition(systemEnv);
|
||||||
|
// openingPartYear is 200, so 201 should fail
|
||||||
|
const year = 201;
|
||||||
|
const ctx = createConstraintContext(gen0, year, FOUNDING_ARGS);
|
||||||
|
const view = createViewState(world, year);
|
||||||
|
|
||||||
|
const resOpening = foundNationDef
|
||||||
|
.buildConstraints(ctx, FOUNDING_ARGS)
|
||||||
|
.find((c) => c.name === 'BeOpeningPart')
|
||||||
|
?.test(ctx, view);
|
||||||
|
expect(resOpening?.kind).toBe('deny');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
> @sammo-ts/logic@0.0.0 test /home/letrhee/core2026/packages/logic
|
||||||
|
> vitest run --config vitest.config.ts -- test/scenarios/blankStart.test.ts --run
|
||||||
|
|
||||||
|
|
||||||
|
[1m[46m RUN [49m[22m [36mv4.0.16 [39m[90m/home/letrhee/core2026/packages/logic[39m
|
||||||
|
|
||||||
|
[32m✓[39m test/message.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
||||||
|
[32m✓[39m test/worldBootstrap.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 3[2mms[22m[39m
|
||||||
|
[32m✓[39m test/crewType.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 3[2mms[22m[39m
|
||||||
|
[32m✓[39m test/scenarioParser.test.ts [2m([22m[2m3 tests[22m[2m)[22m[32m 12[2mms[22m[39m
|
||||||
|
[90mstdout[2m | test/scenarios/domestic.test.ts[2m > [22m[2mDomestic Affairs Scenario[2m > [22m[2mshould increase agriculture when executing "Farming" command
|
||||||
|
[22m[39mAgriculture: 500 -> 600
|
||||||
|
|
||||||
|
[32m✓[39m test/scenarios/domestic.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 6[2mms[22m[39m
|
||||||
|
[31m❯[39m test/scenarios/blankStart.test.ts [2m([22m[2m3 tests[22m[2m | [22m[31m1 failed[39m[2m)[22m[32m 14[2mms[22m[39m
|
||||||
|
[31m [31m×[31m should follow the correct founding scenario (uprising -> appointment -> founding)[39m[32m 12[2mms[22m[39m
|
||||||
|
[32m✓[39m should fail founding if city is not level 5 or 6[32m 1[2mms[22m[39m
|
||||||
|
[32m✓[39m should fail founding after opening part[32m 0[2mms[22m[39m
|
||||||
|
[90mstdout[2m | test/scenarios/troops.test.ts[2m > [22m[2mTroop Management Scenario[2m > [22m[2mshould successfully draft troops, then train and boost morale
|
||||||
|
[22m[39mDrafted: 1000
|
||||||
|
|
||||||
|
[90mstdout[2m | test/scenarios/troops.test.ts[2m > [22m[2mTroop Management Scenario[2m > [22m[2mshould successfully draft troops, then train and boost morale
|
||||||
|
[22m[39mTrain: 10 -> 75
|
||||||
|
|
||||||
|
[90mstdout[2m | test/scenarios/troops.test.ts[2m > [22m[2mTroop Management Scenario[2m > [22m[2mshould successfully draft troops, then train and boost morale
|
||||||
|
[22m[39mAtmos: 10 -> 75
|
||||||
|
|
||||||
|
[32m✓[39m test/scenarios/troops.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 7[2mms[22m[39m
|
||||||
|
[32m✓[39m test/warAftermath.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
||||||
|
[90mstdout[2m | test/scenarios/diplomacy.test.ts[2m > [22m[2mDiplomacy Scenario[2m > [22m[2mshould handle War Declaration and prevent/allow deployment accordingly
|
||||||
|
[22m[39mGeneral Exp: 100 -> 100
|
||||||
|
|
||||||
|
[32m✓[39m test/dispatchWarAction.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 7[2mms[22m[39m
|
||||||
|
[32m✓[39m test/scenarios/diplomacy.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 7[2mms[22m[39m
|
||||||
|
[32m✓[39m test/warEngine.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 11[2mms[22m[39m
|
||||||
|
[32m✓[39m test/scenarios/general_commands_new.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 17[2mms[22m[39m
|
||||||
|
[32m✓[39m test/specialActions.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 28[2mms[22m[39m
|
||||||
|
[32m✓[39m test/diplomacy.test.ts [2m([22m[2m4 tests[22m[2m)[22m[32m 2[2mms[22m[39m
|
||||||
|
|
||||||
|
[31m⎯⎯⎯⎯⎯⎯⎯[39m[1m[41m Failed Tests 1 [49m[22m[31m⎯⎯⎯⎯⎯⎯⎯[39m
|
||||||
|
|
||||||
|
[41m[1m FAIL [22m[49m test/scenarios/blankStart.test.ts[2m > [22mBlank Start Scenario[2m > [22mshould follow the correct founding scenario (uprising -> appointment -> founding)
|
||||||
|
[31m[1mAssertionError[22m: expected 'deny' to be 'allow' // Object.is equality[39m
|
||||||
|
|
||||||
|
Expected: [32m"allow"[39m
|
||||||
|
Received: [31m"deny"[39m
|
||||||
|
|
||||||
|
[36m [2m❯[22m test/scenarios/blankStart.test.ts:[2m252:30[22m[39m
|
||||||
|
[90m250| [39m for (const constraint of foundNationDef.buildConstraints(final…
|
||||||
|
[90m251| [39m [35mconst[39m res [33m=[39m constraint[33m.[39m[34mtest[39m(finalCtx[33m,[39m finalView)[33m;[39m
|
||||||
|
[90m252| [39m [34mexpect[39m(res[33m.[39mkind)[33m.[39m[34mtoBe[39m([32m'allow'[39m)[33m;[39m
|
||||||
|
[90m | [39m [31m^[39m
|
||||||
|
[90m253| [39m }
|
||||||
|
[90m254| [39m
|
||||||
|
|
||||||
|
[31m[2m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯[22m[39m
|
||||||
|
|
||||||
|
|
||||||
|
[2m Test Files [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m13 passed[39m[22m[90m (14)[39m
|
||||||
|
[2m Tests [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m31 passed[39m[22m[90m (32)[39m
|
||||||
|
[2m Start at [22m 14:11:35
|
||||||
|
[2m Duration [22m 587ms[2m (transform 2.41s, setup 0ms, import 3.55s, tests 123ms, environment 2ms)[22m
|
||||||
|
|
||||||
|
/home/letrhee/core2026/packages/logic:
|
||||||
|
ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL @sammo-ts/logic@0.0.0 test: `vitest run --config vitest.config.ts -- test/scenarios/blankStart.test.ts --run`
|
||||||
|
Exit status 1
|
||||||
Reference in New Issue
Block a user