五子棋是大家很熟悉的一种小游戏,本文给大家介绍如何制作一个简易的网页版五子棋游戏,并且考虑实现普通 DOM 和 Canvas 两种 UI 绘图模式供随时切换。最终的实现效果参考:https://littuomuxin.github.io/gobang/

思路

该简易版五子棋主要包含以下基本功能:

  1. 下棋:五子棋对战分为黑棋和白棋两方,双方依次在棋盘上落一颗棋子

  2. 悔棋:一方在棋盘上落一颗棋子之后,在对方还未落棋子之前,允许悔棋

  3. 撤销悔棋:悔棋时,也可以重新将棋子落在悔棋前的位置

  4. 判断胜负:总共有4种赢法,同一种颜色的棋子在横、竖、正斜、反斜任意一个方向连成5个,其代表的这一方即获胜

  5. 重玩:一盘棋局分出胜负后,可以清理掉棋盘上的棋子,重来一局

在代码设计上,我们将整个程序分为控制层和渲染层,控制器负责逻辑实现,并通过调用渲染器来实现绘制工作。谈到网页绘图,简单的效果完全可以通过普通的 DOM 来实现,但如果图形过于复杂,我们则应该考虑更为专业的绘图 API,如 Canvas。本文将实现普通 DOM 和 Canvas 两个版本的渲染器,并介绍如何轻松地在这两个渲染器之间进行切换。

控制器实现

控制器定义了一个五子棋类 Gobang。要实现上述功能,需要在控制器类构造器中定义如下一些私有状态和数据:棋局状态、下棋的角色、下棋数据、悔棋数据等。此外,还需要初始化棋盘数据,本例中的实现是一个 15 * 15 的棋盘,所以需要初始化一个 15 * 15 的二维数组。最后,再定义一些游戏中的话术,用于在游戏过程中调用另外实现的 notice 方法进行相应的通知提示。

构造器具体的实现代码如下:

function Gobang() {this._status = 0; // 棋局状态,0表示对战中,1表示已分胜负this._role = 0; // 下棋的角色,0表示黑棋,1表示白棋this._chessDatas = []; // 存放下棋数据this._resetStepData = []; // 存放悔棋数据this._gridNum = 15; // 棋盘行列数this._chessBoardDatas = this._initChessBoardDatas(); // 初始化棋盘数据this._notice = window.notice;this._msgs = {'start': '比赛开始!','reStart': '比赛重新开始!','blackWin': '黑棋胜!','whiteWin': '白棋胜!',};
}

然后,控制器还需要暴露一个实例方法供外部初始化调用,并依赖外部传入一个渲染器实例,控制器内部会通过调用该渲染器实例的各种方法来实现五子棋里的绘图工作。代码如下所示:

/*** 初始化* @param {Object} renderer 渲染器*/
Gobang.prototype.init = function(renderer) {var _this = this;// 游戏开始setTimeout(function() {_this._notice.showMsg(_this._msgs.start, 1000);}, 1000);if (!renderer) throw new Error('缺少渲染器!');_this.renderer = renderer;renderer.renderChessBoard(); // 绘制棋盘renderer.bindEvents(_this); // 绑定事件
};

上述构造器和初始化方法实现后,接下来的下棋、悔棋、撤销悔棋、判断胜负、重玩等所有操作即是对控制器内私有状态和数据进行更改,与此同时,再调用渲染器进行相应的绘制工作。

首先是下棋方法 goStep 的实现。下棋时需要判断相应位置是否有棋子(_hasChess),没有棋子的位置才可以落棋子,落棋后需要更新棋盘数据(_chessBoardDatas)、下棋数据(_chessDatas),并调用渲染器方法 _this.renderer.renderStep 更新绘图界面。然后还需要判断棋局胜负是否已分(_isWin),分出胜负的情况下调用 notice 方法给出相应提示,最后还要切换下棋的角色(_role)。代码如下:

/*** 判断一个位置是否有棋子* @param {Number} x 水平坐标* @param {Number} y 垂直坐标* @returns {Boolean} 初始棋盘数据*/
Gobang.prototype._hasChess = function(x, y) {var _this = this;var hasChess = false;_this._chessDatas.forEach(function(item) {if (item.x === x && item.y === y) hasChess = true;});return hasChess;
};/*** 下一步棋* @param {Number} x 水平坐标* @param {Number} y 垂直坐标* @param {Boolean} normal 正常下棋,不是撤销悔棋之类* @returns {Boolean} 是否成功下棋*/
Gobang.prototype.goStep = function(x, y, normal) {var _this = this;if (_this._status) return false;if (_this._hasChess(x, y)) return false;_this._chessBoardDatas[x][y] = _this._role;var step = {x: x,y: y,role: _this._role};_this._chessDatas.push(step);// 存入 localstoragelocalStorage && (localStorage.chessDatas = JSON.stringify(_this._chessDatas));// 绘制棋子_this.renderer.renderStep(step);// 判断是否胜出if (_this._isWin(step.x, step.y)) {// 获胜_this._status = 1;var msg = _this._role ? _this._msgs.whiteWin : _this._msgs.blackWin;setTimeout(function() {_this._notice.showMsg(msg, 5000);}, 500);}// 切换角色_this._role = 1 - _this._role;// 清除悔棋数据if (normal) _this._resetStepData = [];return true;
};

悔棋 resetStep 为下棋的逆操作,需要将下棋数据数组 _chessDatas 做一个 pop 操作,将棋盘数据 _chessBoardDatas 相对应的数组元素恢复成初始值,并存储悔棋数据 _resetStepData;然后是切换下棋角色 _role,调用 _this.renderer.renderUndo 更新绘图界面。

/*** 悔一步棋*/
Gobang.prototype.resetStep = function() {var _this = this;if (_this._chessDatas.length < 1) return;_this._status = 0; // 即使分出了胜负,悔棋后也回到了对战状态var lastStep = _this._chessDatas.pop();// 存入 localstoragelocalStorage && (localStorage.chessDatas = JSON.stringify(_this._chessDatas));// 修改棋盘数据_this._chessBoardDatas[lastStep.x][lastStep.y] = undefined;// 存储悔棋数据_this._resetStepData.push(lastStep);// 切换用户角色_this._role = 1 - _this._role;// 移除棋子_this.renderer.renderUndo(lastStep, _this._chessDatas);
};

撤销悔棋 reResetStep 是悔棋的逆操作,也就相当于是下棋操作,只是这一步棋的位置是从悔棋数据 _resetStepData 中自动取出的:

/*** 撤销悔棋*/
Gobang.prototype.reResetStep = function() {var _this = this;if (_this._resetStepData.length < 1) return;var lastStep = _this._resetStepData.pop();_this.goStep(lastStep.x, lastStep.y);// 绘制棋子_this.renderer.renderStep(lastStep);
};

接下来介绍判断胜负方法 _isWin 的实现。我们知道五子棋总共有4种赢法,即同一种颜色的棋子在横、竖、正斜、反斜任意一个方向连成5个,其代表的这一方即获胜。所以,当前棋子落定后,我们需要根据该棋子所在的位置,从四个方向上计算与之相连的相同颜色的棋子的数量。具体的实现代码如下:

/*** 判断某个单元格是否在棋盘上* @param {Number} x 水平坐标* @param {Number} y 垂直坐标* @returns {Boolean} 指定坐标是否在棋盘范围内*/
Gobang.prototype._inRange = function(x, y) {return x >= 0 && x < this._gridNum && y >= 0 && y < this._gridNum;
};/*** 判断在某个方向上有多少个同样的棋子* @param {Number} xPos 水平坐标* @param {Number} yPos 垂直坐标* @param {Number} deltaX 水平移动方向* @param {Number} deltaY 垂直移动方向* @returns {Number} 与给定位置棋子朝给定位置上计算得到的相同的棋子数量*/
Gobang.prototype._getCount = function(xPos, yPos, deltaX, deltaY) {var _this = this;var count = 0;while (true) {xPos += deltaX;yPos += deltaY;if (!_this._inRange(xPos, yPos) || _this._chessBoardDatas[xPos][yPos] != _this._role)break;count++;}return count;
};/*** 判断在某个方向上是否获胜* @param {Number} x 水平坐标* @param {Number} y 垂直坐标* @param {Object} direction 方向* @returns {Boolean} 在某个方向上是否获胜*/
Gobang.prototype._isWinInDirection = function(x, y, direction) {var _this = this;var count = 1;count += _this._getCount(x, y, direction.deltaX, direction.deltaY);count += _this._getCount(x, y, -1 * direction.deltaX, -1 * direction.deltaY);return count >= 5;
};/*** 判断是否获胜* @param {Number} x 水平坐标* @param {Number} y 垂直坐标* @returns {Boolean} 是否获胜*/
Gobang.prototype._isWin = function(x, y) {var _this = this;var length = _this._chessDatas.length;if (length < 9) return 0;// 4种赢法:横、竖、正斜、反斜var directions = [{deltaX: 1,deltaY: 0}, {deltaX: 0,deltaY: 1}, {deltaX: 1,deltaY: 1}, {deltaX: 1,deltaY: -1}];for (var i = 0; i < 4; i++) {if (_this._isWinInDirection(x, y, directions[i])) {return true;}}
};

最后,当棋局胜负已分后,我们可以通过清除所有数据和绘制工作来重新开始新的一局:

/*** 清除一切重新开始*/
Gobang.prototype.clear = function() {var _this = this;_this._status = 0;_this._role = 0;if (_this._chessDatas.length < 1) return;// 清除棋子_this.renderer.renderClear();_this._chessDatas = [];localStorage && (localStorage.chessDatas = '');this._resetStepData = [];_this._chessBoardDatas = _this._initChessBoardDatas();_this._notice.showMsg(_this._msgs.reStart, 1000);
};

渲染器实现

渲染器的工作主要包括以下几个:

  1. 棋盘的绘制工作

  2. 下一个棋子的绘制工作

  3. 悔一个棋子的绘制工作

  4. 清除所有棋子的绘制工作

  5. 棋盘界面的事件交互工作:用户点击棋盘中的某个位置落棋

其中事件交互工作中需要调用控制器来控制下棋逻辑。

因为需要实现普通 DOM 和 Canvas 两个版本的渲染器,并且供控制器灵活切换,所以这两个渲染器需要暴露相同的实例方法。 根据上述介绍的渲染器的5项工作,它需要的暴露的5个方法如下:

  1. renderChessBoard

  2. renderStep

  3. renderUndo

  4. renderClear

  5. bindEvents

下面分别介绍普通 DOM 渲染器和 Canvas 渲染器的具体实现。

普通 DOM 渲染器

普通 DOM 渲染器需要绘制 15 * 15 的网格,对应 15 * 15 个 div 元素,每个元素在初始化的过程中可以通过定义 attr-data 属性来标示其对应的网格位置。相关实现如下:

/*** 普通 Dom 版本五子棋渲染器构造函数* @param {Object} container 渲染所在的 DOM 容器*/
function DomRenderer(container) {this._chessBoardWidth = 450; // 棋盘宽度this._chessBoardPadding = 4; // 棋盘内边距this._gridNum = 15; // 棋盘行列数this._gridDoms = []; // 存放棋盘 DOMthis._chessboardContainer = container; // 容器this.chessBoardRendered = false; // 是否渲染了棋盘this.eventsBinded = false; // 是否绑定了事件
}/*** 渲染棋盘*/
DomRenderer.prototype.renderChessBoard = function() {var _this = this;_this._chessboardContainer.style.width = _this._chessBoardWidth + 'px';_this._chessboardContainer.style.height = _this._chessBoardWidth + 'px';_this._chessboardContainer.style.padding = _this._chessBoardPadding + 'px';_this._chessboardContainer.style.backgroundImage = 'url(./imgs/board.jpg)';_this._chessboardContainer.style.backgroundSize = 'cover';var fragment = '';for (var i = 0; i < _this._gridNum * _this._gridNum; i++) {fragment += '<div class="chess-grid" attr-data="' + i + '"></div>';}_this._chessboardContainer.innerHTML = fragment;_this._gridDoms = _this._chessboardContainer.getElementsByClassName('chess-grid');_this.chessBoardRendered = true;
};

每个网格对应的 div 有三种状态,没有棋子、有黑棋、有白棋三种状态,这三种状态可以通过给 div 添加不同的三种样式来实现。然后,下一个棋子和悔一个棋子的绘制工作即通过切换相应 div 的样式来实现;清除所有棋子的绘制工作则是将所有的 div 样式恢复成没有棋子的状态:

/*** 渲染一步棋子* @param {Object} step 棋的位置*/
DomRenderer.prototype.renderStep = function(step) {var _this = this;if (!step) return;var index = step.x + _this._gridNum * step.y;var domGrid = _this._gridDoms[index];domGrid.className = 'chess-grid ' + (step.role ? 'white-chess' : 'black-chess');
};/*** 悔一步棋子* @param {Object} step 棋的位置* @param {Array} allSteps 剩下的所有棋的位置*/
DomRenderer.prototype.renderUndo = function(step) {var _this = this;if (!step) return;var index = step.x + _this._gridNum * step.y;var domGrid = _this._gridDoms[index];domGrid.className = 'chess-grid';
};/*** 清除所有棋子*/
DomRenderer.prototype.renderClear = function() {var _this = this;for (var i = 0; i < _this._gridDoms.length; i++) {_this._gridDoms[i].className = 'chess-grid';}
};

最后是棋盘界面的事件交互工作,用户点击其中任意一个网格 div,都需要做出响应,该响应事件即为下一步棋,通过传入的控制器对象的 goStep 方法实现。为了性能考虑,我们不应该给每个棋盘网格 div 绑定点击事件,而是在棋盘容器上绑定一个点击事件即可,通过真实 targetattr-data 属性即可轻松计算得到下棋的位置,传给 goStep 方法。下面是具体的实现:

/*** 绑定事件* @param {Object} controllerObj 控制器对象*/
DomRenderer.prototype.bindEvents = function(controllerObj) {var _this = this;_this._chessboardContainer.addEventListener('click', function(ev) {var target = ev.target;var attrData = target.getAttribute('attr-data');if (attrData === undefined || attrData === null) return;var position = attrData - 0;var x = position % _this._gridNum;var y = parseInt(position / _this._gridNum, 10);controllerObj.goStep(x, y, true);}, false);_this.eventsBinded = true;
};

Canvas 渲染器

接下来是 Canvas 渲染器的具体实现。为了性能考虑,我们可以用多个 Canvas 画布叠加实现整个绘图效果,每个画布负责单一元素的绘制,不变的元素和变化的元素尽量绘制到不同的画布。本示例中创建了三个画布:绘制背景的画布、绘制阴影的画布和绘制棋子的画布。相关实现代码如下:

/*** Canvas 版本五子棋渲染器构造函数* @param {Object} container 渲染所在的 DOM 容器*/
function CanvasRenderer(container) {this._chessBoardWidth = 450; // 棋盘宽度this._chessBoardPadding = 4; // 棋盘内边距this._gridNum = 15; // 棋盘行列数this._padding = 4; // 棋盘内边距this._gridWidth = 30; // 棋盘格宽度this._chessRadius = 13; // 棋子的半径this._container = container; // 创建 canvas 的 DOM 容器this.chessBoardRendered = false; // 是否渲染了棋盘this.eventsBinded = false; // 是否绑定了事件this._init();
}/*** 初始化操作,创建画布*/
CanvasRenderer.prototype._init = function() {var _this = this;var width = _this._chessBoardWidth + _this._chessBoardPadding * 2;// 创建绘制背景的画布_this._bgCanvas = document.createElement('canvas');_this._bgCanvas.setAttribute('width', width);_this._bgCanvas.setAttribute('height', width);// 创建绘制阴影的画布_this._shadowCanvas = document.createElement('canvas');_this._shadowCanvas.setAttribute('width', width);_this._shadowCanvas.setAttribute('height', width);// 创建绘制棋子的画布_this._chessCanvas = document.createElement('canvas');_this._chessCanvas.setAttribute('width', width);_this._chessCanvas.setAttribute('height', width);// 在容器中插入画布_this._container.appendChild(_this._bgCanvas);_this._container.appendChild(_this._shadowCanvas);_this._container.appendChild(_this._chessCanvas);// 棋子的绘图环境_this._context = _this._chessCanvas.getContext('2d');
};

棋子的绘制过程则是使用棋子画布的 2D 绘图环境绘制一个圆形,具体代码如下:

/*** 渲染一步棋子* @param {Object} step 棋的位置*/
CanvasRenderer.prototype.renderStep = function(step) {var _this = this;if (!step) return;var x = _this._padding + (step.x + 0.5) * _this._gridWidth;var y = _this._padding + (step.y + 0.5) * _this._gridWidth;_this._context.beginPath();_this._context.arc(x, y, _this._chessRadius, 0, 2 * Math.PI);if (step.role) {_this._context.fillStyle = '#FFFFFF';} else {_this._context.fillStyle = '#000000';}_this._context.fill();_this._context.closePath();
};

因为棋子都被绘制在一个画布上,所以清除所有棋子很简单,只用清除整个画布的绘制即可。因为 Canvas 在宽度或高度被重设时,画布内容就会被清空,所以可以用以下方法快速清除画布:

/*** 清除所有棋子*/
CanvasRenderer.prototype.renderClear = function() {this._chessCanvas.height = this._chessCanvas.height; // 快速清除画布
};

而悔一步棋则相对复杂一点,我们采取的方案是先清除整个画布,然后重新绘制前面的棋局状态:

/*** 悔一步棋子* @param {Object} step 当前这一步棋的位置* @param {Array} allSteps 剩下的所有棋的位置*/
CanvasRenderer.prototype.renderUndo = function(step, allSteps) {var _this = this;if (!step) return;_this._chessCanvas.height = _this._chessCanvas.height; // 快速清除画布if (allSteps.length < 1) return;// 重绘allSteps.forEach(function(p) {_this.renderStep(p);});
};

最后是事件交互工作:鼠标在棋盘上移动时,绘制阴影;鼠标在棋盘上点击时,通过传入的控制器对象的 goStep 方法实现下棋操作,能够成功绘制时,还需要注意清除阴影。具体实现如下:

/*** 判断某个单元格是否在棋盘上* @param {Number} x 水平坐标* @param {Number} y 垂直坐标* @returns {Boolean} 指定坐标是否在棋盘范围内*/
CanvasRenderer.prototype._inRange = function(x, y) {return x >= 0 && x < this._gridNum && y >= 0 && y < this._gridNum;
};/*** 绑定事件* @param {Object} controllerObj 控制器对象*/
CanvasRenderer.prototype.bindEvents = function(controllerObj) {var _this = this;var chessShodowContext = _this._shadowCanvas.getContext('2d');// 鼠标移出画布时隐藏画阴影的画布document.body.addEventListener('mousemove', function(ev) {if (ev.target.nodeName !== 'CANVAS') {_this._shadowCanvas.style.display = 'none';}}, false);// 鼠标在画布移动时绘制阴影效果_this._container.addEventListener('mousemove', function(ev) {var xPos = ev.offsetX;var yPos = ev.offsetY;var i = Math.floor((xPos - _this._padding) / _this._gridWidth);var j = Math.floor((yPos - _this._padding) / _this._gridWidth);var x = _this._padding + (i + 0.5) * _this._gridWidth;var y = _this._padding + (j + 0.5) * _this._gridWidth;// 显示画阴影的画布_this._shadowCanvas.style.display = 'block';// 快速清除画布_this._shadowCanvas.height = _this._shadowCanvas.height;// 超出棋盘范围不要阴影效果if (!_this._inRange(i, j)) return;// 有棋子的地方不要阴影效果if (controllerObj._chessBoardDatas[i][j] !== undefined) return;chessShodowContext.beginPath();chessShodowContext.arc(x, y, _this._gridWidth / 2, 0, 2 * Math.PI);chessShodowContext.fillStyle = 'rgba(0, 0, 0, 0.2)';chessShodowContext.fill();chessShodowContext.closePath();}, false);// 鼠标在棋盘点击下棋_this._container.addEventListener('click', function(ev) {var x = ev.offsetX;var y = ev.offsetY;var i = Math.floor((x - _this._padding) / _this._gridWidth);var j = Math.floor((y - _this._padding) / _this._gridWidth);var success = controllerObj.goStep(i, j, true);if (success) {// 清除阴影_this._shadowCanvas.height = _this._shadowCanvas.height;}}, false);_this.eventsBinded = true;
};

切换绘图模式

两种绘图模式可以随时切换,渲染器是供控制器调用的,所以在控制器中需要暴露一个切换渲染器的方法。切换渲染器的操作分为以下三步:

  1. 旧的渲染器清除其所有的绘制工作

  2. 新的渲染器初始化棋盘绘制工作

  3. 根据已下棋数据重新绘制当前棋局

具体实现如下:

/*** 切换渲染器* @param {Object} renderer 渲染器对象*/
Gobang.prototype.changeRenderer = function(renderer) {var _this = this;if (!renderer) return;_this.renderer = renderer;// 先清除棋盘,再根据当前数据绘制棋局状态renderer.renderClear();if (!renderer.chessBoardRendered) renderer.renderChessBoard();if (!renderer.eventsBinded) renderer.bindEvents(_this);_this._chessDatas.forEach(function(step) {renderer.renderStep(step);});
};

因为两个渲染器暴露的可供控制器调用的实例方法完全一致,所以上述几个简单步骤即可实现无缝切换,接下来的下棋游戏可以继续进行!

总结

要完整的制作一个网页五子棋游戏产品,还需要考虑网络对战、AI 对战等。本文只是一个简易版本的网页五子棋实现,重点在于多渲染器及其切换的实现思路,希望在这一方面能起到一点参考意义。

最后

  • 欢迎加我微信(winty230),拉你进技术群,长期交流学习...

  • 欢迎关注「前端Q」,认真学前端,做个有专业的技术人...

在看点这里

多 UI 版本网页五子棋实现相关推荐

  1. html css制作五子棋,js实现带简单Ai的网页五子棋制作(UI篇)

    前言: 最近学了html,css,js,想找个项目来练习一下,就做了一个简单的网页五子棋,主要用到了canvas画图,逻辑部分都是js来写的,很简单,在这里简单的记录一下开发流程. 创建棋盘: 用ca ...

  2. jQuery版本的网页开关灯、jQuery版本网页开关灯的另一种写法

    jQuery版本的网页开关灯 <!DOCTYPE html> <html lang="en"> <head><meta charset=& ...

  3. 小程序源码:喝酒神器新UI版本带特效和音效,-多玩法安装简单

    这是一款也是自带音效和特效的微信小程序源码 由多个喝酒小游戏组合而成,具体如下: 大话骰(带音效) 愤怒大叔(带音效,多个皮肤模板用户可选择) 指尖轮盘(带音效特效) 剪刀石头布(带音效特效) 789 ...

  4. 微信小程序:喝酒神器新UI版本带特效和音效,缩减版本

    这是一款也是自带音效和特效的微信小程序源码 由多个喝酒小游戏组合而成,具体如下: 大话骰(带音效) 愤怒大叔(带音效,多个皮肤模板用户可选择) 指尖轮盘(带音效特效) 剪刀石头布(带音效特效) 789 ...

  5. 喝酒神器新UI版本带特效和音效,缩减版本微信小程序源码下载

    这是一款也是自带音效和特效的微信小程序源码 由多个喝酒小游戏组合而成,具体如下: 大话骰(带音效) 愤怒大叔(带音效,多个皮肤模板用户可选择) 指尖轮盘(带音效特效) 剪刀石头布(带音效特效) 789 ...

  6. 小程序源码:喝酒神器新UI版本带特效和音效,缩减版本微信小程序

    这是一款也是自带音效和特效的微信小程序源码 由多个喝酒小游戏组合而成,具体如下: 大话骰(带音效) 愤怒大叔(带音效,多个皮肤模板用户可选择) 指尖轮盘(带音效特效) 剪刀石头布(带音效特效) 789 ...

  7. 【已结束】结果将于近期公布:有奖 PK丨用 Amaze UI 写网页 大奖抱回家​

    有奖 PK丨用 Amaze UI 写网页 大奖抱回家 上次零食没吃到?没关系!这次卷土重来! Hi,帅气可爱的前端工程师们: 感谢你一直以来对"妹子UI"的关注与支持.经过7月份几 ...

  8. 【小程序源码】小游戏神器新UI版本带特效和音效,缩减版本微信小程序源码下载

    这是一款也是自带音效和特效的微信小程序源码 由多个小游戏组合而成,具体如下: 大话骰(带音效) 愤怒大叔(带音效,多个皮肤模板用户可选择) 指尖轮盘(带音效特效) 剪刀石头布(带音效特效) 789骰子 ...

  9. 网页模板素材|解救不会编程的UI设计师网页设计者!

    优质的网页设计需要更多的基于用户的体验,以最好的方式呈现内容. 但是大多数的UI设计 师网页设计者没有编程的技能. 高质量,最新潮流的网页模板由专业的开发者提供功能,是设计师友好的帮助文件! 搜优(S ...

  10. vue3版本网页小游戏

    目录 1.前言 2.实现过程 2.1目录 2.2文件介绍 3.核心逻辑分步骤详解 4.总结 1.前言 最近火爆全网的羊了个羊小程序,背景是根据官方介绍,"羊了个羊"是一款闯关消除小 ...

最新文章

  1. pipeline和java的区别_总结:四个Pipeline脚本式与声明式语法差异
  2. 一网打尽,最全面的跨域解决方案来了!
  3. java异常 字节码,Java字节码角度分析异常处理
  4. python 浏览器显示本地文件夹_浏览器读取本地文件
  5. python——爬虫实现网页信息抓取
  6. git rebase 的使用
  7. 拖来拖去今天终于重装系统了
  8. C,C++中转义字符的用法
  9. 基于麻雀搜索算法优化概率神经网络PNN的分类预测-附代码
  10. [Selenium]怎样验证页面是否有无变化
  11. python批量查询高德地图经纬度(支持xlxs)
  12. word中套用表格样式在哪里_表格套用表格样式在哪 word表格自动套用样式在哪
  13. 苹果 CEO 库克:我每天晚上 9 点半睡觉,凌晨 4 点起床,希望员工重返线下工作...
  14. Android百日程序:绘画程序-画手指路径
  15. 第一台电子计算机教授领导,第一台电子计算机
  16. 成绩管理系统管理学生属于计算机,学生成绩管理系统属于计算机哪方面的应用?...
  17. 随笔——不要活在别人的眼里
  18. Linux下安装免费杀毒软件---ClamAV
  19. 激光打印机的工作原理2
  20. qcom 8953 usb hub device descriptor read/64 error -71

热门文章

  1. 谁说EMC、IBM不能替换,还你一个存储虚拟化的真相!
  2. 为什么说炒股票必亏无疑?
  3. EasyUI 日历实现日程提醒
  4. xp系统 共享文件夹服务器,WinXP如何共享文件夹?共享文件夹的方法
  5. 仓储系统主要注意事项
  6. 谨防Wi-Fi时代的***屠城
  7. 标准时间格式与时间戳的转化
  8. 2019年保研夏令营时间经验汇总
  9. Altium Designer--如何用快捷键进行元器件旋转
  10. php实现阳历阴历互转的方法