Add 3 files
Browse files- README.md +7 -5
- index.html +793 -19
- prompts.txt +0 -0
README.md
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: static
|
7 |
pinned: false
|
|
|
|
|
8 |
---
|
9 |
|
10 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: blackjack-probability-calculator
|
3 |
+
emoji: 🐳
|
4 |
+
colorFrom: pink
|
5 |
+
colorTo: pink
|
6 |
sdk: static
|
7 |
pinned: false
|
8 |
+
tags:
|
9 |
+
- deepsite
|
10 |
---
|
11 |
|
12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
index.html
CHANGED
@@ -1,19 +1,793 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Blackjack Probability Calculator</title>
|
7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
9 |
+
<style>
|
10 |
+
.card {
|
11 |
+
width: 60px;
|
12 |
+
height: 90px;
|
13 |
+
border-radius: 5px;
|
14 |
+
display: inline-flex;
|
15 |
+
justify-content: center;
|
16 |
+
align-items: center;
|
17 |
+
margin: 0 5px;
|
18 |
+
font-weight: bold;
|
19 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
20 |
+
}
|
21 |
+
/* Card color is purely cosmetic for rank display; not tracking actual suits. */
|
22 |
+
.card-red {
|
23 |
+
background: linear-gradient(135deg, #fff, #ffdddd);
|
24 |
+
color: #d00;
|
25 |
+
border: 1px solid #d00;
|
26 |
+
}
|
27 |
+
.card-black {
|
28 |
+
background: linear-gradient(135deg, #fff, #eeeeee);
|
29 |
+
color: #000;
|
30 |
+
border: 1px solid #000;
|
31 |
+
}
|
32 |
+
.progress-bar {
|
33 |
+
height: 20px;
|
34 |
+
border-radius: 10px;
|
35 |
+
overflow: hidden;
|
36 |
+
background-color: #e5e7eb;
|
37 |
+
}
|
38 |
+
.progress-fill {
|
39 |
+
height: 100%;
|
40 |
+
transition: width 0.3s ease;
|
41 |
+
}
|
42 |
+
.strategy-action {
|
43 |
+
animation: pulse 2s infinite;
|
44 |
+
}
|
45 |
+
@keyframes pulse {
|
46 |
+
0% { transform: scale(1); }
|
47 |
+
50% { transform: scale(1.05); }
|
48 |
+
100% { transform: scale(1); }
|
49 |
+
}
|
50 |
+
.tooltip {
|
51 |
+
position: relative;
|
52 |
+
display: inline-block;
|
53 |
+
}
|
54 |
+
.tooltip .tooltiptext {
|
55 |
+
visibility: hidden;
|
56 |
+
width: 200px;
|
57 |
+
background-color: #333;
|
58 |
+
color: #fff;
|
59 |
+
text-align: center;
|
60 |
+
border-radius: 6px;
|
61 |
+
padding: 5px;
|
62 |
+
position: absolute;
|
63 |
+
z-index: 1;
|
64 |
+
bottom: 125%;
|
65 |
+
left: 50%;
|
66 |
+
margin-left: -100px;
|
67 |
+
opacity: 0;
|
68 |
+
transition: opacity 0.3s;
|
69 |
+
}
|
70 |
+
.tooltip:hover .tooltiptext {
|
71 |
+
visibility: visible;
|
72 |
+
opacity: 1;
|
73 |
+
}
|
74 |
+
</style>
|
75 |
+
</head>
|
76 |
+
<body class="bg-gray-100 min-h-screen">
|
77 |
+
<div class="container mx-auto px-4 py-8">
|
78 |
+
<!-- Header -->
|
79 |
+
<header class="mb-8 text-center">
|
80 |
+
<h1 class="text-4xl font-bold text-gray-800 mb-2">
|
81 |
+
<i class="fas fa-calculator text-blue-600"></i> Blackjack Probability Calculator
|
82 |
+
</h1>
|
83 |
+
<p class="text-gray-600 max-w-2xl mx-auto">
|
84 |
+
Calculate probabilities and optimal strategy for any blackjack situation. Perfect for players who want to make mathematically sound decisions.
|
85 |
+
</p>
|
86 |
+
</header>
|
87 |
+
|
88 |
+
<!-- Main Calculator -->
|
89 |
+
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
|
90 |
+
<!-- Configuration Section -->
|
91 |
+
<div class="p-6 border-b border-gray-200">
|
92 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">
|
93 |
+
<i class="fas fa-cog text-blue-500 mr-2"></i>Game Configuration
|
94 |
+
</h2>
|
95 |
+
|
96 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
97 |
+
<div>
|
98 |
+
<label for="numDecks" class="block text-sm font-medium text-gray-700 mb-1">Number of Decks (1-8)</label>
|
99 |
+
<div class="flex items-center">
|
100 |
+
<input type="number" id="numDecks" min="1" max="8" value="1"
|
101 |
+
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">
|
102 |
+
</div>
|
103 |
+
</div>
|
104 |
+
|
105 |
+
<div class="flex items-center pt-5">
|
106 |
+
<input type="checkbox" id="h17Rule" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
107 |
+
<label for="h17Rule" class="ml-2 block text-sm text-gray-700">
|
108 |
+
Dealer Hits on Soft 17 (H17)
|
109 |
+
</label>
|
110 |
+
</div>
|
111 |
+
|
112 |
+
<div class="flex items-center pt-5">
|
113 |
+
<input type="checkbox" id="dasRule" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
114 |
+
<label for="dasRule" class="ml-2 block text-sm text-gray-700">
|
115 |
+
Double After Split Allowed (DAS)
|
116 |
+
</label>
|
117 |
+
</div>
|
118 |
+
</div>
|
119 |
+
</div>
|
120 |
+
|
121 |
+
<!-- Player's Hand Section -->
|
122 |
+
<div class="p-6 border-b border-gray-200">
|
123 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">
|
124 |
+
<i class="fas fa-user text-green-500 mr-2"></i>Player's Hand
|
125 |
+
</h2>
|
126 |
+
|
127 |
+
<div class="mb-4">
|
128 |
+
<label for="playerCards" class="block text-sm font-medium text-gray-700 mb-1">
|
129 |
+
Your Cards (comma separated, e.g. A, 10, 3)
|
130 |
+
</label>
|
131 |
+
<input type="text" id="playerCards" value="10, 5"
|
132 |
+
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">
|
133 |
+
</div>
|
134 |
+
|
135 |
+
<div class="flex flex-wrap gap-2" id="playerCardsDisplay">
|
136 |
+
<!-- Cards will be displayed here -->
|
137 |
+
</div>
|
138 |
+
</div>
|
139 |
+
|
140 |
+
<!-- Dealer's Up Card Section -->
|
141 |
+
<div class="p-6">
|
142 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">
|
143 |
+
<i class="fas fa-user-tie text-red-500 mr-2"></i>Dealer's Up Card
|
144 |
+
</h2>
|
145 |
+
|
146 |
+
<div class="mb-4">
|
147 |
+
<label for="dealerCard" class="block text-sm font-medium text-gray-700 mb-1">
|
148 |
+
Dealer's Face-Up Card
|
149 |
+
</label>
|
150 |
+
<input type="text" id="dealerCard" value="A"
|
151 |
+
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">
|
152 |
+
</div>
|
153 |
+
|
154 |
+
<div class="flex" id="dealerCardDisplay">
|
155 |
+
<!-- Dealer card will be displayed here -->
|
156 |
+
</div>
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
|
160 |
+
<!-- Strategy Advice -->
|
161 |
+
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-8">
|
162 |
+
<div class="p-6">
|
163 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">
|
164 |
+
<i class="fas fa-chess-knight text-purple-500 mr-2"></i>Optimal Strategy Advice
|
165 |
+
</h2>
|
166 |
+
|
167 |
+
<div id="strategyResult" class="text-center py-4">
|
168 |
+
<div class="text-lg font-bold text-blue-600 mb-2">Recommended Action: --</div>
|
169 |
+
<div class="text-sm text-gray-600">
|
170 |
+
H: Hit, S: Stand, D: Double (if allowed), P: Split (if allowed)<br>
|
171 |
+
Note: Strategy for 4-8 decks. D/P generally on first 2 cards. DAS considered.
|
172 |
+
</div>
|
173 |
+
</div>
|
174 |
+
</div>
|
175 |
+
</div>
|
176 |
+
|
177 |
+
<!-- Calculation Buttons -->
|
178 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
179 |
+
<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">
|
180 |
+
<i class="fas fa-user-cog mr-2"></i>Calculate Player Probabilities
|
181 |
+
</button>
|
182 |
+
<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">
|
183 |
+
<i class="fas fa-user-tie mr-2"></i>Calculate Dealer Probabilities
|
184 |
+
</button>
|
185 |
+
</div>
|
186 |
+
|
187 |
+
<!-- Results Section -->
|
188 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
189 |
+
<!-- Player Results -->
|
190 |
+
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
|
191 |
+
<div class="p-6">
|
192 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">
|
193 |
+
<i class="fas fa-chart-line text-green-500 mr-2"></i>Player Next Card Probabilities
|
194 |
+
</h2>
|
195 |
+
|
196 |
+
<div id="playerResults" class="space-y-4">
|
197 |
+
<div>
|
198 |
+
<div class="text-gray-700">Current Hand Value:</div>
|
199 |
+
<div id="playerHandValue" class="text-2xl font-bold">--</div>
|
200 |
+
</div>
|
201 |
+
|
202 |
+
<div>
|
203 |
+
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
204 |
+
<span>Probability of Safe Card (≤21)</span>
|
205 |
+
<span id="safeProbText">--%</span>
|
206 |
+
</div>
|
207 |
+
<div class="progress-bar">
|
208 |
+
<div id="safeProbBar" class="progress-fill bg-green-500" style="width: 0%"></div>
|
209 |
+
</div>
|
210 |
+
</div>
|
211 |
+
|
212 |
+
<div>
|
213 |
+
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
214 |
+
<span>Probability of Bust Card (>21)</span>
|
215 |
+
<span id="bustProbText">--%</span>
|
216 |
+
</div>
|
217 |
+
<div class="progress-bar">
|
218 |
+
<div id="bustProbBar" class="progress-fill bg-red-500" style="width: 0%"></div>
|
219 |
+
</div>
|
220 |
+
</div>
|
221 |
+
|
222 |
+
<div id="playerStatus" class="text-sm italic text-gray-500"></div>
|
223 |
+
</div>
|
224 |
+
</div>
|
225 |
+
</div>
|
226 |
+
|
227 |
+
<!-- Dealer Results -->
|
228 |
+
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
|
229 |
+
<div class="p-6">
|
230 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">
|
231 |
+
<i class="fas fa-chart-pie text-red-500 mr-2"></i>Dealer Outcome Probabilities
|
232 |
+
</h2>
|
233 |
+
|
234 |
+
<div id="dealerResults" class="space-y-2">
|
235 |
+
<div class="bg-gray-50 p-4 rounded-lg h-64 overflow-y-auto">
|
236 |
+
<div id="dealerResultsText" class="text-sm">
|
237 |
+
Dealer probabilities will appear here.
|
238 |
+
</div>
|
239 |
+
</div>
|
240 |
+
<div id="dealerStatus" class="text-sm italic text-gray-500"></div>
|
241 |
+
</div>
|
242 |
+
</div>
|
243 |
+
</div>
|
244 |
+
</div>
|
245 |
+
|
246 |
+
<!-- Legend Section -->
|
247 |
+
<div class="mt-8 bg-white rounded-xl shadow-lg overflow-hidden">
|
248 |
+
<div class="p-6">
|
249 |
+
<h2 class="text-xl font-semibold text-gray-800 mb-4">
|
250 |
+
<i class="fas fa-info-circle text-yellow-500 mr-2"></i>Blackjack Strategy Legend
|
251 |
+
</h2>
|
252 |
+
|
253 |
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
254 |
+
<div class="bg-blue-50 p-4 rounded-lg">
|
255 |
+
<div class="font-bold text-blue-800 mb-1">H: Hit</div>
|
256 |
+
<div class="text-sm text-gray-600">Take another card from the dealer</div>
|
257 |
+
</div>
|
258 |
+
<div class="bg-green-50 p-4 rounded-lg">
|
259 |
+
<div class="font-bold text-green-800 mb-1">S: Stand</div>
|
260 |
+
<div class="text-sm text-gray-600">Keep your current hand and end your turn</div>
|
261 |
+
</div>
|
262 |
+
<div class="bg-purple-50 p-4 rounded-lg">
|
263 |
+
<div class="font-bold text-purple-800 mb-1">D: Double</div>
|
264 |
+
<div class="text-sm text-gray-600">Double your bet and take exactly one more card</div>
|
265 |
+
</div>
|
266 |
+
<div class="bg-yellow-50 p-4 rounded-lg">
|
267 |
+
<div class="font-bold text-yellow-800 mb-1">P: Split</div>
|
268 |
+
<div class="text-sm text-gray-600">Split your pair into two separate hands (when allowed)</div>
|
269 |
+
</div>
|
270 |
+
</div>
|
271 |
+
</div>
|
272 |
+
</div>
|
273 |
+
</div>
|
274 |
+
|
275 |
+
<script>
|
276 |
+
// Card values and deck logic
|
277 |
+
const CARD_VALUES = {
|
278 |
+
'A': 11, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8,
|
279 |
+
'9': 9, '10': 10, 'J': 10, 'Q': 10, 'K': 10
|
280 |
+
};
|
281 |
+
const VALID_CARD_INPUTS = Object.keys(CARD_VALUES);
|
282 |
+
const DEALER_CARD_MAP = Object.fromEntries(
|
283 |
+
VALID_CARD_INPUTS.map(k => [k, ['J', 'Q', 'K'].includes(k) ? '10' : k])
|
284 |
+
);
|
285 |
+
|
286 |
+
// Strategy tables (H17 and S17)
|
287 |
+
const STRATEGY_TABLES = { /* Copied from previous HTML, no change here */
|
288 |
+
'H17': {
|
289 |
+
'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'}},
|
290 |
+
'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'}},
|
291 |
+
'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'}}
|
292 |
+
},
|
293 |
+
'S17': {
|
294 |
+
'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'}},
|
295 |
+
'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'}},
|
296 |
+
'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'}}
|
297 |
+
}
|
298 |
+
};
|
299 |
+
|
300 |
+
// Complete strategy tables (auto-fill common cases)
|
301 |
+
const uniqueDealerColumnKeys = [...new Set(Object.values(DEALER_CARD_MAP))].sort();
|
302 |
+
for (const ruleSet of Object.values(STRATEGY_TABLES)) {
|
303 |
+
for (let i = 5; i < 9; i++) { // Hard 5-8
|
304 |
+
if (!ruleSet.hard[i]) ruleSet.hard[i] = {};
|
305 |
+
uniqueDealerColumnKeys.forEach(k => { if (!ruleSet.hard[i][k]) ruleSet.hard[i][k] = 'H'; });
|
306 |
+
}
|
307 |
+
for (let i = 17; i < 22; i++) { // Hard 17-21
|
308 |
+
if (!ruleSet.hard[i]) ruleSet.hard[i] = {};
|
309 |
+
uniqueDealerColumnKeys.forEach(k => { if (!ruleSet.hard[i][k]) ruleSet.hard[i][k] = 'S'; });
|
310 |
+
}
|
311 |
+
[20, 21].forEach(i => { // Soft 20, 21
|
312 |
+
if (!ruleSet.soft[i]) ruleSet.soft[i] = {};
|
313 |
+
uniqueDealerColumnKeys.forEach(k => { if (!ruleSet.soft[i][k]) ruleSet.soft[i][k] = 'S'; });
|
314 |
+
});
|
315 |
+
}
|
316 |
+
|
317 |
+
|
318 |
+
// Initialize the UI
|
319 |
+
document.addEventListener('DOMContentLoaded', function() {
|
320 |
+
updateCardDisplays();
|
321 |
+
updateStrategyAdvice(); // Initial strategy update
|
322 |
+
|
323 |
+
document.getElementById('playerCards').addEventListener('input', function() {
|
324 |
+
updateCardDisplays();
|
325 |
+
updateStrategyAdvice();
|
326 |
+
});
|
327 |
+
document.getElementById('dealerCard').addEventListener('input', function() {
|
328 |
+
updateCardDisplays();
|
329 |
+
updateStrategyAdvice();
|
330 |
+
});
|
331 |
+
document.getElementById('numDecks').addEventListener('change', updateStrategyAdvice);
|
332 |
+
document.getElementById('h17Rule').addEventListener('change', updateStrategyAdvice);
|
333 |
+
document.getElementById('dasRule').addEventListener('change', updateStrategyAdvice); // DAS rule listener
|
334 |
+
|
335 |
+
document.getElementById('calcPlayerBtn').addEventListener('click', calculatePlayerProbabilities);
|
336 |
+
document.getElementById('calcDealerBtn').addEventListener('click', calculateDealerProbabilities);
|
337 |
+
});
|
338 |
+
|
339 |
+
// Helper functions
|
340 |
+
function parsePlayerCards() {
|
341 |
+
const cardsStr = document.getElementById('playerCards').value.trim();
|
342 |
+
if (!cardsStr) return { cards: [], error: "Player cards field is empty." };
|
343 |
+
|
344 |
+
const playerCardsList = cardsStr.split(',').map(c => c.trim().toUpperCase()).filter(c => c);
|
345 |
+
|
346 |
+
if (!playerCardsList.length) {
|
347 |
+
return { cards: [], error: "Player cards field is empty or invalid." };
|
348 |
+
}
|
349 |
+
|
350 |
+
for (const card of playerCardsList) {
|
351 |
+
if (!VALID_CARD_INPUTS.includes(card)) {
|
352 |
+
return { cards: null, error: `Invalid player card: '${card}'. Use A,K,Q,J,10-2.` };
|
353 |
+
}
|
354 |
+
}
|
355 |
+
return { cards: playerCardsList, error: null };
|
356 |
+
}
|
357 |
+
|
358 |
+
function calculateHandValueDetailed(handCardsStrings) {
|
359 |
+
if (!handCardsStrings || !handCardsStrings.length) {
|
360 |
+
return { value: 0, isSoft: false, isBlackjack: false };
|
361 |
+
}
|
362 |
+
let value = 0;
|
363 |
+
let numAcesInHand = 0;
|
364 |
+
for (const cardStr of handCardsStrings) {
|
365 |
+
const cardVal = CARD_VALUES[cardStr];
|
366 |
+
if (cardStr === 'A') numAcesInHand++;
|
367 |
+
value += cardVal;
|
368 |
+
}
|
369 |
+
let acesCountedAs11 = numAcesInHand;
|
370 |
+
while (value > 21 && acesCountedAs11 > 0) {
|
371 |
+
value -= 10;
|
372 |
+
acesCountedAs11--;
|
373 |
+
}
|
374 |
+
const isBlackjack = (value === 21 && handCardsStrings.length === 2);
|
375 |
+
const isSoft = (value <= 21 && acesCountedAs11 > 0);
|
376 |
+
return { value, isSoft, isBlackjack };
|
377 |
+
}
|
378 |
+
|
379 |
+
function createDeck(numDecks = 1) {
|
380 |
+
const deck = [];
|
381 |
+
for (let i = 0; i < numDecks; i++) {
|
382 |
+
for (const cardFace of Object.keys(CARD_VALUES)) {
|
383 |
+
deck.push(...Array(4).fill(cardFace));
|
384 |
+
}
|
385 |
+
}
|
386 |
+
return deck;
|
387 |
+
}
|
388 |
+
|
389 |
+
function getEffectiveRemainingDeck(playerHandStrings, dealerUpCardString, numDecks) {
|
390 |
+
const fullDeck = createDeck(numDecks);
|
391 |
+
const allKnownCards = [];
|
392 |
+
if (playerHandStrings) allKnownCards.push(...playerHandStrings.map(c => c.toUpperCase()));
|
393 |
+
if (dealerUpCardString) allKnownCards.push(dealerUpCardString.toUpperCase());
|
394 |
+
|
395 |
+
for (const cardStr of allKnownCards) {
|
396 |
+
if (!VALID_CARD_INPUTS.includes(cardStr)) {
|
397 |
+
return { deck: null, error: `Invalid card '${cardStr}' in known cards.` };
|
398 |
+
}
|
399 |
+
}
|
400 |
+
const knownCardCounts = allKnownCards.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {});
|
401 |
+
const fullDeckCounts = fullDeck.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {});
|
402 |
+
|
403 |
+
for (const [card, count] of Object.entries(knownCardCounts)) {
|
404 |
+
if (count > (fullDeckCounts[card] || 0)) {
|
405 |
+
return { deck: null, error: `Too many '${card}' cards specified (${count}) than available in ${numDecks} deck(s) (${fullDeckCounts[card] || 0}).` };
|
406 |
+
}
|
407 |
+
}
|
408 |
+
const remainingDeck = [...fullDeck];
|
409 |
+
for (const cardToRemove of allKnownCards) {
|
410 |
+
const index = remainingDeck.indexOf(cardToRemove);
|
411 |
+
if (index === -1) return { deck: null, error: `Error removing '${cardToRemove}' from deck. Card count mismatch.` };
|
412 |
+
remainingDeck.splice(index, 1);
|
413 |
+
}
|
414 |
+
return { deck: remainingDeck, error: null };
|
415 |
+
}
|
416 |
+
|
417 |
+
function getOptimalAction(playerHandStrings, dealerUpCardStr, isH17Rule, dasAllowed) { // Added dasAllowed
|
418 |
+
if (!playerHandStrings || playerHandStrings.length < 2) {
|
419 |
+
return { action: null, message: "Player hand must have at least 2 cards for strategy." };
|
420 |
+
}
|
421 |
+
if (!dealerUpCardStr) {
|
422 |
+
return { action: null, message: "Dealer up-card is required for strategy." };
|
423 |
+
}
|
424 |
+
dealerUpCardStr = dealerUpCardStr.toUpperCase();
|
425 |
+
if (!VALID_CARD_INPUTS.includes(dealerUpCardStr)) {
|
426 |
+
return { action: null, message: `Invalid dealer up-card: ${dealerUpCardStr}` };
|
427 |
+
}
|
428 |
+
|
429 |
+
const mappedDealerCard = DEALER_CARD_MAP[dealerUpCardStr] || dealerUpCardStr;
|
430 |
+
const { value: playerValue, isSoft } = calculateHandValueDetailed(playerHandStrings);
|
431 |
+
|
432 |
+
if (playerValue > 21) return { action: 'S', message: "Player busts, stands (no action)." };
|
433 |
+
if (playerValue === 21) return { action: 'S', message: "Player has 21, stands." };
|
434 |
+
|
435 |
+
const ruleType = isH17Rule ? 'H17' : 'S17';
|
436 |
+
const strategy = STRATEGY_TABLES[ruleType];
|
437 |
+
let action = null;
|
438 |
+
const isTwoCardHand = playerHandStrings.length === 2;
|
439 |
+
|
440 |
+
if (isTwoCardHand && playerHandStrings[0] === playerHandStrings[1]) {
|
441 |
+
const pairCardValStr = playerHandStrings[0];
|
442 |
+
const pairLookupKey = DEALER_CARD_MAP[pairCardValStr] || pairCardValStr;
|
443 |
+
|
444 |
+
if (pairLookupKey === '5') { // 5,5 is Hard 10
|
445 |
+
action = strategy.hard[10]?.[mappedDealerCard];
|
446 |
+
} else if (strategy.pairs[pairLookupKey] && strategy.pairs[pairLookupKey][mappedDealerCard]) {
|
447 |
+
action = strategy.pairs[pairLookupKey][mappedDealerCard];
|
448 |
+
|
449 |
+
// DAS-dependent strategy adjustment: 4,4 vs 5 or 6
|
450 |
+
if (pairLookupKey === '4' && (mappedDealerCard === '5' || mappedDealerCard === '6') && action === 'P' && !dasAllowed) {
|
451 |
+
action = 'H'; // Hit if can't split 4,4 vs 5,6 with DAS
|
452 |
+
}
|
453 |
+
// Add other DAS-dependent rules here if needed
|
454 |
+
}
|
455 |
+
}
|
456 |
+
|
457 |
+
if (action === null && isSoft) {
|
458 |
+
if (strategy.soft[playerValue] && strategy.soft[playerValue][mappedDealerCard]) {
|
459 |
+
action = strategy.soft[playerValue][mappedDealerCard];
|
460 |
+
}
|
461 |
+
}
|
462 |
+
|
463 |
+
if (action === null) { // Hard total or fallback
|
464 |
+
if (playerValue < 9) action = 'H';
|
465 |
+
else if (playerValue >= 17) action = 'S';
|
466 |
+
else if (strategy.hard[playerValue] && strategy.hard[playerValue][mappedDealerCard]) {
|
467 |
+
action = strategy.hard[playerValue][mappedDealerCard];
|
468 |
+
}
|
469 |
+
}
|
470 |
+
if (action === null) { // Ultimate fallback if table somehow missed a case
|
471 |
+
action = playerValue < 17 ? 'H' : 'S';
|
472 |
+
}
|
473 |
+
|
474 |
+
// Adjust for >2 cards: D becomes H, P becomes H/S based on value after hit
|
475 |
+
if (!isTwoCardHand) {
|
476 |
+
if (action === 'D') {
|
477 |
+
// More nuanced: Some Doubles (e.g. Soft 18 vs dealer 3-6) become Stand
|
478 |
+
if (isSoft && playerValue === 18 && ['3','4','5','6'].includes(mappedDealerCard)) {
|
479 |
+
action = 'S';
|
480 |
+
} else if (isSoft && playerValue === 19 && ruleType === 'S17' && mappedDealerCard === '6') {
|
481 |
+
action = 'S';
|
482 |
+
}
|
483 |
+
else {
|
484 |
+
action = 'H';
|
485 |
+
}
|
486 |
+
} else if (action === 'P') {
|
487 |
+
// If split was recommended, but >2 cards, play current hand as non-pair hard/soft total.
|
488 |
+
if (isSoft) {
|
489 |
+
const softAction = strategy.soft[playerValue]?.[mappedDealerCard] || 'H';
|
490 |
+
action = softAction === 'D' ? 'H' : softAction;
|
491 |
+
} else {
|
492 |
+
if (playerValue < 9) action = 'H';
|
493 |
+
else if (playerValue >= 17) action = 'S';
|
494 |
+
else {
|
495 |
+
const hardAction = strategy.hard[playerValue]?.[mappedDealerCard] || 'H';
|
496 |
+
action = hardAction === 'D' ? 'H' : hardAction;
|
497 |
+
}
|
498 |
+
}
|
499 |
+
}
|
500 |
+
}
|
501 |
+
|
502 |
+
const actionMap = { 'H': "Hit", 'S': "Stand", 'D': "Double", 'P': "Split" };
|
503 |
+
return {
|
504 |
+
action: actionMap[action] || action,
|
505 |
+
message: `Optimal action: ${actionMap[action] || 'Unknown'}`
|
506 |
+
};
|
507 |
+
}
|
508 |
+
|
509 |
+
function calculatePlayerNextCardProbabilities(playerHandStrings, dealerUpCardString, numDecks) {
|
510 |
+
const { value: currentPlayerValue } = calculateHandValueDetailed(playerHandStrings);
|
511 |
+
if (currentPlayerValue > 21) {
|
512 |
+
return { currentValue: currentPlayerValue, probSafe: 0, probBust: 100, error: null };
|
513 |
+
}
|
514 |
+
const { deck: remainingDeck, error: deckError } = getEffectiveRemainingDeck(playerHandStrings, dealerUpCardString, numDecks);
|
515 |
+
if (deckError) {
|
516 |
+
return { currentValue: currentPlayerValue, probSafe: 0, probBust: 0, error: deckError };
|
517 |
+
}
|
518 |
+
if (!remainingDeck || !remainingDeck.length) {
|
519 |
+
return { currentValue: currentPlayerValue, probSafe: currentPlayerValue <= 21 ? 100 : 0, probBust: currentPlayerValue <= 21 ? 0 : 100, error: "No cards left in deck for player to draw." };
|
520 |
+
}
|
521 |
+
let safeOutcomes = 0;
|
522 |
+
let bustOutcomes = 0;
|
523 |
+
for (const nextCardCandidate of remainingDeck) {
|
524 |
+
const tempHand = [...playerHandStrings, nextCardCandidate];
|
525 |
+
const { value: newValue } = calculateHandValueDetailed(tempHand);
|
526 |
+
if (newValue <= 21) safeOutcomes++;
|
527 |
+
else bustOutcomes++;
|
528 |
+
}
|
529 |
+
const totalRemainingCards = remainingDeck.length;
|
530 |
+
const probSafePercent = totalRemainingCards > 0 ? (safeOutcomes / totalRemainingCards) * 100 : 0;
|
531 |
+
const probBustPercent = totalRemainingCards > 0 ? (bustOutcomes / totalRemainingCards) * 100 : 0;
|
532 |
+
return { currentValue: currentPlayerValue, probSafe: probSafePercent, probBust: probBustPercent, error: null };
|
533 |
+
}
|
534 |
+
|
535 |
+
const memoDealerDist = {};
|
536 |
+
function getDealerFinalValueDistribution(currentDealerHandStrings, currentDeckState, hitSoft17) {
|
537 |
+
const handTuple = [...currentDealerHandStrings].sort().join(',');
|
538 |
+
const deckCounts = currentDeckState.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {});
|
539 |
+
const deckCountsTuple = Object.entries(deckCounts).sort().map(([k, v]) => `${k}:${v}`).join('|');
|
540 |
+
const memoKey = `${handTuple}|${deckCountsTuple}|${hitSoft17}`;
|
541 |
+
if (memoDealerDist[memoKey]) return memoDealerDist[memoKey];
|
542 |
+
|
543 |
+
const { value, isSoft, isBlackjack } = calculateHandValueDetailed(currentDealerHandStrings);
|
544 |
+
if (isBlackjack) { const result = { 'BJ': 1.0 }; memoDealerDist[memoKey] = result; return result; }
|
545 |
+
if (value > 21) { const result = { 'Bust': 1.0 }; memoDealerDist[memoKey] = result; return result; }
|
546 |
+
if (value >= 17) {
|
547 |
+
if (value === 17 && isSoft && hitSoft17) { /* Continue */ }
|
548 |
+
else { const result = { [value]: 1.0 }; memoDealerDist[memoKey] = result; return result; }
|
549 |
+
}
|
550 |
+
if (!currentDeckState.length) { const result = value <= 21 ? { [value]: 1.0 } : { 'Bust': 1.0 }; memoDealerDist[memoKey] = result; return result; }
|
551 |
+
|
552 |
+
const finalDistribution = {};
|
553 |
+
const totalCardsInDeck = currentDeckState.length;
|
554 |
+
const uniqueCardsInDeckCounts = currentDeckState.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {});
|
555 |
+
|
556 |
+
for (const [cardDrawn, count] of Object.entries(uniqueCardsInDeckCounts)) {
|
557 |
+
const probDrawingThisCard = count / totalCardsInDeck;
|
558 |
+
const newHandStrings = [...currentDealerHandStrings, cardDrawn];
|
559 |
+
const newDeckState = [...currentDeckState]; // Create a copy
|
560 |
+
const indexToRemove = newDeckState.indexOf(cardDrawn);
|
561 |
+
if (indexToRemove > -1) newDeckState.splice(indexToRemove, 1); // Remove one instance
|
562 |
+
|
563 |
+
const recursiveDistribution = getDealerFinalValueDistribution(newHandStrings, newDeckState, hitSoft17);
|
564 |
+
for (const [outcome, prob] of Object.entries(recursiveDistribution)) {
|
565 |
+
finalDistribution[outcome] = (finalDistribution[outcome] || 0) + probDrawingThisCard * prob;
|
566 |
+
}
|
567 |
+
}
|
568 |
+
memoDealerDist[memoKey] = finalDistribution;
|
569 |
+
return finalDistribution;
|
570 |
+
}
|
571 |
+
|
572 |
+
function calculateDealerOutcomeProbabilities(dealerUpCardString, playerHandStrings, numDecks, hitSoft17) {
|
573 |
+
Object.keys(memoDealerDist).forEach(key => delete memoDealerDist[key]);
|
574 |
+
if (!dealerUpCardString) return { probabilities: null, error: "Dealer up-card must be provided." };
|
575 |
+
dealerUpCardString = dealerUpCardString.toUpperCase();
|
576 |
+
if (!VALID_CARD_INPUTS.includes(dealerUpCardString)) return { probabilities: null, error: `Invalid dealer up-card: ${dealerUpCardString}` };
|
577 |
+
|
578 |
+
const { deck: deckForHoleCard, error: deckError } = getEffectiveRemainingDeck(playerHandStrings, dealerUpCardString, numDecks);
|
579 |
+
if (deckError) return { probabilities: null, error: deckError };
|
580 |
+
if (!deckForHoleCard || !deckForHoleCard.length) return { probabilities: null, error: "No cards left in deck for dealer's hole card." };
|
581 |
+
|
582 |
+
const overallDealerOutcomeDistribution = {};
|
583 |
+
const totalPossibleHoleCards = deckForHoleCard.length;
|
584 |
+
const holeCardCounts = deckForHoleCard.reduce((acc, card) => { acc[card] = (acc[card] || 0) + 1; return acc; }, {});
|
585 |
+
|
586 |
+
for (const [holeCardCandidate, count] of Object.entries(holeCardCounts)) {
|
587 |
+
const probThisHoleCardIsDrawn = count / totalPossibleHoleCards;
|
588 |
+
const dealerInitialHand = [dealerUpCardString, holeCardCandidate];
|
589 |
+
|
590 |
+
const deckForDealerPlay = [...deckForHoleCard]; // Create a copy
|
591 |
+
const indexToRemove = deckForDealerPlay.indexOf(holeCardCandidate);
|
592 |
+
if (indexToRemove > -1) deckForDealerPlay.splice(indexToRemove, 1); // Remove one instance
|
593 |
+
|
594 |
+
const distributionForThisHoleCard = getDealerFinalValueDistribution(dealerInitialHand, deckForDealerPlay, hitSoft17);
|
595 |
+
for (const [outcome, prob] of Object.entries(distributionForThisHoleCard)) {
|
596 |
+
overallDealerOutcomeDistribution[outcome] = (overallDealerOutcomeDistribution[outcome] || 0) + probThisHoleCardIsDrawn * prob;
|
597 |
+
}
|
598 |
+
}
|
599 |
+
const finalProbabilitiesPercent = Object.fromEntries(Object.entries(overallDealerOutcomeDistribution).map(([k, v]) => [k, v * 100]));
|
600 |
+
return { probabilities: finalProbabilitiesPercent, error: null };
|
601 |
+
}
|
602 |
+
|
603 |
+
// UI Update Functions
|
604 |
+
function updateCardDisplays() {
|
605 |
+
const { cards: playerCards } = parsePlayerCards();
|
606 |
+
const dealerCard = document.getElementById('dealerCard').value.trim().toUpperCase();
|
607 |
+
const playerCardsDisplay = document.getElementById('playerCardsDisplay');
|
608 |
+
playerCardsDisplay.innerHTML = '';
|
609 |
+
if (playerCards && playerCards.length) {
|
610 |
+
playerCards.forEach(card => {
|
611 |
+
// Simplified red/black for display, not true suit tracking.
|
612 |
+
const isRedSuitChar = ['H', 'D'].includes(card.slice(-1)) && card.length > 1; // Avoids 'H' from 'HIT'
|
613 |
+
const cardEl = document.createElement('div');
|
614 |
+
cardEl.className = `card ${isRedSuitChar ? 'card-red' : 'card-black'}`;
|
615 |
+
cardEl.textContent = card;
|
616 |
+
playerCardsDisplay.appendChild(cardEl);
|
617 |
+
});
|
618 |
+
}
|
619 |
+
const dealerCardDisplay = document.getElementById('dealerCardDisplay');
|
620 |
+
dealerCardDisplay.innerHTML = '';
|
621 |
+
if (dealerCard && VALID_CARD_INPUTS.includes(dealerCard)) {
|
622 |
+
const isRedSuitChar = ['H', 'D'].includes(dealerCard.slice(-1)) && dealerCard.length > 1;
|
623 |
+
const cardEl = document.createElement('div');
|
624 |
+
cardEl.className = `card ${isRedSuitChar ? 'card-red' : 'card-black'}`;
|
625 |
+
cardEl.textContent = dealerCard;
|
626 |
+
dealerCardDisplay.appendChild(cardEl);
|
627 |
+
const faceDownCard = document.createElement('div');
|
628 |
+
faceDownCard.className = 'card bg-gray-800 text-white';
|
629 |
+
faceDownCard.innerHTML = '<i class="fas fa-question"></i>';
|
630 |
+
dealerCardDisplay.appendChild(faceDownCard);
|
631 |
+
}
|
632 |
+
}
|
633 |
+
|
634 |
+
function updateStrategyAdvice() {
|
635 |
+
const { cards: playerCards, error: playerError } = parsePlayerCards();
|
636 |
+
const dealerCard = document.getElementById('dealerCard').value.trim().toUpperCase();
|
637 |
+
const isH17 = document.getElementById('h17Rule').checked;
|
638 |
+
const dasAllowed = document.getElementById('dasRule').checked; // Get DAS rule
|
639 |
+
const strategyResultDiv = document.getElementById('strategyResult');
|
640 |
+
|
641 |
+
if (playerError && playerCards === null) {
|
642 |
+
strategyResultDiv.innerHTML = `<div class="text-red-500">${playerError}</div>`; return;
|
643 |
+
}
|
644 |
+
if (!playerCards || !playerCards.length || !dealerCard) {
|
645 |
+
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;
|
646 |
+
}
|
647 |
+
if (!VALID_CARD_INPUTS.includes(dealerCard)) {
|
648 |
+
strategyResultDiv.innerHTML = `<div class="text-red-500">Invalid dealer card '${dealerCard}'</div>`; return;
|
649 |
+
}
|
650 |
+
if (playerCards.length < 2) {
|
651 |
+
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;
|
652 |
+
}
|
653 |
+
|
654 |
+
const { action, message } = getOptimalAction(playerCards, dealerCard, isH17, dasAllowed); // Pass dasAllowed
|
655 |
+
if (action) {
|
656 |
+
let actionClass = 'text-blue-600'; // Default for Stand
|
657 |
+
if (action === 'Hit') actionClass = 'text-green-600';
|
658 |
+
else if (action === 'Double') actionClass = 'text-purple-600';
|
659 |
+
else if (action === 'Split') actionClass = 'text-yellow-600';
|
660 |
+
|
661 |
+
strategyResultDiv.innerHTML = `
|
662 |
+
<div class="text-lg font-bold ${actionClass} mb-2 strategy-action">Recommended Action: ${action}</div>
|
663 |
+
<div class="text-sm text-gray-600">${message}</div>
|
664 |
+
`;
|
665 |
+
} else {
|
666 |
+
strategyResultDiv.innerHTML = `<div class="text-gray-500">${message || 'Could not determine action.'}</div>`;
|
667 |
+
}
|
668 |
+
}
|
669 |
+
|
670 |
+
function calculatePlayerProbabilities() {
|
671 |
+
const playerStatus = document.getElementById('playerStatus');
|
672 |
+
playerStatus.textContent = '';
|
673 |
+
updateStrategyAdvice();
|
674 |
+
const { cards: playerCards, error } = parsePlayerCards();
|
675 |
+
const playerHandValueEl = document.getElementById('playerHandValue');
|
676 |
+
const safeProbTextEl = document.getElementById('safeProbText');
|
677 |
+
const bustProbTextEl = document.getElementById('bustProbText');
|
678 |
+
const safeProbBarEl = document.getElementById('safeProbBar');
|
679 |
+
const bustProbBarEl = document.getElementById('bustProbBar');
|
680 |
+
|
681 |
+
function resetPlayerProbs() {
|
682 |
+
playerHandValueEl.textContent = '--';
|
683 |
+
safeProbTextEl.textContent = '--%';
|
684 |
+
bustProbTextEl.textContent = '--%';
|
685 |
+
safeProbBarEl.style.width = '0%';
|
686 |
+
bustProbBarEl.style.width = '0%';
|
687 |
+
}
|
688 |
+
|
689 |
+
if (error && playerCards === null) {
|
690 |
+
playerStatus.textContent = error; resetPlayerProbs(); return;
|
691 |
+
}
|
692 |
+
if (!playerCards || !playerCards.length) {
|
693 |
+
playerStatus.textContent = "Player cards are empty for probability calculation."; resetPlayerProbs(); return;
|
694 |
+
}
|
695 |
+
|
696 |
+
const numDecks = parseInt(document.getElementById('numDecks').value);
|
697 |
+
const dealerUpCard = document.getElementById('dealerCard').value.trim().toUpperCase();
|
698 |
+
let effectiveDealerCard = null;
|
699 |
+
if (dealerUpCard && VALID_CARD_INPUTS.includes(dealerUpCard)) effectiveDealerCard = dealerUpCard;
|
700 |
+
else if (dealerUpCard) playerStatus.textContent = `Warning: Invalid dealer up-card '${dealerUpCard}'. Ignored for card removal in player prob calc.`;
|
701 |
+
|
702 |
+
const { currentValue, probSafe, probBust, error: calcError } = calculatePlayerNextCardProbabilities(playerCards, effectiveDealerCard, numDecks);
|
703 |
+
if (calcError) {
|
704 |
+
const currentStatus = playerStatus.textContent;
|
705 |
+
playerStatus.textContent = `${currentStatus} Error: ${calcError}`.trim(); resetPlayerProbs();
|
706 |
+
} else {
|
707 |
+
playerHandValueEl.textContent = currentValue;
|
708 |
+
safeProbTextEl.textContent = `${probSafe.toFixed(2)}%`;
|
709 |
+
bustProbTextEl.textContent = `${probBust.toFixed(2)}%`;
|
710 |
+
safeProbBarEl.style.width = `${probSafe}%`;
|
711 |
+
bustProbBarEl.style.width = `${probBust}%`;
|
712 |
+
const currentStatus = playerStatus.textContent;
|
713 |
+
const calcMessage = currentValue > 21 ? "Player is Busted." : "Player probabilities calculated.";
|
714 |
+
playerStatus.textContent = currentStatus.includes("Warning") ? `${currentStatus} ${calcMessage}` : calcMessage;
|
715 |
+
}
|
716 |
+
}
|
717 |
+
|
718 |
+
function calculateDealerProbabilities() {
|
719 |
+
const dealerStatus = document.getElementById('dealerStatus');
|
720 |
+
dealerStatus.textContent = '';
|
721 |
+
updateStrategyAdvice();
|
722 |
+
const { cards: playerCards, error: playerError } = parsePlayerCards();
|
723 |
+
if (playerError && playerCards === null) { dealerStatus.textContent = `Player card error: ${playerError}`; return; }
|
724 |
+
|
725 |
+
const numDecks = parseInt(document.getElementById('numDecks').value);
|
726 |
+
const dealerUpCard = document.getElementById('dealerCard').value.trim().toUpperCase();
|
727 |
+
const hitSoft17 = document.getElementById('h17Rule').checked;
|
728 |
+
|
729 |
+
if (!dealerUpCard) { dealerStatus.textContent = "Dealer up-card is required for dealer calculation."; return; }
|
730 |
+
if (!VALID_CARD_INPUTS.includes(dealerUpCard)) { dealerStatus.textContent = `Invalid dealer up-card: '${dealerUpCard}'.`; return; }
|
731 |
+
|
732 |
+
dealerStatus.textContent = "Calculating dealer probabilities... Please wait.";
|
733 |
+
|
734 |
+
setTimeout(() => {
|
735 |
+
const { probabilities, error } = calculateDealerOutcomeProbabilities(dealerUpCard, playerCards || [], numDecks, hitSoft17);
|
736 |
+
const dealerResultsTextEl = document.getElementById('dealerResultsText');
|
737 |
+
dealerResultsTextEl.innerHTML = '';
|
738 |
+
|
739 |
+
if (error) {
|
740 |
+
dealerStatus.textContent = error;
|
741 |
+
dealerResultsTextEl.innerHTML = "Error during calculation.";
|
742 |
+
} else if (probabilities) {
|
743 |
+
dealerStatus.textContent = "Dealer probabilities calculated.";
|
744 |
+
// Expanded display order
|
745 |
+
const displayOrder = ['BJ', 21, 20, 19, 18, 17, 16, 15, 'Bust'];
|
746 |
+
let outputHTML = "<div class='space-y-1'><div class='font-bold mb-2'>Dealer Final Hand Probabilities:</div>";
|
747 |
+
const processedOutcomes = new Set();
|
748 |
+
|
749 |
+
for (const outcomeKey of displayOrder) {
|
750 |
+
if (probabilities[outcomeKey] !== undefined) {
|
751 |
+
const displayKeyStr = outcomeKey === 'BJ' ? "Blackjack" : outcomeKey.toString();
|
752 |
+
const probVal = probabilities[outcomeKey];
|
753 |
+
outputHTML += `<div class="flex justify-between"><span>P(Dealer gets ${displayKeyStr}):</span><span class="font-bold">${probVal.toFixed(2)}%</span></div>`;
|
754 |
+
processedOutcomes.add(outcomeKey);
|
755 |
+
}
|
756 |
+
}
|
757 |
+
|
758 |
+
const remainingItems = [];
|
759 |
+
for (const key in probabilities) {
|
760 |
+
if (!processedOutcomes.has(key) && !processedOutcomes.has(parseInt(key))) { // Check both string and int versions for safety
|
761 |
+
remainingItems.push([key, probabilities[key]]);
|
762 |
+
}
|
763 |
+
}
|
764 |
+
// Sort remaining: Numbers desc (except Bust/BJ), then BJ, then Bust
|
765 |
+
remainingItems.sort((a, b) => {
|
766 |
+
const keyA = a[0]; const keyB = b[0];
|
767 |
+
const isNumA = !isNaN(parseFloat(keyA)); const isNumB = !isNaN(parseFloat(keyB));
|
768 |
+
if (keyA === 'BJ') return -1; if (keyB === 'BJ') return 1;
|
769 |
+
if (keyA === 'Bust') return 1; if (keyB === 'Bust') return -1;
|
770 |
+
if (isNumA && isNumB) return parseFloat(keyB) - parseFloat(keyA); // Numbers descending
|
771 |
+
if (isNumA) return -1; // Numbers before other strings
|
772 |
+
if (isNumB) return 1;
|
773 |
+
return keyA.localeCompare(keyB); // Other strings alphabetically
|
774 |
+
});
|
775 |
+
|
776 |
+
for (const [outcomeKey, probVal] of remainingItems) {
|
777 |
+
const displayKeyStr = outcomeKey === 'BJ' ? "Blackjack" : outcomeKey.toString();
|
778 |
+
outputHTML += `<div class="flex justify-between"><span>P(Dealer gets ${displayKeyStr}):</span><span class="font-bold">${probVal.toFixed(2)}%</span></div>`;
|
779 |
+
}
|
780 |
+
|
781 |
+
const totalProbCheck = Object.values(probabilities).reduce((sum, val) => sum + val, 0);
|
782 |
+
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>`;
|
783 |
+
outputHTML += "</div>";
|
784 |
+
dealerResultsTextEl.innerHTML = outputHTML;
|
785 |
+
} else {
|
786 |
+
dealerStatus.textContent = "No results from dealer calculation.";
|
787 |
+
dealerResultsTextEl.innerHTML = "No results.";
|
788 |
+
}
|
789 |
+
}, 50); // Reduced timeout slightly, 100ms is quite long for simple UI update.
|
790 |
+
}
|
791 |
+
</script>
|
792 |
+
<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>
|
793 |
+
</html>
|
prompts.txt
ADDED
File without changes
|