关于”2048“那些事儿

205 阅读5分钟

在一段没有啥活的过程中,思考了一下不然做个摸鱼2048呗。

说干就干! 那么怎么开始呢?哦,2048是什么?

游戏规则:

1.滑动手指移动所有的方块

2.当两个数字相同的方块相遇时,就合并为两数之合

3.当格子内填满数字游戏就失败了

4.当数字合并成2048时,游戏胜利

OKOK 了解到2048游戏规则,接下来具体怎么实现呢? 那下面就是我思考的方向,我们先捋一下思路哈

基本思路:

HTML:构建2048基本html结构,包括格子和对应的样式 ,使用jq获取游戏区域的dom

数据模型:在jq中,初始化游戏界面和数据模型,设置监听器,相应玩家移动操作

移动逻辑:通过键盘使劲按(在移动端,无法通过鼠标事件,使用移动端的触摸事件来替代鼠标事件)监听玩家操作,然后根据玩家输入,更新数据模型和游戏界面。使用lodash数据库处理数组操作,如过滤、映射和合并等

判断胜负:在每次移动后,判断游戏是否胜利或失败,根据游戏状态显示对应的提示信息。

动画效果:使用jQuery的动画函数,为移动过程和方块合并添加动画效果,提升游戏的交互体验

好的那么综上我们把大致的思路捋清楚了,也确定了使用原生jq和lodash写

那么第一步:构建2048基本html结构

      <h1>2048 Game</h1>
      <div class="restart">新游戏</div>
      <div class="game-area">
        <!-- 游戏区域,包含16个格子 -->
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <div class="grid-cell"></div>
        <!-- 其他格子... -->
      </div>
      <div class="score">Score: <span id="score">0</span></div>
      <div id="bot-mode">机器人模式</div>

      <!-- <div class="game-over">Game Over!</div> -->
      <!-- 其他游戏提示信息等... -->
    </div>

第二步:数据模型

首先进入游戏需要初始化游戏状态

// 初始化游戏状态
let gameBoard = Array(4).fill(null).map(() => Array(4).fill(0))  // 4x4的游戏板
let score = 0
let gameOver = false
let robot = false

// 开始游戏
function startGame () {
  initializeBoard()
  drawBoard()
}
// 初始化游戏板
function initializeBoard () {
  // 清空游戏板
  gameBoard = Array(4).fill(null).map(() => Array(4).fill(0))
  // 随机生成两个初始数字方块
  generateRandomTile()
  generateRandomTile()
}
// 随机在空白格子生成一个数字方块(2或4)
function generateRandomTile () {
  const emptyCells = []
  _.forEach(gameBoard, (rowArray, row) => {
    _.forEach(rowArray, (cell, col) => {
      if (cell === 0) {
        emptyCells.push({ row, col })
      }
    })
  })
  if (emptyCells.length > 0) {
    const randomCell = _.sample(emptyCells)
    gameBoard[randomCell.row][randomCell.col] = _.random(0.9) ? 2 : 4
  }
}
// 绘制游戏板
function drawBoard () {
  const gridCells = document.querySelectorAll(".grid-cell")
  _.forEach(gameBoard, (rowArray, row) => {
    _.forEach(rowArray, (cell, col) => {
      const cellIndex = row * 4 + col
      const cellElement = gridCells[cellIndex]
      cellElement.textContent = cell === 0 ? "" : cell
      cellElement.className = "grid-cell tile-" + cell
    })
  })
  $("#score").text(score)
}

当游戏的初始页面及随机数相对应生成后,需要如何执行对应的游戏初始化逻辑

$(document).ready(() => {
  startGame()
  // 添加机器人模式按钮点击事件
  $("#bot-mode").on("click", () => {
    playAutomatically()
  })
  //重新开始新游戏的点击事件
  $(".restart").on("click", () => {
    restartGame()
  })
  $(".game-area").on("touchstart", (event) => {
    startX = event.originalEvent.touches[0].clientX
    startY = event.originalEvent.touches[0].clientY
  })

  $(".game-area").on("touchend", (event) => {
    endX = event.originalEvent.changedTouches[0].clientX
    endY = event.originalEvent.changedTouches[0].clientY

    const deltaX = endX - startX
    const deltaY = endY - startY
    const sensitivity = 50

    if (Math.abs(deltaX) > sensitivity || Math.abs(deltaY) > sensitivity) {
      if (Math.abs(deltaX) > Math.abs(deltaY)) {
        if (deltaX > 0) {
          move("right")
        } else {
          move("left")
        }
      } else {
        if (deltaY > 0) {
          move("down")
        } else {
          move("up")
        }
      }
    }
  })
})

接下来好好思考一下移动的逻辑应该怎么写

向上移动逻辑: 从上往下遍历每一列,从第二行开始。 如果格子不为空,则将格子向上移动,直到遇到边界或者遇到不为空的格子。 如果能够合并的格子相邻且数值相等,则将其合并并计分。 根据移动和合并的结果,更新moved变量的值。

向下移动逻辑: 从下往上遍历每一列,从倒数第二行开始。 如果格子不为空,则将格子向下移动,直到遇到边界或者遇到不为空的格子。 如果能够合并的格子相邻且数值相等,则将其合并并计分。 根据移动和合并的结果,更新moved变量的值。

向左移动逻辑: 从左往右遍历每一行,从第二列开始。 如果格子不为空,则将格子向左移动,直到遇到边界或者遇到不为空的格子。 如果能够合并的格子相邻且数值相等,则将其合并并计分。 根据移动和合并的结果,更新moved变量的值。

向右移动逻辑: 从右往左遍历每一行,从倒数第二列开始。 如果格子不为空,则将格子向右移动,直到遇到边界或者遇到不为空的格子。 如果能够合并的格子相邻且数值相等,则将其合并并计分。 根据移动和合并的结果,更新moved变量的值。 判断游戏是否胜利或失败的逻辑和处理需要根据游戏规则和具体实现进行编写。

大致的移动逻辑就是这样啦,在移动逻辑中加上判断游戏胜负的步骤,那么我们将代码复现

// 处理移动操作
function move (direction) {
  console.log("Moving " + direction)
  if (gameOver) return

  let moved = false // 用于判断是否有格子移动
  // 保存当前棋盘状态,以便比较是否发生了移动
  const prevBoard = _.cloneDeep(gameBoard)
  // 根据不同的方向进行遍历和移动操作
  switch (direction) {
    case "up":
      for (let col = 0; col < 4; col++) {
        for (let row = 1; row < 4; row++) {
          if (gameBoard[row][col] !== 0) {
            let newRow = row
            while (newRow > 0 && gameBoard[newRow - 1][col] === 0) {
              gameBoard[newRow - 1][col] = gameBoard[newRow][col]
              gameBoard[newRow][col] = 0
              newRow--
              moved = true
            }
            if (newRow > 0 && gameBoard[newRow - 1][col] === gameBoard[newRow][col]) {
              gameBoard[newRow - 1][col] *= 2
              gameBoard[newRow][col] = 0
              score += gameBoard[newRow - 1][col] // 计分
              moved = true
            }
          }
        }
      }
      break
    case "down":
      for (let col = 0; col < 4; col++) {
        for (let row = 2; row >= 0; row--) {
          if (gameBoard[row][col] !== 0) {
            let newRow = row
            while (newRow < 3 && gameBoard[newRow + 1][col] === 0) {
              gameBoard[newRow + 1][col] = gameBoard[newRow][col]
              gameBoard[newRow][col] = 0
              newRow++
              moved = true
            }
            if (newRow < 3 && gameBoard[newRow + 1][col] === gameBoard[newRow][col]) {
              gameBoard[newRow + 1][col] *= 2
              gameBoard[newRow][col] = 0
              score += gameBoard[newRow + 1][col] // 计分
              moved = true
            }
          }
        }
      }
      break
    case "left":
      for (let row = 0; row < 4; row++) {
        for (let col = 1; col < 4; col++) {
          if (gameBoard[row][col] !== 0) {
            let newCol = col
            while (newCol > 0 && gameBoard[row][newCol - 1] === 0) {
              gameBoard[row][newCol - 1] = gameBoard[row][newCol]
              gameBoard[row][newCol] = 0
              newCol--
              moved = true
            }
            if (newCol > 0 && gameBoard[row][newCol - 1] === gameBoard[row][newCol]) {
              gameBoard[row][newCol - 1] *= 2
              gameBoard[row][newCol] = 0
              score += gameBoard[row][newCol - 1] // 计分
              moved = true
            }
          }
        }
      }
      break
    case "right":
      for (let row = 0; row < 4; row++) {
        for (let col = 2; col >= 0; col--) {
          if (gameBoard[row][col] !== 0) {
            let newCol = col
            while (newCol < 3 && gameBoard[row][newCol + 1] === 0) {
              gameBoard[row][newCol + 1] = gameBoard[row][newCol]
              gameBoard[row][newCol] = 0
              newCol++
              moved = true
            }
            if (newCol < 3 && gameBoard[row][newCol + 1] === gameBoard[row][newCol]) {
              gameBoard[row][newCol + 1] *= 2
              gameBoard[row][newCol] = 0
              score += gameBoard[row][newCol + 1] // 计分
              moved = true
            }
          }
        }
      }
      break
    default:
      break
  }
  // 如果有格子移动,则生成新的随机数字方块
  if (moved) {
    generateRandomTile()
    drawBoard()

    // 检查游戏是否胜利或失败
    console.log("oo", robot)
    if (robot == false && isGameWon()) {
      // 处理游戏胜利逻辑
      alert("赢啦!")
      gameOver = true
    } else if (robot == false && isGameLost()) {
      // 处理游戏失败逻辑
      const restart = confirm("游戏结束,是否重新开始")
      if (restart) {
        restartGame()
      } else {
        gameOver = true
      }
    }
  }
}

// 检查游戏是否获胜
function isGameWon () {
  return _.some(gameBoard, rowArray => _.some(rowArray, cell => cell >= 2048))
}
// 检查游戏是否失败
function isGameLost () {
  return (
    !_.some(gameBoard, rowArray => _.includes(rowArray, 0)) &&
    _.every(gameBoard, (rowArray, row) => {
      return (
        _.every(rowArray, (cell, col) => {
          return (
            (col === 0 || cell !== rowArray[col - 1]) && // 不与左侧相等
            (col === 3 || cell !== rowArray[col + 1]) && // 不与右侧相等
            (row === 0 || cell !== gameBoard[row - 1][col]) && // 不与上方相等
            (row === 3 || cell !== gameBoard[row + 1][col]) // 不与下方相等
          )
        })
      )
    })
  )
}

其次关于游戏动画,可以通过函数形式,分别将每个颜色对应其对应的数字块上

function getBackgroundColor (num) {
  switch (num) {
    case 2: return "#eee4da"; case 4: return "#eee0c8"; case 8: return '#f2b179'; case 16: return '#f59563'; case 32: return '#f67c5f'; case 64: return '#f65e3b'; case 128: return '#edcf72'; case 256: return '#edcc61'; case 512: return '#9c0'; case 1024: return '#33b5e5'; case 2048: return '#09c'
  }
  return "#fbf8cd"
}
//在绘制游戏板中添加
const backgroundColor = getBackgroundColor(cell)
cellElement.style.backgroundColor = backgroundColor

那么一个简单的2048就完成啦!最后大家可以思考一下如何能够通过AI达成一个必胜策略呢?