fix(game-engine): 밀린 턴을 월 경계별로 처리

실시간 게임 시계가 여러 달 앞선 경우 장수 턴을 현재 월 경계까지만 실행해 월 표시와 상태 전이 순서를 보존한다. 턴 데몬 전용 NODE_OPTIONS도 지원해 다른 런타임 프로세스와 heap 상한을 분리한다.
This commit is contained in:
2026-08-16 06:48:35 +00:00
parent d8c60bd80f
commit 34a6be0c44
5 changed files with 36 additions and 9 deletions
@@ -183,11 +183,15 @@ export class TurnDaemonLifecycle {
const gameNowMs = gameClock?.now.getTime() ?? nowMs;
const nextTurnMs = nextRunTime.getTime();
if (gameNowMs >= nextTurnMs) {
// Ref checkDelay() executes every turn due at the observed
// wall-clock time in one snapshot. Using only the oldest due
// timestamp lets generals created by that batch run before a
// monthly boundary, although Ref defers them to the next pass.
await this.runOnce({ reason: 'schedule', targetTime: new Date(gameNowMs) });
// Preserve Ref's chronological boundary even when realtime has
// fallen multiple months behind. Draining every general through
// gameNow before advancing the first pending month labels and
// resolves all of those future commands in the stale month and
// can build an unbounded same-month catch-up backlog. Within the current
// month we still observe every due general through gameNow; once
// a month boundary is overdue, process only through that boundary.
const nextMonthMs = this.getNextTickTime(new Date(this.status.lastTurnTime!)).getTime();
await this.runOnce({ reason: 'schedule', targetTime: new Date(Math.min(gameNowMs, nextMonthMs)) });
continue;
}
@@ -447,11 +447,11 @@ describe('TurnDaemonLifecycle', () => {
await loop;
});
it('catches up through the observed clock with queue and checkpoint context', async () => {
it('limits realtime backlog to the next month with queue and checkpoint context', async () => {
const turnTermMinutes = 10;
const lastTurnTime = new Date(2026, 0, 2, 2, 0, 0, 0);
const generalTurnQueue = [addMinutes(lastTurnTime, 5), addMinutes(lastTurnTime, 20)];
const expectedRunTimeMs = addMinutes(lastTurnTime, 30).getTime();
const expectedRunTimeMs = addMinutes(lastTurnTime, turnTermMinutes).getTime();
const checkpoint = {
turnTime: lastTurnTime.toISOString(),
generalId: 101,
@@ -524,11 +524,11 @@ describe('TurnDaemonLifecycle', () => {
await loop;
});
it('catches up through the observed clock when tick boundary precedes the queue front', async () => {
it('limits realtime backlog to the next month when the boundary precedes the queue front', async () => {
const turnTermMinutes = 10;
const lastTurnTime = new Date(2026, 0, 2, 2, 0, 0, 0);
const generalTurnQueue = [addMinutes(lastTurnTime, 15), addMinutes(lastTurnTime, 30)];
const expectedRunTimeMs = addMinutes(lastTurnTime, 30).getTime();
const expectedRunTimeMs = addMinutes(lastTurnTime, turnTermMinutes).getTime();
const checkpoint = {
turnTime: lastTurnTime.toISOString(),
generalId: 102,