Last active
November 2, 2024 00:19
-
-
Save bellbind/a28b7a106c82e598987ce515e452760f to your computer and use it in GitHub Desktop.
[react][redux]Reversi Game with React/Redux and 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://unpkg.com/[email protected]/dist/react.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/react-dom.js" | |
></script> | |
<script src="https://unpkg.com/[email protected]/dist/redux.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/react-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", side, x, y}); | |
}, 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": | |
if (state.side !== action.side) return state; | |
return ReversiGame.nextState(state, action.x, action.y); | |
default: return state; | |
} | |
}; | |
const reversi = Redux.createStore(reducer); | |
// AI spawned from subscribe | |
reversi.subscribe(() => { | |
if (reversi.getState().side === Board.WHITE) { | |
comPlay(Board.WHITE, reversi); | |
} | |
}); | |
// ReactRedux containers must be wrapped with ReactRedux Provider | |
const root = React.createFactory(ReactRedux.Provider)( | |
{store: reversi}, | |
React.createFactory(ReversiView)({boardSize: size, cellSize: 50})); | |
ReactDOM.render(root, main); | |
}, false); | |
// React-Redux View | |
const ReversiView = ({boardSize, cellSize}) => { | |
// ReactRedux Containers: state to props, dispatch to props, and the view | |
const msg = ReactRedux.connect( | |
({msg}) => ({msg}) | |
)(({msg}) => React.DOM.h3({}, msg)); | |
const newgame = ReactRedux.connect( | |
null, | |
dispatch => ({onClick: () => dispatch({type: "new"})}) | |
)(({onClick}) => React.DOM.button({onClick}, "New Game")); | |
const board = ReactRedux.connect( | |
({turn}) => ({boardSize, cellSize, turn}), | |
dispatch => ({onSelect: (x, y) => dispatch( | |
{type: "next", side: Board.BLACK, x, y})}) | |
)(ReversiBoard); | |
return React.DOM.div( | |
{}, | |
React.createFactory(msg)(), | |
React.createFactory(board)(), | |
React.createFactory(newgame)() | |
); | |
}; | |
// React Views (as state less function component) | |
const ReversiBoard = ({cellSize, boardSize, turn, onSelect}) => { | |
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); | |
const Cell = ({style, x, y, side, onSelect}) => { | |
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
This version use views as react-redux container:
Old versions: