271 lines
12 KiB
HTML
271 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Blackjack Theory Trainer</title>
|
|
<style>
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
body{background:#2b2b2b;color:#fff;font-family:Arial,sans-serif;display:flex;justify-content:center;min-height:100vh;padding:20px}
|
|
#app{width:100%;max-width:600px;display:flex;flex-direction:column;align-items:center}
|
|
#header{width:100%;display:flex;justify-content:flex-end;padding:8px 10px;min-height:36px}
|
|
#streak{color:#FFD700;font-size:18px;font-weight:700;display:none}
|
|
.section-label{color:#ccc;font-size:14px;margin-bottom:4px;text-align:center}
|
|
.card-row{display:flex;justify-content:center;gap:10px;min-height:140px}
|
|
.card-row img{width:125px;height:125px;border-radius:8px;object-fit:contain}
|
|
#dealer-section,#player-section{margin-bottom:8px}
|
|
#table{background:#333;border-radius:12px;padding:24px 20px;width:100%;display:flex;flex-direction:column;align-items:center}
|
|
#actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%;max-width:340px;margin:16px 0 8px}
|
|
.action-btn{padding:12px;font-size:20px;border:none;border-radius:8px;cursor:pointer;background:#fff;color:#000;font-weight:700;transition:background .15s,color .15s}
|
|
.action-btn:disabled{background:#555;color:#999;cursor:not-allowed}
|
|
#btn-hint{background:#fff;color:#000;font-size:20px;border:none;border-radius:8px;padding:12px 40px;cursor:pointer;margin:8px 0;font-weight:700}
|
|
#btn-hint:disabled{background:#555;color:#999;cursor:not-allowed}
|
|
#feedback{font-size:20px;text-align:center;min-height:60px;padding:10px;width:100%;max-width:550px;line-height:1.4;word-wrap:break-word}
|
|
#btn-next{background:#fff;color:#000;font-size:20px;border:none;border-radius:8px;padding:12px 40px;cursor:pointer;margin:8px 0;font-weight:700}
|
|
#btn-next:disabled{background:#555;color:#999;cursor:not-allowed}
|
|
#toast{position:fixed;top:20px;right:20px;background:#3a7ebf;color:#fff;padding:12px 24px;border-radius:8px;font-size:16px;font-weight:700;opacity:0;transform:translateY(-20px);transition:opacity .3s,transform .3s;pointer-events:none;z-index:1000}
|
|
#toast.show{opacity:1;transform:translateY(0)}
|
|
#lightbox{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);justify-content:center;align-items:center;z-index:999;cursor:pointer}
|
|
#lightbox.open{display:flex}
|
|
#lightbox img{max-width:90vw;max-height:90vh;border-radius:8px}
|
|
#start-screen{display:flex;justify-content:center;align-items:center;min-height:80vh;width:100%}
|
|
#start-content{text-align:center;max-width:500px}
|
|
#start-content h1{font-size:32px;margin-bottom:24px;color:#fff}
|
|
#start-content p{font-size:16px;line-height:1.6;color:#ccc;margin-bottom:32px}
|
|
#btn-start{background:#2aa44f;color:#fff;font-size:22px;border:none;border-radius:8px;padding:14px 48px;cursor:pointer;font-weight:700}
|
|
#btn-start:hover{background:#238c44}
|
|
#game-screen{width:100%}
|
|
#stats-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);justify-content:center;align-items:center;z-index:999}
|
|
#stats-modal.open{display:flex}
|
|
#stats-content{background:#2b2b2b;padding:32px;border-radius:12px;max-width:450px;width:90%;text-align:center}
|
|
#stats-content h2{font-size:24px;margin-bottom:20px;color:#fff}
|
|
#stats-data{text-align:left;margin-bottom:24px}
|
|
.stat-row{display:flex;justify-content:space-between;padding:6px 0;font-size:16px;color:#ccc;border-bottom:1px solid #444}
|
|
.stat-row:last-child{border-bottom:none}
|
|
.stat-label{color:#aaa}
|
|
.stat-value{color:#fff;font-weight:700}
|
|
.scenario-header{font-size:16px;font-weight:700;color:#fff;margin-top:16px;margin-bottom:8px}
|
|
#btn-back-to-start{background:#2aa44f;color:#fff;font-size:18px;border:none;border-radius:8px;padding:12px 36px;cursor:pointer;font-weight:700;margin-top:8px}
|
|
#btn-back-to-start:hover{background:#238c44}
|
|
#btn-exit{background:#555;color:#ccc;font-size:16px;border:none;border-radius:6px;padding:8px 24px;cursor:pointer;font-weight:400;margin-top:4px}
|
|
#btn-exit:hover{background:#666;color:#fff}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<div id="start-screen">
|
|
<div id="start-content">
|
|
<h1>🃏 Blackjack Theory Trainer</h1>
|
|
<p>This is a blackjack basic strategy trainer and not a real blackjack game.</p>
|
|
<p>Each round you're dealt a hand and asked to make the correct play (Hit, Stand, Double, or Split) according to basic strategy. You get immediate feedback and track your accuracy over time.</p>
|
|
<p>Have Fun!</p>
|
|
<button id="btn-start">Start Training</button>
|
|
</div>
|
|
</div>
|
|
<div id="game-screen">
|
|
<div id="header">
|
|
<span id="streak"></span>
|
|
</div>
|
|
<div id="table">
|
|
<div id="dealer-section">
|
|
<div class="section-label">Dealer</div>
|
|
<div class="card-row" id="dealer-cards"></div>
|
|
</div>
|
|
<div id="player-section">
|
|
<div class="section-label">Your Hand</div>
|
|
<div class="card-row" id="player-cards"></div>
|
|
</div>
|
|
<div id="actions">
|
|
<button class="action-btn" id="btn-hit">Hit</button>
|
|
<button class="action-btn" id="btn-stand">Stand</button>
|
|
<button class="action-btn" id="btn-double">Double</button>
|
|
<button class="action-btn" id="btn-split">Split</button>
|
|
</div>
|
|
<button id="btn-hint">Hint</button>
|
|
<div id="feedback"></div>
|
|
<button id="btn-next" disabled>Next Round</button>
|
|
<button id="btn-exit">Exit</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="stats-modal">
|
|
<div id="stats-content">
|
|
<h2>Your Statistics</h2>
|
|
<div id="stats-data"></div>
|
|
<button id="btn-back-to-start">Back to Start</button>
|
|
</div>
|
|
</div>
|
|
<div id="toast"></div>
|
|
<div id="lightbox">
|
|
<img src="/images/hint.jpg" alt="Strategy hint">
|
|
</div>
|
|
|
|
<script>
|
|
const COOKIE = 'session_id';
|
|
const btnHit=document.getElementById('btn-hit');
|
|
const btnStand=document.getElementById('btn-stand');
|
|
const btnDouble=document.getElementById('btn-double');
|
|
const btnSplit=document.getElementById('btn-split');
|
|
const btnHint=document.getElementById('btn-hint');
|
|
const btnNext=document.getElementById('btn-next');
|
|
const dealerCards=document.getElementById('dealer-cards');
|
|
const playerCards=document.getElementById('player-cards');
|
|
const feedback=document.getElementById('feedback');
|
|
const streak=document.getElementById('streak');
|
|
const toast=document.getElementById('toast');
|
|
const lightbox=document.getElementById('lightbox');
|
|
const actionBtns=[btnHit,btnStand,btnDouble,btnSplit];
|
|
|
|
function cardUrl(name){return '/images/cards/'+name+'.png'}
|
|
|
|
function setStreak(n){
|
|
if(n>=3){streak.style.display='inline';streak.textContent='\uD83D\uDD25 '+n}
|
|
else{streak.style.display='none'}
|
|
}
|
|
|
|
function showToast(msg){
|
|
toast.textContent=msg;toast.classList.add('show');
|
|
setTimeout(()=>toast.classList.remove('show'),1500);
|
|
}
|
|
|
|
function enableActions(enabled){
|
|
const state=enabled?'normal':'disabled';
|
|
actionBtns.forEach(b=>b.disabled=!enabled);
|
|
}
|
|
|
|
function resetButtonColors(){
|
|
actionBtns.forEach(b=>{b.style.background='#fff';b.style.color='#000'});
|
|
}
|
|
|
|
function markButton(btn,correct){
|
|
btn.style.background=correct?'#2aa44f':'#e74c3c';
|
|
btn.style.color='#fff';
|
|
}
|
|
|
|
function renderCards(dc,hc){
|
|
dealerCards.innerHTML='';
|
|
playerCards.innerHTML='';
|
|
var d=document.createElement('img');
|
|
d.src=cardUrl(dc);d.alt='Dealer card';
|
|
dealerCards.appendChild(d);
|
|
hc.forEach(function(c){
|
|
var i=document.createElement('img');
|
|
i.src=cardUrl(c);i.alt='Player card';
|
|
playerCards.appendChild(i);
|
|
});
|
|
}
|
|
|
|
async function startRound(){
|
|
try{
|
|
var r=await fetch('/api/start-round',{method:'POST'});
|
|
var d=await r.json();
|
|
renderCards(d.dealer_card,d.player_hand);
|
|
feedback.textContent='';
|
|
setStreak(d.current_streak);
|
|
btnHint.disabled=false;
|
|
btnNext.disabled=true;
|
|
enableActions(true);
|
|
resetButtonColors();
|
|
if(d.has_blackjack){
|
|
feedback.textContent='Blackjack! You win or get a push!';
|
|
feedback.style.color='green';
|
|
enableActions(false);
|
|
btnNext.disabled=false;
|
|
}
|
|
if(d.can_split){btnSplit.disabled=false}
|
|
else{btnSplit.disabled=true}
|
|
}catch(e){feedback.textContent='Error connecting to server';feedback.style.color='red'}
|
|
}
|
|
|
|
async function checkDecision(action,btn){
|
|
try{
|
|
var r=await fetch('/api/check-decision',{
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body:JSON.stringify({action:action})
|
|
});
|
|
if(!r.ok){feedback.textContent='Session expired. Starting new round...';feedback.style.color='red';startRound();return}
|
|
var d=await r.json();
|
|
feedback.textContent=d.feedback;
|
|
feedback.style.color=d.correct?'green':'red';
|
|
markButton(btn,d.correct);
|
|
enableActions(false);
|
|
btnNext.disabled=false;
|
|
setStreak(d.stats.current_streak);
|
|
if(d.streak_ended){showToast(d.streak_ended)}
|
|
}catch(e){feedback.textContent='Error checking decision';feedback.style.color='red'}
|
|
}
|
|
|
|
btnHit.addEventListener('click',function(){checkDecision('Hit',btnHit)});
|
|
btnStand.addEventListener('click',function(){checkDecision('Stand',btnStand)});
|
|
btnDouble.addEventListener('click',function(){checkDecision('Double',btnDouble)});
|
|
btnSplit.addEventListener('click',function(){checkDecision('Split',btnSplit)});
|
|
btnNext.addEventListener('click',startRound);
|
|
btnHint.addEventListener('click',function(){lightbox.classList.add('open');btnHint.disabled=true});
|
|
lightbox.addEventListener('click',function(){lightbox.classList.remove('open')});
|
|
|
|
// Screen management
|
|
var startScreen=document.getElementById('start-screen');
|
|
var gameScreen=document.getElementById('game-screen');
|
|
var statsModal=document.getElementById('stats-modal');
|
|
var statsData=document.getElementById('stats-data');
|
|
var btnStart=document.getElementById('btn-start');
|
|
var btnExit=document.getElementById('btn-exit');
|
|
var btnBackToStart=document.getElementById('btn-back-to-start');
|
|
|
|
function showStartScreen(){
|
|
startScreen.style.display='flex';
|
|
gameScreen.style.display='none';
|
|
}
|
|
|
|
function showGameScreen(){
|
|
startScreen.style.display='none';
|
|
gameScreen.style.display='block';
|
|
startRound();
|
|
}
|
|
|
|
function deleteCookie(name){
|
|
document.cookie=name+'=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
|
}
|
|
|
|
async function showStats(){
|
|
try{
|
|
var r=await fetch('/api/stats');
|
|
if(!r.ok)return;
|
|
var s=await r.json();
|
|
var total=s.correct_decisions+s.incorrect_decisions;
|
|
var acc=total>0?(s.correct_decisions/total*100).toFixed(1):0;
|
|
var html='';
|
|
html+='<div class="stat-row"><span class="stat-label">Total Rounds</span><span class="stat-value">'+s.total_rounds+'</span></div>';
|
|
html+='<div class="stat-row"><span class="stat-label">Accuracy</span><span class="stat-value">'+acc+'%</span></div>';
|
|
html+='<div class="stat-row"><span class="stat-label">Current Streak</span><span class="stat-value">'+s.current_streak+'</span></div>';
|
|
html+='<div class="stat-row"><span class="stat-label">Longest Streak</span><span class="stat-value">'+s.longest_streak+'</span></div>';
|
|
html+='<div class="scenario-header">Scenario Breakdown</div>';
|
|
for(var key in s.by_scenario){
|
|
var d=s.by_scenario[key];
|
|
var t=d.correct+d.incorrect;
|
|
var a=t>0?(d.correct/t*100).toFixed(1):0;
|
|
var label=key.replace(/_/g,' ').replace(/\b\w/g,function(l){return l.toUpperCase()});
|
|
html+='<div class="stat-row"><span class="stat-label">'+label+'</span><span class="stat-value">'+d.correct+'/'+t+' ('+a+'%)</span></div>';
|
|
}
|
|
statsData.innerHTML=html;
|
|
statsModal.classList.add('open');
|
|
}catch(e){}
|
|
}
|
|
|
|
function backToStart(){
|
|
statsModal.classList.remove('open');
|
|
deleteCookie(COOKIE);
|
|
showStartScreen();
|
|
}
|
|
|
|
btnStart.addEventListener('click',showGameScreen);
|
|
btnExit.addEventListener('click',showStats);
|
|
btnBackToStart.addEventListener('click',backToStart);
|
|
|
|
// Start on the start screen
|
|
showStartScreen();
|
|
</script>
|
|
</body>
|
|
</html>
|