|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// SPDX-FileCopyrightText: 2025 hyperpolymath |
| 3 | +// |
| 4 | +// BetLang playground — probabilistic layer. |
| 5 | +// |
| 6 | +// Builds the non-uniform / predicate-driven choice forms on top of the |
| 7 | +// lazy ternary core: |
| 8 | +// (bet/weighted ...) — non-uniform probabilities |
| 9 | +// (bet_conditional ...) — predicate-driven selection |
| 10 | +// |
| 11 | +// Evaluation stays lazy: a Bet is a *description* of a choice; only the |
| 12 | +// branch that wins a draw is forced. |
| 13 | + |
| 14 | +import { bet, type Tri } from './ternary.ts'; |
| 15 | + |
| 16 | +/** A lazy, weighted branch: a relative weight and a thunk producing a value. */ |
| 17 | +export interface Branch<A> { |
| 18 | + weight: number; |
| 19 | + value: () => A; |
| 20 | +} |
| 21 | + |
| 22 | +/** Deterministic, seedable PRNG (mulberry32) so demos/tests are reproducible. */ |
| 23 | +export function rng(seed: number): () => number { |
| 24 | + let s = seed >>> 0; |
| 25 | + return () => { |
| 26 | + s = (s + 0x6d2b79f5) >>> 0; |
| 27 | + let t = s; |
| 28 | + t = Math.imul(t ^ (t >>> 15), t | 1); |
| 29 | + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); |
| 30 | + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; |
| 31 | + }; |
| 32 | +} |
| 33 | + |
| 34 | +/** Force exactly one branch, chosen with probability proportional to weight. */ |
| 35 | +export function betWeighted<A>(branches: Branch<A>[], draw: () => number): A { |
| 36 | + const total = branches.reduce((acc, b) => acc + b.weight, 0); |
| 37 | + if (total <= 0) throw new Error('betWeighted: weights must sum to a positive number'); |
| 38 | + let r = draw() * total; |
| 39 | + for (const b of branches) { |
| 40 | + r -= b.weight; |
| 41 | + if (r <= 0) return b.value(); |
| 42 | + } |
| 43 | + return branches[branches.length - 1].value(); // float-rounding fallback |
| 44 | +} |
| 45 | + |
| 46 | +/** |
| 47 | + * Predicate-driven ternary selection. The predicate yields a Tri; a definite |
| 48 | + * answer takes the matching branch, Unknown defers to the `uncertain` branch. |
| 49 | + */ |
| 50 | +export function betConditional<A>( |
| 51 | + predicate: () => Tri, |
| 52 | + ifTrue: () => A, |
| 53 | + uncertain: () => A, |
| 54 | + ifFalse: () => A, |
| 55 | +): A { |
| 56 | + return bet(predicate, ifTrue, uncertain, ifFalse); |
| 57 | +} |
| 58 | + |
| 59 | +/** Monte-Carlo expectation of a numeric weighted bet over `n` samples. */ |
| 60 | +export function expectation(branches: Branch<number>[], n: number, draw: () => number): number { |
| 61 | + let sum = 0; |
| 62 | + for (let i = 0; i < n; i++) sum += betWeighted(branches, draw); |
| 63 | + return sum / n; |
| 64 | +} |
| 65 | + |
| 66 | +export function main(): void { |
| 67 | + console.log('=== BetLang Probabilistic Layer ===\n'); |
| 68 | + |
| 69 | + // A loaded three-sided "coin": 60% True, 30% Unknown, 10% False. |
| 70 | + const loaded: Branch<Tri>[] = [ |
| 71 | + { weight: 0.6, value: () => 'T' as Tri }, |
| 72 | + { weight: 0.3, value: () => 'U' as Tri }, |
| 73 | + { weight: 0.1, value: () => 'F' as Tri }, |
| 74 | + ]; |
| 75 | + const draw = rng(42); |
| 76 | + const counts: Record<Tri, number> = { T: 0, U: 0, F: 0 }; |
| 77 | + const N = 100_000; |
| 78 | + for (let i = 0; i < N; i++) counts[betWeighted(loaded, draw)]++; |
| 79 | + console.log(`Empirical distribution over ${N.toLocaleString()} draws (target 0.60/0.30/0.10):`); |
| 80 | + console.log( |
| 81 | + ` T=${(counts.T / N).toFixed(3)} U=${(counts.U / N).toFixed(3)} ` + |
| 82 | + `F=${(counts.F / N).toFixed(3)}`, |
| 83 | + ); |
| 84 | + |
| 85 | + // Expected payout of a weighted numeric bet. |
| 86 | + const payout: Branch<number>[] = [ |
| 87 | + { weight: 1, value: () => 100 }, |
| 88 | + { weight: 2, value: () => 10 }, |
| 89 | + { weight: 7, value: () => 0 }, |
| 90 | + ]; |
| 91 | + const ev = expectation(payout, 200_000, rng(7)); |
| 92 | + console.log(`\nExpected payout (analytic = 12.0): ${ev.toFixed(2)}`); |
| 93 | + |
| 94 | + // Conditional choice that stays total under Unknown. |
| 95 | + const choice = betConditional( |
| 96 | + () => 'U' as Tri, |
| 97 | + () => 'committed', |
| 98 | + () => 'hedged (predicate was Unknown)', |
| 99 | + () => 'declined', |
| 100 | + ); |
| 101 | + console.log(`\nbet_conditional under Unknown -> ${choice}`); |
| 102 | +} |
| 103 | + |
| 104 | +if (import.meta.main) { |
| 105 | + main(); |
| 106 | +} |
0 commit comments