Using JS State Machines to fight monsters

Robots vs Monsters

What Are State Machines?

Wikipedia defines a finite-state machine (FSM) as:

An abstract machine that can be in exactly one of a finite number of states at any given time

The FSM can change from one state to another in response to some external inputs; the change from one state to another is called a transition

An FSM is defined by a list of its states, its initial state, and the conditions for each transition.

The IMportant Bits

  • A finite list of possible states and an initial state
  • The machine can be in exactly one state at any given time
  • Can change (transition) from one state to another in response to some external input

What is State?

A state is a description of the status of a system that is waiting to execute a transition

The Robots attack

Critical Hit

Roll

Double Click

Do Nothing

Use Tech

Check HP

Idle

Tech Effect

Attack

Play Attack

Play Special Attack

Play Destroyed

async function attack(robot, target) {
  if (!robot.isIdle) {
    return;
  }
  const roll = engine.rollAttack(robot);
  if (roll.isCritical) {
    await SpecialAttackAnimation(robot);
  } else {
    await AttackAnimation(robot);
  }
  robot.power > 0 ? PoweredIdleAnimation(robot) : IdleAnimation(robot);
}

async function useTech(robot, tech) {
  if (!robot.isIdle) {
    return;
  }
  const roll = engine.useTech(robot, tech);
  await TechAnimation(robot, tech);
  if (robot.hp <= 0) {
    await DestroyedAnimation(robot);
  } else {
    robot.power > 0 ? PoweredIdleAnimation(robot) : IdleAnimation(robot);
  }
}

5 flags (Boolean States)

  • Is HP below 0
  • Is Power above 0
  • Is the Attack Critical
  • Is the robot currently idle
  • Are we "awaiting" an animation end

32 Possible States

32 Possible States

async function attack(robot, target) {
  if (!robot.isIdle) {
    return;
  }
  const roll = engine.rollAttack(robot);
  if (roll.isCritical) {
    await SpecialAttackAnimation(robot);
  } else {
    await AttackAnimation(robot);
  }
  robot.power > 0 ? PoweredIdleAnimation(robot) : IdleAnimation(robot);
}

async function useTech(robot, tech) {
  if (!robot.isIdle) {
    return;
  }
  const roll = engine.useTech(robot, tech);
  await TechAnimation(robot, tech);
  if (robot.hp <= 0) {
    await DestroyedAnimation(robot);
  } else {
    robot.power > 0 ? PoweredIdleAnimation(robot) : IdleAnimation(robot);
  }
}

The Robots attack

Idle

Attacking

Critical Hit

Roll

Special
Attacking

Use Tech

Check HP

Destroyed

Using Tech

Attack

Powered Idle

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
  },
  states: {
    idle: {},
    attacking: {},
    specialAttacking: {},
    usingTech: {},
    destroyed: {},
  },
};
export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {},
    attacking: {},
    specialAttacking: {},
    usingTech: {},
    destroyed: {},
  },
};
export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {},
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    destroyed: {},
  },
};
export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {},
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {},
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {},
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        ATTACK: attack,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        ATTACK: attack,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTechLethal: {
      on: {
        ANIMATION_END: 'destroyed',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTechLethal: {
      on: {
        ANIMATION_END: 'destroyed',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        enter() {
          if (this.robot.power > 0) {
            this.state = 'poweredIdle';
          }
        },
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTechLethal: {
      on: {
        ANIMATION_END: 'destroyed',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  state: 'idle',
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        enter() {
          if (this.robot.power > 0) {
            this.state = 'poweredIdle';
          }
        },
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    poweredIdle: {
      on: {
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTechLethal: {
      on: {
        ANIMATION_END: 'destroyed',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  _state: 'idle',
  get state() {
    return this._state;
  },
  set state(value) {
    this._state = value;
    const state = this.states[this._state];
    if (state?.on?.enter) {
      state.on.enter.call(this);
    }
  },
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        enter() {
          if (this.robot.power > 0) {
            this.state = 'poweredIdle';
          }
        },
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    poweredIdle: {
      on: {
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTechLethal: {
      on: {
        ANIMATION_END: 'destroyed',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  _state: 'idle',
  get state() {
    return this._state;
  },
  set state(value) {
    this._state = value;
    const state = this.states[this._state];
    if (state?.on?.enter) {
      state.on.enter.call(this);
    }
  },
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        enter() {
          if (this.robot.power > 0) {
            this.state = 'poweredIdle';
          }
        },
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    poweredIdle: {
      on: {
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTechLethal: {
      on: {
        ANIMATION_END: 'destroyed',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  _state: 'idle',
  get state() {
    return this._state;
  },
  set state(value) {
    this._state = value;
    const state = this.states[this._state];
    if (state?.on?.enter) {
      state.on.enter.call(this);
    }
  },
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        enter() {
          if (this.robot.power > 0) {
            this.state = 'poweredIdle';
          }
        },
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    poweredIdle: {
      on: {
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTechLethal: {
      on: {
        ANIMATION_END: 'destroyed',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};
function attack(event) {
  const roll = engine.roll(event.robot);
  if (roll.isCritical) {
    this.state = 'specialAttacking';
  } else {
    this.state = 'attacking';
  }
}

function useTech(event) {
  const robot = this.robot;
  engine.useTech(robot, event.tech);
  if (robot.hp <= 0) {
    this.state = 'usingTechLethal';
  } else {
    this.state = 'usingTech';
  }
}

export let robotAttackMachine = {
  _state: 'idle',
  get state() {
    return this._state;
  },
  set state(value) {
    this._state = value;
    const state = this.states[this._state];
    if (state?.on?.enter) {
      state.on.enter.call(this);
    }
  },
  robot: { atk: 1, def: 0, hp: 1, power: 0 },
  dispatch(event) {
    const state = this.states[this.state];
    if (state.type === 'final') {
      return;
    }
    const transition = state.on[event.type];

    if (typeof transition === 'function') {
      transition.call(this, event);
    } else if (transition in this.states) {
      this.state = transition;
    }
  },
  states: {
    idle: {
      on: {
        enter() {
          if (this.robot.power > 0) {
            this.state = 'poweredIdle';
          }
        },
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    poweredIdle: {
      on: {
        ATTACK: attack,
        USE_TECH: useTech,
      },
    },
    attacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    specialAttacking: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTech: {
      on: {
        ANIMATION_END: 'idle',
      },
    },
    usingTechLethal: {
      on: {
        ANIMATION_END: 'destroyed',
      },
    },
    destroyed: {
      type: 'final',
    },
  },
};

The Robots attack

Idle

Attacking

Critical Hit

Roll

Special
Attacking

Use Tech

Check HP

Destroyed

Using Tech

Attack

Powered Idle

But that Was A lot of code...

Let's Take a break from all this and Have Some FUN

Clap Like it's Over

Now Go Crazy and Cheer Really Loud!!

Someone yell "Bravo!"

NOW SILENCE!!!

Now overly EXAGGERATED Laughter!!!

Good Job

&

Using JS State Machines to fight monsters

Using JS State Machines to fight monsters

By Adam L Barrett

Using JS State Machines to fight monsters

Finite State Machines are a powerful tool used to fight complexity in software. State Machines in JavaScript make our code more readable and maintainable. Let's learn about state machines and refactor a simple game where awesome battle robots fight horrible monsters from the deep.

  • 49