🎮ArcadeLab

1024 连连看 · 第一关

by FrostWave14
499 lines15.9 KB
▶ Play
<!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>1024 连连看 · 第一关</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
  html,body {
    width:100%; height:100%; overflow:hidden; background:#060a10;
    font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
    display:flex; align-items:center; justify-content:center;
  }
  #stage { transform-origin:center center; }

  #game {
    position:relative; width:500px; height:700px; border-radius:26px; overflow:hidden;
    background:radial-gradient(circle at 50% 26%,#1a2a3d 0%,#101923 45%,#080d14 100%);
    box-shadow:0 0 0 1px rgba(90,160,255,.12), 0 30px 80px rgba(0,0,0,.75);
  }
  #game::before {
    content:''; position:absolute; inset:0; pointer-events:none;
    background-image:
      linear-gradient(rgba(90,160,255,.045) 1px, transparent 1px),
      linear-gradient(90deg, rgba(90,160,255,.045) 1px, transparent 1px);
    background-size:25px 25px;
  }

  /* ---------- 顶栏 ---------- */
  #hud {
    position:absolute; top:0; left:0; right:0; height:64px; z-index:50;
    display:flex; align-items:center; justify-content:space-between; padding:0 24px;
  }
  .hud-item { font-size:13px; color:#5d7a9a; }
  .hud-item b { color:#7cc4ff; font-size:17px; margin-left:3px; font-family:"SF Mono",Consolas,monospace; }
  .hud-title { font-size:14px; font-weight:700; letter-spacing:3px; color:#8fb6e8; }

  /* ---------- 槽位底框 ---------- */
  .slot-bg {
    position:absolute; width:64px; height:64px; border-radius:12px; z-index:1;
    border:1.5px dashed rgba(120,170,255,.2);
    background:rgba(120,170,255,.025);
  }

  /* ---------- 卡片 ---------- */
  .card {
    position:absolute; width:64px; height:64px; border-radius:12px;
    display:flex; align-items:center; justify-content:center;
    font-size:30px; font-weight:800; font-family:"SF Mono",Consolas,monospace;
    cursor:pointer; user-select:none;
    transition:
      left .26s cubic-bezier(.25,.9,.3,1.1),
      top  .26s cubic-bezier(.25,.9,.3,1.1),
      transform .18s ease, opacity .25s ease, filter .2s ease;
    will-change:left,top,transform;
  }
  .card[data-num="1"] {
    color:#86c8ff;
    background:linear-gradient(160deg,#1b3a5c,#0e1e30);
    border:1.5px solid rgba(90,170,255,.55);
    box-shadow:0 4px 0 rgba(0,0,0,.45), 0 0 14px rgba(80,160,255,.18);
  }
  .card[data-num="0"] {
    color:#5cf0b0;
    background:linear-gradient(160deg,#123a2e,#0a1f19);
    border:1.5px solid rgba(70,230,160,.5);
    box-shadow:0 4px 0 rgba(0,0,0,.45), 0 0 14px rgba(70,230,160,.16);
  }
  .card[data-num="2"] {
    color:#ffc85c;
    background:linear-gradient(160deg,#3d2e12,#221808);
    border:1.5px solid rgba(255,190,70,.5);
    box-shadow:0 4px 0 rgba(0,0,0,.45), 0 0 14px rgba(255,190,70,.16);
  }
  .card[data-num="4"] {
    color:#c79bff;
    background:linear-gradient(160deg,#2e1c45,#190f28);
    border:1.5px solid rgba(180,120,255,.5);
    box-shadow:0 4px 0 rgba(0,0,0,.45), 0 0 14px rgba(180,120,255,.16);
  }
  .card.covered { filter:brightness(.4) saturate(.3); cursor:default; }
  .card:not(.covered):hover { transform:translateY(-3px) scale(1.04); filter:brightness(1.18); }
  .card:not(.covered):active { transform:scale(.94); }
  .card.pop { transform:scale(1.4); opacity:0; }

  @keyframes shake {
    0%,100% { transform:translateX(0); }
    25%     { transform:translateX(-5px); }
    75%     { transform:translateX(5px); }
  }
  .card.shake { animation:shake .3s ease; }

  @keyframes flipY {
    0%   { transform:rotateY(0) scale(1); }
    50%  { transform:rotateY(90deg) scale(.9); }
    100% { transform:rotateY(0) scale(1); }
  }
  .card.flip { animation:flipY .4s ease; }

  /* ---------- 道具 ---------- */
  #tools {
    position:absolute; bottom:12px; left:0; right:0; z-index:60;
    display:flex; justify-content:center; gap:16px;
  }
  #tools button {
    padding:9px 24px; border-radius:20px; font-size:14px; cursor:pointer;
    color:#9dc4f0; border:1px solid rgba(120,170,255,.28);
    background:rgba(120,170,255,.08);
    transition:all .2s ease;
  }
  #tools button:hover:not(:disabled) { background:rgba(120,170,255,.2); color:#cfe6ff; }
  #tools button:disabled { opacity:.28; cursor:default; }

  /* ---------- 结算 ---------- */
  #overlay {
    position:absolute; inset:0; z-index:300;
    display:flex; align-items:center; justify-content:center;
    background:rgba(5,8,13,.86); backdrop-filter:blur(5px);
    transition:opacity .3s ease;
  }
  #overlay.hidden { opacity:0; pointer-events:none; }
  .panel {
    text-align:center; padding:38px 46px; border-radius:20px;
    background:linear-gradient(160deg,#16233a,#0d1522);
    border:1px solid rgba(120,170,255,.2);
    box-shadow:0 20px 60px rgba(0,0,0,.65);
  }
  .panel h2 { font-size:25px; margin-bottom:14px; color:#8fc4ff; letter-spacing:1px; }
  .panel h2.lose { color:#ff8a8a; }
  .panel p { font-size:13px; line-height:1.7; color:#6d8bab; margin-bottom:26px; max-width:260px; }
  .panel button {
    padding:11px 36px; border-radius:24px; border:none; cursor:pointer;
    background:linear-gradient(135deg,#3b82f6,#6366f1);
    color:#fff; font-size:15px; font-weight:600;
    box-shadow:0 8px 24px rgba(59,130,246,.35);
    transition:transform .15s ease;
  }
  .panel button:hover { transform:translateY(-2px); }
</style>
</head>
<body>

<div id="stage">
  <div id="game">
    <div id="hud">
      <div class="hud-item">剩余 <b id="leftCount">30</b></div>
      <div class="hud-title">1024 · 连连看</div>
      <div class="hud-item">槽位 <b id="slotCount">0</b>/7</div>
    </div>

    <div id="slotArea"></div>

    <div id="tools">
      <button id="btnUndo">撤销 ×1</button>
      <button id="btnShuffle">洗牌 ×1</button>
    </div>

    <div id="overlay" class="hidden">
      <div class="panel">
        <h2 id="ovTitle">通关</h2>
        <p id="ovDesc"></p>
        <button id="btnRestart">再来一局</button>
      </div>
    </div>
  </div>
</div>

<script>
(function () {
  'use strict';

  /* ================= 常量 ================= */
  const CARD = 64;
  const GAP  = 6;
  const SLOTS = 7;
  const BOARD_W = 500;
  const BOARD_H = 700;

  const PILE_LEFT = 122;   // (500 - 256) / 2
  const PILE_TOP  = 192;

  const SLOT_Y = 560;
  const SLOT_TOTAL_W = SLOTS * CARD + (SLOTS - 1) * GAP; // 484
  const SLOT_LEFT = (BOARD_W - SLOT_TOTAL_W) / 2;        // 8

  const NUMS = ['1', '0', '2', '4'];

  /* ================= DOM ================= */
  const stage      = document.getElementById('stage');
  const gameEl     = document.getElementById('game');
  const slotArea   = document.getElementById('slotArea');
  const leftCountEl= document.getElementById('leftCount');
  const slotCountEl= document.getElementById('slotCount');
  const overlay    = document.getElementById('overlay');
  const ovTitle    = document.getElementById('ovTitle');
  const ovDesc     = document.getElementById('ovDesc');
  const btnUndo    = document.getElementById('btnUndo');
  const btnShuffle = document.getElementById('btnShuffle');
  const btnRestart = document.getElementById('btnRestart');

  /* ================= 状态 ================= */
  let tiles = [];
  let slot = [];
  let running = false;
  let undoLeft = 1;
  let shuffleLeft = 1;
  let lastTile = null;

  /* ================= 工具函数 ================= */
  function slotX(i) { return SLOT_LEFT + i * (CARD + GAP); }

  function shuffleArr(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 buildTiles() {
    // 金字塔四层:4x4 / 3x3 / 2x2 / 1x1 = 30 张,正好 10 组
    const layers = [
      { n: 4, off: 0   },
      { n: 3, off: 0.5 },
      { n: 2, off: 1   },
      { n: 1, off: 1.5 },
    ];
    const list = [];
    let id = 0;
    layers.forEach((L, layer) => {
      const base = L.off * CARD;
      for (let r = 0; r < L.n; r++) {
        for (let c = 0; c < L.n; c++) {
          list.push({
            id: id++,
            layer,
            x: base + c * CARD,
            y: base + r * CARD,
            num: null,
            state: 'board',   // board | slot | gone
            el: null,
          });
        }
      }
    });
    return list;
  }

  /* 两张牌是否在平面上重叠(紧贴不算重叠) */
  function overlaps(a, b) {
    return Math.abs(a.x - b.x) < CARD - 0.5 &&
           Math.abs(a.y - b.y) < CARD - 0.5;
  }

  /* 是否被更高层的牌压住 */
  function isCovered(tile, actives) {
    for (let i = 0; i < actives.length; i++) {
      const t = actives[i];
      if (t.layer > tile.layer && overlaps(t, tile)) return true;
    }
    return false;
  }

  /* ================= 逆向生成保证可解 =================
     1. 模拟一个合法的点击顺序(每次从"当前可点击"的牌里随机取一张)
     2. 按这个顺序,每 3 张连续分配同一个数字
     这样只要玩家按顺序点,槽位里最多同时存在 2 张,绝不会爆槽
  ==================================================== */
  function assignNumbers(list) {
    let remaining = list.slice();
    const order = [];

    while (remaining.length) {
      const clickable = remaining.filter(t => !isCovered(t, remaining));
      const pick = clickable[Math.floor(Math.random() * clickable.length)];
      order.push(pick);
      remaining = remaining.filter(t => t !== pick);
    }

    const groupCount = order.length / 3;
    // 保证 1/0/2/4 四种数字都出现,视觉更丰富
    const seq = [];
    for (let i = 0; i < groupCount; i++) seq.push(NUMS[i % NUMS.length]);
    shuffleArr(seq);

    for (let i = 0; i < groupCount; i++) {
      for (let k = 0; k < 3; k++) {
        order[i * 3 + k].num = seq[i];
      }
    }
  }

  /* ================= 渲染 ================= */
  function createCardEl(tile) {
    const el = document.createElement('div');
    el.className = 'card';
    el.dataset.num = tile.num;
    el.textContent = tile.num;
    el.style.left = (PILE_LEFT + tile.x) + 'px';
    el.style.top  = (PILE_TOP  + tile.y) + 'px';
    el.style.zIndex = 10 + tile.layer * 10;
    el.addEventListener('click', () => handleClick(tile));
    gameEl.appendChild(el);
    tile.el = el;
  }

  function refreshBoardCover() {
    const actives = tiles.filter(t => t.state === 'board');
    actives.forEach(t => {
      t.el.classList.toggle('covered', isCovered(t, actives));
    });
  }

  function refreshSlot() {
    slot.forEach((t, i) => {
      t.el.style.left = slotX(i) + 'px';
      t.el.style.top  = SLOT_Y + 'px';
      t.el.style.zIndex = 200 + i;
    });
  }

  function updateHUD() {
    leftCountEl.textContent = tiles.filter(t => t.state !== 'gone').length;
    slotCountEl.textContent = slot.length;
  }

  /* ================= 交互 ================= */
  function handleClick(tile) {
    if (!running || tile.state !== 'board') return;

    const actives = tiles.filter(t => t.state === 'board');
    if (isCovered(tile, actives)) {
      tile.el.classList.remove('shake');
      void tile.el.offsetWidth;
      tile.el.classList.add('shake');
      return;
    }
    if (slot.length >= SLOTS) return;

    // 移入槽位
    tile.state = 'slot';
    tile.el.classList.remove('covered');

    // 按数字分组插入,同数字挨在一起
    let idx = slot.length;
    for (let i = slot.length - 1; i >= 0; i--) {
      if (slot[i].num === tile.num) { idx = i + 1; break; }
    }
    slot.splice(idx, 0, tile);
    lastTile = tile;

    refreshBoardCover();
    refreshSlot();
    updateHUD();

    const matched = checkMatch();
    if (!matched && slot.length >= SLOTS) {
      gameOver(false);
    }
  }

  function checkMatch() {
    const groups = {};
    slot.forEach(t => { (groups[t.num] = groups[t.num] || []).push(t); });

    for (const num in groups) {
      if (groups[num].length >= 3) {
        const three = groups[num].slice(0, 3);
        three.forEach(t => {
          t.state = 'gone';
          t.el.classList.add('pop');
          const el = t.el;
          setTimeout(() => el.remove(), 340);
        });
        slot = slot.filter(t => three.indexOf(t) === -1);
        refreshSlot();
        updateHUD();

        setTimeout(() => {
          if (running && tiles.every(t => t.state === 'gone')) gameOver(true);
        }, 360);
        return true;
      }
    }
    return false;
  }

  /* ================= 道具 ================= */
  function doUndo() {
    if (!running || undoLeft <= 0 || !lastTile) return;
    if (lastTile.state !== 'slot') return;
    const idx = slot.indexOf(lastTile);
    if (idx === -1) return;

    slot.splice(idx, 1);
    lastTile.state = 'board';
    lastTile.el.style.left = (PILE_LEFT + lastTile.x) + 'px';
    lastTile.el.style.top  = (PILE_TOP  + lastTile.y) + 'px';
    lastTile.el.style.zIndex = 10 + lastTile.layer * 10;

    undoLeft--;
    btnUndo.disabled = true;
    btnUndo.textContent = '撤销 ×0';
    lastTile = null;

    refreshBoardCover();
    refreshSlot();
    updateHUD();
  }

  function doShuffle() {
    if (!running || shuffleLeft <= 0) return;

    const boardTiles = tiles.filter(t => t.state === 'board');
    const nums = boardTiles.map(t => t.num);
    shuffleArr(nums);

    boardTiles.forEach((t, i) => {
      t.num = nums[i];
      t.el.dataset.num = t.num;
      t.el.textContent = t.num;
      t.el.classList.remove('flip');
      void t.el.offsetWidth;
      t.el.classList.add('flip');
    });

    shuffleLeft--;
    btnShuffle.disabled = true;
    btnShuffle.textContent = '洗牌 ×0';
  }

  /* ================= 结算 ================= */
  function gameOver(win) {
    running = false;
    overlay.classList.remove('hidden');
    if (win) {
      ovTitle.textContent = '🎉 1024 通关!';
      ovTitle.className = '';
      ovDesc.textContent = '所有数字都找到了伙伴。今天的 Bug,也会这样悄悄消失的。';
    } else {
      ovTitle.textContent = '槽位满了';
      ovTitle.className = 'lose';
      ovDesc.textContent = '这一关闭着眼都能过,再来一次吧。';
    }
  }

  /* ================= 初始化 ================= */
  function init() {
    // 清理旧卡片
    gameEl.querySelectorAll('.card').forEach(el => el.remove());

    slot = [];
    lastTile = null;
    undoLeft = 1;
    shuffleLeft = 1;
    btnUndo.disabled = false;
    btnShuffle.disabled = false;
    btnUndo.textContent = '撤销 ×1';
    btnShuffle.textContent = '洗牌 ×1';
    overlay.classList.add('hidden');

    // 槽位底框
    slotArea.innerHTML = '';
    for (let i = 0; i < SLOTS; i++) {
      const d = document.createElement('div');
      d.className = 'slot-bg';
      d.style.left = slotX(i) + 'px';
      d.style.top  = SLOT_Y + 'px';
      slotArea.appendChild(d);
    }

    tiles = buildTiles();
    assignNumbers(tiles);
    tiles.forEach(createCardEl);

    refreshBoardCover();
    updateHUD();
    running = true;
  }

  /* ================= 自适应缩放 ================= */
  function fit() {
    const s = Math.min(window.innerWidth / 530, window.innerHeight / 730, 1.3);
    stage.style.transform = 'scale(' + s + ')';
  }

  /* ================= 事件绑定 ================= */
  btnUndo.addEventListener('click', doUndo);
  btnShuffle.addEventListener('click', doShuffle);
  btnRestart.addEventListener('click', init);
  window.addEventListener('resize', fit);

  fit();
  init();
})();
</script>
</body>
</html>

Game Source: 1024 连连看 · 第一关

Creator: FrostWave14

Libraries: none

Complexity: complex (499 lines, 15.9 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: 1024-frostwave14" to link back to the original. Then publish at arcadelab.ai/publish.