Appearance
2048
得分:0
最高分:0
+0
html
<div class="game-2048">
<div class="header">
<h1>2048</h1>
<div class="scores">
<div class="score">得分:{{ score }}</div>
<div class="best">最高分:{{ bestScore }}</div>
</div>
<button class="restart" @click="restart">重新开始</button>
</div>
<div class="grid" @touchstart="onTouchStart" @touchend="onTouchEnd">
<!-- 底板 -->
<div class="cell-bg" v-for="i in 16" :key="i"></div>
<!-- 方块 -->
<template v-for="(row, i) in grid">
<div v-for="(val, j) in row" :key="`${i}-${j}-${val}`" class="tile"
:class="[`tile-${val}`, { merge: isMerging }]" v-show="val !== 0" :style="{
top: `${i * (tileSize + gap) + gap}px`,
left: `${j * (tileSize + gap) + gap}px`,
width: `${tileSize}px`,
height: `${tileSize}px`
}">
{{ val }}
</div>
</template>
</div>
<div class="score-add" :class="{ show: addScoreShow }">
+{{ addScore }}
</div>
</div>js
import { ref, onMounted, onUnmounted, watch } from 'vue'
const size = 4
const tileSize = 68
const gap = 8
const grid = ref([])
const score = ref(0)
const bestScore = ref(0)
const isMerging = ref(false)
const addScore = ref(0)
const addScoreShow = ref(false)
let startX = 0
let startY = 0
// 初始化
function init() {
grid.value = Array.from({ length: size }, () => Array(size).fill(0))
score.value = 0
addRandom()
addRandom()
}
// 随机生成 2/4
function addRandom() {
const list = []
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (grid.value[i][j] === 0) list.push([i, j])
}
}
if (!list.length) return
const [i, j] = list[Math.random() * list.length | 0]
grid.value[i][j] = Math.random() < 0.9 ? 2 : 4
}
// 合并一行
function mergeLine(line) {
let arr = line.filter(v => v)
let res = []
let add = 0
let i = 0
while (i < arr.length) {
if (i + 1 < arr.length && arr[i] === arr[i + 1]) {
const val = arr[i] * 2
res.push(val)
add += val
i += 2
} else {
res.push(arr[i])
i++
}
}
while (res.length < size) res.push(0)
return { res, add }
}
// 转置
function transpose(g) {
return g[0].map((_, c) => g.map(r => r[c]))
}
// 方向
function moveLeft() {
let total = 0
grid.value = grid.value.map(row => {
const m = mergeLine(row)
total += m.add
return m.res
})
return total
}
function moveRight() {
let total = 0
grid.value = grid.value.map(row => {
const rev = [...row].reverse()
const m = mergeLine(rev)
total += m.add
return m.res.reverse()
})
return total
}
function moveUp() {
const t = transpose(grid.value)
let total = 0
const newT = t.map(row => {
const m = mergeLine(row)
total += m.add
return m.res
})
grid.value = transpose(newT)
return total
}
function moveDown() {
const t = transpose(grid.value)
let total = 0
const newT = t.map(row => {
const rev = [...row].reverse()
const m = mergeLine(rev)
total += m.add
return m.res.reverse()
})
grid.value = transpose(newT)
return total
}
// 执行移动
function doMove(fn) {
const old = JSON.stringify(grid.value)
const add = fn()
if (add > 0) {
addScore.value = add
addScoreShow.value = true
setTimeout(() => addScoreShow.value = false, 600)
}
if (JSON.stringify(grid.value) !== old) {
score.value += add
isMerging.value = true
setTimeout(() => isMerging.value = false, 100)
addRandom()
checkOver()
}
}
// 键盘
function onKey(e) {
switch (e.key) {
case 'ArrowUp': doMove(moveUp); break
case 'ArrowDown': doMove(moveDown); break
case 'ArrowLeft': doMove(moveLeft); break
case 'ArrowRight': doMove(moveRight); break
}
}
// 滑动
function onTouchStart(e) {
startX = e.touches[0].clientX
startY = e.touches[0].clientY
}
function onTouchEnd(e) {
const dx = e.changedTouches[0].clientX - startX
const dy = e.changedTouches[0].clientY - startY
if (Math.abs(dx) < 20 && Math.abs(dy) < 20) return
if (Math.abs(dx) > Math.abs(dy)) {
dx > 0 ? doMove(moveRight) : doMove(moveLeft)
} else {
dy > 0 ? doMove(moveDown) : doMove(moveUp)
}
}
// 结束判断
function checkOver() {
for (let i = 0; i < size; i++)
for (let j = 0; j < size; j++)
if (!grid.value[i][j]) return
for (let i = 0; i < size; i++)
for (let j = 0; j < size - 1; j++)
if (grid.value[i][j] === grid.value[i][j + 1]) return
const t = transpose(grid.value)
for (let i = 0; i < size; i++)
for (let j = 0; j < size - 1; j++)
if (t[i][j] === t[i][j + 1]) return
setTimeout(() => alert('游戏结束!得分:' + score.value), 200)
}
// 重新开始
function restart() {
init()
}
// 最高分
watch(score, val => {
if (val > bestScore.value) {
bestScore.value = val
localStorage.setItem('best2048', val)
}
})
onMounted(() => {
bestScore.value = +localStorage.getItem('best2048') || 0
init()
window.addEventListener('keydown', onKey)
})
onUnmounted(() => {
window.removeEventListener('keydown', onKey)
})css
.game-2048 {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
background: #faf8ef;
min-height: 100vh;
user-select: none;
position: relative;
}
.header {
text-align: center;
margin-bottom: 16px;
}
h1 {
font-size: 36px;
color: #776e65;
margin: 0 0 8px;
}
.scores {
display: flex;
gap: 16px;
justify-content: center;
margin-bottom: 10px;
font-size: 16px;
color: #776e65;
}
.restart {
padding: 8px 16px;
background: #8f7a66;
color: #fff;
border: none;
border-radius: 6px;
font-weight: bold;
cursor: pointer;
}
.grid {
position: relative;
width: calc(4 * 68px + 5 * 8px);
height: calc(4 * 68px + 5 * 8px);
background: #bbada0;
border-radius: 8px;
padding: 8px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 8px;
}
.cell-bg {
background: #cdc1b4;
border-radius: 4px;
}
.tile {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
color: #776e65;
border-radius: 4px;
transition: transform 0.14s ease;
animation: appear 0.16s ease;
}
.tile.merge {
animation: merge 0.2s ease;
}
@keyframes appear {
0% {
transform: scale(0.4);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes merge {
0% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
100% {
transform: scale(1);
}
}
.score-add {
position: fixed;
top: 140px;
font-size: 20px;
color: #8f7a66;
font-weight: bold;
opacity: 0;
transition: all 0.6s ease;
}
.score-add.show {
opacity: 1;
transform: translateY(-20px);
}
.tile-2 {
background: #eee4da;
}
.tile-4 {
background: #ede0c8;
}
.tile-8 {
background: #f2b179;
color: #fff;
}
.tile-16 {
background: #f59563;
color: #fff;
}
.tile-32 {
background: #f67c5f;
color: #fff;
}
.tile-64 {
background: #f65e3b;
color: #fff;
}
.tile-128 {
background: #edcf72;
color: #fff;
}
.tile-256 {
background: #edcc61;
color: #fff;
}
.tile-512 {
background: #ecc850;
color: #fff;
}
.tile-1024,
.tile-2048 {
background: #edc22e;
color: #fff;
}