首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >JavaScript OOD: 2048

JavaScript OOD: 2048
EN

Code Review用户
提问于 2021-03-03 19:00:18
回答 1查看 581关注 0票数 10

我用JavaScript编写了一个带有面向对象范例的2048年游戏。游戏板上有一个二维数组,每个平铺都有一个整数.

以下是实现:

代码语言:javascript
复制
class Game {
  SIZE = 4
  constructor() {
    this.board = Array.from({ length: this.SIZE * this.SIZE }, () => 0).reduce(
      (arrays, curr) => {
        const lastArray = arrays[arrays.length - 1]
        if (lastArray.length < this.SIZE) lastArray.push(curr)
        else arrays.push([curr])
        return arrays
      },
      [[]]
    )
    this.isWin = false
    this._init()
  }

  _init() {
    const pickedTiles = this._randomlyPick(2)
    for (const [row, col] of pickedTiles) {
      this.board[row][col] = Game.generateTile()
    }
  }

  static generateTile() {
    if (Math.random() > 0.5) return 2
    return 4
  }

  _getEmptyTiles() {
    const emptyTiles = []
    for (let row = 0; row < this.SIZE; row++) {
      for (let col = 0; col < this.SIZE; col++) {
        if (this.board[row][col] === 0) emptyTiles.push([col, row])
      }
    }
    return emptyTiles
  }

  _randomlyPick(numOfItems) {
    const emptyTiles = this._getEmptyTiles()
    for (let i = 0; i < numOfItems; i++) {
      const toSwap = i + Math.floor(Math.random() * (emptyTiles.length - i))
      ;[emptyTiles[i], emptyTiles[toSwap]] = [emptyTiles[toSwap], emptyTiles[i]]
    }
    return emptyTiles.slice(0, numOfItems)
  }

  spawn() {
    // randomly spawn empty tiles with 2 or 4
    const [emtpyTile] = this._randomlyPick(1)
    this.board[emtpyTile[0]][emtpyTile[1]] = Game.generateTile()
  }

  play(dir) {
    if (this.canPlay()) {
      switch (dir) {
        case Game.moveUp:
          this._mergeUp()
          break
        case Game.moveRight:
          this._mergeRight()
          break
        case Game.moveLeft:
          this._mergeLeft()
          break
        case Game.moveDown:
          this._mergeDown()
          break
      }
      this.spawn()

      return true
    }
    return false
  }
  checkIsWin() {
    return this.isWin
  }

  static peek(array) {
    return array[array.length - 1]
  }

  static zip(arrays) {
    const result = []
    for (let i = 0; i < arrays[0].length; ++i) {
      result.push(arrays.map((array) => array[i]))
    }
    return result
  }

  _mergeRowRight(sparseRow) {
    const row = sparseRow.filter((x) => x !== 0)
    const result = []
    while (row.length) {
      let value = row.pop()
      if (Game.peek(row) === value) value += row.pop()
      result.unshift(value)
    }

    while (result.length < 4) result.unshift(0)
    return result
  }

  _mergeRowLeft(row) {
    return this._mergeRowRight([...row].reverse()).reverse()
  }

  _mergeUp() {
    this.board = Game.zip(Game.zip(this.board).map(row => this._mergeRowLeft(row)))
  }
  _mergeDown() {
    this.board = Game.zip(Game.zip(this.board).map(row => this._mergeRight(row)))
  }

  _mergeRight() {
    this.board = this.board.map((row) => this._mergeRowRight(row))
  }
  _mergeLeft() {
    this.board = this.board.map((row) => this._mergeRowLeft(row))
  }

  canPlay() {
    const dirs = [
      [0, 1],
      [1, 0],
      [-1, 0],
      [0, -1],
    ]
    const visited = new Set()
    for (let row = 0; row < this.SIZE; row++) {
      for (let col = 0; col < this.SIZE; col++) {
        if (visited.has([row, col].toString())) continue
        const tile = this.board[row][col]
        if (tile === 2048) {
          this.isWin = true
          return false
        }
        if (tile === 0) return true
        for (const [dx, dy] of dirs) {
          if (this.board[row + dx]?.[col + dy] === tile) return true
        }
        visited.add([row, col].toString())
      }
    }
    return false
  }
}

Game.moveUp = Symbol('moveUp')
Game.moveDown = Symbol('moveUp')
Game.moveLeft = Symbol('moveUp')
Game.moveRight = Symbol('moveUp')

const game = new Game()
console.log(game.board);
game.play(Game.moveUp)
console.log(game.board);
game.play(Game.moveRight)
console.log(game.board);

欢迎任何反馈意见。具体来说,我想知道:

  1. 在OO中,使用静态方法存储诸如zip这样的使用函数来沿对角线翻转板是否是惯用的?
  2. 有更好的方法来组织这个班吗?我觉得我在划分不同行为的逻辑方面做得不太好。
  3. 有什么特定的设计模式可以用来改进这个类吗?
  4. 最后,我使用symbol表示用户可以进行的移动的方向,并将它们作为实例变量附加到类中。同样,我也不确定这在OO中是否是惯用的,因为我对这一范式有点陌生。
EN

回答 1

Code Review用户

发布于 2021-03-09 14:57:32

符号

关于符号的使用的快速回答。

“最后,我使用符号来表示用户可以进行的移动的方向,并将它们作为实例变量附加到类中。同样,我不确定这在OO中是否是惯用的,因为我对这个范例有点陌生。”

创建符号有两个原因

  1. 为了保证唯一性,Symbol("foo") === Symbol("foo");总是错误的。
  2. 若要在独立作用域之间共享唯一值,请执行以下操作。Symbol.for("foo") === Symbol.for("foo");在全球范围内是正确的。在使用模块时很有用。

Pros

见上文

Cons

  • 符号的开销比访问数字慢5%左右,所以在性能非常关键的应用程序中,应该避免使用符号。
  • 创建一个新符号Symbol()比创建唯一数字i++慢2个数量级。
  • 符号不能序列化。eg JSON.stringify({a: symbol("foo")})创建"{}"
  • 由于全局符号Symbol.for的性质,因此创建的每个符号表示一个不可恢复的内存赋值约50字节加上每个符号的名称长度(Unicode)。这同样适用于范围符号,但是,当范围被取消引用时,可以恢复内存。必须注意不要在循环中创建符号。

我提到这些缺点,虽然它们在这种情况下不适用,但相反,使用它们可能成为习惯,如果不知道问题,就很容易被发现。

回答问题

你对符号的使用习惯吗?

不是的。对唯一性的要求并不能保证增加复杂性。使用一组数字就可以很容易地实现识别移动的需要。

代码语言:javascript
复制
Game.moveUp = 0;
Game.moveDown = 1;
Game.moveLeft = 2;
Game.moveRight = 3;

不存在冲突的可能性,因为唯一性域仅限于这4个实体。

编码的第一条规则。在一切条件相同的情况下,最简单的解决办法是最好的。

BTW

名字写得不好。

如果您找到一组变量名,它们都以相同的名称/前缀开头,这是一个符号,表示它应该是一个对象。

使用块作用域计数器的示例枚举。

代码语言:javascript
复制
{
    let i = 0;
    Game.moves = {
        up: i++,
        down: i++,
        left: i++,
        right: i++,
    };
}

或者更好的常量

代码语言:javascript
复制
{
    let i = 0;
    Game.moves = Object.freeze({
        UP: i++,
        DOWN: i++,
        LEFT: i++,
        RIGHT: i++,
    });
}
票数 6
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/256691

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档