@idlekit/core

中文文档
0.7.0发布于 2026/11/01MITTypeScriptES Module构建来源已验证

Bun-first scenario compilation, simulation, analysis, and reporting for idle game economies.

原始使用文档
文档目录6 节
  1. LTV estimates
  2. Observed cohort retention
  3. Game-specific offline settlement
  4. Quick example
  5. Runtime
  6. Documentation

@idlekit/core

Simulation and analysis primitives for idle game economy design. It compiles scenarios, executes runs, and evaluates long-horizon KPIs and design-facing metrics. Official support in v1: Bun >=1.4.2 only. Node.js and browser runtimes are not part of the v1 compatibility contract.

bun add @idlekit/core

Requires Bun >=1.4.2.

Use this package when you need:

  • scenario validation / compilation
  • simulation loop and stepOnce
  • built-in strategies and tuning primitives
  • growth, milestone, session-pattern, and Monte Carlo analysis
  • hypothetical LTV estimates via deriveMonetizationConfig, estimateLtvPerUser and estimateLtvDistribution

LTV estimates

Import the three LTV functions, the retentionAtDay / progressionFactor helpers and the MonetizationConfig, LtvEstimateArgs, LtvDistributionArgs, LtvPointEstimate and LtvDistribution types from @idlekit/core. Pass a ScenarioV1 to deriveMonetizationConfig; pass the returned config, horizonDays and progression to estimateLtvPerUser. estimateLtvDistribution also reads draws, quantiles and seed; an omitted one falls back to config.uncertainty, and an explicit one wins. It always samples: config.uncertainty.enabled is the caller's switch for whether to call it, as in the CLI. The CLI uses the same implementation.

LTV is undiscounted cumulative value. A horizon of H days covers install days [0, H): day 0 is the install day, active by definition, as in telemetry and idk calibrate. A fractional last day is prorated at that day's retention, so 0.5 days is half of the install day. Earlier releases covered days [1, H], dropped install-day revenue and charged a 0.5-day horizon at D1 retention. The same horizon now adds the install day and drops day H, so LTV values change and usually rise.

Inputs are hypothetical assumptions, in one caller-chosen currency per installed user. Defaults are not observed retention or measured revenue. Net revenue applies platform fees and gross margin; LTV then subtracts CPI, including at a zero-day horizon. Finite progression is clamped to 0..1. Quantile bands describe sensitivity to assumptions, not fitted confidence intervals or evidence of enjoyment/retention.

Synchronous limits: 0..36500 days, 1..100000 draws and at most 10000000 draw-days (at least one work unit per draw). Quantiles must be 1..101 distinct values in 0..1. Each output key is the exact percent (q50, q90, q2.5 for 0.025, q12.4 for 0.124), so keys never collide or round. The seed must be an integer in 0..4294967295; other seeds throw instead of being folded. retentionAtDay requires a finite day >= 0 and a valid curve (day 0 is 1, any day in (0, 1] reads D1, and anchors from D1 on are linear); progressionFactor rejects NaN logs and a span that is not finite and positive, and treats an infinite worth log (zero worth) as no progression. Invalid inputs and numeric overflow throw RangeError. Sampling shrinks inconsistent correlation matrices toward independence. Complete-cohort retention observation and safe patch application are available in core; the public idk calibrate CLI shares the observation and planning implementation. Public estimate configs accept quantile endpoints too. The point estimate validates the whole config, including the uncertainty defaults the distribution reads; key distinctness and the seed range are checked when the distribution uses them. ScenarioV1 authored quantiles retain their existing (0, 1) schema when passed to deriveMonetizationConfig.

Partial scenario retention overrides keep the CLI's independent defaults. For example, d1: 0.1 alone retains the default d7: 0.2; estimates preserve that existing interpolation. Explicit complete scenario curves still follow scenario ordering validation. Direct estimate configs validate each rate's range without adding an ordering constraint to completed defaults. The point projection and the distribution interpolate such a rising curve the same way: one shared retention shock scales every anchor, and sampling no longer clips a later anchor to the preceding one, so a zero-uncertainty distribution equals the point projection.

Observed cohort retention

Import observeCohort, compareRetention, planRetentionPatch and applyRetentionPatch from @idlekit/core. CohortActivityRow requires userId and integer day; active defaults to true. Money fields are optional and are never fitted by these retention APIs. Every installed user needs a day-zero row, including nonreturners, and the caller must provide a complete activity export with a common, fully mature observedThroughDay. The API checks consistency; it cannot prove export completeness.

observeCohort(rows, cutoff) preserves future rates/intervals as null. planRetentionPatch returns ready, unavailable or requires-review; every unobserved target day requires an explicit retained assumption. Observations that rise or conflict with retained values remain unchanged and cannot become an automatic patch. applyRetentionPatch({ scenario, observation }) recomputes the plan, validates and returns an independent scenario copy. It throws if the patch is not ready and never replaces revenue, fees, CPI, uncertainty, cohort size or unobserved retention. assertCohortObservation validates serialized input.

See the complete SDK example and the cohort export guide.

Game-specific offline settlement

simulateSessionPattern({ scenario, pattern, prepareOffline }) optionally calls a synchronous SessionOfflinePreparer once per nonempty absence. It receives ctx, model, a state snapshot, seconds, wallStartT, wallEndT and the current policy snapshot. Numeric prototypes are preserved in the snapshots; treat the shared engine, context and model as immutable. Use the explicit wall timestamps for expiry checks after earlier capped gaps.

Return any of model, stepSec and policy. They apply only to this gap; omission inherits the scenario setting. policy replaces cap/decay, so {} removes both. The session's action policy stays in scenario.run.offline.actions; a callback cannot override it. A replacement model declares its own clocks.respondsTo needs. The SDK still performs reward ticks, goals, until checks, action filtering, observations and wall/reward accounting.

A game with one atomic settlement can compute it on the snapshot and return a model that imports its result in one reward tick, with stepSec equal to its effective credited seconds (use a positive step when credit is zero). Stops are checked at tick boundaries; a one-tick settlement does not expose intermediate game states. Preparation itself must not grant an external reward: zero credit or an already-reached goal may execute no transition at all. For partial settlement or intra-gap goals, supply a model that evolves per smaller tick.

This is an SDK callback, not a JSON scenario field. The default session path is unchanged. See the big-number offline example.

Quick example

import {
  analyzeGrowth,
  compileScenario,
  createModelRegistry,
  createNumberEngine,
  defineModelFactory,
  runScenario,
  validateScenarioV1,
} from "@idlekit/core";

type Vars = { owned: number };

const linearFactory = defineModelFactory<number, "COIN", Vars>({
  id: "linear",
  version: 1,
  create() {
    return {
      id: "linear",
      version: 1,
      income(ctx, state) {
        return {
          unit: ctx.unit,
          amount: ctx.E.add(1, Number(state.vars.owned ?? 0)),
        };
      },
      actions() {
        return [];
      },
    };
  },
});

const registry = createModelRegistry([linearFactory]);

const scenario = {
  schemaVersion: 1,
  unit: { code: "COIN" },
  policy: { mode: "drop" },
  model: { id: "linear", version: 1 },
  initial: {
    wallet: { unit: "COIN", amount: "0" },
    vars: { owned: 0 },
  },
  clock: { stepSec: 1, durationSec: 60 },
};

const validated = validateScenarioV1(scenario, registry);
if (!validated.ok || !validated.scenario) {
  throw new Error(`scenario should validate: ${JSON.stringify(validated.issues)}`);
}

const E = createNumberEngine();
const compiled = compileScenario({
  E,
  scenario: validated.scenario,
  registry,
});
const run = runScenario(compiled);
const growth = analyzeGrowth({ run, series: "money", windowSec: 10 });

console.log(
  JSON.stringify({
    endMoney: E.toString(run.end.wallet.money.amount),
    growthSegments: growth.segments,
  }),
);

Runtime

For the duration of an in-memory runScenario or applyOfflineSeconds result, import runElapsedSec from @idlekit/core and read runElapsedSec(run). It uses run.stop.elapsedSec, the sum of tick seconds. state.t is the absolute reward timestamp; at a large starting timestamp, run.end.t - run.start.t can round differently. For example, ten 0.1-second ticks starting at 1e15 simulate about one second while the timestamps differ by 1.25 seconds. Results without a stop record fall back to that timestamp difference. A step-budget stop can shorten an individual run before the requested duration; check run.stop?.reason or use assertHorizonReached(run, label) when the full horizon is required.

For simulateSessionPattern, read summary.elapsedSec for wall time, summary.activeSec for active tick seconds, and summary.offlineCreditedSec for offline reward tick seconds. The aggregate session.run has no per-run stop record, so runElapsedSec(session.run) uses the timestamp fallback. summary.rewardSec is also a reward timestamp difference; use the active and offline credited totals when you need simulated tick seconds.

@idlekit/core is maintained as a Bun-first ESM package.

Documentation

版本与发布记录

@idlekit/core 共发布 10 个版本,首次发布于 2026/10/11;过去 12 个月发布了 10 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.7.0latest2026/11/012 天前
0.6.02026/10/1816 天前
0.5.02026/10/1717 天前
0.4.12026/10/1618 天前
0.4.02026/10/1519 天前
0.3.12026/10/1321 天前

关于 @idlekit/core 的常见问题

@idlekit/core 是什么?

为空闲的游戏经济编写、模拟、分析和报告。

如何安装 @idlekit/core?

在项目目录运行 npm install @idlekit/core,或使用 pnpm add / yarn add / bun add。

@idlekit/core 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@idlekit/core 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@idlekit/core 可以商用吗?

@idlekit/core 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@idlekit/core 还在维护吗?

最新版本 0.7.0 发布于 2026/11/01(2 天前);至今共发布 10 个版本,其中过去 12 个月发布了 10 个;首次发布于 2026/10/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。