Last active
May 21, 2016 21:03
-
-
Save bellbind/7fb2c61f00e4d8f3031fe368152b8df6 to your computer and use it in GitHub Desktop.
[react][redux]Reversi Game with React/Redux
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="https://fb.me/react-15.1.0.js"></script> | |
<script src="https://fb.me/react-dom-15.1.0.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js" | |
></script> | |
<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 { | |
static newGame(size) { | |
return { | |
turn: Board.newGame(size), | |
side: 1, | |
msg: "Black turn" | |
}; | |
} | |
static nextState(state, x, y) { | |
if (state.turn.isEnd()) return state; | |
if (state.turn.reverseCount(state.side, x, y) === 0) { | |
const cur = ReversiGame.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 = ReversiGame.name(side), prev = ReversiGame.name(-side); | |
const tailmsg = nextPassed ? ` (${prev} passed)` : ""; | |
const msg = `${next} turn${tailmsg}`; | |
return {turn, side, msg}; | |
} | |
static name(side) {return side === Board.BLACK ? "Black" : "White";} | |
} | |
// Simple COM player | |
function comAction(state) { | |
return state.turn.blanks.reduce((r, p) => { | |
const up = state.turn.reverseCount(state.side, p.x, p.y); | |
if (up === 0) return r; | |
const next = state.turn.nextTurn(state.side, p.x, p.y); | |
const down = next.blanks.reduce((nmax, p) => Math.max( | |
next.reverseCount(-state.side, p.x, p.y), nmax), -1); | |
const count = up - down; | |
return r.count < count ? {count, p} : r; | |
}, {count: -Math.pow(state.turn.size, 2), p: null}).p; | |
} | |
function comPlay(side, reversi) { | |
const state = reversi.getState(); | |
if (state.side !== side || state.turn.isEnd()) return; | |
setTimeout(_ => { | |
const {x, y} = comAction(state); | |
reversi.dispatch({type: "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; | |
setupStylesheet(); | |
const main = document.createElement("main"); | |
document.body.appendChild(main); | |
// Redux | |
const reducer = (state = ReversiGame.newGame(size), action) => { | |
switch (action.type) { | |
case "new": return ReversiGame.newGame(size); | |
case "next": return ReversiGame.nextState(state, action.x, action.y); | |
default: return state; | |
} | |
}; | |
const reversi = Redux.createStore(reducer); | |
const update = () => { | |
const props = { | |
boardSize: size, cellSize: 50, | |
state: reversi.getState(), | |
onSelect(x, y) { | |
if (reversi.getState().side !== Board.BLACK) return; | |
reversi.dispatch({type: "next", x, y}); | |
comPlay(Board.WHITE, reversi); | |
}, | |
onNewGame() { | |
reversi.dispatch({type: "new"}); | |
} | |
}; | |
ReactDOM.render(ViewElement(props), main); | |
}; | |
reversi.subscribe(update); | |
update(); | |
}, false); | |
// React Views | |
class ReversiView extends React.Component { | |
render () { | |
const {cellSize, boardSize, state, onSelect, onNewGame} = this.props; | |
const msg = React.DOM.h3({}, state.msg); | |
const board = BoardElement( | |
{cellSize, boardSize, turn: state.turn, onSelect}); | |
const newgame = React.DOM.button({onClick: onNewGame}, "New Game"); | |
return React.DOM.div({}, msg, board, newgame); | |
} | |
} | |
const ViewElement = React.createFactory(ReversiView); | |
class ReversiBoard extends React.Component { | |
render() { | |
const {cellSize, boardSize, turn, onSelect} = this.props; | |
const border = cellSize / 10; | |
const span = cellSize + border, edge = border * 2; | |
const boardWidth = `${edge + span * boardSize + border}px`; | |
const style = {width: boardWidth, height: boardWidth}; | |
const cells = Array.from(Array(boardSize * boardSize), (_, key) => { | |
const x = key % boardSize; | |
const y = (key - x) / boardSize; | |
const left = `${edge + x * span}px`, top = `${edge + y * span}px`; | |
const width = `${cellSize}px`, height = `${cellSize}px`; | |
const style = {width, height, left, top}; | |
const side = turn.board[y][x]; | |
return CellElement({key, style, x, y, side, onSelect}); | |
}); | |
const board = React.DOM.div({className: "board", style}, cells); | |
return React.DOM.div({style}, board); | |
} | |
} | |
const BoardElement = React.createFactory(ReversiBoard); | |
class Cell extends React.Component { | |
render() { | |
const {style, x, y, side, onSelect} = this.props; | |
const visibility = side === Board.BLANK ? "hidden" : "visible"; | |
const transform = `rotateY(${side === Board.BLACK ? 0 : 180}deg)`; | |
const stoneStyle = {visibility, transform}; | |
const black = React.DOM.div({className: "black"}); | |
const white = React.DOM.div({className: "white"}); | |
const stone = React.DOM.div( | |
{className: "stone", style: stoneStyle}, black, white); | |
return React.DOM.div( | |
{className: "cell", style, onClick: () => onSelect(x, y)}, stone); | |
} | |
} | |
const CellElement = React.createFactory(Cell); | |
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); | |
} |
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/7fb2c61f00e4d8f3031fe368152b8df6/raw/index.html
see: