Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active May 21, 2016 21:02
Show Gist options
  • Save bellbind/94176790cfdf3449e70fba6bb71b67cc to your computer and use it in GitHub Desktop.
Save bellbind/94176790cfdf3449e70fba6bb71b67cc to your computer and use it in GitHub Desktop.
[es6][browser]Reversi Game
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script src="reversi.js"></script>
<script src="reversi-ui.js"></script>
</head>
<body></body>
</html>
"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";
}
}
};
}
"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;
@bellbind
Copy link
Author

bellbind commented May 14, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment