diff --git a/playground/deno.lock b/playground/deno.lock
new file mode 100644
index 0000000..afe945c
--- /dev/null
+++ b/playground/deno.lock
@@ -0,0 +1,24 @@
+{
+ "version": "5",
+ "specifiers": {
+ "jsr:@std/assert@1": "1.0.19",
+ "jsr:@std/internal@^1.0.12": "1.0.13"
+ },
+ "jsr": {
+ "@std/assert@1.0.19": {
+ "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e",
+ "dependencies": [
+ "jsr:@std/internal"
+ ]
+ },
+ "@std/internal@1.0.13": {
+ "integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0"
+ }
+ },
+ "workspace": {
+ "dependencies": [
+ "jsr:@std/assert@1",
+ "jsr:@std/testing@1"
+ ]
+ }
+}
diff --git a/playground/examples/uncertainty.ts b/playground/examples/uncertainty.ts
new file mode 100644
index 0000000..2dad43f
--- /dev/null
+++ b/playground/examples/uncertainty.ts
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: PMPL-1.0-or-later
+// SPDX-FileCopyrightText: 2025 hyperpolymath
+//
+// BetLang playground — uncertainty-aware number tower (sample).
+//
+// The full language exposes ~14 uncertainty number systems as its type
+// system. This example demonstrates two of them — interval arithmetic and
+// Gaussian (mean ± sd) propagation — and how the lazy ternary core lets a
+// comparison return Unknown when two uncertain quantities overlap.
+
+import { type Tri } from '../src/ternary.ts';
+
+/** Closed real interval [lo, hi]. */
+export interface Interval {
+ lo: number;
+ hi: number;
+}
+
+export const iv = (lo: number, hi: number): Interval => ({
+ lo: Math.min(lo, hi),
+ hi: Math.max(lo, hi),
+});
+
+export function addI(a: Interval, b: Interval): Interval {
+ return iv(a.lo + b.lo, a.hi + b.hi);
+}
+
+export function mulI(a: Interval, b: Interval): Interval {
+ const ps = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi];
+ return iv(Math.min(...ps), Math.max(...ps));
+}
+
+/** Ternary comparison: definite when disjoint, Unknown when they overlap. */
+export function ltI(a: Interval, b: Interval): Tri {
+ if (a.hi < b.lo) return 'T';
+ if (a.lo > b.hi) return 'F';
+ return 'U';
+}
+
+/** Gaussian number: mean with standard deviation. */
+export interface Gaussian {
+ mu: number;
+ sd: number;
+}
+
+export const gauss = (mu: number, sd: number): Gaussian => ({ mu, sd: Math.abs(sd) });
+
+/** First-order (uncorrelated) propagation through sum and product. */
+export function addG(a: Gaussian, b: Gaussian): Gaussian {
+ return gauss(a.mu + b.mu, Math.hypot(a.sd, b.sd));
+}
+
+export function mulG(a: Gaussian, b: Gaussian): Gaussian {
+ const mu = a.mu * b.mu;
+ const rel = Math.hypot(a.sd / a.mu, b.sd / b.mu);
+ return gauss(mu, Math.abs(mu) * rel);
+}
+
+function main(): void {
+ console.log('=== BetLang Uncertainty Modeling ===\n');
+
+ const a = iv(2, 4);
+ const b = iv(3, 5);
+ console.log(`Intervals: a=[${a.lo},${a.hi}] b=[${b.lo},${b.hi}]`);
+ console.log(` a + b = [${addI(a, b).lo}, ${addI(a, b).hi}]`);
+ console.log(` a * b = [${mulI(a, b).lo}, ${mulI(a, b).hi}]`);
+ console.log(` a < b ? -> ${ltI(a, b)} (overlap => Unknown, not a false certainty)`);
+ console.log(` [0,1] < [5,6] ? -> ${ltI(iv(0, 1), iv(5, 6))}`);
+
+ const g1 = gauss(10, 1);
+ const g2 = gauss(20, 2);
+ const s = addG(g1, g2);
+ const p = mulG(g1, g2);
+ console.log(`\nGaussians: g1=${g1.mu}±${g1.sd} g2=${g2.mu}±${g2.sd}`);
+ console.log(` g1 + g2 = ${s.mu}±${s.sd.toFixed(4)}`);
+ console.log(` g1 * g2 = ${p.mu}±${p.sd.toFixed(4)}`);
+}
+
+if (import.meta.main) {
+ main();
+}
diff --git a/playground/src/main.ts b/playground/src/main.ts
new file mode 100644
index 0000000..bdf82a0
--- /dev/null
+++ b/playground/src/main.ts
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: PMPL-1.0-or-later
+// SPDX-FileCopyrightText: 2025 hyperpolymath
+//
+// BetLang playground entry point (`deno task dev` / `just dev`).
+//
+// Runs the ternary core demo and the probabilistic layer demo in sequence,
+// giving a contributor a single runnable surface for the sandbox.
+
+import { main as ternaryDemo } from './ternary.ts';
+import { main as probabilityDemo } from './probability.ts';
+
+function main(): void {
+ console.log('BetLang Playground — Symbolic Probabilistic Metalanguage\n');
+ ternaryDemo();
+ console.log('\n' + '-'.repeat(60) + '\n');
+ probabilityDemo();
+ console.log('\nDone. See `just --list` for individual demos.');
+}
+
+if (import.meta.main) {
+ main();
+}
diff --git a/playground/src/probability.ts b/playground/src/probability.ts
new file mode 100644
index 0000000..16fa898
--- /dev/null
+++ b/playground/src/probability.ts
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: PMPL-1.0-or-later
+// SPDX-FileCopyrightText: 2025 hyperpolymath
+//
+// BetLang playground — probabilistic layer.
+//
+// Builds the non-uniform / predicate-driven choice forms on top of the
+// lazy ternary core:
+// (bet/weighted ...) — non-uniform probabilities
+// (bet_conditional ...) — predicate-driven selection
+//
+// Evaluation stays lazy: a Bet is a *description* of a choice; only the
+// branch that wins a draw is forced.
+
+import { bet, type Tri } from './ternary.ts';
+
+/** A lazy, weighted branch: a relative weight and a thunk producing a value. */
+export interface Branch {
+ weight: number;
+ value: () => A;
+}
+
+/** Deterministic, seedable PRNG (mulberry32) so demos/tests are reproducible. */
+export function rng(seed: number): () => number {
+ let s = seed >>> 0;
+ return () => {
+ s = (s + 0x6d2b79f5) >>> 0;
+ let t = s;
+ t = Math.imul(t ^ (t >>> 15), t | 1);
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+ };
+}
+
+/** Force exactly one branch, chosen with probability proportional to weight. */
+export function betWeighted(branches: Branch[], draw: () => number): A {
+ const total = branches.reduce((acc, b) => acc + b.weight, 0);
+ if (total <= 0) throw new Error('betWeighted: weights must sum to a positive number');
+ let r = draw() * total;
+ for (const b of branches) {
+ r -= b.weight;
+ if (r <= 0) return b.value();
+ }
+ return branches[branches.length - 1].value(); // float-rounding fallback
+}
+
+/**
+ * Predicate-driven ternary selection. The predicate yields a Tri; a definite
+ * answer takes the matching branch, Unknown defers to the `uncertain` branch.
+ */
+export function betConditional(
+ predicate: () => Tri,
+ ifTrue: () => A,
+ uncertain: () => A,
+ ifFalse: () => A,
+): A {
+ return bet(predicate, ifTrue, uncertain, ifFalse);
+}
+
+/** Monte-Carlo expectation of a numeric weighted bet over `n` samples. */
+export function expectation(branches: Branch[], n: number, draw: () => number): number {
+ let sum = 0;
+ for (let i = 0; i < n; i++) sum += betWeighted(branches, draw);
+ return sum / n;
+}
+
+export function main(): void {
+ console.log('=== BetLang Probabilistic Layer ===\n');
+
+ // A loaded three-sided "coin": 60% True, 30% Unknown, 10% False.
+ const loaded: Branch[] = [
+ { weight: 0.6, value: () => 'T' as Tri },
+ { weight: 0.3, value: () => 'U' as Tri },
+ { weight: 0.1, value: () => 'F' as Tri },
+ ];
+ const draw = rng(42);
+ const counts: Record = { T: 0, U: 0, F: 0 };
+ const N = 100_000;
+ for (let i = 0; i < N; i++) counts[betWeighted(loaded, draw)]++;
+ console.log(`Empirical distribution over ${N.toLocaleString()} draws (target 0.60/0.30/0.10):`);
+ console.log(
+ ` T=${(counts.T / N).toFixed(3)} U=${(counts.U / N).toFixed(3)} ` +
+ `F=${(counts.F / N).toFixed(3)}`,
+ );
+
+ // Expected payout of a weighted numeric bet.
+ const payout: Branch[] = [
+ { weight: 1, value: () => 100 },
+ { weight: 2, value: () => 10 },
+ { weight: 7, value: () => 0 },
+ ];
+ const ev = expectation(payout, 200_000, rng(7));
+ console.log(`\nExpected payout (analytic = 12.0): ${ev.toFixed(2)}`);
+
+ // Conditional choice that stays total under Unknown.
+ const choice = betConditional(
+ () => 'U' as Tri,
+ () => 'committed',
+ () => 'hedged (predicate was Unknown)',
+ () => 'declined',
+ );
+ console.log(`\nbet_conditional under Unknown -> ${choice}`);
+}
+
+if (import.meta.main) {
+ main();
+}
diff --git a/playground/src/ternary.ts b/playground/src/ternary.ts
new file mode 100644
index 0000000..1ba6ad9
--- /dev/null
+++ b/playground/src/ternary.ts
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: PMPL-1.0-or-later
+// SPDX-FileCopyrightText: 2025 hyperpolymath
+//
+// BetLang playground — ternary core.
+//
+// BetLang's minimal core is a Kleene strong three-valued logic over
+// { True, False, Unknown } plus the lazy ternary choice primitive
+// (bet A B C)
+// where only the selected branch is ever evaluated.
+//
+// The AND truth table here is exactly the one documented in the playground
+// Justfile (`just ternary-demo`): conjunction is `min`, disjunction is `max`
+// under the order F < U < T.
+
+/** The three logical values of the BetLang core. */
+export type Tri = 'T' | 'F' | 'U';
+
+const ORDER: Record = { F: 0, U: 1, T: 2 };
+const BY_RANK: Tri[] = ['F', 'U', 'T'];
+
+/** Kleene negation: swaps T/F, fixes U. */
+export function not(a: Tri): Tri {
+ if (a === 'T') return 'F';
+ if (a === 'F') return 'T';
+ return 'U';
+}
+
+/** Kleene conjunction = min under F < U < T. */
+export function and(a: Tri, b: Tri): Tri {
+ return BY_RANK[Math.min(ORDER[a], ORDER[b])];
+}
+
+/** Kleene disjunction = max under F < U < T. */
+export function or(a: Tri, b: Tri): Tri {
+ return BY_RANK[Math.max(ORDER[a], ORDER[b])];
+}
+
+/** Material implication, defined as `or(not(a), b)`. */
+export function implies(a: Tri, b: Tri): Tri {
+ return or(not(a), b);
+}
+
+/**
+ * The lazy ternary choice primitive `(bet A B C)`.
+ *
+ * Branches are passed as thunks; exactly one is forced. `selector` decides
+ * which branch wins — when it returns 'U' the middle branch is taken, which
+ * is what makes the choice *total* even under uncertainty.
+ */
+export function bet(
+ selector: () => Tri,
+ onTrue: () => A,
+ onUnknown: () => A,
+ onFalse: () => A,
+): A {
+ switch (selector()) {
+ case 'T':
+ return onTrue();
+ case 'F':
+ return onFalse();
+ default:
+ return onUnknown();
+ }
+}
+
+/** Render a full binary truth table for a Tri operator. */
+function table(name: string, op: (a: Tri, b: Tri) => Tri): string {
+ const vals: Tri[] = ['T', 'U', 'F'];
+ const rows = vals.flatMap((a) => vals.map((b) => ` ${a} ${name} ${b} = ${op(a, b)}`));
+ return [`${name} truth table:`, ...rows].join('\n');
+}
+
+export function main(): void {
+ console.log('=== BetLang Ternary Core ===');
+ console.log('Values: True (T), False (F), Unknown (U)\n');
+ console.log(table('AND', and));
+ console.log();
+ console.log(table('OR', or));
+ console.log();
+ console.log('NOT: NOT T = ' + not('T') + ', NOT U = ' + not('U') + ', NOT F = ' + not('F'));
+ console.log();
+
+ // Laziness demonstration: only the selected thunk runs.
+ let evaluated = '';
+ const result = bet(
+ () => 'U',
+ () => {
+ evaluated = 'true-branch';
+ return 1;
+ },
+ () => {
+ evaluated = 'unknown-branch';
+ return 0;
+ },
+ () => {
+ evaluated = 'false-branch';
+ return -1;
+ },
+ );
+ console.log(`Lazy (bet ? : :) on Unknown -> result=${result}, evaluated=${evaluated}`);
+ console.log('(only the Unknown branch ran; True/False thunks were never forced)');
+}
+
+if (import.meta.main) {
+ main();
+}
diff --git a/playground/test/probability_test.ts b/playground/test/probability_test.ts
new file mode 100644
index 0000000..cd0c69c
--- /dev/null
+++ b/playground/test/probability_test.ts
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: PMPL-1.0-or-later
+// SPDX-FileCopyrightText: 2025 hyperpolymath
+
+import { assert, assertEquals, assertThrows } from '@std/assert';
+import { betConditional, betWeighted, type Branch, expectation, rng } from '../src/probability.ts';
+import { type Tri } from '../src/ternary.ts';
+
+Deno.test('rng is deterministic for a fixed seed and stays in [0,1)', () => {
+ const a = rng(123);
+ const b = rng(123);
+ for (let i = 0; i < 50; i++) {
+ const x = a();
+ assertEquals(x, b());
+ assert(x >= 0 && x < 1);
+ }
+});
+
+Deno.test('betWeighted respects weights within Monte-Carlo tolerance', () => {
+ const branches: Branch[] = [
+ { weight: 0.6, value: () => 'T' },
+ { weight: 0.3, value: () => 'U' },
+ { weight: 0.1, value: () => 'F' },
+ ];
+ const draw = rng(42);
+ const counts: Record = { T: 0, U: 0, F: 0 };
+ const N = 50_000;
+ for (let i = 0; i < N; i++) counts[betWeighted(branches, draw)]++;
+ assert(Math.abs(counts.T / N - 0.6) < 0.02);
+ assert(Math.abs(counts.U / N - 0.3) < 0.02);
+ assert(Math.abs(counts.F / N - 0.1) < 0.02);
+});
+
+Deno.test('betWeighted rejects non-positive total weight', () => {
+ assertThrows(() => betWeighted([{ weight: 0, value: () => 1 }], rng(1)));
+});
+
+Deno.test('expectation converges to the analytic mean', () => {
+ const payout: Branch[] = [
+ { weight: 1, value: () => 100 },
+ { weight: 2, value: () => 10 },
+ { weight: 7, value: () => 0 },
+ ];
+ // analytic EV = (1*100 + 2*10 + 7*0) / 10 = 12
+ assert(Math.abs(expectation(payout, 100_000, rng(7)) - 12) < 0.5);
+});
+
+Deno.test('betConditional defers to the uncertain branch on Unknown', () => {
+ assertEquals(
+ betConditional(() => 'U', () => 'yes', () => 'maybe', () => 'no'),
+ 'maybe',
+ );
+ assertEquals(
+ betConditional(() => 'T', () => 'yes', () => 'maybe', () => 'no'),
+ 'yes',
+ );
+});
diff --git a/playground/test/ternary_test.ts b/playground/test/ternary_test.ts
new file mode 100644
index 0000000..6de8905
--- /dev/null
+++ b/playground/test/ternary_test.ts
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: PMPL-1.0-or-later
+// SPDX-FileCopyrightText: 2025 hyperpolymath
+
+import { assertEquals } from '@std/assert';
+import { and, bet, implies, not, or, type Tri } from '../src/ternary.ts';
+
+const ALL: Tri[] = ['T', 'U', 'F'];
+
+Deno.test('negation is involutive and fixes Unknown', () => {
+ assertEquals(not('T'), 'F');
+ assertEquals(not('F'), 'T');
+ assertEquals(not('U'), 'U');
+ for (const v of ALL) assertEquals(not(not(v)), v);
+});
+
+Deno.test('AND matches the documented Justfile truth table (min)', () => {
+ assertEquals(and('T', 'T'), 'T');
+ assertEquals(and('T', 'U'), 'U');
+ assertEquals(and('T', 'F'), 'F');
+ assertEquals(and('U', 'U'), 'U');
+ assertEquals(and('U', 'F'), 'F');
+ assertEquals(and('F', 'F'), 'F');
+});
+
+Deno.test('AND and OR are commutative; De Morgan holds', () => {
+ for (const a of ALL) {
+ for (const b of ALL) {
+ assertEquals(and(a, b), and(b, a));
+ assertEquals(or(a, b), or(b, a));
+ assertEquals(not(and(a, b)), or(not(a), not(b)));
+ }
+ }
+});
+
+Deno.test('implies(a,b) == or(not a, b)', () => {
+ for (const a of ALL) {
+ for (const b of ALL) assertEquals(implies(a, b), or(not(a), b));
+ }
+});
+
+Deno.test('bet is lazy: only the selected branch is forced', () => {
+ const forced: string[] = [];
+ const r = bet(
+ () => 'F',
+ () => {
+ forced.push('T');
+ return 1;
+ },
+ () => {
+ forced.push('U');
+ return 2;
+ },
+ () => {
+ forced.push('F');
+ return 3;
+ },
+ );
+ assertEquals(r, 3);
+ assertEquals(forced, ['F']);
+});