|
| 1 | +#!/usr/bin/env node |
| 2 | +'use strict' |
| 3 | + |
| 4 | +const { playHand } = require('./index.js') |
| 5 | +const { minPassLineMaxOddsPlaceSixEight } = require('./betting.js') |
| 6 | + |
| 7 | +function parseOdds (str) { |
| 8 | + const parts = String(str).split('-').map(n => parseInt(n, 10)) |
| 9 | + if (parts.length !== 3 || parts.some(n => isNaN(n))) return {} |
| 10 | + return { |
| 11 | + 4: parts[0], |
| 12 | + 5: parts[1], |
| 13 | + 6: parts[2], |
| 14 | + 8: parts[2], |
| 15 | + 9: parts[1], |
| 16 | + 10: parts[0] |
| 17 | + } |
| 18 | +} |
| 19 | + |
| 20 | +function percentile (sorted, p) { |
| 21 | + if (sorted.length === 0) return 0 |
| 22 | + const idx = Math.ceil((p / 100) * sorted.length) - 1 |
| 23 | + return sorted[Math.min(Math.max(idx, 0), sorted.length - 1)] |
| 24 | +} |
| 25 | + |
| 26 | +function summary (arr) { |
| 27 | + const sorted = [...arr].sort((a, b) => a - b) |
| 28 | + return { |
| 29 | + min: sorted[0], |
| 30 | + max: sorted[sorted.length - 1], |
| 31 | + p1: percentile(sorted, 1), |
| 32 | + p5: percentile(sorted, 5), |
| 33 | + p10: percentile(sorted, 10), |
| 34 | + p25: percentile(sorted, 25), |
| 35 | + p50: percentile(sorted, 50), |
| 36 | + p75: percentile(sorted, 75), |
| 37 | + p90: percentile(sorted, 90), |
| 38 | + p95: percentile(sorted, 95), |
| 39 | + p99: percentile(sorted, 99) |
| 40 | + } |
| 41 | +} |
| 42 | + |
| 43 | +function summaryTable (arr) { |
| 44 | + const obj = summary(arr) |
| 45 | + const order = ['min', 'p1', 'p5', 'p10', 'p25', 'p50', 'p75', 'p90', 'p95', 'p99', 'max'] |
| 46 | + return order.map(k => ({ stat: k, value: obj[k] })) |
| 47 | +} |
| 48 | + |
| 49 | +function simulateTrial ({ handsPerTrial, startingBankroll, rules }) { |
| 50 | + let balance = startingBankroll |
| 51 | + let rolls = 0 |
| 52 | + for (let i = 0; i < handsPerTrial; i++) { |
| 53 | + const { history, balance: result } = playHand({ |
| 54 | + rules, |
| 55 | + bettingStrategy: minPassLineMaxOddsPlaceSixEight |
| 56 | + }) |
| 57 | + balance += result |
| 58 | + rolls += history.length |
| 59 | + } |
| 60 | + return { balance, rolls } |
| 61 | +} |
| 62 | + |
| 63 | +function monteCarlo ({ trials, handsPerTrial, startingBankroll, rules }) { |
| 64 | + const results = [] |
| 65 | + for (let i = 0; i < trials; i++) { |
| 66 | + results.push(simulateTrial({ handsPerTrial, startingBankroll, rules })) |
| 67 | + } |
| 68 | + return results |
| 69 | +} |
| 70 | + |
| 71 | +function printResults (results) { |
| 72 | + console.log('\nTrial Results') |
| 73 | + console.table(results.map((r, i) => ({ trial: i + 1, balance: r.balance, rolls: r.rolls }))) |
| 74 | + |
| 75 | + console.log('\nFinal Balance Summary') |
| 76 | + console.table(summaryTable(results.map(r => r.balance))) |
| 77 | + |
| 78 | + console.log('\nRoll Count Summary') |
| 79 | + console.table(summaryTable(results.map(r => r.rolls))) |
| 80 | +} |
| 81 | + |
| 82 | +if (require.main === module) { |
| 83 | + const trials = parseInt(process.argv[2], 10) |
| 84 | + const handsPerTrial = parseInt(process.argv[3], 10) |
| 85 | + const startingBankroll = parseInt(process.argv[4], 10) |
| 86 | + const minBet = parseInt(process.argv[5], 10) |
| 87 | + const oddsInput = process.argv[6] || '3-4-5' |
| 88 | + |
| 89 | + const rules = { |
| 90 | + minBet, |
| 91 | + maxOddsMultiple: parseOdds(oddsInput) |
| 92 | + } |
| 93 | + |
| 94 | + console.log(`Running ${trials} trials with ${handsPerTrial} hand(s) each`) |
| 95 | + console.log(`[table rules] minimum bet: $${minBet}, odds ${oddsInput}`) |
| 96 | + const results = monteCarlo({ trials, handsPerTrial, startingBankroll, rules }) |
| 97 | + printResults(results) |
| 98 | +} else { |
| 99 | + module.exports = { monteCarlo, simulateTrial, summary } |
| 100 | +} |
0 commit comments