Skip to content

Commit

Permalink
Merge pull request #37 from regal/feat/26-undo-command
Browse files Browse the repository at this point in the history
feat(api): Implements Game.postUndoCommand
  • Loading branch information
jcowman2 committed Sep 9, 2018
2 parents de22bb8 + 83a9e82 commit dc171b4
Show file tree
Hide file tree
Showing 10 changed files with 355 additions and 33 deletions.
71 changes: 71 additions & 0 deletions src/agents/agent-revert.ts
@@ -0,0 +1,71 @@
import { noop, on } from "../events";
import { Agent } from "./agent-model";
import { AgentRecord, PropertyChange } from "./agent-record";
import { InstanceAgents, propertyIsAgentId } from "./instance-agents";
import { StaticAgentRegistry } from "./static-agent";

/**
* Builds a `TrackedEvent` that reverts all the changes to a given `InstanceAgents` since a specified event.
*
* Does not modify the `InstanceAgents` argument.
*
* @param agents The agent history on which the revert function will be based.
* @param revertTo The id of the `TrackedEvent` to which the state will be reverted. Defaults to 0 (the default event id).
* @returns A `TrackedEvent` that will perform the revert function onto the `GameInstance` on which it's invoked.
*/
export const buildRevertFunction = (
agents: InstanceAgents,
revertTo: number = 0
) => {
const agentIds = Object.keys(agents)
.filter(propertyIsAgentId)
.map(idStr => Number.parseInt(idStr, 10));

return on("REVERT", game => {
const target = game.agents;

agentIds.forEach(id => {
const record = agents[id] as AgentRecord;
const recordKeys = Object.keys(record);

// Proxy for all changes
const agent = new Agent(id, target.game);

recordKeys
// Exclude game and id properties
.filter(key => key !== "game" && key !== "_id")
.forEach(key => {
const changelog: PropertyChange[] = record[key];

// Get the changes that happened before the target event
const acceptableChanges = changelog.filter(
change => change.eventId <= revertTo
);

if (acceptableChanges.length === 0) {
// If all changes to the property happened after the target event, delete/reset it
if (StaticAgentRegistry.hasAgentProperty(id, key)) {
agent[key] = StaticAgentRegistry.getAgentProperty(
id,
key
);
} else {
delete agent[key];
}
} else {
// Otherwise, set the property to its value right after the target event
const lastAcceptableValue = acceptableChanges[0].final;

if (
target.getAgentProperty(id, key) !==
lastAcceptableValue
) {
agent[key] = lastAcceptableValue;
}
}
});
});

return noop;
});
};
1 change: 1 addition & 0 deletions src/agents/index.ts
Expand Up @@ -4,3 +4,4 @@ export { AgentRecord, PropertyOperation, PropertyChange } from "./agent-record";
export { AgentReference } from "./agent-reference";
export { InstanceAgents } from "./instance-agents";
export { InstanceState } from "./instance-state";
export { buildRevertFunction } from "./agent-revert";
8 changes: 8 additions & 0 deletions src/agents/instance-agents.ts
Expand Up @@ -63,6 +63,14 @@ export class InstanceAgents {
}
} else {
value = agentRecord.getProperty(property);

if (
value === undefined &&
!agentRecord.propertyWasDeleted(property) &&
StaticAgentRegistry.hasAgentProperty(agentId, property)
) {
value = StaticAgentRegistry.getAgentProperty(agentId, property);
}
}

if (isAgentReference(value)) {
Expand Down
35 changes: 31 additions & 4 deletions src/game-api.ts
@@ -1,6 +1,6 @@
import { StaticAgentRegistry } from "./agents";
import { buildRevertFunction, StaticAgentRegistry } from "./agents";
import { HookManager } from "./api-hooks";
import { GameOptions, OPTION_KEYS } from "./config";
import { GameOptions, MetadataManager, OPTION_KEYS } from "./config";
import { RegalError } from "./error";
import GameInstance from "./game-instance";
import { GameOutput } from "./output";
Expand Down Expand Up @@ -121,8 +121,35 @@ export class Game {
}

public static postUndoCommand(instance: GameInstance): GameResponse {
// TODO
throw new Error("Method not implemented.");
let newInstance: GameInstance;
let err: RegalError;

try {
validateGameInstance(instance);

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

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

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

public static postOptionCommand(
Expand Down
181 changes: 172 additions & 9 deletions test/agents.test.ts
Expand Up @@ -8,7 +8,8 @@ import {
StaticAgentRegistry,
AgentRecord,
AgentReference,
PropertyOperation
PropertyOperation,
buildRevertFunction
} from "../src/agents";
import { log, getDemoMetadata } from "./test-utils";
import { on, noop, EventRecord } from "../src/events";
Expand All @@ -21,18 +22,15 @@ class Dummy extends Agent {
}

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

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

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

describe("Agent Behavior", function() {
it("Registering an agent adds its properties to the instance", function() {
const myGame = new GameInstance();
Expand Down Expand Up @@ -389,14 +387,13 @@ describe("Agents", function() {
on("MODIFY", game => {
const myDummy = dummy.register(game);
myDummy.health += 15;
myDummy.name = "Jeff";
myDummy["newProp"] = "newValue";

return noop;
})(myGame);

expect(myGame.agents.getAgentProperty(1, "health")).to.equal(25);
expect(myGame.agents.getAgentProperty(1, "name")).to.equal("Jeff");
expect(myGame.agents.getAgentProperty(1, "name")).to.equal("D1");
expect(myGame.agents.getAgentProperty(1, "newProp")).to.equal(
"newValue"
);
Expand Down Expand Up @@ -1091,4 +1088,170 @@ describe("Agents", function() {
});
});
});

describe("Reverting", function() {
it("Reverting the effects of one event to the one before", function() {
const start = on("START", game => {
game.state.foo = true;
game.state.dummy = new Dummy("Lars", 10);
return noop;
});

const mod = (name: string) =>
on("MOD", game => {
game.state.foo = false;
game.state.dummy.name = name;
return noop;
});

const myGame = new GameInstance();
start(myGame);
mod("Jimbo")(myGame);

expect(myGame.state.foo).to.be.false;
expect(myGame.state.dummy.name).to.equal("Jimbo");
expect(myGame.state.dummy.health).to.equal(10);

const revert = buildRevertFunction(myGame.agents, 1);
revert(myGame);

expect(myGame.state.foo).to.be.true;
expect(myGame.state.dummy.name).to.equal("Lars");
expect(myGame.state.dummy.health).to.equal(10);
});

it("Reverting the effects of many events back to the first one", function() {
const init = on("INIT", game => {
game.state.foo = "Hello, world!";
return noop;
});

const mod = (num: number) =>
on(`MOD ${num}`, game => {
game.state.foo += `-${num}`;
game.state[num] = `Yo ${num}`;
return noop;
});

const myGame = new GameInstance();
init(myGame);

for (let x = 0; x < 10; x++) {
mod(x)(myGame);
}

expect(myGame.state.foo).to.equal(
"Hello, world!-0-1-2-3-4-5-6-7-8-9"
);
expect(myGame.state[0]).to.equal("Yo 0");
expect(myGame.state[5]).to.equal("Yo 5");
expect(myGame.state[9]).to.equal("Yo 9");

const revert = buildRevertFunction(myGame.agents, 1);
revert(myGame);

expect(myGame.state.foo).to.equal("Hello, world!");
expect(myGame.state[0]).to.be.undefined;
expect(myGame.state[5]).to.be.undefined;
expect(myGame.state[9]).to.be.undefined;
});

it("Reverting the effects of many events over multiple steps", function() {
const init = on("INIT", game => {
game.state.foo = "Hello, world!";
return noop;
});

const mod = (num: number) =>
on(`MOD ${num}`, game => {
game.state.foo += `-${num}`;
game.state[num] = `Yo ${num}`;
return noop;
});

const myGame = new GameInstance();
init(myGame);

for (let x = 0; x < 10; x++) {
mod(x)(myGame);
}

expect(myGame.state.foo).to.equal(
"Hello, world!-0-1-2-3-4-5-6-7-8-9"
);
expect(myGame.state[0]).to.equal("Yo 0");
expect(myGame.state[5]).to.equal("Yo 5");
expect(myGame.state[9]).to.equal("Yo 9");

buildRevertFunction(myGame.agents, 8)(myGame);

expect(myGame.state.foo).to.equal("Hello, world!-0-1-2-3-4-5-6");
expect(myGame.state[0]).to.equal("Yo 0");
expect(myGame.state[5]).to.equal("Yo 5");
expect(myGame.state[9]).to.be.undefined;

buildRevertFunction(myGame.agents, 6)(myGame);

expect(myGame.state.foo).to.equal("Hello, world!-0-1-2-3-4");
expect(myGame.state[0]).to.equal("Yo 0");
expect(myGame.state[5]).to.be.undefined;
expect(myGame.state[9]).to.be.undefined;

buildRevertFunction(myGame.agents, 4)(myGame);

expect(myGame.state.foo).to.equal("Hello, world!-0-1-2");
expect(myGame.state[0]).to.equal("Yo 0");
expect(myGame.state[5]).to.be.undefined;
expect(myGame.state[9]).to.be.undefined;

buildRevertFunction(myGame.agents, 1)(myGame);

expect(myGame.state.foo).to.equal("Hello, world!");
expect(myGame.state[0]).to.be.undefined;
expect(myGame.state[5]).to.be.undefined;
expect(myGame.state[9]).to.be.undefined;
});

it("Reverting to before an agent was registered", function() {
const init = on("INIT", game => {
game.state.foo = "Hello, world!";
game.state.dummy = new Dummy("Lars", 10);
return noop;
});

const myGame = new GameInstance();
init(myGame);
buildRevertFunction(myGame.agents, 0)(myGame);

expect(myGame.state.foo).to.be.undefined;
expect(myGame.state.dummy).to.be.undefined;
expect(myGame.agents.getAgentProperty(1, "name")).to.be.undefined;
expect(myGame.agents.getAgentProperty(1, "health")).to.be.undefined;
});

it("Reverting changes to static agents", function() {
const staticDummy = new Dummy("Lars", 15).static();

const myGame = new GameInstance();
on("FUNC", game => {
const dummy = staticDummy.register(game);
dummy.name = "Jimbo";
dummy["bippity"] = "boppity";
return noop;
})(myGame);

expect(myGame.agents.getAgentProperty(1, "name")).to.equal("Jimbo");
expect(myGame.agents.getAgentProperty(1, "health")).to.equal(15);
expect(myGame.agents.getAgentProperty(1, "bippity")).to.equal(
"boppity"
);

buildRevertFunction(myGame.agents)(myGame);

expect(myGame.agents.getAgentProperty(1, "name")).to.equal("Lars");
expect(myGame.agents.getAgentProperty(1, "health")).to.equal(15);
expect(myGame.agents.getAgentProperty(1, "bippity")).to.be
.undefined;
});
});
});
9 changes: 3 additions & 6 deletions test/api-hooks.test.ts
Expand Up @@ -15,18 +15,15 @@ import { RegalError } from "../src/error";
import { MetadataManager } from "../src/config";

describe("API Hooks", function() {
before(function() {
beforeEach(function() {
HookManager.resetHooks();
MetadataManager.forceConfig(getDemoMetadata());
});

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

beforeEach(function() {
HookManager.resetHooks();
});

it("playerCommandHook starts out undefined", function() {
expect(HookManager.playerCommandHook).to.be.undefined;
});
Expand Down
4 changes: 2 additions & 2 deletions test/events.test.ts
Expand Up @@ -19,11 +19,11 @@ import { OutputLineType } from "../src/output";
import { MetadataManager } from "../src/config";

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

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

Expand Down

0 comments on commit dc171b4

Please sign in to comment.