@holoscript/std 2.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +197 -0
  2. package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
  3. package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
  4. package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
  5. package/dist/__tests__/Sprint33.test.d.ts.map +1 -0
  6. package/dist/__tests__/Sprint56.test.d.ts.map +1 -0
  7. package/dist/__tests__/collections.test.d.ts.map +1 -0
  8. package/dist/__tests__/events.test.d.ts.map +1 -0
  9. package/dist/__tests__/materials.test.d.ts.map +1 -0
  10. package/dist/__tests__/math.test.d.ts.map +1 -0
  11. package/dist/__tests__/physics.test.d.ts.map +1 -0
  12. package/dist/__tests__/spatial.test.d.ts.map +1 -0
  13. package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
  14. package/dist/__tests__/string.test.d.ts.map +1 -0
  15. package/dist/__tests__/time.test.d.ts.map +1 -0
  16. package/dist/collections.d.ts.map +1 -1
  17. package/dist/events.d.ts.map +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +42 -9
  20. package/dist/materials.d.ts.map +1 -0
  21. package/dist/math.d.ts.map +1 -1
  22. package/dist/physics.d.ts.map +1 -0
  23. package/dist/spatial.d.ts.map +1 -0
  24. package/dist/string.d.ts.map +1 -1
  25. package/dist/time.d.ts.map +1 -1
  26. package/dist/traits/ARTraits.d.ts.map +1 -0
  27. package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
  28. package/dist/traits/EconomicTraits.d.ts.map +1 -0
  29. package/dist/traits/IoTTraits.d.ts.map +1 -0
  30. package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
  31. package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
  32. package/dist/traits/VRRTraits.d.ts.map +1 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +17 -6
  35. package/src/__tests__/EconomicPrimitives.test.ts +690 -0
  36. package/src/__tests__/EconomicTraits.test.ts +425 -0
  37. package/src/__tests__/SimulationLabPrimitives.test.ts +415 -0
  38. package/src/__tests__/Sprint33.test.ts +1301 -0
  39. package/src/__tests__/Sprint56.test.ts +1070 -0
  40. package/src/__tests__/collections.test.ts +182 -0
  41. package/src/__tests__/events.test.ts +135 -0
  42. package/src/__tests__/materials.test.ts +84 -0
  43. package/src/__tests__/math.test.ts +246 -0
  44. package/src/__tests__/physics.test.ts +83 -0
  45. package/src/__tests__/spatial.test.ts +333 -0
  46. package/src/__tests__/string-decoupled.test.ts +16 -0
  47. package/src/__tests__/string.test.ts +164 -0
  48. package/src/__tests__/time.test.ts +110 -0
  49. package/src/collections.ts +8 -2
  50. package/src/events.ts +88 -0
  51. package/src/index.ts +161 -11
  52. package/src/materials.ts +109 -0
  53. package/src/math.ts +30 -14
  54. package/src/physics.ts +141 -0
  55. package/src/spatial.ts +320 -0
  56. package/src/string.basic.test.ts +14 -0
  57. package/src/string.test.ts +335 -0
  58. package/src/string.ts +19 -2
  59. package/src/time.ts +9 -10
  60. package/src/traits/ARTraits.ts +103 -0
  61. package/src/traits/EconomicPrimitives.ts +755 -0
  62. package/src/traits/EconomicTraits.ts +552 -0
  63. package/src/traits/IoTTraits.ts +102 -0
  64. package/src/traits/SimulationLabPrimitives.ts +650 -0
  65. package/src/traits/SimulationLabTraits.ts +191 -0
  66. package/src/traits/VRRTraits.ts +326 -0
  67. package/src/types.ts +47 -12
  68. package/vitest.config.ts +10 -0
  69. package/dist/collections.d.ts +0 -177
  70. package/dist/collections.js +0 -720
  71. package/dist/collections.js.map +0 -1
  72. package/dist/index.d.ts +0 -89
  73. package/dist/index.js.map +0 -1
  74. package/dist/math.d.ts +0 -162
  75. package/dist/math.js +0 -539
  76. package/dist/math.js.map +0 -1
  77. package/dist/string.d.ts +0 -208
  78. package/dist/string.js +0 -443
  79. package/dist/string.js.map +0 -1
  80. package/dist/time.d.ts +0 -215
  81. package/dist/time.js +0 -530
  82. package/dist/time.js.map +0 -1
  83. package/dist/types.d.ts +0 -193
  84. package/dist/types.js +0 -198
  85. package/dist/types.js.map +0 -1
@@ -0,0 +1,755 @@
1
+ /**
2
+ * @fileoverview Economic Primitives - Runtime math and types for virtual economy self-regulation
3
+ * @module @holoscript/std/economic
4
+ *
5
+ * Provides the mathematical building blocks for composable economic behaviors in HoloScript.
6
+ * These primitives implement research-proven mechanisms from control theory, DeFi AMMs,
7
+ * and progressive taxation -- adapted for spatial computing economies.
8
+ *
9
+ * References:
10
+ * - P.030.01: Dual-Loop PID Economy Controller (Alter Aeon model)
11
+ * - P.030.02: Bonding Curve Marketplace (DeFi AMM adaptation)
12
+ * - P.030.03: Progressive Wealth Recycling (logarithmic tax + UBI)
13
+ * - W.031: Faucet-Sink Ratio Is the Master Variable
14
+ * - W.032: Dual-Loop Feedback Control Solves Long-Term Stability
15
+ * - W.034: Wealth Tax > Income Tax for Virtual Gini Reduction
16
+ * - W.035: Bonding Curves Provide Intrinsic Price Discovery
17
+ *
18
+ * @version 1.0.0
19
+ * @category economic
20
+ */
21
+
22
+ // =============================================================================
23
+ // CORE TYPES
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Represents an in-world currency amount. Always positive.
28
+ * Uses number to stay compatible with HoloScript's numeric system.
29
+ */
30
+ export type Currency = number;
31
+
32
+ /**
33
+ * An agent identifier (player, NPC, or system account).
34
+ */
35
+ export type AgentID = string;
36
+
37
+ /**
38
+ * Result type for economic operations that can fail.
39
+ */
40
+ export type EconomicResult<T> = { ok: true; value: T } | { ok: false; error: EconomicError };
41
+
42
+ /**
43
+ * Economic error codes.
44
+ */
45
+ export type EconomicError =
46
+ | 'INSUFFICIENT_FUNDS'
47
+ | 'UNAUTHORIZED'
48
+ | 'INVALID_AMOUNT'
49
+ | 'BELOW_MINIMUM'
50
+ | 'ABOVE_MAXIMUM'
51
+ | 'ITEM_DESTROYED'
52
+ | 'TRANSFER_BLOCKED'
53
+ | 'CURVE_EXHAUSTED'
54
+ | 'PID_DIVERGED'
55
+ | 'TAX_EXEMPT'
56
+ | 'INVALID_PARAMETER';
57
+
58
+ /**
59
+ * RBAC permission categories needed for economic operations.
60
+ * These map to the existing HoloScript RBAC system (RBACTrait.ts).
61
+ */
62
+ export type EconomicPermission =
63
+ | 'economy.trade' // Transfer items/currency between agents
64
+ | 'economy.mint' // Create new currency (faucet operation)
65
+ | 'economy.burn' // Destroy currency (sink operation)
66
+ | 'economy.set_price' // Modify prices on bonding curves
67
+ | 'economy.tax' // Levy and collect taxes
68
+ | 'economy.redistribute' // Distribute collected tax proceeds
69
+ | 'economy.tune_pid' // Modify PID controller parameters
70
+ | 'economy.audit' // View economic state and logs
71
+ | 'economy.*'; // Full economic authority
72
+
73
+ // =============================================================================
74
+ // TRADEABLE PRIMITIVES
75
+ // =============================================================================
76
+
77
+ /**
78
+ * Ownership record for a tradeable entity.
79
+ */
80
+ export interface OwnershipRecord {
81
+ /** Current owner */
82
+ owner: AgentID;
83
+ /** Transfer history (most recent first, capped at last 50) */
84
+ history: TransferRecord[];
85
+ /** Whether transfers are currently locked */
86
+ locked: boolean;
87
+ /** Optional lock reason */
88
+ lockReason?: string;
89
+ }
90
+
91
+ /**
92
+ * A single transfer event in the ownership history.
93
+ */
94
+ export interface TransferRecord {
95
+ from: AgentID;
96
+ to: AgentID;
97
+ price: Currency;
98
+ timestamp: number;
99
+ /** Transaction hash for integrity verification (P.030.04) */
100
+ txHash: string;
101
+ }
102
+
103
+ /**
104
+ * Generate a transaction hash from transfer details.
105
+ * Uses a simple deterministic hash for in-engine verification.
106
+ * NOT cryptographically secure -- server authority provides integrity (W.037).
107
+ */
108
+ export function generateTxHash(
109
+ from: AgentID,
110
+ to: AgentID,
111
+ price: Currency,
112
+ timestamp: number
113
+ ): string {
114
+ // Simple FNV-1a style hash for fast deterministic ID generation
115
+ let hash = 2166136261;
116
+ const input = `${from}:${to}:${price}:${timestamp}`;
117
+ for (let i = 0; i < input.length; i++) {
118
+ hash ^= input.charCodeAt(i);
119
+ hash = Math.imul(hash, 16777619);
120
+ }
121
+ return (hash >>> 0).toString(16).padStart(8, '0');
122
+ }
123
+
124
+ /**
125
+ * Execute a trade (transfer ownership for a price).
126
+ * Returns a new OwnershipRecord if successful.
127
+ */
128
+ export function executeTrade(
129
+ record: OwnershipRecord,
130
+ buyer: AgentID,
131
+ price: Currency,
132
+ timestamp: number
133
+ ): EconomicResult<OwnershipRecord> {
134
+ if (record.locked) {
135
+ return { ok: false, error: 'TRANSFER_BLOCKED' };
136
+ }
137
+ if (price < 0) {
138
+ return { ok: false, error: 'INVALID_AMOUNT' };
139
+ }
140
+
141
+ const txHash = generateTxHash(record.owner, buyer, price, timestamp);
142
+
143
+ const transfer: TransferRecord = {
144
+ from: record.owner,
145
+ to: buyer,
146
+ price,
147
+ timestamp,
148
+ txHash,
149
+ };
150
+
151
+ // Keep history capped at 50 entries
152
+ const history = [transfer, ...record.history].slice(0, 50);
153
+
154
+ return {
155
+ ok: true,
156
+ value: {
157
+ owner: buyer,
158
+ history,
159
+ locked: false,
160
+ },
161
+ };
162
+ }
163
+
164
+ // =============================================================================
165
+ // DEPRECIATION PRIMITIVES
166
+ // =============================================================================
167
+
168
+ /**
169
+ * Configuration for asset depreciation (hard sink mechanic).
170
+ */
171
+ export interface DepreciationConfig {
172
+ /** Per-second decay rate (0.0 - 1.0). E.g., 0.001 = 0.1% per second */
173
+ decayRate: number;
174
+ /** Current condition (0.0 = destroyed, 1.0 = mint condition) */
175
+ condition: number;
176
+ /** Minimum condition before the item is considered destroyed */
177
+ destroyThreshold: number;
178
+ /** Whether repairs are allowed */
179
+ repairable: boolean;
180
+ /** Cost multiplier for repairs (relative to original value) */
181
+ repairCostMultiplier: number;
182
+ }
183
+
184
+ /**
185
+ * Default depreciation configuration.
186
+ */
187
+ export const DEFAULT_DEPRECIATION: DepreciationConfig = {
188
+ decayRate: 0.0001, // 0.01% per second (~36% per hour)
189
+ condition: 1.0,
190
+ destroyThreshold: 0.01,
191
+ repairable: true,
192
+ repairCostMultiplier: 0.5,
193
+ };
194
+
195
+ /**
196
+ * Calculate the current condition of an asset after elapsed time.
197
+ * Uses exponential decay: condition(t) = condition(0) * e^(-rate * elapsed)
198
+ */
199
+ export function calculateDepreciation(
200
+ initialCondition: number,
201
+ decayRate: number,
202
+ elapsedSeconds: number
203
+ ): number {
204
+ if (decayRate <= 0) return initialCondition;
205
+ if (elapsedSeconds <= 0) return initialCondition;
206
+ return initialCondition * Math.exp(-decayRate * elapsedSeconds);
207
+ }
208
+
209
+ /**
210
+ * Calculate the effective value of an asset based on condition.
211
+ * Value scales linearly with condition.
212
+ */
213
+ export function depreciatedValue(baseValue: Currency, condition: number): Currency {
214
+ return baseValue * Math.max(0, Math.min(1, condition));
215
+ }
216
+
217
+ /**
218
+ * Check if an asset has been destroyed by depreciation.
219
+ */
220
+ export function isDestroyed(condition: number, threshold: number): boolean {
221
+ return condition <= threshold;
222
+ }
223
+
224
+ /**
225
+ * Calculate repair cost to restore an asset to full condition.
226
+ */
227
+ export function calculateRepairCost(
228
+ baseValue: Currency,
229
+ currentCondition: number,
230
+ repairCostMultiplier: number
231
+ ): Currency {
232
+ const conditionDeficit = 1.0 - Math.max(0, Math.min(1, currentCondition));
233
+ return baseValue * conditionDeficit * repairCostMultiplier;
234
+ }
235
+
236
+ // =============================================================================
237
+ // BONDING CURVE PRIMITIVES
238
+ // =============================================================================
239
+
240
+ /**
241
+ * Bonding curve types with different economic properties.
242
+ *
243
+ * - linear: P = R * S (gentle, predictable)
244
+ * - exponential: P = R * S^n (early-adopter reward)
245
+ * - logarithmic: P = R * ln(S + 1) (stability-favoring)
246
+ * - sigmoid: P = R * S^n / (K^n + S^n) (bounded with plateau)
247
+ */
248
+ export type BondingCurveType = 'linear' | 'exponential' | 'logarithmic' | 'sigmoid';
249
+
250
+ /**
251
+ * Configuration for a bonding curve marketplace.
252
+ */
253
+ export interface BondingCurveConfig {
254
+ /** Curve type */
255
+ curveType: BondingCurveType;
256
+ /** Reserve ratio (base price multiplier) */
257
+ reserveRatio: number;
258
+ /** Curve steepness (n in P = R * S^(1/n)) */
259
+ curveSteepness: number;
260
+ /** Current supply */
261
+ currentSupply: number;
262
+ /** Reserve pool balance (collateral backing the curve) */
263
+ reserveBalance: Currency;
264
+ /** Transaction fee percentage (hard sink, 0-1) */
265
+ transactionFee: number;
266
+ /** Half-saturation constant for sigmoid curves */
267
+ sigmoidK: number;
268
+ /** Optional spatial distance factor for VR marketplaces */
269
+ spatialDistanceFactor: number;
270
+ }
271
+
272
+ /**
273
+ * Default bonding curve configuration.
274
+ */
275
+ export const DEFAULT_BONDING_CURVE: BondingCurveConfig = {
276
+ curveType: 'exponential',
277
+ reserveRatio: 1.0,
278
+ curveSteepness: 2.0,
279
+ currentSupply: 0,
280
+ reserveBalance: 0,
281
+ transactionFee: 0.02, // 2% hard sink
282
+ sigmoidK: 100,
283
+ spatialDistanceFactor: 0.0,
284
+ };
285
+
286
+ /**
287
+ * Calculate the spot price on a bonding curve given current supply.
288
+ *
289
+ * P = R * S^(1/n) for exponential
290
+ * P = R * S for linear
291
+ * P = R * ln(S+1) for logarithmic
292
+ * P = R * S^n / (K^n + S^n) for sigmoid
293
+ */
294
+ export function bondingCurvePrice(
295
+ supply: number,
296
+ reserveRatio: number,
297
+ curveSteepness: number,
298
+ curveType: BondingCurveType = 'exponential',
299
+ sigmoidK: number = 100
300
+ ): Currency {
301
+ if (supply <= 0) return 0;
302
+ if (reserveRatio <= 0) return 0;
303
+
304
+ switch (curveType) {
305
+ case 'linear':
306
+ return reserveRatio * supply;
307
+
308
+ case 'exponential':
309
+ return reserveRatio * Math.pow(supply, 1.0 / curveSteepness);
310
+
311
+ case 'logarithmic':
312
+ return reserveRatio * Math.log(supply + 1);
313
+
314
+ case 'sigmoid': {
315
+ const sn = Math.pow(supply, curveSteepness);
316
+ const kn = Math.pow(sigmoidK, curveSteepness);
317
+ return reserveRatio * (sn / (kn + sn));
318
+ }
319
+
320
+ default:
321
+ return reserveRatio * Math.pow(supply, 1.0 / curveSteepness);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Calculate the cost to buy `amount` tokens on the bonding curve.
327
+ * Integrates the price function from currentSupply to currentSupply + amount.
328
+ */
329
+ export function bondingCurveBuyCost(config: BondingCurveConfig, amount: number): Currency {
330
+ if (amount <= 0) return 0;
331
+
332
+ // Numerical integration using Simpson's rule (good enough for game economies)
333
+ const n = Math.max(10, Math.ceil(amount));
334
+ const h = amount / n;
335
+ let sum = 0;
336
+ const s0 = config.currentSupply;
337
+
338
+ for (let i = 0; i <= n; i++) {
339
+ const s = s0 + i * h;
340
+ const p = bondingCurvePrice(
341
+ s,
342
+ config.reserveRatio,
343
+ config.curveSteepness,
344
+ config.curveType,
345
+ config.sigmoidK
346
+ );
347
+
348
+ if (i === 0 || i === n) {
349
+ sum += p;
350
+ } else if (i % 2 === 1) {
351
+ sum += 4 * p;
352
+ } else {
353
+ sum += 2 * p;
354
+ }
355
+ }
356
+
357
+ const cost = (h / 3) * sum;
358
+ return cost * (1 + config.transactionFee); // Include fee
359
+ }
360
+
361
+ /**
362
+ * Calculate the refund for selling `amount` tokens on the bonding curve.
363
+ */
364
+ export function bondingCurveSellRefund(config: BondingCurveConfig, amount: number): Currency {
365
+ if (amount <= 0) return 0;
366
+ if (amount > config.currentSupply) return 0;
367
+
368
+ const n = Math.max(10, Math.ceil(amount));
369
+ const h = amount / n;
370
+ let sum = 0;
371
+ const s0 = config.currentSupply - amount;
372
+
373
+ for (let i = 0; i <= n; i++) {
374
+ const s = s0 + i * h;
375
+ const p = bondingCurvePrice(
376
+ s,
377
+ config.reserveRatio,
378
+ config.curveSteepness,
379
+ config.curveType,
380
+ config.sigmoidK
381
+ );
382
+
383
+ if (i === 0 || i === n) {
384
+ sum += p;
385
+ } else if (i % 2 === 1) {
386
+ sum += 4 * p;
387
+ } else {
388
+ sum += 2 * p;
389
+ }
390
+ }
391
+
392
+ const refund = (h / 3) * sum;
393
+ return refund * (1 - config.transactionFee); // Deduct fee
394
+ }
395
+
396
+ /**
397
+ * Apply spatial distance factor to a bonding curve price.
398
+ * P_spatial = P_bonding * (1 + distance_factor * distance)
399
+ * Creates emergent trade routes in VR economies (P.030.02 ENRICHED).
400
+ */
401
+ export function spatialPrice(
402
+ basePrice: Currency,
403
+ distanceToHub: number,
404
+ distanceFactor: number
405
+ ): Currency {
406
+ if (distanceFactor <= 0 || distanceToHub <= 0) return basePrice;
407
+ return basePrice * (1 + distanceFactor * distanceToHub);
408
+ }
409
+
410
+ // =============================================================================
411
+ // TAXABLE WEALTH PRIMITIVES
412
+ // =============================================================================
413
+
414
+ /**
415
+ * Configuration for progressive wealth taxation.
416
+ */
417
+ export interface WealthTaxConfig {
418
+ /** Wealth threshold below which no tax is levied */
419
+ threshold: Currency;
420
+ /** Base tax rate (multiplied by log of excess wealth) */
421
+ baseRate: number;
422
+ /** Maximum effective tax rate cap (prevents punishing engagement) */
423
+ maxEffectiveRate: number;
424
+ /** Tax collection interval description */
425
+ collectionInterval: 'hourly' | 'daily' | 'weekly';
426
+ /** Whether to redistribute to lowest quintile */
427
+ enableRedistribution: boolean;
428
+ /** Fraction of collected tax that goes to redistribution (remainder is burned as hard sink) */
429
+ redistributionFraction: number;
430
+ }
431
+
432
+ /**
433
+ * Default wealth tax configuration.
434
+ */
435
+ export const DEFAULT_WEALTH_TAX: WealthTaxConfig = {
436
+ threshold: 10000,
437
+ baseRate: 0.01, // 1% base
438
+ maxEffectiveRate: 0.05, // 5% cap
439
+ collectionInterval: 'daily',
440
+ enableRedistribution: true,
441
+ redistributionFraction: 0.7, // 70% redistributed, 30% burned as hard sink
442
+ };
443
+
444
+ /**
445
+ * Calculate the effective tax rate for a given wealth level.
446
+ * Uses logarithmic scaling: rate = min(maxRate, log(wealth/threshold) * baseRate)
447
+ * Returns 0 if wealth is below threshold (W.034).
448
+ */
449
+ export function calculateTaxRate(
450
+ wealth: Currency,
451
+ threshold: Currency,
452
+ baseRate: number,
453
+ maxEffectiveRate: number
454
+ ): number {
455
+ if (wealth <= threshold) return 0;
456
+ if (threshold <= 0) return 0;
457
+
458
+ const rate = Math.log(wealth / threshold) * baseRate;
459
+ return Math.min(rate, maxEffectiveRate);
460
+ }
461
+
462
+ /**
463
+ * Calculate the tax amount owed for a given wealth level.
464
+ */
465
+ export function calculateTaxAmount(wealth: Currency, config: WealthTaxConfig): Currency {
466
+ const rate = calculateTaxRate(wealth, config.threshold, config.baseRate, config.maxEffectiveRate);
467
+ return wealth * rate;
468
+ }
469
+
470
+ /**
471
+ * Calculate redistribution amounts from a tax pool.
472
+ * Returns the per-recipient amount and the amount burned as a hard sink.
473
+ */
474
+ export function calculateRedistribution(
475
+ totalTaxCollected: Currency,
476
+ recipientCount: number,
477
+ redistributionFraction: number
478
+ ): { perRecipient: Currency; burned: Currency } {
479
+ if (recipientCount <= 0 || totalTaxCollected <= 0) {
480
+ return { perRecipient: 0, burned: totalTaxCollected };
481
+ }
482
+
483
+ const redistributed = totalTaxCollected * redistributionFraction;
484
+ const burned = totalTaxCollected - redistributed;
485
+ const perRecipient = redistributed / recipientCount;
486
+
487
+ return { perRecipient, burned };
488
+ }
489
+
490
+ // =============================================================================
491
+ // PID CONTROLLER PRIMITIVES
492
+ // =============================================================================
493
+
494
+ /**
495
+ * Configuration for a PID controller (P.030.01).
496
+ */
497
+ export interface PIDConfig {
498
+ /** Proportional gain */
499
+ kp: number;
500
+ /** Integral gain */
501
+ ki: number;
502
+ /** Derivative gain */
503
+ kd: number;
504
+ /** Target setpoint (e.g., target money supply) */
505
+ setpoint: number;
506
+ /** Output limits [min, max] */
507
+ outputMin: number;
508
+ outputMax: number;
509
+ /** Anti-windup integral limit */
510
+ integralLimit: number;
511
+ }
512
+
513
+ /**
514
+ * Internal state of a PID controller.
515
+ */
516
+ export interface PIDState {
517
+ /** Previous error (for derivative term) */
518
+ previousError: number;
519
+ /** Accumulated integral */
520
+ integral: number;
521
+ /** Last output value */
522
+ lastOutput: number;
523
+ /** Whether the controller has ever been updated */
524
+ initialized: boolean;
525
+ }
526
+
527
+ /**
528
+ * Default PID configuration for economy flow control.
529
+ */
530
+ export const DEFAULT_PID: PIDConfig = {
531
+ kp: 0.5,
532
+ ki: 0.01,
533
+ kd: 0.1,
534
+ setpoint: 0,
535
+ outputMin: -1.0,
536
+ outputMax: 1.0,
537
+ integralLimit: 10.0,
538
+ };
539
+
540
+ /**
541
+ * Create initial PID state.
542
+ */
543
+ export function createPIDState(): PIDState {
544
+ return {
545
+ previousError: 0,
546
+ integral: 0,
547
+ lastOutput: 0,
548
+ initialized: false,
549
+ };
550
+ }
551
+
552
+ /**
553
+ * Update the PID controller with a new measurement.
554
+ *
555
+ * Returns the control output (e.g., faucet rate multiplier).
556
+ * Positive output means increase faucet rate, negative means decrease.
557
+ *
558
+ * @param config PID configuration
559
+ * @param state Current PID state (mutated in place for performance)
560
+ * @param measurement Current measured value (e.g., total money supply)
561
+ * @param dt Time delta in seconds
562
+ * @returns The control output value
563
+ */
564
+ export function updatePID(
565
+ config: PIDConfig,
566
+ state: PIDState,
567
+ measurement: number,
568
+ dt: number
569
+ ): number {
570
+ if (dt <= 0) return state.lastOutput;
571
+
572
+ const error = config.setpoint - measurement;
573
+
574
+ // Proportional term
575
+ const p = config.kp * error;
576
+
577
+ // Integral term with anti-windup
578
+ state.integral += error * dt;
579
+ state.integral = Math.max(-config.integralLimit, Math.min(config.integralLimit, state.integral));
580
+ const i = config.ki * state.integral;
581
+
582
+ // Derivative term (skip on first update to avoid spike)
583
+ let d = 0;
584
+ if (state.initialized) {
585
+ d = config.kd * ((error - state.previousError) / dt);
586
+ }
587
+
588
+ // Compute output with clamping
589
+ let output = p + i + d;
590
+ output = Math.max(config.outputMin, Math.min(config.outputMax, output));
591
+
592
+ // Update state
593
+ state.previousError = error;
594
+ state.lastOutput = output;
595
+ state.initialized = true;
596
+
597
+ return output;
598
+ }
599
+
600
+ /**
601
+ * Dual-loop PID controller configuration (P.030.01).
602
+ *
603
+ * Inner loop: Per-source faucet adjustment (fast response)
604
+ * Outer loop: Global money supply targeting (slow response, adjusts inner setpoint)
605
+ */
606
+ export interface DualLoopPIDConfig {
607
+ /** Inner loop PID (fast: adjusts faucet rates) */
608
+ innerLoop: PIDConfig;
609
+ /** Outer loop PID (slow: adjusts inner loop setpoint based on total supply) */
610
+ outerLoop: PIDConfig;
611
+ /** Target total money supply */
612
+ targetMoneySupply: number;
613
+ /** Active player count (for per-capita targeting) */
614
+ activePlayerCount: number;
615
+ /** Per-capita money supply target */
616
+ perCapitaTarget: number;
617
+ }
618
+
619
+ /**
620
+ * State for the dual-loop PID controller.
621
+ */
622
+ export interface DualLoopPIDState {
623
+ innerState: PIDState;
624
+ outerState: PIDState;
625
+ /** Current effective faucet rate multiplier (0.0 - 2.0, where 1.0 = normal) */
626
+ faucetMultiplier: number;
627
+ }
628
+
629
+ /**
630
+ * Create initial dual-loop PID state.
631
+ */
632
+ export function createDualLoopPIDState(): DualLoopPIDState {
633
+ return {
634
+ innerState: createPIDState(),
635
+ outerState: createPIDState(),
636
+ faucetMultiplier: 1.0,
637
+ };
638
+ }
639
+
640
+ /**
641
+ * Update the dual-loop PID controller.
642
+ *
643
+ * @param config Dual-loop configuration
644
+ * @param state Current state (mutated in place)
645
+ * @param totalMoneySupply Current total money supply
646
+ * @param currentFaucetRate Current inflow rate
647
+ * @param dt Time delta in seconds
648
+ * @returns The new faucet rate multiplier
649
+ */
650
+ export function updateDualLoopPID(
651
+ config: DualLoopPIDConfig,
652
+ state: DualLoopPIDState,
653
+ totalMoneySupply: number,
654
+ currentFaucetRate: number,
655
+ dt: number
656
+ ): number {
657
+ // Outer loop: adjust target based on total money supply vs. target
658
+ const targetSupply = config.perCapitaTarget * config.activePlayerCount;
659
+ const outerConfig = { ...config.outerLoop, setpoint: targetSupply };
660
+ const outerOutput = updatePID(outerConfig, state.outerState, totalMoneySupply, dt);
661
+
662
+ // Outer output adjusts inner loop setpoint (desired faucet rate)
663
+ const desiredFaucetRate = currentFaucetRate * (1 + outerOutput * 0.1);
664
+
665
+ // Inner loop: adjust faucet multiplier to achieve desired rate
666
+ const innerConfig = { ...config.innerLoop, setpoint: desiredFaucetRate };
667
+ const innerOutput = updatePID(innerConfig, state.innerState, currentFaucetRate, dt);
668
+
669
+ // Convert inner output to faucet multiplier (0.0 - 2.0 range)
670
+ state.faucetMultiplier = Math.max(0, Math.min(2, 1 + innerOutput));
671
+
672
+ return state.faucetMultiplier;
673
+ }
674
+
675
+ // =============================================================================
676
+ // FAUCET-SINK TRACKING
677
+ // =============================================================================
678
+
679
+ /**
680
+ * Tracks the faucet/sink ratio -- the master variable for economy health (W.031).
681
+ */
682
+ export interface FaucetSinkMetrics {
683
+ /** Total currency created in the current period */
684
+ totalFaucet: Currency;
685
+ /** Total currency destroyed in the current period */
686
+ totalSink: Currency;
687
+ /** Current faucet/sink ratio (target: ~1.0) */
688
+ ratio: number;
689
+ /** Velocity of money (transactions per unit per period) */
690
+ velocity: number;
691
+ /** Period start timestamp */
692
+ periodStart: number;
693
+ /** Historical ratio samples */
694
+ ratioHistory: number[];
695
+ }
696
+
697
+ /**
698
+ * Create initial faucet-sink metrics.
699
+ */
700
+ export function createFaucetSinkMetrics(timestamp: number): FaucetSinkMetrics {
701
+ return {
702
+ totalFaucet: 0,
703
+ totalSink: 0,
704
+ ratio: 1.0,
705
+ velocity: 0,
706
+ periodStart: timestamp,
707
+ ratioHistory: [],
708
+ };
709
+ }
710
+
711
+ /**
712
+ * Record a faucet event (currency creation).
713
+ */
714
+ export function recordFaucet(metrics: FaucetSinkMetrics, amount: Currency): void {
715
+ metrics.totalFaucet += amount;
716
+ metrics.ratio =
717
+ metrics.totalSink > 0
718
+ ? metrics.totalFaucet / metrics.totalSink
719
+ : metrics.totalFaucet > 0
720
+ ? Infinity
721
+ : 1.0;
722
+ }
723
+
724
+ /**
725
+ * Record a sink event (currency destruction).
726
+ */
727
+ export function recordSink(metrics: FaucetSinkMetrics, amount: Currency): void {
728
+ metrics.totalSink += amount;
729
+ metrics.ratio =
730
+ metrics.totalSink > 0
731
+ ? metrics.totalFaucet / metrics.totalSink
732
+ : metrics.totalFaucet > 0
733
+ ? Infinity
734
+ : 1.0;
735
+ }
736
+
737
+ /**
738
+ * Reset metrics for a new period, archiving the current ratio.
739
+ */
740
+ export function resetMetricsPeriod(metrics: FaucetSinkMetrics, timestamp: number): void {
741
+ // Archive current ratio
742
+ if (metrics.totalFaucet > 0 || metrics.totalSink > 0) {
743
+ metrics.ratioHistory.push(metrics.ratio);
744
+ // Keep last 100 periods
745
+ if (metrics.ratioHistory.length > 100) {
746
+ metrics.ratioHistory.shift();
747
+ }
748
+ }
749
+
750
+ metrics.totalFaucet = 0;
751
+ metrics.totalSink = 0;
752
+ metrics.ratio = 1.0;
753
+ metrics.velocity = 0;
754
+ metrics.periodStart = timestamp;
755
+ }