Skip to content

Instantly share code, notes, and snippets.

@bellbind
Last active November 2, 2024 00:19
Show Gist options
  • Save bellbind/a28b7a106c82e598987ce515e452760f to your computer and use it in GitHub Desktop.
Save bellbind/a28b7a106c82e598987ce515e452760f to your computer and use it in GitHub Desktop.
[react][redux]Reversi Game with React/Redux and React-Redux
<!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>
"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);
}
"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

This version use views as react-redux container:

Old versions:

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