Update on Age of Sigmar Reminders - Three Months Later

19 Aug 2019
AoS Reminders - Mobile UI

Three months ago, I posted on my website about a tool I had built for my Warhammer army. At the time, it only supported one army - Seraphon - and it only had a few basic features. I received an overwhelming amount of positive feedback from Lustria Online, TGA, and Reddit.

It looked like this at the time: Note the multiple dropdowns. Yuck!

It seemed like everyone wanted me to add their army, ASAP. No problem, except… there are a lot of armies in Age of Sigmar. People also wanted to be able to add spells, endless spells, realmscapes, soup armies, command traits, triumphs, and more.

What is AoS Reminders?

For those who aren’t familiar, Warhammer: Age of Sigmar is the successor to Warhammer Fantasy. Battles are fought over the course of five turns. Each turn consists of your round, and your opponent’s round. A single round has the following phases:

  1. Hero Phase (spells, buffs, heals, etc)
  2. Movement Phase
  3. Shooting Phase
  4. Charging Phase
  5. Combat Phase
  6. Battleshock Phase (see if anyone runs away after losing casualties)

Each of those phases is further divided into three sections:

  1. Start of Phase
  2. During Phase
  3. End of Phase

Let’s say we have a powerful artifact equipped by our general. A typical artifact may have rules that specifically activate at the “start of the combat phase” and “during the battleshock phase”.

A Wizard may have a spell that is cast during the Hero phase, but affects units during the Movement and Shooting phase.

One of the things about Warhammer is that you can’t “go back” during a phase. If you start using your rules that are triggered during combat, you can’t go “oh, wait, I need to use this start of combat rule first!” This makes remembering your rules of paramount importance.

As you might imagine, the amount of rules for a large army can be staggering - and a lot of Warhammer battles boil down to endlessly consulting your rulebook to make sure you’re not missing a rule. However, there’s not a single Warhammer player on the planet that hasn’t exclaimed “Awww shit, I forgot to activate a rule last phase!”

A lot of players build custom spreadsheets and handwritten notes reminding them when to use certain abilities. I decided to overengineer my solution a little bit by building AoS Reminders.

Starting to grow the codebase

I started to realize that building this thing was a daunting task after I added my first few armies to the application. I was pretty pleased at this point - I had a decently architected codebase, and data entry was fairly painless. For the most part, adding an army just worked.

However, as I moved through the first four armies, I started to notice there were units, abilities, and rules that didn’t exactly match my early mental model of how an AoS army worked. This meant that I had to add more logic and options to the code.

At this point, I wasn’t using any sort of store management - the app is written in React - all of the application state was simply stored in my App.tsx file and passed down to child elements when necessary. I started to have tons of handlers for updating state littered across the codebase. Which obviously begs the question…

Why no Redux at this point?

It just didn’t feel necessary when I was first building out the page. Remember, this started out as a toy application that handled one use case - letting me dynamically build a list of reminders for my Seraphon army.

Well, we need Redux now!

With the amount of information that I was tucking into my state, the whole app was re-rendering constantly whenever a single change was made. I started out by building my Redux store the old fashioned way - which I hate doing. And then - I found Redux Starter Kit - and my whole life changed.

Redux Starter Kit comes with a ton of features out of the box. It automatically manages different slices of your state, provides actionHandlers, selectors, and reducers, and uses Immer to produce immutable state updates. It was super easy to install and use, and it only took me one night to convert the application from local state management to a Redux store.

Contributors to the rescue

Whenever I posted my latest progress on Reddit or other forums, I got essentially two pieces of feedback:

  1. “Looks great, this is awesome!”
  2. “Can you add my army?”

I spent night after night adding Sylvaneth, Gloomspite Gitz, and Idoneth Deepkin, and I started to think… this is going to be nearly impossible to pull off alone. I started changing the tone of my forum posts, soliciting help and reminding people that they could add their army with only a basic level of dev experience.

My first contributor added a patch to the codebase 12 days after I released it on Reddit. I was thrilled - no one had ever contributed to any of my side projects before. I quickly wrote up a better README, created a sample army for contributors to base their armies on, and added a contribution guide. I created a Discord channel after someone asked me where they could chat with me.

Before long, I had quite a few contributors, an active Discord, and thriving Github activity. It seemed like every morning I woke up to a couple PR’s and tons of new issues. I was trying to keep ahead of the curve - I added pre-commit tooling to the codebase, tests, and helper utilities to check code quality. I was also making sure to properly type everything using Typescript - this helped ensure that contributors were entering data in the correct format.

After a while, I was able to almost entirely give away the task of data entry - people were submitting code for their favorite armies, spells, and abilities constantly. I began to get a little overwhelmed with requests for features - all of them good, but I was the only person really working on the React side of things.

As of today, I am really proud of the fact that we have fourteen contributors to the project, some of which have contributed thousands of lines of data and code. I am enormously grateful to each and every one of them.

We’ve currently closed 143 pull requests and 119 issues. We have 7 issues currently open. If you’re feeling adventurous, why not check out our Github? We’d love to have you.

The evolution of the tech stack

This project started as a simple Create React App build (with Typescript, of course).

At this point, we’re using these libraries (and more) to accomplish our goals.

  • Typescript
    • Invaluable to any project, but especially great for a data-heavy app.
  • Immer
    • Used for updating state and performing immutable data operations.
  • Redux Starter Kit
    • Takes away a lot of the pain of setting up a Redux store.
  • react-select
    • Gives us those beautiful multi-select, searchable, clearable dropdowns.
  • husky, prettier, and pretty-quick
    • Handles pre-commit chores for us, ensures code quality is consistent.
  • Continuous Integration via Codeship
    • Builds and deploys our master branch to production automatically.

The entire project is written with React Hooks and functional components. There are no references to this, no ugly classes, and certainly no componentDidMount :)

Using Hooks made this project fun to write and debug. I really fell in love with useMemo when writing utilities to fetch army metadata - the average army has anywhere from 3,000 - 5,000 lines of rules associated with it. Quickly parsing those rules and delivering them to the user became a fun challenge for my coding skills.

P.S. Speaking of React, I’ll be going to React Conf 2019! Last year was a blast - send me an email if you’ll be there. Let’s meet up and talk about Hooks :)

Deployment of this project was initially handled by me running a basic bash script. The script ran yarn build and then uploaded the results to an S3 bucket. As the number of contributors and PR’s grew, an automated solution became more and more necessary. Enter Codeship! They offer 100 free builds a month, a great UI, and easy integration with Github. I really enjoy using them.

When a PR is submitted, approved, and squashed and merged to master, Codeship automatically kicks off a build - it checks for the latest dependencies, runs yarn build, and if there are no errors, deploys the result to an S3 bucket. It’s fast, easy, and most of all, automated!

Success of the project

In the span of three months, the page has received 34,000 pageviews (the real number is higher - I’m sure a large portion of the gaming community blocks the Google Analytics script).

It’s been shared across Reddit, Twitter, Facebook, and a number of smaller communities. It supports a ton of armies, scenery, and special rules.

It has been a labor of love for me and my contributors, and I’ll list some of our features here:

Supported Armies

  • Beastclaw Raiders
  • Beasts Of Chaos
  • Chaos
  • Daughters Of Khaine
  • Dispossessed
  • Death
  • Destruction
  • Everchosen
  • Flesh Eater Courts
  • Fyreslayers
  • Gloomspite Gitz
  • Gutbusters
  • Idoneth Deepkin
  • Ironjawz
  • Kharadron Overlords
  • Khorne
  • Legions Of Azgorh
  • Legions Of Nagash
  • Lethisian Army
  • Mercenary Companies
  • Nighthaunt
  • Nurgle
  • Order
  • Seraphon
  • Skaven
  • Slaanesh
  • Slaves To Darkness
  • Stormcast Eternals
  • Sylvaneth
  • Tamurkhans Horde
  • Tzeentch

Features

  • Units
  • Battalions
  • Spells
  • Endless Spells
  • Command Traits
  • Command Abilities
  • Allegiance Abilities
  • Realmscape Artifacts
  • Realmscape Spells
  • Realmscape Features
  • Grand Alliance Spells
  • Grand Alliance Artifacts
  • Triumphs
  • Allied Armies
The current desktop UI

If you haven’t used AoS Reminders yet, go try it!

Next Steps

At this point, the bulk of the work is done. We’ve added all of the current Age of Sigmar armies, and even a few that aren’t officially supported.

We have a few UI things we’d like to change around. We’re going to add drag and drop capabilities.

One of my most frequent requests is for a native iOS/Android application. I may tackle this down the line, once all of the current issues are resolved.

I could foresee this expanding to Warhammer 40k - however, as someone who’s never played a game of 40k, it is a daunting thought!

Conclusion

When I started writing this codebase, I thought it would just be a fun little indulgence, like most of the code I write. I never thought I would have over a dozen contributors, tens of thousands of users, and fans all over the world. It has really been great!

Cheers, everyone!


Dread Saurian

18 Aug 2019

Three months after getting this beautiful model for my birthday, I’m calling it finished. Here’s my Dread Saurian.

Photo Credit: NOVA Open Capital Palette

Dread Saurian - Color Scheme by Amanda Killingsworth

Dread Saurian - Color Scheme by Amanda Killingsworth

Dread Saurian

Dread Saurian

Dread Saurian Dread Saurian
Dread Saurian Dread Saurian

Using Proxy() in Javascript to Detect Changes

01 Aug 2019

I ran into an issue with my side project AoS Reminders recently where I was mutating an object that wasn’t supposed to be mutated.

I was having trouble tracking down the mutation, until I stumbled upon the Proxy object, a helpful tool for any Javascript developer.

const obj = { key1: 'value', key2: 'value2' }

const handler = {
  get(target, key) {
    console.log('Reading value from ' + key)
    return target[key]
  },
}

const proxiedObj = new Proxy(obj, handler)

// When we run this `if` statement, console will log
// 'Reading value from key1'
if (proxiedObj.key1) { }

This can be useful for detecting when your objects are being accessed.

Another great example of using Proxy is for object validation:

// Use the proxy handler as a validator
const validator = {
  set: (obj, prop, value) => {
    // Validate that we passed in a correct `type` value
    if (prop === 'type') {
      const validTypes = ['Mage', 'Barbarian', 'Archer']
      if (value === null) {
        throw new TypeError('Cannot set to type to null');
      }
      if (!validTypes.includes(value)) {
        throw new TypeError('Cannot set to type to ' + value);
      }
    }

    // Store the new value in your object
    obj[prop] = value;

    // `set` expects a boolean response if successful
    return true;
  }
};

let char = new Proxy({age: 40, stars: 5}, validator);

char.name = 'Dennis'      // Other keys are not validated
char.type = 'Golden God'  // Throws `Cannot set to type to Golden God`
console.log(char.type)    // undefined
char.type = 'Mage'        // Passes validation
console.log(char.type)    // 'Mage'
char.type = null;         // Throws `Cannot set to type to null`
console.log(char.type)    // 'Mage'

While useful, you should really be using Typescript and enforcing your object keys that way ;)

Hope this helps someone! Just thought I’d share a small, neat feature of JS I hadn’t used before.


The Case for Using Immer

26 Jul 2019

In this post, I’d like to convince you to use Immer, 2019’s hottest JavaScript framework.

What is Immer?

Immer is a tiny package that allows you to work with immutable state in a more convenient way.

The basic idea is that you will apply all your changes to a temporary draftState, which is a proxy of the currentState. Once all your mutations are completed, Immer will produce the nextState based on the mutations to the draftState. This means that you can interact with your data by simply modifying it while keeping all the benefits of immutable data.

Why Immer?

We write a lot of boilerplate when using Redux and updating our store. Every developer familiar with even remotely complex reducers has seen this pattern before:

const someReducer = (state, action) => {
  const newEntry = { 
      ...state.entries[action.payload.id], 
      completed: true 
  }
  return {
      ...state, 
      entries: { 
          ...state.entries, 
          [action.payload.id]: newEntry 
      } 
    }
}

All that code just to set one entry to completed: true! This “extra” destructuring work is required because Redux needs us to return a complete copy of the state.

What if we could just modify the entry we want, and Redux could figure out the rest? Well, that may be expecting too much of Redux, but luckily, we can now use Immer!

With Immer, the above code would look like:

import produce from "immer"

const someReducer = produce((state, action) => {
  state.entries[action.payload.id].completed = true
})

From a developer point of view, I think the benefits are obvious. We’re no longer spending time trying to destructure and then restructure state. We’re just performing a simple operation and moving on with our lives.

Generic Examples

To begin with, what does an Immer function even look like?

Here’s an example:

import produce from 'immer'

const firstState = {}

const secondState = produce(firstState, draft => {
  // empty function
})

console.log(secondState === firstState) // true

// Let's begin using Immer by adding a { 'key': 'value' } pair to the state
const thirdState = produce(firstState, draft => {
  draft['key'] = 'value'
})

console.log(firstState) // {}
console.log(thirdState) // { key: 'value' }

// Now we get to see the magic of Immer
const fourthState = produce(thirdState, draft => {
  delete draft['key']
  draft['newKey'] = { one: 'two' }
  draft['rubicon'] = 'awesome'
})

// thirdState is unmodified, of course!
console.log(thirdState) // { key: 'value' }
console.log(fourthState)
// {
//   newKey: { one: 'two' },
//   rubicon: 'awesome'
// }

Okay, let’s do something a little more realistic. Let’s pretend we’re building an app where users can check a certain field. A todo list or grocery shopping app would make sense.

import produce from 'immer'

const state = [
  {
    id: 1,
    checked: false
  },
  {
    id: 2,
    checked: false
  }
]

const result = produce(state, draft => {
  draft[0].checked = true
  draft.push({ id: 3, checked: false })
})

console.log(result)

// [
//   { id: 1, checked: true },
//   { id: 2, checked: false },
//   { id: 3, checked: false }
// ]

Real World Examples

I think you’re probably starting to get the idea. But let’s get into why this would work for advanced applications where you’re using and abusing Redux heavily.

Here is a real example of a very painful piece of code that we had written at work. We have a UX flow that allows the user to change the height of a displayed chart. The only thing this action handler wants to do is update the height property of a specific chart. But unfortunately, we have to fully recreate the state via destructuring - and it gets ugly!

Before Immer
export const setChartHeightActionHandler = (state, { meta }) => {
  const { chartId, height } = meta
  const { byId } = state

  const chartObject = { ...byId[chartId] }
  const { chartDimensions } = chartObject
  const thisChartDimensions = chartDimensions[chartId]

  return {
    ...state,
    byId: {
      ...byId,
      [chartId]: {
        ...chartObject,
        chartDimensions: {
          ...chartDimensions,
          [chartId]: {
            ...thisChartDimensions,
            height // This is literally the only change made
          }
        }
      }
    }
  }
}

Ugh! That is pretty gross. Let’s use Immer instead.

After Immer
import produce from "immer"

export const setChartHeightActionHandler = produce((state, { meta }) => {
  const { chartId, height } = meta
  state.byId[chartId].chartDimensions[chartId].height = height
})

Boom! The exact same functionality as above.

(If this example doesn’t win you over, I have nothing to offer you.)

Guess which one is easier to understand? Guess which one doesn’t take 15 minutes and two developers to write? (True story).

Let’s refactor another action handler - this one deletes a single entry in the store.

Before Immer
export const deleteExperimentActionHandler = (state, { meta }) => {
  const { experimentId, projectId } = meta

  if (!state.byId[experimentId]) return state

  const byId = {
    ...state.byId
  }
  delete byId[experimentId] // The only operation actually needed

  return {
    ...state,
    byProject: {
      [projectId]: Object.values(byId)
    },
    byId
  }
}
After Immer
import produce from "immer"

export const deleteExperimentActionHandler = produce((state, { meta }) => {
  // Returning undefined is the same as saying "keep the same state"
  if (!state.byId[meta.experimentId]) return
  delete state.byId[meta.experimentId]
})

Is that super dope or what? We accomplish the same task with two lines of code.

Caveats

Immer does have a downside - namely, it’s about twice as slow as a well-written reducer. Immer recommends not using this library when operating on tens of thousands of objects - otherwise, the performance difference is neglible.

Summary

Use Immer wherever it makes sense to you. It’s easy to start with, and the developer benefits will become clear immediately. Your functions will also become easier to test and debug, since you’ll be rid of the noise that comes along with re-creating state all the time!

For those of you using TypeScript - the Immer library is strongly typed! Very cool!