Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milesburton/998a76815559d07ec93eee117aad2d37 to your computer and use it in GitHub Desktop.
Save milesburton/998a76815559d07ec93eee117aad2d37 to your computer and use it in GitHub Desktop.
Conway's Game of life in React/Redux/RXJS/CSS-Grid/Styled-Components
<div id="mount" style="height:100%"></div>
// tl;dr no imports on Codepen, so everything that's importated via amd is attached to window.
const { createStore, bindActionCreators, combineReducers, applyMiddleware} = Redux;
const { connect, Provider } = ReactRedux;
const { logger } = reduxLogger;
const { timer } = rxjs;
const { createSelector } = Reselect;
/*
A somewhat convoluted version of Game Of Life written in React/Redux with a springling of Styled Components to control visuals. Intentionally mucking around with some more advanced concepts for experimental purposes (overloading react, slow DOM, performance tricks etc)
CSS Grid provides the layout
RXJS triggers the tick cycle
Improvements:
- Switch to a 2D array
- Implement Immutable
- Make it possible to switch out the render target, maybe use a provider to supply the output - ie Canvas/DOM/WebGL
*/
// ************************
// ACTIONS
// ************************
const TICK_ACTION = 'TICK_ACTION';
const START_GAME_ACTION = 'START_GAME_ACTION';
const startGame = (width, height) => ({
type: START_GAME_ACTION,
payload: {width, height}
});
const gameTick = () => ({type: TICK_ACTION});
// ************************
// Reducers (and helper functions)
// ************************
const makeAliveSiblingCounter = (state) => {
const makeIndexToXyConverter = (width) => (idx) =>{
const y = Math.trunc(Math.floor(idx / width));
const x = idx - (y*width);
return {x,y};
}
const makeXyToIndexConverter = (width) => (x, y) => {
return y * width + x;
}
const arrayCombinations = (arr1, arr2) => arr1.reduce( (accumulator, x) =>
[...accumulator, ...arr2.map(y=>[x,y])],
[]);
const generateCoordinatesForSiblingCells = ({startX, startY, endX, endY}) => {
const xAxisArray = [...Array((endX-startX)+1)].map((_,idx)=>startX + idx);
const yAxisArray = [...Array((endY-startY)+1)].map((_,idx)=>startY + idx);
const combinations = arrayCombinations(xAxisArray, yAxisArray)
const indicies = combinations.map(([x,y])=>xyToIndex(x,y));
return indicies;
}
const determineSiblingCoordinateBoundaries = ({x,y}) => {
// Identify where we should start and end the search through the array. We need to check north east, north, north west, west, south west, south, south east.
// Also we want to be bound by the the array upper and lower bounds.
const startX = Math.max(x-1,0);
const startY = Math.max(y-1,0);
const endX = Math.min(x+1, width -1);
const endY = Math.min(y+1, height -1);
return {startX, startY, endX, endY};
}
const makeCellAliveCounter = (grid) => (indicies) => {
const numberAlive = indicies
.map(idx=>grid.get(idx))
.filter(isAlive=>isAlive)
.length
return numberAlive;
}
const width = state.get('width');
const height = state.get('height');
const grid = state.get('grid');
const indexToXy = makeIndexToXyConverter(width);
const xyToIndex = makeXyToIndexConverter(width);
const countNumberOfCellsAliveWithinGrid = makeCellAliveCounter(grid);
return (indexOfCurrentCell) => {
const currentCellCordinates = indexToXy(indexOfCurrentCell);
const siblingBoundaries = determineSiblingCoordinateBoundaries(currentCellCordinates);
const indicies = generateCoordinatesForSiblingCells(siblingBoundaries);
const numberAlive = countNumberOfCellsAliveWithinGrid(indicies);
return numberAlive;
}
}
const makeShouldCellBeAlive = ({grid}) => ({isCellAlive, numberAlive}) => {
// Number alive includes the current cell
// Any dead cell with exactly three live neighbors becomes a live cell, as if by reproduction.
if(!isCellAlive && numberAlive === 3){
return true;
}
// Any live cell with fewer than two live neighbors dies, as if by under population.
if(isCellAlive && numberAlive < 3){
return false;
}
// Any live cell with more than three live neighbors dies, as if by overpopulation.
if(isCellAlive && numberAlive > 4){
return false;
}
// Any live cell with two or three live neighbors lives on to the next generation.
if(isCellAlive && (numberAlive === 3 || numberAlive == 4)){
return null; // No change in state (What I'd do for scala optionals right now...)
}
return null;
}
const generateGridArray = (width, height) => {
return Immutable.fromJS([...Array(width*height)].map(_=>Math.random() < 0.3)); // 1 dimentional array, with true/false values randomly generated
}
const gameReducer = (state = Immutable.fromJS({
width: 0,
height: 0,
grid: [],
}), {type, payload}) =>{
switch(type){
case START_GAME_ACTION: {
const {width, height} = payload;
const grid = generateGridArray(width, height);
return state
.set('width', width)
.set('height', height)
.set('grid', grid);
}
case TICK_ACTION: {
var t0 = performance.now();
const getAliveSiblings = makeAliveSiblingCounter(state);
const shouldCellBeAlive = makeShouldCellBeAlive(state);
const immutableGrid = state.get('grid');
const newGrid = immutableGrid.map((isCellAlive, idx)=>{
const numberAlive = getAliveSiblings(idx);
const isAlive = shouldCellBeAlive({isCellAlive, numberAlive});
return {
isAlive,
idx
}
})
.filter(({isAlive})=>isAlive!==null) // If the state hasn't changed, don't do anything. Ordinarily would be optional with flatmap
.reduce((acc,{isAlive, idx})=>{ // Generate the differential between the old a new grid (the changes)
return acc.set(idx, isAlive);
},immutableGrid);
var t1 = performance.now();
console.log(`Call to ${TICK_ACTION} took ${(t1 - t0)} milliseconds.`); // thanks MDN
return state.set('grid', newGrid);
}
default:
return state;
}
};
// ************************
// Styled Components for DOM
// ************************
const StyledCell = styled.default.div`
background: ${({isAlive})=>isAlive ? 'black' : 'white'};
`;
const StyledPetridish = styled.default.div`
width: 100vw;
height: 100vh;
border: 1px solid black;
display: grid;
grid-template-columns: repeat(${props=>props.width}, 1fr);
grid-auto-rows: 1f;
`;
// ************************
// Game class for React
// ************************
const generateCellComponent = (CellComponent, stateIndex) =>{
// TODO: I have my doubts if this is doing anything useful
const getGrid = createSelector(
(state) => state.gameReducer.getIn(['grid',stateIndex]),
(isAlive) => isAlive);
const cellComponentMapStateToProps = (state)=> {
return {
isAlive: getGrid(state)
};
};
return connect(cellComponentMapStateToProps)(CellComponent);
}
class Game extends React.PureComponent {
state = {petridish : [], width: 0}
static getDerivedStateFromProps({width, height}){
const petridish = [...Array(width * height)].map((_,idx)=>{
const Cell = generateCellComponent(StyledCell, idx)
return <Cell key={idx} />;
});
return {
petridish,
width
}
}
render(){
return (
<StyledPetridish width={this.state.width}>
{
this.state.petridish
}
</StyledPetridish>);
}
}
const gameMapStateToProps = ({gameReducer})=> {
return {
width:gameReducer.get('width'),
height: gameReducer.get('height')
};
}
const ConnectedGame = connect(gameMapStateToProps)(Game);
// ************************
// React/Redux/DOM setup
// ************************
const rootReducer = combineReducers({
gameReducer
});
const store = createStore(
rootReducer,
applyMiddleware(logger));
ReactDOM.render(
<Provider store={store}>
<ConnectedGame />
</Provider>,
document.getElementById('mount')
);
// ************************
// Triggers for game and tick cycle
// ************************
store.dispatch(startGame(50,10));
timer(500,500).subscribe(_=>store.dispatch(gameTick()));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.0/redux.min.js"></script>
<script src="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.7/react-redux.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/redux-logger@3.0.6/dist/redux-logger.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.0/rxjs.umd.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.2/immutable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/3.0.1/reselect.js"></script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment