使用到的库:Vue2,Vant2,Less

目录

前言

一、HTML部分

二、CSS样式(LESS)

三、Data数据

四、methods

五、预览效果

总结


前言

一开始在力扣看到扫雷的题,发现内容并不复杂,就想通过这个逻辑写一个单个html页面就能解决的小游戏,使用了Vue2+Vant2,因为内容不多所以没有使用脚手架,所有库直接使用cdn引入,所有功能在一个页面里解决,通过v-if进行Dom节点的渲染。

完整代码地址:

GitHub - jujubefoxx/MineSweeper: 用Vue(cdn)写个扫雷网页版用Vue(cdn)写个扫雷网页版. Contribute to jujubefoxx/MineSweeper development by creating an account on GitHub.https://github.com/jujubefoxx/MineSweeper在线玩:

来扫雷咯https://jujubefoxx.github.io/MineSweeper/


一、HTML部分

  • 顶部信息栏:进入页面后显示请选择难度提示,在选择难度后展示当前难度和进行的时间、剩余的炸弹数量以及当前最高纪录。
<div class="top"><div v-if="hasChoseLevel" class="top-info"><div class="top-info__list"><div>难度:{{ gameInfo.level }}</div><div>时间:{{ gameInfo.time }}s</div></div><div class="top-info__list"><div>剩余:{{ gameInfo.boomNum - flagNum < 0 ? 0 : gameInfo.boomNum - flagNum }}</div><div>最高纪录:{{ gameInfo.record ? `${gameInfo.record}s` : '无' }}</div></div></div><div v-else>请选择难度</div>
</div>
  • 主页:进行游戏难度的选择和自定义图标
    <div v-if="!hasChoseLevel"><div class="level"><div class="level-list" v-for="(item,index) in config" :key="index" @click="choseLevel(item)">{{`${item.level} (${item.xNum}×${item.yNum})`}}</div></div><div class="custom"><van-button plain type="info" @click="showOverlay = true">自定义棋盘图片</van-button></div></div>
  • 棋盘:根据生成的随机二维数组生成对应难度的棋盘格,并根据格子状态显示对应的样式,底部排列重新开始和插旗和返回主页按钮(这里的数字格子我用了自己的图片,如果没有的话直接使用column.data即可)

    <div v-else class="game-content"><div class="board"><div v-for="(row,index) in board" class="board-row"><div v-for="(column,key) in row":class="['board-column',gameInfo.yNum <= 9?'board-column--small':gameInfo.yNum < 30?'board-column--middle':'board-column--large']"@click="isSetFlag ? setFlag([index,key], column) : isInit ? updateBoard([index,key],column.data) : setBoom([index,key])"><div :class="['board-column__list',column.isShow ? 'board-column__list--show':'board-column__list--unknown']"><img v-if="column.data === 'X'" class="board-column__img" :src="boomImg"alt="炸弹"><img v-else-if="column.data === 'F'":class="['board-column__img',!isSetFlag ? 'board-column__img--disable' : '']":src="flagImg"alt="旗帜"><!--                        <div v-else-if="parseInt(column.data) > 0">{{ column.data }}</div>--><img v-else-if="parseInt(column.data) > 0"class="board-column__img board-column__img--number":src="`static/images/shuzi${column.data}.svg`":alt="column.data"></div></div></div></div><div class="bottom"><div class="bottom-button"><div v-if="!over":class="['bottom-button__list bottom-button__flag',isSetFlag ? 'bottom-button__flag--active':'']"@click="isSetFlag = !isSetFlag">插旗</div><div class="bottom-button__list bottom-button__restart"@click="handleRestart">重新开始</div><div class="bottom-button__list bottom-button__choose"@click="handleRestart(false)">重新选择难度</div></div><div v-if="over" class="bottom-tips">踩到地雷,游戏结束</div></div></div>
  • 自定义图标的弹出层:弹出一个遮罩层,进行自定义图标的上传和效果预览
    <van-overlay :show="showOverlay" @click="showOverlay = false"><div class="custom-wrapper" @click.stop><div class="custom-wrapper__upload"><van-uploader class="custom-wrapper__upload-list":before-read="beforeRead":after-read="(file)=> afterRead(file,'boom')"@oversize="onOversize"><van-button icon="plus" type="default">更换地雷图标</van-button></van-uploader><van-uploader class="custom-wrapper__upload-list":before-read="beforeRead":after-read="(file)=> afterRead(file,'flag')"@oversize="onOversize"><van-button icon="plus" type="default">更换旗帜图标</van-button></van-uploader><van-button class="custom-wrapper__upload-list" icon="replay" type="default" @click="resetImg">还原默认图标</van-button></div><div class="custom-wrapper__preview"><div class="custom-wrapper__preview-title">预览效果</div><div class="custom-wrapper__preview-board"><div class="board-row"><div :class="['board-column','board-column--small']"><div :class="['board-column__list','board-column__list--unknown']"></div></div><div :class="['board-column','board-column--small']"><div :class="['board-column__list','board-column__list--show']"><img class="board-column__img":src="boomImg"alt="炸弹"></div></div><div :class="['board-column','board-column--small']"><div :class="['board-column__list','board-column__list--unknown']"><img class="board-column__img":src="flagImg"alt="旗帜"></div></div></div></div></div><div class="custom-wrapper__tips">请上传图片(建议使用白色或透明底的正方形图片)<br/>注意:清除缓存会还原设置的图片</div></div></van-overlay>

二、CSS样式(LESS)

// 自定义遮罩层
.custom {text-align: center;margin: 20px auto;&-wrapper {width: 300px;margin: 30px auto;padding: 20px;background: #fff;border-radius: 4px;&__upload {display: flex;flex-direction: column;justify-content: space-around;align-items: center;margin: 10px 0;&-list {margin: 5px 0 !important;}}&__tips {margin-top: 30px;font-size: 12px;color: #606266;text-align: center;}&__preview {margin-top: 30px;text-align: center;&-title {margin: 10px;}}}&-cropper {width: 500px;height: 500px;}
}//顶部
.top {margin: 10px auto;color: #222;font-weight: bold;max-width: 370px;font-size: 14px;&-info {display: flex;flex-direction: column;&__list {display: flex;justify-content: space-between;padding: 0 10px;flex: 1;}}
}//棋盘
.board {max-width: 370px;background: whitesmoke;padding: 10px;margin: 10px auto;&-row {display: flex;overflow: hidden;justify-content: center;}&-column {display: flex;justify-content: center;align-items: center;//flex: 1;margin: 1px;background: gainsboro;overflow: hidden;&--small {width: 38px;height: 38px;}&--middle {width: 20px;height: 20px;font-size: 12px;}&--large {width: 20px;height: 20px;font-size: 12px;}&__list {display: flex;justify-content: center;align-items: center;width: 100%;height: 100%;overflow: hidden;&--unknown {cursor: pointer;}&--show {box-sizing: border-box;background: #fff;box-shadow: gainsboro 2px 2px inset;padding: 2px 0 0 2px;}}&__img {width: 85%;&--disable {cursor: auto;}}}
}//底部
.bottom {margin-top: 20px;&-tips {margin-top: 20px;text-align: center;color: #aaa;font-size: 24px;font-weight: 600;}&-button {display: flex;align-items: center;justify-content: center;flex-direction: column;&__list {display: inline-block;width: 130px;height: 44px;line-height: 44px;text-align: center;white-space: nowrap;cursor: pointer;background: #fff;border: 1px solid #dcdfe6;color: #606266;box-sizing: border-box;outline: none;margin: 10px;transition: .1s;font-weight: 500;font-size: 14px;border-radius: 4px;}&__restart {color: #fff;background: #909399;}&__flag {color: #fff;background-color: #409eff;border-color: #409eff;&--active {color: #409eff;background: #ecf5ff;border-color: #b3d8ff;}}}
}//等级
.level {display: flex;align-items: center;flex-direction: column;justify-content: space-around;max-width: 360px;margin: 10px auto;padding: 16px;border: 2px dashed #409eff;&-list {display: inline-block;width: 200px;height: 30px;line-height: 30px;margin: 10px;white-space: nowrap;cursor: pointer;-webkit-appearance: none;text-align: center;box-sizing: border-box;outline: none;transition: .1s;font-weight: 500;-moz-user-select: none;-webkit-user-select: none;-ms-user-select: none;font-size: 14px;border-radius: 20px;color: #fff;background-color: #409eff;border-color: #409eff;}
}

三、Data数据

  • 初始化的数据
created() {// 判断本地缓存中是否已上传自定义图标['boom', 'flag'].forEach((item) => {if (localStorage.getItem(`${item}_img`)) {this[`${item}Img`] = localStorage.getItem(`${item}_img`);}})
},
  • Data内

    data() {return {showOverlay: false,// 遮罩层开关hasChoseLevel: false, // 难度选择页面开关boomImg: 'static/images/icon_boom1.svg',flagImg: 'static/images/flag.svg', // 默认//难度配置config: [{alias: 'easy',level: '青铜',xNum: 9, // 列数yNum: 9, // 行数boomNum: 10, // 炸弹数}, {alias: 'middle',level: '白银',xNum: 16,yNum: 16,boomNum: 40,}, {alias: 'hard',level: '黄金',xNum: 16,yNum: 30,boomNum: 99,}],// 难度评级配置scoreLevel: {easy: {0.49: '100',40: '99',70: '88',90: '80',110: '77',180: '66',240: '55',500: '35',800: '15',1000: '10',1300: '1'},// 452 293middle: {7.03: '100',250: '99',500: '88',800: '77',1000: '66',1200: '55',1500: '35',2000: '15',2500: '10'},hard: {31.13: '100',500: '99',800: '88',1200: '77',1800: '66',2300: '55',3000: '35',5000: '15',8000: '10'}},// 当前游戏信息gameInfo: {alias: 'easy',level: '青铜', // 难度等级time: 0, // 时间record: undefined,// 最高纪录xNum: 9, // 列数yNum: 9, // 行数boomNum: 10, // 炸弹数},flagNum: 0, // 旗帜数isInit: false, // 保证第一个点击的格子不是雷board: [], // 棋盘over: false, // 游戏是否结束isSetFlag: false,// 是否插旗状态timer: undefined,// 计时器}
    },
  • 计算属性
    computed: {// 计算已翻开的格子数 当翻开的格子数=安全的格子数时游戏胜利showCount() {const {board} = this;const arr = board.flat();let num = 0;arr.forEach((item) => {if (item.isShow) {num++}})return num},// 当前难度的非雷格子数saveNum() {const {gameInfo: {xNum, yNum, boomNum}} = this;return yNum * xNum - boomNum},
    },
  • 监听
    watch: {// 监听翻开的格子数,判断游戏是否结束showCount() {const {gameInfo: {time, alias, record}, over, showCount, saveNum, scoreLevel} = this;if (over) return; // 处理最后一个点击到炸弹的异常情况if (showCount === saveNum) {// 停止计时clearInterval(this.timer)// 击败玩家百分比let percent = '1';for (const key in scoreLevel[alias]) {console.log(key)if (parseFloat(time) < key) {percent = scoreLevel[alias][key]console.log(key, '是这里', percent)break}}// 判断是否为最高纪录if (!record || parseFloat(time) < parseFloat(record)) {localStorage.setItem(`${alias}_record`, time)this.gameInfo.record = time;}// 弹出对话框vant.Dialog({message: `恭喜用时${time}秒挑战成功!\n击败了${percent}%的玩家!\n您的最高纪录:${this.gameInfo.record}秒`,confirmButtonColor: '#409eff',confirmButtonText: '重新开始',showCancelButton: true,cancelButtonText: '返回主页'}).then(() => {// 重新开始this.restart();}).catch(() => {// 返回主页this.setLevelPage();});}}
    },

    四、methods

  • 选择难度:将选择的难度配置赋值到当前游戏信息上,并获取最高记录
    /*** 选择难度** @param   {Object} item 选择的难度数据*/
    choseLevel(item) {const {alias} = item;this.gameInfo = {...this.gameInfo,...item,record: localStorage.getItem(`${alias}_record`)}this.hasChoseLevel = true;this.restart();
    },
  • 开始游戏:根据难度生成指定二维数组(全部不为炸弹,避免出现第一个点击的格子为炸弹的情况),初始化所有数据,开始执行定时器计时。
     // 开始/重新开始restart() {const {yNum, xNum} = this.gameInfo;this.board = new Array(yNum).fill(new Array(xNum).fill({data: '-1',isShow: false,isBoom: false}));[this.isInit, this.over, this.isSetFlag, this.gameInfo.time, this.flagNum] = [false, false, false, 0, 0];this.timer = setInterval(() => {const num = parseFloat(this.gameInfo.time) + 0.01this.gameInfo.time = num.toFixed(2)}, 10)}
  • 第一下点击棋盘后,初始化棋盘,生成指定数目炸弹并随机放置在除当前点击的格子外的位置
    /*** 修改坐标数据** @param   {String|Number} x   需要修改的x坐标* @param   {String|Number} y   需要修改的y坐标* @param   {Object} data   改变后的数据*/
    editBoard(x, y, data) {const {board} = this;const row = [...board[y]];// 获取那一行的数据row.splice(x, 1, data) // 修改this.$set(board, y, row)
    },
    /*** 初始化棋盘* @return  {Promise}   Promise* @param   {Array} click 点击的坐标***/
    setBoom(click) {const {gameInfo: {xNum, yNum, boomNum}} = this;const [cY, cX] = click;const dx = [], dy = [];//生成炸弹while (dx.length < boomNum) {const randomX = Math.round(Math.random() * (xNum - 1));//获取一个范围内的随机数const randomY = Math.round(Math.random() * (yNum - 1));//获取一个范围内的随机数const isClick = (randomX === parseInt(cX)) && (randomY === parseInt(cY));// 是if (!isClick && (dx.indexOf(randomX) === -1 || dx.indexOf(randomX) !== dy.ind// 如果没有重复且不是点击的坐标则推入dx.push(randomX)dy.push(randomY)}}// 修改炸弹数据for (let i = 0; i < dx.length; i++) {// console.log(JSON.stringify([dx[i]][dy[i]]))const obj = {data: '-1',isShow: false,isBoom: true //是炸弹}this.editBoard(dx[i], dy[i], obj);}this.isInit = true;this.updateBoard(click);
    },
  • 根据当前点击的格子信息更新格子状态,如果周围都没有雷则递归其它格子进行展开(根据Leecode题的基础逻辑进行了一些修改)

    /*** 更新格子状态** @param   {Array} click 点击的坐标* @param   {Object} data 点击坐标数据值*/
    updateBoard(click, data = undefined) {const {board, over, gameInfo: {xNum, yNum}} = this;if (over || data === 'F') return;// 如果游戏已经结束或为旗帜 直接返回const dx = [1, -1, 0, 0, -1, 1, -1, 1]; // 横坐标const dy = [0, 0, 1, -1, 1, -1, -1, 1]; // 纵坐标const inBound = (x, y) => x >= 0 && x < xNum && y >= 0 && y < yNum; /const update = (x, y) => {if (!inBound(x, y) || board[y][x].isShow || board[y][x].data === let count = 0;for (let i = 0; i < 8; i++) { // 统计周围雷的个数const nX = x + dx[i];const nY = y + dy[i];if (inBound(nX, nY) && board[nY][nX].isBoom) {count++;}}if (count === 0) { // 如果周围没有雷,翻开且标记0,递归const obj = {data: '0',// 0翻开的空格子isShow: true,// 已翻开isBoom: false // 不是炸弹}this.editBoard(x, y, obj)for (let i = 0; i < 8; i++) {update(x + dx[i], y + dy[i]);}} else {const obj = {data: count + '',// 数字1-9附近有炸弹isShow: true,// 已翻开isBoom: false // 不是炸弹}this.editBoard(x, y, obj)}};const [cY, cX] = click;if (board[cY][cX].isBoom) { // 踩雷了const obj = {data: 'X',// X炸弹isShow: true,// 已翻开isBoom: true // 是炸弹};this.editBoard(cX, cY, obj);this.over = true;clearInterval(this.timer)} else {update(cX, cY); // 开启dfs}
    }
  • 开启插旗后的更新逻辑

    /*** 插旗** @param   {Array} click 点击的坐标* @param   {Object} column 点击坐标数据*/
    setFlag(click, column) {const [cY, cX] = click;const {data, isBoom, isShow} = column;if (isShow) return; // 如果这个格子已经翻开 直接返回const obj = {data: 'F',// F旗帜isShow: false,// 未翻开isBoom: isBoom // 炸弹};if (data === 'F') {obj.data = '-1'; // 未翻开的空格子this.flagNum -= 1} else {this.flagNum += 1}this.editBoard(cX, cY, obj);
    },
  • 重新开始,重新选择难度

    // 打开难度选择页面
    setLevelPage() {clearInterval(this.timer); // 清除定时器this.hasChoseLevel = false;
    },
    // 重新开始确认
    handleRestart(isRestart = true) {// 停止计时clearInterval(this.timer)if (this.over) {if (isRestart) {// 重新开始this.restart();} else {// 返回主页this.setLevelPage();}} else {// 弹出对话框vant.Dialog({message: `确认要重新${isRestart ? '开始' : '选择难度'}吗?`,confirmButtonColor: '#409eff',confirmButtonText: '确认',showCancelButton: true,cancelButtonText: '取消'}).then(() => {if (isRestart) {// 重新开始this.restart();} else {// 返回主页this.setLevelPage();}}).catch(() => {// 重新计时this.timer = setInterval(() => {const num = parseFloat(this.gameInfo.time) + 0.01this.gameInfo.time = num.toFixed(2)}, 10)});}
    },
  • 上传图标:将上传的图片存入本地缓存,如果图片太大则压缩图片再进行上传

    // 返回布尔值
    beforeRead(file) {if (file.type.indexOf('image') === -1) {vant.Toast('请上传正确的图片');return false;}return true;
    },
    // 上传回调
    afterRead(files) {const {file} = files;console.log(files, file)// 保存图片到本地缓存let canvas = document.createElement('canvas') // 创建Canvas对象(画布)let context = canvas.getContext('2d')let img = new Image()img.src = files.contentimg.onload = () => {canvas.width = 100canvas.height = 100context.drawImage(img, 0, 0, canvas.width, canvas.height)if (file.size > 2 * 1024) {//如果图片大小大于2Mvant.Toast.loading({message: '正在压缩图片...',forbidClick: true,});files.content = canvas.toDataURL(file.type)localStorage.setItem(`${string}_img`, files.content)this[`${string}Img`] = files.content;vant.Toast('上传成功');} else {localStorage.setItem(`${string}_img`, files.content)this[`${string}Img`] = files.content;vant.Toast('上传成功');}}
    },

五、预览效果


总结

以上就是一个简易的可自定义图标的扫雷小游戏,本文仅仅简单介绍了内容逻辑和方法,样式已经做自适应,完整代码可以在github中查看,有些地方做的可能也不够精细,如果有什么更好的建议和想法欢迎提出。

Vue2+Vant2:一个可定制图标的简易扫雷小游戏相关推荐

  1. 扫雷html5简单初级,纯原生JS用面向对象class方法实现简易扫雷小游戏

    Demo介绍 纯原生js 实现 且用ES6语法class写的扫雷小游戏:布局为10*10的网格,随机生成10-20个雷. 左键点击扫雷.右键标记该地方有雷.该demo下载下来复制到html文件中直接可 ...

  2. 纯原生JS用面向对象class方法实现简易扫雷小游戏

    Demo介绍 纯原生js 实现 且用ES6语法class写的扫雷小游戏:布局为10*10的网格,随机生成10-20个雷. 左键点击扫雷.右键标记该地方有雷.该demo下载下来复制到html文件中直接可 ...

  3. C语言实现简易扫雷小游戏

    扫雷(简易版): 游戏规则:电脑随机生成雷,玩家随机扫一个坐标,如果该坐标是生成雷的位置,则踩到雷.如果没有则显示该坐标附近八个坐标雷的总数,一直循环至所有不是雷的坐标全部扫完 下面图片红色代表雷,黑 ...

  4. 使用C语言写一个扫雷小游戏

    前言 相信扫雷游戏小伙伴们肯定都玩过吧,学习了C语言中的数组.函数等基础内容之后就可以自己写一个简易的扫雷小游戏了,今天就我写扫雷小游戏的过程及思路写一篇博客,希望大家看完我的博客能有所收获. 软件及 ...

  5. C# 游戏制作 | ✨ 简易文字小游戏

    简易文字小游戏 在学习了一些C#的基础知识后就要做一些小东西来练练手,就比如本文所介绍的一个通过用VS中的C#写的一个简易文字小游戏 这个小游戏只由一个脚本完成,主要是用来拿C#中的一些基础知识完成, ...

  6. 用C语言实现一个简单的扫雷小游戏(附全代码及教程)

    本文实例为大家分享了C语言实现扫雷游戏的具体代码,供大家参考,具体内容如下: 首先,创建一个text.c文件: 编写主函数: int main() {test();return 0; } 定义test ...

  7. 《uni-app》一个非canvas的飞机对战小游戏实现(一)准备

    这是一个没有套路的前端博主,热衷各种前端向的骚操作,经常想到哪就写到哪,如果有感兴趣的技术和前端效果可以留言-博主看到后会去代替大家踩坑的-接下来的几篇都是uni-app的小实战,有助于我们更好的去学 ...

  8. 一个扫雷小游戏带你初识VUE3和typescript

    一个扫雷小游戏带你初识VUE3和typescript 阅读本文你会了解到: vue3的部分新特性 typescript的基本使用 部分es6语法 基础部分 为什么要使用ref和reactive来声明变 ...

  9. C实现扫雷小游戏(简易版)

    你知道,有些鸟儿是注定不会被关在牢笼里的,它们的每一片羽毛都闪耀着自由的光辉.--<肖申克的救赎> 目录 1.设计框架 2.设计流程 2.1菜单 2.2初始化雷阵 2.3生成雷 2.4玩家 ...

  10. 一个适合初学者的C++推箱子小游戏

    一个适合初学者的C++推箱子小游戏 博主最近在学习关于C++的一些基础,这是本人突发奇想做的一个小游戏,编程其实并不难,重要的是,你的思路,以及优化,当然,这个小游戏,本人也是基于一个学习者编写的 本 ...

最新文章

  1. 【模型评估与选择】sklearn.model_selection.KFold
  2. python pandas DataFrame 查找NaN所在的位置
  3. 从一次故障聊聊前端 UI 自动化测试
  4. MATLAB实战系列(三十四)-MATLAB基于PCA-LDA模糊神经网络的人脸识别
  5. 新人问一般都用哪些 Linux 命令,我把这个扔了过去
  6. java hadoop api,Hadoop API,HadoopAPI
  7. MySQL 数据库中如何将表字段的空值全部替换成空字符串
  8. Java的finally理解
  9. 前端、后端、全栈都要学什么?薪资前景如何?
  10. java 打包运行环境_Jar 打包 EXE文件,可以脱离java环境运行 Jsmooth的使用
  11. sql连接本地数据库
  12. word在html中预览,在网页中预览word和excel
  13. MapAbc使用体验
  14. 新能源整车控制器VCU开发过程分享
  15. Map Coloring
  16. [渝粤教育] 中国科学技术大学 化学实验安全知识 参考 资料
  17. 6 生僻字_《生僻字》歌词拼音与注释,跟着音乐学汉字
  18. google浏览器被2345强制绑定
  19. 这图怎么画 | 相关分析棒棒糖图
  20. 程序员用代码写合租广告,网友神评亮了

热门文章

  1. matlab窄带高斯随机信号,06实验六:窄带随机信号仿真与分析
  2. python爬取2017年统计用区划代码和城乡划分代码(截止2017年10月31日)
  3. 漫画算法python篇pdf_用Python抓取漫画并制作mobi格式电子书
  4. 业务模式制胜,BLM战略规划七步法
  5. PCB Layout各层含义与分层原则
  6. canvas+websocket+vue做一个你画我猜小游戏
  7. c#语言猜数字游戏,C#实现猜数字小游戏
  8. 【Java题解】小米算法面试题
  9. Abaqus的inp文件详解
  10. STM32串口通讯——中断方式