1
0
Fork 0
This commit is contained in:
Michaël Lemaire 2019-11-21 23:14:27 +01:00
parent ae8ad13a84
commit 6890118b83
317 changed files with 35293 additions and 29349 deletions

10
.editorconfig Normal file
View file

@ -0,0 +1,10 @@
root = true
[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
max_line_length = off

15
.gitignore vendored
View file

@ -1,12 +1,5 @@
.venv .venv
coverage node_modules
/node_modules .rts2_cache_*
/out/assets .coverage
/out/app.* /dist/
/out/tests.*
/out/dependencies.js
/graphics/**/*.blend?*
/graphics/**/output.png
/typings/
*.log
*.tsbuildinfo

11
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,11 @@
image: node:latest
cache:
paths:
- node_modules/
test:
before_script:
- npm install
script:
- npm test

View file

@ -3,7 +3,7 @@
# source activate_node # source activate_node
vdir="./.venv" vdir="./.venv"
expected="10.15.3" expected="12.13.0"
if [ \! -f "./activate_node" ] if [ \! -f "./activate_node" ]
then then

BIN
graphics/ships/_base.blend1 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
graphics/title.blend1 Normal file

Binary file not shown.

27
jest.config.js Normal file
View file

@ -0,0 +1,27 @@
module.exports = {
transform: {
"^.+\\.ts$": "ts-jest"
},
moduleFileExtensions: [
"ts",
"js",
"json",
"node"
],
watchPathIgnorePatterns: [
"<rootDir>/dist/",
"<rootDir>/node_modules/",
],
restoreMocks: true,
collectCoverage: true,
collectCoverageFrom: [
"src/**/*.ts",
"!src/**/*.test.ts",
],
coverageDirectory: ".coverage",
coverageReporters: [
"lcovonly",
"html",
"text-summary"
]
}

View file

@ -1,23 +0,0 @@
var handler = {
get(target, name) {
return new Proxy(function () { }, handler);
}
}
var Phaser = new Proxy(function () { }, handler);
//var debug = console.log;
var debug = function () { };
importScripts("app.js");
onmessage = function (e) {
debug("[AI Worker] Received", e.data.length);
var serializer = new TK.Serializer(TK.SpaceTac);
var battle = serializer.unserialize(e.data);
var processing = new TK.SpaceTac.AIWorker(battle);
processing.processHere(function (maneuver) {
debug("[AI Worker] Send", maneuver);
postMessage(serializer.serialize(maneuver));
return maneuver.apply(battle);
}).catch(postMessage);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View file

@ -1,54 +0,0 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SpaceTac</title>
<style>
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
background: #000000;
padding: 0;
margin: 0;
}
.game {
width: 100%;
height: 100vh;
}
@font-face {
font-family: 'SpaceTac';
src: url('fonts/daggersquare.regular.otf');
}
body {
font-family: 'SpaceTac';
}
.fontLoader {
position: absolute;
left: -1000px;
}
</style>
</head>
<body>
<div id="-space-tac" class="game"></div>
<div class=".fontLoader">.</div>
<script src="dependencies.js"></script>
<script src="app.js"></script>
<script>
window.onload = function () {
window.game = new TK.SpaceTac.MainUI();
};
</script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -1,68 +0,0 @@
const Pool = require('process-pool').default;
const pool = new Pool({ processLimit: 8 });
const work = pool.prepare(function () {
const App = require("./app").TK.SpaceTac;
async function doOneBattle(i) {
let ai1 = new App.TacticalAI();
let ai2 = new App.TacticalAI();
// Prepare battle
let battle = App.Battle.newQuickRandom(true, 1, 2 + i % 4);
battle.fleets.forEach((fleet, findex) => {
fleet.ships.forEach((ship, sindex) => {
ship.name = `F${findex + 1}S${sindex + 1} (${ship.model.name})`;
});
});
// Run battle
while (!battle.ended && battle.cycle < 100) {
let playing = battle.playing_ship;
if (playing) {
let ai = (playing.fleet == battle.fleets[0]) ? ai1 : ai2;
ai.ship = playing;
await ai.play();
}
}
// Collect results
if (battle.outcome && battle.outcome.winner) {
let results = {};
battle.fleets.forEach(fleet => {
fleet.ships.forEach(ship => {
let name = `Level ${ship.level.get()} ${ship.model.name}`;
results[name] = (results[name] || 0) + (ship.fleet === battle.outcome.winner ? 1 : 0);
});
});
return results;
} else {
return {};
}
}
return (i) => doOneBattle(i);
});
let played = {};
let winned = {};
let works = Array.from({ length: 1000 }, (v, i) => i).map(i => {
return work(i).then(result => {
Object.keys(result).forEach(model => {
if (result[model]) {
winned[model] = (winned[model] || 0) + 1;
}
played[model] = (played[model] || 0) + 1;
});
console.warn("------------------------------------------------");
console.warn(`--- Results after battle ${i}`);
Object.keys(played).sort().forEach(model => {
let factor = (winned[model] || 0) / played[model];
console.warn(`${model} ${Math.round(factor * 100)}%`);
});
console.warn("------------------------------------------------");
});
});
Promise.all(works).then(() => process.exit(0));

View file

@ -1,34 +0,0 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SpaceTac - Unit tests</title>
<link rel="stylesheet" href="/jasmine/jasmine.css">
<style>
canvas {
display: none;
}
.jasmine-result-message {
white-space: pre-wrap !important;
}
</style>
</head>
<body>
<div style="display: none; visibility: hidden; height: 0; overflow: hidden;">
<div id="-space-tac" class="game"></div>
</div>
<script src="/jasmine/jasmine.js"></script>
<script src="/jasmine/jasmine-html.js"></script>
<script src="/jasmine/boot.js"></script>
<script src="dependencies.js"></script>
<script src="app.js"></script>
<script src="tests.js"></script>
</body>
</html>

7649
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,21 +2,31 @@
"name": "spacetac", "name": "spacetac",
"version": "0.1.0", "version": "0.1.0",
"description": "A tactical RPG set in space", "description": "A tactical RPG set in space",
"main": "out/build.js", "main": "dist/spacetac.umd.js",
"scripts": { "scripts": {
"build": "run build", "build": "microbundle build -f modern,umd",
"test": "run ci", "test": "jest",
"start": "run continuous" "start": "run continuous",
"normalize": "tk-base",
"dev": "run-p dev:*",
"dev:test": "jest --watchAll",
"dev:build": "microbundle watch -f modern,umd",
"dev:serve": "live-server --host=localhost --port=5000 --no-browser --ignorePattern='.*\\.d\\.ts' dist",
"prepare": "npm run build",
"prepublishOnly": "npm test"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://code.thunderk.net/michael/spacetac.git" "url": "https://code.thunderk.net/games/spacetac.git"
}, },
"author": "Michael Lemaire", "author": {
"license": "MIT", "name": "Michaël Lemaire",
"url": "https://thunderk.net"
},
"license": "ISC",
"devDependencies": { "devDependencies": {
"@types/jasmine": "^3.3.12", "@types/jasmine": "^3.3.12",
"codecov": "^3.4.0", "@types/parse": "^2.9.0",
"gamefroot-texture-packer": "github:Gamefroot/Gamefroot-Texture-Packer#f3687111afc94f80ea8f2877c188fb8e2004e8ff", "gamefroot-texture-packer": "github:Gamefroot/Gamefroot-Texture-Packer#f3687111afc94f80ea8f2877c188fb8e2004e8ff",
"glob": "^7.1.4", "glob": "^7.1.4",
"glob-watcher": "^5.0.3", "glob-watcher": "^5.0.3",
@ -27,20 +37,31 @@
"karma-coverage": "^1.1.2", "karma-coverage": "^1.1.2",
"karma-jasmine": "^2.0.1", "karma-jasmine": "^2.0.1",
"karma-spec-reporter": "^0.0.32", "karma-spec-reporter": "^0.0.32",
"live-server": "1.2.1",
"process-pool": "^0.3.5", "process-pool": "^0.3.5",
"remap-istanbul": "^0.13.0",
"runjs": "^4.4.2", "runjs": "^4.4.2",
"shelljs": "^0.8.3", "shelljs": "^0.8.3",
"terser": "^3.17.0", "tk-base": "^0.2.5"
"typescript": "^3.5.0-rc"
}, },
"dependencies": { "dependencies": {
"parse": "^2.4.0", "parse": "^2.4.0",
"phaser": "^3.17.0" "phaser": "^3.20.1"
}, },
"dependenciesMap": { "dependenciesMap": {
"parse": "dist/parse.min.js", "parse": "dist/parse.min.js",
"phaser": "dist/phaser.min.js" "phaser": "dist/phaser.min.js"
} },
"source": "src/index.ts",
"module": "dist/spacetac.modern.js",
"types": "dist/index.d.ts",
"files": [
"/src",
"/dist"
],
"bugs": {
"url": "https://gitlab.com/thunderk/spacetac/issues"
},
"homepage": "https://code.thunderk.net/tslib/spacetac",
"keywords": [
"typescript"
]
} }

View file

@ -1,7 +1,9 @@
/// <reference path="../node_modules/phaser/types/phaser.d.ts"/> import { testing } from "./common/Testing";
import { bool } from "./common/Tools";
import { GameSession } from "./core/GameSession";
import { setupEmptyView } from "./ui/TestGame";
module TK.SpaceTac.UI.Specs { class FakeStorage {
class FakeStorage {
data: any = {} data: any = {}
getItem(name: string) { getItem(name: string) {
return this.data[name]; return this.data[name];
@ -9,9 +11,9 @@ module TK.SpaceTac.UI.Specs {
setItem(name: string, value: string) { setItem(name: string, value: string) {
this.data[name] = value; this.data[name] = value;
} }
} }
testing("MainUI", test => { testing("MainUI", test => {
let testgame = setupEmptyView(test); let testgame = setupEmptyView(test);
test.case("saves games in local browser storage", check => { test.case("saves games in local browser storage", check => {
@ -37,5 +39,4 @@ module TK.SpaceTac.UI.Specs {
check.same(ui.session.universe.stars.length, systems); check.same(ui.session.universe.stars.length, systems);
check.same(ui.session.universe.starlinks.length, links); check.same(ui.session.universe.starlinks.length, links);
}); });
}); });
}

View file

@ -1,41 +1,33 @@
/// <reference path="../node_modules/phaser/types/phaser.d.ts"/> /// <reference path="../node_modules/phaser/types/phaser.d.ts"/>
declare var global: any; import { RandomGenerator } from "./common/RandomGenerator"
declare var module: any; import { iteritems, keys } from "./common/Tools"
import { GameSession } from "./core/GameSession"
import { AssetLoading } from "./ui/AssetLoading"
import { BaseView } from "./ui/BaseView"
import { BattleView } from "./ui/battle/BattleView"
import { Boot } from "./ui/Boot"
import { FleetCreationView } from "./ui/character/FleetCreationView"
import { AudioManager } from "./ui/common/AudioManager"
import { IntroView } from "./ui/intro/IntroView"
import { UniverseMapView } from "./ui/map/UniverseMapView"
import { MainMenu } from "./ui/menu/MainMenu"
import { GameOptions } from "./ui/options/GameOptions"
import { Router } from "./ui/Router"
if (typeof window != "undefined") { /**
// If jasmine is not present, ignore describe
(<any>window).describe = (<any>window).describe || function () { };
} else {
if (typeof global != "undefined") {
// In node, does not extend Phaser classes
var handler = {
get(target: any, name: any): any {
return new Proxy(function () { }, handler);
}
}
global.Phaser = new Proxy(function () { }, handler);
}
if (typeof module != "undefined") {
module.exports = { TK };
}
}
module TK.SpaceTac {
/**
* Main class to bootstrap the whole game * Main class to bootstrap the whole game
*/ */
export class MainUI extends Phaser.Game { export class MainUI extends Phaser.Game {
// Current game session // Current game session
session: GameSession session: GameSession
session_token: string | null session_token: string | null
// Audio manager // Audio manager
audio = new UI.Audio(this) audio = new AudioManager(this)
// Game options // Game options
options = new UI.GameOptions(this) options = new GameOptions(this)
// Storage used // Storage used
storage: Storage storage: Storage
@ -73,14 +65,14 @@ module TK.SpaceTac {
this.scene.scenes.forEach(scene => this.scene.resume(scene)); this.scene.scenes.forEach(scene => this.scene.resume(scene));
}); });
this.scene.add('boot', UI.Boot); this.scene.add('boot', Boot);
this.scene.add('loading', UI.AssetLoading); this.scene.add('loading', AssetLoading);
this.scene.add('mainmenu', UI.MainMenu); this.scene.add('mainmenu', MainMenu);
this.scene.add('router', UI.Router); this.scene.add('router', Router);
this.scene.add('battle', UI.BattleView); this.scene.add('battle', BattleView);
this.scene.add('intro', UI.IntroView); this.scene.add('intro', IntroView);
this.scene.add('creation', UI.FleetCreationView); this.scene.add('creation', FleetCreationView);
this.scene.add('universe', UI.UniverseMapView); this.scene.add('universe', UniverseMapView);
this.goToScene('boot'); this.goToScene('boot');
} }
@ -102,7 +94,7 @@ module TK.SpaceTac {
* Display a popup message in current view * Display a popup message in current view
*/ */
displayMessage(message: string) { displayMessage(message: string) {
iteritems(<any>this.scene.keys, (key: string, scene: UI.BaseView) => { iteritems(<any>this.scene.keys, (key: string, scene: BaseView) => {
if (scene.messages && this.scene.isVisible(key)) { if (scene.messages && this.scene.isVisible(key)) {
scene.messages.addMessage(message); scene.messages.addMessage(message);
} }
@ -213,5 +205,4 @@ module TK.SpaceTac {
return true; return true;
} }
} }
}
} }

View file

@ -1,9 +1,8 @@
module TK.Specs { class TestState {
class TestState {
counter = 0 counter = 0
} }
class TestDiff extends Diff<TestState> { class TestDiff extends Diff<TestState> {
private value: number private value: number
constructor(value = 1) { constructor(value = 1) {
super(); super();
@ -15,9 +14,9 @@ module TK.Specs {
getReverse() { getReverse() {
return new TestDiff(-this.value); return new TestDiff(-this.value);
} }
} }
testing("DiffLog", test => { testing("DiffLog", test => {
test.case("stores sequential events", check => { test.case("stores sequential events", check => {
let log = new DiffLog<TestState>(); let log = new DiffLog<TestState>();
check.equals(log.count(), 0); check.equals(log.count(), 0);
@ -44,9 +43,9 @@ module TK.Specs {
log.clear(); log.clear();
check.equals(log.count(), 0); check.equals(log.count(), 0);
}) })
}) })
testing("DiffLogClient", test => { testing("DiffLogClient", test => {
test.case("adds diffs to the log", check => { test.case("adds diffs to the log", check => {
let log = new DiffLog<TestState>(); let log = new DiffLog<TestState>();
let state = new TestState(); let state = new TestState();
@ -226,5 +225,4 @@ module TK.Specs {
check.equals(state.counter, 6); check.equals(state.counter, 6);
check.equals(inter, [5, -1, 2]); check.equals(inter, [5, -1, 2]);
}) })
}) })
}

View file

@ -1,15 +1,11 @@
import { Timer } from "./Timer";
/** /**
* 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. * Base class for a single diff.
* *
* This represents an atomic change of the state, that can be applied, or reverted. * This represents an atomic change of the state, that can be applied, or reverted.
*/ */
export class Diff<T> { export class Diff<T> {
/** /**
* Apply the diff on a given state * Apply the diff on a given state
* *
@ -35,12 +31,12 @@ module TK {
protected getReverse(): Diff<T> { protected getReverse(): Diff<T> {
return new Diff<T>(); return new Diff<T>();
} }
} }
/** /**
* Collection of sequential diffs * Collection of sequential diffs
*/ */
export class DiffLog<T> { export class DiffLog<T> {
private diffs: Diff<T>[] = [] private diffs: Diff<T>[] = []
/** /**
@ -72,12 +68,12 @@ module TK {
clear(start = 0): void { clear(start = 0): void {
this.diffs = this.diffs.slice(0, start); this.diffs = this.diffs.slice(0, start);
} }
} }
/** /**
* Client for a DiffLog, able to go forward or backward in the log, applying diffs as needed * Client for a DiffLog, able to go forward or backward in the log, applying diffs as needed
*/ */
export class DiffLogClient<T> { export class DiffLogClient<T> {
private state: T private state: T
private log: DiffLog<T> private log: DiffLog<T>
private cursor = -1 private cursor = -1
@ -245,5 +241,4 @@ module TK {
truncate(): void { truncate(): void {
this.log.clear(this.cursor + 1); this.log.clear(this.cursor + 1);
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK { testing("Iterators", test => {
testing("Iterators", test => {
function checkit<T>(check: TestContext, base_iterator: Iterable<T>, values: T[], infinite = false) { function checkit<T>(check: TestContext, base_iterator: Iterable<T>, values: T[], infinite = false) {
function checker(check: TestContext) { function checker(check: TestContext) {
let iterator = base_iterator[Symbol.iterator](); let iterator = base_iterator[Symbol.iterator]();
@ -237,5 +236,4 @@ module TK {
check.equals(imax(IEMPTY), -Infinity); check.equals(imax(IEMPTY), -Infinity);
check.equals(imax(iarray([3, 8, 2, 4])), 8); check.equals(imax(iarray([3, 8, 2, 4])), 8);
}); });
}); });
}

View file

@ -1,34 +1,25 @@
import { contains } from "./Tools";
/** /**
* 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 Iterable are "materializing", meaning that they
* may consume iterators up to the end, and will not work well on infinite iterators.
*
* These iterators are guaranteed to be repeatable, meaning that calling Symbol.iterator on them will start over.
*/
module TK {
/**
* Empty iterator * Empty iterator
*/ */
export const IATEND: Iterator<any> = { export const IATEND: Iterator<any> = {
next: function () { next: function () {
return { done: true, value: undefined }; return { done: true, value: undefined };
} }
} }
/** /**
* Empty iterable * Empty iterable
*/ */
export const IEMPTY: Iterable<any> = { export const IEMPTY: Iterable<any> = {
[Symbol.iterator]: () => IATEND [Symbol.iterator]: () => IATEND
} }
/** /**
* Iterable constructor, from an initial value, and a step value * Iterable constructor, from an initial value, and a step value
*/ */
export function irecur<T, S>(start: T, step: (a: T) => T | null): Iterable<T> { export function irecur<T, S>(start: T, step: (a: T) => T | null): Iterable<T> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let val: T | null = start; let val: T | null = start;
@ -38,34 +29,34 @@ module TK {
} while (val !== null); } while (val !== null);
} }
} }
} }
/** /**
* Iterable constructor, from an array * Iterable constructor, from an array
* *
* The iterator will yield the next value each time it is called, then undefined when the array's end is reached. * The iterator will yield the next value each time it is called, then undefined when the array's end is reached.
*/ */
export function iarray<T>(array: T[], offset = 0): Iterable<T> { export function iarray<T>(array: T[], offset = 0): Iterable<T> {
return { return {
[Symbol.iterator]: function () { [Symbol.iterator]: function () {
return array.slice(offset)[Symbol.iterator](); return array.slice(offset)[Symbol.iterator]();
} }
} }
} }
/** /**
* Iterable constructor, from a single value * Iterable constructor, from a single value
* *
* The value will be yielded only once, not repeated over. * The value will be yielded only once, not repeated over.
*/ */
export function isingle<T>(value: T): Iterable<T> { export function isingle<T>(value: T): Iterable<T> {
return iarray([value]); return iarray([value]);
} }
/** /**
* Iterable that repeats the same value. * Iterable that repeats the same value.
*/ */
export function irepeat<T>(value: T, count = -1): Iterable<T> { export function irepeat<T>(value: T, count = -1): Iterable<T> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let n = count; let n = count;
@ -75,37 +66,37 @@ module TK {
} }
} }
} }
} }
/** /**
* Equivalent of Array.forEach for all iterables. * Equivalent of Array.forEach for all iterables.
* *
* If the callback returns *stopper*, the iteration is stopped. * If the callback returns *stopper*, the iteration is stopped.
*/ */
export function iforeach<T>(iterable: Iterable<T>, callback: (_: T) => any, stopper: any = null): void { export function iforeach<T>(iterable: Iterable<T>, callback: (_: T) => any, stopper: any = null): void {
for (let value of iterable) { for (let value of iterable) {
if (callback(value) === stopper) { if (callback(value) === stopper) {
break; break;
} }
} }
} }
/** /**
* Returns the first item passing a predicate * Returns the first item passing a predicate
*/ */
export function ifirst<T>(iterable: Iterable<T>, predicate: (item: T) => boolean): T | null { export function ifirst<T>(iterable: Iterable<T>, predicate: (item: T) => boolean): T | null {
for (let value of iterable) { for (let value of iterable) {
if (predicate(value)) { if (predicate(value)) {
return value; return value;
} }
} }
return null; return null;
} }
/** /**
* Returns the first non-null result of a value-yielding predicate, applied to each iterator element * Returns the first non-null result of a value-yielding predicate, applied to each iterator element
*/ */
export function ifirstmap<T1, T2>(iterable: Iterable<T1>, predicate: (item: T1) => T2 | null): T2 | null { export function ifirstmap<T1, T2>(iterable: Iterable<T1>, predicate: (item: T1) => T2 | null): T2 | null {
for (let value of iterable) { for (let value of iterable) {
let res = predicate(value); let res = predicate(value);
if (res !== null) { if (res !== null) {
@ -113,15 +104,15 @@ module TK {
} }
} }
return null; return null;
} }
/** /**
* Materialize an array from consuming an iterable * Materialize an array from consuming an iterable
* *
* To avoid materializing infinite iterators (and bursting memory), the item count is limited to 1 million, and an * 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. * exception is thrown when this limit is reached.
*/ */
export function imaterialize<T>(iterable: Iterable<T>, limit = 1000000): T[] { export function imaterialize<T>(iterable: Iterable<T>, limit = 1000000): T[] {
let result: T[] = []; let result: T[] = [];
for (let value of iterable) { for (let value of iterable) {
@ -132,14 +123,14 @@ module TK {
} }
return result; return result;
} }
/** /**
* Iterate over natural integers * Iterate over natural integers
* *
* If *count* is not specified, the iterator is infinite * If *count* is not specified, the iterator is infinite
*/ */
export function irange(count: number = -1, start = 0, step = 1): Iterable<number> { export function irange(count: number = -1, start = 0, step = 1): Iterable<number> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let i = start; let i = start;
@ -151,16 +142,16 @@ module TK {
} }
} }
} }
} }
/** /**
* Iterate over numbers, by applying a step taken from an other iterator * Iterate over numbers, by applying a step taken from an other iterator
* *
* This iterator stops when the "step iterator" stops * This iterator stops when the "step iterator" stops
* *
* With no argument, istep() == irange() * With no argument, istep() == irange()
*/ */
export function istep(start = 0, step_iterable = irepeat(1)): Iterable<number> { export function istep(start = 0, step_iterable = irepeat(1)): Iterable<number> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let i = start; let i = start;
@ -171,12 +162,12 @@ module TK {
} }
} }
} }
} }
/** /**
* Skip a given number of values from an iterator, discarding them. * Skip a given number of values from an iterator, discarding them.
*/ */
export function iskip<T>(iterable: Iterable<T>, count = 1): Iterable<T> { export function iskip<T>(iterable: Iterable<T>, count = 1): Iterable<T> {
return { return {
[Symbol.iterator]: function () { [Symbol.iterator]: function () {
let iterator = iterable[Symbol.iterator](); let iterator = iterable[Symbol.iterator]();
@ -187,12 +178,12 @@ module TK {
return iterator; return iterator;
} }
} }
} }
/** /**
* Return the value at a given position in the iterator * Return the value at a given position in the iterator
*/ */
export function iat<T>(iterable: Iterable<T>, position: number): T | null { export function iat<T>(iterable: Iterable<T>, position: number): T | null {
if (position < 0) { if (position < 0) {
return null; return null;
} else { } else {
@ -203,14 +194,14 @@ module TK {
let state = iterator.next(); let state = iterator.next();
return state.done ? null : state.value; return state.done ? null : state.value;
} }
} }
/** /**
* Chain an iterable of iterables. * Chain an iterable of iterables.
* *
* This will yield values from the first yielded iterator, then the second one, and so on... * This will yield values from the first yielded iterator, then the second one, and so on...
*/ */
export function ichainit<T>(iterables: Iterable<Iterable<T>>): Iterable<T> { export function ichainit<T>(iterables: Iterable<Iterable<T>>): Iterable<T> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
for (let iterable of iterables) { for (let iterable of iterables) {
@ -220,29 +211,29 @@ module TK {
} }
} }
} }
} }
/** /**
* Chain iterables. * Chain iterables.
* *
* This will yield values from the first iterator, then the second one, and so on... * This will yield values from the first iterator, then the second one, and so on...
*/ */
export function ichain<T>(...iterables: Iterable<T>[]): Iterable<T> { export function ichain<T>(...iterables: Iterable<T>[]): Iterable<T> {
if (iterables.length == 0) { if (iterables.length == 0) {
return IEMPTY; return IEMPTY;
} else { } else {
return ichainit(iterables); return ichainit(iterables);
} }
} }
/** /**
* Loop an iterator for a number of times. * Loop an iterator for a number of times.
* *
* If count is negative, if will loop forever (infinite iterator). * If count is negative, if will loop forever (infinite iterator).
* *
* onloop may be used to know when the iterator resets. * onloop may be used to know when the iterator resets.
*/ */
export function iloop<T>(base: Iterable<T>, count = -1, onloop?: Function): Iterable<T> { export function iloop<T>(base: Iterable<T>, count = -1, onloop?: Function): Iterable<T> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let n = count; let n = count;
@ -261,12 +252,12 @@ module TK {
} }
} }
} }
} }
/** /**
* Iterator version of "map". * Iterator version of "map".
*/ */
export function imap<T1, T2>(iterable: Iterable<T1>, mapfunc: (_: T1) => T2): Iterable<T2> { export function imap<T1, T2>(iterable: Iterable<T1>, mapfunc: (_: T1) => T2): Iterable<T2> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
for (let value of iterable) { for (let value of iterable) {
@ -274,23 +265,23 @@ module TK {
} }
} }
} }
} }
/** /**
* Iterator version of "reduce". * Iterator version of "reduce".
*/ */
export function ireduce<T>(iterable: Iterable<T>, reduce: (item1: T, item2: T) => T, init: T): T { export function ireduce<T>(iterable: Iterable<T>, reduce: (item1: T, item2: T) => T, init: T): T {
let result = init; let result = init;
for (let value of iterable) { for (let value of iterable) {
result = reduce(result, value); result = reduce(result, value);
} }
return result; return result;
} }
/** /**
* Iterator version of "filter". * Iterator version of "filter".
*/ */
export function ifilter<T>(iterable: Iterable<T>, filterfunc: (_: T) => boolean): Iterable<T> { export function ifilter<T>(iterable: Iterable<T>, filterfunc: (_: T) => boolean): Iterable<T> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
for (let value of iterable) { for (let value of iterable) {
@ -300,38 +291,38 @@ module TK {
} }
} }
} }
} }
/** /**
* Type filter, to return a list of instances of a given type * Type filter, to return a list of instances of a given type
*/ */
export function ifiltertype<T>(iterable: Iterable<any>, filter: (item: any) => item is T): Iterable<T> { export function ifiltertype<T>(iterable: Iterable<any>, filter: (item: any) => item is T): Iterable<T> {
return ifilter(iterable, filter); return ifilter(iterable, filter);
} }
/** /**
* Class filter, to return a list of instances of a given type * Class filter, to return a list of instances of a given type
*/ */
export function ifilterclass<T>(iterable: Iterable<any>, classref: { new(...args: any[]): T }): Iterable<T> { export function ifilterclass<T>(iterable: Iterable<any>, classref: { new(...args: any[]): T }): Iterable<T> {
return ifilter(iterable, (item): item is T => item instanceof classref); return ifilter(iterable, (item): item is T => item instanceof classref);
} }
/** /**
* Combine two iterables. * Combine two iterables.
* *
* This iterates through the second one several times, so if one iterator may be infinite, * This iterates through the second one several times, so if one iterator may be infinite,
* it should be the first one. * it should be the first one.
*/ */
export function icombine<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1, T2]> { export function icombine<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1, T2]> {
return ichainit(imap(it1, v1 => imap(it2, (v2): [T1, T2] => [v1, v2]))); return ichainit(imap(it1, v1 => imap(it2, (v2): [T1, T2] => [v1, v2])));
} }
/** /**
* Advance through two iterables at the same time, yielding item pairs * Advance through two iterables at the same time, yielding item pairs
* *
* Iteration will stop at the first of the two iterators that stops. * Iteration will stop at the first of the two iterators that stops.
*/ */
export function izip<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1, T2]> { export function izip<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1, T2]> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let iterator1 = it1[Symbol.iterator](); let iterator1 = it1[Symbol.iterator]();
@ -345,14 +336,14 @@ module TK {
} }
} }
} }
} }
/** /**
* Advance two iterables at the same time, yielding item pairs (greedy version) * Advance two iterables at the same time, yielding item pairs (greedy version)
* *
* Iteration will stop when both iterators are consumed, returning partial couples (undefined in the peer) if needed. * Iteration will stop when both iterators are consumed, returning partial couples (undefined in the peer) if needed.
*/ */
export function izipg<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1 | undefined, T2 | undefined]> { export function izipg<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1 | undefined, T2 | undefined]> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let iterator1 = it1[Symbol.iterator](); let iterator1 = it1[Symbol.iterator]();
@ -366,19 +357,19 @@ module TK {
} }
} }
} }
} }
/** /**
* Partition in two iterables, one with values that pass the predicate, the other with values that don't * Partition in two iterables, one with values that pass the predicate, the other with values that don't
*/ */
export function ipartition<T>(iterable: Iterable<T>, predicate: (item: T) => boolean): [Iterable<T>, Iterable<T>] { export function ipartition<T>(iterable: Iterable<T>, predicate: (item: T) => boolean): [Iterable<T>, Iterable<T>] {
return [ifilter(iterable, predicate), ifilter(iterable, x => !predicate(x))]; return [ifilter(iterable, predicate), ifilter(iterable, x => !predicate(x))];
} }
/** /**
* Alternate between several iterables (pick one from the first one, then one from the second...) * Alternate between several iterables (pick one from the first one, then one from the second...)
*/ */
export function ialternate<T>(iterables: Iterable<T>[]): Iterable<T> { export function ialternate<T>(iterables: Iterable<T>[]): Iterable<T> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let iterators = iterables.map(iterable => iterable[Symbol.iterator]()); let iterators = iterables.map(iterable => iterable[Symbol.iterator]());
@ -396,9 +387,9 @@ module TK {
} while (done); } while (done);
} }
} }
} }
/** /**
* Yield items from an iterator only once. * 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 * Beware that even if this function is not materializing, it keeps track of yielded item, and may choke on
@ -407,7 +398,7 @@ module TK {
* *
* This function is O(n²) * This function is O(n²)
*/ */
export function iunique<T>(iterable: Iterable<T>, limit = 1000000): Iterable<T> { export function iunique<T>(iterable: Iterable<T>, limit = 1000000): Iterable<T> {
return { return {
[Symbol.iterator]: function* () { [Symbol.iterator]: function* () {
let done: T[] = []; let done: T[] = [];
@ -424,13 +415,12 @@ module TK {
} }
} }
} }
} }
/** /**
* Common reduce shortcuts * Common reduce shortcuts
*/ */
export const isum = (iterable: Iterable<number>) => ireduce(iterable, (a, b) => a + b, 0); export const isum = (iterable: Iterable<number>) => ireduce(iterable, (a, b) => a + b, 0);
export const icat = (iterable: Iterable<string>) => ireduce(iterable, (a, b) => a + b, ""); export const icat = (iterable: Iterable<string>) => ireduce(iterable, (a, b) => a + b, "");
export const imin = (iterable: Iterable<number>) => ireduce(iterable, Math.min, Infinity); export const imin = (iterable: Iterable<number>) => ireduce(iterable, Math.min, Infinity);
export const imax = (iterable: Iterable<number>) => ireduce(iterable, Math.max, -Infinity); export const imax = (iterable: Iterable<number>) => ireduce(iterable, Math.max, -Infinity);
}

View file

@ -1,13 +1,12 @@
module TK.Specs { export class TestRObject extends RObject {
export class TestRObject extends RObject {
x: number x: number
constructor(x = RandomGenerator.global.random()) { constructor(x = RandomGenerator.global.random()) {
super(); super();
this.x = x; this.x = x;
} }
}; };
testing("RObject", test => { testing("RObject", test => {
test.setup(function () { test.setup(function () {
(<any>RObject)._next_id = 0; (<any>RObject)._next_id = 0;
}) })
@ -63,9 +62,9 @@ module TK.Specs {
serializer.unserialize(packed); serializer.unserialize(packed);
check.equals(new TestRObject().id, 3); check.equals(new TestRObject().id, 3);
}) })
}) })
testing("RObjectContainer", test => { testing("RObjectContainer", test => {
test.case("stored objects and get them by their id", check => { test.case("stored objects and get them by their id", check => {
let o1 = new TestRObject(); let o1 = new TestRObject();
let store = new RObjectContainer([o1]); let store = new RObjectContainer([o1]);
@ -121,5 +120,4 @@ module TK.Specs {
check.same(store.get(o2.id), o2, "o2 present"); check.same(store.get(o2.id), o2, "o2 present");
}); });
}) })
}) })
}

View file

@ -1,19 +1,21 @@
module TK { import { iarray } from "./Iterators";
export type RObjectId = number import { values } from "./Tools";
/** export type RObjectId = number
/**
* Returns the id of an object * Returns the id of an object
*/ */
export function rid(obj: RObject | RObjectId): number { export function rid(obj: RObject | RObjectId): number {
return (obj instanceof RObject) ? obj.id : obj; return (obj instanceof RObject) ? obj.id : obj;
} }
/** /**
* Referenced objects, with unique ID. * Referenced objects, with unique ID.
* *
* Objects extending this class will have an automatic unique ID, and may be tracked from an RObjectContainer. * Objects extending this class will have an automatic unique ID, and may be tracked from an RObjectContainer.
*/ */
export class RObject { export class RObject {
readonly id: RObjectId = RObject._next_id++ readonly id: RObjectId = RObject._next_id++
private static _next_id = 0 private static _next_id = 0
@ -35,12 +37,12 @@ module TK {
return this.id === other; return this.id === other;
} }
} }
} }
/** /**
* Container to track referenced objects * Container to track referenced objects
*/ */
export class RObjectContainer<T extends RObject> { export class RObjectContainer<T extends RObject> {
private objects: { [index: number]: T } = {} private objects: { [index: number]: T } = {}
constructor(objects: T[] = []) { constructor(objects: T[] = []) {
@ -96,5 +98,4 @@ module TK {
iterator(): Iterable<T> { iterator(): Iterable<T> {
return iarray(this.list()); return iarray(this.list());
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK { testing("RandomGenerator", test => {
testing("RandomGenerator", test => {
test.case("produces floats", check => { test.case("produces floats", check => {
var gen = new RandomGenerator(); var gen = new RandomGenerator();
@ -88,5 +87,4 @@ module TK {
check.equals(gen.random(), 0.7); check.equals(gen.random(), 0.7);
check.equals(gen.random(), 0.7); check.equals(gen.random(), 0.7);
}); });
}); });
}

View file

@ -1,8 +1,9 @@
module TK { import { range, sum } from "./Tools";
/*
/*
* Random generator. * Random generator.
*/ */
export class RandomGenerator { export class RandomGenerator {
static global: RandomGenerator = new RandomGenerator(); static global: RandomGenerator = new RandomGenerator();
postUnserialize() { postUnserialize() {
@ -85,12 +86,12 @@ module TK {
} }
return range(length).map(() => this.choice(<any>chars)).join(""); return range(length).map(() => this.choice(<any>chars)).join("");
} }
} }
/* /*
* Random generator that produces a series of fixed numbers before going back to random ones. * Random generator that produces a series of fixed numbers before going back to random ones.
*/ */
export class SkewedRandomGenerator extends RandomGenerator { export class SkewedRandomGenerator extends RandomGenerator {
i = 0; i = 0;
suite: number[]; suite: number[];
loop: boolean; loop: boolean;
@ -110,5 +111,4 @@ module TK {
} }
return (typeof result == "undefined") ? Math.random() : result; return (typeof result == "undefined") ? Math.random() : result;
} }
}
} }

View file

@ -1,24 +1,23 @@
module TK.Specs { export class TestSerializerObj1 {
export class TestSerializerObj1 {
a: number; a: number;
constructor(a = 0) { constructor(a = 0) {
this.a = a; this.a = a;
} }
} }
export class TestSerializerObj2 { export class TestSerializerObj2 {
a = () => 1 a = () => 1
b = [(obj: any) => 2] b = [(obj: any) => 2]
} }
export class TestSerializerObj3 { export class TestSerializerObj3 {
a = [1, 2]; a = [1, 2];
postUnserialize() { postUnserialize() {
remove(this.a, 2); remove(this.a, 2);
} }
} }
testing("Serializer", test => { testing("Serializer", test => {
function checkReversability(obj: any, namespace = TK.Specs): any { function checkReversability(obj: any, namespace = TK.Specs): any {
var serializer = new Serializer(TK.Specs); var serializer = new Serializer(TK.Specs);
var data = serializer.serialize(obj); var data = serializer.serialize(obj);
@ -100,5 +99,4 @@ module TK.Specs {
checkReversability(new Timer()); checkReversability(new Timer());
checkReversability(new RandomGenerator()); checkReversability(new RandomGenerator());
}); });
}); });
}

View file

@ -1,17 +1,17 @@
module TK { import { add, classname, crawl, merge, STOP_CRAWLING } from "./Tools";
function isObject(value: any): boolean { function isObject(value: any): boolean {
return value instanceof Object && !Array.isArray(value); return value instanceof Object && !Array.isArray(value);
} }
/** /**
* A typescript object serializer. * A typescript object serializer.
*/ */
export class Serializer { export class Serializer {
namespace: any; namespace: { [name: string]: (...args: any) => any };
ignored: string[] = []; ignored: string[] = [];
constructor(namespace: any = TK) { constructor(namespace: { [name: string]: (...args: any) => any } = {}) {
this.namespace = namespace; this.namespace = namespace;
} }
@ -30,10 +30,6 @@ module TK {
return {}; return {};
} else { } else {
let cl = this.namespace[ctype]; let cl = this.namespace[ctype];
if (cl) {
return Object.create(cl.prototype);
} else {
cl = (<any>TK)[ctype];
if (cl) { if (cl) {
return Object.create(cl.prototype); return Object.create(cl.prototype);
} else { } else {
@ -42,7 +38,6 @@ module TK {
} }
} }
} }
}
/** /**
* Serialize an object to a string * Serialize an object to a string
@ -59,7 +54,7 @@ module TK {
add(objects, value); add(objects, value);
return value; return value;
} else { } else {
return TK.STOP_CRAWLING; return STOP_CRAWLING;
} }
} else { } else {
return value; return value;
@ -107,5 +102,4 @@ module TK {
// First object was the root // First object was the root
return objects[0]; return objects[0];
} }
}
} }

View file

@ -1,14 +1,12 @@
/** import { Timer } from "./Timer";
* Various testing functions.
*/
module TK {
export type FakeClock = { forward: (milliseconds: number) => void }
export type Mock<F extends Function> = { func: F, getCalls: () => any[][], reset: () => void }
/** export type FakeClock = { forward: (milliseconds: number) => void }
export type Mock<F extends Function> = { func: F, getCalls: () => any[][], reset: () => void }
/**
* Main test suite descriptor * Main test suite descriptor
*/ */
export function testing(desc: string, body: (test: TestSuite) => void) { export function testing(desc: string, body: (test: TestSuite) => void) {
if (typeof describe != "undefined") { if (typeof describe != "undefined") {
describe(desc, () => { describe(desc, () => {
beforeEach(() => jasmine.addMatchers(CUSTOM_MATCHERS)); beforeEach(() => jasmine.addMatchers(CUSTOM_MATCHERS));
@ -17,12 +15,12 @@ module TK {
body(test); body(test);
}); });
} }
} }
/** /**
* Test suite (group of test cases) * Test suite (group of test cases)
*/ */
export class TestSuite { export class TestSuite {
private desc: string private desc: string
constructor(desc: string) { constructor(desc: string) {
this.desc = desc; this.desc = desc;
@ -100,12 +98,12 @@ module TK {
get check(): TestContext { get check(): TestContext {
return new TestContext(); return new TestContext();
} }
} }
/** /**
* A test context, with assertion helpers * A test context, with assertion helpers
*/ */
export class TestContext { export class TestContext {
info: string[]; info: string[];
constructor(info: string[] = []) { constructor(info: string[] = []) {
@ -304,9 +302,9 @@ module TK {
fail(message?: string): void { fail(message?: string): void {
fail(this.message(message)); fail(this.message(message));
} }
} }
const CUSTOM_MATCHERS = { const CUSTOM_MATCHERS = {
toEqual: function (util: any, customEqualityTesters: any) { toEqual: function (util: any, customEqualityTesters: any) {
customEqualityTesters = customEqualityTesters || []; customEqualityTesters = customEqualityTesters || [];
@ -326,5 +324,4 @@ module TK {
} }
}; };
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.Specs { testing("Timer", test => {
testing("Timer", test => {
let clock = test.clock(); let clock = test.clock();
test.case("schedules and cancels future calls", check => { test.case("schedules and cancels future calls", check => {
@ -113,5 +112,4 @@ module TK.Specs {
check.equals(Timer.nowMs(), 15); check.equals(Timer.nowMs(), 15);
check.equals(Timer.fromMs(t), 10); check.equals(Timer.fromMs(t), 10);
}); });
}); });
}

View file

@ -1,10 +1,11 @@
module TK { import { add, remove } from "./Tools";
/**
/**
* Timing utility. * Timing utility.
* *
* This extends the standard setTimeout feature. * This extends the standard setTimeout feature.
*/ */
export class Timer { export class Timer {
// Global timer shared by the whole project // Global timer shared by the whole project
static global = new Timer(); static global = new Timer();
@ -96,5 +97,4 @@ module TK {
postUnserialize() { postUnserialize() {
this.scheduled = []; this.scheduled = [];
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.Specs { testing("Toggle", test => {
testing("Toggle", test => {
let on_calls = 0; let on_calls = 0;
let off_calls = 0; let off_calls = 0;
let clock = test.clock(); let clock = test.clock();
@ -154,5 +153,4 @@ module TK.Specs {
check.equals(toggle.isOn(), true); check.equals(toggle.isOn(), true);
checkstate(0, 0); checkstate(0, 0);
}) })
}) })
}

View file

@ -1,5 +1,7 @@
module TK { import { Timer } from "./Timer"
/** import { add, contains, remove } from "./Tools"
/**
* Client for Toggle object, allowing to manipulate it * Client for Toggle object, allowing to manipulate it
* *
* *state* may be: * *state* may be:
@ -9,14 +11,14 @@ module TK {
* *
* The function returns the actual state after applying the requirement * The function returns the actual state after applying the requirement
*/ */
export type ToggleClient = (state?: boolean | number) => boolean export type ToggleClient = (state?: boolean | number) => boolean
/** /**
* A toggle between two states (on and off). * A toggle between two states (on and off).
* *
* A toggle will be on if at least one ToggleClient requires it to be on. * A toggle will be on if at least one ToggleClient requires it to be on.
*/ */
export class Toggle { export class Toggle {
private on: Function private on: Function
private off: Function private off: Function
private status = false private status = false
@ -89,5 +91,4 @@ module TK {
this.off(); this.off();
} }
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.Specs { testing("Tools", test => {
testing("Tools", test => {
test.case("returns boolean equivalent", check => { test.case("returns boolean equivalent", check => {
check.same(bool(null), false, "null"); check.same(bool(null), false, "null");
check.same(bool(undefined), false, "undefined"); check.same(bool(undefined), false, "undefined");
@ -519,5 +518,4 @@ module TK.Specs {
check.equals(disjunctunion([6], [7]), [6, 7]); check.equals(disjunctunion([6], [7]), [6, 7]);
check.equals(disjunctunion([1, 8, 2], [2, 8, 4]), [1, 4]); check.equals(disjunctunion([1, 8, 2], [2, 8, 4]), [1, 4]);
}); });
}); });
}

View file

@ -1,25 +1,23 @@
import { Serializer } from "./Serializer";
/** /**
* Various utility functions.
*/
module TK {
/**
* Functions that does nothing (useful for default callbacks) * Functions that does nothing (useful for default callbacks)
*/ */
export function nop(): void { export function nop(): void {
} }
/** /**
* Identity function (returns the sole argument untouched) * Identity function (returns the sole argument untouched)
*/ */
export function identity<T>(input: T): T { export function identity<T>(input: T): T {
return input; return input;
} }
/** /**
* Check a value for a boolean equivalent * Check a value for a boolean equivalent
*/ */
export function bool<T>(value: T | null | undefined): value is T; export function bool<T>(value: T | null | undefined): value is T;
export function bool(value: any): boolean { export function bool(value: any): boolean {
if (!value) { if (!value) {
return false; return false;
} else if (typeof value == "object") { } else if (typeof value == "object") {
@ -27,54 +25,54 @@ module TK {
} else { } else {
return true; return true;
} }
} }
/** /**
* Return a default value if the given one is undefined * Return a default value if the given one is undefined
*/ */
export function coalesce(value: string | null | undefined, fallback: string): string; export function coalesce(value: string | null | undefined, fallback: string): string;
export function coalesce(value: number | null | undefined, fallback: number): number; export function coalesce(value: number | null | undefined, fallback: number): number;
export function coalesce<T>(value: T | null | undefined, fallback: T): T { export function coalesce<T>(value: T | null | undefined, fallback: T): T {
if (typeof value == "undefined" || value === null) { if (typeof value == "undefined" || value === null) {
return fallback; return fallback;
} else { } else {
return value; return value;
} }
} }
/** /**
* Check for an object being an instance of a type, an returns casted version * Check for an object being an instance of a type, an returns casted version
* *
* Throws an error on failure to cast * Throws an error on failure to cast
*/ */
export function as<T>(classref: { new(...args: any[]): T }, obj: any): T { export function as<T>(classref: { new(...args: any[]): T }, obj: any): T {
if (obj instanceof classref) { if (obj instanceof classref) {
return obj; return obj;
} else { } else {
console.error("Bad cast", obj, classref); console.error("Bad cast", obj, classref);
throw new Error("Bad cast"); throw new Error("Bad cast");
} }
} }
/** /**
* Apply a functor on the result of another function * Apply a functor on the result of another function
*/ */
export function fmap<U, V>(g: (arg: U) => V, f: (...args: any[]) => U): (...args: any[]) => V { export function fmap<U, V>(g: (arg: U) => V, f: (...args: any[]) => U): (...args: any[]) => V {
// TODO variadic typing, as soon as supported by typescript // TODO variadic typing, as soon as supported by typescript
return (...args) => g(f(...args)); return (...args) => g(f(...args));
} }
/** /**
* Apply a default value to nulls or undefineds returned by a function * Apply a default value to nulls or undefineds returned by a function
*/ */
export function nnf<T>(fallback: T, f: (...args: any[]) => T | null): (...args: any[]) => T { export function nnf<T>(fallback: T, f: (...args: any[]) => T | null): (...args: any[]) => T {
return fmap(val => val === null ? fallback : val, f); return fmap(val => val === null ? fallback : val, f);
} }
/** /**
* Check if a value if null, throwing an exception if its the case * Check if a value if null, throwing an exception if its the case
*/ */
export function nn<T>(value: T | null | undefined): T { export function nn<T>(value: T | null | undefined): T {
if (value === null) { if (value === null) {
throw new Error("Null value"); throw new Error("Null value");
} else if (typeof value == "undefined") { } else if (typeof value == "undefined") {
@ -82,19 +80,19 @@ module TK {
} else { } else {
return value; return value;
} }
} }
/** /**
* Remove null values from an array * Remove null values from an array
*/ */
export function nna<T>(array: (T | null)[]): T[] { export function nna<T>(array: (T | null)[]): T[] {
return <T[]>array.filter(item => item !== null); return <T[]>array.filter(item => item !== null);
} }
/** /**
* Compare operator, that can be used in sort() calls. * Compare operator, that can be used in sort() calls.
*/ */
export function cmp(a: any, b: any, reverse = false): number { export function cmp(a: any, b: any, reverse = false): number {
if (a > b) { if (a > b) {
return reverse ? -1 : 1; return reverse ? -1 : 1;
} else if (a < b) { } else if (a < b) {
@ -102,12 +100,12 @@ module TK {
} else { } else {
return 0; return 0;
} }
} }
/** /**
* Clamp a value in a range. * Clamp a value in a range.
*/ */
export function clamp<T>(value: T, min: T, max: T): T { export function clamp<T>(value: T, min: T, max: T): T {
if (value < min) { if (value < min) {
return min; return min;
} else if (value > max) { } else if (value > max) {
@ -115,102 +113,102 @@ module TK {
} else { } else {
return value; return value;
} }
} }
/** /**
* Perform a linear interpolation between two values (factor is between 0 and 1). * Perform a linear interpolation between two values (factor is between 0 and 1).
*/ */
export function lerp(factor: number, min: number, max: number): number { export function lerp(factor: number, min: number, max: number): number {
return min + (max - min) * factor; return min + (max - min) * factor;
} }
/** /**
* Make a deep copy of any object. * Make a deep copy of any object.
* *
* Serializer is used for this, and needs a namespace to work. * Serializer is used for this, and needs a namespace to work.
* *
* Please be aware that contained RObjects will be duplicated, but keep their ID, thus breaking the uniqueness. * Please be aware that contained RObjects will be duplicated, but keep their ID, thus breaking the uniqueness.
*/ */
export function duplicate<T>(obj: T, namespace: Object = TK): T { export function duplicate<T>(obj: T, namespace: { [name: string]: (...args: any) => any } = {}): T {
let serializer = new Serializer(namespace); let serializer = new Serializer(namespace);
let serialized = serializer.serialize({ dat: obj }); let serialized = serializer.serialize({ dat: obj });
return serializer.unserialize(serialized).dat; return serializer.unserialize(serialized).dat;
} }
/** /**
* Make a shallow copy of an array. * Make a shallow copy of an array.
*/ */
export function acopy<T>(input: T[]): T[] { export function acopy<T>(input: T[]): T[] {
return input.slice(); return input.slice();
} }
/** /**
* Call a function for each member of an array, sorted by a key. * Call a function for each member of an array, sorted by a key.
*/ */
export function itersorted<T>(input: T[], keyfunc: (item: T) => any, callback: (item: T) => void): void { export function itersorted<T>(input: T[], keyfunc: (item: T) => any, callback: (item: T) => void): void {
var array = acopy(input); var array = acopy(input);
array.sort((item1, item2) => cmp(keyfunc(item1), keyfunc(item2))); array.sort((item1, item2) => cmp(keyfunc(item1), keyfunc(item2)));
array.forEach(callback); array.forEach(callback);
} }
/** /**
* Capitalize the first letter of an input string. * Capitalize the first letter of an input string.
*/ */
export function capitalize(input: string): string { export function capitalize(input: string): string {
return input.charAt(0).toLocaleUpperCase() + input.slice(1); return input.charAt(0).toLocaleUpperCase() + input.slice(1);
}; };
/** /**
* Check if an array contains an item. * Check if an array contains an item.
*/ */
export function contains<T>(array: T[], item: T): boolean { export function contains<T>(array: T[], item: T): boolean {
return array.indexOf(item) >= 0; return array.indexOf(item) >= 0;
} }
/** /**
* Produce an n-sized array, with integers counting from 0 * Produce an n-sized array, with integers counting from 0
*/ */
export function range(n: number): number[] { export function range(n: number): number[] {
var result: number[] = []; var result: number[] = [];
for (var i = 0; i < n; i++) { for (var i = 0; i < n; i++) {
result.push(i); result.push(i);
} }
return result; return result;
} }
/** /**
* Produce an array of couples, build from the common length of two arrays * Produce an array of couples, build from the common length of two arrays
*/ */
export function zip<T1, T2>(array1: T1[], array2: T2[]): [T1, T2][] { export function zip<T1, T2>(array1: T1[], array2: T2[]): [T1, T2][] {
var result: [T1, T2][] = []; var result: [T1, T2][] = [];
var n = (array1.length > array2.length) ? array2.length : array1.length; var n = (array1.length > array2.length) ? array2.length : array1.length;
for (var i = 0; i < n; i++) { for (var i = 0; i < n; i++) {
result.push([array1[i], array2[i]]); result.push([array1[i], array2[i]]);
} }
return result; return result;
} }
/** /**
* Produce two arrays, build from an array of couples * Produce two arrays, build from an array of couples
*/ */
export function unzip<T1, T2>(array: [T1, T2][]): [T1[], T2[]] { export function unzip<T1, T2>(array: [T1, T2][]): [T1[], T2[]] {
return [array.map(x => x[0]), array.map(x => x[1])]; return [array.map(x => x[0]), array.map(x => x[1])];
} }
/** /**
* Partition a list by a predicate, returning the items that pass the predicate, then the ones that don't pass it * Partition a list by a predicate, returning the items that pass the predicate, then the ones that don't pass it
*/ */
export function binpartition<T>(array: T[], predicate: (item: T) => boolean): [T[], T[]] { export function binpartition<T>(array: T[], predicate: (item: T) => boolean): [T[], T[]] {
let pass: T[] = []; let pass: T[] = [];
let fail: T[] = []; let fail: T[] = [];
array.forEach(item => (predicate(item) ? pass : fail).push(item)); array.forEach(item => (predicate(item) ? pass : fail).push(item));
return [pass, fail]; return [pass, fail];
} }
/** /**
* Yields the neighbors tuple list * Yields the neighbors tuple list
*/ */
export function neighbors<T>(array: T[], wrap = false): [T, T][] { export function neighbors<T>(array: T[], wrap = false): [T, T][] {
var result: [T, T][] = []; var result: [T, T][] = [];
if (array.length > 0) { if (array.length > 0) {
var previous = array[0]; var previous = array[0];
@ -225,33 +223,33 @@ module TK {
} else { } else {
return []; return [];
} }
} }
/** /**
* Type filter, to return a list of instances of a given type * Type filter, to return a list of instances of a given type
*/ */
export function tfilter<T>(array: any[], filter: (item: any) => item is T): T[] { export function tfilter<T>(array: any[], filter: (item: any) => item is T): T[] {
return array.filter(filter); return array.filter(filter);
} }
/** /**
* Class filter, to return a list of instances of a given type * Class filter, to return a list of instances of a given type
*/ */
export function cfilter<T>(array: any[], classref: { new(...args: any[]): T }): T[] { export function cfilter<T>(array: any[], classref: { new(...args: any[]): T }): T[] {
return array.filter((item): item is T => item instanceof classref); return array.filter((item): item is T => item instanceof classref);
} }
/** /**
* Flatten a list of lists * Flatten a list of lists
*/ */
export function flatten<T>(array: T[][]): T[] { export function flatten<T>(array: T[][]): T[] {
return array.reduce((a, b) => a.concat(b), []); return array.reduce((a, b) => a.concat(b), []);
} }
/** /**
* Count each element in an array * Count each element in an array
*/ */
export function counter<T>(array: T[], equals: (a: T, b: T) => boolean = (a, b) => a === b): [T, number][] { export function counter<T>(array: T[], equals: (a: T, b: T) => boolean = (a, b) => a === b): [T, number][] {
var result: [T, number][] = []; var result: [T, number][] = [];
array.forEach(item => { array.forEach(item => {
var found = first(result, iter => equals(iter[0], item)); var found = first(result, iter => equals(iter[0], item));
@ -262,63 +260,63 @@ module TK {
} }
}); });
return result; return result;
} }
/** /**
* Return the first element of the array that matches the predicate, null if not found * Return the first element of the array that matches the predicate, null if not found
*/ */
export function first<T>(array: T[], predicate: (item: T) => boolean): T | null { export function first<T>(array: T[], predicate: (item: T) => boolean): T | null {
for (var i = 0; i < array.length; i++) { for (var i = 0; i < array.length; i++) {
if (predicate(array[i])) { if (predicate(array[i])) {
return array[i]; return array[i];
} }
} }
return null; return null;
} }
/** /**
* Return whether if any element in the array matches the predicate * Return whether if any element in the array matches the predicate
*/ */
export function any<T>(array: T[], predicate: (item: T) => boolean): boolean { export function any<T>(array: T[], predicate: (item: T) => boolean): boolean {
return first(array, predicate) != null; return first(array, predicate) != null;
} }
/** /**
* Return an iterator over an array * Return an iterator over an array
* *
* An iterator is a function yielding the next value each time, until the end of array where it yields null. * An iterator is a function yielding the next value each time, until the end of array where it yields null.
* *
* For more powerful iterators, see Iterators * For more powerful iterators, see Iterators
*/ */
export function iterator<T>(array: T[]): () => T | null { export function iterator<T>(array: T[]): () => T | null {
let i = 0; let i = 0;
return () => (i < array.length) ? array[i++] : null; return () => (i < array.length) ? array[i++] : null;
} }
/** /**
* Iterate a list of (key, value) in an object. * Iterate a list of (key, value) in an object.
*/ */
export function iteritems<T>(obj: { [key: string]: T }, func: (key: string, value: T) => void) { export function iteritems<T>(obj: { [key: string]: T }, func: (key: string, value: T) => void) {
for (var key in obj) { for (var key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
func(key, obj[key]); func(key, obj[key]);
} }
} }
} }
/** /**
* Transform an dictionary object to a list of couples (key, value). * Transform an dictionary object to a list of couples (key, value).
*/ */
export function items<T>(obj: { [key: string]: T }): [string, T][] { export function items<T>(obj: { [key: string]: T }): [string, T][] {
let result: [string, T][] = []; let result: [string, T][] = [];
iteritems(obj, (key, value) => result.push([key, value])); iteritems(obj, (key, value) => result.push([key, value]));
return result; return result;
} }
/** /**
* Return the list of keys from an object. * Return the list of keys from an object.
*/ */
export function keys<T extends object>(obj: T): (Extract<keyof T, string>)[] { export function keys<T extends object>(obj: T): (Extract<keyof T, string>)[] {
var result: (Extract<keyof T, string>)[] = []; var result: (Extract<keyof T, string>)[] = [];
for (var key in obj) { for (var key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
@ -326,12 +324,12 @@ module TK {
} }
} }
return result; return result;
} }
/** /**
* Return the list of values from an object. * Return the list of values from an object.
*/ */
export function values<T>(obj: { [key: string]: T }): T[] { export function values<T>(obj: { [key: string]: T }): T[] {
var result: T[] = []; var result: T[] = [];
for (var key in obj) { for (var key in obj) {
if (obj.hasOwnProperty(key)) { if (obj.hasOwnProperty(key)) {
@ -339,62 +337,62 @@ module TK {
} }
} }
return result; return result;
} }
/** /**
* Iterate an enum values. * Iterate an enum values.
*/ */
export function iterenum<T>(obj: T, callback: (item: number) => void) { export function iterenum<T>(obj: T, callback: (item: number) => void) {
for (var val in obj) { for (var val in obj) {
var parsed = parseInt(val, 10); var parsed = parseInt(val, 10);
if (!isNaN(parsed)) { if (!isNaN(parsed)) {
callback(parsed); callback(parsed);
} }
} }
} }
/** /**
* Collect enum values. * Collect enum values.
*/ */
export function enumvalues<T>(obj: T): number[] { export function enumvalues<T>(obj: T): number[] {
let result: number[] = []; let result: number[] = [];
iterenum(obj, val => result.push(val)); iterenum(obj, val => result.push(val));
return result; return result;
} }
/** /**
* Create a dictionary from a list of couples * Create a dictionary from a list of couples
*/ */
export function dict<T>(array: [string, T][]): { [index: string]: T } { export function dict<T>(array: [string, T][]): { [index: string]: T } {
let result: { [index: string]: T } = {}; let result: { [index: string]: T } = {};
array.forEach(([key, value]) => result[key] = value); array.forEach(([key, value]) => result[key] = value);
return result; return result;
} }
/** /**
* Create a dictionnary index from a list of objects * Create a dictionnary index from a list of objects
*/ */
export function index<T>(array: T[], keyfunc: (obj: T) => string): { [key: string]: T } { export function index<T>(array: T[], keyfunc: (obj: T) => string): { [key: string]: T } {
var result: { [key: string]: T } = {}; var result: { [key: string]: T } = {};
array.forEach(obj => result[keyfunc(obj)] = obj); array.forEach(obj => result[keyfunc(obj)] = obj);
return result; return result;
} }
/** /**
* Add an item to the end of a list, only if not already there * Add an item to the end of a list, only if not already there
*/ */
export function add<T>(array: T[], item: T): boolean { export function add<T>(array: T[], item: T): boolean {
if (!contains(array, item)) { if (!contains(array, item)) {
array.push(item); array.push(item);
return true; return true;
} else { } else {
return false; return false;
} }
} }
/** /**
* Remove an item from a list if found. Return true if changed. * Remove an item from a list if found. Return true if changed.
*/ */
export function remove<T>(array: T[], item: T): boolean { export function remove<T>(array: T[], item: T): boolean {
var idx = array.indexOf(item); var idx = array.indexOf(item);
if (idx >= 0) { if (idx >= 0) {
array.splice(idx, 1); array.splice(idx, 1);
@ -402,106 +400,106 @@ module TK {
} else { } else {
return false; return false;
} }
} }
/** /**
* Check if two standard objects are equal. * Check if two standard objects are equal.
*/ */
export function equals<T>(obj1: { [key: string]: T }, obj2: { [key: string]: T }): boolean { export function equals<T>(obj1: { [key: string]: T }, obj2: { [key: string]: T }): boolean {
return JSON.stringify(obj1) == JSON.stringify(obj2); return JSON.stringify(obj1) == JSON.stringify(obj2);
} }
/** /**
* Call a function on any couple formed from combining two arrays. * Call a function on any couple formed from combining two arrays.
*/ */
export function combicall<T>(array1: T[], array2: T[], callback: (item1: T, item2: T) => void): void { export function combicall<T>(array1: T[], array2: T[], callback: (item1: T, item2: T) => void): void {
array1.forEach(item1 => array2.forEach(item2 => callback(item1, item2))); array1.forEach(item1 => array2.forEach(item2 => callback(item1, item2)));
} }
/** /**
* Combinate two filter functions (predicates), with a boolean and. * Combinate two filter functions (predicates), with a boolean and.
*/ */
export function andfilter<T>(filt1: (item: T) => boolean, filt2: (item: T) => boolean): (item: T) => boolean { export function andfilter<T>(filt1: (item: T) => boolean, filt2: (item: T) => boolean): (item: T) => boolean {
return (item: T) => filt1(item) && filt2(item); return (item: T) => filt1(item) && filt2(item);
} }
/** /**
* Get the class name of an object. * Get the class name of an object.
*/ */
export function classname(obj: Object): string { export function classname(obj: Object): string {
return (<any>obj.constructor).name; return (<any>obj.constructor).name;
} }
/** /**
* Get the lowest item of an array, using a mapping function. * Get the lowest item of an array, using a mapping function.
*/ */
export function lowest<T>(array: T[], rating: (item: T) => number): T { export function lowest<T>(array: T[], rating: (item: T) => number): T {
var rated = array.map((item: T): [T, number] => [item, rating(item)]); var rated = array.map((item: T): [T, number] => [item, rating(item)]);
rated.sort((a, b) => cmp(a[1], b[1])); rated.sort((a, b) => cmp(a[1], b[1]));
return rated[0][0]; return rated[0][0];
} }
/** /**
* Return a function bound to an object. * Return a function bound to an object.
* *
* This is useful to pass the bound function as callback directly. * This is useful to pass the bound function as callback directly.
*/ */
export function bound<T, K extends keyof T>(obj: T, func: K): T[K] { export function bound<T, K extends keyof T>(obj: T, func: K): T[K] {
let attr = obj[func]; let attr = obj[func];
if (attr instanceof Function) { if (attr instanceof Function) {
return attr.bind(obj); return attr.bind(obj);
} else { } else {
return <any>(() => attr); return <any>(() => attr);
} }
} }
/** /**
* Return a 0.0-1.0 factor of progress between two limits. * Return a 0.0-1.0 factor of progress between two limits.
*/ */
export function progress(value: number, min: number, max: number) { export function progress(value: number, min: number, max: number) {
var result = (value - min) / (max - min); var result = (value - min) / (max - min);
return clamp(result, 0.0, 1.0); return clamp(result, 0.0, 1.0);
} }
/** /**
* Copy all fields of an object in another (shallow copy) * Copy all fields of an object in another (shallow copy)
*/ */
export function copyfields<T>(src: Partial<T>, dest: Partial<T>) { export function copyfields<T>(src: Partial<T>, dest: Partial<T>) {
for (let key in src) { for (let key in src) {
if (src.hasOwnProperty(key)) { if (src.hasOwnProperty(key)) {
dest[key] = src[key]; dest[key] = src[key];
} }
} }
} }
/** /**
* Copy an object (only a shallow copy of immediate properties) * Copy an object (only a shallow copy of immediate properties)
*/ */
export function copy<T extends Object>(object: T): T { export function copy<T extends Object>(object: T): T {
let objectCopy = <T>Object.create(object.constructor.prototype); let objectCopy = <T>Object.create(object.constructor.prototype);
copyfields(object, objectCopy); copyfields(object, objectCopy);
return objectCopy; return objectCopy;
} }
/** /**
* Merge an object into another * Merge an object into another
*/ */
export function merge<T>(base: T, incoming: Partial<T>): T { export function merge<T>(base: T, incoming: Partial<T>): T {
let result = copy(base); let result = copy(base);
copyfields(incoming, result); copyfields(incoming, result);
return result; return result;
} }
export const STOP_CRAWLING = {}; export const STOP_CRAWLING = {};
/** /**
* Recursively crawl through an object, yielding any defined value found along the way * Recursively crawl through an object, yielding any defined value found along the way
* *
* If *replace* is set to true, the current object is replaced (in array or object attribute) by the result of the callback * If *replace* is set to true, the current object is replaced (in array or object attribute) by the result of the callback
* *
* *memo* is used to prevent circular references to be traversed * *memo* is used to prevent circular references to be traversed
*/ */
export function crawl(obj: any, callback: (item: any) => any, replace = false, memo: any[] = []) { export function crawl(obj: any, callback: (item: any) => any, replace = false, memo: any[] = []) {
if (obj instanceof Object && !Array.isArray(obj)) { if (obj instanceof Object && !Array.isArray(obj)) {
if (memo.indexOf(obj) >= 0) { if (memo.indexOf(obj) >= 0) {
return obj; return obj;
@ -538,103 +536,102 @@ module TK {
} else { } else {
return obj; return obj;
} }
} }
/** /**
* Return the minimal value of an array * Return the minimal value of an array
*/ */
export function min<T>(array: T[]): T { export function min<T>(array: T[]): T {
return array.reduce((a, b) => a < b ? a : b); return array.reduce((a, b) => a < b ? a : b);
} }
/** /**
* Return the maximal value of an array * Return the maximal value of an array
*/ */
export function max<T>(array: T[]): T { export function max<T>(array: T[]): T {
return array.reduce((a, b) => a > b ? a : b); return array.reduce((a, b) => a > b ? a : b);
} }
/** /**
* Return the sum of an array * Return the sum of an array
*/ */
export function sum(array: number[]): number { export function sum(array: number[]): number {
return array.reduce((a, b) => a + b, 0); return array.reduce((a, b) => a + b, 0);
} }
/** /**
* Return the average of an array * Return the average of an array
*/ */
export function avg(array: number[]): number { export function avg(array: number[]): number {
return sum(array) / array.length; return sum(array) / array.length;
} }
/** /**
* Return value, with the same sign as base * Return value, with the same sign as base
*/ */
export function samesign(value: number, base: number): number { export function samesign(value: number, base: number): number {
return Math.abs(value) * (base < 0 ? -1 : 1); return Math.abs(value) * (base < 0 ? -1 : 1);
} }
/** /**
* Return a copy of the array, sorted by a cmp function (equivalent of javascript sort) * Return a copy of the array, sorted by a cmp function (equivalent of javascript sort)
*/ */
export function sorted<T>(array: T[], cmpfunc: (v1: T, v2: T) => number): T[] { export function sorted<T>(array: T[], cmpfunc: (v1: T, v2: T) => number): T[] {
return acopy(array).sort(cmpfunc); return acopy(array).sort(cmpfunc);
} }
/** /**
* Return a copy of the array, sorted by the result of a function applied to each item * Return a copy of the array, sorted by the result of a function applied to each item
*/ */
export function sortedBy<T1, T2>(array: T1[], func: (val: T1) => T2, reverse = false): T1[] { export function sortedBy<T1, T2>(array: T1[], func: (val: T1) => T2, reverse = false): T1[] {
return sorted(array, (a, b) => cmp(func(a), func(b), reverse)); return sorted(array, (a, b) => cmp(func(a), func(b), reverse));
} }
/** /**
* Return the minimum of an array transformed by a function * Return the minimum of an array transformed by a function
*/ */
export function minBy<T1, T2>(array: T1[], func: (val: T1) => T2): T1 { export function minBy<T1, T2>(array: T1[], func: (val: T1) => T2): T1 {
return array.reduce((a, b) => func(a) < func(b) ? a : b); return array.reduce((a, b) => func(a) < func(b) ? a : b);
} }
/** /**
* Return the maximum of an array transformed by a function * Return the maximum of an array transformed by a function
*/ */
export function maxBy<T1, T2>(array: T1[], func: (val: T1) => T2): T1 { export function maxBy<T1, T2>(array: T1[], func: (val: T1) => T2): T1 {
return array.reduce((a, b) => func(a) > func(b) ? a : b); return array.reduce((a, b) => func(a) > func(b) ? a : b);
} }
/** /**
* Return a copy of an array, containing each value only once * Return a copy of an array, containing each value only once
*/ */
export function unique<T>(array: T[]): T[] { export function unique<T>(array: T[]): T[] {
return array.filter((value, index, self) => self.indexOf(value) === index); return array.filter((value, index, self) => self.indexOf(value) === index);
} }
/** /**
* Return the union of two arrays (items in either array) * Return the union of two arrays (items in either array)
*/ */
export function union<T>(array1: T[], array2: T[]): T[] { export function union<T>(array1: T[], array2: T[]): T[] {
return array1.concat(difference(array2, array1)); return array1.concat(difference(array2, array1));
} }
/** /**
* Return the difference between two arrays (items in the first, but not in the second) * Return the difference between two arrays (items in the first, but not in the second)
*/ */
export function difference<T>(array1: T[], array2: T[]): T[] { export function difference<T>(array1: T[], array2: T[]): T[] {
return array1.filter(value => !contains(array2, value)); return array1.filter(value => !contains(array2, value));
} }
/** /**
* Return the intersection of two arrays (items in both arrays) * Return the intersection of two arrays (items in both arrays)
*/ */
export function intersection<T>(array1: T[], array2: T[]): T[] { export function intersection<T>(array1: T[], array2: T[]): T[] {
return array1.filter(value => contains(array2, value)); return array1.filter(value => contains(array2, value));
} }
/** /**
* Return the disjunctive union of two arrays (items not in both arrays) * Return the disjunctive union of two arrays (items not in both arrays)
*/ */
export function disjunctunion<T>(array1: T[], array2: T[]): T[] { export function disjunctunion<T>(array1: T[], array2: T[]): T[] {
return difference(union(array1, array2), intersection(array1, array2)); return difference(union(array1, array2), intersection(array1, array2));
}
} }

View file

@ -1,10 +1,9 @@
module TK.SpaceTac.Specs { function checkLocation(check: TestContext, got: IArenaLocation, expected_x: number, expected_y: number) {
function checkLocation(check: TestContext, got: IArenaLocation, expected_x: number, expected_y: number) {
check.equals(got.x, expected_x, `x differs (${got.x},${got.y}) (${expected_x},${expected_y})`); check.equals(got.x, expected_x, `x differs (${got.x},${got.y}) (${expected_x},${expected_y})`);
check.equals(got.y, expected_y, `y differs (${got.x},${got.y}) (${expected_x},${expected_y})`); check.equals(got.y, expected_y, `y differs (${got.x},${got.y}) (${expected_x},${expected_y})`);
} }
testing("HexagonalArenaGrid", test => { testing("HexagonalArenaGrid", test => {
test.case("snaps coordinates to the nearest grid point, on a biased grid", check => { test.case("snaps coordinates to the nearest grid point, on a biased grid", check => {
let grid = new HexagonalArenaGrid(4, 0.75); let grid = new HexagonalArenaGrid(4, 0.75);
checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0); checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0);
@ -25,5 +24,4 @@ module TK.SpaceTac.Specs {
checkLocation(check, grid.snap({ x: 8, y: 0 }), 10, 0); checkLocation(check, grid.snap({ x: 8, y: 0 }), 10, 0);
checkLocation(check, grid.snap({ x: 1, y: 6 }), 5, 10 * Math.sqrt(0.75)); checkLocation(check, grid.snap({ x: 1, y: 6 }), 5, 10 * Math.sqrt(0.75));
}); });
}); });
}

View file

@ -1,19 +1,20 @@
module TK.SpaceTac { import { ArenaLocation, IArenaLocation } from "./ArenaLocation";
/**
/**
* Abstract grid for the arena where the battle takes place * Abstract grid for the arena where the battle takes place
* *
* The grid is used to snap arena coordinates for ships and targets * The grid is used to snap arena coordinates for ships and targets
*/ */
export interface IArenaGrid { export interface IArenaGrid {
snap(loc: IArenaLocation): IArenaLocation; snap(loc: IArenaLocation): IArenaLocation;
} }
/** /**
* Hexagonal unbounded arena grid * Hexagonal unbounded arena grid
* *
* This grid is composed of regular hexagons where all vertices are at a same distance "unit" of the hexagon center * This grid is composed of regular hexagons where all vertices are at a same distance "unit" of the hexagon center
*/ */
export class HexagonalArenaGrid implements IArenaGrid { export class HexagonalArenaGrid implements IArenaGrid {
private yunit: number; private yunit: number;
constructor(private unit: number, private yfactor = Math.sqrt(0.75)) { constructor(private unit: number, private yfactor = Math.sqrt(0.75)) {
@ -30,5 +31,4 @@ module TK.SpaceTac {
} }
return new ArenaLocation((xr * this.unit) || 0, (yr * this.yunit) || 0); return new ArenaLocation((xr * this.unit) || 0, (yr * this.yunit) || 0);
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.SpaceTac.Specs { testing("ArenaLocation", test => {
testing("ArenaLocation", test => {
test.case("gets distance and angle between two locations", check => { test.case("gets distance and angle between two locations", check => {
check.nears(arenaDistance({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.sqrt(2)); check.nears(arenaDistance({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.sqrt(2));
check.nears(arenaAngle({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.PI / 4); check.nears(arenaAngle({ x: 0, y: 0 }, { x: 1, y: 1 }), Math.PI / 4);
@ -18,5 +17,4 @@ module TK.SpaceTac.Specs {
check.nears(degrees(Math.PI / 2), 90); check.nears(degrees(Math.PI / 2), 90);
check.nears(radians(45), Math.PI / 4); check.nears(radians(45), Math.PI / 4);
}); });
}); });
}

View file

@ -1,12 +1,11 @@
module TK.SpaceTac { /**
/**
* Location in the arena (coordinates only) * Location in the arena (coordinates only)
*/ */
export interface IArenaLocation { export interface IArenaLocation {
x: number x: number
y: number y: number
} }
export class ArenaLocation implements IArenaLocation { export class ArenaLocation implements IArenaLocation {
x: number x: number
y: number y: number
@ -14,86 +13,85 @@ module TK.SpaceTac {
this.x = x; this.x = x;
this.y = y; this.y = y;
} }
} }
/** /**
* Location in the arena, with a facing angle in radians * Location in the arena, with a facing angle in radians
*/ */
export interface IArenaLocationAngle { export interface IArenaLocationAngle {
x: number x: number
y: number y: number
angle: number angle: number
} }
export class ArenaLocationAngle extends ArenaLocation implements IArenaLocationAngle { export class ArenaLocationAngle extends ArenaLocation implements IArenaLocationAngle {
angle: number angle: number
constructor(x = 0, y = 0, angle = 0) { constructor(x = 0, y = 0, angle = 0) {
super(x, y); super(x, y);
this.angle = angle; this.angle = angle;
} }
} }
/** /**
* Circle area in the arena * Circle area in the arena
*/ */
export interface IArenaCircleArea { export interface IArenaCircleArea {
x: number x: number
y: number y: number
radius: number radius: number
} }
export class ArenaCircleArea extends ArenaLocation implements IArenaCircleArea { export class ArenaCircleArea extends ArenaLocation implements IArenaCircleArea {
radius: number radius: number
constructor(x = 0, y = 0, radius = 0) { constructor(x = 0, y = 0, radius = 0) {
super(x, y); super(x, y);
this.radius = radius; this.radius = radius;
} }
} }
/** /**
* Get the normalized angle (in radians) between two locations * Get the normalized angle (in radians) between two locations
*/ */
export function arenaAngle(loc1: IArenaLocation, loc2: IArenaLocation): number { export function arenaAngle(loc1: IArenaLocation, loc2: IArenaLocation): number {
return Math.atan2(loc2.y - loc1.y, loc2.x - loc1.x); return Math.atan2(loc2.y - loc1.y, loc2.x - loc1.x);
} }
/** /**
* Get the "angular difference" between two angles in radians, in ]-pi,pi] range. * Get the "angular difference" between two angles in radians, in ]-pi,pi] range.
*/ */
export function angularDifference(angle1: number, angle2: number): number { export function angularDifference(angle1: number, angle2: number): number {
let diff = angle2 - angle1; let diff = angle2 - angle1;
return diff - Math.PI * 2 * Math.floor((diff + Math.PI) / (Math.PI * 2)); return diff - Math.PI * 2 * Math.floor((diff + Math.PI) / (Math.PI * 2));
} }
/** /**
* Get the normalized distance between two locations * Get the normalized distance between two locations
*/ */
export function arenaDistance(loc1: IArenaLocation, loc2: IArenaLocation): number { export function arenaDistance(loc1: IArenaLocation, loc2: IArenaLocation): number {
let dx = loc2.x - loc1.x; let dx = loc2.x - loc1.x;
let dy = loc2.y - loc1.y; let dy = loc2.y - loc1.y;
return Math.sqrt(dx * dx + dy * dy); return Math.sqrt(dx * dx + dy * dy);
} }
/** /**
* Check if a location is inside an area * Check if a location is inside an area
*/ */
export function arenaInside(loc1: IArenaLocation, loc2: IArenaCircleArea, border_inclusive = true): boolean { export function arenaInside(loc1: IArenaLocation, loc2: IArenaCircleArea, border_inclusive = true): boolean {
let dist = arenaDistance(loc1, loc2); let dist = arenaDistance(loc1, loc2);
return border_inclusive ? (dist <= loc2.radius) : (dist < loc2.radius); return border_inclusive ? (dist <= loc2.radius) : (dist < loc2.radius);
} }
/** /**
* Convert radians angle to degrees * Convert radians angle to degrees
*/ */
export function degrees(angle: number): number { export function degrees(angle: number): number {
return angle * 180 / Math.PI; return angle * 180 / Math.PI;
} }
/** /**
* Convert degrees angle to radians * Convert degrees angle to radians
*/ */
export function radians(angle: number): number { export function radians(angle: number): number {
return angle * Math.PI / 180; return angle * Math.PI / 180;
}
} }

View file

@ -1,5 +1,4 @@
module TK.SpaceTac { testing("Battle", test => {
testing("Battle", test => {
test.case("defines play order by initiative throws", check => { test.case("defines play order by initiative throws", check => {
var fleet1 = new Fleet(); var fleet1 = new Fleet();
var fleet2 = new Fleet(); var fleet2 = new Fleet();
@ -377,5 +376,4 @@ module TK.SpaceTac {
check.equals(battle.log.count(), 0, "log count=0"); check.equals(battle.log.count(), 0, "log count=0");
}); });
}) })
}); });
}

View file

@ -1,8 +1,29 @@
module TK.SpaceTac { import { iarray, ichainit, ifilter, iforeach, imap, imaterialize } from "../common/Iterators"
/** import { RandomGenerator } from "../common/RandomGenerator"
import { RObjectContainer, RObjectId } from "../common/RObject"
import { bool, flatten } from "../common/Tools"
import { EndTurnAction } from "./actions/EndTurnAction"
import { AIWorker } from "./ai/AIWorker"
import { HexagonalArenaGrid, IArenaGrid } from "./ArenaGrid"
import { BattleChecks } from "./BattleChecks"
import { BattleLog, BattleLogClient } from "./BattleLog"
import { BattleOutcome } from "./BattleOutcome"
import { BattleStats } from "./BattleStats"
import { BaseBattleDiff } from "./diffs/BaseBattleDiff"
import { EndBattleDiff } from "./diffs/EndBattleDiff"
import { ShipActionEndedDiff } from "./diffs/ShipActionEndedDiff"
import { ShipActionUsedDiff } from "./diffs/ShipActionUsedDiff"
import { Drone } from "./Drone"
import { BaseEffect } from "./effects/BaseEffect"
import { Fleet } from "./Fleet"
import { Player } from "./Player"
import { Ship } from "./Ship"
import { Target } from "./Target"
/**
* A turn-based battle between fleets * A turn-based battle between fleets
*/ */
export class Battle { export class Battle {
// Grid for the arena // Grid for the arena
grid?: IArenaGrid grid?: IArenaGrid
@ -412,5 +433,4 @@ module TK.SpaceTac {
} }
client.truncate(); client.truncate();
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.SpaceTac.Specs { testing("BattleCheats", test => {
testing("BattleCheats", test => {
test.case("wins a battle", check => { test.case("wins a battle", check => {
let battle = Battle.newQuickRandom(); let battle = Battle.newQuickRandom();
let cheats = new BattleCheats(battle, battle.fleets[0].player); let cheats = new BattleCheats(battle, battle.fleets[0].player);
@ -21,5 +20,4 @@ module TK.SpaceTac.Specs {
check.same(nn(battle.outcome).winner, battle.fleets[1].id, "winner"); check.same(nn(battle.outcome).winner, battle.fleets[1].id, "winner");
check.equals(any(battle.fleets[0].ships, ship => ship.alive), false, "all allies dead"); check.equals(any(battle.fleets[0].ships, ship => ship.alive), false, "all allies dead");
}) })
}) })
}

View file

@ -1,10 +1,14 @@
module TK.SpaceTac { import { iforeach } from "../common/Iterators";
/** import { first } from "../common/Tools";
import { Battle } from "./Battle";
import { Player } from "./Player";
/**
* Cheat helpers for current battle * Cheat helpers for current battle
* *
* May be used from the console to help development * May be used from the console to help development
*/ */
export class BattleCheats { export class BattleCheats {
battle: Battle battle: Battle
player: Player player: Player
@ -36,5 +40,4 @@ module TK.SpaceTac {
}); });
this.battle.endBattle(first(this.battle.fleets, fleet => !this.player.is(fleet.player))); this.battle.endBattle(first(this.battle.fleets, fleet => !this.player.is(fleet.player)));
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.SpaceTac.Specs { testing("BattleChecks", test => {
testing("BattleChecks", test => {
test.case("detects victory conditions", check => { test.case("detects victory conditions", check => {
let battle = new Battle(); let battle = new Battle();
let ship1 = battle.fleets[0].addShip(); let ship1 = battle.fleets[0].addShip();
@ -102,5 +101,4 @@ module TK.SpaceTac.Specs {
], "effects diff"); ], "effects diff");
}); });
}) })
}) })
}

View file

@ -1,12 +1,23 @@
module TK.SpaceTac { import { ifirst, iforeach, imaterialize } from "../common/Iterators";
/** import { RObjectContainer } from "../common/RObject";
import { any, first, flatten, keys } from "../common/Tools";
import { Battle } from "./Battle";
import { BaseBattleDiff } from "./diffs/BaseBattleDiff";
import { EndBattleDiff } from "./diffs/EndBattleDiff";
import { ShipEffectAddedDiff, ShipEffectRemovedDiff } from "./diffs/ShipEffectAddedDiff";
import { ShipValueDiff } from "./diffs/ShipValueDiff";
import { StickyEffect } from "./effects/StickyEffect";
import { Ship } from "./Ship";
import { SHIP_VALUES } from "./ShipValue";
/**
* List of checks to apply at the end of an action, to ensure a correct battle state * List of checks to apply at the end of an action, to ensure a correct battle state
* *
* This is useful when the list of effects simulated by an action was missing something * This is useful when the list of effects simulated by an action was missing something
* *
* To fix the state, new diffs will be applied * To fix the state, new diffs will be applied
*/ */
export class BattleChecks { export class BattleChecks {
constructor(private battle: Battle) { constructor(private battle: Battle) {
} }
@ -160,5 +171,4 @@ module TK.SpaceTac {
let ships = imaterialize(this.battle.iships(true)); let ships = imaterialize(this.battle.iships(true));
return flatten(ships.map(ship => this.getAreaEffectsDiff(ship))); return flatten(ships.map(ship => this.getAreaEffectsDiff(ship)));
} }
}
} }

View file

@ -1,15 +1,14 @@
/// <reference path="../common/DiffLog.ts" /> import { DiffLog, DiffLogClient } from "../common/DiffLog";
import { Battle } from "./Battle";
module TK.SpaceTac { /**
/**
* Log of diffs that change the state of a battle * Log of diffs that change the state of a battle
*/ */
export class BattleLog extends DiffLog<Battle> { export class BattleLog extends DiffLog<Battle> {
} }
/** /**
* Client for a battle log * Client for a battle log
*/ */
export class BattleLogClient extends DiffLogClient<Battle> { export class BattleLogClient extends DiffLogClient<Battle> {
}
} }

View file

@ -1,5 +1,4 @@
module TK.SpaceTac.Specs { testing("BattleOutcome", test => {
testing("BattleOutcome", test => {
test.case("grants experience", check => { test.case("grants experience", check => {
let fleet1 = new Fleet(); let fleet1 = new Fleet();
let ship1a = fleet1.addShip(new Ship()); let ship1a = fleet1.addShip(new Ship());
@ -32,5 +31,4 @@ module TK.SpaceTac.Specs {
check.equals(ship2a.level.getExperience(), 1518); check.equals(ship2a.level.getExperience(), 1518);
check.equals(ship2b.level.getExperience(), 2818); check.equals(ship2b.level.getExperience(), 2818);
}); });
}); });
}

View file

@ -1,10 +1,13 @@
module TK.SpaceTac { import { RObjectId } from "../common/RObject";
/** import { flatten, sum } from "../common/Tools";
import { Fleet } from "./Fleet";
/**
* Result of an ended battle * Result of an ended battle
* *
* This stores the winner, and the retrievable loot * This stores the winner, and the retrievable loot
*/ */
export class BattleOutcome { export class BattleOutcome {
// Indicates if the battle is a draw (no winner) // Indicates if the battle is a draw (no winner)
draw: boolean draw: boolean
@ -30,5 +33,4 @@ module TK.SpaceTac {
}); });
}); });
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.SpaceTac.Specs { testing("BattleStats", test => {
testing("BattleStats", test => {
test.case("collects stats", check => { test.case("collects stats", check => {
let stats = new BattleStats(); let stats = new BattleStats();
check.equals(stats.stats, {}); check.equals(stats.stats, {});
@ -71,5 +70,4 @@ module TK.SpaceTac.Specs {
stats.processLog(battle.log, battle.fleets[0], true); stats.processLog(battle.log, battle.fleets[0], true);
check.equals(stats.stats, { "Drones deployed": [1, 1] }); check.equals(stats.stats, { "Drones deployed": [1, 1] });
}) })
}) })
}

View file

@ -1,8 +1,15 @@
module TK.SpaceTac { import { any, iteritems } from "../common/Tools";
/** import { BattleLog } from "./BattleLog";
import { BaseBattleShipDiff } from "./diffs/BaseBattleDiff";
import { DroneDeployedDiff } from "./diffs/DroneDeployedDiff";
import { ShipDamageDiff } from "./diffs/ShipDamageDiff";
import { ShipMoveDiff } from "./diffs/ShipMoveDiff";
import { Fleet } from "./Fleet";
/**
* Statistics collection over a battle * Statistics collection over a battle
*/ */
export class BattleStats { export class BattleStats {
stats: { [name: string]: [number, number] } = {} stats: { [name: string]: [number, number] } = {}
/** /**
@ -60,5 +67,4 @@ module TK.SpaceTac {
} }
} }
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.SpaceTac.Specs { testing("Cooldown", test => {
testing("Cooldown", test => {
test.case("applies overheat and cooldown", check => { test.case("applies overheat and cooldown", check => {
let cooldown = new Cooldown(); let cooldown = new Cooldown();
check.equals(cooldown.canUse(), true); check.equals(cooldown.canUse(), true);
@ -34,5 +33,4 @@ module TK.SpaceTac.Specs {
cooldown.cool(); cooldown.cool();
check.equals(cooldown.canUse(), true); check.equals(cooldown.canUse(), true);
}); });
}); });
}

View file

@ -1,8 +1,7 @@
module TK.SpaceTac { /**
/**
* Cooldown system for equipments * Cooldown system for equipments
*/ */
export class Cooldown { export class Cooldown {
// Number of uses in the current turn // Number of uses in the current turn
uses = 0 uses = 0
@ -89,5 +88,4 @@ module TK.SpaceTac {
this.uses = 0; this.uses = 0;
this.heat = 0; this.heat = 0;
} }
}
} }

View file

@ -1,5 +1,4 @@
module TK.SpaceTac { testing("Drone", test => {
testing("Drone", test => {
test.case("applies area effects when deployed", check => { test.case("applies area effects when deployed", check => {
let battle = TestTools.createBattle(); let battle = TestTools.createBattle();
let ship = nn(battle.playing_ship); let ship = nn(battle.playing_ship);
@ -58,5 +57,4 @@ module TK.SpaceTac {
] ]
check.equals(drone.getDescription(), "While deployed:\n• do 5 damage\n• evasion +1"); check.equals(drone.getDescription(), "While deployed:\n• do 5 damage\n• evasion +1");
}); });
}); });
}

View file

@ -1,8 +1,16 @@
module TK.SpaceTac { import { ifilter, imaterialize } from "../common/Iterators"
/** import { RObject, RObjectId } from "../common/RObject"
import { DeployDroneAction } from "./actions/DeployDroneAction"
import { ArenaLocation } from "./ArenaLocation"
import { Battle } from "./Battle"
import { BaseEffect } from "./effects/BaseEffect"
import { Ship } from "./Ship"
import { Target } from "./Target"
/**
* Drones are static objects that apply effects in a circular zone around themselves. * Drones are static objects that apply effects in a circular zone around themselves.
*/ */
export class Drone extends RObject { export class Drone extends RObject {
// ID of the owning ship // ID of the owning ship
owner: RObjectId owner: RObjectId
@ -59,5 +67,4 @@ module TK.SpaceTac {
let ships = ifilter(battle.iships(), ship => ship.alive && ship.isInCircle(this.x, this.y, this.radius)); let ships = ifilter(battle.iships(), ship => ship.alive && ship.isInCircle(this.x, this.y, this.radius));
return imaterialize(ships); return imaterialize(ships);
} }
}
} }

View file

@ -1,5 +1,9 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("ExclusionAreas", test => { import { ArenaLocationAngle } from "./ArenaLocation";
import { Battle } from "./Battle";
import { ExclusionAreas } from "./ExclusionAreas";
testing("ExclusionAreas", test => {
test.case("constructs from a ship or battle", check => { test.case("constructs from a ship or battle", check => {
let battle = new Battle(); let battle = new Battle();
battle.border = 17; battle.border = 17;
@ -39,5 +43,4 @@ module TK.SpaceTac.Specs {
check.equals(exclusion.effective_obstacle, 31); check.equals(exclusion.effective_obstacle, 31);
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]); check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]);
}) })
}) })
}

View file

@ -1,5 +1,11 @@
module TK.SpaceTac { import { ifilter, imap, imaterialize } from "../common/Iterators"
/** import { cmp, contains, sorted } from "../common/Tools"
import { arenaDistance, ArenaLocation } from "./ArenaLocation"
import { Battle } from "./Battle"
import { Ship } from "./Ship"
import { Target } from "./Target"
/**
* Helper for working with exclusion areas (areas where a ship cannot go) * Helper for working with exclusion areas (areas where a ship cannot go)
* *
* There are three types of exclusion: * There are three types of exclusion:
@ -7,7 +13,7 @@ module TK.SpaceTac {
* - Hard obstacle exclusion, that prevents two ships from being too close to each other * - Hard obstacle exclusion, that prevents two ships from being too close to each other
* - Soft obstacle exclusion, usually associated with an engine, that prevents a ship from moving too close to others * - Soft obstacle exclusion, usually associated with an engine, that prevents a ship from moving too close to others
*/ */
export class ExclusionAreas { export class ExclusionAreas {
xmin: number xmin: number
xmax: number xmax: number
ymin: number ymin: number
@ -92,5 +98,4 @@ module TK.SpaceTac {
return new ArenaLocation(target.x, target.y); return new ArenaLocation(target.x, target.y);
} }
}
} }

View file

@ -1,5 +1,12 @@
module TK.SpaceTac { import { RObjectId } from "../common/RObject";
testing("Fleet", test => { import { testing } from "../common/Testing";
import { Battle } from "./Battle";
import { Fleet } from "./Fleet";
import { Ship } from "./Ship";
import { StarLocationType } from "./StarLocation";
import { Universe } from "./Universe";
testing("Fleet", test => {
test.case("get average level", check => { test.case("get average level", check => {
var fleet = new Fleet(); var fleet = new Fleet();
check.equals(fleet.getLevel(), 0); check.equals(fleet.getLevel(), 0);
@ -164,5 +171,4 @@ module TK.SpaceTac {
ship4.setDead(); ship4.setDead();
check.equals(fleet.isAlive(), false); check.equals(fleet.isAlive(), false);
}); });
}); });
}

View file

@ -1,8 +1,14 @@
module TK.SpaceTac { import { RObject, RObjectId } from "../common/RObject"
/** import { add, any, remove } from "../common/Tools"
import { Battle } from "./Battle"
import { Player } from "./Player"
import { Ship } from "./Ship"
import { StarLocation, StarLocationType } from "./StarLocation"
/**
* A fleet of ships, all belonging to the same player * A fleet of ships, all belonging to the same player
*/ */
export class Fleet extends RObject { export class Fleet extends RObject {
// Fleet owner // Fleet owner
player: Player player: Player
@ -147,5 +153,4 @@ module TK.SpaceTac {
return any(this.ships, ship => ship.alive); return any(this.ships, ship => ship.alive);
} }
} }
}
} }

View file

@ -1,6 +1,12 @@
module TK.SpaceTac { import { RandomGenerator } from "../common/RandomGenerator";
// Generator of balanced ships to form a fleet import { range } from "../common/Tools";
export class FleetGenerator { import { Fleet } from "./Fleet";
import { ShipModel } from "./models/ShipModel";
import { Player } from "./Player";
import { ShipGenerator } from "./ShipGenerator";
// Generator of balanced ships to form a fleet
export class FleetGenerator {
// Random generator to use // Random generator to use
random: RandomGenerator; random: RandomGenerator;
@ -25,5 +31,4 @@ module TK.SpaceTac {
return fleet; return fleet;
} }
}
} }

View file

@ -1,5 +1,12 @@
module TK.SpaceTac.Specs { import { SkewedRandomGenerator } from "../common/RandomGenerator";
testing("GameSession", test => { import { RObjectContainer } from "../common/RObject";
import { testing } from "../common/Testing";
import { nn } from "../common/Tools";
import { Fleet } from "./Fleet";
import { GameSession } from "./GameSession";
import { StarLocation, StarLocationType } from "./StarLocation";
testing("GameSession", test => {
/** /**
* Compare two sessions * Compare two sessions
*/ */
@ -144,5 +151,4 @@ module TK.SpaceTac.Specs {
check.equals(session.universe.stars.length, 50); check.equals(session.universe.stars.length, 50);
}); });
});*/ });*/
}); });
}

View file

@ -1,10 +1,21 @@
module TK.SpaceTac { import { NAMESPACE } from ".."
/** import { iforeach } from "../common/Iterators"
import { RandomGenerator } from "../common/RandomGenerator"
import { Serializer } from "../common/Serializer"
import { Battle } from "./Battle"
import { Fleet } from "./Fleet"
import { FleetGenerator } from "./FleetGenerator"
import { PersonalityReactions } from "./PersonalityReactions"
import { Player } from "./Player"
import { StarLocation } from "./StarLocation"
import { Universe } from "./Universe"
/**
* A game session, binding a universe and a player * A game session, binding a universe and a player
* *
* This represents the current state of game * This represents the current state of game
*/ */
export class GameSession { export class GameSession {
// "Hopefully" unique session id // "Hopefully" unique session id
id: string id: string
@ -55,13 +66,13 @@ module TK.SpaceTac {
// Load a game state from a string // Load a game state from a string
static loadFromString(serialized: string): GameSession { static loadFromString(serialized: string): GameSession {
var serializer = new Serializer(TK.SpaceTac); var serializer = new Serializer(NAMESPACE);
return <GameSession>serializer.unserialize(serialized); return <GameSession>serializer.unserialize(serialized);
} }
// Serializes the game state to a string // Serializes the game state to a string
saveToString(): string { saveToString(): string {
var serializer = new Serializer(TK.SpaceTac); var serializer = new Serializer(NAMESPACE);
return serializer.serialize(this); return serializer.serialize(this);
} }
@ -201,5 +212,4 @@ module TK.SpaceTac {
isIntroViewed(): boolean { isIntroViewed(): boolean {
return this.introduced; return this.introduced;
} }
}
} }

View file

@ -1,5 +1,24 @@
module TK.SpaceTac.Specs { import { iarray, imaterialize } from "../common/Iterators";
testing("MoveFireSimulator", test => { import { testing } from "../common/Testing";
import { nn } from "../common/Tools";
import { BaseAction } from "./actions/BaseAction";
import { MoveAction } from "./actions/MoveAction";
import { TriggerAction } from "./actions/TriggerAction";
import { ArenaLocationAngle } from "./ArenaLocation";
import { Battle } from "./Battle";
import { EndBattleDiff } from "./diffs/EndBattleDiff";
import { ProjectileFiredDiff } from "./diffs/ProjectileFiredDiff";
import { ShipActionUsedDiff } from "./diffs/ShipActionUsedDiff";
import { ShipDamageDiff } from "./diffs/ShipDamageDiff";
import { ShipDeathDiff } from "./diffs/ShipDeathDiff";
import { ShipMoveDiff } from "./diffs/ShipMoveDiff";
import { ShipValueDiff } from "./diffs/ShipValueDiff";
import { ApproachSimulationError, MoveFireSimulator } from "./MoveFireSimulator";
import { Ship } from "./Ship";
import { Target } from "./Target";
import { TestTools } from "./TestTools";
testing("MoveFireSimulator", test => {
function simpleWeaponCase(distance = 10, ship_ap = 5, weapon_ap = 3, engine_distance = 5): [Ship, MoveFireSimulator, BaseAction] { function simpleWeaponCase(distance = 10, ship_ap = 5, weapon_ap = 3, engine_distance = 5): [Ship, MoveFireSimulator, BaseAction] {
let ship = new Ship(); let ship = new Ship();
@ -203,5 +222,4 @@ module TK.SpaceTac.Specs {
check.equals(enemy.getValue("hull"), 2); check.equals(enemy.getValue("hull"), 2);
check.equals(enemy.getValue("hull"), 2); check.equals(enemy.getValue("hull"), 2);
}); });
}); });
}

View file

@ -1,26 +1,36 @@
module TK.SpaceTac { import { NAMESPACE } from ".."
/** import { ichainit, iforeach, imap, irepeat, istep } from "../common/Iterators"
import { cfilter, duplicate, first, minBy, nn } from "../common/Tools"
import { BaseAction } from "./actions/BaseAction"
import { MoveAction } from "./actions/MoveAction"
import { arenaDistance } from "./ArenaLocation"
import { Battle } from "./Battle"
import { BaseBattleDiff } from "./diffs/BaseBattleDiff"
import { Ship } from "./Ship"
import { Target } from "./Target"
/**
* Error codes for approach simulation * Error codes for approach simulation
*/ */
export enum ApproachSimulationError { export enum ApproachSimulationError {
NO_MOVE_NEEDED, NO_MOVE_NEEDED,
NO_VECTOR_FOUND, NO_VECTOR_FOUND,
} }
/** /**
* A single action in the sequence result from the simulator * A single action in the sequence result from the simulator
*/ */
export type MoveFirePart = { export type MoveFirePart = {
action: BaseAction action: BaseAction
target: Target target: Target
ap: number ap: number
possible: boolean possible: boolean
} }
/** /**
* A simulation result * A simulation result
*/ */
export class MoveFireResult { export class MoveFireResult {
// Simulation success, false only if no route can be found // Simulation success, false only if no route can be found
success = false success = false
// Ideal successive parts to make the full move+fire // Ideal successive parts to make the full move+fire
@ -38,14 +48,14 @@ module TK.SpaceTac {
can_fire = false can_fire = false
total_fire_ap = 0 total_fire_ap = 0
fire_location = new Target(0, 0, null) fire_location = new Target(0, 0, null)
}; };
/** /**
* Utility to simulate a move+fire action. * Utility to simulate a move+fire action.
* *
* This is also a helper to bring a ship in range to fire a weapon. * This is also a helper to bring a ship in range to fire a weapon.
*/ */
export class MoveFireSimulator { export class MoveFireSimulator {
ship: Ship; ship: Ship;
constructor(ship: Ship) { constructor(ship: Ship) {
@ -194,7 +204,7 @@ module TK.SpaceTac {
* The original battle passed as parameter will be duplicated, and not altered * The original battle passed as parameter will be duplicated, and not altered
*/ */
getExpectedDiffs(battle: Battle, simulation: MoveFireResult): BaseBattleDiff[] { getExpectedDiffs(battle: Battle, simulation: MoveFireResult): BaseBattleDiff[] {
let sim_battle = duplicate(battle, TK.SpaceTac); let sim_battle = duplicate(battle, NAMESPACE);
let sim_ship = nn(sim_battle.getShip(this.ship.id)); let sim_ship = nn(sim_battle.getShip(this.ship.id));
let results: BaseBattleDiff[] = []; let results: BaseBattleDiff[] = [];
simulation.parts.forEach(part => { simulation.parts.forEach(part => {
@ -207,5 +217,4 @@ module TK.SpaceTac {
}); });
return results; return results;
} }
}
} }

View file

@ -1,5 +1,8 @@
module TK.SpaceTac.Specs { import { SkewedRandomGenerator } from "../common/RandomGenerator";
testing("NameGenerator", test => { import { testing } from "../common/Testing";
import { NameGenerator } from "./NameGenerator";
testing("NameGenerator", test => {
test.case("generates unique names", check => { test.case("generates unique names", check => {
var random = new SkewedRandomGenerator([0.48, 0.9, 0.1]); var random = new SkewedRandomGenerator([0.48, 0.9, 0.1]);
var gen = new NameGenerator(["a", "b", "c"], random); var gen = new NameGenerator(["a", "b", "c"], random);
@ -9,5 +12,4 @@ module TK.SpaceTac.Specs {
check.equals(gen.getName(), "a"); check.equals(gen.getName(), "a");
check.equals(gen.getName(), null); check.equals(gen.getName(), null);
}); });
}); });
}

View file

@ -1,6 +1,8 @@
module TK.SpaceTac { import { RandomGenerator } from "../common/RandomGenerator";
// A unique name generator import { acopy } from "../common/Tools";
export class NameGenerator {
// A unique name generator
export class NameGenerator {
// List of available choices // List of available choices
private choices: string[]; private choices: string[];
@ -23,5 +25,4 @@ module TK.SpaceTac {
this.choices.splice(index, 1); this.choices.splice(index, 1);
return result; return result;
} }
}
} }

View file

@ -1,15 +1,14 @@
module TK.SpaceTac { /**
/**
* List of personality traits (may be used with "keyof"). * List of personality traits (may be used with "keyof").
*/ */
export interface IPersonalityTraits { export interface IPersonalityTraits {
aggressive: number aggressive: number
funny: number funny: number
heroic: number heroic: number
optimistic: number optimistic: number
} }
/** /**
* A personality is a set of traits that defines how a character thinks and behaves * A personality is a set of traits that defines how a character thinks and behaves
* *
* Each trait is a number between -1 and 1 * Each trait is a number between -1 and 1
@ -17,7 +16,7 @@ module TK.SpaceTac {
* In the game, a personality represents an artificial intelligence, and is transferable * In the game, a personality represents an artificial intelligence, and is transferable
* from one ship (body) to another. This is why a personality has a name * from one ship (body) to another. This is why a personality has a name
*/ */
export class Personality implements IPersonalityTraits { export class Personality implements IPersonalityTraits {
// Name of this personality // Name of this personality
name = "" name = ""
@ -32,5 +31,4 @@ module TK.SpaceTac {
// Optimistic 1 / Pessimistic -1 // Optimistic 1 / Pessimistic -1
optimistic = 0 optimistic = 0
}
} }

View file

@ -1,5 +1,11 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("PersonalityReactions", test => { import { Battle } from "./Battle";
import { ShipDamageDiff } from "./diffs/ShipDamageDiff";
import { BUILTIN_REACTION_POOL, PersonalityReaction, PersonalityReactionConversation, PersonalityReactions, ReactionPool } from "./PersonalityReactions";
import { Player } from "./Player";
import { Ship } from "./Ship";
testing("PersonalityReactions", test => {
function apply(pool: ReactionPool): PersonalityReaction | null { function apply(pool: ReactionPool): PersonalityReaction | null {
let reactions = new PersonalityReactions(); let reactions = new PersonalityReactions();
return reactions.check(new Player(), null, null, null, pool); return reactions.check(new Player(), null, null, null, pool);
@ -61,5 +67,4 @@ module TK.SpaceTac.Specs {
check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship2a, 50, 10)), [], "enemy shoot"); check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship2a, 50, 10)), [], "enemy shoot");
check.equals(condition(player, battle, ship2a, new ShipDamageDiff(ship2a, 50, 10)), [], "other player event"); check.equals(condition(player, battle, ship2a, new ShipDamageDiff(ship2a, 50, 10)), [], "other player event");
}) })
}) })
}

View file

@ -1,20 +1,28 @@
module TK.SpaceTac { import { RandomGenerator } from "../common/RandomGenerator"
// Reaction triggered import { difference, keys, nna } from "../common/Tools"
export type PersonalityReaction = PersonalityReactionConversation import { Battle } from "./Battle"
import { BaseBattleDiff } from "./diffs/BaseBattleDiff"
import { ShipDamageDiff } from "./diffs/ShipDamageDiff"
import { IPersonalityTraits } from "./Personality"
import { Player } from "./Player"
import { Ship } from "./Ship"
// Condition to check if a reaction may happen, returning involved ships (order is important) // Reaction triggered
export type ReactionCondition = (player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null) => Ship[] export type PersonalityReaction = PersonalityReactionConversation
// Reaction profile, giving a probability for types of personality, and an associated reaction constructor // Condition to check if a reaction may happen, returning involved ships (order is important)
export type ReactionProfile = [(traits: IPersonalityTraits) => number, (ships: Ship[]) => PersonalityReaction] export type ReactionCondition = (player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null) => Ship[]
// Reaction config (condition, chance, profiles) // Reaction profile, giving a probability for types of personality, and an associated reaction constructor
export type ReactionConfig = [ReactionCondition, number, ReactionProfile[]] export type ReactionProfile = [(traits: IPersonalityTraits) => number, (ships: Ship[]) => PersonalityReaction]
// Pool of reaction config // Reaction config (condition, chance, profiles)
export type ReactionPool = { [code: string]: ReactionConfig } export type ReactionConfig = [ReactionCondition, number, ReactionProfile[]]
/** // Pool of reaction config
export type ReactionPool = { [code: string]: ReactionConfig }
/**
* Reactions to external events according to personalities. * Reactions to external events according to personalities.
* *
* This allows for a more "alive" world, as characters tend to speak to react to events. * This allows for a more "alive" world, as characters tend to speak to react to events.
@ -22,7 +30,7 @@ module TK.SpaceTac {
* This object will store the previous reactions to avoid too much recurrence, and should be global to a whole * This object will store the previous reactions to avoid too much recurrence, and should be global to a whole
* game session. * game session.
*/ */
export class PersonalityReactions { export class PersonalityReactions {
done: string[] = [] done: string[] = []
random = RandomGenerator.global random = RandomGenerator.global
@ -64,34 +72,34 @@ module TK.SpaceTac {
return null; return null;
} }
} }
} }
/** /**
* One kind of personality reaction: saying something out loud * One kind of personality reaction: saying something out loud
*/ */
export class PersonalityReactionConversation { export class PersonalityReactionConversation {
messages: { interlocutor: Ship, message: string }[] messages: { interlocutor: Ship, message: string }[]
constructor(messages: { interlocutor: Ship, message: string }[]) { constructor(messages: { interlocutor: Ship, message: string }[]) {
this.messages = messages; this.messages = messages;
} }
} }
/** /**
* Standard reaction pool * Standard reaction pool
*/ */
export const BUILTIN_REACTION_POOL: ReactionPool = { export const BUILTIN_REACTION_POOL: ReactionPool = {
friendly_fire: [cond_friendly_fire, 1, [ friendly_fire: [cond_friendly_fire, 1, [
[traits => 1, ships => new PersonalityReactionConversation([ [traits => 1, ships => new PersonalityReactionConversation([
{ interlocutor: ships[0], message: "Hey !!! Watch where you're shooting !" }, { interlocutor: ships[0], message: "Hey !!! Watch where you're shooting !" },
{ interlocutor: ships[1], message: "Sorry mate..." }, { interlocutor: ships[1], message: "Sorry mate..." },
])] ])]
]] ]]
} }
/** /**
* Check for a friendly fire condition (one of player's ships fired on another) * Check for a friendly fire condition (one of player's ships fired on another)
*/ */
function cond_friendly_fire(player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null): Ship[] { function cond_friendly_fire(player: Player, battle: Battle | null, ship: Ship | null, event: BaseBattleDiff | null): Ship[] {
if (battle && ship && event) { if (battle && ship && event) {
if (event instanceof ShipDamageDiff && player.is(ship.fleet.player) && !ship.is(event.ship_id)) { if (event instanceof ShipDamageDiff && player.is(ship.fleet.player) && !ship.is(event.ship_id)) {
let hurt = battle.getShip(event.ship_id); let hurt = battle.getShip(event.ship_id);
@ -102,5 +110,4 @@ module TK.SpaceTac {
} else { } else {
return []; return [];
} }
}
} }

View file

@ -1,5 +1,9 @@
module TK.SpaceTac { import { testing } from "../common/Testing";
testing("Player", test => { import { Player } from "./Player";
import { StarLocationType } from "./StarLocation";
import { Universe } from "./Universe";
testing("Player", test => {
test.case("keeps track of visited locations", check => { test.case("keeps track of visited locations", check => {
let player = new Player(); let player = new Player();
let universe = new Universe(); let universe = new Universe();
@ -34,5 +38,4 @@ module TK.SpaceTac {
player.fleet.setLocation(loc2a); player.fleet.setLocation(loc2a);
checkVisited(true, true, true, true, true, false); checkVisited(true, true, true, true, true, false);
}); });
}); });
}

View file

@ -1,10 +1,17 @@
/// <reference path="../common/RObject.ts" /> import { RObject } from "../common/RObject";
import { contains, intersection } from "../common/Tools";
import { Battle } from "./Battle";
import { BattleCheats } from "./BattleCheats";
import { Fleet } from "./Fleet";
import { FleetGenerator } from "./FleetGenerator";
import { ActiveMissions } from "./missions/ActiveMissions";
import { Star } from "./Star";
import { StarLocation } from "./StarLocation";
module TK.SpaceTac { /**
/**
* One player (human or IA) * One player (human or IA)
*/ */
export class Player extends RObject { export class Player extends RObject {
// Player's name // Player's name
name: string name: string
@ -74,5 +81,4 @@ module TK.SpaceTac {
this.fleet.setBattle(battle); this.fleet.setBattle(battle);
this.missions.checkStatus(); this.missions.checkStatus();
} }
}
} }

View file

@ -1,5 +1,7 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("Range", test => { import { IntegerRange } from "./Range";
testing("Range", test => {
test.case("can work with proportional values", check => { test.case("can work with proportional values", check => {
var range = new Range(1, 5); var range = new Range(1, 5);
@ -19,9 +21,9 @@ module TK.SpaceTac.Specs {
check.equals(range.getReverseProportional(0), 0); check.equals(range.getReverseProportional(0), 0);
check.equals(range.getReverseProportional(6), 1); check.equals(range.getReverseProportional(6), 1);
}); });
}); });
testing("IntegerRange", test => { testing("IntegerRange", test => {
test.case("can work with proportional values", check => { test.case("can work with proportional values", check => {
var range = new IntegerRange(1, 5); var range = new IntegerRange(1, 5);
@ -41,5 +43,4 @@ module TK.SpaceTac.Specs {
check.equals(range.getReverseProportional(4), 0.6); check.equals(range.getReverseProportional(4), 0.6);
check.equals(range.getReverseProportional(5), 0.8); check.equals(range.getReverseProportional(5), 0.8);
}); });
}); });
}

View file

@ -1,6 +1,5 @@
module TK.SpaceTac { // Range of number values
// Range of number values export class Range {
export class Range {
// Minimal value // Minimal value
min = 0 min = 0
@ -48,17 +47,17 @@ module TK.SpaceTac {
isInRange(value: number): boolean { isInRange(value: number): boolean {
return value >= this.min && value <= this.max; return value >= this.min && value <= this.max;
} }
} }
// Range of integer values // Range of integer values
// //
// This differs from Range in that it adds space in proportional values to include the 'max'. // This differs from Range in that it adds space in proportional values to include the 'max'.
// Typically, using Range for integers will only yield 'max' for exactly 1.0 proportional, not for 0.999999. // Typically, using Range for integers will only yield 'max' for exactly 1.0 proportional, not for 0.999999.
// This fixes this behavior. // This fixes this behavior.
// //
// As this rounds values to integer, the 'reverse' proportional is no longer a bijection. // As this rounds values to integer, the 'reverse' proportional is no longer a bijection.
export class IntegerRange extends Range { export class IntegerRange extends Range {
getProportional(cursor: number): number { getProportional(cursor: number): number {
if (cursor <= 0.0) { if (cursor <= 0.0) {
return this.min; return this.min;
@ -78,5 +77,4 @@ module TK.SpaceTac {
return (expected - this.min) * 1.0 / (this.max - this.min + 1); return (expected - this.min) * 1.0 / (this.max - this.min + 1);
} }
} }
}
} }

View file

@ -1,5 +1,21 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("Ship", test => { import { nn } from "../common/Tools";
import { BaseAction } from "./actions/BaseAction";
import { ToggleAction } from "./actions/ToggleAction";
import { Battle } from "./Battle";
import { ShipActionToggleDiff } from "./diffs/ShipActionToggleDiff";
import { ShipAttributeDiff } from "./diffs/ShipAttributeDiff";
import { ShipDeathDiff } from "./diffs/ShipDeathDiff";
import { ShipEffectRemovedDiff } from "./diffs/ShipEffectAddedDiff";
import { ShipValueDiff } from "./diffs/ShipValueDiff";
import { AttributeEffect } from "./effects/AttributeEffect";
import { AttributeLimitEffect } from "./effects/AttributeLimitEffect";
import { StickyEffect } from "./effects/StickyEffect";
import { ShipModel } from "./models/ShipModel";
import { Ship } from "./Ship";
import { TestTools } from "./TestTools";
testing("Ship", test => {
test.case("creates a full name", check => { test.case("creates a full name", check => {
let ship = new Ship(); let ship = new Ship();
check.equals(ship.getName(false), "Ship"); check.equals(ship.getName(false), "Ship");
@ -191,5 +207,4 @@ module TK.SpaceTac.Specs {
new ShipDeathDiff(battle, ship), new ShipDeathDiff(battle, ship),
]); ]);
}); });
}); });
}

View file

@ -1,10 +1,32 @@
/// <reference path="../common/RObject.ts" /> import { RandomGenerator } from "../common/RandomGenerator"
import { RObject, RObjectContainer } from "../common/RObject"
import { bool, cfilter, flatten, keys, sum } from "../common/Tools"
import { ActionList } from "./actions/ActionList"
import { BaseAction } from "./actions/BaseAction"
import { ToggleAction } from "./actions/ToggleAction"
import { ArenaLocationAngle } from "./ArenaLocation"
import { Battle } from "./Battle"
import { BaseBattleDiff } from "./diffs/BaseBattleDiff"
import { ShipDeathDiff } from "./diffs/ShipDeathDiff"
import { ShipEffectRemovedDiff } from "./diffs/ShipEffectAddedDiff"
import { ShipValueDiff } from "./diffs/ShipValueDiff"
import { AttributeEffect } from "./effects/AttributeEffect"
import { AttributeLimitEffect } from "./effects/AttributeLimitEffect"
import { AttributeMultiplyEffect } from "./effects/AttributeMultiplyEffect"
import { BaseEffect } from "./effects/BaseEffect"
import { StickyEffect } from "./effects/StickyEffect"
import { Fleet } from "./Fleet"
import { ShipModel, ShipUpgrade } from "./models/ShipModel"
import { Personality } from "./Personality"
import { Player } from "./Player"
import { ShipLevel } from "./ShipLevel"
import { ShipAttributes, ShipValues, SHIP_VALUES, SHIP_VALUES_DESCRIPTIONS } from "./ShipValue"
import { Target } from "./Target"
module TK.SpaceTac { /**
/**
* A single ship in a fleet * A single ship in a fleet
*/ */
export class Ship extends RObject { export class Ship extends RObject {
// Ship model // Ship model
model: ShipModel model: ShipModel
@ -437,5 +459,4 @@ module TK.SpaceTac {
let sources = diffs.concat(limits).join("\n"); let sources = diffs.concat(limits).join("\n");
return sources ? (result + "\n\n" + sources) : result; return sources ? (result + "\n\n" + sources) : result;
} }
}
} }

View file

@ -1,5 +1,8 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("ShipGenerator", test => { import { ShipModel } from "./models/ShipModel";
import { ShipGenerator } from "./ShipGenerator";
testing("ShipGenerator", test => {
test.case("can use ship model", check => { test.case("can use ship model", check => {
var gen = new ShipGenerator(); var gen = new ShipGenerator();
var model = new ShipModel("test", "Test"); var model = new ShipModel("test", "Test");
@ -7,5 +10,4 @@ module TK.SpaceTac.Specs {
check.same(ship.model, model); check.same(ship.model, model);
check.same(ship.level.get(), 3); check.same(ship.level.get(), 3);
}); });
}); });
}

View file

@ -1,8 +1,11 @@
module TK.SpaceTac { import { RandomGenerator } from "../common/RandomGenerator";
/** import { ShipModel } from "./models/ShipModel";
import { Ship } from "./Ship";
/**
* Generator of random ship * Generator of random ship
*/ */
export class ShipGenerator { export class ShipGenerator {
// Random number generator used // Random number generator used
random: RandomGenerator random: RandomGenerator
@ -45,5 +48,4 @@ module TK.SpaceTac {
return result; return result;
} }
}
} }

View file

@ -1,5 +1,7 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("ShipLevel", test => { import { ShipLevel } from "./ShipLevel";
testing("ShipLevel", test => {
test.case("level up from experience points", check => { test.case("level up from experience points", check => {
let level = new ShipLevel(); let level = new ShipLevel();
check.equals(level.get(), 1); check.equals(level.get(), 1);
@ -66,5 +68,4 @@ module TK.SpaceTac.Specs {
level.clearUpgrades(); level.clearUpgrades();
check.equals(level.getUpgrades(), []); check.equals(level.getUpgrades(), []);
}); });
}); });
}

View file

@ -1,8 +1,11 @@
module TK.SpaceTac { import { imap, irange, isum } from "../common/Iterators";
/** import { acopy, add, contains, remove } from "../common/Tools";
import { ShipUpgrade } from "./models/ShipModel";
/**
* Level and experience system for a ship, with enabled upgrades. * Level and experience system for a ship, with enabled upgrades.
*/ */
export class ShipLevel { export class ShipLevel {
private level = 1 private level = 1
private experience = 0 private experience = 0
private upgrades: string[] = [] private upgrades: string[] = []
@ -115,5 +118,4 @@ module TK.SpaceTac {
clearUpgrades(): void { clearUpgrades(): void {
this.upgrades = []; this.upgrades = [];
} }
}
} }

View file

@ -1,5 +1,7 @@
module TK.SpaceTac { import { testing } from "../common/Testing";
testing("ShipAttribute", test => { import { ShipAttribute } from "./ShipValue";
testing("ShipAttribute", test => {
test.case("applies cumulative, multiplier and limit", check => { test.case("applies cumulative, multiplier and limit", check => {
let attribute = new ShipAttribute(); let attribute = new ShipAttribute();
check.equals(attribute.get(), 0, "initial"); check.equals(attribute.get(), 0, "initial");
@ -43,5 +45,4 @@ module TK.SpaceTac {
check.equals(attribute.getMaximal(), 4, "maximal value"); check.equals(attribute.getMaximal(), 4, "maximal value");
}); });
}); });
}); });
}

View file

@ -1,9 +1,10 @@
module TK.SpaceTac { import { min, remove, sum } from "../common/Tools"
type ShipValuesMapping = {
[P in (keyof ShipValues | keyof ShipAttributes)]: string
}
export const SHIP_VALUES_DESCRIPTIONS: ShipValuesMapping = { type ShipValuesMapping = {
[P in (keyof ShipValues | keyof ShipAttributes)]: string
}
export const SHIP_VALUES_DESCRIPTIONS: ShipValuesMapping = {
"initiative": "Capacity to play before others in a battle", "initiative": "Capacity to play before others in a battle",
"hull": "Physical structure of the ship", "hull": "Physical structure of the ship",
"shield": "Shield around the ship that may absorb damage", "shield": "Shield around the ship that may absorb damage",
@ -12,9 +13,9 @@ module TK.SpaceTac {
"shield_capacity": "Maximal Shield value to protect the hull from damage", "shield_capacity": "Maximal Shield value to protect the hull from damage",
"power_capacity": "Maximal Power value to use equipment", "power_capacity": "Maximal Power value to use equipment",
"evasion": "Damage points that may be evaded by maneuvering", "evasion": "Damage points that may be evaded by maneuvering",
} }
export const SHIP_VALUES_NAMES: ShipValuesMapping = { export const SHIP_VALUES_NAMES: ShipValuesMapping = {
"initiative": "initiative", "initiative": "initiative",
"hull": "hull", "hull": "hull",
"shield": "shield", "shield": "shield",
@ -23,12 +24,12 @@ module TK.SpaceTac {
"shield_capacity": "shield capacity", "shield_capacity": "shield capacity",
"power_capacity": "power capacity", "power_capacity": "power capacity",
"evasion": "evasion", "evasion": "evasion",
} }
/** /**
* A ship attribute is a number resulting of a list of modifiers. * A ship attribute is a number resulting of a list of modifiers.
*/ */
export class ShipAttribute { export class ShipAttribute {
// Current value // Current value
private current = 0 private current = 0
@ -110,12 +111,12 @@ module TK.SpaceTac {
} }
this.current = value; this.current = value;
} }
} }
/** /**
* Set of ShipAttribute for a ship * Set of ShipAttribute for a ship
*/ */
export class ShipAttributes { export class ShipAttributes {
// Initiative (capacity to play first) // Initiative (capacity to play first)
initiative = new ShipAttribute() initiative = new ShipAttribute()
// Maximal hull value // Maximal hull value
@ -126,30 +127,29 @@ module TK.SpaceTac {
evasion = new ShipAttribute() evasion = new ShipAttribute()
// Maximal power value // Maximal power value
power_capacity = new ShipAttribute() power_capacity = new ShipAttribute()
} }
/** /**
* Set of simple values for a ship * Set of simple values for a ship
*/ */
export class ShipValues { export class ShipValues {
hull = 0 hull = 0
shield = 0 shield = 0
power = 0 power = 0
} }
/** /**
* Static attributes and values object for property queries * Static attributes and values object for property queries
*/ */
export const SHIP_ATTRIBUTES = new ShipAttributes(); export const SHIP_ATTRIBUTES = new ShipAttributes();
export const SHIP_VALUES = new ShipValues(); export const SHIP_VALUES = new ShipValues();
/** /**
* Type guards * Type guards
*/ */
export function isShipValue(key: string): key is keyof ShipValues { export function isShipValue(key: string): key is keyof ShipValues {
return SHIP_VALUES.hasOwnProperty(key); return SHIP_VALUES.hasOwnProperty(key);
} }
export function isShipAttribute(key: string): key is keyof ShipAttributes { export function isShipAttribute(key: string): key is keyof ShipAttributes {
return SHIP_ATTRIBUTES.hasOwnProperty(key); return SHIP_ATTRIBUTES.hasOwnProperty(key);
}
} }

View file

@ -1,5 +1,11 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("Shop", test => { import { Mission } from "./missions/Mission";
import { Player } from "./Player";
import { Shop } from "./Shop";
import { StarLocation } from "./StarLocation";
import { Universe } from "./Universe";
testing("Shop", test => {
test.case("generates secondary missions", check => { test.case("generates secondary missions", check => {
let universe = new Universe(); let universe = new Universe();
universe.generate(4); universe.generate(4);
@ -35,5 +41,4 @@ module TK.SpaceTac.Specs {
check.equals(player.missions.secondary, [mission]); check.equals(player.missions.secondary, [mission]);
check.same(mission.fleet, player.fleet); check.same(mission.fleet, player.fleet);
}); });
}); });
}

View file

@ -1,8 +1,14 @@
module TK.SpaceTac { import { RandomGenerator } from "../common/RandomGenerator";
/** import { contains, remove } from "../common/Tools";
import { Mission } from "./missions/Mission";
import { MissionGenerator } from "./missions/MissionGenerator";
import { Player } from "./Player";
import { StarLocation } from "./StarLocation";
/**
* A shop is a place to buy/sell equipments * A shop is a place to buy/sell equipments
*/ */
export class Shop { export class Shop {
// Average level of equipment // Average level of equipment
private level: number private level: number
@ -47,5 +53,4 @@ module TK.SpaceTac {
return false; return false;
} }
} }
}
} }

View file

@ -1,5 +1,9 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("Star", test => { import { Star } from "./Star";
import { StarLink } from "./StarLink";
import { Universe } from "./Universe";
testing("Star", test => {
test.case("lists links to other stars", check => { test.case("lists links to other stars", check => {
var universe = new Universe(); var universe = new Universe();
universe.stars.push(new Star(universe, 0, 0, "Star A")); universe.stars.push(new Star(universe, 0, 0, "Star A"));
@ -32,5 +36,4 @@ module TK.SpaceTac.Specs {
check.contains(neighbors, universe.stars[1]); check.contains(neighbors, universe.stars[1]);
check.contains(neighbors, universe.stars[3]); check.contains(neighbors, universe.stars[3]);
}); });
}); });
}

View file

@ -1,6 +1,11 @@
module TK.SpaceTac { import { RandomGenerator } from "../common/RandomGenerator";
// A star system import { nna } from "../common/Tools";
export class Star { import { StarLink } from "./StarLink";
import { StarLocation, StarLocationType } from "./StarLocation";
import { Universe } from "./Universe";
// A star system
export class Star {
// Available names for star systems // Available names for star systems
static NAMES_POOL = [ static NAMES_POOL = [
@ -194,5 +199,4 @@ module TK.SpaceTac {
return result; return result;
} }
}
} }

View file

@ -1,5 +1,8 @@
module TK.SpaceTac.Specs { import { testing } from "../common/Testing";
testing("StarLink", test => { import { Star } from "./Star";
import { StarLink } from "./StarLink";
testing("StarLink", test => {
test.case("checks link intersection", check => { test.case("checks link intersection", check => {
var star1 = new Star(null, 0, 0); var star1 = new Star(null, 0, 0);
var star2 = new Star(null, 0, 1); var star2 = new Star(null, 0, 1);
@ -34,5 +37,4 @@ module TK.SpaceTac.Specs {
check.same(link1.getPeer(star2), star1); check.same(link1.getPeer(star2), star1);
check.equals(link1.getPeer(star3), null); check.equals(link1.getPeer(star3), null);
}); });
}); });
}

View file

@ -1,6 +1,7 @@
module TK.SpaceTac { import { Star } from "./Star";
// An hyperspace link between two star systems
export class StarLink { // An hyperspace link between two star systems
export class StarLink {
// Stars // Stars
first: Star; first: Star;
second: Star; second: Star;
@ -45,5 +46,4 @@ module TK.SpaceTac {
return null; return null;
} }
} }
}
} }

View file

@ -1,5 +1,10 @@
module TK.SpaceTac.Specs { import { SkewedRandomGenerator } from "../common/RandomGenerator";
testing("StarLocation", test => { import { testing } from "../common/Testing";
import { nn } from "../common/Tools";
import { Fleet } from "./Fleet";
import { StarLocation, StarLocationType } from "./StarLocation";
testing("StarLocation", test => {
test.case("removes generated encounters that lose", check => { test.case("removes generated encounters that lose", check => {
var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0); var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0);
var fleet = new Fleet(); var fleet = new Fleet();
@ -33,5 +38,4 @@ module TK.SpaceTac.Specs {
location.resolveEncounter(nn(battle.outcome)); location.resolveEncounter(nn(battle.outcome));
check.notequals(location.encounter, null); check.notequals(location.encounter, null);
}); });
}); });
}

View file

@ -1,18 +1,27 @@
/// <reference path="../common/RObject.ts" /> import { RandomGenerator } from "../common/RandomGenerator"
import { RObject } from "../common/RObject"
import { add, remove } from "../common/Tools"
import { Battle } from "./Battle"
import { BattleOutcome } from "./BattleOutcome"
import { Fleet } from "./Fleet"
import { FleetGenerator } from "./FleetGenerator"
import { Player } from "./Player"
import { Shop } from "./Shop"
import { Star } from "./Star"
import { Universe } from "./Universe"
module TK.SpaceTac { export enum StarLocationType {
export enum StarLocationType {
STAR, STAR,
WARP, WARP,
PLANET, PLANET,
ASTEROID, ASTEROID,
STATION STATION
} }
/** /**
* Point of interest in a star system * Point of interest in a star system
*/ */
export class StarLocation extends RObject { export class StarLocation extends RObject {
// Parent star system // Parent star system
star: Star star: Star
@ -177,5 +186,4 @@ module TK.SpaceTac {
this.clearEncounter(); this.clearEncounter();
} }
} }
}
} }

Some files were not shown because too many files have changed in this diff Show more