-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9b93289
commit 6ed4b07
Showing
5 changed files
with
292 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<!doctype html> | ||
<title>blackjack</title> | ||
<link rel="stylesheet" type="text/css" href="style.css"></link> | ||
<script src="https://unpkg.com/[email protected]/mithril.min.js"></script> | ||
<script src="setImmediate.js"></script> | ||
<script src="math.js"></script> | ||
<script src="ui.js"></script> | ||
<body> | ||
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
"use strict"; | ||
const deck = 'A 2 3 4 5 6 7 8 9 10 J Q K'.split(' ') | ||
const deckCount = 2 | ||
const binAlign = true | ||
const binMultiplier = binAlign | ||
? 2 ** Math.ceil(Math.log2(deckCount * 4 + 1)) | ||
: deckCount * 4 + 1 | ||
|
||
const binDeck = binFromString(deck) * 4 * deckCount | ||
|
||
if (binDeck > Number.MAX_SAFE_INTEGER) { | ||
throw new RangeError("Too many decks, JS's 53 bit ints are insufficient.") | ||
} | ||
|
||
// console.log(countAll(binDeck) == deck.length * 4 * deckCount) | ||
// console.log(count(binDeck, 0) == 4 * deckCount) | ||
// console.log(count(binDeck, 8) == 4 * deckCount) | ||
// console.log(count(binDeck, 9) == 4 * 4 * deckCount) | ||
// console.log(value(binDeck)[0] == 85 * 4 * deckCount) | ||
// console.log(value(binFromString('A K'))[0] == 21) | ||
// console.log(value(binFromString('A K K'))[0] == 21) | ||
// console.log(bustChance(0).toFixed(5) == 0.28258) | ||
|
||
function exceededFromString(string) { | ||
return (String(string) | ||
.toUpperCase() | ||
.match(/10|[2-9JQKA]/g) || [] | ||
).find((v, i, a) => a.filter(v2 => v2 == v).length > deckCount * 4) | ||
} | ||
|
||
function binFromString(string) { | ||
return (String(string) | ||
.toUpperCase() | ||
.replace(/[JQK]/g, '10') | ||
.match(/10|[2-9JQKA]/g) || [] | ||
).map(card => binMultiplier ** deck.indexOf(card)) | ||
.reduce((a, b) => a + b, 0) | ||
} | ||
|
||
function binCountAll(binCards) { | ||
let r = Math.trunc(binCards / binMultiplier ** 9) | ||
binCards = binCards % binMultiplier ** 9 | ||
while (binCards) { | ||
r += binCards % binMultiplier | ||
binCards = Math.trunc(binCards / binMultiplier) | ||
} | ||
return r | ||
} | ||
|
||
function binCount(binCards, idx) { | ||
let r = Math.trunc(binCards / binMultiplier ** idx) | ||
return idx < 9 ? r % binMultiplier : r | ||
} | ||
|
||
function binValue(binCards) { | ||
let a = binCards % binMultiplier | ||
let c = Math.trunc(binCards / binMultiplier ** 9) | ||
let r = Math.trunc(binCards / binMultiplier ** 9) * 10 | ||
binCards = binCards % binMultiplier ** 9 | ||
let v = 1 | ||
while (binCards) { | ||
c += (binCards % binMultiplier) | ||
r += (binCards % binMultiplier) * v | ||
binCards = Math.trunc(binCards / binMultiplier) | ||
v++ | ||
} | ||
if (c == 2 && r == 11 && a) r += .1 | ||
return (r < 12 && a) ? [r + 10, true] : [r, false] | ||
} | ||
|
||
function binValueChance(binHand, binOthers = 0, soft17hit = false, target = 22, maxDepth = 10000, cache = {}, depth = 0) { | ||
const [v, soft] = binValue(binHand) | ||
if (v >= target) return 1 | ||
if (v > 17) return 0 | ||
if (v == 17 && (!soft17hit || !soft)) return 0 | ||
if (depth == maxDepth) return 0 | ||
if (cache[binHand]) return cache[binHand] | ||
let result = 0 | ||
let all = 0 | ||
let binCard = 1 | ||
let remain = binDeck - binHand - binOthers | ||
for (let idx = 0; idx < 9; idx++) { | ||
const sel = remain % binMultiplier | ||
remain = Math.trunc(remain / binMultiplier) | ||
all += sel | ||
if (sel) result += sel * binValueChance(binHand + binCard, binOthers, soft17hit, target, maxDepth, cache, depth+1) | ||
binCard *= binMultiplier | ||
} | ||
all += remain | ||
if (remain) result += remain * binValueChance(binHand + binCard, binOthers, soft17hit, target, maxDepth, cache, depth+1) | ||
return cache[binHand] = result / all | ||
} | ||
|
||
function binDealerOdds(binHand, binPlayer, binOthers, soft17hit) { | ||
const valPlayer = binValue(binPlayer)[0] | ||
const bust = binValueChance(binHand, binPlayer + binOthers, soft17hit, 22) | ||
const win = (valPlayer < 22 ? binValueChance(binHand, binPlayer + binOthers, soft17hit, valPlayer + .01) : 1) - bust | ||
const loss = valPlayer < 22 ? 1 - binValueChance(binHand, binPlayer + binOthers, soft17hit, valPlayer) : 0 | ||
const draw = 1 - win - bust - loss | ||
return [bust, bust + loss, draw, win] | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
setImmediate.queue = [] | ||
setImmediate.keys = new WeakMap | ||
addEventListener('message', e => { | ||
if (e.data == 'runImmediate') { | ||
e.stopImmediatePropagation() | ||
const process = setImmediate.queue | ||
setImmediate.queue = [] | ||
for (const f of process) { | ||
try { | ||
f && f() | ||
} | ||
catch (e) { | ||
setTimeout(() => {throw e}) | ||
} | ||
} | ||
} | ||
}) | ||
function setImmediate(f) { | ||
const key = {} | ||
const idx = setImmediate.queue.length | ||
setImmediate.keys.set(key, [setImmediate.queue, idx]) | ||
setImmediate.queue[idx] = f | ||
if (idx == 0) { | ||
postMessage('runImmediate') | ||
} | ||
return key | ||
} | ||
function clearImmediate(key) { | ||
const [queue, idx] = setImmediate.keys.get(key) || [] | ||
if (idx >= 0) queue[idx] = null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
* { | ||
font-family: monospace; | ||
white-space: pre-wrap; | ||
box-sizing: border-box; | ||
font-size: 10pt; | ||
} | ||
label { | ||
display: block; | ||
margin: 1em 0 0; | ||
} | ||
input,textarea { | ||
text-overflow: ellipsis; | ||
border: 0px none; | ||
padding: 0; | ||
background: #88888822; | ||
width: 40ch; | ||
vertical-align: top; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
function app() { | ||
let player = '', updatePlayer = chain(filterInput, e => player = e.target.value) | ||
let dealer = '', updateDealer = chain(filterInput, e => dealer = e.target.value) | ||
let others = '', updateOthers = chain(filterInput, e => others = e.target.value) | ||
let soft17hit = false | ||
|
||
function select10s(e) { | ||
e.redraw = false | ||
const el = e.target | ||
const value = el.value | ||
let [ss, se, sd] = [el.selectionStart, el.selectionEnd, el.selectionDirection] | ||
const same = ss == se | ||
if (value[ss] == '0' && value[ss-1] == '1') { | ||
if (el.lastSelection && el.lastSelection[0] == ss - 1) ss++ | ||
else ss-- | ||
} | ||
if (same) { | ||
se = ss | ||
} | ||
if (value[se] == '0' && value[se-1] == '1') { | ||
if (el.lastSelection && el.lastSelection[1] == se + 1) se-- | ||
else se++ | ||
} | ||
el.setSelectionRange(ss, se, sd) | ||
el.lastSelection = [el.selectionStart, el.selectionEnd, el.selectionDirection] | ||
} | ||
|
||
function filterInput(e) { | ||
const el = e.target | ||
let value = el.value | ||
let [ss, se, sd] = [el.selectionStart, el.selectionEnd, el.selectionDirection] | ||
const same = ss == se | ||
|
||
// the cursor did not move, assume "del" key | ||
const del = el.lastSelection && el.lastSelection[0] == ss && el.lastSelection[1] == ss | ||
// the cursor moved back 1 space, assume "backspace" key | ||
const bsp = el.lastSelection && el.lastSelection[0] == ss + 1 && el.lastSelection[1] == ss + 1 | ||
if (same && !del && !bsp) { | ||
// check if typed a partial 10 and expand it automatically | ||
const partial = value[ss-1] == '1' && value[ss] != '0' || value[ss-1] == '0' && value[ss-2] != '1' | ||
if (partial) value = value.slice(0, ss-1) + '10' + value.slice(ss) | ||
} | ||
|
||
const cards = value.toUpperCase().match(/10|[2-9JQKA]/g) || [] | ||
value = cards.join(' ') | ||
|
||
const delta = value.length - el.value.length | ||
se += delta | ||
if (se < 0) se = 0 | ||
if (same) { | ||
if (delta < 0 && del) se = ss | ||
if (delta > 0 && bsp) se = ss | ||
ss = se | ||
} | ||
el.value = value | ||
el.setSelectionRange(ss, se, sd) | ||
el.dispatchEvent(new Event('selectionchange')) | ||
el.lastValue = el.value | ||
} | ||
|
||
function chain(...handlers) { | ||
return function (...args) { | ||
handlers.forEach(f => f.call(this, ...args)) | ||
} | ||
} | ||
|
||
function value(hand) { | ||
return binValue(binFromString(hand))[0] | ||
} | ||
function bust(hand, others, soft17hit) { | ||
return binValueChance(binFromString(hand), binFromString(others), soft17hit) | ||
} | ||
function dealerOdds(hand, player, others, soft17hit) { | ||
if (exceededFromString(hand + player + others)) { | ||
return [NaN, NaN, NaN, NaN] | ||
} | ||
const [binHand, binPlayer, binOthers] = [hand, player, others].map(binFromString) | ||
return binDealerOdds(binHand, binPlayer, binOthers, soft17hit) | ||
} | ||
|
||
return { | ||
view: () => [ | ||
'12345678901234567890123456789012345678901234567890123456789012345678901234567890\n', | ||
" ' 10| ' 20| ' 30| ' 40| ' 50| ' 60| ' 70| ' 80|\n", | ||
m('label', 'dealer: ', m('input', { | ||
value: dealer, | ||
oninput: updateDealer, | ||
onselectionchange: select10s, | ||
})), | ||
' ', | ||
'value: ', value(dealer), | ||
dealerOdds(dealer, player, others, soft17hit).map( | ||
(v,i) => '\n '+[' bust: ', ' lose: ', ' draw: ', ' win: '][i] + +(v*100).toFixed(3)+'%' | ||
).join(' '), | ||
|
||
m('label', 'player: ', m('input', { | ||
value: player, | ||
oninput: updatePlayer, | ||
onselectionchange: select10s, | ||
})), | ||
' ', | ||
'value: ', value(player), | ||
'\n ', | ||
//'bust on hit: ', bustChance1(player, dealer + others), | ||
'\n ', | ||
'bust by 17: ', bust(player, dealer + others), | ||
m('label', 'others: ', m('textarea', { | ||
rows: 3, | ||
value: others, | ||
oninput: updateOthers, | ||
onselectionchange: select10s, | ||
})), | ||
exceededFromString(player + dealer + others) && m('',{style:'color:red'}, '\nToo many ', exceededFromString(player + dealer + others)) | ||
], | ||
} | ||
} | ||
m.mount(document.body, app) | ||
|
||
function selectionChangeEventFix(e) { | ||
if (e.isTrusted) { | ||
const el = document.activeElement | ||
if (!el || !("selectionStart" in el)) return | ||
if (el == e.target) { | ||
// seems like the browser supports properly targeted selectionchange events already | ||
console.log('selectionchange events already supported, disabling fix') | ||
document.removeEventListener('selectionchange', selectionChangeEventFix) | ||
return | ||
} | ||
el.dispatchEvent(new Event('selectionchange')) | ||
} | ||
} | ||
document.addEventListener('selectionchange', selectionChangeEventFix) |