Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype Registry: Replace numeric RandomRecord.id with PK<RandomRecord> #111

Merged
merged 2 commits into from
Jul 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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