Skip to content

Commit

Permalink
Merge pull request #34 from regal/feat/26-api
Browse files Browse the repository at this point in the history
feat(api): Implements Game.postStartCommand and related game configuration
  • Loading branch information
jcowman2 committed Sep 7, 2018
2 parents bd62043 + b19c9ed commit d0d0dde
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 46 deletions.
14 changes: 1 addition & 13 deletions src/game-config.ts → src/config/game-metadata.ts
@@ -1,16 +1,4 @@
/**
* Represents game options that are configurable by a Regal client.
*/
export interface GameOptions {
/** Whether output of type `DEBUG` should be returned to the client. Defaults to false. */
debug?: boolean;

/** Whether output of type `MINOR` should be returned to the client. Defaults to true. */
showMinor?: boolean;

/** Game options that cannot be changed by a Regal client. Defaults to none. */
forbidChanges?: string[];
}
import { GameOptions } from "./game-options";

/**
* Metadata about the game, such as its title and author.
Expand Down
75 changes: 75 additions & 0 deletions src/config/game-options.ts
@@ -0,0 +1,75 @@
import { RegalError } from "../error";

/**
* Represents game options that are configurable by a Regal client.
*/
export interface GameOptions {
/** Whether output of type `DEBUG` should be returned to the client. Defaults to false. */
debug: boolean;

/**
* Game options that cannot be changed by a Regal client.
* Can be an array of strings or a boolean.
*
* If an array of strings, these options will not be configurable by a Regal client.
* If `true`, no options will be configurable.
* If `false`, all options will be configurable.
*/
forbidChanges: string[] | boolean;

/** Whether output of type `MINOR` should be returned to the client. Defaults to true. */
showMinor: boolean;
}

export const DEFAULT_GAME_OPTIONS: GameOptions = {
debug: false,
forbidChanges: false,
showMinor: true
};

export const OPTION_KEYS = Object.keys(DEFAULT_GAME_OPTIONS);

export const validateOptions = (options: Partial<GameOptions>): void => {
// Ensure no extraneous options were included.
Object.keys(options).forEach(key => {
if (!OPTION_KEYS.includes(key)) {
throw new RegalError(`Invalid option name <${key}>.`);
}
});

const checkTypeIfDefined = (
key: keyof GameOptions,
expectedType: string
): void => {
const value = options[key];
const actualType = typeof value;

if (options[key] !== undefined) {
if (actualType !== expectedType) {
throw new RegalError(
`The option <${key}> is of type <${actualType}>, must be of type <${expectedType}>.`
);
}
}
};

checkTypeIfDefined("debug", "boolean");

if (options.forbidChanges !== undefined) {
if (Array.isArray(options.forbidChanges)) {
options.forbidChanges.forEach(optionName => {
if (!OPTION_KEYS.includes(optionName)) {
throw new RegalError(
`The option <${optionName}> does not exist.`
);
}
});
} else if (typeof options.forbidChanges !== "boolean") {
throw new RegalError(
`The option <forbidChanges> is of type <${typeof options.forbidChanges}>, must be of type <boolean> or <string[]>.`
);
}
}

checkTypeIfDefined("showMinor", "boolean");
};
3 changes: 3 additions & 0 deletions src/config/index.ts
@@ -0,0 +1,3 @@
export { InstanceOptions } from "./instance-options";
export { GameOptions, DEFAULT_GAME_OPTIONS, OPTION_KEYS } from "./game-options";
export { GameMetadata } from "./game-metadata";
57 changes: 57 additions & 0 deletions src/config/instance-options.ts
@@ -0,0 +1,57 @@
import { RegalError } from "../error";
import GameInstance from "../game-instance";
import {
DEFAULT_GAME_OPTIONS,
GameOptions,
validateOptions
} from "./game-options";

const OPTION_OVERRIDES_PROXY_HANDLER = {
set(
target: Partial<GameOptions>,
propertKey: PropertyKey,
value: any,
receiver: object
) {
throw new RegalError(
"Cannot modify the properties of the instance overrides."
);
}
};

const INSTANCE_OPTIONS_PROXY_HANDLER = {
get(target: InstanceOptions, propertyKey: PropertyKey, receiver: object) {
return target[propertyKey] === undefined
? DEFAULT_GAME_OPTIONS[propertyKey]
: Reflect.get(target, propertyKey, receiver);
},

set(
target: InstanceOptions,
propertKey: PropertyKey,
value: any,
receiver: object
) {
throw new RegalError(
"Cannot modify the properties of InstanceOptions."
);
}
};

export class InstanceOptions implements GameOptions {
public debug: boolean;
public forbidChanges: string[] | boolean;
public showMinor: boolean;

public overrides: Readonly<Partial<GameOptions>>;

constructor(public game: GameInstance, overrides: Partial<GameOptions>) {
this.overrides = new Proxy(overrides, OPTION_OVERRIDES_PROXY_HANDLER);

validateOptions(overrides);

Object.keys(overrides).forEach(key => (this[key] = overrides[key]));

return new Proxy(this, INSTANCE_OPTIONS_PROXY_HANDLER);
}
}
77 changes: 48 additions & 29 deletions src/game-api.ts
@@ -1,7 +1,7 @@
import { StaticAgentRegistry } from "./agents";
import { HookManager } from "./api-hooks";
import { GameOptions } from "./config";
import { RegalError } from "./error";
import { GameOptions } from "./game-config";
import GameInstance from "./game-instance";
import { GameOutput } from "./output";

Expand Down Expand Up @@ -37,6 +37,31 @@ const wrapApiErrorAsRegalError = (err: any): RegalError => {
return newErr;
};

const buildGameResponse = (err: RegalError, newInstance: GameInstance) => {
let response: GameResponse;

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

return response;
};

export class Game {
public static getOptionCommand(
instance: GameInstance,
Expand Down Expand Up @@ -78,35 +103,29 @@ export class Game {
err = wrapApiErrorAsRegalError(error);
}

let response: GameResponse;

if (err !== undefined) {
const output: GameOutput = {
error: err,
wasSuccessful: false
};

response = {
output
};
} else {
const output: GameOutput = {
log: newInstance.output.lines,
wasSuccessful: true
};

response = {
instance: newInstance,
output
};
}

return response;
return buildGameResponse(err, newInstance);
}

public static postStartCommand(options: GameOptions): GameResponse {
// TODO
throw new Error("Method not implemented.");
public static postStartCommand(
options: Partial<GameOptions> = {}
): GameResponse {
let newInstance: GameInstance;
let err: RegalError;

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

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

return buildGameResponse(err, newInstance);
}

public static postUndoCommand(instance: GameInstance): GameResponse {
Expand All @@ -119,7 +138,7 @@ export class Game {
options: GameOptions
): GameResponse {
// TODO
throw new Error("Method not implemented");
throw new Error("Method not implemented.");
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/game-instance.ts
@@ -1,22 +1,25 @@
import { InstanceAgents, InstanceState } from "./agents";
import { GameOptions, InstanceOptions } from "./config";
import { InstanceEvents } from "./events";
import { InstanceOutput } from "./output";

export default class GameInstance {
public agents: InstanceAgents;
public events: InstanceEvents;
public output: InstanceOutput;
public options: InstanceOptions;
public state: any;

constructor() {
constructor(options: Partial<GameOptions> = {}) {
this.agents = new InstanceAgents(this);
this.events = new InstanceEvents(this);
this.output = new InstanceOutput(this);
this.options = new InstanceOptions(this, options);
this.state = new InstanceState(this);
}

public cycle(): GameInstance {
const newGame = new GameInstance();
const newGame = new GameInstance(this.options.overrides);
newGame.events = this.events.cycle(newGame);
newGame.agents = this.agents.cycle(newGame);
newGame.output = this.output.cycle(newGame);
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Expand Up @@ -17,7 +17,7 @@ export {
on
} from "./events";
export { Game, GameResponse } from "./game-api";
export { GameOptions, GameMetadata } from "./game-config";
export { GameOptions, GameMetadata } from "./config";
export * from "./game-instance";
export {
OutputLineType,
Expand Down
2 changes: 1 addition & 1 deletion src/output.ts
Expand Up @@ -5,8 +5,8 @@
* @license MIT (see https://github.com/regal/regal)
*/

import { GameMetadata, GameOptions } from "./config";
import { RegalError } from "./error";
import { GameMetadata, GameOptions } from "./game-config";
import GameInstance from "./game-instance";

/**
Expand Down

0 comments on commit d0d0dde

Please sign in to comment.