Random Redux

I’ve been using Redux (RC1) for a small weekend toy project. One thing I was really excited to try out was Dan Abramov’s new Redux-DevTools project on top of Redux.

The project is a moderately complex solitaire card game called Onirim. I figured it’d be a perfect candidate to use to take Redux/ReactDnD/etc for a spin.

The way the Redux DevTools work is that they basically replay the entire history of actions in the session every time React Hot Loader detects a code change.
(Actually it seems it does it for every action at the moment!)

So that’s very cool for rapid development (check out Dan’s React Europe talk talk for a mind blowing demo, and general info on Redux/React Hot Loader if you aren’t familiar) — However, Onirim involves a lot of shuffling, and very quickly weird stuff started to happen. Cards would be swapping around randomly and the app would end up in a broken state due to rules being broken or actions being prevented due to the different cards in hand/etc.

Shuffling uses Math.random().

Picard Facepalm

So I thought about it a bit, and I realized that I can simply fake the random number generator while using the Redux DevTools so that it takes numbers from a specified set of pre-generated numbers, and every time the actions are re-run, we just reset the index of the number to take.

It’s kind of ridiculous how simple it was to make such a dirty hack to my code and fix all of the randomization problems.

Fake Random Numbers

So here it is:

const NUMS = [
  // ~2000 pre-generated random numbers
  // which is probably enough... depends on your app
]
const MAX = NUMS.length

Math.__fakeRandomIndex = 0
Math.random = function() {
  return NUMS[Math.__fakeRandomIndex++ % MAX]
}

You can generate the numbers just once and use them every time or you could generate them on page-load so you can get a new game (or whatever) when you want one.

If you want the same set every time just do something like this, and paste the output as NUMS above:

JSON.stringify((
   m = 2000, n = new Array(m),
   function() { for(i=0;i<m;i++) { n[i] = Math.random() } }(),
   n
))

There are other ways to approach this part, too. For instance see this StackOverflow question for some discussion. Personally I prefer running the real Math.random and just re-using the results. The others suffer from annoyances such as being biased toward 0 and 1 or requiring an external dependency.

The Reducer

Now we need to hook that up to Redux so that no matter how many times your actions are re-run, or DevTools restores the state from localStorage, or you commit and rollback, the shuffling always results in the exact same card order. We can simply compose in a new reducer that handles the state of the fake random index:

export default function fakeRandom( state = 0, action ) {
  console.log( 'Resetting Math.random()!' )
  return Math.__fakeRandomIndex = state
}

Simplest reducer ever? Just reset it if Redux doesn’t give a value. Otherwise use the value we’re passed.

Notice that Redux is actually doing all the work here. We’re exploiting the fact that we’re passed either the current state or undefined. Ultimately we only care about the undefined value which we use to reset the faker to 0. Since we’re re-running all of the actions every time no matter what, we don’t really need to care what the state is in between each action (or bother updating it). We just need to make sure it starts at 0.

Finally of course we have to actually use this reducer. That’s easy, we just compose it in with our other reducers:

import { combineReducers, compose, createStore } from 'redux'
import { devTools } from 'redux-devtools'

// our reducers
import fakeRandom from 'reducers/fake-random'
import game from 'reducers/game'

/*                 this is the important bit ↓       */
const reducer = combineReducers( { game, fakeRandom } )
const creator = compose( devTools(), createStore )
const store = creator( reducer )

That’s it! If you do something like this make sure to disable it before building your production bundle! Otherwise you’ll be facepalm-ing all over again :)