merge: 통일 완료 후 턴 중단 반영
This commit is contained in:
@@ -152,6 +152,12 @@ export class TurnDaemonLifecycle {
|
|||||||
this.status.state = 'idle';
|
this.status.state = 'idle';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((await this.stateStore.shouldHaltScheduledRuns?.()) ?? false) {
|
||||||
|
this.status.nextTurnTime = undefined;
|
||||||
|
await this.clock.sleepMs(500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.pendingRun) {
|
if (this.pendingRun) {
|
||||||
await this.runOnce(this.pendingRun);
|
await this.runOnce(this.pendingRun);
|
||||||
this.pendingRun = null;
|
this.pendingRun = null;
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export interface TurnStateStore {
|
|||||||
saveLastTurnTime(turnTime: Date): Promise<void>;
|
saveLastTurnTime(turnTime: Date): Promise<void>;
|
||||||
loadCheckpoint(): Promise<TurnCheckpoint | undefined>;
|
loadCheckpoint(): Promise<TurnCheckpoint | undefined>;
|
||||||
saveCheckpoint(checkpoint?: TurnCheckpoint): Promise<void>;
|
saveCheckpoint(checkpoint?: TurnCheckpoint): Promise<void>;
|
||||||
|
shouldHaltScheduledRuns?(): Promise<boolean>;
|
||||||
loadGameClock?(wallNow?: Date): Promise<{ mode: GameClockMode; now: Date }>;
|
loadGameClock?(wallNow?: Date): Promise<{ mode: GameClockMode; now: Date }>;
|
||||||
advanceGameClockTo?(target: Date, wallNow: Date): Promise<void>;
|
advanceGameClockTo?(target: Date, wallNow: Date): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { TurnCheckpoint, TurnStateStore } from '../lifecycle/types.js';
|
import type { TurnCheckpoint, TurnStateStore } from '../lifecycle/types.js';
|
||||||
|
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||||
|
|
||||||
export class InMemoryTurnStateStore implements TurnStateStore {
|
export class InMemoryTurnStateStore implements TurnStateStore {
|
||||||
@@ -29,6 +30,11 @@ export class InMemoryTurnStateStore implements TurnStateStore {
|
|||||||
this.world.setCheckpoint(checkpoint);
|
this.world.setCheckpoint(checkpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async shouldHaltScheduledRuns(): Promise<boolean> {
|
||||||
|
const meta = asRecord(this.world.getState().meta);
|
||||||
|
return asNumber(meta.isunited ?? meta.isUnited, 0) >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
async loadGameClock(wallNow = new Date(Date.now())): Promise<{ mode: 'realtime' | 'manual'; now: Date }> {
|
async loadGameClock(wallNow = new Date(Date.now())): Promise<{ mode: 'realtime' | 'manual'; now: Date }> {
|
||||||
return {
|
return {
|
||||||
mode: this.world.getGameClockState().mode,
|
mode: this.world.getGameClockState().mode,
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ export class InMemoryTurnProcessor implements TurnProcessor {
|
|||||||
const deadlineMs = startMs + Math.max(0, budget.budgetMs);
|
const deadlineMs = startMs + Math.max(0, budget.budgetMs);
|
||||||
const isBudgetExpired = () => Date.now() >= deadlineMs;
|
const isBudgetExpired = () => Date.now() >= deadlineMs;
|
||||||
|
|
||||||
|
if (isWorldUnited(this.world)) {
|
||||||
|
return {
|
||||||
|
lastTurnTime: this.world.getState().lastTurnTime.toISOString(),
|
||||||
|
processedGenerals: 0,
|
||||||
|
processedTurns: 0,
|
||||||
|
durationMs: Math.max(0, Date.now() - startMs),
|
||||||
|
partial: false,
|
||||||
|
checkpoint,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.world.setCheckpoint(checkpoint);
|
this.world.setCheckpoint(checkpoint);
|
||||||
this.world.updateWorldMeta({
|
this.world.updateWorldMeta({
|
||||||
refreshLimit: calculateAccessRefreshLimit(this.world.getState().tickSeconds),
|
refreshLimit: calculateAccessRefreshLimit(this.world.getState().tickSeconds),
|
||||||
|
|||||||
@@ -14,6 +14,40 @@ import {
|
|||||||
const addMinutes = (time: Date, minutes: number): Date => new Date(time.getTime() + minutes * 60_000);
|
const addMinutes = (time: Date, minutes: number): Date => new Date(time.getTime() + minutes * 60_000);
|
||||||
|
|
||||||
describe('TurnDaemonLifecycle', () => {
|
describe('TurnDaemonLifecycle', () => {
|
||||||
|
it('does not schedule another processor run after the world reaches a terminal united state', async () => {
|
||||||
|
const clock = new ManualClock(new Date('2026-01-01T00:00:00.000Z').getTime());
|
||||||
|
const controlQueue = new InMemoryControlQueue();
|
||||||
|
const processor = { run: vi.fn() };
|
||||||
|
const lifecycle = new TurnDaemonLifecycle(
|
||||||
|
{
|
||||||
|
clock,
|
||||||
|
controlQueue,
|
||||||
|
getNextTickTime: (value) => addMinutes(value, 60),
|
||||||
|
stateStore: {
|
||||||
|
loadLastTurnTime: async () => new Date('2042-01-01T00:00:00.000Z'),
|
||||||
|
loadNextGeneralTurnTime: async () => new Date('2042-01-01T00:30:00.000Z'),
|
||||||
|
saveLastTurnTime: async () => {},
|
||||||
|
loadCheckpoint: async () => undefined,
|
||||||
|
saveCheckpoint: async () => {},
|
||||||
|
shouldHaltScheduledRuns: async () => {
|
||||||
|
controlQueue.enqueue({ type: 'shutdown', reason: 'terminal world verified' });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
processor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
profile: 'terminal-united',
|
||||||
|
defaultBudget: { budgetMs: 100, maxGenerals: 10, catchUpCap: 1 },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await lifecycle.start();
|
||||||
|
|
||||||
|
expect(processor.run).not.toHaveBeenCalled();
|
||||||
|
expect(lifecycle.getStatus().nextTurnTime).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('runs manual game time to each monthly snapshot without waiting for wall time', async () => {
|
it('runs manual game time to each monthly snapshot without waiting for wall time', async () => {
|
||||||
const wallNow = new Date('2026-01-01T00:00:00.000Z');
|
const wallNow = new Date('2026-01-01T00:00:00.000Z');
|
||||||
const operationalClock = new ManualClock(wallNow.getTime());
|
const operationalClock = new ManualClock(wallNow.getTime());
|
||||||
|
|||||||
@@ -286,5 +286,14 @@ describe('InMemoryTurnProcessor ordering', () => {
|
|||||||
|
|
||||||
expect(result.processedTurns).toBe(1);
|
expect(result.processedTurns).toBe(1);
|
||||||
expect(world.getState()).toMatchObject({ currentYear: 189, currentMonth: 2, meta: { isUnited: 2 } });
|
expect(world.getState()).toMatchObject({ currentYear: 189, currentMonth: 2, meta: { isUnited: 2 } });
|
||||||
|
|
||||||
|
world.updateWorldMeta({ refreshLimit: 12_000 });
|
||||||
|
const haltedResult = await processor.run(addMinutes(baseTime, 60), {
|
||||||
|
budgetMs: 1_000,
|
||||||
|
maxGenerals: 10,
|
||||||
|
catchUpCap: 10,
|
||||||
|
});
|
||||||
|
expect(haltedResult).toMatchObject({ processedGenerals: 0, processedTurns: 0 });
|
||||||
|
expect(world.getState().meta).toMatchObject({ isUnited: 2, refreshLimit: 12_000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe('readReleaseManifest', () => {
|
|||||||
await expect(readReleaseManifest(workspaceRoot)).resolves.toMatchObject({
|
await expect(readReleaseManifest(workspaceRoot)).resolves.toMatchObject({
|
||||||
controllerProtocol: RELEASE_CONTROLLER_PROTOCOL,
|
controllerProtocol: RELEASE_CONTROLLER_PROTOCOL,
|
||||||
gatewaySchemaHead: '20260819000000_backfill_profile_release_source',
|
gatewaySchemaHead: '20260819000000_backfill_profile_release_source',
|
||||||
gameSchemaHead: '20260820000000_widen_emperor_officer_pictures',
|
gameSchemaHead: '20260820001000_restore_united_turn_halt',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
-- isunited 2/3에서 Ref 턴 실행은 중단되고, 통일 시 늘린 refreshLimit가 유지된다.
|
||||||
|
-- 기존 core daemon pass가 기본값으로 되돌린 행만 식별해 100배 값을 복구한다.
|
||||||
|
UPDATE "world_state"
|
||||||
|
SET "meta" = jsonb_set(
|
||||||
|
"meta",
|
||||||
|
'{refreshLimit}',
|
||||||
|
to_jsonb((("meta" ->> 'refreshLimit')::integer * 100)),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
WHERE COALESCE("meta" ->> 'isunited', "meta" ->> 'isUnited', '0') ~ '^[0-9]+$'
|
||||||
|
AND COALESCE("meta" ->> 'isunited', "meta" ->> 'isUnited', '0')::integer >= 2
|
||||||
|
AND COALESCE("meta" ->> 'refreshLimit', '') ~ '^[0-9]+$'
|
||||||
|
AND ("meta" ->> 'refreshLimit')::integer =
|
||||||
|
ROUND(POWER("tick_seconds"::numeric / 60, 0.6) * 3)::integer * 10;
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"controllerProtocol": 2,
|
"controllerProtocol": 2,
|
||||||
"gatewaySchemaHead": "20260819000000_backfill_profile_release_source",
|
"gatewaySchemaHead": "20260819000000_backfill_profile_release_source",
|
||||||
"gameSchemaHead": "20260820000000_widen_emperor_officer_pictures",
|
"gameSchemaHead": "20260820001000_restore_united_turn_halt",
|
||||||
"components": ["gateway-api", "gateway-frontend", "release-controller", "game-api", "game-engine", "game-frontend"]
|
"components": ["gateway-api", "gateway-frontend", "release-controller", "game-api", "game-engine", "game-frontend"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user