diff --git a/packages/core/src/achievement.spec.ts b/packages/core/src/achievement.spec.ts new file mode 100644 index 0000000..9ad4795 --- /dev/null +++ b/packages/core/src/achievement.spec.ts @@ -0,0 +1,33 @@ +import { expect, test, describe } from 'bun:test' +import { createState } from './state' +import { trigger } from './achievement' +import { enableMapSet } from 'immer' +enableMapSet() + +describe('Achievement', () => { + const profile = { + times: 0, + talents: new Set([]), + achievements: new Set([]), + events: new Set([]), + } + const allocation = { + charm: 5, + intelligence: 5, + strength: 5, + money: 5, + } + + test('trigger [times:500]', () => { + const p = { ...profile, times: 500 } + const result = trigger('END', createState(allocation, []), p) + expect(result.state.achievements).toContain(101) // 既视感 + expect(result.state.achievements).toContain(102) // 孟婆愁 + expect(result.state.achievements).toContain(103) // 所有人都是我 + expect(result.state.achievements).toContain(104) // Rewrite + expect(result.triggers).toContain(101) + expect(result.triggers).toContain(102) + expect(result.triggers).toContain(103) + expect(result.triggers).toContain(104) + }) +}) diff --git a/packages/core/src/achievement.ts b/packages/core/src/achievement.ts new file mode 100644 index 0000000..ceb0010 --- /dev/null +++ b/packages/core/src/achievement.ts @@ -0,0 +1,33 @@ +import type { + Achievement, + AchievementOpportunity, +} from '@remake/data/achievement' +import achievements from '@remake/data/achievement' +import type { GameState, ProfileState } from '@/state' +import { createFlatState } from './state' +import { check } from '@remake/condition' +import { produce } from 'immer' +import type { TriggerResult } from '@/game' + +const OpportunityMap = Map.groupBy( + achievements.keys(), + a => achievements.get(a)!.opportunity, +) + +export function trigger( + opportunity: AchievementOpportunity, + state: GameState, + profile: ProfileState, +): TriggerResult { + const flatState = createFlatState(state, profile) + const triggers = OpportunityMap.get(opportunity)!.filter(a => { + if (state.achievements.has(a)) return false + if (profile.achievements.has(a)) return false + const { condition } = achievements.get(a)! + return check(flatState, condition) + }) + const newState = produce(state, draft => { + draft.achievements = new Set([...draft.achievements, ...triggers]) + }) + return { state: newState, triggers } +} diff --git a/packages/core/src/event.spec.ts b/packages/core/src/event.spec.ts index e3e4008..7ccf126 100644 --- a/packages/core/src/event.spec.ts +++ b/packages/core/src/event.spec.ts @@ -26,13 +26,13 @@ describe('Event', () => { ) expect(result.state.life).toBe(1) expect(result.state.events).toEqual(new Set([10003, 10004])) - expect(result.events).toEqual([10003, 10004]) + expect(result.triggers).toEqual([10003, 10004]) }) test('branch 10003:1 [死了]', () => { const result = trigger(10003, createState(allocation, []), profile) expect(result.state.life).toBe(0) expect(result.state.events).toEqual(new Set([10003, 10000])) - expect(result.events).toEqual([10003, 10000]) + expect(result.triggers).toEqual([10003, 10000]) }) }) diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 69bae86..22119cd 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -3,22 +3,31 @@ import events from '@remake/data/event' import type { Properties } from '@/state' import type { GameState, ProfileState } from '@/state' import { propsEffect, createFlatState } from '@/state' -import { check } from '@remake/condition' +import { check as checkCondition } from '@remake/condition' import { produce } from 'immer' +import type { TriggerResult } from '@/game' -export interface TriggerResult { - state: GameState - events: Event['id'][] +export function check( + event: Event['id'], + state: GameState, + profile: ProfileState, +) { + const { include, exclude, NoRandom } = events.get(event)! + if (NoRandom) return false + const flatState = createFlatState(state, profile) + if (exclude && checkCondition(flatState, exclude)) return false + if (include) return checkCondition(flatState, include) + return true } export function trigger( - eventId: number, + event: number, state: GameState, profile: ProfileState, -): TriggerResult { - const { effect, branch } = events.get(eventId)! +): TriggerResult { + const { effect, branch } = events.get(event)! const newState = produce(state, draft => { - draft.events.add(eventId) + draft.events.add(event) if (!effect) return if (effect.LIF) draft.life += effect.LIF const pe = {} as Partial @@ -32,14 +41,14 @@ export function trigger( const flatState = createFlatState(newState, profile) if (branch) { for (const { condition, event } of branch) { - if (check(flatState, condition)) { + if (checkCondition(flatState, condition)) { const result = trigger(event, newState, profile) return { state: result.state, - events: [eventId, ...result.events], + triggers: [event, ...result.triggers], } } } } - return { state: newState, events: [eventId] } + return { state: newState, triggers: [event] } } diff --git a/packages/core/src/game.spec.ts b/packages/core/src/game.spec.ts new file mode 100644 index 0000000..c81d1ab --- /dev/null +++ b/packages/core/src/game.spec.ts @@ -0,0 +1,104 @@ +import { expect, test, describe } from 'bun:test' +import { createState, propsEffect, summary as stateSummary } from './state' +import { talentsPicked, start, next, summary, end } from './game' +import { produce } from 'immer' +import { enableMapSet } from 'immer' +enableMapSet() + +describe('Achievement', () => { + const profile = { + times: 0, + talents: new Set([]), + achievements: new Set([]), + events: new Set([]), + } + const allocation = { + charm: 5, + intelligence: 5, + strength: 5, + money: 5, + } + + test('talentsPicked', () => { + // 挑战者、阴间福袋、轮盘赌 + const result = talentsPicked([1122, 1145, 1146]) + expect(result.talents.chains.size).toBeGreaterThan(1) + expect(result.additionalPoints.source).toContainEqual({ + talent: 1122, + points: -20, + }) + }) + + test('start', () => { + const result = start(profile, allocation, [1001, 1111, 1130]) + expect(result.state.achievements).toContain(228) // 只通一窍 + }) + + test('next', () => { + // 白色胶囊 + const state = produce( + createState(allocation, [1001, 1002, 1103]), + draft => { + draft.props = propsEffect(draft.props, { age: 10 }) + }, + ) + const result = next(state, profile) + console.debug(result) + expect(result.state.props.current.age).toBe(10) // 十岁 + expect(result.state.events).toContain(10588) // 无事发生 + }) + + test('summary', () => { + const state = produce( + createState(allocation, [1001, 1111, 1130]), + draft => { + draft.props = propsEffect(draft.props, { + charm: 1000, + intelligence: 1000, + strength: 1000, + money: 1000, + spirit: 1000, + age: 501, + }) + }, + ) + const result = summary(state, profile) + const s = stateSummary(state) + expect(s).toBeGreaterThan(10000) // 总评大于10000 + expect(result.state.achievements).toContain(245) // 传奇人物 + expect(result.state.achievements).toContain(246) // 不可思议 + expect(result.state.achievements).toContain(247) // 大能者 + expect(result.state.achievements).toContain(248) // 无上存在 + }) + + test('end', () => { + const p = { ...profile, times: 500, talents: new Set([1002, 1003]) } + const state = produce( + createState(allocation, [1001, 1111, 1130]), + draft => { + draft.props = propsEffect(draft.props, { + charm: 1000, + intelligence: 1000, + strength: 1000, + money: 1000, + spirit: 1000, + age: 501, + }) + }, + ) + const result = end(state, p) + expect(result.profile.achievements).toContain(101) // 既视感 + expect(result.profile.achievements).toContain(102) // 孟婆愁 + expect(result.profile.achievements).toContain(103) // 所有人都是我 + expect(result.profile.achievements).toContain(104) // Rewrite + expect(result.achievements).toContain(101) + expect(result.achievements).toContain(102) + expect(result.achievements).toContain(103) + expect(result.achievements).toContain(104) + expect(result.profile.talents).toContain(1001) + expect(result.profile.talents).toContain(1002) + expect(result.profile.talents).toContain(1003) + expect(result.profile.talents).toContain(1111) + expect(result.profile.talents).toContain(1130) + }) +}) diff --git a/packages/core/src/game.ts b/packages/core/src/game.ts index 17f48e8..71d5fa8 100644 --- a/packages/core/src/game.ts +++ b/packages/core/src/game.ts @@ -1,5 +1,119 @@ -import type { GameState, ProfileState } from '@/state' -import { enableMapSet } from 'immer' +import ages from '@remake/data/age' +import { propsEffect } from '@/state' +import { enableMapSet, produce } from 'immer' enableMapSet() +export type * as achievement from '@/achievement' +export type * as event from '@/event' +export type * as talent from '@/talent' +export type * as state from '@/state' +import type { GameState, ProfileState } from '@/state' export type { GameState, ProfileState } +export interface TriggerResult { + state: GameState + triggers: T[] +} + +export { pull } from '@/talent' +// export { summary, nextProfile } from '@/state' + +import { replacement, additionalPoints } from '@/talent' +import type { ReplacementResult, AdditionalPoints } from '@/talent' + +export interface TalentsPickedResult { + talents: ReplacementResult + additionalPoints: AdditionalPoints +} +export function talentsPicked(talents: Iterable) { + const r = replacement(talents) + const ap = additionalPoints(r.talents) + return { talents: r, additionalPoints: ap } +} + +import { createState } from '@/state' + +export interface StartResult { + state: GameState + achievements: Achievement['id'][] +} +export function start( + profile: ProfileState, + ...args: Parameters +): StartResult { + const state = createState(...args) + const ar = achievementTrigger('START', state, profile) + return { state: ar.state, achievements: ar.triggers } +} + +import type { Achievement } from '@remake/data/achievement' +import type { Event } from '@remake/data/event' +import type { Talent } from '@remake/data/talent' +import { trigger as achievementTrigger } from '@/achievement' +import { trigger as eventTrigger } from '@/event' +import { trigger as talentTrigger } from '@/talent' +import { check as eventCheck } from '@/event' +import { pickWeight } from '@remake/vitex' + +export interface NextResult { + state: GameState + age: number + achievements: Achievement['id'][] + events: Event['id'][] + talents: Talent['id'][] + end: boolean +} + +export function next(state: GameState, profile: ProfileState): NextResult { + let s = produce(state, draft => { + draft.props = propsEffect(state.props, { age: 1 }) + }) + const age = s.props.current.age + const tr = talentTrigger(s, profile) + const events = ages + .get(age)! + .event.filter(([e]) => eventCheck(e, tr.state, profile)) + const event = pickWeight(events)! + const er = eventTrigger(event, tr.state, profile) + const ar = achievementTrigger('TRAJECTORY', er.state, profile) + const end = ar.state.life < 1 + return { + state: ar.state, + age, + achievements: ar.triggers, + events: er.triggers, + talents: tr.triggers, + end, + } +} + +import { summary as stateSummary } from '@/state' + +export interface SummaryResult { + state: GameState + summary: number + achievements: Achievement['id'][] +} +export function summary( + state: GameState, + profile: ProfileState, +): SummaryResult { + const ar = achievementTrigger('SUMMARY', state, profile) + const s = stateSummary(ar.state) + return { state: ar.state, summary: s, achievements: ar.triggers } +} + +import { nextProfile } from '@/state' +export interface EndResult { + profile: ProfileState + achievements: Achievement['id'][] +} + +export function end( + state: GameState, + profile: ProfileState, + lockedTalent?: Talent['id'], +) { + const ar = achievementTrigger('END', state, profile) + const p = nextProfile(profile, ar.state, lockedTalent) + return { profile: p, achievements: ar.triggers } +} diff --git a/packages/core/src/state.spec.ts b/packages/core/src/state.spec.ts index b26bb79..7bbcd46 100644 --- a/packages/core/src/state.spec.ts +++ b/packages/core/src/state.spec.ts @@ -234,7 +234,7 @@ describe('State', () => { const k = key as keyof Properties const c = props.current[k] const be = c + effect[k] - expect(c).toBe(be) + expect(effected.current[k]).toBe(be) expect(effected.highest[k]).toBe(Math.max(c, be)) expect(effected.lowest[k]).toBe(Math.min(c, be)) } diff --git a/packages/core/src/state.ts b/packages/core/src/state.ts index 4508ddb..81ca331 100644 --- a/packages/core/src/state.ts +++ b/packages/core/src/state.ts @@ -63,6 +63,13 @@ export function createState( talentTriggers: new Map(), } } + +export function summary({ props }: GameState) { + const { age, ...others } = props.highest + const s = sum(Object.values(others)) + return Math.floor(s * 2 + age / 2) +} + export interface FlatState { AGE: GameState['props']['current']['age'] CHR: GameState['props']['current']['charm'] @@ -131,11 +138,7 @@ const FlatMappers = { AEVT: state => state.profile.events, ATLT: state => state.profile.talents, AACH: state => state.profile.achievements, - SUM: ({ game: { props } }) => { - const { age, ...others } = props.highest - const s = sum(Object.values(others)) - return Math.floor(s * 2 + age / 2) - }, + SUM: ({ game }) => summary(game), } as { [Key in FlatStateKey]: FlatMapper } export const SupportedFlatStateKeys = new Set(Object.keys(FlatMappers)) diff --git a/packages/core/src/talent.spec.ts b/packages/core/src/talent.spec.ts index 1609a74..41a9807 100644 --- a/packages/core/src/talent.spec.ts +++ b/packages/core/src/talent.spec.ts @@ -1,6 +1,6 @@ import { expect, test, describe } from 'bun:test' import type { ProfileState } from './state' -import type { TalentPullOptions } from './talent' +import type { PullOptions } from './talent' import { pull, exclude, additionalPoints, replacement } from './talent' import { createState } from './state' import talents from '@remake/data/talent' @@ -27,7 +27,7 @@ describe('Talent', () => { ]), additions: {}, }, - } as TalentPullOptions + } as PullOptions test('pull external', () => { const p = produce(profile, draft => { @@ -71,13 +71,10 @@ describe('Talent', () => { }) }) - const allo = { charm: 5, intelligence: 5, strength: 5, money: 5 } test('replacement talent [阴间福袋]', () => { const talent = 1145 - const state = createState(allo, [talent]) - const result = replacement(state) - expect(result.changed).toBe(true) - expect(result.state.talents.size).toBeGreaterThan(1) + const result = replacement([talent]) + expect(result.talents.size).toBeGreaterThan(1) expect(result.chains.get(talent)).toBeDefined() const source = talents.get(talent)!.replacement!.talent! const replaced = result.chains.get(talent)!.at(0)! @@ -86,10 +83,8 @@ describe('Talent', () => { test('replacement grade [橙色转盘]', () => { const talent = 1144 - const state = createState(allo, [talent]) - const result = replacement(state) - expect(result.changed).toBe(true) - expect(result.state.talents.size).toBeGreaterThan(1) + const result = replacement([talent]) + expect(result.talents.size).toBeGreaterThan(1) expect(result.chains.get(talent)).toBeDefined() const source = talents.get(talent)!.replacement!.grade! const replaced = result.chains.get(talent)!.at(0)! diff --git a/packages/core/src/talent.ts b/packages/core/src/talent.ts index f7c35dd..d3dd8a4 100644 --- a/packages/core/src/talent.ts +++ b/packages/core/src/talent.ts @@ -6,6 +6,7 @@ import { propsEffect, createFlatState } from './state' import { check } from '@remake/condition' import { pick, pickWeight } from '@remake/vitex' import { produce } from 'immer' +import type { TriggerResult } from '@/game' const Grades: TalentGrade[] = [0, 1, 2, 3] as const const GradeMap = Map.groupBy( @@ -19,23 +20,23 @@ export function get(talent: number) { return talents.get(talent) } -export type TalentPullRate = Map -export type TalentPullRateAddition = { +export type PullRate = Map +export type PullRateAddition = { mode: 'add' | 'multiply' value: number } -export type TalentPullRateAdditions = { +export type PullRateAdditions = { [key in keyof ProfileState]?: ( value: ProfileState[key], - ) => Map + ) => Map } -export interface TalentRateOptions { - base: TalentPullRate - additions: TalentPullRateAdditions +export interface RateOptions { + base: PullRate + additions: PullRateAdditions } const AddidtionInit = Grades.map(g => [g, 1]) as [TalentGrade, number][] function talentRateWithAddition( - { base, additions }: TalentRateOptions, + { base, additions }: RateOptions, profile: ProfileState, ) { const rate = new Map(AddidtionInit) @@ -58,12 +59,12 @@ function talentRateWithAddition( return rate } -export interface TalentPullOptions { +export interface PullOptions { count: number - rate: TalentRateOptions + rate: RateOptions } -export function pull(options: TalentPullOptions, profile: ProfileState) { +export function pull(options: PullOptions, profile: ProfileState) { const rate = Array.from( talentRateWithAddition(options.rate, profile).entries(), ) @@ -99,12 +100,6 @@ export function exclude(talent: Talent['id'], list: Iterable) { return null } -export interface TalentReplacementResult { - state: GameState - chains: Map - changed: boolean -} - function chainReplace(t: Talent['id'], ts: Set) { const { replacement: r } = talents.get(t)! if (!r) return null @@ -126,22 +121,21 @@ function chainReplace(t: Talent['id'], ts: Set) { return [picked] } -export function replacement(state: GameState): TalentReplacementResult { - const chains = new Map() as TalentReplacementResult['chains'] - const newTalents = new Set(state.talents) - let changed = false - for (const talent of state.talents) { - const chain = chainReplace(talent, newTalents) +export interface ReplacementResult { + talents: Set + chains: Map +} + +export function replacement(list: Iterable): ReplacementResult { + const set = new Set(list) + const chains = new Map() as ReplacementResult['chains'] + for (const talent of list) { + const chain = chainReplace(talent, set) if (!chain) continue - changed = true chains.set(talent, chain) - chain.forEach(t => newTalents.add(t)) + chain.forEach(t => set.add(t)) } - if (!changed) return { state, chains, changed } - const newState = produce(state, draft => { - draft.talents = newTalents - }) - return { state: newState, chains, changed } + return { talents: set, chains } } export interface AdditionalPoint { @@ -154,7 +148,9 @@ export interface AdditionalPoints { points: number } -export function additionalPoints(list: Talent['id'][]) { +export function additionalPoints( + list: Iterable, +): AdditionalPoints { const source: AdditionalPoint[] = [] let total = 0 for (const id of list) { @@ -166,23 +162,18 @@ export function additionalPoints(list: Talent['id'][]) { return { source, points: total } } -const TalentEffectRandomProperties = [ +const EffectRandomProperties = [ 'money', 'strength', 'intelligence', 'charm', 'spirit', ] as (keyof Properties)[] -export interface TalentTriggerResult { - state: GameState - triggers: Talent['id'][] - changed: boolean -} export function trigger( state: GameState, profile: ProfileState, -): TalentTriggerResult { +): TriggerResult { const flatState = createFlatState(state, profile) const triggers: Talent['id'][] = [] for (const talent of state.talents) { @@ -191,7 +182,7 @@ export function trigger( if (condition && !check(flatState, condition)) continue triggers.push(talent) } - if (triggers.length === 0) return { state, triggers, changed: false } + if (triggers.length === 0) return { state, triggers } const newState = produce(state, draft => { for (const talent of triggers) { const times = draft.talentTriggers.get(talent) ?? 0 @@ -205,11 +196,11 @@ export function trigger( if (effect.MNY) pe.money = effect.MNY if (effect.SPR) pe.spirit = effect.SPR if (effect.RND) { - const key = pick(TalentEffectRandomProperties)! + const key = pick(EffectRandomProperties)! pe[key] = (pe[key] ?? 0) || effect.RND } draft.props = propsEffect(draft.props, pe) } }) - return { state: newState, triggers, changed: true } + return { state: newState, triggers } }