我是不喜欢学习理论的程序员,有没有人和我一样,看到原理都有点烦(((φ(◎ロ◎;)φ)))
效果
仅仅演示了失败的,成功的emmm…你们试试看吧
平台
- 移动端 [🤔应该没有多兼容吧]
- 桌面端 [😀应该和移动端一样]
主要逻辑
就是三层循环,应该不是最优逻辑吧,但是目前我的技术刚刚好能够捋明白的。
如图所示
蓝色对应第一层循环的列/行
绿色对应第二层循环的块
橙色对应第三层需要和绿色对比的块
- 主要判断橙色的值是否空
- 不为空 看绿色是否为空
- 不为空 值是否相同
- 相同 合并
- 不相同 跳过
- 为空 移动
- 不为空 值是否相同
- 为空 跳过
- 不为空 看绿色是否为空
对应代码
for (let i = 0; i < config.gridSize; i++) {
let initJ = -1;
for (let j = 0; j < config.gridSize - 1; j++) {
for (let k = j + 1; k < config.gridSize; k++) {
let curPos = ''
let nexPos = ''
if (dx == 1) {
curPos = `${config.gridSize - 1 - j},${i}`
nexPos = `${config.gridSize - 1 - k},${i}`
}
if (dx == -1) {
curPos = `${j},${i}`
nexPos = `${k},${i}`
}
if (dy == -1) {
curPos = `${i},${j}`
nexPos = `${i},${k}`
}
if (dy == 1) {
curPos = `${i},${config.gridSize - 1 - j}`
nexPos = `${i},${config.gridSize - 1 - k}`
}
const curVal = girdMap[curPos]
const nexVal = girdMap[nexPos]
if (nexVal) {
if (!curVal) { // 位置为空 移动
numberOfTimes++
moveElement(nexPos, curPos)
j = initJ;
break;
}
else if (curVal == nexVal) { // 位置值相等 合并
numberOfTimes++
mergeElement(nexPos, curPos)
initJ = j;
break;
}
else if (curVal != nexVal) break; // 位置有值且不相同 跳过
}
}
}
}
源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./assets/global.css">
<style>
.game-container {
display: flex;
width: 100%;
max-height: 100vh;
max-width: 400px;
background-color: rgb(252, 244, 233);
padding: 16px;
box-sizing: border-box;
flex-direction: column;
}
.game-board {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
}
.game-board-title {
font-size: 40px;
font-weight: bold;
color: rgb(168, 141, 102);
}
.game-board-subtitle {
font-size: 14px;
color: rgb(99, 76, 47);
}
.game-board-subtitle span {
font-weight: bold;
}
.game-board-curscore,
.game-board-maxscore {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
background-color: rgb(185, 146, 99);
color: #faf3e0;
top: 0;
padding: 4px 10px;
}
.game-board-maxscore {
right: 0;
}
.game-board-curscore {
right: 80px;
}
.game-board-label {
font-size: 12px;
}
.game-grid {
width: 100%;
margin-top: 20px;
background-color: #bbada0;
position: relative;
display: grid;
padding: 10px;
grid-row-gap: 10px;
grid-column-gap: 10px;
box-sizing: border-box;
}
.game-grid-gray {
background-color: rgba(238, 228, 218, 0.35);
}
.game-grid-item {
position: absolute;
font-size: 20px;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
transition: all .25s ease-in-out;
}
.game-grid-item.init {
animation-duration: .25s;
animation-name: init;
animation-iteration-count: unset;
}
.game-grid-item.merge {
animation-delay: .25s;
animation-duration: .25s;
animation-name: merge;
animation-iteration-count: unset;
}
.game-grid-pane {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, .6);
display: flex;
align-items: center;
justify-content: center;
}
@keyframes init {
0% {
transform: scale(.5);
}
100% {
transform: scale(1);
}
}
@keyframes merge {
0% {
transform: scale(.8);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
</style>
</head>
<body>
<div class="game-container">
<div class="game-board">
<div class="game-board-title">2048</div>
<div class="game-board-subtitle">通过移动组合数字,目标达到<span>2048</span>!</div>
<div class="game-board-curscore">
<div class="game-board-label">当前分</div>
<div class="game-board-value">0</div>
</div>
<div class="game-board-maxscore">
<div class="game-board-label">最高分</div>
<div class="game-board-value">0</div>
</div>
</div>
<div class="game-grid"></div>
</div>
</body>
<script type="module">
import { Maths } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/maths.js";
import { Randoms } from "https://gcore.jsdelivr.net/npm/@3r/tool/lib/randoms.js";
// 宽高一致
let gameGridDom = document.querySelector('.game-grid')
let gameCurScoreDom = document.querySelector('.game-board-curscore .game-board-value')
let gameMaxScoreDom = document.querySelector('.game-board-maxscore .game-board-value')
let gameMaxScoreKey = '2048MaxScore';
let gameCurScore = 0;
let gameMaxScore = localStorage.getItem(gameMaxScoreKey) || 0
gameMaxScoreDom.textContent = gameMaxScore
gameGridDom.style.height = `${gameGridDom.clientWidth}px`
// 配置文件
let config = {
// 格子4×4
gridSize: 4,
gridItemColorList: [
'rgb(238, 228, 218)',
'rgb(237, 224, 200)',
'rgb(242, 177, 121)',
'rgb(245, 149, 99)',
'rgb(246, 124, 95)',
'rgb(246, 94, 59)',
'rgb(237, 206, 115)',
'rgb(236, 201, 97)',
'rgb(238, 199, 80)',
'rgb(239, 196, 65)',
'rgb(239, 193, 46)',
'rgb(255, 60, 61)',
'rgb(255, 30, 32)',
]
}
// 格子元素字典
let girdDomDict = {}
// 格子地图
let girdMap = {}
// 格子元素地图
let girdMapDom = {}
// 游戏结束
let isGameOver = false;
// 输入回调参数
let inputCallParam = {
ArrowLeft: [-1, 0],
ArrowRight: [1, 0],
ArrowUp: [0, -1],
ArrowDown: [0, 1],
}
// 初始化格子
function initGrid() {
for (let y = 0; y < config.gridSize; y++) {
for (let x = 0; x < config.gridSize; x++) {
let gridGray = document.createElement("div")
girdDomDict[`${x},${y}`] = gridGray
gridGray.classList.add('game-grid-gray')
gameGridDom.appendChild(gridGray)
}
}
gameGridDom.style.gridTemplateColumns = `repeat(${config.gridSize},calc((100% - 30px) / ${config.gridSize}))`
gameGridDom.style.gridTemplateRows = `repeat(${config.gridSize},calc((100% - 30px) / ${config.gridSize}))`
}
// 随机生成元素
function randomGenerateElement() {
// 判断游戏是否结束 逻辑限制此问题不存在
// if (Object.values(girdMap).filter(item => item).length === config.gridSize * config.gridSize) {
// isGameOver = true;
// showGameOver('游戏结束 啊啦啦啦!/(ㄒoㄒ)/~~')
// return;
// }
let x = Randoms.getRandomInt(0, config.gridSize);
let y = Randoms.getRandomInt(0, config.gridSize);
// 如果格子存在元素 则重新随机
if (girdMapDom[`${x},${y}`]) {
return randomGenerateElement()
}
// 随机是 2 或者 4
let value = Randoms.getRandomInt(0, 2) ? 2 : 4;
let gridGray = girdDomDict[`${x},${y}`]
let gridItem = document.createElement("div")
gridItem.classList.add('game-grid-item')
gridItem.classList.add('init')
gridItem.style.left = gridGray.offsetLeft + 'px'
gridItem.style.top = gridGray.offsetTop + 'px'
gridItem.style.width = gridGray.offsetWidth + 'px'
gridItem.style.height = gridGray.offsetHeight + 'px'
gridItem.textContent = value;
girdMap[`${x},${y}`] = value
elementColorTran(gridItem, value)
girdMapDom[`${x},${y}`] = gridItem;
gameGridDom.appendChild(gridItem)
calculateCurScore();
printMap()
setTimeout(() => {
gridItem.classList.remove('init')
}, 250);
// 检测是否还能合并
checkGameOver()
}
let isSlideDirection = false
// 滑动方向
function slideDirection(dx, dy) {
if (isSlideDirection) return;
isSlideDirection = true;
// -1 , 0 向左
// 1 , 0 向右
// 0 , -1 向上 由于元素是从左上角为原点渲染的 所以这里就使用了 -1
// 0 , 1 向下
let numberOfTimes = 0; // 变更次数
for (let i = 0; i < config.gridSize; i++) {
let initJ = -1;
for (let j = 0; j < config.gridSize - 1; j++) {
for (let k = j + 1; k < config.gridSize; k++) {
let curPos = ''
let nexPos = ''
if (dx == 1) {
curPos = `${config.gridSize - 1 - j},${i}`
nexPos = `${config.gridSize - 1 - k},${i}`
}
if (dx == -1) {
curPos = `${j},${i}`
nexPos = `${k},${i}`
}
if (dy == -1) {
curPos = `${i},${j}`
nexPos = `${i},${k}`
}
if (dy == 1) {
curPos = `${i},${config.gridSize - 1 - j}`
nexPos = `${i},${config.gridSize - 1 - k}`
}
const curVal = girdMap[curPos]
const nexVal = girdMap[nexPos]
if (nexVal) {
if (!curVal) { // 位置为空 移动
numberOfTimes++
moveElement(nexPos, curPos)
j = initJ;
break;
}
else if (curVal == nexVal) { // 位置值相等 合并
numberOfTimes++
mergeElement(nexPos, curPos)
initJ = j;
break;
}
else if (curVal != nexVal) break; // 位置有值且不相同 跳过
}
}
}
}
if (numberOfTimes) {
setTimeout(() => {
randomGenerateElement()
isSlideDirection = false;
}, 200);
} else {
isSlideDirection = false;
}
}
// 移动元素
function moveElement(origin, target) {
let [ox, oy] = origin.split(',')
let [tx, ty] = target.split(',')
girdMap[target] = girdMap[origin]
girdMap[origin] = undefined
girdMapDom[target] = girdMapDom[origin]
girdMapDom[origin] = undefined
let gridGray = girdDomDict[`${tx},${ty}`]
girdMapDom[target].style.left = gridGray.offsetLeft + 'px'
girdMapDom[target].style.top = gridGray.offsetTop + 'px'
}
// 合并元素
function mergeElement(origin, target) {
/** @type {HTMLDivElement} */
let originDom = girdMapDom[origin]
/** @type {HTMLDivElement} */
let targetDom = girdMapDom[target]
let originVal = girdMap[origin]
let targetVal = girdMap[target]
let value = originVal + targetVal
moveElement(origin, target)
girdMap[target] = value
elementColorTran(originDom, value)
originDom.style.zIndex = 1;
originDom.classList.add('merge')
// 合成2048啦
if (value == 2048) {
isGameOver = true;
showGameOver('👍(。^▽^)厉害了 哥们儿')
}
setTimeout(function () {
originDom.textContent = value
originDom.style.zIndex = 0;
targetDom.remove();
}, 250);
setTimeout(function () {
originDom.classList.remove('merge')
}, 500);
}
// 打印地图
function printMap() {
let str = ''
for (let y = 0; y < config.gridSize; y++) {
for (let x = 0; x < config.gridSize; x++) {
str += `${girdMap[`${x},${y}`] || 0} `
}
str += '\n'
}
console.log(str)
}
// 元素颜色变换
function elementColorTran(dom, val) {
// 换算成二进制 获取0的个数
let numberOfZeros = val.toString(2).split('').filter(item => item == '0').length
// 颜色的索引
let colorIndex = numberOfZeros - 1
// 获取颜色
let color = config.gridItemColorList[colorIndex]
// 给背景设置颜色
dom.style.background = color
}
// 计算得分
function calculateCurScore() {
gameCurScore = Maths.sum(Object.values(girdMap).filter(item => item))
gameCurScoreDom.textContent = gameCurScore;
if (gameCurScore > gameMaxScore) {
gameMaxScore = gameCurScore;
gameMaxScoreDom.textContent = gameCurScore;
localStorage.setItem(gameMaxScoreKey, gameCurScore)
}
}
// 游戏胜利
function showGameOver(conclusion) {
let gameOverPane = document.querySelector('.game-grid-pane') || document.createElement('div')
gameOverPane.classList.add('game-grid-pane')
gameOverPane.textContent = conclusion
gameOverPane.style.zIndex = 2;
gameGridDom.appendChild(gameOverPane)
}
// 检查死棋
function checkGameOver() {
let itemCount = Object.values(girdMap).filter(item => item).length
// 当场上出现濒临死棋判断一下
if (itemCount == config.gridSize * config.gridSize) {
for (let x = 0; x < config.gridSize; x++) {
for (let y = 0; y < config.gridSize; y++) {
let topKey = `${x},${y - 1}`
let bottomKey = `${x},${y + 1}`
let leftKey = `${x - 1},${y}`
let rightKey = `${x + 1},${y}`
let selfKey = `${x},${y}`
let topVal = girdMap[topKey]
let bottomVal = girdMap[bottomKey]
let leftVal = girdMap[leftKey]
let rightVal = girdMap[rightKey]
let selfVal = girdMap[selfKey]
if (topVal == selfVal) return;
if (bottomVal == selfVal) return;
if (leftVal == selfVal) return;
if (rightVal == selfVal) return;
}
}
isGameOver = true;
showGameOver('游戏结束 啊啦啦啦!/(ㄒoㄒ)/~~')
}
}
initGrid();
// 生成两个 就掉了两次
randomGenerateElement();
randomGenerateElement();
document.addEventListener("keydown", (ev) => {
if (isGameOver) {
return;
}
if (inputCallParam[ev.code]) {
slideDirection(...inputCallParam[ev.code])
}
})
// 兼容触摸屏
let touchStartX = null
let touchStartY = null
document.addEventListener("touchstart", (ev) => {
let { pageX, pageY } = ev.changedTouches[0]
touchStartX = pageX;
touchStartY = pageY;
})
document.addEventListener("touchend", (ev) => {
let { pageX, pageY } = ev.changedTouches[0]
let offsetX = pageX - touchStartX;
let offsetY = pageY - touchStartY;
if (Math.abs(offsetX) > 20 || Math.abs(offsetY) > 20) {
if (Math.abs(offsetX) > Math.abs(offsetY)) {
slideDirection(Math.sign(offsetX), 0)
}
else {
slideDirection(0, Math.sign(offsetY))
}
}
})
</script>
</html>
在线预览
https://linyisonger.github.io/H5.Examples/
源码仓库
https://github.com/linyisonger/H5.Examples.git