From a2bb667b57ef6a1503834d4f615ad51d0d8134f3 Mon Sep 17 00:00:00 2001 From: Hide_D Date: Tue, 3 Feb 2026 18:37:11 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20acquireType=EC=9D=84=20UniqueAcquireTyp?= =?UTF-8?q?e=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/logic/src/rewards/uniqueLottery.ts | 12 ++-- packages/logic/test/uniqueLottery.test.ts | 66 +++++++++++++++++++-- 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/packages/logic/src/rewards/uniqueLottery.ts b/packages/logic/src/rewards/uniqueLottery.ts index 4448670..fc0a9b3 100644 --- a/packages/logic/src/rewards/uniqueLottery.ts +++ b/packages/logic/src/rewards/uniqueLottery.ts @@ -4,6 +4,9 @@ import type { ItemModule } from '../items/types.js'; export type UniqueItemPool = Record>; +export const UNIQUE_ACQUIRE_TYPES = ['아이템', '설문조사', '랜덤 임관', '건국'] as const; +export type UniqueAcquireType = typeof UNIQUE_ACQUIRE_TYPES[number]; + export type UniqueLotteryConfig = { allItems: UniqueItemPool; maxUniqueItemLimit: Array<[number, number]>; @@ -25,7 +28,7 @@ export type UniqueLotteryInput = { startYear: number; initYear: number; initMonth: number; - acquireType?: string; + acquireType?: UniqueAcquireType; inheritRandomUnique?: boolean; }; @@ -156,6 +159,7 @@ export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => { acquireType, inheritRandomUnique, } = input; + const resolvedAcquireType = acquireType ?? '아이템'; if (userCount <= 0) { return null; @@ -215,9 +219,9 @@ export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => { prob = 1 / (userCount * itemTypeCnt); } - if (acquireType === '설문조사') { + if (resolvedAcquireType === '설문조사') { prob = 1 / (userCount * itemTypeCnt * 0.7 / 3); - } else if (acquireType === '랜덤 임관') { + } else if (resolvedAcquireType === '랜덤 임관') { prob = 1 / (userCount * itemTypeCnt / 10 / 2); } @@ -231,7 +235,7 @@ export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => { if (inheritRandomUnique && availableBuyUnique) { prob = 1; - } else if (acquireType === '건국') { + } else if (resolvedAcquireType === '건국') { prob = 1; } diff --git a/packages/logic/test/uniqueLottery.test.ts b/packages/logic/test/uniqueLottery.test.ts index e61e0cb..dc9920f 100644 --- a/packages/logic/test/uniqueLottery.test.ts +++ b/packages/logic/test/uniqueLottery.test.ts @@ -4,6 +4,7 @@ import { LiteHashDRBG, RandUtil } from '@sammo-ts/common'; import type { GeneralItemSlots } from '../src/domain/entities.js'; import type { ItemModule } from '../src/items/types.js'; import { + type UniqueAcquireType, buildVoteUniqueSeed, countOccupiedUniqueItems, resolveUniqueConfig, @@ -24,11 +25,11 @@ const buildItem = (key: string, slot: ItemModule['slot'], buyable = false): Item }); describe('unique lottery', () => { - it('returns deterministic item for fixed seed', () => { - const itemRegistry = new Map([ - ['itemB', buildItem('itemB', 'weapon', false)], - ]); - const config = resolveUniqueConfig({ + const buildRegistry = (): Map => + new Map([['itemB', buildItem('itemB', 'weapon', false)]]); + + const buildConfig = (overrides?: Partial>) => + resolveUniqueConfig({ allItems: { weapon: { itemB: 1, @@ -38,8 +39,13 @@ describe('unique lottery', () => { uniqueTrialCoef: 10, maxUniqueTrialProb: 10, minMonthToAllowInheritItem: 0, + ...(overrides ?? {}), }); + it('returns deterministic item for fixed seed', () => { + const itemRegistry = buildRegistry(); + const config = buildConfig(); + const rngSeed = buildVoteUniqueSeed('seed', 1, 1); const rng = new RandUtil(LiteHashDRBG.build(rngSeed)); const result = rollUniqueLottery({ @@ -61,6 +67,56 @@ describe('unique lottery', () => { expect(result).toBe('itemB'); }); + it('supports acquire types that can yield unique items', () => { + const itemRegistry = buildRegistry(); + const config = buildConfig(); + const acquireTypes: UniqueAcquireType[] = ['아이템', '설문조사', '랜덤 임관']; + + for (const acquireType of acquireTypes) { + const rng = new RandUtil(LiteHashDRBG.build(buildVoteUniqueSeed('seed', 2, 2))); + const result = rollUniqueLottery({ + rng, + config, + itemRegistry, + generalItems: { horse: null, weapon: null, book: null, item: null }, + occupiedUniqueCounts: new Map(), + scenarioId: 200, + userCount: 1, + currentYear: 200, + currentMonth: 1, + startYear: 180, + initYear: 180, + initMonth: 1, + acquireType, + }); + + expect(result).toBe('itemB'); + } + }); + + it('guarantees unique on founding acquire type', () => { + const itemRegistry = buildRegistry(); + const config = buildConfig({ uniqueTrialCoef: 0, maxUniqueTrialProb: 0 }); + const rng = new RandUtil(LiteHashDRBG.build(buildVoteUniqueSeed('seed', 3, 3))); + const result = rollUniqueLottery({ + rng, + config, + itemRegistry, + generalItems: { horse: null, weapon: null, book: null, item: null }, + occupiedUniqueCounts: new Map(), + scenarioId: 200, + userCount: 1, + currentYear: 200, + currentMonth: 1, + startYear: 180, + initYear: 180, + initMonth: 1, + acquireType: '건국', + }); + + expect(result).toBe('itemB'); + }); + it('counts only non-buyable equipped items', () => { const itemRegistry = new Map([ ['uniqueItem', buildItem('uniqueItem', 'weapon', false)],