LMLK's picture
Add 3 files
1e43f0c verified
<!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 color is purely cosmetic for rank display; not tracking actual suits. */
.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 -->
<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>
<!-- Main Calculator -->
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
<!-- Configuration Section -->
<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>
<!-- Player's Hand Section -->
<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">
<!-- Cards will be displayed here -->
</div>
</div>
<!-- Dealer's Up Card Section -->
<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">
<!-- Dealer card will be displayed here -->
</div>
</div>
</div>
<!-- Strategy Advice -->
<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>
<!-- Calculation Buttons -->
<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>
<!-- Results Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Player Results -->
<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>
<!-- Dealer Results -->
<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>
<!-- Legend Section -->
<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>
// Card values and deck logic
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])
);
// Strategy tables (H17 and S17)
const STRATEGY_TABLES = { /* Copied from previous HTML, no change here */
'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'}}
}
};
// Complete strategy tables (auto-fill common cases)
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++) { // Hard 5-8
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++) { // Hard 17-21
if (!ruleSet.hard[i]) ruleSet.hard[i] = {};
uniqueDealerColumnKeys.forEach(k => { if (!ruleSet.hard[i][k]) ruleSet.hard[i][k] = 'S'; });
}
[20, 21].forEach(i => { // Soft 20, 21
if (!ruleSet.soft[i]) ruleSet.soft[i] = {};
uniqueDealerColumnKeys.forEach(k => { if (!ruleSet.soft[i][k]) ruleSet.soft[i][k] = 'S'; });
});
}
// Initialize the UI
document.addEventListener('DOMContentLoaded', function() {
updateCardDisplays();
updateStrategyAdvice(); // Initial strategy update
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); // DAS rule listener
document.getElementById('calcPlayerBtn').addEventListener('click', calculatePlayerProbabilities);
document.getElementById('calcDealerBtn').addEventListener('click', calculateDealerProbabilities);
});
// Helper functions
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) { // Added 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') { // 5,5 is Hard 10
action = strategy.hard[10]?.[mappedDealerCard];
} else if (strategy.pairs[pairLookupKey] && strategy.pairs[pairLookupKey][mappedDealerCard]) {
action = strategy.pairs[pairLookupKey][mappedDealerCard];
// DAS-dependent strategy adjustment: 4,4 vs 5 or 6
if (pairLookupKey === '4' && (mappedDealerCard === '5' || mappedDealerCard === '6') && action === 'P' && !dasAllowed) {
action = 'H'; // Hit if can't split 4,4 vs 5,6 with DAS
}
// Add other DAS-dependent rules here if needed
}
}
if (action === null && isSoft) {
if (strategy.soft[playerValue] && strategy.soft[playerValue][mappedDealerCard]) {
action = strategy.soft[playerValue][mappedDealerCard];
}
}
if (action === null) { // Hard total or fallback
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) { // Ultimate fallback if table somehow missed a case
action = playerValue < 17 ? 'H' : 'S';
}
// Adjust for >2 cards: D becomes H, P becomes H/S based on value after hit
if (!isTwoCardHand) {
if (action === 'D') {
// More nuanced: Some Doubles (e.g. Soft 18 vs dealer 3-6) become Stand
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 split was recommended, but >2 cards, play current hand as non-pair hard/soft total.
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) { /* Continue */ }
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]; // Create a copy
const indexToRemove = newDeckState.indexOf(cardDrawn);
if (indexToRemove > -1) newDeckState.splice(indexToRemove, 1); // Remove one instance
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]; // Create a copy
const indexToRemove = deckForDealerPlay.indexOf(holeCardCandidate);
if (indexToRemove > -1) deckForDealerPlay.splice(indexToRemove, 1); // Remove one instance
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 };
}
// UI Update Functions
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 => {
// Simplified red/black for display, not true suit tracking.
const isRedSuitChar = ['H', 'D'].includes(card.slice(-1)) && card.length > 1; // Avoids 'H' from 'HIT'
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; // Get DAS rule
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); // Pass dasAllowed
if (action) {
let actionClass = 'text-blue-600'; // Default for Stand
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.";
// Expanded display order
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))) { // Check both string and int versions for safety
remainingItems.push([key, probabilities[key]]);
}
}
// Sort remaining: Numbers desc (except Bust/BJ), then BJ, then Bust
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); // Numbers descending
if (isNumA) return -1; // Numbers before other strings
if (isNumB) return 1;
return keyA.localeCompare(keyB); // Other strings alphabetically
});
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); // Reduced timeout slightly, 100ms is quite long for simple UI update.
}
</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>