1024 连连看 · 第一关
by FrostWave14499 lines15.9 KB
<!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.