d1demo / index.html
HelloSun's picture
copy: clarify bust round label
ca82eb0 verified
Raw History Blame Contribute Delete
81 kB
<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>d1 × d1 21 點:破產對決</title>
<meta name="description" content="兩個 LiquidAI d1-3B 決策模型,用 llama.cpp 一路打到雙方資金歸零">
<style>
:root{
--bg:#07120e; --bg2:#0b1c16; --panel:#0e241c; --panel2:#123026;
--line:rgba(255,255,255,.09); --text:#e8f3ed; --muted:#8fae9f;
--gold:#f6c453; --green:#39d98a; --red:#ff6b6b; --blue:#5cc8ff; --amber:#ffb020;
--hit:#ff6b6b; --stand:#5cc8ff; --double:#f6c453;
--shadow:0 18px 50px rgba(0,0,0,.45);
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{
background:
radial-gradient(1100px 700px at 12% -10%, rgba(57,217,138,.13), transparent 60%),
radial-gradient(900px 600px at 92% 0%, rgba(246,196,83,.10), transparent 55%),
var(--bg);
color:var(--text);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans TC","PingFang TC","Microsoft JhengHei",sans-serif;
line-height:1.6; min-height:100vh;
}
a{color:var(--gold)}
button{font:inherit;color:inherit;cursor:pointer}
.topbar{
position:sticky;top:0;z-index:50;backdrop-filter:blur(14px);
background:rgba(7,18,14,.8);border-bottom:1px solid var(--line);
}
.topbar-inner{max-width:1180px;margin:0 auto;padding:12px 20px;display:flex;gap:18px;align-items:center;flex-wrap:wrap}
.brand{display:flex;gap:10px;align-items:center;font-weight:800;letter-spacing:.02em}
.brand .logo{
width:30px;height:30px;border-radius:9px;display:grid;place-items:center;
background:linear-gradient(145deg,#f6c453,#b8860b);color:#1b1200;font-weight:900;
}
nav{display:flex;gap:4px;margin-left:auto;flex-wrap:wrap}
nav button{
border:1px solid transparent;background:transparent;padding:8px 14px;border-radius:999px;
color:var(--muted);font-weight:600;
}
nav button:hover{color:var(--text);background:rgba(255,255,255,.05)}
nav button.active{color:#1b1200;background:var(--gold);border-color:var(--gold)}
main{max-width:1180px;margin:0 auto;padding:28px 20px 60px}
.view{display:none}
.view.active{display:block;animation:fade .25s ease}
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
.hero{padding:18px 0 6px}
.hero h1{font-size:clamp(30px,4vw,50px);line-height:1.15;margin:0 0 10px;letter-spacing:-.01em}
.hero h1 em{font-style:normal;color:var(--gold)}
.hero p{color:var(--muted);margin:0;max-width:760px;font-size:16px}
.chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}
.chip{
font-size:12.5px;color:#cfe4d9;background:rgba(255,255,255,.045);
border:1px solid var(--line);border-radius:999px;padding:5px 11px;
}
.chip b{color:var(--gold)}
.scoreboard{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin:22px 0 24px}
.score{
position:relative;overflow:hidden;border:1px solid var(--line);border-radius:18px;
padding:18px 18px 16px;background:linear-gradient(160deg,var(--panel2),var(--panel));
box-shadow:var(--shadow);
}
.score:before{content:"";position:absolute;inset:0 auto 0 0;width:4px;background:var(--accent,var(--gold))}
.score .id{font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.score .name{font-size:22px;font-weight:800;margin:2px 0 0}
.score .style{font-size:13px;color:var(--accent,var(--gold))}
.score .bank{font-size:34px;font-weight:900;letter-spacing:-.02em;margin-top:10px;font-variant-numeric:tabular-nums}
.score .sub{font-size:13px;color:var(--muted)}
.big-delta{font-weight:800}
.pos{color:var(--green)}.neg{color:var(--red)}.neutral{color:var(--muted)}
.panel{
border:1px solid var(--line);border-radius:20px;background:linear-gradient(180deg,rgba(255,255,255,.025),rgba(0,0,0,.12));
box-shadow:var(--shadow);
}
.table-panel{padding:16px}
.round-toolbar{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:14px}
.icon-btn{
border:1px solid var(--line);background:rgba(255,255,255,.05);border-radius:10px;
padding:8px 13px;font-weight:700;color:var(--text);
}
.icon-btn:hover{background:rgba(255,255,255,.1)}
.icon-btn:disabled{opacity:.35;cursor:not-allowed}
.round-pills{display:flex;gap:6px;overflow-x:auto;padding:2px;max-width:100%;scrollbar-width:thin}
.pill{
flex:0 0 auto;min-width:38px;height:34px;border-radius:9px;border:1px solid var(--line);
background:rgba(255,255,255,.04);font-weight:800;font-size:13px;color:var(--muted);
display:grid;place-items:center;padding:0 8px;
}
.pill:hover{border-color:rgba(246,196,83,.5);color:var(--text)}
.pill.active{background:var(--gold);border-color:var(--gold);color:#1b1200}
.pill .dot{width:6px;height:6px;border-radius:50%;display:block;margin:0 auto 2px}
.pill.win .dot{background:var(--green)}
.pill.lose .dot{background:var(--red)}
.pill.mixed .dot{background:var(--amber)}
.pill.push .dot{background:#93a89d}
.round-label{margin-left:auto;color:var(--muted);font-size:13px;font-variant-numeric:tabular-nums}
.round-head{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:10px}
.round-head h2{margin:0;font-size:20px}
.round-head .shoe{color:var(--muted);font-size:12.5px}
.dealer{
border:1px solid rgba(246,196,83,.25);border-radius:16px;padding:14px 16px;margin-bottom:14px;
background:radial-gradient(700px 200px at 20% 0%,rgba(246,196,83,.1),transparent 70%),rgba(0,0,0,.18);
display:flex;gap:16px;align-items:center;flex-wrap:wrap;
}
.dealer .label{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);font-weight:800}
.dealer .total-badge{font-size:26px;font-weight:900;font-variant-numeric:tabular-nums}
.dealer .note{color:var(--muted);font-size:13px;flex:1;min-width:180px}
.players{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.player{
border:1px solid var(--line);border-radius:16px;padding:15px;background:rgba(255,255,255,.025);
}
.player.win{border-color:rgba(57,217,138,.45);box-shadow:inset 0 0 0 1px rgba(57,217,138,.12)}
.player.lose{border-color:rgba(255,107,107,.35)}
.player.blackjack{border-color:rgba(246,196,83,.6)}
.p-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start;flex-wrap:wrap}
.p-head h3{margin:0;font-size:18px}
.p-head .style{font-size:12.5px;color:var(--accent,var(--gold))}
.p-head .bet{
font-size:12.5px;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:4px 10px;
background:rgba(0,0,0,.2);white-space:nowrap;
}
.p-head .bet b{color:var(--gold);font-size:15px}
.hand{display:flex;gap:14px;align-items:center;margin:12px 0 8px;flex-wrap:wrap}
.cards{display:flex;gap:6px;flex-wrap:wrap}
.card{
width:52px;height:72px;border-radius:8px;background:#f7f7f2;color:#141414;
display:flex;flex-direction:column;align-items:center;justify-content:center;
font-weight:900;box-shadow:0 6px 14px rgba(0,0,0,.35);border:1px solid rgba(0,0,0,.15);
line-height:1;user-select:none;
}
.card .r{font-size:21px}
.card .s{font-size:17px;margin-top:2px}
.card.red{color:#c0182b}
.card.down{background:repeating-linear-gradient(45deg,#123c6b,#123c6b 5px,#0b2a4d 5px,#0b2a4d 10px);color:transparent;border-color:#0a2340}
.total{
font-size:30px;font-weight:900;font-variant-numeric:tabular-nums;display:flex;align-items:baseline;gap:6px;
}
.total small{font-size:11px;font-weight:700;color:var(--muted);border:1px solid var(--line);border-radius:5px;padding:1px 5px}
.result{
display:inline-flex;align-items:center;gap:6px;font-size:12.5px;font-weight:800;
padding:3px 10px;border-radius:999px;border:1px solid var(--line);
}
.result.win{color:var(--green);border-color:rgba(57,217,138,.4);background:rgba(57,217,138,.1)}
.result.lose{color:var(--red);border-color:rgba(255,107,107,.4);background:rgba(255,107,107,.08)}
.result.push{color:#b9c9c0;background:rgba(255,255,255,.05)}
.result.blackjack{color:var(--gold);border-color:rgba(246,196,83,.5);background:rgba(246,196,83,.1)}
.bet-line{display:flex;gap:8px;align-items:center;flex-wrap:wrap;font-size:12.5px;color:var(--muted);margin:6px 0 2px}
.bet-line .tag{border:1px solid var(--line);border-radius:6px;padding:1px 7px;color:#cfe4d9}
details{margin-top:8px}
summary{cursor:pointer;color:var(--muted);font-size:12.5px;font-weight:700}
summary:hover{color:var(--text)}
details .body{padding:8px 0 0;color:var(--muted);font-size:13px}
.steps{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:8px}
.step{
border:1px solid var(--line);border-radius:11px;padding:9px 11px;background:rgba(0,0,0,.2);
}
.step.enter{animation:slide .3s ease}
@keyframes slide{from{opacity:0;transform:translateX(-6px)}to{opacity:1;transform:none}}
.step-head{display:flex;gap:8px;align-items:center;flex-wrap:wrap;font-size:12.5px}
.step-no{color:var(--muted);font-weight:800;font-variant-numeric:tabular-nums}
.mini{font-weight:800;font-variant-numeric:tabular-nums}
.action{
font-weight:900;font-size:12px;letter-spacing:.04em;padding:2px 9px;border-radius:6px;color:#0b0f0d;
}
.action.hit{background:var(--hit)}.action.stand{background:var(--stand)}.action.double{background:var(--double)}
.action.unknown{background:#7a8a81}
.step .meta{margin-left:auto;color:var(--muted);font-size:11.5px;display:flex;gap:8px;flex-wrap:wrap}
.probs{display:grid;grid-template-columns:64px 1fr 46px;gap:6px;align-items:center;margin-top:7px;font-size:11.5px;color:var(--muted)}
.bar{height:7px;border-radius:4px;background:rgba(255,255,255,.07);overflow:hidden}
.bar>i{display:block;height:100%;border-radius:4px;background:var(--c,var(--gold));width:0;transition:width .5s ease}
.probs .pct{text-align:right;font-variant-numeric:tabular-nums;color:#d6e7de}
.step-foot{display:flex;gap:10px;flex-wrap:wrap;color:var(--muted);font-size:11.5px;margin-top:7px}
.step-foot b{color:#d6e7de}
.net{margin-top:11px;padding-top:10px;border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;font-size:13px}
.net .amt{font-size:19px;font-weight:900;font-variant-numeric:tabular-nums}
.events{margin-top:14px;border-top:1px dashed var(--line);padding-top:10px;color:var(--muted);font-size:12.5px}
.events ul{margin:6px 0 0;padding-left:18px}
.events li{margin:2px 0}
.arena-note{margin-top:14px;color:var(--muted);font-size:12px}
.section-title{font-size:22px;margin:26px 0 12px}
.stats-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.stat-card{border:1px solid var(--line);border-radius:16px;padding:16px;background:rgba(255,255,255,.025)}
.stat-card h3{margin:0 0 4px;font-size:17px}
.stat-card .style{font-size:12.5px;color:var(--accent,var(--gold))}
.kv{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:12px}
.kv .k{font-size:11.5px;color:var(--muted)}
.kv .v{font-size:19px;font-weight:800;font-variant-numeric:tabular-nums}
.dist{margin-top:12px}
.dist .row{display:grid;grid-template-columns:66px 1fr 44px;gap:7px;align-items:center;font-size:12px;margin:5px 0;color:var(--muted)}
.mini-title{font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-top:14px}
.chart-wrap{margin-top:14px;border:1px solid var(--line);border-radius:16px;padding:14px;background:rgba(0,0,0,.18)}
.chart-legend{display:flex;gap:14px;font-size:12px;color:var(--muted);flex-wrap:wrap;margin-bottom:6px}
.chart-legend i{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:5px}
.h2h{margin-top:14px;border:1px solid var(--line);border-radius:16px;padding:16px;background:rgba(255,255,255,.025)}
.h2h .big{font-size:30px;font-weight:900;color:var(--gold)}
.code-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
.code-tabs button{border:1px solid var(--line);background:rgba(255,255,255,.04);border-radius:8px;padding:6px 12px;font-size:12.5px;color:var(--muted)}
.code-tabs button.active{background:var(--gold);color:#1b1200;border-color:var(--gold);font-weight:800}
.code-bar{display:flex;gap:10px;align-items:center;margin-bottom:8px;color:var(--muted);font-size:12.5px;flex-wrap:wrap}
pre.code{
margin:0;border:1px solid var(--line);border-radius:14px;background:#04100c;padding:16px;
overflow:auto;max-height:70vh;font-size:12.5px;line-height:1.55;
}
pre.code code{font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,monospace}
.data-box{border:1px solid var(--line);border-radius:16px;padding:16px;background:rgba(255,255,255,.025);margin-bottom:14px}
.data-box h3{margin:0 0 8px;font-size:17px}
.data-box p{color:var(--muted);margin:6px 0;font-size:14px}
.btn{
display:inline-flex;align-items:center;gap:7px;border:1px solid rgba(246,196,83,.45);
background:rgba(246,196,83,.1);color:var(--gold);padding:7px 13px;border-radius:9px;
text-decoration:none;font-weight:700;font-size:13px;
}
.btn:hover{background:rgba(246,196,83,.18)}
.btn.ghost{border-color:var(--line);background:rgba(255,255,255,.04);color:var(--text)}
footer{border-top:1px solid var(--line);margin-top:30px;padding:22px 20px 40px;color:var(--muted);font-size:12.5px;text-align:center}
.loading{padding:40px;text-align:center;color:var(--muted)}
.err{border:1px solid rgba(255,107,107,.4);background:rgba(255,107,107,.08);padding:16px;border-radius:12px;color:#ffd4d4}
@media (max-width:760px){
.scoreboard,.stats-grid,.players{grid-template-columns:1fr}
.card{width:44px;height:62px}
.card .r{font-size:18px}
.total{font-size:25px}
.round-label{margin-left:0}
}
</style>
</head>
<body>
<header class="topbar">
<div class="topbar-inner">
<div class="brand"><span class="logo">21</span><span>d1 × d1 對戰實驗室</span></div>
<nav>
<button data-view="battle" class="active">對戰</button>
<button data-view="stats">統計</button>
<button data-view="code">整理程式</button>
<button data-view="data">原始資料</button>
</nav>
</div>
</header>
<main>
<div id="app"><div class="loading">載入對戰資料中…</div></div>
</main>
<footer>
<div>由 llama.cpp 上的兩個 <b>LiquidAI d1-3B</b> 決策模型自動產生 · 頁面由 build_site.py 靜態編譯</div>
<div style="margin-top:6px">Hosted on Hugging Face Static Space · <a href="https://huggingface.co/spaces/HelloSun/d1demo">HelloSun/d1demo</a></div>
</footer>
<script>
window.BATTLE_DATA = {"meta": {"title": "d1 × d1 21 點:破產對決", "subtitle": "兩個 LiquidAI d1-3B 決策模型,用 llama.cpp 一路打到雙方資金歸零", "created_at": "2026-10-21T11:32:55+02:00", "seed": 20261021, "model": "LiquidAI/d1-3B-GGUF", "quant": "Q8_0", "llama_build": "b11786 (0.7.0-dev)", "rules": {"decks": 6, "dealer": "莊家軟 17 停牌 (S17)", "blackjack_pays": "3:2", "double": "僅限硬 9–11 或軟 13–18 時加倍,加倍後只補一張", "split": "本局不開放分牌", "insurance": "不開放保險", "reshuffle": "剩餘牌數低於 25% 時重洗", "start_bankroll": 1000, "betting": "注碼依當前資金比例:小注 50%、大注 70%、全押 100%(取整到 10)", "rebuy": "無補充籌碼,兩邊都歸零才結束"}, "start_bankroll": 1000, "bet_fractions": {"min": 0.5, "standard": 0.7, "max": 1.0}, "end_reason": "both_broke", "agents": [{"id": "a", "name": "阿保", "style": "保守算牌派", "description": "以保住本金為第一優先,只有在牌面明顯有利時才加注,遇到風險寧可停牌。", "url": "http://127.0.0.1:8081", "final_bankroll": 0, "peak_bankroll": 1000, "bust_round": 10, "rounds_played": 10}, {"id": "b", "name": "阿衝", "style": "激進壓迫派", "description": "相信進攻是最好的防守,喜歡加倍與大注,願意為了期望值承擔波動。", "url": "http://127.0.0.1:8082", "final_bankroll": 0, "peak_bankroll": 1000, "bust_round": 11, "rounds_played": 11}], "rounds_played": 11, "duration_sec": 138.93, "inference": {"a": {"calls": 23, "avg_latency_ms": 3012}, "b": {"calls": 22, "avg_latency_ms": 3165}}}, "rounds": [{"n": 1, "shoe_before": {"decks": 6, "remaining_cards": 306, "penetration_pct": 2, "cards_left_by_rank": {"A": 23, "2": 24, "3": 23, "4": 24, "5": 24, "6": 23, "7": 23, "8": 24, "9": 24, "10": 24, "J": 24, "Q": 23, "K": 23}, "hi_lo_running_count": -1, "true_count": -0.17}, "dealer_up": "A♣", "dealer_hole": "K♣", "dealer_revealed": true, "dealer": {"cards": ["A♣", "K♣"], "total": 21, "soft": true, "blackjack": true, "bust": false}, "events": ["莊家明牌為 A 或 10 點,進行暗牌確認", "莊家直接拿到 Blackjack,本局立即結束"], "players": [{"cards": ["7♥", "3♣"], "bet": 500, "doubled": false, "blackjack": false, "bust": false, "total": 10, "soft": false, "result": "lose", "net": -500, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.7495426029302982, "standard": 0.19354882832047426, "max": 0.05690856874922748}, "confidence": 0.6243139043954472}, "bet": 500, "confidence": {"type": "score", "score": 1.1935795415714954, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.19777645506034436, "1": 0.4973906472646646, "2": 0.21830979871814207, "3": 0.08652309895684891}, "confidence": 0.4108675483078158}, "latency_ms": 2694}, "bankroll_after": 500, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["Q♦", "6♠"], "bet": 700, "doubled": false, "blackjack": false, "bust": false, "total": 16, "soft": false, "result": "lose", "net": -700, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.23967294369887637, "standard": 0.5817249980285547, "max": 0.17860205827256884}, "confidence": 0.37258749704283206}, "bet": 700, "confidence": {"type": "score", "score": 1.507238814141059, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.14445230077935894, "1": 0.3625481640958943, "2": 0.33430795532907537, "3": 0.15869157979567128}, "confidence": 0.20385658430022313}, "latency_ms": 2499}, "bankroll_after": 300, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 306, "penetration_pct": 2, "cards_left_by_rank": {"A": 23, "2": 24, "3": 23, "4": 24, "5": 24, "6": 23, "7": 23, "8": 24, "9": 24, "10": 24, "J": 24, "Q": 23, "K": 23}, "hi_lo_running_count": -1, "true_count": -0.17}}, {"n": 2, "shoe_before": {"decks": 6, "remaining_cards": 300, "penetration_pct": 4, "cards_left_by_rank": {"A": 22, "2": 24, "3": 22, "4": 24, "5": 23, "6": 23, "7": 23, "8": 23, "9": 24, "10": 23, "J": 24, "Q": 23, "K": 22}, "hi_lo_running_count": -2, "true_count": -0.35}, "dealer_up": "8♣", "dealer_hole": "A♠", "dealer_revealed": true, "dealer": {"cards": ["8♣", "A♠"], "total": 19, "soft": true, "blackjack": false, "bust": false}, "events": ["莊家最終 19 點"], "players": [{"cards": ["5♠", "3♥", "9♠"], "bet": 250, "doubled": false, "total": 17, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["5♠", "3♥"], "total": 8, "soft": false, "dealer_up": "8♣", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.7888347603567517, "stand": 0.2111652396432483}, "confidence": 0.5776695207135034}, "risk": {"type": "score", "score": 2.2652645474088886, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.03996224301656414, "1": 0.10200571269114653, "2": 0.41083729815912584, "3": 0.4471947461331635}, "confidence": 0.2652645474088887}, "latency_ms": 3151, "drawn": "9♠", "total_after": 17}, {"type": "action", "cards": ["5♠", "3♥", "9♠"], "total": 17, "soft": false, "dealer_up": "8♣", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.37302117687837494, "stand": 0.626978823121625}, "confidence": 0.25395764624325}, "risk": {"type": "score", "score": 1.4085793790866217, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.13348583156305185, "1": 0.3936259060555138, "2": 0.4037113141131949, "3": 0.06917694826823936}, "confidence": 0.2702254825501431}, "latency_ms": 3468, "drawn": null, "total_after": 17}], "player_id": "a", "result": "lose", "net": -250, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.6968753406570245, "standard": 0.25124407766170376, "max": 0.05188058168127172}, "confidence": 0.5453130109855368}, "bet": 250, "confidence": {"type": "score", "score": 1.153562244852418, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.19629014603126313, "1": 0.5138686227524958, "2": 0.229830071548801, "3": 0.0600111596674401}, "confidence": 0.4538574630850557}, "latency_ms": 2709}, "bankroll_after": 250, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["K♦", "10♣"], "bet": 210, "doubled": false, "total": 20, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["K♦", "10♣"], "total": 20, "soft": false, "dealer_up": "8♣", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.30708643375615396, "stand": 0.6929135662438459}, "confidence": 0.38582713248769185}, "risk": {"type": "score", "score": 1.1558612595776556, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.27435705365012364, "1": 0.35797206875513765, "2": 0.30512344196169827, "3": 0.06254743563304048}, "confidence": 0.2954246331220971}, "latency_ms": 2990, "drawn": null, "total_after": 20}], "player_id": "b", "result": "win", "net": 210, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.2589391915119814, "standard": 0.6021647784241172, "max": 0.13889603006390153}, "confidence": 0.4032471676361758}, "bet": 210, "confidence": {"type": "score", "score": 1.6222850123507153, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.12053751205265624, "1": 0.3180917564832067, "2": 0.37991893852490216, "3": 0.18145179293923472}, "confidence": 0.25938142647224605}, "latency_ms": 2426}, "bankroll_after": 510, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 299, "penetration_pct": 4, "cards_left_by_rank": {"A": 22, "2": 24, "3": 22, "4": 24, "5": 23, "6": 23, "7": 23, "8": 23, "9": 23, "10": 23, "J": 24, "Q": 23, "K": 22}, "hi_lo_running_count": -2, "true_count": -0.35}}, {"n": 3, "shoe_before": {"decks": 6, "remaining_cards": 293, "penetration_pct": 6, "cards_left_by_rank": {"A": 22, "2": 22, "3": 21, "4": 24, "5": 23, "6": 22, "7": 23, "8": 23, "9": 23, "10": 21, "J": 24, "Q": 23, "K": 22}, "hi_lo_running_count": 0, "true_count": 0.0}, "dealer_up": "3♦", "dealer_hole": "2♠", "dealer_revealed": true, "dealer": {"cards": ["3♦", "2♠", "2♠", "3♦", "A♣"], "total": 21, "soft": true, "blackjack": false, "bust": false}, "events": ["莊家補牌 2♠ → 7 點", "莊家補牌 3♦ → 10 點", "莊家補牌 A♣ → 21 點", "莊家最終 21 點"], "players": [{"cards": ["10♠", "2♦", "8♣"], "bet": 120, "doubled": false, "total": 20, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["10♠", "2♦"], "total": 12, "soft": false, "dealer_up": "3♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.6558900475681183, "stand": 0.34410995243188175}, "confidence": 0.3117800951362366}, "risk": {"type": "score", "score": 1.769234618561999, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.07876321188385403, "1": 0.2653560066817271, "2": 0.4637637324229848, "3": 0.1921170490114341}, "confidence": 0.3850005205391307}, "latency_ms": 3146, "drawn": "8♣", "total_after": 20}, {"type": "action", "cards": ["10♠", "2♦", "8♣"], "total": 20, "soft": false, "dealer_up": "3♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.2298190158896557, "stand": 0.7701809841103443}, "confidence": 0.5403619682206886}, "risk": {"type": "score", "score": 1.2032165044695071, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.21444016577017114, "1": 0.4154791783036063, "2": 0.3225046416127668, "3": 0.04757601431345572}, "confidence": 0.36790316399015066}, "latency_ms": 3084, "drawn": null, "total_after": 20}], "player_id": "a", "result": "lose", "net": -120, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.7021353615602709, "standard": 0.24459731120528871, "max": 0.053267327234440304}, "confidence": 0.5532030423404063}, "bet": 120, "confidence": {"type": "score", "score": 1.243907468048795, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.1647109410528787, "1": 0.49476116179321544, "2": 0.2724373852061379, "3": 0.06809051194776787}, "confidence": 0.42667064984544767}, "latency_ms": 2681}, "bankroll_after": 130, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["6♦", "10♠", "4♠"], "bet": 360, "doubled": false, "total": 20, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["6♦", "10♠"], "total": 16, "soft": false, "dealer_up": "3♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.6360780916514454, "stand": 0.3639219083485547}, "confidence": 0.2721561833028907}, "risk": {"type": "score", "score": 1.496846018692059, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.13424558064122727, "1": 0.3356707776533976, "2": 0.4290756840774641, "3": 0.10100795762791105}, "confidence": 0.29483010343623683}, "latency_ms": 3970, "drawn": "4♠", "total_after": 20}, {"type": "action", "cards": ["6♦", "10♠", "4♠"], "total": 20, "soft": false, "dealer_up": "3♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.3360724625093578, "stand": 0.6639275374906422}, "confidence": 0.3278550749812843}, "risk": {"type": "score", "score": 1.3092554578091316, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.2074752250590991, "1": 0.35679367170712645, "2": 0.35473152359931803, "3": 0.08099957963445631}, "confidence": 0.27579409207267025}, "latency_ms": 3577, "drawn": null, "total_after": 20}], "player_id": "b", "result": "lose", "net": -360, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.256591764511596, "standard": 0.6231699492197297, "max": 0.12023828626867433}, "confidence": 0.4347549238295945}, "bet": 360, "confidence": {"type": "score", "score": 1.637742613857751, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.12226879726860378, "1": 0.306868514496184, "2": 0.3817139653440694, "3": 0.1891487228911428}, "confidence": 0.2594451680754656}, "latency_ms": 2819}, "bankroll_after": 150, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 288, "penetration_pct": 8, "cards_left_by_rank": {"A": 21, "2": 21, "3": 20, "4": 23, "5": 23, "6": 22, "7": 23, "8": 22, "9": 23, "10": 21, "J": 24, "Q": 23, "K": 22}, "hi_lo_running_count": 2, "true_count": 0.36}}, {"n": 4, "shoe_before": {"decks": 6, "remaining_cards": 282, "penetration_pct": 10, "cards_left_by_rank": {"A": 20, "2": 21, "3": 19, "4": 23, "5": 23, "6": 22, "7": 22, "8": 21, "9": 22, "10": 21, "J": 24, "Q": 23, "K": 21}, "hi_lo_running_count": 1, "true_count": 0.18}, "dealer_up": "7♠", "dealer_hole": "K♥", "dealer_revealed": true, "dealer": {"cards": ["7♠", "K♥"], "total": 17, "soft": false, "blackjack": false, "bust": false}, "events": ["莊家最終 17 點"], "players": [{"cards": ["8♥", "A♦"], "bet": 60, "doubled": false, "total": 19, "soft": true, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["8♥", "A♦"], "total": 19, "soft": true, "dealer_up": "7♠", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.30128956481413705, "stand": 0.698710435185863}, "confidence": 0.3974208703717259}, "risk": {"type": "score", "score": 1.1084635753875032, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.25095460756718285, "1": 0.42334035297948597, "2": 0.2919918959519758, "3": 0.03371314350135523}, "confidence": 0.3896272094781309}, "latency_ms": 3396, "drawn": null, "total_after": 19}], "player_id": "a", "result": "win", "net": 60, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.6926127915764545, "standard": 0.23626710905817105, "max": 0.07112009936537435}, "confidence": 0.5389191873646817}, "bet": 60, "confidence": {"type": "score", "score": 1.146617901430409, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.21917876559390995, "1": 0.47945507834068335, "2": 0.23693564510649448, "3": 0.06443051095891222}, "confidence": 0.4150245673817712}, "latency_ms": 2582}, "bankroll_after": 190, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["9♠", "3♦", "K♥"], "bet": 100, "doubled": false, "total": 22, "soft": false, "bust": true, "blackjack": false, "steps": [{"type": "action", "cards": ["9♠", "3♦"], "total": 12, "soft": false, "dealer_up": "7♠", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.734403302613461, "stand": 0.265596697386539}, "confidence": 0.46880660522692197}, "risk": {"type": "score", "score": 1.5869452568879086, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.11743767228218839, "1": 0.3123418203798986, "2": 0.43605808550572894, "3": 0.13416242183218405}, "confidence": 0.3186204132235405}, "latency_ms": 3680, "drawn": "K♥", "total_after": 22}], "player_id": "b", "result": "lose", "net": -100, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.291170385524682, "standard": 0.5778208078311198, "max": 0.1310088066441981}, "confidence": 0.36673121174667966}, "bet": 100, "confidence": {"type": "score", "score": 1.6308974422103073, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.11805143814025988, "1": 0.3116761645734334, "2": 0.3915959142220468, "3": 0.17867648306426004}, "confidence": 0.2735444760817869}, "latency_ms": 2598}, "bankroll_after": 50, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 281, "penetration_pct": 10, "cards_left_by_rank": {"A": 20, "2": 21, "3": 19, "4": 23, "5": 23, "6": 22, "7": 22, "8": 21, "9": 22, "10": 21, "J": 24, "Q": 23, "K": 20}, "hi_lo_running_count": 0, "true_count": 0.0}}, {"n": 5, "shoe_before": {"decks": 6, "remaining_cards": 275, "penetration_pct": 12, "cards_left_by_rank": {"A": 19, "2": 20, "3": 19, "4": 23, "5": 23, "6": 21, "7": 21, "8": 21, "9": 22, "10": 20, "J": 23, "Q": 23, "K": 20}, "hi_lo_running_count": -1, "true_count": -0.19}, "dealer_up": "A♣", "dealer_hole": "J♠", "dealer_revealed": true, "dealer": {"cards": ["A♣", "J♠"], "total": 21, "soft": true, "blackjack": true, "bust": false}, "events": ["莊家明牌為 A 或 10 點,進行暗牌確認", "莊家直接拿到 Blackjack,本局立即結束"], "players": [{"cards": ["10♦", "2♦"], "bet": 100, "doubled": false, "blackjack": false, "bust": false, "total": 12, "soft": false, "result": "lose", "net": -100, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.7169821239824508, "standard": 0.22839151917086004, "max": 0.05462635684668915}, "confidence": 0.5754731859736761}, "bet": 100, "confidence": {"type": "score", "score": 1.2329249170160193, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.18594652365117773, "1": 0.47498597711682466, "2": 0.25926355779679827, "3": 0.07980394143519932}, "confidence": 0.39518203568162535}, "latency_ms": 2717}, "bankroll_after": 90, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["7♠", "6♦"], "bet": 40, "doubled": false, "blackjack": false, "bust": false, "total": 13, "soft": false, "result": "lose", "net": -40, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.33016281821538473, "standard": 0.5556542153278266, "max": 0.11418296645678869}, "confidence": 0.3334813229917398}, "bet": 40, "confidence": {"type": "score", "score": 1.7355514551814188, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.11075179507713576, "1": 0.26294508405667505, "2": 0.40630299147382376, "3": 0.22000012939236546}, "confidence": 0.2955511963966879}, "latency_ms": 2743}, "bankroll_after": 10, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 275, "penetration_pct": 12, "cards_left_by_rank": {"A": 19, "2": 20, "3": 19, "4": 23, "5": 23, "6": 21, "7": 21, "8": 21, "9": 22, "10": 20, "J": 23, "Q": 23, "K": 20}, "hi_lo_running_count": -1, "true_count": -0.19}}, {"n": 6, "shoe_before": {"decks": 6, "remaining_cards": 269, "penetration_pct": 14, "cards_left_by_rank": {"A": 17, "2": 20, "3": 18, "4": 23, "5": 22, "6": 21, "7": 21, "8": 21, "9": 21, "10": 20, "J": 23, "Q": 23, "K": 19}, "hi_lo_running_count": -2, "true_count": -0.39}, "dealer_up": "9♦", "dealer_hole": "5♣", "dealer_revealed": true, "dealer": {"cards": ["9♦", "5♣", "8♣"], "total": 22, "soft": false, "blackjack": false, "bust": true}, "events": ["莊家補牌 8♣ → 22 點", "莊家最終 22 點,爆牌"], "players": [{"cards": ["3♠", "A♦", "K♣", "7♠"], "bet": 40, "doubled": false, "total": 21, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["3♠", "A♦"], "total": 14, "soft": true, "dealer_up": "9♦", "allowed": ["hit", "stand", "double"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.4651867184249835, "stand": 0.2528327433499288, "double": 0.2819805382250877}, "confidence": 0.19778007763747524}, "risk": {"type": "score", "score": 1.661289775579295, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.0810802022698142, "1": 0.30672360983786334, "2": 0.48202239793553586, "3": 0.13017378995678672}, "confidence": 0.40094219566572153}, "latency_ms": 3258, "drawn": "K♣", "total_after": 14}, {"type": "action", "cards": ["3♠", "A♦", "K♣"], "total": 14, "soft": false, "dealer_up": "9♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.5558925013659549, "stand": 0.444107498634045}, "confidence": 0.11178500273190983}, "risk": {"type": "score", "score": 1.7006468099820728, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.09792126538793425, "1": 0.2820300499548643, "2": 0.4415292939443961, "3": 0.17851939071280543}, "confidence": 0.3436080285564618}, "latency_ms": 3196, "drawn": "7♠", "total_after": 21}], "player_id": "a", "result": "win", "net": 40, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.7430508242637351, "standard": 0.21044654186705883, "max": 0.04650263386920589}, "confidence": 0.6145762363956027}, "bet": 40, "confidence": {"type": "score", "score": 1.2067890088652962, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.17395648436116945, "1": 0.5092699103248166, "2": 0.2528017174015624, "3": 0.06397188791245155}, "confidence": 0.44529802241236505}, "latency_ms": 2700}, "bankroll_after": 130, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["A♠", "K♣"], "bet": 10, "doubled": false, "total": 21, "soft": true, "bust": false, "blackjack": true, "steps": [], "player_id": "b", "result": "blackjack", "net": 15, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.3639541492057857, "standard": 0.45853490874439773, "max": 0.17751094204981663}, "confidence": 0.1878023631165966}, "bet": 10, "confidence": {"type": "score", "score": 1.7830460880445884, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.11593409029821067, "1": 0.23620551558754246, "2": 0.3967406098856946, "3": 0.25111978422855225}, "confidence": 0.28080651958748404}, "latency_ms": 3523}, "bankroll_after": 25, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 266, "penetration_pct": 15, "cards_left_by_rank": {"A": 17, "2": 20, "3": 18, "4": 23, "5": 22, "6": 21, "7": 20, "8": 20, "9": 21, "10": 20, "J": 23, "Q": 23, "K": 18}, "hi_lo_running_count": -3, "true_count": -0.59}}, {"n": 7, "shoe_before": {"decks": 6, "remaining_cards": 260, "penetration_pct": 17, "cards_left_by_rank": {"A": 16, "2": 19, "3": 17, "4": 23, "5": 22, "6": 21, "7": 20, "8": 19, "9": 21, "10": 18, "J": 23, "Q": 23, "K": 18}, "hi_lo_running_count": -4, "true_count": -0.8}, "dealer_up": "8♠", "dealer_hole": "3♦", "dealer_revealed": true, "dealer": {"cards": ["8♠", "3♦", "7♦"], "total": 18, "soft": false, "blackjack": false, "bust": false}, "events": ["莊家補牌 7♦ → 18 點", "莊家最終 18 點"], "players": [{"cards": ["10♣", "2♣", "K♠"], "bet": 60, "doubled": false, "total": 22, "soft": false, "bust": true, "blackjack": false, "steps": [{"type": "action", "cards": ["10♣", "2♣"], "total": 12, "soft": false, "dealer_up": "8♠", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.6460448859018025, "stand": 0.35395511409819747}, "confidence": 0.29208977180360507}, "risk": {"type": "score", "score": 1.6362591849140533, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.09561823679794927, "1": 0.3051076825522483, "2": 0.4666707395876024, "3": 0.1326033410622001}, "confidence": 0.37105250278965307}, "latency_ms": 3078, "drawn": "K♠", "total_after": 22}], "player_id": "a", "result": "lose", "net": -60, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.6935360880721767, "standard": 0.23786889435416095, "max": 0.06859501757366232}, "confidence": 0.540304132108265}, "bet": 60, "confidence": {"type": "score", "score": 1.2270464612686922, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.18249082291867635, "1": 0.47888782719333944, "2": 0.2677054155885997, "3": 0.07091593429938442}, "confidence": 0.4079718928939551}, "latency_ms": 2817}, "bankroll_after": 70, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["10♠", "A♠"], "bet": 20, "doubled": false, "total": 21, "soft": true, "bust": false, "blackjack": true, "steps": [], "player_id": "b", "result": "blackjack", "net": 30, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.3162061736048218, "standard": 0.5628196491147833, "max": 0.12097417728039492}, "confidence": 0.344229473672175}, "bet": 20, "confidence": {"type": "score", "score": 1.7782719673372593, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.09884107421643187, "1": 0.25211204418752536, "2": 0.42098072163839456, "3": 0.22806615995764823}, "confidence": 0.32213964742196266}, "latency_ms": 3509}, "bankroll_after": 55, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 258, "penetration_pct": 17, "cards_left_by_rank": {"A": 16, "2": 19, "3": 17, "4": 23, "5": 22, "6": 21, "7": 19, "8": 19, "9": 21, "10": 18, "J": 23, "Q": 23, "K": 17}, "hi_lo_running_count": -5, "true_count": -1.01}}, {"n": 8, "shoe_before": {"decks": 6, "remaining_cards": 252, "penetration_pct": 19, "cards_left_by_rank": {"A": 16, "2": 19, "3": 16, "4": 23, "5": 21, "6": 21, "7": 19, "8": 19, "9": 19, "10": 17, "J": 23, "Q": 22, "K": 17}, "hi_lo_running_count": -5, "true_count": -1.03}, "dealer_up": "10♦", "dealer_hole": "9♦", "dealer_revealed": true, "dealer": {"cards": ["10♦", "9♦"], "total": 19, "soft": false, "blackjack": false, "bust": false}, "events": ["莊家明牌為 A 或 10 點,進行暗牌確認", "確認結果:莊家沒有 Blackjack", "莊家最終 19 點"], "players": [{"cards": ["3♥", "Q♦"], "bet": 40, "doubled": false, "total": 13, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["3♥", "Q♦"], "total": 13, "soft": false, "dealer_up": "10♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.4424722777749632, "stand": 0.5575277222250369}, "confidence": 0.11505544445007376}, "risk": {"type": "score", "score": 1.5409442966500433, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.1296583923426123, "1": 0.32512322804031185, "2": 0.41983407024149666, "3": 0.12538430937557934}, "confidence": 0.2901756778988842}, "latency_ms": 3108, "drawn": null, "total_after": 13}], "player_id": "a", "result": "lose", "net": -40, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.7478566569390724, "standard": 0.2078704395438535, "max": 0.044272903517074004}, "confidence": 0.6217849854086086}, "bet": 40, "confidence": {"type": "score", "score": 1.2114043087298945, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.18940221712773983, "1": 0.4777441109615313, "2": 0.26490081796382325, "3": 0.0679528539469056}, "confidence": 0.4097912570146257}, "latency_ms": 2546}, "bankroll_after": 30, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["5♦", "9♣", "3♠"], "bet": 40, "doubled": false, "total": 17, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["5♦", "9♣"], "total": 14, "soft": false, "dealer_up": "10♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.6366657698736238, "stand": 0.3633342301263762}, "confidence": 0.27333153974724755}, "risk": {"type": "score", "score": 1.5978170782853303, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.12817755378927442, "1": 0.30062624017468276, "2": 0.416397779997481, "3": 0.1547984260385618}, "confidence": 0.28822022620820653}, "latency_ms": 3025, "drawn": "3♠", "total_after": 17}, {"type": "action", "cards": ["5♦", "9♣", "3♠"], "total": 17, "soft": false, "dealer_up": "10♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.36328371279258476, "stand": 0.6367162872074151}, "confidence": 0.27343257441483027}, "risk": {"type": "score", "score": 1.2671422123346068, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.23159921542459727, "1": 0.35512809851530586, "2": 0.32780394436098953, "3": 0.08546874169910729}, "confidence": 0.26965935681619857}, "latency_ms": 3556, "drawn": null, "total_after": 17}], "player_id": "b", "result": "lose", "net": -40, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.3279833978847047, "standard": 0.5686003179254366, "max": 0.10341628418985868}, "confidence": 0.35290047688815496}, "bet": 40, "confidence": {"type": "score", "score": 1.7173934276050429, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.11675764826951873, "1": 0.2680269086363328, "2": 0.39627981031373566, "3": 0.2189356327804129}, "confidence": 0.279522162044217}, "latency_ms": 2841}, "bankroll_after": 15, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 251, "penetration_pct": 20, "cards_left_by_rank": {"A": 16, "2": 19, "3": 15, "4": 23, "5": 21, "6": 21, "7": 19, "8": 19, "9": 19, "10": 17, "J": 23, "Q": 22, "K": 17}, "hi_lo_running_count": -4, "true_count": -0.83}}, {"n": 9, "shoe_before": {"decks": 6, "remaining_cards": 245, "penetration_pct": 21, "cards_left_by_rank": {"A": 14, "2": 19, "3": 15, "4": 23, "5": 21, "6": 20, "7": 18, "8": 19, "9": 18, "10": 17, "J": 23, "Q": 22, "K": 16}, "hi_lo_running_count": -6, "true_count": -1.27}, "dealer_up": "K♥", "dealer_hole": "7♣", "dealer_revealed": true, "dealer": {"cards": ["K♥", "7♣"], "total": 17, "soft": false, "blackjack": false, "bust": false}, "events": ["莊家明牌為 A 或 10 點,進行暗牌確認", "確認結果:莊家沒有 Blackjack", "莊家最終 17 點"], "players": [{"cards": ["6♦", "9♦"], "bet": 20, "doubled": false, "total": 15, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["6♦", "9♦"], "total": 15, "soft": false, "dealer_up": "K♥", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.40606821706912394, "stand": 0.5939317829308761}, "confidence": 0.18786356586175224}, "risk": {"type": "score", "score": 1.437199237330251, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.16489995037620225, "1": 0.343417591465515, "2": 0.38126572861011254, "3": 0.11041672954817025}, "confidence": 0.21636577823391023}, "latency_ms": 3109, "drawn": null, "total_after": 15}], "player_id": "a", "result": "lose", "net": -20, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.7689639297520218, "standard": 0.17759980649282997, "max": 0.053436263755148254}, "confidence": 0.6534458946280326}, "bet": 20, "confidence": {"type": "score", "score": 1.2731228885649526, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.16945966343355875, "1": 0.4645385057129478, "2": 0.2894211097084756, "3": 0.07658072114501789}, "confidence": 0.38795778456792984}, "latency_ms": 2842}, "bankroll_after": 10, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["A♥", "A♠", "Q♠", "8♣"], "bet": 10, "doubled": false, "total": 20, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["A♥", "A♠"], "total": 12, "soft": true, "dealer_up": "K♥", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.5659932347237522, "stand": 0.4340067652762479}, "confidence": 0.13198646944750436}, "risk": {"type": "score", "score": 1.4485356769211377, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.1924393543370901, "1": 0.30915360571570805, "2": 0.35583904863617616, "3": 0.14256799131102574}, "confidence": 0.16339969429908596}, "latency_ms": 3556, "drawn": "Q♠", "total_after": 12}, {"type": "action", "cards": ["A♥", "A♠", "Q♠"], "total": 12, "soft": false, "dealer_up": "K♥", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.5899106356195244, "stand": 0.41008936438047566}, "confidence": 0.17982127123904879}, "risk": {"type": "score", "score": 1.5111841800652206, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.1735935846414281, "1": 0.3039357858868495, "2": 0.36016349423679583, "3": 0.16230713523492646}, "confidence": 0.1865699095953678}, "latency_ms": 3376, "drawn": "8♣", "total_after": 20}, {"type": "action", "cards": ["A♥", "A♠", "Q♠", "8♣"], "total": 20, "soft": false, "dealer_up": "K♥", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.2939866919333886, "stand": 0.7060133080666113}, "confidence": 0.41202661613322267}, "risk": {"type": "score", "score": 1.2513615989988762, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.25479942083547175, "1": 0.33969869996162716, "2": 0.30484273857145383, "3": 0.10065914063144715}, "confidence": 0.23903955933018006}, "latency_ms": 3218, "drawn": null, "total_after": 20}], "player_id": "b", "result": "win", "net": 10, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.3413432398179133, "standard": 0.5195029054186392, "max": 0.13915385476344747}, "confidence": 0.2792543581279588}, "bet": 10, "confidence": {"type": "score", "score": 1.8336160013048923, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.09634988931490898, "1": 0.22764980821676772, "2": 0.4220347143168453, "3": 0.253965588151478}, "confidence": 0.3256848250019363}, "latency_ms": 3391}, "bankroll_after": 25, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 243, "penetration_pct": 22, "cards_left_by_rank": {"A": 14, "2": 19, "3": 15, "4": 23, "5": 21, "6": 20, "7": 18, "8": 18, "9": 18, "10": 17, "J": 23, "Q": 21, "K": 16}, "hi_lo_running_count": -7, "true_count": -1.5}}, {"n": 10, "shoe_before": {"decks": 6, "remaining_cards": 237, "penetration_pct": 24, "cards_left_by_rank": {"A": 14, "2": 18, "3": 15, "4": 22, "5": 19, "6": 20, "7": 18, "8": 18, "9": 17, "10": 17, "J": 23, "Q": 21, "K": 15}, "hi_lo_running_count": -4, "true_count": -0.88}, "dealer_up": "4♦", "dealer_hole": "5♣", "dealer_revealed": true, "dealer": {"cards": ["4♦", "5♣", "A♦"], "total": 20, "soft": true, "blackjack": false, "bust": false}, "events": ["莊家補牌 A♦ → 20 點", "莊家最終 20 點", "阿保 資金歸零,破產出局"], "players": [{"cards": ["5♥", "2♦", "3♦", "5♥", "Q♠"], "bet": 10, "doubled": false, "total": 25, "soft": false, "bust": true, "blackjack": false, "steps": [{"type": "action", "cards": ["5♥", "2♦"], "total": 7, "soft": false, "dealer_up": "4♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.8228014784139931, "stand": 0.17719852158600685}, "confidence": 0.6456029568279862}, "risk": {"type": "score", "score": 2.0828155414615814, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.05554269378511206, "1": 0.15559576403255418, "2": 0.43936484911797424, "3": 0.3494966930643596}, "confidence": 0.3838221553328621}, "latency_ms": 3285, "drawn": "3♦", "total_after": 10}, {"type": "action", "cards": ["5♥", "2♦", "3♦"], "total": 10, "soft": false, "dealer_up": "4♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.7921569733596483, "stand": 0.20784302664035162}, "confidence": 0.5843139467192966}, "risk": {"type": "score", "score": 1.6480482983213403, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.0896576388732094, "1": 0.3050623635674854, "2": 0.47285405792406077, "3": 0.1324259396352445}, "confidence": 0.38319641905085133}, "latency_ms": 3331, "drawn": "5♥", "total_after": 15}, {"type": "action", "cards": ["5♥", "2♦", "3♦", "5♥"], "total": 15, "soft": false, "dealer_up": "4♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "hit", "probabilities": {"hit": 0.6126276995310727, "stand": 0.3873723004689274}, "confidence": 0.2252553990621453}, "risk": {"type": "score", "score": 1.4291449486846561, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.12795107433644726, "1": 0.3840434516073026, "2": 0.4189149250913974, "3": 0.06909054896485294}, "confidence": 0.29096385075495}, "latency_ms": 3193, "drawn": "Q♠", "total_after": 25}], "player_id": "a", "result": "lose", "net": -10, "bet_decision": {"decision": {"type": "choice", "choice": "min", "probabilities": {"min": 0.795570618394425, "standard": 0.12263428043646388, "max": 0.08179510116911126}, "confidence": 0.6933559275916374}, "bet": 10, "confidence": {"type": "score", "score": 1.180788745938273, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.2499900936043853, "1": 0.40658705109017834, "2": 0.25606687106821446, "3": 0.08735598423722198}, "confidence": 0.3192310668529563}, "latency_ms": 3191}, "bankroll_after": 0, "id": "a", "name": "阿保", "style": "保守算牌派", "peak_bankroll": 1000}, {"cards": ["K♠", "9♥"], "bet": 20, "doubled": false, "total": 19, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["K♠", "9♥"], "total": 19, "soft": false, "dealer_up": "4♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.2774787174955811, "stand": 0.7225212825044188}, "confidence": 0.44504256500883765}, "risk": {"type": "score", "score": 0.98858174892492, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.3667697022460044, "1": 0.33877008990969004, "2": 0.23356896451768655, "3": 0.06089124332661898}, "confidence": 0.011418251075079988}, "latency_ms": 3675, "drawn": null, "total_after": 19}], "player_id": "b", "result": "lose", "net": -20, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.3162865016372793, "standard": 0.5560665356529187, "max": 0.12764696270980197}, "confidence": 0.3340998034793781}, "bet": 20, "confidence": {"type": "score", "score": 1.7996943182411123, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.10475256570473437, "1": 0.2378297236005541, "2": 0.4103885374435761, "3": 0.24702917325113533}, "confidence": 0.3056359717388418}, "latency_ms": 3009}, "bankroll_after": 5, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 233, "penetration_pct": 25, "cards_left_by_rank": {"A": 13, "2": 18, "3": 14, "4": 22, "5": 18, "6": 20, "7": 18, "8": 18, "9": 17, "10": 17, "J": 23, "Q": 20, "K": 15}, "hi_lo_running_count": -4, "true_count": -0.89}}, {"n": 11, "shoe_before": {"decks": 6, "remaining_cards": 229, "penetration_pct": 27, "cards_left_by_rank": {"A": 13, "2": 17, "3": 13, "4": 22, "5": 18, "6": 20, "7": 18, "8": 18, "9": 17, "10": 17, "J": 23, "Q": 20, "K": 13}, "hi_lo_running_count": -4, "true_count": -0.91}, "dealer_up": "2♦", "dealer_hole": "3♥", "dealer_revealed": true, "dealer": {"cards": ["2♦", "3♥", "J♥", "6♦"], "total": 21, "soft": false, "blackjack": false, "bust": false}, "events": ["阿保 資金為 0,本局無法下注", "莊家補牌 J♥ → 15 點", "莊家補牌 6♦ → 21 點", "莊家最終 21 點", "阿衝 資金歸零,破產出局"], "players": [{"cards": ["K♦", "K♠"], "bet": 5, "doubled": false, "total": 20, "soft": false, "bust": false, "blackjack": false, "steps": [{"type": "action", "cards": ["K♦", "K♠"], "total": 20, "soft": false, "dealer_up": "2♦", "allowed": ["hit", "stand"], "decision": {"type": "choice", "choice": "stand", "probabilities": {"hit": 0.27488101924853414, "stand": 0.7251189807514659}, "confidence": 0.4502379615029317}, "risk": {"type": "score", "score": 0.845673517325465, "legend": {"0": "非常有利", "1": "稍微有利", "2": "有點不利", "3": "非常不利"}, "probabilities": {"0": 0.4556057466582429, "1": 0.2881020785069146, "2": 0.21130508568597703, "3": 0.04498708914886543}, "confidence": 0.15432648267453497}, "latency_ms": 2968, "drawn": null, "total_after": 20}], "player_id": "b", "result": "lose", "net": -5, "bet_decision": {"decision": {"type": "choice", "choice": "standard", "probabilities": {"min": 0.28565455669419343, "standard": 0.5302136995077115, "max": 0.18413174379809508}, "confidence": 0.2953205492615672}, "bet": 5, "confidence": {"type": "score", "score": 1.799522969300636, "legend": {"0": "沒信心", "1": "普通", "2": "有信心", "3": "非常有信心"}, "probabilities": {"0": 0.11793350579907283, "1": 0.2275882904104801, "2": 0.3914999324811851, "3": 0.26297827130926194}, "confidence": 0.2735664266821123}, "latency_ms": 2672}, "bankroll_after": 0, "id": "b", "name": "阿衝", "style": "激進壓迫派", "peak_bankroll": 1000}], "shoe_after": {"decks": 6, "remaining_cards": 227, "penetration_pct": 27, "cards_left_by_rank": {"A": 13, "2": 17, "3": 13, "4": 22, "5": 18, "6": 19, "7": 18, "8": 18, "9": 17, "10": 17, "J": 22, "Q": 20, "K": 13}, "hi_lo_running_count": -4, "true_count": -0.92}}]};
const SUIT_RED = new Set(["♥","♦"]);
const LABEL = {hit:"補牌", stand:"停牌", double:"加倍", min:"小注", standard:"大注", max:"全押"};
const VALUE = {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 RESULT_LABEL = {win:"勝", lose:"負", push:"和局", blackjack:"Blackjack"};
const $ = (s,el=document)=>el.querySelector(s);
let DATA=null, roundIdx=0, autoTimer=null, dealerOpen=true;
function money(n){ return (n>0?"+":"") + n.toLocaleString("en-US"); }
function cls(n){ return n>0?"pos":(n<0?"neg":"neutral"); }
function pct(x){ return (Math.max(0,Math.min(1,x||0))*100).toFixed(0)+"%"; }
function cardHTML(card, hidden){
if(hidden) return '<div class="card down">?</div>';
const r=card.slice(0,-1), s=card.slice(-1);
return '<div class="card '+(SUIT_RED.has(s)?"red":"")+'"><span class="r">'+r+'</span><span class="s">'+s+'</span></div>';
}
function cardsHTML(cards, opts={}){
return '<div class="cards">'+cards.map((c,i)=>cardHTML(c, opts.hideIndex===i)).join("")+'</div>';
}
function agentColor(id){ return id==="a" ? "var(--green)" : "var(--blue)"; }
function agentMeta(id){ return DATA.meta.agents.find(a=>a.id===id) || {name:id,style:""}; }
function totalsFor(round){
const out={};
for(const p of round.players){
out[p.id]=out[p.id]||{bet:0,net:0};
out[p.id].bet+=p.bet; out[p.id].net+=p.net;
}
return out;
}
function init(){
DATA = window.BATTLE_DATA;
if(!DATA){ $("#app").innerHTML='<div class="err">找不到 battle.json,請用 HTTP 伺服器開啟此頁。</div>'; return; }
document.title = DATA.meta.title + " · d1demo";
$("#app").innerHTML = `
<section class="view active" id="view-battle">
<div class="hero">
<h1>${DATA.meta.title.split(" ")[0]} <em>×</em> d1:21 點決策對戰</h1>
<p>${DATA.meta.subtitle}。每一注、每一次補牌/停牌/加倍,都是模型即時輸出的機率分佈,不是寫死的策略表。</p>
<div class="chips" id="metaChips"></div>
</div>
<div class="scoreboard" id="scoreboard"></div>
<div class="panel table-panel">
<div class="round-toolbar">
<button class="icon-btn" id="prevRound">← 上一局</button>
<div class="round-pills" id="roundPills"></div>
<button class="icon-btn" id="nextRound">下一局 →</button>
<button class="icon-btn" id="playBtn">▶ 自動播放</button>
<span class="round-label" id="roundLabel"></span>
</div>
<div id="roundView"></div>
<div class="arena-note">莊家由固定規則操作(S17),兩個玩家各自連到一個獨立的 llama.cpp server;模型只知道牌面、剩餘牌組成、真數與資金,不知道對手的手牌。</div>
</div>
</section>
<section class="view" id="view-stats"></section>
<section class="view" id="view-code">
<h2 class="section-title">整理程式</h2>
<div class="data-box">
<p>這一頁不是手寫的靜態稿:<b>blackjack.py</b> 負責發牌、呼叫兩個 d1 server 做決策並記錄每一步;<b>build_site.py</b> 把 battle.json 編譯成你現在看到的網站。原始碼同步放在這個 Space 的 <code>source/</code> 目錄。</p>
</div>
<div class="code-tabs" id="codeTabs"></div>
<div class="code-bar">
<span id="codeName">blackjack.py</span>
<a class="btn ghost" id="codeDownload" href="source/blackjack.py" download>下載此檔</a>
<button class="icon-btn" id="copyCode">複製</button>
</div>
<pre class="code"><code id="codeBody">載入中…</code></pre>
</section>
<section class="view" id="view-data">
<h2 class="section-title">原始資料</h2>
<div class="data-box">
<h3>battle.json</h3>
<p>完整對戰紀錄:每一局的牌、注碼、決策機率、信心度、風險評分、延遲、莊家補牌與資金變化。</p>
<p><a class="btn" href="battle.json" download>下載 battle.json</a>
<a class="btn ghost" href="battle.json" target="_blank" rel="noopener">在瀏覽器開啟</a></p>
</div>
<div class="data-box">
<h3>重現方式</h3>
<p>需要 llama.cpp b11786 以上版本、LiquidAI/d1-3B-GGUF(Q8_0)與 Python 3。</p>
<pre class="code"><code id="repro"></code></pre>
</div>
<div class="data-box">
<h3>本次執行摘要</h3>
<pre class="code"><code id="metaJson"></code></pre>
</div>
</section>
<footer style="border:0;padding-bottom:0"></footer>
`;
renderChips();
renderScoreboard();
renderPills();
renderRound();
renderStats();
renderCodeTabs();
bind();
}
function renderChips(){
const m=DATA.meta;
const chips=[
["模型",m.model],["量化",m.quant],["llama.cpp",m.llama_build],
["局數",m.rounds_played],["隨機種子",m.seed],["耗時",m.duration_sec+"s"],
["賽制","打到歸零"],["莊家","S17"],["BJ 賠率","3:2"]
];
$("#metaChips").innerHTML=chips.map(([k,v])=>`<span class="chip">${k} <b>${v}</b></span>`).join("");
}
function renderScoreboard(){
const agents=DATA.meta.agents;
$("#scoreboard").innerHTML=agents.map(a=>{
const profit=a.final_bankroll-DATA.meta.start_bankroll;
const color=agentColor(a.id);
const status=a.bust_round?`第 ${a.bust_round} 局破產`:`出賽 ${a.rounds_played} 局`;
return `<div class="score" style="--accent:${color}">
<div class="id">PLAYER ${a.id.toUpperCase()}</div>
<div class="name">${a.name}</div>
<div class="style">${a.style}</div>
<div class="bank" style="color:${color}">${a.final_bankroll.toLocaleString("en-US")}</div>
<div class="sub">起始 ${DATA.meta.start_bankroll} · 峰值 ${a.peak_bankroll} · 損益 <span class="big-delta ${cls(profit)}">${money(profit)}</span></div>
<div class="sub">${status} · ${a.bust_round?"資金歸零,破產出局":"仍在牌桌上"}</div>
</div>`;
}).join("");
}
function roundTone(r){
const res=r.players.map(p=>p.result);
const wins=res.filter(x=>x==="win"||x==="blackjack").length;
const loses=res.filter(x=>x==="lose").length;
if(wins===res.length) return "win";
if(loses===res.length) return "lose";
if(res.every(x=>x==="push")) return "push";
return "mixed";
}
function renderPills(){
$("#roundPills").innerHTML=DATA.rounds.map((r,i)=>{
return `<button class="pill ${roundTone(r)} ${i===roundIdx?"active":""}" data-round="${i}" title="第 ${r.n} 局">
<span class="dot"></span>${r.n}</button>`;
}).join("");
$("#roundPills").querySelectorAll(".pill").forEach(b=>b.onclick=()=>{stopAuto();roundIdx=+b.dataset.round;dealerOpen=true;renderRound();renderPills();});
}
function stepHTML(step, i){
const d=step.decision||{};
const probs=d.probabilities||{};
const keys=Object.keys(probs).sort((a,b)=>probs[b]-probs[a]);
const risk=step.risk;
const riskText=risk ? `風險評分 <b>${risk.score.toFixed(2)}</b>/3 · 信心 ${pct(risk.confidence)}` : "";
const drawn=step.drawn ? `補到 <b>${step.drawn}</b> → <b>${step.total_after}</b> 點` : (d.choice==="stand"?"維持 <b>"+step.total+"</b> 點":`→ <b>${step.total_after}</b> 點`);
return `<li class="step">
<div class="step-head">
<span class="step-no">#${i+1}</span>
<span class="mini">${step.cards.join(" ")}</span>
<span class="mini" style="color:var(--muted)">(${step.total}${step.soft?" 軟":""})</span>
<span class="action ${LABEL[d.choice]?d.choice:"unknown"}">${LABEL[d.choice]||d.choice||"?"}</span>
<span class="meta">信心 ${pct(d.confidence)} · ${((step.latency_ms||0)/1000).toFixed(2)}s</span>
</div>
<div class="probs">
${keys.map(k=>`<span>${LABEL[k]||k}</span><span class="bar"><i style="--c:var(--${k==="hit"?"hit":k==="stand"?"stand":"double"});width:${(probs[k]*100).toFixed(1)}%"></i></span><span class="pct">${pct(probs[k])}</span>`).join("")}
</div>
<div class="step-foot"><span>${drawn}</span>${riskText?`<span>${riskText}</span>`:""}${d.fallback?`<span style="color:var(--red)">伺服器無回應,改用規則備援</span>`:""}</div>
</li>`;
}
function playerHTML(p){
const steps=(p.steps||[]).map(stepHTML).join("");
const bd=p.bet_decision||{};
const betDecision=bd.decision||{};
const betProbs=betDecision.probabilities||{};
const betLabel=LABEL[betDecision.choice]||betDecision.choice||"";
return `<article class="player ${p.result} ${p.blackjack?"blackjack":""}" style="--accent:${agentColor(p.id)}">
<div class="p-head">
<div><h3>${p.name}</h3><div class="style">${p.style||""}</div></div>
<div class="bet">注碼 <b>${p.bet}</b>${p.doubled?" · 已加倍":""}</div>
</div>
<div class="hand">
${cardsHTML(p.cards)}
<div class="total">${p.total}<small>${p.bust?"BUST":p.soft?"軟牌":p.blackjack?"BJ":"硬牌"}</small></div>
<span class="result ${p.result}">${RESULT_LABEL[p.result]||p.result}</span>
</div>
<div class="bet-line">
<span class="tag">注碼決策 ${betLabel}</span>
${p.rebuy?`<span class="tag" style="color:var(--amber)">補充籌碼 +${p.rebuy}</span>`:""}
<span>信心 ${pct(betDecision.confidence)}</span>
${Object.keys(betProbs).length?`<span>${Object.keys(betProbs).sort((a,b)=>betProbs[b]-betProbs[a]).map(k=>`${LABEL[k]||k} ${pct(betProbs[k])}`).join(" · ")}</span>`:""}
</div>
<details><summary>看模型怎麼決定注碼</summary>
<div class="body">狀態包含資金、剩余牌、Hi-Lo 真數與規則;模型在 min / standard / max 三種注碼中挑一個,並另外給出信心評分。</div>
</details>
${steps?`<ol class="steps">${steps}</ol>`:`<div class="step-foot" style="margin-top:12px">莊家直接 Blackjack,玩家沒有動作機會。</div>`}
<div class="net"><span>本局結果</span><span class="amt ${cls(p.net)}">${money(p.net)} <small style="font-size:12px;color:var(--muted)">→ ${p.bankroll_after}</small></span></div>
</article>`;
}
function brokeHTML(a){
const m=agentMeta(a.id);
const round=a.bust_round?`第 ${a.bust_round} 局`:"本局";
return `<article class="player lose" style="--accent:${agentColor(a.id)}">
<div class="p-head">
<div><h3>${a.name}</h3><div class="style">${a.style||""}</div></div>
<div class="bet">注碼 <b>0</b></div>
</div>
<div class="hand">
<div class="total">0<small>BROKE</small></div>
<span class="result lose">已破產</span>
</div>
<div class="step-foot" style="margin-top:12px">${round}資金歸零,之後無法再下注;只能在桌邊看對手打完。</div>
<div class="net"><span>本局結果</span><span class="amt neutral">—</span></div>
</article>`;
}
function renderRound(){
const r=DATA.rounds[roundIdx];
if(!r){ return; }
const d=r.dealer||{};
const hideHole=!dealerOpen && r.dealer_revealed;
const dealerCards=(d.cards||[]).map((c,i)=>cardHTML(c, hideHole && i===1)).join("");
const totals=totalsFor(r);
const pillTone=roundTone(r);
$("#roundLabel").textContent=`第 ${r.n} / ${DATA.rounds.length} 局 · 剩餘 ${r.shoe_before.remaining_cards} 張 · 真數 ${r.shoe_before.true_count}`;
$("#roundView").innerHTML=`
<div class="round-head">
<h2>第 ${r.n} 局</h2>
<span class="shoe">洗牌前剩餘 ${r.shoe_before.remaining_cards} 張(穿透率 ${r.shoe_before.penetration_pct}%) · Hi-Lo 真數 ${r.shoe_before.true_count}</span>
</div>
<div class="dealer">
<div>
<div class="label">DEALER · 莊家</div>
<div class="total-badge">${hideHole?"?":d.total}${d.bust?' <span style="font-size:12px;color:var(--red)">BUST</span>':""}${d.blackjack?' <span style="font-size:12px;color:var(--gold)">BJ</span>':""}</div>
</div>
<div class="cards">${dealerCards}</div>
<div class="note">${r.events.join(" · ")||"莊家依規則補牌"}</div>
${r.dealer_revealed?`<button class="icon-btn" id="revealBtn">${dealerOpen?"蓋回暗牌":"揭開暗牌"}</button>`:""}
</div>
<div class="players">${DATA.meta.agents.map(a=>{
const p=r.players.find(x=>x.id===a.id);
return p?playerHTML(p):brokeHTML(a);
}).join("")}</div>
<div class="events"><b>牌桌事件</b><ul>${r.events.map(e=>`<li>${e}</li>`).join("")}</ul></div>
`;
const rb=$("#revealBtn");
if(rb) rb.onclick=()=>{dealerOpen=!dealerOpen;renderRound();};
$("#prevRound").disabled=roundIdx===0;
$("#nextRound").disabled=roundIdx===DATA.rounds.length-1;
document.querySelectorAll(".pill").forEach((b,i)=>b.classList.toggle("active",i===roundIdx));
}
function bind(){
$("#prevRound").onclick=()=>{stopAuto(); if(roundIdx>0){roundIdx--;dealerOpen=true;renderRound();}};
$("#nextRound").onclick=()=>{stopAuto(); if(roundIdx<DATA.rounds.length-1){roundIdx++;dealerOpen=true;renderRound();}};
$("#playBtn").onclick=()=>{
if(autoTimer){stopAuto();return;}
roundIdx=0;dealerOpen=true;renderRound();
$("#playBtn").textContent="⏸ 暫停";
autoTimer=setInterval(()=>{
if(roundIdx>=DATA.rounds.length-1){stopAuto();return;}
roundIdx++;dealerOpen=true;renderRound();
},4000);
};
document.querySelectorAll("nav button").forEach(b=>{
b.onclick=()=>{
document.querySelectorAll("nav button").forEach(x=>x.classList.toggle("active",x===b));
document.querySelectorAll(".view").forEach(v=>v.classList.toggle("active",v.id==="view-"+b.dataset.view));
window.scrollTo({top:0,behavior:"smooth"});
};
});
}
function stopAuto(){ if(autoTimer){clearInterval(autoTimer);autoTimer=null;} const b=$("#playBtn"); if(b)b.textContent="▶ 自動播放"; }
function statsFor(id){
const s={rounds:0,win:0,lose:0,push:0,bj:0,bust:0,double:0,net:0,wagered:0,steps:0,conf:0,lat:0,actions:{hit:0,stand:0,double:0},firstActions:[]};
for(const r of DATA.rounds){
const p=r.players.find(x=>x.id===id); if(!p) continue;
s.rounds++;
if(p.result==="win")s.win++;
else if(p.result==="lose")s.lose++;
else if(p.result==="push")s.push++;
else if(p.result==="blackjack")s.win++;
if(p.blackjack)s.bj++;
if(p.bust)s.bust++;
if(p.doubled)s.double++;
s.net+=p.net; s.wagered+=p.bet;
(p.steps||[]).forEach(st=>{
s.steps++; s.conf+=st.decision.confidence||0; s.lat+=st.latency_ms||0;
s.actions[st.decision.choice]=(s.actions[st.decision.choice]||0)+1;
});
const p1=(p.steps||[])[0];
if(p1) s.firstActions.push({round:r.n,action:p1.decision.choice});
}
s.avgConf=s.steps?s.conf/s.steps:0;
s.avgLat=s.steps?s.lat/s.steps:0;
return s;
}
function renderStats(){
const a=statsFor("a"), b=statsFor("b");
const agree=a.firstActions.filter(x=>{
const y=b.firstActions.find(z=>z.round===x.round);
return y && y.action===x.action;
}).length;
const compared=a.firstActions.filter(x=>b.firstActions.some(z=>z.round===x.round)).length;
const dealerBust=DATA.rounds.filter(r=>r.dealer&&r.dealer.bust).length;
const dealerBj=DATA.rounds.filter(r=>r.dealer&&r.dealer.blackjack).length;
const dealerAvg=DATA.rounds.reduce((sum,r)=>sum+((r.dealer||{}).total||0),0)/Math.max(DATA.rounds.length,1);
const totalNet=a.net+b.net;
const houseEdge=totalNet;
function statCard(id,s){
const m=agentMeta(id);
const totalActions=Math.max(s.steps,1);
return `<div class="stat-card" style="--accent:${agentColor(id)}">
<h3>${m.name}</h3><div class="style">${m.style}</div>
<div class="kv">
<div><div class="k">損益</div><div class="v ${cls(s.net)}">${money(s.net)}</div></div>
<div><div class="k">勝 / 負 / 和</div><div class="v">${s.win} / ${s.lose} / ${s.push}</div></div>
<div><div class="k">Blackjack</div><div class="v">${s.bj}</div></div>
<div><div class="k">爆牌</div><div class="v">${s.bust}</div></div>
<div><div class="k">加倍次數</div><div class="v">${s.double}</div></div>
<div><div class="k">總下注</div><div class="v">${s.wagered}</div></div>
<div><div class="k">出賽局數</div><div class="v">${m.rounds_played}</div></div>
<div><div class="k">峰值資金</div><div class="v">${m.peak_bankroll}</div></div>
<div><div class="k">破產局數</div><div class="v">${m.bust_round?"第 "+m.bust_round+" 局":"未破產"}</div></div>
<div><div class="k">平均信心</div><div class="v">${pct(s.avgConf)}</div></div>
<div><div class="k">平均推理</div><div class="v">${(s.avgLat/1000).toFixed(2)}s</div></div>
</div>
<div class="dist">
<div class="mini-title">動作分佈(${s.steps} 次)</div>
${["hit","stand","double"].map(k=>`<div class="row"><span>${LABEL[k]}</span><span class="bar"><i style="--c:var(--${k});width:${(100*s.actions[k]/totalActions).toFixed(1)}%"></i></span><span class="pct">${s.actions[k]}</span></div>`).join("")}
</div>
</div>`;
}
const chart=bankrollChart();
$("#view-stats").innerHTML=`
<h2 class="section-title">決策統計</h2>
<div class="stats-grid">${statCard("a",a)}${statCard("b",b)}</div>
<div class="chart-wrap">
<div class="chart-legend">
<span><i style="background:var(--green)"></i>${agentMeta("a").name}</span>
<span><i style="background:var(--blue)"></i>${agentMeta("b").name}</span>
<span>起始資金 ${DATA.meta.start_bankroll}</span>
</div>
${chart}
</div>
<div class="h2h">
<h3 style="margin:0 0 6px">兩個 d1 有多像?</h3>
<p style="color:var(--muted);margin:4px 0 10px">比較每一局雙方第一次動作(同一副牌、同一個莊家明牌、相同的剩餘牌資訊)。</p>
<div class="big">${compared?Math.round(100*agree/compared):0}%</div>
<div style="color:var(--muted);font-size:13px">首個動作一致率 · 共比較 ${compared} 局</div>
<div class="kv" style="grid-template-columns:repeat(3,1fr);margin-top:16px">
<div><div class="k">莊家爆牌率</div><div class="v">${Math.round(100*dealerBust/DATA.rounds.length)}%</div></div>
<div><div class="k">莊家 Blackjack</div><div class="v">${dealerBj} 局</div></div>
<div><div class="k">莊家平均點數</div><div class="v">${dealerAvg.toFixed(1)}</div></div>
</div>
</div>
<div class="data-box" style="margin-top:14px">
<h3>終局:牌桌關閉</h3>
<p style="font-size:15px">${DATA.meta.end_reason==="both_broke"?"兩邊資金都歸零,遊戲在第 "+DATA.meta.rounds_played+" 局結束。": "達到局數上限,遊戲結束。"}
${DATA.meta.agents.map(a=>`${a.name} 在第 ${a.bust_round||"—"} 局破產(峰值 ${a.peak_bankroll})`).join(" · ")}</p>
</div>
<div class="data-box" style="margin-top:14px">
<h3>兩個模型合計對抗莊家</h3>
<p style="font-size:15px">總損益 <b class="${cls(totalNet)}" style="font-size:24px">${money(totalNet)}</b> · 每位玩家起始 ${DATA.meta.start_bankroll},合計起始 ${DATA.meta.start_bankroll*2}</p>
</div>
`;
}
function bankrollChart(){
const W=760,H=230,pad={l:52,r:14,t:14,b:26};
const agents=DATA.meta.agents;
const series=agents.map(a=>{
const vals=[DATA.meta.start_bankroll];
for(const r of DATA.rounds){
const p=r.players.find(x=>x.id===a.id);
vals.push(p?p.bankroll_after:vals[vals.length-1]);
}
return {a,vals,color:a.id==="a"?"#39d98a":"#5cc8ff"};
});
const all=series.flatMap(s=>s.vals);
let min=Math.min(...all),max=Math.max(...all);
const span=Math.max(max-min,100);
min=Math.max(0,Math.floor((min-span*0.1)/50)*50); max=Math.ceil((max+span*0.1)/50)*50;
const X=i=>pad.l+(W-pad.l-pad.r)*(i/(DATA.rounds.length||1));
const Y=v=>H-pad.b-(H-pad.t-pad.b)*((v-min)/(max-min||1));
const step=Math.max(50,Math.ceil((max-min)/4/50)*50);
min=Math.floor(min/step)*step; max=Math.ceil(max/step)*step;
const ticks=Math.round((max-min)/step);
let grid="";
for(let i=0;i<=ticks;i++){
const v=min+(max-min)*i/ticks, y=Y(v);
grid+=`<line x1="${pad.l}" y1="${y}" x2="${W-pad.r}" y2="${y}" stroke="rgba(255,255,255,.07)"/>`;
grid+=`<text x="${pad.l-8}" y="${y+4}" fill="#8fae9f" font-size="10" text-anchor="end">${Math.round(v)}</text>`;
}
const paths=series.map(s=>{
const d=s.vals.map((v,i)=>`${i?"L":"M"}${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(" ");
const dots=s.vals.map((v,i)=>`<circle cx="${X(i)}" cy="${Y(v)}" r="2.6" fill="${s.color}"><title>第 ${i} 局後:${v}</title></circle>`).join("");
return `<path d="${d}" fill="none" stroke="${s.color}" stroke-width="2.4" stroke-linejoin="round"/>${dots}`;
}).join("");
const stepX=Math.max(1,Math.ceil(DATA.rounds.length/8));
let xlabels="";
for(let i=0;i<=DATA.rounds.length;i+=stepX){
xlabels+=`<text x="${X(i)}" y="${H-6}" fill="#8fae9f" font-size="10" text-anchor="middle">${i}</text>`;
}
return `<svg viewBox="0 0 ${W} ${H}" width="100%" role="img" aria-label="資金曲線">${grid}${paths}${xlabels}</svg>`;
}
const CODE_FILES=[
{file:"blackjack.py",label:"blackjack.py · 對戰與記錄"},
{file:"build_site.py",label:"build_site.py · 網頁編譯"},
{file:"run_demo.sh",label:"run_demo.sh · 一鍵執行"},
];
function renderCodeTabs(){
$("#codeTabs").innerHTML=CODE_FILES.map((f,i)=>`<button data-file="${f.file}" class="${i===0?"active":""}">${f.label}</button>`).join("");
$("#codeTabs").querySelectorAll("button").forEach(b=>b.onclick=()=>{
$("#codeTabs").querySelectorAll("button").forEach(x=>x.classList.toggle("active",x===b));
loadCode(b.dataset.file);
});
loadCode("blackjack.py");
$("#copyCode").onclick=async()=>{
try{await navigator.clipboard.writeText($("#codeBody").textContent);$("#copyCode").textContent="已複製";setTimeout(()=>$("#copyCode").textContent="複製",1200);}catch(e){}
};
$("#repro").textContent=`# 1) 下載模型與 llama.cpp(CPU 版)
llama-server -m d1-3B-Q8_0.gguf --port 8081 -c 2048 -np 1 --no-webui &
llama-server -m d1-3B-Q8_0.gguf --port 8082 -c 2048 -np 1 --no-webui &
# 2) 讓兩個 d1 打 12 局,並把每一步寫成 JSON
python3 blackjack.py --rounds 12 --seed ${DATA.meta.seed} --out battle.json
# 3) 把 JSON 編譯成這個靜態網站
python3 build_site.py --battle battle.json --out site
# 4) 上傳 static Space
python3 - <<'PY'
from huggingface_hub import HfApi
HfApi().upload_folder(folder_path="site", repo_id="HelloSun/d1demo", repo_type="space")
PY`;
$("#metaJson").textContent=JSON.stringify(DATA.meta,null,2);
}
function loadCode(file){
$("#codeName").textContent=file;
$("#codeDownload").href="source/"+file;
$("#codeBody").textContent="載入中…";
fetch("source/"+file).then(r=>{if(!r.ok)throw new Error(r.status);return r.text();})
.then(t=>{$("#codeBody").textContent=t;})
.catch(()=>{$("#codeBody").textContent="(無法載入 "+file+",請確認 source/ 目錄存在)";});
}
init();
</script>
</body>
</html>