npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

regal

v2.0.0

Published

TypeScript package for games developed in the Regal framework.

Downloads

203

Readme

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! :tada:

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.

import { on, enqueue } from "regal";

const hitGround = (item: string) =>
    on(`HIT GROUND <${item}>`, game => {
        game.output.write(`${item} hits the ground. Thud!`);
    });

const fall = (item: string) =>
    on(`FALL <${item}>`, game => {
        game.output.write(`${item} falls.`);
        return enqueue(hitGround(item));
    });

const drop = on("DROP ITEMS", game => {
        let q = enqueue();
        for (let item of game.state.items) {
            q = q.enqueue(fall(item));
        }
        return q;
    });

Note: This example is available here.

We'll walk through each line, starting from the drop function.

const drop = on("DROP ITEMS", game => {

The enqueue() function takes zero or more TrackedEvents as arguments, which it uses to build an EventQueue. Creating an empty queue has no effect; it simply provides us a reference to which we can add additional events.

let q = enqueue();

In addition to being a standalone function, enqueue() is also a method of EventQueue. Calling EventQueue.enqueue() creates a new queue with all previous events plus the new event(s).

for (let item of game.state.items) {
    q = q.enqueue(fall(item));
}

The previous two code blocks could be simplified by using JavaScript's map like so:

const q = enqueue(game.state.items.map(item => fall(item)));

Finally, we return the event queue.

        return q;
    });

The fall event is simpler. It outputs a message and adds a hitGround event to the end of the queue for a single item.

const fall = (item: string) =>
    on(`FALL <${item}>`, game => {
        game.output.write(`${item} falls.`);
        return enqueue(hitGround(item));
    });

The hitGround event outputs a final message.

const hitGround = (item: string) =>
    on(`HIT GROUND <${item}>`, game => {
        game.output.write(`${item} hits the ground. Thud!`);
    });

Deciding that game.state.items contains ["Hat", "Duck", "Spoon"], executing the drop event would produce an output like this:

FALL <Hat>: Hat falls.
FALL <Duck>: Duck falls.
FALL <Spoon>: Spoon falls.
HIT GROUND <Hat>: Hat hits the ground. Thud!
HIT GROUND <Duck>: Duck hits the ground. Thud!
HIT GROUND <Spoon>: Spoon hits the ground. Thud!

If you're still confused about the difference between TrackedEvent.then() and EventQueue.enqueue(), here's what the output would have been if TrackedEvent.then() was used instead:

FALL <Hat>: Hat falls.
HIT GROUND <Hat>: Hat hits the ground. Thud!
FALL <Duck>: Duck falls.
HIT GROUND <Duck>: Duck hits the ground. Thud!
FALL <Spoon>: Spoon falls.
HIT GROUND <Spoon>: Spoon hits the ground. Thud!

Remember, enqueue() is useful for situations where you have multiple series of events, like our [fall -> hitGround] series, and you want each series to execute their alike events in the same "round." We didn't want hat to finish hitting the ground before duck fell, we wanted all of the items to fall together and hit the ground together.

TrackedEvent.then() is for immediate exeuction and enqueue() is for delayed exeuction.

Event Chains

The event API is chainable, meaning that the queueing methods can be called multiple times to create more complex event chains.

// Immediately executes events 1-4
event1.then(event2, event3, event4);
event1.then(event2).then(event3).then(event4);
event1.then(event2.then(event3, event4));
event1.then(event2, event3.then(event4));

// Immediately executes event1, delays 2-4.
event1.then(enqueue(event2, event3, event4));
event1.thenq(event2, event3, event4); // TrackedEvent.thenq is shorthand for TrackedEvent.then(enqueue(...))

// Immediately executes events 1-2, delays 3-4.
event1.then(event2).enqueue(event3, event4);
event1.then(event2, enqueue(event3, event4));
event1.then(event2).enqueue(event3).enqueue(event4);

// Delays events 1-4.
enqueue(event1, event2, event3, event4);
enqueue(event1.then(event2, event3, event4));

If you prefer, you can use the shorthand nq instead of writing enqueue(). We're all about efficiency. :+1:

import { nq } from "regal";

let q = nq(event1, event2);
q = q.nq(event3);

When creating event chains, keep in mind that all immediate events must be added to the queue before delayed events.

event1.then(nq(event2, event3), event4); // ERROR
event1.then(event4, nq(event2, event3)); // Okay

For more information, consult the API Reference.

When to Use noop

noop is a special TrackedEvent that stands for no operation. When the event executor runs into noop, it ignores it.

import { on, noop } from "regal";

const end = on("END", game => {
    game.output.write("The end!");
    return noop; // Nothing will happen
});

EventFunction doesn't have a required return, so most of the time you can just return nothing instead of returning noop.

const end = on("END", game => {
    game.output.write("The end!");
});

However, noop might be necessary in cases where you want to use a ternary to make things simpler:

on("EVENT", game => 
    // Return another event to be executed if some condition holds, otherwise stop.
    game.state.someCondition ? otherEvent | noop; 
);

If you have a TypeScript project with noImplicitReturns enabled, noop is useful for making sure all code paths return a value.

// Error: [ts] Not all code paths return a value.
on("EVENT", game => {
    if (game.state.someCondition) {
        return otherEvent;
    }
});

// Will work as intended
on("EVENT", game => {
    if (game.state.someCondition) {
        return otherEvent;
    }
    return noop;
});

Parameterizing Events

The StateType type parameter of GameInstance can be used to declare the type of GameInstance.state inside the scope of individual events.

EventFunction, TrackedEvent, and EventQueue all allow an optional type parameter that, if used, will type-check GameInstance.state inside the body of the event.

import { on } from "regal";

interface MyState {
    num: number;
    names: string[];
}

const init = on<MyState>("INIT", game => {
    game.state.num = 0; // Type checked!
    game.state.names = ["spot", "buddy", "lucky"]; // Type checked!
});

const pick = on<MyState>("PICK", game => {
    const choice = game.state.names[game.state.num]; // Type checked!
    game.output.write(`You picked ${choice}!`);
    game.state.num++; // Type checked!
});

Including the type parameter for every event gets unwieldy for games with many events, so the type GameEventBuilder can be used to alias a customized on.

import { on as _on, GameEventBuilder } from "regal";

interface MyState {
    num: number;
    names: string[];
}

const on: GameEventBuilder<MyState> = _on; // Declare `on` as our parameterized version

const init = on("INIT", game => {
    game.state.num = 0; // Type checked!
    game.state.names = ["spot", "buddy", "lucky"]; // Type checked!
});

const pick = on("PICK", game => {
    const choice = game.state.names[game.state.num]; // Type checked!
    game.output.write(`You picked ${choice}!`);
    game.state.num++; // Type checked!
});

Note: This example is available here.

Using the redefined on from this example, the following event would not compile:

on("BAD", game => {
    game.state.nams = []; // Error: [ts] Property 'nams' does not exist on type 'MyState'. Did you mean 'names'?
});

Agents

Where events describe any change that occurs within a Regal game, agents are the objects on which these changes take place.

Every object that contains game state (like players, items, and score) is considered an agent.

Defining Agents

The Regal Game Library offers the Agent class, which you can extend to create custom agents for your game.

Here is an example:

import { Agent } from "regal";

class Bucket extends Agent {
    constructor(
        public size: number,
        public contents: string,
        public isFull: boolean
    ) {
        super();
    }
}

Now, a Bucket can be instantiated and used just like any other class.

const bucket = new Bucket(5, "water", true);
bucket.size === 5; // True

Furthermore, you can use them in events.

const init = on("INIT", game => {
    game.state.bucket = new Bucket(5, "famous chili", true);
});

const pour = on("POUR", game => {
    const bucket: Bucket = game.state.bucket;

    if (bucket.isFull) {
        bucket.isFull = false;
        game.output.write(`You pour out the ${bucket.contents}.`);
    } else {
        game.output.write("The bucket is already empty!");
    }
});

Executing init.then(pour, pour) on a game instance would give the following output:

POUR: You pour out the famous chili.
POUR: The bucket is already empty!

Note: this example is available here.

Active and Inactive Agents

Agents have a single caveat that may seem strange at first, but is important to understand:

Before an agent's properties can be accessed in a game cycle, the agent must first be activated.

To activate an agent simply means to register it with the GameInstance. Much like how all changes to game state need to take place inside tracked events, the agents that contain this state need to be tracked as well.

Activating an agent allows it to be tracked by the GameInstance, and can happen either implicitly or explicitly.

If you try to modify an inactive agent within a game cycle, a RegalError will be thrown. For example:

const illegalEvent = on("EVENT", game => {
    const waterBucket = new Bucket(1, "water", true); // Create an inactive agent
    waterBucket.isFull = false; // Uh-oh!
});

Executing illegalEvent will throw the following error:

RegalError: The properties of an inactive agent cannot be set within a game cycle.

Note: this example is available here.

Activating Agents Implicitly

Most of the time, agents will activate themselves without you having to do any extra work. In fact, you've already seen an example of this with the Bucket agent above.

One of the ways that agents may be activated implicitly is by setting them as a property of an already-active agent.

const init = on("INIT", game => {
    game.state.bucket = new Bucket(5, "famous chili", true);
});

When our new Bucket agent was assigned to the game instance's state.bucket property, it was activated implicitly. This is because GameInstance.state is actually an active agent.

Whenever agents are set as properties of GameInstance.state, they are activated implicitly.

There are five ways to activate agents implicitly. The first way, which was demonstrated above, is to set the agent as a property of an active agent.

class Parent extends Agent {
    constructor(
        public num: number, 
        public child?: Agent // Optional child property
    ) {
        super();
    }
}

game.state.myAgent = new Parent(1); // #1 is activated by GameInstance.state
game.state.myAgent.child = new Parent(2); // #2 is activated by #1

Second, all agents that are properties of an inactive agent are activated when the parent agent is activated.

const p = new Parent(1, new Parent(2)); // #1 and #2 are both inactive
game.state.myAgent = p; // #1 and #2 are activated by GameInstance.state

Third, all agents in arrays that are properties of an inactive agent are activated when the parent agent is activated.

class MultiParent extends Agent {
    constructor(
        public num: number,
        public children: Agent[] = [] // Default to an empty array
    ) {
        super();
    }
}

const mp = new MultiParent(1, [ new Parent(2), new Parent(3) ]); // #1, #2, and #3 are inactive
game.state.myAgent = mp; // #1, #2, and #3 are activated by GameInstance.state

Fourth, all agents in an array are activated when it is set as the property of an already-active agent.

game.state.myAgent = new MultiParent(1); // #1 is activated by GameInstance.state
game.state.myAgent.children = [ new Parent(2), new Parent(3) ]; // #2 and #3 are activated by #1

Finally, an agent is activated when it is added to an array that's a property of an already-active agent.

game.state.myAgent = new MultiParent(1, [ new Parent(2) ]); // #1 and #2 are activated by GameInstance.state
game.state.myAgent.children.push(new Parent(3)); // #3 is activated by #1

Note: this example is available here.

Activating Agents Explicitly

Agents can be activated explicitly with GameInstance.using().

GameInstance.using() activates one or more agents and returns references to them. The method takes a single argument, which can be one of a few types.

First, a single agent may be activated.

class CustomAgent extends Agent {
    constructor(public num: number) {
        super();
    }
}

const agent = game.using(new CustomAgent(1)); // #1 is activated

Second, an array of agents may be activated:

const agents = game.using([ new CustomAgent(1), new CustomAgent(2) ]); // #1 and #2 are activated

Finally, the argument can be an object where every property is an agent to be activated:

const { agent1, agent2 } = game.using({
    agent1: new CustomAgent(1),
    agent2: new CustomAgent(2)
}); // #1 and #2 are activated

Note: this example is available here.

Explicit activation is useful for situations where agents aren't being activated implicitly. If you can't figure out whether an agent is getting activated implicitly or not, you may want to explicitly use GameInstance.using() just to be safe. Activating an agent multiple times has no effect.

Modifying Inactive Agents

Inactive agents aren't truly immutable. In fact, any property of an inactive agent may be set once before the agent is activated. Otherwise, setting properties within constructors wouldn't be possible.

Technically, this means doing something like this is valid (although not recommended):

const a = new CustomAgent(1);
(a as any).someOtherProperty = "foo";
game.state.myAgent = a;

It's important to note that the properties of inactive agents are only inaccessible within the context of a game cycle (i.e. inside a TrackedEvent). When agents are declared outside of events, they are called static agents and have special characteristics. These are explained in the next section.

Static Agents

All Regal game data is considered either static or instance-specific. The agents we've created up to this point have all been instance-specific, meaning that they are defined inside events and their data is stored in the instance state.

Although it's perfectly valid to store every agent in the instance state, this usually isn't necessary. Most games have a lot of data that is rarely, or never, modified. Rather than storing this data in every GameInstance, it's more efficient to store these agents in the game's static context. Remember, static data is defined outside the game cycle and is separate from the instance state.

Static agents are agents defined outside of the game cycle.

Once activated, static agents can be used inside events just like non-static agents.

// Declare a new agent class called Book
class Book extends Agent {
    constructor(
        public title: string,
        public author: string,
        public content: string
    ) {
        super();
    }
}

// Declare a static Book agent
const NOVEL = new Book(
    "Frankenstein",
    "Mary Shelley",
    /* really long string */ 
);

const read = on("READ", game => {
    const novel = game.using(NOVEL); // Activate the static agent
    game.output.write(`You open ${novel.title}, by ${novel.author}.`);

    const excerpt = novel.content.split(" ").slice(0, 4).join(" "); // Grab the first 4 words
    game.output.write(`It begins, "${excerpt}..."`);
});

Note: This example is available here.

Executing read on a GameInstance would produce the following output:

READ: You open Frankenstein, by Mary Shelley.
READ: It begins, "To Mrs. Saville, England..."

Unlike non-static agents, static agents may have their properties read or modified, but only outside of the game cycle. For example, this would be okay:

const NOVEL = new Book(
    "Frankenstein",
    "Mary Shelley",
    /* really long string */ 
);

NOVEL.title += ", or The Modern Prometheus"; // No error!

In order to use a static agent's properties within a game cycle, however, it must be activated.

This event modifies several properties of the NOVEL static agent:

const revise = (playerName: string, forward: string) =>
    on("REVISE", game => {
        const novel = game.using(NOVEL);
        novel.content = forward + " " + novel.content;
        novel.author += ` (with a forward by ${playerName})`
    });

Executing the queue revise("Lars", "Pancakes!").then(read) on a GameInstance would produce the following output:

READ: You open Frankenstein, by Mary Shelley (with a forward by Lars).
READ: It begins, "Pancakes! To Mrs. Saville,..."

Both events, read and revise, activated NOVEL independently of each other, yet the changes made in revise were still there in read. This is because the changes made to static agents are stored in the instance state. The static agent's properties that weren't changed (in this case, just the author) don't need to be stored in the state.

Static agents save space by storing only the changes made to their properties by a specific game instance in that instance's state.

If two different game instances reference the same static agent, their changes will not affect each other. This is because changes made to a static agent don't actually modify the static agent at all; they are simply stored in the instance state.

Randomness

Randomness is an essential part of many games, so the Regal Game Library provides a convenient way to generate random values through the GameInstance.

Generating Random Values

GameInstance.random contains an object of type InstanceRandom, which is an interface for generating random values.

InstanceRandom has methods for generating random integers, decimals, strings, and booleans.

const randos = on("RANDOS", game => {
    const bool = game.random.boolean(); // Either true or false
    game.output.write(`Boolean -> ${bool}`);

    const int = game.random.int(1, 10); // Integer between 1 and 10, inclusive
    game.output.write(`Int -> ${int}`);

    const dec = game.random.decimal(); // Random decimal betweeen 0 and 1
    game.output.write(`Decimal -> ${dec}`); 

    const str = game.random.string(10); // Random string of length 10
    game.output.write(`String -> ${str}`);
});

Note: This example is available here.

Executing randos on a GameInstance would produce values like the following:

RANDOS: Boolean -> true
RANDOS: Int -> 5
RANDOS: Decimal -> 0.38769409744713784
RANDOS: String -> qj$4d-28DX

InstanceRandom.string() has an optional charset property that can be used to specify the characters that are chosen from when the string is generated.

For example, if charset is "aeiou", then the random string will only contain vowels.

Charsets is a static object that contains several common charset strings for this purpose.

import { Charsets } from "regal";

const rstrings = on("RSTRINGS", game => {
    const alphanumeric = game.random.string(10, Charsets.ALHPANUMERIC_CHARSET);
    game.output.write(`Alphanumeric -> ${alphanumeric}`);

    const alphabet = game.random.string(10, Charsets.ALPHABET_CHARSET);
    game.output.write(`Alphabet -> ${alphabet}`);

    const numbers = game.random.string(10, Charsets.NUMBERS_CHARSET);
    game.output.write(`Numbers -> ${numbers}`);

    const hex = game.random.string(10, Charsets.NUMBERS_CHARSET + "ABCDEF");
    game.output.write(`Hex -> ${hex}`);

    const binary = game.random.string(10, "10");
    game.output.write(`Binary -> ${binary}`);

    game.output.write(`Old MacDonald had a farm, ${game.random.string(5, "eio")}.`);
});

Executing rstrings on a GameInstance would produce values like the following:

RSTRINGS: Alphanumeric -> AEeLn85uLT
RSTRINGS: Alphabet -> RoGfYDtwcL
RSTRINGS: Numbers -> 2132069253
RSTRINGS: Hex -> 69072CF9B5
RSTRINGS: Binary -> 1111011001
RSTRINGS: Old MacDonald had a farm, oeioe.

InstanceRandom.choice() chooses a random element from an array without modifying anything. This works with arrays of primitives or agents.

class Item extends Agent {
    constructor(public name: string) {
        super();
    }
}

const init = on("INIT", game => {
    game.state.items = [
        new Item("Yo-Yo"),
        new Item("Pigeon"),
        new Item("Corn cob")
    ];
});

const rpick = on("RPICK", game => {
    const i: Item = game.random.choice(game.state.items);
    game.output.write(`You picked the ${i.name}!`);
});

Executing init.then(rpick, rpick, rpick) on a GameInstance would produce values like the following:

RPICK: You picked the Pigeon!
RPICK: You picked the Yo-Yo!
RPICK: You picked the Pigeon!

Deterministic Randomness

Regal games are by definition deterministic, meaning that they always return the same output when given the same input. The methods for generating random values described above might seem to disobey this principle, but they do not.

When a GameInstance is created, it is given a special value called a seed. This seed value initializes the game instance's internal random number generator and has a direct influence on all random values that come out of it.

The seed value may be set manually as a configuration option. If no seed is set, one will be generated randomly at runtime.

A game instance's seed is considered part of its input. Therefore, it plays a factor in determining the game's output. If two game instances have the same seed, they will generate the exact same sequence of random values. If a GameInstance is reset with an undo command, then its random value stream will be reset as well.

In order for Regal to work properly, all random values should be generated by InstanceRandom. JavaScript's Math.Random() or other libraries for generating random values are not recommended.

Output

Up to this point, you've probably noticed the other examples calling game.output.write() to send messages to the game's output. write() is one of several methods provided by InstanceOutput, the interface for controlling and emitting output through the GameInstance.

InstanceOutput is accessible through GameInstance.output.

Output is handled line-by-line. An OutputLine is modeled as a text string (data) with a property that specifies its semantic meaning (type). These types, which are stored as an enum called OutputLineType, include NORMAL, MAJOR, MINOR, DEBUG, and SECTION_TITLE.

InstanceOutput contains a method for emitting each of these types of output.

const out = on("OUT", game => {
    game.output.writeNormal("This is normal output. Most of your game's output will be this type.");
    game.output.write("InstanceOutput.write is just a shorthand for writeNormal!");

    game.output.writeMajor("This is major output. Save this for really important stuff.");

    game.output.writeMinor("This is minor output. Use this for repetitive/flavor text that isn't necessary for the player to see.");

    game.output.writeDebug("This is debug output. It's only visible when the debug option is enabled.")

    game.output.writeTitle("This is a title.");
});

Note: This example is available here.

Executing the out event on a GameInstance would produce the following output:

OUT: This is normal output. Most of your game's output will be this type.
OUT: InstanceOutput.write is just a shorthand for writeNormal!
OUT: This is major output. Save this for really important stuff.
OUT: This is minor output. Use this for repetitive/flavor text that isn't necessary for the player to see.
OUT: This is a title.

Notice that the line of debug output didn't show up. That's because output lines of type DEBUG are only emitted when the debug option is set to true, and it is set to false by default. Similarly, output lines of type MINOR are only emitted when the showMinor option is set to true, which is its default value.

Executing the event again with debug: true and showMinor: false in the game configuration produces the following output:

OUT: This is normal output. Most of your game's output will be this type.
OUT: InstanceOutput.write is just a shorthand for writeNormal!
OUT: This is major output. Save this for really important stuff.
OUT: This is debug output. It's only visible when the debug option is enabled.
OUT: This is a title.

Despite each of these lines having different types, they all look the same when printed via the default Regal client. This is because OutputLine.type is left to the client for interpretation; it's up to the Regal client to decide how each type of output is handled. For more information, see Handling Responses.

GameApi and API Hooks

Regal games are played through a GameApi, which is the public interface for interacting with a Regal game. A client application will consume this API, using its methods to generate game instances, post commands, and receive output.

GameApi has the following signature:

interface GameApi {
    getMetadataCommand(): GameResponse
    postPlayerCommand(instance: GameInstance, command: string): GameResponse
    postStartCommand(options?: Partial<GameOptions>): GameResponse
    postUndoCommand(instance: GameInstance): GameResponse
    postOptionCommand(instance: GameInstance, options: Partial<GameOptions>): GameResponse
}

Each method is a different type of command that can be sent to the Regal game and returns some GameResponse.

Three of these commands, postPlayer, postUndo, and postOption, all require the current GameInstance. The other two, getMetadata and postStart, do not.

Some of these commands will be described in more detail below. Consult the API Reference for a complete description.

Handling Commands with API Hooks

Out of the five game commands listed above, two of them must be handled explicitly by the game developer. These commands, postStart and postPlayer, are handled by the hook functions onStartCommand and onPlayerCommand respectively.

A hook function is used to control what happens when a command is received by the GameApi.

onStartCommand takes an EventFunction as its only argument. This event function is executed when a postStart command is sent to the GameApi.

import { onStartCommand } from "regal";

onStartCommand(game => {
    game.output.write("Hello, world!");
});

With this hook in place, starting a new game will return the following output to the player:

START: Hello, world!

onPlayerCommand is slightly more complicated. It takes a function which receives a string as input and outputs an EventFunction. This string will be any text command sent by the player and the event will be executed when a postPlayer command is sent to the GameApi.

import { onPlayerCommand } from "regal";

onPlayerCommand(command => game => {
    game.output.write(`You wrote '${command}'!`);
});

With this hook in place, a player's input (like dance) will return the following output:

INPUT: You wrote 'dance'!

Both onStartCommand and onPlayerCommand must be called exactly once from somewhere in your game's source.

Using Game

The Regal Game Library's global implementation of the extended game API is called Game. The Game object is used for external interaction with the game and shouldn't be accessed within the game itself.

Usually, you won't use Game directly. Regal games should be bundled before they are used by clients, and these bundles have a different way of exposing a GameApi. See bundling for more information.

However, there are certain cases where you might prefer to access Game directly, such as for unit tests. In these situations, both Game and your game's source need to be imported.

import { Game } from "regal";
import "./my-game-src"; // Imports the game's root file, which has no exports

Before any command may be executed, Game.init() must be called first. This method takes a GameMetadata object, which must contain at least a name and an author.

Game.init({
    name: "My Cool Game",
    author: "Me"
});

Using the hooks defined earlier, a new game instance can be generated with Game.postStartCommand():

const startResponse = Game.postStartCommand();

The resulting GameResponse has two properties: instance and output. instance refers to the current GameInstance. output contains all output generated by the last game cycle, which here would be:

{
    "log": [
        {
            "data": "Hello, world!",
            "id": 1,
            "type": "NORMAL"
        }
    ],
    "wasSuccessful": true
}

Similarly, a player command can be executed with Game.postPlayerCommand(). This method takes two arguments: instance and command. instance refers to the current GameInstance, and command is a string containing the player's command.

const playerResponse = Game.postPlayerCommand(startResponse.instance, "bark");

This response contains a new GameInstance, leaving the original instance unchanged, and the following output:

{
    "log": [
        {
            "data": "You wrote 'bark'!",
            "id": 2,
            "type": "NORMAL"
        }
    ],
    "wasSuccessful": true
}

When a client plays a Regal game, all it has to do behind the scenes is send a series of these commands.

Note: This example is available here.

Undoing Player Commands

Because Regal games are deterministic, the effects of any command on the GameInstance can be undone. Essentially, this "rolls back" the instance state to the state it was at before the reverted command was executed.

For example, here is a game that stores each command in an array in the state:

// undo-game-src.ts
import { onPlayerCommand, onStartCommand, on } from "regal";

// Print out the list of items in the state
const printItems = on("ITEMS", game => {
    const itemsString = game.state.items.join(", ");
    game.output.write(`Items (${game.state.items.length}) -> [${itemsString}]`);
});

onStartCommand(game => {
    game.state.items = []; // Initialize state.items with an empty array
    return printItems;
});

onPlayerCommand(command => game => {
    game.output.write(`Adding ${command}.`);
    game.state.items.push(command); // Add the new item to the state
    return printItems;
});

Note: This example is available here.

Executing a start command would produce the following output:

ITEMS: Items (0) -> []

Executing three player commands, cat, dog, and mouse, would result in this output:

INPUT: Adding cat.
ITEMS: Items (1) -> [cat]

INPUT: Adding dog.
ITEMS: Items (2) -> [cat, dog]

INPUT: Adding mouse.
ITEMS: Items (3) -> [cat, dog, mouse]

A client can undo the last player or undo command executed on the GameInstance with Game.postUndoCommand(). This method takes only one argument, which is the current GameInstance.

If the following line was executed:

const newInstance = Game.postUndoCommand(oldInstance);

Then newInstance would contain the state of the instance from before the player's mouse command was executed. oldInstance would remain unchanged.

If another player command, goose, was executed, then this would be the output:

Adding goose.
Items (3) -> [cat, dog, goose]

While being able to undo commands can be quite useful, it doesn't make sense for all games. Therefore, the game developer can control whether the GameApi should allow clients to execute undo commands. This is accomplished with an API hook function, onBeforeUndoCommand.

Like the other hook functions, onBeforeUndoCommand controls the behavior of the Game API. Its only argument is a function that receives a GameInstance and returns a boolean.

Whenever a postUndo command is sent to the GameApi, the beforeUndo hook is executed on the current GameInstance. If the function returns true, the undo will proceed. If it returns false, an error will throw instead.

To prevent all undo commands, simply make the beforeUndo hook always return false:

import { onBeforeUndoCommand } from "regal";

onBeforeUndoCommand(game => false);

With this configuration, executing a postUndo command would return an unsuccessful response with the following error:

RegalError: Undo is not allowed here.

onBeforeUndoCommand can be used to allow or deny undo commands based on some condition of the game state. For instance, the following hook prevents our example from being undone when a previous player command was goose:

onBeforeUndoCommand(game =>
    !game.state.items.includes("goose") // Block undo if there's a goose in the array
);

A call to onBeforeUndoCommand is not required, and will default to always allowing undo commands if not otherwise set.

Configuration

A Regal game's configuration consists of two types: GameMetadata and GameOptions.

GameMetadata contains metadata about the game, such as its title and author. GameOptions contains configurable options to control the game's behavior.

Using Configuration Files

Configuration for a Regal game is usually kept in the project's root directory, in a file called regal.json. This file contains both metadata and options.

A game's regal.json file might look something like this:

{
    "game": {
        "name": "My Awesome Game",
        "author": "Joe Cowman",
        "headline": "This is the best game ever.",
        "description": "Let me tell you why this is the best game ever...",
        "options": {
            "debug": true,
            "seed": "1234"
        }
    }
}

Note that a regal.json file is optional. The essential properties of GameMetadata, which are name and author, as well as others properties will be taken from those of the same keys in package.json if they aren't specified in a regal.json file.

A configuration loading tool like regal-bundler is needed if using regal.json or the regal property in package.json. See bundling for more information.

Alternatively, metadata values can be passed explicitly via GameApiExtended.init(). Either way, a metadata object with at least the name and author properties specified is required before a game can receive commands.

import { Game } from "regal";

Game.init({
    name: "My Game",
    author: "Me"
});

Configuring Game Options

The Regal Game Library provides several options for configuring the behavior of game instances. These options are stored as an interface called GameOptions.

When a game is initialized, either through GameApiExtended.init() or loading a regal.json file, any values provided in GameMetadata.options will become the default values for every instance of that game.

Here is an example:

// options-demo.ts
import { Game } from "regal";
import "./options-game-src";

Game.init({
    name: "My Game",
    author: "Me",
    options: {
        debug: true,   // Set debug to true
        seed: "Hello!" // Set seed to "Hello!"
    }
});

With this configuration, every instance of My Game would start with its debug option set to true instead of false, and its seed set to "Hello!".

A convenient way to check the values of an instance's GameOptions is by using GameInstance.options, which is a read-only container for all current options in the instance. The property is of type InstanceOptions.

This can be combined with InstanceOutput.writeDebug() to produce some helpful information for development:

// options-game-src.ts
onStartCommand(game => {
    game.output.write("Startup successful.");
    game.output.writeDebug(`Current seed -> ${game.options.seed}`);
});

Starting a new game instance using the configuration from earlier would yield the following output:

START: Startup successful.
START: Current seed -> Hello!

If you wanted to generate a GameInstance with debug disabled, you could pass in any option overrides to the optional argument of Game.postStartCommand().

// options-demo.ts
let res = Game.postStartCommand({ debug: false });

With debug set to false, res would contain only the one line of output:

START: Startup successful.

To modify the options of a pre-existing GameInstance, use Game.postOptionCommand().

res = Game.postOptionCommand(res.instance, { showMinor: false });

This would disable showMinor for all future game cycles of the GameInstance (unless it got changed again).

Note: This example is available here.

The priority of GameOptions from highest to lowest is:

  1. Instance-specific overrides (i.e. values passed in Game.postStartCommand() or Game.postOptionCommand()).
  2. Static overrides (i.e. values loaded from regal.json or set with Game.init()).
  3. The options' default values.

Bundling

In order for a Regal game to be played by clients, it must be bundled first. Bundling is the process of converting a Regal game's development source (i.e. the TypeScript 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.

Game bundles are the form through which Regal games are shared, downloaded, and played.

Using the CLI

The easiest way to bundle a game is with the Regal CLI.

Once you have the CLI installed, use the bundle command to generate a game bundle.

$ regal bundle
Created a game bundle for 'my-first-game' at /Users/user/myDir/my-first-game.regal.js

This creates a JavaScript file, which contains all the game's dependencies (including the Game Library) and exports an implementation of GameApi for playing the game. By default, the generated bundle file will be named my-game.regal.js, where my-game is a sanitized version of the game's name, as specified in GameMetadata.

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

Using regal-bundler

You can access the bundling API directly with regal-bundler.

First, install regal-bundler:

npm install --save-dev regal-bundler

Generate a game bundle with the asynchronous bundle() function.

import { bundle } from "regal-bundler";

bundle(); // Creates a bundle file

bundle() accepts an optional configuration object that can be used to control certain behaviors of the bundler, such as the location of input or output files, the module format of the bundle, or whether minification s