HTML5 独立可玩源码包 (index.html)
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>午夜西洋跳棋 · Checkers Lounge: Royal Draughts</title>
<style>
:root {
--bg: #0b0713;
--panel: #150e24;
--accent: #ef4444;
--accent2: #fde047;
--text: #f8f4fc;
--muted: rgba(248, 244, 252, 0.65);
}
* { box-sizing: border-box; user-select: none; -webkit-user-select: none; }
html, body {
margin: 0; padding: 0; width: 100%; height: 100%;
background: radial-gradient(circle at 50% 0%, #24133d 0%, var(--bg) 75%);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Noto Sans SC", sans-serif;
overflow-x: hidden;
}
button { font: inherit; color: inherit; cursor: pointer; }
.shell {
max-width: 780px; margin: 0 auto; min-height: 100svh;
display: flex; flex-direction: column; padding: 12px 14px; gap: 10px;
}
.topbar {
display: flex; align-items: center; justify-content: space-between; gap: 8px;
padding: 10px 14px; border-radius: 14px;
background: rgba(21, 14, 36, 0.85);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.badge {
padding: 3px 9px; border-radius: 999px; font-size: 11px; font-weight: 800;
background: linear-gradient(90deg, var(--accent), var(--accent2)); color: #12091c;
}
.brand h1 { margin: 0; font-size: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.stats { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pill {
padding: 4px 10px; border-radius: 999px; font-size: 12px;
background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.12);
}
.tbtn {
padding: 5px 10px; border-radius: 999px; font-size: 12px;
background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.16);
}
.banner {
padding: 8px 14px; border-radius: 10px; font-size: 13px; text-align: center;
background: rgba(255, 255, 255, 0.05); color: var(--accent2);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.stage {
flex: 1; display: flex; align-items: center; justify-content: center;
background: rgba(15, 10, 28, 0.78); border-radius: 18px; padding: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.55);
}
.table-felt { width: 100%; text-align: center; display: flex; flex-direction: column; gap: 12px; }
.hand-zone { display: flex; flex-direction: column; gap: 6px; align-items: center; }
.zone-label { font-size: 12px; color: var(--muted); letter-spacing: 0.06em; }
.cards-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; min-height: 68px; }
.pcard {
width: 50px; height: 68px; border-radius: 9px; background: #fcfaf7; color: #181224;
display: flex; flex-direction: column; justify-content: space-between; padding: 5px 6px;
font-weight: 800; box-shadow: 0 4px 12px rgba(0,0,0,.45); border: 1px solid #ddd;
}
.pcard.red { color: #e11d48; }
.pcard.back {
background: linear-gradient(135deg, var(--accent), #2e1065); color: #fff;
display: grid; place-items: center; font-size: 24px; border-color: rgba(255,255,255,.3);
}
.pcard span { font-size: 20px; align-self: center; }
.btn-bar { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
.gbtn {
padding: 9px 15px; border-radius: 11px; font-weight: 700; font-size: 13px;
background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.18);
transition: transform 0.1s, background 0.15s;
}
.gbtn:hover { background: rgba(255, 255, 255, 0.15); }
.gbtn:active { transform: scale(0.96); }
.gbtn.primary {
background: linear-gradient(90deg, var(--accent), var(--accent2));
color: #13091f; border-color: transparent;
}
.gbtn.accent {
border-color: var(--accent2); color: var(--accent2);
}
</style>
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="brand">
<span class="badge">CHECKERS</span>
<h1 id="titleText">午夜西洋跳棋</h1>
</div>
<div class="stats">
<span class="pill">SCORE <b id="scoreVal">0</b></span>
<span class="pill">BEST <b id="bestVal">0</b></span>
<button class="tbtn" id="langBtn">中/EN</button>
<button class="tbtn" id="muteBtn">🔊</button>
<button class="tbtn" id="resetBtn">重开</button>
</div>
</header>
<div class="banner" id="statusBanner">8x8国际跳棋博弈。斜向推进、跨子连跳吃子、底线升变加冕王棋(King),与智能俱乐部AI一决高下。</div>
<main class="stage" id="gameStage"></main>
</div>
<script>
const GAME_ID = "checkers-lounge";
const ZH_TITLE = "午夜西洋跳棋";
const EN_TITLE = "Checkers Lounge: Royal Draughts";
// Shared Web Audio synth & helpers
let audioCtx = null, muted = false, lang = (navigator.language || 'zh').startsWith('zh') ? 'zh' : 'en';
function sfx(freq=440, dur=0.08, type='sine', vol=0.12) {
if (muted) return;
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
if (audioCtx.state === 'suspended') audioCtx.resume();
const o = audioCtx.createOscillator(), g = audioCtx.createGain();
o.type = type; o.frequency.setValueAtTime(freq, audioCtx.currentTime);
g.gain.setValueAtTime(vol, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);
o.connect(g); g.connect(audioCtx.destination);
o.start(); o.stop(audioCtx.currentTime + dur);
} catch {}
}
function chordWin() { sfx(523,0.1,'triangle'); setTimeout(()=>sfx(659,0.1,'triangle'),80); setTimeout(()=>sfx(784,0.18,'triangle'),160); }
function chordLose() { sfx(300,0.14,'sawtooth'); setTimeout(()=>sfx(220,0.22,'sawtooth'),120); }
const scoreEl = document.getElementById('scoreVal');
const bestEl = document.getElementById('bestVal');
const statusEl = document.getElementById('statusBanner');
const stageEl = document.getElementById('gameStage');
const BEST_KEY = 'hg_best_' + GAME_ID;
let score = 0, best = Number(localStorage.getItem(BEST_KEY) || 0);
bestEl.textContent = best;
function setScore(v) {
score = Math.max(0, Math.round(v));
scoreEl.textContent = score;
if (score > best) { best = score; bestEl.textContent = best; try { localStorage.setItem(BEST_KEY, String(best)); } catch {} }
}
function addScore(d) { setScore(score + d); }
function msg(zhText, enText) { statusEl.textContent = lang === 'zh' ? zhText : enText; }
function t(zhText, enText) { return lang === 'zh' ? zhText : enText; }
document.getElementById('muteBtn').onclick = () => {
muted = !muted;
document.getElementById('muteBtn').textContent = muted ? '🔇' : '🔊';
};
document.getElementById('langBtn').onclick = () => {
lang = lang === 'zh' ? 'en' : 'zh';
document.getElementById('titleText').textContent = lang === 'zh' ? ZH_TITLE : EN_TITLE;
document.getElementById('resetBtn').textContent = t('重开', 'Reset');
if (window.onLangChange) window.onLangChange();
};
const SUITS = ['♠','♥','♦','♣'];
const RANKS = ['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
function makeDeck() {
const d = [];
for (const s of SUITS) for (let i=0;i<RANKS.length;i++) d.push({s, r:RANKS[i], v:i+1, red:s==='♥'||s==='♦'});
for (let i=d.length-1;i>0;i--) { const j=Math.floor(Math.random()*(i+1)); [d[i],d[j]]=[d[j],d[i]]; }
return d;
}
function cardHTML(c, hidden=false) {
if (hidden) return `<div class="pcard back"><span>♛</span></div>`;
return `<div class="pcard ${c.red?'red':''}"><b>${c.r}${c.s}</b><span>${c.s}</span></div>`;
}
const ENGINES = {
"neon-blackjack": function() {
let bank = 1000, bet = 50, deck = [], player = [], dealer = [], state = 'bet';
setScore(bank);
function handVal(h) {
let sum = 0, aces = 0;
for (const c of h) { if (c.v===1) { aces++; sum+=11; } else sum += Math.min(10, c.v); }
while (sum > 21 && aces > 0) { sum -= 10; aces--; }
return sum;
}
function render() {
const pv = handVal(player), dv = state==='play' ? handVal(dealer.slice(0,1)) : handVal(dealer);
stageEl.innerHTML = `
<div class="table-felt">
<div class="hand-zone">
<div class="zone-label">${t('荷官手牌','Dealer')} (${state==='play' ? dv+'+?' : dv})</div>
<div class="cards-row">${dealer.map((c,i)=>cardHTML(c, state==='play' && i===1)).join('')}</div>
</div>
<div class="hand-zone">
<div class="zone-label">${t('你的手牌','Your Hand')} (${pv}) · ${t('底池','Bet')} $${bet}</div>
<div class="cards-row">${player.map(c=>cardHTML(c)).join('')}</div>
</div>
<div class="btn-bar">
${state==='play' ? `
<button class="gbtn primary" id="bjHit">${t('要牌 Hit','Hit')}</button>
<button class="gbtn" id="bjStand">${t('停牌 Stand','Stand')}</button>
${player.length===2 && bank>=bet ? `<button class="gbtn accent" id="bjDouble">${t('双倍 Double','Double')}</button>` : ''}
` : `
<button class="gbtn" data-chip="25">+$25</button>
<button class="gbtn" data-chip="100">+$100</button>
<button class="gbtn" id="bjClear">${t('重置注额','Min Bet')}</button>
<button class="gbtn primary" id="bjDeal">${t('发牌 Deal','Deal Hand')}</button>
`}
</div>
</div>`;
if (state==='play') {
document.getElementById('bjHit').onclick = () => {
sfx(520,0.06); player.push(deck.pop());
if (handVal(player) > 21) endRound(-bet, t('爆牌!庄家胜','Bust! Dealer wins'));
else render();
};
document.getElementById('bjStand').onclick = stand;
const db = document.getElementById('bjDouble');
if (db) db.onclick = () => { bank -= bet; bet *= 2; player.push(deck.pop()); if (handVal(player)>21) endRound(0, t('双倍爆牌!','Double Bust!'), true); else stand(true); };
} else {
stageEl.querySelectorAll('[data-chip]').forEach(b => b.onclick = () => { sfx(600,0.05); bet = Math.min(bank, bet + Number(b.dataset.chip)); render(); });
document.getElementById('bjClear').onclick = () => { bet = Math.min(bank, 50); render(); };
document.getElementById('bjDeal').onclick = deal;
}
}
function deal() {
if (bank < 10) bank = 500;
bet = Math.min(bank, Math.max(25, bet));
bank -= bet;
deck = makeDeck();
player = [deck.pop(), deck.pop()];
dealer = [deck.pop(), deck.pop()];
state = 'play'; sfx(440,0.08);
msg('请选择要牌或停牌', 'Choose Hit, Stand, or Double Down');
if (handVal(player) === 21) stand(true);
else render();
}
function stand(doubled=false) {
state = 'done';
while (handVal(dealer) < 17) dealer.push(deck.pop());
const pv = handVal(player), dv = handVal(dealer);
if (pv === 21 && player.length === 2 && dv !== 21) { bank += Math.round(bet * 2.5); chordWin(); msg('Blackjack 21点!赢得 1.5 倍彩金','Blackjack! 3:2 Payout!'); }
else if (dv > 21 || pv > dv) { bank += bet * 2; chordWin(); msg(`你赢了!(${pv} vs ${dv})`, `You win! (${pv} vs ${dv})`); }
else if (pv === dv) { bank += bet; sfx(400,0.1); msg(`平局 Push (${pv})`, `Push tie (${pv})`); }
else { chordLose(); msg(`庄家胜 (${dv} vs ${pv})`, `Dealer wins (${dv} vs ${pv})`); }
if (doubled) bet = Math.max(25, Math.floor(bet/2));
setScore(bank); render();
}
function endRound(delta, text, alreadyDeducted=false) {
state = 'done'; chordLose(); setScore(bank); msg(text, text); render();
}
deal();
},
"velvet-roulette": function() {
const REDS = new Set([1,3,5,7,9,12,14,16,18,19,21,23,25,27,30,32,34,36]);
let bank = 1000, betType = 'red', betNum = 7, stake = 50, lastNum = null, spinning = false;
setScore(bank);
function render() {
const col = lastNum===null ? '#888' : lastNum===0 ? '#10b981' : REDS.has(lastNum) ? '#e11d48' : '#1e293b';
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:flex;align-items:center;justify-content:center;gap:22px;flex-wrap:wrap;">
<div id="wheelPocket" style="width:118px;height:118px;border-radius:50%;border:5px solid var(--accent);display:grid;place-items:center;font-size:38px;font-weight:900;background:${col};box-shadow:0 0 28px rgba(245,166,35,.35);">
${lastNum === null ? '◎' : lastNum}
</div>
<div style="text-align:left;max-width:260px;">
<div style="font-size:14px;color:var(--muted);">${t('当前筹码','Bankroll')}: <b style="color:#fff">$${bank}</b> · ${t('单注','Stake')} $${stake}</div>
<div style="margin-top:6px;font-size:15px;">${t('押注项','Selected Bet')}: <b style="color:var(--accent2)">${betType.toUpperCase()}${betType==='number'?' #'+betNum:''}</b></div>
</div>
</div>
<div class="btn-bar" style="margin-top:8px;">
<button class="gbtn ${betType==='red'?'primary':''}" data-bt="red">${t('红 Red (1:1)','Red (1:1)')}</button>
<button class="gbtn ${betType==='black'?'primary':''}" data-bt="black">${t('黑 Black (1:1)','Black (1:1)')}</button>
<button class="gbtn ${betType==='odd'?'primary':''}" data-bt="odd">${t('奇数 Odd (1:1)','Odd (1:1)')}</button>
<button class="gbtn ${betType==='even'?'primary':''}" data-bt="even">${t('偶数 Even (1:1)','Even (1:1)')}</button>
<button class="gbtn ${betType==='1st12'?'primary':''}" data-bt="1st12">1–12 (2:1)</button>
<button class="gbtn ${betType==='2nd12'?'primary':''}" data-bt="2nd12">13–24 (2:1)</button>
<button class="gbtn ${betType==='3rd12'?'primary':''}" data-bt="3rd12">25–36 (2:1)</button>
<button class="gbtn ${betType==='number'?'primary':''}" data-bt="number">${t('幸运号','Lucky')} #${betNum} (35:1)</button>
</div>
<div class="btn-bar">
<button class="gbtn" id="rlPick">${t('换号 (0-36)','Pick # (0-36)')}</button>
<button class="gbtn" id="rlStake">${t('切换注额','Bet')} $${stake}</button>
<button class="gbtn accent" id="rlSpin" ${spinning?'disabled':''}>${t('转动轮盘 Spin','Spin Wheel')}</button>
</div>
</div>`;
stageEl.querySelectorAll('[data-bt]').forEach(b => b.onclick = () => { sfx(500,0.05); betType = b.dataset.bt; render(); });
document.getElementById('rlPick').onclick = () => { betNum = (betNum + 1) % 37; betType = 'number'; sfx(580,0.05); render(); };
document.getElementById('rlStake').onclick = () => { stake = stake===50 ? 100 : stake===100 ? 250 : 50; render(); };
document.getElementById('rlSpin').onclick = spin;
}
function spin() {
if (spinning) return;
if (bank < stake) bank = 500;
bank -= stake; spinning = true;
let ticks = 18;
const timer = setInterval(() => {
lastNum = Math.floor(Math.random() * 37);
sfx(320 + ticks*14, 0.04);
render();
if (--ticks <= 0) {
clearInterval(timer); spinning = false;
let mult = 0;
if (betType==='red' && REDS.has(lastNum)) mult = 2;
else if (betType==='black' && lastNum>0 && !REDS.has(lastNum)) mult = 2;
else if (betType==='odd' && lastNum>0 && lastNum%2===1) mult = 2;
else if (betType==='even' && lastNum>0 && lastNum%2===0) mult = 2;
else if (betType==='1st12' && lastNum>=1 && lastNum<=12) mult = 3;
else if (betType==='2nd12' && lastNum>=13 && lastNum<=24) mult = 3;
else if (betType==='3rd12' && lastNum>=25 && lastNum<=36) mult = 3;
else if (betType==='number' && lastNum===betNum) mult = 36;
if (mult > 0) {
const win = stake * mult; bank += win; chordWin();
msg(`开出 ${lastNum} 号!赢得 $${win}`, `Ball landed on ${lastNum}! Won $${win}!`);
} else {
chordLose();
msg(`开出 ${lastNum} 号,未命中`, `Ball landed on ${lastNum}. Try again!`);
}
setScore(bank); render();
}
}, 65);
}
msg('选择押注区域并转动轮盘', 'Select your bet and spin the wheel');
render();
},
"midnight-slots": function() {
const SYMS = ['🍒','🍋','🍇','🔔','💋','💎','7️⃣'];
const PAY = {'🍒':3,'🍋':4,'🍇':6,'🔔':10,'💋':15,'💎':25,'7️⃣':50};
let credits = 1000, bet = 25, freeSpins = 0, grid = [['7️⃣','💎','7️⃣'],['💋','7️⃣','💋'],['🍒','🔔','🍒']], spinning = false;
setScore(credits);
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:flex;gap:14px;justify-content:center;font-size:14px;color:var(--muted);">
<span>${t('筹码','Credits')}: <b style="color:#fff">$${credits}</b></span>
<span>${t('单线注','Bet')}: <b style="color:var(--accent2)">$${bet}</b></span>
${freeSpins>0 ? `<span style="color:#4ade80;font-weight:700;">🎁 Free Spins: ${freeSpins}</span>` : ''}
</div>
<div style="display:grid;grid-template-columns:repeat(3,86px);gap:10px;justify-content:center;margin:10px auto;padding:14px;background:rgba(0,0,0,.45);border-radius:18px;border:2px solid var(--accent);">
${grid.flat().map(s => `<div style="height:72px;display:grid;place-items:center;font-size:38px;background:rgba(255,255,255,.06);border-radius:12px;">${s}</div>`).join('')}
</div>
<div class="btn-bar">
<button class="gbtn" id="slBet">${t('切换注额','Bet')} $${bet}</button>
<button class="gbtn primary" id="slSpin" ${spinning?'disabled':''}>${freeSpins>0 ? t('免费旋转!','FREE SPIN!') : t('拉杆旋转 SPIN','SPIN REELS')}</button>
</div>
</div>`;
document.getElementById('slBet').onclick = () => { bet = bet===25 ? 50 : bet===50 ? 100 : 25; render(); };
document.getElementById('slSpin').onclick = spin;
}
function spin() {
if (spinning) return;
if (freeSpins > 0) freeSpins--;
else { if (credits < bet) credits = 500; credits -= bet; }
spinning = true;
let steps = 10;
const timer = setInterval(() => {
grid = Array.from({length:3}, () => Array.from({length:3}, () => SYMS[Math.floor(Math.random()*SYMS.length)]));
sfx(380 + steps*25, 0.04, 'triangle');
render();
if (--steps <= 0) {
clearInterval(timer); spinning = false;
const lines = [
[grid[0][0],grid[0][1],grid[0][2]],
[grid[1][0],grid[1][1],grid[1][2]],
[grid[2][0],grid[2][1],grid[2][2]],
[grid[0][0],grid[1][1],grid[2][2]],
[grid[2][0],grid[1][1],grid[0][2]],
];
let totalWin = 0;
for (const ln of lines) {
const nonWild = ln.filter(x => x !== '💎');
if (nonWild.length === 0) totalWin += bet * PAY['💎'];
else if (nonWild.every(x => x === nonWild[0])) totalWin += bet * PAY[nonWild[0]];
else if (ln[0] === ln[1] || ln[1] === ln[2]) totalWin += Math.round(bet * 0.8);
}
const kisses = grid.flat().filter(x => x === '💋').length;
if (kisses >= 3) { freeSpins += 3; totalWin += bet * 3; }
if (totalWin > 0) {
credits += totalWin; chordWin();
msg(`中奖!赢得 $${totalWin}${kisses>=3?' + 3次免费旋转':''}`, `JACKPOT! Won $${totalWin}${kisses>=3?' + 3 Free Spins':''}`);
} else {
msg('差一点点!再来一局', 'So close! Spin again');
}
setScore(credits); render();
}
}, 65);
}
msg('💎百搭任意图标,3个💋触发免费旋转!', '💎 is Wild! 3+ 💋 awards Free Spins!');
render();
},
"texas-holdem-duel": function() {
let chips = 1000, pot = 0, stage = 0, deck = [], my = [], ai = [], comm = [], over = false;
setScore(chips);
function rankScore(cards) {
const counts = {};
for (const c of cards) counts[c.v] = (counts[c.v]||0) + 1;
const pairs = Object.entries(counts).map(([v,c])=>({v:Number(v)===1?14:Number(v),c})).sort((a,b)=>b.c-a.c||b.v-a.v);
return pairs[0].c * 100 + pairs[0].v + (pairs[1]?.c||0)*15 + (pairs[1]?.v||0)*0.1;
}
function startHand() {
if (chips < 50) chips = 800;
deck = makeDeck(); my = [deck.pop(), deck.pop()]; ai = [deck.pop(), deck.pop()];
comm = []; stage = 0; over = false; chips -= 50; pot = 100;
setScore(chips); sfx(460,0.07);
msg('翻牌前 Pre-Flop:选择过牌跟注或加注', 'Pre-Flop: Check/Call or Raise');
render();
}
function advance(raiseAmt) {
if (raiseAmt > 0 && chips >= raiseAmt) { chips -= raiseAmt; pot += raiseAmt * 2; sfx(620,0.07); }
else sfx(440,0.06);
stage++;
if (stage === 1) comm.push(deck.pop(), deck.pop(), deck.pop());
else if (stage === 2 || stage === 3) comm.push(deck.pop());
else {
over = true;
const ms = rankScore([...my, ...comm]), as = rankScore([...ai, ...comm]);
if (ms >= as) { chips += pot; chordWin(); msg(`摊牌获胜!赢得底池 $${pot}`, `Showdown Win! Collected $${pot}`); }
else { chordLose(); msg(`对手牌力更强,失去底池 $${pot}`, `Opponent wins showdown ($${pot})`); }
}
setScore(chips); render();
}
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div class="hand-zone">
<div class="zone-label">${t('对手手牌','Rival')} · ${t('底池 Pot','Pot')} $${pot}</div>
<div class="cards-row">${ai.map(c=>cardHTML(c, !over)).join('')}</div>
</div>
<div class="hand-zone">
<div class="zone-label">${t('公共牌 Community','Community Cards')} (${comm.length}/5)</div>
<div class="cards-row">${comm.length ? comm.map(c=>cardHTML(c)).join('') : '<span style="color:var(--muted)">Waiting for Flop...</span>'}</div>
</div>
<div class="hand-zone">
<div class="zone-label">${t('你的底牌','Your Hole Cards')} · $${chips}</div>
<div class="cards-row">${my.map(c=>cardHTML(c)).join('')}</div>
</div>
<div class="btn-bar">
${!over ? `
<button class="gbtn" id="thCheck">${t('过牌/跟注 Check','Check / Call')}</button>
<button class="gbtn primary" id="thRaise">${t('加注 +$50','Raise +$50')}</button>
<button class="gbtn accent" id="thAll">${t('全下 All-In','All-In +$150')}</button>
` : `<button class="gbtn primary" id="thNext">${t('下一局 Next Hand','Deal Next Hand')}</button>`}
</div>
</div>`;
if (!over) {
document.getElementById('thCheck').onclick = () => advance(0);
document.getElementById('thRaise').onclick = () => advance(Math.min(chips, 50));
document.getElementById('thAll').onclick = () => advance(Math.min(chips, 150));
} else document.getElementById('thNext').onclick = startHand;
}
window.resetGame = startHand;
startHand();
},
"crystal-mahjong-solitaire": function() {
const ICONS = ['🀇','🀈','🀉','🀙','🀚','🀛','🀐','🀑','🀒','🀄','🀅','🀆'];
let tiles = [], sel = null;
function init() {
const pool = [];
for (let i=0;i<18;i++) { const sym = ICONS[i % ICONS.length]; pool.push(sym, sym); }
for (let i=pool.length-1;i>0;i--) { const j=Math.floor(Math.random()*(i+1)); [pool[i],pool[j]]=[pool[j],pool[i]]; }
tiles = [];
let idx = 0;
for (let r=0;r<4;r++) for (let c=0;c<6;c++) tiles.push({id:idx, r, c, z:0, sym:pool[idx++], gone:false});
for (let r=1;r<3;r++) for (let c=1;c<5;c++) tiles.push({id:idx, r, c, z:1, sym:pool[idx++], gone:false});
for (let r=1;r<3;r++) for (let c=2;c<4;c++) tiles.push({id:idx, r, c, z:2, sym:pool[idx++], gone:false});
sel = null;
msg('配对消除顶层或左右无遮挡的麻将牌', 'Match free uncovered Mahjong tiles');
render();
}
function isFree(tile) {
if (tile.gone) return false;
const onTop = tiles.some(o => !o.gone && o.z === tile.z + 1 && Math.abs(o.r - tile.r) <= 0 && Math.abs(o.c - tile.c) <= 0);
if (onTop) return false;
const leftBlocked = tiles.some(o => !o.gone && o.z === tile.z && o.r === tile.r && o.c === tile.c - 1);
const rightBlocked = tiles.some(o => !o.gone && o.z === tile.z && o.r === tile.r && o.c === tile.c + 1);
return !(leftBlocked && rightBlocked);
}
function shuffleRemaining() {
const rem = tiles.filter(x => !x.gone);
const syms = rem.map(x => x.sym).sort(() => Math.random() - 0.5);
rem.forEach((x, i) => x.sym = syms[i]);
sel = null; sfx(500,0.08); render();
}
function render() {
const rem = tiles.filter(x => !x.gone).length;
stageEl.innerHTML = `
<div class="table-felt">
<div style="position:relative;width:360px;height:260px;margin:0 auto;">
${tiles.filter(x=>!x.gone).sort((a,b)=>a.z-b.z).map(t => {
const free = isFree(t), picked = sel && sel.id === t.id;
const left = 18 + t.c * 54 - t.z * 5, top = 16 + t.r * 58 - t.z * 6;
return `<button data-tid="${t.id}" style="position:absolute;left:${left}px;top:${top}px;width:48px;height:54px;border-radius:10px;border:2px solid ${picked?'var(--accent2)':free?'rgba(255,255,255,.45)':'rgba(255,255,255,.12)'};background:${picked?'#3b1d5e':t.z===2?'#251b3d':t.z===1?'#1c1530':'#130e22'};font-size:28px;opacity:${free?1:0.52};box-shadow:0 ${4+t.z*3}px 12px rgba(0,0,0,.6);">${t.sym}</button>`;
}).join('')}
</div>
<div class="btn-bar">
<span style="color:var(--muted);font-size:13px;">${t('剩余牌数','Tiles Left')}: ${rem}</span>
<button class="gbtn" id="mjShuf">${t('洗牌重排 Shuffle','Shuffle Tiles')}</button>
</div>
</div>`;
document.getElementById('mjShuf').onclick = shuffleRemaining;
stageEl.querySelectorAll('[data-tid]').forEach(b => b.onclick = () => {
const tile = tiles.find(x => x.id === Number(b.dataset.tid));
if (!isFree(tile)) { sfx(220,0.06,'sawtooth'); return; }
if (!sel || sel.id === tile.id) { sel = tile; sfx(520,0.05); render(); return; }
if (sel.sym === tile.sym) {
sel.gone = tile.gone = true; sel = null; addScore(50); sfx(720,0.09);
if (tiles.every(x => x.gone)) { chordWin(); addScore(300); msg('恭喜清空全部水晶麻将!','All Mahjong tiles cleared!'); setTimeout(init, 1200); }
} else { sel = tile; sfx(300,0.07); }
render();
});
}
window.resetGame = init;
init();
},
"silk-memory-match": function() {
const EMOJIS = ['💋','🍷','🌹','💎','🎭','🔥','🌙','🦋','🍒','👑'];
let level = 1, cards = [], open = [], streak = 0, lock = false;
function init() {
const pairs = Math.min(10, 5 + level);
const deck = EMOJIS.slice(0, pairs).flatMap((s, i) => [{id:i*2, s, done:false}, {id:i*2+1, s, done:false}]);
deck.sort(() => Math.random() - 0.5);
cards = deck; open = []; streak = 0; lock = false;
msg(`第 ${level} 关:翻开两张相同图案配对`, `Stage ${level}: Match all pairs`);
render();
}
function render() {
const cols = cards.length <= 12 ? 4 : 5;
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:grid;grid-template-columns:repeat(${cols},64px);gap:10px;justify-content:center;margin:8px auto;">
${cards.map((c, idx) => {
const show = c.done || open.includes(idx);
return `<button data-idx="${idx}" style="height:68px;border-radius:12px;border:2px solid ${c.done?'var(--accent2)':show?'var(--accent)':'rgba(255,255,255,.18)'};background:${show?'rgba(236,72,153,.22)':'rgba(255,255,255,.05)'};font-size:28px;">${show ? c.s : '✦'}</button>`;
}).join('')}
</div>
</div>`;
stageEl.querySelectorAll('[data-idx]').forEach(b => b.onclick = () => {
if (lock) return;
const idx = Number(b.dataset.idx);
if (cards[idx].done || open.includes(idx)) return;
open.push(idx); sfx(540,0.05); render();
if (open.length === 2) {
lock = true;
const [a, bIdx] = open;
if (cards[a].s === cards[bIdx].s) {
cards[a].done = cards[bIdx].done = true; open = []; lock = false; streak++;
addScore(30 * streak); sfx(760,0.09);
if (cards.every(c => c.done)) { chordWin(); level++; setTimeout(init, 700); }
else render();
} else {
streak = 0;
setTimeout(() => { open = []; lock = false; render(); }, 550);
}
}
});
}
window.resetGame = () => { level = 1; setScore(0); init(); };
init();
},
"neon-brick-breaker": function() {
stageEl.innerHTML = `<canvas id="cv" width="480" height="340" style="width:100%;max-width:480px;background:#090612;border-radius:14px;border:1px solid rgba(255,255,255,.15);touch-action:none;"></canvas>`;
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
let px = 190, pw = 92, bx = 240, by = 290, vx = 3.4, vy = -3.6, bricks = [], anim;
function resetBricks() {
bricks = [];
const colors = ['#f43f5e','#ec4899','#a855f7','#06b6d4','#eab308'];
for (let r=0;r<5;r++) for (let c=0;c<8;c++) bricks.push({x:18+c*56, y:28+r*24, w:48, h:16, hp:r===0?2:1, col:colors[r]});
bx = 240; by = 290; vx = 3.4 * (Math.random()>.5?1:-1); vy = -3.6;
}
resetBricks();
msg('左右移动鼠标或手指反弹光球击碎砖块', 'Move mouse or touch to bounce the ball');
const move = clientX => {
const rect = cv.getBoundingClientRect();
px = Math.max(8, Math.min(cv.width - pw - 8, ((clientX - rect.left) / rect.width) * cv.width - pw/2));
};
cv.onmousemove = e => move(e.clientX);
cv.ontouchmove = e => { e.preventDefault(); move(e.touches[0].clientX); };
function loop() {
ctx.clearRect(0,0,cv.width,cv.height);
bx += vx; by += vy;
if (bx < 8 || bx > cv.width - 8) { vx = -vx; sfx(420,0.03); }
if (by < 8) { vy = -vy; sfx(460,0.03); }
if (by > 306 && by < 322 && bx >= px && bx <= px + pw && vy > 0) {
vy = -Math.abs(vy); vx = ((bx - (px + pw/2)) / (pw/2)) * 4.5; sfx(580,0.05);
}
if (by > cv.height) { bx = px + pw/2; by = 295; vy = -3.6; chordLose(); }
for (const b of bricks) {
if (b.hp <= 0) continue;
if (bx > b.x && bx < b.x + b.w && by > b.y && by < b.y + b.h) {
b.hp--; vy = -vy; addScore(25); sfx(680,0.04); break;
}
}
if (bricks.every(b => b.hp <= 0)) { chordWin(); addScore(200); resetBricks(); }
for (const b of bricks) {
if (b.hp <= 0) continue;
ctx.fillStyle = b.col; ctx.beginPath(); ctx.roundRect(b.x, b.y, b.w, b.h, 5); ctx.fill();
}
ctx.fillStyle = '#22d3ee'; ctx.beginPath(); ctx.roundRect(px, 312, pw, 12, 6); ctx.fill();
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(bx, by, 7, 0, Math.PI*2); ctx.fill();
anim = requestAnimationFrame(loop);
}
window.resetGame = () => { cancelAnimationFrame(anim); setScore(0); resetBricks(); loop(); };
loop();
},
"velvet-bubble-shooter": function() {
stageEl.innerHTML = `<canvas id="cv" width="420" height="350" style="width:100%;max-width:420px;background:#0b0714;border-radius:14px;border:1px solid rgba(255,255,255,.15);cursor:crosshair;"></canvas>`;
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const COLS = ['#f43f5e','#38bdf8','#facc15','#4ade80','#c084fc'];
const R = 16, ROWS = 9, C = 11;
let grid = [], angle = -Math.PI/2, shot = null, curCol = COLS[0];
function init() {
grid = Array.from({length:ROWS}, (_, r) => Array.from({length:C}, () => r < 4 ? COLS[Math.floor(Math.random()*COLS.length)] : null));
curCol = COLS[Math.floor(Math.random()*COLS.length)]; shot = null;
msg('点击画布朝目标方向发射同色泡泡(3个以上消除)', 'Click or tap to shoot bubbles (3+ same color pops)');
}
function cellPos(r, c) { return { x: 26 + c * 34 + (r%2)*17, y: 24 + r * 30 }; }
function neighbors(r, c) {
const odd = r % 2 === 1;
const d = [[0,-1],[0,1],[-1,odd?0:-1],[-1,odd?1:0],[1,odd?0:-1],[1,odd?1:0]];
return d.map(([dr,dc])=>[r+dr,c+dc]).filter(([nr,nc])=>nr>=0&&nr<ROWS&&nc>=0&&nc<C);
}
function popCluster(sr, sc, col) {
const q = [[sr,sc]], seen = new Set([sr+','+sc]), cluster = [[sr,sc]];
while (q.length) {
const [r,c] = q.shift();
for (const [nr,nc] of neighbors(r,c)) {
const k = nr+','+nc;
if (!seen.has(k) && grid[nr][nc] === col) { seen.add(k); q.push([nr,nc]); cluster.push([nr,nc]); }
}
}
if (cluster.length >= 3) {
cluster.forEach(([r,c]) => grid[r][c] = null);
addScore(cluster.length * 30); sfx(740,0.08);
if (grid.flat().every(x => !x)) { chordWin(); addScore(300); init(); }
}
}
cv.onmousemove = e => {
const rect = cv.getBoundingClientRect();
const x = ((e.clientX - rect.left)/rect.width)*cv.width, y = ((e.clientY - rect.top)/rect.height)*cv.height;
angle = Math.max(-2.85, Math.min(-0.3, Math.atan2(y - 325, x - 210)));
};
cv.onclick = e => {
cv.onmousemove(e);
if (!shot) { shot = {x:210, y:325, vx:Math.cos(angle)*8, vy:Math.sin(angle)*8, col:curCol}; curCol = COLS[Math.floor(Math.random()*COLS.length)]; sfx(500,0.05); }
};
function loop() {
ctx.clearRect(0,0,cv.width,cv.height);
if (shot) {
shot.x += shot.vx; shot.y += shot.vy;
if (shot.x < R || shot.x > cv.width - R) shot.vx = -shot.vx;
let hit = shot.y < 26;
for (let r=0;r<ROWS && !hit;r++) for (let c=0;c<C;c++) if (grid[r][c]) {
const p = cellPos(r,c);
if (Math.hypot(shot.x - p.x, shot.y - p.y) < R * 1.85) { hit = true; break; }
}
if (hit) {
let best = null, bestD = 1e9;
for (let r=0;r<ROWS;r++) for (let c=0;c<C;c++) if (!grid[r][c]) {
const p = cellPos(r,c), d = Math.hypot(shot.x - p.x, shot.y - p.y);
if (d < bestD) { bestD = d; best = [r,c]; }
}
if (best) { grid[best[0]][best[1]] = shot.col; popCluster(best[0], best[1], shot.col); }
shot = null;
}
}
for (let r=0;r<ROWS;r++) for (let c=0;c<C;c++) if (grid[r][c]) {
const p = cellPos(r,c);
ctx.fillStyle = grid[r][c]; ctx.beginPath(); ctx.arc(p.x, p.y, R-1, 0, Math.PI*2); ctx.fill();
}
ctx.strokeStyle = 'rgba(255,255,255,0.35)'; ctx.setLineDash([5,5]);
ctx.beginPath(); ctx.moveTo(210,325); ctx.lineTo(210+Math.cos(angle)*90, 325+Math.sin(angle)*90); ctx.stroke(); ctx.setLineDash([]);
ctx.fillStyle = shot ? shot.col : curCol;
ctx.beginPath(); ctx.arc(shot ? shot.x : 210, shot ? shot.y : 325, R, 0, Math.PI*2); ctx.fill();
requestAnimationFrame(loop);
}
window.resetGame = init;
init(); loop();
},
"gem-crush-fever": function() {
const GEMS = ['💎','🔮','❤️','⚡','🍀','🌙'];
let N = 7, board = [], sel = null;
function init() {
board = Array.from({length:N}, () => Array.from({length:N}, () => GEMS[Math.floor(Math.random()*GEMS.length)]));
resolveMatches(false);
msg('点击相邻两颗宝石交换位置,凑成3颗以上连线消除', 'Tap two adjacent gems to swap and match 3+');
render();
}
function resolveMatches(scoreIt=true) {
let any = false;
for (let loop=0;loop<8;loop++) {
const mark = Array.from({length:N}, () => Array(N).fill(false));
for (let r=0;r<N;r++) for (let c=0;c<N-2;c++) if (board[r][c] && board[r][c]===board[r][c+1] && board[r][c]===board[r][c+2]) mark[r][c]=mark[r][c+1]=mark[r][c+2]=true;
for (let c=0;c<N;c++) for (let r=0;r<N-2;r++) if (board[r][c] && board[r][c]===board[r+1][c] && board[r][c]===board[r+2][c]) mark[r][c]=mark[r+1][c]=mark[r+2][c]=true;
let cnt = 0;
for (let r=0;r<N;r++) for (let c=0;c<N;c++) if (mark[r][c]) { board[r][c] = null; cnt++; }
if (!cnt) break;
any = true;
if (scoreIt) addScore(cnt * 20);
for (let c=0;c<N;c++) {
const col = [];
for (let r=N-1;r>=0;r--) if (board[r][c]) col.push(board[r][c]);
while (col.length < N) col.push(GEMS[Math.floor(Math.random()*GEMS.length)]);
for (let r=N-1,i=0;r>=0;r--,i++) board[r][c] = col[i];
}
}
return any;
}
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:grid;grid-template-columns:repeat(${N},44px);gap:6px;justify-content:center;">
${board.map((row, r) => row.map((g, c) => {
const picked = sel && sel[0]===r && sel[1]===c;
return `<button data-rc="${r},${c}" style="height:44px;border-radius:10px;border:2px solid ${picked?'var(--accent2)':'rgba(255,255,255,.14)'};background:${picked?'rgba(168,85,247,.35)':'rgba(255,255,255,.05)'};font-size:22px;">${g}</button>`;
}).join('')).join('')}
</div>
</div>`;
stageEl.querySelectorAll('[data-rc]').forEach(b => b.onclick = () => {
const [r,c] = b.dataset.rc.split(',').map(Number);
if (!sel) { sel = [r,c]; sfx(500,0.04); render(); return; }
const [sr,sc] = sel; sel = null;
if (Math.abs(sr-r) + Math.abs(sc-c) === 1) {
[board[sr][sc], board[r][c]] = [board[r][c], board[sr][sc]];
if (resolveMatches(true)) chordWin();
else { [board[sr][sc], board[r][c]] = [board[r][c], board[sr][sc]]; sfx(240,0.07,'sawtooth'); }
}
render();
});
}
window.resetGame = init;
init();
},
"cyber-block-drop": function() {
stageEl.innerHTML = `
<div style="display:flex;gap:14px;align-items:center;justify-content:center;flex-wrap:wrap;">
<canvas id="cv" width="220" height="352" style="background:#080510;border-radius:12px;border:2px solid var(--accent);"></canvas>
<div style="display:grid;grid-template-columns:repeat(2,78px);gap:8px;">
<button class="gbtn" id="tbL">◀</button>
<button class="gbtn" id="tbR">▶</button>
<button class="gbtn primary" id="tbRot">↻ ${t('旋转','Rot')}</button>
<button class="gbtn accent" id="tbDrop">⏬ ${t('落底','Drop')}</button>
</div>
</div>`;
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const W = 10, H = 16, S = 22;
const SHAPES = [
{m:[[1,1,1,1]], c:'#22d3ee'},
{m:[[1,1],[1,1]], c:'#facc15'},
{m:[[0,1,0],[1,1,1]], c:'#c084fc'},
{m:[[1,0,0],[1,1,1]], c:'#fb923c'},
{m:[[0,1,1],[1,1,0]], c:'#4ade80'},
{m:[[1,1,0],[0,1,1]], c:'#f43f5e'},
];
let grid = Array.from({length:H}, () => Array(W).fill(null)), cur, timer;
function spawn() {
const p = SHAPES[Math.floor(Math.random()*SHAPES.length)];
cur = {m:p.m.map(r=>[...r]), c:p.c, x:3, y:0};
if (collide(cur.x, cur.y, cur.m)) { grid = Array.from({length:H}, () => Array(W).fill(null)); chordLose(); }
}
function collide(nx, ny, mat) {
for (let r=0;r<mat.length;r++) for (let c=0;c<mat[0].length;c++) if (mat[r][c]) {
const x = nx + c, y = ny + r;
if (x < 0 || x >= W || y >= H || (y >= 0 && grid[y][x])) return true;
}
return false;
}
function lock() {
for (let r=0;r<cur.m.length;r++) for (let c=0;c<cur.m[0].length;c++) if (cur.m[r][c] && cur.y+r>=0) grid[cur.y+r][cur.x+c] = cur.c;
let cleared = 0;
for (let r=H-1;r>=0;r--) if (grid[r].every(Boolean)) { grid.splice(r,1); grid.unshift(Array(W).fill(null)); cleared++; r++; }
if (cleared) { addScore(cleared * cleared * 100); chordWin(); }
spawn();
}
function draw() {
ctx.clearRect(0,0,cv.width,cv.height);
for (let r=0;r<H;r++) for (let c=0;c<W;c++) if (grid[r][c]) { ctx.fillStyle = grid[r][c]; ctx.fillRect(c*S+1,r*S+1,S-2,S-2); }
for (let r=0;r<cur.m.length;r++) for (let c=0;c<cur.m[0].length;c++) if (cur.m[r][c]) { ctx.fillStyle = cur.c; ctx.fillRect((cur.x+c)*S+1,(cur.y+r)*S+1,S-2,S-2); }
}
function step() { if (!collide(cur.x, cur.y+1, cur.m)) cur.y++; else lock(); draw(); }
const rot = () => { const m = cur.m[0].map((_,i)=>cur.m.map(r=>r[i]).reverse()); if (!collide(cur.x,cur.y,m)) { cur.m=m; sfx(540,0.04); draw(); } };
document.getElementById('tbL').onclick = () => { if (!collide(cur.x-1,cur.y,cur.m)) { cur.x--; sfx(420,0.03); draw(); } };
document.getElementById('tbR').onclick = () => { if (!collide(cur.x+1,cur.y,cur.m)) { cur.x++; sfx(420,0.03); draw(); } };
document.getElementById('tbRot').onclick = rot;
document.getElementById('tbDrop').onclick = () => { while (!collide(cur.x,cur.y+1,cur.m)) cur.y++; lock(); sfx(640,0.06); draw(); };
window.onkeydown = e => {
if (e.key==='ArrowLeft') document.getElementById('tbL').click();
if (e.key==='ArrowRight') document.getElementById('tbR').click();
if (e.key==='ArrowUp') rot();
if (e.key==='ArrowDown') step();
if (e.key===' ') document.getElementById('tbDrop').click();
};
msg('方向键或右侧按钮控制方块旋转与落底', 'Use Arrow keys or buttons to rotate and drop blocks');
spawn(); draw();
timer = setInterval(step, 520);
window.resetGame = () => { clearInterval(timer); grid = Array.from({length:H}, () => Array(W).fill(null)); spawn(); draw(); timer = setInterval(step, 520); };
},
"serpent-neon-run": function() {
stageEl.innerHTML = `
<div style="display:flex;gap:14px;align-items:center;justify-content:center;flex-wrap:wrap;">
<canvas id="cv" width="320" height="320" style="background:#070b12;border-radius:14px;border:2px solid var(--accent);"></canvas>
<div style="display:grid;grid-template-columns:repeat(3,48px);gap:6px;">
<div></div><button class="gbtn" data-d="0,-1">▲</button><div></div>
<button class="gbtn" data-d="-1,0">◀</button><button class="gbtn" data-d="0,1">▼</button><button class="gbtn" data-d="1,0">▶</button>
</div>
</div>`;
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
const N = 16, S = 20;
let snake = [{x:8,y:8},{x:7,y:8},{x:6,y:8}], dir = {x:1,y:0}, food = {x:12,y:8}, timer;
function placeFood() { food = {x:Math.floor(Math.random()*N), y:Math.floor(Math.random()*N)}; }
function step() {
const head = {x:(snake[0].x + dir.x + N)%N, y:(snake[0].y + dir.y + N)%N};
if (snake.some(p => p.x===head.x && p.y===head.y)) { chordLose(); snake = [{x:8,y:8},{x:7,y:8},{x:6,y:8}]; dir={x:1,y:0}; return; }
snake.unshift(head);
if (head.x===food.x && head.y===food.y) { addScore(35); sfx(700,0.06); placeFood(); }
else snake.pop();
ctx.clearRect(0,0,cv.width,cv.height);
ctx.fillStyle = '#f43f5e'; ctx.beginPath(); ctx.arc(food.x*S+10, food.y*S+10, 8, 0, Math.PI*2); ctx.fill();
snake.forEach((p, i) => { ctx.fillStyle = i===0 ? '#4ade80' : '#22d3ee'; ctx.fillRect(p.x*S+2, p.y*S+2, S-4, S-4); });
}
stageEl.querySelectorAll('[data-d]').forEach(b => b.onclick = () => {
const [dx,dy] = b.dataset.d.split(',').map(Number);
if (dx !== -dir.x || dy !== -dir.y) dir = {x:dx,y:dy};
});
window.onkeydown = e => {
const m = {ArrowUp:[0,-1],ArrowDown:[0,1],ArrowLeft:[-1,0],ArrowRight:[1,0]}[e.key];
if (m && (m[0]!==-dir.x || m[1]!==-dir.y)) dir = {x:m[0],y:m[1]};
};
msg('吃掉霓虹果实不断变长,避免撞到自身', 'Eat glowing orbs to grow without hitting your tail');
timer = setInterval(step, 135);
window.resetGame = () => { clearInterval(timer); snake = [{x:8,y:8},{x:7,y:8},{x:6,y:8}]; setScore(0); timer = setInterval(step, 135); };
},
"flappy-cupid-wings": function() {
stageEl.innerHTML = `<canvas id="cv" width="440" height="320" style="width:100%;max-width:440px;background:linear-gradient(#120824,#261038);border-radius:14px;border:1px solid rgba(255,255,255,.18);cursor:pointer;"></canvas>`;
const cv = document.getElementById('cv'), ctx = cv.getContext('2d');
let y = 150, vy = 0, pipes = [{x:420, gapY:140, passed:false}], anim;
const flap = () => { vy = -5.2; sfx(560,0.05); };
cv.onclick = flap;
window.onkeydown = e => { if (e.key === ' ') flap(); };
msg('点击画面或按空格键扇动翅膀穿越光柱', 'Tap screen or press Space to flap through neon pillars');
function loop() {
ctx.clearRect(0,0,cv.width,cv.height);
vy += 0.26; y += vy;
if (pipes[pipes.length-1].x < 240) pipes.push({x:450, gapY:65 + Math.random()*170, passed:false});
for (const p of pipes) {
p.x -= 2.5;
ctx.fillStyle = '#fb7185';
ctx.fillRect(p.x, 0, 42, p.gapY - 52);
ctx.fillRect(p.x, p.gapY + 52, 42, cv.height);
if (!p.passed && p.x + 42 < 90) { p.passed = true; addScore(25); sfx(740,0.06); }
if (90 > p.x - 10 && 90 < p.x + 52 && (y < p.gapY - 44 || y > p.gapY + 44)) { y = 150; vy = 0; pipes = [{x:420, gapY:150, passed:false}]; chordLose(); }
}
if (y < 0 || y > cv.height) { y = 150; vy = 0; chordLose(); }
pipes = pipes.filter(p => p.x > -60);
ctx.font = '26px serif'; ctx.fillText('💘', 76, y + 8);
anim = requestAnimationFrame(loop);
}
window.resetGame = () => { cancelAnimationFrame(anim); y = 150; vy = 0; pipes = [{x:420, gapY:140, passed:false}]; loop(); };
loop();
},
"minesweeper-noir": function() {
const N = 9, MINES = 10;
let grid = [], flagMode = false, first = true, over = false;
function init() {
grid = Array.from({length:N}, (_,r) => Array.from({length:N}, (_,c) => ({r,c,m:false,open:false,flag:false,n:0})));
first = true; over = false;
msg('点击翻开安全格,切换插旗标记地雷', 'Reveal safe tiles or toggle Flag mode to mark mines');
render();
}
function plant(sr, sc) {
let left = MINES;
while (left > 0) {
const r = Math.floor(Math.random()*N), c = Math.floor(Math.random()*N);
if (!grid[r][c].m && (Math.abs(r-sr)>1 || Math.abs(c-sc)>1)) { grid[r][c].m = true; left--; }
}
for (let r=0;r<N;r++) for (let c=0;c<N;c++) {
let cnt = 0;
for (let dr=-1;dr<=1;dr++) for (let dc=-1;dc<=1;dc++) if (grid[r+dr]?.[c+dc]?.m) cnt++;
grid[r][c].n = cnt;
}
}
function flood(r, c) {
const cell = grid[r]?.[c];
if (!cell || cell.open || cell.flag) return;
cell.open = true; addScore(10);
if (cell.n === 0 && !cell.m) for (let dr=-1;dr<=1;dr++) for (let dc=-1;dc<=1;dc++) flood(r+dr, c+dc);
}
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div class="btn-bar" style="margin-bottom:8px;">
<button class="gbtn ${flagMode?'primary':''}" id="msFlag">${flagMode ? t('🚩 插旗模式 ON','🚩 Flag Mode ON') : t('🔍 翻开模式 Dig','🔍 Dig Mode')}</button>
</div>
<div style="display:grid;grid-template-columns:repeat(${N},34px);gap:4px;justify-content:center;">
${grid.flat().map(cell => `<button data-rc="${cell.r},${cell.c}" style="height:34px;border-radius:7px;border:1px solid rgba(255,255,255,.15);background:${cell.open?'rgba(255,255,255,.12)':'#1a1326'};font-weight:700;color:${cell.m?'#f43f5e':'#38bdf8'};">${cell.open ? (cell.m ? '💣' : (cell.n || '')) : (cell.flag ? '🚩' : '')}</button>`).join('')}
</div>
</div>`;
document.getElementById('msFlag').onclick = () => { flagMode = !flagMode; render(); };
stageEl.querySelectorAll('[data-rc]').forEach(b => {
b.oncontextmenu = e => { e.preventDefault(); const [r,c]=b.dataset.rc.split(',').map(Number); grid[r][c].flag=!grid[r][c].flag; render(); };
b.onclick = () => {
if (over) return;
const [r,c] = b.dataset.rc.split(',').map(Number);
const cell = grid[r][c];
if (flagMode) { if (!cell.open) cell.flag = !cell.flag; sfx(500,0.04); render(); return; }
if (cell.flag) return;
if (first) { first = false; plant(r,c); }
if (cell.m) { over = true; grid.flat().forEach(x => { if (x.m) x.open = true; }); chordLose(); msg('踩中地雷!点击重开再来一局','BOOM! Hit a mine!'); }
else {
flood(r,c); sfx(560,0.04);
if (grid.flat().filter(x => !x.m).every(x => x.open)) { over = true; chordWin(); addScore(200); msg('完美排雷通关!','All mines cleared!'); }
}
render();
};
});
}
window.resetGame = init;
init();
},
"sudoku-after-dark": function() {
const SOL = [
[5,3,4,6,7,8,9,1,2],[6,7,2,1,9,5,3,4,8],[1,9,8,3,4,2,5,6,7],
[8,5,9,7,6,1,4,2,3],[4,2,6,8,5,3,7,9,1],[7,1,3,9,2,4,8,5,6],
[9,6,1,5,3,7,2,8,4],[2,8,7,4,1,9,6,3,5],[3,4,5,2,8,6,1,7,9]
];
let board = [], fixed = [], sel = [0,0];
function init() {
const shift = Math.floor(Math.random()*9);
board = SOL.map(r => r.map(v => ((v + shift - 1) % 9) + 1));
fixed = board.map(r => r.map(() => Math.random() < 0.56));
for (let r=0;r<9;r++) for (let c=0;c<9;c++) if (!fixed[r][c]) board[r][c] = 0;
msg('选中空格填入1-9,使每行每列每宫不重复', 'Select a cell and enter 1-9 without repeats');
render();
}
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:grid;grid-template-columns:repeat(9,33px);gap:2px;justify-content:center;margin-bottom:10px;">
${board.map((row,r) => row.map((v,c) => {
const isSel = sel[0]===r && sel[1]===c;
return `<button data-rc="${r},${c}" style="height:33px;border-radius:5px;border:1px solid ${isSel?'var(--accent2)':'rgba(255,255,255,.15)'};background:${fixed[r][c]?'rgba(255,255,255,.12)':'#140f24'};color:${fixed[r][c]?'#fff':'var(--accent)'};font-weight:700;">${v||''}</button>`;
}).join('')).join('')}
</div>
<div class="btn-bar">
${[1,2,3,4,5,6,7,8,9].map(n => `<button class="gbtn" data-n="${n}">${n}</button>`).join('')}
</div>
</div>`;
stageEl.querySelectorAll('[data-rc]').forEach(b => b.onclick = () => { sel = b.dataset.rc.split(',').map(Number); render(); });
stageEl.querySelectorAll('[data-n]').forEach(b => b.onclick = () => {
const [r,c] = sel;
if (fixed[r][c]) return;
board[r][c] = Number(b.dataset.n); addScore(15); sfx(600,0.04);
if (board.flat().every(x => x > 0)) { chordWin(); addScore(300); msg('数独解谜完成!','Sudoku Solved!'); }
render();
});
}
window.resetGame = init;
init();
},
"sliding-portrait-15": function() {
let tiles = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0], moves = 0;
function shuffle() {
tiles = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0];
for (let i=0;i<120;i++) {
const z = tiles.indexOf(0), r = Math.floor(z/4), c = z%4, nbs = [];
if (r>0) nbs.push(z-4); if (r<3) nbs.push(z+4); if (c>0) nbs.push(z-1); if (c<3) nbs.push(z+1);
const pick = nbs[Math.floor(Math.random()*nbs.length)];
[tiles[z], tiles[pick]] = [tiles[pick], tiles[z]];
}
moves = 0; msg('点击空格旁边的滑块,按 1-15 顺序复原棋盘', 'Slide tiles into order 1–15'); render();
}
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:grid;grid-template-columns:repeat(4,62px);gap:8px;justify-content:center;">
${tiles.map((v, i) => v === 0 ? `<div style="height:62px;border-radius:12px;background:rgba(0,0,0,.35);"></div>` : `<button data-i="${i}" style="height:62px;border-radius:12px;border:1px solid rgba(255,255,255,.22);background:linear-gradient(135deg,#2e1646,#1a102b);font-size:20px;font-weight:800;color:${v===i+1?'#4ade80':'#fff'};">${v}</button>`).join('')}
</div>
<div style="margin-top:8px;color:var(--muted);font-size:13px;">${t('步数 Moves','Moves')}: ${moves}</div>
</div>`;
stageEl.querySelectorAll('[data-i]').forEach(b => b.onclick = () => {
const i = Number(b.dataset.i), z = tiles.indexOf(0);
if (Math.abs(Math.floor(i/4)-Math.floor(z/4)) + Math.abs((i%4)-(z%4)) === 1) {
[tiles[i], tiles[z]] = [tiles[z], tiles[i]]; moves++; sfx(520,0.04);
if (tiles.slice(0,15).every((v,idx)=>v===idx+1)) { chordWin(); addScore(500); msg('恭喜复原15滑块拼图!','15-Puzzle Solved!'); }
render();
}
});
}
window.resetGame = shuffle;
shuffle();
},
"reversi-royal": function() {
let b = [];
function init() {
b = Array.from({length:8}, () => Array(8).fill(0));
b[3][3]=2; b[3][4]=1; b[4][3]=1; b[4][4]=2;
msg('你执黑先行(⚫),夹击翻转白子', 'You play Black (⚫). Outflank White discs!');
render();
}
function flips(r, c, p) {
if (b[r][c] !== 0) return [];
const opp = 3 - p, res = [];
for (let dr=-1;dr<=1;dr++) for (let dc=-1;dc<=1;dc++) if (dr||dc) {
let nr=r+dr, nc=c+dc, line=[];
while (nr>=0&&nr<8&&nc>=0&&nc<8&&b[nr][nc]===opp) { line.push([nr,nc]); nr+=dr; nc+=dc; }
if (line.length && nr>=0&&nr<8&&nc>=0&&nc<8&&b[nr][nc]===p) res.push(...line);
}
return res;
}
function render() {
const black = b.flat().filter(x=>x===1).length, white = b.flat().filter(x=>x===2).length;
stageEl.innerHTML = `
<div class="table-felt">
<div style="margin-bottom:6px;font-size:14px;">⚫ ${black} : ⚪ ${white}</div>
<div style="display:grid;grid-template-columns:repeat(8,36px);gap:3px;justify-content:center;">
${b.map((row,r)=>row.map((v,c)=>{
const valid = flips(r,c,1).length > 0;
return `<button data-rc="${r},${c}" style="height:36px;border-radius:7px;background:#0d3b2e;border:1px solid ${valid?'#34d399':'rgba(255,255,255,.12)'};font-size:20px;">${v===1?'⚫':v===2?'⚪':valid?'·':''}</button>`;
}).join('')).join('')}
</div>
</div>`;
stageEl.querySelectorAll('[data-rc]').forEach(btn => btn.onclick = () => {
const [r,c] = btn.dataset.rc.split(',').map(Number);
const f = flips(r,c,1);
if (!f.length) return;
b[r][c] = 1; f.forEach(([fr,fc])=>b[fr][fc]=1); addScore(f.length*15); sfx(580,0.05);
setTimeout(() => {
const moves = [];
for (let ar=0;ar<8;ar++) for (let ac=0;ac<8;ac++) { const af=flips(ar,ac,2); if (af.length) moves.push({ar,ac,af}); }
if (moves.length) {
moves.sort((x,y)=>y.af.length-x.af.length);
const m = moves[0]; b[m.ar][m.ac]=2; m.af.forEach(([fr,fc])=>b[fr][fc]=2);
}
render();
}, 240);
render();
});
}
window.resetGame = init;
init();
},
"gomoku-velvet": function() {
const N = 11;
let b = [], over = false;
function init() {
b = Array.from({length:N}, () => Array(N).fill(0)); over = false;
msg('你执黑子(●),在棋盘上连成五子即获胜', 'Connect 5 stones in a row to win');
render();
}
function checkWin(p) {
const dirs = [[0,1],[1,0],[1,1],[1,-1]];
for (let r=0;r<N;r++) for (let c=0;c<N;c++) if (b[r][c]===p) {
for (const [dr,dc] of dirs) {
let cnt = 0;
for (let k=0;k<5;k++) if (b[r+dr*k]?.[c+dc*k]===p) cnt++;
if (cnt===5) return true;
}
}
return false;
}
function aiMove() {
let best = [5,5], bestS = -1;
for (let r=0;r<N;r++) for (let c=0;c<N;c++) if (b[r][c]===0) {
let sc = (5 - Math.hypot(r-5, c-5));
for (const [dr,dc] of [[0,1],[1,0],[1,1],[1,-1]]) {
for (const p of [1,2]) {
let run = 0;
for (let k=1;k<=4;k++) if (b[r+dr*k]?.[c+dc*k]===p) run++; else break;
for (let k=1;k<=4;k++) if (b[r-dr*k]?.[c-dc*k]===p) run++; else break;
if (run>=4) sc += p===2 ? 10000 : 8000;
else if (run===3) sc += p===2 ? 900 : 750;
else if (run===2) sc += 120;
}
}
if (sc > bestS) { bestS = sc; best = [r,c]; }
}
b[best[0]][best[1]] = 2;
if (checkWin(2)) { over = true; chordLose(); msg('AI 五子连珠获胜!','AI connected 5 in a row!'); }
}
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:grid;grid-template-columns:repeat(${N},30px);gap:2px;justify-content:center;">
${b.map((row,r)=>row.map((v,c)=>`<button data-rc="${r},${c}" style="height:30px;background:#2b1d12;border:1px solid rgba(255,255,255,.15);border-radius:4px;font-size:18px;">${v===1?'⚫':v===2?'⚪':''}</button>`).join('')).join('')}
</div>
</div>`;
stageEl.querySelectorAll('[data-rc]').forEach(btn => btn.onclick = () => {
if (over) return;
const [r,c] = btn.dataset.rc.split(',').map(Number);
if (b[r][c] !== 0) return;
b[r][c] = 1; sfx(540,0.04);
if (checkWin(1)) { over = true; chordWin(); addScore(250); msg('五子连珠!你赢了!','Five in a row! You win!'); render(); return; }
aiMove(); render();
});
}
window.resetGame = init;
init();
},
"checkers-lounge": function() {
let b = [], sel = null;
function init() {
b = Array.from({length:8}, (_,r) => Array.from({length:8}, (_,c) => (r+c)%2===1 ? (r<3 ? 2 : r>4 ? 1 : 0) : 0));
sel = null;
msg('点击红棋(🔴)斜向推进或跳过黑棋吃子', 'Select your Red piece (🔴) to advance or jump');
render();
}
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:grid;grid-template-columns:repeat(8,36px);gap:2px;justify-content:center;">
${b.map((row,r)=>row.map((v,c)=>{
const dark = (r+c)%2===1, picked = sel && sel[0]===r && sel[1]===c;
return `<button data-rc="${r},${c}" style="height:36px;background:${picked?'#7c2d12':dark?'#1e1b2e':'#312e48'};border:1px solid rgba(255,255,255,.1);font-size:20px;">${v===1?'🔴':v===3?'👑':v===2?'⚫':''}</button>`;
}).join('')).join('')}
</div>
</div>`;
stageEl.querySelectorAll('[data-rc]').forEach(btn => btn.onclick = () => {
const [r,c] = btn.dataset.rc.split(',').map(Number);
if (b[r][c]===1 || b[r][c]===3) { sel = [r,c]; sfx(500,0.04); render(); return; }
if (!sel || b[r][c]!==0) return;
const [sr,sc] = sel, p = b[sr][sc];
const dr = r - sr, dc = c - sc;
if ((p===1 && dr===-1 && Math.abs(dc)===1) || (p===3 && Math.abs(dr)===1 && Math.abs(dc)===1)) {
b[r][c] = r===0 ? 3 : p; b[sr][sc] = 0; sel = null; sfx(580,0.05); aiStep(); render();
} else if (Math.abs(dr)===2 && Math.abs(dc)===2 && b[(sr+r)/2][(sc+c)/2]===2) {
b[r][c] = r===0 ? 3 : p; b[sr][sc] = 0; b[(sr+r)/2][(sc+c)/2] = 0; sel = null; addScore(40); chordWin(); aiStep(); render();
}
});
}
function aiStep() {
for (let r=0;r<7;r++) for (let c=0;c<8;c++) if (b[r][c]===2) {
for (const dc of [-1,1]) if (b[r+1]?.[c+dc]===0) { b[r+1][c+dc]=2; b[r][c]=0; return; }
}
}
window.resetGame = init;
init();
},
"solitaire-klondike-noir": function() {
let cols = [], stock = [], waste = [], found = { '♠':0, '♥':0, '♦':0, '♣':0 };
function init() {
const d = makeDeck();
cols = Array.from({length:7}, (_,i) => d.splice(0, i+1));
stock = d; waste = [stock.pop()]; found = { '♠':0, '♥':0, '♦':0, '♣':0 };
msg('点击牌列顶牌自动移至基座(A→K)或红黑交替接龙', 'Tap any bottom card to auto-move to Foundation or Columns');
render();
}
function tryMove(card, fromArr) {
if (card.v === found[card.s] + 1) { found[card.s]++; fromArr.pop(); addScore(25); sfx(680,0.06); return true; }
for (const col of cols) {
if (col === fromArr) continue;
const top = col[col.length-1];
if ((!top && card.v===13) || (top && top.red !== card.red && top.v === card.v + 1)) {
col.push(fromArr.pop()); addScore(10); sfx(520,0.05); return true;
}
}
return false;
}
function render() {
stageEl.innerHTML = `
<div class="table-felt">
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
<div style="display:flex;gap:8px;align-items:center;">
<button class="gbtn primary" id="solDraw">${t('发牌 Stock','Draw')} (${stock.length})</button>
<div id="solWaste" style="cursor:pointer;">${waste.length ? cardHTML(waste[waste.length-1]) : ''}</div>
</div>
<div style="display:flex;gap:8px;">
${SUITS.map(s=>`<div style="padding:8px 12px;border-radius:10px;border:1px solid rgba(255,255,255,.2);background:rgba(0,0,0,.3);">${s} ${found[s] ? RANKS[found[s]-1] : '-'}</div>`).join('')}
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:6px;">
${cols.map((col, ci) => `<div data-col="${ci}" style="min-height:130px;background:rgba(255,255,255,.03);border-radius:10px;padding:4px;cursor:pointer;display:flex;flex-direction:column;gap:4px;align-items:center;">${col.map(c=>cardHTML(c)).join('')}</div>`).join('')}
</div>
</div>`;
document.getElementById('solDraw').onclick = () => {
if (!stock.length) { stock = waste.reverse(); waste = []; }
if (stock.length) waste.push(stock.pop());
sfx(480,0.04); render();
};
const wEl = document.getElementById('solWaste');
if (wEl) wEl.onclick = () => { if (waste.length && tryMove(waste[waste.length-1], waste)) render(); };
stageEl.querySelectorAll('[data-col]').forEach(el => el.onclick = () => {
const col = cols[Number(el.dataset.col)];
if (col.length && tryMove(col[col.length-1], col)) render();
});
}
window.resetGame = init;
init();
},
"dice-yacht-club": function() {
let dice = [1,2,3,4,5], held = [false,false,false,false,false], rolls = 3, round = 1;
function roll() {
if (rolls <= 0) return;
dice = dice.map((v, i) => held[i] ? v : 1 + Math.floor(Math.random()*6));
rolls--; sfx(540,0.06,'triangle'); render();
}
function calcCats() {
const cnt = [0,0,0,0,0,0,0]; dice.forEach(d => cnt[d]++);
const sum = dice.reduce((a,b)=>a+b,0);
const maxC = Math.max(...cnt);
const yacht = maxC===5 ? 50 : 0;
const four = maxC>=4 ? sum : 0;
const full = (cnt.includes(3) && cnt.includes(2)) || maxC===5 ? 25 : 0;
return [
{name:t('全选总和 Choice','Choice Sum'), pts:sum},
{name:t('四条 Four of a Kind','4 of a Kind'), pts:four},
{name:t('葫芦 Full House','Full House (25)'), pts:full},
{name:t('快艇 Yacht (50)','Yacht 5-Kind (50)'), pts:yacht},
];
}
function render() {
const cats = calcCats();
stageEl.innerHTML = `
<div class="table-felt">
<div style="font-size:14px;color:var(--muted);">${t('第','Round')} ${round} ${t('局 · 剩余投掷','· Rolls Left')}: <b style="color:#fff">${rolls}</b></div>
<div style="display:flex;gap:10px;justify-content:center;margin:12px 0;">
${dice.map((d,i)=>`<button data-di="${i}" style="width:56px;height:56px;border-radius:14px;border:2px solid ${held[i]?'var(--accent2)':'rgba(255,255,255,.22)'};background:${held[i]?'rgba(167,139,250,.3)':'#171126'};font-size:24px;font-weight:800;">🎲${d}<div style="font-size:10px;color:var(--accent2)">${held[i]?t('锁定','HOLD'):''}</div></button>`).join('')}
</div>
<div class="btn-bar">
<button class="gbtn primary" id="ydRoll" ${rolls<=0?'disabled':''}>${t('摇骰子 Roll Dice','Roll Dice')} (${rolls})</button>
</div>
<div class="btn-bar" style="margin-top:8px;">
${cats.map(c => `<button class="gbtn" data-pts="${c.pts}">${c.name}: +${c.pts}</button>`).join('')}
</div>
</div>`;
document.getElementById('ydRoll').onclick = roll;
stageEl.querySelectorAll('[data-di]').forEach(b => b.onclick = () => { const i=Number(b.dataset.di); held[i]=!held[i]; sfx(480,0.04); render(); });
stageEl.querySelectorAll('[data-pts]').forEach(b => b.onclick = () => {
const pts = Number(b.dataset.pts); addScore(pts);
if (pts >= 25) chordWin(); else sfx(620,0.06);
round++; rolls = 3; held = [false,false,false,false,false]; roll();
});
}
msg('每局可摇3次骰子,点击骰子锁定保留,选择计分项结算', 'Roll up to 3 times, click dice to Hold, then score');
window.resetGame = () => { round = 1; rolls = 3; held = [false,false,false,false,false]; setScore(0); roll(); };
roll();
}
};
window.onLangChange = () => { if (ENGINES[GAME_ID]) ENGINES[GAME_ID](); };
document.getElementById('resetBtn').onclick = () => { if (window.resetGame) window.resetGame(); else ENGINES[GAME_ID](); };
ENGINES[GAME_ID]();
</script>
</body>
</html>