Files
core2026/app/game-engine/test/centennialAllStarPool.test.ts
T
Hide_D 85591c68ad fix: Ref 게임 로직과 시나리오 풀 호환을 보정
월 경계, 전투 기술 상한, 연감과 베팅·설문·경매 정산 순서를 Ref 계약에 맞춘다.\n\n시나리오 일반 풀을 ENGINE mutation과 logical tick 기반으로 직렬화하고 914·915 catalog 및 조건부 100기 pool 실행 경계를 추가한다.\n\n경매 worker는 세대별 durable event만 만들고 ENGINE이 row lock 후 상태 전이와 정산을 단일 transaction으로 소유한다.
2026-08-23 16:26:14 +00:00

116 lines
4.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { LiteHashDRBG, RandUtil } from '@sammo-ts/common';
import { loadGeneralPoolEntries } from '../src/scenario/generalPoolLoader.js';
import {
buildSelectPoolSeed,
calculateSelectionCandidateWeight,
claimWeightedSelectionCandidates,
type SelectPoolCandidateInfo,
} from '../src/turn/selectPoolService.js';
const toCandidate = (info: Record<string, unknown>): SelectPoolCandidateInfo => {
const dex = info.dex;
if (
typeof info.uniqueName !== 'string' ||
typeof info.generalName !== 'string' ||
typeof info.leadership !== 'number' ||
typeof info.strength !== 'number' ||
typeof info.intel !== 'number' ||
(info.specialDomestic !== null && typeof info.specialDomestic !== 'string') ||
!Array.isArray(dex) ||
dex.length !== 5 ||
dex.some((value) => typeof value !== 'number') ||
(info.imgsvr !== 0 && info.imgsvr !== 1) ||
typeof info.picture !== 'string'
) {
throw new Error('invalid SPoolUnderU100 test entry');
}
return {
uniqueName: info.uniqueName,
generalName: info.generalName,
leadership: info.leadership,
strength: info.strength,
intel: info.intel,
specialDomestic: info.specialDomestic,
dex: dex as [number, number, number, number, number],
imgsvr: info.imgsvr,
picture: info.picture,
};
};
describe('SPoolUnderU100 deterministic selection', () => {
it('uses the Ref user/NPC weight contract including the zero-dex floor', () => {
const candidate = toCandidate({
uniqueName: 'A1000001',
generalName: 'weight fixture',
leadership: 70,
strength: 60,
intel: 60,
specialDomestic: null,
dex: [0, 0, 0, 0, 0],
imgsvr: 0,
picture: '0',
});
expect(calculateSelectionCandidateWeight('SPoolUnderU100', candidate, false)).toBe(100_000);
expect(calculateSelectionCandidateWeight('SPoolUnderU100', candidate, true)).toBe(150_000);
});
it('keeps the fixed-seed 14-candidate draw stable', async () => {
const entries = await loadGeneralPoolEntries('SPoolUnderU100');
const rows = entries.map((entry, index) => ({ id: index + 1, ...entry }));
const rng = new RandUtil(new LiteHashDRBG(buildSelectPoolSeed('s100-vector-hidden', 42, 72_000_000)));
const draws: string[] = [];
const selected = await claimWeightedSelectionCandidates({
weighted: rows.map((row) => [
row,
calculateSelectionCandidateWeight('SPoolUnderU100', toCandidate(row.info), true),
]),
rng,
count: 14,
claim: async () => true,
onDraw: (candidate) => draws.push(candidate.uniqueName),
});
expect(selected.map((candidate) => candidate.uniqueName)).toEqual([
'A1004478',
'A1000583',
'A1002480',
'A1001485',
'A1002714',
'A1004544',
'A1000918',
'A1004549',
'A1003871',
'A1002678',
'A1000531',
'A1003379',
'A1004275',
'A1003449',
]);
expect(draws).toEqual(selected.map((candidate) => candidate.uniqueName));
});
it('keeps duplicate draws in the RNG stream while claiming each row once', async () => {
const candidates = [
{ id: 1, uniqueName: 'first' },
{ id: 2, uniqueName: 'second' },
];
const draws: string[] = [];
const selected = await claimWeightedSelectionCandidates({
weighted: [
[candidates[0]!, 3],
[candidates[1]!, 1],
],
rng: new RandUtil(new LiteHashDRBG('s100-duplicate-retry-vector')),
count: 2,
claim: async () => true,
onDraw: (candidate) => draws.push(candidate.uniqueName),
});
expect(draws).toEqual(['first', 'first', 'first', 'first', 'first', 'first', 'first', 'second']);
expect(selected.map((candidate) => candidate.uniqueName)).toEqual(['first', 'second']);
});
});