mirror of
https://github.com/VickScarlet/lifeRestart.git
synced 2026-08-28 09:16:47 +08:00
add: character
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)]),
|
||||
)
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user