Skip to content

maurice/yay-machine

Repository files navigation

Logo

Doggie

yay-machine is a modern, simple, lightweight, zero-dependency, TypeScript state-machine library.


Why you should learn and use state-machines

learn and use repeat state-machines

State-machines have many desirable qualities, eg

  • they are versatile, and can model many different problem domains
  • they are declarative, focusing on the important "what", "when", "how" and "why" questions
  • they are concise, conveying a lot of information in a small amount of co-located code
  • they are extremely predictable
  • and more

Read the Why state-machines? article for more on this.

yay-machine is a TypeScript state-machine library for everyone

Unlike other state-machine libraries that are complex and vast, or overly simplistic, yay-machine gives you power and flexibility without cognitive load.

Read the About yay-machine article for more on this.

Install

npm add yay-machine

Example

Define the machine at compile-time

πŸ’‘ View this example's source and test on GitHub

// guessMachine.ts
import { defineMachine } from "yay-machine";

interface GuessState {
  readonly name:
    | "pickNumber"
    | "playing"
    | "guessedCorrectly"
    | "tooManyIncorrectGuesses";
  readonly answer: number;
  readonly numGuesses: number;
  readonly maxGuesses: number;
}

interface GuessEvent {
  readonly type: "GUESS";
  readonly guess: number;
}

interface NewGameEvent {
  readonly type: "NEW_GAME";
}

const incrementNumGuesses = ({
  state,
}: {
  readonly state: GuessState;
}): GuessState => ({
  ...state,
  numGuesses: state.numGuesses + 1,
});

/**
 * Guess the number from 1 to 10
 */
export const guessMachine = defineMachine<
  GuessState,
  GuessEvent | NewGameEvent
>({
  initialState: { name: "pickNumber", answer: 0, numGuesses: 0, maxGuesses: 5 },
  states: {
    pickNumber: {
      always: {
        to: "playing",
        data: ({ state }) => ({
          ...state,
          answer: Math.ceil(Math.random() * 10),
          numGuesses: 0,
        }),
      },
    },
    playing: {
      on: {
        GUESS: [
          {
            to: "guessedCorrectly",
            when: ({ state, event }) => state.answer === event.guess,
            data: incrementNumGuesses,
          },
          {
            to: "tooManyIncorrectGuesses",
            when: ({ state }) => state.numGuesses + 1 === state.maxGuesses,
            data: incrementNumGuesses,
          },
          {
            to: "playing",
            data: incrementNumGuesses,
          },
        ],
      },
    },
    guessedCorrectly: {
      on: {
        NEW_GAME: { to: "pickNumber", data: ({ state }) => state },
      },
    },
    tooManyIncorrectGuesses: {
      on: {
        NEW_GAME: { to: "pickNumber", data: ({ state }) => state },
      },
    },
  },
});

Create instances and operate them at run-time

import assert from "assert";
import { guessMachine } from "./guessMachine";

// create and start a new instance of the guess game machine
const guess = guessMachine.newInstance().start();
assert(guess.state.name === "playing");

// subscribe to the machine's state as it changes
const unsubscribe = guess.subscribe(({ state }) => {
  if (state.name === "guessedCorrectly") {
    console.log("game over: yay, we won :)");
  } else if (state.name === "tooManyIncorrectGuesses") {
    console.log("game over: boo, we lost :(");
  } else {
    return;
  }
  unsubscribe();
});

// play a single game
while (guess.state.name === "playing") {
  guess.send({ type: "GUESS", guess: Math.ceil(Math.random() * 10) });
}

Where next?