하이로우 포커
🃏 하이로우 포커
7포커 · 하이 / 로우 / 스윙
블라인드 500원 · 시작 천만원
게임 시작
🃏 버릴 카드를 선택하세요!
1장 선택 후 확인
취소
확인
// ══════════════════════════════════════
// 뷰포트
// ══════════════════════════════════════
(function(){
function setSize(){
const w=window.visualViewport?window.visualViewport.width:window.innerWidth;
const h=window.visualViewport?window.visualViewport.height:window.innerHeight;
document.documentElement.style.setProperty('--real-w',w+'px');
document.documentElement.style.setProperty('--real-h',h+'px');
const root=document.getElementById('root');if(!root)return;
const isP=window.innerHeight>window.innerWidth;
if(isP){root.style.width=h+'px';root.style.height=w+'px';root.style.left=w+'px'}
else{root.style.width=w+'px';root.style.height=h+'px';root.style.left='0'}
}
window.addEventListener('load',()=>{setTimeout(()=>window.scrollTo(0,1),300);setSize()});
window.addEventListener('resize',setSize);
window.addEventListener('orientationchange',()=>setTimeout(setSize,300));
if(window.visualViewport){window.visualViewport.addEventListener('resize',setSize);window.visualViewport.addEventListener('scroll',setSize)}
document.addEventListener('DOMContentLoaded',setSize);
})();
// ══════════════════════════════════════
// 레이아웃 — 상대 플레이어 슬롯
// ══════════════════════════════════════
// 시계방향 8자리: tl tm tr mr br bm bl ml
const SEAT_POSITIONS=['tl','tm','tr','mr','bl','bm','br','ml'];
// CSS 클래스 매핑
const SEAT_CLASS={tl:'opp-tl',tm:'opp-tm',tr:'opp-tr',mr:'opp-mr',bl:'opp-bl',bm:'opp-bm',br:'opp-br',ml:'opp-ml'};
// 동적 위치 CSS (JS로 주입)
function injectSeatCSS(){
const s=document.createElement('style');
s.textContent=`
.opp-tl{position:absolute;top:6px;left:6px}
.opp-tm{position:absolute;top:6px;left:50%;transform:translateX(-50%)}
.opp-tr{position:absolute;top:6px;right:6px}
.opp-mr{position:absolute;top:50%;right:6px;transform:translateY(-50%)}
.opp-br{position:absolute;bottom:58px;right:6px}
.opp-bm{position:absolute;bottom:58px;left:50%;transform:translateX(-50%)}
.opp-bl{position:absolute;bottom:58px;left:6px}
.opp-ml{position:absolute;top:50%;left:6px;transform:translateY(-50%)}
`;
document.head.appendChild(s);
}
function buildLayout(){
injectSeatCSS();
const total=G.players.length;
// AI 플레이어 최대 7명 → 7자리
const seats=['tl','tm','tr','mr','br','bl','ml'];
const sm={};
const container=document.getElementById('opp-slots');
container.innerHTML='';
for(let i=1;i{
container.appendChild(makeOppZone(p,seat));
});
// 슬롯 초기화는 약간 뒤에
setTimeout(initCardSlots,0);
}
function makeOppZone(p,seat){
const div=document.createElement('div');
div.className=`opp-zone ${SEAT_CLASS[seat]||'opp-tl'}`;
div.id=`opp-zone-${p.id}`;
div.innerHTML=`
`;
return div;
}
// ══════════════════════════════════════
// 카드 슬롯 초기화
// ══════════════════════════════════════
function initCardSlots(){
// 내 카드
const myEl=document.getElementById('my-cards');
myEl.innerHTML='';
for(let i=0;i<7;i++){
const ph=document.createElement('div');
ph.className='my-slot-ph';ph.id=`my-slot-${i}`;
myEl.appendChild(ph);
}
document.getElementById('my-hand-label').textContent='';
// AI 슬롯
G.players.filter(p=>p.id!==0).forEach(p=>{
const el=document.getElementById(`pcards-${p.id}`);
if(!el)return;
el.innerHTML='';
for(let i=0;i<7;i++){
const ph=document.createElement('div');
ph.className='slot-ph';ph.id=`slot-${p.id}-${i}`;
el.appendChild(ph);
}
});
}
function resetCardSlots(){
const myEl=document.getElementById('my-cards');
myEl.innerHTML='';
for(let i=0;i<7;i++){
const ph=document.createElement('div');ph.className='my-slot-ph';ph.id=`my-slot-${i}`;myEl.appendChild(ph);
}
document.getElementById('my-hand-label').textContent='';
document.getElementById('info-panel').style.display='none';
G.players.filter(p=>p.id!==0).forEach(p=>{
const el=document.getElementById(`pcards-${p.id}`);if(!el)return;
el.innerHTML='';
const inner=document.getElementById(`opp-inner-${p.id}`);
if(inner){inner.classList.remove('boss-hi','folded-dim');}
for(let i=0;i<7;i++){
const ph=document.createElement('div');ph.className='slot-ph';ph.id=`slot-${p.id}-${i}`;el.appendChild(ph);
}
const hel=document.getElementById(`phand-${p.id}`);if(hel)hel.textContent='';
const bel=document.getElementById(`pbet-${p.id}`);if(bel)bel.innerHTML='';
});
}
// ══════════════════════════════════════
// 카드 렌더링 (슬롯 채우기)
// ══════════════════════════════════════
const isRed=s=>s==='♥'||s==='♦';
function cardHTML(c,cls,back){
if(!c||back)return`
`;
const col=isRed(c.suit)?'red':'blk';
if(cls==='tc'){
return``;
}
return``;
}
function renderPlayerCards(p){
if(p.id===0){
for(let i=0;i<7;i++){
const slotEl=document.getElementById(`my-slot-${i}`);if(!slotEl)continue;
const c=p.cards[i];
if(!c){if(slotEl.classList.contains('my-slot-ph'))continue;const ph=document.createElement('div');ph.className='my-slot-ph';ph.id=`my-slot-${i}`;slotEl.replaceWith(ph);continue;}
if(slotEl.classList.contains('card'))continue;
const isHid=p.hidden.includes(c);
const d=document.createElement('div');d.innerHTML=cardHTML(c,'myc',false);
const card=d.firstChild;if(isHid)card.classList.add('hidden-mine');
card.classList.add('new-card');card.id=`my-slot-${i}`;slotEl.replaceWith(card);
}
if(p.highHand){
document.getElementById('my-hand-label').textContent=`하이:${hiName(p.highHand)} / 로우:${loName(p.lowHand)}`;
}
updateInfoPanel(p);return;
}
const el=document.getElementById(`pcards-${p.id}`);if(!el)return;
const inner=document.getElementById(`opp-inner-${p.id}`);
if(p.folded){if(inner)inner.classList.add('folded-dim');return;}
if(inner)inner.classList.remove('folded-dim');
for(let i=0;i<7;i++){
const slotEl=document.getElementById(`slot-${p.id}-${i}`);if(!slotEl)continue;
const c=p.cards[i];
if(!c)continue;
if(slotEl.classList.contains('card')||slotEl.classList.contains('pmc'))continue;
const isOpen=p.open.includes(c);
let newEl;
if(isOpen){const d=document.createElement('div');d.innerHTML=cardHTML(c,'tc',false);newEl=d.firstChild;}
else{newEl=document.createElement('div');newEl.className='pmc';}
newEl.classList.add('new-card');newEl.id=`slot-${p.id}-${i}`;slotEl.replaceWith(newEl);
}
const hel=document.getElementById(`phand-${p.id}`);if(hel&&p.highHand)hel.textContent=hiName(p.highHand);
}
// ══════════════════════════════════════
// HUD
// ══════════════════════════════════════
function updateHUD(){
const p0=G.players[0];
document.getElementById('my-chips').textContent=fmt(p0.chips);
document.getElementById('hd-pot').textContent=fmt(G.pot);
document.getElementById('felt-pot').textContent=fmt(G.pot);
document.getElementById('hd-stage').textContent={deal:'딜',discard:'버리기',bet4:'4번째',bet5:'5번째',bet6:'6번째',bet7:'최종베팅',declare:'선언',result:'결과'}[G.stage]||G.stage;
if(G.pot>0)document.getElementById('chip-stack').style.display='flex';
G.players.forEach(p=>{
if(p.id===0)return;
const cel=document.getElementById(`opp-chips-${p.id}`);if(cel)cel.textContent=fmt(p.chips);
const bel=document.getElementById(`pbet-${p.id}`);if(!bel)return;
bel.innerHTML='';
if(p.bet>0)bel.textContent=fmt(p.bet);
if(p.chips===0&&!p.folded)bel.innerHTML+=' ALL-IN ';
if(p.decl){
const badge={high:'hi',low:'lo',swing:'sw'}[p.decl];
const lbl={high:'▲하이',low:'▼로우',swing:'↕스윙'}[p.decl];
bel.innerHTML+=` ${lbl} `;
}
// 아바타 보스 강조
const ava=document.getElementById(`opp-ava-${p.id}`);
if(ava)ava.classList.toggle('boss',p.id===G.bossId);
const inner=document.getElementById(`opp-inner-${p.id}`);
if(inner)inner.classList.toggle('boss-hi',p.id===G.bossId);
});
// 내 아바타
document.getElementById('my-ava').classList.toggle('boss',G.bossId===0);
updateBetAmounts();
}
function updateInfoPanel(p){
if(!p.highHand){document.getElementById('info-panel').style.display='none';return}
document.getElementById('info-panel').style.display='';
document.getElementById('ip-hi-name').textContent='하이: '+hiName(p.highHand);
document.getElementById('ip-lo-name').textContent='로우: '+loName(p.lowHand);
const hiPct=Math.min(100,Math.round((p.highHand.tier/9)*100+10));
const loPct=p.lowHand&&!p.lowHand.penalty?Math.round(Math.max(8,100-(p.lowHand.top-1)*12)):5;
document.getElementById('ip-hi-pct').textContent=hiPct+'%';
document.getElementById('ip-lo-pct').textContent=loPct+'%';
document.getElementById('ip-hi-bar').style.width=hiPct+'%';
document.getElementById('ip-lo-bar').style.width=loPct+'%';
const tiers=[{n:'로티플',v:9},{n:'스트플',v:8},{n:'포카드',v:7},{n:'풀하우스',v:6},{n:'플러시',v:5},{n:'스트레이트',v:4},{n:'트리플',v:3},{n:'투페어',v:2},{n:'원페어',v:1},{n:'하이카드',v:0}];
const cur=p.highHand.tier;
document.getElementById('ip-hand-rows').innerHTML=tiers.slice(0,6).map(t=>`${t.n}
`).join('');
}
// ══════════════════════════════════════
// 위치 계산
// ══════════════════════════════════════
function getSlotRect(playerId){
if(playerId===0){const el=document.getElementById('my-zone');return el?el.getBoundingClientRect():null}
const el=document.getElementById(`opp-zone-${playerId}`);
return el?el.getBoundingClientRect():null;
}
function getSlotCenter(playerId){const r=getSlotRect(playerId);if(!r)return null;return{x:r.left+r.width/2,y:r.top+r.height/2}}
function getPotCenter(){const el=document.getElementById('felt-pot');if(!el)return{x:window.innerWidth/2,y:window.innerHeight/2};const r=el.getBoundingClientRect();return{x:r.left+r.width/2,y:r.top+r.height/2}}
// ══════════════════════════════════════
// 베팅 금액 미리보기
// ══════════════════════════════════════
function updateBetAmounts(){
const p=G.players[0];if(!p)return;
const pot=G.pot,call=G.currentBet,chips=p.chips;
const amts={'amt-call':Math.min(call,chips),'amt-bbang':Math.min(call*2||G.blind,chips),'amt-ttadang':Math.min(call*3||G.blind*3,chips),'amt-qtr':Math.min(Math.floor(pot/4),chips),'amt-half':Math.min(Math.floor(pot/2),chips)};
Object.entries(amts).forEach(([id,v])=>{const el=document.getElementById(id);if(el)el.textContent=v>0?fmt(v):''});
}
// ══════════════════════════════════════
// 보스 마커
// ══════════════════════════════════════
let _bossMarkerEl=null;
function updateBossMarker(){
const bossId=G.bossId;
if(!_bossMarkerEl){
_bossMarkerEl=document.createElement('div');
_bossMarkerEl.className='boss-marker';
document.body.appendChild(_bossMarkerEl);
}
_bossMarkerEl.style.display='flex';
let targetEl=null;
if(bossId===0)targetEl=document.getElementById('my-ava');
else targetEl=document.getElementById(`opp-ava-${bossId}`);
if(targetEl){const r=targetEl.getBoundingClientRect();_bossMarkerEl.style.left=(r.right-12)+'px';_bossMarkerEl.style.top=(r.top-4)+'px'}
}
// ══════════════════════════════════════
// 베팅 팝업 / 선언 팝업 / 칩 이동
// ══════════════════════════════════════
function showBetPopup(playerId,text,type){
const pos=getSlotCenter(playerId);if(!pos)return;
const el=document.createElement('div');el.className='slot-popup bet';
el.innerHTML=`${text}
`;
el.style.left=pos.x+'px';el.style.top=(pos.y-4)+'px';
document.body.appendChild(el);setTimeout(()=>el.remove(),1800);
}
function showDeclPopup(playerId,decl){
const pos=getSlotCenter(playerId);if(!pos)return;
const map={high:``,low:``,swing:``};
const el=document.createElement('div');el.className='slot-popup decl';el.innerHTML=map[decl]||'';
el.style.left=pos.x+'px';el.style.top=(pos.y+2)+'px';
document.body.appendChild(el);setTimeout(()=>el.remove(),2500);
}
function flyChipToPot(playerId,amount){
const from=getSlotCenter(playerId);const to=getPotCenter();if(!from||!to)return;
const count=Math.min(Math.max(1,Math.ceil(Math.log2(amount/cfg.blind+1))),5);
for(let i=0;i{
const chip=document.createElement('div');
chip.className='flying-chip '+(amount>=cfg.blind*20?'red':amount>=cfg.blind*5?'blue':'gold');
const ox=(Math.random()-.5)*14,oy=(Math.random()-.5)*8;
chip.style.left=(from.x+ox-7)+'px';chip.style.top=(from.y+oy-7)+'px';
document.body.appendChild(chip);
requestAnimationFrame(()=>requestAnimationFrame(()=>{
const dur=420+Math.random()*160;const start=performance.now();
function frame(now){
const t=Math.min((now-start)/dur,1);const mt=t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2;
chip.style.left=(from.x+ox+(to.x-from.x-ox)*mt-7)+'px';
const yLine=from.y+oy+(to.y-from.y-oy)*mt-7;
chip.style.top=(yLine-(1-Math.pow(mt*2-1,2))*44)+'px';
chip.style.opacity=t>0.75?(1-(t-0.75)*4).toString():'1';
chip.style.transform=`scale(${1-t*0.5})`;
if(t<1)requestAnimationFrame(frame);else chip.remove();
}
chip.style.transition='none';requestAnimationFrame(frame);
}));
},i*80);
}
}
// ══════════════════════════════════════
// 덱 카드 딜 애니메이션
// ══════════════════════════════════════
function updateDeckCount(){const el=document.getElementById('deck-count');if(el)el.textContent=G.deck?G.deck.length:'52'}
function dealCardAnim(toPlayerId,delay,onLand){
setTimeout(()=>{
const deckEl=document.getElementById('deck-pile');
const toRect=getSlotRect(toPlayerId);
if(!deckEl||!toRect){onLand&&onLand();return}
const fr=deckEl.getBoundingClientRect();
const fromX=fr.left+fr.width/2-10,fromY=fr.top+fr.height/2-14;
const toX=toRect.left+toRect.width/2-10,toY=toRect.top+toRect.height/2-14;
const card=document.createElement('div');card.className='dealing-card';
card.style.left=fromX+'px';card.style.top=fromY+'px';
document.body.appendChild(card);
requestAnimationFrame(()=>requestAnimationFrame(()=>{
const dur=300+Math.random()*80;const start=performance.now();
function frame(now){
const t=Math.min((now-start)/dur,1);const mt=t<.5?2*t*t:1-Math.pow(-2*t+2,2)/2;
card.style.left=(fromX+(toX-fromX)*mt)+'px';
card.style.top=(fromY+(toY-fromY)*mt-(1-Math.pow(mt*2-1,2))*28)+'px';
card.style.opacity=t>0.85?(1-(t-0.85)/0.15).toString():'1';
card.style.transform=`rotate(${mt*12}deg) scale(${1-mt*0.2})`;
if(t<1)requestAnimationFrame(frame);else{card.remove();onLand&&onLand();}
}
requestAnimationFrame(frame);
}));
},delay);
}
function runDealAnimation(players,cardsPerPlayer,onDone){
const GAP=85;let t=0,done=0,total=players.length*cardsPerPlayer;
for(let round=0;round{
dealCardAnim(p.id,t,()=>{done++;updateDeckCount();if(done>=total)setTimeout(onDone,150);});
t+=GAP;
});
}
}
function runSingleDealAnim(players,onDone){
const GAP=110;let done=0;
players.forEach((p,i)=>{dealCardAnim(p.id,i*GAP,()=>{done++;updateDeckCount();if(done>=players.length)setTimeout(onDone,150);});});
}
// ══════════════════════════════════════
// 게임 시작
// ══════════════════════════════════════
function startGame(){
hide('ov-start');
G={deck:shuffle(createDeck()),pot:cfg.blind*(cfg.aiNum+1),stage:'deal',blind:cfg.blind,players:[],currentBet:cfg.blind,round:1,logs:[],seatMap:{}};
const emojis=['🧑','🦁','🐸','🦊','🎭','🤖','👱','🧔'];
const names=['나','AI 고래','AI 개구리','AI 여우','AI 탈','AI 로봇','AI 금발','AI 수염'];
for(let i=0;i{p.cards=[];p.open=[];p.hidden=[];p.folded=false;p.decl=null;p.highHand=null;p.lowHand=null;p.bet=cfg.blind});
G.players.forEach(p=>{for(let i=0;ievalHands(p));
G.bossId=calcBoss();
addLog(`보스: ${G.players.find(p=>p.id===G.bossId)?.name||'?'}`);
document.getElementById('decl-count-row').style.display='none';
document.getElementById('call-box').style.display='none';
document.getElementById('bet-call-info').style.display='none';
document.getElementById('bet-pot-info').style.display='none';
updateDeckCount();
resetCardSlots();
const dealPlayers=getBossOrder();
runDealAnimation(dealPlayers,cfg.mode,()=>{
renderAll();updateHUD();updateBossMarker();
if(cfg.mode===4){G.players.filter(p=>p.isAI&&!p.folded).forEach(p=>aiDiscard(p));showDiscardUI();}
else startBetRound('bet4');
});
}
function checkLastPlayerWins(){
const alive=G.players.filter(p=>!p.folded);
if(alive.length===1){
const winner=alive[0];winner.chips+=G.pot;
addLog(`${winner.name}: 단독 생존 (${fmt(G.pot)})`);
updateHUD();renderAll();
const gains=G.players.map(p=>({id:p.id,gain:p.id===winner.id?G.pot:0}));
G.stage='result';
setTimeout(()=>showResultOverlay(gains,alive),400);
return true;
}
return false;
}
function aiDiscard(p){const w=p.cards.reduce((a,c)=>rv(c)c!==w);p.hidden=[p.cards[0],p.cards[1]];p.open=[p.cards[2]];evalHands(p)}
function showDiscardUI(){
const p=G.players[0];
document.getElementById('discard-cards').innerHTML=p.cards.map((c,i)=>`${cardHTML(c,'dsc',false)}
✓
`).join('');
show('ov-discard');
}
function toggleDiscard(el,idx){document.querySelectorAll('#discard-cards .ov-wrap.sel').forEach(e=>e.classList.remove('sel'));el.classList.add('sel');G._discardIdx=idx}
function hideDiscard(){hide('ov-discard')}
function confirmDiscard(){
if(G._discardIdx===undefined){alert('버릴 카드를 선택하세요');return}
const p=G.players[0];p.cards.splice(G._discardIdx,1);p.hidden=[p.cards[0],p.cards[1]];p.open=[p.cards[2]];evalHands(p);G._discardIdx=undefined;
hideDiscard();renderAll();setTimeout(()=>startBetRound('bet4'),400);
}
// ══════════════════════════════════════
// 베팅 라운드
// ══════════════════════════════════════
const stageSeq=['bet4','bet5','bet6','bet7','declare'];
function startBetRound(stage){
G.stage=stage;
const proceed=()=>{
renderAll();updateHUD();updateBossMarker();
document.getElementById('round-badge').textContent=`${G.round}라운드 · 보스: ${G.players.find(p=>p.id===G.bossId)?.name||'?'}`;
if(stage==='declare'){doDeclaration();return}
const betOrder=getBossOrder().filter(p=>p.isAI);
let delay=0;
betOrder.forEach(p=>{
setTimeout(()=>{
if(p.folded)return;
const score=p.highHand?p.highHand.tier:0,r=Math.random();
if(G.currentBet===0){
if(score>=5&&r<0.5){const pay=Math.min(Math.floor(G.pot*0.5),p.chips);p.chips-=pay;p.bet+=pay;G.pot+=pay;G.currentBet=pay;showBetPopup(p.id,`빵! ${fmt(pay)}`,'bbang');flyChipToPot(p.id,pay);addLog(`${p.name}: 빵`)}
} else {
if(score>=4||r<0.5){const pay=Math.min(G.currentBet,p.chips);p.chips-=pay;p.bet+=pay;G.pot+=pay;showBetPopup(p.id,'콜','call');flyChipToPot(p.id,pay);addLog(`${p.name}: 콜`)}
else{p.folded=true;showBetPopup(p.id,'다이','die');addLog(`${p.name}: 다이`);G.bossId=calcBoss();updateBossMarker();if(checkLastPlayerWins())return;}
}
updateHUD();renderAll();
},delay);
delay+=380;
});
setTimeout(()=>{
updateHUD();renderAll();G.bossId=calcBoss();updateBossMarker();
if(G.currentBet>0){
document.getElementById('bet-call-info').style.display='';
document.getElementById('bet-pot-info').style.display='';
document.getElementById('call-amt').textContent=fmt(G.currentBet);
document.getElementById('max-amt').textContent=fmt(G.pot);
document.getElementById('call-box').style.display='flex';
document.getElementById('felt-call').textContent=fmt(G.currentBet);
}
updateBetAmounts();enableBtns(true);showTurn(true);
},delay+200);
};
if(stage!=='bet4'){
const dealOrder=getBossOrder();
dealOrder.forEach(p=>{const c=G.deck.pop();p.cards.push(c);if(stage==='bet7')p.hidden.push(c);else p.open.push(c);evalHands(p);});
G.bossId=calcBoss();updateDeckCount();
runSingleDealAnim(dealOrder,proceed);
} else proceed();
}
function playerAct(action){
enableBtns(false);showTurn(false);
const p=G.players[0];let pay=0,type=action,label='';
if(action==='die'){
p.folded=true;label='다이';type='die';G.bossId=calcBoss();updateBossMarker();
addLog('나: 다이');showBetPopup(0,'다이','die');updateHUD();renderAll();
if(checkLastPlayerWins())return;
const idx=stageSeq.indexOf(G.stage);setTimeout(()=>startBetRound(idx0)flyChipToPot(0,pay);
updateHUD();renderAll();
const idx=stageSeq.indexOf(G.stage);
setTimeout(()=>startBetRound(idxp.isAI&&!p.folded).forEach(p=>{
if(isAutoSwing(p.highHand))p.decl='swing';
else{const hs=p.highHand?p.highHand.tier:0,ls=p.lowHand&&!p.lowHand.penalty?(14-p.lowHand.top):0;p.decl=hs>=6&&ls>=6?'swing':hs>=ls?'high':'low'}
if(p.decl==='high')cHi++;else if(p.decl==='low')cLo++;else cSw++;
});
document.getElementById('cnt-hi').textContent=cHi;
document.getElementById('cnt-lo').textContent=cLo;
document.getElementById('cnt-sw').textContent=cSw;
const p0=G.players[0];
if(p0.folded){startRevealSequence();return}
if(isAutoSwing(p0.highHand)){p0.decl='swing';cSw++;document.getElementById('cnt-sw').textContent=cSw;setTimeout(startRevealSequence,600);return}
document.getElementById('decl-hi-sub').textContent=hiName(p0.highHand);
document.getElementById('decl-lo-sub').textContent=loName(p0.lowHand);
document.getElementById('decl-hand-info').innerHTML=`하이: ${hiName(p0.highHand)} | 로우: ${loName(p0.lowHand)} `;
document.getElementById('decl-hi-cnt').textContent=cHi;
document.getElementById('decl-lo-cnt').textContent=cLo;
document.getElementById('decl-sw-cnt').textContent=cSw;
document.getElementById('decl-count-row').style.display='none';
show('ov-declare');
}
function playerDeclare(d){
hide('ov-declare');G.players[0].decl=d;addLog('나: '+d+' 선언');
document.getElementById('decl-count-row').style.display='flex';
const k={high:'cnt-hi',low:'cnt-lo',swing:'cnt-sw'}[d];
if(k){const el=document.getElementById(k);if(el)el.textContent=parseInt(el.textContent)+1}
setTimeout(startRevealSequence,400);
}
// ══════════════════════════════════════
// 리벌 시퀀스
// ══════════════════════════════════════
function showDeclRevealPopup(playerId,decl){
const pos=getSlotCenter(playerId);if(!pos)return;
const arrows={high:'▲',low:'▼',swing:'⬆⬇'};const labels={high:'하이',low:'로우',swing:'스윙'};const cls={high:'hi',low:'lo',swing:'sw'};
const el=document.createElement('div');el.className='decl-reveal-popup';
el.innerHTML=`${arrows[decl]||'?'}
${labels[decl]||decl}
`;
el.style.left=pos.x+'px';el.style.top=(pos.y+4)+'px';
document.body.appendChild(el);setTimeout(()=>el.remove(),4200);
}
function getHandPopupDur(tier){return[2.0,2.1,2.3,2.5,2.7,3.0,3.2,3.6,4.0,4.4][tier]||2.0}
function showHandPopup(playerId,html,tier,isLo){
const pos=getSlotCenter(playerId);if(!pos)return;
const dur=isLo?2.5+(tier||0)*0.2:getHandPopupDur(tier||0);
const el=document.createElement('div');el.className='hand-popup';el.style.setProperty('--dur',dur+'s');
function getLoCls(s){return s<=1?'best':s<=2?'strong':s<=4?'mid':'weak'}
el.innerHTML=`${html}
`;
el.style.left=pos.x+'px';el.style.top=(pos.y-8)+'px';
document.body.appendChild(el);setTimeout(()=>el.remove(),(dur+0.2)*1000);
}
function getLoPopupInfo(lowHand){
if(!lowHand)return{text:'—',score:5};
if(lowHand.penalty)return{text:`${lowHand.top}탑(패)`,score:5};
const top=lowHand.top===14?1:lowHand.top;
const cards=[...lowHand.cards].sort((a,b)=>{const av=rv(a)===14?1:rv(a),bv=rv(b)===14?1:rv(b);return av-bv});
const cardStr=cards.map(c=>rv(c)===14?1:rv(c)).join(' ');
let score=top<=6?0:top<=7?1:top<=9?2:top<=11?3:4;
return{text:`${top===1?'A':top}탑 ${cardStr}`,score};
}
function flipHiddenCards(player,onDone){
if(player.id===0){
const cardsEl=document.getElementById('my-cards');
const cardEls=cardsEl.querySelectorAll('.card.hidden-mine');let i=0;
function flipNext(){if(i>=cardEls.length){setTimeout(onDone,300);return}const c=cardEls[i];c.classList.remove('hidden-mine');c.style.animation='none';void c.offsetHeight;c.classList.add('flip-reveal');i++;setTimeout(flipNext,700)}
flipNext();return;
}
const el=document.getElementById(`pcards-${player.id}`);
if(!el){onDone();return}
const pmcs=el.querySelectorAll('.pmc');const hiddens=player.hidden;let i=0;
function flipNext(){
if(i>=pmcs.length||i>=hiddens.length){setTimeout(onDone,300);return}
const pmc=pmcs[i];const c=hiddens[i];
const d=document.createElement('div');d.innerHTML=cardHTML(c,'tc',false);
const newCard=d.firstChild;newCard.style.animation='none';pmc.replaceWith(newCard);
void newCard.offsetHeight;newCard.classList.add('flip-reveal');i++;setTimeout(flipNext,700);
}
flipNext();
}
const SPECIAL_CONFIGS={9:{title:'🃏 로얄 스트레이트 플러시',sub:'AUTO SWING',color:'#ffdd0033',pcolor:'#ffdd00',text:'#ffe566',glow:'#ffdd00',subColor:'#ffcc55',dur:4500},8:{title:'🔥 스트레이트 플러시',sub:'AUTO SWING',color:'#ff008833',pcolor:'#ff66cc',text:'#ff88dd',glow:'#ff0088',subColor:'#ff88cc',dur:3800},7:{title:'💥 포 카드',sub:'AUTO SWING',color:'#ff440022',pcolor:'#ff8844',text:'#ff9955',glow:'#ff4400',subColor:'#ffaa66',dur:3400}};
function showSpecialSplash(tier,playerName,onDone){
const cfg2=SPECIAL_CONFIGS[tier];if(!cfg2){onDone();return}
const splash=document.getElementById('special-splash');
document.getElementById('sp-title').textContent=cfg2.title;document.getElementById('sp-title').style.color=cfg2.text;document.getElementById('sp-title').style.textShadow=`0 0 22px ${cfg2.glow},0 0 44px ${cfg2.glow}`;
document.getElementById('sp-sub').textContent=cfg2.sub;document.getElementById('sp-sub').style.color=cfg2.subColor;
document.getElementById('sp-name').textContent=playerName;
const pc=document.getElementById('sp-particles');pc.innerHTML='';
for(let i=0;i<28;i++){const p=document.createElement('div');p.className='sp-particle';const ang=Math.random()*Math.PI*2,dist=120+Math.random()*180;p.style.setProperty('--tx',Math.cos(ang)*dist+'px');p.style.setProperty('--ty',Math.sin(ang)*dist-60+'px');p.style.setProperty('--spd',(1.6+Math.random()*1.4)+'s');p.style.setProperty('--del',(Math.random()*0.6)+'s');p.style.setProperty('--sp-pcolor',cfg2.pcolor);p.style.left=(40+Math.random()*20)+'%';p.style.top=(40+Math.random()*20)+'%';pc.appendChild(p);}
splash.style.setProperty('--sp-color',cfg2.color);splash.classList.add('show');
setTimeout(()=>{splash.classList.remove('show');onDone()},cfg2.dur);
}
function startRevealSequence(){
G.stage='result';document.getElementById('decl-count-row').style.display='flex';
const active=G.players.filter(p=>!p.folded);
active.forEach(p=>{if(p.decl==='swing'){p.highHand=bestHi(p.cards);p.lowHand=swingLo(p.cards)}else if(p.decl==='high'){p.highHand=bestHi(p.cards);p.lowHand=null}else{p.lowHand=bestLo(p.cards);p.highHand=null}});
const revealOrder=getBossOrder().filter(p=>!p.folded);
let t=0;const DECL_GAP=1600;
revealOrder.forEach((p,i)=>{setTimeout(()=>{showDeclRevealPopup(p.id,p.decl);},t+i*DECL_GAP);});
t+=revealOrder.length*DECL_GAP+800;
const CARD_GAP=1400;
revealOrder.forEach((p,i)=>{setTimeout(()=>{flipHiddenCards(p,()=>{setTimeout(()=>showHandRevealPopup(p),400);});},t+i*(CARD_GAP*2));});
t+=revealOrder.length*(CARD_GAP*2)+1200;
let specialQueue=[];
revealOrder.forEach(p=>{if(p.highHand&&p.highHand.tier>=7)specialQueue.push({tier:p.highHand.tier,name:p.emoji+' '+p.name});});
specialQueue.sort((a,b)=>b.tier-a.tier);
if(specialQueue.length){const run=(idx)=>{if(idx>=specialQueue.length){setTimeout(()=>finishSettle(active),400);return}setTimeout(()=>showSpecialSplash(specialQueue[idx].tier,specialQueue[idx].name,()=>run(idx+1)),idx===0?t:200)};setTimeout(()=>run(0),t);}
else setTimeout(()=>finishSettle(active),t);
}
function showHandRevealPopup(p){
if(p.decl==='high'||p.decl==='swing'){const h=p.highHand;if(h)showHandPopup(p.id,hiName(h),h.tier,false);}
if(p.decl==='low'||p.decl==='swing'){const l=p.lowHand;if(l){const info=getLoPopupInfo(l);setTimeout(()=>showHandPopup(p.id,info.text,info.score,true),p.decl==='swing'?900:0);}}
}
function finishSettle(active){
const gains=settleGame(active,G.pot);
gains.forEach(g=>{const p=G.players.find(x=>x.id===g.id);if(p)p.chips+=g.gain});
renderAll();updateHUD();setTimeout(()=>showResultOverlay(gains,active),500);
}
function showResultOverlay(gains,active){
const allHi=active.filter(p=>p.decl==='high'||p.decl==='swing');
const allLo=active.filter(p=>p.decl==='low'||p.decl==='swing');
let hiW=null,loW=null;
if(allHi.length)hiW=allHi.reduce((b,p)=>p.highHand&&(!b.highHand||cmpHi(p.highHand,b.highHand)>0)?p:b);
if(allLo.length)loW=allLo.reduce((b,p)=>p.lowHand&&(!b.lowHand||cmpLo(p.lowHand,b.lowHand)>0)?p:b);
const banners=document.getElementById('win-banners');banners.innerHTML='';
if(hiW)banners.innerHTML+=`▲ 하이 WIN ${hiW.emoji} ${hiW.name}
`;
if(loW&&loW!==hiW)banners.innerHTML+=`▼ 로우 WIN ${loW.emoji} ${loW.name}
`;
const maxGain=gains.reduce((m,g)=>g.gain>m?g.gain:m,0);
const bgEl=document.getElementById('big-gain');
if(maxGain>0){bgEl.style.display='';bgEl.textContent='+'+fmt(maxGain)}else bgEl.style.display='none';
document.getElementById('result-rows').innerHTML=active.map(p=>{
const g=gains.find(x=>x.id===p.id)||{gain:0},net=g.gain-p.bet;
const dlbl={high:'▲하이',low:'▼로우',swing:'↕스윙'}[p.decl]||'—';
const dcls={high:'hi',low:'lo',swing:'sw'}[p.decl]||'';
const hand=p.decl==='high'?hiName(p.highHand):p.decl==='low'?loName(p.lowHand):`${hiName(p.highHand)}/${loName(p.lowHand)}`;
return`${p.emoji} ${p.name}
${hand}
${dlbl} ${net>0?'+':''}${fmt(net)} `;
}).join('');
show('ov-result');
}
function nextRound(){
hide('ov-result');G.round++;G.deck=shuffle(createDeck());
G.pot=cfg.blind*G.players.length;G.currentBet=cfg.blind;
G.players.forEach(p=>{p.bet=cfg.blind;p.chips-=cfg.blind});
if(_bossMarkerEl)_bossMarkerEl.style.display='none';
doDeal();
}
// ══════════════════════════════════════
// 유틸
// ══════════════════════════════════════
function evalHands(p){if(p.cards.length<5)return;p.highHand=bestHi(p.cards);p.lowHand=bestLo(p.cards)}
function renderAll(){G.players.forEach(p=>renderPlayerCards(p));updateHUD()}
function enableBtns(on){document.querySelectorAll('.bbar-btn').forEach(b=>b.classList.toggle('active',on))}
function showTurn(on){
document.getElementById('turn-badge').style.display=on?'':'none';
const tb=document.getElementById('tb');
if(on){tb.style.animation='none';void tb.offsetHeight;tb.style.animation='tdown 20s linear forwards'}else tb.style.width='0';
}
function show(id){document.getElementById(id).classList.add('show')}
function hide(id){document.getElementById(id).classList.remove('show')}
function addLog(msg){G.logs=G.logs||[];G.logs.push(msg);if(G.logs.length>60)G.logs.shift()}
function showLog(){
const el=document.getElementById('log-panel');
el.style.display=el.style.display==='none'?'block':'none';
if(el.style.display!=='none')el.innerHTML=(G.logs||[]).slice(-20).reverse().map(l=>`${l}
`).join('');
}