前言: 一年來一直做得是後端的東西,沒有寫前端代碼,忘乾凈了,之前也只用jQuery,好歹還能做點效果。想著撿起來一點,要不然枉費了師傅的栽培。 一直在看博客,但沒有屬於自己的東西。this has to change. why not start now?俗話說只要代碼敲得夠快,悲傷就追不上我。 ...
前言:
一年來一直做得是後端的東西,沒有寫前端代碼,忘乾凈了,之前也只用jQuery,好歹還能做點效果。想著撿起來一點,要不然枉費了師傅的栽培。
一直在看博客,但沒有屬於自己的東西。this has to change.
why not start now?俗話說只要代碼敲得夠快,悲傷就追不上我。
此篇開博,從簡單小游戲開始。目的是做出一些可以快點看到效果的東西,撿回一點程式員的信心。
先看最終效果:
1. 2048游戲功能分析:
核心是4個方向的移動的處理。
往一個方向移動,先看是否有數字的合併。一次移動,一行只允許合併一次,且從終點往起點合併。
合併完成之後,將合併後的數字和其他數字移動,貼邊。
在空白位置隨機生成一個數字2,移動結束。
演算法實現:
考慮採用二維數組存儲4*4個格子中的值,如果移動,只需要更新數組,然後用數組的值來更新dom元素即可。
(這種想法不太面向對象。要是將每個格子都理解為一個對象,不知道好不好做,等以後研究--TODO)
2. 要用到的東西
2.1 html頁面佈局,4*4個方塊得整齣來
2.2 簡單css:不同的數字設置不同顏色
2.3 簡單dom操作:取元素,變換顯示的數字,改變class
2.4 取隨機數演算法
2.5 按鍵事件監聽
2.6 一點點交互操作考慮,比如判定游戲失敗結束,成功結束,返回上一步。
3. 代碼結構:
較為粗糙,js幾乎全部是直接用html引用了,不會其他方式。先實現功能;
幾個圖片是:返回上一步按鈕,上下左右按鈕,重新開始按鈕。
js區分:
arrayHelper是操作數組的;
domOperation顧文思意,操作dom;
eventListener,處理按鍵事件
common:頁面初始化,隨機數生成等
4. 核心代碼
4.1 向方向【左】移動的邏輯
評論:寫的比較死,一步一步,按照規則來即可
// 向一個方向移動,更新數組(左),其他方向也是一樣的 function moveLeft(arrayCopy){ var gameSuccess = false; // 是否有合併或者移動,如果沒有合併也沒有移動,就是沒有操作那就不要生成新的數字 var anyChange = false; // 每一行,從【左】往【右】對比是否有一樣的數字(排除0)--並不需要挨著!中間隔了空氣也可以合併 for (var i=0;i<4;i++){ var firstIndex = 0; var secondIndex = 1; while (secondIndex<4){ if (arrayCopy[i][firstIndex] == 0){ // 第一個是0,不可能有合併,往後移動 firstIndex++; secondIndex++; continue; } if (arrayCopy[i][secondIndex] == 0){ // 第二個是0,第二個繼續往後找,第一個不變 secondIndex++; // 1. 後面找不到數字,一直往後找,結束迴圈 continue; } // 有一次一樣的數字:計算得到和,放到第一個位置,這一行完了 else if (arrayCopy[i][firstIndex] == arrayCopy[i][secondIndex]){ arrayCopy[i][firstIndex] = arrayCopy[i][firstIndex] * 2; arrayCopy[i][secondIndex] = 0; anyChange = true; if (arrayCopy[i][firstIndex] >= 2048){ // 得到了2048,游戲結束 gameSuccess = true; } // 2. 後面找到數字,且可以合併,就合併,結束迴圈 break; } else{ // 3. 後面找到數字,但是不能合併,更新第一個數字為當前數字,第二個數字為下一個數字 firstIndex = secondIndex; secondIndex += 1; continue; } } } // 將每一行數字向【左】挪動 for (var i=0;i<4;i++){ var newLine = [];//臨時存儲從【左】到【右】的非0數字 var index = 0; for (var j=0;j<4;j++){ if (arrayCopy[i][j] != 0){ newLine[index] = arrayCopy[i][j]; index++; } } // 用臨時存儲的數字給數組更新 for (var m=0;m<index;m++){ if (arrayCopy[i][m] != newLine[m]){ anyChange = true; } arrayCopy[i][m] = newLine[m]; } // 剩餘的位置給0 for (var n=index;n<4;n++){ if (arrayCopy[i][n] != 0){ anyChange = true; } arrayCopy[i][n] = 0; } } if (!anyChange){ console.log("no change after this move!"); if (!isEmptyCell(arrayCopy)){ // 本步不能移動,且沒有剩餘格子,且任意相鄰格子沒有相同的,就結束游戲 if (!canMerge(arrayCopy)){ console.log("Game Over! Try again."); return 'fail'; } } if (gameSuccess){ return 'success'; } // 沒有移動 return 'unmoved'; } // 給空閑位置設置數字2 if (isEmptyCell(arrayCopy)) { initOnePosition(arrayCopy); } if (gameSuccess){ return 'success'; } return 'moved'; }
4.2. 通用的移動處理。
因為往一個方向移動,和往其他方向移動處理是一樣的,不可能複製以上代碼4次吧。
換個方向看:往左移動和往右移動對數組的操作,只是行處理相反,列處理一樣;
其他方向移動類似,不值得copy整個方法。
// 通用方法 // 所有移動都轉換成一個方向上的移動:例如,都想象成向左移動,只需要倒轉數組即可,完成移動再倒轉回來! function move(direction){ if (flagSuccess){ if (confirm("You've got your 2048! Start Another One?\n已經通關!開始新游戲?")){ gameRestart(); } return; } // 移動之前,記錄上一步的內容,移動失敗之後可以恢復 var lastStepUnchange = createNewArray(); updateFrontArray(lastStepUnchange,lastStepArray); // 將上一步狀態設置為當前的 updateFrontArray(lastStepArray,arrayInit); // 將當前的去更新,更新失敗則回滾上一步的狀態 // 1. 數組倒轉複製 var arrayCopy = new Array(); if (direction == 'left'){ arrayCopy = copyArrayLeft(); }else if (direction == 'right'){ arrayCopy = copyArrayRight(); }else if (direction == 'up'){ arrayCopy = copyArrayUp(); }else if (direction == 'down'){ arrayCopy = copyArrayDown(); } // 2. 都向一個方向移動 var moveResult = moveLeft(arrayCopy); if ('fail' == moveResult){ updateFrontArray(lastStepArray,lastStepUnchange); // 移動失敗,沒有任何改變 if (confirm('Game Over! Try again?\n游戲結束,再來一次?')){ gameRestart(); return; }else{ // 最後一步了,允許回退 clickedLastStep = false; return; } return; } if ('unmoved' == moveResult){ return; } // 3. 從倒轉的數組更新原數組 if (direction == 'left'){ restoreArrayLeft(arrayCopy); }else if (direction == 'right'){ restoreArrayRight(arrayCopy); }else if (direction == 'up'){ restoreArrayUp(arrayCopy); }else if (direction == 'down'){ restoreArrayDown(arrayCopy); } // 4. 根據數字更新頁面元素 updatePageByArray(); updateColor(); if ('success' == moveResult){ // 因為操作DOM不是實時的,所以要等一段事件操作完成之後再彈出確認框 setTimeout(function(){ // 游戲通關,是否重新開始 if (confirm('Congragulations! Start Another Game?\n恭喜通關!是否再來一把?')){ gameRestart(); }else{ flagSuccess = true; } },200); return; } // 移動完成 clickedLastStep = false; }
4.3 隨機數的生成:
開始做的是生成一個隨機數,16個位置的任意一個,如果這個位置已經有人了,就放棄,重新生成一個,直到找到空位為止。
但是到空位變少的時候,這樣效率低下。
優化:只在空位中找隨機位置。
// 生成隨機數,轉化成0-15的整數; function getRamdom(count) { // 0-1 var r0 = Math.random(); // 0-16 var r1 = r0 * count; // 只取整數位數的數 return Math.floor(r1); } // 改進:只在當前可用位置中隨機找,不用找不可用的位置 function getOneEmptyCoordinate(array) { // 1. 得到當前數組 var countEmptyCells = 0; var mapIndex2Coordinate = {}; var coordinates = null; for (var i=0;i<4;i++){ for (var j=0;j<4;j++){ if (array[i][j] == 0){ countEmptyCells++; coordinates = new Array(); coordinates.push(i); coordinates.push(j); mapIndex2Coordinate[countEmptyCells] = coordinates; } } } // 取可用位置的隨機位置 var random = getRamdom(countEmptyCells); return mapIndex2Coordinate[random]; }
5. 欠缺:
5.1 佈局,樣式的調整比較嫌麻煩,瞎調的。
5.2 js代碼關係--沒什麼關係,忘了前端是怎麼組織的了;
5.3 刷新頁面多次,可能出現從初始化異常,可能是文件載入順序問題,初始化時dom操作比較慢等原因
5.4 看看別人實現思路如何
-- http://www.cnblogs.com/-lizi/p/8431030.html 這個寫的很漂亮
6. 總結:
開發時間:5天下班時間,實際投入:15個小時左右。
代碼規模:js400行;
7. 完整代碼粘貼
index.html
<head> <meta http-equiv="content-type" content="text/html;charset=utf-8"> </head> <link href="../css/common.css" rel="stylesheet"> <body> <script type="text/javascript" src="../js/common.js"></script> <script type="text/javascript" src="../js/eventListener.js"></script> <script type="text/javascript" src="../js/domOperation.js"></script> <div id="main"> <div id="refreshDiv" onclick="gameRestart()"> <span id="refreshButton"> <img src="../resource/refreshButton.png" alt="refresh" /> <a>重新開始</a> </span> </div> <div id="mainTable"> </div> <div id="explation"> <span> 2048游戲說明:<br> 按上下左右方向鍵可以移動有數字的方塊,<br> 相鄰的相同數字的方塊往一個方向移動會合併成更大的數字。<br> 當最大數字出現2048時,游戲勝利!<br> 溫馨提示:你可以使用空格鍵回退上一步。 </span> </div> <div id="rights"> <span> All rights reserved to S.C. Contact me at [email protected]. </span> </div> </div> <!-- 操作 --> <div id="keys"> <div> <span id="moveUpButton"onclick="move('up')"> <img src="../resource/direction.png" alt="direction" /> </span> </div> <div> <span id="moveLeftButton"onclick="move('left')"> <img src="../resource/direction.png" alt="direction" /> </span> <span id="moveBackButton" onclick="eventSpaceKey()"> <img src="../resource/moveBack.png" alt="moveBack" /> </span> <span id="moveRightButton"onclick="move('right')"> <img src="../resource/direction.png" alt="direction" /> </span> </div> <div> <span id="moveDownButton"onclick="move('down')"> <img src="../resource/direction.png" alt="direction" /> </span> </div> </div> <script type="text/javascript"> window.onload = function(){ pageInit(); } </script> </body>
common.css
#main
{
margin-left:500px;
margin-top:100px;
width:404px;
heignt:400px;
border: 0px solid gray;
border-radius: 1px;
}
#keys
{
margin-left:1150px;
margin-top:-430px;
width:303px;
heignt:300px;
border: 3px solid green;
border-radius: 20px;
}
#keys div
{
display:flex;
height:75px;
}
#keys div span
{
height:75px;
width:100px;
border: 1px solid gray;
background:blue;
}
#moveLeftButton img
{
height:75px;
width:100px;
-ms-transform:rotate(180deg); /* IE 9 */
-moz-transform:rotate(180deg); /* Firefox */
-webkit-transform:rotate(180deg); /* Safari and Chrome */
-o-transform:rotate(180deg); /* Opera */
}
#moveRightButton img
{
height:75px;
width:100px;
}
#moveUpButton
{
margin-left:100px;
}
#moveUpButton img
{
margin-top: -13px;
margin-left: 12px;
height:100px;
width:75px;
-ms-transform:rotate(270deg); /* IE 9 */
-moz-transform:rotate(270deg); /* Firefox */
-webkit-transform:rotate(270deg); /* Safari and Chrome */
-o-transform:rotate(270deg); /* Opera */
}
#moveDownButton
{
margin-left:100px;
}
#moveDownButton img
{
margin-left: 12px;
margin-top: -13px;
height:100px;
width:75px;
-ms-transform:rotate(90deg); /* IE 9 */
-moz-transform:rotate(90deg); /* Firefox */
-webkit-transform:rotate(90deg); /* Safari and Chrome */
-o-transform:rotate(90deg); /* Opera */
}
#moveBackButton
{
height:100px;
width:75px;
background:yellow !important;
}
#refreshDiv
{
height:40px;
width:133px;
margin-left:140px;
cursor: pointer;
background:#ba3537;
border-radius: 2px;
}
#refreshDiv span
{
display: contents;
font-family: '微軟雅黑';
margin-left:159px;
font-size:20;
}
#refreshDiv span a
{
margin-bottom: 10px;
}
#explation
{
margin-top:30px;
}
#rights
{
margin-top:100px;
}
#rights span
{
font-size:10;
}
#explation span
{
font-size:14;
}
#mainTable
{
margin-top:20px;
background:#f5f5f5;
border: 2px solid #478dcd;
border-radius: 3px;
}
#mainTable div
{
display:flex;
width:400px;
heignt:100px;
}
#mainTable span
{
height:100px;
width:100px;
border: 1px solid gray;
font-size: 50;
text-align: center;
font-weight:bold;
}
.color0
{
background:#f5f5f5;
}
.color2
{
background:#f5c7ad;
}
.color4
{
background:#ec9362;
}
.color8
{
background:#e3631e;
}
.color16
{
background:#c2fcb1;
font-size: 45 !important;
}
.color32
{
background:#76f850;
font-size: 45 !important;
}
.color64
{
background:#2dad07;
font-size: 45 !important;
}
.color128
{
background:#a8a6f9;
font-size: 40 !important;
}
.color256
{
background:#4b47f1;
font-size: 40 !important;
}
.color512
{
background:#110da4;
font-size: 40 !important;
}
.color1024
{
background:#f799ef;
font-size: 35 !important;
}
.color2048
{
background:#a30e98;
font-size: 35 !important;
}
common.js
// 拼成了2048,凍結操作,不允許移動了 var flagSuccess = false; // 剛纔點擊了回退,不允許重覆點擊,要移動之後再點擊 var clickedLastStep = false; // 記錄上一步,用於回退 var lastStepArray = [ [0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0] ]; // 數組初始化--正常初始化 var arrayInit = [ [0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0] ]; //[//調試--將要success // [0,0,0,0], // [0,128,0,0], // [0,0,512,1024], // [0,0,0,1024] //]; //[//調試--將要fail // [1,2,3,4], // [5,128,6,7], // [8,9,512,1024], // [13,22,11,11] //]; // 頁面載入初始化 function pageInit() { addHelper(); // initArray(); // 頁面元素初始化(單元格) initElements(); // 初始數據 initTwoPositions(); // 根據初始數據給單元格填充值 updatePageByArray(); // 根據單元格的值給單元格設置樣式 updateColor(); } // 重新開始 function gameRestart(){ initArray(); pageInit(); } function initArray(){ flagSuccess = false; arrayInit = createNewArray(); clickedLastStep = false; lastStepArray = createNewArray(); } // 給兩個位置設置數字2 function initTwoPositions() { initOnePosition(arrayInit); initOnePosition(arrayInit); } // 給一個位置設置數字2 function initOnePosition(array){ // 取隨機數方式1 // var num = getRamdom(16); // var line = Math.floor(num/4); // var col = num % 4; // var curNum = array[line][col]; // if (curNum == 0) // { // // 成功找到空閑位置 // array[line][col] = 2; // return true; // } // else{ // // 遞歸調用,必須找到一個可用位置才算完 // return initOnePosition(array); // } // 取隨機數方式2 var coordinate = getOneEmptyCoordinate(array); array[coordinate[0]][coordinate[1]] = 2; } function isEmptyCell(arrayCopy) { for (var i=0;i<4;i++) { for (var j=0;j<4;j++) { if (arrayCopy[i][j] == 0) { return true; } } } return false; } // 生成隨機數,轉化成0-15的整數; function getRamdom(count) { // 0-1 var r0 = Math.random(); // 0-16 var r1 = r0 * count; // 只取整數位數的數 return Math.floor(r1); } // 改進:只在當前可用位置中隨機找,不用找不可用的位置 function getOneEmptyCoordinate(array) { // 1. 得到當前數組 var countEmptyCells = 0; var mapIndex2Coordinate = {}; var coordinates = null; for (var i=0;i<4;i++){ for (var j=0;j<4;j++){ if (array[i][j] == 0){ countEmptyCells++; coordinates = new Array(); coordinates.push(i); coordinates.push(j); mapIndex2Coordinate[countEmptyCells] = coordinates; } } } // 取可用位置的隨機位置 var random = getRamdom(countEmptyCells); return mapIndex2Coordinate[random]; }
eventListener,js
/** * 引用工具類 */ function addHelper(){ var newscript = document.createElement('script'); newscript.setAttribute('type','text/javascript'); newscript.setAttribute('src','../js/arrayHelper.js'); document.body.appendChild(newscript); } //捕捉按鍵事件 document.onkeyup = function(event) { event = event || window.event; if (event.keyCode == 37){//left move('left'); }else if (event.keyCode == 39){//right move('right'); }else if (event.keyCode == 38){//up move('up'); }else if (event.keyCode == 40){//down move('down'); }else if (event.keyCode == 32){//space eventSpaceKey(); } } function eventSpaceKey(){ // 回退上一步 if (clickedLastStep){ console.log('can not move back again'); return; } moveBack(); flagSuccess = false; clickedLastStep = true; } // 回退上一步 function moveBack(){ // 判斷上一步全是0(剛剛初始化) var justStarted = true; for (var i=0;i<4;i++){ for (var j=0;j<4;j++){ if (lastStepArray[i][j] != 0){ justStarted = false; } } } if(justStarted){ return; } // 1. 用備份的數組給當前數組設置值 updateFrontArray(arrayInit,lastStepArray); // 3. 更新頁面 updatePageByArray(); updateColor(); } // 通用方法 // 所有移動都轉換成一個方向上的移動:例如,都想象成向左移動,只需要倒轉數組即可,完成移動再倒轉回來! function move(direction){ if (flagSuccess){ if (confirm("You've got your 2048! Start Another One?\n已經通關!開始新游戲?")){ gameRestart(); } return; } // 移動之前,記錄上一步的內容,移動失敗之後可以恢復 var lastStepUnchange = createNewArray(); updateFrontArray(lastStepUnchange,lastStepArray); // 將上一步狀態設置為當前的 updateFrontArray(lastStepArray,arrayInit); // 將當前的去更新,更新失敗則回滾上一步的狀態 // 1. 數組倒轉複製 var arrayCopy = new Array(); if (direction == 'left'){ arrayCopy = copyArrayLeft(); }else if (direction == 'right'){ arrayCopy = copyArrayRight(); }else if (direction == 'up'){ arrayCopy = copyArrayUp(); }else if (direction == 'down'){ arrayCopy = copyArrayDown(); } // 2. 都向一個方向移動 var moveResult = moveLeft(arrayCopy); if ('fail' == moveResult){ updateFrontArray(lastStepArray,lastStepUnchange); // 移動失敗,沒有任何改變 if (confirm('Game Over! Try again?\n游戲結束,再來一次?')){ gameRestart(); return; }else{ // 最後一步了,允許回退 clickedLastStep = false; return; } return; } if ('unmoved' == moveResult){ return; } // 3. 從倒轉的數組更新原數組 if (direction == 'left'){ restoreArrayLeft(arrayCopy); }else if (direction == 'right'){ restoreArrayRight(arrayCopy); }else if (direction == 'up'){ restoreArrayUp(arrayCopy); }else if (direction == 'down'){ restoreArrayDown(arrayCopy); } // 4. 根據數字更新頁面元素 updatePageByArray(); updateColor(); if ('success' == moveResult){ // 因為操作DOM不是實時的,所以要等一段事件操作完成之後再彈出確認框 setTimeout(function(){ // 游戲通關,是否重新開始 if (confirm('Congragulations! Start Another Game?\n恭喜通關!是否再來一把?')){ gameRestart(); }else{ flagSuccess = true; } },200); return; } // 移動完成 clickedLastStep = false; } // 向一個方向移動,更新數組(左),其他方向也是一樣的 function moveLeft(arrayCopy){ var gameSuccess = false; // 是否有合併或者移動,如果沒有合併也沒有移動,就是沒有操作那就不要生成新的數字 var anyChange = false; // 每一行,從【左】往【右】對比是否有一樣的數字(排除0)--並不需要挨著!中間隔了空氣也可以合併 for (var i=0;i<4;i++){ var firstIndex = 0; var secondIndex = 1; while (secondIndex<4){ if (arrayCopy[i][firstIndex] == 0){ // 第一個是0,不可能有合併,往後移動 firstIndex++; secondIndex++; continue; } if (arrayCopy[i][secondIndex] == 0){ // 第二個是0,第二個繼續往後找,第一個不變 secondIndex++; // 1. 後面找不到數字,一直往後找,結束迴圈 continue; } // 有一次一樣的數字:計算得到和,放到第一個位置,這一行完了 else if (arrayCopy[i][firstIndex] == arrayCopy[i][secondIndex]){ arrayCopy[i][firstIndex] = arrayCopy[i][firstIndex] * 2; arrayCopy[i][secondIndex] = 0; anyChange = true; if (arrayCopy[i][firstIndex] >= 2048){ // 得到了2048,游戲結束 gameSuccess = true; } // 2. 後面找到數字,且可以合併,就合併,結束迴圈 break; } else{ // 3. 後面找到數字,但是不能合併,更新第一個數字為當前數字,第二個數字為下一個數字 firstIndex = secondIndex; secondIndex += 1; continue; } } } // 將每一行數字向【左】挪動 for (var i=0;i<4;i++){ var newLine = [];//臨時存儲從【左】到【右】的非0數字 var index = 0; for (var j=0;j<4;j++){ if (arrayCopy[i][j] != 0){ newLine[index] = arrayCopy[i][j]; index++; } } // 用臨時存儲的數字給數組更新 for (var m=0;m<index;m++){ if (arrayCopy[i][m] != newLine[m]){ anyChange = true; } arrayCopy[i][m] = newLine[m]; } // 剩餘的位置給0 for (var n=index;n<4;n++){ if (arrayCopy[i][n] != 0){ anyChange = true; } arrayCopy[i][n] = 0; } } if (!anyChange){ console.log("no change after this move!"); if (!isEmptyCell(arrayCopy)){ // 本步不能移動,且沒有剩餘格子,且任意相鄰格子沒有相同的,就結束游戲 if (!canMerge(arrayCopy)){ console.log("Game Over! Try again."); return 'fail'; } } if (gameSuccess){ return 'success'; } // 沒有移動 return 'unmoved'; } // 給空