360前端星技术笔试中的一道题目,现把解决过程记录如下:
1、首先是界面
2048
CSS中设置了4个类,当方格内的数字分别为个位、十位、百位、千位(最大值2048)时,通过添加不同的类,让数字能完整的放置在方格内。
.grid-container {
margin: auto;
width: 400px;
height: 400px;
background-color: rgb(220, 160, 20);
}
.grid-row {
display: block;
padding-left: 8px;
}
.grid-row:first-child {
padding-top: 8px;
}
.grid-cell {
/* display: inline-block; */
position: relative;
float: left;
margin: 7px;
width: 82px;
height: 82px;
background-color: rgb(230, 210, 170);
text-align: center;
}
.numOne {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 35%;
font-size: 48px;
}
.numTwo {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 16%;
font-size: 48px;
}
.numThree {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 9%;
font-size: 38px;
}
.numFour {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 9%;
font-size: 28px;
}
2、存放游戏状态
let gameState = [
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0]
];
通过数组存放4乘4方格内的数字,0代表方格内数字为空。
3、初始化
(function () {
const initBlockCount = 3;
let initCount = 0;
while (initCount < 2) {
let randIndex = Math.floor(Math.random()*9);
if (gameState[parseInt(randIndex/3)][randIndex%3] === 0) {
let randNum = Math.random() > 0.5 ? 2 : 4;
gameState[parseInt(randIndex/3)][randIndex%3] = randNum;
initCount++;
}
}
updateUI();
})();
随机将4乘4方格中的某2个方格的值设为2或4。
4、监听按键
$('body').on('keydown', function (e) {
switch (e.which) {
case 37: //左
case 65:
left();
break;
case 38: //上
case 87:
up();
break;
case 39: //右
case 68:
right();
break;
case 40: //下
case 83:
down();
break;
}
});
监听上下左右及WSAD键按下。
5、改变游戏状态
以按下向左或A键为例:
function left () {
for (let i = 0; i < 4; i++) {
let zeroIndex = -1;
for (let j = 0; j < 4; j++) {
if (zeroIndex === -1) {
if (gameState[i][j] === 0) {
zeroIndex = j;
}
}else{
if (gameState[i][j] !== 0) {
gameState[i][zeroIndex] = gameState[i][j];
gameState[i][j] = 0;
zeroIndex++;
}
}
}
}
mergeLeft();
initRight();
updateUI();
}
首先每一行从左向右遍历,找到第一个为0的位置(i, zeroIndex),然后找到zeroIndex后第一个不为0的位置(i, j),将(i, zeroIndex)的值替换为(i, j)位置的值,并将(i, j)位置的值归0,之后继续将后面非0的元素插入到zeroIndex+n位置(n是已插入元素数)。这一部分相当于将每一行的元素“左对齐”。
之后需要检查每一行“对齐”后的数列中最后两元素是否相同,相同则合并,这部分放在mergeLeft()中。
function mergeLeft() {
for (let i = 0; i < 4; i++) {
let zeroIndex = gameState[i].indexOf(0)===-1 ? 4 : gameState[i].indexOf(0);
if (zeroIndex > 1 && gameState[i][zeroIndex-1] === gameState[i][zeroIndex-2]) {
gameState[i][zeroIndex-1] = 0;
gameState[i][zeroIndex-2] *= 2;
}
}
}
之后要从最右边一列随机生成1-2个新元素。当最右边一列被占满无法生成新元素时,则判定游戏结束。并且为避免每次都在第一位生成新元素,适当调低一些生成概率。
function initRight() {
let nullBlock = 0;
for (let i = 0; i < 4; i++) {
if (gameState[i][3] === 0) {
if (Math.random() > 0.3 && nullBlock < 2) { //调低一点,避免都产生在第一位
gameState[i][3] = Math.random() > 0.5 ? 2 : 4;
nullBlock++;
}
}
}
if (nullBlock === 0) {
gameOver();
}
}
向上,向右,向下的代码部分如下。
function up() {
for (let j = 0; j < 4; j++) {
let zeroIndex = -1;
for (let i = 0; i < 4; i++) {
if (zeroIndex === -1) {
if (gameState[i][j] === 0) {
zeroIndex = i;
}
}else{
if (gameState[i][j] !== 0) {
gameState[zeroIndex][j] = gameState[i][j];
gameState[i][j] = 0;
zeroIndex++;
}
}
}
}
mergeUp();
initDown();
updateUI();
}
function mergeUp() {
for (let j = 0; j < 4; j++) {
for (let i = 0; i < 4; i++) {
if (gameState[i][j] === 0) {
if (i > 1 && gameState[i-1][j] === gameState[i-2][j]) {
gameState[i-1][j] = 0;
gameState[i-2][j] *= 2;
}
break;
}else if (i === 3) {
if (gameState[3][j] === gameState[2][j]) {
gameState[3][j] = 0;
gameState[2][j] *= 2;
}
}
}
}
}
function initDown() {
let nullBlock = 0;
for (let i = 0; i < 4; i++) {
if (gameState[3][i] === 0) {
if (Math.random() > 0.3 && nullBlock < 2) {
gameState[3][i] = Math.random() > 0.5 ? 2 : 4;
nullBlock++;
}
}
}
if (nullBlock === 0) {
gameOver();
}
}
function right () {
for (let i = 0; i < 4; i++) {
let zeroIndex = -1;
for (let j = 3; j >= 0; j--) {
if (zeroIndex === -1) {
if (gameState[i][j] === 0) {
zeroIndex = j;
}
}else{
if (gameState[i][j] !== 0) {
gameState[i][zeroIndex] = gameState[i][j];
gameState[i][j] = 0;
zeroIndex--;
}
}
}
}
mergeRight();
initLeft();
updateUI();
}
function mergeRight() {
for (let i = 0; i < 4; i++) {
let zeroIndex = -1;
for (let j = 3; j >= 0; j--) {
if (gameState[i][j] === 0) {
zeroIndex = j;
break;
}
}
if (zeroIndex < 2 && gameState[i][zeroIndex+1] === gameState[i][zeroIndex+2]) {
gameState[i][zeroIndex+1] = 0;
gameState[i][zeroIndex+2] *= 2;
}
}
}
function initLeft() {
let nullBlock = 0;
for (let i = 0; i < 4; i++) {
if (gameState[i][0] === 0) {
if (Math.random() > 0.3 && nullBlock < 2) { //调低一点,避免都产生在第一位
gameState[i][0] = Math.random() > 0.5 ? 2 : 4;
nullBlock++;
}
}
}
if (nullBlock === 0) {
gameOver();
}
}
function down() {
for (let j = 0; j < 4; j++) {
let zeroIndex = -1;
for (let i = 3; i >= 0; i--) {
if (zeroIndex === -1) {
if (gameState[i][j] === 0) {
zeroIndex = i;
}
}else{
if (gameState[i][j] !== 0) {
gameState[zeroIndex][j] = gameState[i][j];
gameState[i][j] = 0;
zeroIndex--;
}
}
}
}
mergeDown();
initUp();
updateUI();
}
function mergeDown() {
for (let j = 0; j < 4; j++) {
for (let i = 3; i >= 0; i--) {
if (gameState[i][j] === 0) {
if (i < 2 && gameState[i+1][j] === gameState[i+2][j]) {
gameState[i+1][j] = 0;
gameState[i+2][j] *= 2;
}
break;
}else if (i === 0) {
if (gameState[1][j] === gameState[0][j]) {
gameState[1][j] = 0;
gameState[0][j] *= 2;
}
}
}
}
}
function initUp() {
let nullBlock = 0;
for (let i = 0; i < 4; i++) {
if (gameState[0][i] === 0) {
if (Math.random() > 0.3 && nullBlock < 2) {
gameState[0][i] = Math.random() > 0.5 ? 2 : 4;
nullBlock++;
}
}
}
if (nullBlock === 0) {
gameOver();
}
}
6、更新游戏界面
根据游戏状态数组更新界面。
function updateUI() {
for (let i = 0; i < 4; i++) {
for (let j = 0; j< 4; j++) {
if (gameState[i][j] !== 0) {
let id = '#num' + i + j;
$(id).removeClass();
if (gameState[i][j]/1000 > 1) {
$(id).addClass('numFout');
}else if (gameState[i][j]/100 > 1) {
$(id).addClass('numThree');
}else if (gameState[i][j]/10 > 1) {
$(id).addClass('numTwo');
}else{
$(id).addClass('numOne');
}
$(id).text(gameState[i][j]);
}else{
let id = '#num' + i + j;
$(id).text('');
}
}
}
}
7、合并逻辑的BUG
之前只考虑到空元素前两个元素需要合并,未考虑到游戏进行过程中应该是任意与按键相同方向的两相同元素都可以合并。例如下图第二行第一个元素(1, 0)与第三行第一个元素(2, 0)均为16,若此时按下向上或向下键后,两位置元素应合并。
改进: