Skip to main content
utils-book provides the functions that drive the core game loop: receiving a book (JSON payload) from the RGS and processing its events array one-by-one in order.

createPlayBookUtils()

Factory function that returns playBookEvent and playBookEvents. You pass it the bookEventHandlerMap for your game once at startup.
import { createPlayBookUtils } from 'utils-book';

export function createPlayBookUtils<
  TBookEventHandlerMap extends BookEventHandlerMap<any, any>
>({
  bookEventHandlerMap,
  debug,
}: {
  bookEventHandlerMap: TBookEventHandlerMap;
  debug?: boolean;
})

Parameters

ParameterTypeDescription
bookEventHandlerMapBookEventHandlerMap<TBookEvent, THandlerContext>Map from bookEvent.type string to an async handler function.
debugboolean?When true, logs each bookEvent to the console before dispatching it.

Return value

{
  playBookEvent:  (bookEvent: TBookEvent, bookEventContext: TBookEventContext) => Promise<void>;
  playBookEvents: (bookEvents: TBookEvent[], bookEventContext?: BookEventContextFromMapWithoutBookEvents) => Promise<void>;
}

playBookEvents()

Processes an entire book.events array, resolving each handler one after another using sequence() from utils-shared. The order of events is critical — it determines the visual sequence of the game (e.g. spin before win reveal).
const playBookEvents = async (
  bookEvents: TBookEvent[],
  bookEventContext?: BookEventContextFromMapWithoutBookEvents,
) => {
  const finalBookEventContext =
    bookEventContext || ({} as BookEventContextFromMapWithoutBookEvents);

  await sequence(bookEvents, async (bookEvent) => {
    await playBookEvent(bookEvent, { ...finalBookEventContext, bookEvents });
  });
};
bookEvents is automatically injected into the context of every handler call, so handlers can inspect the full event list.
sequence() from utils-shared resolves async functions one-after-another, unlike Promise.all() which fires them simultaneously. This is essential for maintaining the correct animation order.

playBookEvent()

Dispatches a single bookEvent to the matching handler in bookEventHandlerMap. If no handler is found for bookEvent.type, an error is logged.
const playBookEvent = async (
  bookEvent: TBookEvent,
  bookEventContext: TBookEventContext,
) => {
  const bookEventHandler = bookEventHandlerMap?.[bookEvent.type];
  if (bookEventHandler) {
    if (debug) console.log(bookEvent);
    await bookEventHandler(bookEvent, bookEventContext);
  } else {
    console.error('Missing bookEventHandler in "bookEventHandlerMap" for: ', bookEvent);
  }
};
This is also used directly by the MODE_<GAME_MODE>/bookEvent/<TYPE> Storybook stories to test individual events in isolation.

Types

// A bookEvent must always have these two fields
export type BaseBookEvent = { index: number; type: string };

// A handler for a single bookEvent type
export type BookEventHandler<
  TBookEvent extends BaseBookEvent,
  THandlerContext extends object,
> = (bookEvent: TBookEvent, context: THandlerContext) => Promise<void>;

// The full map: keys are bookEvent.type strings
export type BookEventHandlerMap<
  TBookEvent extends BaseBookEvent,
  THandlerContext extends object,
> = Record<string, BookEventHandler<TBookEvent, THandlerContext>>;

// Helper to extract the BookEvent type from a map
export type GetBookEventFromMap<T extends BookEventHandlerMap<any, any>> =
  T extends BookEventHandlerMap<infer U, any> ? U : never;

// Helper to extract the context type from a map
export type GetBookEventContextFromMap<T extends BookEventHandlerMap<any, any>> =
  T extends BookEventHandlerMap<any, infer U> ? U : never;

Real-world usage

// apps/lines/src/game/bookEventHandlerMap.ts

export const bookEventHandlerMap: BookEventHandlerMap<BookEvent, BookEventContext> = {
  reveal: async (bookEvent) => {
    await eventEmitter.broadcastAsync({ type: 'boardSpin', board: bookEvent.board });
  },
  setTotalWin: async (bookEvent) => {
    eventEmitter.broadcast({ type: 'totalWinUpdate', amount: bookEvent.amount });
  },
  updateFreeSpin: async (bookEvent) => {
    eventEmitter.broadcast({ type: 'freeSpinCounterShow' });
    eventEmitter.broadcast({
      type: 'freeSpinCounterUpdate',
      current: bookEvent.amount,
      total: bookEvent.total,
    });
  },
};

// Create utilities once at game startup
export const { playBookEvent, playBookEvents } = createPlayBookUtils({
  bookEventHandlerMap,
  debug: import.meta.env.DEV,
});

createMultiBookUtils()

For games that receive multiple books simultaneously (e.g. multi-way games), createMultiBookUtils provides helpers to split, merge, and re-assemble books into rounds.
import { createMultiBookUtils } from 'utils-book';

const { booksToRounds, roundsToBooks } = createMultiBookUtils();

booksToRounds()

Splits multiple books into parallel rounds aligned by a splitBy event type.
booksToRounds<TBookEvent>({
  books: TBookEvent[][],  // array of books (one per game mode)
  splitBy: TBookEvent['type'][],  // event type(s) that delimit round boundaries
}): TBookEvent[][][]

roundsToBooks()

Reassembles rounds back into per-book arrays:
roundsToBooks<TBookEvent>({
  rounds: TBookEvent[][][],
}): TBookEvent[][]

Build docs developers (and LLMs) love