Random musings on programming, mostly.
Last blog post I discussed some of the strategies and techniques I used for building a board game using React. I talked mostly about reducers and redux-saga. That post can be read here.
Just to recap quickly though, I discussed how I came to design my reducers, briefly how reducers handle actions, and that reducers have to be pure, that is, they do not mutate the state. If you are still struggling with how to write pure functions I suggest taking a look at this. Finally we discussed sagas and how I used those to model much of my game logic. Sagas allow us to listen for actions that are dispatched to the redux store and then issue actions in response. This comes in very handy when, for example, a user is done moving her pawn. Our saga listens for that action, then inspects the user’s hand to determine if any cards need to be discarded or played, it then dispatches an action to reflect that.
Now that we are back up to speed, I’ve decided to shift gears and talk about a few of the UI elements I used. In particular Modals and Alerts. These are the icing on the cake, that sweet sugary goodness that makes the game look good when playing.
Modals are windows that essentially force themselves to the front. In my use case these come in handy when a user logins, so he or she can choose to start a new game or load a game. I also used a modal to signify the end of the game, this way the modal pops up indicating the game is over and allows the user to start a new game if they so choose. There are several Modal Component libraries out there, and even React-Bootstrap has them if you are so inclined. I didn’t want the entire React-Bootstrap library though so I chose to use React-Modal. The developers of React-Modal didn’t a brilliant job of creating components that are both easy to use and easy to customize. Usage is well layed out in the docs, but let’s take a look anyway. The following is the basic component I designed for my end game modal.
import React, { Component } from 'react' import Modal from 'react-modal' import { connect } from 'react-redux' import { closeEndGameModal, newGame } from '../actions/ActionCreators' const customStyles = { content : { top : '50%', left : '50%', right : 'auto', bottom : 'auto', marginRight : '-50%', transform : 'translate(-50%, -50%)', zIndex : 1, backgroundColor : 'black', textAlign : 'center', } }; class EndGameModal extends Component { afterOpenModal() { // references are now sync'd and can be accessed. } closeModal() { this.props.dispatch(newGame()) this.props.dispatch(closeEndGameModal()) } render() { return ( <div> <Modal isOpen={this.props.modalIsOpen} onAfterOpen={() => this.afterOpenModal()} onRequestClose={(e) => e.preventDefault()} style={customStyles} contentLabel="End Game Modal" > <h2>Game Over!</h2> <button onClick={() => this.closeModal()}>Play Again!</button> </Modal> </div> ); } } export default connect()(EndGameModal)
Most notably the component requires two props, isOpen and contentLabel. Content label, is simply a label for the component, it can be displayed on the modal but doesn’t have to be. isOpen is a boolean that tells the modal whether it is open or not. In most cases you could have left the state in the modal itself. I chose to put it in the redux store just like the rest of the state though, this allows our sagas to check the status of the modal and send actions in response. You can render these anywhere you like, I chose to render it on my Board component.
Alerts, in this case, are those spiffy little windows that pop up in the lower left corner when a city is infected. Again there are many different alerts available online if you don’t want to create your own. I chose React-Alert. This is another top notch component that is both easy to use and easy to customize to your liking. In fact these are so easy to use, a quick glance at the docs should get you up and running with no trouble, but let’s take a look anyway.
import Alert from 'react-s-alert' import 'react-s-alert/dist/s-alert-css-effects/jelly.css'; import './Alerts.css' ... <Alert stack= effect={'jelly'} timeout={5000} />
That’s pretty much it. Some custom css, a jelly effect that the developer of React-Alert provides for you if you like and the component itself. You should render these in the component that you want the alert to display in. In this case I chose App. This ensures the Alerts display in the bottom left corner of the entire window. So how do we trigger an alert? That is also about as simple as can be.
Alert.info(`Infection Phase: Placing ${color} cube in ${name}`,{ position: 'bottom-left' })
This is found in the infect saga. The program infects cities after the player is done moving and discarding and then displays an alert in the bottom-left of the screen saying which city and the color of the disease. Awesome!
I hope any of you who took the time to read this learned something or at least got some ideas for future projects. It has truly been a blast building this, although the work is not done by any means. Feel free to checkout the project on the links at the top of the page. I’d love to get some feedback, there is still much work to do and much refactoring, but the product to date has turned out much better than I had ever envisioned.
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.