Skip to content

regal/regal

Repository files navigation

Regal

npm version CircleCI Coverage Status code style: prettier

Introduction

The Regal Game Library is the official TypeScript library for the Regal Framework, a project designed to help developers bring text-driven games and story experiences to players in exciting new ways.

What is the Regal Framework?

The Regal Framework is a set of tools for developers to create text-driven games, sometimes called Interactive Fiction (IF), as pure functions.

For more information, check out the project's about page.

How is the Regal Game Library Used?

The Regal Game Library, often referred to as the Game Library or its package name regal, is the JavaScript library that game developers use to create games for the Regal Framework. It was designed with first-class support for TypeScript, but doesn't require it.

When a game is created using the Regal Game Library, it can be played by any Regal client automatically.

What's the point?

Similar to Java's "write once, run anywhere" mantra, the goal of the Regal Framework is to produce games that can be played on all kinds of platforms without needing to rewrite any code.

The name Regal is an acronym for Reinventing Gameplay through Audio and Language. The project was inspired by the idea of playing adventure games on smart voice assistants, but it doesn't have to stop there. Chatrooms, consoles, smart fridges...they're all within reach!

Table of Contents

Installation

regal is available on npm and can be installed with the following command:

npm install regal

If you're using TypeScript (highly recommended), import it into your files like so:

import { GameInstance } from "regal";

Otherwise, using Node's require works as well:

const regal = require("regal");

Project Roadmap

The Regal Game Library has been in development since June 2018. The first stable version, alias Beacon, was released on February 1st, 2019.

Regal 2.0.0, alias Dakota, was released on November 11th, 2019. This version included some sweeping refactors that fixed bugs in the initial release, namely adding the Agent Prototype Registry to support class methods for Agents.

Moving forward, the most pressing features that should be added to the Game Library are the player command and plugin interfaces.

Outside of the library, other priorities include:

  • Improving the development tooling surrounding the framework, such as expanding regal-bundler and creating a CLI.
  • Building clients to play Regal games on various platforms.
  • Creating fun Regal games.

Contributing

Currently, the Regal Framework is developed solely by Joe Cowman (jcowman2), but pull requests, bug reports, suggestions, and questions are all more than welcome!

If you would like to get involved, please see the contributing page or the project's about page.

Guide: Creating Your First Regal Game

The following is a step-by-step guide for creating a basic game of Rock, Paper, Scissors with Regal and TypeScript.

For more detailed information on any topic, see the API Reference below. Everything in this guide is available in the Regal demos repository as well.

Step 1. Set up project

Start with an empty folder. Create a package.json file in your project's root directory with at least the following properties:

{
    "name": "my-first-game",
    "author": "Your Name Here"
}

Then, install the regal dependency.

npm install regal

Since your game will be written in TypeScript (as is recommended for all Regal games), you'll need to install typescript as well:

npm install --save-dev typescript

Create a src directory and a new file called index.ts inside it. This is where you'll write your game logic.

At this point, your project should have the following structure:

.
├── node_modules
├── package.json
├── package-lock.json
└── src
    └── index.ts

Step 2. Write game logic

In index.ts, place the following import statement on the top line:

import { onPlayerCommand, onStartCommand } from "regal";

The Regal Game Library has way more tools to help you make games, but these imports are all you need for a game this basic.

Beneath the import line, paste the following constants. You'll use these when writing the game's logic. WIN_TABLE is a lookup table to see if one move beats another. For example, WIN_TABLE.paper.scissors is false, since paper loses to scissors.

const POSSIBLE_MOVES = ["rock", "paper", "scissors"];
const WIN_TABLE = {
    rock: {
        paper: false,
        scissors: true
    },
    paper: {
        rock: true,
        scissors: false
    },
    scissors: {
        rock: false,
        paper: true
    }
}

Next, you'll set the game's start behavior with onStartCommand. When a player starts a new game, both the player's and the opponent's scores will be initialized to zero, and a prompt will be displayed. Paste the following block of code beneath your constants:

onStartCommand(game => {
    // Initialize state
    game.state.playerWins = 0;
    game.state.opponentWins = 0;

    // Prompt the player
    game.output.write("Play rock, paper, or scissors:");
});

Finally, you need the actual gameplay. The following block should be pasted at the end of your file. It contains the behavior that runs every time the player enters a command.

onPlayerCommand(command => game => {
    // Sanitize the player's command
    const playerMove = command.toLowerCase().trim();

    // Make sure the command is valid
    if (POSSIBLE_MOVES.includes(playerMove)) {
        // Choose a move for the opponent
        const opponentMove = game.random.choice(POSSIBLE_MOVES);
        game.output.write(`The opponent plays ${opponentMove}.`);

        if (playerMove === opponentMove) {
            game.output.write("It's a tie!");
        } else {
            // Look up who wins in the win table
            const isPlayerWin = WIN_TABLE[playerMove][opponentMove];

            if (isPlayerWin) {
                game.output.write(`Your ${playerMove} beats the opponent's ${opponentMove}!`);
                game.state.playerWins++;
            } else {
                game.output.write(`The opponent's ${opponentMove} beats your ${playerMove}...`);
                game.state.opponentWins++;
            }
        }
        // Print win totals
        game.output.write(
            `Your wins: ${game.state.playerWins}. The opponent's wins: ${game.state.opponentWins}`
        );
    } else {
        // Print an error message if the command isn't rock, paper, or scissors
        game.output.write(`I don't understand that command: ${playerMove}.`);
    }

    // Prompt the player again
    game.output.write("Play rock, paper, or scissors:");
});

One last thing: the line if (POSSIBLE_MOVES.includes(playerMove)) { uses Array.prototype.includes, which is new in ECMAScript 2016. To make the TypeScript compiler compatible with this, add a tsconfig.json file to your project's root directory with the following contents:

{
    "compilerOptions": {
        "lib": ["es2016"]
    }
}

Step 3. Bundle game

Before your game can be played, it must be bundled. Bundling is the process of converting a Regal game's development source (i.e. the TypeScript or JavaScript source files that the game developer writes) into a game bundle, which is a self-contained file that contains all the code necessary to play the game via a single API.

You can use the Regal CLI to create Regal game bundles from the command line. Install it like so:

npm install -g regal-cli regal

To bundle your game, execute this command in your project's root directory:

regal bundle

This should generate a new file in your project's directory, called my-first-game.regal.js. Your first game is bundled and ready to be played!

For a list of configuration options you can use, consult the CLI's documentation.

Step 4. Play game

To load a Regal game bundle for playing, use the Regal CLI play command.

regal play my-first-game.regal.js

The game should open in your terminal. Enter rock, paper, or scissors to play, or :quit to exit the game. The sequence should look something like this:

Now Playing: my-first-game by Your Name Here
Type :quit to exit the game.

Play rock, paper, or scissors:
paper

The opponent plays rock.
Your paper beats the opponent's rock!
Your wins: 1. The opponent's wins: 0
Play rock, paper, or scissors:

Congratulations, you've created your first game with the Regal Framework! 🎉

Documentation

The following sections provide a guide to each aspect of the Regal Game Library. For detailed information on a specific item, consult the API Reference.

Core Concepts

The Regal Game Library is a JavaScript package that is required by games to be used within the Regal Framework. A game that is built using the Game Library is called a Regal game.

Regal games have the following qualities:

  • They are text-based. Simply put, gameplay consists of the player putting text in and the game sending text back in response.
  • They are deterministic. When a Regal game is given some input, it should return the same output every time. (To see how this applies to random values, read here.)

These two qualities allow Regal games to be thought of as pure functions. A pure function is a function that is deterministic and has no side-effects. In other words, a Regal game is totally self-contained and predictable.

Think of playing a Regal game like the following equation:

g1(x) = g2

where x is the player's command
g1 is the Regal game before the command
g2 is the Regal game after the command

Entering the player's command into the first game instance creates another game instance with the effects of the player's command applied. For example, if g1 contains a scene where a player is fighting an orc, and x is "stab orc", g2 might show the player killing that orc. Note that g1 is unmodified by the player's command.

The process of one game instance interpreting a command and outputting another game instance is called a game cycle.

Game Data

All data in a Regal game is in one of two forms: static or instance-specific.

Static data is defined in the game's source code, and is the same for every instance of the game. Game events, for example, are considered static because they are defined the same way for everyone playing the game (even though they may have different effects). Metadata values for the game, such as its title and author, are also static.

Instance-specific data, more frequently called game state, is unique to a single instance of the game. A common example of game state is a player's stats, such as health or experience. Because this data is unique to one player of the game and is not shared by all players, it's considered instance-specific.

Understanding the difference between static data and game state is important. Everything that's declared in a Regal game will be in one of these two contexts.

GameInstance

The cornerstone of the Regal Game Library is the GameInstance.

A GameInstance represents a unique instance of a Regal game. It contains (1) the game's current state and (2) all the interfaces used to interact with the game during a game cycle.

GameInstance vs Game

To understand how a game instance differs from the game itself, it can be helpful to think of a Regal game like a class. The game's static context is like a class definition, which contains all the immutable events and constants that are the same for every player.

When a player starts a new Regal game, they receive an object of that class. This game instance is a snapshot of the Regal game that is unique to that player. It contains the effects of every command made by the player, and has no bearing on any other players' game instances.

Two people playing different games of Solitare are playing the same game, but different game instances.

Instance State

All game state is stored in a GameInstance object. Some of this state is hidden from view, but custom properties can be set directly in GameInstance.state.

// Assumes there's a GameInstance called myGame
myGame.state.foo = "bar";
myGame.state.arr = [1, 2, 3];

Properties set within the state object are maintained between game cycles, so it can be used to store game data long-term.

GameInstance.state is of of type any, meaning that its properties are totally customizable. Optionally, the state type may be set using a type parameter (see Using StateType for more information).

InstanceX Interfaces

In addition to storing game state, GameInstance contains several interfaces for controlling the game instance's behavior.

Property Type Controls
events InstanceEvents Events
output InstanceOutput Output
options InstanceOptions Options
random InstanceRandom Randomness
state any or StateType Miscellaneous state

Each of these interfaces is described in more detail below.

Using StateType

GameInstance.state is of of type any, meaning that its properties are totally customizable. Optionally, the state type may be set using a type parameter called StateType.

The StateType parameter allows you to type-check the structure of GameInstance.state against a custom interface anywhere GameInstance is used.

interface MyState {
    foo: boolean;
}

(myGame: GameInstance<MyState>) => {
    const a = myGame.state.foo; // Compiles
    const b = myGame.state.bar; // Won't compile!
};

Keep in mind that StateType is strictly a compile-time check, and no steps are taken to ensure that the state object actually matches the structure of StateType at runtime.

(myGame: GameInstance<string>) => myGame.state.substring(2); // Compiles fine, but will throw an error at runtime because state is an object.

StateType is especially useful for parameterizing events.

Events

A game where everything stays the same isn't much of a game. Therefore, Regal games are powered by events.

An event can be thought of as anything that happens when someone plays a Regal game. Any time the game's state changes, it happens inside of an event.

Event Functions

Events in the Regal Game Library share a common type: EventFunction.

An EventFunction takes a GameInstance as its only argument, modifies it, and may return the next EventFunction to be executed, if one exists.

Here is a simplified declaration of the EventFunction type:

type EventFunction = (game: GameInstance) => EventFunction | void;

An EventFunction can be invoked by passing it a GameInstance:

// Assumes there's a GameInstance called myGame 
// and an EventFunction called event1, which returns void.

event1(myGame); // Invoke the event.

// Now, myGame contains the changes made by event1.

EventFunction has two subtypes: TrackedEvent and EventQueue. Both are described below.

Declaring Events

The most common type of event used in Regal games is the TrackedEvent. A TrackedEvent is simply an EventFunction that is tracked by the GameInstance.

In order for Regal to work properly, all modifications to game state should take place inside tracked events.

To declare a TrackedEvent, use the on function:

import { on } from "regal";

const greet = on("GREET", game => {
    game.output.write("Hello, world!");
});

The on function takes an event name and an event function to construct a TrackedEvent. The event declared above could be invoked like this:

// Assumes there's a GameInstance called myGame.

greet(myGame); // Invoke the greet event.

This would cause myGame to have the following output:

GREET: Hello, world!

Causing Additional Events

As stated earlier, an EventFunction may return another EventFunction. This tells the event executor that another event should be executed on the game instance.

Here's an example:

const day = on("DAY", game => {
    game.output.write("The sun shines brightly overhead.");
});

const night = on("NIGHT", game => {
    game.output.write("The moon glows softly overhead.");
});

const outside = on("OUTSIDE", game => {
    game.output.write("You go outside.");
    return game.state.isDay ? day : night;
});

When the outside event is executed, it checks the value of game.state.isDay and returns the appropriate event to be executed next.

// Assume that myGame.state.isDay is false.

outside(myGame);

myGame's output would look like this:

OUTSIDE: You go outside.
NIGHT: The moon glows softly overhead.

Causing Multiple Events

It's possible to have one EventFunction cause multiple events with the use of an EventQueue.

An EventQueue is a special type of TrackedEvent that contains a collection of events. These events are executed sequentially when the EventQueue is invoked.

Queued events may be immediate or delayed, depending on when you want them to be executed.

Immediate Execution

To have one event be executed immediately after another, use the TrackedEvent.then() method. This is useful in situations where multiple events should be executed in direct sequence.

To demonstrate, here's an example of a player crafting a sword. When the makeSword event is executed, the sword is immediately added to the player's inventory (addItemToInventory) and the player learns the blacksmithing skill (learnSkill).

const learnSkill = (name: string, skill: string) =>
    on(`LEARN SKILL <${skill}>`, game => {
        game.output.write(`${name} learned ${skill}!`);
    });

const addItemToInventory = (name: string, item: string) =>
    on(`ADD ITEM <${item}>`, game => {
        game.output.write(`Added ${item} to ${name}'s inventory.`);
    });

const makeSword = (name: string) =>
    on(`MAKE SWORD`, game => {
        game.output.write(`${name} made a sword!`);
        return learnSkill(name, "Blacksmithing")
            .then(addItemToInventory(name, "Sword"));
    });

Note: This example is available here.

Execute the makeSword event on a GameInstance called myGame like so:

makeSword("King Arthur")(myGame);

This would produce the following output for myGame:

MAKE SWORD: King Arthur made a sword!
ADD ITEM <Sword>: Added Sword to King Arthur's inventory.
LEARN SKILL <Blacksmithing>: King Arthur learned Blacksmithing!

Delayed Execution

Alternatively, an event may be scheduled to execute only after all of the immediate events are finished by using enqueue(). This is useful in situations where you have multiple series of events, and you want each series to execute their events in the same "round."

This is best illustrated with an example. Here's a situation where a player executes a command that drops a list of items from their inventory.