clovers-reversi
Version:
Reversi game logic for use in clovers.network
681 lines (630 loc) • 19 kB
JavaScript
import BN from 'bn.js'
class Reversi {
constructor(startVal) {
this.STARTMOVE = [2, 3] // C4
this.BOARDDIM = 8
this.EMPTY = 3
this.BLACK = 1
this.WHITE = 2
this.clearAttrs()
if (startVal) {
Object.assign(this, startVal)
}
}
clearAttrs() {
this.error = false
this.complete = false
this.symmetrical = false
this.RotSym = false
this.Y0Sym = false
this.X0Sym = false
this.XYSym = false
this.XnYSym = false
this.currentPlayer = this.BLACK
this.blackScore = 0
this.whiteScore = 0
// this.board is an array of columns, visually the board should be arranged by arrays of rows
this.board = new Array(this.BOARDDIM)
.fill(0)
.map(c => new Array(this.BOARDDIM).fill(this.EMPTY))
this.rowBoard = new Array(this.BOARDDIM)
.fill(0)
.map(c => new Array(this.BOARDDIM).fill(this.EMPTY))
this.visualBoard = []
this.board[this.BOARDDIM / 2 - 1][this.BOARDDIM / 2 - 1] = this.WHITE
this.board[this.BOARDDIM / 2][this.BOARDDIM / 2] = this.WHITE
this.board[this.BOARDDIM / 2 - 1][this.BOARDDIM / 2] = this.BLACK
this.board[this.BOARDDIM / 2][this.BOARDDIM / 2 - 1] = this.BLACK
this.rowBoard[this.BOARDDIM / 2 - 1][this.BOARDDIM / 2 - 1] = this.BLACK
this.rowBoard[this.BOARDDIM / 2][this.BOARDDIM / 2] = this.BLACK
this.rowBoard[this.BOARDDIM / 2 - 1][this.BOARDDIM / 2] = this.WHITE
this.rowBoard[this.BOARDDIM / 2][this.BOARDDIM / 2 - 1] = this.WHITE
this.moves = []
this.movesString = ''
this.byteBoard = ''
this.byteFirst32Moves = ''
this.byteLastMoves = ''
this.moveKey = 0
this.msg = ''
}
thisMovesToByteMoves(moves = this.moves) {
moves = this.stringMovesToBinaryMoves(moves.join('')).match(/.{1,224}/g)
let foo = new BN(moves[0], 2)
let bar = new BN(moves[1], 2)
this.byteFirst32Moves = foo.toString(16)
this.byteLastMoves = bar.toString(16)
}
thisBoardToByteBoard() {
this.byteBoard = this.colArrayBoardToByteBoard(this.board)
}
makeMove(move) {
let col = move[0]
let row = move[1]
if (this.board[col][row] !== this.EMPTY) {
this.error = true
this.msg = 'Invalid Game (square is already occupied)'
return
}
let possibleDirections = this.possibleDirections(col, row)
if (possibleDirections.length === 0) {
this.error = true
this.msg = 'Invalid Game (doesnt border other tiles)'
return
}
let flipped = false
for (let i = 0; i < possibleDirections.length; i++) {
let possibleDirection = possibleDirections[i]
let flips = this.traverseDirection(possibleDirection, col, row)
for (let j = 0; j < flips.length; j++) {
flipped = true
this.board[flips[j][0]][flips[j][1]] = this.currentPlayer
this.rowBoard[flips[j][1]][flips[j][0]] = this.currentPlayer
}
}
if (flipped) {
this.board[col][row] = this.currentPlayer
this.rowBoard[row][col] = this.currentPlayer
} else {
this.error = true
this.msg = 'Invalid Game (doesnt flip any other tiles)'
return
}
this.currentPlayer =
this.currentPlayer === this.BLACK ? this.WHITE : this.BLACK
}
possibleDirections(col, row) {
let dirs = [
[-1, -1],
[-1, 0],
[-1, 1],
[0, -1],
[0, 1],
[1, -1],
[1, 0],
[1, 1]
]
let possibleDirections = []
for (let i = 0; i < dirs.length; i++) {
let dir = dirs[i]
let fooCol = col + dir[0]
let fooRow = row + dir[1]
if (!(fooCol > 7 || fooCol < 0 || fooRow > 7 || fooRow < 0)) {
let fooTile = this.board[fooCol][fooRow]
if (fooTile !== this.currentPlayer && fooTile !== this.EMPTY) {
possibleDirections.push(dir)
}
}
}
return possibleDirections
}
traverseDirection(possibleDirection, col, row) {
let flips = []
let skip = false
let opponentPlayer =
this.currentPlayer === this.BLACK ? this.WHITE : this.BLACK
for (let i = 1; i < this.BOARDDIM + 1 && !skip; i++) {
let fooCol = i * possibleDirection[0] + col
let fooRow = i * possibleDirection[1] + row
if (fooCol > 7 || fooCol < 0 || fooRow > 7 || fooRow < 0) {
// ran off the board before hitting your own tile
skip = true
flips = []
} else {
let fooTile = this.board[fooCol][fooRow]
if (fooTile === opponentPlayer) {
// if tile is opposite color it could be flipped, so add to potential flip array
flips.push([fooCol, fooRow])
} else if (fooTile === this.currentPlayer && i > 1) {
// hit current players tile which means capture is complete
skip = true
} else {
// either hit current players own color before hitting an opponent's
// or hit an empty space
flips = []
skip = true
}
}
}
return flips
}
isComplete() {
if (this.moveKey === 60) {
this.complete = true
this.msg = 'good game'
return
}
let empties = []
for (let i = 0; i < this.BOARDDIM; i++) {
for (let j = 0; j < this.BOARDDIM; j++) {
if (this.board[i][j] === this.EMPTY) {
empties.push([i, j])
}
}
}
let validMovesRemain = false
if (empties.length) {
for (let i = 0; i < empties.length && !validMovesRemain; i++) {
let gameCopy = new Reversi(this)
// Object.assign(gameCopy, JSON.parse(JSON.stringify(this)))
gameCopy.currentPlayer = this.BLACK
gameCopy.makeMove(empties[i])
if (!gameCopy.error) {
validMovesRemain = true
}
gameCopy = new Reversi(this)
// Object.assign(gameCopy, JSON.parse(JSON.stringify(this)))
gameCopy.currentPlayer = this.WHITE
gameCopy.makeMove(empties[i])
if (!gameCopy.error) {
validMovesRemain = true
}
gameCopy = undefined
}
}
if (validMovesRemain) {
this.error = true
this.msg = 'Invalid Game (moves still available)'
} else {
this.complete = true
this.msg = 'good game'
}
}
calcWinners() {
this.isComplete()
if (this.error) return new Error('Game not complete')
let w = 0
let b = 0
for (let i = 0; i < 64; i++) {
let row = Math.floor(i / 8)
let col = i % 8
w += this.board[col][row] === this.WHITE ? 1 : 0
b += this.board[col][row] === this.BLACK ? 1 : 0
}
this.whiteScore = w
this.blackScore = b
}
isSymmetrical() {
let RotSym = true
let Y0Sym = true
let X0Sym = true
let XYSym = true
let XnYSym = true
for (
let i = 0;
i < this.BOARDDIM && (RotSym || Y0Sym || X0Sym || XYSym || XnYSym);
i++
) {
for (
let j = 0;
j < this.BOARDDIM && (RotSym || Y0Sym || X0Sym || XYSym || XnYSym);
j++
) {
// rotational symmetry
if (this.board[i][j] != this.board[7 - i][7 - j]) {
RotSym = false
}
// symmetry on y = 0
if (this.board[i][j] != this.board[i][7 - j]) {
Y0Sym = false
}
// symetry on x = 0
if (this.board[i][j] != this.board[7 - i][j]) {
X0Sym = false
}
// symmetry on x = y
if (this.board[i][j] != this.board[7 - j][7 - i]) {
XYSym = false
}
// symmetry on x = -y
if (this.board[i][j] != this.board[j][i]) {
XnYSym = false
}
}
}
if (RotSym || Y0Sym || X0Sym || XYSym || XnYSym) {
this.symmetrical = true
this.RotSym = RotSym
this.Y0Sym = Y0Sym
this.X0Sym = X0Sym
this.XYSym = XYSym
this.XnYSym = XnYSym
}
}
buildMovesString() {
this.movesString = this.moves
.map(move => {
return this.arrayToMove(move[0], move[1])
})
.join('')
}
pickRandomMove() {
let validMoves = this.getValidMoves()
if (!validMoves.length) {
this.currentPlayer =
this.currentPlayer === this.BLACK ? this.WHITE : this.BLACK
validMoves = this.getValidMoves()
}
return (
validMoves.length !== 0 &&
validMoves[Math.floor(Math.random() * validMoves.length)]
)
}
mine() {
this.clearAttrs()
let skip = false
for (let i = 0; i < 60 && !skip; i++) {
let move = i == 0 ? this.STARTMOVE : this.pickRandomMove() // first move is always C4 to prevent translation solutions
if (move) {
this.moves.push(move)
this.buildMovesString()
this.moveKey++
this.makeMove(move)
if (this.error) {
this.error = false
this.currentPlayer =
this.currentPlayer === this.BLACK ? this.WHITE : this.BLACK
this.makeMove(move)
if (this.error) {
skip = true
}
}
} else {
skip = true
}
}
this.thisBoardToByteBoard()
this.isComplete()
this.calcWinners()
this.isSymmetrical()
this.thisMovesToByteMoves()
}
getValidMoves() {
let validMoves = []
for (let i = 0; i < this.BOARDDIM; i++) {
for (let j = 0; j < this.BOARDDIM; j++) {
if (this.board[i][j] === this.EMPTY) {
let move = [i, j]
let testGame = new Reversi(JSON.parse(JSON.stringify(this)))
testGame.makeMove(move)
if (!testGame.error) {
validMoves.push(move)
}
}
}
}
return validMoves
}
makeVisualBoard() {
this.visualBoard = this.arrayBoardToRows(
this.board
.map(c => c.map(t => (t === 1 ? 'b' : t === 2 ? 'w' : '-')).join(''))
.join('')
.match(/.{1,1}/g)
).map(r => {
return r.map(t => (t === 'b' ? '⬛️' : t === 'w' ? '⬜️' : '❎'))
})
return this.visualBoard
}
moveToArray(moveArray) {
return [
moveArray[0].toLowerCase().charCodeAt(0) - 97 + 0,
parseInt(moveArray[1]) - 1 + 0
]
}
stringMovesToBinaryMoves(stringMoves = false) {
if (!stringMoves) return
stringMoves = stringMoves
.match(/.{1,2}/g)
.map(move => {
if (move.length < 2) return
let moveArray = move.match(/.{1,1}/g)
let m = this.moveToArray(moveArray)
let foo = new BN(m[0] + m[1] * 8 + 64)
return foo.toString(2)
})
.join('')
if (stringMoves.length < 64 * 7) {
let padding = 64 * 7 - stringMoves.length
padding = new Array(padding)
padding = padding.fill('0').join('')
stringMoves += padding
}
return stringMoves
}
playGameMovesString(moves = null) {
return this.playGameMovesArray(this.stringMovesToArrayMoves(moves))
}
playGameByteMoves(
byteFirst32Moves = this.byteFirst32Moves,
byteLastMoves = this.byteLastMoves
) {
this.playGameMovesString(
this.byteMovesToStringMoves(byteFirst32Moves, byteLastMoves)
)
return this
}
playMove(moveKey = 0, moves = this.moves) {
if (typeof moves[moveKey] === 'undefined') return true
let move = moves[moveKey]
this.movesString += move
let skip = false
this.moveKey++
let validMoves = this.getValidMoves()
let moveArray = this.moveToArray(move)
let contained = validMoves.findIndex((m) => m[0] === moveArray[0] && m[1] === moveArray[1])
if (contained < 0 && validMoves.length > 0) {
this.error = 'invalid move'
return true
}
this.makeMove(moveArray)
if (this.error) {
this.error = false
this.currentPlayer =
this.currentPlayer === this.BLACK ? this.WHITE : this.BLACK
this.makeMove(moveArray)
if (this.error) {
skip = true
}
}
return skip
}
playGameMovesArray(moves = this.moves) {
if (moves.length === 0) return
this.clearAttrs()
this.moves = moves
this.thisMovesToByteMoves()
let skip = false
for (let i = 0; i < this.moves.length && !skip; i++) {
skip = this.playMove(i)
}
this.thisBoardToByteBoard()
if (!this.error) {
this.isComplete()
this.calcWinners()
this.isSymmetrical()
}
return this
// this.makeVisualBoard()
}
colArrayBoardToBinaryBoard(colArrayBoard = []) {
if (!colArrayBoard.length) return
let boardString = ''
for (let col = 0; col < colArrayBoard.length; col++) {
for (let row = 0; row < colArrayBoard[col].length; row++) {
let tile = colArrayBoard[col][row]
boardString += tile === 1 ? '01' : tile === 2 ? '10' : '11'
}
}
return boardString
}
colArrayBoardToByteBoard(colArrayBoard = []) {
if (!colArrayBoard.length) return
return this.binaryBoardToByteBoard(
this.colArrayBoardToBinaryBoard(colArrayBoard)
)
}
binaryBoardToByteBoard(binaryBoard) {
let foo = new BN(binaryBoard, 2)
return foo.toString(16)
}
sliceBinaryMovesToBytes(binaryMoves) {
let len = binaryMoves.length
if (len < 64 * 7) {
let padding = 64 * 7 - len
padding = new Array(padding)
padding = padding.fill('0').join('')
binaryMoves = binaryMoves + padding
}
return [
this.binaryMovesToByteMoves(binaryMoves.slice(0, 224)),
this.binaryMovesToByteMoves(binaryMoves.slice(224))
]
}
byteBoardPopulateBoard(byteBoard = this.byteBoard) {
this.board = this.arrayBoardToCols(
this.byteBoardToArrayBoard(byteBoard).map(t => {
return t === 'b' ? 1 : t === 'w' ? 2 : 3
})
)
}
byteBoardToArrayBoard(byteBoard = this.byteBoard) {
if (byteBoard.slice(0, 2) === '0x') {
byteBoard = byteBoard.slice(2)
}
byteBoard = new BN(byteBoard, 16)
byteBoard = byteBoard.toString(2)
let len = byteBoard.length
if (len < 128) {
let padding = 128 - len
padding = new Array(padding)
padding = padding.fill('0').join('')
byteBoard = padding + byteBoard
}
return byteBoard.match(/.{1,2}/g).map(tile => {
return tile === '01' ? 'b' : tile === '10' ? 'w' : '-'
})
}
byteBoardToRowArray(byteBoard = this.byteBoard) {
return this.arrayBoardToRows(this.byteBoardToArrayBoard(byteBoard))
}
byteBoardToColArray(byteBoard = this.byteBoard) {
return this.arrayBoardToCols(this.byteBoardToArrayBoard(byteBoard))
}
multiArrayBoardToRows(arrayBoard = []) {
return this.arrayBoardToRows2D(
arrayBoard.map(r =>
r.map(t => (t === this.BLACK ? 'b' : t === this.WHITE ? 'w' : '-'))
)
)
}
arrayBoardToRows2D(arrayBoard = []) {
let rowsArray = []
for (let col = 0; col < arrayBoard.length; col++) {
for (let i = 0; i < arrayBoard[col].length; i++) {
if (!rowsArray[col]) rowsArray[col] = []
rowsArray[col].push(arrayBoard[i][col])
}
}
return rowsArray
}
arrayBoardToRows(arrayBoard = []) {
let rowsArray = []
for (let i = 0; i < 64; i++) {
let row = i % 8
if (!rowsArray[row]) rowsArray[row] = []
rowsArray[row].push(arrayBoard[i])
}
return rowsArray
}
arrayBoardToCols(arrayBoard = []) {
let colsArray = []
for (let i = 0; i < 64; i++) {
let col = Math.floor(i / 8)
if (!colsArray[col]) colsArray[col] = []
colsArray[col].push(arrayBoard[i])
}
return colsArray
}
stringBoardToArrayBoard(stringBoard = false) {
return (
stringBoard &&
stringBoard
.match(/.{1,1}/g)
.map(spot => {
return spot === 'b' ? '01' : spot === 'w' ? '10' : '11'
})
.join('')
)
}
stringMovesToArrayMoves(stringMoves = false) {
if (!stringMoves) return
return stringMoves.match(/.{1,2}/g)
}
pickRandomMove() {
let validMoves = this.getValidMoves()
if (!validMoves.length) {
this.currentPlayer =
this.currentPlayer === this.BLACK ? this.WHITE : this.BLACK
validMoves = this.getValidMoves()
}
return (
validMoves.length !== 0 &&
validMoves[Math.floor(Math.random() * validMoves.length)]
)
}
byteMovesToStringMoves(byteFirst32Moves = 0, byteLastMoves = 0) {
if (byteFirst32Moves.slice(0, 2) === '0x') {
byteFirst32Moves = byteFirst32Moves.slice(2)
}
if (byteLastMoves.slice(0, 2) === '0x') {
byteLastMoves = byteLastMoves.slice(2)
}
let byteMoves = byteFirst32Moves + byteLastMoves
byteMoves = new BN(byteMoves, 16)
let binaryMoves = byteMoves.toString(2)
return this.binaryMovesToStringMoves(binaryMoves)
}
returnByteMoves() {
return [this.byteFirst32Moves, this.byteLastMoves]
}
binaryMovesToByteMoves(binaryMoves = 0) {
if (!binaryMoves) return
let foo = new BN(binaryMoves, 2)
return foo.toString(16)
}
stringMovesToByteMoves(stringMoves) {
return this.binaryMovesToByteMoves(
this.stringMovesToBinaryMoves(stringMoves)
)
}
binaryMovesToStringMoves(binaryMoves = 0) {
binaryMoves = binaryMoves && new BN(binaryMoves, 2)
binaryMoves = binaryMoves.toString(2)
if (binaryMoves.length < 64 * 7) {
let padding = 64 * 7 - binaryMoves.length
padding = new Array(padding)
padding = padding.fill('0').join('')
binaryMoves += padding
}
return binaryMoves
.match(/.{1,7}/g)
.map(move => {
move = new BN(move, 2).toNumber(10)
if (move < 64) {
return false
} else {
move -= 64
let col = move % 8
move -= col
let row = move / 8
return this.arrayToMove(col, row)
}
})
.filter(move => move)
.join('')
.toUpperCase()
}
translateToC4Version(moves = this.moves) {
if (!moves.length) return new Error('No Moves')
let translated = []
for (let i = 0; i < moves.length; i++) {
let move = moves[i]
move = this.moveToArray(move)
switch (moves[0].toLowerCase()) {
case 'f5':
move = move.map(m => 7 - m)
break
case 'd3':
move = [move[1], move[0]]
break
case 'e6':
move = [7 - move[1], 7 - move[0]]
break
case 'c4':
break
default:
throw new Error('impossibru!!!' + moves[0].toLowerCase())
}
move = this.arrayToMove(move[0], move[1])
translated[i] = move
}
this.moves = translated
this.playGameMovesArray()
}
arrayToMove(col, row) {
return 'abcdefghijklmnopqrstuvwxyz'[col] + (row + 1)
}
sliceMovesStringToBytes(moves = '') {
return this.sliceBinaryMovesToBytes(this.vesToBinaryMoves(moves))
}
returnSymmetriesAsBN() {
var symmetries = 0
if (this.RotSym) symmetries += 0b10000
if (this.Y0Sym) symmetries += 0b01000
if (this.X0Sym) symmetries += 0b00100
if (this.XYSym) symmetries += 0b00010
if (this.XnYSym) symmetries += 0b00001
return new BN(symmetries)
}
}
export default Reversi