前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个 ECharts 做的简易扫雷

一个 ECharts 做的简易扫雷

作者头像
ZXand618
发布2022-04-10 10:01:05
8480
发布2022-04-10 10:01:05
举报

最近突然想做个扫雷玩,因为发现 heatmap 就可以做(最近和 heatmap 杠上了),于是尝试了下。思路如下:

  1. 用二维数组做地雷数据,数组下标对应地雷的位置坐标,用数组的值表示砖块的状态(是否有地雷,是否翻开,周围地雷数);
  2. 把地雷数据转换成 heatmap 数据;
  3. 用 heatmap 做扫雷游戏区(砖块),绘制图形;
  4. 鼠标左键点击翻开砖块,判断结果,更新图表(扫雷游戏区);
  5. 鼠标右键点击标记地雷/取消标记。

地雷数据实现

定义一个生成地雷数据的函数,根据 x、y 轴尺寸(每行、每列砖块数),以及地雷数量随机生成二维数组:

代码语言:javascript
复制
// 随机生成地雷数据的函数
function generateMinesData(sizeX, sizeY, mines) {
    var size = sizeX * sizeY;
    var numList = [];
    var minesList = [];
    var res = [];
    
    // 准备一个 0 到 sizeX * sizeY - 1 的自然数列表(即所有砖块的顺序编号)
    for (var k = 0; k < size; k++) {
        numList.push(k);
    }
    
    // 从 numList 自然数列表中抽地雷
    for (var m = 0; m < mines; m++) {

        // 从 numList 中随机抽取一个元素,通过 splice 将其删除,并存入 minesList
        minesList.push(numList.splice(Math.floor(Math.random() * numList.length), 1)[0]);
    }

    // 生成二维数组地雷数据,格式为 res[x][y] = value 先全部置 10
    for (var i = 0; i < sizeX; i++) {
        res[i] = [];
        for (var j = 0; j < sizeY; j++) {
            res[i].push(10);
        }
    }

    // 遍历地雷列表,更新二维数组地雷数据,标记地雷 + 更新地雷周围的数字(砖块的附近地雷数)
    for (var n = 0; n < minesList.length; n++) {

        // 地雷顺序号换算 x,y 坐标
        x = Math.floor(minesList[n] / sizeX);
        y = minesList[n] % sizeY;

        // 标记地雷
        res[x][y] = 19;

        // 雷周围砖块数字加 1
        typeof(res[x][y + 1]) != 'undefined' && (res[x][y + 1] != 19) && (res[x][y + 1] += 1);
        typeof(res[x][y - 1]) != 'undefined' && (res[x][y - 1] != 19) && (res[x][y - 1] += 1);

        if (typeof(res[x + 1]) != 'undefined') {
            typeof(res[x + 1][y]) != 'undefined' && (res[x + 1][y] != 19) && (res[x + 1][y] += 1);
            typeof(res[x + 1][y + 1]) != 'undefined' && (res[x + 1][y + 1] != 19) && (res[x + 1][y + 1] += 1);
            typeof(res[x + 1][y - 1]) != 'undefined' && (res[x + 1][y - 1] != 19) && (res[x + 1][y - 1] += 1);

        }

        if (typeof(res[x - 1]) != 'undefined') {
            typeof(res[x - 1][y]) != 'undefined' && (res[x - 1][y] != 19) && (res[x - 1][y] += 1);
            typeof(res[x - 1][y + 1]) != 'undefined' && (res[x - 1][y + 1] != 19) && (res[x - 1][y + 1] += 1);
            typeof(res[x - 1][y - 1]) != 'undefined' && (res[x - 1][y - 1] != 19) && (res[x - 1][y - 1] += 1);
        }
    }
    return res;
}
  1. 根据输入参数 sizeX 和 sizeY ,生成一个砖块顺序号列表 numList,范围是 0 到 sizeX * sizeY - 1 ;
  2. 利用 Math.random() 和 Math.floor() 从砖块顺序号列表 numList 中随机抽取砖块,作为地雷的位置,存入地雷列表 MinesList ;
  3. 利用循环嵌套生成所有值都是 10 的二维数组 res (10 代表:砖块未翻开,并且砖块周围没有地雷);
  4. 遍历地雷列表 MinesList ,通过取模和取余运算把地雷顺序号换算成地雷坐标 x、y,把二维数组 res 中对应位置标记为地雷,再把该位置周围的无地雷砖块 value 加 1 (周围地雷数 + 1);
  5. 返回最终的二维数组 res (地雷数据)。

heatmap 实现

heatmap 数据格式如下:

代码语言:javascript
复制
series: [{
    data: [
        // 维度X   维度Y   其他维度 ...
        [  3.4,    4.5,   15,   43],
        [  4.2,    2.3,   20,   91],
        [  10.8,   9.5,   30,   18],
        [  7.2,    8.8,   18,   57]
    ]
}]

//摘自 ECharts 配置项手册 series[i]-heatmap.data[i] 处

为了省事,我就用了一个其他维度,格式如下:

代码语言:javascript
复制
[
  [ x, y, value],
  ...
]

其中 value 的取值含义为:

  • 10-18 代表周围的地雷数为 0-8(未翻开)
  • 0-8 代表周围地雷数为 0-8(已翻开)
  • 20-28 代表周围的地雷数为 0-8(已标记「P」)
  • 19 代表此处有地雷(未翻开)
  • 9 代表此处有地雷(已翻开)
  • 29 代表此处有地雷(已标记「P」)

砖块的坐标(x, y)

这块由于我个人奇葩偏好,希望横着看是 (0,0),(0,1),(0,2),... 这样的,于是把 x、y 的坐标互换了:

将地雷数据转换为 heatmap 数据:

代码语言:javascript
复制
// 地雷数组转换为 heatmap 数据:[[value,value,value,value,...],...] => [[x,y,value],...]
function covertData(arr) {
    var res = [];
    for (var i = 0; i < arr.length; i++) {
        for (var j = 0; j < arr[i].length; j++) {
            
            // 调换一下 x、y,这样横着看是 (0,0),(0,1),(0,2),个人感觉比较舒服……
            res.push([j, i, arr[i][j]]);
        }
    }
    return res;
}

用 heatmap 做扫雷游戏区(砖块)

生成配置项 option,并用其渲染图表(这里是写了个函数,通过函数返回 option )

代码语言:javascript
复制
// option设置,通过回调函数自定义标签文字(P 代表标记,* 代表地雷,数字代表周围地雷数)和砖块颜色(浅色代表翻开)
function getOption(data) {
    option = {
        title: {
            text: '扫雷游戏',
            subtext: '剩余雷数:' + minesLeft
        },
        tooltip: {
            show: false
        },
        grid: {
            width: '80%',
            height: '80%',
            left: '10px',
            top: '15%'
        },
        xAxis: {
            show: false,
            type: 'category',
            splitArea: {
                show: true
            }
        },
        yAxis: {
            show: false,
            type: 'category',
            splitArea: {
                show: true
            }
        },
        series: [{
            id: 'btnPanel',
            type: 'heatmap',
            label: {
                normal: {
                    show: true,
                    color: '#000',
                    
                    // 回调函数自定义标签文字
                    formatter: function(params) {
                        if (params.data[2] >= 20) {
                            return 'P';
                        } else if (params.data[2] >= 10 || params.data[2] === 0) {
                            return '';
                        } else if (params.data[2] === 9) {
                            return '*';
                        } else {
                            return params.data[2];
                        }
                    }
                }
            },
            itemStyle: {
                
                // 回调函数自定义砖块颜色
                color: function(params) {
                    if (params.data[2] >= 10) {
                        return '#ddd';
                    }
                    return '#fff';
                },
                borderColor: '#AAA',
                borderWidth: 2
            },
            data: data
        }]
    };
    return option;
}


// 渲染图表
myChart.setOption(getOption(heatmapData));
  • option.series[i]-heatmap.label.normal.formatter 是标签的格式,也就是砖块上的文字,这里使用回调函数判断 params.data[2] 的数值,根据判断结果显示数字 / 显示标记 / 显示数值,其中标记「P」代表旗帜,「*」代表地雷(失败的时候显示);
  • option.series[i]-heatmap.itemStyle.color 是砖块的颜色,这里使用回调函数判断砖块是否被翻开(小于 10),如翻开砖块是白色。

鼠标左键翻开砖块

监听、响应鼠标单击事件:

代码语言:javascript
复制
// 点击热力图时调用 btnClick 函数 (翻开砖块)
myChart.on('click', function(params) {
    if (params.seriesId === 'btnPanel' && flag === 0) {

        // 因为调换了 x、y,这里也要把 params.data[1] 放在前面
        btnClick(params.data[1], params.data[0]);
    }
});

btnClick() 用于完成翻开砖块的操作:

代码语言:javascript
复制
// 按钮点击响应函数
function btnClick(btnX, btnY) {

    // 点中已标记的砖块,不做操作,退出
    if (minesData[btnX][btnY] > 19) {
        return;
    }

    // 如果点中地雷(19),则游戏结束(失败)
    if (minesData[btnX][btnY] === 19) {
        flag = 1;
        minesData[btnX][btnY] -= 10;
        
        // 更新地图数据,修改自定义标签规则,把所有地雷显示出来
        return myChart.setOption({
            title: {
                subtext: '游戏结束…'
            },
            series: {
                label: {
                    formatter: function(params) {

                        if (params.data[2] >= 20) {
                            return 'P';
                        } else if (params.data[2] === 9 || params.data[2] === 19) {
                            return '*';
                        } else if (params.data[2] >= 10 || params.data[2] === 0) {
                            return '';
                        } else {
                            return params.data[2];
                        }

                    }
                },
                data: covertData(minesData)
            }
        });

    // 翻到附近有地雷的砖块,更新 value 值,更新剩余待翻开砖块数,显示数字(更新 heatmap 数据)   
    } else if (minesData[btnX][btnY] > 10) {

        minesData[btnX][btnY] -= 10;
        bricksLeft--;
        
        // 三元表达式,如果剩余带翻开砖块为 0 则提示胜利,否则正常更新 heatmap 数据
        bricksLeft === 0 ?
        myChart.setOption({
            title:{
                subtext:'胜利'
            },
            series: {
                data: covertData(minesData)
            }
        }) :
        myChart.setOption({
            series: {
                data: covertData(minesData)
            }
        });

    // 翻到附近没有地雷的砖块,自动翻开周围的砖块(更新其 value 值),然后更新
    } else if (minesData[btnX][btnY] === 10) {

        // 调用自动翻开周围砖块的函数
        autoClick(btnX, btnY);
        
        // 三元表达式,如果剩余带翻开砖块为 0 则提示胜利,否则正常更新 heatmap 数据
        bricksLeft === 0 ?
        myChart.setOption({
            title:{
                subtext:'胜利'
            },
            series: {
                data: covertData(minesData)
            }
        }) :
        myChart.setOption({
            series: {
                data: covertData(minesData)
            }
        });
    }
}
  • 点中已标记的砖块,不做操作,退出;
  • 如果点中地雷(value 等于 19),则游戏结束(失败),修改自定义标签规则,将全部地雷显示出来,副标题提示失败;
  • 翻到附近有地雷的砖块(value 不等于 10),则直接翻开(value 减 10,剩余待翻砖块数减 bricksLeft 10),然后判断是否已翻开全部待翻砖块(bricksLeft 等于 0),提示胜利或仅更新 heatmap 数据;
  • 翻到附近没有地雷的砖块,调用 autoClick() 函数自动翻开周围的砖块(更新其 value 值),然后判断是否已翻开全部待翻砖块,提示胜利或仅更新 heatmap 数据。

autoClick() 用于对目标砖块及其()周围的砖块进行递归验证、翻开(更新地雷数据的 value):

代码语言:javascript
复制
// 对 minesData[x][y] 周围的砖块进行递归验证、翻开
function autoClick(x, y) {

    // 定义子函数,翻开目标砖块,并判断该砖块是否需要递归处理,返回 true/false
    function check(x1, y1) {

        // 如果 minesData[x1] 未定义(目标砖块不存在)则退出
        if (typeof(minesData[x1]) == 'undefined') {
            return false;
        }

        // 如砖块未翻开并且未标记,则翻开判断周围有没有地雷,没有雷返回 true
        if (minesData[x1][y1] > 10 && minesData[x1][y1] < 20) {
            minesData[x1][y1] -= 10;
            bricksLeft--;
            return false;
        }
        
        if (minesData[x1][y1] === 10) {
            return true;
        }
    }

    // 翻开当前砖块
    minesData[x][y] -= 10;
    bricksLeft--;

    // 判断周围砖块,根据情况递归
    check(x, y + 1) && autoClick(x, y + 1);
    check(x, y - 1) && autoClick(x, y - 1);

    check(x + 1, y) && autoClick(x + 1, y);
    check(x - 1, y) && autoClick(x - 1, y);

    check(x + 1, y + 1) && autoClick(x + 1, y + 1);
    check(x + 1, y - 1) && autoClick(x + 1, y - 1);

    check(x - 1, y + 1) && autoClick(x - 1, y + 1);
    check(x - 1, y - 1) && autoClick(x - 1, y - 1);

}
  • 定义 check() 子函数,用于翻开目标砖块,并判断该砖块是否需要递归处理,返回 true/false;
    • minesData[x1] 未定义(目标砖块不存在)则退出;
    • 如砖块未翻开并且未标记,则判断周围有没有地雷,如没有雷直接返回 true,有雷则翻开 + 返回 false;
  • 翻开当前砖块( autoClick 函数的输入参数 x、y 对应的砖块);
  • 分别调用 check() 子函数判断周围砖块,根据情况调用 autoClick() 递归处理。

鼠标右键标记地雷 / 取消标记

标记地雷 / 取消标记:

代码语言:javascript
复制
// 去除默认的鼠标事件
document.oncontextmenu = function() {
    return false;
};

// 新加上鼠标右击事件(标记地雷 / 取消标记)
myChart.on('contextmenu', function(params) {

    if (params.seriesId === 'btnPanel' && flag === 0) {

        // 如果 value 大于 19 ,将已标记的砖块取消标记
        if (minesData[params.data[1]][params.data[0]] > 19) {
            minesData[params.data[1]][params.data[0]] -= 10;
            minesLeft++;
        
        // 或者如果 value 大于等于 10 ,标记砖块
        } else if (minesData[params.data[1]][params.data[0]] >= 10) {
            minesData[params.data[1]][params.data[0]] += 10;
            minesLeft--;
        }
        
        // 更新 heatmap 数据(这块漏了剩余地雷数的更新- -b )
        myChart.setOption({
            series: {
                data: covertData(minesData)
            }
        });
    }
    //console.log(params);
});

如果游戏未结束(flag 等于 0),则响应右键点击:

  • 如果 value 大于 19 ,将已标记的砖块取消标记;
  • 或者如果 value 大于等于 10 ,标记砖块;
  • 更新 heatmap 数据。

P.S. 本来用这样写的循环 for (let i = 0 ; i < 100; i++){} ,结果手机打不开,只好把 let 都改成 var 了……

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ZXand618的ECharts之旅 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
图数据库 KonisGraph
图数据库 KonisGraph(TencentDB for KonisGraph)是一种云端图数据库服务,基于腾讯在海量图数据上的实践经验,提供一站式海量图数据存储、管理、实时查询、计算、可视化分析能力;KonisGraph 支持属性图模型和 TinkerPop Gremlin 查询语言,能够帮助用户快速完成对图数据的建模、查询和可视化分析。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档