Skip to content

Commit

Permalink
Merge pull request #36 from regal/feat/26-get-post-option
Browse files Browse the repository at this point in the history
feat(api): Implements Game.postOptionCommand
  • Loading branch information
jcowman2 committed Sep 8, 2018
2 parents b5d4c64 + f332997 commit de22bb8
Show file tree
Hide file tree
Showing 17 changed files with 397 additions and 127 deletions.
27 changes: 0 additions & 27 deletions src/config/game-metadata.ts

This file was deleted.

39 changes: 24 additions & 15 deletions src/config/game-options.ts
Expand Up @@ -4,26 +4,29 @@ 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.
* Game options that can be overridden by a Regal client.
* Can be an array of strings or a boolean. Defaults to true.
*
* 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.
* If an array of strings, these options will be configurable by a Regal client.
* Note that `allowOptions` is never configurable, and including it will throw an error.
*
* If `true`, all options except `allowOverrides` will be configurable.
*
* If `false`, no options will be configurable.
*/
forbidChanges: string[] | boolean;
allowOverrides: string[] | boolean;

/** 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;
}

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

Expand Down Expand Up @@ -55,18 +58,24 @@ export const validateOptions = (options: Partial<GameOptions>): void => {

checkTypeIfDefined("debug", "boolean");

if (options.forbidChanges !== undefined) {
if (Array.isArray(options.forbidChanges)) {
options.forbidChanges.forEach(optionName => {
if (options.allowOverrides !== undefined) {
if (Array.isArray(options.allowOverrides)) {
options.allowOverrides.forEach(optionName => {
if (!OPTION_KEYS.includes(optionName)) {
throw new RegalError(
`The option <${optionName}> does not exist.`
);
}
});
} else if (typeof options.forbidChanges !== "boolean") {

if (options.allowOverrides.includes("allowOverrides")) {
throw new RegalError(
"The option <allowOverrides> is not allowed to be overridden."
);
}
} else if (typeof options.allowOverrides !== "boolean") {
throw new RegalError(
`The option <forbidChanges> is of type <${typeof options.forbidChanges}>, must be of type <boolean> or <string[]>.`
`The option <allowOverrides> is of type <${typeof options.allowOverrides}>, must be of type <boolean> or <string[]>.`
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/config/index.ts
@@ -1,3 +1,3 @@
export { InstanceOptions } from "./instance-options";
export { InstanceOptions, ensureOverridesAllowed } from "./instance-options";
export { GameOptions, DEFAULT_GAME_OPTIONS, OPTION_KEYS } from "./game-options";
export { GameMetadata } from "./game-metadata";
export { GameMetadata, MetadataManager } from "./metadata";
68 changes: 52 additions & 16 deletions src/config/instance-options.ts
Expand Up @@ -5,14 +5,10 @@ import {
GameOptions,
validateOptions
} from "./game-options";
import { MetadataManager } from "./metadata";

const OPTION_OVERRIDES_PROXY_HANDLER = {
set(
target: Partial<GameOptions>,
propertKey: PropertyKey,
value: any,
receiver: object
) {
set() {
throw new RegalError(
"Cannot modify the properties of the InstanceOption option overrides."
);
Expand All @@ -26,32 +22,72 @@ const INSTANCE_OPTIONS_PROXY_HANDLER = {
: Reflect.get(target, propertyKey, receiver);
},

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

export const ensureOverridesAllowed = (
overrides: Partial<GameOptions>,
allowOverrides: string[] | boolean
): void => {
if (overrides.allowOverrides !== undefined) {
throw new RegalError(
"The allowOverrides option can never be overridden."
);
}

if (Array.isArray(allowOverrides)) {
const overrideKeys = Object.keys(overrides);
const forbiddenKeys = overrideKeys.filter(
key => !allowOverrides.includes(key)
);

if (forbiddenKeys.length > 0) {
throw new RegalError(
`The following option overrides are forbidden: <${forbiddenKeys}>.`
);
}
} else {
// Option is a boolean
if (!allowOverrides && Object.keys(overrides).length > 0) {
throw new RegalError("No option overrides are allowed.");
}
}
};

export class InstanceOptions implements GameOptions {
public allowOverrides: string[] | boolean;
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]));
const configOpts = MetadataManager.getMetadata().options;
validateOptions(configOpts);

const allowOverrides =
configOpts.allowOverrides !== undefined
? configOpts.allowOverrides
: DEFAULT_GAME_OPTIONS.allowOverrides;

ensureOverridesAllowed(overrides, allowOverrides);

const overrideKeys = Object.keys(overrides);
const configKeys = Object.keys(configOpts);

configKeys
.filter(key => !overrideKeys.includes(key))
.forEach(key => (this[key] = configOpts[key]));

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

this.overrides = new Proxy(overrides, OPTION_OVERRIDES_PROXY_HANDLER);
return new Proxy(this, INSTANCE_OPTIONS_PROXY_HANDLER);
}
}
47 changes: 47 additions & 0 deletions src/config/metadata.ts
@@ -0,0 +1,47 @@
import { GameOptions } from "./game-options";

/**
* Metadata about the game, such as its title and author.
*/
export interface GameMetadata {
/** The game's title. */
name: string;

/** The game's author. */
author?: string;

/** A brief description of the game. */
headline?: string;

/** The full description of the game. */
description?: string;

/** The URL of the project's homepage. */
homepage?: string;

/** The URL of the project's repository */
repository?: string;

/** User-defined values for the game's options. */
options: Partial<GameOptions>;
}

const accessConfigFile = (): GameMetadata => {
throw new Error("Not yet implemented."); // TODO
};

export class MetadataManager {
public static getMetadata(): GameMetadata {
return this._retrievalFunction();
}

public static forceConfig(config: GameMetadata): void {
this._retrievalFunction = () => config;
}

public static reset(): void {
this._retrievalFunction = accessConfigFile;
}

private static _retrievalFunction = accessConfigFile;
}
54 changes: 39 additions & 15 deletions src/game-api.ts
@@ -1,6 +1,6 @@
import { StaticAgentRegistry } from "./agents";
import { HookManager } from "./api-hooks";
import { GameOptions } from "./config";
import { GameOptions, OPTION_KEYS } from "./config";
import { RegalError } from "./error";
import GameInstance from "./game-instance";
import { GameOutput } from "./output";
Expand Down Expand Up @@ -37,7 +37,7 @@ const wrapApiErrorAsRegalError = (err: any): RegalError => {
return newErr;
};

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

if (err !== undefined) {
Expand All @@ -63,14 +63,6 @@ const buildGameResponse = (err: RegalError, newInstance: GameInstance) => {
};

export class Game {
public static getOptionCommand(
instance: GameInstance,
options: string[]
): GameResponse {
// TODO
throw new Error("Method not implemented.");
}

public static getMetadataCommand(): GameResponse {
// TODO
throw new Error("Method not implemented.");
Expand Down Expand Up @@ -103,7 +95,7 @@ export class Game {
err = wrapApiErrorAsRegalError(error);
}

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

public static postStartCommand(
Expand All @@ -125,7 +117,7 @@ export class Game {
err = wrapApiErrorAsRegalError(error);
}

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

public static postUndoCommand(instance: GameInstance): GameResponse {
Expand All @@ -135,10 +127,42 @@ export class Game {

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

try {
validateGameInstance(instance);

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

const newOptions: Partial<GameOptions> = {};

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

newInstance = instance.cycle(newOptions);
} catch (error) {
err = wrapApiErrorAsRegalError(error);
}

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

Expand Down
7 changes: 5 additions & 2 deletions src/game-instance.ts
Expand Up @@ -18,8 +18,11 @@ export default class GameInstance {
this.state = new InstanceState(this);
}

public cycle(): GameInstance {
const newGame = new GameInstance(this.options.overrides);
public cycle(newOptions?: Partial<GameOptions>): GameInstance {
const opts =
newOptions === undefined ? this.options.overrides : newOptions;

const newGame = new GameInstance(opts);
newGame.events = this.events.cycle(newGame);
newGame.agents = this.agents.cycle(newGame);
newGame.output = this.output.cycle(newGame);
Expand Down
3 changes: 0 additions & 3 deletions src/output.ts
Expand Up @@ -193,9 +193,6 @@ export interface GameOutput {
/** Contains any lines of output emitted because of the request. */
log?: OutputLine[];

/** Contains any game options requested by `Game.getOptionCommand` or updated by `Game.postOptionCommand`. */
options?: GameOptions;

/** Contains the game's metadata if `Game.getMetdataCommand` was called. */
metadata?: GameMetadata;
}
11 changes: 10 additions & 1 deletion test/agents.test.ts
Expand Up @@ -10,8 +10,9 @@ import {
AgentReference,
PropertyOperation
} from "../src/agents";
import { log } from "./utils";
import { log, getDemoMetadata } from "./test-utils";
import { on, noop, EventRecord } from "../src/events";
import { MetadataManager } from "../src/config";

class Dummy extends Agent {
constructor(public name: string, public health: number) {
Expand All @@ -20,6 +21,14 @@ class Dummy extends Agent {
}

describe("Agents", function() {
before(function() {
MetadataManager.forceConfig(getDemoMetadata());
});

after(function() {
MetadataManager.reset();
});

beforeEach(function() {
StaticAgentRegistry.resetRegistry();
});
Expand Down

0 comments on commit de22bb8

Please sign in to comment.