|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Blackjack Probability Calculator</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.card { |
|
width: 60px; |
|
height: 90px; |
|
border-radius: 5px; |
|
display: inline-flex; |
|
justify-content: center; |
|
align-items: center; |
|
margin: 0 5px; |
|
font-weight: bold; |
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2); |
|
} |
|
|
|
.card-red { |
|
background: linear-gradient(135deg, #fff, #ffdddd); |
|
color: #d00; |
|
border: 1px solid #d00; |
|
} |
|
.card-black { |
|
background: linear-gradient(135deg, #fff, #eeeeee); |
|
color: #000; |
|
border: 1px solid #000; |
|
} |
|
.progress-bar { |
|
height: 20px; |
|
border-radius: 10px; |
|
overflow: hidden; |
|
background-color: #e5e7eb; |
|
} |
|
.progress-fill { |
|
height: 100%; |
|
transition: width 0.3s ease; |
|
} |
|
.strategy-action { |
|
animation: pulse 2s infinite; |
|
} |
|
@keyframes pulse { |
|
0% { transform: scale(1); } |
|
50% { transform: scale(1.05); } |
|
100% { transform: scale(1); } |
|
} |
|
.tooltip { |
|
position: relative; |
|
display: inline-block; |
|
} |
|
.tooltip .tooltiptext { |
|
visibility: hidden; |
|
width: 200px; |
|
background-color: #333; |
|
color: #fff; |
|
text-align: center; |
|
border-radius: 6px; |
|
padding: 5px; |
|
position: absolute; |
|
z-index: 1; |
|
bottom: 125%; |
|
left: 50%; |
|
margin-left: -100px; |
|
opacity: 0; |
|
transition: opacity 0.3s; |
|
} |
|
.tooltip:hover .tooltiptext { |
|
visibility: visible; |
|
opacity: 1; |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-100 min-h-screen"> |
|
<div class="container mx-auto px-4 py-8"> |
|
|
|
<header class="mb-8 text-center"> |
|
<h1 class="text-4xl font-bold text-gray-800 mb-2"> |
|
<i class="fas fa-calculator text-blue-600"></i> Blackjack Probability Calculator |
|
</h1> |
|
<p class="text-gray-600 max-w-2xl mx-auto"> |
|
Calculate probabilities and optimal strategy for any blackjack situation. Perfect for players who want to make mathematically sound decisions. |
|
</p> |
|
</header> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8"> |
|
|
|
<div class="p-6 border-b border-gray-200"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4"> |
|
<i class="fas fa-cog text-blue-500 mr-2"></i>Game Configuration |
|
</h2> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
|
<div> |
|
<label for="numDecks" class="block text-sm font-medium text-gray-700 mb-1">Number of Decks (1-8)</label> |
|
<div class="flex items-center"> |
|
<input type="number" id="numDecks" min="1" max="8" value="1" |
|
class="w-20 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
|
</div> |
|
</div> |
|
|
|
<div class="flex items-center pt-5"> |
|
<input type="checkbox" id="h17Rule" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> |
|
<label for="h17Rule" class="ml-2 block text-sm text-gray-700"> |
|
Dealer Hits on Soft 17 (H17) |
|
</label> |
|
</div> |
|
|
|
<div class="flex items-center pt-5"> |
|
<input type="checkbox" id="dasRule" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> |
|
<label for="dasRule" class="ml-2 block text-sm text-gray-700"> |
|
Double After Split Allowed (DAS) |
|
</label> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="p-6 border-b border-gray-200"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4"> |
|
<i class="fas fa-user text-green-500 mr-2"></i>Player's Hand |
|
</h2> |
|
|
|
<div class="mb-4"> |
|
<label for="playerCards" class="block text-sm font-medium text-gray-700 mb-1"> |
|
Your Cards (comma separated, e.g. A, 10, 3) |
|
</label> |
|
<input type="text" id="playerCards" value="10, 5" |
|
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
|
</div> |
|
|
|
<div class="flex flex-wrap gap-2" id="playerCardsDisplay"> |
|
|
|
</div> |
|
</div> |
|
|
|
|
|
<div class="p-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4"> |
|
<i class="fas fa-user-tie text-red-500 mr-2"></i>Dealer's Up Card |
|
</h2> |
|
|
|
<div class="mb-4"> |
|
<label for="dealerCard" class="block text-sm font-medium text-gray-700 mb-1"> |
|
Dealer's Face-Up Card |
|
</label> |
|
<input type="text" id="dealerCard" value="A" |
|
class="mt-1 block w-20 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> |
|
</div> |
|
|
|
<div class="flex" id="dealerCardDisplay"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8"> |
|
<div class="p-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4"> |
|
<i class="fas fa-chess-knight text-purple-500 mr-2"></i>Optimal Strategy Advice |
|
</h2> |
|
|
|
<div id="strategyResult" class="text-center py-4"> |
|
<div class="text-lg font-bold text-blue-600 mb-2">Recommended Action: --</div> |
|
<div class="text-sm text-gray-600"> |
|
H: Hit, S: Stand, D: Double (if allowed), P: Split (if allowed)<br> |
|
Note: Strategy for 4-8 decks. D/P generally on first 2 cards. DAS considered. |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8"> |
|
<button id="calcPlayerBtn" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200"> |
|
<i class="fas fa-user-cog mr-2"></i>Calculate Player Probabilities |
|
</button> |
|
<button id="calcDealerBtn" class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200"> |
|
<i class="fas fa-user-tie mr-2"></i>Calculate Dealer Probabilities |
|
</button> |
|
</div> |
|
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
|
|
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden"> |
|
<div class="p-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4"> |
|
<i class="fas fa-chart-line text-green-500 mr-2"></i>Player Next Card Probabilities |
|
</h2> |
|
|
|
<div id="playerResults" class="space-y-4"> |
|
<div> |
|
<div class="text-gray-700">Current Hand Value:</div> |
|
<div id="playerHandValue" class="text-2xl font-bold">--</div> |
|
</div> |
|
|
|
<div> |
|
<div class="flex justify-between text-sm text-gray-600 mb-1"> |
|
<span>Probability of Safe Card (≤21)</span> |
|
<span id="safeProbText">--%</span> |
|
</div> |
|
<div class="progress-bar"> |
|
<div id="safeProbBar" class="progress-fill bg-green-500" style="width: 0%"></div> |
|
</div> |
|
</div> |
|
|
|
<div> |
|
<div class="flex justify-between text-sm text-gray-600 mb-1"> |
|
<span>Probability of Bust Card (>21)</span> |
|
<span id="bustProbText">--%</span> |
|
</div> |
|
<div class="progress-bar"> |
|
<div id="bustProbBar" class="progress-fill bg-red-500" style="width: 0%"></div> |
|
</div> |
|
</div> |
|
|
|
<div id="playerStatus" class="text-sm italic text-gray-500"></div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden"> |
|
<div class="p-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4"> |
|
<i class="fas fa-chart-pie text-red-500 mr-2"></i>Dealer Outcome Probabilities |
|
</h2> |
|
|
|
<div id="dealerResults" class="space-y-2"> |
|
<div class="bg-gray-50 p-4 rounded-lg h-64 overflow-y-auto"> |
|
<div id="dealerResultsText" class="text-sm"> |
|
Dealer probabilities will appear here. |
|
</div> |
|
</div> |
|
<div id="dealerStatus" class="text-sm italic text-gray-500"></div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="mt-8 bg-white rounded-xl shadow-lg overflow-hidden"> |
|
<div class="p-6"> |
|
<h2 class="text-xl font-semibold text-gray-800 mb-4"> |
|
<i class="fas fa-info-circle text-yellow-500 mr-2"></i>Blackjack Strategy Legend |
|
</h2> |
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4"> |
|
<div class="bg-blue-50 p-4 rounded-lg"> |
|
<div class="font-bold text-blue-800 mb-1">H: Hit</div> |
|
<div class="text-sm text-gray-600">Take another card from the dealer</div> |
|
</div> |
|
<div class="bg-green-50 p-4 rounded-lg"> |
|
<div class="font-bold text-green-800 mb-1">S: Stand</div> |
|
<div class="text-sm text-gray-600">Keep your current hand and end your turn</div> |
|
</div> |
|
<div class="bg-purple-50 p-4 rounded-lg"> |
|
<div class="font-bold text-purple-800 mb-1">D: Double</div> |
|
<div class="text-sm text-gray-600">Double your bet and take exactly one more card</div> |
|
</div> |
|
<div class="bg-yellow-50 p-4 rounded-lg"> |
|
<div class="font-bold text-yellow-800 mb-1">P: Split</div> |
|
<div class="text-sm text-gray-600">Split your pair into two separate hands (when allowed)</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
const CARD_VALUES = { |
|
'A': 11, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, |
|
'9': 9, '10': 10, 'J': 10, 'Q': 10, 'K': 10 |
|
}; |
|
const VALID_CARD_INPUTS = Object.keys(CARD_VALUES); |
|
const DEALER_CARD_MAP = Object.fromEntries( |
|
VALID_CARD_INPUTS.map(k => [k, ['J', 'Q', 'K'].includes(k) ? '10' : k]) |
|
); |
|
|
|
|
|
const STRATEGY_TABLES = { |
|
'H17': { |
|
'hard': { 9: {'2':'H', '3':'D', '4':'D', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 10: {'2':'D', '3':'D', '4':'D', '5':'D', '6':'D', '7':'D', '8':'D', '9':'D', '10':'H', 'A':'H'}, 11: {'2':'D', '3':'D', '4':'D', '5':'D', '6':'D', '7':'D', '8':'D', '9':'D', '10':'D', 'A':'H'}, 12: {'2':'H', '3':'H', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 13: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 14: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 15: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 16: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}}, |
|
'soft': { 13: {'2':'H', '3':'H', '4':'H', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 14: {'2':'H', '3':'H', '4':'H', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 15: {'2':'H', '3':'H', '4':'D', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 16: {'2':'H', '3':'H', '4':'D', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 17: {'2':'H', '3':'D', '4':'D', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 18: {'2':'S', '3':'D', '4':'D', '5':'D', '6':'D', '7':'S', '8':'S', '9':'H', '10':'H', 'A':'H'}, 19: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'S', '8':'S', '9':'S', '10':'S', 'A':'S'}}, |
|
'pairs': {'A': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'P', '9':'P', '10':'P', 'A':'P'}, '10': {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'S', '8':'S', '9':'S', '10':'S', 'A':'S'}, '9': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'S', '8':'P', '9':'P', '10':'S', 'A':'S'}, '8': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'P', '9':'P', '10':'P', 'A':'P'}, '7': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'H', '9':'H', '10':'H', 'A':'H'}, '6': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, '5': {}, '4': {'2':'H', '3':'H', '4':'H', '5':'P', '6':'P', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, '3': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'H', '9':'H', '10':'H', 'A':'H'}, '2': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'H', '9':'H', '10':'H', 'A':'H'}} |
|
}, |
|
'S17': { |
|
'hard': { 9: {'2':'H', '3':'D', '4':'D', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 10: {'2':'D', '3':'D', '4':'D', '5':'D', '6':'D', '7':'D', '8':'D', '9':'D', '10':'H', 'A':'H'}, 11: {'2':'D', '3':'D', '4':'D', '5':'D', '6':'D', '7':'D', '8':'D', '9':'D', '10':'D', 'A':'D'}, 12: {'2':'H', '3':'H', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 13: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 14: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 15: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 16: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}}, |
|
'soft': { 13: {'2':'H', '3':'H', '4':'H', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 14: {'2':'H', '3':'H', '4':'H', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 15: {'2':'H', '3':'H', '4':'D', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 16: {'2':'H', '3':'H', '4':'D', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 17: {'2':'H', '3':'D', '4':'D', '5':'D', '6':'D', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, 18: {'2':'S', '3':'D', '4':'D', '5':'D', '6':'D', '7':'S', '8':'S', '9':'S', '10':'S', 'A':'S'}, 19: {'2':'S', '3':'S', '4':'S', '5':'S', '6':'D', '7':'S', '8':'S', '9':'S', '10':'S', 'A':'S'}}, |
|
'pairs': {'A': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'P', '9':'P', '10':'P', 'A':'P'}, '10': {'2':'S', '3':'S', '4':'S', '5':'S', '6':'S', '7':'S', '8':'S', '9':'S', '10':'S', 'A':'S'}, '9': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'S', '8':'P', '9':'P', '10':'S', 'A':'S'}, '8': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'P', '9':'P', '10':'P', 'A':'P'}, '7': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'H', '9':'H', '10':'H', 'A':'H'}, '6': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, '5': {}, '4': {'2':'H', '3':'H', '4':'H', '5':'P', '6':'P', '7':'H', '8':'H', '9':'H', '10':'H', 'A':'H'}, '3': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'H', '9':'H', '10':'H', 'A':'H'}, '2': {'2':'P', '3':'P', '4':'P', '5':'P', '6':'P', '7':'P', '8':'H', '9':'H', '10':'H', 'A':'H'}} |
|
} |
|
}; |
|
|
|
|
|
const uniqueDealerColumnKeys = [...new Set(Object.values(DEALER_CARD_MAP))].sort(); |
|
for (const ruleSet of Object.values(STRATEGY_TABLES)) { |
|
for (let i = 5; i < 9; i++) { |
|
if (!ruleSet.hard[i]) ruleSet.hard[i] = {}; |
|
uniqueDealerColumnKeys.forEach(k => { if (!ruleSet.hard[i][k]) ruleSet.hard[i][k] = 'H'; }); |
|
} |
|
for (let i = 17; i < 22; i++) { |
|
if (!ruleSet.hard[i]) ruleSet.hard[i] = {}; |
|
uniqueDealerColumnKeys.forEach(k => { if (!ruleSet.hard[i][k]) ruleSet.hard[i][k] = 'S'; }); |
|
} |
|
[20, 21].forEach(i => { |
|
if (!ruleSet.soft[i]) ruleSet.soft[i] = {}; |
|
uniqueDealerColumnKeys.forEach(k => { if (!ruleSet.soft[i][k]) ruleSet.soft[i][k] = 'S'; }); |
|
}); |
|
} |
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
updateCardDisplays(); |
|
updateStrategyAdvice(); |
|
|
|
document.getElementById('playerCards').addEventListener('input', function() { |
|
updateCardDisplays(); |
|
updateStrategyAdvice(); |
|
}); |
|
document.getElementById('dealerCard').addEventListener('input', function() { |
|
updateCardDisplays(); |
|
updateStrategyAdvice(); |
|
}); |
|
document.getElementById('numDecks').addEventListener('change', updateStrategyAdvice); |
|
document.getElementById('h17Rule').addEventListener('change', updateStrategyAdvice); |
|
document.getElementById('dasRule').addEventListener('change', updateStrategyAdvice); |
|
|
|
document.getElementById('calcPlayerBtn').addEventListener('click', calculatePlayerProbabilities); |
|
document.getElementById('calcDealerBtn').addEventListener('click', calculateDealerProbabilities); |
|
}); |
|
|
|
|
|
function parsePlayerCards() { |
|
const cardsStr = document.getElementById('playerCards').value.trim(); |
|
if (!cardsStr) return { cards: [], error: "Player cards field is empty." }; |
|
|
|
const playerCardsList = cardsStr.split(',').map(c => c.trim().toUpperCase()).filter(c => c); |
|
|
|
if (!playerCardsList.length) { |
|
return { cards: [], error: "Player cards field is empty or invalid." }; |
|
} |
|
|
|
for (const card of playerCardsList) { |
|
if (!VALID_CARD_INPUTS.includes(card)) { |
|
return { cards: null, error: `Invalid player card: '${card}'. Use A,K,Q,J,10-2.` }; |
|
} |
|
} |
|
return { cards: playerCardsList, error: null }; |
|
} |
|
|
|
function calculateHandValueDetailed(handCardsStrings) { |
|
if (!handCardsStrings || !handCardsStrings.length) { |
|
return { value: 0, isSoft: false, isBlackjack: false }; |
|
} |
|
let value = 0; |
|
let numAcesInHand = 0; |
|
for (const cardStr of handCardsStrings) { |
|
const cardVal = CARD_VALUES[cardStr]; |
|
if (cardStr === 'A') numAcesInHand++; |
|
value += cardVal; |
|
} |
|
let acesCountedAs11 = numAcesInHand; |
|
while (value > 21 && acesCountedAs11 > 0) { |
|
value -= 10; |
|
acesCountedAs11--; |
|
} |
|
const isBlackjack = (value === 21 && handCardsStrings.length === 2); |
|
const isSoft = (value <= 21 && acesCountedAs11 > 0); |
|
return { value, isSoft, isBlackjack }; |
|
} |
|
|
|
function createDeck(numDecks = 1) { |
|
const deck = []; |
|
for (let i = 0; i < numDecks; i++) { |
|
for (const cardFace of Object.keys(CARD_VALUES)) { |
|
deck.push(...Array(4).fill(cardFace)); |
|
} |
|
} |
|
return deck; |
|
} |
|
|
|
function getEffectiveRemainingDeck(playerHandStrings, dealerUpCardString, numDecks) { |
|
const fullDeck = createDeck(numDecks); |
|
const allKnownCards = []; |
|
if (playerHandStrings) allKnownCards.push(...playerHandStrings.map(c => c.toUpperCase())); |
|
if (dealerUpCardString) allKnownCards.push(dealerUpCardString.toUpperCase()); |
|
|
|
for (const cardStr of allKnownCards) { |
|
if (!VALID_CARD_INPUTS.includes(cardStr)) { |
|
return { deck: null, error: `Invalid card '${cardStr}' in known cards.` }; |
|
} |
|
} |
|
const knownCardCounts = allKnownCards.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {}); |
|
const fullDeckCounts = fullDeck.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {}); |
|
|
|
for (const [card, count] of Object.entries(knownCardCounts)) { |
|
if (count > (fullDeckCounts[card] || 0)) { |
|
return { deck: null, error: `Too many '${card}' cards specified (${count}) than available in ${numDecks} deck(s) (${fullDeckCounts[card] || 0}).` }; |
|
} |
|
} |
|
const remainingDeck = [...fullDeck]; |
|
for (const cardToRemove of allKnownCards) { |
|
const index = remainingDeck.indexOf(cardToRemove); |
|
if (index === -1) return { deck: null, error: `Error removing '${cardToRemove}' from deck. Card count mismatch.` }; |
|
remainingDeck.splice(index, 1); |
|
} |
|
return { deck: remainingDeck, error: null }; |
|
} |
|
|
|
function getOptimalAction(playerHandStrings, dealerUpCardStr, isH17Rule, dasAllowed) { |
|
if (!playerHandStrings || playerHandStrings.length < 2) { |
|
return { action: null, message: "Player hand must have at least 2 cards for strategy." }; |
|
} |
|
if (!dealerUpCardStr) { |
|
return { action: null, message: "Dealer up-card is required for strategy." }; |
|
} |
|
dealerUpCardStr = dealerUpCardStr.toUpperCase(); |
|
if (!VALID_CARD_INPUTS.includes(dealerUpCardStr)) { |
|
return { action: null, message: `Invalid dealer up-card: ${dealerUpCardStr}` }; |
|
} |
|
|
|
const mappedDealerCard = DEALER_CARD_MAP[dealerUpCardStr] || dealerUpCardStr; |
|
const { value: playerValue, isSoft } = calculateHandValueDetailed(playerHandStrings); |
|
|
|
if (playerValue > 21) return { action: 'S', message: "Player busts, stands (no action)." }; |
|
if (playerValue === 21) return { action: 'S', message: "Player has 21, stands." }; |
|
|
|
const ruleType = isH17Rule ? 'H17' : 'S17'; |
|
const strategy = STRATEGY_TABLES[ruleType]; |
|
let action = null; |
|
const isTwoCardHand = playerHandStrings.length === 2; |
|
|
|
if (isTwoCardHand && playerHandStrings[0] === playerHandStrings[1]) { |
|
const pairCardValStr = playerHandStrings[0]; |
|
const pairLookupKey = DEALER_CARD_MAP[pairCardValStr] || pairCardValStr; |
|
|
|
if (pairLookupKey === '5') { |
|
action = strategy.hard[10]?.[mappedDealerCard]; |
|
} else if (strategy.pairs[pairLookupKey] && strategy.pairs[pairLookupKey][mappedDealerCard]) { |
|
action = strategy.pairs[pairLookupKey][mappedDealerCard]; |
|
|
|
|
|
if (pairLookupKey === '4' && (mappedDealerCard === '5' || mappedDealerCard === '6') && action === 'P' && !dasAllowed) { |
|
action = 'H'; |
|
} |
|
|
|
} |
|
} |
|
|
|
if (action === null && isSoft) { |
|
if (strategy.soft[playerValue] && strategy.soft[playerValue][mappedDealerCard]) { |
|
action = strategy.soft[playerValue][mappedDealerCard]; |
|
} |
|
} |
|
|
|
if (action === null) { |
|
if (playerValue < 9) action = 'H'; |
|
else if (playerValue >= 17) action = 'S'; |
|
else if (strategy.hard[playerValue] && strategy.hard[playerValue][mappedDealerCard]) { |
|
action = strategy.hard[playerValue][mappedDealerCard]; |
|
} |
|
} |
|
if (action === null) { |
|
action = playerValue < 17 ? 'H' : 'S'; |
|
} |
|
|
|
|
|
if (!isTwoCardHand) { |
|
if (action === 'D') { |
|
|
|
if (isSoft && playerValue === 18 && ['3','4','5','6'].includes(mappedDealerCard)) { |
|
action = 'S'; |
|
} else if (isSoft && playerValue === 19 && ruleType === 'S17' && mappedDealerCard === '6') { |
|
action = 'S'; |
|
} |
|
else { |
|
action = 'H'; |
|
} |
|
} else if (action === 'P') { |
|
|
|
if (isSoft) { |
|
const softAction = strategy.soft[playerValue]?.[mappedDealerCard] || 'H'; |
|
action = softAction === 'D' ? 'H' : softAction; |
|
} else { |
|
if (playerValue < 9) action = 'H'; |
|
else if (playerValue >= 17) action = 'S'; |
|
else { |
|
const hardAction = strategy.hard[playerValue]?.[mappedDealerCard] || 'H'; |
|
action = hardAction === 'D' ? 'H' : hardAction; |
|
} |
|
} |
|
} |
|
} |
|
|
|
const actionMap = { 'H': "Hit", 'S': "Stand", 'D': "Double", 'P': "Split" }; |
|
return { |
|
action: actionMap[action] || action, |
|
message: `Optimal action: ${actionMap[action] || 'Unknown'}` |
|
}; |
|
} |
|
|
|
function calculatePlayerNextCardProbabilities(playerHandStrings, dealerUpCardString, numDecks) { |
|
const { value: currentPlayerValue } = calculateHandValueDetailed(playerHandStrings); |
|
if (currentPlayerValue > 21) { |
|
return { currentValue: currentPlayerValue, probSafe: 0, probBust: 100, error: null }; |
|
} |
|
const { deck: remainingDeck, error: deckError } = getEffectiveRemainingDeck(playerHandStrings, dealerUpCardString, numDecks); |
|
if (deckError) { |
|
return { currentValue: currentPlayerValue, probSafe: 0, probBust: 0, error: deckError }; |
|
} |
|
if (!remainingDeck || !remainingDeck.length) { |
|
return { currentValue: currentPlayerValue, probSafe: currentPlayerValue <= 21 ? 100 : 0, probBust: currentPlayerValue <= 21 ? 0 : 100, error: "No cards left in deck for player to draw." }; |
|
} |
|
let safeOutcomes = 0; |
|
let bustOutcomes = 0; |
|
for (const nextCardCandidate of remainingDeck) { |
|
const tempHand = [...playerHandStrings, nextCardCandidate]; |
|
const { value: newValue } = calculateHandValueDetailed(tempHand); |
|
if (newValue <= 21) safeOutcomes++; |
|
else bustOutcomes++; |
|
} |
|
const totalRemainingCards = remainingDeck.length; |
|
const probSafePercent = totalRemainingCards > 0 ? (safeOutcomes / totalRemainingCards) * 100 : 0; |
|
const probBustPercent = totalRemainingCards > 0 ? (bustOutcomes / totalRemainingCards) * 100 : 0; |
|
return { currentValue: currentPlayerValue, probSafe: probSafePercent, probBust: probBustPercent, error: null }; |
|
} |
|
|
|
const memoDealerDist = {}; |
|
function getDealerFinalValueDistribution(currentDealerHandStrings, currentDeckState, hitSoft17) { |
|
const handTuple = [...currentDealerHandStrings].sort().join(','); |
|
const deckCounts = currentDeckState.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {}); |
|
const deckCountsTuple = Object.entries(deckCounts).sort().map(([k, v]) => `${k}:${v}`).join('|'); |
|
const memoKey = `${handTuple}|${deckCountsTuple}|${hitSoft17}`; |
|
if (memoDealerDist[memoKey]) return memoDealerDist[memoKey]; |
|
|
|
const { value, isSoft, isBlackjack } = calculateHandValueDetailed(currentDealerHandStrings); |
|
if (isBlackjack) { const result = { 'BJ': 1.0 }; memoDealerDist[memoKey] = result; return result; } |
|
if (value > 21) { const result = { 'Bust': 1.0 }; memoDealerDist[memoKey] = result; return result; } |
|
if (value >= 17) { |
|
if (value === 17 && isSoft && hitSoft17) { } |
|
else { const result = { [value]: 1.0 }; memoDealerDist[memoKey] = result; return result; } |
|
} |
|
if (!currentDeckState.length) { const result = value <= 21 ? { [value]: 1.0 } : { 'Bust': 1.0 }; memoDealerDist[memoKey] = result; return result; } |
|
|
|
const finalDistribution = {}; |
|
const totalCardsInDeck = currentDeckState.length; |
|
const uniqueCardsInDeckCounts = currentDeckState.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {}); |
|
|
|
for (const [cardDrawn, count] of Object.entries(uniqueCardsInDeckCounts)) { |
|
const probDrawingThisCard = count / totalCardsInDeck; |
|
const newHandStrings = [...currentDealerHandStrings, cardDrawn]; |
|
const newDeckState = [...currentDeckState]; |
|
const indexToRemove = newDeckState.indexOf(cardDrawn); |
|
if (indexToRemove > -1) newDeckState.splice(indexToRemove, 1); |
|
|
|
const recursiveDistribution = getDealerFinalValueDistribution(newHandStrings, newDeckState, hitSoft17); |
|
for (const [outcome, prob] of Object.entries(recursiveDistribution)) { |
|
finalDistribution[outcome] = (finalDistribution[outcome] || 0) + probDrawingThisCard * prob; |
|
} |
|
} |
|
memoDealerDist[memoKey] = finalDistribution; |
|
return finalDistribution; |
|
} |
|
|
|
function calculateDealerOutcomeProbabilities(dealerUpCardString, playerHandStrings, numDecks, hitSoft17) { |
|
Object.keys(memoDealerDist).forEach(key => delete memoDealerDist[key]); |
|
if (!dealerUpCardString) return { probabilities: null, error: "Dealer up-card must be provided." }; |
|
dealerUpCardString = dealerUpCardString.toUpperCase(); |
|
if (!VALID_CARD_INPUTS.includes(dealerUpCardString)) return { probabilities: null, error: `Invalid dealer up-card: ${dealerUpCardString}` }; |
|
|
|
const { deck: deckForHoleCard, error: deckError } = getEffectiveRemainingDeck(playerHandStrings, dealerUpCardString, numDecks); |
|
if (deckError) return { probabilities: null, error: deckError }; |
|
if (!deckForHoleCard || !deckForHoleCard.length) return { probabilities: null, error: "No cards left in deck for dealer's hole card." }; |
|
|
|
const overallDealerOutcomeDistribution = {}; |
|
const totalPossibleHoleCards = deckForHoleCard.length; |
|
const holeCardCounts = deckForHoleCard.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {}); |
|
|
|
for (const [holeCardCandidate, count] of Object.entries(holeCardCounts)) { |
|
const probThisHoleCardIsDrawn = count / totalPossibleHoleCards; |
|
const dealerInitialHand = [dealerUpCardString, holeCardCandidate]; |
|
|
|
const deckForDealerPlay = [...deckForHoleCard]; |
|
const indexToRemove = deckForDealerPlay.indexOf(holeCardCandidate); |
|
if (indexToRemove > -1) deckForDealerPlay.splice(indexToRemove, 1); |
|
|
|
const distributionForThisHoleCard = getDealerFinalValueDistribution(dealerInitialHand, deckForDealerPlay, hitSoft17); |
|
for (const [outcome, prob] of Object.entries(distributionForThisHoleCard)) { |
|
overallDealerOutcomeDistribution[outcome] = (overallDealerOutcomeDistribution[outcome] || 0) + probThisHoleCardIsDrawn * prob; |
|
} |
|
} |
|
const finalProbabilitiesPercent = Object.fromEntries(Object.entries(overallDealerOutcomeDistribution).map(([k, v]) => [k, v * 100])); |
|
return { probabilities: finalProbabilitiesPercent, error: null }; |
|
} |
|
|
|
|
|
function updateCardDisplays() { |
|
const { cards: playerCards } = parsePlayerCards(); |
|
const dealerCard = document.getElementById('dealerCard').value.trim().toUpperCase(); |
|
const playerCardsDisplay = document.getElementById('playerCardsDisplay'); |
|
playerCardsDisplay.innerHTML = ''; |
|
if (playerCards && playerCards.length) { |
|
playerCards.forEach(card => { |
|
|
|
const isRedSuitChar = ['H', 'D'].includes(card.slice(-1)) && card.length > 1; |
|
const cardEl = document.createElement('div'); |
|
cardEl.className = `card ${isRedSuitChar ? 'card-red' : 'card-black'}`; |
|
cardEl.textContent = card; |
|
playerCardsDisplay.appendChild(cardEl); |
|
}); |
|
} |
|
const dealerCardDisplay = document.getElementById('dealerCardDisplay'); |
|
dealerCardDisplay.innerHTML = ''; |
|
if (dealerCard && VALID_CARD_INPUTS.includes(dealerCard)) { |
|
const isRedSuitChar = ['H', 'D'].includes(dealerCard.slice(-1)) && dealerCard.length > 1; |
|
const cardEl = document.createElement('div'); |
|
cardEl.className = `card ${isRedSuitChar ? 'card-red' : 'card-black'}`; |
|
cardEl.textContent = dealerCard; |
|
dealerCardDisplay.appendChild(cardEl); |
|
const faceDownCard = document.createElement('div'); |
|
faceDownCard.className = 'card bg-gray-800 text-white'; |
|
faceDownCard.innerHTML = '<i class="fas fa-question"></i>'; |
|
dealerCardDisplay.appendChild(faceDownCard); |
|
} |
|
} |
|
|
|
function updateStrategyAdvice() { |
|
const { cards: playerCards, error: playerError } = parsePlayerCards(); |
|
const dealerCard = document.getElementById('dealerCard').value.trim().toUpperCase(); |
|
const isH17 = document.getElementById('h17Rule').checked; |
|
const dasAllowed = document.getElementById('dasRule').checked; |
|
const strategyResultDiv = document.getElementById('strategyResult'); |
|
|
|
if (playerError && playerCards === null) { |
|
strategyResultDiv.innerHTML = `<div class="text-red-500">${playerError}</div>`; return; |
|
} |
|
if (!playerCards || !playerCards.length || !dealerCard) { |
|
strategyResultDiv.innerHTML = '<div class="text-lg font-bold text-blue-600 mb-2">Recommended Action: --</div><div class="text-sm text-gray-500">Need player & dealer cards.</div>'; return; |
|
} |
|
if (!VALID_CARD_INPUTS.includes(dealerCard)) { |
|
strategyResultDiv.innerHTML = `<div class="text-red-500">Invalid dealer card '${dealerCard}'</div>`; return; |
|
} |
|
if (playerCards.length < 2) { |
|
strategyResultDiv.innerHTML = '<div class="text-lg font-bold text-blue-600 mb-2">Recommended Action: --</div><div class="text-sm text-gray-500">Need at least 2 player cards.</div>'; return; |
|
} |
|
|
|
const { action, message } = getOptimalAction(playerCards, dealerCard, isH17, dasAllowed); |
|
if (action) { |
|
let actionClass = 'text-blue-600'; |
|
if (action === 'Hit') actionClass = 'text-green-600'; |
|
else if (action === 'Double') actionClass = 'text-purple-600'; |
|
else if (action === 'Split') actionClass = 'text-yellow-600'; |
|
|
|
strategyResultDiv.innerHTML = ` |
|
<div class="text-lg font-bold ${actionClass} mb-2 strategy-action">Recommended Action: ${action}</div> |
|
<div class="text-sm text-gray-600">${message}</div> |
|
`; |
|
} else { |
|
strategyResultDiv.innerHTML = `<div class="text-gray-500">${message || 'Could not determine action.'}</div>`; |
|
} |
|
} |
|
|
|
function calculatePlayerProbabilities() { |
|
const playerStatus = document.getElementById('playerStatus'); |
|
playerStatus.textContent = ''; |
|
updateStrategyAdvice(); |
|
const { cards: playerCards, error } = parsePlayerCards(); |
|
const playerHandValueEl = document.getElementById('playerHandValue'); |
|
const safeProbTextEl = document.getElementById('safeProbText'); |
|
const bustProbTextEl = document.getElementById('bustProbText'); |
|
const safeProbBarEl = document.getElementById('safeProbBar'); |
|
const bustProbBarEl = document.getElementById('bustProbBar'); |
|
|
|
function resetPlayerProbs() { |
|
playerHandValueEl.textContent = '--'; |
|
safeProbTextEl.textContent = '--%'; |
|
bustProbTextEl.textContent = '--%'; |
|
safeProbBarEl.style.width = '0%'; |
|
bustProbBarEl.style.width = '0%'; |
|
} |
|
|
|
if (error && playerCards === null) { |
|
playerStatus.textContent = error; resetPlayerProbs(); return; |
|
} |
|
if (!playerCards || !playerCards.length) { |
|
playerStatus.textContent = "Player cards are empty for probability calculation."; resetPlayerProbs(); return; |
|
} |
|
|
|
const numDecks = parseInt(document.getElementById('numDecks').value); |
|
const dealerUpCard = document.getElementById('dealerCard').value.trim().toUpperCase(); |
|
let effectiveDealerCard = null; |
|
if (dealerUpCard && VALID_CARD_INPUTS.includes(dealerUpCard)) effectiveDealerCard = dealerUpCard; |
|
else if (dealerUpCard) playerStatus.textContent = `Warning: Invalid dealer up-card '${dealerUpCard}'. Ignored for card removal in player prob calc.`; |
|
|
|
const { currentValue, probSafe, probBust, error: calcError } = calculatePlayerNextCardProbabilities(playerCards, effectiveDealerCard, numDecks); |
|
if (calcError) { |
|
const currentStatus = playerStatus.textContent; |
|
playerStatus.textContent = `${currentStatus} Error: ${calcError}`.trim(); resetPlayerProbs(); |
|
} else { |
|
playerHandValueEl.textContent = currentValue; |
|
safeProbTextEl.textContent = `${probSafe.toFixed(2)}%`; |
|
bustProbTextEl.textContent = `${probBust.toFixed(2)}%`; |
|
safeProbBarEl.style.width = `${probSafe}%`; |
|
bustProbBarEl.style.width = `${probBust}%`; |
|
const currentStatus = playerStatus.textContent; |
|
const calcMessage = currentValue > 21 ? "Player is Busted." : "Player probabilities calculated."; |
|
playerStatus.textContent = currentStatus.includes("Warning") ? `${currentStatus} ${calcMessage}` : calcMessage; |
|
} |
|
} |
|
|
|
function calculateDealerProbabilities() { |
|
const dealerStatus = document.getElementById('dealerStatus'); |
|
dealerStatus.textContent = ''; |
|
updateStrategyAdvice(); |
|
const { cards: playerCards, error: playerError } = parsePlayerCards(); |
|
if (playerError && playerCards === null) { dealerStatus.textContent = `Player card error: ${playerError}`; return; } |
|
|
|
const numDecks = parseInt(document.getElementById('numDecks').value); |
|
const dealerUpCard = document.getElementById('dealerCard').value.trim().toUpperCase(); |
|
const hitSoft17 = document.getElementById('h17Rule').checked; |
|
|
|
if (!dealerUpCard) { dealerStatus.textContent = "Dealer up-card is required for dealer calculation."; return; } |
|
if (!VALID_CARD_INPUTS.includes(dealerUpCard)) { dealerStatus.textContent = `Invalid dealer up-card: '${dealerUpCard}'.`; return; } |
|
|
|
dealerStatus.textContent = "Calculating dealer probabilities... Please wait."; |
|
|
|
setTimeout(() => { |
|
const { probabilities, error } = calculateDealerOutcomeProbabilities(dealerUpCard, playerCards || [], numDecks, hitSoft17); |
|
const dealerResultsTextEl = document.getElementById('dealerResultsText'); |
|
dealerResultsTextEl.innerHTML = ''; |
|
|
|
if (error) { |
|
dealerStatus.textContent = error; |
|
dealerResultsTextEl.innerHTML = "Error during calculation."; |
|
} else if (probabilities) { |
|
dealerStatus.textContent = "Dealer probabilities calculated."; |
|
|
|
const displayOrder = ['BJ', 21, 20, 19, 18, 17, 16, 15, 'Bust']; |
|
let outputHTML = "<div class='space-y-1'><div class='font-bold mb-2'>Dealer Final Hand Probabilities:</div>"; |
|
const processedOutcomes = new Set(); |
|
|
|
for (const outcomeKey of displayOrder) { |
|
if (probabilities[outcomeKey] !== undefined) { |
|
const displayKeyStr = outcomeKey === 'BJ' ? "Blackjack" : outcomeKey.toString(); |
|
const probVal = probabilities[outcomeKey]; |
|
outputHTML += `<div class="flex justify-between"><span>P(Dealer gets ${displayKeyStr}):</span><span class="font-bold">${probVal.toFixed(2)}%</span></div>`; |
|
processedOutcomes.add(outcomeKey); |
|
} |
|
} |
|
|
|
const remainingItems = []; |
|
for (const key in probabilities) { |
|
if (!processedOutcomes.has(key) && !processedOutcomes.has(parseInt(key))) { |
|
remainingItems.push([key, probabilities[key]]); |
|
} |
|
} |
|
|
|
remainingItems.sort((a, b) => { |
|
const keyA = a[0]; const keyB = b[0]; |
|
const isNumA = !isNaN(parseFloat(keyA)); const isNumB = !isNaN(parseFloat(keyB)); |
|
if (keyA === 'BJ') return -1; if (keyB === 'BJ') return 1; |
|
if (keyA === 'Bust') return 1; if (keyB === 'Bust') return -1; |
|
if (isNumA && isNumB) return parseFloat(keyB) - parseFloat(keyA); |
|
if (isNumA) return -1; |
|
if (isNumB) return 1; |
|
return keyA.localeCompare(keyB); |
|
}); |
|
|
|
for (const [outcomeKey, probVal] of remainingItems) { |
|
const displayKeyStr = outcomeKey === 'BJ' ? "Blackjack" : outcomeKey.toString(); |
|
outputHTML += `<div class="flex justify-between"><span>P(Dealer gets ${displayKeyStr}):</span><span class="font-bold">${probVal.toFixed(2)}%</span></div>`; |
|
} |
|
|
|
const totalProbCheck = Object.values(probabilities).reduce((sum, val) => sum + val, 0); |
|
outputHTML += `<div class="mt-2 pt-2 border-t border-gray-200 text-sm"><div class="flex justify-between"><span>Total Probability Sum:</span><span class="font-bold">${totalProbCheck.toFixed(2)}%</span></div><div class="text-xs text-gray-500">(should be close to 100%)</div></div>`; |
|
outputHTML += "</div>"; |
|
dealerResultsTextEl.innerHTML = outputHTML; |
|
} else { |
|
dealerStatus.textContent = "No results from dealer calculation."; |
|
dealerResultsTextEl.innerHTML = "No results."; |
|
} |
|
}, 50); |
|
} |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LMLK/blackjack-probability-calculator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |