🎮ArcadeLab

斗地主Fight the Landlord

by SwiftGalaxy49
1416 lines43.8 KB
▶ Play
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>斗地主Fight the Landlord</title>
<style>
  * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
  html, body {
    margin: 0; padding: 0; height: 100%;
    font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
    background: radial-gradient(ellipse at 50% 40%, #1b6b3a 0%, #0d3d21 60%, #062512 100%);
    color: #fff; overflow: hidden; user-select: none;
  }
  #app { position: relative; display: flex; flex-direction: column; height: 100vh; width: 100vw; }

  .top-row {
    flex: 0 0 auto;
    display: flex; justify-content: space-between; align-items: flex-start;
    padding: 8px 10px 0;
    gap: 6px;
  }
  .panel { width: 31%; max-width: 280px; min-width: 110px; }
  .panel.right { text-align: right; }
  .player-tag {
    display: inline-flex; align-items: center; gap: 6px;
    background: rgba(0,0,0,.45);
    border: 1px solid rgba(255,255,255,.16);
    border-radius: 20px; padding: 5px 12px;
    font-size: 13px; white-space: nowrap;
    backdrop-filter: blur(4px);
  }
  .player-tag.landlord { border-color: #ffcc33; box-shadow: 0 0 10px rgba(255,204,51,.5); }
  .player-tag .badge {
    background: linear-gradient(180deg,#ffd75e,#e8a400); color:#4a2c00;
    font-size: 11px; font-weight: bold; padding: 1px 6px; border-radius: 8px;
  }
  .player-tag .badge.blue {
    background: linear-gradient(180deg,#9fd8ff,#4a9fe8); color:#00243d;
  }
  .player-tag .badge.gray {
    background: linear-gradient(180deg,#cfcfcf,#8d8d8d); color:#222;
  }
  .player-tag .cnt { color: #9fe0b6; font-size: 12px; }
  .player-tag.thinking { animation: pulse 1s infinite; }
  @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.45} }

  .played {
    display: flex; flex-wrap: wrap; gap: 2px;
    margin-top: 6px; min-height: 52px;
    align-items: center;
  }
  .panel.right .played { justify-content: flex-end; }

  .mid {
    flex: 1 1 auto; position: relative;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 12px; min-height: 0;
  }

  /* ============ 底牌区域(可收起 / 展开) ============ */
  .bottom-cards-wrap {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
  }
  .bottom-cards { display: flex; gap: 4px; }

  .bottom-toggle {
    display: inline-flex; align-items: center; gap: 5px;
    background: rgba(0,0,0,.38);
    border: 1px solid rgba(255,255,255,.14);
    border-radius: 13px;
    padding: 3px 11px;
    font-size: 12px; color: #8ecfa8; letter-spacing: 2px;
    line-height: 1.4;
    transition: background .15s, border-color .15s, transform .15s, color .15s;
  }
  .bottom-toggle .arrow {
    display: inline-block;
    font-size: 11px; letter-spacing: 0;
    transition: transform .22s ease;
  }
  .bottom-toggle.clickable {
    cursor: pointer;
    border-color: rgba(255,210,77,.5);
    color: #ffe9a8;
    box-shadow: 0 0 8px rgba(255,210,77,.18);
  }
  .bottom-toggle.clickable:hover { background: rgba(0,0,0,.62); }
  .bottom-toggle.clickable:active { transform: scale(.94); }

  .bottom-cards-wrap.collapsed .bottom-cards { display: none; }
  .bottom-cards-wrap.collapsed .arrow { transform: rotate(-90deg); }

  .center-play {
    display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
    gap: 2px; min-height: 60px; max-width: 92vw;
  }

  .card {
    position: relative;
    flex: 0 0 auto;
    width: 62px; height: 88px;
    background: linear-gradient(180deg, #ffffff 0%, #f3f3f3 100%);
    border-radius: 7px;
    border: 1px solid #adadad;
    box-shadow: 0 1px 4px rgba(0,0,0,.42), inset 0 0 0 1px rgba(255,255,255,.7);
    overflow: hidden;
    color: #1a1a1a;
    transition: transform .12s ease, box-shadow .12s ease;
  }
  .card.red { color: #d21f26; }
  .card.black { color: #1a1a1a; }

  .card .corner {
    position: absolute;
    display: flex; flex-direction: column; align-items: center;
    line-height: 1;
    font-weight: 800;
    font-family: "Arial Black", Arial, "Helvetica Neue", sans-serif;
    letter-spacing: -0.5px;
  }
  .card .corner .r { font-size: 16px; }
  .card .corner .s { font-size: 13px; margin-top: 1px; }
  .card .corner.tl { top: 3px; left: 4px; }
  .card .corner.br { bottom: 3px; right: 4px; transform: rotate(180deg); }

  .card .pip {
    position: absolute; inset: 0;
    display: flex; align-items: center; justify-content: center;
    font-size: 30px; opacity: .88;
  }
  .card .pip.joker-pip { font-size: 26px; font-weight: 800; }

  .card.back {
    background: repeating-linear-gradient(45deg, #2b4d94 0 6px, #24407c 6px 12px);
    border: 1px solid #16295a;
    box-shadow: 0 1px 4px rgba(0,0,0,.5);
  }
  .card.back::after {
    content: ""; position: absolute; inset: 4px;
    border: 1px solid rgba(255,255,255,.22); border-radius: 4px;
  }

  .card.small { width: 46px; height: 66px; border-radius: 5px; }
  .card.small .corner.tl { top: 2px; left: 3px; }
  .card.small .corner.br { bottom: 2px; right: 3px; }
  .card.small .corner .r { font-size: 12px; }
  .card.small .corner .s { font-size: 10px; margin-top: 0; }
  .card.small .pip { font-size: 20px; }
  .card.small .pip.joker-pip { font-size: 17px; }

  .bottom-area { flex: 0 0 auto; padding-bottom: 4px; }
  .hand {
    display: flex; justify-content: center; align-items: flex-end;
    height: 118px; padding: 26px 6px 0;
    position: relative; overflow: visible;
  }
  .hand .card { cursor: pointer; }
  .hand .card:hover { transform: translateY(-6px); }
  .hand .card.sel {
    transform: translateY(-22px);
    box-shadow: 0 8px 16px rgba(255, 215, 0, .9), 0 0 0 2px #ffd24d;
  }
  .hand .card.sel:hover { transform: translateY(-24px); }

  .controls {
    display: flex; justify-content: center; align-items: center; gap: 12px;
    padding: 4px 10px 12px;
    flex-wrap: wrap;
  }
  .btn {
    padding: 9px 22px; border-radius: 22px; border: 1px solid rgba(255,255,255,.3);
    background: rgba(255,255,255,.12); color: #fff;
    font-size: 15px; font-weight: 600; cursor: pointer;
    transition: all .15s; backdrop-filter: blur(4px);
    font-family: inherit;
  }
  .btn:hover:not(:disabled) { background: rgba(255,255,255,.24); transform: translateY(-1px); }
  .btn:active:not(:disabled) { transform: translateY(1px); }
  .btn:disabled { opacity: .32; cursor: not-allowed; }
  .btn.primary {
    background: linear-gradient(180deg, #ffd75e, #e8a400);
    color: #4a2c00; border-color: #ffdf7e;
    box-shadow: 0 3px 10px rgba(232,164,0,.4);
  }
  .btn.primary:hover:not(:disabled) { background: linear-gradient(180deg, #ffe58c, #f5b41a); }
  .btn.sort { background: rgba(80,180,255,.2); border-color: rgba(120,200,255,.5); }

  #toast {
    position: fixed; left: 50%; bottom: 42%; transform: translateX(-50%);
    background: rgba(0,0,0,.84); color: #ffe9a8;
    padding: 10px 24px; border-radius: 20px; font-size: 15px;
    opacity: 0; pointer-events: none; transition: opacity .25s;
    z-index: 500; white-space: nowrap; border: 1px solid rgba(255,210,77,.4);
  }
  #toast.show { opacity: 1; }

  #overlay {
    position: fixed; inset: 0; background: rgba(0,0,0,.72);
    display: none; align-items: center; justify-content: center;
    z-index: 900; backdrop-filter: blur(3px);
    padding: 12px;
  }
  #overlay.show { display: flex; }

  .result-box {
    background: linear-gradient(180deg, #1e5c38, #0e3620);
    border: 2px solid #ffd24d; border-radius: 18px;
    padding: 26px 30px;
    text-align: center;
    box-shadow: 0 12px 40px rgba(0,0,0,.6);
    animation: pop .3s ease;
    max-width: min(94vw, 560px);
    max-height: 90vh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  @keyframes pop { from { transform: scale(.8); opacity: 0; } to { transform: scale(1); opacity: 1; } }
  .result-box h1 { margin: 0 0 8px; font-size: 30px; color: #ffd24d; letter-spacing: 4px; }
  .result-box p { margin: 0 0 16px; font-size: 14px; color: #a9e3c0; line-height: 1.6; }

  /* ============ ★ 结算时的“剩余手牌”展示区 ============ */
  .reveal-section {
    display: flex; flex-direction: column; gap: 12px;
    margin: 0 0 20px;
    padding: 12px 12px 14px;
    background: rgba(0,0,0,.24);
    border: 1px solid rgba(255,255,255,.09);
    border-radius: 12px;
    text-align: left;
  }
  .reveal-title {
    font-size: 11px; color: #8ecfa8; letter-spacing: 3px;
    text-align: center;
    padding-bottom: 2px;
    border-bottom: 1px dashed rgba(255,255,255,.12);
  }
  .reveal-row { display: flex; flex-direction: column; gap: 5px; }
  .reveal-name {
    display: flex; align-items: center; gap: 8px;
    font-size: 12px; color: #a9e3c0;
  }
  .reveal-name .who { font-weight: 700; }
  .reveal-name.landlord .who { color: #ffd24d; }
  .reveal-name.winner .who { color: #7dffb0; }
  .reveal-name .n {
    margin-left: auto;
    font-size: 11px; color: #7fb894;
    background: rgba(255,255,255,.07);
    padding: 1px 7px; border-radius: 8px;
  }
  .reveal-cards {
    display: flex; flex-wrap: wrap; gap: 4px 3px;
    min-height: 22px;
  }
  .reveal-empty { font-size: 12px; color: #6fae86; padding: 4px 2px; font-style: italic; }

  .tag-you { color: #ffd24d; }

  @media (max-width: 560px) {
    .card { width: 52px; height: 74px; border-radius: 6px; }
    .card .corner .r { font-size: 13px; }
    .card .corner .s { font-size: 11px; }
    .card .corner.tl { top: 2px; left: 3px; }
    .card .corner.br { bottom: 2px; right: 3px; }
    .card .pip { font-size: 24px; }
    .card .pip.joker-pip { font-size: 21px; }

    .card.small { width: 40px; height: 58px; border-radius: 5px; }
    .card.small .corner .r { font-size: 11px; }
    .card.small .corner .s { font-size: 9px; }
    .card.small .pip { font-size: 17px; }
    .card.small .pip.joker-pip { font-size: 14px; }

    .hand { height: 104px; padding: 24px 4px 0; }
    .hand .card.sel { transform: translateY(-18px); }
    .hand .card.sel:hover { transform: translateY(-18px); }

    .btn { padding: 8px 16px; font-size: 13px; }
    .player-tag { font-size: 11px; padding: 4px 9px; }
    .played { min-height: 46px; }

    .bottom-toggle { font-size: 11px; padding: 2px 9px; gap: 4px; }
    .bottom-toggle .arrow { font-size: 10px; }

    .result-box { padding: 20px 16px; }
    .result-box h1 { font-size: 24px; letter-spacing: 2px; }
    .result-box p { font-size: 13px; margin-bottom: 12px; }
    .reveal-section { padding: 10px; gap: 10px; }
  }
</style>
</head>
<body>
<div id="app">
  <div class="top-row">
    <div class="panel left">
      <div class="player-tag" id="tag-2"><span class="name">左家</span></div>
      <div class="played" id="played-2"></div>
    </div>

    <div class="bottom-cards-wrap" id="bottomWrap">
      <div class="bottom-toggle" id="bottomToggle">
        <span class="txt">底 牌</span><span class="arrow">▾</span>
      </div>
      <div class="bottom-cards" id="bottomCards"></div>
    </div>

    <div class="panel right">
      <div class="player-tag" id="tag-1"><span class="name">右家</span></div>
      <div class="played" id="played-1"></div>
    </div>
  </div>

  <div class="mid">
    <div class="center-play" id="played-0"></div>
  </div>

  <div class="bottom-area">
    <div class="hand" id="myHand"></div>
    <div class="controls" id="controls"></div>
  </div>
</div>

<div id="toast"></div>
<div id="overlay"><div class="result-box" id="resultBox"></div></div>

<script>
/* =========================================================
   基础数据
   ========================================================= */
const SUITS = ['♠', '♥', '♣', '♦'];
const SUIT_ORDER = { '♠': 0, '♥': 1, '♣': 2, '♦': 3, 'joker': 4 };
const RANK_NAME = { 3:'3',4:'4',5:'5',6:'6',7:'7',8:'8',9:'9',10:'10',11:'J',12:'Q',13:'K',14:'A',15:'2' };
const NAMES = ['你', '右家', '左家'];

/* 叫地主 AI 门槛 */
const BID_THRESHOLD = { call: 21, rob: 28 };

function createDeck() {
  const deck = [];
  let id = 0;
  for (let v = 3; v <= 15; v++) {
    for (const s of SUITS) deck.push({ id: id++, value: v, rank: RANK_NAME[v], suit: s });
  }
  deck.push({ id: id++, value: 16, rank: '小王', suit: 'joker' });
  deck.push({ id: id++, value: 17, rank: '大王', suit: 'joker' });
  return deck;
}

function shuffle(a) {
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

/* =========================================================
   牌型判定
   ========================================================= */
function isConsec(vals) {
  for (let i = 1; i < vals.length; i++) if (vals[i] !== vals[i - 1] + 1) return false;
  return true;
}

function findSeq(arr, m) {
  for (let i = 0; i + m <= arr.length; i++) {
    let ok = true;
    for (let j = 1; j < m; j++) {
      if (arr[i + j] !== arr[i + j - 1] + 1) { ok = false; break; }
    }
    if (ok) return arr.slice(i, i + m);
  }
  return null;
}

function getType(cards) {
  if (!cards || cards.length === 0) return null;
  const n = cards.length;
  const cnt = new Map();
  for (const c of cards) cnt.set(c.value, (cnt.get(c.value) || 0) + 1);
  const vals = [...cnt.keys()].sort((a, b) => a - b);
  const C = v => cnt.get(v) || 0;

  if (n === 2 && C(16) === 1 && C(17) === 1) return { type: 'rocket', value: 100, size: 2 };
  if (n === 4 && vals.length === 1) return { type: 'bomb', value: vals[0], size: 4 };
  if (n === 1) return { type: 'single', value: vals[0], size: 1 };
  if (n === 2 && vals.length === 1) return { type: 'pair', value: vals[0], size: 2 };
  if (n === 3 && vals.length === 1) return { type: 'trio', value: vals[0], size: 3 };

  if (n === 4 && vals.length === 2) {
    const t = vals.find(v => C(v) === 3);
    if (t !== undefined) return { type: 'trio1', value: t, size: 4 };
  }
  if (n === 5 && vals.length === 2) {
    const t = vals.find(v => C(v) === 3);
    if (t !== undefined && vals.some(v => C(v) === 2)) return { type: 'trio2', value: t, size: 5 };
  }
  if (n >= 5 && vals.length === n && vals[n - 1] <= 14 && isConsec(vals)) {
    return { type: 'straight', value: vals[n - 1], size: n };
  }
  if (n >= 6 && n % 2 === 0 && vals.length === n / 2 &&
      vals.every(v => C(v) === 2) && vals[vals.length - 1] <= 14 && isConsec(vals)) {
    return { type: 'pairseq', value: vals[vals.length - 1], size: n };
  }

  const triVals = vals.filter(v => C(v) >= 3 && v <= 14);

  for (let m = Math.floor(n / 3); m >= 2; m--) {
    if (n !== 3 * m) continue;
    const s = findSeq(triVals, m);
    if (s) return { type: 'plane', value: s[s.length - 1], size: n, m };
  }
  for (let m = Math.floor(n / 4); m >= 2; m--) {
    if (n !== 4 * m) continue;
    const s = findSeq(triVals, m);
    if (!s) continue;
    const rem = new Map(cnt);
    s.forEach(v => rem.set(v, rem.get(v) - 3));
    let ok = true, total = 0;
    rem.forEach((c) => {
      if (c <= 0) return;
      total += c;
      if (c >= 3) ok = false;
    });
    const rocketInWing = (rem.get(16) || 0) > 0 && (rem.get(17) || 0) > 0;
    if (ok && total === m && !rocketInWing) {
      return { type: 'plane1', value: s[s.length - 1], size: n, m };
    }
  }
  for (let m = Math.floor(n / 5); m >= 2; m--) {
    if (n !== 5 * m) continue;
    const s = findSeq(triVals, m);
    if (!s) continue;
    const rem = new Map(cnt);
    s.forEach(v => rem.set(v, rem.get(v) - 3));
    let ok = true, pairs = 0;
    rem.forEach(c => { if (c > 0) { if (c !== 2) ok = false; else pairs++; } });
    if (ok && pairs === m) return { type: 'plane2', value: s[s.length - 1], size: n, m };
  }
  if (n === 6) {
    const q = vals.find(v => C(v) === 4);
    if (q !== undefined) {
      const rem = new Map(cnt); rem.set(q, 0);
      let total = 0; rem.forEach(c => total += c);
      const rocket = (rem.get(16) || 0) > 0 && (rem.get(17) || 0) > 0;
      if (total === 2 && !rocket) return { type: 'four2', value: q, size: 6 };
    }
  }
  if (n === 8) {
    const q = vals.find(v => C(v) === 4);
    if (q !== undefined) {
      const rem = new Map(cnt); rem.set(q, 0);
      let ok = true, pairs = 0;
      rem.forEach(c => { if (c > 0) { if (c !== 2) ok = false; else pairs++; } });
      if (ok && pairs === 2) return { type: 'four2p', value: q, size: 8 };
    }
  }
  return null;
}

function canBeat(a, b) {
  if (!a) return false;
  if (!b) return true;
  if (a.type === 'rocket') return true;
  if (b.type === 'rocket') return false;
  if (a.type === 'bomb' && b.type === 'bomb') return a.value > b.value;
  if (a.type === 'bomb') return true;
  if (b.type === 'bomb') return false;
  if (a.type !== b.type) return false;
  if (a.size !== b.size) return false;
  return a.value > b.value;
}

/* =========================================================
   游戏状态
   ========================================================= */
const G = {
  hands: [[], [], []],
  bottom: [],
  landlord: -1,
  current: 0,
  lastPlay: null,
  tableCards: [[], [], []],
  passShown: [false, false, false],
  phase: 'idle',
  selected: new Set(),
  sortMode: 'value',
  bid: null,
  bidOptions: null,
  bidMark: ['', '', ''],
  playCount: [0, 0, 0],
  aiTilt: [0, 0, 0],
  bottomOpen: true,
};

/* =========================================================
   DOM
   ========================================================= */
const $ = id => document.getElementById(id);
const elHand = $('myHand');
const elControls = $('controls');
const elPlayed = [ $('played-0'), $('played-1'), $('played-2') ];
const elTag = [ null, $('tag-1'), $('tag-2') ];
const elBottom = $('bottomCards');
const elBottomWrap = $('bottomWrap');
const elBottomToggle = $('bottomToggle');
const elToast = $('toast');
const elOverlay = $('overlay');
const elResult = $('resultBox');

let toastTimer = null;
function toast(msg) {
  elToast.textContent = msg;
  elToast.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer = setTimeout(() => elToast.classList.remove('show'), 1600);
}

/* 点击箭头:展开 / 收起底牌面板 */
elBottomToggle.addEventListener('click', () => {
  if (G.landlord === -1) return;
  G.bottomOpen = !G.bottomOpen;
  renderBottom();
});

/* =========================================================
   牌面渲染
   ========================================================= */
function cardEl(card, faceUp, small) {
  const d = document.createElement('div');
  d.className = 'card' + (small ? ' small' : '');
  if (!faceUp) { d.classList.add('back'); return d; }

  const isJoker = card.value >= 16;
  const red = isJoker ? (card.value === 17) : (card.suit === '♥' || card.suit === '♦');
  d.classList.add(red ? 'red' : 'black');

  if (isJoker) {
    const t = card.value === 17 ? '大' : '小';
    d.innerHTML =
      `<div class="corner tl"><span class="r">${t}</span><span class="s">王</span></div>` +
      `<div class="pip joker-pip">${t}王</div>` +
      `<div class="corner br"><span class="r">${t}</span><span class="s">王</span></div>`;
  } else {
    d.innerHTML =
      `<div class="corner tl"><span class="r">${card.rank}</span><span class="s">${card.suit}</span></div>` +
      `<div class="pip">${card.suit}</div>` +
      `<div class="corner br"><span class="r">${card.rank}</span><span class="s">${card.suit}</span></div>`;
  }
  return d;
}

/* =========================================================
   排序
   ========================================================= */
function sortHand(hand) {
  if (G.sortMode === 'value') {
    hand.sort((a, b) => b.value - a.value || SUIT_ORDER[a.suit] - SUIT_ORDER[b.suit]);
  } else {
    const cnt = {};
    hand.forEach(c => cnt[c.value] = (cnt[c.value] || 0) + 1);
    const cat = c => {
      const n = cnt[c.value];
      if (n >= 4) return 0;
      if (n === 3) return 1;
      if (n === 2) return 2;
      return 3;
    };
    hand.sort((a, b) =>
      cat(a) - cat(b) || b.value - a.value || SUIT_ORDER[a.suit] - SUIT_ORDER[b.suit]);
  }
}

/* 统一的大→小排序,供结算展示用 */
function sortDesc(hand) {
  return hand.slice().sort(
    (a, b) => b.value - a.value || SUIT_ORDER[a.suit] - SUIT_ORDER[b.suit]
  );
}

/* =========================================================
   手牌布局
   ========================================================= */
function layoutMyHand() {
  const cards = [...elHand.children];
  const n = cards.length;
  if (!n) return;
  const cw = cards[0].offsetWidth || 62;
  const avail = elHand.clientWidth - 10;
  const isPhone = window.innerWidth <= 560;
  const minStep = isPhone ? 15 : 21;

  let step = (n === 1) ? cw : (avail - cw) / (n - 1);
  step = Math.min(step, cw - 2);
  step = Math.max(step, minStep);

  cards.forEach((c, i) => {
    c.style.marginLeft = (i === 0) ? '0px' : (step - cw) + 'px';
  });
}
window.addEventListener('resize', () => { layoutMyHand(); });

/* =========================================================
   渲染
   ========================================================= */
function renderMyHand() {
  const hand = G.hands[0];
  sortHand(hand);
  elHand.innerHTML = '';

  hand.forEach((card, i) => {
    const ce = cardEl(card, true, false);
    const sel = G.selected.has(card.id);
    ce.style.zIndex = sel ? (200 + i) : i;
    if (sel) ce.classList.add('sel');

    ce.addEventListener('click', () => {
      if (G.phase !== 'play' || G.current !== 0) return;
      if (G.selected.has(card.id)) {
        G.selected.delete(card.id);
        ce.classList.remove('sel');
        ce.style.zIndex = i;
      } else {
        G.selected.add(card.id);
        ce.classList.add('sel');
        ce.style.zIndex = 200 + i;
      }
      renderControls();
    });
    elHand.appendChild(ce);
  });

  requestAnimationFrame(layoutMyHand);
}

function renderPlayed(p) {
  const el = elPlayed[p];
  el.innerHTML = '';
  const cards = G.tableCards[p];
  if (cards && cards.length) {
    cards.forEach(c => el.appendChild(cardEl(c, true, true)));
  } else if (G.passShown[p]) {
    const s = document.createElement('div');
    s.style.cssText = 'font-size:14px;color:#ffb0b0;font-weight:bold;letter-spacing:2px;padding:4px 10px;';
    s.textContent = '不出';
    el.appendChild(s);
  }
}

/* 底牌渲染:确定地主后默认收起,只留可点击的箭头标签 */
function renderBottom() {
  const revealed = G.landlord !== -1;

  elBottom.innerHTML = '';
  G.bottom.forEach(c => elBottom.appendChild(cardEl(c, revealed, true)));

  elBottomToggle.classList.toggle('clickable', revealed);
  elBottomWrap.classList.toggle('collapsed', revealed && !G.bottomOpen);
}

function renderTags() {
  for (let p = 1; p <= 2; p++) {
    const el = elTag[p];
    const isL = p === G.landlord;
    el.classList.toggle('landlord', isL);
    el.classList.toggle('thinking', G.phase === 'play' && G.current === p);

    let extra = '';
    if (G.phase === 'bid' && G.bidMark[p]) {
      const cls = (G.bidMark[p] === '不叫' || G.bidMark[p] === '不抢') ? 'gray' : 'blue';
      extra = `<span class="badge ${cls}">${G.bidMark[p]}</span>`;
    }

    el.innerHTML =
      `<span class="name">${NAMES[p]}</span>` +
      `<span class="cnt">${G.hands[p].length}张</span>` +
      (isL ? '<span class="badge">地主</span>' : '') +
      extra;
  }
}

function renderControls() {
  elControls.innerHTML = '';

  if (G.phase === 'over') {
    const b = document.createElement('button');
    b.className = 'btn primary';
    b.textContent = '再来一局';
    b.onclick = () => { elOverlay.classList.remove('show'); newGame(); };
    elControls.appendChild(b);
    return;
  }

  if (G.bidOptions && G.bidOptions.length) {
    G.bidOptions.forEach(opt => {
      const b = document.createElement('button');
      b.className = 'btn' + (opt.indexOf('不') === -1 ? ' primary' : '');
      b.textContent = opt;
      b.onclick = () => {
        const action =
          opt === '叫地主' ? 'call' :
          opt === '抢地主' ? 'rob' :
          opt === '不叫'   ? 'pass' : 'norob';
        G.bidOptions = null;
        renderControls();
        submitBid(action);
      };
      elControls.appendChild(b);
    });
    return;
  }

  if (G.phase === 'play') {
    const sb = document.createElement('button');
    sb.className = 'btn sort';
    sb.textContent = '理牌:' + (G.sortMode === 'value' ? '按点数' : '按牌型');
    sb.onclick = () => {
      G.sortMode = G.sortMode === 'value' ? 'type' : 'value';
      renderMyHand();
      renderControls();
    };
    elControls.appendChild(sb);

    const myTurn = G.current === 0;
    const canPass = myTurn && G.lastPlay && G.lastPlay.player !== 0;

    const pb = document.createElement('button');
    pb.className = 'btn';
    pb.textContent = '不出';
    pb.disabled = !canPass;
    pb.onclick = humanPass;
    elControls.appendChild(pb);

    const ob = document.createElement('button');
    ob.className = 'btn primary';
    ob.textContent = '出牌';
    ob.disabled = !myTurn;
    ob.onclick = humanPlay;
    elControls.appendChild(ob);
    return;
  }

  if (G.phase === 'bid') {
    const tip = document.createElement('div');
    tip.style.cssText = 'font-size:14px;color:#a9e3c0;';
    tip.textContent = '等待其他玩家叫地主…';
    elControls.appendChild(tip);
    return;
  }

  if (G.phase === 'idle') {
    const b = document.createElement('button');
    b.className = 'btn primary';
    b.textContent = '开始游戏';
    b.onclick = () => newGame();
    elControls.appendChild(b);
  }
}

function render() {
  renderMyHand();
  renderPlayed(0); renderPlayed(1); renderPlayed(2);
  renderBottom();
  renderTags();
  renderControls();
}

/* =========================================================
   发牌
   ========================================================= */
function newGame() {
  G.selected.clear();
  G.landlord = -1;
  G.lastPlay = null;
  G.tableCards = [[], [], []];
  G.passShown = [false, false, false];
  G.playCount = [0, 0, 0];
  G.bidOptions = null;
  G.bidMark = ['', '', ''];
  G.phase = 'bid';
  G.bottomOpen = true;

  G.aiTilt = [0, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 6];

  const deck = shuffle(createDeck());
  G.hands = [[], [], []];
  for (let i = 0; i < 51; i++) G.hands[i % 3].push(deck[i]);
  G.bottom = deck.slice(51, 54);
  G.hands.forEach(h => sortHand(h));

  render();

  const start = Math.floor(Math.random() * 3);
  startBid(start);
}

/* =========================================================
   叫地主 / 抢地主 状态机
   ========================================================= */
function startBid(start) {
  G.bid = {
    order: [start, (start + 1) % 3, (start + 2) % 3],
    idx: 0,
    stage: 'call',
    firstCaller: -1,
    lastRobber: -1,
    robQueue: [],
    robIdx: 0,
    robRound: 0,
    robRoundRobbed: false,
  };
  G.bidMark = ['', '', ''];
  stepBid();
}

function stepBid() {
  const B = G.bid;
  if (!B || G.phase !== 'bid') return;

  if (B.stage === 'call') {
    if (B.idx >= B.order.length) {
      if (B.firstCaller === -1) {
        toast('三家都不叫,重新发牌');
        setTimeout(() => { if (G.phase === 'bid') newGame(); }, 1500);
        return;
      }
      B.stage = 'rob';
      B.robRound = 1;
      B.robQueue = [ (B.firstCaller + 1) % 3, (B.firstCaller + 2) % 3 ];
      B.robIdx = 0;
      B.robRoundRobbed = false;
      stepBid();
      return;
    }
    askBid(B.order[B.idx], 'call');
    return;
  }

  if (B.robIdx >= B.robQueue.length) {
    if (B.robRoundRobbed) {
      if (B.robRound >= 3) {
        finishBid(B.lastRobber);
        return;
      }
      B.robRound++;
      B.robQueue = [ (B.lastRobber + 1) % 3, (B.lastRobber + 2) % 3 ];
      B.robIdx = 0;
      B.robRoundRobbed = false;
      stepBid();
      return;
    }
    finishBid(B.lastRobber !== -1 ? B.lastRobber : B.firstCaller);
    return;
  }

  askBid(B.robQueue[B.robIdx], 'rob');
}

function askBid(p, stage) {
  if (p === 0) {
    G.bidOptions = stage === 'call' ? ['叫地主', '不叫'] : ['抢地主', '不抢'];
    renderControls();
  } else {
    renderControls();
    const delay = 550 + Math.random() * 600;
    setTimeout(() => {
      if (G.phase !== 'bid' || !G.bid) return;
      aiBidDecision(p, stage);
    }, delay);
  }
}

function submitBid(action) {
  const B = G.bid;
  if (!B || G.phase !== 'bid') return;

  let p;
  if (B.stage === 'call') p = B.order[B.idx];
  else                    p = B.robQueue[B.robIdx];
  if (p === undefined) return;

  if (B.stage === 'call') {
    if (action === 'call') { B.firstCaller = p; G.bidMark[p] = '叫地主'; }
    else                   { G.bidMark[p] = '不叫'; }
    B.idx++;
  } else {
    if (action === 'rob') {
      B.lastRobber = p;
      B.robRoundRobbed = true;
      G.bidMark[p] = '抢地主';
    } else {
      G.bidMark[p] = '不抢';
    }
    B.robIdx++;
  }

  G.bidOptions = null;
  render();
  setTimeout(stepBid, 320);
}

function finishBid(landlord) {
  G.bid = null;
  G.bidOptions = null;
  if (landlord === -1) {
    toast('无人叫地主,重新发牌');
    setTimeout(() => { if (G.phase === 'bid') newGame(); }, 1500);
    return;
  }
  setLandlord(landlord);
}

/* 底牌一定塞进地主手里,然后强制重渲染 */
function setLandlord(p) {
  G.landlord = p;
  G.bottomOpen = false;

  for (const c of G.bottom) G.hands[p].push(c);
  sortHand(G.hands[p]);

  G.phase = 'play';
  G.current = p;
  G.lastPlay = null;
  G.tableCards = [[], [], []];
  G.passShown = [false, false, false];
  G.bidOptions = null;
  G.bidMark = ['', '', ''];
  G.selected.clear();

  render();

  const n = G.hands[p].length;
  toast(`${NAMES[p]} 成为地主 · 手牌 ${n} 张`);

  if (p !== 0) setTimeout(aiTurn, 1000);
}

/* =========================================================
   AI 叫地主评估函数
   ========================================================= */
function evaluateHand(hand) {
  const cnt = new Map();
  for (const c of hand) cnt.set(c.value, (cnt.get(c.value) || 0) + 1);
  const C = v => cnt.get(v) || 0;
  const vals = [...cnt.keys()].sort((a, b) => a - b);

  let score = 0;

  score += C(17) * 9;
  score += C(16) * 7;
  score += C(15) * 4;
  score += C(14) * 2.2;
  score += C(13) * 1.2;

  for (const v of vals) if (C(v) === 4) score += 11;
  if (C(16) && C(17)) score += 7;

  for (const v of vals) if (v <= 14 && C(v) === 3) score += 2.5;

  let run = 0, bestRun = 0;
  for (let v = 3; v <= 14; v++) {
    if (C(v) >= 1) { run++; if (run > bestRun) bestRun = run; }
    else run = 0;
  }
  if (bestRun >= 5) score += (bestRun - 4) * 1.8;

  let prun = 0, bestPrun = 0;
  for (let v = 3; v <= 14; v++) {
    if (C(v) >= 2) { prun++; if (prun > bestPrun) bestPrun = prun; }
    else prun = 0;
  }
  if (bestPrun >= 3) score += (bestPrun - 2) * 1.5;

  let junk = 0;
  for (const v of vals) if (C(v) === 1 && v <= 11) junk++;
  score -= junk * 1.2;

  let smallPairs = 0;
  for (const v of vals) if (C(v) === 2 && v <= 10) smallPairs++;
  score -= smallPairs * 0.6;

  return score;
}

function aiBidDecision(p, stage) {
  const base = evaluateHand(G.hands[p]);
  const s = base + (G.aiTilt[p] || 0);

  let action;
  if (stage === 'call') {
    action = s >= BID_THRESHOLD.call ? 'call' : 'pass';
  } else {
    action = s >= BID_THRESHOLD.rob ? 'rob' : 'norob';
  }
  submitBid(action);
}

/* =========================================================
   出牌流程
   ========================================================= */
function playCards(p, cards) {
  if (!cards || cards.length === 0) return;
  const type = getType(cards);
  if (!type) return;

  const ids = new Set(cards.map(c => c.id));
  G.hands[p] = G.hands[p].filter(c => !ids.has(c.id));
  G.playCount[p]++;

  G.lastPlay = { player: p, cards: cards.slice(), type };
  G.tableCards = [[], [], []];
  G.tableCards[p] = cards.slice();
  G.passShown = [false, false, false];

  if (p === 0) G.selected.clear();

  render();

  if (G.hands[p].length === 0) { endGame(p); return; }

  G.current = (p + 1) % 3;
  render();

  if (G.current !== 0) setTimeout(aiTurn, 850);
}

function passTurn(p) {
  G.passShown[p] = true;
  const next = (p + 1) % 3;

  if (G.lastPlay && G.lastPlay.player === next) {
    G.tableCards = [[], [], []];
    G.passShown = [false, false, false];
  } else {
    G.tableCards[p] = [];
  }

  G.current = next;
  render();

  if (G.phase !== 'play') return;
  if (next !== 0) setTimeout(aiTurn, 850);
}

/* =========================================================
   玩家操作
   ========================================================= */
function humanPlay() {
  if (G.phase !== 'play' || G.current !== 0) return;
  const cards = G.hands[0].filter(c => G.selected.has(c.id));
  if (cards.length === 0) { toast('请先选择要出的牌'); return; }

  const t = getType(cards);
  if (!t) { toast('牌型不合法'); return; }

  if (G.lastPlay && G.lastPlay.player !== 0) {
    if (!canBeat(t, G.lastPlay.type)) { toast('管不上上家的牌'); return; }
  }
  playCards(0, cards);
}

function humanPass() {
  if (G.phase !== 'play' || G.current !== 0) return;
  if (!G.lastPlay || G.lastPlay.player === 0) return;
  G.selected.clear();
  renderMyHand();
  passTurn(0);
}

/* =========================================================
   搜索可压制牌型
   ========================================================= */
function findBeats(hand, lt) {
  const res = [];
  if (!lt || lt.type === 'rocket') return res;

  const byVal = new Map();
  hand.forEach(c => {
    if (!byVal.has(c.value)) byVal.set(c.value, []);
    byVal.get(c.value).push(c);
  });
  const vals = [...byVal.keys()].sort((a, b) => a - b);
  const cnt = v => (byVal.get(v) || []).length;
  const take = (v, k) => byVal.get(v).slice(0, k);

  switch (lt.type) {
    case 'single':
      for (const v of vals) if (v > lt.value) res.push(take(v, 1));
      break;
    case 'pair':
      for (const v of vals) if (v > lt.value && cnt(v) >= 2) res.push(take(v, 2));
      break;
    case 'trio':
      for (const v of vals) if (v > lt.value && cnt(v) >= 3) res.push(take(v, 3));
      break;
    case 'trio1':
      for (const v of vals) {
        if (v <= lt.value || cnt(v) < 3) continue;
        const k = vals.find(x => x !== v && cnt(x) >= 1);
        if (k !== undefined) res.push([...take(v, 3), take(k, 1)[0]]);
      }
      break;
    case 'trio2':
      for (const v of vals) {
        if (v <= lt.value || cnt(v) < 3) continue;
        const k = vals.find(x => x !== v && cnt(x) >= 2);
        if (k !== undefined) res.push([...take(v, 3), ...take(k, 2)]);
      }
      break;
    case 'straight': {
      const len = lt.size;
      for (let st = 3; st + len - 1 <= 14; st++) {
        const end = st + len - 1;
        if (end <= lt.value) continue;
        let ok = true;
        for (let v = st; v <= end; v++) if (cnt(v) < 1) { ok = false; break; }
        if (!ok) continue;
        const cards = [];
        for (let v = st; v <= end; v++) cards.push(byVal.get(v)[0]);
        res.push(cards);
      }
      break;
    }
    case 'pairseq': {
      const len = lt.size / 2;
      for (let st = 3; st + len - 1 <= 14; st++) {
        const end = st + len - 1;
        if (end <= lt.value) continue;
        let ok = true;
        for (let v = st; v <= end; v++) if (cnt(v) < 2) { ok = false; break; }
        if (!ok) continue;
        const cards = [];
        for (let v = st; v <= end; v++) cards.push(...take(v, 2));
        res.push(cards);
      }
      break;
    }
    case 'plane': {
      const m = lt.m;
      for (let st = 3; st + m - 1 <= 14; st++) {
        const end = st + m - 1;
        if (end <= lt.value) continue;
        let ok = true;
        for (let v = st; v <= end; v++) if (cnt(v) < 3) { ok = false; break; }
        if (!ok) continue;
        const cards = [];
        for (let v = st; v <= end; v++) cards.push(...take(v, 3));
        res.push(cards);
      }
      break;
    }
    case 'plane1': {
      const m = lt.m;
      for (let st = 3; st + m - 1 <= 14; st++) {
        const end = st + m - 1;
        if (end <= lt.value) continue;
        let ok = true;
        for (let v = st; v <= end; v++) if (cnt(v) < 3) { ok = false; break; }
        if (!ok) continue;
        const used = new Set();
        for (let v = st; v <= end; v++) used.add(v);
        const wings = vals.filter(v => !used.has(v))
                          .sort((a, b) => cnt(a) - cnt(b) || a - b);
        if (wings.length < m) continue;
        const cards = [];
        for (let v = st; v <= end; v++) cards.push(...take(v, 3));
        for (let i = 0; i < m; i++) cards.push(byVal.get(wings[i])[0]);
        res.push(cards);
      }
      break;
    }
    case 'plane2': {
      const m = lt.m;
      for (let st = 3; st + m - 1 <= 14; st++) {
        const end = st + m - 1;
        if (end <= lt.value) continue;
        let ok = true;
        for (let v = st; v <= end; v++) if (cnt(v) < 3) { ok = false; break; }
        if (!ok) continue;
        const used = new Set();
        for (let v = st; v <= end; v++) used.add(v);
        const wings = vals.filter(v => !used.has(v) && cnt(v) >= 2);
        if (wings.length < m) continue;
        const cards = [];
        for (let v = st; v <= end; v++) cards.push(...take(v, 3));
        for (let i = 0; i < m; i++) cards.push(...take(wings[i], 2));
        res.push(cards);
      }
      break;
    }
    default:
      break;
  }

  for (const v of vals) {
    if (cnt(v) === 4) {
      if (lt.type !== 'bomb' || v > lt.value) res.push(take(v, 4));
    }
  }
  if (cnt(16) >= 1 && cnt(17) >= 1) res.push([byVal.get(16)[0], byVal.get(17)[0]]);

  return res;
}

/* =========================================================
   AI 出牌
   ========================================================= */
function aiChooseLead(p, hand) {
  if (hand.length === 0) return [];

  const whole = getType(hand);
  if (whole) return hand.slice();

  const byVal = new Map();
  hand.forEach(c => {
    if (!byVal.has(c.value)) byVal.set(c.value, []);
    byVal.get(c.value).push(c);
  });
  const vals = [...byVal.keys()].sort((a, b) => a - b);
  const cnt = v => (byVal.get(v) || []).length;

  for (let len = 8; len >= 5; len--) {
    for (let st = 3; st + len - 1 <= 14; st++) {
      let ok = true;
      for (let v = st; v < st + len; v++) if (cnt(v) < 1) { ok = false; break; }
      if (!ok) continue;
      let safe = true;
      for (let v = st; v < st + len; v++) if (cnt(v) >= 3) { safe = false; break; }
      if (!safe) continue;
      const cards = [];
      for (let v = st; v < st + len; v++) cards.push(byVal.get(v)[0]);
      return cards;
    }
  }
  for (let len = 4; len >= 3; len--) {
    for (let st = 3; st + len - 1 <= 14; st++) {
      let ok = true;
      for (let v = st; v < st + len; v++) if (cnt(v) < 2) { ok = false; break; }
      if (!ok) continue;
      let safe = true;
      for (let v = st; v < st + len; v++) if (cnt(v) >= 4) { safe = false; break; }
      if (!safe) continue;
      const cards = [];
      for (let v = st; v < st + len; v++) cards.push(...byVal.get(v).slice(0, 2));
      return cards;
    }
  }
  for (let len = 4; len >= 2; len--) {
    for (let st = 3; st + len - 1 <= 14; st++) {
      let ok = true;
      for (let v = st; v < st + len; v++) if (cnt(v) < 3) { ok = false; break; }
      if (!ok) continue;
      const cards = [];
      for (let v = st; v < st + len; v++) cards.push(...byVal.get(v).slice(0, 3));
      return cards;
    }
  }
  for (const v of vals) if (cnt(v) === 3) return byVal.get(v).slice(0, 3);
  for (const v of vals) if (cnt(v) === 2 && v < 15) return byVal.get(v).slice(0, 2);
  for (const v of vals) if (cnt(v) === 1 && v < 16) return [byVal.get(v)[0]];
  for (const v of vals) if (cnt(v) === 1) return [byVal.get(v)[0]];
  for (const v of vals) if (cnt(v) === 2) return byVal.get(v).slice(0, 2);
  for (const v of vals) if (cnt(v) === 4) return byVal.get(v).slice(0, 4);
  return [hand[hand.length - 1]];
}

function aiTurn() {
  if (G.phase !== 'play') return;
  const p = G.current;
  if (p === 0) return;
  const hand = G.hands[p];
  if (hand.length === 0) return;

  const needFollow = G.lastPlay && G.lastPlay.player !== p;

  if (!needFollow) {
    playCards(p, aiChooseLead(p, hand));
    return;
  }

  const lastP = G.lastPlay.player;
  const iAmLandlord = p === G.landlord;
  const lastIsLandlord = lastP === G.landlord;

  if (!iAmLandlord && !lastIsLandlord) { passTurn(p); return; }

  const beats = findBeats(hand, G.lastPlay.type);
  if (beats.length === 0) { passTurn(p); return; }

  const tagged = beats.map(cards => ({ cards, t: getType(cards) })).filter(x => x.t);
  const normal = tagged.filter(x => x.t.type !== 'bomb' && x.t.type !== 'rocket');
  const bombs  = tagged.filter(x => x.t.type === 'bomb' || x.t.type === 'rocket');

  if (normal.length > 0) {
    normal.sort((a, b) => a.t.value - b.t.value || a.cards.length - b.cards.length);
    const pick = normal[0];
    const oppLeft = iAmLandlord
      ? Math.min(G.hands[1].length, G.hands[2].length)
      : G.hands[G.landlord].length;
    if (pick.t.value <= 12 || oppLeft <= 3 || hand.length <= 6 || pick.t.value >= 15) {
      playCards(p, pick.cards);
      return;
    }
    if (Math.random() < 0.75) { playCards(p, pick.cards); return; }
    passTurn(p);
    return;
  }

  const enemyIdx = iAmLandlord ? [1, 2] : [G.landlord];
  const enemyMin = Math.min(...enemyIdx.map(i => G.hands[i].length));

  if (bombs.length > 0 && (enemyMin <= 3 || hand.length <= 5)) {
    bombs.sort((a, b) => a.t.value - b.t.value);
    playCards(p, bombs[0].cards);
    return;
  }

  passTurn(p);
}

/* =========================================================
   ★ 结算:不立刻重开,先把三家剩余手牌按人展示出来
   ========================================================= */
function endGame(winner) {
  G.phase = 'over';

  const landlordWin = winner === G.landlord;
  let extra = '';
  if (landlordWin) {
    if (G.playCount[1] === 0 && G.playCount[2] === 0) extra = '🌸 春天!';
  } else {
    if (G.playCount[G.landlord] === 1) extra = '🌸 反春天!';
  }

  render();   // 先把桌面上最后一手牌、各家剩余张数刷新出来

  const title = landlordWin ? '地主胜利' : '农民胜利';
  const youWin = (G.landlord === 0) === landlordWin;

  /* ---------- 组装结算面板 ---------- */
  elResult.innerHTML = '';

  const h1 = document.createElement('h1');
  h1.textContent = title;
  elResult.appendChild(h1);

  const pEl = document.createElement('p');
  pEl.innerHTML = (extra ? extra + '<br>' : '') +
    (youWin ? '<span class="tag-you">你赢了!</span>' : '你输了,再接再厉~');
  elResult.appendChild(pEl);

  /* ---- 剩余手牌展示区:按 你 / 右家 / 左家 分开列出 ---- */
  const section = document.createElement('div');
  section.className = 'reveal-section';

  const secTitle = document.createElement('div');
  secTitle.className = 'reveal-title';
  secTitle.textContent = '各 家 剩 余 手 牌';
  section.appendChild(secTitle);

  [0, 1, 2].forEach(pl => {
    const hand = sortDesc(G.hands[pl]);   // 大 → 小,方便查看

    const row = document.createElement('div');
    row.className = 'reveal-row';

    // 玩家名 + 标签
    const nameEl = document.createElement('div');
    nameEl.className = 'reveal-name'
      + (pl === G.landlord ? ' landlord' : '')
      + (pl === winner ? ' winner' : '');
    nameEl.innerHTML =
      `<span class="who">${NAMES[pl]}` +
      (pl === G.landlord ? ' · 地主' : '') +
      (pl === winner ? ' 🏆' : '') +
      `</span><span class="n">${hand.length} 张</span>`;
    row.appendChild(nameEl);

    // 牌面(已出完则给个提示)
    const cardsWrap = document.createElement('div');
    cardsWrap.className = 'reveal-cards';
    if (hand.length === 0) {
      const em = document.createElement('div');
      em.className = 'reveal-empty';
      em.textContent = '已出完';
      cardsWrap.appendChild(em);
    } else {
      hand.forEach(c => cardsWrap.appendChild(cardEl(c, true, true)));
    }
    row.appendChild(cardsWrap);

    section.appendChild(row);
  });

  elResult.appendChild(section);

  /* ---- 再来一局 ---- */
  const btn = document.createElement('button');
  btn.className = 'btn primary';
  btn.textContent = '再来一局';
  btn.onclick = () => { elOverlay.classList.remove('show'); newGame(); };
  elResult.appendChild(btn);

  // 稍微延迟一下再弹出,让玩家看清桌面上的最后一手牌
  setTimeout(() => elOverlay.classList.add('show'), 600);
}

/* =========================================================
   启动
   ========================================================= */
render();
newGame();
</script>
</body>
</html>

Game Source: 斗地主Fight the Landlord

Creator: SwiftGalaxy49

Libraries: none

Complexity: complex (1416 lines, 43.8 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-swiftgalaxy49" to link back to the original. Then publish at arcadelab.ai/publish.