유산 랜덤 턴 초기화의 다다음 턴 적용과 분초 표시 수정
This commit is contained in:
@@ -139,9 +139,9 @@ const buildTurnTimeZoneList = (tickMinutes: number): string[] => {
|
|||||||
|
|
||||||
const formatTurnTimeBaseLabel = (value: number): string => {
|
const formatTurnTimeBaseLabel = (value: number): string => {
|
||||||
const wholeSeconds = Math.trunc(value);
|
const wholeSeconds = Math.trunc(value);
|
||||||
const hours = String(Math.trunc(wholeSeconds / 3600)).padStart(2, '0');
|
const minutes = String(Math.trunc(wholeSeconds / 60)).padStart(2, '0');
|
||||||
const minutes = String(Math.trunc((wholeSeconds % 3600) / 60)).padStart(2, '0');
|
const seconds = String(wholeSeconds % 60).padStart(2, '0');
|
||||||
return `${hours}:${minutes}`;
|
return `${minutes}:${seconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resolveResetTurnTimeBase = (options: {
|
export const resolveResetTurnTimeBase = (options: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import { resolveResetTurnTimeBase } from '../src/router/inherit/index.js';
|
import { resolveResetTurnTimeBase } from '../src/router/inherit/index.js';
|
||||||
|
|
||||||
describe('inherit reset turn time Ref compatibility', () => {
|
describe('inherit reset turn time Ref compatibility', () => {
|
||||||
it('matches the Ref PHP deterministic seed, offset, and displayed minute', () => {
|
it('matches the Ref PHP deterministic seed, offset, and displayed minutes and seconds', () => {
|
||||||
const result = resolveResetTurnTimeBase({
|
const result = resolveResetTurnTimeBase({
|
||||||
hiddenSeed: 'hidden-seed',
|
hiddenSeed: 'hidden-seed',
|
||||||
userId: 'user-7',
|
userId: 'user-7',
|
||||||
@@ -12,7 +12,7 @@ describe('inherit reset turn time Ref compatibility', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.nextTurnTimeBase).toBeCloseTo(302.5143852464758, 12);
|
expect(result.nextTurnTimeBase).toBeCloseTo(302.5143852464758, 12);
|
||||||
expect(result.nextTurnTimeLabel).toBe('00:05');
|
expect(result.nextTurnTimeLabel).toBe('05:02');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the prior pending base as the next deterministic seed input', () => {
|
it('uses the prior pending base as the next deterministic seed input', () => {
|
||||||
|
|||||||
@@ -1905,13 +1905,25 @@ export class InMemoryTurnWorld {
|
|||||||
schedule: this.schedule,
|
schedule: this.schedule,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextTurnAt = result.nextTurnAt ?? getNextTurnAt(currentGeneral.turnTime, this.schedule);
|
let nextTurnAt = result.nextTurnAt ?? getNextTurnAt(currentGeneral.turnTime, this.schedule);
|
||||||
if (!result.deleted?.general) {
|
if (!result.deleted?.general) {
|
||||||
const resolvedGeneral = result.general ?? currentGeneral;
|
const resolvedGeneral = result.general ?? currentGeneral;
|
||||||
const clock = this.getGameClock();
|
const clock = this.getGameClock();
|
||||||
const currentTurnTick = currentGeneral.turnTick ?? clock.dateToTick(currentGeneral.turnTime);
|
const currentTurnTick = currentGeneral.turnTick ?? clock.dateToTick(currentGeneral.turnTime);
|
||||||
const nextTurnTick =
|
let nextTurnTick =
|
||||||
currentTurnTick + (clock.dateToTick(nextTurnAt) - clock.dateToTick(currentGeneral.turnTime));
|
currentTurnTick + (clock.dateToTick(nextTurnAt) - clock.dateToTick(currentGeneral.turnTime));
|
||||||
|
const nextTurnTimeBase = readMetaNumber(resolvedGeneral.meta, 'nextTurnTimeBase');
|
||||||
|
if (nextTurnTimeBase !== null && nextTurnTimeBase >= 0) {
|
||||||
|
// Ref: addTurn → cutTurn → 새 offset. 기존 분·초를 더하지 않고
|
||||||
|
// 논리 월 경계에서 교체해야 다다음 턴과 로그가 일치한다.
|
||||||
|
// Date를 경유하면 난수의 sub-ms tick과 기존 tick 꼬리가 섞인다.
|
||||||
|
nextTurnTick =
|
||||||
|
Math.floor(nextTurnTick / GAME_TICKS_PER_TURN) * GAME_TICKS_PER_TURN +
|
||||||
|
Math.round(nextTurnTimeBase * clock.ticksPerSecond);
|
||||||
|
nextTurnAt = clock.tickToDate(nextTurnTick);
|
||||||
|
resolvedGeneral.meta = { ...resolvedGeneral.meta };
|
||||||
|
delete resolvedGeneral.meta.nextTurnTimeBase;
|
||||||
|
}
|
||||||
const recentWarTimeChanged =
|
const recentWarTimeChanged =
|
||||||
(resolvedGeneral.recentWarTime?.getTime() ?? null) !==
|
(resolvedGeneral.recentWarTime?.getTime() ?? null) !==
|
||||||
(currentGeneral.recentWarTime?.getTime() ?? null);
|
(currentGeneral.recentWarTime?.getTime() ?? null);
|
||||||
|
|||||||
@@ -177,9 +177,9 @@ export const buildResetStatRandomBonus = (
|
|||||||
|
|
||||||
const formatTurnTimeBaseLabel = (value: number): string => {
|
const formatTurnTimeBaseLabel = (value: number): string => {
|
||||||
const wholeSeconds = Math.trunc(value);
|
const wholeSeconds = Math.trunc(value);
|
||||||
const hours = String(Math.trunc(wholeSeconds / 3_600)).padStart(2, '0');
|
const minutes = String(Math.trunc(wholeSeconds / 60)).padStart(2, '0');
|
||||||
const minutes = String(Math.trunc((wholeSeconds % 3_600) / 60)).padStart(2, '0');
|
const seconds = String(wholeSeconds % 60).padStart(2, '0');
|
||||||
return `${hours}:${minutes}`;
|
return `${minutes}:${seconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resolveResetTurnTimeBase = (options: {
|
const resolveResetTurnTimeBase = (options: {
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import {
|
|||||||
resolveUniqueConfig,
|
resolveUniqueConfig,
|
||||||
readScenarioGeneralPoolClaim,
|
readScenarioGeneralPoolClaim,
|
||||||
rollUniqueLotteryDetailed,
|
rollUniqueLotteryDetailed,
|
||||||
getNextTurnAt,
|
|
||||||
getBillByLevel,
|
getBillByLevel,
|
||||||
LEGACY_DEFAULT_MAX_LEVEL,
|
LEGACY_DEFAULT_MAX_LEVEL,
|
||||||
orderLegacyActionLoggerFlush,
|
orderLegacyActionLoggerFlush,
|
||||||
@@ -2196,7 +2195,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
aiDecisionDurationNs: generalAiDecisionDurationNs,
|
aiDecisionDurationNs: generalAiDecisionDurationNs,
|
||||||
actionDurationNs: generalActionDurationNs,
|
actionDurationNs: generalActionDurationNs,
|
||||||
});
|
});
|
||||||
let nextTurnAt = 'nextTurnAt' in generalResult ? generalResult.nextTurnAt : undefined;
|
const nextTurnAt = 'nextTurnAt' in generalResult ? generalResult.nextTurnAt : undefined;
|
||||||
options.reservedTurns.shiftGeneralTurns(currentGeneral.id, -1);
|
options.reservedTurns.shiftGeneralTurns(currentGeneral.id, -1);
|
||||||
|
|
||||||
const worldMeta = asRecord(context.world.meta);
|
const worldMeta = asRecord(context.world.meta);
|
||||||
@@ -2241,13 +2240,6 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
Math.trunc(autorunLimitMinutes / turnMinutes);
|
Math.trunc(autorunLimitMinutes / turnMinutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextTurnTimeBase = readMetaNumber(currentGeneral.meta, 'nextTurnTimeBase', -1);
|
|
||||||
if (nextTurnTimeBase >= 0) {
|
|
||||||
const alignedNextTurn = nextTurnAt ?? getNextTurnAt(currentGeneral.turnTime, context.schedule);
|
|
||||||
nextTurnAt = new Date(alignedNextTurn.getTime() + nextTurnTimeBase * 1000);
|
|
||||||
delete currentGeneral.meta.nextTurnTimeBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
const explicitlyRetired = generalResult.actionKey === 'che_은퇴' && generalResult.completed;
|
const explicitlyRetired = generalResult.actionKey === 'che_은퇴' && generalResult.completed;
|
||||||
let lifecycleOutcome: 'active' | 'detached' | 'deleted' | 'retired' = explicitlyRetired
|
let lifecycleOutcome: 'active' | 'detached' | 'deleted' | 'retired' = explicitlyRetired
|
||||||
? 'retired'
|
? 'retired'
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { LEGACY_RANK_DATA_TYPES } from '@sammo-ts/common';
|
import { GAME_TICKS_PER_TURN, LEGACY_RANK_DATA_TYPES } from '@sammo-ts/common';
|
||||||
import { finalizeLogEntry, type TurnSchedule } from '@sammo-ts/logic';
|
import { finalizeLogEntry, type TurnSchedule } from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
import type { GamePrisma } from '@sammo-ts/infra';
|
||||||
|
import { executeInheritanceAction } from '../src/turn/inheritanceActionService.js';
|
||||||
|
|
||||||
import { rankMetaKey } from '../src/turn/rankData.js';
|
import { rankMetaKey } from '../src/turn/rankData.js';
|
||||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||||
import { createTurnTestHarness } from './helpers/turnTestHarness.js';
|
import { createTurnTestHarness } from './helpers/turnTestHarness.js';
|
||||||
@@ -282,6 +285,74 @@ describe('legacy general turn lifecycle', () => {
|
|||||||
expect(updated.turnTime.toISOString()).toBe('0200-01-01T00:10:30.000Z');
|
expect(updated.turnTime.toISOString()).toBe('0200-01-01T00:10:30.000Z');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies the purchased and logged random offset on the second upcoming turn, then keeps it', async () => {
|
||||||
|
const generalTurnTime = new Date('0200-01-01T00:07:43.000Z');
|
||||||
|
const harness = await createTurnTestHarness({
|
||||||
|
snapshot: makeSnapshot([
|
||||||
|
makeGeneral({
|
||||||
|
userId: 'user-7',
|
||||||
|
turnTime: generalTurnTime,
|
||||||
|
meta: { killturn: 24, nextTurnTimeBase: 123_456 },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
state: makeState({ hiddenSeed: 'hidden-seed' }),
|
||||||
|
schedule,
|
||||||
|
map,
|
||||||
|
});
|
||||||
|
const createLog = vi.fn(async () => ({}));
|
||||||
|
const db = {
|
||||||
|
$queryRaw: vi.fn(async () => [{ value: 100_000 }]),
|
||||||
|
inheritanceLog: { create: createLog },
|
||||||
|
} as unknown as GamePrisma.TransactionClient;
|
||||||
|
const result = await executeInheritanceAction({
|
||||||
|
db,
|
||||||
|
world: harness.world,
|
||||||
|
command: { type: 'inheritanceAction', userId: 'user-7', input: { action: 'resetTurnTime' } },
|
||||||
|
gameNow: start,
|
||||||
|
});
|
||||||
|
expect(result).toMatchObject({ ok: true, nextTurnTimeBase: 302.5143852464758, nextTurnTimeLabel: '05:02' });
|
||||||
|
expect(createLog).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({ text: expect.stringContaining('다다음 턴부터 05:02 적용') }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(harness.world.getGeneralById(1)!.turnTime).toEqual(generalTurnTime);
|
||||||
|
await harness.runOneTick();
|
||||||
|
const updated = harness.world.getGeneralById(1)!;
|
||||||
|
expect(updated.meta.nextTurnTimeBase).toBeUndefined();
|
||||||
|
expect(updated.turnTime.toISOString()).toBe('0200-01-01T00:15:02.514Z');
|
||||||
|
const expectedTick = GAME_TICKS_PER_TURN + Math.round((302.5143852464758 * GAME_TICKS_PER_TURN) / 600);
|
||||||
|
expect(updated.turnTick).toBe(expectedTick);
|
||||||
|
expect(updated.meta.inherit_lived_month).toBe(1);
|
||||||
|
await harness.runOneTick();
|
||||||
|
expect(harness.world.getGeneralById(1)!.turnTick).toBe(expectedTick + GAME_TICKS_PER_TURN);
|
||||||
|
expect(harness.world.getGeneralById(1)!.turnTime.toISOString()).toBe('0200-01-01T00:25:02.514Z');
|
||||||
|
expect(harness.world.getGeneralById(1)!.meta.inherit_lived_month).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([0, 30, 599.999])(
|
||||||
|
'replaces a nonzero old offset with %s seconds at the logical boundary',
|
||||||
|
async (offset) => {
|
||||||
|
const harness = await createTurnTestHarness({
|
||||||
|
snapshot: makeSnapshot([
|
||||||
|
makeGeneral({
|
||||||
|
turnTime: new Date('0200-01-01T00:09:43.123Z'),
|
||||||
|
turnTick: 34_987_381,
|
||||||
|
meta: { killturn: 24, nextTurnTimeBase: offset },
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
state: makeState(),
|
||||||
|
schedule,
|
||||||
|
map,
|
||||||
|
});
|
||||||
|
await harness.runOneTick({ maxGenerals: 1 });
|
||||||
|
expect(harness.world.getGeneralById(1)!.turnTick).toBe(
|
||||||
|
GAME_TICKS_PER_TURN + Math.round((offset * GAME_TICKS_PER_TURN) / 600)
|
||||||
|
);
|
||||||
|
expect(harness.world.getGeneralById(1)!.meta.nextTurnTimeBase).toBeUndefined();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('detaches an expired possessed NPC instead of deleting its body', async () => {
|
it('detaches an expired possessed NPC instead of deleting its body', async () => {
|
||||||
const harness = await createTurnTestHarness({
|
const harness = await createTurnTestHarness({
|
||||||
snapshot: makeSnapshot([
|
snapshot: makeSnapshot([
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import type { TurnDaemonCommand, TurnDaemonCommandResult } from '@sammo-ts/common';
|
import { GAME_TICKS_PER_TURN, type TurnDaemonCommand, type TurnDaemonCommandResult } from '@sammo-ts/common';
|
||||||
import { createGamePostgresConnector, type GamePrisma, type GamePrismaClient } from '@sammo-ts/infra';
|
import { createGamePostgresConnector, type GamePrisma, type GamePrismaClient } from '@sammo-ts/infra';
|
||||||
import type { MapDefinition, ScenarioConfig, ScenarioMeta, TurnSchedule } from '@sammo-ts/logic';
|
import type { MapDefinition, ScenarioConfig, ScenarioMeta, TurnSchedule } from '@sammo-ts/logic';
|
||||||
|
|
||||||
@@ -55,6 +55,14 @@ const state: TurnWorldState = {
|
|||||||
currentMonth: 4,
|
currentMonth: 4,
|
||||||
tickSeconds: 600,
|
tickSeconds: 600,
|
||||||
lastTurnTime: new Date('2026-08-24T00:00:00.000Z'),
|
lastTurnTime: new Date('2026-08-24T00:00:00.000Z'),
|
||||||
|
clockBaseTime: new Date('2026-08-24T00:00:00.000Z'),
|
||||||
|
clockTick: 0,
|
||||||
|
lastTurnTick: 0,
|
||||||
|
clockMode: 'manual',
|
||||||
|
clockPhase: 'MANUAL',
|
||||||
|
clockWallAnchor: new Date('2026-08-24T00:00:00.000Z'),
|
||||||
|
clockRevision: 1,
|
||||||
|
deadlineGeneration: 1,
|
||||||
meta: { hiddenSeed: 'inheritance-atomic-seed', season: 77, isunited: 0, scenarioMeta },
|
meta: { hiddenSeed: 'inheritance-atomic-seed', season: 77, isunited: 0, scenarioMeta },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,6 +191,14 @@ integration('inheritance action PostgreSQL atomic persistence', () => {
|
|||||||
currentYear: state.currentYear,
|
currentYear: state.currentYear,
|
||||||
currentMonth: state.currentMonth,
|
currentMonth: state.currentMonth,
|
||||||
tickSeconds: state.tickSeconds,
|
tickSeconds: state.tickSeconds,
|
||||||
|
clockBaseTime: state.clockBaseTime,
|
||||||
|
clockTick: 0n,
|
||||||
|
lastTurnTick: 0n,
|
||||||
|
clockMode: state.clockMode,
|
||||||
|
clockPhase: state.clockPhase,
|
||||||
|
clockWallAnchor: state.clockWallAnchor,
|
||||||
|
clockRevision: 1n,
|
||||||
|
deadlineGeneration: 1n,
|
||||||
config: JSON.parse(JSON.stringify(scenarioConfig)) as GamePrisma.InputJsonValue,
|
config: JSON.parse(JSON.stringify(scenarioConfig)) as GamePrisma.InputJsonValue,
|
||||||
meta: state.meta as GamePrisma.InputJsonValue,
|
meta: state.meta as GamePrisma.InputJsonValue,
|
||||||
},
|
},
|
||||||
@@ -427,5 +443,51 @@ integration('inheritance action PostgreSQL atomic persistence', () => {
|
|||||||
},
|
},
|
||||||
inheritancePoints: { previous: 5_800 },
|
inheritancePoints: { previous: 5_800 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const oldTurnTime = new Date('2026-08-24T00:17:43.123Z');
|
||||||
|
world.updateGeneral(actorGeneralId, { turnTime: oldTurnTime });
|
||||||
|
const resetCommand = buildCommand('reset-turn', { action: 'resetTurnTime' });
|
||||||
|
await createInputEvent(resetCommand);
|
||||||
|
const resetResult = await execute(resetCommand);
|
||||||
|
if (resetResult.type !== 'inheritanceAction' || !resetResult.ok || resetResult.nextTurnTimeBase === undefined) {
|
||||||
|
throw new Error('Expected a successful reset with its pending offset.');
|
||||||
|
}
|
||||||
|
const offset = resetResult.nextTurnTimeBase;
|
||||||
|
const label = `${String(Math.trunc(offset / 60)).padStart(2, '0')}:${String(Math.trunc(offset) % 60).padStart(2, '0')}`;
|
||||||
|
expect(resetResult.nextTurnTimeLabel).toBe(label);
|
||||||
|
await expect(
|
||||||
|
db.inheritanceLog.findFirstOrThrow({
|
||||||
|
where: { userId: actorUserId, text: { contains: '다다음 턴부터' } },
|
||||||
|
})
|
||||||
|
).resolves.toMatchObject({ text: `1000 포인트로 턴 시간을 바꾸어 다다음 턴부터 ${label} 적용` });
|
||||||
|
const pending = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
|
||||||
|
const pendingGeneral = pending.snapshot.generals.find((general) => general.id === actorGeneralId)!;
|
||||||
|
expect(pendingGeneral.turnTime).toEqual(oldTurnTime);
|
||||||
|
expect(pendingGeneral.meta.nextTurnTimeBase).toBe(offset);
|
||||||
|
expect(pendingGeneral.inheritancePoints?.previous).toBe(4_800);
|
||||||
|
// 저장한 pending을 새 world에서 실행하고 flush/reload한다.
|
||||||
|
const restarted = new InMemoryTurnWorld(pending.state, pending.snapshot, { schedule });
|
||||||
|
restarted.executeGeneralTurn(restarted.getGeneralById(actorGeneralId)!);
|
||||||
|
const expectedTick = 2 * GAME_TICKS_PER_TURN + Math.round((offset * GAME_TICKS_PER_TURN) / 600);
|
||||||
|
expect(restarted.getGeneralById(actorGeneralId)!.turnTick).toBe(expectedTick);
|
||||||
|
const restartedHooks = await createDatabaseTurnHooks(databaseUrl!, restarted);
|
||||||
|
try {
|
||||||
|
await restartedHooks.hooks.flushChanges!({
|
||||||
|
lastTurnTime: pending.state.lastTurnTime.toISOString(),
|
||||||
|
processedGenerals: 1,
|
||||||
|
processedTurns: 0,
|
||||||
|
durationMs: 0,
|
||||||
|
partial: false,
|
||||||
|
});
|
||||||
|
const applied = await loadTurnWorldFromDatabase({ databaseUrl: databaseUrl! });
|
||||||
|
const appliedGeneral = applied.snapshot.generals.find((general) => general.id === actorGeneralId)!;
|
||||||
|
expect(appliedGeneral.turnTick).toBe(expectedTick);
|
||||||
|
expect(appliedGeneral.meta.nextTurnTimeBase).toBeUndefined();
|
||||||
|
const resumed = new InMemoryTurnWorld(applied.state, applied.snapshot, { schedule });
|
||||||
|
resumed.executeGeneralTurn(resumed.getGeneralById(actorGeneralId)!);
|
||||||
|
expect(resumed.getGeneralById(actorGeneralId)!.turnTick).toBe(expectedTick + GAME_TICKS_PER_TURN);
|
||||||
|
} finally {
|
||||||
|
await restartedHooks.close();
|
||||||
|
}
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -214,6 +214,50 @@ describe('inheritance action service', () => {
|
|||||||
expect(createLog).toHaveBeenCalled();
|
expect(createLog).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('draws once per successful reset, chains pending seeds, and does not draw on insufficient points', async () => {
|
||||||
|
const draw = vi.spyOn(LiteHashDRBG.prototype, 'nextFloat1');
|
||||||
|
try {
|
||||||
|
const world = buildWorld({});
|
||||||
|
const { db } = buildDatabase();
|
||||||
|
const before = world.getGeneralById(1)!.turnTime;
|
||||||
|
const first = await execute(world, db, { action: 'resetTurnTime' });
|
||||||
|
const second = await execute(world, db, { action: 'resetTurnTime' });
|
||||||
|
if (!first.ok || !second.ok) throw new Error('Expected successful resets');
|
||||||
|
expect(draw).toHaveBeenCalledTimes(2);
|
||||||
|
expect(second.nextTurnTimeBase).not.toBe(first.nextTurnTimeBase);
|
||||||
|
expect(world.getGeneralById(1)!.turnTime).toEqual(before);
|
||||||
|
expect(world.getGeneralById(1)!.meta.nextTurnTimeBase).toBe(second.nextTurnTimeBase);
|
||||||
|
const failedDb = buildDatabase({ point: 0 });
|
||||||
|
await expect(execute(world, failedDb.db, { action: 'resetTurnTime' })).resolves.toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
reason: '충분한 유산 포인트를 가지고 있지 않습니다.',
|
||||||
|
});
|
||||||
|
expect(draw).toHaveBeenCalledTimes(2);
|
||||||
|
expect(failedDb.createLog).not.toHaveBeenCalled();
|
||||||
|
expect(world.getGeneralById(1)!.meta.nextTurnTimeBase).toBe(second.nextTurnTimeBase);
|
||||||
|
} finally {
|
||||||
|
draw.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('samples the whole turn interval reproducibly across fixed hidden seeds', async () => {
|
||||||
|
const offsets: number[] = [];
|
||||||
|
for (let seed = 0; seed < 64; seed++) {
|
||||||
|
const world = buildWorld({ worldMeta: { hiddenSeed: `reset-sample-${seed}` } });
|
||||||
|
const result = await execute(world, buildDatabase().db, { action: 'resetTurnTime' });
|
||||||
|
if (!result.ok || result.nextTurnTimeBase === undefined) throw new Error('Expected a sampled offset');
|
||||||
|
expect(result.nextTurnTimeBase).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(result.nextTurnTimeBase).toBeLessThanOrEqual(3_600);
|
||||||
|
offsets.push(result.nextTurnTimeBase);
|
||||||
|
}
|
||||||
|
expect(new Set(offsets).size).toBe(64);
|
||||||
|
expect(new Set(offsets.map((offset) => Math.floor(offset / 900))).size).toBe(4);
|
||||||
|
const replay = await execute(buildWorld({ worldMeta: { hiddenSeed: 'reset-sample-0' } }), buildDatabase().db, {
|
||||||
|
action: 'resetTurnTime',
|
||||||
|
});
|
||||||
|
expect(replay).toMatchObject({ nextTurnTimeBase: offsets[0] });
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps free ResetStat at zero spend and uses the Ref-compatible fixed-seed bonus', async () => {
|
it('keeps free ResetStat at zero spend and uses the Ref-compatible fixed-seed bonus', async () => {
|
||||||
const world = buildWorld({});
|
const world = buildWorld({});
|
||||||
const { db } = buildDatabase({ point: 0 });
|
const { db } = buildDatabase({ point: 0 });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
import { dirname, extname, resolve } from 'node:path';
|
import { dirname, extname, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ const installFixture = async (
|
|||||||
}
|
}
|
||||||
if (name === 'inherit.resetTurnTime') {
|
if (name === 'inherit.resetTurnTime') {
|
||||||
resetTurnMutationCount += 1;
|
resetTurnMutationCount += 1;
|
||||||
return response({ ok: true, nextTurnTimeBase: 302.5143852464758, nextTurnTimeLabel: '00:05' });
|
return response({ ok: true, nextTurnTimeBase: 302.5143852464758, nextTurnTimeLabel: '05:02' });
|
||||||
}
|
}
|
||||||
if (name === 'inherit.openUniqueAuction') {
|
if (name === 'inherit.openUniqueAuction') {
|
||||||
uniqueAuctionRequests.push(requestBody);
|
uniqueAuctionRequests.push(requestBody);
|
||||||
@@ -252,8 +252,25 @@ test.describe('inheritance management legacy parity', () => {
|
|||||||
});
|
});
|
||||||
await button.click();
|
await button.click();
|
||||||
|
|
||||||
await expect(item).toContainText('적용 시간: 00:05');
|
await expect(item).toContainText('적용 시간: 05:02');
|
||||||
expect(fixture.resetTurnMutationCount()).toBe(1);
|
expect(fixture.resetTurnMutationCount()).toBe(1);
|
||||||
|
if (artifactRoot) {
|
||||||
|
await page.screenshot({ path: resolve(artifactRoot, 'inherit-reset-turn.png'), fullPage: true });
|
||||||
|
await writeFile(
|
||||||
|
resolve(artifactRoot, 'inherit-reset-turn.json'),
|
||||||
|
JSON.stringify(
|
||||||
|
await item.evaluate((element) => ({
|
||||||
|
text: element.textContent,
|
||||||
|
html: element.outerHTML,
|
||||||
|
rect: element.getBoundingClientRect().toJSON(),
|
||||||
|
font: getComputedStyle(element).font,
|
||||||
|
buttonDisabled: element.querySelector('button')?.disabled,
|
||||||
|
})),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('matches the ref 1000px grid and computed styles on desktop and mobile', async ({ page }) => {
|
test('matches the ref 1000px grid and computed styles on desktop and mobile', async ({ page }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user