From e50f29494e1eff6132807d04562112c193dff80c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Mar 2026 12:11:32 +0000 Subject: [PATCH] Add fourBetGrindLimit strategy with per-hand budget Implements a variant of the four bet grind with a finite budget of four units per hand. Each bet placed costs one unit; each win (profit > 0) credits one unit back. When the budget reaches zero, no further bets are placed for the remainder of the hand. Tracks state in playerMind.fourBetGrindLimit. Includes 9 new unit tests covering budget tracking, win crediting, and stop-betting behavior. https://claude.ai/code/session_016BNAgRcsdzfAz6Wj4GiZJ5 --- betting.js | 75 ++++++++++++++++++++++++++- betting.test.js | 132 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 1 deletion(-) diff --git a/betting.js b/betting.js index 44228f3..9dd7c0c 100644 --- a/betting.js +++ b/betting.js @@ -392,6 +392,75 @@ function fourBetGrindContinuous (opts) { return bets } +function fourBetGrindLimit (opts) { + const { rules, bets: existingBets = {}, hand, playerMind } = opts + const bets = Object.assign({ new: 0 }, existingBets) + + // Initialize per-hand state + if (!playerMind.fourBetGrindLimit) { + playerMind.fourBetGrindLimit = { unitsRemaining: 4 } + } + const state = playerMind.fourBetGrindLimit + + // Credit wins from the previous roll + if (hand.payouts) { + for (const payout of hand.payouts) { + if (payout.profit > 0) { + state.unitsRemaining += 1 + if (process.env.DEBUG) console.log(`[four-bet-grind] win credited, unitsRemaining: ${state.unitsRemaining}`) + } + } + } + + // No budget left — sit out the rest of the hand + if (state.unitsRemaining <= 0) { + if (process.env.DEBUG) console.log('[four-bet-grind] no units remaining, skipping bet') + return bets + } + + // On come-out: place don't pass if not present + if (hand.isComeOut) { + if (!bets?.dontPass?.line) { + bets.dontPass = { line: { amount: rules.minBet } } + bets.new += rules.minBet + state.unitsRemaining -= 1 + if (process.env.DEBUG) console.log(`[four-bet-grind] make dont pass line bet $${rules.minBet}, unitsRemaining: ${state.unitsRemaining}`) + } + return bets + } + + // Point phase: work through the same sequence as the continuous variant + bets.come = bets.come || {} + bets.come.pending = bets.come.pending || [] + bets.come.points = bets.come.points || {} + bets.dontCome = bets.dontCome || {} + bets.dontCome.pending = bets.dontCome.pending || [] + bets.dontCome.points = bets.dontCome.points || {} + + const comePointCount = Object.values(bets.come.points).reduce((sum, arr) => sum + arr.length, 0) + const comePendingCount = bets.come.pending.length + const dontComePointCount = Object.values(bets.dontCome.points).reduce((sum, arr) => sum + arr.length, 0) + const dontComePendingCount = bets.dontCome.pending.length + + if (comePendingCount === 0 && comePointCount < 2) { + bets.come.pending.push({ amount: rules.minBet }) + bets.new += rules.minBet + state.unitsRemaining -= 1 + if (process.env.DEBUG) console.log(`[four-bet-grind] make come line bet $${rules.minBet}, unitsRemaining: ${state.unitsRemaining}`) + return bets + } + + if (comePointCount >= 2 && comePendingCount === 0 && dontComePointCount === 0 && dontComePendingCount === 0) { + bets.dontCome.pending.push({ amount: rules.minBet }) + bets.new += rules.minBet + state.unitsRemaining -= 1 + if (process.env.DEBUG) console.log(`[four-bet-grind] make dont come line bet $${rules.minBet}, unitsRemaining: ${state.unitsRemaining}`) + return bets + } + + return bets +} + function noBetting () { return { new: 0 } } @@ -478,6 +547,9 @@ minDontPassOnly.description = "Bet the minimum on the don't pass line each come- fourBetGrindContinuous.title = 'Four Bet Grind Continuous' fourBetGrindContinuous.description = "Places four bets in sequence at table minimum: don't pass, come, come, don't come. Each bet is placed only after the previous is established (moved to a point). Once all four are established, no new bets are placed unless one needs to be replaced. Continuous — no per-hand limit on replacement." +fourBetGrindLimit.title = 'Four Bet Grind Limit' +fourBetGrindLimit.description = "Same sequence as Four Bet Grind Continuous (don't pass, come, come, don't come) but with a per-hand budget of four units at table minimum. Each new bet costs one unit. Each win earns one unit back. Once the budget reaches zero, no further bets are placed for the remainder of the hand." + export { noBetting, minDontPassOnly, @@ -498,5 +570,6 @@ export { minComeLineMaxOdds, passCome68, passcome2place68, - fourBetGrindContinuous + fourBetGrindContinuous, + fourBetGrindLimit } diff --git a/betting.test.js b/betting.test.js index e0f0cdc..4d5aaa0 100644 --- a/betting.test.js +++ b/betting.test.js @@ -1574,3 +1574,135 @@ tap.test('fourBetGrindContinuous: new come-out after point win places dont pass' t.equal(bets.new, fourBetRules.minBet) t.end() }) + +tap.test('fourBetGrindLimit: come-out places dont pass and decrements budget', (t) => { + const hand = { isComeOut: true } + const playerMind = {} + const bets = lib.fourBetGrindLimit({ rules: fourBetRules, hand, playerMind }) + + t.equal(bets.dontPass.line.amount, fourBetRules.minBet, "don't pass placed at minBet") + t.equal(bets.new, fourBetRules.minBet) + t.equal(playerMind.fourBetGrindLimit.unitsRemaining, 3, 'budget decremented to 3') + t.end() +}) + +tap.test('fourBetGrindLimit: come-out with existing dont pass does nothing', (t) => { + const hand = { isComeOut: true } + const playerMind = { fourBetGrindLimit: { unitsRemaining: 3 } } + const bets = lib.fourBetGrindLimit({ + rules: fourBetRules, + hand, + playerMind, + bets: { dontPass: { line: { amount: 5 } } } + }) + + t.equal(bets.new, 0, 'no new bets when dont pass already placed') + t.equal(playerMind.fourBetGrindLimit.unitsRemaining, 3, 'budget unchanged') + t.end() +}) + +tap.test('fourBetGrindLimit: point phase places first come bet', (t) => { + const hand = { isComeOut: false, point: 6 } + const playerMind = { fourBetGrindLimit: { unitsRemaining: 3 } } + const existingBets = { dontPass: { line: { amount: 5 } } } + const bets = lib.fourBetGrindLimit({ rules: fourBetRules, hand, playerMind, bets: existingBets }) + + t.equal(bets.come.pending.length, 1, 'one come bet pending') + t.equal(bets.come.pending[0].amount, fourBetRules.minBet) + t.equal(bets.new, fourBetRules.minBet) + t.equal(playerMind.fourBetGrindLimit.unitsRemaining, 2, 'budget decremented to 2') + t.end() +}) + +tap.test('fourBetGrindLimit: stops betting when budget reaches zero', (t) => { + const hand = { isComeOut: false, point: 6 } + const playerMind = { fourBetGrindLimit: { unitsRemaining: 0 } } + const existingBets = { dontPass: { line: { amount: 5 } } } + const bets = lib.fourBetGrindLimit({ rules: fourBetRules, hand, playerMind, bets: existingBets }) + + t.equal(bets.new, 0, 'no bets placed when budget is zero') + t.equal(bets.come, undefined, 'no come bet structure added') + t.end() +}) + +tap.test('fourBetGrindLimit: win credits one unit back to budget', (t) => { + const hand = { + isComeOut: false, + point: 6, + payouts: [{ type: 'come point win', principal: 5, profit: 5 }] + } + const playerMind = { fourBetGrindLimit: { unitsRemaining: 0 } } + const existingBets = { + dontPass: { line: { amount: 5 } }, + come: { pending: [], points: { 9: [{ line: { amount: 5 } }] } } + } + const bets = lib.fourBetGrindLimit({ rules: fourBetRules, hand, playerMind, bets: existingBets }) + + t.equal(playerMind.fourBetGrindLimit.unitsRemaining, 0, 'win added 1, then bet placed cost 1 — net zero') + t.equal(bets.come.pending.length, 1, 'was able to place a come bet after win credited') + t.equal(bets.new, fourBetRules.minBet) + t.end() +}) + +tap.test('fourBetGrindLimit: multiple wins in one roll credit multiple units', (t) => { + const hand = { + isComeOut: false, + point: 6, + payouts: [ + { type: 'come point win', principal: 5, profit: 5 }, + { type: 'dont come win', principal: 5, profit: 5 } + ] + } + const playerMind = { fourBetGrindLimit: { unitsRemaining: 0 } } + const existingBets = { dontPass: { line: { amount: 5 } } } + const bets = lib.fourBetGrindLimit({ rules: fourBetRules, hand, playerMind, bets: existingBets }) + + t.equal(playerMind.fourBetGrindLimit.unitsRemaining, 1, '2 wins credited, 1 bet placed — net 1 remaining') + t.equal(bets.new, fourBetRules.minBet, 'one bet placed') + t.end() +}) + +tap.test('fourBetGrindLimit: push (profit=0) does not credit a unit', (t) => { + const hand = { + isComeOut: false, + point: 6, + payouts: [{ type: 'push', principal: 5, profit: 0 }] + } + const playerMind = { fourBetGrindLimit: { unitsRemaining: 0 } } + const existingBets = { dontPass: { line: { amount: 5 } } } + const bets = lib.fourBetGrindLimit({ rules: fourBetRules, hand, playerMind, bets: existingBets }) + + t.equal(playerMind.fourBetGrindLimit.unitsRemaining, 0, 'push does not increase budget') + t.equal(bets.new, 0, 'no bet placed') + t.end() +}) + +tap.test('fourBetGrindLimit: all four established - no new bets', (t) => { + const hand = { isComeOut: false, point: 6 } + const playerMind = { fourBetGrindLimit: { unitsRemaining: 0 } } + const existingBets = { + dontPass: { line: { amount: 5 } }, + come: { pending: [], points: { 4: [{ line: { amount: 5 } }], 9: [{ line: { amount: 5 } }] } }, + dontCome: { pending: [], points: { 8: [{ line: { amount: 5 } }] } } + } + const bets = lib.fourBetGrindLimit({ rules: fourBetRules, hand, playerMind, bets: existingBets }) + + t.equal(bets.new, 0, 'no bets when all four established and budget zero') + t.end() +}) + +tap.test('fourBetGrindLimit: does not replace lost come bet when budget exhausted', (t) => { + const hand = { isComeOut: false, point: 6 } + const playerMind = { fourBetGrindLimit: { unitsRemaining: 0 } } + // One come bet was lost, only one remains — but budget is zero + const existingBets = { + dontPass: { line: { amount: 5 } }, + come: { pending: [], points: { 4: [{ line: { amount: 5 } }] } }, + dontCome: { pending: [], points: { 8: [{ line: { amount: 5 } }] } } + } + const bets = lib.fourBetGrindLimit({ rules: fourBetRules, hand, playerMind, bets: existingBets }) + + t.equal(bets.come.pending.length, 0, 'no replacement come bet when budget is zero') + t.equal(bets.new, 0) + t.end() +})