浏览器即玩的《羊了个羊》极简克隆方案

Vibe Coding 使用 AI 回答

代码问题:

我想要一个类似羊了个羊的游戏

代码解决方案:

下面给出一种“极简可跑起来”的《羊了个羊》克隆思路,只需要浏览器就能玩。
把下面两段代码分别保存成 index.html 和 game.css,双击 index.html 即可体验,后续你可以继续加皮肤、加音效、做排行榜……


index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>羊了个羊·简化版</title>
<link rel="stylesheet" href="game.css" />
</head>
<body>
  <h1>羊了个羊 · 简化克隆版</h1>

  <!-- 3 个卡槽 -->
  <div id="slots">
    <div class="slot" id="slot0"></div>
    <div class="slot" id="slot1"></div>
    <div class="slot" id="slot2"></div>
  </div>

  <!-- 游戏主区域 -->
  <div id="board"></div>

  <!-- 按钮 -->
  <div id="controls">
    <button id="shuffleBtn">洗牌</button>
    <button id="restartBtn">重新开始</button>
  </div>

<script>
/*****************************************************************
 *  1.  配置:随便改数字就能调难度
 *****************************************************************/
const COLORS = 12;            // 12 种图标(12 套 3 张 = 36 张)
const LAYERS = 3;             // 3 层叠叠乐
const PER_COLOR = 3;          // 每种图标 3 张
const SLOT_MAX = 3;           // 卡槽最多 3 张相同才能消除

/*****************************************************************
 *  2. 初始化
 *****************************************************************/
let board = [];               // 当前场上所有卡片
let slots = Array(SLOT_MAX).fill(null);   // 3 个卡槽
let selected = null;          // 当前高亮的卡片



// 生成唯一卡片 36 张
const allCards = [];
for (let i = 0; i < COLORS; i++) {
  for (let j = 0; j < PER_COLOR; j++) {
    allCards.push(i);         // 用数字做图标
  }
}
shuffle(allCards);

// 造 3 层塔
const boardEl = document.getElementById('board');
for (let layer = 0; layer < LAYERS; layer++) {
  const layerCards = allCards.splice(0, 12); // 每层 12 张
  layerCards.forEach((type, idx) => {
    // 简单摆摆,真实项目可以随机坐标
    const card = document.createElement('div');
    card.className = 'card';
    card.dataset.type = type;
    card.dataset.layer = layer;
    card.style.left = 60 + (idx % 6) * 70 + 'px';
    card.style.top  = 20 + layer * 30  + 'px';
    card.style.zIndex = 100 - layer;
    card.textContent = type;
    card.addEventListener('click', onCardClick);
    boardEl.appendChild(card);
    board.push(card);
  });
}

// 绑定按钮
document.getElementById('restartBtn').onclick = initGame;
document.getElementById('shuffleBtn').onclick = shuffleRemain;

/*****************************************************************
 *  3. 卡片点击逻辑
 *****************************************************************/
function onCardClick(e) {
  const card = e.currentTarget;
  if (!canPick(card)) return;           // 被压住的不能点

  // 放到卡槽
  const slotIdx = slots.findIndex(v => v === null);
  if (slotIdx === -1) {
    alert('卡槽已满!游戏失败'); return;
  }

  slots[slotIdx] = card.dataset.type;
  renderSlots();
  card.remove();                        // 从场上移除
  board = board.filter(c => c !== card);

  // 消除检测
  checkMatch();
  // 通关检测
  if (board.length === 0) alert('恭喜通关!');
}

// 判断卡片能否点击:上层没有别的卡片压着
function canPick(card) {
  const layer = +card.dataset.layer;
  const [x, y, w, h] = [card.offsetLeft, card.offsetTop, 60, 80];
  for (const upper of board) {
    if (+upper.dataset.layer <= layer) continue;
    const [ux, uy, uw, uh] = [upper.offsetLeft, upper.offsetTop, 60, 80];
    if (!(x + w < ux || ux + uw < x || y + h < uy || uy + uh < y)) {
      return false;
    }
  }
  return true;
}

/*****************************************************************
 *  4. 卡槽渲染 & 消除
 *****************************************************************/
function renderSlots() {
  slots.forEach((t, i) => {
    document.getElementById(`slot${i}`).textContent = t ?? '';
  });
}

function checkMatch() {
  // 遍历 3 个槽位,把相同 type 且满 3 张的清除
  const count = {};
  slots.forEach(t => {
    if (t !== null) count[t] = (count[t] || 0) + 1;
  });
  Object.entries(count).forEach(([type, cnt]) => {
    if (cnt === SLOT_MAX) {
      slots = slots.map(s => s === type ? null : s);
    }
  });
  renderSlots();
}

/*****************************************************************
 *  5. 洗牌 / 重启
 *****************************************************************/
function shuffleRemain() {
  const remain = board.map(c => c.dataset.type);
  shuffle(remain);
  board.forEach((c, i) => {
    c.dataset.type = remain[i];
    c.textContent = remain[i];
  });
}

function initGame() {
  location.reload();  // 最简单粗暴的重启
}

// Fisher–Yates 洗牌
function shuffle(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
}
</script>
</body>
</html>

game.css

body{
  font-family: Arial, sans-serif;
  text-align:center;
  background:#f5f5f5;
}
h1{margin:10px 0;}
#slots{
  display:flex;
  justify-content:center;
  gap:10px;
  margin:10px 0;
}
.slot{
  width:60px;
  height:80px;
  background:#fff;
  border:2px dashed #aaa;
  border-radius:8px;
  line-height:80px;
  font-size:28px;
  color:#555;
}
#board{
  position:relative;
  width:500px;
  height:260px;
  margin:0 auto;
  background:#eee;
  border-radius:10px;
}
.card{
  position:absolute;
  width:60px;
  height:80px;
  background:#fff;
  border:1px solid #bbb;
  border-radius:6px;
  font-size:28px;
  line-height:80px;
  cursor:pointer;
  transition:all .15s;
  box-shadow:0 2px 4px rgba(0,0,0,.2);
}
.card:hover{
  transform:scale(1.05);
  box-shadow:0 4px 8px rgba(0,0,0,.3);
}
#controls{margin-top:15px;}
button{
  margin:0 5px;
  padding:8px 16px;
  font-size:16px;
}

使用与扩展思路

  1. 美术:把数字换成 emoji 或自己画的羊、草、萝卜等。
  2. 关卡:用 JSON 描述每层摆法,随机生成可解但较难的布局(主流做法:先用 DFS 生成可通关的牌池,再随机覆盖)。
  3. 道具:洗牌、撤回、移除三张卡,道具冷却与广告。
  4. 后端:最简单的云开发(微信/字节/Firebase)存每日排行榜,统计同一省份人数。
  5. 商业化:看广告得金币、买皮肤、抽盲盒。

代码仅 200 行左右,足够你继续魔改。祝玩得开心!