Skip to content

Commit

Permalink
Merge pull request #31 from regal/feat/26-api
Browse files Browse the repository at this point in the history
API Hooks and Game.postPlayerCommand
  • Loading branch information
jcowman2 committed Aug 29, 2018
2 parents 125290a + 08f1f9b commit e761e72
Show file tree
Hide file tree
Showing 17 changed files with 1,479 additions and 171 deletions.
54 changes: 53 additions & 1 deletion src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GameInstance, RegalError } from "./game";
import { EventRecord } from "./event";
import { RegalError } from "./error";
import GameInstance from "./game-instance";

const StaticAgentProxyHandler = {
get(target: Agent, propertyKey: PropertyKey, receiver: object) {
Expand Down Expand Up @@ -183,6 +184,11 @@ export class AgentReference {
constructor(public refId: number) {}
}

const propertyIsAgentId = (property: string) => {
const tryNum = Math.floor(Number(property));
return tryNum !== Infinity && String(tryNum) === property && tryNum >= 0;
}

export class InstanceAgents {

constructor(public game: GameInstance) {}
Expand Down Expand Up @@ -296,6 +302,52 @@ export class InstanceAgents {
agentPropertyWasDeleted(agentId: number, property: PropertyKey): boolean {
return this.hasOwnProperty(agentId) && (<AgentRecord>this[agentId]).propertyWasDeleted(property);
}

/**
* Creates a new `InstanceAgents` for the new game cycle.
* **Don't call this unless you know what you're doing.**
* @param current The `GameInstance` for the new game cycle.
*/
cycle(current: GameInstance): InstanceAgents {
const newAgents = new InstanceAgents(current);

const agentKeys = Object.keys(this).filter(propertyIsAgentId);
const agentRecords = agentKeys.map(key => <AgentRecord>this[key]);

for (let i = 0; i < agentRecords.length; i++) {
const formerAgent = agentRecords[i];
const keysToAdd = Object.keys(formerAgent)
.filter(key => key !== "game" && key !== "_id")

// Create new Agent with the old agent's id and the new GameInstance.
const id = Number.parseInt(agentKeys[i]);
const newAgent = new Agent(id, current);
newAgents.addAgent(newAgent, EventRecord.default); // Note: If the agent is static, this won't do anything.

// For each updated property on the old agent, add its last value to the new agent.
keysToAdd.forEach(key => {

if (formerAgent.propertyWasDeleted(key)) {

if (staticAgentRegistry.hasAgentProperty(id, key)) {
newAgents.deleteAgentProperty(id, key, EventRecord.default); // Record deletions to static agents.
}

return; // If the property was deleted, don't add it to the new record.
}

let formerPropertyValue = formerAgent.getProperty(key);

if (isAgentReference(formerPropertyValue)) {
formerPropertyValue = new AgentReference(formerPropertyValue.refId);
}

newAgents.setAgentProperty(newAgent.id, key, formerPropertyValue, EventRecord.default);
});
}

return newAgents;
}
}

export enum PropertyOperation {
Expand Down
120 changes: 120 additions & 0 deletions src/api-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* Functions for hooking into the Regal Game Library's Game API.
*
* A hook is a function that is called internally by the Game API,
* and can be implemented by the game developer. This allows the
* developer to modify the behavior of the API.
*
* @since 0.3.0
* @author Joe Cowman
* @license MIT (see https://github.com/regal/regal)
*/

import { EventFunction, TrackedEvent, on, isTrackedEvent } from "./event";
import GameInstance from "./game-instance";
import { RegalError } from "./error";

/** Default implementation of `beforeUndoCommandHook`; always returns true. */
const returnTrue = (game: GameInstance) => true;

/**
* Manager for the Game's API hooks.
*/
export class HookManager {

/** `TrackedEvent` to be executed whenver `Game.postPlayerCommand` is called. */
static playerCommandHook: (command: string) => TrackedEvent;

/** `TrackedEvent` to be executed whenever `Game.postStartCommand` is called. */
static startCommandHook: TrackedEvent;

/**
* Executes whenever `Game.postUndoCommand` is called, before the undo operation is executed.
* Defaults to always return true.
* @returns Whether the undo operation is allowed.
*/
static beforeUndoCommandHook: (game: GameInstance) => boolean = returnTrue;

/**
* Resets the API hooks to their default values.
*/
static resetHooks() {
this.playerCommandHook = undefined;
this.startCommandHook = undefined;
this.beforeUndoCommandHook = returnTrue;
}
}

/**
* Sets the function to be executed whenever a player command is sent to the Game API
* via `Game.postPlayerCommand`.
* @param handler A function that takes a string containing the player's command and
* generates an `EventFunction`. May be an `EventFunction`, `TrackedEvent`, or `EventQueue`.
*/
export const onPlayerCommand = (handler: (command: string) => EventFunction): void => {
if (HookManager.playerCommandHook !== undefined) {
throw new RegalError("Cannot call onPlayerCommand more than once.");
}
if (handler === undefined) {
throw new RegalError("Handler must be defined.");
}

// Generate a TrackedEvent called INPUT
const trackedEvent = (cmd: string) => on("INPUT", game => {
const activatedHandler = handler(cmd);

// Allow the handler to be an EventFunction, a TrackedEvent, or an EventQueue
if (isTrackedEvent(activatedHandler)) {
return activatedHandler;
} else {
return activatedHandler(game);
}
});

HookManager.playerCommandHook = trackedEvent;
};

/**
* Sets the function to be executed whenever a start command is sent to the Game API
* via `Game.postStartCommand`.
* @param handler The `EventFunction` to be executed. May be an `EventFunction`, `TrackedEvent`, or `EventQueue`.
*/
export const onStartCommand = (handler: EventFunction): void => {
if (HookManager.startCommandHook !== undefined) {
throw new RegalError("Cannot call onStartCommand more than once.");
}
if (handler === undefined) {
throw new RegalError("Handler must be defined.");
}

// Generate a TrackedEvent called START
const trackedEvent = on("START", game => {
// Allow the handler to be an EventFunction, a TrackedEvent, or an EventQueue
if (isTrackedEvent(handler)) {
return handler;
} else {
return handler(game);
}
});

HookManager.startCommandHook = trackedEvent;
};

/**
* Sets the function to be executed whenever `Game.postUndoCommand` is called,
* before the undo operation is executed.
*
* If this function is never called, all valid undo operations will be allowed.
*
* @param handler Returns whether the undo operation is allowed, given the current `GameInstance`.
*/
export const onBeforeUndoCommand = (handler: (game: GameInstance) => boolean): void => {
if (HookManager.beforeUndoCommandHook !== returnTrue) {
throw new RegalError("Cannot call onBeforeUndoCommand more than once.");
}
if (handler === undefined) {
throw new RegalError("Handler must be defined.");
}

HookManager.beforeUndoCommandHook = handler;
};
50 changes: 0 additions & 50 deletions src/api.ts

This file was deleted.

7 changes: 7 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class RegalError extends Error {

constructor(message: string = "") {
super(`RegalError: ${message}`);
Object.setPrototypeOf(this, new.target.prototype);
}
}
26 changes: 22 additions & 4 deletions src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* @license MIT (see https://github.com/regal/regal)
*/

import { GameInstance, RegalError } from './game';
import GameInstance from './game-instance';
import { PropertyChange, PropertyOperation } from './agent';
import { OutputLine } from './output';
import { RegalError } from './error';

/** Event ID for untracked EventFunctions. */
export const DEFAULT_EVENT_ID: number = 0;
Expand Down Expand Up @@ -187,16 +188,19 @@ export class InstanceEvents {
/** Contains records of the past events executed during the game cycle. */
history: EventRecord[] = [];

/** ID of the most recently generated EventRecord. */
private _lastEventId = DEFAULT_EVENT_ID;
/** Internal member for the ID of the most recently generated EventRecord. */
private _lastEventId;
/** Internal queue of events that have yet to be executed. */
private _queue: EventRecord[] = [];

/**
* Constructs an InstanceEvents.
* @param game The game instance that owns this InstanceEvents.
* @param startingEventId Optional starting ID for new `EventRecord`s.
*/
constructor(public game: GameInstance) {}
constructor(public game: GameInstance, startingEventId = DEFAULT_EVENT_ID) {
this._lastEventId = startingEventId;
}

/** The current EventRecord. */
get current(): EventRecord {
Expand All @@ -209,6 +213,11 @@ export class InstanceEvents {
return event;
}

/** The ID of the most recently generated `EventRecord`. */
get lastEventId() {
return this._lastEventId;
}

/**
* Executes the given event and all events caused by it.
* @param event The TrackedEvent to be invoked.
Expand Down Expand Up @@ -280,6 +289,15 @@ export class InstanceEvents {
delete this.current.func;
this.history.unshift(this._queue.shift());
}

/**
* Creates a new `InstanceEvents` for the new game cycle.
* **Don't call this unless you know what you're doing.**
* @param current The `GameInstance` for the new game cycle.
*/
cycle(current: GameInstance): InstanceEvents {
return new InstanceEvents(current, this.lastEventId);
}
}

/** Creates a function that returns an error upon invocation. */
Expand Down

0 comments on commit e761e72

Please sign in to comment.