Skip to content

Commit

Permalink
Merge pull request #56 from regal/fix/47-scrub-agent
Browse files Browse the repository at this point in the history
fix(agent): All inaccessible data is scrubbed from InstanceAgents before each player command
  • Loading branch information
jcowman2 committed Oct 30, 2018
2 parents 96d5242 + e9e24b5 commit 4cad6dd
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 9 deletions.
45 changes: 45 additions & 0 deletions src/agents/agent-scrub.ts
@@ -0,0 +1,45 @@
/**
* Contains `scrubAgents` function, which deletes all agents in an
* `InstanceAgents` that are not accessible from the instance's state.
*
* Copyright (c) 2018 Joseph R Cowman
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { isAgent } from "./agent-model";
import { InstanceAgents, propertyIsAgentId } from "./instance-agents";

/**
* Traverses all agents that are accessible from the `GameInstance`'s
* state via breadth-first search. All remaining agents (the ones with
* no references to them) are deleted from the `InstanceAgents`.
*
* If run at the improper time, this will break event sourcing and/or
* instance reverting. Make sure to use this correctly.
*
* @param agents The `InstanceAgents` to be scrubbed.
*/
export const scrubAgents = (agents: InstanceAgents): void => {
const seen = new Set<number>();
const q = [0]; // Start at the state, which always has an id of zero

while (q.length > 0) {
const id = q.shift();
seen.add(id);

for (const prop of agents.getAgentPropertyKeys(id)) {
const val = agents.getAgentProperty(id, prop);
if (isAgent(val) && !seen.has(val.id)) {
q.push(val.id);
}
}
}

const waste = Object.keys(agents)
.filter(propertyIsAgentId)
.filter(id => !seen.has(Number(id)));

for (const id of waste) {
delete agents[id];
}
};
1 change: 1 addition & 0 deletions src/agents/index.ts
Expand Up @@ -15,3 +15,4 @@ export { StaticAgentRegistry } from "./static-agent-registry";
export { buildRevertFunction } from "./agent-revert";
export { PropertyChange, PropertyOperation } from "./agent-properties";
export { activateAgent } from "./activate-agent";
export { scrubAgents } from "./agent-scrub";
12 changes: 6 additions & 6 deletions src/agents/instance-agents.ts
Expand Up @@ -127,6 +127,7 @@ export const recycleInstanceAgents = (
newInstance: GameInstance
): InstanceAgents => {
const newAgents = new InstanceAgentsImpl(newInstance);
newAgents._nextId = (oldAgents as InstanceAgentsImpl)._nextId;

for (const formerAgent of oldAgents.agentManagers()) {
const id = formerAgent.id;
Expand Down Expand Up @@ -167,8 +168,11 @@ export const propertyIsAgentId = (property: PropertyKey) => {

/** Implementation of `InstanceAgents`. */
class InstanceAgentsImpl implements InstanceAgents {
public _nextId: number;

constructor(public game: GameInstance) {
this.createAgentManager(0);
this._nextId = StaticAgentRegistry.getNextAvailableId();
}

public agentManagers(): AgentManager[] {
Expand All @@ -178,12 +182,8 @@ class InstanceAgentsImpl implements InstanceAgents {
}

public reserveNewId(): number {
const agentKeys = Object.keys(this).filter(propertyIsAgentId);
let id = StaticAgentRegistry.getNextAvailableId();

while (agentKeys.includes(id.toString())) {
id++;
}
const id = this._nextId;
this._nextId++;

this.createAgentManager(id);

Expand Down
7 changes: 6 additions & 1 deletion src/game-api.ts
Expand Up @@ -8,7 +8,11 @@
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { buildRevertFunction, StaticAgentRegistry } from "./agents";
import {
buildRevertFunction,
scrubAgents,
StaticAgentRegistry
} from "./agents";
import { HookManager } from "./api-hooks";
import { GameMetadata, GameOptions, MetadataManager } from "./config";
import { ContextManager } from "./context-manager";
Expand Down Expand Up @@ -191,6 +195,7 @@ export class Game {
}

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

const activatedEvent = HookManager.playerCommandHook(command);
newInstance.events.invoke(activatedEvent);
Expand Down
92 changes: 91 additions & 1 deletion test/unit/agents.test.ts
Expand Up @@ -8,7 +8,8 @@ import {
Agent,
PropertyOperation,
StaticAgentRegistry,
buildRevertFunction
buildRevertFunction,
scrubAgents
} from "../../src/agents";
import GameInstance from "../../src/game-instance";
import { RegalError } from "../../src/error";
Expand Down Expand Up @@ -545,6 +546,7 @@ describe("Agents", function() {
})(myGame);

expect(myGame.agents).to.deep.equal({
_nextId: 2,
game: myGame,
0: {
id: 0,
Expand Down Expand Up @@ -619,6 +621,7 @@ describe("Agents", function() {
})(myGame);

expect(myGame.agents).to.deep.equal({
_nextId: 2,
game: myGame,
0: {
id: 0,
Expand Down Expand Up @@ -751,6 +754,7 @@ describe("Agents", function() {
})(myGame);

expect(myGame.agents).to.deep.equal({
_nextId: 5,
game: myGame,
0: {
id: 0,
Expand Down Expand Up @@ -1170,6 +1174,7 @@ describe("Agents", function() {
myGame.state.arr = arr;

expect(myGame.agents).to.deep.equal({
_nextId: 10,
game: myGame,
0: {
id: 0,
Expand Down Expand Up @@ -1864,6 +1869,7 @@ describe("Agents", function() {

// Verify initial condition
expect(myGame.agents).to.deep.equal({
_nextId: 2,
game: myGame,
0: {
id: 0,
Expand Down Expand Up @@ -1923,6 +1929,7 @@ describe("Agents", function() {
const myGame2 = myGame.recycle();

expect(myGame2.agents).to.deep.equal({
_nextId: 2,
game: myGame2,
0: {
id: 0,
Expand Down Expand Up @@ -1990,6 +1997,7 @@ describe("Agents", function() {

// Verify initial condition
expect(myGame.agents).to.deep.equal({
_nextId: 2,
game: myGame,
0: {
id: 0,
Expand Down Expand Up @@ -2042,6 +2050,7 @@ describe("Agents", function() {
const myGame2 = myGame.recycle();

expect(myGame2.agents).to.deep.equal({
_nextId: 2,
game: myGame2,
0: {
id: 0,
Expand Down Expand Up @@ -2112,6 +2121,7 @@ describe("Agents", function() {
const game2 = myGame.recycle();

expect(game2.agents).to.deep.equal({
_nextId: 2,
game: game2,
0: {
id: 0,
Expand Down Expand Up @@ -2171,6 +2181,7 @@ describe("Agents", function() {
const game2 = myGame.recycle();

expect(game2.agents).to.deep.equal({
_nextId: 2,
game: game2,
0: {
id: 0,
Expand Down Expand Up @@ -2222,6 +2233,7 @@ describe("Agents", function() {
const game2 = myGame.recycle();

expect(game2.agents).to.deep.equal({
_nextId: 2,
game: game2,
0: {
id: 0,
Expand Down Expand Up @@ -2834,4 +2846,82 @@ describe("Agents", function() {
expect(res.state.agents[5].name).to.equal("D5");
});
});

describe("Scrubbing", function() {
it("Scrubbing an InstanceAgents deletes all agents that have no references to them", function() {
Game.init();
const myGame = new GameInstance();

myGame.state.dummy = new Parent(new Dummy("D1", 10));
const float = myGame.using(new Dummy("D2", 15));

expect(
myGame.agents.agentManagers().map(am => am.id)
).to.deep.equal([0, 1, 2, 3]);
expect(myGame.state.dummy).to.deep.equal({
id: 1,
child: {
id: 2,
name: "D1",
health: 10
}
});
expect(myGame.agents.getAgentProperty(3, "name")).to.equal("D2");

scrubAgents(myGame.agents);

expect(
myGame.agents.agentManagers().map(am => am.id)
).to.deep.equal([0, 1, 2]);
expect(myGame.state.dummy).to.deep.equal({
id: 1,
child: {
id: 2,
name: "D1",
health: 10
}
});

expect(() => myGame.agents.getAgentProperty(3, "name")).to.throw(
RegalError,
"No agent with the id <3> exists."
);
});

it("Scrubbing an InstanceAgents finds agent array references", function() {
Game.init();
const myGame = new GameInstance();

const p = myGame.using(
new MultiParent([new Dummy("D1", 1), new Dummy("D2", 2)])
);
myGame.state.arr = [true, new Dummy("D3", 3), p, p.children[0]];

expect(
myGame.agents.agentManagers().map(am => am.id)
).to.deep.equal([0, 1, 2, 3, 4, 5, 6]);
expect(myGame.state.arr[3]).to.deep.equal({
id: 3,
name: "D1",
health: 1
});

scrubAgents(myGame.agents);
expect(
myGame.agents.agentManagers().map(am => am.id)
).to.deep.equal([0, 1, 2, 3, 4, 5, 6]);

(myGame.state.arr as any[]).splice(2, 1);
scrubAgents(myGame.agents);

expect(
myGame.agents.agentManagers().map(am => am.id)
).to.deep.equal([0, 3, 5, 6]);
expect(myGame.state.arr[2]).to.deep.equal({
id: 3,
name: "D1",
health: 1
});
});
});
});
6 changes: 6 additions & 0 deletions test/unit/config.test.ts
Expand Up @@ -231,6 +231,7 @@ describe("Config", function() {
});

expect(response.instance.agents).to.deep.equal({
_nextId: 1,
game: response.instance,
"0": {
id: 0,
Expand Down Expand Up @@ -271,6 +272,7 @@ describe("Config", function() {
response = Game.postPlayerCommand(response.instance, "Lars");

expect(response.instance.agents).to.deep.equal({
_nextId: 2,
game: response.instance,
"0": {
id: 0,
Expand Down Expand Up @@ -407,6 +409,7 @@ describe("Config", function() {
response = Game.postPlayerCommand(response.instance, "Jeffrey");

expect(response.instance.agents).to.deep.equal({
_nextId: 3,
game: response.instance,
"0": {
id: 0,
Expand Down Expand Up @@ -578,6 +581,7 @@ describe("Config", function() {
});

expect(response.instance.agents).to.deep.equal({
_nextId: 1,
game: response.instance,
"0": {
id: 0,
Expand Down Expand Up @@ -609,6 +613,7 @@ describe("Config", function() {
response = Game.postPlayerCommand(response.instance, "Lars");

expect(response.instance.agents).to.deep.equal({
_nextId: 2,
game: response.instance,
"0": {
id: 0,
Expand Down Expand Up @@ -685,6 +690,7 @@ describe("Config", function() {
response = Game.postPlayerCommand(response.instance, "Jeffrey");

expect(response.instance.agents).to.deep.equal({
_nextId: 3,
game: response.instance,
"0": {
id: 0,
Expand Down

0 comments on commit 4cad6dd

Please sign in to comment.