Skip to content

Commit

Permalink
Merge pull request #111 from regal/feat-104/prototype-registry
Browse files Browse the repository at this point in the history
Prototype Registry: Replace numeric RandomRecord.id with PK<RandomRecord>
  • Loading branch information
jcowman2 committed Jul 23, 2019
2 parents fc2b122 + 84bac49 commit fa3f6e3
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 39 deletions.
10 changes: 9 additions & 1 deletion src/common/impl/pk-provider-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { RegalError } from "../../error";
import { PK, PKProvider, ReservedPKSet } from "../keys";
import { FK, PK, PKProvider, ReservedPKSet } from "../keys";

export class PKProviderImpl<PKClass> implements PKProvider<PKClass> {
public static readonly START_VALUE = 0;
Expand Down Expand Up @@ -111,4 +111,12 @@ export class PKProviderImpl<PKClass> implements PKProvider<PKClass> {
public countGenerated(): number {
return this.lastPK.index();
}

public forkAfterKey(key: PK<PKClass> | FK<PKClass>): PKProvider<PKClass> {
return new PKProviderImpl(this.buildPK, key, this.reservedPKs);
}

public previous(): PK<PKClass> {
return this.lastPK;
}
}
9 changes: 9 additions & 0 deletions src/common/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ export interface PKProvider<T> {
* Returns the number of keys generated by this `PKProvider`.
*/
countGenerated(): number;

/**
* Creates a new fork of the `PKProvider` which behaves as if `key` was the
* last key generated by it.
*/
forkAfterKey(key: PK<T> | FK<T>): PKProvider<T>;

/** Returns the key that was last generated. */
previous(): PK<T>;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/output/output-line.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { PK } from "../common";

/*
* Contains interfaces that represent lines of output generated
* within a Regal game.
Expand All @@ -8,6 +6,8 @@ import { PK } from "../common";
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { PK } from "../common";

/**
* Conveys semantic meaning of an `OutputLine` to the client.
*/
Expand Down
37 changes: 21 additions & 16 deletions src/random/impl/instance-random-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,58 @@
*/

import Prando from "prando";
import { buildPKProvider, PKProvider } from "../../common";
import { RegalError } from "../../error";
import { GameInstanceInternal } from "../../state";
import { EXPANDED_CHARSET } from "../charsets";
import { InstanceRandomInternal } from "../instance-random-internal";
import { RandomRecord } from "../random-record";

/**
* Constructs an `InstanceRandom` using the current implementation.
* @param game The managing `GameInstance`.
* @param numGenerations The number of generations to start from.
* Defaults to zero.
* @param pkProvider The existing random PK provider (optional).
*/
export const buildInstanceRandom = (
game: GameInstanceInternal,
numGenerations: number = 0
): InstanceRandomInternal => new InstanceRandomImpl(game, numGenerations);
pkProvider?: PKProvider<RandomRecord>
): InstanceRandomInternal => new InstanceRandomImpl(game, pkProvider);

class InstanceRandomImpl implements InstanceRandomInternal {
private _numGenerations: number;
private _generator: Prando;

/** The internal `RandomRecord` `PKProvider`. */
private _pkProvider: PKProvider<RandomRecord>;

public get seed() {
return this.game.options.seed;
}

public get numGenerations() {
return this._numGenerations;
return this._pkProvider.countGenerated();
}

public get lastKey() {
return this._pkProvider.previous();
}

constructor(public game: GameInstanceInternal, numGenerations: number) {
constructor(
public game: GameInstanceInternal,
pkProvider: PKProvider<RandomRecord>
) {
if (this.seed === undefined) {
throw new RegalError(
"Seed must be defined before an InstanceRandom can be constructed."
);
}

this._numGenerations = numGenerations;
this._pkProvider = pkProvider ? pkProvider : buildPKProvider();
this._generator = new Prando(this.seed);
this._generator.skip(numGenerations);
this._generator.skip(this.numGenerations);
}

public recycle(newInstance: GameInstanceInternal): InstanceRandomInternal {
return new InstanceRandomImpl(newInstance, this.numGenerations);
return new InstanceRandomImpl(newInstance, this._pkProvider);
}

public int(min: number, max: number): number {
Expand All @@ -60,14 +70,12 @@ class InstanceRandomImpl implements InstanceRandomInternal {
const value = this._generator.nextInt(min, max);
this.trackRandom(value);

this._numGenerations++;
return value;
}

public decimal(): number {
const value = this._generator.next(0, 1);
this.trackRandom(value);
this._numGenerations++;
return value;
}

Expand All @@ -87,7 +95,6 @@ class InstanceRandomImpl implements InstanceRandomInternal {
const value = this._generator.nextString(length, charset);
this.trackRandom(value);

this._numGenerations++;
return value;
}

Expand All @@ -99,21 +106,19 @@ class InstanceRandomImpl implements InstanceRandomInternal {
const idx = this._generator.nextInt(0, array.length - 1);
this.trackRandom(idx); // Track the index of the selected element, rather than the element itself

this._numGenerations++;
return array[idx];
}

public boolean(): boolean {
const value = this._generator.nextBoolean();
this.trackRandom(value);
this._numGenerations++;
return value;
}

/** Internal helper method to add the `RandomRecord` to the current `EventRecord`. */
private trackRandom(value: string | number | boolean): void {
this.game.events.current.trackRandom({
id: this.numGenerations,
id: this._pkProvider.next(),
value
});
}
Expand Down
8 changes: 8 additions & 0 deletions src/random/instance-random-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { FK } from "../common";
import { GameInstanceInternal } from "../state";
import { InstanceRandom } from "./instance-random";
import { RandomRecord } from "./random-record";

/**
* Internal interface for `InstanceRandom`.
Expand All @@ -23,6 +25,12 @@ export interface InstanceRandomInternal extends InstanceRandom {
*/
readonly numGenerations: number;

/**
* The key of the last `RandomRecord` that was generated, or the default key if
* none have been.
*/
readonly lastKey: FK<RandomRecord>;

/**
* Generates a new `InstanceRandomInternal` for the new `GameInstance`, preserving
* the old `numGenerations` (as `seed` is preserved by the `GameInstance` itself).
Expand Down
6 changes: 4 additions & 2 deletions src/random/random-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
* Licensed under MIT License (see https://github.com/regal/regal)
*/

import { PK } from "../common";

/**
* Record of a single random value's generation.
*/
export interface RandomRecord {
/** The random value's numeric id. */
id: number;
/** The random value's unique id. */
id: PK<RandomRecord>;
/** The random value. */
value: number | string | boolean;
}
23 changes: 16 additions & 7 deletions src/state/impl/game-instance-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "../../agents";
import { isAgentArrayReference } from "../../agents/agent-array-reference";
import { isAgentReference } from "../../agents/agent-reference";
import { FK } from "../../common";
import { buildPKProvider, FK } from "../../common";
import {
buildInstanceOptions,
GameOptions,
Expand All @@ -32,7 +32,11 @@ import {
TrackedEvent
} from "../../events";
import { buildInstanceOutput, InstanceOutputInternal } from "../../output";
import { buildInstanceRandom, InstanceRandomInternal } from "../../random";
import {
buildInstanceRandom,
InstanceRandomInternal,
RandomRecord
} from "../../random";
import { ContextManager } from "../context-manager";
import { GameInstanceInternal } from "../game-instance-internal";

Expand Down Expand Up @@ -191,7 +195,7 @@ class GameInstanceImpl<StateType = any>
*/
private _buildRandomRevertCtor(revertTo: FK<EventRecord>) {
return (game: GameInstanceImpl) => {
let numGens: number = this.random.numGenerations;
let lastKey = this.random.lastKey;

const eventsWithRandoms = this.events.history.filter(
er => er.randoms !== undefined && er.randoms.length > 0
Expand All @@ -204,16 +208,21 @@ class GameInstanceImpl<StateType = any>

if (lastEvent === undefined) {
// All random values were generated after the target event
numGens =
lastKey =
eventsWithRandoms[eventsWithRandoms.length - 1]
.randoms[0].id;
} else {
// Otherwise, set num generations to its value AFTER (+1) the target event
// Otherwise, set lastKey to its value AFTER (+1) the target event
const lastRandoms = lastEvent.randoms;
numGens = lastRandoms[lastRandoms.length - 1].id + 1;
lastKey = lastRandoms[lastRandoms.length - 1].id.plus(1);
}
}
return buildInstanceRandom(game, numGens);
return buildInstanceRandom(
game,
// Subtract lastKey by one to fix an off-by-one error caused by
// using PK indices to skip the Prando generator
buildPKProvider<RandomRecord>().forkAfterKey(lastKey.minus(1))
);
};
}

Expand Down
6 changes: 6 additions & 0 deletions test/test-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { inspect } from "util";

export const log = (o: any, title?: string) =>
console.log(
`${title ? `${title}: ` : ""}${inspect(o, { depth: Infinity })}`
);
15 changes: 10 additions & 5 deletions test/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { inspect } from "util";
import { GameMetadata, GameOptions } from "../src/config";
import { version as regalVersion } from "../package.json";
import { expect } from "chai";
import { getGameInstancePK, Agent } from "../src/agents";
import { buildPKProvider, PK } from "../src/common";
import { OutputLine } from "../src/output";
import { getUntrackedEventPK } from "../src/events";
import { RandomRecord } from "../src/random";

export const log = (o: any, title?: string) =>
console.log(
`${title ? `${title}: ` : ""}${inspect(o, { depth: Infinity })}`
);
// log had to be moved to its own file to eliminate circular dependencies
// when used to debug src
export { log } from "./test-log";

export const getDemoMetadata = (): GameMetadata => ({
name: "Demo Game",
Expand Down Expand Up @@ -82,6 +81,8 @@ export const smartObjectEquals = (actual: object, expected: object) => {

export const getInitialOutputPK = () => buildPKProvider<OutputLine>().next();

export const getInitialRandomPK = () => buildPKProvider<RandomRecord>().next();

const pks = <T>(additional: number, initialPK: PK<T>) => {
const result = [initialPK];
for (let i = 0; i < additional; i++) {
Expand All @@ -103,3 +104,7 @@ export const ePKs = (additional: number) =>
pks(additional, getUntrackedEventPK());

export const ePKAtNum = (index: number) => getUntrackedEventPK().plus(index);

// RandomRecord PKs
export const rPKs = (additional: number) =>
pks(additional, getInitialRandomPK());
18 changes: 18 additions & 0 deletions test/unit/keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,22 @@ describe("Keys", function() {
prov.next();
expect(prov.countGenerated()).to.equal(baseCount + 1);
});

it("PKProvider.previous is equal to the previous key", function() {
const prov = buildPKProvider(RESERVED_KEYS);
const first = prov.next();
expect(prov.previous().equals(first)).to.be.true;
});

it("PKProvider.forkAfterKey creates a fork as if the new provider's last key was the given key", function() {
const prov = buildPKProvider(RESERVED_KEYS);
const provKey = prov.next();
const fork = prov.forkAfterKey(provKey.plus(100));
expect(
prov
.next()
.plus(100)
.equals(fork.next())
).to.be.true;
});
});
13 changes: 7 additions & 6 deletions test/unit/random.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from "../../src/random";
import { buildGameInstance } from "../../src/state";
import { RegalError } from "../../src/error";
import { log, getDemoMetadata, ePKs } from "../test-utils";
import { log, getDemoMetadata, ePKs, rPKs } from "../test-utils";
import { Game } from "../../src/api";
import { Agent, isAgent } from "../../src/agents";
import { on } from "../../src/events";
Expand Down Expand Up @@ -352,6 +352,7 @@ describe("Random", function() {
rand1.then(rand2)(myGame);

const [_epk0, epk1, epk2] = ePKs(2);
const [rpk0, rpk1, rpk2, rpk3, rpk4] = rPKs(4);

expect(myGame.state.randos).to.deep.equal([
false,
Expand All @@ -366,17 +367,17 @@ describe("Random", function() {
id: epk2,
name: "RAND2",
randoms: [
{ id: 2, value: 10 },
{ id: 3, value: "IcR*G" },
{ id: 4, value: 0 } // InstanceRandom.choice records the index of the selected element, not the element itself
{ id: rpk2, value: 10 },
{ id: rpk3, value: "IcR*G" },
{ id: rpk4, value: 0 } // InstanceRandom.choice records the index of the selected element, not the element itself
]
},
{
id: epk1,
name: "RAND1",
randoms: [
{ id: 0, value: false },
{ id: 1, value: 0.0217038414025921 }
{ id: rpk0, value: false },
{ id: rpk1, value: 0.0217038414025921 }
]
}
]);
Expand Down

0 comments on commit fa3f6e3

Please sign in to comment.