diff --git a/README.md b/README.md index 95f6370..604ee11 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,37 @@ Hand: 1 └─────────┴──────┴──────┴─────────┴─────────────┴───────────┴───────┘ ``` +## strategies + +The `betting` module exposes a few simple strategy helpers: + +- `minPassLineOnly` – always maintains a minimum pass line bet +- `minPassLineMaxOdds` – adds maximum odds on the pass line point +- `minComeLineMaxOdds` – once a point is set, places come bets with max odds + +## table rules + +`playHand` accepts a `rules` object that controls minimum bets and odds limits. +You can now also customize which numbers win or lose on the come out roll. + +```js +const rules = { + minBet: 5, + maxOddsMultiple: { /* ... */ }, + comeOutWin: [7, 11], + comeOutLoss: [2, 3, 12] // default +} +``` + +For example, to make boxcars (12) a come out win instead of a loss: + +```js +const rules = { + comeOutLoss: [2, 3], + comeOutWin: [7, 11, 12] +} +``` + ## what? why? I like to play craps sometimes. I have a handful of strategies I like to play. It is time consuming to play in an app. I'd like to play 5, 50, 500 hands very fast using various strategies. Which strategies are best is well understood, the variability comes in with how aggressive your strategies are and the level of risk you assume at any given moment. And of course the dice outcomes and their deviation from long term probabilities and how they interact with the strategies you employ is the fun part. This simulator lets me scratch my craps itch very quickly. diff --git a/betting.js b/betting.js index 47623f4..1238e61 100644 --- a/betting.js +++ b/betting.js @@ -35,7 +35,51 @@ function minPassLineMaxOdds (opts) { return bets } +function minComeLineMaxOdds (opts) { + const { rules, hand } = opts + const bets = minPassLineMaxOdds(opts) + + if (!hand.isComeOut && !bets?.come?.line) { + bets.come = { + line: { amount: rules.minBet }, + isComeOut: true + } + bets.new += rules.minBet + } + + if (bets?.come?.line && !bets.come.isComeOut && !bets.come.odds) { + const oddsAmount = rules.maxOddsMultiple[bets.come.point] * bets.come.line.amount + bets.come.odds = { amount: oddsAmount } + bets.new += oddsAmount + } + + return bets +} + +function placeSixEight (opts) { + const { rules, bets: existingBets = {}, hand } = opts + const bets = Object.assign({ new: 0 }, existingBets) + + if (hand.isComeOut) return bets + + bets.place = bets.place || {} + + if (!bets.place.six) { + bets.place.six = { amount: rules.minBet } + bets.new += bets.place.six.amount + } + + if (!bets.place.eight) { + bets.place.eight = { amount: rules.minBet } + bets.new += bets.place.eight.amount + } + + return bets +} + module.exports = { minPassLineOnly, - minPassLineMaxOdds + minPassLineMaxOdds, + minComeLineMaxOdds, + placeSixEight } diff --git a/betting.test.js b/betting.test.js index 1387e26..f0d6e58 100644 --- a/betting.test.js +++ b/betting.test.js @@ -209,3 +209,124 @@ tap.test('minPassLineMaxOdds: continue existing bet', (t) => { t.end() }) + +tap.test('minComeLineMaxOdds: place come bet after point set', (t) => { + const rules = { + minBet: 5, + maxOddsMultiple: { + 4: 3, + 5: 4, + 6: 5, + 8: 5, + 9: 4, + 10: 3 + } + } + + const hand = { isComeOut: false, point: 5 } + const bets = { + pass: { line: { amount: 5, isContract: true }, odds: { amount: 20 } }, + new: 0 + } + + const updatedBets = lib.minComeLineMaxOdds({ rules, bets, hand }) + t.equal(updatedBets.come.line.amount, rules.minBet) + t.ok(updatedBets.come.isComeOut) + t.equal(updatedBets.new, rules.minBet) + t.end() +}) + +tap.test('placeSixEight: make new place bets after point set', (t) => { + const rules = { + minBet: 6 + } + + const hand = { + isComeOut: false, + point: 5 + } + + const updatedBets = lib.placeSixEight({ rules, hand }) + + t.equal(updatedBets.place.six.amount, rules.minBet) + t.equal(updatedBets.place.eight.amount, rules.minBet) + t.equal(updatedBets.new, rules.minBet * 2) + + t.end() +}) + +tap.test('minComeLineMaxOdds: add odds after come point', (t) => { + const rules = { + minBet: 5, + maxOddsMultiple: { + 4: 3, + 5: 4, + 6: 5, + 8: 5, + 9: 4, + 10: 3 + } + } + + const hand = { isComeOut: false, point: 5 } + const bets = { + pass: { line: { amount: 5, isContract: true }, odds: { amount: 20 } }, + come: { line: { amount: 5 }, isComeOut: false, point: 4 }, + new: 0 + } + + const updatedBets = lib.minComeLineMaxOdds({ rules, bets, hand }) + t.equal(updatedBets.come.odds.amount, rules.maxOddsMultiple['4'] * 5) + t.equal(updatedBets.new, rules.maxOddsMultiple['4'] * 5) + + t.end() +}) + +tap.test('placeSixEight: no new bets on comeout', (t) => { + const rules = { minBet: 6 } + const hand = { isComeOut: true } + + const updatedBets = lib.placeSixEight({ rules, hand }) + + t.notOk(updatedBets.place) + t.notOk(updatedBets.new) + + t.end() +}) + +tap.test('placeSixEight: existing bets remain', (t) => { + const rules = { minBet: 6 } + const hand = { isComeOut: false, point: 8 } + + const bets = { place: { six: { amount: 6 }, eight: { amount: 6 } } } + + const updatedBets = lib.placeSixEight({ rules, bets, hand }) + + t.equal(updatedBets.place.six.amount, 6) + t.equal(updatedBets.place.eight.amount, 6) + t.notOk(updatedBets.new) + + t.end() +}) + +tap.test('placeSixEight: place bets even when point is 6 or 8', (t) => { + const rules = { minBet: 6 } + const handSix = { isComeOut: false, point: 6 } + + const firstBets = lib.placeSixEight({ rules, hand: handSix }) + + t.equal(firstBets.place.six.amount, rules.minBet) + t.equal(firstBets.place.eight.amount, rules.minBet) + t.equal(firstBets.new, rules.minBet * 2) + + delete firstBets.new + + const handEight = { isComeOut: false, point: 8 } + const bets = lib.placeSixEight({ rules, bets: firstBets, hand: handEight }) + + t.equal(bets.place.six.amount, rules.minBet) + t.equal(bets.place.eight.amount, rules.minBet) + t.notOk(bets.new) + + t.end() +}) diff --git a/hands.js b/hands.js index 7c56797..14adaf2 100644 --- a/hands.js +++ b/hands.js @@ -1,13 +1,13 @@ 'use strict' const { playHand } = require('./index.js') -const { minPassLineMaxOdds } = require('./betting.js') +const { placeSixEight } = require('./betting.js') const numHands = parseInt(process.argv.slice(2)[0], 10) const showDetail = process.argv.slice(2)[1] console.log(`Simulating ${numHands} Craps Hand(s)`) -console.log('Using betting strategy: minPassLineMaxOdds') +console.log('Using betting strategy: placeSixEight') const summaryTemplate = { balance: 0, @@ -51,7 +51,7 @@ const rules = { console.log(`[table rules] minimum bet: $${rules.minBet}`) for (let i = 0; i < numHands; i++) { - const hand = playHand({ rules, bettingStrategy: minPassLineMaxOdds }) + const hand = playHand({ rules, bettingStrategy: placeSixEight }) hand.summary = Object.assign({}, summaryTemplate) sessionSummary.balance += hand.balance diff --git a/index.js b/index.js index d52aeba..eb61ddf 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,13 @@ function rollD6 () { return 1 + Math.floor(Math.random() * 6) } -function shoot (before, dice) { +const defaultRules = { + comeOutLoss: [2, 3, 12], + comeOutWin: [7, 11] +} + +function shoot (before, dice, rules = defaultRules) { + rules = Object.assign({}, defaultRules, rules) const sortedDice = dice.sort() const after = { @@ -19,10 +25,10 @@ function shoot (before, dice) { // game logic based on: https://github.com/tphummel/dice-collector/blob/master/PyTom/Dice/logic.py if (before.isComeOut) { - if ([2, 3, 12].indexOf(after.diceSum) !== -1) { + if (rules.comeOutLoss.includes(after.diceSum)) { after.result = 'comeout loss' after.isComeOut = true - } else if ([7, 11].indexOf(after.diceSum) !== -1) { + } else if (rules.comeOutWin.includes(after.diceSum)) { after.result = 'comeout win' after.isComeOut = true } else { @@ -65,7 +71,8 @@ function playHand ({ rules, bettingStrategy, roll = rollD6 }) { hand = shoot( hand, - [roll(), roll()] + [roll(), roll()], + rules ) if (process.env.DEBUG) console.log(`[roll] ${hand.result} (${hand.diceSum})`) @@ -88,5 +95,6 @@ module.exports = { rollD6, shoot, playHand, - betting + betting, + defaultRules } diff --git a/index.test.js b/index.test.js index ea3e556..0210561 100644 --- a/index.test.js +++ b/index.test.js @@ -199,6 +199,27 @@ tap.test('comeout', function (suite) { suite.end() }) +tap.test('comeout with custom rules', (t) => { + const handState = { + isComeOut: true + } + + const rules = { + comeOutLoss: [2, 3], + comeOutWin: [7, 11, 12] + } + + const result = lib.shoot(handState, [6, 6], rules) + t.equal(result.result, 'comeout win') + t.notOk(result.point) + t.equal(result.die1, 6) + t.equal(result.die2, 6) + t.equal(result.diceSum, 12) + t.equal(result.isComeOut, true) + + t.end() +}) + tap.test('point set', (suite) => { suite.test('neutral 2', (t) => { const handState = { @@ -479,3 +500,43 @@ tap.test('integration: minPassLineMaxOdds, one hand with everything', (suite) => suite.end() }) + +tap.test('integration: minComeLineMaxOdds, one hand with come bets', (suite) => { + let rollCount = -1 + const fixedRolls = [ + 4, 3, // comeout win + 5, 6, // comeout win + 2, 2, // comeout loss + 3, 3, // point set + 5, 4, // come bet sets point + 5, 4, // come bet point win + 3, 4 // seven out + ] + + function testRoll () { + rollCount++ + if (!fixedRolls[rollCount]) { + console.log('falsy return from fixed dice') + process.exit(1) + } + return fixedRolls[rollCount] + } + + const rules = { + minBet: 5, + maxOddsMultiple: { + 4: 3, + 5: 4, + 6: 5, + 8: 5, + 9: 4, + 10: 3 + } + } + + const hand = lib.playHand({ rules, roll: testRoll, bettingStrategy: betting.minComeLineMaxOdds }) + suite.ok(Array.isArray(hand.history)) + suite.type(hand.balance, 'number') + + suite.end() +}) diff --git a/settle.js b/settle.js index 7af2783..fdb67f6 100644 --- a/settle.js +++ b/settle.js @@ -19,6 +19,15 @@ function passLine ({ bets, hand, rules }) { return { payout, bets } } +const oddsPayouts = { + 4: 2, + 5: 3 / 2, + 6: 6 / 5, + 8: 6 / 5, + 9: 3 / 2, + 10: 2 +} + function passOdds ({ bets, hand, rules }) { if (!bets?.pass?.odds) return { bets } @@ -27,19 +36,10 @@ function passOdds ({ bets, hand, rules }) { if (!betHasAction) return { bets } // keep bets intact if no action - const payouts = { - 4: 2, - 5: 3 / 2, - 6: 6 / 5, - 8: 6 / 5, - 9: 3 / 2, - 10: 2 - } - const payout = { type: 'pass odds win', principal: bets.pass.odds.amount, - profit: bets.pass.odds.amount * payouts[hand.diceSum] + profit: bets.pass.odds.amount * oddsPayouts[hand.diceSum] } delete bets.pass.odds // clear pass odds bet on action @@ -49,6 +49,111 @@ function passOdds ({ bets, hand, rules }) { return { payout, bets } } +function comeLine ({ bets, hand, rules }) { + if (!bets?.come?.line) return { bets } + + const bet = bets.come + let payout + + if (bet.isComeOut) { + if ([2, 3, 12].includes(hand.diceSum)) { + delete bets.come + return { bets } + } + if ([7, 11].includes(hand.diceSum)) { + payout = { + type: 'come win', + principal: bet.line.amount, + profit: bet.line.amount * 1 + } + delete bets.come + return { payout, bets } + } + + bet.point = hand.diceSum + bet.isComeOut = false + bet.line.isContract = true + return { bets } + } + + if (hand.diceSum === bet.point) { + payout = { + type: 'come win', + principal: bet.line.amount, + profit: bet.line.amount * 1 + } + delete bet.line + if (!bet.odds) delete bets.come + return { payout, bets } + } + + if (hand.diceSum === 7) { + delete bet.line + if (!bet.odds) delete bets.come + return { bets } + } + + return { bets } +} + +function comeOdds ({ bets, hand, rules }) { + if (!bets?.come?.odds) return { bets } + + const bet = bets.come + + if (hand.diceSum === bet.point) { + const payout = { + type: 'come odds win', + principal: bet.odds.amount, + profit: bet.odds.amount * oddsPayouts[bet.point] + } + delete bet.odds + if (!bet.line) delete bets.come + return { payout, bets } + } + + if (hand.diceSum === 7) { + delete bet.odds + if (!bet.line) delete bets.come + return { bets } + } + + return { bets } +} + +function placeBet ({ bets, hand, placeNumber }) { + const label = placeNumber === 6 ? 'six' : placeNumber === 8 ? 'eight' : String(placeNumber) + + if (!bets?.place?.[label]) return { bets } + if (hand.isComeOut && hand.result !== 'seven out') return { bets } + if (hand.result === 'point set') return { bets } + + if (hand.diceSum === 7) { + delete bets.place[label] + if (Object.keys(bets.place).length === 0) delete bets.place + return { bets } + } + + if (hand.diceSum === placeNumber) { + const payout = { + type: `place ${placeNumber} win`, + principal: 0, + profit: bets.place[label].amount * (7 / 6) + } + + return { payout, bets } + } + + return { bets } +} + +function placeSix (opts) { + return placeBet({ ...opts, placeNumber: 6 }) +} + +function placeEight (opts) { + return placeBet({ ...opts, placeNumber: 8 }) +} function all ({ bets, hand, rules }) { const payouts = [] @@ -62,6 +167,25 @@ function all ({ bets, hand, rules }) { bets = passOddsResult.bets payouts.push(passOddsResult.payout) + const comeLineResult = comeLine({ bets, hand, rules }) + bets = comeLineResult.bets + payouts.push(comeLineResult.payout) + + const comeOddsResult = comeOdds({ bets, hand, rules }) + + bets = comeOddsResult.bets + payouts.push(comeOddsResult.payout) + + const placeSixResult = placeSix({ bets, hand }) + + bets = placeSixResult.bets + payouts.push(placeSixResult.payout) + + const placeEightResult = placeEight({ bets, hand }) + + bets = placeEightResult.bets + payouts.push(placeEightResult.payout) + bets.payouts = payouts.reduce((memo, payout) => { if (!payout) return memo @@ -83,5 +207,10 @@ function all ({ bets, hand, rules }) { module.exports = { passLine, passOdds, + comeLine, + comeOdds, + placeBet, + placeSix, + placeEight, all } diff --git a/settle.test.js b/settle.test.js index 0e542d7..ff10626 100644 --- a/settle.test.js +++ b/settle.test.js @@ -357,6 +357,174 @@ tap.test('passOdds: odds bet, seven out', function (t) { t.end() }) +tap.test('comeLine: come win on 7', function (t) { + const bets = { + come: { + line: { amount: 5 }, + isComeOut: true + } + } + + const hand = { diceSum: 7 } + + const result = settle.comeLine({ hand, bets }) + t.equal(result.payout.type, 'come win') + t.equal(result.payout.principal, 5) + t.equal(result.payout.profit, 5) + t.notOk(result.bets.come) + + t.end() +}) + +tap.test('comeLine: craps 2', function (t) { + const bets = { + come: { + line: { amount: 5 }, + isComeOut: true + } + } + + const hand = { diceSum: 2 } + + const result = settle.comeLine({ hand, bets }) + t.notOk(result.payout) + t.notOk(result.bets.come) + + t.end() +}) + +tap.test('comeLine: craps 3', function (t) { + const bets = { + come: { + line: { amount: 5 }, + isComeOut: true + } + } + + const hand = { diceSum: 3 } + + const result = settle.comeLine({ hand, bets }) + t.notOk(result.payout) + t.notOk(result.bets.come) + + t.end() +}) + +tap.test('comeLine: craps 12', function (t) { + const bets = { + come: { + line: { amount: 5 }, + isComeOut: true + } + } + + const hand = { diceSum: 12 } + + const result = settle.comeLine({ hand, bets }) + t.notOk(result.payout) + t.notOk(result.bets.come) + + t.end() +}) + +tap.test('comeLine: come win on 11', function (t) { + const bets = { + come: { + line: { amount: 5 }, + isComeOut: true + } + } + + const hand = { diceSum: 11 } + + const result = settle.comeLine({ hand, bets }) + t.equal(result.payout.type, 'come win') + t.equal(result.payout.principal, 5) + t.equal(result.payout.profit, 5) + t.notOk(result.bets.come) + + t.end() +}) + +tap.test('comeLine: point win', function (t) { + const bets = { + come: { + line: { amount: 5 }, + isComeOut: false, + point: 6 + } + } + + const hand = { diceSum: 6 } + + const result = settle.comeLine({ hand, bets }) + t.equal(result.payout.type, 'come win') + t.equal(result.payout.principal, 5) + t.equal(result.payout.profit, 5) + t.notOk(result.bets.come) + + t.end() +}) + +tap.test('comeOdds: odds bet, win', function (t) { + const bets = { + come: { + line: { amount: 5 }, + odds: { amount: 10 }, + isComeOut: false, + point: 4 + } + } + + const hand = { diceSum: 4 } + + const result = settle.comeOdds({ hand, bets }) + t.equal(result.payout.type, 'come odds win') + t.equal(result.payout.principal, 10) + t.equal(result.payout.profit, 20) + t.notOk(result.bets.come.odds) + + t.end() +}) + +tap.test('comeOdds: odds bet, seven out', function (t) { + const bets = { + come: { + line: { amount: 5 }, + odds: { amount: 10 }, + isComeOut: false, + point: 4 + } + } + + const hand = { diceSum: 7 } + + const result = settle.comeOdds({ hand, bets }) + t.notOk(result.payout) + t.notOk(result.bets.come.odds) + + t.end() +}) + +tap.test('comeOdds: odds bet, no action', function (t) { + const bets = { + come: { + line: { amount: 5 }, + odds: { amount: 10 }, + isComeOut: false, + point: 4 + } + } + + const hand = { diceSum: 5 } + + const result = settle.comeOdds({ hand, bets }) + t.notOk(result.payout) + t.strictSame(result.bets, bets, 'bets unchanged') + + t.end() +}) + tap.test('all: pass line win', (t) => { const bets = { pass: { @@ -385,3 +553,114 @@ tap.test('all: pass line win', (t) => { t.end() }) + +tap.test('placeSix: win', (t) => { + const bets = { place: { six: { amount: 6 } } } + + const hand = { + result: 'neutral', + isComeOut: false, + diceSum: 6 + } + + const result = settle.placeSix({ bets, hand }) + + t.equal(result.payout.type, 'place 6 win') + t.equal(result.payout.profit, 7) + t.equal(result.payout.principal, 0) + t.equal(result.bets.place.six.amount, 6) + + t.end() +}) + +tap.test('placeEight: win', (t) => { + const bets = { place: { eight: { amount: 6 } } } + + const hand = { + result: 'neutral', + isComeOut: false, + diceSum: 8 + } + + const result = settle.placeEight({ bets, hand }) + + t.equal(result.payout.type, 'place 8 win') + t.equal(result.payout.profit, 7) + t.equal(result.payout.principal, 0) + t.equal(result.bets.place.eight.amount, 6) + + t.end() +}) + +tap.test('place bets: seven out removes bets', (t) => { + const bets = { place: { six: { amount: 6 }, eight: { amount: 6 } } } + + const hand = { + result: 'seven out', + isComeOut: true, + diceSum: 7 + } + + const settled = settle.all({ bets, hand }) + + t.notOk(settled.place) + t.equal(settled.payouts.total, 0) + + t.end() +}) + +tap.test('place bets: no action on comeout roll', (t) => { + const bets = { place: { six: { amount: 6 }, eight: { amount: 6 } } } + + const hand = { + result: 'comeout win', + isComeOut: true, + diceSum: 7 + } + + const settled = settle.all({ bets, hand }) + + t.equal(settled.place.six.amount, 6) + t.equal(settled.place.eight.amount, 6) + t.equal(settled.payouts.total, 0) + + t.end() +}) + +tap.test('place bet on 6 persists when point is 6', (t) => { + const bets = { place: { six: { amount: 6 }, eight: { amount: 6 } } } + + const hand = { + result: 'point set', + isComeOut: false, + diceSum: 6, + point: 6 + } + + const settled = settle.all({ bets, hand }) + + t.equal(settled.place.six.amount, 6) + t.equal(settled.place.eight.amount, 6) + t.equal(settled.payouts.total, 0) + + t.end() +}) + +tap.test('place bet on 8 persists when point is 8', (t) => { + const bets = { place: { six: { amount: 6 }, eight: { amount: 6 } } } + + const hand = { + result: 'point set', + isComeOut: false, + diceSum: 8, + point: 8 + } + + const settled = settle.all({ bets, hand }) + + t.equal(settled.place.six.amount, 6) + t.equal(settled.place.eight.amount, 6) + t.equal(settled.payouts.total, 0) + + t.end() +})