Browse Source

Dependencies update

master
Michaël Lemaire 2 years ago
parent
commit
b69db4e796
  1. 3
      .gitmodules
  2. 3
      TODO.md
  3. 8
      activate_node
  4. 3244
      package-lock.json
  5. 32
      package.json
  6. 48
      src/MainUI.ts
  7. 1
      src/common
  8. 232
      src/common/DiffLog.spec.ts
  9. 249
      src/common/DiffLog.ts
  10. 207
      src/common/Iterators.spec.ts
  11. 363
      src/common/Iterators.ts
  12. 3
      src/common/README.md
  13. 127
      src/common/RObject.spec.ts
  14. 100
      src/common/RObject.ts
  15. 92
      src/common/RandomGenerator.spec.ts
  16. 114
      src/common/RandomGenerator.ts
  17. 104
      src/common/Serializer.spec.ts
  18. 111
      src/common/Serializer.ts
  19. 330
      src/common/Testing.ts
  20. 117
      src/common/Timer.spec.ts
  21. 100
      src/common/Timer.ts
  22. 158
      src/common/Toggle.spec.ts
  23. 93
      src/common/Toggle.ts
  24. 517
      src/common/Tools.spec.ts
  25. 629
      src/common/Tools.ts
  26. 52583
      src/lib/phaser.d.ts
  27. 18
      src/ui/BaseView.ts
  28. 2
      src/ui/TestGame.ts
  29. 7
      src/ui/battle/Arena.ts
  30. 2
      src/ui/battle/LogProcessor.ts
  31. 3
      src/ui/character/CharacterSheet.spec.ts
  32. 7
      src/ui/common/InputManager.ts
  33. 18
      src/ui/common/UIBuilder.spec.ts

3
.gitmodules

@ -1,3 +0,0 @@
[submodule "src/common"]
path = src/common
url = https://code.thunderk.net/michael/tscommon.git

3
TODO.md

@ -4,7 +4,6 @@ To-Do-list
Phaser 3 migration
------------------
* Restore fullscreen mode (and add a fullscreen incentive before the menu)
* Fix valuebar requiring to be in root display list
* Restore unit tests about boundaries (in UITools)
@ -96,6 +95,7 @@ Artificial Intelligence
Common UI
---------
* Add a fullscreen incentive at game start
* Fix calling setHoverClick several times on the same button not working as expected
* Fix tooltip remaining when the hovered object is hidden by animations
* If ProgressiveMessage animation performance is bad, show the text directly
@ -108,6 +108,7 @@ Common UI
Technical
---------
* Use tk-serializer package (may need to switch to webpack)
* Fix tooltips and input events on mobile
* Pause timers when the game is paused (at least animation timers)
* Pack sounds

8
activate_node

@ -2,13 +2,15 @@
# Usage:
# source activate_node
vdir="./.venv"
expected="10.15.3"
if [ \! -f "./activate_node" ]
then
echo "Not in project directory"
exit 1
fi
vdir="./.venv"
test -x "${vdir}/bin/nodeenv" || ( virtualenv -p python3 "${vdir}" && "${vdir}/bin/pip" install --upgrade nodeenv )
test -e "${vdir}/node/bin/activate" || "${vdir}/bin/nodeenv" --node=10.3.0 --force "${vdir}/node"
test -x "${vdir}/bin/nodeenv" || ( python3 -m venv "${vdir}" && "${vdir}/bin/pip" install --upgrade nodeenv )
test "$(${vdir}/node/bin/nodejs --version)" = "v${expected}" || "${vdir}/bin/nodeenv" --node=${expected} --force "${vdir}/node"
source "${vdir}/node/bin/activate"

3244
package-lock.json

File diff suppressed because it is too large

32
package.json

@ -18,29 +18,29 @@
"author": "Michael Lemaire",
"license": "MIT",
"devDependencies": {
"@types/jasmine": "^2.8.8",
"@types/jasmine": "^3.3.12",
"babel-polyfill": "6.26.0",
"codecov": "^3.0.2",
"codecov": "^3.3.0",
"gamefroot-texture-packer": "github:Gamefroot/Gamefroot-Texture-Packer#f3687111afc94f80ea8f2877c188fb8e2004e8ff",
"glob": "^7.1.2",
"glob-watcher": "^5.0.1",
"jasmine": "^3.1.0",
"karma": "^2.0.2",
"glob": "^7.1.3",
"glob-watcher": "^5.0.3",
"jasmine": "^3.4.0",
"karma": "^4.1.0",
"karma-coverage": "^1.1.2",
"karma-jasmine": "^1.1.2",
"karma-jasmine": "^2.0.1",
"karma-phantomjs-launcher": "1.0.4",
"karma-spec-reporter": "^0.0.32",
"live-server": "1.2.0",
"remap-istanbul": "^0.11.1",
"runjs": "^4.3.2",
"shelljs": "^0.8.2",
"typescript": "^2.9.1",
"uglify-js": "^3.4.0"
"live-server": "1.2.1",
"remap-istanbul": "^0.13.0",
"runjs": "^4.4.2",
"shelljs": "^0.8.3",
"typescript": "^3.4.5",
"uglify-js": "^3.5.11"
},
"dependencies": {
"jasmine-core": "^3.1.0",
"parse": "^1.11.1",
"phaser": "^3.10.1",
"jasmine-core": "^3.4.0",
"parse": "^2.4.0",
"phaser": "^3.16.2",
"process-pool": "^0.3.5"
}
}

48
src/MainUI.ts

@ -41,15 +41,18 @@ module TK.SpaceTac {
// Current scaling
scaling = 1
constructor(headless: boolean = false) {
constructor(private testmode = false) {
super({
width: 1920,
height: 1080,
type: headless ? Phaser.HEADLESS : Phaser.AUTO,
type: Phaser.AUTO, // cannot really use HEADLESS because of bugs
backgroundColor: '#000000',
parent: '-space-tac',
disableContextMenu: true,
"render.autoResize": true,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
},
});
this.storage = localStorage;
@ -57,7 +60,7 @@ module TK.SpaceTac {
this.session = new GameSession();
this.session_token = null;
if (!headless) {
if (!testmode) {
this.events.on("blur", () => {
this.scene.scenes.forEach(scene => this.scene.pause(scene));
});
@ -74,34 +77,17 @@ module TK.SpaceTac {
this.scene.add('creation', UI.FleetCreationView);
this.scene.add('universe', UI.UniverseMapView);
this.resizeToFitWindow();
window.addEventListener("resize", () => this.resizeToFitWindow());
this.goToScene('boot');
}
}
resize(width: number, height: number, scaling?: number) {
super.resize(width, height);
this.scaling = scaling ? scaling : 1;
cfilter(this.scene.scenes, UI.BaseView).forEach(scene => scene.resize());
}
resizeToFitWindow() {
let width = window.innerWidth;
let height = window.innerHeight;
let scale = Math.min(width / 1920, height / 1080);
this.resize(1920 * scale, 1080 * scale, scale);
}
boot() {
super.boot();
this.options = new UI.GameOptions(this);
}
get headless(): boolean {
return this.config.renderType == Phaser.HEADLESS;
get isTesting(): boolean {
return this.testmode;
}
/**
@ -124,7 +110,7 @@ module TK.SpaceTac {
if (active) {
let scene = this.scene.getScene(active);
return (scene instanceof UI.BaseView) ? scene : null;
} else if (this.headless) {
} else if (this.isTesting) {
return this.scene.scenes[0];
} else {
return null;
@ -237,9 +223,7 @@ module TK.SpaceTac {
* Check if the game is currently fullscreen
*/
isFullscreen(): boolean {
// FIXME
return false;
//return this.scale.isFullScreen;
return this.scale.isFullscreen;
}
/**
@ -248,15 +232,13 @@ module TK.SpaceTac {
* Returns true if the result is fullscreen
*/
toggleFullscreen(active: boolean | null = null): boolean {
// FIXME
/*if (active === false || (active !== true && this.isFullscreen())) {
this.scale.stopFullScreen();
if (active === false || (active !== true && this.isFullscreen())) {
this.scale.stopFullscreen();
return false;
} else {
this.scale.startFullScreen(true);
this.scale.startFullscreen();
return true;
}*/
return false;
}
}
}
}

1
src/common

@ -1 +0,0 @@
Subproject commit 1425cb08935dd996a4c7a644ab793ff3b8355c9b

232
src/common/DiffLog.spec.ts

@ -0,0 +1,232 @@
/// <reference path="DiffLog.ts" />
module TK.Specs {
class TestState {
counter = 0
}
class TestDiff extends Diff<TestState> {
private value: number
constructor(value = 1) {
super();
this.value = value;
}
apply(state: TestState) {
state.counter += this.value;
}
getReverse() {
return new TestDiff(-this.value);
}
}
testing("DiffLog", test => {
test.case("stores sequential events", check => {
let log = new DiffLog<TestState>();
check.equals(log.count(), 0);
check.equals(log.get(0), null);
check.equals(log.get(1), null);
check.equals(log.get(2), null);
log.add(new TestDiff(2));
check.equals(log.count(), 1);
check.equals(log.get(0), new TestDiff(2));
check.equals(log.get(1), null);
check.equals(log.get(2), null);
log.add(new TestDiff(-4));
check.equals(log.count(), 2);
check.equals(log.get(0), new TestDiff(2));
check.equals(log.get(1), new TestDiff(-4));
check.equals(log.get(2), null);
log.clear(1);
check.equals(log.count(), 1);
check.equals(log.get(0), new TestDiff(2));
log.clear();
check.equals(log.count(), 0);
})
})
testing("DiffLogClient", test => {
test.case("adds diffs to the log", check => {
let log = new DiffLog<TestState>();
let state = new TestState();
let client = new DiffLogClient(state, log);
check.equals(client.atEnd(), true, "client is empty, should be at end");
check.equals(log.count(), 0, "log is empty initially");
check.equals(state.counter, 0, "initial state is 0");
client.add(new TestDiff(3));
check.equals(client.atEnd(), true, "client still at end");
check.equals(log.count(), 1, "diff added to log");
check.equals(state.counter, 3, "diff applied to state");
client.add(new TestDiff(2), false);
check.equals(client.atEnd(), false, "client lapsing behind");
check.equals(log.count(), 2, "diff added to log");
check.equals(state.counter, 3, "diff not applied to state");
})
test.case("initializes at current state (end of log)", check => {
let state = new TestState();
let log = new DiffLog<TestState>();
log.add(new TestDiff(7));
let client = new DiffLogClient(state, log);
check.equals(client.atStart(), false);
check.equals(client.atEnd(), true);
check.equals(state.counter, 0);
client.forward();
check.equals(state.counter, 0);
client.backward();
check.equals(state.counter, -7);
})
test.case("moves forward or backward in the log", check => {
let log = new DiffLog<TestState>();
let state = new TestState();
let client = new DiffLogClient(state, log);
log.add(new TestDiff(7));
log.add(new TestDiff(-2));
log.add(new TestDiff(4));
check.equals(state.counter, 0, "initial state is 0");
check.equals(client.atStart(), true, "client is at start");
check.equals(client.atEnd(), false, "client is not at end");
client.forward();
check.equals(state.counter, 7, "0+7 => 7");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), false, "client is not at end");
client.forward();
check.equals(state.counter, 5, "7-2 => 5");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), false, "client is not at end");
client.forward();
check.equals(state.counter, 9, "5+4 => 9");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), true, "client is at end");
client.forward();
check.equals(state.counter, 9, "at end, still 9");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), true, "client is at end");
client.backward();
check.equals(state.counter, 5, "9-4=>5");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), false, "client is not at end");
client.backward();
check.equals(state.counter, 7, "5+2=>7");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), false, "client is not at end");
client.backward();
check.equals(state.counter, 0, "7-7=>0");
check.equals(client.atStart(), true, "client is back at start");
check.equals(client.atEnd(), false, "client is not at end");
client.backward();
check.equals(state.counter, 0, "at start, still 0");
check.equals(client.atStart(), true, "client is at start");
check.equals(client.atEnd(), false, "client is not at end");
})
test.case("jumps to start or end of the log", check => {
let log = new DiffLog<TestState>();
let state = new TestState();
let client = new DiffLogClient(state, log);
client.add(new TestDiff(7));
log.add(new TestDiff(-2));
log.add(new TestDiff(4));
check.equals(state.counter, 7, "initial state is 7");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), false, "client is not at end");
client.jumpToEnd();
check.equals(state.counter, 9, "7-2+4=>9");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), true, "client at end");
client.jumpToEnd();
check.equals(state.counter, 9, "still 9");
check.equals(client.atStart(), false, "client is not at start");
check.equals(client.atEnd(), true, "client at end");
client.jumpToStart();
check.equals(state.counter, 0, "9-4+2-7=>0");
check.equals(client.atStart(), true, "client is at start");
check.equals(client.atEnd(), false, "client at not end");
client.jumpToStart();
check.equals(state.counter, 0, "still 0");
check.equals(client.atStart(), true, "client is at start");
check.equals(client.atEnd(), false, "client at not end");
})
test.case("truncate the log", check => {
let log = new DiffLog<TestState>();
let state = new TestState();
let client = new DiffLogClient(state, log);
client.add(new TestDiff(7));
client.add(new TestDiff(3));
client.add(new TestDiff(5));
check.in("initial state", check => {
check.equals(state.counter, 15, "state=15");
check.equals(log.count(), 3, "count=3");
});
client.backward();
check.in("after backward", check => {
check.equals(state.counter, 10, "state=10");
check.equals(log.count(), 3, "count=3");
});
client.truncate();
check.in("after truncate", check => {
check.equals(state.counter, 10, "state=10");
check.equals(log.count(), 2, "count=2");
});
client.truncate();
check.in("after another truncate", check => {
check.equals(state.counter, 10, "state=10");
check.equals(log.count(), 2, "count=2");
});
})
test.acase("plays the log continuously", async check => {
let log = new DiffLog<TestState>();
let state = new TestState();
let client = new DiffLogClient(state, log);
let inter: number[] = [];
let promise = client.play(diff => {
inter.push((<any>diff).value);
return Promise.resolve();
});
log.add(new TestDiff(5));
log.add(new TestDiff(-1));
log.add(new TestDiff(2));
client.stop(false);
await promise;
check.equals(state.counter, 6);
check.equals(inter, [5, -1, 2]);
})
})
}

249
src/common/DiffLog.ts

@ -0,0 +1,249 @@
/**
* Framework to maintain a state from a log of changes
*
* This allows for repeatable, serializable and revertable state modifications.
*/
module TK {
/**
* Base class for a single diff.
*
* This represents an atomic change of the state, that can be applied, or reverted.
*/
export class Diff<T> {
/**
* Apply the diff on a given state
*
* By default it does nothing
*/
apply(state: T): void {
}
/**
* Reverts the diff from a given state
*
* By default it applies the reverse event
*/
revert(state: T): void {
this.getReverse().apply(state);
}
/**
* Get the reverse event
*
* By default it returns a stub event that does nothing
*/
protected getReverse(): Diff<T> {
return new Diff<T>();
}
}
/**
* Collection of sequential diffs
*/
export class DiffLog<T> {
private diffs: Diff<T>[] = []
/**
* Add a single diff at the end of the log
*/
add(diff: Diff<T>): void {
this.diffs.push(diff);
}
/**
* Get the diff at a specific index
*/
get(idx: number): Diff<T> | null {
return this.diffs[idx] || null;
}
/**
* Return the total count of diffs
*/
count(): number {
return this.diffs.length;
}
/**
* Clean all stored diffs, starting at a given index
*
* The caller should be sure that no log client is beyond the cut index.
*/
clear(start = 0): void {
this.diffs = this.diffs.slice(0, start);
}
}
/**
* Client for a DiffLog, able to go forward or backward in the log, applying diffs as needed
*/
export class DiffLogClient<T> {
private state: T
private log: DiffLog<T>
private cursor = -1
private playing = false
private stopping = false
private paused = false
private timer = Timer.global
constructor(state: T, log: DiffLog<T>) {
this.state = state;
this.log = log;
this.cursor = log.count() - 1;
}
/**
* Returns true if the log is currently playing
*/
isPlaying(): boolean {
return this.playing && !this.paused && !this.stopping;
}
/**
* Get the current diff pointed at
*/
getCurrent(): Diff<T> | null {
return this.log.get(this.cursor);
}
/**
* Push a diff to the underlying log, applying it immediately if required
*/
add(diff: Diff<T>, apply = true): void {
this.log.add(diff);
if (apply) {
this.jumpToEnd();
}
}
/**
* Apply the underlying log continuously, until *stop* is called
*
* If *after_apply* is provided, it will be called after each diff is applied, and waited upon before resuming
*/
async play(after_apply?: (diff: Diff<T>) => Promise<void>): Promise<void> {
if (this.playing) {
console.error("DiffLogClient already playing", this);
return;
}
this.playing = true;
this.stopping = false;
while (this.playing) {
if (!this.paused) {
let diff = this.forward();
if (diff && after_apply) {
await after_apply(diff);
}
}
if (this.atEnd()) {
if (this.stopping) {
break;
} else {
await this.timer.sleep(50);
}
}
}
}
/**
* Stop the previous *play*
*/
stop(immediate = true): void {
if (!this.playing) {
console.error("DiffLogClient not playing", this);
return;
}
if (immediate) {
this.playing = false;
}
this.stopping = true;
}
/**
* Make a step backward in time (revert one diff)
*/
backward(): Diff<T> | null {
if (!this.atStart()) {
this.cursor -= 1;
this.paused = true;
let diff = this.log.get(this.cursor + 1);
if (diff) {
diff.revert(this.state);
}
return diff;
} else {
return null;
}
}
/**
* Make a step forward in time (apply one diff)
*/
forward(): Diff<T> | null {
if (!this.atEnd()) {
this.cursor += 1;
if (this.atEnd()) {
this.paused = false;
}
let diff = this.log.get(this.cursor);
if (diff) {
diff.apply(this.state);
}
return diff;
} else {
return null;
}
}
/**
* Jump to the start of the log
*
* This will rewind all applied event
*/
jumpToStart() {
while (!this.atStart()) {
this.backward();
}
}
/**
* Jump to the end of the log
*
* This will apply all remaining event
*/
jumpToEnd() {
while (!this.atEnd()) {
this.forward();
}
}
/**
* Check if we are currently at the start of the log
*/
atStart(): boolean {
return this.cursor < 0;
}
/**
* Check if we are currently at the end of the log
*/
atEnd(): boolean {
return this.cursor >= this.log.count() - 1;
}
/**
* Truncate all diffs after the current position
*
* This is useful when using the log to "undo" something
*/
truncate(): void {
this.log.clear(this.cursor + 1);
}
}
}

207
src/common/Iterators.spec.ts

@ -0,0 +1,207 @@
module TK {
testing("Iterators", test => {
function checkit<T>(base_iterator: Iterator<T>, ...values: (T | null)[]) {
let iterator = base_iterator;
values.forEach(value => {
let [head, tail] = iterator();
test.check.equals(head, value);
iterator = tail;
});
// second iteration to check for repeatability
iterator = base_iterator;
values.forEach(value => {
let [head, tail] = iterator();
test.check.equals(head, value);
iterator = tail;
});
}
function checkarray<T>(iterator: Iterator<T>, values: T[]) {
test.check.equals(imaterialize(iterator), values);
// second iteration to check for repeatability
test.check.equals(imaterialize(iterator), values);
}
test.case("calls a function for each yielded value", check => {
let iterator = iarray([1, 2, 3]);
let result: number[] = [];
iforeach(iterator, bound(result, "push"));
check.equals(result, [1, 2, 3]);
result = [];
iforeach(iterator, i => {
result.push(i);
if (i == 2) {
return null;
} else {
return undefined;
}
});
check.equals(result, [1, 2]);
result = [];
iforeach(iterator, i => {
result.push(i);
return i;
}, 2);
check.equals(result, [1, 2]);
});
test.case("creates an iterator from an array", check => {
checkit(iarray([]), null, null, null);
checkit(iarray([1, 2, 3]), 1, 2, 3, null, null, null);
});
test.case("creates an iterator from a single value", check => {
checkarray(isingle(1), [1]);
checkarray(isingle("a"), ["a"]);
});
test.case("finds the first item passing a predicate", check => {
check.equals(ifirst(iarray(<number[]>[]), i => i % 2 == 0), null);
check.equals(ifirst(iarray([1, 2, 3]), i => i % 2 == 0), 2);
check.equals(ifirst(iarray([1, 3, 5]), i => i % 2 == 0), null);
});
test.case("finds the first item mapping to a value", check => {
let predicate = (i: number) => i % 2 == 0 ? (i * 4).toString() : null
check.equals(ifirstmap(iarray([]), predicate), null);
check.equals(ifirstmap(iarray([1, 2, 3]), predicate), "8");
check.equals(ifirstmap(iarray([1, 3, 5]), predicate), null);
});
test.case("materializes an array from an iterator", check => {
check.equals(imaterialize(iarray([1, 2, 3])), [1, 2, 3]);
check.throw(() => imaterialize(iarray([1, 2, 3, 4, 5]), 2), "Length limit on iterator materialize");
});
test.case("creates an iterator in a range of integers", check => {
checkarray(irange(4), [0, 1, 2, 3]);
checkarray(irange(4, 1), [1, 2, 3, 4]);
checkarray(irange(5, 3, 2), [3, 5, 7, 9, 11]);
checkit(irange(), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
});
test.case("uses a step iterator to scan numbers", check => {
checkit(istep(), 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
checkit(istep(3), 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);
checkarray(istep(3, irepeat(1, 4)), [3, 4, 5, 6, 7]);
checkarray(istep(8, IEMPTY), [8]);
checkit(istep(1, irange()), 1, 1, 2, 4, 7, 11, 16);
});
test.case("skips a number of values", check => {
checkarray(iskip(irange(7), 3), [3, 4, 5, 6]);
checkarray(iskip(irange(7), 12), []);
checkarray(iskip(IEMPTY, 3), []);
});
test.case("gets a value at an iterator position", check => {
check.equals(iat(irange(), -1), null);
check.equals(iat(irange(), 0), 0);
check.equals(iat(irange(), 8), 8);
check.equals(iat(irange(5), 8), null);
check.equals(iat(IEMPTY, 0), null);
});
test.case("chains iterators", check => {
checkarray(ichain(), []);
checkarray(ichain(irange(3)), [0, 1, 2]);
checkarray(ichain(iarray([1, 2]), iarray([]), iarray([3, 4, 5])), [1, 2, 3, 4, 5]);
});
test.case("chains iterator of iterators", check => {
checkarray(ichainit(IEMPTY), []);
checkarray(ichainit(iarray([iarray([1, 2, 3]), iarray([]), iarray([4, 5])])), [1, 2, 3, 4, 5]);
});
test.case("repeats a value", check => {
checkit(irepeat("a"), "a", "a", "a", "a");
checkarray(irepeat("a", 3), ["a", "a", "a"]);
});
test.case("loops an iterator", check => {
checkarray(iloop(irange(3), 2), [0, 1, 2, 0, 1, 2]);
checkit(iloop(irange(1)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
let onloop = check.mockfunc("onloop");
let iterator = iloop(irange(2), 3, onloop.func);
function next() {
let value;
[value, iterator] = iterator();
return value;
}
check.equals(next(), 0);
check.called(onloop, 0);
check.equals(next(), 1);
check.called(onloop, 0);
check.equals(next(), 0);
check.called(onloop, 1);
check.equals(next(), 1);
check.called(onloop, 0);
check.equals(next(), 0);
check.called(onloop, 1);
check.equals(next(), 1);
check.called(onloop, 0);
check.equals(next(), null);
check.called(onloop, 0);
});
test.case("maps an iterator", check => {
checkarray(imap(IEMPTY, i => i * 2), []);
checkarray(imap(irange(3), i => i * 2), [0, 2, 4]);
});
test.case("filters an iterator", check => {
checkarray(imap(IEMPTY, i => i % 3 == 0), []);
checkarray(ifilter(irange(12), i => i % 3 == 0), [0, 3, 6, 9]);
});
test.case("combines iterators", check => {
let iterator = icombine(iarray([1, 2, 3]), iarray(["a", "b"]));
checkarray(iterator, [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]]);
});
test.case("zips iterators", check => {
checkarray(izip(IEMPTY, IEMPTY), []);
checkarray(izip(iarray([1, 2, 3]), iarray(["a", "b"])), [[1, "a"], [2, "b"]]);
checkarray(izipg(IEMPTY, IEMPTY), []);
checkarray(izipg(iarray([1, 2, 3]), iarray(["a", "b"])), <[number | null, string | null][]>[[1, "a"], [2, "b"], [3, null]]);
});
test.case("partitions iterators", check => {
let [it1, it2] = ipartition(IEMPTY, () => true);
checkarray(it1, []);
checkarray(it2, []);
[it1, it2] = ipartition(irange(5), i => i % 2 == 0);
checkarray(it1, [0, 2, 4]);
checkarray(it2, [1, 3]);
});
test.case("returns unique items", check => {
checkarray(iunique(IEMPTY), []);
checkarray(iunique(iarray([5, 3, 2, 3, 4, 5])), [5, 3, 2, 4]);
checkarray(iunique(iarray([5, 3, 2, 3, 4, 5]), 4), [5, 3, 2, 4]);
check.throw(() => imaterialize(iunique(iarray([5, 3, 2, 3, 4, 5]), 3)), "Unique count limit on iterator");
});
test.case("uses ireduce for some common functions", check => {
check.equals(isum(IEMPTY), 0);
check.equals(isum(irange(4)), 6);
check.equals(icat(IEMPTY), "");
check.equals(icat(iarray(["a", "bc", "d"])), "abcd");
check.equals(imin(IEMPTY), Infinity);
check.equals(imin(iarray([3, 8, 2, 4])), 2);
check.equals(imax(IEMPTY), -Infinity);
check.equals(imax(iarray([3, 8, 2, 4])), 8);
});
});
}

363
src/common/Iterators.ts

@ -0,0 +1,363 @@
/**
* Lazy iterators to work on dynamic data sets without materializing them.
*
* They allow to work on infinite streams of values, with limited memory consumption.
*
* Functions in this file that do not return an Iterator are "materializing", meaning that they
* may consume iterators up to the end, and will not work well on infinite iterators.
*/
module TK {
/**
* An iterator is a function without side effect, that returns the current value
* and an iterator over the next values.
*/
export type Iterator<T> = () => [T | null, Iterator<T>];
function _getIEND(): [null, Iterator<any>] {
return [null, _getIEND];
}
/**
* IEND is a return value for iterators, indicating end of iteration.
*/
export const IEND: [null, Iterator<any>] = [null, _getIEND];
/**
* Empty iterator, returning IEND
*/
export const IEMPTY = () => IEND;
/**
* Equivalent of Array.forEach for lazy iterators.
*
* If the callback returns *stopper*, the iteration is stopped.
*/
export function iforeach<T>(iterator: Iterator<T>, callback: (_: T) => any, stopper: any = null) {
let value: T | null;
[value, iterator] = iterator();
while (value !== null) {
let returned = callback(value);
if (returned === stopper) {
return;
}
[value, iterator] = iterator();
}
}
/**
* Get an iterator on an array
*
* The iterator will yield the next value each time it is called, then null when the array's end is reached.
*/
export function iarray<T>(array: T[], offset = 0): Iterator<T> {
return () => {
if (offset < array.length) {
return [array[offset], iarray(array, offset + 1)];
} else {
return IEND;
}
}
}
/**
* Get an iterator yielding a single value
*/
export function isingle<T>(value: T): Iterator<T> {
return iarray([value]);
}
/**
* Returns the first item passing a predicate
*/
export function ifirst<T>(iterator: Iterator<T>, predicate: (item: T) => boolean): T | null {
let result: T | null = null;
iforeach(iterator, item => {
if (predicate(item)) {
result = item;
return null;
} else {
return undefined;
}
});
return result;
}
/**
* Returns the first non-null result of a value-yielding predicate, applied to each iterator element
*/
export function ifirstmap<T1, T2>(iterator: Iterator<T1>, predicate: (item: T1) => T2 | null): T2 | null {
let result: T2 | null = null;
iforeach(iterator, item => {
let mapped = predicate(item);
if (mapped) {
result = mapped;
return null;
} else {
return undefined;
}
});
return result;
}
/**
* Materialize an array from consuming an iterator
*
* To avoid materializing infinite iterators (and bursting memory), the item count is limited to 1 million, and an
* exception is thrown when this limit is reached.
*/
export function imaterialize<T>(iterator: Iterator<T>, limit = 1000000): T[] {
let result: T[] = [];
iforeach(iterator, value => {
result.push(value);
if (result.length >= limit) {
throw new Error("Length limit on iterator materialize");
}
});
return result;
}
/**
* Iterate over natural integers
*
* If *count* is not specified, the iterator is infinite
*/
export function irange(count: number = -1, start = 0, step = 1): Iterator<number> {
return () => (count != 0) ? [start, irange(count - 1, start + step, step)] : IEND;
}
/**
* Iterate over numbers, by applying a step taken from an other iterator
*
* This iterator stops when the "step iterator" stops
*
* With no argument, istep() == irange()
*/
export function istep(start = 0, step = irepeat(1)): Iterator<number> {
return () => {
let [value, iterator] = step();
return [start, value === null ? IEMPTY : istep(start + value, iterator)];
}
}
/**
* Skip a given number of values from an iterator, discarding them.
*/
export function iskip<T>(iterator: Iterator<T>, count = 1): Iterator<T> {
let value: T | null;
while (count--) {
[value, iterator] = iterator();
}
return iterator;
}
/**
* Return the value at a given position in the iterator
*/
export function iat<T>(iterator: Iterator<T>, position: number): T | null {
if (position < 0) {
return null;
} else {
if (position > 0) {
iterator = iskip(iterator, position);
}
return iterator()[0];
}
}
/**
* Chain an iterator of iterators.
*
* This will yield values from the first yielded iterator, then the second one, and so on...
*/
export function ichainit<T>(iterators: Iterator<Iterator<T>>): Iterator<T> {
return function () {
let [iterators_head, iterators_tail] = iterators();
if (iterators_head == null) {
return IEND;
} else {
let [head, tail] = iterators_head();
while (head == null) {
[iterators_head, iterators_tail] = iterators_tail();
if (iterators_head == null) {
break;
}
[head, tail] = iterators_head();
}
return [head, ichain(tail, ichainit(iterators_tail))];
}
}
}
/**
* Chain iterators.
*
* This will yield values from the first iterator, then the second one, and so on...
*/
export function ichain<T>(...iterators: Iterator<T>[]): Iterator<T> {
if (iterators.length == 0) {
return IEMPTY;
} else {
return ichainit(iarray(iterators));
}
}
/**
* Wrap an iterator, calling *onstart* when the first value of the wrapped iterator is yielded.
*/
function ionstart<T>(iterator: Iterator<T>, onstart: Function): Iterator<T> {
return () => {
let [head, tail] = iterator();
if (head !== null) {
onstart();
}
return [head, tail];
}
}
/**
* Iterator that repeats the same value.
*/
export function irepeat<T>(value: T, count = -1): Iterator<T> {
return iloop(iarray([value]), count);
}
/**
* Loop an iterator for a number of times.
*
* If count is negative, if will loop forever (infinite iterator).
*
* onloop may be used to know when the iterator resets.
*/
export function iloop<T>(base: Iterator<T>, count = -1, onloop?: Function): Iterator<T> {
if (count == 0) {
return IEMPTY;
} else {
let next = onloop ? ionstart(base, onloop) : base;
return ichainit(() => [base, iarray([iloop(next, count - 1)])]);
}
}
/**
* Iterator version of "map".
*/
export function imap<T1, T2>(iterator: Iterator<T1>, mapfunc: (_: T1) => T2): Iterator<T2> {
return () => {
let [head, tail] = iterator();
if (head === null) {
return IEND;
} else {
return [mapfunc(head), imap(tail, mapfunc)];
}
}
}
/**
* Iterator version of "reduce".
*/
export function ireduce<T>(iterator: Iterator<T>, reduce: (item1: T, item2: T) => T, init: T): T {
let result = init;
iforeach(iterator, item => {
result = reduce(result, item);
});
return result;
}
/**
* Iterator version of "filter".
*/
export function ifilter<T>(iterator: Iterator<T>, filterfunc: (_: T) => boolean): Iterator<T> {
return () => {
let [value, iter] = iterator();
while (value !== null && !filterfunc(value)) {
[value, iter] = iter();
}
return [value, ifilter(iter, filterfunc)];
}
}
/**
* Combine two iterators.
*
* This iterates through the second one several times, so if one iterator may be infinite,
* it should be the first one.
*/
export function icombine<T1, T2>(it1: Iterator<T1>, it2: Iterator<T2>): Iterator<[T1, T2]> {
return ichainit(imap(it1, v1 => imap(it2, (v2): [T1, T2] => [v1, v2])));
}
/**
* Advance two iterators at the same time, yielding item pairs
*
* Iteration will stop at the first of the two iterators that stops.
*/
export function izip<T1, T2>(it1: Iterator<T1>, it2: Iterator<T2>): Iterator<[T1, T2]> {
return () => {
let [val1, nit1] = it1();
let [val2, nit2] = it2();
if (val1 !== null && val2 !== null) {
return [[val1, val2], izip(nit1, nit2)];
} else {
return IEND;
}
}
}
/**
* Advance two iterators at the same time, yielding item pairs (greedy version)
*
* Iteration will stop when both iterators are consumed, returning partial couples (null in the peer) if needed.
*/
export function izipg<T1, T2>(it1: Iterator<T1>, it2: Iterator<T2>): Iterator<[T1 | null, T2 | null]> {
return () => {
let [val1, nit1] = it1();
let [val2, nit2] = it2();
if (val1 === null && val2 === null) {
return IEND;
} else {
return [[val1, val2], izipg(nit1, nit2)];
}
}
}
/**
* Partition in two iterators, one with values that pass the predicate, the other with values that don't
*/
export function ipartition<T>(it: Iterator<T>, predicate: (item: T) => boolean): [Iterator<T>, Iterator<T>] {
return [ifilter(it, predicate), ifilter(it, x => !predicate(x))];
}
/**
* Yield items from an iterator only once.
*
* Beware that even if this function is not materializing, it keeps track of yielded item, and may choke on
* infinite or very long streams. Thus, no more than *limit* items will be yielded (an error is thrown
* when this limit is reached).
*
* This function is O(n²)
*/
export function iunique<T>(it: Iterator<T>, limit = 1000000): Iterator<T> {
function internal(it: Iterator<T>, limit: number, done: T[]): Iterator<T> {
let [value, iterator] = it();
while (value !== null && contains(done, value)) {
[value, iterator] = iterator();
}
if (value === null) {
return IEMPTY;
} else if (limit <= 0) {
throw new Error("Unique count limit on iterator");
} else {
let head = value;
return () => [head, internal(it, limit - 1, done.concat([head]))];
}
}
return internal(it, limit, []);
}
/**
* Common reduce shortcuts
*/
export const isum = (iterator: Iterator<number>) => ireduce(iterator, (a, b) => a + b, 0);
export const icat = (iterator: Iterator<string>) => ireduce(iterator, (a, b) => a + b, "");
export const imin = (iterator: Iterator<number>) => ireduce(iterator, Math.min, Infinity);
export const imax = (iterator: Iterator<number>) => ireduce(iterator, Math.max, -Infinity);
}

3
src/common/README.md

@ -0,0 +1,3 @@
# tscommon
Typescript tools shared between projects

127
src/common/RObject.spec.ts

@ -0,0 +1,127 @@
/// <reference path="RObject.ts" />
module TK.Specs {
export class TestRObject extends RObject {
x: number
constructor(x = RandomGenerator.global.random()) {
super();
this.x = x;
}
};
testing("RObject", test => {
test.setup(function () {
(<any>RObject)._next_id = 0;
})
test.case("gets a sequential id", check => {
let o1 = new TestRObject();
check.equals(o1.id, 0);
let o2 = new TestRObject();
let o3 = new TestRObject();
check.equals(o2.id, 1);
check.equals(o3.id, 2);
check.equals(rid(o3), 2);
check.equals(rid(o3.id), 2);
})
test.case("checks object identity", check => {
let o1 = new TestRObject(1);
let o2 = new TestRObject(1);
let o3 = duplicate(o1, TK.Specs);
check.equals(o1.is(o1), true, "o1 is o1");
check.equals(o1.is(o2), false, "o1 is not o2");
check.equals(o1.is(o3), true, "o1 is o3");
check.equals(o1.is(null), false, "o1 is not null");
check.equals(o2.is(o1), false, "o2 is not o1");
check.equals(o2.is(o2), true, "o2 is o2");
check.equals(o2.is(o3), false, "o2 is not o3");
check.equals(o2.is(null), false, "o2 is not null");
check.equals(o3.is(o1), true, "o3 is o1");
check.equals(o3.is(o2), false, "o3 is not o2");
check.equals(o3.is(o3), true, "o3 is o3");
check.equals(o3.is(null), false, "o3 is not null");
})
test.case("resets global id on unserialize", check => {
let o1 = new TestRObject();
check.equals(o1.id, 0);
let o2 = new TestRObject();
check.equals(o2.id, 1);
let serializer = new Serializer(TK.Specs);
let packed = serializer.serialize({ objs: [o1, o2] });
(<any>RObject)._next_id = 0;
check.equals(new TestRObject().id, 0);
let unpacked = serializer.unserialize(packed);
check.equals(unpacked, { objs: [o1, o2] });
check.equals(new TestRObject().id, 2);
serializer.unserialize(packed);
check.equals(new TestRObject().id, 3);
})
})
testing("RObjectContainer", test => {
test.case("stored objects and get them by their id", check => {
let o1 = new TestRObject();
let store = new RObjectContainer([o1]);
let o2 = new TestRObject();
check.same(store.get(o1.id), o1);
check.equals(store.get(o2.id), null);
store.add(o2);
check.same(store.get(o1.id), o1);
check.same(store.get(o2.id), o2);
})
test.case("lists contained objects", check => {
let store = new RObjectContainer<TestRObject>();
let o1 = store.add(new TestRObject());
let o2 = store.add(new TestRObject());
check.equals(store.count(), 2, "count=2");
let objects = store.list();
check.equals(objects.length, 2, "list length=2");
check.contains(objects, o1, "list contains o1");
check.contains(objects, o2, "list contains o2");
objects = imaterialize(store.iterator());
check.equals(objects.length, 2, "list length=2");
check.contains(objects, o1, "list contains o1");
check.contains(objects, o2, "list contains o2");
let ids = store.ids();
check.equals(ids.length, 2, "ids length=2");
check.contains(ids, o1.id, "list contains o1.id");
check.contains(ids, o2.id, "list contains o2.id");
})
test.case("removes objects", check => {
let store = new RObjectContainer<TestRObject>();
let o1 = store.add(new TestRObject());
let o2 = store.add(new TestRObject());
check.in("initial", check => {
check.equals(store.count(), 2, "count=2");
check.same(store.get(o1.id), o1, "o1 present");
check.same(store.get(o2.id), o2, "o2 present");
});
store.remove(o1);
check.in("removed o1", check => {
check.equals(store.count(), 1, "count=1");
check.same(store.get(o1.id), null, "o1 missing");
check.same(store.get(o2.id), o2, "o2 present");
});
})
})
}

100
src/common/RObject.ts

@ -0,0 +1,100 @@
module TK {
export type RObjectId = number
/**
* Returns the id of an object
*/
export function rid(obj: RObject | RObjectId): number {
return (obj instanceof RObject) ? obj.id : obj;
}
/**
* Referenced objects, with unique ID.
*
* Objects extending this class will have an automatic unique ID, and may be tracked from an RObjectContainer.
*/
export class RObject {
readonly id: RObjectId = RObject._next_id++
private static _next_id = 0
postUnserialize() {
if (this.id >= RObject._next_id) {
RObject._next_id = this.id + 1;
}
}
/**
* Check that two objects are the same (only by comparing their ID)
*/
is(other: RObject | RObjectId | null): boolean {
if (other === null) {
return false;
} else if (other instanceof RObject) {
return this.id === other.id;