add: character

This commit is contained in:
Vick Scarlet
2026-08-16 20:06:16 +08:00
committed by 神戸小鳥
parent 46a8528864
commit afb4276dd1
18 changed files with 194 additions and 79 deletions
+61
View File
@@ -0,0 +1,61 @@
import { expect, test, describe } from 'bun:test'
import { pullCharacter, uniqueGenerate } from './character'
import { enableMapSet } from 'immer'
enableMapSet()
describe('Character', () => {
const sum = (map: Map<any, number>) =>
Array.from(map.values()).reduce((acc, val) => acc + val, 0)
test('pull', () => {
let result = pullCharacter(
{ count: 10, knife: 10 },
{ times: 0, drawns: new Map() },
)
expect(result.characters).not.toContainValue(null)
expect(result.characters.length).toBe(10)
expect(result.times.times).toBe(10)
expect(result.times.drawns.size).toBe(10)
result = pullCharacter({ count: 3, knife: 10 }, result.times)
expect(result.characters).not.toContainValue(null)
expect(result.characters.length).toBe(3)
expect(result.times.times).toBe(13)
expect(sum(result.times.drawns)).toBe(13)
result = pullCharacter({ count: 3, knife: 10 }, result.times)
expect(result.characters).not.toContainValue(null)
expect(result.characters.length).toBe(3)
expect(result.times.times).toBe(16)
expect(sum(result.times.drawns)).toBe(16)
result = pullCharacter({ count: 20, knife: 1 }, result.times)
expect(result.characters).not.toContainValue(null)
expect(result.characters.length).toBe(20)
expect(result.times.times).toBe(36)
expect(sum(result.times.drawns)).toBe(36)
})
const wr = (s: number, e: number) => {
const length = e - s + 1
return Array.from(
{ length },
(_, i) => [s + i, Math.min(i + 1, length - i)] as const,
)
}
test('unique', () => {
const unique = uniqueGenerate({ prop: wr(0, 10), talent: wr(1, 5) })
expect(unique.property).toHaveProperty('CHR')
expect(unique.property).toHaveProperty('INT')
expect(unique.property).toHaveProperty('STR')
expect(unique.property).toHaveProperty('MNY')
expect(unique.property.CHR).toBeGreaterThanOrEqual(0)
expect(unique.property.INT).toBeGreaterThanOrEqual(0)
expect(unique.property.STR).toBeGreaterThanOrEqual(0)
expect(unique.property.MNY).toBeGreaterThanOrEqual(0)
expect(unique.talent.length).toBeGreaterThanOrEqual(0)
expect(unique.property.CHR).toBeLessThanOrEqual(10)
expect(unique.property.INT).toBeLessThanOrEqual(10)
expect(unique.property.STR).toBeLessThanOrEqual(10)
expect(unique.property.MNY).toBeLessThanOrEqual(10)
expect(unique.talent.length).toBeLessThanOrEqual(5)
console.debug(unique)
})
})
+71
View File
@@ -0,0 +1,71 @@
import { type Character, type Talent, characters, talents } from '@remake/data'
import { type RNG, type WeightItem, pick, pickWeight } from '@remake/vitex'
export type UniqueChara = Omit<Character, 'id' | 'name'>
export interface UniqueGenCfg {
prop: WeightItem<number>[]
talent: WeightItem<number>[]
}
export function uniqueGenerate(config: UniqueGenCfg, rng?: RNG) {
const CHR = pickWeight(config.prop, rng) ?? 0
const INT = pickWeight(config.prop, rng) ?? 0
const STR = pickWeight(config.prop, rng) ?? 0
const MNY = pickWeight(config.prop, rng) ?? 0
let count = pickWeight(config.talent, rng) ?? 0
const property = { CHR, INT, STR, MNY }
const ids = Array.from(talents.keys())
const picked = new Set<Talent['id']>()
while (count > 0) {
const t = pick(ids, rng)!
if (picked.has(t)) continue
if (talents.get(t)!.exclusive) continue
picked.add(t)
count--
}
return { property, talent: Array.from(picked) } satisfies UniqueChara
}
export interface PullCharaOpt {
count: number
knife: number
}
export interface PullCharaTms {
times: number
drawns: Map<Character['id'], number>
}
export interface PullCharaRet {
characters: Character['id'][]
times: PullCharaTms
}
export function pullCharacter(
opt: PullCharaOpt,
tms: PullCharaTms,
rng?: RNG,
): PullCharaRet {
const { count, knife } = opt
const drawns = new Map(tms.drawns)
const picked = new Set<Character['id']>()
for (let i = 0; i < count; i++) {
const weightMap = deriveWeightMap(tms.times + i, knife, drawns)
picked.forEach(id => weightMap.delete(id))
const id = pickWeight(Array.from(weightMap.entries()), rng)!
picked.add(id)
drawns.set(id, (drawns.get(id) ?? 0) + 1)
}
return {
characters: Array.from(picked),
times: { times: tms.times + count, drawns },
}
}
function deriveWeightMap(
times: number,
knife: number,
drawns: PullCharaTms['drawns'],
): Map<Character['id'], number> {
const max = Math.max(0, ...drawns.values())
const base = times - knife * Math.floor((times - max) / (knife || 1)) || 1
return new Map(
Array.from(characters.keys(), id => [id, base - (drawns.get(id) ?? 0)]),
)
}
+3 -5
View File
@@ -1,11 +1,9 @@
import type { Event } from '@remake/data/event'
import events from '@remake/data/event'
import type { Properties } from './state'
import type { GameState, ProfileState } from './state'
import { type Event, events } from '@remake/data'
import type { Properties, GameState, ProfileState } from './state'
import { propsEffect, createFlatState } from './state'
import { check as checkCondition } from '@remake/condition'
import { produce } from 'immer'
import type { TriggerResult } from './game'
import { produce } from 'immer'
export function check(
event: Event['id'],
+2 -2
View File
@@ -1,6 +1,6 @@
import { expect, test, describe } from 'bun:test'
import { createState, propsEffect, summary as stateSummary } from './state'
import { talentsPicked, start, next, summary, end } from './game'
import { pick, start, next, summary, end } from './game'
import { produce } from 'immer'
import { enableMapSet } from 'immer'
enableMapSet()
@@ -22,7 +22,7 @@ describe('Achievement', () => {
test('talentsPicked', () => {
// 挑战者、阴间福袋、轮盘赌
const result = talentsPicked([1122, 1145, 1146])
const result = pick([1122, 1145, 1146])
expect(result.talents.chains.size).toBeGreaterThan(1)
expect(result.additionalPoints.source).toContainEqual({
talent: 1122,
+4 -12
View File
@@ -1,11 +1,9 @@
import type { Achievement, Event, Talent } from '@remake/data'
import { ages, AchievementOpportunity as Ao } from '@remake/data'
import type { Allocation, GameState, ProfileState } from './state'
import type { Properties } from './state'
import type { GameState, ProfileState } from './state'
import { createState, nextProfile, propsEffect } from './state'
import { summary as stateSummary } from './state'
import type { PullOptions, ReplacementResult } from './talent'
import type { AdditionalPoint, AdditionalPoints } from './talent'
import type { ReplacementResult, AdditionalPoints } from './talent'
import { pull, exclude, replacement, additionalPoints } from './talent'
import { trigger as ttr } from './talent'
import { trigger as atr } from './achievement'
@@ -19,14 +17,11 @@ export interface TriggerResult<T> {
state: GameState
triggers: T[]
}
export interface TalentsPickedResult {
export interface PickResult {
talents: ReplacementResult
additionalPoints: AdditionalPoints
}
export function talentsPicked(
talents: Iterable<Talent['id']>,
rng?: RNG,
): TalentsPickedResult {
export function pick(talents: Iterable<Talent['id']>, rng?: RNG): PickResult {
const r = replacement(talents, rng)
const ap = additionalPoints(r.talents)
return { talents: r, additionalPoints: ap }
@@ -110,6 +105,3 @@ export function end(
}
export { pull, exclude }
export type { GameState, ProfileState, Properties, RNG }
export type { PullOptions, ReplacementResult, Allocation }
export type { AdditionalPoint, AdditionalPoints }
+10 -1
View File
@@ -1 +1,10 @@
export * from './game'
export type { RNG } from '@remake/vitex'
export type { GameState, ProfileState, Properties, Allocation } from './state'
export type { PullOptions, ReplacementResult } from './talent'
export type { AdditionalPoint, AdditionalPoints } from './talent'
export type { PullCharaOpt, PullCharaTms, PullCharaRet } from './character'
export type { UniqueChara, UniqueGenCfg } from './character'
export type { PickResult, StartResult } from './game'
export type { NextResult, SummaryResult, EndResult } from './game'
export { pull, exclude } from './talent'
export { pick, start, next, summary, end } from './game'
+1 -5
View File
@@ -1,4 +1,4 @@
import type { Achievement, Event, Talent, Character } from '@remake/data'
import type { Achievement, Event, Talent } from '@remake/data'
import { produce } from 'immer'
import { sum, keys } from '@remake/vitex'
@@ -37,9 +37,6 @@ export interface GameState {
talentTriggers: Map<Talent['id'], number> // 本局天赋触发次数
}
/** 唯一角色 */
export type UniqueCharacter = Omit<Character, 'id' | 'name'>
/** 持久化存储的数据 */
export interface ProfileState {
times: number // 游戏次数
@@ -49,7 +46,6 @@ export interface ProfileState {
achievements: Set<Achievement['id']> // 达成的成就
highest?: Properties // 历史最高属性
lowest?: Properties // 历史最低属性
unique?: UniqueCharacter // 唯一角色属性
}
export function createState(
+2 -4
View File
@@ -1,7 +1,5 @@
import type { Talent, TalentGrade } from '@remake/data'
import { talents } from '@remake/data'
import type { Properties } from './state'
import type { GameState, ProfileState } from './state'
import { type Talent, type TalentGrade, talents } from '@remake/data'
import type { Properties, GameState, ProfileState } from './state'
import { propsEffect, createFlatState } from './state'
import { check } from '@remake/condition'
import { pick, pickWeight, type RNG } from '@remake/vitex'