Just an experiment with react..
A Pen by Miles Burton on CodePen.
Just an experiment with react..
A Pen by Miles Burton on CodePen.
<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> |