Я написал игру 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);
Любые отзывы приветствуются. В частности, хотелось бы знать:
- Идиоматично ли в объектно-ориентированном стиле использовать статические методы для хранения служебных функций, таких как
zipперевернуть доску по диагонали. - Есть ли лучший способ структурировать класс? Мне кажется, что я не очень хорошо разбираюсь в логике различных действий.
- Есть ли какой-то конкретный шаблон проектирования, который я могу использовать для улучшения класса?
- Наконец, я использую
symbolчтобы представить направление движения, которое пользователь может сделать, и прикрепить их как переменную экземпляра к классу. Опять же, я не уверен, является ли это идиоматикой в объектно ориентированном стиле, поскольку я новичок в этой парадигме.
