Random musings on programming, mostly.
React
My final Flatiron project looms nigh. What to build? I’ve decided to challenge myself and build a clone of the popular board game Pandemic. I’m likely jumping in way over my head, but what better way to learn. This blog will hopefully highlight a small portion of the design process involved in a fairly complex board game built using React, Redux and other frameworks as necessary. For those of you unfamiliar with the board game Pandemic it is a team based effort (2-4 players), either the team wins or loses, to eradicate all 4 of the diseases represented by the Pandemic game. The disease are represented by color: black, blue, red, and yellow. The players win when they find cures for all four diseases. The players lose when one of several things happen: the Player Deck has no more cards, there are no more cubes to place for a particular color of disease, or if the outbreak marker reaches the last spot of the outbreak meter. Pandemic is a complex game that requires a complex game state. Thankfully Redux comes to the rescue, Redux allows us to store a complex state and filter it with reducers. Let’s consider our use-case and think about how to design our reducers.
What data do we need to represent to create our game state? Maybe a better way of asking the question is what information does the board represent when playing on a physical board? If you are familiar with Pandemic, a few pieces of information necessary to represent the game state will probably jump out at you. Pandemic has a Player Deck, an Infection Deck, and a Map (our Map contains further state information, for example, Player Position, Research Stations, etc…) as most basic requirements. This information allows us to create reducers that provide us with the information necessary to render any react component we choose to implement. Several reducers jump right out at us, including, PlayerDeckReducer, InfectionDeckReducer, MapReducer (later to be renamed CitiesReducer, since the map tiles are not updated but city information will be), and probably a GameState or GameStatus reducer to keep track of the game status ( i.e. gameOver, diseasesCured,…). See an example reducer below.
import update from 'react-addons-update' export function gameStatusReducer(state={ red: 'Uncured', black: 'Uncured', blue: 'Uncured', yellow: 'Uncured', phase: 'Move', isGameOver: false, winner: null, isGameEndModalOpen: false, isLoginModalOpen: true, isLoggedIn:false, user: null, id: null, token: null }, action) { switch(action.type) { case 'CURE_DISEASE': return update(state, {[action.color]: {$set: 'Cured'}}) case 'SET_GAME_PHASE': return update(state, {phase: {$set: action.phase}}) case 'END_GAME': return update(state, {isGameOver: {$set: true}}) case 'OPEN_END_GAME_MODAL': return update(state, {isGameEndModalOpen: {$set: true}}) case 'CLOSE_END_GAME_MODAL': return update(state, {isGameEndModalOpen: {$set: false}}) case 'OPEN_LOGIN_MODAL': return update(state, {isLoginModalOpen: {$set: true}}) case 'CLOSE_LOGIN_MODAL': return update(state, {isLoginModalOpen: {$set: false}}) case 'LOGIN_SUCCESS': return Object.assign({}, state, {isLoggedIn: true, user: action.email, token: action.token, id: action.id}) case 'LOGOUT': return Object.assign({}, state, {isLoggedIn: false, user: null, token: null, id: null, isLoginModalOpen: true}) default: return state } }
cityCards is just a file containing an array of cards, and prepInfectionDeck is a helper method that shuffles and injects infection cards into the deck. Reducer functions must have an initial state passed to them, in this case it’s just some common sense default values. It must also be passed an action so it knows what to do. If action.type does not match any of the cases it simply returns the state with no changes and the action is filtered through any other reducers we have. If action.type is any of the cases in the switch it returns the new state, in a way that is pure, that is it does not alter the state passed in in anyway, it must return a new state. If this concept is difficult to grasp don’t worry, it’s hard, even when you do understand what a pure function is it may be tricky to actually implement. In this case I use react-addons-update and Object.assign() to modify the state in a pure way.
As this project evolved need for the ability to issue actions in response to actions previously issued became clear. Enter Redux-Saga redux saga allows us to ‘listen’ for defined actions and respond accordingly, with more actions, if necessary. While game logic may not be the use-case that redux-saga was designed for, it works well in this situation. When an action is dispatched we can create sagas that listen for particular actions. These actions work there way through the root reducer and any child reducer as they normally would but they are also intercepted by the saga. Here is where we test game logic and respond with actions to dispatch in kind. Take a peek at the code below. Sagas are tricky, mostly becuase they look funny. Sagas are generator functions. These functions yield a certain result and may or may not be called again to pick up where they left off. Our sagas will listen for certain key actions, for example, when a player draws cards from the Player Deck. This indicates that the Move phase is done and that it’s time to advance the game. We need to do several things at this point: determine whether the current player has any epidemic cards in hand and deal with them accordingly, next determine if the hand needs to discard because it has more than 7 cards, then it needs to handle the Infection stage. But before doing those checks it probably makes sense to check if the game is over.
import { select, takeEvery, put, call } from 'redux-saga/effects' import { setGamePhase, endGame, openEndGameModal } from '../actions/ActionCreators' const getCurrentPlayer = state => state.playersReducer.find(player => player.currentPlayer === true) const getPlayerDeck = state => state.playerDeckReducer const getGameStatus = state => state.gameStatusReducer export function* nextPhaseSage() { yield takeEvery('DRAW_PLAYER_CARDS', checkEndGame) yield takeEvery('DRAW_PLAYER_CARDS', nextPhase) } export function* checkEndGame(action) { const playerDeck = yield select(getPlayerDeck) const gameStatus = yield select(getGameStatus) if (playerDeck.length === 0) { yield put(endGame({winner: 'Diseases'})) yield put(openEndGameModal()) } if (gameStatus.red === 'Cured' && gameStatus.black === 'Cured' && gameStatus.blue === 'Cured' && gameStatus.yellow === 'Cured') { yield put(endGame({winner: 'Players'})) yield put(openEndGameModal()) } export function* nextPhase(action) { const currentPlayer = yield select(getCurrentPlayer) if(currentPlayer.hand.filter(card => card.name === 'Epidemic').length > 0 ) { yield put(setGamePhase('Epidemic')) } else if((currentPlayer.hand.filter(card => card.name !== 'Epidemic').length > 7 )) { yield put(setGamePhase('Discard')) } else { yield put(setGamePhase('Infect')) } } }
So as you can see above we have direct access to our Redux state in our sagas, we use this to our advantage by checking various conditions in the state to determine the next action. We DO NOT change state here though, that is a job for the reducers. We simply listen for certain actions, in this case DRAW_PLAYER_CARDS and then decide which actions to dispatch in response, if any.
While reducers and sagas are only a small portion of my app. They are arguably the most important portions. They both interact with each other to form the logic of the game. Both reducers and sagas are challenging to implement. Reducers mostly due to the need to be pure, and sagas mostly due to the fact that generator functions and yields are often strange concepts to developers who have never seen them before. Hopefully you got some useful information. Please feel free to look at the repo for the entire game.