Skip to content

Commit

Permalink
fix(api): Split Game into GameApi and GameApiExtended interfaces, whi…
Browse files Browse the repository at this point in the history
…ch are implemented by an object
  • Loading branch information
jcowman2 committed Dec 14, 2018
1 parent 6afcfee commit e4b6e4f
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 285 deletions.
31 changes: 31 additions & 0 deletions src/api/game-api-extended.ts
@@ -0,0 +1,31 @@
/*
* Contains the extended Game API interface.
*
* Copyright (c) 2018 Joseph R Cowman
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { GameMetadata } from "../config";
import { GameApi } from "./game-api";

/**
* API for interacting with the Regal game.
*
* Contains the standard methods from `GameApi`, as well as additional
* methods for advanced control.
*/
export interface GameApiExtended extends GameApi {
/** Whether `Game.init` has been called. */
isInitialized(): boolean;

/**
* Initializes the game with the given metadata.
* This must be called before any game commands may be executed.
*
* @param metadata The game's configuration metadata.
*/
init(metadata: GameMetadata): void;

/** Resets the game's static classes. */
reset(): void;
}
293 changes: 11 additions & 282 deletions src/api/game-api.ts
Expand Up @@ -8,162 +8,22 @@
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { StaticAgentRegistry } from "../agents";
import { GameMetadata, GameOptions, MetadataManager } from "../config";
import { RegalError } from "../error";
import {
buildGameInstance,
ContextManager,
GameInstance,
GameInstanceInternal
} from "../state";
import { HookManager } from "./api-hook-manager";
import { GameResponse, GameResponseOutput } from "./game-response";
import { GameOptions } from "../config";
import { GameInstance } from "../state";
import { GameResponse } from "./game-response";

/**
* Throws an error if `instance` or any of its properties are undefined.
* @param instance The `GameInstance` to validate.
* Public API for interacting with the Regal game.
*/
const validateGameInstance = (instance: GameInstanceInternal): void => {
if (
instance === undefined ||
instance.agents === undefined ||
instance.events === undefined ||
instance.output === undefined ||
instance.state === undefined
) {
throw new RegalError("Invalid GameInstance.");
}
};

/**
* Wraps an error into a `RegalError` (if it's not already) that is
* parseable by a `GameResponse`.
*
* @param err Some error object; should have `name`, `stack`, and `message` properties.
*/
const wrapApiErrorAsRegalError = (err: any): RegalError => {
if (!err || !err.name || !err.stack || !err.message) {
return new RegalError("Invalid error object.");
}

// If err is already a RegalError, return it
if (err.message.indexOf("RegalError:") !== -1) {
return err;
}

// Else, create a RegalError
const msg = `An error occurred while executing the request. Details: <${
err.name
}: ${err.message}>`;
const newErr = new RegalError(msg);
newErr.stack = err.stack;

return newErr;
};

/**
* Helper function to build a `GameResponse` based on the return values of
* `Game.postPlayerCommand` or `Game.postStartCommand`, including any output logs.
*
* @param err Any error that was thrown. If defined, the response will be considered failed.
* @param newInstance The new `GameInstance` to be returned to the client.
*/
const buildLogResponse = (
err: RegalError,
newInstance: GameInstance
): GameResponse => {
let response: GameResponse;

if (err !== undefined) {
const output: GameResponseOutput = {
error: err,
wasSuccessful: false
};
response = {
output
};
} else {
const output: GameResponseOutput = {
log: newInstance.output.lines,
wasSuccessful: true
};
response = {
instance: newInstance,
output
};
}

return response;
};

const NOT_INITALIZED_ERROR_MSG =
"Game has not been initalized. Did you remember to call Game.init?";

/**
* Game API static class.
*
* Each method returns a `GameResponse` and does not modify
* any arguments passed into it.
*/
export class Game {
/** Whether `Game.init` has been called. */
public static get isInitialized() {
return this._isInitialized;
}

/**
* Initializes the game with the given metadata.
* This must be called before any game commands may be executed.
*
* @param metadata The game's configuration metadata.
*/
public static init(metadata: GameMetadata): void {
if (Game._isInitialized) {
throw new RegalError("Game has already been initialized.");
}
Game._isInitialized = true;

ContextManager.init();
MetadataManager.setMetadata(metadata);
}

/** Resets the game's static classes. */
public static reset(): void {
Game._isInitialized = false;
ContextManager.reset();
HookManager.reset();
StaticAgentRegistry.reset();
MetadataManager.reset();
}

export interface GameApi {
/**
* Gets the game's metadata. Note that this is not specific
* to any `GameInstance`, but refers to the game's static context.
*
* @returns A `GameResponse` containing the game's metadata as output,
* if the request was successful. Otherwise, the response will contain an error.
*/
public static getMetadataCommand(): GameResponse {
let metadata: GameMetadata;
let err: RegalError;

try {
if (!Game._isInitialized) {
throw new RegalError(NOT_INITALIZED_ERROR_MSG);
}
metadata = MetadataManager.getMetadata();
} catch (error) {
err = wrapApiErrorAsRegalError(error);
}

const output =
err !== undefined
? { error: err, wasSuccessful: false }
: { metadata, wasSuccessful: true };

return { output };
}
getMetadataCommand(): GameResponse;

/**
* Submits a command that was entered by the player, usually to trigger
Expand All @@ -179,40 +39,7 @@ export class Game {
* values and any output logged during the game cycle's events, if the request
* was successful. Otherwise, the response will contain an error.
*/
public static postPlayerCommand(
instance: GameInstance,
command: string
): GameResponse {
let newInstance: GameInstanceInternal;
let err: RegalError;

try {
if (!Game._isInitialized) {
throw new RegalError(NOT_INITALIZED_ERROR_MSG);
}
const oldInstance = instance as GameInstanceInternal;
validateGameInstance(oldInstance);

if (command === undefined) {
throw new RegalError("Command must be defined.");
}
if (HookManager.playerCommandHook === undefined) {
throw new RegalError(
"onPlayerCommand has not been implemented by the game developer."
);
}

newInstance = oldInstance.recycle();
newInstance.agents.scrubAgents();

const activatedEvent = HookManager.playerCommandHook(command);
newInstance.events.invoke(activatedEvent);
} catch (error) {
err = wrapApiErrorAsRegalError(error);
}

return buildLogResponse(err, newInstance);
}
postPlayerCommand(instance: GameInstance, command: string): GameResponse;

/**
* Triggers the start of a new game instance.
Expand All @@ -227,30 +54,7 @@ export class Game {
* logged during the game cycle's events, if the request was successful.
* Otherwise, the response will contain an error.
*/
public static postStartCommand(
options: Partial<GameOptions> = {}
): GameResponse {
let newInstance: GameInstance;
let err: RegalError;

try {
if (!Game._isInitialized) {
throw new RegalError(NOT_INITALIZED_ERROR_MSG);
}
if (HookManager.startCommandHook === undefined) {
throw new RegalError(
"onStartCommand has not been implemented by the game developer."
);
}

newInstance = buildGameInstance(options);
newInstance.events.invoke(HookManager.startCommandHook);
} catch (error) {
err = wrapApiErrorAsRegalError(error);
}

return buildLogResponse(err, newInstance);
}
postStartCommand(options?: Partial<GameOptions>): GameResponse;

/**
* Reverts the effects of the last command that modified the game instance.
Expand All @@ -264,40 +68,7 @@ export class Game {
* @returns A `GameResponse` containing a new `GameInstance` with updated
* values, if the request was successful. Otherwise, the response will contain an error.
*/
public static postUndoCommand(instance: GameInstance): GameResponse {
let newInstance: GameInstanceInternal;
let err: RegalError;

try {
if (!Game._isInitialized) {
throw new RegalError(NOT_INITALIZED_ERROR_MSG);
}
const oldInstance = instance as GameInstanceInternal;
validateGameInstance(oldInstance);

if (!HookManager.beforeUndoCommandHook(instance)) {
throw new RegalError("Undo is not allowed here.");
}

newInstance = oldInstance.revert();
} catch (error) {
err = wrapApiErrorAsRegalError(error);
}

return err !== undefined
? {
output: {
error: err,
wasSuccessful: false
}
}
: {
instance: newInstance,
output: {
wasSuccessful: true
}
};
}
postUndoCommand(instance: GameInstance): GameResponse;

/**
* Updates the values of the named game options in the `GameInstance`.
Expand All @@ -309,50 +80,8 @@ export class Game {
* @returns A `GameResponse` containing a new `GameInstance` with updated
* options, if the request was successful. Otherwise, the response will contain an error.
*/
public static postOptionCommand(
postOptionCommand(
instance: GameInstance,
options: Partial<GameOptions>
): GameResponse {
let newInstance: GameInstanceInternal;
let err: RegalError;

try {
if (!Game._isInitialized) {
throw new RegalError(NOT_INITALIZED_ERROR_MSG);
}
const oldInstance = instance as GameInstanceInternal;
validateGameInstance(oldInstance);

const oldOverrideKeys = Object.keys(oldInstance.options.overrides);
const newOptionKeys = Object.keys(options);

const newOptions: Partial<GameOptions> = {};

oldOverrideKeys
.filter(key => !newOptionKeys.includes(key))
.forEach(key => (newOptions[key] = oldInstance.options[key]));
newOptionKeys.forEach(key => (newOptions[key] = options[key]));

newInstance = oldInstance.recycle(newOptions);
} catch (error) {
err = wrapApiErrorAsRegalError(error);
}

return err !== undefined
? {
output: {
error: err,
wasSuccessful: false
}
}
: {
instance: newInstance,
output: {
wasSuccessful: true
}
};
}

/** Internal variable to track whether Game.init has been called. */
private static _isInitialized: boolean = false;
): GameResponse;
}

0 comments on commit e4b6e4f

Please sign in to comment.