feat: complete legacy-compatible auction system

This commit is contained in:
2026-07-25 10:43:49 +00:00
parent 26c3d5ce16
commit 93ae4df519
21 changed files with 1355 additions and 216 deletions
+52
View File
@@ -0,0 +1,52 @@
import { LiteHashDRBG, RandUtil } from '@sammo-ts/common';
import { simpleSerialize } from '../war/utils.js';
const DEFAULT_FIRST_NAMES = [
'가', '간', '감', '강', '고', '공', '공손', '곽', '관', '괴', '교', '금', '노', '뇌', '능', '도', '동',
'두', '등', '마', '맹', '문', '미', '반', '방', '부', '비', '사', '사마', '서', '설', '성', '소', '손',
'송', '순', '신', '심', '악', '안', '양', '엄', '여', '염', '오', '왕', '요', '우', '원', '위', '유',
'육', '윤', '이', '장', '저', '전', '정', '제갈', '조', '종', '주', '진', '채', '태사', '하', '하후',
'학', '한', '향', '허', '호', '화', '황', '공손', '손', '왕', '유', '장', '조',
] as const;
const DEFAULT_LAST_NAMES = [
'가', '간', '강', '거', '건', '검', '견', '경', '공', '광', '권', '규', '녕', '단', '대', '도', '등',
'람', '량', '례', '로', '료', '모', '민', '박', '범', '보', '비', '사', '상', '색', '서', '소', '속',
'송', '수', '순', '습', '승', '양', '연', '영', '온', '옹', '완', '우', '웅', '월', '위', '유', '윤',
'융', '이', '익', '임', '정', '제', '조', '주', '준', '지', '찬', '책', '충', '탁', '택', '통', '패',
'평', '포', '합', '해', '혁', '현', '화', '환', '회', '횡', '후', '훈', '휴', '흠', '흥',
] as const;
const readNameParts = (value: unknown, fallback: readonly string[]): string[] => {
if (!Array.isArray(value)) {
return [...fallback];
}
const result = value.filter((entry): entry is string => typeof entry === 'string');
return result.length > 0 ? result : [...fallback];
};
export const buildAuctionAlias = (
generalId: number,
hiddenSeed: string | number,
configConst: Record<string, unknown> = {}
): string => {
const firstNames = readNameParts(configConst.randGenFirstName, DEFAULT_FIRST_NAMES);
const middleNames = readNameParts(configConst.randGenMiddleName, ['']);
const lastNames = readNameParts(configConst.randGenLastName, DEFAULT_LAST_NAMES);
const pool: string[] = [];
for (const first of firstNames) {
for (const middle of middleNames) {
for (const last of lastNames) {
pool.push(`${first}${middle}${last}`);
}
}
}
const shuffled = new RandUtil(
new LiteHashDRBG(simpleSerialize(hiddenSeed, 'obfuscatedNamePool'))
).shuffle(pool);
const normalizedId = Math.max(0, Math.floor(generalId));
const duplicateIndex = Math.floor(normalizedId / shuffled.length);
const name = shuffled[normalizedId % shuffled.length] ?? `익명${normalizedId}`;
return duplicateIndex === 0 ? name : `${name}${duplicateIndex}`;
};
+1
View File
@@ -1,6 +1,7 @@
export * from './domain/entities.js';
export type { RandomGenerator } from '@sammo-ts/common';
export * from './actions/index.js';
export * from './auction/alias.js';
export * from './constraints/index.js';
export * from './crewType/index.js';
export * from './diplomacy/index.js';
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { buildAuctionAlias } from '../src/auction/alias.js';
describe('buildAuctionAlias', () => {
it('returns a stable alias for the same world seed and general id', () => {
const first = buildAuctionAlias(17, 'legacy-compatible-seed');
const second = buildAuctionAlias(17, 'legacy-compatible-seed');
expect(second).toBe(first);
expect(first.length).toBeGreaterThan(1);
});
it('uses scenario-specific name pools without exposing a general name', () => {
const config = {
randGenFirstName: ['청'],
randGenMiddleName: ['운'],
randGenLastName: ['객', '상'],
};
expect(buildAuctionAlias(0, 'seed', config)).toMatch(/^청운(객|상)$/);
expect(buildAuctionAlias(1, 'seed', config)).toMatch(/^청운(객|상)$/);
expect(buildAuctionAlias(2, 'seed', config)).toMatch(/^청운(객|상)1$/);
});
});