JavaScript实现经典消方块游戏

发布于:2023-01-10 ⋅ 阅读:(376) ⋅ 点赞:(0)

诶哟图丢了

操作方式

在游戏区域中任意位置滑动手势,点击屏幕下方的按钮,键盘WASD都可以操作。

游戏动作 操作
方块向左移动 左划、按下蓝色键(左一)、A
方块向右移动 右划、按下橙色键(右一)、D
强制方块下落 下划、按下粉色键(左二)、S
改变方块方向 上划或轻触、按下绿色键(右二)、W

游戏内容

初始化游戏区域啥都没有,每种方块面积都是4个方格,如果水平一行充满方块那么这一行就被整体消去,否则方块堆积越来越高直到超过一个阈值使游戏结束。每消一行加一分,同时游戏速度提高10ms。整个游戏就一个文件,浏览器打开。颜色好看。

代码

用了es6的语法,浏览器不能太老。

<!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, maximum-scale=1.0, user-scalable=0;" />
    <title>Demo</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            user-select: none;
        }

        body {
            overflow: hidden;
        }

        #win {
            height: 75vh;
        }

        @media only screen and(min-aspect-ratio:1/1) {
            #win {
                height: 80vmin;
            }
        }

        #peek {
            max-height: 100%;
            max-width: 100%;
        }

        #score {
            font-family: sans-serif;
            font-size: 30px;
            text-align: center;
            padding: 0.5rem 0;
        }

        .container {
            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
        }

        .flex {
            display: flex;
        }

        .center {
            justify-content: space-around;
        }

        #toolbox>* {
            text-align: center;
            font-size: 7.5vmin;
            width: 15vmin;
            height: 15vmin;
            line-height: 15vmin;
        }

        #toolbox>div {
            border-radius: 20%;
            transition: background linear 0.5s;
        }

        #toolbox>div:active {
            background: #fff;
            transition: background linear 0s;
        }

        #toolbox>div svg {
            margin: 25%;
        }

        #toolbox>div svg * {
            fill: #fff;
        }
    </style>
</head>

<body>
    <div class="container">
        <div id="score"></div>
        <div class="flex center">
            <canvas id="win"></canvas>
        </div>
        <div>&emsp;</div>
        <div id="toolbox" class="flex center">
            <div onclick="handleButtonDown(65)" style="background: #08f;">
                <svg viewBox="0 0 10 10">
                    <polygon points="0,5 10,0 10,10" />
                </svg>
            </div>
            <div onclick="handleButtonDown(83)" style="background: #f08;">
                <svg viewBox="0 0 10 10">
                    <polygon points="0,0 10,0 5,10" />
                </svg>
            </div>
            <span>
                <canvas id="peek"></canvas>
            </span>
            <div onclick="handleButtonDown(87)" style="background: #8c0;">
                <svg viewBox="0 0 10 10">
                    <circle cx="5" cy="5" r="5" />
                </svg>
            </div>
            <div onclick="handleButtonDown(68)" style="background: #f80;">
                <svg viewBox="0 0 10 10">
                    <polygon points="0,10 0,0 10,5" />
                </svg>
            </div>
        </div>
        <div>&emsp;</div>
    </div>
</body>
<script>
    const BLOCKS = initBlockSet([
        [
            [0, 1, 1],
            [1, 0, 0],
        ], [
            [1, 1, 0],
            [0, 0, 1],
        ], [
            [1, 0, 1],
            [0, 1, 0],
        ], [
            [1, 1],
            [0, 1],
        ], [
            [1, 1, 1],
        ], [
            [1,],
        ] // 改这些数字还可以用不同面积的方块游戏,这是3块的
    ]);

    const GAME = document.getElementById('win');
    const PEEK = document.getElementById('peek');
    const SCORE = document.getElementById('score');
    const CTX = GAME.getContext('2' + 'd');
    const PEEK_CTX = PEEK.getContext('2' + 'd'); // 预览下一块
    const FLEX = 2;
    const WIDTH = 10; // 游戏屏幕宽度,单位是方格
    const HEIGHT = 18; // 游戏屏幕高度
    const INIT_DELAY = 400; // 初始游戏速度,值越小速度越快
    const SENSITIVITY = 50; // 移动端触屏滑动距离超过这个值就移动方块,否则改变方块方向
    const TOUCH_MOVE_DELAY = 50; // 连续滑动改为间歇

    // 添加触摸事件
    GAME.addEventListener('touchstart', handleHandDown, false);
    GAME.addEventListener('touchend', handleHandUp, false);
    GAME.addEventListener('touchmove', handleHandMove, false);

    // 初始化游戏和控制用变量~
    var game = Game();
    var lastTime = new Date().getTime();
    var lastMove = new Date().getTime();
    var blockSize = 10;
    var key = '';
    var touchX = 0;
    var touchY = 0;

    // 屏幕尺寸变化时canvas的图像可能失真,所以要计算一个格的宽度以便画出清晰的游戏画面
    function handleResize () {
        let rect = GAME.getBoundingClientRect();
        blockSize = GAME.offsetWidth / WIDTH;
    }

    // 用户操作时调用
    function handleHandDown (e) {
        e.preventDefault();
        let touch = e.touches[0] || e.changedTouches[0] || { clientX: e.clientX, clientY: e.clientX };
        touchX = touch.clientX;
        touchY = touch.clientY;

    }

    function handleHandMove (e) {
        let now = new Date().getTime();
        if (now - lastMove < TOUCH_MOVE_DELAY) {
            return false;
        }
        lastMove = now;
        let touch = e.touches[0] || e.changedTouches[0] || { clientX: e.clientX, clientY: e.clientX };
        if (touch.clientX - touchX > SENSITIVITY) {
            handleKeyDown({ keyCode: 68 })
        } else if (touchX - touch.clientX > SENSITIVITY) {
            handleKeyDown({ keyCode: 65 })
        }
    }

    function handleHandUp (e) {
        let touch = e.touches[0] || e.changedTouches[0] || { clientX: e.clientX, clientY: e.clientX };
        if (touch.clientX - touchX > SENSITIVITY) {
            handleKeyDown({ keyCode: 68 })
        } else if (touchX - touch.clientX > SENSITIVITY) {
            handleKeyDown({ keyCode: 65 })
        } else if (touch.clientY - touchY > SENSITIVITY) {
            handleKeyDown({ keyCode: 83 })
        } else if (touchY - touch.clientY >= 0) {
            handleKeyDown({ keyCode: 87 })
        }
    }

    function handleButtonDown (input) {
        handleKeyDown({ keyCode: input })
    }

    function handleKeyDown (e) {
        if (!game.isLiving()) {
            game = Game(); // 不判断按键,只要游戏结束,按任意键都能开局
            return;
        }
        switch (e.keyCode) {
            case 37:
            case 65:
                key = 'a';
                break;
            case 38:
            case 87:
                key = 'w';
                break;
            case 39:
            case 68:
                key = 'd';
                break;
            case 40:
            case 83:
                key = 's';
                break;
            default:
                key = '';
                break;
        }
    }

    // 初始化方块数据,计算各种方块不同方向的形状
    function initBlockSet (seed) {
        const FANCY_COUNT = 4;
        let ret = [];
        for (let styleIndex = 0, styleCount = seed.length; styleIndex < styleCount; styleIndex++) {
            const model = [];
            let style = seed[styleIndex];
            for (let fancyIndex = 0; fancyIndex < FANCY_COUNT; fancyIndex++) {
                let fancy = [];
                for (let x = 0, w = style[0].length; x < w; x++) {
                    let line = [];
                    for (let y = style.length - 1; y >= 0; y--) {
                        const pixel = style[y][x];
                        line.push(pixel);
                    }
                    fancy.push(line);
                }
                model.push(fancy);
                style = fancy;
            }
            ret.push(model);
        }
        return ret;
    }

    // 初始化一个方块,颜色、方向、种类都随机,位置在屏幕顶端居中
    function initBlock (x, y, style, fancy, color) {
        return {
            color: color || `hsl(${Math.random() * 360},60%,70%)`,
            fancy: (fancy === undefined || fancy === null) ? Math.floor(Math.random() * 4) : fancy,
            style: (style === undefined || style === null) ? Math.floor(Math.random() * BLOCKS.length) : style,
            x: (x === undefined || x === null) ? Math.floor(WIDTH / 2) : x,
            y: (y === undefined || y === null) ? 0 : y,
        }
    }

    // 判断方块能否安放当前位置,若方块有一部分超出屏幕两侧或下端或者与其它已经放置的方块重合都返回false
    function canFill (block, map) {
        const model = BLOCKS[block.style][block.fancy % BLOCKS[block.style].length];
        const w = model[0].length;
        const h = model.length;
        const left = block.x - Math.floor(w / 2);
        const top = block.y - Math.floor(h / 2);
        for (let y = 0; y < h; y++) {
            const absoluteY = top + y;
            if (absoluteY < 0) {
                continue;
            }
            for (let x = 0; x < w; x++) {
                const absoluteX = left + x;
                const blockPoint = model[y][x];
                if (blockPoint) {
                    if (absoluteX < 0 || absoluteX >= WIDTH) {
                        return false;
                    }
                    if (absoluteY >= HEIGHT) {
                        return false;
                    }
                    const mapPoint = map[absoluteY] ? map[absoluteY][absoluteX] : false;
                    if (mapPoint) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    // 将活动的方块固定
    function fill (block, map) {
        const model = BLOCKS[block.style][block.fancy % BLOCKS[block.style].length];
        const w = model[0].length;
        const h = model.length
        const left = block.x - Math.floor(w / 2)
        const top = block.y - Math.floor(h / 2)
        for (let y = 0; y < h; y++) {
            for (let x = 0; x < w; x++) {
                const blockPoint = model[y][x];
                if (blockPoint && map[top + y]) {
                    map[top + y][left + x] = block.color;
                }
            }
        }
    }

    // 游戏“类”
    function Game () {
        let map = [];
        for (let y = 0; y < HEIGHT; y++) {
            map.push([]);
        }
        var bgColor = `hsla(${Math.random() * 360},100%,30%,0.05)`;
        var score = 0;
        var living = true;
        let hold = initBlock();
        let next = initBlock();
        let falling = false;
        let draw = function () {
            GAME.width = blockSize * WIDTH;
            GAME.height = blockSize * HEIGHT;
            for (let y = 0; y < HEIGHT; y++) {
                for (let x = 0; x < WIDTH; x++) {
                    if (map[y][x]) {
                        CTX.fillStyle = map[y][x];
                        CTX.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
                    } else if (x % 2 != y % 2) {
                        CTX.fillStyle = bgColor;
                        CTX.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
                    }
                }
            }
            let moving = BLOCKS[hold.style][hold.fancy % BLOCKS[hold.style].length];
            const w = moving[0].length;
            const h = moving.length;
            const left = hold.x - Math.floor(w / 2);
            const top = hold.y - Math.floor(h / 2);
            for (let y = 0; y < w; y++) {
                for (let x = 0; x < h; x++) {
                    if (moving[y][x]) {
                        CTX.fillStyle = hold.color;
                        CTX.fillRect((x + left) * blockSize, (y + top) * blockSize, blockSize, blockSize);
                    }
                }
            }
            let peek = BLOCKS[next.style][next.fancy % BLOCKS[next.style].length];
            PEEK.width = blockSize * peek[0].length;
            PEEK.height = blockSize * peek.length;
            for (let y = 0; y < peek.length; y++) {
                for (let x = 0; x < peek[0].length; x++) {
                    if (peek[y][x]) {
                        PEEK_CTX.fillStyle = next.color;
                        PEEK_CTX.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
                    }
                }
            }
            SCORE.innerHTML = score;
        }
        let control = function (key) {
            let moved = hold;
            switch (key) {
                case 'a':
                    moved = initBlock(hold.x - 1, hold.y, hold.style, hold.fancy, hold.color);
                    break;
                case 'w': // 改变了方块的方向将导致方块宽度和高度变化,原本能放下方块的空间可能放不下操作后的方块,需要专门判断
                    moved = initBlock(hold.x, hold.y, hold.style, hold.fancy + 1, hold.color);
                    if (canFill(moved, map)) {
                        break;
                    }
                    //不能移动,左右滑移1-2格判断是否能放置
                    for (let offset = 1, i = 0; offset <= FLEX; offset = (++i % 2 ? offset : (offset + 1)) * -1) {
                        let floating = initBlock(hold.x + offset, hold.y, hold.style, hold.fancy, hold.color);
                        if (canFill(floating, map)) {
                            moved = floating; // 滑移后能放置
                            break;
                        }
                    }
                    break;
                case 's':
                    falling = true;
                    break;
                case 'd':
                    moved = initBlock(hold.x + 1, hold.y, hold.style, hold.fancy, hold.color);
                    break;
            }
            if (canFill(moved, map)) {
                hold = moved; // 放置但不固定,它还没落到底
            }
        }
        let step = function () {
            if (!living) {
                falling = false;
                return;
            }
            let moved = initBlock(hold.x, hold.y + 1, hold.style, hold.fancy, hold.color); // 下降一格
            if (canFill(moved, map)) {
                hold = moved; // 如果下降一格后能放置就放置
            } else { // 如果不能,那么当前是能放置的且方块已经被其它方块或游戏屏幕底部阻挡
                if (map[0].length) { // 如果游戏区域第一行有已经固定的方块那么游戏结束
                    living = false;
                    falling = false;
                    return;
                }
                //将hold置入map
                fill(hold, map);
                hold = next;
                next = initBlock();
                key = '';
                falling = false;
            }
            let cleared = []
            for (let y = HEIGHT - 1; y >= 0; y--) {
                let filled = 0; // 计数,用来判断一行是不是已经满了
                for (let x = 0; x < WIDTH; x++) {
                    if (map[y][x]) {
                        filled++;
                    }
                }
                if (filled < WIDTH) {
                    cleared.unshift(map[y]); // 如果一行没满i就加入更新的游戏区域
                } else {
                    score++;
                }
            }
            while (cleared.length < HEIGHT) {
                cleared.unshift([]); // 补齐高度
            }
            map = cleared;
        }
        return {
            step: step,
            draw: draw,
            control: control,
            getScore: () => score,
            isLiving: () => living,
            isFalling: () => falling,
        };
    }
    function intervalCallback () {
        let now = new Date().getTime();
        // 根据游戏得分以及玩家的操作动态控制游戏进度更新频率
        if (now - lastTime >= INIT_DELAY - game.getScore() * 10 || game.isFalling()) {
            game.step();
            lastTime = new Date().getTime();
        }
        if (game.isLiving()) { // 只要游戏没有结束,就一直刷新,要不然一卡一卡的难受
            game.control(key);
            key = '';
            game.draw();
        }
        requestAnimationFrame(intervalCallback);
    }
    window.onresize = handleResize;
    window.onkeydown = handleKeyDown;
    requestAnimationFrame(intervalCallback);
    handleResize();
</script>

</html>

网站公告

今日签到

点亮在社区的每一天
去签到