hive-game-core
Version:
Various stuff implementing the game and rules of Hive, the strategy game
162 lines (145 loc) • 4.89 kB
JavaScript
const _ = require('lodash')
const hashes = require('jshashes')
const uuid = require('uuid/v4')
const { Board } = require('./Board')
const { Move, MoveValidationError } = require('./Move')
class Game {
constructor(whitePlayer, blackPlayer, opts={}) {
this._id = uuid()
this._hasher = new hashes.SHA256()
this._board = new Board(opts)
this._players = [whitePlayer, blackPlayer]
this._playersBadMoveCount = [0, 0]
this.whitePlayer.white = true
this.blackPlayer.white = false
this._turn = 1
this._beginHistory()
this._logFn = opts.logFn
this._log(`Finished initialising game ${this.id}`)
this._lastChange = new Date()
this._timedOut = null
}
get id() { return this._id }
get whitePlayer() { return this._players[0] }
get blackPlayer() { return this._players[1] }
get currentPlayer() { return this._getPlayerByTurn(this.turn) }
get turn() { return this._turn }
get gameOver() { return this.winner !== null }
get winner() {
if (this._timedOut) {
return (this._timedOut.white ? this.blackPlayer : this.whitePlayer)
}
for (const white of [true, false]) {
const queen = this.board.getQueen(white)
if (!queen) { continue }
const occupiedQueenSpaces = _.filter(
this.board.getAdjacentCells(_.slice(queen.coords3, 0, 2)),
cell => cell.length > 0
).length
if (occupiedQueenSpaces >= 6) {
return (white ? this.blackPlayer : this.whitePlayer)
}
}
return null
}
get timeSinceLastChange() {
return (new Date() - this._lastChange)
}
get board() { return this._board }
get history() { return this._history }
get historyString() { return this.history.join('/') }
get state() {
const state = {
board: this.board.state,
players: _.map(this._players, p => p.state),
turn: JSON.parse(JSON.stringify(this._turn))
}
if (this.gameOver) {
state.gameOver = true
state.winner = JSON.parse(JSON.stringify(this.winner.id))
} else {
state.toMove = JSON.parse(JSON.stringify(this.currentPlayer.id))
}
const hash = this._hasher.hex(JSON.stringify(state))
return { state, hash }
}
async begin() {
this._lastChange = new Date()
try {
this._log(`Beginning game loop`)
while (!this.gameOver) {
this._log(`---`)
this._log(`Beginning turn ${this.turn}`)
this._log(`Player ${this.currentPlayer} to move`)
let move = null
while (!move) {
move = await this.currentPlayer.move(this)
this._lastChange = new Date()
try {
this._validateMove(move)
} catch (e) {
this._log(`Move ${move} is not valid (${e}), requesting another`)
const white = (((this.turn - 1) % 2) === 0)
move = null
this._playersBadMoveCount = [
this._playersBadMoveCount[0] + (white ? 1 : 0),
this._playersBadMoveCount[1] + (white ? 0 : 1),
]
if (this._playersBadMoveCount[white ? 0 : 1] >= 10) {
this._log(`10 bad moves by ${white ? 'white' : 'black'}, they lose`)
return await this._endGame(white ? this.blackPlayer : this.whitePlayer)
}
}
}
this._log(`Received move [ ${move} ] from ${this.currentPlayer}`)
this._applyMove(move)
this._lastChange = new Date()
this._turn++
}
return await this._endGame(this.winner)
} catch(e) {
this._log(`Unhandled exception in game loop`)
console.error(e)
}
}
finishDueToTimeout() {
this._timedOut = this._getPlayerByTurn(this.turn)
}
getPlayerById(id) {
return _.find(this._players, p => p.id === id) || null
}
_applyMove(moveString) {
this._history.push(moveString)
const move = this.board.applyMove(moveString, this.turn)
// Remove piece from player
if (move.isPlacement) {
this.currentPlayer.removePiece(move.piece)
}
}
_beginHistory() {
this._history = []
this._history.push(this.whitePlayer.piecesString)
this._history.push(this.blackPlayer.piecesString)
}
async _endGame(winner) {
this._log(`Game won by ${winner}`)
this._lastChange = new Date()
}
_getPlayerByTurn(turn) {
return this._players[(turn - 1) % 2]
}
_log(s) {
if (!this._logFn) { return }
this._logFn(`Hive [Game:${this.id}] : ${s}`)
}
_validateMove(newMoveString) {
// Do this by creating a fresh game and applying all moves in history to
// it, followed by this new one - boards do validation
const move = new Move(newMoveString)
if (move.isPlacement && !_.includes(this.currentPlayer.pieces, move.piece)) {
throw new MoveValidationError('NO_SUCH_PIECE_IN_HAND')
}
this.board.validateMove(move, this.turn)
}
}
module.exports = { Game }