先放個效果圖吧
首先,先把html結構搭好。建立一個html檔案,設定好标題2048,按鈕New Game以及score:0,這裡按鈕是用a标簽來将javascript中的newgame函數給傳進來,score後面的分數由于是動态的,是以給它添加一個span标簽,并加上一個id,代碼如下
<header>
<h1>2048</h1>
<a href="javascript:newgame();" id="newgamebutton">New Game</a> <!-- 點選a标簽後,執行一個js的自定義函數newgame() -->
<a href="javascript:undo();" id="undobutton">Undo</a> <!-- 點選a标簽後,執行傳回上一步的操作 -->
<p>score: <span id="score">0</span></p>
</header>
然後将所有的格子布置好,這裡是用div标簽來表示每個格子,後續将會用相對定位,絕對定位以及js中的函數來編寫每個格子的位置。代碼如下
<div id="grid_container">
<div class="grid_cell" id="grid_cell_0_0"></div>
<div class="grid_cell" id="grid_cell_0_1"></div>
<div class="grid_cell" id="grid_cell_0_2"></div>
<div class="grid_cell" id="grid_cell_0_3"></div>
<div class="grid_cell" id="grid_cell_1_0"></div>
<div class="grid_cell" id="grid_cell_1_1"></div>
<div class="grid_cell" id="grid_cell_1_2"></div>
<div class="grid_cell" id="grid_cell_1_3"></div>
<div class="grid_cell" id="grid_cell_2_0"></div>
<div class="grid_cell" id="grid_cell_2_1"></div>
<div class="grid_cell" id="grid_cell_2_2"></div>
<div class="grid_cell" id="grid_cell_2_3"></div>
<div class="grid_cell" id="grid_cell_3_0"></div>
<div class="grid_cell" id="grid_cell_3_1"></div>
<div class="grid_cell" id="grid_cell_3_2"></div>
<div class="grid_cell" id="grid_cell_3_3"></div>
</div>
現在html結構已經搭完了,開始編寫css樣式。建立一個css檔案,這裡css樣式其實沒什麼好講的,各塊内容多大字号,什麼背景顔色,padding,margin為多少都是看自己喜好,該居中的居中,我設的大盒子長寬為500px,每個格子長寬為100px,代碼如下
header {
margin: 0 auto;
width: 500px;
text-align: center;
}
header h1 {
font-family: Arial;
font-size: 40px;
font-weight: bold;
}
#newgamebutton {
/* 為了設定各方向的padding和margin,将其變成塊元素 */
display: block;
margin: 20px auto;
width: 100px;
color: white;
background-color: #8f7a66;
text-decoration: none;
padding: 10px;
border-radius: 10px;
font-family: Arial;
}
#newgamebutton:hover {
background-color: #9f8b77;
}
#undobutton {
display: inline-block;
margin: 5px 20px;
width: 100px;
padding: 10px 10px;
background-color: #8f7a66;
font-family: Arial;
color: white;
border-radius: 10px;
text-decoration: none;
}
#undobutton:hover {
background-color: #9f8b77;
}
header p {
font-family: Arial;
font-size: 25px;
margin: 20px auto;
}
#grid_container {
width: 460px;
height: 460px;
padding: 20px;
margin: 30px auto;
border-radius: 10px;
background-color: #bbada0;
/* 給大盒子設定相對定位 */
position: relative;
}
.grid_cell {
width: 100px;
height: 100px;
border-radius: 6px;
background-color: #ccc0b3;
/* 給小盒子設定絕對定位 */
position: absolute;
}
css樣式設定完後,就可以開始編寫js程式了,建立一個js檔案。
首先先想清楚我們需要設定什麼。從頭開始看,我們用a标簽定義了一個按鈕,在a标簽中調用了一個js的函數newgame,那麼點選New Game按鈕會發生什麼呢?初始化,以及産生兩個随機數。
初始化中又有什麼功能?每一個格子的數字全部初始化為0(0這個數字可以自己随便取),那麼我們就需要定義一個數組專門存放數字。并且分數score也歸零。
var board = []; //存放數字的數組
var score = 0; //存放得分
var hasConflicted = []; //存放是否沖突的數組
let preValue = [];//存放每一步時每個格子的值
let preScore = [];//存放每一步時的得分
此處我們定義了一個數組hasConflicted ,現在先不講,等到最後所有功能完成了我們再講。另外我們之前給每個格子設定了絕對定位,但是還沒設定具體的位置,那麼在此處一并設定了。代碼如下
$(function () {
newgame();
})
// 初始化
function newgame() {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
var gridCell = $("#grid_cell_" + i + "_" + j); // 定義變量gridCell來編寫每個格子的位置
gridCell.css("top", getPosTop(i, j));
gridCell.css("left", getPosLeft(i, j)); //隻需定義格子的左上角即可
}
}
for (var i = 0; i < 4; i++) {
board[i] = [];
hasConflicted[i] = [];
for (var j = 0; j < 4; j++) {
board[i][j] = 0; //把board定義為二維數組來更友善地表達出每個格子,board為0即這個格子沒有數字,不為0即有數字
hasConflicted[i][j] = false; //初始化時需要将每個格子是否沖突的屬性定義為false
}
}
updateBoardView(); //定義此函數來将數字放到該格子中
generateOneNumber(); //每次初始化遊戲都會随機産生兩個數字
generateOneNumber();
score = 0; //初始化
updateScore(score);
}
先看第一段循環,此處定義了一個變量gridCell來友善編寫每個格子的位置,然後又調用了兩個函數getPosTop和getPosLeft。
可以看到每個格子的頂部距離大盒子的頂部都是20px+120px乘縱坐标數,同理,左端也是20px+120px乘橫坐标數。是以代碼如下
function getPosTop(i, j) {
return 20 + i * 120; //每個格子的上方與大盒子頂端的距離
}
function getPosLeft(i, j) {
return 20 + j * 120; //每個格子的左邊與大盒子左邊的距離
}
再接着看newgame函數,由于board是存放每個格子的數字的數組,是以它是一個二維數組,是以第二個循環就是完成這件事的,此處我是将board初始化為0來代表這個格子沒有數字,大家也可以定義為其他數字。hasConflicted最後再講。
然後就是定義了一個函數updateBoardView,這個函數是用來将每個數字放到相應的格子中去的。是以我們每次進行上下左右的操作時,都要調用該函數。代碼如下
function updateBoardView() {
$(".number_cell").remove(); //由于每一次操作都要調用該函數,是以每次調用之前都要先将上次操作後産生的數字清除
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
$("#grid_container").append("<div class='number_cell' id='number_cell_" + i + "_" + j + "'>"); //給每個格子添加數字
var theNumberCell = $("#number_cell_" + i + "_" + j); //定義該變量來編寫每個格子中數字的css樣式
if (board[i][j] == 0) {
theNumberCell.css("width", "0"); //當該位置沒有數字時,寬高設為0就行
theNumberCell.css("height", "0");
theNumberCell.css("top", getPosTop(i, j));
theNumberCell.css("left", getPosLeft(i, j));
} else {
theNumberCell.css("width", "100px");
theNumberCell.css("height", "100px");
theNumberCell.css("top", getPosTop(i, j));
theNumberCell.css("left", getPosLeft(i, j));
theNumberCell.css("background-color", getNumberBackgroundColor(board[i][j])); //定義該函數來存放不同數字的背景顔色
theNumberCell.css("color", getNumberColor(board[i][j])); //定義該函數來存放不同數字的顔色
theNumberCell.text(board[i][j]); //将數字值賦予給相應的格子
}
hasConflicted[i][j] = false; //由于每一次操作都要調用該函數,是以每次調用之前都要先将每個格子是否沖突的屬性定義為false
}
}
}
我們剛說了,每一次操作都要調用該函數,那麼相應的,每一次操作後,每個格子中的數字都要先删掉,然後再指派,第一行就是完成這件事。
然後由于不同的格子有不同的數字,是以我們得定義一個變量theNumberCell來友善編寫。要定義該變量,我們就得先給每個格子設定一個number_cell的類和id,是以我們先動态添加一個html标簽。
添加完标簽後,定義變量theNumberCell,然後又由于沒有數字的格子和有數字的格子樣式不一樣,是以此處分開讨論。
當該位置沒有數字時(即board=0),也就是不顯示數字,那麼寬高設為0就行,位置的話我們之前設定了兩個位置函數,這裡直接調用就行。當該位置有數字時,寬高設為100px,位置照樣調用位置函數,背景顔色和數字的顔色我們定義了兩個函數,最後将該數字放到該格子中去就行。
這裡新加了一堆div标簽,是以我們再給它們添加一下css樣式,代碼如下
.number_cell {
font-family: Arial;
font-size: 30px;
font-weight: bold;
line-height: 100px;
text-align: center;
border-radius: 6px;
position: absolute;
}
背景顔色和數字顔色的函數我就直接給代碼了,沒什麼好說的
function getNumberBackgroundColor(number) {
switch (number) {
case 2:
return "#eee4da";
break;
case 4:
return "#ede0c8";
break;
case 8:
return "#f2b179";
break;
case 16:
return "#f59563";
break;
case 32:
return "#f67c5f";
break;
case 64:
return "#f65e3b";
break;
case 128:
return "#edcf72";
break;
case 256:
return "#edcc61";
break;
case 512:
return "#9c0";
break;
case 1024:
return "#33b5e5";
break;
case 2048:
return "#09c";
break;
case 4096:
return "#a6c";
break;
case 8192:
return "#93c";
break;
}
return "black";
}
function getNumberColor(number) {
if (number <= 4) {
return "#776e65";
} else {
return "white";
}
}
回到newgame函數中,那麼現在updateBoardView這個函數也講完了,之後的就是随機生成兩個數字的函數。代碼如下
function generateOneNumber() {
if (nospace(board)) { //先判斷還有沒有空位置
return false;
} else {
//随機生成一個位置
var randx = parseInt(Math.floor(Math.random() * 4));
var randy = parseInt(Math.floor(Math.random() * 4));
var times = 0;
while (times < 50) {
if (board[randx][randy] == 0) {
break; //判斷該位置是否有數字,若沒有,則直接跳出循環,若有,則繼續生成randx和randy
} else {
randx = parseInt(Math.floor(Math.random() * 4));
randy = parseInt(Math.floor(Math.random() * 4));
}
times++;
}
if (times == 50) {
for (var i = 0; i < 4; i++) { //若算法運作了50次後仍然沒有找到空位置,則人工采用for循環來找
for (var j = 0; j < 4; j++) {
if (board[i][j] == 0) {
randx = i;
randy = j;
}
}
}
}
//随機生成一個數字(2或者4)
var randNumber = Math.random() < 0.5 ? 2 : 4; //當随機數小于0.5時,取2,否則取4
//把數字放進空位置
board[randx][randy] = randNumber;
showNumberWithAnimation(randx, randy, randNumber); //給生成數字的動作添加動畫
return true;
}
}
要想生成數字,首先得判斷一下還有沒有空位置,此處調用了一個新函數。怎麼判斷有沒有空位置呢?周遊所有的格子,如果有格子的數字為0,那麼就有空位置,全部周遊完都沒有為0的數字的話,就沒有空位置,代碼如下
function nospace(board) {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if (board[i][j] == 0) {
return false;
}
}
}
return true;
}
回到generateOneNumber函數中,判斷完後,就該生成數字了。生成數字有兩步,先随機生成一個空位置,再随機生成一個數字。
先來看第一步,怎麼随機生成一個空位置呢?由于每個格子的坐标都是從0開始的(從html處編寫的grid_cell的id也可以看出),是以坐标值的話就随機取[0,1,2,3]即可,此處采用Math.random(),該語句代表随機生成0到1的數,是以将它*4後再取floor即可,由于此處生成的還是浮點型資料,我們采用一個parseInt将其改成整型資料。
範圍取好後,再決定随機取哪個位置。此處我們定義一個變量times來代表循環次數,設定一個步數50步。循環中,我們就采用一個if語句便可以完成,該位置沒有數字就跳出循環,有數字就繼續尋找。如果50步仍然沒有找到空位置,那麼我們就人為地利用兩個for循環來尋找。
随機位置生成後,再随機生成數字。此處我們仍然使用Math.random()語句來完成,如果随機生成的數字小于0.5,就取2,否則就取4.
最後就是将該随機數字放進該随機位置,并且此處我們使用了一個動畫來顯示該數字。代碼如下
function showNumberWithAnimation(i, j, randNumber) {
var numberCell = $("#number_cell_" + i + "_" + j);
numberCell.css({
"background-color": getNumberBackgroundColor(randNumber), //當numberCell從沒有數字變到有數字,或者從一個數字變成另一個數字時,該numberCell的css樣式要發生改變
"color": getNumberColor(randNumber)
});
numberCell.text(randNumber);
numberCell.animate({
"width": "100px",
"height": "100px",
"top": getPosTop(i, j),
"left": getPosLeft(i, j)
}, 50);
}
傳入了3個參數,橫縱坐标值以及該位置的數字。首先定義一個變量numberCell,由于該位置從沒有數字變成有數字時,或者從一個數字變成另一個數字時,css樣式會發生改變,是以此處仍然要定義一波該變量的背景顔色以及數字顔色,直接調用這兩個函數就行,最後就是自定義動畫了,寬高和位置照舊,然後賦予一個50ms的時間來完成該動畫。
至此,generateOneNumber函數完成,再回到newgame函數中,最後還初始化了一個score,将其變為0。newgame函數也完成。這時我們已經完成了整個頁面的布局,效果圖應該是和最開始放的那張圖一樣。
然後我們要編寫的就是操作上下左右的程式了。首先編寫一個鍵盤keydown的事件,代碼如下
$(document).keydown(function (event) {
switch (event.keyCode) { //鍵盤敲擊事件,此處為keyCode不是keycode
case 37: //左
if (moveLeft()) {
setTimeout("generateOneNumber()", 210); //先要判斷是否能夠向左動,向左動了後會随機産生一個數字,并判斷遊戲是否結束
setTimeout("isgameover()", 300);
}
break;
case 38: //上
if (moveTop()) {
setTimeout("generateOneNumber()", 210);
setTimeout("isgameover()", 300);
}
break;
case 39: //右
if (moveRight()) {
setTimeout("generateOneNumber()", 210);
setTimeout("isgameover()", 300);
}
break;
case 40: //下
if (moveDown()) {
setTimeout("generateOneNumber()", 210);
setTimeout("isgameover()", 300);
}
break;
default:
break;
}
})
這裡首先要注意的是keyCode處的C是大寫的,這個bug我當初找了好久…
先看向左動,首先寫了一個if語句,如果可以向左動的話,那麼會随機産生一個數字,并且需要判斷會不會遊戲結束,這個setTimeout我們留到後面一起講。
先來看看moveLeft這個函數,代碼如下
function moveLeft() {
if (canMoveLeft(board)) {
for (var i = 0; i < 4; i++) {
for (var j = 1; j < 4; j++) {
if (board[i][j] != 0) {
for (var k = 0; k < j; k++) {
if (board[i][k] == 0 && noBlockHorizontal(i, k, j, board)) { //尋找可以移動到的位置board[i][k]
showMoveAnimation(i, j, i, k); //添加移動時的動畫
board[i][k] = board[i][j];
board[i][j] = 0;
continue;
} else if (board[i][k] == board[i][j] && noBlockHorizontal(i, k, j, board) && !hasConflicted[i][k]) {
showMoveAnimation(i, j, i, k);
board[i][k] += board[i][j];
board[i][j] = 0;
score += board[i][k]; //分數疊加
updateScore(score); //将分數動态傳回#score
hasConflicted[i][k] = true; //每一次移動後都要将該位置是否存在沖突的屬性設為true
continue;
}
}
}
}
}
setTimeout("updateBoardView()", 200); //由于每一次操作,board都會發生變化,是以每一次操作都要調用updateBoardView()
return true;
} else {
return false;
}
}
首先要判斷能不能向左動,此處我們分析一下,最左邊的那一列格子是肯定不能向左動的,是以我們隻需要周遊右邊的那三列就行,是以定義了一個新函數,代碼如下
function canMoveLeft(board) {
for (var i = 0; i < 4; i++) {
for (var j = 1; j < 4; j++) {
if (board[i][j] != 0) {
if (board[i][j - 1] == 0 || board[i][j - 1] == board[i][j]) { //當board[i][j]處左邊的格子沒有數字或者數字和它自身相等時,該格子可以左移
return true;
}
}
}
}
return false;
}
可以看到先周遊右邊的三列,如果某個位置有數字的話,它什麼情況下可以左移呢?如果它左邊那個格子沒有數字或者數字和它相等的話,是不是就可以左移了,該函數即可完成。
回到moveLeft函數,當可以左移時,我們就需要進行左移的操作了。照樣我們周遊右邊的三列,先找到某個有數字的位置board[i][j],然後它要左移的話是不是得先找到一個格子可以讓它左移,是以此處給出一個for循環來尋找j左邊的所有格子,再來一一判斷。
如果找到了某個格子沒有數字,并且他們倆之間沒有障礙物,此時就可以進行左移的操作了。沒有障礙物的函數代碼如下
function noBlockHorizontal(row, col1, col2, board) {
for (var i = col1 + 1; i < col2; i++) {
if (board[row][i] != 0) {
return false;
}
}
return true;
}
就是尋找某一行的兩列之間有沒有數字為0,周遊一遍就行。再回到moveLeft函數,此處我們給移動的動作添加了一個動畫,代碼如下
function showMoveAnimation(fromx, fromy, tox, toy) {
var numberCell = $("#number_cell_" + fromx + "_" + fromy);
numberCell.animate({
"top": getPosTop(tox, toy),
"left": getPosLeft(tox, toy)
}, 200);
}
就是簡單的用到了四個坐标值而已,然後添加了200ms的時間來完成該動畫。
再回到moveLeft函數,添加完動畫後,将board[i][k]處的數字由0變成board[i][j]的數字,并且board[i][j]自己的數字變為0 。
然後當board[i][k]處的數字等于board[i][j]的數字時,照樣先添加一個動畫,然後board[i][k]處的數字加上board[i][j]的數字,board[i][j]的數字也變為0,分數同時也會疊加,并且分數要動态地傳回score中,是以此處定義了一個函數,代碼如下
function updateScore(score) {
$("#score").text(score);
}
再回到moveLeft函數,hasConflicted後面再講,我們之前講過,每做一次操作就需要調用updateBoardView函數,是以在此處循環完後,調用一次該函數,此處設定了一個200ms的setTimeout,是因為我們在showMoveAnimation函數中給動畫添加的完成時間是200ms,如果我們不給updateBoardView添加一個200ms的延遲的話,就會發現動畫還沒完成,數字就已經顯示出來了。
至此,moveLeft就已經講完了,後面的moveRight,moveTop,moveDown大家可以自己試着寫一下,就不細講了,此處我給出moveRight的代碼
function moveRight() {
if (canMoveRight(board)) {
for (var i = 0; i < 4; i++) {
for (var j = 2; j >= 0; j--) {
if (board[i][j] != 0) {
for (var k = 3; k > j; k--) {
if (board[i][k] == 0 && noBlockHorizontal(i, j, k, board)) {
showMoveAnimation(i, j, i, k);
board[i][k] = board[i][j];
board[i][j] = 0;
continue;
} else if (board[i][k] == board[i][j] && noBlockHorizontal(i, j, k, board) && !hasConflicted[i][k]) {
showMoveAnimation(i, j, i, k);
board[i][k] += board[i][j];
board[i][j] = 0;
score += board[i][k];
updateScore(score);
hasConflicted[i][k] = true;
continue;
}
}
}
}
}
setTimeout("updateBoardView()", 200);
return true;
} else {
return false;
}
}
此處有一個地方稍微需要思考一下,為什麼在第二個for循環處j是從大往小寫,而不是跟moveLeft處一樣從小到大周遊?
答案揭曉!如果此處是從小到大周遊的話,我們玩的時候會發現一個問題,當左邊兩列或者三列有數字時,這時我們按右鍵,會發現最左邊的那一列不會右移,為什麼呢?因為j從0開始循環的話,算法會發現最左邊一列的右邊有數字,無法右移,是以右移的算法應該要先從最右邊開始周遊,等右邊的數字移動完了再移動左邊的。
同理,下面的那個定義k的for循環也就得跟着從大往小變化。
當其他的幾個操作編寫完之後,我們在鍵盤keydown事件中還編寫了一個isgameover函數,此時來定義它,代碼如下
function isgameover() {
if (nospace(board) && nomove(board)) {
alert("game over"); //當沒有空位且不能移動時遊戲結束
}
}
什麼時候會gameover呢?當所有的格子都有數字且都不能移動時,遊戲就結束了,我這裡就簡答地寫了一個彈窗,nomove函數的代碼如下
function nomove(board) {
if (canMoveLeft(board) || canMoveRight(board) || canMoveTop(board) || canMoveDown(board)) {
return false;
}
return true;
}
我們之前已經定義了判斷是否可以左移右移上移下移的函數,這裡直接調用就行。
現在理論上是可以玩了,但是我們玩的時候還會發現一個問題。例如現在某一行數字是這樣的,2,2,4,8,這時我們按左移的話,如果是原版2048,這裡會變成4,4,8,0。但是我們編寫的程式會直接變成16,0,0,0。是以我們得編寫一個功能讓某個格子在一次操作時該格子的數字隻能變化一次。
就是我們之前已經出現過的hasConflicted,當時我是說後面再講,首先我們先定義一個空數組
由于它也是每個格子都有的屬性,是以也得把它變成一個二維數組,這是newgame函數的代碼
// 初始化
function newgame() {
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
var gridCell = $("#grid_cell_" + i + "_" + j); // 定義變量gridCell來編寫每個格子的位置
gridCell.css("top", getPosTop(i, j));
gridCell.css("left", getPosLeft(i, j)); //隻需定義格子的左上角即可
}
}
for (var i = 0; i < 4; i++) {
board[i] = [];
hasConflicted[i] = [];
for (var j = 0; j < 4; j++) {
board[i][j] = 0; //把board定義為二維數組來更友善地表達出每個格子,board為0即這個格子沒有數字,不為0即有數字
hasConflicted[i][j] = false; //初始化時需要将每個格子是否沖突的屬性定義為false
}
}
updateBoardView(); //定義此函數來将數字放到該格子中
generateOneNumber(); //每次初始化遊戲都會随機産生兩個數字
generateOneNumber();
score = 0; //初始化
updateScore(score);
}
初始化當然要将它的屬性設為false,也就是這個格子還沒有變化過,然後我們為了防止上述問題的出現,就得在每次操作時加上一個判定
function moveLeft() {
if (canMoveLeft(board)) {
for (var i = 0; i < 4; i++) {
for (var j = 1; j < 4; j++) {
if (board[i][j] != 0) {
for (var k = 0; k < j; k++) {
if (board[i][k] == 0 && noBlockHorizontal(i, k, j, board)) { //尋找可以移動到的位置board[i][k]
showMoveAnimation(i, j, i, k); //添加移動時的動畫
board[i][k] = board[i][j];
board[i][j] = 0;
continue;
} else if (board[i][k] == board[i][j] && noBlockHorizontal(i, k, j, board) && !hasConflicted[i][k]) {
showMoveAnimation(i, j, i, k);
board[i][k] += board[i][j];
board[i][j] = 0;
score += board[i][k]; //分數疊加
updateScore(score); //将分數動态傳回#score
hasConflicted[i][k] = true; //每一次移動後都要将該位置是否存在沖突的屬性設為true
continue;
}
}
}
}
}
setTimeout("updateBoardView()", 200); //由于每一次操作,board都會發生變化,是以每一次操作都要調用updateBoardView()
return true;
} else {
return false;
}
}
可以看到在else if中最後還加了一個!hasConflicted[i][k],意思是該位置還沒有變化過,可以移動,然後将它的屬性設為true,這時該位置在這一次循環時就已經不能再變化了,這時上述問題就已經解決好了。那麼在這次操作完後在哪将它的屬性由true變回false呢?我們不是調用了一個updateBoardView函數了嘛,在這裡将它設定回去就可以了,一定要記得設定回去,不然下次操作這個位置就不能變化了。
function updateBoardView() {
$(".number_cell").remove(); //由于每一次操作都要調用該函數,是以每次調用之前都要先将上次操作後産生的數字清除
for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
$("#grid_container").append("<div class='number_cell' id='number_cell_" + i + "_" + j + "'>"); //給每個格子添加數字
var theNumberCell = $("#number_cell_" + i + "_" + j); //定義該變量來編寫每個格子中數字的css樣式
if (board[i][j] == 0) {
theNumberCell.css("width", "0"); //當該位置沒有數字時,寬高設為0就行
theNumberCell.css("height", "0");
theNumberCell.css("top", getPosTop(i, j));
theNumberCell.css("left", getPosLeft(i, j));
} else {
theNumberCell.css("width", "100px");
theNumberCell.css("height", "100px");
theNumberCell.css("top", getPosTop(i, j));
theNumberCell.css("left", getPosLeft(i, j));
theNumberCell.css("background-color", getNumberBackgroundColor(board[i][j])); //定義該函數來存放不同數字的背景顔色
theNumberCell.css("color", getNumberColor(board[i][j])); //定義該函數來存放不同數字的顔色
theNumberCell.text(board[i][j]); //将數字值賦予給相應的格子
}
hasConflicted[i][j] = false; //由于每一次操作都要調用該函數,是以每次調用之前都要先将每個格子是否沖突的屬性定義為false
}
}
}
然後我們之前還有個地方沒講,之前講鍵盤keydown事件時還設定了兩個setTimeout
$(document).keydown(function (event) {
switch (event.keyCode) { //鍵盤敲擊事件,此處為keyCode不是keycode
case 37: //左
if (moveLeft()) {
setTimeout("generateOneNumber()", 210); //先要判斷是否能夠向左動,向左動了後會随機産生一個數字,并判斷遊戲是否結束
setTimeout("isgameover()", 300);
}
break;
case 38: //上
if (moveTop()) {
setTimeout("generateOneNumber()", 210);
setTimeout("isgameover()", 300);
}
break;
case 39: //右
if (moveRight()) {
setTimeout("generateOneNumber()", 210);
setTimeout("isgameover()", 300);
}
break;
case 40: //下
if (moveDown()) {
setTimeout("generateOneNumber()", 210);
setTimeout("isgameover()", 300);
}
break;
default:
break;
}
})
這裡不加setTimeout的話,會導緻已經彈窗遊戲結束了,最後一個數字還沒有生成,是以給它加一個時間,分别設定為210ms和300ms,這時就大功告成了。
最後再添加一個傳回上一步Undo的操作。思路就是将每一步的資料(每個格子的值和得分)存入數組中,需要時讀取出來即可。
首先定義兩個空數組
let preValue = [];//存放每一步時每個格子的值
let preScore = [];//存放每一步時的得分
然後記錄下每一步的資料。我是在每一次移動的函數中,在移動操作之前先讀取目前位置的資料,例如下面這段代碼就是在moveLeft函數的開頭
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
preValue.push(board[i][j]);
}
}
preScore.push(score);
然後在moveRight(),moveDown()和moveTop()的開頭都加上這一段。之後就是書寫undo函數。由于資料是倒着進數組的,是以讀取時也要倒着讀。其實這就是一個棧,後進先出。
function undo() {
let obj = {};
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (obj[board[i][j]]) {
obj[board[i][j]]++;
} else {
obj[board[i][j]] = 1;
}
}
}
for (let key in obj) {
if (key == 0 && obj[key] == 14) { //當場上隻有兩個格子有數字即newgame時,按下undo無反應
return false;
}
}
for (let i = 3; i >= 0; i--) {
for (let j = 3; j >= 0; j--) {
board[i][j] = preValue.pop();
}
}
score = preScore.pop();
updateBoardView();
updateScore(score);
}
至此所有功能已經書寫完。
代碼已經上傳到gitee上了,請自取:
https://gitee.com/azsx582/game-2048.git
第一次寫這麼長的部落格,廢話有點多,觀衆老爺們湊合着看吧。有錯誤的話歡迎大家指出來,共同進步!