Last active
May 21, 2016 21:02
-
-
Save bellbind/94176790cfdf3449e70fba6bb71b67cc to your computer and use it in GitHub Desktop.
[es6][browser]Reversi Game
This file contains 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
<!doctype html> | |
<html> | |
<head> | |
<meta charset="utf-8" /> | |
<script src="reversi.js"></script> | |
<script src="reversi-ui.js"></script> | |
</head> | |
<body></body> | |
</html> |
This file contains 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
"use strict"; | |
class ReversiGame { | |
constructor (size) { | |
this.subscribers = []; | |
this.newGame(size); | |
} | |
newGame(size) { | |
this.update({ | |
turn: Board.newGame(size), | |
side: 1, | |
msg: "Black turn" | |
}); | |
} | |
subscribe(s) {this.subscribers.push(s);} | |
next(x, y) {this.update(this.nextState(this.state, x, y));} | |
// private | |
update(state) { | |
this.state = state; | |
this.subscribers.forEach(s => s(this.state)); | |
} | |
name(side) {return side === Board.BLACK ? "Black" : "White";} | |
nextState(state, x, y) { | |
if (state.turn.isEnd()) return state; | |
if (state.turn.reverseCount(state.side, x, y) === 0) { | |
const cur = this.name(state.side); | |
const msg = `${cur} turn again (invalid place)`; | |
return {turn: state.turn, side: state.side, msg}; | |
} | |
const turn = state.turn.nextTurn(state.side, x, y); | |
if (turn.isEnd()) { | |
const b = turn.blacks.length, w = turn.whites.length; | |
const r = b === w ? "even" : `${b > w ? "Black" : "White"} win`; | |
const msg = `Game End: ${r}, black = ${b} white = ${w}`; | |
return {turn, side: state.side, msg}; | |
} | |
const nextPassed = turn.isPass(-state.side); | |
const side = nextPassed ? state.side : -state.side; | |
const next = this.name(side), prev = this.name(-side); | |
const tailmsg = nextPassed ? ` (${prev} passed)` : ""; | |
const msg = `${next} turn${tailmsg}`; | |
return {turn, side, msg}; | |
} | |
} | |
// Simple COM player | |
function comAction(state) { | |
return state.turn.blanks.reduce((r, p) => { | |
const count = state.turn.reverseCount(state.side, p.x, p.y); | |
return r.count < count ? {count, p} : r; | |
}, {count: -1, p: null}).p; | |
} | |
function comPlay(side, reversi) { | |
if (reversi.state.side !== side || reversi.state.turn.isEnd()) return; | |
setTimeout(_ => { | |
const {x, y} = comAction(reversi.state); | |
reversi.next(x, y); | |
comPlay(side, reversi); | |
}, 500); | |
} | |
window.addEventListener("load", _ => { | |
const param = /^#(\d+)$/.exec(location.hash); | |
const size = param && param[1] >= 4 && param[1] % 2 === 0 ? +param[1] : 8; | |
const reversi = new ReversiGame(size); | |
setupStylesheet(); | |
const msg = document.createElement("h3"); | |
document.body.appendChild(msg); | |
const view = newView(size, 50); | |
document.body.appendChild(view.view); | |
reversi.subscribe(state => { | |
view.update(state.turn); | |
msg.textContent = state.msg; | |
}); | |
view.subscribe((x, y) => { | |
if (reversi.state.side !== Board.BLACK) return; | |
reversi.next(x, y); | |
comPlay(Board.WHITE, reversi); | |
}); | |
const button = document.createElement("button"); | |
button.textContent = "New Game"; | |
document.body.appendChild(button); | |
button.addEventListener("click", _ => { | |
reversi.newGame(size); | |
}, false); | |
reversi.newGame(size); | |
}, false); | |
function setupStylesheet() { | |
document.head.appendChild(document.createElement("style")); | |
const css = document.styleSheets[document.styleSheets.length - 1]; | |
css.insertRule(`.board { | |
position: absolute; | |
background-color: gray; | |
}`, 0); | |
css.insertRule(`.cell { | |
position: absolute; | |
background-color: green; | |
box-shadow: -0.1rem -0.1rem rgba(0,0,0,0.6); | |
}`, 0); | |
css.insertRule(`.stone { | |
position: absolute; | |
top: 5%; left: 5%; | |
width: 85%; height: 85%; | |
transform-style: preserve-3d; | |
transition: 0.2s; | |
transform: rotateY(0deg); | |
}`, 0); | |
css.insertRule(`.white, .black { | |
position: absolute; | |
width: 100%; height: 100%; | |
backface-visibility: hidden; | |
border-radius: 50%; | |
box-shadow: 0.2rem 0.2rem rgba(0,0,0,0.6); | |
}`, 0); | |
css.insertRule(`.white { | |
background-color: white; | |
transform: rotateY(180deg); | |
}`, 0); | |
css.insertRule(`.black { | |
background-color: black; | |
}`, 0); | |
} | |
function newView(boardSize, cellSize) { | |
const subscribers = []; | |
const border = cellSize / 10; | |
const span = cellSize + border, edge = border * 2; | |
const boardWidth = edge + span * boardSize + border; | |
const view = document.createElement("div"); | |
const board = document.createElement("div"); | |
view.appendChild(board); | |
board.classList.add("board"); | |
view.style.width = view.style.height = | |
board.style.width = board.style.height = `${boardWidth}px`; | |
const cells = Array.from(Array(boardSize), (_, y) => Array.from( | |
Array(boardSize), (_, x) => { | |
const listener = ev => subscribers.forEach(s => s(x, y)); | |
const cell = newCell(cellSize); | |
cell.cell.style.left = `${edge + x * span}px`; | |
cell.cell.style.top = `${edge + y * span}px`; | |
cell.cell.addEventListener("click", listener, false); | |
board.appendChild(cell.cell); | |
return cell; | |
})); | |
return { | |
view, | |
subscribe(s) {subscribers.push(s);}, | |
update(turn) { | |
cells.forEach((line, y) => line.forEach((cell, x) => { | |
cell.setState(turn.board[y][x]); | |
})); | |
} | |
}; | |
} | |
function newCell(size) { | |
const cell = document.createElement("div"); | |
const stone = document.createElement("div"); | |
const white = document.createElement("div"); | |
const black = document.createElement("div"); | |
cell.appendChild(stone); | |
stone.appendChild(white); | |
stone.appendChild(black); | |
cell.classList.add("cell"); | |
stone.classList.add("stone"); | |
black.classList.add("black"); | |
white.classList.add("white"); | |
cell.style.width = cell.style.height = `${size}px`; | |
return { | |
cell, | |
setState(state) { | |
if (state === Board.BLANK) { | |
stone.style.visibility = "hidden"; | |
} else { | |
const deg = state === Board.WHITE ? 180 : 0; | |
stone.style.transform = `rotateY(${deg}deg)`; | |
stone.style.visibility = "visible"; | |
} | |
} | |
}; | |
} |
This file contains 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
"use strict"; | |
// rules of Reversi (with flexible even sizes) | |
class Board { | |
static newGame(size = 8) { | |
console.assert(size >= 4 && size % 2 === 0); | |
const r = size >> 1, l = r - 1; | |
const board = Array.from( | |
Array(size), _ => Array(size).fill(Board.BLANK)); | |
board[l][l] = board[r][r] = Board.WHITE; | |
board[l][r] = board[r][l] = Board.BLACK; | |
return new Board(board); | |
} | |
nextTurn(side, x, y) { | |
console.assert(this.validPlace(side, x, y)); | |
const reverses = this.reverses(side, x, y); | |
console.assert(reverses.length > 0); | |
const newBoard = this.board.map(line => line.concat()); | |
reverses.forEach(p => newBoard[p.y][p.x] = side); | |
newBoard[y][x] = side; | |
return new Board(newBoard); | |
} | |
// state of board | |
isPass(side) { | |
return this.blanks.every( | |
p => this.reverses(side, p.x, p.y).length === 0); | |
} | |
isEnd() { | |
return this.blanks.length === 0 || | |
this.isPass(Board.BLACK) && this.isPass(Board.WHITE); | |
} | |
reverseCount(side, x, y) { | |
return this.validPlace(side, x, y) ? | |
this.reverses(side, x, y).length : 0; | |
} | |
// privates | |
constructor (board) { | |
this.size = board.length; | |
this.board = Object.freeze(board.map(line => Object.freeze(line))); | |
this.blanks = Object.freeze(this.pointsOf(Board.BLANK)); | |
this.blacks = Object.freeze(this.pointsOf(Board.BLACK)); | |
this.whites = Object.freeze(this.pointsOf(Board.WHITE)); | |
Object.seal(this); | |
} | |
points() { | |
return [].concat(...Array.from(Array(this.size), (_, y) => Array.from( | |
Array(this.size), (_, x) => Object.freeze({x, y})))); | |
} | |
pointsOf(state) { | |
return this.points().filter(p => this.board[p.y][p.x] === state); | |
} | |
validPoint(x, y) { | |
return 0 <= x && x < this.size && 0 <= y && y < this.size; | |
} | |
validPlace(side, x, y) { | |
return (side === Board.BLACK || side === Board.WHITE) && | |
this.validPoint(x, y) && this.board[y][x] === Board.BLANK; | |
} | |
reverses(side, x, y) { | |
return [].concat( | |
this.lineReverses(side, x, y, p => ({x: p.x, y: p.y - 1})), | |
this.lineReverses(side, x, y, p => ({x: p.x, y: p.y + 1})), | |
this.lineReverses(side, x, y, p => ({x: p.x - 1, y: p.y})), | |
this.lineReverses(side, x, y, p => ({x: p.x + 1, y: p.y})), | |
this.lineReverses(side, x, y, p => ({x: p.x - 1, y: p.y - 1})), | |
this.lineReverses(side, x, y, p => ({x: p.x + 1, y: p.y - 1})), | |
this.lineReverses(side, x, y, p => ({x: p.x - 1, y: p.y + 1})), | |
this.lineReverses(side, x, y, p => ({x: p.x + 1, y: p.y + 1}))); | |
} | |
lineReverses(side, x, y, next) { | |
const ps = []; | |
for (let p = next({x, y}); this.validPoint(p.x, p.y); p = next(p)) { | |
const s = this.board[p.y][p.x]; | |
if (s === -side) ps.push(Object.freeze(p)); | |
else if (s === side) return ps; | |
else return []; | |
} | |
return []; | |
} | |
} | |
Board.BLANK = 0; | |
Board.BLACK = 1; | |
Board.WHITE = -1; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
demo: https://gist.githack.com/bellbind/94176790cfdf3449e70fba6bb71b67cc/raw/index.html
- size 6: https://gist.githack.com/bellbind/94176790cfdf3449e70fba6bb71b67cc/raw/index.html#6
(The view style is easily applicable to React/Redux state store model)