import { enumerate, outcomes, solve, type Objective, type Outcome, type State } from './pikurn' const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2, }) export const money = (value: number) => currency.format(Math.abs(value) < 0.005 ? 0 : value) export const percent = (value: number) => `${(value * 100).toFixed(value > 0.099 ? 1 : 2)}%` export const symbols: Record = { G: '●', R: '◆', B: '■' } export const colors: Record = { G: 'green', R: 'red', B: 'blue' } export function wagerSnapshot(x: number, y: number) { const values = [4 * (100 - x), 2 * (100 + x - y), 100 + x + y] const mean = values.reduce((a, b) => a + b, 0) / 3 const floor = Math.min(...values) const names = ['R G G', 'G R G', 'G G R'] const scale = 300 / 400 const bars = `Three equally likely endings${values .map((value, index) => { const top = 28 + index * 68 return `${names[index]}${money(value)}` }) .join('')}Terminal bankroll ($)` const px = (v: number) => 55 + (v / 200) * 355 const py = (v: number) => 215 - ((v - 100) / 150) * 185 const grid = [100, 150, 200, 250] .map( (v) => `${v}` ) .join('') const ticks = [0, 50, 100, 150, 200] .map((v) => `${v}`) .join('') const frontier = `Average versus guaranteed bankroll: your strategy and the efficient frontier${grid}${ticks}Average ($)Guaranteed bankroll ($)` return { values, mean, floor, bars, frontier } } export function explorerSnapshot( state: State = { g: 2, r: 1, b: 0, n: 3 }, objective: Objective = 'expected' ) { const paths = enumerate(state, (next) => solve(next, objective).fraction, 100) const average = paths.reduce((sum, path) => sum + path.wealth * path.probability, 0) const wealth = paths.map((path) => path.wealth) const decision = solve(state, objective) const branches = outcomes(state) const urn = (['G', 'R', 'B'] as const) .flatMap((outcome) => Array(state[outcome.toLowerCase() as 'g' | 'r' | 'b']).fill(outcome)) .map((outcome) => ``) .join('') const pathsMarkup = paths .map( (path) => `${path.path.map((outcome) => `${symbols[outcome]} ${outcome}`).join(' → ')}${percent(path.probability)}${money(path.wealth)}` ) .join('') const buckets = new Map() for (const path of paths) buckets.set(path.wealth, (buckets.get(path.wealth) ?? 0) + path.probability) const distributionMarkup = [...buckets] .sort(([a], [b]) => a - b) .map( ([value, probability]) => `
${money(value)}${percent(probability)}
` ) .join('') const tree = (current: State, bankroll: number, depth: number): string => `
    ${outcomes(current) .map((branch) => { const fraction = solve(current, objective).fraction const nextBankroll = bankroll * (branch.outcome === 'G' ? 1 + fraction : branch.outcome === 'R' ? 1 - fraction : 1) const stop = !branch.next || branch.next.n === 0 || depth === 2 const next = stop ? '' : tree(branch.next!, nextBankroll, depth + 1) return `
  • ${branch.outcome} · ${percent(branch.probability)} → ${money(nextBankroll)} · ${stop ? 'finish' : `next wager ${money(nextBankroll * solve(branch.next!, objective).fraction)}`}${next}
  • ` }) .join('')}
` const treeMarkup = `

Start $100 · wager ${money(100 * decision.fraction)}

${tree(state, 100, 0)}` return { paths, average, worst: Math.min(...wealth), best: Math.max(...wealth), wager: 100 * decision.fraction, branches, urn, pathsMarkup, distributionMarkup, treeMarkup, } }