Last active
August 4, 2020 23:23
-
-
Save naosim/5b4889a3a72f48ca6b3644838cd4808a to your computer and use it in GitHub Desktop.
deno-reversi-logic.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as board2d from 'https://raw.githubusercontent.com/naosim/deno-board2d/v1.2.0/src/mod.ts'; | |
export { | |
Board, | |
BoardMutable, | |
Pos, | |
Direction, | |
X, | |
Y | |
} from 'https://raw.githubusercontent.com/naosim/deno-board2d/v1.2.0/src/mod.ts'; | |
/** | |
* Reversi game class | |
* | |
* entry point | |
*/ | |
export class Game { | |
/** | |
* | |
* Use `Game.init ()` instead of the constructor to start the game. | |
* | |
* @param turn | |
* @param board | |
* @param winner null if no win or loss has been decide | |
* @param _eventList history | |
*/ | |
constructor( | |
readonly turn: Disk, | |
readonly board: board2d.Board<Disk>, | |
readonly winner: Disk | null = null, | |
private readonly _eventList: Event[] = [] | |
) {} | |
/** | |
* Start the game | |
*/ | |
static init(size = 8): Game { | |
if(size % 2 != 0) { | |
throw new Error('Board size should be even.'); | |
} | |
var board = board2d.BoardMutable.empty<Disk>(size, size) | |
.put(new board2d.Pos(size / 2 - 1 as board2d.X, size / 2 - 1 as board2d.Y), Disk.light) | |
.put(new board2d.Pos(size / 2 as board2d.X, size / 2 - 1 as board2d.Y), Disk.dark) | |
.put(new board2d.Pos(size / 2 - 1 as board2d.X, size / 2 as board2d.Y), Disk.dark) | |
.put(new board2d.Pos(size / 2 as board2d.X, size / 2 as board2d.Y), Disk.light) | |
.toImmutable(); | |
return new Game( | |
Disk.light, | |
board | |
) | |
} | |
get score(): Score { | |
var lightCount = 0; | |
var darkCount = 0; | |
this.board.forEach((pos, disk) => { | |
if(disk === Disk.light) { | |
lightCount++; | |
} | |
if(disk === Disk.dark) { | |
darkCount++; | |
} | |
}) | |
return new Score(lightCount, darkCount); | |
} | |
/** | |
* Whether the win or loss has been decided | |
*/ | |
get isGameOver(): boolean { | |
return this.winner != null; | |
} | |
getTunableDiskPosList(pos: board2d.PosReadable, disk: Disk): board2d.Pos[] { | |
if(this.isPlaced(pos)) { | |
return [] | |
} | |
const direction8 = [ | |
board2d.Direction.up, | |
board2d.Direction.down, | |
board2d.Direction.right, | |
board2d.Direction.left, | |
board2d.Direction.upRight, | |
board2d.Direction.upLeft, | |
board2d.Direction.downRight, | |
board2d.Direction.downLeft | |
].map(v => board2d.Pos.createFromDirection(v)); | |
return direction8.map(d => { | |
var currentPos = d.add(pos); | |
var result: board2d.Pos[] = []; | |
var i = 0; | |
while(this.isPlaced(currentPos) && this.board.getValue(currentPos) !== disk) { | |
result.push(currentPos); | |
currentPos = currentPos.add(d); | |
i++; | |
if(i > 20) throw new Error("infinite loop"); | |
} | |
return this.board.getValue(currentPos) === disk ? result : [] | |
}).reduce((memo, list) => memo.concat(list), []) | |
} | |
getTunableDiskPosListFromXY(x: board2d.X, y: board2d.Y, disk: Disk): board2d.Pos[] { | |
return this.getTunableDiskPosList(new board2d.Pos(x, y), disk); | |
} | |
/** | |
* Whether the disk is placed in the passed position | |
* @param pos | |
*/ | |
isPlaced(pos: board2d.PosReadable): boolean { | |
return this.board.exists(pos); | |
} | |
/** | |
* Whether the disk is placed in the passed position | |
* @param x | |
* @param y | |
*/ | |
isPlacedFromXY(x: board2d.X, y: board2d.Y): boolean { | |
return this.isPlaced(new board2d.Pos(x, y)); | |
} | |
/** | |
* Place a disk | |
* | |
* The result is returned in callback. | |
* Conditions that cause an error in callback | |
* - The type of disk and the turn do not match | |
* - The position where the disk cannot be placed is specified | |
* @param pos | |
* @param disk | |
* @param callback | |
*/ | |
placeDisk(pos: board2d.Pos, disk: Disk, callback: (e: Error | null, game?: Game)=>void) { | |
if(this.turn != disk) { | |
callback(new Error('The turn is different')); | |
return; | |
} | |
var list = this.getTunableDiskPosList(pos, disk); | |
if(list.length == 0) { | |
callback(new Error('Cannot be placed')); | |
return; | |
} | |
var newBoard: board2d.BoardMutable<Disk> = this.board.toMutable(); | |
newBoard.put(pos, disk); | |
list.forEach(p => newBoard.put(p, disk)); | |
newBoard.toImmutable(); | |
var newEventList = [...this._eventList, new Event(pos, disk)] | |
var nextTurn = this.turn == Disk.light ? Disk.dark : Disk.light; | |
var winner = null; | |
var preGame = new Game(nextTurn, newBoard.toImmutable(), winner, newEventList); | |
if(preGame.hasTurnableDiskPos(nextTurn)) { | |
callback(null, preGame); | |
return; | |
} else if(preGame.hasTurnableDiskPos(this.turn)) { | |
callback(null, new Game(this.turn, newBoard.toImmutable(), winner, newEventList)); | |
return; | |
} else { // winner decided | |
callback(null, new Game(nextTurn, newBoard.toImmutable(), preGame.score.winner, newEventList)); | |
return; | |
} | |
} | |
/** | |
* Is there a place to put the disk? | |
*/ | |
hasTurnableDiskPos(disk: Disk): boolean { | |
return this.board.some((pos, _) => this.getTunableDiskPosList(pos, disk).length > 0) | |
} | |
/** | |
* Place a disk with x and y | |
* @param x | |
* @param y | |
* @param disk | |
* @param callback | |
*/ | |
placeDiskFromXY(x: board2d.X, y: board2d.Y, disk: Disk, callback: (e: Error | null, game?: Game)=>void) { | |
return this.placeDisk(new board2d.Pos(x, y), disk, callback); | |
} | |
/** | |
* Convert board to string (for debugging) | |
* @param board | |
*/ | |
static boardToString(board: board2d.Board<Disk>): string { | |
return board.toMutable().boardCore.values.map(row => row.map(v => v || '_').join('')).join('\n') | |
} | |
} | |
export enum Disk { | |
light = 'l', | |
dark = 'd' | |
} | |
class Event { | |
constructor(readonly pos: board2d.Pos, readonly disk: Disk) { | |
} | |
} | |
class Score { | |
constructor(readonly lightCount: number, readonly darkCount: number) { | |
} | |
getDiskCount(disk: Disk): number { | |
return disk === Disk.light ? this.lightCount : this.darkCount; | |
} | |
get winner(): Disk | null { | |
if(this.lightCount == this.darkCount) { | |
return null; // draw | |
} | |
return this.lightCount > this.darkCount ? Disk.light : Disk.dark; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment