javascript
纯JS实现简易扫雷小游戏网页项目
文章目錄
前言
一、掃雷簡介
二、步驟
1.創建項目文件結構
2.搭骨架和基礎樣式(HTML+CSS)
?3.添加“動作”(JS主體部分)
總結
前言
本項目僅用于個人學習分享,為各位前端學習小伙伴們提供個人學習過程和總結。本項目涉及的技術棧比較有限(純HTML+CSS+JS),同時也只由個人單獨完成僅具備基本功能,故不排除各種待迭代開發的bug,新手可用于練習JS,大佬也請多多指教。
提示:以下是本篇文章正文內容,下面案例可供參考
一、掃雷簡介
掃雷是一款大眾類的益智類游戲,于1992年發布,是我個人比較喜歡的游戲之一。最近準備找實習故用這個作為第一個真正練手的項目,共花費一下午時間完成其基本功能,存在的問題還有待解決。
掃雷玩法不做過多贅述,有需要的同學可參考該地址進行了解學習:
掃雷怎么玩(掃雷游戲規則技巧圖解) - 華風揚 (uxxsn.com)
下圖為三種等級的項目顯示效果:
二、步驟
1.創建項目文件結構
文件結構如下(VSCode編譯器示例):
2.搭骨架和基礎樣式(HTML+CSS)
step 1:首先引入創建好的CSS文件和JS文件;
step 2:分析頁面結構——由以下三部分組成
整個頁面用一個div,主體部分分為三個div,分別是游戲級別(.level)、游戲界面(.gameBox)、剩余雷數(.mineNum);其中.level盒子中每個元素分別是一個button,.gameBox由JS代碼動態生成,.div中剩余雷數值用span包裝等待JS動態賦值。
代碼如下(HTML):
<!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"><title>hzj的掃雷小游戲</title><link rel="stylesheet" href="css/index.css"> </head><body><div id="mine"><div class="level"><button class="active">初級</button><button>中級</button><button>高級</button><button>重新開始</button></div><div class="gameBox"></div><div class="info">剩余雷數:<span class="mineNum"></span></div></div><script src="js/index.js"></script> </body></html>?step 3:書寫頁面樣式
父元素居中放置——margin: 50px auto;
子元素欲在父親中居中顯示——text-align: center;
設置鼠標指針光標樣式——cursor:?pointer;
默認初級等級為選中狀態;
設置雷格陰影效果——border-color:?#fff?#a1a1a1?#a1a1a1?#fff;
將地雷和小紅旗作為背景圖片動態顯示;
代碼如下(CSS):
#mine {margin: 50px auto; }.level {text-align: center;margin-bottom: 10px; }.level button {padding: 5px 15px;background: #02a4ad;border: none;color: #fff;/* outline: 3px; */border-radius: 3px;cursor: pointer; }.level button.active {background: #00abff; }table {border-spacing: 1px;background: #929196;margin: 0 auto; }td {padding: 0;width: 20px;height: 20px;background: #ccc;border: 2px solid;border-color: #fff #a1a1a1 #a1a1a1 #fff;text-align: center;line-height: 20px;font-weight: bold; }td.zero {background-color: #a09f9f;background: #a09f9f; }td.one {background-color: #a09f9f;background: #a09f9f;color: #0332fe; }td.two {background-color: #a09f9f;background: #a09f9f;color: #019f02; }td.three {background-color: #a09f9f;background: #a09f9f;color: #ff2600; }td.four {background-color: #a09f9f;background: #a09f9f;color: #93208f; }td.five {background-color: #a09f9f;background: #a09f9f;color: #ff7f29; }td.six {background-color: #a09f9f;background: #a09f9f;color: #ff3fff; }td.seven {background-color: #a09f9f;background: #a09f9f;color: #3fffbf; }td.eight {background-color: #a09f9f;background: #a09f9f;color: #22ee0f; }.info {margin-top: 10px;text-align: center; }.mine {background: #d9d9d9 url(../images/miner.png) no-repeat center;background-size: cover; }.flag {background: #ccc url(../images/flag.png) no-repeat center;background-size: cover; }?3.添加“動作”(JS主體部分)
本項目對象只有雷,故只需編寫一個Mine的構造函數,含有屬性tr、td、mineNum分別表示行列以及預定義的雷的數量,同時用一個二維數組squares存儲所有方塊的信息、tds存儲所有單元格的DOM;同時我們還需要有一個常量來記錄剩余雷的數量,一開始等于游戲等級中預定義的雷的數量;除此之外,我們還需要一個初值為false的allRight屬性,用于判斷用戶右擊數達到雷數時是否掃雷成功;以上一切屬性設定完成后,因為我們需要在.gameBox中動態生成掃雷表格,故通過document.querySelector('.gameBox')獲取到待添加的父級網頁元素。
完成對象構造函數的編寫以后,我們接下來將一步步在對象Mine的原型上進行對象方法函數的編寫(即Mine.prototype.函數名 = function(){ ...函數方法主體部分... })。
實例方法一:創建表格——createDom()
在該方法中,我們要做的是在網頁中動態創建一個掃雷表格——document.createElement('table') & document.createElement('tr') & document.createElement('td');用this.tds存儲創建的dom元素并把格子對應的行與列存儲到格子自己身上(domTd.pos記錄),給domTd添加鼠標點擊事件,該事件中調用play函數開始游戲(點擊事件可先不寫)。
實例方法二:生成隨機的雷——randomNum()
先生成一個固定長度(tr*td)的空數組,通過一個循環將0~tr*td放入數組中,再使用一個sort函數將其順序打亂,最后取其前mineNum個值,默認為雷在表格中的索引位置。
實例方法三:找周圍非雷格子——getAround(square)
傳入的參數square為中心格子,首先我們可以用兩個變量x,y記錄下中心點的x,y坐標,定義一個二維數組result用于存儲找到的格子。解決該問題我們要知道以下三點:
①中心點與周圍點的x(0°方向),y軸(-90°方向)關系
| (x-1,y-1) | (x,y-1) | (x+1,y-1) |
| (x-1,y) | (x,y) | (x+1,y) |
| (x-1,y+1) | (x,y+1) | (x+1,y+1) |
②行列與x,y軸之間的關系——第i行j列對應坐標軸上(j,i)
③遍歷循環九宮格時的幾種特殊情況——四周(上邊、下邊、左邊、右邊)格子超出范圍、當前中心格子是自己,周圍格子有雷,對應代碼處理部分如下;
//通過坐標循環九宮格for (var i = x - 1; i <= x + 1; i++) {for (var j = y - 1; j <= y + 1; j++) {//特殊情況:格子超出范圍 左邊;上邊;右邊;下邊 當前格子是自己 周圍格子是個雷if (i < 0 || j < 0 || i > this.td - 1 || j > this.tr - 1 || (i == x && j == y) || this.squares[j][i].type == 'mine') {continue;}result.push([j, i]); //以行列形式返回}}實例方法四:更新所有格子值——updateNum()
雙重循環遍歷squares的行和列,跳過周圍沒有雷的方塊,只計算更新周圍有雷的方塊;即碰到一個雷就調用getAround()函數獲取雷周圍的所有非雷格,并逐個進行value值+1的操作。
注:getAround()函數返回的二維數組設為num,其中num[i]對應一個方格的行列,即num[i]——[a,b];故num[i][0]和num[i][1]分別對應取到方格的行和列。
實例方法五:初始化游戲——init()
第一步調用randomNum()函數生成隨機的地雷,遍歷表格行和列設置每個格子對應的類型;
//地雷類型 {type:'mine',x:0,y:0 } //數值類型,value為周圍雷的數量 {type:'number',x:0,y:0,value:2 }第二步更新所有格子值(updateNum()函數);
第三步創建表格;
第四步獲取DOM元素,給結構中第三個div動態添加剩余雷數的值,初始化為預定義的雷數。
實例方法六:開始游戲——play()
完善createDom()函數中的單元格點擊事件體中的開始游戲函數。
開始游戲時當我們按下鍵盤按鍵就會產生keydown, keypress keyup事件,從這些事件中可以獲取按鍵的鍵值,而鍵值保存在了 ev.which ev.keyCode ev.charCode中。
keyCode表示用戶按下鍵的實際的編碼(用戶按下的按鍵的物理編碼),而charCode是指用戶按下字符的編碼。我們需要知道的是用戶點擊的是鼠標的左鍵還是右鍵,故用ev.which即可處理;ev.which==1表示點擊的是左鍵,ev.which==3則表示點擊右鍵。
首先處理左鍵點擊事件,第一點我們需要知道小紅旗是不能進行左鍵點擊的,我們可通過squares和obj.pos獲取到當前被點擊的單元格;這時我們對該單元格進行類型判斷,如果點擊到的是數字類型且數值為0,我們就需要進行一個擴展的操作,否則直接顯示數值(此處為了更加美觀需在CSS中定義不同數值的顯示樣式),如果點擊到的是雷類型,則游戲結束。
其次我們處理右擊事件。第一點,如果是一個數字類型的單元格我們是不能點擊的;其次每次右擊,我們都需要對該單元格進行一個class的切換,即右擊即修改其className為flag(小紅旗),但如果本身就是一個flag,再次右擊則小紅旗消失;第二點,每標注一個小紅旗我們都需判斷其是否為雷,并修改allRight值;第三點,右擊產生一個小紅旗,則剩余雷數動態減一,小紅旗消失剩余雷數加一;最后判斷小紅旗數是否用盡,若用盡則通過allRight判斷游戲是成功還是失敗。
重點難點:左鍵點擊事件中怎么處理0的擴展?——遞歸思想
首先我們考慮一下整個擴展的過程:
- 1.顯示自己(空白)
- 2.以自己為中心找四周
- ??????? ?2.1?顯示四周(如果四周值不為0,停止,不再顯示)
- ?????????2.2?如果值為0
- ? ? ? ? ? ? ? ? ? ? ? 2.2.1?顯示自己(空白)
- ? ? ? ? ? ? ? ? ? ? ? 2.2.2?找四周(如果四周值不為0,停止,不再顯示)
- ?????????????????? ? ? ? ? ? ? ? ? ? ? 2.2.2.1??...
- ?????????????????? ? ? ? ? ? ? ? ??? ? 2.2.2.2??...
故為一個遞歸調用自己的過程——封裝為一個getAllZero()函數。
實例方法七:0遞歸擴展函數——getAllZero(square)
首先獲取當前中心點square的四周格子,取出行、列,判斷其是否仍為值為0的數字方塊,若是,已該方塊為中心繼續找四周(此時注意,我們需要添加一個check屬性去標記某個單元格是否已被遞歸調用過,避免死循環);若不是則直接顯示數字,函數結束。
實例方法八:游戲結束——gameOver()
代碼如下(JS):
//構造函數 function Mine(tr, td, mineNum) {this.tr = tr; //行數this.td = td; //列數this.mineNum = mineNum; //雷的數量//存儲所有方塊的信息,二維數組,行與列的順序排放,存取使用行列的形式this.squares = [];//存儲所有單元格的DOMthis.tds = [];//剩余雷的數量this.surplusMine = mineNum;//右擊標的小紅旗是否全是雷,用于判斷用戶是否游戲成功this.allRight = false;this.parent = document.querySelector('.gameBox'); } //生成n個不重復的數值 Mine.prototype.randomNum = function() {//生成一個有長度的空數組,即格子總數var square = new Array(this.tr * this.td);for (var i = 0; i < square.length; i++) {square[i] = i;}square.sort(function() { return 0.5 - Math.random() });// console.log(square);return square.slice(0, this.mineNum); } Mine.prototype.init = function() {// console.log(this.randomNum());//雷在格子里的位置var rn = this.randomNum();//用于找到格子對應的索引var n = 0;for (var i = 0; i < this.tr; i++) {this.squares[i] = [];for (var j = 0; j < this.td; j++) {// this.squares[i][j]=;// n++;//取一個方塊 在數組里的數據,要使用行與列的形式取,找方塊周圍的方塊用坐標的形式取if (rn.indexOf(++n) != -1) {//如果條件成立,那么當前循環到的這個索引在雷的數組里找到了,即為雷this.squares[i][j] = { type: 'mine', x: j, y: i };} else {this.squares[i][j] = { type: 'number', x: j, y: i, value: 0 };}}// 0,0 0,1 0,2 行和列// 0,0 1,0 2,0 xy軸// {// type:'mine',// x:0,// y:0// }// {// type:'number',// x:0,// y:0,// value:2// }}// console.log(this.squares);this.updateNum();this.createDom();this.parent.oncontextmenu = function() {return false;}//剩余雷數this.mineNumDom = document.querySelector('.mineNum');this.mineNumDom.innerHTML = this.surplusMine;}//創建表格 Mine.prototype.createDom = function() {var This = this;var table = document.createElement('table');for (var i = 0; i < this.tr; i++) { //行var domTr = document.createElement('tr');this.tds[i] = [];for (var j = 0; j < this.td; j++) { //列var domTd = document.createElement('td');// domTd.innerHTML = 0;domTd.pos = [i, j]; //把格子對應的行與列存到格子身上,為了下面通過這個值取到相應的數字domTd.onmousedown = function(Event) {This.play(Event, this); //大的this指的是實例對象,this指的是點擊的Td}//把所有創建的td添加到數組當中this.tds[i][j] = domTd;// if (this.squares[i][j].type == 'mine') {// domTd.className = 'mine';// }// if (this.squares[i][j].type == 'number') {// domTd.innerHTML = this.squares[i][j].value;// }domTr.appendChild(domTd);}table.appendChild(domTr);}this.parent.innerHTML = ''; //避免多次點擊創建多個this.parent.appendChild(table); };//找某個格子周圍的格子 Mine.prototype.getAround = function(square) {var x = square.x;var y = square.y;var result = []; //找到的格子(<=8個)的坐標 二維數組/*** x-1,y-1 x,y-1 x+1,y-1 * x-1,y x,y x+1,y* x-1,y+1 x,y+1 x+1,y+1*///通過坐標循環九宮格for (var i = x - 1; i <= x + 1; i++) {for (var j = y - 1; j <= y + 1; j++) {//特殊情況:格子超出范圍 左邊;上邊;右邊;下邊 當前格子是自己 周圍格子是個雷if (i < 0 || j < 0 || i > this.td - 1 || j > this.tr - 1 || (i == x && j == y) || this.squares[j][i].type == 'mine') {continue;}result.push([j, i]); //以行列形式返回}}return result;}//更新所有數字 Mine.prototype.updateNum = function() {for (var i = 0; i < this.tr; i++) {for (var j = 0; j < this.td; j++) {//要更新的是雷周圍的數字if (this.squares[i][j].type == 'number') {continue;}//獲取到每一個雷周圍的數字var num = this.getAround(this.squares[i][j]);// console.log(num);for (var k = 0; k < num.length; k++) {// num[i]==[0,1]// num[i][0]==0// num[i][1]==1this.squares[num[k][0]][num[k][1]].value += 1;}}}// console.log(this.squares); }Mine.prototype.play = function(ev, obj) {var This = this;if (ev.which == 1 && obj.className != 'flag') {//點擊的是左鍵,并限制不能左鍵點擊小紅旗// console.log(obj);var curSquare = this.squares[obj.pos[0]][obj.pos[1]];var cl = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight'];if (curSquare.type == 'number') {//點到數字// console.log('你點到數字了!');//兩種情況:點到0和非0obj.innerHTML = curSquare.value;obj.className = cl[curSquare.value];if (curSquare.value == 0) {// 如果點到了0/*** 擴展空白——遞歸* 1.顯示自己(空白)* 1.以自己為中心找四周* 1.1 顯示四周(如果四周值不為0,停止,不再顯示)* 1.2 如果值為0* 1.2.1 顯示自己(空白)* 1.2.2 找四周(如果四周值不為0,停止,不再顯示)* 1.2.2.1 ...* 1.2.2.2 ...*/obj.innerHTML = ''; //不顯示0function getAllZero(square) {//找到了周圍n個格子var around = This.getAround(square);for (var i = 0; i < around.length; i++) {var x = around[i][0]; //行var y = around[i][1]; //列This.tds[x][y].className = cl[This.squares[x][y].value];if (This.squares[x][y].value == 0) {//如果以某個格子為中心找到的格子值為0,那就需要接著調用函數(遞歸)if (!This.tds[x][y].check) {//給對應的td添加check屬性,用于決定該格子有沒有被找過,找過改為true,下一次就不會找了This.tds[x][y].check = true;getAllZero(This.squares[x][y]);}} else {//不為0則顯示數字This.tds[x][y].innerHTML = This.squares[x][y].value;}}}getAllZero(curSquare);}} else {//用戶點到雷// console.log('你點到雷了!');this.gameOver(obj);}}//點擊的是右鍵if (ev.which == 3) {//如果右擊的是一個數字就不能點擊if (obj.className && obj.className != 'flag') {return;}obj.className = obj.className == 'flag' ? '' : 'flag'; //切換classif (this.squares[obj.pos[0]][obj.pos[1]].type == 'mine') {//都是雷this.allRight = true;} else {this.allRight = false;}if (obj.className == 'flag') {this.mineNumDom.innerHTML = --this.surplusMine;} else {this.mineNumDom.innerHTML = ++this.surplusMine;}if (this.surplusMine == 0) {//用戶標完了小紅旗的數量,此時該判斷游戲是成功還是結束if (this.allRight) {//該條件成立說明用戶全部標對了alert('恭喜,游戲通過!');} else {alert('游戲失敗!');this.gameOver();}}} };//游戲失敗函數 Mine.prototype.gameOver = function(clickTd) {/*** 1.顯示所有的雷* 2.取消所有格子的點擊事件* 3.給點中的雷標上一個紅*/for (var i = 0; i < this.tr; i++) {for (var j = 0; j < this.td; j++) {if (this.squares[i][j].type == 'mine') {this.tds[i][j].className = 'mine';}this.tds[i][j].onmousedown = null;}}if (clickTd) {clickTd.style.backgroundColor = '#f00';}}//上邊button的功能 var btns = document.querySelectorAll('.level button'); var mine = null; //存儲生成的實例 var ln = 0; //用于處理當前選中的level狀態 var arr = [[9, 9, 9],[16, 16, 40],[28, 28, 99] ]; //不同level的行數列數及雷數for (let i = 0; i < btns.length - 1; i++) {btns[i].onclick = function() {btns[ln].className = '';this.className = 'active';//ES6擴展運算符mine = new Mine(...arr[i]);mine.init();// mine.surplusMine = arr[i][3];ln = i;} } btns[0].onclick(); //主動調事件,初始化一下 btns[3].onclick = function() {mine.init(); }// var mine = new Mine(28, 28, 99); // mine.init();// console.log(mine.getAround(mine.squares[0][0]));總結
以上就是我個人完成掃雷項目的整體框架流程和代碼,由于時間原因仍留有一定bug(如每次點擊level重新開始一次游戲,剩余雷數未進行初始化等問題),該項目也還有很多地方可以右更好的辦法進行優化,今天的博客就分享到這里啦~如果往后我有時間對該項目進行了迭代開發優化,會盡量分享給小伙伴們滴!
?
總結
以上是生活随笔為你收集整理的纯JS实现简易扫雷小游戏网页项目的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php上传文件损坏,PHP 上传文件故障
- 下一篇: C语言的fgets 与 gets