Skip to content

Commit

Permalink
blackjack eval ui
Browse files Browse the repository at this point in the history
  • Loading branch information
anonghuser authored Mar 30, 2024
1 parent 9b93289 commit 6ed4b07
Show file tree
Hide file tree
Showing 5 changed files with 292 additions and 0 deletions.
9 changes: 9 additions & 0 deletions blackjack/index.html
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>
102 changes: 102 additions & 0 deletions blackjack/math.js
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]
}

31 changes: 31 additions & 0 deletions blackjack/setImmediate.js
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
}
18 changes: 18 additions & 0 deletions blackjack/style.css
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;
}
132 changes: 132 additions & 0 deletions blackjack/ui.js
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)

0 comments on commit 6ed4b07

Please sign in to comment.