Bootstrap

H5 2048 都2024年了 应该没人看这个了吧

我是不喜欢学习理论的程序员,有没有人和我一样,看到原理都有点烦(((φ(◎ロ◎;)φ)))

效果

仅仅演示了失败的,成功的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

;