WIP
10
.editorconfig
Normal 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
|
@ -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
|
@ -0,0 +1,11 @@
|
||||||
|
image: node:latest
|
||||||
|
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- node_modules/
|
||||||
|
|
||||||
|
test:
|
||||||
|
before_script:
|
||||||
|
- npm install
|
||||||
|
script:
|
||||||
|
- npm test
|
|
@ -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
BIN
graphics/ships/avenger.blend1
Normal file
BIN
graphics/ships/whirlwind.blend1
Normal file
BIN
graphics/title.blend1
Normal file
27
jest.config.js
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
Before Width: | Height: | Size: 8 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 8 KiB |
Before Width: | Height: | Size: 3.2 KiB |
BIN
out/favicon.ico
Before Width: | Height: | Size: 7 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 59 KiB |
|
@ -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>
|
|
BIN
out/play.png
Before Width: | Height: | Size: 6.8 KiB |
68
out/sim.js
|
@ -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));
|
|
|
@ -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>
|
|
7675
package-lock.json
generated
49
package.json
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,42 @@
|
||||||
/// <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];
|
}
|
||||||
}
|
setItem(name: string, value: string) {
|
||||||
setItem(name: string, value: string) {
|
this.data[name] = value;
|
||||||
this.data[name] = value;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testing("MainUI", test => {
|
|
||||||
let testgame = setupEmptyView(test);
|
|
||||||
|
|
||||||
test.case("saves games in local browser storage", check => {
|
|
||||||
let ui = testgame.ui;
|
|
||||||
ui.storage = <any>new FakeStorage();
|
|
||||||
|
|
||||||
let result = ui.loadGame("spacetac-test-save");
|
|
||||||
check.equals(result, false);
|
|
||||||
|
|
||||||
ui.session.startNewGame();
|
|
||||||
let systems = ui.session.universe.stars.length;
|
|
||||||
let links = ui.session.universe.starlinks.length;
|
|
||||||
|
|
||||||
result = ui.saveGame("spacetac-test-save");
|
|
||||||
check.equals(result, true);
|
|
||||||
check.equals(bool(ui.storage.getItem("spacetac-test-save")), true);
|
|
||||||
|
|
||||||
ui.session = new GameSession();
|
|
||||||
check.notsame(ui.session.universe.stars.length, systems);
|
|
||||||
|
|
||||||
result = ui.loadGame("spacetac-test-save");
|
|
||||||
check.equals(result, true);
|
|
||||||
check.same(ui.session.universe.stars.length, systems);
|
|
||||||
check.same(ui.session.universe.starlinks.length, links);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testing("MainUI", test => {
|
||||||
|
let testgame = setupEmptyView(test);
|
||||||
|
|
||||||
|
test.case("saves games in local browser storage", check => {
|
||||||
|
let ui = testgame.ui;
|
||||||
|
ui.storage = <any>new FakeStorage();
|
||||||
|
|
||||||
|
let result = ui.loadGame("spacetac-test-save");
|
||||||
|
check.equals(result, false);
|
||||||
|
|
||||||
|
ui.session.startNewGame();
|
||||||
|
let systems = ui.session.universe.stars.length;
|
||||||
|
let links = ui.session.universe.starlinks.length;
|
||||||
|
|
||||||
|
result = ui.saveGame("spacetac-test-save");
|
||||||
|
check.equals(result, true);
|
||||||
|
check.equals(bool(ui.storage.getItem("spacetac-test-save")), true);
|
||||||
|
|
||||||
|
ui.session = new GameSession();
|
||||||
|
check.notsame(ui.session.universe.stars.length, systems);
|
||||||
|
|
||||||
|
result = ui.loadGame("spacetac-test-save");
|
||||||
|
check.equals(result, true);
|
||||||
|
check.same(ui.session.universe.stars.length, systems);
|
||||||
|
check.same(ui.session.universe.starlinks.length, links);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
413
src/MainUI.ts
|
@ -1,217 +1,208 @@
|
||||||
/// <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
|
* Main class to bootstrap the whole game
|
||||||
(<any>window).describe = (<any>window).describe || function () { };
|
*/
|
||||||
} else {
|
export class MainUI extends Phaser.Game {
|
||||||
if (typeof global != "undefined") {
|
// Current game session
|
||||||
// In node, does not extend Phaser classes
|
session: GameSession
|
||||||
var handler = {
|
session_token: string | null
|
||||||
get(target: any, name: any): any {
|
|
||||||
return new Proxy(function () { }, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
global.Phaser = new Proxy(function () { }, handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof module != "undefined") {
|
// Audio manager
|
||||||
module.exports = { TK };
|
audio = new AudioManager(this)
|
||||||
}
|
|
||||||
}
|
// Game options
|
||||||
|
options = new GameOptions(this)
|
||||||
module TK.SpaceTac {
|
|
||||||
/**
|
// Storage used
|
||||||
* Main class to bootstrap the whole game
|
storage: Storage
|
||||||
*/
|
|
||||||
export class MainUI extends Phaser.Game {
|
// Debug mode
|
||||||
// Current game session
|
debug = false
|
||||||
session: GameSession
|
|
||||||
session_token: string | null
|
// Current scaling
|
||||||
|
scaling = 1
|
||||||
// Audio manager
|
|
||||||
audio = new UI.Audio(this)
|
constructor(private testmode = false) {
|
||||||
|
super({
|
||||||
// Game options
|
width: 1920,
|
||||||
options = new UI.GameOptions(this)
|
height: 1080,
|
||||||
|
type: testmode ? Phaser.CANVAS : Phaser.WEBGL, // cannot really use HEADLESS because of bugs
|
||||||
// Storage used
|
backgroundColor: '#000000',
|
||||||
storage: Storage
|
parent: '-space-tac',
|
||||||
|
disableContextMenu: true,
|
||||||
// Debug mode
|
scale: {
|
||||||
debug = false
|
mode: Phaser.Scale.FIT,
|
||||||
|
autoCenter: Phaser.Scale.CENTER_BOTH
|
||||||
// Current scaling
|
},
|
||||||
scaling = 1
|
});
|
||||||
|
|
||||||
constructor(private testmode = false) {
|
this.storage = localStorage;
|
||||||
super({
|
|
||||||
width: 1920,
|
this.session = new GameSession();
|
||||||
height: 1080,
|
this.session_token = null;
|
||||||
type: testmode ? Phaser.CANVAS : Phaser.WEBGL, // cannot really use HEADLESS because of bugs
|
|
||||||
backgroundColor: '#000000',
|
if (!testmode) {
|
||||||
parent: '-space-tac',
|
this.events.on("blur", () => {
|
||||||
disableContextMenu: true,
|
this.scene.scenes.forEach(scene => this.scene.pause(scene));
|
||||||
scale: {
|
});
|
||||||
mode: Phaser.Scale.FIT,
|
this.events.on("focus", () => {
|
||||||
autoCenter: Phaser.Scale.CENTER_BOTH
|
this.scene.scenes.forEach(scene => this.scene.resume(scene));
|
||||||
},
|
});
|
||||||
});
|
|
||||||
|
this.scene.add('boot', Boot);
|
||||||
this.storage = localStorage;
|
this.scene.add('loading', AssetLoading);
|
||||||
|
this.scene.add('mainmenu', MainMenu);
|
||||||
this.session = new GameSession();
|
this.scene.add('router', Router);
|
||||||
this.session_token = null;
|
this.scene.add('battle', BattleView);
|
||||||
|
this.scene.add('intro', IntroView);
|
||||||
if (!testmode) {
|
this.scene.add('creation', FleetCreationView);
|
||||||
this.events.on("blur", () => {
|
this.scene.add('universe', UniverseMapView);
|
||||||
this.scene.scenes.forEach(scene => this.scene.pause(scene));
|
|
||||||
});
|
this.goToScene('boot');
|
||||||
this.events.on("focus", () => {
|
|
||||||
this.scene.scenes.forEach(scene => this.scene.resume(scene));
|
|
||||||
});
|
|
||||||
|
|
||||||
this.scene.add('boot', UI.Boot);
|
|
||||||
this.scene.add('loading', UI.AssetLoading);
|
|
||||||
this.scene.add('mainmenu', UI.MainMenu);
|
|
||||||
this.scene.add('router', UI.Router);
|
|
||||||
this.scene.add('battle', UI.BattleView);
|
|
||||||
this.scene.add('intro', UI.IntroView);
|
|
||||||
this.scene.add('creation', UI.FleetCreationView);
|
|
||||||
this.scene.add('universe', UI.UniverseMapView);
|
|
||||||
|
|
||||||
this.goToScene('boot');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get isTesting(): boolean {
|
|
||||||
return this.testmode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the game session
|
|
||||||
*/
|
|
||||||
resetSession(): void {
|
|
||||||
this.session = new GameSession();
|
|
||||||
this.session_token = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a popup message in current view
|
|
||||||
*/
|
|
||||||
displayMessage(message: string) {
|
|
||||||
iteritems(<any>this.scene.keys, (key: string, scene: UI.BaseView) => {
|
|
||||||
if (scene.messages && this.scene.isVisible(key)) {
|
|
||||||
scene.messages.addMessage(message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the active scene
|
|
||||||
*/
|
|
||||||
goToScene(name: string): void {
|
|
||||||
keys(this.scene.keys).forEach(key => {
|
|
||||||
if (this.scene.isActive(key) || this.scene.isVisible(key)) {
|
|
||||||
this.scene.stop(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.scene.start(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Quit the current session, and go back to mainmenu
|
|
||||||
*/
|
|
||||||
quitGame() {
|
|
||||||
this.resetSession();
|
|
||||||
this.goToScene('router');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save current game in local browser storage
|
|
||||||
*/
|
|
||||||
saveGame(name = "spacetac-savegame"): boolean {
|
|
||||||
if (typeof this.storage != "undefined") {
|
|
||||||
this.storage.setItem(name, this.session.saveToString());
|
|
||||||
this.displayMessage("Game saved");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
this.displayMessage("Your browser does not support saving");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current game session, and redirect to view router
|
|
||||||
*/
|
|
||||||
setSession(session: GameSession, token?: string): void {
|
|
||||||
this.session = session;
|
|
||||||
this.session_token = token || null;
|
|
||||||
this.goToScene("router");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load current game from local browser storage
|
|
||||||
*/
|
|
||||||
loadGame(name = "spacetac-savegame"): boolean {
|
|
||||||
if (typeof this.storage != "undefined") {
|
|
||||||
var loaded = this.storage.getItem(name);
|
|
||||||
if (loaded) {
|
|
||||||
this.session = GameSession.loadFromString(loaded);
|
|
||||||
this.session_token = null;
|
|
||||||
console.log("Game loaded");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.warn("No saved game found");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("localStorage not available");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an hopefully unique device identifier
|
|
||||||
*/
|
|
||||||
getDeviceId(): string | null {
|
|
||||||
if (this.storage) {
|
|
||||||
const key = "spacetac-device-id";
|
|
||||||
let stored = this.storage.getItem(key);
|
|
||||||
if (stored) {
|
|
||||||
return stored;
|
|
||||||
} else {
|
|
||||||
let generated = RandomGenerator.global.id(20);
|
|
||||||
this.storage.setItem(key, generated);
|
|
||||||
return generated;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the game is currently fullscreen
|
|
||||||
*/
|
|
||||||
isFullscreen(): boolean {
|
|
||||||
return this.scale.isFullscreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggle fullscreen mode.
|
|
||||||
*
|
|
||||||
* Returns true if the result is fullscreen
|
|
||||||
*/
|
|
||||||
toggleFullscreen(active: boolean | null = null): boolean {
|
|
||||||
if (active === false || (active !== true && this.isFullscreen())) {
|
|
||||||
this.scale.stopFullscreen();
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
this.scale.startFullscreen();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isTesting(): boolean {
|
||||||
|
return this.testmode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the game session
|
||||||
|
*/
|
||||||
|
resetSession(): void {
|
||||||
|
this.session = new GameSession();
|
||||||
|
this.session_token = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a popup message in current view
|
||||||
|
*/
|
||||||
|
displayMessage(message: string) {
|
||||||
|
iteritems(<any>this.scene.keys, (key: string, scene: BaseView) => {
|
||||||
|
if (scene.messages && this.scene.isVisible(key)) {
|
||||||
|
scene.messages.addMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the active scene
|
||||||
|
*/
|
||||||
|
goToScene(name: string): void {
|
||||||
|
keys(this.scene.keys).forEach(key => {
|
||||||
|
if (this.scene.isActive(key) || this.scene.isVisible(key)) {
|
||||||
|
this.scene.stop(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.scene.start(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quit the current session, and go back to mainmenu
|
||||||
|
*/
|
||||||
|
quitGame() {
|
||||||
|
this.resetSession();
|
||||||
|
this.goToScene('router');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current game in local browser storage
|
||||||
|
*/
|
||||||
|
saveGame(name = "spacetac-savegame"): boolean {
|
||||||
|
if (typeof this.storage != "undefined") {
|
||||||
|
this.storage.setItem(name, this.session.saveToString());
|
||||||
|
this.displayMessage("Game saved");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this.displayMessage("Your browser does not support saving");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current game session, and redirect to view router
|
||||||
|
*/
|
||||||
|
setSession(session: GameSession, token?: string): void {
|
||||||
|
this.session = session;
|
||||||
|
this.session_token = token || null;
|
||||||
|
this.goToScene("router");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load current game from local browser storage
|
||||||
|
*/
|
||||||
|
loadGame(name = "spacetac-savegame"): boolean {
|
||||||
|
if (typeof this.storage != "undefined") {
|
||||||
|
var loaded = this.storage.getItem(name);
|
||||||
|
if (loaded) {
|
||||||
|
this.session = GameSession.loadFromString(loaded);
|
||||||
|
this.session_token = null;
|
||||||
|
console.log("Game loaded");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.warn("No saved game found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("localStorage not available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an hopefully unique device identifier
|
||||||
|
*/
|
||||||
|
getDeviceId(): string | null {
|
||||||
|
if (this.storage) {
|
||||||
|
const key = "spacetac-device-id";
|
||||||
|
let stored = this.storage.getItem(key);
|
||||||
|
if (stored) {
|
||||||
|
return stored;
|
||||||
|
} else {
|
||||||
|
let generated = RandomGenerator.global.id(20);
|
||||||
|
this.storage.setItem(key, generated);
|
||||||
|
return generated;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the game is currently fullscreen
|
||||||
|
*/
|
||||||
|
isFullscreen(): boolean {
|
||||||
|
return this.scale.isFullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle fullscreen mode.
|
||||||
|
*
|
||||||
|
* Returns true if the result is fullscreen
|
||||||
|
*/
|
||||||
|
toggleFullscreen(active: boolean | null = null): boolean {
|
||||||
|
if (active === false || (active !== true && this.isFullscreen())) {
|
||||||
|
this.scale.stopFullscreen();
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.scale.startFullscreen();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,230 +1,228 @@
|
||||||
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();
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
apply(state: TestState) {
|
apply(state: TestState) {
|
||||||
state.counter += this.value;
|
state.counter += this.value;
|
||||||
}
|
}
|
||||||
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);
|
||||||
check.equals(log.get(0), null);
|
check.equals(log.get(0), null);
|
||||||
check.equals(log.get(1), null);
|
check.equals(log.get(1), null);
|
||||||
check.equals(log.get(2), null);
|
check.equals(log.get(2), null);
|
||||||
|
|
||||||
log.add(new TestDiff(2));
|
log.add(new TestDiff(2));
|
||||||
check.equals(log.count(), 1);
|
check.equals(log.count(), 1);
|
||||||
check.equals(log.get(0), new TestDiff(2));
|
check.equals(log.get(0), new TestDiff(2));
|
||||||
check.equals(log.get(1), null);
|
check.equals(log.get(1), null);
|
||||||
check.equals(log.get(2), null);
|
check.equals(log.get(2), null);
|
||||||
|
|
||||||
log.add(new TestDiff(-4));
|
log.add(new TestDiff(-4));
|
||||||
check.equals(log.count(), 2);
|
check.equals(log.count(), 2);
|
||||||
check.equals(log.get(0), new TestDiff(2));
|
check.equals(log.get(0), new TestDiff(2));
|
||||||
check.equals(log.get(1), new TestDiff(-4));
|
check.equals(log.get(1), new TestDiff(-4));
|
||||||
check.equals(log.get(2), null);
|
check.equals(log.get(2), null);
|
||||||
|
|
||||||
log.clear(1);
|
log.clear(1);
|
||||||
check.equals(log.count(), 1);
|
check.equals(log.count(), 1);
|
||||||
check.equals(log.get(0), new TestDiff(2));
|
check.equals(log.get(0), new TestDiff(2));
|
||||||
|
|
||||||
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();
|
||||||
let client = new DiffLogClient(state, log);
|
let client = new DiffLogClient(state, log);
|
||||||
|
|
||||||
check.equals(client.atEnd(), true, "client is empty, should be at end");
|
check.equals(client.atEnd(), true, "client is empty, should be at end");
|
||||||
check.equals(log.count(), 0, "log is empty initially");
|
check.equals(log.count(), 0, "log is empty initially");
|
||||||
check.equals(state.counter, 0, "initial state is 0");
|
check.equals(state.counter, 0, "initial state is 0");
|
||||||
|
|
||||||
client.add(new TestDiff(3));
|
client.add(new TestDiff(3));
|
||||||
check.equals(client.atEnd(), true, "client still at end");
|
check.equals(client.atEnd(), true, "client still at end");
|
||||||
check.equals(log.count(), 1, "diff added to log");
|
check.equals(log.count(), 1, "diff added to log");
|
||||||
check.equals(state.counter, 3, "diff applied to state");
|
check.equals(state.counter, 3, "diff applied to state");
|
||||||
|
|
||||||
client.add(new TestDiff(2), false);
|
client.add(new TestDiff(2), false);
|
||||||
check.equals(client.atEnd(), false, "client lapsing behind");
|
check.equals(client.atEnd(), false, "client lapsing behind");
|
||||||
check.equals(log.count(), 2, "diff added to log");
|
check.equals(log.count(), 2, "diff added to log");
|
||||||
check.equals(state.counter, 3, "diff not applied to state");
|
check.equals(state.counter, 3, "diff not applied to state");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("initializes at current state (end of log)", check => {
|
test.case("initializes at current state (end of log)", check => {
|
||||||
let state = new TestState();
|
let state = new TestState();
|
||||||
let log = new DiffLog<TestState>();
|
let log = new DiffLog<TestState>();
|
||||||
log.add(new TestDiff(7));
|
log.add(new TestDiff(7));
|
||||||
let client = new DiffLogClient(state, log);
|
let client = new DiffLogClient(state, log);
|
||||||
check.equals(client.atStart(), false);
|
check.equals(client.atStart(), false);
|
||||||
check.equals(client.atEnd(), true);
|
check.equals(client.atEnd(), true);
|
||||||
check.equals(state.counter, 0);
|
check.equals(state.counter, 0);
|
||||||
client.forward();
|
client.forward();
|
||||||
check.equals(state.counter, 0);
|
check.equals(state.counter, 0);
|
||||||
client.backward();
|
client.backward();
|
||||||
check.equals(state.counter, -7);
|
check.equals(state.counter, -7);
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("moves forward or backward in the log", check => {
|
test.case("moves forward or backward in the log", check => {
|
||||||
let log = new DiffLog<TestState>();
|
let log = new DiffLog<TestState>();
|
||||||
let state = new TestState();
|
let state = new TestState();
|
||||||
let client = new DiffLogClient(state, log);
|
let client = new DiffLogClient(state, log);
|
||||||
|
|
||||||
log.add(new TestDiff(7));
|
log.add(new TestDiff(7));
|
||||||
log.add(new TestDiff(-2));
|
log.add(new TestDiff(-2));
|
||||||
log.add(new TestDiff(4));
|
log.add(new TestDiff(4));
|
||||||
|
|
||||||
check.equals(state.counter, 0, "initial state is 0");
|
check.equals(state.counter, 0, "initial state is 0");
|
||||||
check.equals(client.atStart(), true, "client is at start");
|
check.equals(client.atStart(), true, "client is at start");
|
||||||
check.equals(client.atEnd(), false, "client is not at end");
|
check.equals(client.atEnd(), false, "client is not at end");
|
||||||
|
|
||||||
client.forward();
|
client.forward();
|
||||||
check.equals(state.counter, 7, "0+7 => 7");
|
check.equals(state.counter, 7, "0+7 => 7");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), false, "client is not at end");
|
check.equals(client.atEnd(), false, "client is not at end");
|
||||||
|
|
||||||
client.forward();
|
client.forward();
|
||||||
check.equals(state.counter, 5, "7-2 => 5");
|
check.equals(state.counter, 5, "7-2 => 5");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), false, "client is not at end");
|
check.equals(client.atEnd(), false, "client is not at end");
|
||||||
|
|
||||||
client.forward();
|
client.forward();
|
||||||
check.equals(state.counter, 9, "5+4 => 9");
|
check.equals(state.counter, 9, "5+4 => 9");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), true, "client is at end");
|
check.equals(client.atEnd(), true, "client is at end");
|
||||||
|
|
||||||
client.forward();
|
client.forward();
|
||||||
check.equals(state.counter, 9, "at end, still 9");
|
check.equals(state.counter, 9, "at end, still 9");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), true, "client is at end");
|
check.equals(client.atEnd(), true, "client is at end");
|
||||||
|
|
||||||
client.backward();
|
client.backward();
|
||||||
check.equals(state.counter, 5, "9-4=>5");
|
check.equals(state.counter, 5, "9-4=>5");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), false, "client is not at end");
|
check.equals(client.atEnd(), false, "client is not at end");
|
||||||
|
|
||||||
client.backward();
|
client.backward();
|
||||||
check.equals(state.counter, 7, "5+2=>7");
|
check.equals(state.counter, 7, "5+2=>7");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), false, "client is not at end");
|
check.equals(client.atEnd(), false, "client is not at end");
|
||||||
|
|
||||||
client.backward();
|
client.backward();
|
||||||
check.equals(state.counter, 0, "7-7=>0");
|
check.equals(state.counter, 0, "7-7=>0");
|
||||||
check.equals(client.atStart(), true, "client is back at start");
|
check.equals(client.atStart(), true, "client is back at start");
|
||||||
check.equals(client.atEnd(), false, "client is not at end");
|
check.equals(client.atEnd(), false, "client is not at end");
|
||||||
|
|
||||||
client.backward();
|
client.backward();
|
||||||
check.equals(state.counter, 0, "at start, still 0");
|
check.equals(state.counter, 0, "at start, still 0");
|
||||||
check.equals(client.atStart(), true, "client is at start");
|
check.equals(client.atStart(), true, "client is at start");
|
||||||
check.equals(client.atEnd(), false, "client is not at end");
|
check.equals(client.atEnd(), false, "client is not at end");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("jumps to start or end of the log", check => {
|
test.case("jumps to start or end of the log", check => {
|
||||||
let log = new DiffLog<TestState>();
|
let log = new DiffLog<TestState>();
|
||||||
let state = new TestState();
|
let state = new TestState();
|
||||||
let client = new DiffLogClient(state, log);
|
let client = new DiffLogClient(state, log);
|
||||||
|
|
||||||
client.add(new TestDiff(7));
|
client.add(new TestDiff(7));
|
||||||
log.add(new TestDiff(-2));
|
log.add(new TestDiff(-2));
|
||||||
log.add(new TestDiff(4));
|
log.add(new TestDiff(4));
|
||||||
|
|
||||||
check.equals(state.counter, 7, "initial state is 7");
|
check.equals(state.counter, 7, "initial state is 7");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), false, "client is not at end");
|
check.equals(client.atEnd(), false, "client is not at end");
|
||||||
|
|
||||||
client.jumpToEnd();
|
client.jumpToEnd();
|
||||||
check.equals(state.counter, 9, "7-2+4=>9");
|
check.equals(state.counter, 9, "7-2+4=>9");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), true, "client at end");
|
check.equals(client.atEnd(), true, "client at end");
|
||||||
|
|
||||||
client.jumpToEnd();
|
client.jumpToEnd();
|
||||||
check.equals(state.counter, 9, "still 9");
|
check.equals(state.counter, 9, "still 9");
|
||||||
check.equals(client.atStart(), false, "client is not at start");
|
check.equals(client.atStart(), false, "client is not at start");
|
||||||
check.equals(client.atEnd(), true, "client at end");
|
check.equals(client.atEnd(), true, "client at end");
|
||||||
|
|
||||||
client.jumpToStart();
|
client.jumpToStart();
|
||||||
check.equals(state.counter, 0, "9-4+2-7=>0");
|
check.equals(state.counter, 0, "9-4+2-7=>0");
|
||||||
check.equals(client.atStart(), true, "client is at start");
|
check.equals(client.atStart(), true, "client is at start");
|
||||||
check.equals(client.atEnd(), false, "client at not end");
|
check.equals(client.atEnd(), false, "client at not end");
|
||||||
|
|
||||||
client.jumpToStart();
|
client.jumpToStart();
|
||||||
check.equals(state.counter, 0, "still 0");
|
check.equals(state.counter, 0, "still 0");
|
||||||
check.equals(client.atStart(), true, "client is at start");
|
check.equals(client.atStart(), true, "client is at start");
|
||||||
check.equals(client.atEnd(), false, "client at not end");
|
check.equals(client.atEnd(), false, "client at not end");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("truncate the log", check => {
|
test.case("truncate the log", check => {
|
||||||
let log = new DiffLog<TestState>();
|
let log = new DiffLog<TestState>();
|
||||||
let state = new TestState();
|
let state = new TestState();
|
||||||
let client = new DiffLogClient(state, log);
|
let client = new DiffLogClient(state, log);
|
||||||
|
|
||||||
client.add(new TestDiff(7));
|
client.add(new TestDiff(7));
|
||||||
client.add(new TestDiff(3));
|
client.add(new TestDiff(3));
|
||||||
client.add(new TestDiff(5));
|
client.add(new TestDiff(5));
|
||||||
|
|
||||||
check.in("initial state", check => {
|
check.in("initial state", check => {
|
||||||
check.equals(state.counter, 15, "state=15");
|
check.equals(state.counter, 15, "state=15");
|
||||||
check.equals(log.count(), 3, "count=3");
|
check.equals(log.count(), 3, "count=3");
|
||||||
});
|
});
|
||||||
|
|
||||||
client.backward();
|
client.backward();
|
||||||
|
|
||||||
check.in("after backward", check => {
|
check.in("after backward", check => {
|
||||||
check.equals(state.counter, 10, "state=10");
|
check.equals(state.counter, 10, "state=10");
|
||||||
check.equals(log.count(), 3, "count=3");
|
check.equals(log.count(), 3, "count=3");
|
||||||
});
|
});
|
||||||
|
|
||||||
client.truncate();
|
client.truncate();
|
||||||
|
|
||||||
check.in("after truncate", check => {
|
check.in("after truncate", check => {
|
||||||
check.equals(state.counter, 10, "state=10");
|
check.equals(state.counter, 10, "state=10");
|
||||||
check.equals(log.count(), 2, "count=2");
|
check.equals(log.count(), 2, "count=2");
|
||||||
});
|
});
|
||||||
|
|
||||||
client.truncate();
|
client.truncate();
|
||||||
|
|
||||||
check.in("after another truncate", check => {
|
check.in("after another truncate", check => {
|
||||||
check.equals(state.counter, 10, "state=10");
|
check.equals(state.counter, 10, "state=10");
|
||||||
check.equals(log.count(), 2, "count=2");
|
check.equals(log.count(), 2, "count=2");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
test.acase("plays the log continuously", async check => {
|
test.acase("plays the log continuously", async check => {
|
||||||
let log = new DiffLog<TestState>();
|
let log = new DiffLog<TestState>();
|
||||||
let state = new TestState();
|
let state = new TestState();
|
||||||
let client = new DiffLogClient(state, log);
|
let client = new DiffLogClient(state, log);
|
||||||
|
|
||||||
let inter: number[] = [];
|
let inter: number[] = [];
|
||||||
let promise = client.play(diff => {
|
let promise = client.play(diff => {
|
||||||
inter.push((<any>diff).value);
|
inter.push((<any>diff).value);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
log.add(new TestDiff(5));
|
log.add(new TestDiff(5));
|
||||||
log.add(new TestDiff(-1));
|
log.add(new TestDiff(-1));
|
||||||
log.add(new TestDiff(2));
|
log.add(new TestDiff(2));
|
||||||
client.stop(false);
|
client.stop(false);
|
||||||
|
|
||||||
await promise;
|
await promise;
|
||||||
|
|
||||||
check.equals(state.counter, 6);
|
check.equals(state.counter, 6);
|
||||||
check.equals(inter, [5, -1, 2]);
|
check.equals(inter, [5, -1, 2]);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
|
@ -1,249 +1,244 @@
|
||||||
|
import { Timer } from "./Timer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Framework to maintain a state from a log of changes
|
* Base class for a single diff.
|
||||||
*
|
*
|
||||||
* This allows for repeatable, serializable and revertable state modifications.
|
* This represents an atomic change of the state, that can be applied, or reverted.
|
||||||
*/
|
*/
|
||||||
module TK {
|
export class Diff<T> {
|
||||||
/**
|
/**
|
||||||
* Base class for a single diff.
|
* Apply the diff on a given state
|
||||||
*
|
*
|
||||||
* This represents an atomic change of the state, that can be applied, or reverted.
|
* By default it does nothing
|
||||||
*/
|
*/
|
||||||
export class Diff<T> {
|
apply(state: T): void {
|
||||||
/**
|
}
|
||||||
* Apply the diff on a given state
|
|
||||||
*
|
|
||||||
* By default it does nothing
|
|
||||||
*/
|
|
||||||
apply(state: T): void {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverts the diff from a given state
|
* Reverts the diff from a given state
|
||||||
*
|
*
|
||||||
* By default it applies the reverse event
|
* By default it applies the reverse event
|
||||||
*/
|
*/
|
||||||
revert(state: T): void {
|
revert(state: T): void {
|
||||||
this.getReverse().apply(state);
|
this.getReverse().apply(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the reverse event
|
* Get the reverse event
|
||||||
*
|
*
|
||||||
* By default it returns a stub event that does nothing
|
* By default it returns a stub event that does nothing
|
||||||
*/
|
*/
|
||||||
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>[] = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a single diff at the end of the log
|
* Add a single diff at the end of the log
|
||||||
*/
|
*/
|
||||||
add(diff: Diff<T>): void {
|
add(diff: Diff<T>): void {
|
||||||
this.diffs.push(diff);
|
this.diffs.push(diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the diff at a specific index
|
* Get the diff at a specific index
|
||||||
*/
|
*/
|
||||||
get(idx: number): Diff<T> | null {
|
get(idx: number): Diff<T> | null {
|
||||||
return this.diffs[idx] || null;
|
return this.diffs[idx] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the total count of diffs
|
* Return the total count of diffs
|
||||||
*/
|
*/
|
||||||
count(): number {
|
count(): number {
|
||||||
return this.diffs.length;
|
return this.diffs.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean all stored diffs, starting at a given index
|
* Clean all stored diffs, starting at a given index
|
||||||
*
|
*
|
||||||
* The caller should be sure that no log client is beyond the cut index.
|
* The caller should be sure that no log client is beyond the cut index.
|
||||||
*/
|
*/
|
||||||
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
|
||||||
private playing = false
|
private playing = false
|
||||||
private stopping = false
|
private stopping = false
|
||||||
private paused = false
|
private paused = false
|
||||||
private timer = Timer.global
|
private timer = Timer.global
|
||||||
|
|
||||||
constructor(state: T, log: DiffLog<T>) {
|
constructor(state: T, log: DiffLog<T>) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.log = log;
|
this.log = log;
|
||||||
this.cursor = log.count() - 1;
|
this.cursor = log.count() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the log is currently playing
|
* Returns true if the log is currently playing
|
||||||
*/
|
*/
|
||||||
isPlaying(): boolean {
|
isPlaying(): boolean {
|
||||||
return this.playing && !this.paused && !this.stopping;
|
return this.playing && !this.paused && !this.stopping;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current diff pointed at
|
* Get the current diff pointed at
|
||||||
*/
|
*/
|
||||||
getCurrent(): Diff<T> | null {
|
getCurrent(): Diff<T> | null {
|
||||||
return this.log.get(this.cursor);
|
return this.log.get(this.cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push a diff to the underlying log, applying it immediately if required
|
* Push a diff to the underlying log, applying it immediately if required
|
||||||
*/
|
*/
|
||||||
add(diff: Diff<T>, apply = true): void {
|
add(diff: Diff<T>, apply = true): void {
|
||||||
this.log.add(diff);
|
this.log.add(diff);
|
||||||
if (apply) {
|
if (apply) {
|
||||||
this.jumpToEnd();
|
this.jumpToEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the underlying log continuously, until *stop* is called
|
* Apply the underlying log continuously, until *stop* is called
|
||||||
*
|
*
|
||||||
* If *after_apply* is provided, it will be called after each diff is applied, and waited upon before resuming
|
* If *after_apply* is provided, it will be called after each diff is applied, and waited upon before resuming
|
||||||
*/
|
*/
|
||||||
async play(after_apply?: (diff: Diff<T>) => Promise<void>): Promise<void> {
|
async play(after_apply?: (diff: Diff<T>) => Promise<void>): Promise<void> {
|
||||||
if (this.playing) {
|
if (this.playing) {
|
||||||
console.error("DiffLogClient already playing", this);
|
console.error("DiffLogClient already playing", this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
this.stopping = false;
|
this.stopping = false;
|
||||||
|
|
||||||
while (this.playing) {
|
while (this.playing) {
|
||||||
if (!this.paused) {
|
if (!this.paused) {
|
||||||
let diff = this.forward();
|
let diff = this.forward();
|
||||||
if (diff && after_apply) {
|
if (diff && after_apply) {
|
||||||
await after_apply(diff);
|
await after_apply(diff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.atEnd()) {
|
if (this.atEnd()) {
|
||||||
if (this.stopping) {
|
if (this.stopping) {
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
await this.timer.sleep(50);
|
await this.timer.sleep(50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the previous *play*
|
* Stop the previous *play*
|
||||||
*/
|
*/
|
||||||
stop(immediate = true): void {
|
stop(immediate = true): void {
|
||||||
if (!this.playing) {
|
if (!this.playing) {
|
||||||
console.error("DiffLogClient not playing", this);
|
console.error("DiffLogClient not playing", this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
}
|
}
|
||||||
this.stopping = true;
|
this.stopping = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a step backward in time (revert one diff)
|
* Make a step backward in time (revert one diff)
|
||||||
*/
|
*/
|
||||||
backward(): Diff<T> | null {
|
backward(): Diff<T> | null {
|
||||||
if (!this.atStart()) {
|
if (!this.atStart()) {
|
||||||
this.cursor -= 1;
|
this.cursor -= 1;
|
||||||
this.paused = true;
|
this.paused = true;
|
||||||
|
|
||||||
let diff = this.log.get(this.cursor + 1);
|
let diff = this.log.get(this.cursor + 1);
|
||||||
if (diff) {
|
if (diff) {
|
||||||
diff.revert(this.state);
|
diff.revert(this.state);
|
||||||
}
|
}
|
||||||
return diff;
|
return diff;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a step forward in time (apply one diff)
|
* Make a step forward in time (apply one diff)
|
||||||
*/
|
*/
|
||||||
forward(): Diff<T> | null {
|
forward(): Diff<T> | null {
|
||||||
if (!this.atEnd()) {
|
if (!this.atEnd()) {
|
||||||
this.cursor += 1;
|
this.cursor += 1;
|
||||||
if (this.atEnd()) {
|
if (this.atEnd()) {
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let diff = this.log.get(this.cursor);
|
let diff = this.log.get(this.cursor);
|
||||||
if (diff) {
|
if (diff) {
|
||||||
diff.apply(this.state);
|
diff.apply(this.state);
|
||||||
}
|
}
|
||||||
return diff;
|
return diff;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jump to the start of the log
|
* Jump to the start of the log
|
||||||
*
|
*
|
||||||
* This will rewind all applied event
|
* This will rewind all applied event
|
||||||
*/
|
*/
|
||||||
jumpToStart() {
|
jumpToStart() {
|
||||||
while (!this.atStart()) {
|
while (!this.atStart()) {
|
||||||
this.backward();
|
this.backward();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jump to the end of the log
|
* Jump to the end of the log
|
||||||
*
|
*
|
||||||
* This will apply all remaining event
|
* This will apply all remaining event
|
||||||
*/
|
*/
|
||||||
jumpToEnd() {
|
jumpToEnd() {
|
||||||
while (!this.atEnd()) {
|
while (!this.atEnd()) {
|
||||||
this.forward();
|
this.forward();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we are currently at the start of the log
|
* Check if we are currently at the start of the log
|
||||||
*/
|
*/
|
||||||
atStart(): boolean {
|
atStart(): boolean {
|
||||||
return this.cursor < 0;
|
return this.cursor < 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we are currently at the end of the log
|
* Check if we are currently at the end of the log
|
||||||
*/
|
*/
|
||||||
atEnd(): boolean {
|
atEnd(): boolean {
|
||||||
return this.cursor >= this.log.count() - 1;
|
return this.cursor >= this.log.count() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Truncate all diffs after the current position
|
* Truncate all diffs after the current position
|
||||||
*
|
*
|
||||||
* This is useful when using the log to "undo" something
|
* This is useful when using the log to "undo" something
|
||||||
*/
|
*/
|
||||||
truncate(): void {
|
truncate(): void {
|
||||||
this.log.clear(this.cursor + 1);
|
this.log.clear(this.cursor + 1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,241 +1,239 @@
|
||||||
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]();
|
values.forEach((value, idx) => {
|
||||||
values.forEach((value, idx) => {
|
let state = iterator.next();
|
||||||
let state = iterator.next();
|
check.equals(state.done, false, `index ${idx} not done`);
|
||||||
check.equals(state.done, false, `index ${idx} not done`);
|
check.equals(state.value, value, `index ${idx} value`);
|
||||||
check.equals(state.value, value, `index ${idx} value`);
|
});
|
||||||
});
|
if (!infinite) {
|
||||||
if (!infinite) {
|
range(3).forEach(oidx => {
|
||||||
range(3).forEach(oidx => {
|
let state = iterator.next();
|
||||||
let state = iterator.next();
|
check.equals(state.done, true, `index ${values.length + oidx} done`);
|
||||||
check.equals(state.done, true, `index ${values.length + oidx} done`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
check.in("first iteration", checker);
|
|
||||||
check.in("second iteration", checker);
|
|
||||||
}
|
|
||||||
|
|
||||||
test.case("constructs an iterator from a recurrent formula", check => {
|
|
||||||
checkit(check, irecur(1, x => x + 2), [1, 3, 5], true);
|
|
||||||
checkit(check, irecur(4, x => x ? x - 1 : null), [4, 3, 2, 1, 0]);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test.case("constructs an iterator from an array", check => {
|
check.in("first iteration", checker);
|
||||||
checkit(check, iarray([]), []);
|
check.in("second iteration", checker);
|
||||||
checkit(check, iarray([1, 2, 3]), [1, 2, 3]);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
test.case("constructs an iterator from a single value", check => {
|
test.case("constructs an iterator from a recurrent formula", check => {
|
||||||
checkit(check, isingle(1), [1]);
|
checkit(check, irecur(1, x => x + 2), [1, 3, 5], true);
|
||||||
checkit(check, isingle("a"), ["a"]);
|
checkit(check, irecur(4, x => x ? x - 1 : null), [4, 3, 2, 1, 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("repeats a value", check => {
|
test.case("constructs an iterator from an array", check => {
|
||||||
checkit(check, irepeat("a"), ["a", "a", "a", "a"], true);
|
checkit(check, iarray([]), []);
|
||||||
checkit(check, irepeat("a", 3), ["a", "a", "a"]);
|
checkit(check, iarray([1, 2, 3]), [1, 2, 3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("calls a function for each yielded value", check => {
|
test.case("constructs an iterator from a single value", check => {
|
||||||
let iterator = iarray([1, 2, 3]);
|
checkit(check, isingle(1), [1]);
|
||||||
let result: number[] = [];
|
checkit(check, isingle("a"), ["a"]);
|
||||||
iforeach(iterator, bound(result, "push"));
|
});
|
||||||
check.equals(result, [1, 2, 3]);
|
|
||||||
|
|
||||||
result = [];
|
test.case("repeats a value", check => {
|
||||||
iforeach(iterator, i => {
|
checkit(check, irepeat("a"), ["a", "a", "a", "a"], true);
|
||||||
result.push(i);
|
checkit(check, irepeat("a", 3), ["a", "a", "a"]);
|
||||||
if (i == 2) {
|
});
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
check.equals(result, [1, 2]);
|
|
||||||
|
|
||||||
result = [];
|
test.case("calls a function for each yielded value", check => {
|
||||||
iforeach(iterator, i => {
|
let iterator = iarray([1, 2, 3]);
|
||||||
result.push(i);
|
let result: number[] = [];
|
||||||
return i;
|
iforeach(iterator, bound(result, "push"));
|
||||||
}, 2);
|
check.equals(result, [1, 2, 3]);
|
||||||
check.equals(result, [1, 2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("finds the first item passing a predicate", check => {
|
result = [];
|
||||||
check.equals(ifirst(iarray(<number[]>[]), i => i % 2 == 0), null);
|
iforeach(iterator, i => {
|
||||||
check.equals(ifirst(iarray([1, 2, 3]), i => i % 2 == 0), 2);
|
result.push(i);
|
||||||
check.equals(ifirst(iarray([1, 3, 5]), i => i % 2 == 0), null);
|
if (i == 2) {
|
||||||
});
|
return null;
|
||||||
|
} else {
|
||||||
test.case("finds the first item mapping to a value", check => {
|
return undefined;
|
||||||
let predicate = (i: number) => i % 2 == 0 ? (i * 4).toString() : null
|
}
|
||||||
check.equals(ifirstmap(iarray([]), predicate), null);
|
|
||||||
check.equals(ifirstmap(iarray([1, 2, 3]), predicate), "8");
|
|
||||||
check.equals(ifirstmap(iarray([1, 3, 5]), predicate), null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("materializes an array from an iterator", check => {
|
|
||||||
check.equals(imaterialize(iarray([1, 2, 3])), [1, 2, 3]);
|
|
||||||
|
|
||||||
check.throw(() => imaterialize(iarray([1, 2, 3, 4, 5]), 2), "Length limit on iterator materialize");
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("creates an iterator in a range of integers", check => {
|
|
||||||
checkit(check, irange(4), [0, 1, 2, 3]);
|
|
||||||
checkit(check, irange(4, 1), [1, 2, 3, 4]);
|
|
||||||
checkit(check, irange(5, 3, 2), [3, 5, 7, 9, 11]);
|
|
||||||
checkit(check, irange(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("uses a step iterator to scan numbers", check => {
|
|
||||||
checkit(check, istep(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true);
|
|
||||||
checkit(check, istep(3), [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], true);
|
|
||||||
checkit(check, istep(3, irepeat(1, 4)), [3, 4, 5, 6, 7]);
|
|
||||||
checkit(check, istep(8, IEMPTY), [8]);
|
|
||||||
checkit(check, istep(1, irange()), [1, 1, 2, 4, 7, 11, 16], true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("skips a number of values", check => {
|
|
||||||
checkit(check, iskip(irange(7), 3), [3, 4, 5, 6]);
|
|
||||||
checkit(check, iskip(irange(7), 12), []);
|
|
||||||
checkit(check, iskip(IEMPTY, 3), []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("gets a value at an iterator position", check => {
|
|
||||||
check.equals(iat(irange(), -1), null);
|
|
||||||
check.equals(iat(irange(), 0), 0);
|
|
||||||
check.equals(iat(irange(), 8), 8);
|
|
||||||
check.equals(iat(irange(5), 8), null);
|
|
||||||
check.equals(iat(IEMPTY, 0), null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("chains iterator of iterators", check => {
|
|
||||||
checkit(check, ichainit(IEMPTY), []);
|
|
||||||
checkit(check, ichainit(iarray([iarray([1, 2, 3]), iarray([]), iarray([4, 5])])), [1, 2, 3, 4, 5]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("chains iterators", check => {
|
|
||||||
checkit(check, ichain(), []);
|
|
||||||
checkit(check, ichain(irange(3)), [0, 1, 2]);
|
|
||||||
checkit(check, ichain(iarray([1, 2]), iarray([]), iarray([3, 4, 5])), [1, 2, 3, 4, 5]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("loops an iterator", check => {
|
|
||||||
checkit(check, iloop(irange(3), 2), [0, 1, 2, 0, 1, 2]);
|
|
||||||
checkit(check, iloop(irange(1)), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], true);
|
|
||||||
|
|
||||||
let onloop = check.mockfunc("onloop");
|
|
||||||
let iterator = iloop(irange(2), 3, onloop.func)[Symbol.iterator]();
|
|
||||||
check.in("idx 0", check => {
|
|
||||||
check.equals(iterator.next().value, 0);
|
|
||||||
check.called(onloop, 0);
|
|
||||||
});
|
|
||||||
check.in("idx 1", check => {
|
|
||||||
check.equals(iterator.next().value, 1);
|
|
||||||
check.called(onloop, 0);
|
|
||||||
});
|
|
||||||
check.in("idx 2", check => {
|
|
||||||
check.equals(iterator.next().value, 0);
|
|
||||||
check.called(onloop, 1);
|
|
||||||
});
|
|
||||||
check.in("idx 3", check => {
|
|
||||||
check.equals(iterator.next().value, 1);
|
|
||||||
check.called(onloop, 0);
|
|
||||||
});
|
|
||||||
check.in("idx 4", check => {
|
|
||||||
check.equals(iterator.next().value, 0);
|
|
||||||
check.called(onloop, 1);
|
|
||||||
});
|
|
||||||
check.in("idx 5", check => {
|
|
||||||
check.equals(iterator.next().value, 1);
|
|
||||||
check.called(onloop, 0);
|
|
||||||
});
|
|
||||||
check.in("idx 6", check => {
|
|
||||||
check.equals(iterator.next().value, undefined);
|
|
||||||
check.called(onloop, 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("maps an iterator", check => {
|
|
||||||
checkit(check, imap(IEMPTY, i => i * 2), []);
|
|
||||||
checkit(check, imap(irange(3), i => i * 2), [0, 2, 4]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("reduces an iterator", check => {
|
|
||||||
check.equals(ireduce(IEMPTY, (a, b) => a + b, 2), 2);
|
|
||||||
check.equals(ireduce([9], (a, b) => a + b, 2), 11);
|
|
||||||
check.equals(ireduce([9, 1], (a, b) => a + b, 2), 12);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("filters an iterator with a predicate", check => {
|
|
||||||
checkit(check, imap(IEMPTY, i => i % 3 == 0), []);
|
|
||||||
checkit(check, ifilter(irange(12), i => i % 3 == 0), [0, 3, 6, 9]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("filters an iterator with a type guard", check => {
|
|
||||||
let result = ifiltertype(<(number | string)[]>[1, "a", 2, "b"], (x): x is number => typeof x == "number");
|
|
||||||
checkit(check, result, [1, 2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("filters an iterator with a class type", check => {
|
|
||||||
let o1 = new RObject();
|
|
||||||
let o2 = new RObject();
|
|
||||||
let o3 = new RObjectContainer();
|
|
||||||
let result = ifilterclass([1, "a", o1, 2, o2, o3, "b"], RObject);
|
|
||||||
checkit(check, result, [o1, o2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("combines iterators", check => {
|
|
||||||
let iterator = icombine(iarray([1, 2, 3]), iarray(["a", "b"]));
|
|
||||||
checkit(check, iterator, [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("zips iterators", check => {
|
|
||||||
checkit(check, izip(IEMPTY, IEMPTY), []);
|
|
||||||
checkit(check, izip(iarray([1, 2, 3]), iarray(["a", "b"])), [[1, "a"], [2, "b"]]);
|
|
||||||
|
|
||||||
checkit(check, izipg(IEMPTY, IEMPTY), []);
|
|
||||||
checkit(check, izipg(iarray([1, 2, 3]), iarray(["a", "b"])), <[number | undefined, string | undefined][]>[[1, "a"], [2, "b"], [3, undefined]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("partitions iterators", check => {
|
|
||||||
let [it1, it2] = ipartition(IEMPTY, () => true);
|
|
||||||
checkit(check, it1, []);
|
|
||||||
checkit(check, it2, []);
|
|
||||||
|
|
||||||
[it1, it2] = ipartition(irange(5), i => i % 2 == 0);
|
|
||||||
checkit(check, it1, [0, 2, 4]);
|
|
||||||
checkit(check, it2, [1, 3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("alternatively pick from several iterables", check => {
|
|
||||||
checkit(check, ialternate([]), []);
|
|
||||||
checkit(check, ialternate([[1, 2, 3, 4], [], iarray([5, 6]), IEMPTY, iarray([7, 8, 9])]), [1, 5, 7, 2, 6, 8, 3, 9, 4]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("returns unique items", check => {
|
|
||||||
checkit(check, iunique(IEMPTY), []);
|
|
||||||
checkit(check, iunique(iarray([5, 3, 2, 3, 4, 5])), [5, 3, 2, 4]);
|
|
||||||
checkit(check, iunique(iarray([5, 3, 2, 3, 4, 5]), 4), [5, 3, 2, 4]);
|
|
||||||
check.throw(() => imaterialize(iunique(iarray([5, 3, 2, 3, 4, 5]), 3)), "Unique count limit on iterator");
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("uses ireduce for some common functions", check => {
|
|
||||||
check.equals(isum(IEMPTY), 0);
|
|
||||||
check.equals(isum(irange(4)), 6);
|
|
||||||
|
|
||||||
check.equals(icat(IEMPTY), "");
|
|
||||||
check.equals(icat(iarray(["a", "bc", "d"])), "abcd");
|
|
||||||
|
|
||||||
check.equals(imin(IEMPTY), Infinity);
|
|
||||||
check.equals(imin(iarray([3, 8, 2, 4])), 2);
|
|
||||||
|
|
||||||
check.equals(imax(IEMPTY), -Infinity);
|
|
||||||
check.equals(imax(iarray([3, 8, 2, 4])), 8);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
check.equals(result, [1, 2]);
|
||||||
|
|
||||||
|
result = [];
|
||||||
|
iforeach(iterator, i => {
|
||||||
|
result.push(i);
|
||||||
|
return i;
|
||||||
|
}, 2);
|
||||||
|
check.equals(result, [1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("finds the first item passing a predicate", check => {
|
||||||
|
check.equals(ifirst(iarray(<number[]>[]), i => i % 2 == 0), null);
|
||||||
|
check.equals(ifirst(iarray([1, 2, 3]), i => i % 2 == 0), 2);
|
||||||
|
check.equals(ifirst(iarray([1, 3, 5]), i => i % 2 == 0), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("finds the first item mapping to a value", check => {
|
||||||
|
let predicate = (i: number) => i % 2 == 0 ? (i * 4).toString() : null
|
||||||
|
check.equals(ifirstmap(iarray([]), predicate), null);
|
||||||
|
check.equals(ifirstmap(iarray([1, 2, 3]), predicate), "8");
|
||||||
|
check.equals(ifirstmap(iarray([1, 3, 5]), predicate), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("materializes an array from an iterator", check => {
|
||||||
|
check.equals(imaterialize(iarray([1, 2, 3])), [1, 2, 3]);
|
||||||
|
|
||||||
|
check.throw(() => imaterialize(iarray([1, 2, 3, 4, 5]), 2), "Length limit on iterator materialize");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("creates an iterator in a range of integers", check => {
|
||||||
|
checkit(check, irange(4), [0, 1, 2, 3]);
|
||||||
|
checkit(check, irange(4, 1), [1, 2, 3, 4]);
|
||||||
|
checkit(check, irange(5, 3, 2), [3, 5, 7, 9, 11]);
|
||||||
|
checkit(check, irange(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("uses a step iterator to scan numbers", check => {
|
||||||
|
checkit(check, istep(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true);
|
||||||
|
checkit(check, istep(3), [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], true);
|
||||||
|
checkit(check, istep(3, irepeat(1, 4)), [3, 4, 5, 6, 7]);
|
||||||
|
checkit(check, istep(8, IEMPTY), [8]);
|
||||||
|
checkit(check, istep(1, irange()), [1, 1, 2, 4, 7, 11, 16], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("skips a number of values", check => {
|
||||||
|
checkit(check, iskip(irange(7), 3), [3, 4, 5, 6]);
|
||||||
|
checkit(check, iskip(irange(7), 12), []);
|
||||||
|
checkit(check, iskip(IEMPTY, 3), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("gets a value at an iterator position", check => {
|
||||||
|
check.equals(iat(irange(), -1), null);
|
||||||
|
check.equals(iat(irange(), 0), 0);
|
||||||
|
check.equals(iat(irange(), 8), 8);
|
||||||
|
check.equals(iat(irange(5), 8), null);
|
||||||
|
check.equals(iat(IEMPTY, 0), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("chains iterator of iterators", check => {
|
||||||
|
checkit(check, ichainit(IEMPTY), []);
|
||||||
|
checkit(check, ichainit(iarray([iarray([1, 2, 3]), iarray([]), iarray([4, 5])])), [1, 2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("chains iterators", check => {
|
||||||
|
checkit(check, ichain(), []);
|
||||||
|
checkit(check, ichain(irange(3)), [0, 1, 2]);
|
||||||
|
checkit(check, ichain(iarray([1, 2]), iarray([]), iarray([3, 4, 5])), [1, 2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("loops an iterator", check => {
|
||||||
|
checkit(check, iloop(irange(3), 2), [0, 1, 2, 0, 1, 2]);
|
||||||
|
checkit(check, iloop(irange(1)), [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], true);
|
||||||
|
|
||||||
|
let onloop = check.mockfunc("onloop");
|
||||||
|
let iterator = iloop(irange(2), 3, onloop.func)[Symbol.iterator]();
|
||||||
|
check.in("idx 0", check => {
|
||||||
|
check.equals(iterator.next().value, 0);
|
||||||
|
check.called(onloop, 0);
|
||||||
|
});
|
||||||
|
check.in("idx 1", check => {
|
||||||
|
check.equals(iterator.next().value, 1);
|
||||||
|
check.called(onloop, 0);
|
||||||
|
});
|
||||||
|
check.in("idx 2", check => {
|
||||||
|
check.equals(iterator.next().value, 0);
|
||||||
|
check.called(onloop, 1);
|
||||||
|
});
|
||||||
|
check.in("idx 3", check => {
|
||||||
|
check.equals(iterator.next().value, 1);
|
||||||
|
check.called(onloop, 0);
|
||||||
|
});
|
||||||
|
check.in("idx 4", check => {
|
||||||
|
check.equals(iterator.next().value, 0);
|
||||||
|
check.called(onloop, 1);
|
||||||
|
});
|
||||||
|
check.in("idx 5", check => {
|
||||||
|
check.equals(iterator.next().value, 1);
|
||||||
|
check.called(onloop, 0);
|
||||||
|
});
|
||||||
|
check.in("idx 6", check => {
|
||||||
|
check.equals(iterator.next().value, undefined);
|
||||||
|
check.called(onloop, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("maps an iterator", check => {
|
||||||
|
checkit(check, imap(IEMPTY, i => i * 2), []);
|
||||||
|
checkit(check, imap(irange(3), i => i * 2), [0, 2, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("reduces an iterator", check => {
|
||||||
|
check.equals(ireduce(IEMPTY, (a, b) => a + b, 2), 2);
|
||||||
|
check.equals(ireduce([9], (a, b) => a + b, 2), 11);
|
||||||
|
check.equals(ireduce([9, 1], (a, b) => a + b, 2), 12);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("filters an iterator with a predicate", check => {
|
||||||
|
checkit(check, imap(IEMPTY, i => i % 3 == 0), []);
|
||||||
|
checkit(check, ifilter(irange(12), i => i % 3 == 0), [0, 3, 6, 9]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("filters an iterator with a type guard", check => {
|
||||||
|
let result = ifiltertype(<(number | string)[]>[1, "a", 2, "b"], (x): x is number => typeof x == "number");
|
||||||
|
checkit(check, result, [1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("filters an iterator with a class type", check => {
|
||||||
|
let o1 = new RObject();
|
||||||
|
let o2 = new RObject();
|
||||||
|
let o3 = new RObjectContainer();
|
||||||
|
let result = ifilterclass([1, "a", o1, 2, o2, o3, "b"], RObject);
|
||||||
|
checkit(check, result, [o1, o2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("combines iterators", check => {
|
||||||
|
let iterator = icombine(iarray([1, 2, 3]), iarray(["a", "b"]));
|
||||||
|
checkit(check, iterator, [[1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("zips iterators", check => {
|
||||||
|
checkit(check, izip(IEMPTY, IEMPTY), []);
|
||||||
|
checkit(check, izip(iarray([1, 2, 3]), iarray(["a", "b"])), [[1, "a"], [2, "b"]]);
|
||||||
|
|
||||||
|
checkit(check, izipg(IEMPTY, IEMPTY), []);
|
||||||
|
checkit(check, izipg(iarray([1, 2, 3]), iarray(["a", "b"])), <[number | undefined, string | undefined][]>[[1, "a"], [2, "b"], [3, undefined]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("partitions iterators", check => {
|
||||||
|
let [it1, it2] = ipartition(IEMPTY, () => true);
|
||||||
|
checkit(check, it1, []);
|
||||||
|
checkit(check, it2, []);
|
||||||
|
|
||||||
|
[it1, it2] = ipartition(irange(5), i => i % 2 == 0);
|
||||||
|
checkit(check, it1, [0, 2, 4]);
|
||||||
|
checkit(check, it2, [1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("alternatively pick from several iterables", check => {
|
||||||
|
checkit(check, ialternate([]), []);
|
||||||
|
checkit(check, ialternate([[1, 2, 3, 4], [], iarray([5, 6]), IEMPTY, iarray([7, 8, 9])]), [1, 5, 7, 2, 6, 8, 3, 9, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("returns unique items", check => {
|
||||||
|
checkit(check, iunique(IEMPTY), []);
|
||||||
|
checkit(check, iunique(iarray([5, 3, 2, 3, 4, 5])), [5, 3, 2, 4]);
|
||||||
|
checkit(check, iunique(iarray([5, 3, 2, 3, 4, 5]), 4), [5, 3, 2, 4]);
|
||||||
|
check.throw(() => imaterialize(iunique(iarray([5, 3, 2, 3, 4, 5]), 3)), "Unique count limit on iterator");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("uses ireduce for some common functions", check => {
|
||||||
|
check.equals(isum(IEMPTY), 0);
|
||||||
|
check.equals(isum(irange(4)), 6);
|
||||||
|
|
||||||
|
check.equals(icat(IEMPTY), "");
|
||||||
|
check.equals(icat(iarray(["a", "bc", "d"])), "abcd");
|
||||||
|
|
||||||
|
check.equals(imin(IEMPTY), Infinity);
|
||||||
|
check.equals(imin(iarray([3, 8, 2, 4])), 2);
|
||||||
|
|
||||||
|
check.equals(imax(IEMPTY), -Infinity);
|
||||||
|
check.equals(imax(iarray([3, 8, 2, 4])), 8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,436 +1,426 @@
|
||||||
|
import { contains } from "./Tools";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lazy iterators to work on dynamic data sets without materializing them.
|
* Empty iterator
|
||||||
*
|
|
||||||
* 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 {
|
export const IATEND: Iterator<any> = {
|
||||||
/**
|
next: function () {
|
||||||
* Empty iterator
|
return { done: true, value: undefined };
|
||||||
*/
|
}
|
||||||
export const IATEND: Iterator<any> = {
|
|
||||||
next: function () {
|
|
||||||
return { done: true, value: undefined };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Empty iterable
|
|
||||||
*/
|
|
||||||
export const IEMPTY: Iterable<any> = {
|
|
||||||
[Symbol.iterator]: () => IATEND
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterable constructor, from an initial value, and a step value
|
|
||||||
*/
|
|
||||||
export function irecur<T, S>(start: T, step: (a: T) => T | null): Iterable<T> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let val: T | null = start;
|
|
||||||
do {
|
|
||||||
yield val;
|
|
||||||
val = step(val);
|
|
||||||
} while (val !== null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
export function iarray<T>(array: T[], offset = 0): Iterable<T> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function () {
|
|
||||||
return array.slice(offset)[Symbol.iterator]();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterable constructor, from a single value
|
|
||||||
*
|
|
||||||
* The value will be yielded only once, not repeated over.
|
|
||||||
*/
|
|
||||||
export function isingle<T>(value: T): Iterable<T> {
|
|
||||||
return iarray([value]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterable that repeats the same value.
|
|
||||||
*/
|
|
||||||
export function irepeat<T>(value: T, count = -1): Iterable<T> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let n = count;
|
|
||||||
while (n != 0) {
|
|
||||||
yield value;
|
|
||||||
n--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Equivalent of Array.forEach for all iterables.
|
|
||||||
*
|
|
||||||
* If the callback returns *stopper*, the iteration is stopped.
|
|
||||||
*/
|
|
||||||
export function iforeach<T>(iterable: Iterable<T>, callback: (_: T) => any, stopper: any = null): void {
|
|
||||||
for (let value of iterable) {
|
|
||||||
if (callback(value) === stopper) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the first item passing a predicate
|
|
||||||
*/
|
|
||||||
export function ifirst<T>(iterable: Iterable<T>, predicate: (item: T) => boolean): T | null {
|
|
||||||
for (let value of iterable) {
|
|
||||||
if (predicate(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
for (let value of iterable) {
|
|
||||||
let res = predicate(value);
|
|
||||||
if (res !== null) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* exception is thrown when this limit is reached.
|
|
||||||
*/
|
|
||||||
export function imaterialize<T>(iterable: Iterable<T>, limit = 1000000): T[] {
|
|
||||||
let result: T[] = [];
|
|
||||||
|
|
||||||
for (let value of iterable) {
|
|
||||||
result.push(value);
|
|
||||||
if (result.length >= limit) {
|
|
||||||
throw new Error("Length limit on iterator materialize");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate over natural integers
|
|
||||||
*
|
|
||||||
* If *count* is not specified, the iterator is infinite
|
|
||||||
*/
|
|
||||||
export function irange(count: number = -1, start = 0, step = 1): Iterable<number> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let i = start;
|
|
||||||
let n = count;
|
|
||||||
while (n != 0) {
|
|
||||||
yield i;
|
|
||||||
i += step;
|
|
||||||
n--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate over numbers, by applying a step taken from an other iterator
|
|
||||||
*
|
|
||||||
* This iterator stops when the "step iterator" stops
|
|
||||||
*
|
|
||||||
* With no argument, istep() == irange()
|
|
||||||
*/
|
|
||||||
export function istep(start = 0, step_iterable = irepeat(1)): Iterable<number> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let i = start;
|
|
||||||
yield i;
|
|
||||||
for (let step of step_iterable) {
|
|
||||||
i += step;
|
|
||||||
yield i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Skip a given number of values from an iterator, discarding them.
|
|
||||||
*/
|
|
||||||
export function iskip<T>(iterable: Iterable<T>, count = 1): Iterable<T> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function () {
|
|
||||||
let iterator = iterable[Symbol.iterator]();
|
|
||||||
let n = count;
|
|
||||||
while (n-- > 0) {
|
|
||||||
iterator.next();
|
|
||||||
}
|
|
||||||
return iterator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the value at a given position in the iterator
|
|
||||||
*/
|
|
||||||
export function iat<T>(iterable: Iterable<T>, position: number): T | null {
|
|
||||||
if (position < 0) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
if (position > 0) {
|
|
||||||
iterable = iskip(iterable, position);
|
|
||||||
}
|
|
||||||
let iterator = iterable[Symbol.iterator]();
|
|
||||||
let state = iterator.next();
|
|
||||||
return state.done ? null : state.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chain an iterable of iterables.
|
|
||||||
*
|
|
||||||
* 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> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
for (let iterable of iterables) {
|
|
||||||
for (let value of iterable) {
|
|
||||||
yield value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chain iterables.
|
|
||||||
*
|
|
||||||
* This will yield values from the first iterator, then the second one, and so on...
|
|
||||||
*/
|
|
||||||
export function ichain<T>(...iterables: Iterable<T>[]): Iterable<T> {
|
|
||||||
if (iterables.length == 0) {
|
|
||||||
return IEMPTY;
|
|
||||||
} else {
|
|
||||||
return ichainit(iterables);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loop an iterator for a number of times.
|
|
||||||
*
|
|
||||||
* If count is negative, if will loop forever (infinite iterator).
|
|
||||||
*
|
|
||||||
* onloop may be used to know when the iterator resets.
|
|
||||||
*/
|
|
||||||
export function iloop<T>(base: Iterable<T>, count = -1, onloop?: Function): Iterable<T> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let n = count;
|
|
||||||
let start = false;
|
|
||||||
while (n-- != 0) {
|
|
||||||
for (let value of base) {
|
|
||||||
if (start) {
|
|
||||||
if (onloop) {
|
|
||||||
onloop();
|
|
||||||
}
|
|
||||||
start = false;
|
|
||||||
}
|
|
||||||
yield value;
|
|
||||||
}
|
|
||||||
start = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator version of "map".
|
|
||||||
*/
|
|
||||||
export function imap<T1, T2>(iterable: Iterable<T1>, mapfunc: (_: T1) => T2): Iterable<T2> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
for (let value of iterable) {
|
|
||||||
yield mapfunc(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator version of "reduce".
|
|
||||||
*/
|
|
||||||
export function ireduce<T>(iterable: Iterable<T>, reduce: (item1: T, item2: T) => T, init: T): T {
|
|
||||||
let result = init;
|
|
||||||
for (let value of iterable) {
|
|
||||||
result = reduce(result, value);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator version of "filter".
|
|
||||||
*/
|
|
||||||
export function ifilter<T>(iterable: Iterable<T>, filterfunc: (_: T) => boolean): Iterable<T> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
for (let value of iterable) {
|
|
||||||
if (filterfunc(value)) {
|
|
||||||
yield value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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> {
|
|
||||||
return ifilter(iterable, filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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> {
|
|
||||||
return ifilter(iterable, (item): item is T => item instanceof classref);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine two iterables.
|
|
||||||
*
|
|
||||||
* This iterates through the second one several times, so if one iterator may be infinite,
|
|
||||||
* it should be the first one.
|
|
||||||
*/
|
|
||||||
export function icombine<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1, T2]> {
|
|
||||||
return ichainit(imap(it1, v1 => imap(it2, (v2): [T1, T2] => [v1, v2])));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Advance through two iterables at the same time, yielding item pairs
|
|
||||||
*
|
|
||||||
* Iteration will stop at the first of the two iterators that stops.
|
|
||||||
*/
|
|
||||||
export function izip<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1, T2]> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let iterator1 = it1[Symbol.iterator]();
|
|
||||||
let iterator2 = it2[Symbol.iterator]();
|
|
||||||
let state1 = iterator1.next();
|
|
||||||
let state2 = iterator2.next();
|
|
||||||
while (!state1.done && !state2.done) {
|
|
||||||
yield [state1.value, state2.value];
|
|
||||||
state1 = iterator1.next();
|
|
||||||
state2 = iterator2.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
export function izipg<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1 | undefined, T2 | undefined]> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let iterator1 = it1[Symbol.iterator]();
|
|
||||||
let iterator2 = it2[Symbol.iterator]();
|
|
||||||
let state1 = iterator1.next();
|
|
||||||
let state2 = iterator2.next();
|
|
||||||
while (!state1.done || !state2.done) {
|
|
||||||
yield [state1.value, state2.value];
|
|
||||||
state1 = iterator1.next();
|
|
||||||
state2 = iterator2.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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>] {
|
|
||||||
return [ifilter(iterable, predicate), ifilter(iterable, x => !predicate(x))];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alternate between several iterables (pick one from the first one, then one from the second...)
|
|
||||||
*/
|
|
||||||
export function ialternate<T>(iterables: Iterable<T>[]): Iterable<T> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let iterators = iterables.map(iterable => iterable[Symbol.iterator]());
|
|
||||||
let done: boolean;
|
|
||||||
do {
|
|
||||||
done = false;
|
|
||||||
// TODO Remove "dried-out" iterators
|
|
||||||
for (let iterator of iterators) {
|
|
||||||
let state = iterator.next();
|
|
||||||
if (!state.done) {
|
|
||||||
done = true;
|
|
||||||
yield state.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (done);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Yield items from an iterator only once.
|
|
||||||
*
|
|
||||||
* Beware that even if this function is not materializing, it keeps track of yielded item, and may choke on
|
|
||||||
* infinite or very long streams. Thus, no more than *limit* items will be yielded (an error is thrown
|
|
||||||
* when this limit is reached).
|
|
||||||
*
|
|
||||||
* This function is O(n²)
|
|
||||||
*/
|
|
||||||
export function iunique<T>(iterable: Iterable<T>, limit = 1000000): Iterable<T> {
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: function* () {
|
|
||||||
let done: T[] = [];
|
|
||||||
let n = limit;
|
|
||||||
for (let value of iterable) {
|
|
||||||
if (!contains(done, value)) {
|
|
||||||
if (n-- > 0) {
|
|
||||||
done.push(value);
|
|
||||||
yield value;
|
|
||||||
} else {
|
|
||||||
throw new Error("Unique count limit on iterator");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common reduce shortcuts
|
|
||||||
*/
|
|
||||||
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 imin = (iterable: Iterable<number>) => ireduce(iterable, Math.min, Infinity);
|
|
||||||
export const imax = (iterable: Iterable<number>) => ireduce(iterable, Math.max, -Infinity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty iterable
|
||||||
|
*/
|
||||||
|
export const IEMPTY: Iterable<any> = {
|
||||||
|
[Symbol.iterator]: () => IATEND
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterable constructor, from an initial value, and a step value
|
||||||
|
*/
|
||||||
|
export function irecur<T, S>(start: T, step: (a: T) => T | null): Iterable<T> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let val: T | null = start;
|
||||||
|
do {
|
||||||
|
yield val;
|
||||||
|
val = step(val);
|
||||||
|
} while (val !== null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
export function iarray<T>(array: T[], offset = 0): Iterable<T> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function () {
|
||||||
|
return array.slice(offset)[Symbol.iterator]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterable constructor, from a single value
|
||||||
|
*
|
||||||
|
* The value will be yielded only once, not repeated over.
|
||||||
|
*/
|
||||||
|
export function isingle<T>(value: T): Iterable<T> {
|
||||||
|
return iarray([value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterable that repeats the same value.
|
||||||
|
*/
|
||||||
|
export function irepeat<T>(value: T, count = -1): Iterable<T> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let n = count;
|
||||||
|
while (n != 0) {
|
||||||
|
yield value;
|
||||||
|
n--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent of Array.forEach for all iterables.
|
||||||
|
*
|
||||||
|
* If the callback returns *stopper*, the iteration is stopped.
|
||||||
|
*/
|
||||||
|
export function iforeach<T>(iterable: Iterable<T>, callback: (_: T) => any, stopper: any = null): void {
|
||||||
|
for (let value of iterable) {
|
||||||
|
if (callback(value) === stopper) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first item passing a predicate
|
||||||
|
*/
|
||||||
|
export function ifirst<T>(iterable: Iterable<T>, predicate: (item: T) => boolean): T | null {
|
||||||
|
for (let value of iterable) {
|
||||||
|
if (predicate(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
for (let value of iterable) {
|
||||||
|
let res = predicate(value);
|
||||||
|
if (res !== null) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* exception is thrown when this limit is reached.
|
||||||
|
*/
|
||||||
|
export function imaterialize<T>(iterable: Iterable<T>, limit = 1000000): T[] {
|
||||||
|
let result: T[] = [];
|
||||||
|
|
||||||
|
for (let value of iterable) {
|
||||||
|
result.push(value);
|
||||||
|
if (result.length >= limit) {
|
||||||
|
throw new Error("Length limit on iterator materialize");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over natural integers
|
||||||
|
*
|
||||||
|
* If *count* is not specified, the iterator is infinite
|
||||||
|
*/
|
||||||
|
export function irange(count: number = -1, start = 0, step = 1): Iterable<number> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let i = start;
|
||||||
|
let n = count;
|
||||||
|
while (n != 0) {
|
||||||
|
yield i;
|
||||||
|
i += step;
|
||||||
|
n--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over numbers, by applying a step taken from an other iterator
|
||||||
|
*
|
||||||
|
* This iterator stops when the "step iterator" stops
|
||||||
|
*
|
||||||
|
* With no argument, istep() == irange()
|
||||||
|
*/
|
||||||
|
export function istep(start = 0, step_iterable = irepeat(1)): Iterable<number> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let i = start;
|
||||||
|
yield i;
|
||||||
|
for (let step of step_iterable) {
|
||||||
|
i += step;
|
||||||
|
yield i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skip a given number of values from an iterator, discarding them.
|
||||||
|
*/
|
||||||
|
export function iskip<T>(iterable: Iterable<T>, count = 1): Iterable<T> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function () {
|
||||||
|
let iterator = iterable[Symbol.iterator]();
|
||||||
|
let n = count;
|
||||||
|
while (n-- > 0) {
|
||||||
|
iterator.next();
|
||||||
|
}
|
||||||
|
return iterator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the value at a given position in the iterator
|
||||||
|
*/
|
||||||
|
export function iat<T>(iterable: Iterable<T>, position: number): T | null {
|
||||||
|
if (position < 0) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
if (position > 0) {
|
||||||
|
iterable = iskip(iterable, position);
|
||||||
|
}
|
||||||
|
let iterator = iterable[Symbol.iterator]();
|
||||||
|
let state = iterator.next();
|
||||||
|
return state.done ? null : state.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chain an iterable of iterables.
|
||||||
|
*
|
||||||
|
* 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> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
for (let iterable of iterables) {
|
||||||
|
for (let value of iterable) {
|
||||||
|
yield value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chain iterables.
|
||||||
|
*
|
||||||
|
* This will yield values from the first iterator, then the second one, and so on...
|
||||||
|
*/
|
||||||
|
export function ichain<T>(...iterables: Iterable<T>[]): Iterable<T> {
|
||||||
|
if (iterables.length == 0) {
|
||||||
|
return IEMPTY;
|
||||||
|
} else {
|
||||||
|
return ichainit(iterables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loop an iterator for a number of times.
|
||||||
|
*
|
||||||
|
* If count is negative, if will loop forever (infinite iterator).
|
||||||
|
*
|
||||||
|
* onloop may be used to know when the iterator resets.
|
||||||
|
*/
|
||||||
|
export function iloop<T>(base: Iterable<T>, count = -1, onloop?: Function): Iterable<T> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let n = count;
|
||||||
|
let start = false;
|
||||||
|
while (n-- != 0) {
|
||||||
|
for (let value of base) {
|
||||||
|
if (start) {
|
||||||
|
if (onloop) {
|
||||||
|
onloop();
|
||||||
|
}
|
||||||
|
start = false;
|
||||||
|
}
|
||||||
|
yield value;
|
||||||
|
}
|
||||||
|
start = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator version of "map".
|
||||||
|
*/
|
||||||
|
export function imap<T1, T2>(iterable: Iterable<T1>, mapfunc: (_: T1) => T2): Iterable<T2> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
for (let value of iterable) {
|
||||||
|
yield mapfunc(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator version of "reduce".
|
||||||
|
*/
|
||||||
|
export function ireduce<T>(iterable: Iterable<T>, reduce: (item1: T, item2: T) => T, init: T): T {
|
||||||
|
let result = init;
|
||||||
|
for (let value of iterable) {
|
||||||
|
result = reduce(result, value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator version of "filter".
|
||||||
|
*/
|
||||||
|
export function ifilter<T>(iterable: Iterable<T>, filterfunc: (_: T) => boolean): Iterable<T> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
for (let value of iterable) {
|
||||||
|
if (filterfunc(value)) {
|
||||||
|
yield value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
||||||
|
return ifilter(iterable, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
||||||
|
return ifilter(iterable, (item): item is T => item instanceof classref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine two iterables.
|
||||||
|
*
|
||||||
|
* This iterates through the second one several times, so if one iterator may be infinite,
|
||||||
|
* it should be the first one.
|
||||||
|
*/
|
||||||
|
export function icombine<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1, T2]> {
|
||||||
|
return ichainit(imap(it1, v1 => imap(it2, (v2): [T1, T2] => [v1, v2])));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance through two iterables at the same time, yielding item pairs
|
||||||
|
*
|
||||||
|
* Iteration will stop at the first of the two iterators that stops.
|
||||||
|
*/
|
||||||
|
export function izip<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1, T2]> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let iterator1 = it1[Symbol.iterator]();
|
||||||
|
let iterator2 = it2[Symbol.iterator]();
|
||||||
|
let state1 = iterator1.next();
|
||||||
|
let state2 = iterator2.next();
|
||||||
|
while (!state1.done && !state2.done) {
|
||||||
|
yield [state1.value, state2.value];
|
||||||
|
state1 = iterator1.next();
|
||||||
|
state2 = iterator2.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
export function izipg<T1, T2>(it1: Iterable<T1>, it2: Iterable<T2>): Iterable<[T1 | undefined, T2 | undefined]> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let iterator1 = it1[Symbol.iterator]();
|
||||||
|
let iterator2 = it2[Symbol.iterator]();
|
||||||
|
let state1 = iterator1.next();
|
||||||
|
let state2 = iterator2.next();
|
||||||
|
while (!state1.done || !state2.done) {
|
||||||
|
yield [state1.value, state2.value];
|
||||||
|
state1 = iterator1.next();
|
||||||
|
state2 = iterator2.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>] {
|
||||||
|
return [ifilter(iterable, predicate), ifilter(iterable, x => !predicate(x))];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alternate between several iterables (pick one from the first one, then one from the second...)
|
||||||
|
*/
|
||||||
|
export function ialternate<T>(iterables: Iterable<T>[]): Iterable<T> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let iterators = iterables.map(iterable => iterable[Symbol.iterator]());
|
||||||
|
let done: boolean;
|
||||||
|
do {
|
||||||
|
done = false;
|
||||||
|
// TODO Remove "dried-out" iterators
|
||||||
|
for (let iterator of iterators) {
|
||||||
|
let state = iterator.next();
|
||||||
|
if (!state.done) {
|
||||||
|
done = true;
|
||||||
|
yield state.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (done);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yield items from an iterator only once.
|
||||||
|
*
|
||||||
|
* Beware that even if this function is not materializing, it keeps track of yielded item, and may choke on
|
||||||
|
* infinite or very long streams. Thus, no more than *limit* items will be yielded (an error is thrown
|
||||||
|
* when this limit is reached).
|
||||||
|
*
|
||||||
|
* This function is O(n²)
|
||||||
|
*/
|
||||||
|
export function iunique<T>(iterable: Iterable<T>, limit = 1000000): Iterable<T> {
|
||||||
|
return {
|
||||||
|
[Symbol.iterator]: function* () {
|
||||||
|
let done: T[] = [];
|
||||||
|
let n = limit;
|
||||||
|
for (let value of iterable) {
|
||||||
|
if (!contains(done, value)) {
|
||||||
|
if (n-- > 0) {
|
||||||
|
done.push(value);
|
||||||
|
yield value;
|
||||||
|
} else {
|
||||||
|
throw new Error("Unique count limit on iterator");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common reduce shortcuts
|
||||||
|
*/
|
||||||
|
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 imin = (iterable: Iterable<number>) => ireduce(iterable, Math.min, Infinity);
|
||||||
|
export const imax = (iterable: Iterable<number>) => ireduce(iterable, Math.max, -Infinity);
|
||||||
|
|
|
@ -1,125 +1,123 @@
|
||||||
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;
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("gets a sequential id", check => {
|
test.case("gets a sequential id", check => {
|
||||||
let o1 = new TestRObject();
|
let o1 = new TestRObject();
|
||||||
check.equals(o1.id, 0);
|
check.equals(o1.id, 0);
|
||||||
let o2 = new TestRObject();
|
let o2 = new TestRObject();
|
||||||
let o3 = new TestRObject();
|
let o3 = new TestRObject();
|
||||||
check.equals(o2.id, 1);
|
check.equals(o2.id, 1);
|
||||||
check.equals(o3.id, 2);
|
check.equals(o3.id, 2);
|
||||||
|
|
||||||
check.equals(rid(o3), 2);
|
check.equals(rid(o3), 2);
|
||||||
check.equals(rid(o3.id), 2);
|
check.equals(rid(o3.id), 2);
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("checks object identity", check => {
|
test.case("checks object identity", check => {
|
||||||
let o1 = new TestRObject(1);
|
let o1 = new TestRObject(1);
|
||||||
let o2 = new TestRObject(1);
|
let o2 = new TestRObject(1);
|
||||||
let o3 = duplicate(o1, TK.Specs);
|
let o3 = duplicate(o1, TK.Specs);
|
||||||
|
|
||||||
check.equals(o1.is(o1), true, "o1 is o1");
|
check.equals(o1.is(o1), true, "o1 is o1");
|
||||||
check.equals(o1.is(o2), false, "o1 is not o2");
|
check.equals(o1.is(o2), false, "o1 is not o2");
|
||||||
check.equals(o1.is(o3), true, "o1 is o3");
|
check.equals(o1.is(o3), true, "o1 is o3");
|
||||||
check.equals(o1.is(null), false, "o1 is not null");
|
check.equals(o1.is(null), false, "o1 is not null");
|
||||||
|
|
||||||
check.equals(o2.is(o1), false, "o2 is not o1");
|
check.equals(o2.is(o1), false, "o2 is not o1");
|
||||||
check.equals(o2.is(o2), true, "o2 is o2");
|
check.equals(o2.is(o2), true, "o2 is o2");
|
||||||
check.equals(o2.is(o3), false, "o2 is not o3");
|
check.equals(o2.is(o3), false, "o2 is not o3");
|
||||||
check.equals(o2.is(null), false, "o2 is not null");
|
check.equals(o2.is(null), false, "o2 is not null");
|
||||||
|
|
||||||
check.equals(o3.is(o1), true, "o3 is o1");
|
check.equals(o3.is(o1), true, "o3 is o1");
|
||||||
check.equals(o3.is(o2), false, "o3 is not o2");
|
check.equals(o3.is(o2), false, "o3 is not o2");
|
||||||
check.equals(o3.is(o3), true, "o3 is o3");
|
check.equals(o3.is(o3), true, "o3 is o3");
|
||||||
check.equals(o3.is(null), false, "o3 is not null");
|
check.equals(o3.is(null), false, "o3 is not null");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("resets global id on unserialize", check => {
|
test.case("resets global id on unserialize", check => {
|
||||||
let o1 = new TestRObject();
|
let o1 = new TestRObject();
|
||||||
check.equals(o1.id, 0);
|
check.equals(o1.id, 0);
|
||||||
let o2 = new TestRObject();
|
let o2 = new TestRObject();
|
||||||
check.equals(o2.id, 1);
|
check.equals(o2.id, 1);
|
||||||
|
|
||||||
let serializer = new Serializer(TK.Specs);
|
let serializer = new Serializer(TK.Specs);
|
||||||
let packed = serializer.serialize({ objs: [o1, o2] });
|
let packed = serializer.serialize({ objs: [o1, o2] });
|
||||||
|
|
||||||
(<any>RObject)._next_id = 0;
|
(<any>RObject)._next_id = 0;
|
||||||
|
|
||||||
check.equals(new TestRObject().id, 0);
|
check.equals(new TestRObject().id, 0);
|
||||||
let unpacked = serializer.unserialize(packed);
|
let unpacked = serializer.unserialize(packed);
|
||||||
check.equals(unpacked, { objs: [o1, o2] });
|
check.equals(unpacked, { objs: [o1, o2] });
|
||||||
check.equals(new TestRObject().id, 2);
|
check.equals(new TestRObject().id, 2);
|
||||||
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]);
|
||||||
|
|
||||||
let o2 = new TestRObject();
|
let o2 = new TestRObject();
|
||||||
check.same(store.get(o1.id), o1);
|
check.same(store.get(o1.id), o1);
|
||||||
check.equals(store.get(o2.id), null);
|
check.equals(store.get(o2.id), null);
|
||||||
|
|
||||||
store.add(o2);
|
store.add(o2);
|
||||||
check.same(store.get(o1.id), o1);
|
check.same(store.get(o1.id), o1);
|
||||||
check.same(store.get(o2.id), o2);
|
check.same(store.get(o2.id), o2);
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("lists contained objects", check => {
|
test.case("lists contained objects", check => {
|
||||||
let store = new RObjectContainer<TestRObject>();
|
let store = new RObjectContainer<TestRObject>();
|
||||||
let o1 = store.add(new TestRObject());
|
let o1 = store.add(new TestRObject());
|
||||||
let o2 = store.add(new TestRObject());
|
let o2 = store.add(new TestRObject());
|
||||||
|
|
||||||
check.equals(store.count(), 2, "count=2");
|
check.equals(store.count(), 2, "count=2");
|
||||||
|
|
||||||
let objects = store.list();
|
let objects = store.list();
|
||||||
check.equals(objects.length, 2, "list length=2");
|
check.equals(objects.length, 2, "list length=2");
|
||||||
check.contains(objects, o1, "list contains o1");
|
check.contains(objects, o1, "list contains o1");
|
||||||
check.contains(objects, o2, "list contains o2");
|
check.contains(objects, o2, "list contains o2");
|
||||||
|
|
||||||
objects = imaterialize(store.iterator());
|
objects = imaterialize(store.iterator());
|
||||||
check.equals(objects.length, 2, "list length=2");
|
check.equals(objects.length, 2, "list length=2");
|
||||||
check.contains(objects, o1, "list contains o1");
|
check.contains(objects, o1, "list contains o1");
|
||||||
check.contains(objects, o2, "list contains o2");
|
check.contains(objects, o2, "list contains o2");
|
||||||
|
|
||||||
let ids = store.ids();
|
let ids = store.ids();
|
||||||
check.equals(ids.length, 2, "ids length=2");
|
check.equals(ids.length, 2, "ids length=2");
|
||||||
check.contains(ids, o1.id, "list contains o1.id");
|
check.contains(ids, o1.id, "list contains o1.id");
|
||||||
check.contains(ids, o2.id, "list contains o2.id");
|
check.contains(ids, o2.id, "list contains o2.id");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("removes objects", check => {
|
test.case("removes objects", check => {
|
||||||
let store = new RObjectContainer<TestRObject>();
|
let store = new RObjectContainer<TestRObject>();
|
||||||
let o1 = store.add(new TestRObject());
|
let o1 = store.add(new TestRObject());
|
||||||
let o2 = store.add(new TestRObject());
|
let o2 = store.add(new TestRObject());
|
||||||
|
|
||||||
check.in("initial", check => {
|
check.in("initial", check => {
|
||||||
check.equals(store.count(), 2, "count=2");
|
check.equals(store.count(), 2, "count=2");
|
||||||
check.same(store.get(o1.id), o1, "o1 present");
|
check.same(store.get(o1.id), o1, "o1 present");
|
||||||
check.same(store.get(o2.id), o2, "o2 present");
|
check.same(store.get(o2.id), o2, "o2 present");
|
||||||
});
|
});
|
||||||
|
|
||||||
store.remove(o1);
|
store.remove(o1);
|
||||||
|
|
||||||
check.in("removed o1", check => {
|
check.in("removed o1", check => {
|
||||||
check.equals(store.count(), 1, "count=1");
|
check.equals(store.count(), 1, "count=1");
|
||||||
check.same(store.get(o1.id), null, "o1 missing");
|
check.same(store.get(o1.id), null, "o1 missing");
|
||||||
check.same(store.get(o2.id), o2, "o2 present");
|
check.same(store.get(o2.id), o2, "o2 present");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
|
@ -1,100 +1,101 @@
|
||||||
module TK {
|
import { iarray } from "./Iterators";
|
||||||
export type RObjectId = number
|
import { values } from "./Tools";
|
||||||
|
|
||||||
/**
|
export type RObjectId = number
|
||||||
* Returns the id of an object
|
|
||||||
*/
|
|
||||||
export function rid(obj: RObject | RObjectId): number {
|
|
||||||
return (obj instanceof RObject) ? obj.id : obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Referenced objects, with unique ID.
|
* Returns the id of an object
|
||||||
*
|
*/
|
||||||
* Objects extending this class will have an automatic unique ID, and may be tracked from an RObjectContainer.
|
export function rid(obj: RObject | RObjectId): number {
|
||||||
*/
|
return (obj instanceof RObject) ? obj.id : obj;
|
||||||
export class RObject {
|
}
|
||||||
readonly id: RObjectId = RObject._next_id++
|
|
||||||
private static _next_id = 0
|
/**
|
||||||
|
* Referenced objects, with unique ID.
|
||||||
postUnserialize() {
|
*
|
||||||
if (this.id >= RObject._next_id) {
|
* Objects extending this class will have an automatic unique ID, and may be tracked from an RObjectContainer.
|
||||||
RObject._next_id = this.id + 1;
|
*/
|
||||||
}
|
export class RObject {
|
||||||
}
|
readonly id: RObjectId = RObject._next_id++
|
||||||
|
private static _next_id = 0
|
||||||
/**
|
|
||||||
* Check that two objects are the same (only by comparing their ID)
|
postUnserialize() {
|
||||||
*/
|
if (this.id >= RObject._next_id) {
|
||||||
is(other: RObject | RObjectId | null): boolean {
|
RObject._next_id = this.id + 1;
|
||||||
if (other === null) {
|
}
|
||||||
return false;
|
}
|
||||||
} else if (other instanceof RObject) {
|
|
||||||
return this.id === other.id;
|
/**
|
||||||
} else {
|
* Check that two objects are the same (only by comparing their ID)
|
||||||
return this.id === other;
|
*/
|
||||||
}
|
is(other: RObject | RObjectId | null): boolean {
|
||||||
}
|
if (other === null) {
|
||||||
}
|
return false;
|
||||||
|
} else if (other instanceof RObject) {
|
||||||
/**
|
return this.id === other.id;
|
||||||
* Container to track referenced objects
|
} else {
|
||||||
*/
|
return this.id === other;
|
||||||
export class RObjectContainer<T extends RObject> {
|
}
|
||||||
private objects: { [index: number]: T } = {}
|
}
|
||||||
|
}
|
||||||
constructor(objects: T[] = []) {
|
|
||||||
objects.forEach(obj => this.add(obj));
|
/**
|
||||||
}
|
* Container to track referenced objects
|
||||||
|
*/
|
||||||
/**
|
export class RObjectContainer<T extends RObject> {
|
||||||
* Add an object to the container
|
private objects: { [index: number]: T } = {}
|
||||||
*/
|
|
||||||
add(object: T): T {
|
constructor(objects: T[] = []) {
|
||||||
this.objects[object.id] = object;
|
objects.forEach(obj => this.add(obj));
|
||||||
return object;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Add an object to the container
|
||||||
* Remove an object from the container
|
*/
|
||||||
*/
|
add(object: T): T {
|
||||||
remove(object: T): void {
|
this.objects[object.id] = object;
|
||||||
delete this.objects[object.id];
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an object from the container by its id
|
* Remove an object from the container
|
||||||
*/
|
*/
|
||||||
get(id: RObjectId): T | null {
|
remove(object: T): void {
|
||||||
return this.objects[id] || null;
|
delete this.objects[object.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count the number of objects
|
* Get an object from the container by its id
|
||||||
*/
|
*/
|
||||||
count(): number {
|
get(id: RObjectId): T | null {
|
||||||
return this.list().length;
|
return this.objects[id] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all contained ids (list)
|
* Count the number of objects
|
||||||
*/
|
*/
|
||||||
ids(): RObjectId[] {
|
count(): number {
|
||||||
return this.list().map(obj => obj.id);
|
return this.list().length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all contained objects (list)
|
* Get all contained ids (list)
|
||||||
*/
|
*/
|
||||||
list(): T[] {
|
ids(): RObjectId[] {
|
||||||
return values(this.objects);
|
return this.list().map(obj => obj.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all contained objects (iterator)
|
* Get all contained objects (list)
|
||||||
*/
|
*/
|
||||||
iterator(): Iterable<T> {
|
list(): T[] {
|
||||||
return iarray(this.list());
|
return values(this.objects);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Get all contained objects (iterator)
|
||||||
|
*/
|
||||||
|
iterator(): Iterable<T> {
|
||||||
|
return iarray(this.list());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,92 +1,90 @@
|
||||||
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();
|
|
||||||
|
|
||||||
var i = 100;
|
var i = 100;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
var value = gen.random();
|
var value = gen.random();
|
||||||
check.greaterorequal(value, 0);
|
check.greaterorequal(value, 0);
|
||||||
check.greater(1, value);
|
check.greater(1, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("produces integers", check => {
|
test.case("produces integers", check => {
|
||||||
var gen = new RandomGenerator();
|
var gen = new RandomGenerator();
|
||||||
|
|
||||||
var i = 100;
|
var i = 100;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
var value = gen.randInt(5, 12);
|
var value = gen.randInt(5, 12);
|
||||||
check.equals(Math.round(value), value);
|
check.equals(Math.round(value), value);
|
||||||
check.greater(value, 4);
|
check.greater(value, 4);
|
||||||
check.greater(13, value);
|
check.greater(13, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("chooses from an array", check => {
|
test.case("chooses from an array", check => {
|
||||||
var gen = new RandomGenerator();
|
var gen = new RandomGenerator();
|
||||||
|
|
||||||
check.equals(gen.choice([5]), 5);
|
check.equals(gen.choice([5]), 5);
|
||||||
|
|
||||||
var i = 100;
|
var i = 100;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
var value = gen.choice(["test", "thing"]);
|
var value = gen.choice(["test", "thing"]);
|
||||||
check.contains(["thing", "test"], value);
|
check.contains(["thing", "test"], value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("samples from an array", check => {
|
test.case("samples from an array", check => {
|
||||||
var gen = new RandomGenerator();
|
var gen = new RandomGenerator();
|
||||||
|
|
||||||
var i = 100;
|
var i = 100;
|
||||||
while (i-- > 1) {
|
while (i-- > 1) {
|
||||||
var input = [1, 2, 3, 4, 5];
|
var input = [1, 2, 3, 4, 5];
|
||||||
var sample = gen.sample(input, i % 5 + 1);
|
var sample = gen.sample(input, i % 5 + 1);
|
||||||
check.same(sample.length, i % 5 + 1);
|
check.same(sample.length, i % 5 + 1);
|
||||||
sample.forEach((num, idx) => {
|
sample.forEach((num, idx) => {
|
||||||
check.contains(input, num);
|
check.contains(input, num);
|
||||||
check.notcontains(sample.filter((ival, iidx) => iidx != idx), num);
|
check.notcontains(sample.filter((ival, iidx) => iidx != idx), num);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("choose from weighted ranges", check => {
|
test.case("choose from weighted ranges", check => {
|
||||||
let gen = new RandomGenerator();
|
let gen = new RandomGenerator();
|
||||||
|
|
||||||
check.equals(gen.weighted([]), -1);
|
check.equals(gen.weighted([]), -1);
|
||||||
check.equals(gen.weighted([1]), 0);
|
check.equals(gen.weighted([1]), 0);
|
||||||
check.equals(gen.weighted([0, 1, 0]), 1);
|
check.equals(gen.weighted([0, 1, 0]), 1);
|
||||||
check.equals(gen.weighted([0, 12, 0]), 1);
|
check.equals(gen.weighted([0, 12, 0]), 1);
|
||||||
|
|
||||||
gen = new SkewedRandomGenerator([0, 0.5, 0.7, 0.8, 0.9999]);
|
gen = new SkewedRandomGenerator([0, 0.5, 0.7, 0.8, 0.9999]);
|
||||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 0);
|
check.equals(gen.weighted([4, 3, 0, 2, 1]), 0);
|
||||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 1);
|
check.equals(gen.weighted([4, 3, 0, 2, 1]), 1);
|
||||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 3);
|
check.equals(gen.weighted([4, 3, 0, 2, 1]), 3);
|
||||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 3);
|
check.equals(gen.weighted([4, 3, 0, 2, 1]), 3);
|
||||||
check.equals(gen.weighted([4, 3, 0, 2, 1]), 4);
|
check.equals(gen.weighted([4, 3, 0, 2, 1]), 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("generates ids", check => {
|
test.case("generates ids", check => {
|
||||||
let gen = new SkewedRandomGenerator([0, 0.4, 0.2, 0.1, 0.3, 0.8]);
|
let gen = new SkewedRandomGenerator([0, 0.4, 0.2, 0.1, 0.3, 0.8]);
|
||||||
check.equals(gen.id(6, "abcdefghij"), "aecbdi");
|
check.equals(gen.id(6, "abcdefghij"), "aecbdi");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("can be skewed", check => {
|
test.case("can be skewed", check => {
|
||||||
var gen = new SkewedRandomGenerator([0, 0.5, 0.2, 0.9]);
|
var gen = new SkewedRandomGenerator([0, 0.5, 0.2, 0.9]);
|
||||||
|
|
||||||
check.equals(gen.random(), 0);
|
check.equals(gen.random(), 0);
|
||||||
check.equals(gen.random(), 0.5);
|
check.equals(gen.random(), 0.5);
|
||||||
check.equals(gen.randInt(4, 8), 5);
|
check.equals(gen.randInt(4, 8), 5);
|
||||||
check.equals(gen.random(), 0.9);
|
check.equals(gen.random(), 0.9);
|
||||||
|
|
||||||
var value = gen.random();
|
var value = gen.random();
|
||||||
check.greaterorequal(value, 0);
|
check.greaterorequal(value, 0);
|
||||||
check.greater(1, value);
|
check.greater(1, value);
|
||||||
|
|
||||||
gen = new SkewedRandomGenerator([0.7], true);
|
gen = new SkewedRandomGenerator([0.7], true);
|
||||||
check.equals(gen.random(), 0.7);
|
check.equals(gen.random(), 0.7);
|
||||||
check.equals(gen.random(), 0.7);
|
check.equals(gen.random(), 0.7);
|
||||||
check.equals(gen.random(), 0.7);
|
check.equals(gen.random(), 0.7);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
|
@ -1,114 +1,114 @@
|
||||||
module TK {
|
import { range, sum } from "./Tools";
|
||||||
/*
|
|
||||||
* Random generator.
|
|
||||||
*/
|
|
||||||
export class RandomGenerator {
|
|
||||||
static global: RandomGenerator = new RandomGenerator();
|
|
||||||
|
|
||||||
postUnserialize() {
|
/*
|
||||||
this.random = Math.random;
|
* Random generator.
|
||||||
}
|
*/
|
||||||
|
export class RandomGenerator {
|
||||||
|
static global: RandomGenerator = new RandomGenerator();
|
||||||
|
|
||||||
/**
|
postUnserialize() {
|
||||||
* Get a random number in the (0.0 included -> 1.0 excluded) range
|
this.random = Math.random;
|
||||||
*/
|
}
|
||||||
random = Math.random;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a random number in the (*from* included -> *to* included) range
|
* Get a random number in the (0.0 included -> 1.0 excluded) range
|
||||||
*/
|
*/
|
||||||
randInt(from: number, to: number): number {
|
random = Math.random;
|
||||||
return Math.floor(this.random() * (to - from + 1)) + from;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Choose a random item in an array
|
* Get a random number in the (*from* included -> *to* included) range
|
||||||
*/
|
*/
|
||||||
choice<T>(input: T[]): T {
|
randInt(from: number, to: number): number {
|
||||||
return input[this.randInt(0, input.length - 1)];
|
return Math.floor(this.random() * (to - from + 1)) + from;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Choose a random sample of items from an array
|
* Choose a random item in an array
|
||||||
*/
|
*/
|
||||||
sample<T>(input: T[], count: number): T[] {
|
choice<T>(input: T[]): T {
|
||||||
var minput = input.slice();
|
return input[this.randInt(0, input.length - 1)];
|
||||||
var result: T[] = [];
|
}
|
||||||
while (count--) {
|
|
||||||
var idx = this.randInt(0, minput.length - 1);
|
|
||||||
result.push(minput[idx]);
|
|
||||||
minput.splice(idx, 1);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a random boolean (coin toss)
|
* Choose a random sample of items from an array
|
||||||
*/
|
*/
|
||||||
bool(): boolean {
|
sample<T>(input: T[], count: number): T[] {
|
||||||
return this.randInt(0, 1) == 0;
|
var minput = input.slice();
|
||||||
}
|
var result: T[] = [];
|
||||||
|
while (count--) {
|
||||||
|
var idx = this.randInt(0, minput.length - 1);
|
||||||
|
result.push(minput[idx]);
|
||||||
|
minput.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the range in which the number falls, ranges being weighted
|
* Get a random boolean (coin toss)
|
||||||
*/
|
*/
|
||||||
weighted(weights: number[]): number {
|
bool(): boolean {
|
||||||
if (weights.length == 0) {
|
return this.randInt(0, 1) == 0;
|
||||||
return -1;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let total = sum(weights);
|
/**
|
||||||
if (total == 0) {
|
* Get the range in which the number falls, ranges being weighted
|
||||||
return 0;
|
*/
|
||||||
} else {
|
weighted(weights: number[]): number {
|
||||||
let cumul = 0;
|
if (weights.length == 0) {
|
||||||
weights = weights.map(weight => {
|
return -1;
|
||||||
cumul += weight / total;
|
|
||||||
return cumul;
|
|
||||||
});
|
|
||||||
let r = this.random();
|
|
||||||
for (let i = 0; i < weights.length; i++) {
|
|
||||||
if (r < weights[i]) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return weights.length - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a random id string, composed of ascii characters
|
|
||||||
*/
|
|
||||||
id(length: number, chars?: string): string {
|
|
||||||
if (!chars) {
|
|
||||||
chars = range(94).map(i => String.fromCharCode(i + 33)).join("");
|
|
||||||
}
|
|
||||||
return range(length).map(() => this.choice(<any>chars)).join("");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
let total = sum(weights);
|
||||||
* Random generator that produces a series of fixed numbers before going back to random ones.
|
if (total == 0) {
|
||||||
*/
|
return 0;
|
||||||
export class SkewedRandomGenerator extends RandomGenerator {
|
} else {
|
||||||
i = 0;
|
let cumul = 0;
|
||||||
suite: number[];
|
weights = weights.map(weight => {
|
||||||
loop: boolean;
|
cumul += weight / total;
|
||||||
|
return cumul;
|
||||||
constructor(suite: number[], loop = false) {
|
});
|
||||||
super();
|
let r = this.random();
|
||||||
|
for (let i = 0; i < weights.length; i++) {
|
||||||
this.suite = suite;
|
if (r < weights[i]) {
|
||||||
this.loop = loop;
|
return i;
|
||||||
}
|
|
||||||
|
|
||||||
random = () => {
|
|
||||||
var result = this.suite[this.i];
|
|
||||||
this.i += 1;
|
|
||||||
if (this.loop && this.i == this.suite.length) {
|
|
||||||
this.i = 0;
|
|
||||||
}
|
|
||||||
return (typeof result == "undefined") ? Math.random() : result;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return weights.length - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random id string, composed of ascii characters
|
||||||
|
*/
|
||||||
|
id(length: number, chars?: string): string {
|
||||||
|
if (!chars) {
|
||||||
|
chars = range(94).map(i => String.fromCharCode(i + 33)).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.
|
||||||
|
*/
|
||||||
|
export class SkewedRandomGenerator extends RandomGenerator {
|
||||||
|
i = 0;
|
||||||
|
suite: number[];
|
||||||
|
loop: boolean;
|
||||||
|
|
||||||
|
constructor(suite: number[], loop = false) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.suite = suite;
|
||||||
|
this.loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
random = () => {
|
||||||
|
var result = this.suite[this.i];
|
||||||
|
this.i += 1;
|
||||||
|
if (this.loop && this.i == this.suite.length) {
|
||||||
|
this.i = 0;
|
||||||
|
}
|
||||||
|
return (typeof result == "undefined") ? Math.random() : result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,104 +1,102 @@
|
||||||
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 {
|
|
||||||
a = () => 1
|
|
||||||
b = [(obj: any) => 2]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TestSerializerObj3 {
|
|
||||||
a = [1, 2];
|
|
||||||
postUnserialize() {
|
|
||||||
remove(this.a, 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testing("Serializer", test => {
|
|
||||||
function checkReversability(obj: any, namespace = TK.Specs): any {
|
|
||||||
var serializer = new Serializer(TK.Specs);
|
|
||||||
var data = serializer.serialize(obj);
|
|
||||||
serializer = new Serializer(TK.Specs);
|
|
||||||
var loaded = serializer.unserialize(data);
|
|
||||||
test.check.equals(loaded, obj);
|
|
||||||
return loaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
test.case("serializes simple objects", check => {
|
|
||||||
var obj = {
|
|
||||||
"a": 5,
|
|
||||||
"b": null,
|
|
||||||
"c": [{ "a": 2 }, "test"]
|
|
||||||
};
|
|
||||||
checkReversability(obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("restores objects constructed from class", check => {
|
|
||||||
var loaded = checkReversability(new TestSerializerObj1(5));
|
|
||||||
check.equals(loaded.a, 5);
|
|
||||||
check.same(loaded instanceof TestSerializerObj1, true, "not a TestSerializerObj1 instance");
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("stores one version of the same object", check => {
|
|
||||||
var a = new TestSerializerObj1(8);
|
|
||||||
var b = new TestSerializerObj1(8);
|
|
||||||
var c = {
|
|
||||||
'r': a,
|
|
||||||
's': ["test", a],
|
|
||||||
't': a,
|
|
||||||
'u': b
|
|
||||||
};
|
|
||||||
var loaded = checkReversability(c);
|
|
||||||
check.same(loaded.t, loaded.r);
|
|
||||||
check.same(loaded.s[1], loaded.r);
|
|
||||||
check.notsame(loaded.u, loaded.r);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("handles circular references", check => {
|
|
||||||
var a: any = { b: {} };
|
|
||||||
a.b.c = a;
|
|
||||||
|
|
||||||
var loaded = checkReversability(a);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("ignores some classes", check => {
|
|
||||||
var serializer = new Serializer(TK.Specs);
|
|
||||||
serializer.addIgnoredClass("TestSerializerObj1");
|
|
||||||
|
|
||||||
var data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
|
|
||||||
var loaded = serializer.unserialize(data);
|
|
||||||
|
|
||||||
check.equals(loaded, { a: 5, b: undefined });
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("ignores functions", check => {
|
|
||||||
let serializer = new Serializer(TK.Specs);
|
|
||||||
let data = serializer.serialize({ obj: new TestSerializerObj2() });
|
|
||||||
let loaded = serializer.unserialize(data);
|
|
||||||
|
|
||||||
let expected = <any>new TestSerializerObj2();
|
|
||||||
expected.a = undefined;
|
|
||||||
expected.b[0] = undefined;
|
|
||||||
check.equals(loaded, { obj: expected });
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("calls specific postUnserialize", check => {
|
|
||||||
let serializer = new Serializer(TK.Specs);
|
|
||||||
let data = serializer.serialize({ obj: new TestSerializerObj3() });
|
|
||||||
let loaded = serializer.unserialize(data);
|
|
||||||
|
|
||||||
let expected = new TestSerializerObj3();
|
|
||||||
expected.a = [1];
|
|
||||||
check.equals(loaded, { obj: expected });
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("finds TS namespace, even from sub-namespace", check => {
|
|
||||||
checkReversability(new Timer());
|
|
||||||
checkReversability(new RandomGenerator());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TestSerializerObj2 {
|
||||||
|
a = () => 1
|
||||||
|
b = [(obj: any) => 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestSerializerObj3 {
|
||||||
|
a = [1, 2];
|
||||||
|
postUnserialize() {
|
||||||
|
remove(this.a, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testing("Serializer", test => {
|
||||||
|
function checkReversability(obj: any, namespace = TK.Specs): any {
|
||||||
|
var serializer = new Serializer(TK.Specs);
|
||||||
|
var data = serializer.serialize(obj);
|
||||||
|
serializer = new Serializer(TK.Specs);
|
||||||
|
var loaded = serializer.unserialize(data);
|
||||||
|
test.check.equals(loaded, obj);
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.case("serializes simple objects", check => {
|
||||||
|
var obj = {
|
||||||
|
"a": 5,
|
||||||
|
"b": null,
|
||||||
|
"c": [{ "a": 2 }, "test"]
|
||||||
|
};
|
||||||
|
checkReversability(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("restores objects constructed from class", check => {
|
||||||
|
var loaded = checkReversability(new TestSerializerObj1(5));
|
||||||
|
check.equals(loaded.a, 5);
|
||||||
|
check.same(loaded instanceof TestSerializerObj1, true, "not a TestSerializerObj1 instance");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("stores one version of the same object", check => {
|
||||||
|
var a = new TestSerializerObj1(8);
|
||||||
|
var b = new TestSerializerObj1(8);
|
||||||
|
var c = {
|
||||||
|
'r': a,
|
||||||
|
's': ["test", a],
|
||||||
|
't': a,
|
||||||
|
'u': b
|
||||||
|
};
|
||||||
|
var loaded = checkReversability(c);
|
||||||
|
check.same(loaded.t, loaded.r);
|
||||||
|
check.same(loaded.s[1], loaded.r);
|
||||||
|
check.notsame(loaded.u, loaded.r);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("handles circular references", check => {
|
||||||
|
var a: any = { b: {} };
|
||||||
|
a.b.c = a;
|
||||||
|
|
||||||
|
var loaded = checkReversability(a);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("ignores some classes", check => {
|
||||||
|
var serializer = new Serializer(TK.Specs);
|
||||||
|
serializer.addIgnoredClass("TestSerializerObj1");
|
||||||
|
|
||||||
|
var data = serializer.serialize({ a: 5, b: new TestSerializerObj1() });
|
||||||
|
var loaded = serializer.unserialize(data);
|
||||||
|
|
||||||
|
check.equals(loaded, { a: 5, b: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("ignores functions", check => {
|
||||||
|
let serializer = new Serializer(TK.Specs);
|
||||||
|
let data = serializer.serialize({ obj: new TestSerializerObj2() });
|
||||||
|
let loaded = serializer.unserialize(data);
|
||||||
|
|
||||||
|
let expected = <any>new TestSerializerObj2();
|
||||||
|
expected.a = undefined;
|
||||||
|
expected.b[0] = undefined;
|
||||||
|
check.equals(loaded, { obj: expected });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("calls specific postUnserialize", check => {
|
||||||
|
let serializer = new Serializer(TK.Specs);
|
||||||
|
let data = serializer.serialize({ obj: new TestSerializerObj3() });
|
||||||
|
let loaded = serializer.unserialize(data);
|
||||||
|
|
||||||
|
let expected = new TestSerializerObj3();
|
||||||
|
expected.a = [1];
|
||||||
|
check.equals(loaded, { obj: expected });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("finds TS namespace, even from sub-namespace", check => {
|
||||||
|
checkReversability(new Timer());
|
||||||
|
checkReversability(new RandomGenerator());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,111 +1,105 @@
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a class to the ignore list
|
* Add a class to the ignore list
|
||||||
*/
|
*/
|
||||||
addIgnoredClass(name: string) {
|
addIgnoredClass(name: string) {
|
||||||
this.ignored.push(name);
|
this.ignored.push(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an object from a constructor name
|
* Construct an object from a constructor name
|
||||||
*/
|
*/
|
||||||
constructObject(ctype: string): Object {
|
constructObject(ctype: string): Object {
|
||||||
if (ctype == "Object") {
|
if (ctype == "Object") {
|
||||||
return {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
let cl = this.namespace[ctype];
|
let cl = this.namespace[ctype];
|
||||||
if (cl) {
|
if (cl) {
|
||||||
return Object.create(cl.prototype);
|
return Object.create(cl.prototype);
|
||||||
} else {
|
} else {
|
||||||
cl = (<any>TK)[ctype];
|
console.error("Can't find class", ctype);
|
||||||
if (cl) {
|
return {};
|
||||||
return Object.create(cl.prototype);
|
}
|
||||||
} else {
|
}
|
||||||
console.error("Can't find class", ctype);
|
}
|
||||||
return {};
|
|
||||||
}
|
/**
|
||||||
}
|
* Serialize an object to a string
|
||||||
}
|
*/
|
||||||
}
|
serialize(obj: any): string {
|
||||||
|
// Collect objects
|
||||||
/**
|
var objects: Object[] = [];
|
||||||
* Serialize an object to a string
|
var stats: any = {};
|
||||||
*/
|
crawl(obj, value => {
|
||||||
serialize(obj: any): string {
|
if (isObject(value)) {
|
||||||
// Collect objects
|
var vtype = classname(value);
|
||||||
var objects: Object[] = [];
|
if (vtype != "" && this.ignored.indexOf(vtype) < 0) {
|
||||||
var stats: any = {};
|
stats[vtype] = (stats[vtype] || 0) + 1;
|
||||||
crawl(obj, value => {
|
add(objects, value);
|
||||||
if (isObject(value)) {
|
return value;
|
||||||
var vtype = classname(value);
|
} else {
|
||||||
if (vtype != "" && this.ignored.indexOf(vtype) < 0) {
|
return STOP_CRAWLING;
|
||||||
stats[vtype] = (stats[vtype] || 0) + 1;
|
}
|
||||||
add(objects, value);
|
} else {
|
||||||
return value;
|
return value;
|
||||||
} else {
|
}
|
||||||
return TK.STOP_CRAWLING;
|
});
|
||||||
}
|
//console.log("Serialize stats", stats);
|
||||||
} else {
|
|
||||||
return value;
|
// Serialize objects list, transforming deeper objects to links
|
||||||
}
|
var fobjects = objects.map(value => <Object>{ $c: classname(value), $f: merge({}, value) });
|
||||||
});
|
return JSON.stringify(fobjects, (key, value) => {
|
||||||
//console.log("Serialize stats", stats);
|
if (key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && !value.hasOwnProperty("$i")) {
|
||||||
|
return { $i: objects.indexOf(value) };
|
||||||
// Serialize objects list, transforming deeper objects to links
|
} else {
|
||||||
var fobjects = objects.map(value => <Object>{ $c: classname(value), $f: merge({}, value) });
|
return value;
|
||||||
return JSON.stringify(fobjects, (key, value) => {
|
}
|
||||||
if (key != "$f" && isObject(value) && !value.hasOwnProperty("$c") && !value.hasOwnProperty("$i")) {
|
});
|
||||||
return { $i: objects.indexOf(value) };
|
}
|
||||||
} else {
|
|
||||||
return value;
|
/**
|
||||||
}
|
* Unserialize a string to an object
|
||||||
});
|
*/
|
||||||
}
|
unserialize(data: string): any {
|
||||||
|
// Unserialize objects list
|
||||||
/**
|
var fobjects = JSON.parse(data);
|
||||||
* Unserialize a string to an object
|
|
||||||
*/
|
// Reconstruct objects
|
||||||
unserialize(data: string): any {
|
var objects = fobjects.map((objdata: any) => merge(this.constructObject(objdata['$c']), objdata['$f']));
|
||||||
// Unserialize objects list
|
|
||||||
var fobjects = JSON.parse(data);
|
// Reconnect links
|
||||||
|
crawl(objects, value => {
|
||||||
// Reconstruct objects
|
if (value instanceof Object && value.hasOwnProperty('$i')) {
|
||||||
var objects = fobjects.map((objdata: any) => merge(this.constructObject(objdata['$c']), objdata['$f']));
|
return objects[value['$i']];
|
||||||
|
} else {
|
||||||
// Reconnect links
|
return value;
|
||||||
crawl(objects, value => {
|
}
|
||||||
if (value instanceof Object && value.hasOwnProperty('$i')) {
|
}, true);
|
||||||
return objects[value['$i']];
|
|
||||||
} else {
|
// Post unserialize hooks
|
||||||
return value;
|
crawl(objects[0], value => {
|
||||||
}
|
if (value instanceof Object && typeof value.postUnserialize == "function") {
|
||||||
}, true);
|
value.postUnserialize();
|
||||||
|
}
|
||||||
// Post unserialize hooks
|
});
|
||||||
crawl(objects[0], value => {
|
|
||||||
if (value instanceof Object && typeof value.postUnserialize == "function") {
|
// First object was the root
|
||||||
value.postUnserialize();
|
return objects[0];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// First object was the root
|
|
||||||
return objects[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,330 +1,327 @@
|
||||||
|
import { Timer } from "./Timer";
|
||||||
|
|
||||||
|
export type FakeClock = { forward: (milliseconds: number) => void }
|
||||||
|
export type Mock<F extends Function> = { func: F, getCalls: () => any[][], reset: () => void }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Various testing functions.
|
* Main test suite descriptor
|
||||||
*/
|
*/
|
||||||
module TK {
|
export function testing(desc: string, body: (test: TestSuite) => void) {
|
||||||
export type FakeClock = { forward: (milliseconds: number) => void }
|
if (typeof describe != "undefined") {
|
||||||
export type Mock<F extends Function> = { func: F, getCalls: () => any[][], reset: () => void }
|
describe(desc, () => {
|
||||||
|
beforeEach(() => jasmine.addMatchers(CUSTOM_MATCHERS));
|
||||||
|
|
||||||
/**
|
let test = new TestSuite(desc);
|
||||||
* Main test suite descriptor
|
body(test);
|
||||||
*/
|
});
|
||||||
export function testing(desc: string, body: (test: TestSuite) => void) {
|
}
|
||||||
if (typeof describe != "undefined") {
|
}
|
||||||
describe(desc, () => {
|
|
||||||
beforeEach(() => jasmine.addMatchers(CUSTOM_MATCHERS));
|
|
||||||
|
|
||||||
let test = new TestSuite(desc);
|
/**
|
||||||
body(test);
|
* Test suite (group of test cases)
|
||||||
});
|
*/
|
||||||
}
|
export class TestSuite {
|
||||||
|
private desc: string
|
||||||
|
constructor(desc: string) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a setup step for each case of the suite
|
||||||
|
*/
|
||||||
|
setup(body: Function, cleanup?: Function): void {
|
||||||
|
beforeEach(() => body());
|
||||||
|
if (cleanup) {
|
||||||
|
afterEach(() => cleanup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an asynchronous setup step for each case of the suite
|
||||||
|
*/
|
||||||
|
asetup(body: () => Promise<void>, cleanup?: () => Promise<void>): void {
|
||||||
|
beforeEach(async () => await body());
|
||||||
|
if (cleanup) {
|
||||||
|
afterEach(async () => await cleanup());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describe a single test case
|
||||||
|
*/
|
||||||
|
case(desc: string, body: (ctx: TestContext) => void): void {
|
||||||
|
it(desc, () => {
|
||||||
|
console.debug(`${this.desc} ${desc}`);
|
||||||
|
body(new TestContext())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describe an asynchronous test case
|
||||||
|
*/
|
||||||
|
acase<T>(desc: string, body: (ctx: TestContext) => Promise<T>): void {
|
||||||
|
it(desc, done => {
|
||||||
|
console.debug(`${this.desc} ${desc}`);
|
||||||
|
body(new TestContext()).then(done).catch(done.fail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup fake clock for the suite
|
||||||
|
*/
|
||||||
|
clock(): FakeClock {
|
||||||
|
let current = 0;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
current = 0;
|
||||||
|
jasmine.clock().install();
|
||||||
|
spyOn(Timer, "nowMs").and.callFake(() => current);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
jasmine.clock().uninstall();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
forward: milliseconds => {
|
||||||
|
current += milliseconds;
|
||||||
|
jasmine.clock().tick(milliseconds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Out-of-context assertion helpers
|
||||||
|
*
|
||||||
|
* It is better to use in-context checks, for better information
|
||||||
|
*/
|
||||||
|
get check(): TestContext {
|
||||||
|
return new TestContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A test context, with assertion helpers
|
||||||
|
*/
|
||||||
|
export class TestContext {
|
||||||
|
info: string[];
|
||||||
|
|
||||||
|
constructor(info: string[] = []) {
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a sub context (adds information for all assertions done with this context)
|
||||||
|
*/
|
||||||
|
sub(info: string): TestContext {
|
||||||
|
return new TestContext(this.info.concat([info]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a body in a sub context
|
||||||
|
*/
|
||||||
|
in(info: string, body: (ctx: TestContext) => void): void {
|
||||||
|
body(this.sub(info));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a message, with context information added
|
||||||
|
*/
|
||||||
|
message(message?: string): string | undefined {
|
||||||
|
let parts = this.info;
|
||||||
|
if (message) {
|
||||||
|
parts = parts.concat([message]);
|
||||||
|
}
|
||||||
|
return parts.length ? parts.join(" - ") : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch an object's method with a mock
|
||||||
|
*
|
||||||
|
* Replacement may be:
|
||||||
|
* - undefined to call through
|
||||||
|
* - null to not call anything
|
||||||
|
* - a fake function to call instead
|
||||||
|
*
|
||||||
|
* All patches are removed at the end of a case
|
||||||
|
*/
|
||||||
|
patch<T extends Object, K extends keyof T, F extends T[K] & Function>(obj: T, method: K, replacement?: F | null): Mock<F> {
|
||||||
|
let spy = spyOn(obj, <any>method);
|
||||||
|
if (replacement === null) {
|
||||||
|
spy.and.stub();
|
||||||
|
} else if (replacement) {
|
||||||
|
spy.and.callFake(<any>replacement);
|
||||||
|
} else {
|
||||||
|
spy.and.callThrough();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Test suite (group of test cases)
|
func: <any>spy,
|
||||||
*/
|
getCalls: () => spy.calls.all().map(info => info.args),
|
||||||
export class TestSuite {
|
reset: () => spy.calls.reset()
|
||||||
private desc: string
|
}
|
||||||
constructor(desc: string) {
|
}
|
||||||
this.desc = desc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a setup step for each case of the suite
|
* Create a mock function
|
||||||
*/
|
*/
|
||||||
setup(body: Function, cleanup?: Function): void {
|
mockfunc<F extends Function>(name = "mock", replacement?: F): Mock<F> {
|
||||||
beforeEach(() => body());
|
let spy = jasmine.createSpy(name, <any>replacement);
|
||||||
if (cleanup) {
|
|
||||||
afterEach(() => cleanup());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (replacement) {
|
||||||
* Add an asynchronous setup step for each case of the suite
|
spy = spy.and.callThrough();
|
||||||
*/
|
|
||||||
asetup(body: () => Promise<void>, cleanup?: () => Promise<void>): void {
|
|
||||||
beforeEach(async () => await body());
|
|
||||||
if (cleanup) {
|
|
||||||
afterEach(async () => await cleanup());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Describe a single test case
|
|
||||||
*/
|
|
||||||
case(desc: string, body: (ctx: TestContext) => void): void {
|
|
||||||
it(desc, () => {
|
|
||||||
console.debug(`${this.desc} ${desc}`);
|
|
||||||
body(new TestContext())
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Describe an asynchronous test case
|
|
||||||
*/
|
|
||||||
acase<T>(desc: string, body: (ctx: TestContext) => Promise<T>): void {
|
|
||||||
it(desc, done => {
|
|
||||||
console.debug(`${this.desc} ${desc}`);
|
|
||||||
body(new TestContext()).then(done).catch(done.fail);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setup fake clock for the suite
|
|
||||||
*/
|
|
||||||
clock(): FakeClock {
|
|
||||||
let current = 0;
|
|
||||||
|
|
||||||
beforeEach(function () {
|
|
||||||
current = 0;
|
|
||||||
jasmine.clock().install();
|
|
||||||
spyOn(Timer, "nowMs").and.callFake(() => current);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
jasmine.clock().uninstall();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
forward: milliseconds => {
|
|
||||||
current += milliseconds;
|
|
||||||
jasmine.clock().tick(milliseconds);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Out-of-context assertion helpers
|
|
||||||
*
|
|
||||||
* It is better to use in-context checks, for better information
|
|
||||||
*/
|
|
||||||
get check(): TestContext {
|
|
||||||
return new TestContext();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* A test context, with assertion helpers
|
func: <any>spy,
|
||||||
*/
|
getCalls: () => spy.calls.all().map(info => info.args),
|
||||||
export class TestContext {
|
reset: () => spy.calls.reset()
|
||||||
info: string[];
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(info: string[] = []) {
|
/**
|
||||||
this.info = info;
|
* Check that a mock have been called a given number of times, or with specific args
|
||||||
}
|
*/
|
||||||
|
called(mock: Mock<any>, calls: number | any[][], reset = true): void {
|
||||||
/**
|
if (typeof calls == "number") {
|
||||||
* Create a sub context (adds information for all assertions done with this context)
|
expect(mock.getCalls().length).toEqual(calls, this.message());
|
||||||
*/
|
} else {
|
||||||
sub(info: string): TestContext {
|
expect(mock.getCalls()).toEqual(calls, this.message());
|
||||||
return new TestContext(this.info.concat([info]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a body in a sub context
|
|
||||||
*/
|
|
||||||
in(info: string, body: (ctx: TestContext) => void): void {
|
|
||||||
body(this.sub(info));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a message, with context information added
|
|
||||||
*/
|
|
||||||
message(message?: string): string | undefined {
|
|
||||||
let parts = this.info;
|
|
||||||
if (message) {
|
|
||||||
parts = parts.concat([message]);
|
|
||||||
}
|
|
||||||
return parts.length ? parts.join(" - ") : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch an object's method with a mock
|
|
||||||
*
|
|
||||||
* Replacement may be:
|
|
||||||
* - undefined to call through
|
|
||||||
* - null to not call anything
|
|
||||||
* - a fake function to call instead
|
|
||||||
*
|
|
||||||
* All patches are removed at the end of a case
|
|
||||||
*/
|
|
||||||
patch<T extends Object, K extends keyof T, F extends T[K] & Function>(obj: T, method: K, replacement?: F | null): Mock<F> {
|
|
||||||
let spy = spyOn(obj, <any>method);
|
|
||||||
if (replacement === null) {
|
|
||||||
spy.and.stub();
|
|
||||||
} else if (replacement) {
|
|
||||||
spy.and.callFake(<any>replacement);
|
|
||||||
} else {
|
|
||||||
spy.and.callThrough();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
func: <any>spy,
|
|
||||||
getCalls: () => spy.calls.all().map(info => info.args),
|
|
||||||
reset: () => spy.calls.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a mock function
|
|
||||||
*/
|
|
||||||
mockfunc<F extends Function>(name = "mock", replacement?: F): Mock<F> {
|
|
||||||
let spy = jasmine.createSpy(name, <any>replacement);
|
|
||||||
|
|
||||||
if (replacement) {
|
|
||||||
spy = spy.and.callThrough();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
func: <any>spy,
|
|
||||||
getCalls: () => spy.calls.all().map(info => info.args),
|
|
||||||
reset: () => spy.calls.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that a mock have been called a given number of times, or with specific args
|
|
||||||
*/
|
|
||||||
called(mock: Mock<any>, calls: number | any[][], reset = true): void {
|
|
||||||
if (typeof calls == "number") {
|
|
||||||
expect(mock.getCalls().length).toEqual(calls, this.message());
|
|
||||||
} else {
|
|
||||||
expect(mock.getCalls()).toEqual(calls, this.message());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reset) {
|
|
||||||
mock.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that a function call throws an error
|
|
||||||
*/
|
|
||||||
throw(call: Function, error?: string | Error): void {
|
|
||||||
if (typeof error == "undefined") {
|
|
||||||
expect(call).toThrow();
|
|
||||||
} else if (typeof error == "string") {
|
|
||||||
expect(call).toThrowError(error);
|
|
||||||
} else {
|
|
||||||
expect(call).toThrow(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that an object is an instance of a given type
|
|
||||||
*/
|
|
||||||
instance<T>(obj: any, classref: { new(...args: any[]): T }, message: string): obj is T {
|
|
||||||
let result = obj instanceof classref;
|
|
||||||
expect(result).toBe(true, this.message(message));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that two references are the same object
|
|
||||||
*/
|
|
||||||
same<T extends Object>(ref1: T | null | undefined, ref2: T | null | undefined, message?: string): void {
|
|
||||||
expect(ref1).toBe(ref2, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that two references are not the same object
|
|
||||||
*/
|
|
||||||
notsame<T extends Object>(ref1: T | null, ref2: T | null, message?: string): void {
|
|
||||||
expect(ref1).not.toBe(ref2, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that two values are equal, in the sense of deep comparison
|
|
||||||
*/
|
|
||||||
equals<T>(val1: T, val2: T, message?: string): void {
|
|
||||||
expect(val1).toEqual(val2, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that two values differs, in the sense of deep comparison
|
|
||||||
*/
|
|
||||||
notequals<T>(val1: T, val2: T, message?: string): void {
|
|
||||||
expect(val1).not.toEqual(val2, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that a numerical value is close to another, at a given number of digits precision
|
|
||||||
*/
|
|
||||||
nears(val1: number, val2: number, precision = 8, message?: string): void {
|
|
||||||
if (precision != Math.round(precision)) {
|
|
||||||
throw new Error(`'nears' precision should be integer, not {precision}`);
|
|
||||||
}
|
|
||||||
expect(val1).toBeCloseTo(val2, precision, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that a numerical value is greater than another
|
|
||||||
*/
|
|
||||||
greater(val1: number, val2: number, message?: string): void {
|
|
||||||
expect(val1).toBeGreaterThan(val2, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that a numerical value is greater than or equal to another
|
|
||||||
*/
|
|
||||||
greaterorequal(val1: number, val2: number, message?: string): void {
|
|
||||||
expect(val1).toBeGreaterThanOrEqual(val2, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that a string matches a regex
|
|
||||||
*/
|
|
||||||
regex(pattern: RegExp, value: string, message?: string): void {
|
|
||||||
expect(value).toMatch(pattern, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that an array contains an item
|
|
||||||
*/
|
|
||||||
contains<T>(array: T[], item: T, message?: string): void {
|
|
||||||
expect(array).toContain(item, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that an array does not contain an item
|
|
||||||
*/
|
|
||||||
notcontains<T>(array: T[], item: T, message?: string): void {
|
|
||||||
expect(array).not.toContain(item, this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check than an object contains a set of properties
|
|
||||||
*/
|
|
||||||
containing<T>(val: T, props: Partial<T>, message?: string): void {
|
|
||||||
expect(val).toEqual(jasmine.objectContaining(props), this.message(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fail the whole case
|
|
||||||
*/
|
|
||||||
fail(message?: string): void {
|
|
||||||
fail(this.message(message));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CUSTOM_MATCHERS = {
|
if (reset) {
|
||||||
toEqual: function (util: any, customEqualityTesters: any) {
|
mock.reset();
|
||||||
customEqualityTesters = customEqualityTesters || [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
compare: function (actual: any, expected: any, message?: string) {
|
|
||||||
let result: any = { pass: false };
|
|
||||||
let diffBuilder = (<any>jasmine).DiffBuilder();
|
|
||||||
|
|
||||||
result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder);
|
|
||||||
|
|
||||||
result.message = diffBuilder.getMessage();
|
|
||||||
if (message) {
|
|
||||||
result.message += " " + message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a function call throws an error
|
||||||
|
*/
|
||||||
|
throw(call: Function, error?: string | Error): void {
|
||||||
|
if (typeof error == "undefined") {
|
||||||
|
expect(call).toThrow();
|
||||||
|
} else if (typeof error == "string") {
|
||||||
|
expect(call).toThrowError(error);
|
||||||
|
} else {
|
||||||
|
expect(call).toThrow(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that an object is an instance of a given type
|
||||||
|
*/
|
||||||
|
instance<T>(obj: any, classref: { new(...args: any[]): T }, message: string): obj is T {
|
||||||
|
let result = obj instanceof classref;
|
||||||
|
expect(result).toBe(true, this.message(message));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that two references are the same object
|
||||||
|
*/
|
||||||
|
same<T extends Object>(ref1: T | null | undefined, ref2: T | null | undefined, message?: string): void {
|
||||||
|
expect(ref1).toBe(ref2, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that two references are not the same object
|
||||||
|
*/
|
||||||
|
notsame<T extends Object>(ref1: T | null, ref2: T | null, message?: string): void {
|
||||||
|
expect(ref1).not.toBe(ref2, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that two values are equal, in the sense of deep comparison
|
||||||
|
*/
|
||||||
|
equals<T>(val1: T, val2: T, message?: string): void {
|
||||||
|
expect(val1).toEqual(val2, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that two values differs, in the sense of deep comparison
|
||||||
|
*/
|
||||||
|
notequals<T>(val1: T, val2: T, message?: string): void {
|
||||||
|
expect(val1).not.toEqual(val2, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a numerical value is close to another, at a given number of digits precision
|
||||||
|
*/
|
||||||
|
nears(val1: number, val2: number, precision = 8, message?: string): void {
|
||||||
|
if (precision != Math.round(precision)) {
|
||||||
|
throw new Error(`'nears' precision should be integer, not {precision}`);
|
||||||
|
}
|
||||||
|
expect(val1).toBeCloseTo(val2, precision, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a numerical value is greater than another
|
||||||
|
*/
|
||||||
|
greater(val1: number, val2: number, message?: string): void {
|
||||||
|
expect(val1).toBeGreaterThan(val2, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a numerical value is greater than or equal to another
|
||||||
|
*/
|
||||||
|
greaterorequal(val1: number, val2: number, message?: string): void {
|
||||||
|
expect(val1).toBeGreaterThanOrEqual(val2, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that a string matches a regex
|
||||||
|
*/
|
||||||
|
regex(pattern: RegExp, value: string, message?: string): void {
|
||||||
|
expect(value).toMatch(pattern, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that an array contains an item
|
||||||
|
*/
|
||||||
|
contains<T>(array: T[], item: T, message?: string): void {
|
||||||
|
expect(array).toContain(item, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that an array does not contain an item
|
||||||
|
*/
|
||||||
|
notcontains<T>(array: T[], item: T, message?: string): void {
|
||||||
|
expect(array).not.toContain(item, this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check than an object contains a set of properties
|
||||||
|
*/
|
||||||
|
containing<T>(val: T, props: Partial<T>, message?: string): void {
|
||||||
|
expect(val).toEqual(jasmine.objectContaining(props), this.message(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fail the whole case
|
||||||
|
*/
|
||||||
|
fail(message?: string): void {
|
||||||
|
fail(this.message(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CUSTOM_MATCHERS = {
|
||||||
|
toEqual: function (util: any, customEqualityTesters: any) {
|
||||||
|
customEqualityTesters = customEqualityTesters || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
compare: function (actual: any, expected: any, message?: string) {
|
||||||
|
let result: any = { pass: false };
|
||||||
|
let diffBuilder = (<any>jasmine).DiffBuilder();
|
||||||
|
|
||||||
|
result.pass = util.equals(actual, expected, customEqualityTesters, diffBuilder);
|
||||||
|
|
||||||
|
result.message = diffBuilder.getMessage();
|
||||||
|
if (message) {
|
||||||
|
result.message += " " + message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,117 +1,115 @@
|
||||||
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 => {
|
||||||
let timer = new Timer();
|
let timer = new Timer();
|
||||||
|
|
||||||
let called: any[] = [];
|
let called: any[] = [];
|
||||||
let callback = (item: any) => called.push(item);
|
let callback = (item: any) => called.push(item);
|
||||||
|
|
||||||
let s1 = timer.schedule(50, () => callback(1));
|
let s1 = timer.schedule(50, () => callback(1));
|
||||||
let s2 = timer.schedule(150, () => callback(2));
|
let s2 = timer.schedule(150, () => callback(2));
|
||||||
let s3 = timer.schedule(250, () => callback(3));
|
let s3 = timer.schedule(250, () => callback(3));
|
||||||
|
|
||||||
check.equals(called, []);
|
check.equals(called, []);
|
||||||
clock.forward(100);
|
clock.forward(100);
|
||||||
check.equals(called, [1]);
|
check.equals(called, [1]);
|
||||||
timer.cancel(s1);
|
timer.cancel(s1);
|
||||||
check.equals(called, [1]);
|
check.equals(called, [1]);
|
||||||
clock.forward(100);
|
clock.forward(100);
|
||||||
check.equals(called, [1, 2]);
|
check.equals(called, [1, 2]);
|
||||||
timer.cancel(s3);
|
timer.cancel(s3);
|
||||||
clock.forward(100);
|
clock.forward(100);
|
||||||
check.equals(called, [1, 2]);
|
check.equals(called, [1, 2]);
|
||||||
clock.forward(1000);
|
clock.forward(1000);
|
||||||
check.equals(called, [1, 2]);
|
check.equals(called, [1, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("may cancel all scheduled", check => {
|
test.case("may cancel all scheduled", check => {
|
||||||
let timer = new Timer();
|
let timer = new Timer();
|
||||||
|
|
||||||
let called: any[] = [];
|
let called: any[] = [];
|
||||||
let callback = (item: any) => called.push(item);
|
let callback = (item: any) => called.push(item);
|
||||||
|
|
||||||
timer.schedule(150, () => callback(1));
|
timer.schedule(150, () => callback(1));
|
||||||
timer.schedule(50, () => callback(2));
|
timer.schedule(50, () => callback(2));
|
||||||
timer.schedule(500, () => callback(3));
|
timer.schedule(500, () => callback(3));
|
||||||
|
|
||||||
check.equals(called, []);
|
check.equals(called, []);
|
||||||
|
|
||||||
clock.forward(100);
|
clock.forward(100);
|
||||||
|
|
||||||
check.equals(called, [2]);
|
check.equals(called, [2]);
|
||||||
|
|
||||||
clock.forward(100);
|
clock.forward(100);
|
||||||
|
|
||||||
check.equals(called, [2, 1]);
|
check.equals(called, [2, 1]);
|
||||||
|
|
||||||
timer.cancelAll();
|
timer.cancelAll();
|
||||||
|
|
||||||
clock.forward(1000);
|
clock.forward(1000);
|
||||||
|
|
||||||
check.equals(called, [2, 1]);
|
check.equals(called, [2, 1]);
|
||||||
|
|
||||||
timer.schedule(50, () => callback(4));
|
timer.schedule(50, () => callback(4));
|
||||||
timer.schedule(150, () => callback(5));
|
timer.schedule(150, () => callback(5));
|
||||||
|
|
||||||
clock.forward(100);
|
clock.forward(100);
|
||||||
|
|
||||||
check.equals(called, [2, 1, 4]);
|
check.equals(called, [2, 1, 4]);
|
||||||
|
|
||||||
timer.cancelAll(true);
|
timer.cancelAll(true);
|
||||||
|
|
||||||
clock.forward(100);
|
clock.forward(100);
|
||||||
|
|
||||||
check.equals(called, [2, 1, 4]);
|
check.equals(called, [2, 1, 4]);
|
||||||
|
|
||||||
timer.schedule(50, () => callback(6));
|
timer.schedule(50, () => callback(6));
|
||||||
|
|
||||||
clock.forward(100);
|
clock.forward(100);
|
||||||
|
|
||||||
check.equals(called, [2, 1, 4]);
|
check.equals(called, [2, 1, 4]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("may switch to synchronous mode", check => {
|
test.case("may switch to synchronous mode", check => {
|
||||||
let timer = new Timer(true);
|
let timer = new Timer(true);
|
||||||
let called: any[] = [];
|
let called: any[] = [];
|
||||||
let callback = (item: any) => called.push(item);
|
let callback = (item: any) => called.push(item);
|
||||||
|
|
||||||
timer.schedule(50, () => callback(1));
|
timer.schedule(50, () => callback(1));
|
||||||
check.equals(called, [1]);
|
check.equals(called, [1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.acase("sleeps asynchronously", async check => {
|
test.acase("sleeps asynchronously", async check => {
|
||||||
let timer = new Timer();
|
let timer = new Timer();
|
||||||
let x = 1;
|
let x = 1;
|
||||||
|
|
||||||
let promise = timer.sleep(500).then(() => {
|
let promise = timer.sleep(500).then(() => {
|
||||||
x++;
|
x++;
|
||||||
});
|
|
||||||
check.equals(x, 1);
|
|
||||||
|
|
||||||
clock.forward(300);
|
|
||||||
check.equals(x, 1);
|
|
||||||
|
|
||||||
clock.forward(300);
|
|
||||||
check.equals(x, 1);
|
|
||||||
|
|
||||||
await promise;
|
|
||||||
check.equals(x, 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("gives current time in milliseconds", check => {
|
|
||||||
check.equals(Timer.nowMs(), 0);
|
|
||||||
|
|
||||||
clock.forward(5);
|
|
||||||
|
|
||||||
check.equals(Timer.nowMs(), 5);
|
|
||||||
let t = Timer.nowMs();
|
|
||||||
|
|
||||||
clock.forward(10);
|
|
||||||
|
|
||||||
check.equals(Timer.nowMs(), 15);
|
|
||||||
check.equals(Timer.fromMs(t), 10);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
check.equals(x, 1);
|
||||||
|
|
||||||
|
clock.forward(300);
|
||||||
|
check.equals(x, 1);
|
||||||
|
|
||||||
|
clock.forward(300);
|
||||||
|
check.equals(x, 1);
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
check.equals(x, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("gives current time in milliseconds", check => {
|
||||||
|
check.equals(Timer.nowMs(), 0);
|
||||||
|
|
||||||
|
clock.forward(5);
|
||||||
|
|
||||||
|
check.equals(Timer.nowMs(), 5);
|
||||||
|
let t = Timer.nowMs();
|
||||||
|
|
||||||
|
clock.forward(10);
|
||||||
|
|
||||||
|
check.equals(Timer.nowMs(), 15);
|
||||||
|
check.equals(Timer.fromMs(t), 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,100 +1,100 @@
|
||||||
module TK {
|
import { add, remove } from "./Tools";
|
||||||
/**
|
|
||||||
* Timing utility.
|
|
||||||
*
|
|
||||||
* This extends the standard setTimeout feature.
|
|
||||||
*/
|
|
||||||
export class Timer {
|
|
||||||
// Global timer shared by the whole project
|
|
||||||
static global = new Timer();
|
|
||||||
|
|
||||||
// Global synchronous timer for unit tests
|
/**
|
||||||
static synchronous = new Timer(true);
|
* Timing utility.
|
||||||
|
*
|
||||||
|
* This extends the standard setTimeout feature.
|
||||||
|
*/
|
||||||
|
export class Timer {
|
||||||
|
// Global timer shared by the whole project
|
||||||
|
static global = new Timer();
|
||||||
|
|
||||||
private sync = false;
|
// Global synchronous timer for unit tests
|
||||||
|
static synchronous = new Timer(true);
|
||||||
|
|
||||||
private locked = false;
|
private sync = false;
|
||||||
|
|
||||||
private scheduled: any[] = [];
|
private locked = false;
|
||||||
|
|
||||||
constructor(sync = false) {
|
private scheduled: any[] = [];
|
||||||
this.sync = sync;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
constructor(sync = false) {
|
||||||
* Get the current timestamp in milliseconds
|
this.sync = sync;
|
||||||
*/
|
}
|
||||||
static nowMs(): number {
|
|
||||||
return (new Date()).getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the elapsed time in milliseconds since a previous timestamp
|
* Get the current timestamp in milliseconds
|
||||||
*/
|
*/
|
||||||
static fromMs(previous: number): number {
|
static nowMs(): number {
|
||||||
return this.nowMs() - previous;
|
return (new Date()).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if the timer is synchronous
|
* Get the elapsed time in milliseconds since a previous timestamp
|
||||||
*/
|
*/
|
||||||
isSynchronous() {
|
static fromMs(previous: number): number {
|
||||||
return this.sync;
|
return this.nowMs() - previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel all scheduled calls.
|
* Return true if the timer is synchronous
|
||||||
*
|
*/
|
||||||
* If lock=true, no further scheduling will be allowed.
|
isSynchronous() {
|
||||||
*/
|
return this.sync;
|
||||||
cancelAll(lock = false) {
|
}
|
||||||
this.locked = lock;
|
|
||||||
|
|
||||||
let scheduled = this.scheduled;
|
/**
|
||||||
this.scheduled = [];
|
* Cancel all scheduled calls.
|
||||||
|
*
|
||||||
|
* If lock=true, no further scheduling will be allowed.
|
||||||
|
*/
|
||||||
|
cancelAll(lock = false) {
|
||||||
|
this.locked = lock;
|
||||||
|
|
||||||
scheduled.forEach(handle => clearTimeout(handle));
|
let scheduled = this.scheduled;
|
||||||
}
|
this.scheduled = [];
|
||||||
|
|
||||||
/**
|
scheduled.forEach(handle => clearTimeout(handle));
|
||||||
* Cancel a scheduled callback.
|
}
|
||||||
*/
|
|
||||||
cancel(scheduled: any) {
|
|
||||||
if (remove(this.scheduled, scheduled)) {
|
|
||||||
clearTimeout(scheduled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule a callback to be called later (time is in milliseconds).
|
* Cancel a scheduled callback.
|
||||||
*/
|
*/
|
||||||
schedule(delay: number, callback: Function): any {
|
cancel(scheduled: any) {
|
||||||
if (this.locked) {
|
if (remove(this.scheduled, scheduled)) {
|
||||||
return null;
|
clearTimeout(scheduled);
|
||||||
} else if (this.sync || delay <= 0) {
|
|
||||||
callback();
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
let handle = setTimeout(() => {
|
|
||||||
remove(this.scheduled, handle);
|
|
||||||
callback();
|
|
||||||
}, delay);
|
|
||||||
add(this.scheduled, handle);
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously sleep a given time.
|
|
||||||
*/
|
|
||||||
async sleep(ms: number): Promise<any> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
this.schedule(ms, resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
postUnserialize() {
|
|
||||||
this.scheduled = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a callback to be called later (time is in milliseconds).
|
||||||
|
*/
|
||||||
|
schedule(delay: number, callback: Function): any {
|
||||||
|
if (this.locked) {
|
||||||
|
return null;
|
||||||
|
} else if (this.sync || delay <= 0) {
|
||||||
|
callback();
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
let handle = setTimeout(() => {
|
||||||
|
remove(this.scheduled, handle);
|
||||||
|
callback();
|
||||||
|
}, delay);
|
||||||
|
add(this.scheduled, handle);
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously sleep a given time.
|
||||||
|
*/
|
||||||
|
async sleep(ms: number): Promise<any> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.schedule(ms, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
postUnserialize() {
|
||||||
|
this.scheduled = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,158 +1,156 @@
|
||||||
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();
|
|
||||||
|
|
||||||
test.setup(function () {
|
test.setup(function () {
|
||||||
on_calls = 0;
|
on_calls = 0;
|
||||||
off_calls = 0;
|
off_calls = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
function newToggle(): Toggle {
|
function newToggle(): Toggle {
|
||||||
return new Toggle(() => on_calls++, () => off_calls++);
|
return new Toggle(() => on_calls++, () => off_calls++);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkstate(on: number, off: number) {
|
function checkstate(on: number, off: number) {
|
||||||
test.check.same(on_calls, on);
|
test.check.same(on_calls, on);
|
||||||
test.check.same(off_calls, off);
|
test.check.same(off_calls, off);
|
||||||
on_calls = 0;
|
on_calls = 0;
|
||||||
off_calls = 0;
|
off_calls = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
test.case("toggles on and off", check => {
|
test.case("toggles on and off", check => {
|
||||||
let toggle = newToggle();
|
let toggle = newToggle();
|
||||||
let client = toggle.manipulate("test");
|
let client = toggle.manipulate("test");
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
let result = client(true);
|
let result = client(true);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(1, 0);
|
checkstate(1, 0);
|
||||||
|
|
||||||
result = client(true);
|
result = client(true);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
clock.forward(10000000);
|
clock.forward(10000000);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
result = client(false);
|
result = client(false);
|
||||||
check.equals(result, false);
|
check.equals(result, false);
|
||||||
checkstate(0, 1);
|
checkstate(0, 1);
|
||||||
|
|
||||||
result = client(false);
|
result = client(false);
|
||||||
check.equals(result, false);
|
check.equals(result, false);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
clock.forward(10000000);
|
clock.forward(10000000);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
result = client(true);
|
result = client(true);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(1, 0);
|
checkstate(1, 0);
|
||||||
|
|
||||||
let client2 = toggle.manipulate("test2");
|
let client2 = toggle.manipulate("test2");
|
||||||
result = client2(true);
|
result = client2(true);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
result = client(false);
|
result = client(false);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
result = client2(false);
|
result = client2(false);
|
||||||
check.equals(result, false);
|
check.equals(result, false);
|
||||||
checkstate(0, 1);
|
checkstate(0, 1);
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("switches between on and off", check => {
|
test.case("switches between on and off", check => {
|
||||||
let toggle = newToggle();
|
let toggle = newToggle();
|
||||||
let client = toggle.manipulate("test");
|
let client = toggle.manipulate("test");
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
let result = client();
|
let result = client();
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(1, 0);
|
checkstate(1, 0);
|
||||||
|
|
||||||
result = client();
|
result = client();
|
||||||
check.equals(result, false);
|
check.equals(result, false);
|
||||||
checkstate(0, 1);
|
checkstate(0, 1);
|
||||||
|
|
||||||
result = client();
|
result = client();
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(1, 0);
|
checkstate(1, 0);
|
||||||
|
|
||||||
let client2 = toggle.manipulate("test2");
|
let client2 = toggle.manipulate("test2");
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
result = client2();
|
result = client2();
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
result = client();
|
result = client();
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
result = client2();
|
result = client2();
|
||||||
check.equals(result, false);
|
check.equals(result, false);
|
||||||
checkstate(0, 1);
|
checkstate(0, 1);
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("toggles on for a given time", check => {
|
test.case("toggles on for a given time", check => {
|
||||||
let toggle = newToggle();
|
let toggle = newToggle();
|
||||||
let client = toggle.manipulate("test");
|
let client = toggle.manipulate("test");
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
|
|
||||||
let result = client(100);
|
let result = client(100);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(1, 0);
|
checkstate(1, 0);
|
||||||
|
|
||||||
check.equals(toggle.isOn(), true);
|
check.equals(toggle.isOn(), true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
clock.forward(60);
|
clock.forward(60);
|
||||||
check.equals(toggle.isOn(), true);
|
check.equals(toggle.isOn(), true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
clock.forward(60);
|
clock.forward(60);
|
||||||
check.equals(toggle.isOn(), false);
|
check.equals(toggle.isOn(), false);
|
||||||
checkstate(0, 1);
|
checkstate(0, 1);
|
||||||
|
|
||||||
result = client(100);
|
result = client(100);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(1, 0);
|
checkstate(1, 0);
|
||||||
result = client(200);
|
result = client(200);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
clock.forward(150);
|
clock.forward(150);
|
||||||
check.equals(toggle.isOn(), true);
|
check.equals(toggle.isOn(), true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
clock.forward(150);
|
clock.forward(150);
|
||||||
check.equals(toggle.isOn(), false);
|
check.equals(toggle.isOn(), false);
|
||||||
checkstate(0, 1);
|
checkstate(0, 1);
|
||||||
|
|
||||||
let client2 = toggle.manipulate("test2");
|
let client2 = toggle.manipulate("test2");
|
||||||
result = client(100);
|
result = client(100);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(1, 0);
|
checkstate(1, 0);
|
||||||
result = client2(200);
|
result = client2(200);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
clock.forward(150);
|
clock.forward(150);
|
||||||
check.equals(toggle.isOn(), true);
|
check.equals(toggle.isOn(), true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
clock.forward(150);
|
clock.forward(150);
|
||||||
check.equals(toggle.isOn(), false);
|
check.equals(toggle.isOn(), false);
|
||||||
checkstate(0, 1);
|
checkstate(0, 1);
|
||||||
|
|
||||||
result = client(100);
|
result = client(100);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(1, 0);
|
checkstate(1, 0);
|
||||||
result = client(true);
|
result = client(true);
|
||||||
check.equals(result, true);
|
check.equals(result, true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
check.equals(toggle.isOn(), true);
|
check.equals(toggle.isOn(), true);
|
||||||
clock.forward(2000);
|
clock.forward(2000);
|
||||||
check.equals(toggle.isOn(), true);
|
check.equals(toggle.isOn(), true);
|
||||||
checkstate(0, 0);
|
checkstate(0, 0);
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
|
@ -1,93 +1,94 @@
|
||||||
module TK {
|
import { Timer } from "./Timer"
|
||||||
/**
|
import { add, contains, remove } from "./Tools"
|
||||||
* Client for Toggle object, allowing to manipulate it
|
|
||||||
*
|
|
||||||
* *state* may be:
|
|
||||||
* - a boolean to require on or off
|
|
||||||
* - a number to require on for the duration in milliseconds
|
|
||||||
* - undefined to switch between on and off (based on the client state, not the toggle state)
|
|
||||||
*
|
|
||||||
* The function returns the actual state after applying the requirement
|
|
||||||
*/
|
|
||||||
export type ToggleClient = (state?: boolean | number) => boolean
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A toggle between two states (on and off).
|
* Client for Toggle object, allowing to manipulate it
|
||||||
*
|
*
|
||||||
* A toggle will be on if at least one ToggleClient requires it to be on.
|
* *state* may be:
|
||||||
*/
|
* - a boolean to require on or off
|
||||||
export class Toggle {
|
* - a number to require on for the duration in milliseconds
|
||||||
private on: Function
|
* - undefined to switch between on and off (based on the client state, not the toggle state)
|
||||||
private off: Function
|
*
|
||||||
private status = false
|
* The function returns the actual state after applying the requirement
|
||||||
private clients: string[] = []
|
*/
|
||||||
private scheduled: { [client: string]: any } = {}
|
export type ToggleClient = (state?: boolean | number) => boolean
|
||||||
private timer = Timer.global
|
|
||||||
|
|
||||||
constructor(on: Function = () => null, off: Function = () => null) {
|
/**
|
||||||
this.on = on;
|
* A toggle between two states (on and off).
|
||||||
this.off = off;
|
*
|
||||||
|
* A toggle will be on if at least one ToggleClient requires it to be on.
|
||||||
|
*/
|
||||||
|
export class Toggle {
|
||||||
|
private on: Function
|
||||||
|
private off: Function
|
||||||
|
private status = false
|
||||||
|
private clients: string[] = []
|
||||||
|
private scheduled: { [client: string]: any } = {}
|
||||||
|
private timer = Timer.global
|
||||||
|
|
||||||
|
constructor(on: Function = () => null, off: Function = () => null) {
|
||||||
|
this.on = on;
|
||||||
|
this.off = off;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current state is on
|
||||||
|
*/
|
||||||
|
isOn(): boolean {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a client to manipulate the toggle's state
|
||||||
|
*/
|
||||||
|
manipulate(client: string): ToggleClient {
|
||||||
|
return state => {
|
||||||
|
if (this.scheduled[client]) {
|
||||||
|
this.timer.cancel(this.scheduled[client]);
|
||||||
|
this.scheduled[client] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof state == "undefined") {
|
||||||
|
if (contains(this.clients, client)) {
|
||||||
|
this.stop(client);
|
||||||
|
} else {
|
||||||
|
this.start(client);
|
||||||
}
|
}
|
||||||
|
} else if (typeof state == "number") {
|
||||||
/**
|
if (state > 0) {
|
||||||
* Check if the current state is on
|
this.start(client);
|
||||||
*/
|
this.scheduled[client] = this.timer.schedule(state, () => this.stop(client));
|
||||||
isOn(): boolean {
|
} else {
|
||||||
return this.status;
|
this.stop(client);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a client to manipulate the toggle's state
|
|
||||||
*/
|
|
||||||
manipulate(client: string): ToggleClient {
|
|
||||||
return state => {
|
|
||||||
if (this.scheduled[client]) {
|
|
||||||
this.timer.cancel(this.scheduled[client]);
|
|
||||||
this.scheduled[client] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof state == "undefined") {
|
|
||||||
if (contains(this.clients, client)) {
|
|
||||||
this.stop(client);
|
|
||||||
} else {
|
|
||||||
this.start(client);
|
|
||||||
}
|
|
||||||
} else if (typeof state == "number") {
|
|
||||||
if (state > 0) {
|
|
||||||
this.start(client);
|
|
||||||
this.scheduled[client] = this.timer.schedule(state, () => this.stop(client));
|
|
||||||
} else {
|
|
||||||
this.stop(client);
|
|
||||||
}
|
|
||||||
} else if (!state) {
|
|
||||||
this.stop(client);
|
|
||||||
} else {
|
|
||||||
this.start(client);
|
|
||||||
}
|
|
||||||
return this.status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the toggle for a client (set the status *on*)
|
|
||||||
*/
|
|
||||||
private start(client: string) {
|
|
||||||
add(this.clients, client);
|
|
||||||
if (!this.status) {
|
|
||||||
this.status = true;
|
|
||||||
this.on();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the toggle (set the status *off*)
|
|
||||||
*/
|
|
||||||
private stop(client: string) {
|
|
||||||
remove(this.clients, client);
|
|
||||||
if (this.status && this.clients.length == 0) {
|
|
||||||
this.status = false;
|
|
||||||
this.off();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (!state) {
|
||||||
|
this.stop(client);
|
||||||
|
} else {
|
||||||
|
this.start(client);
|
||||||
|
}
|
||||||
|
return this.status;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the toggle for a client (set the status *on*)
|
||||||
|
*/
|
||||||
|
private start(client: string) {
|
||||||
|
add(this.clients, client);
|
||||||
|
if (!this.status) {
|
||||||
|
this.status = true;
|
||||||
|
this.on();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the toggle (set the status *off*)
|
||||||
|
*/
|
||||||
|
private stop(client: string) {
|
||||||
|
remove(this.clients, client);
|
||||||
|
if (this.status && this.clients.length == 0) {
|
||||||
|
this.status = false;
|
||||||
|
this.off();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
1271
src/common/Tools.ts
|
@ -1,29 +1,27 @@
|
||||||
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 => {
|
|
||||||
test.case("snaps coordinates to the nearest grid point, on a biased grid", check => {
|
|
||||||
let grid = new HexagonalArenaGrid(4, 0.75);
|
|
||||||
checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0);
|
|
||||||
checkLocation(check, grid.snap({ x: 1, y: 0 }), 0, 0);
|
|
||||||
checkLocation(check, grid.snap({ x: 1.9, y: 0 }), 0, 0);
|
|
||||||
checkLocation(check, grid.snap({ x: 2.1, y: 0 }), 4, 0);
|
|
||||||
checkLocation(check, grid.snap({ x: 1, y: 1 }), 0, 0);
|
|
||||||
checkLocation(check, grid.snap({ x: 1, y: 2 }), 2, 3);
|
|
||||||
checkLocation(check, grid.snap({ x: -1, y: -1 }), 0, 0);
|
|
||||||
checkLocation(check, grid.snap({ x: -2, y: -2 }), -2, -3);
|
|
||||||
checkLocation(check, grid.snap({ x: -3, y: -1 }), -4, 0);
|
|
||||||
checkLocation(check, grid.snap({ x: 6, y: -5 }), 8, -6);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("snaps coordinates to the nearest grid point, on a regular grid", check => {
|
|
||||||
let grid = new HexagonalArenaGrid(10);
|
|
||||||
checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 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));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testing("HexagonalArenaGrid", test => {
|
||||||
|
test.case("snaps coordinates to the nearest grid point, on a biased grid", check => {
|
||||||
|
let grid = new HexagonalArenaGrid(4, 0.75);
|
||||||
|
checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 0);
|
||||||
|
checkLocation(check, grid.snap({ x: 1, y: 0 }), 0, 0);
|
||||||
|
checkLocation(check, grid.snap({ x: 1.9, y: 0 }), 0, 0);
|
||||||
|
checkLocation(check, grid.snap({ x: 2.1, y: 0 }), 4, 0);
|
||||||
|
checkLocation(check, grid.snap({ x: 1, y: 1 }), 0, 0);
|
||||||
|
checkLocation(check, grid.snap({ x: 1, y: 2 }), 2, 3);
|
||||||
|
checkLocation(check, grid.snap({ x: -1, y: -1 }), 0, 0);
|
||||||
|
checkLocation(check, grid.snap({ x: -2, y: -2 }), -2, -3);
|
||||||
|
checkLocation(check, grid.snap({ x: -3, y: -1 }), -4, 0);
|
||||||
|
checkLocation(check, grid.snap({ x: 6, y: -5 }), 8, -6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("snaps coordinates to the nearest grid point, on a regular grid", check => {
|
||||||
|
let grid = new HexagonalArenaGrid(10);
|
||||||
|
checkLocation(check, grid.snap({ x: 0, y: 0 }), 0, 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
module TK.SpaceTac {
|
import { ArenaLocation, IArenaLocation } from "./ArenaLocation";
|
||||||
/**
|
|
||||||
* Abstract grid for the arena where the battle takes place
|
|
||||||
*
|
|
||||||
* The grid is used to snap arena coordinates for ships and targets
|
|
||||||
*/
|
|
||||||
export interface IArenaGrid {
|
|
||||||
snap(loc: IArenaLocation): IArenaLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hexagonal unbounded arena grid
|
* Abstract grid for the arena where the battle takes place
|
||||||
*
|
*
|
||||||
* This grid is composed of regular hexagons where all vertices are at a same distance "unit" of the hexagon center
|
* The grid is used to snap arena coordinates for ships and targets
|
||||||
*/
|
*/
|
||||||
export class HexagonalArenaGrid implements IArenaGrid {
|
export interface IArenaGrid {
|
||||||
private yunit: number;
|
snap(loc: IArenaLocation): IArenaLocation;
|
||||||
|
}
|
||||||
constructor(private unit: number, private yfactor = Math.sqrt(0.75)) {
|
|
||||||
this.yunit = unit * yfactor;
|
/**
|
||||||
}
|
* Hexagonal unbounded arena grid
|
||||||
|
*
|
||||||
snap(loc: IArenaLocation): IArenaLocation {
|
* This grid is composed of regular hexagons where all vertices are at a same distance "unit" of the hexagon center
|
||||||
let yr = Math.round(loc.y / this.yunit);
|
*/
|
||||||
let xr: number;
|
export class HexagonalArenaGrid implements IArenaGrid {
|
||||||
if (yr % 2 == 0) {
|
private yunit: number;
|
||||||
xr = Math.round(loc.x / this.unit);
|
|
||||||
} else {
|
constructor(private unit: number, private yfactor = Math.sqrt(0.75)) {
|
||||||
xr = Math.round((loc.x - 0.5 * this.unit) / this.unit) + 0.5;
|
this.yunit = unit * yfactor;
|
||||||
}
|
}
|
||||||
return new ArenaLocation((xr * this.unit) || 0, (yr * this.yunit) || 0);
|
|
||||||
}
|
snap(loc: IArenaLocation): IArenaLocation {
|
||||||
}
|
let yr = Math.round(loc.y / this.yunit);
|
||||||
|
let xr: number;
|
||||||
|
if (yr % 2 == 0) {
|
||||||
|
xr = Math.round(loc.x / this.unit);
|
||||||
|
} else {
|
||||||
|
xr = Math.round((loc.x - 0.5 * this.unit) / this.unit) + 0.5;
|
||||||
|
}
|
||||||
|
return new ArenaLocation((xr * this.unit) || 0, (yr * this.yunit) || 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,20 @@
|
||||||
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);
|
})
|
||||||
})
|
|
||||||
|
|
||||||
test.case("computes an angular difference", check => {
|
test.case("computes an angular difference", check => {
|
||||||
check.equals(angularDifference(0.5, 1.5), 1.0);
|
check.equals(angularDifference(0.5, 1.5), 1.0);
|
||||||
check.nears(angularDifference(0.5, 1.5 + Math.PI * 6), 1.0);
|
check.nears(angularDifference(0.5, 1.5 + Math.PI * 6), 1.0);
|
||||||
check.same(angularDifference(0.5, -0.5), -1.0);
|
check.same(angularDifference(0.5, -0.5), -1.0);
|
||||||
check.nears(angularDifference(0.5, -0.3 - Math.PI * 4), -0.8);
|
check.nears(angularDifference(0.5, -0.3 - Math.PI * 4), -0.8);
|
||||||
check.nears(angularDifference(-3 * Math.PI / 4, 3 * Math.PI / 4), -Math.PI / 2);
|
check.nears(angularDifference(-3 * Math.PI / 4, 3 * Math.PI / 4), -Math.PI / 2);
|
||||||
check.nears(angularDifference(3 * Math.PI / 4, -3 * Math.PI / 4), Math.PI / 2);
|
check.nears(angularDifference(3 * Math.PI / 4, -3 * Math.PI / 4), Math.PI / 2);
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("converts between degrees and radians", check => {
|
test.case("converts between degrees and radians", check => {
|
||||||
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
|
@ -1,99 +1,97 @@
|
||||||
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
|
|
||||||
|
constructor(x = 0, y = 0) {
|
||||||
constructor(x = 0, y = 0) {
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,381 +1,379 @@
|
||||||
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();
|
|
||||||
|
var ship1 = new Ship(fleet1, "F1S1");
|
||||||
var ship1 = new Ship(fleet1, "F1S1");
|
TestTools.setAttribute(ship1, "initiative", 2);
|
||||||
TestTools.setAttribute(ship1, "initiative", 2);
|
var ship2 = new Ship(fleet1, "F1S2");
|
||||||
var ship2 = new Ship(fleet1, "F1S2");
|
TestTools.setAttribute(ship2, "initiative", 4);
|
||||||
TestTools.setAttribute(ship2, "initiative", 4);
|
var ship3 = new Ship(fleet1, "F1S3");
|
||||||
var ship3 = new Ship(fleet1, "F1S3");
|
TestTools.setAttribute(ship3, "initiative", 1);
|
||||||
TestTools.setAttribute(ship3, "initiative", 1);
|
var ship4 = new Ship(fleet2, "F2S1");
|
||||||
var ship4 = new Ship(fleet2, "F2S1");
|
TestTools.setAttribute(ship4, "initiative", 8);
|
||||||
TestTools.setAttribute(ship4, "initiative", 8);
|
var ship5 = new Ship(fleet2, "F2S2");
|
||||||
var ship5 = new Ship(fleet2, "F2S2");
|
TestTools.setAttribute(ship5, "initiative", 2);
|
||||||
TestTools.setAttribute(ship5, "initiative", 2);
|
|
||||||
|
var battle = new Battle(fleet1, fleet2);
|
||||||
var battle = new Battle(fleet1, fleet2);
|
check.equals(battle.play_order.length, 0);
|
||||||
check.equals(battle.play_order.length, 0);
|
|
||||||
|
var gen = new SkewedRandomGenerator([1.0, 0.1, 1.0, 0.2, 0.6]);
|
||||||
var gen = new SkewedRandomGenerator([1.0, 0.1, 1.0, 0.2, 0.6]);
|
battle.throwInitiative(gen);
|
||||||
battle.throwInitiative(gen);
|
|
||||||
|
check.equals(battle.play_order.length, 5);
|
||||||
check.equals(battle.play_order.length, 5);
|
check.equals(battle.play_order, [ship1, ship4, ship5, ship3, ship2]);
|
||||||
check.equals(battle.play_order, [ship1, ship4, ship5, ship3, ship2]);
|
});
|
||||||
});
|
|
||||||
|
test.case("places ships on lines, facing the arena center", check => {
|
||||||
test.case("places ships on lines, facing the arena center", check => {
|
var fleet1 = new Fleet();
|
||||||
var fleet1 = new Fleet();
|
var fleet2 = new Fleet();
|
||||||
var fleet2 = new Fleet();
|
|
||||||
|
var ship1 = new Ship(fleet1, "F1S1");
|
||||||
var ship1 = new Ship(fleet1, "F1S1");
|
var ship2 = new Ship(fleet1, "F1S2");
|
||||||
var ship2 = new Ship(fleet1, "F1S2");
|
var ship3 = new Ship(fleet1, "F1S3");
|
||||||
var ship3 = new Ship(fleet1, "F1S3");
|
var ship4 = new Ship(fleet2, "F2S1");
|
||||||
var ship4 = new Ship(fleet2, "F2S1");
|
var ship5 = new Ship(fleet2, "F2S2");
|
||||||
var ship5 = new Ship(fleet2, "F2S2");
|
|
||||||
|
var battle = new Battle(fleet1, fleet2, 1000, 500);
|
||||||
var battle = new Battle(fleet1, fleet2, 1000, 500);
|
battle.placeShips();
|
||||||
battle.placeShips();
|
|
||||||
|
check.nears(ship1.arena_x, 250);
|
||||||
check.nears(ship1.arena_x, 250);
|
check.nears(ship1.arena_y, 150);
|
||||||
check.nears(ship1.arena_y, 150);
|
check.nears(ship1.arena_angle, 0);
|
||||||
check.nears(ship1.arena_angle, 0);
|
|
||||||
|
check.nears(ship2.arena_x, 250);
|
||||||
check.nears(ship2.arena_x, 250);
|
check.nears(ship2.arena_y, 250);
|
||||||
check.nears(ship2.arena_y, 250);
|
check.nears(ship2.arena_angle, 0);
|
||||||
check.nears(ship2.arena_angle, 0);
|
|
||||||
|
check.nears(ship3.arena_x, 250);
|
||||||
check.nears(ship3.arena_x, 250);
|
check.nears(ship3.arena_y, 350);
|
||||||
check.nears(ship3.arena_y, 350);
|
check.nears(ship3.arena_angle, 0);
|
||||||
check.nears(ship3.arena_angle, 0);
|
|
||||||
|
check.nears(ship4.arena_x, 750);
|
||||||
check.nears(ship4.arena_x, 750);
|
check.nears(ship4.arena_y, 300);
|
||||||
check.nears(ship4.arena_y, 300);
|
check.nears(ship4.arena_angle, Math.PI);
|
||||||
check.nears(ship4.arena_angle, Math.PI);
|
|
||||||
|
check.nears(ship5.arena_x, 750);
|
||||||
check.nears(ship5.arena_x, 750);
|
check.nears(ship5.arena_y, 200);
|
||||||
check.nears(ship5.arena_y, 200);
|
check.nears(ship5.arena_angle, Math.PI);
|
||||||
check.nears(ship5.arena_angle, Math.PI);
|
});
|
||||||
});
|
|
||||||
|
test.case("advances to next ship in play order", check => {
|
||||||
test.case("advances to next ship in play order", check => {
|
var fleet1 = new Fleet();
|
||||||
var fleet1 = new Fleet();
|
var fleet2 = new Fleet();
|
||||||
var fleet2 = new Fleet();
|
|
||||||
|
var ship1 = new Ship(fleet1, "ship1");
|
||||||
var ship1 = new Ship(fleet1, "ship1");
|
var ship2 = new Ship(fleet1, "ship2");
|
||||||
var ship2 = new Ship(fleet1, "ship2");
|
var ship3 = new Ship(fleet2, "ship3");
|
||||||
var ship3 = new Ship(fleet2, "ship3");
|
|
||||||
|
var battle = new Battle(fleet1, fleet2);
|
||||||
var battle = new Battle(fleet1, fleet2);
|
battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0));
|
||||||
battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0));
|
|
||||||
|
// Check empty play_order case
|
||||||
// Check empty play_order case
|
check.equals(battle.playing_ship, null);
|
||||||
check.equals(battle.playing_ship, null);
|
battle.advanceToNextShip();
|
||||||
battle.advanceToNextShip();
|
check.equals(battle.playing_ship, null);
|
||||||
check.equals(battle.playing_ship, null);
|
|
||||||
|
// Force play order
|
||||||
// Force play order
|
iforeach(battle.iships(), ship => TestTools.setAttribute(ship, "initiative", 1));
|
||||||
iforeach(battle.iships(), ship => TestTools.setAttribute(ship, "initiative", 1));
|
var gen = new SkewedRandomGenerator([0.1, 0.2, 0.0]);
|
||||||
var gen = new SkewedRandomGenerator([0.1, 0.2, 0.0]);
|
battle.throwInitiative(gen);
|
||||||
battle.throwInitiative(gen);
|
check.equals(battle.playing_ship, null);
|
||||||
check.equals(battle.playing_ship, null);
|
|
||||||
|
battle.advanceToNextShip();
|
||||||
battle.advanceToNextShip();
|
check.same(battle.playing_ship, ship2);
|
||||||
check.same(battle.playing_ship, ship2);
|
|
||||||
|
battle.advanceToNextShip();
|
||||||
battle.advanceToNextShip();
|
check.same(battle.playing_ship, ship1);
|
||||||
check.same(battle.playing_ship, ship1);
|
|
||||||
|
battle.advanceToNextShip();
|
||||||
battle.advanceToNextShip();
|
check.same(battle.playing_ship, ship3);
|
||||||
check.same(battle.playing_ship, ship3);
|
|
||||||
|
battle.advanceToNextShip();
|
||||||
battle.advanceToNextShip();
|
check.same(battle.playing_ship, ship2);
|
||||||
check.same(battle.playing_ship, ship2);
|
|
||||||
|
// A dead ship is skipped
|
||||||
// A dead ship is skipped
|
ship1.setDead();
|
||||||
ship1.setDead();
|
battle.advanceToNextShip();
|
||||||
battle.advanceToNextShip();
|
check.same(battle.playing_ship, ship3);
|
||||||
check.same(battle.playing_ship, ship3);
|
|
||||||
|
// Playing ship dies
|
||||||
// Playing ship dies
|
ship3.setDead();
|
||||||
ship3.setDead();
|
battle.advanceToNextShip();
|
||||||
battle.advanceToNextShip();
|
check.same(battle.playing_ship, ship2);
|
||||||
check.same(battle.playing_ship, ship2);
|
});
|
||||||
});
|
|
||||||
|
test.case("handles the suicide case (playing ship dies because of its action)", check => {
|
||||||
test.case("handles the suicide case (playing ship dies because of its action)", check => {
|
let battle = TestTools.createBattle(3, 1);
|
||||||
let battle = TestTools.createBattle(3, 1);
|
let [ship1, ship2, ship3, ship4] = battle.play_order;
|
||||||
let [ship1, ship2, ship3, ship4] = battle.play_order;
|
ship1.setArenaPosition(0, 0);
|
||||||
ship1.setArenaPosition(0, 0);
|
ship2.setArenaPosition(0, 0);
|
||||||
ship2.setArenaPosition(0, 0);
|
ship3.setArenaPosition(1000, 1000);
|
||||||
ship3.setArenaPosition(1000, 1000);
|
ship4.setArenaPosition(1000, 1000);
|
||||||
ship4.setArenaPosition(1000, 1000);
|
let weapon = TestTools.addWeapon(ship1, 8000, 0, 50, 100);
|
||||||
let weapon = TestTools.addWeapon(ship1, 8000, 0, 50, 100);
|
|
||||||
|
check.in("initially", check => {
|
||||||
check.in("initially", check => {
|
check.same(battle.playing_ship, ship1, "playing ship");
|
||||||
check.same(battle.playing_ship, ship1, "playing ship");
|
check.equals(battle.ships.list().filter(ship => ship.alive), [ship1, ship2, ship3, ship4], "alive ships");
|
||||||
check.equals(battle.ships.list().filter(ship => ship.alive), [ship1, ship2, ship3, ship4], "alive ships");
|
|
||||||
});
|
|
||||||
|
|
||||||
let result = battle.applyOneAction(weapon.id, Target.newFromLocation(0, 0));
|
|
||||||
check.equals(result, true, "action applied successfully");
|
|
||||||
check.in("after weapon", check => {
|
|
||||||
check.same(battle.playing_ship, ship3, "playing ship");
|
|
||||||
check.equals(battle.ships.list().filter(ship => ship.alive), [ship3, ship4], "alive ships");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("detects victory condition and logs a final EndBattleEvent", check => {
|
|
||||||
var fleet1 = new Fleet();
|
|
||||||
var fleet2 = new Fleet();
|
|
||||||
|
|
||||||
var ship1 = new Ship(fleet1, "F1S1");
|
|
||||||
var ship2 = new Ship(fleet1, "F1S2");
|
|
||||||
let ship3 = new Ship(fleet2, "F2S1");
|
|
||||||
|
|
||||||
var battle = new Battle(fleet1, fleet2);
|
|
||||||
battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0));
|
|
||||||
battle.start();
|
|
||||||
battle.play_order = [ship3, ship2, ship1];
|
|
||||||
check.equals(battle.ended, false);
|
|
||||||
|
|
||||||
ship1.setDead();
|
|
||||||
ship2.setDead();
|
|
||||||
battle.advanceToNextShip();
|
|
||||||
|
|
||||||
check.equals(battle.ended, true);
|
|
||||||
let diff = battle.log.get(battle.log.count() - 1);
|
|
||||||
if (diff instanceof EndBattleDiff) {
|
|
||||||
check.notequals(diff.outcome.winner, null);
|
|
||||||
check.same(diff.outcome.winner, fleet2.id);
|
|
||||||
} else {
|
|
||||||
check.fail("Not an EndBattleDiff");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("handles a draw in end battle", check => {
|
|
||||||
var fleet1 = new Fleet();
|
|
||||||
var fleet2 = new Fleet();
|
|
||||||
|
|
||||||
var ship1 = new Ship(fleet1, "F1S1");
|
|
||||||
var ship2 = new Ship(fleet1, "F1S2");
|
|
||||||
var ship3 = new Ship(fleet2, "F2S1");
|
|
||||||
|
|
||||||
var battle = new Battle(fleet1, fleet2);
|
|
||||||
|
|
||||||
battle.start();
|
|
||||||
check.equals(battle.ended, false);
|
|
||||||
|
|
||||||
ship1.setDead();
|
|
||||||
ship2.setDead();
|
|
||||||
ship3.setDead();
|
|
||||||
|
|
||||||
battle.log.clear();
|
|
||||||
check.equals(battle.ended, false);
|
|
||||||
battle.performChecks();
|
|
||||||
|
|
||||||
check.equals(battle.ended, true);
|
|
||||||
check.equals(battle.log.count(), 1);
|
|
||||||
let diff = battle.log.get(0);
|
|
||||||
if (diff instanceof EndBattleDiff) {
|
|
||||||
check.equals(diff.outcome.winner, null);
|
|
||||||
} else {
|
|
||||||
check.fail("Not an EndBattleDiff");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("collects ships present in a circle", check => {
|
|
||||||
var fleet1 = new Fleet();
|
|
||||||
var ship1 = new Ship(fleet1, "F1S1");
|
|
||||||
ship1.setArenaPosition(0, 0);
|
|
||||||
var ship2 = new Ship(fleet1, "F1S2");
|
|
||||||
ship2.setArenaPosition(5, 8);
|
|
||||||
var ship3 = new Ship(fleet1, "F1S3");
|
|
||||||
ship3.setArenaPosition(6.5, 9.5);
|
|
||||||
var ship4 = new Ship(fleet1, "F1S4");
|
|
||||||
ship4.setArenaPosition(12, 12);
|
|
||||||
|
|
||||||
var battle = new Battle(fleet1);
|
|
||||||
battle.throwInitiative(new SkewedRandomGenerator([5, 4, 3, 2]));
|
|
||||||
|
|
||||||
var result = battle.collectShipsInCircle(Target.newFromLocation(5, 8), 3);
|
|
||||||
check.equals(result, [ship2, ship3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("adds and remove drones", check => {
|
|
||||||
let battle = new Battle();
|
|
||||||
let ship = new Ship();
|
|
||||||
let drone = new Drone(ship);
|
|
||||||
check.equals(battle.drones.count(), 0);
|
|
||||||
|
|
||||||
battle.addDrone(drone);
|
|
||||||
check.equals(battle.drones.count(), 1);
|
|
||||||
check.same(battle.drones.get(drone.id), drone);
|
|
||||||
|
|
||||||
battle.addDrone(drone);
|
|
||||||
check.equals(battle.drones.count(), 1);
|
|
||||||
|
|
||||||
battle.removeDrone(drone);
|
|
||||||
check.equals(battle.drones.count(), 0);
|
|
||||||
|
|
||||||
battle.removeDrone(drone);
|
|
||||||
check.equals(battle.drones.count(), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("checks if a player is able to play", check => {
|
|
||||||
let battle = new Battle();
|
|
||||||
let player = new Player();
|
|
||||||
|
|
||||||
check.equals(battle.canPlay(player), false);
|
|
||||||
|
|
||||||
let ship = new Ship();
|
|
||||||
TestTools.setShipPlaying(battle, ship);
|
|
||||||
|
|
||||||
check.equals(battle.canPlay(player), false);
|
|
||||||
|
|
||||||
ship.fleet.setPlayer(player);
|
|
||||||
|
|
||||||
check.equals(battle.canPlay(player), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("gets the number of turns before a specific ship plays", check => {
|
|
||||||
let battle = TestTools.createBattle(2, 1);
|
|
||||||
|
|
||||||
check.in("initial", check => {
|
|
||||||
check.same(battle.playing_ship, battle.play_order[0], "first ship playing");
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[0]), 0);
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[1]), 1);
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[2]), 2);
|
|
||||||
});
|
|
||||||
|
|
||||||
battle.advanceToNextShip();
|
|
||||||
|
|
||||||
check.in("1 step", check => {
|
|
||||||
check.same(battle.playing_ship, battle.play_order[1], "second ship playing");
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[0]), 2);
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[1]), 0);
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[2]), 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
battle.advanceToNextShip();
|
|
||||||
|
|
||||||
check.in("2 steps", check => {
|
|
||||||
check.same(battle.playing_ship, battle.play_order[2], "third ship playing");
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[0]), 1);
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[1]), 2);
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[2]), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
battle.advanceToNextShip();
|
|
||||||
|
|
||||||
check.in("3 steps", check => {
|
|
||||||
check.same(battle.playing_ship, battle.play_order[0], "first ship playing");
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[0]), 0);
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[1]), 1);
|
|
||||||
check.equals(battle.getPlayOrder(battle.play_order[2]), 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("lists area effects", check => {
|
|
||||||
let battle = new Battle();
|
|
||||||
let ship = battle.fleets[0].addShip();
|
|
||||||
let peer = battle.fleets[1].addShip();
|
|
||||||
peer.setArenaPosition(100, 50);
|
|
||||||
|
|
||||||
check.equals(battle.getAreaEffects(peer), [], "initial");
|
|
||||||
|
|
||||||
let drone1 = new Drone(ship);
|
|
||||||
drone1.x = 120;
|
|
||||||
drone1.y = 60;
|
|
||||||
drone1.radius = 40;
|
|
||||||
drone1.effects = [new DamageEffect(12)];
|
|
||||||
battle.addDrone(drone1);
|
|
||||||
let drone2 = new Drone(ship);
|
|
||||||
drone2.x = 130;
|
|
||||||
drone2.y = 70;
|
|
||||||
drone2.radius = 20;
|
|
||||||
drone2.effects = [new DamageEffect(14)];
|
|
||||||
battle.addDrone(drone2);
|
|
||||||
|
|
||||||
check.equals(battle.getAreaEffects(peer), [[drone1, drone1.effects[0]]], "drone effects");
|
|
||||||
|
|
||||||
let eq1 = new ToggleAction("eq1", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 1)] });
|
|
||||||
ship.actions.addCustom(eq1);
|
|
||||||
ship.actions.toggle(eq1, true);
|
|
||||||
let eq2 = new ToggleAction("eq2", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 2)] });
|
|
||||||
ship.actions.addCustom(eq2);
|
|
||||||
ship.actions.toggle(eq2, false);
|
|
||||||
let eq3 = new ToggleAction("eq3", { power: 0, radius: 100, effects: [new AttributeEffect("initiative", 3)] });
|
|
||||||
ship.actions.addCustom(eq3);
|
|
||||||
ship.actions.toggle(eq3, true);
|
|
||||||
|
|
||||||
check.equals(battle.getAreaEffects(peer), [
|
|
||||||
[drone1, drone1.effects[0]],
|
|
||||||
[ship, eq1.effects[0]],
|
|
||||||
], "drone and toggle effects");
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("is serializable", check => {
|
|
||||||
let battle = Battle.newQuickRandom();
|
|
||||||
battle.ai_playing = true;
|
|
||||||
|
|
||||||
let serializer = new Serializer(TK.SpaceTac);
|
|
||||||
let data = serializer.serialize(battle);
|
|
||||||
|
|
||||||
let loaded = serializer.unserialize(data);
|
|
||||||
|
|
||||||
check.equals(loaded.ai_playing, false, "ai playing is reset");
|
|
||||||
battle.ai_playing = false;
|
|
||||||
check.equals(loaded, battle, "unserialized == initial");
|
|
||||||
|
|
||||||
let session = new GameSession();
|
|
||||||
session.startNewGame();
|
|
||||||
session.start_location.setupEncounter();
|
|
||||||
session.start_location.enterLocation(session.player.fleet);
|
|
||||||
let battle1 = nn(session.getBattle());
|
|
||||||
let data1 = serializer.serialize(battle1);
|
|
||||||
|
|
||||||
let ratio = data.length / data1.length;
|
|
||||||
check.greaterorequal(ratio, 1.2, `quick battle serialized size (${data.length}) should be larger than campaign's (${data1.length})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("can revert the last action", check => {
|
|
||||||
let battle = new Battle();
|
|
||||||
let ship = battle.fleets[0].addShip();
|
|
||||||
ship.setValue("hull", 13);
|
|
||||||
battle.log.clear();
|
|
||||||
battle.log.add(new ShipValueDiff(ship, "hull", 4));
|
|
||||||
battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)));
|
|
||||||
battle.log.add(new ShipValueDiff(ship, "hull", 7));
|
|
||||||
battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)));
|
|
||||||
battle.log.add(new ShipValueDiff(ship, "hull", 2));
|
|
||||||
|
|
||||||
check.in("initial state", check => {
|
|
||||||
check.equals(ship.getValue("hull"), 13, "hull=13");
|
|
||||||
check.equals(battle.log.count(), 5, "log count=5");
|
|
||||||
});
|
|
||||||
|
|
||||||
battle.revertOneAction();
|
|
||||||
|
|
||||||
check.in("revert 1 action", check => {
|
|
||||||
check.equals(ship.getValue("hull"), 11, "hull=11");
|
|
||||||
check.equals(battle.log.count(), 3, "log count=3");
|
|
||||||
});
|
|
||||||
|
|
||||||
battle.revertOneAction();
|
|
||||||
|
|
||||||
check.in("revert 2 actions", check => {
|
|
||||||
check.equals(ship.getValue("hull"), 4, "hull=4");
|
|
||||||
check.equals(battle.log.count(), 1, "log count=1");
|
|
||||||
});
|
|
||||||
|
|
||||||
battle.revertOneAction();
|
|
||||||
|
|
||||||
check.in("revert 3 actions", check => {
|
|
||||||
check.equals(ship.getValue("hull"), 0, "hull=0");
|
|
||||||
check.equals(battle.log.count(), 0, "log count=0");
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
let result = battle.applyOneAction(weapon.id, Target.newFromLocation(0, 0));
|
||||||
|
check.equals(result, true, "action applied successfully");
|
||||||
|
check.in("after weapon", check => {
|
||||||
|
check.same(battle.playing_ship, ship3, "playing ship");
|
||||||
|
check.equals(battle.ships.list().filter(ship => ship.alive), [ship3, ship4], "alive ships");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("detects victory condition and logs a final EndBattleEvent", check => {
|
||||||
|
var fleet1 = new Fleet();
|
||||||
|
var fleet2 = new Fleet();
|
||||||
|
|
||||||
|
var ship1 = new Ship(fleet1, "F1S1");
|
||||||
|
var ship2 = new Ship(fleet1, "F1S2");
|
||||||
|
let ship3 = new Ship(fleet2, "F2S1");
|
||||||
|
|
||||||
|
var battle = new Battle(fleet1, fleet2);
|
||||||
|
battle.ships.list().forEach(ship => TestTools.setShipModel(ship, 10, 0));
|
||||||
|
battle.start();
|
||||||
|
battle.play_order = [ship3, ship2, ship1];
|
||||||
|
check.equals(battle.ended, false);
|
||||||
|
|
||||||
|
ship1.setDead();
|
||||||
|
ship2.setDead();
|
||||||
|
battle.advanceToNextShip();
|
||||||
|
|
||||||
|
check.equals(battle.ended, true);
|
||||||
|
let diff = battle.log.get(battle.log.count() - 1);
|
||||||
|
if (diff instanceof EndBattleDiff) {
|
||||||
|
check.notequals(diff.outcome.winner, null);
|
||||||
|
check.same(diff.outcome.winner, fleet2.id);
|
||||||
|
} else {
|
||||||
|
check.fail("Not an EndBattleDiff");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("handles a draw in end battle", check => {
|
||||||
|
var fleet1 = new Fleet();
|
||||||
|
var fleet2 = new Fleet();
|
||||||
|
|
||||||
|
var ship1 = new Ship(fleet1, "F1S1");
|
||||||
|
var ship2 = new Ship(fleet1, "F1S2");
|
||||||
|
var ship3 = new Ship(fleet2, "F2S1");
|
||||||
|
|
||||||
|
var battle = new Battle(fleet1, fleet2);
|
||||||
|
|
||||||
|
battle.start();
|
||||||
|
check.equals(battle.ended, false);
|
||||||
|
|
||||||
|
ship1.setDead();
|
||||||
|
ship2.setDead();
|
||||||
|
ship3.setDead();
|
||||||
|
|
||||||
|
battle.log.clear();
|
||||||
|
check.equals(battle.ended, false);
|
||||||
|
battle.performChecks();
|
||||||
|
|
||||||
|
check.equals(battle.ended, true);
|
||||||
|
check.equals(battle.log.count(), 1);
|
||||||
|
let diff = battle.log.get(0);
|
||||||
|
if (diff instanceof EndBattleDiff) {
|
||||||
|
check.equals(diff.outcome.winner, null);
|
||||||
|
} else {
|
||||||
|
check.fail("Not an EndBattleDiff");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("collects ships present in a circle", check => {
|
||||||
|
var fleet1 = new Fleet();
|
||||||
|
var ship1 = new Ship(fleet1, "F1S1");
|
||||||
|
ship1.setArenaPosition(0, 0);
|
||||||
|
var ship2 = new Ship(fleet1, "F1S2");
|
||||||
|
ship2.setArenaPosition(5, 8);
|
||||||
|
var ship3 = new Ship(fleet1, "F1S3");
|
||||||
|
ship3.setArenaPosition(6.5, 9.5);
|
||||||
|
var ship4 = new Ship(fleet1, "F1S4");
|
||||||
|
ship4.setArenaPosition(12, 12);
|
||||||
|
|
||||||
|
var battle = new Battle(fleet1);
|
||||||
|
battle.throwInitiative(new SkewedRandomGenerator([5, 4, 3, 2]));
|
||||||
|
|
||||||
|
var result = battle.collectShipsInCircle(Target.newFromLocation(5, 8), 3);
|
||||||
|
check.equals(result, [ship2, ship3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("adds and remove drones", check => {
|
||||||
|
let battle = new Battle();
|
||||||
|
let ship = new Ship();
|
||||||
|
let drone = new Drone(ship);
|
||||||
|
check.equals(battle.drones.count(), 0);
|
||||||
|
|
||||||
|
battle.addDrone(drone);
|
||||||
|
check.equals(battle.drones.count(), 1);
|
||||||
|
check.same(battle.drones.get(drone.id), drone);
|
||||||
|
|
||||||
|
battle.addDrone(drone);
|
||||||
|
check.equals(battle.drones.count(), 1);
|
||||||
|
|
||||||
|
battle.removeDrone(drone);
|
||||||
|
check.equals(battle.drones.count(), 0);
|
||||||
|
|
||||||
|
battle.removeDrone(drone);
|
||||||
|
check.equals(battle.drones.count(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("checks if a player is able to play", check => {
|
||||||
|
let battle = new Battle();
|
||||||
|
let player = new Player();
|
||||||
|
|
||||||
|
check.equals(battle.canPlay(player), false);
|
||||||
|
|
||||||
|
let ship = new Ship();
|
||||||
|
TestTools.setShipPlaying(battle, ship);
|
||||||
|
|
||||||
|
check.equals(battle.canPlay(player), false);
|
||||||
|
|
||||||
|
ship.fleet.setPlayer(player);
|
||||||
|
|
||||||
|
check.equals(battle.canPlay(player), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("gets the number of turns before a specific ship plays", check => {
|
||||||
|
let battle = TestTools.createBattle(2, 1);
|
||||||
|
|
||||||
|
check.in("initial", check => {
|
||||||
|
check.same(battle.playing_ship, battle.play_order[0], "first ship playing");
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[0]), 0);
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[1]), 1);
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[2]), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
battle.advanceToNextShip();
|
||||||
|
|
||||||
|
check.in("1 step", check => {
|
||||||
|
check.same(battle.playing_ship, battle.play_order[1], "second ship playing");
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[0]), 2);
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[1]), 0);
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[2]), 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
battle.advanceToNextShip();
|
||||||
|
|
||||||
|
check.in("2 steps", check => {
|
||||||
|
check.same(battle.playing_ship, battle.play_order[2], "third ship playing");
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[0]), 1);
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[1]), 2);
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[2]), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
battle.advanceToNextShip();
|
||||||
|
|
||||||
|
check.in("3 steps", check => {
|
||||||
|
check.same(battle.playing_ship, battle.play_order[0], "first ship playing");
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[0]), 0);
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[1]), 1);
|
||||||
|
check.equals(battle.getPlayOrder(battle.play_order[2]), 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("lists area effects", check => {
|
||||||
|
let battle = new Battle();
|
||||||
|
let ship = battle.fleets[0].addShip();
|
||||||
|
let peer = battle.fleets[1].addShip();
|
||||||
|
peer.setArenaPosition(100, 50);
|
||||||
|
|
||||||
|
check.equals(battle.getAreaEffects(peer), [], "initial");
|
||||||
|
|
||||||
|
let drone1 = new Drone(ship);
|
||||||
|
drone1.x = 120;
|
||||||
|
drone1.y = 60;
|
||||||
|
drone1.radius = 40;
|
||||||
|
drone1.effects = [new DamageEffect(12)];
|
||||||
|
battle.addDrone(drone1);
|
||||||
|
let drone2 = new Drone(ship);
|
||||||
|
drone2.x = 130;
|
||||||
|
drone2.y = 70;
|
||||||
|
drone2.radius = 20;
|
||||||
|
drone2.effects = [new DamageEffect(14)];
|
||||||
|
battle.addDrone(drone2);
|
||||||
|
|
||||||
|
check.equals(battle.getAreaEffects(peer), [[drone1, drone1.effects[0]]], "drone effects");
|
||||||
|
|
||||||
|
let eq1 = new ToggleAction("eq1", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 1)] });
|
||||||
|
ship.actions.addCustom(eq1);
|
||||||
|
ship.actions.toggle(eq1, true);
|
||||||
|
let eq2 = new ToggleAction("eq2", { power: 0, radius: 500, effects: [new AttributeEffect("initiative", 2)] });
|
||||||
|
ship.actions.addCustom(eq2);
|
||||||
|
ship.actions.toggle(eq2, false);
|
||||||
|
let eq3 = new ToggleAction("eq3", { power: 0, radius: 100, effects: [new AttributeEffect("initiative", 3)] });
|
||||||
|
ship.actions.addCustom(eq3);
|
||||||
|
ship.actions.toggle(eq3, true);
|
||||||
|
|
||||||
|
check.equals(battle.getAreaEffects(peer), [
|
||||||
|
[drone1, drone1.effects[0]],
|
||||||
|
[ship, eq1.effects[0]],
|
||||||
|
], "drone and toggle effects");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("is serializable", check => {
|
||||||
|
let battle = Battle.newQuickRandom();
|
||||||
|
battle.ai_playing = true;
|
||||||
|
|
||||||
|
let serializer = new Serializer(TK.SpaceTac);
|
||||||
|
let data = serializer.serialize(battle);
|
||||||
|
|
||||||
|
let loaded = serializer.unserialize(data);
|
||||||
|
|
||||||
|
check.equals(loaded.ai_playing, false, "ai playing is reset");
|
||||||
|
battle.ai_playing = false;
|
||||||
|
check.equals(loaded, battle, "unserialized == initial");
|
||||||
|
|
||||||
|
let session = new GameSession();
|
||||||
|
session.startNewGame();
|
||||||
|
session.start_location.setupEncounter();
|
||||||
|
session.start_location.enterLocation(session.player.fleet);
|
||||||
|
let battle1 = nn(session.getBattle());
|
||||||
|
let data1 = serializer.serialize(battle1);
|
||||||
|
|
||||||
|
let ratio = data.length / data1.length;
|
||||||
|
check.greaterorequal(ratio, 1.2, `quick battle serialized size (${data.length}) should be larger than campaign's (${data1.length})`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("can revert the last action", check => {
|
||||||
|
let battle = new Battle();
|
||||||
|
let ship = battle.fleets[0].addShip();
|
||||||
|
ship.setValue("hull", 13);
|
||||||
|
battle.log.clear();
|
||||||
|
battle.log.add(new ShipValueDiff(ship, "hull", 4));
|
||||||
|
battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)));
|
||||||
|
battle.log.add(new ShipValueDiff(ship, "hull", 7));
|
||||||
|
battle.log.add(new ShipActionUsedDiff(ship, EndTurnAction.SINGLETON, Target.newFromShip(ship)));
|
||||||
|
battle.log.add(new ShipValueDiff(ship, "hull", 2));
|
||||||
|
|
||||||
|
check.in("initial state", check => {
|
||||||
|
check.equals(ship.getValue("hull"), 13, "hull=13");
|
||||||
|
check.equals(battle.log.count(), 5, "log count=5");
|
||||||
|
});
|
||||||
|
|
||||||
|
battle.revertOneAction();
|
||||||
|
|
||||||
|
check.in("revert 1 action", check => {
|
||||||
|
check.equals(ship.getValue("hull"), 11, "hull=11");
|
||||||
|
check.equals(battle.log.count(), 3, "log count=3");
|
||||||
|
});
|
||||||
|
|
||||||
|
battle.revertOneAction();
|
||||||
|
|
||||||
|
check.in("revert 2 actions", check => {
|
||||||
|
check.equals(ship.getValue("hull"), 4, "hull=4");
|
||||||
|
check.equals(battle.log.count(), 1, "log count=1");
|
||||||
|
});
|
||||||
|
|
||||||
|
battle.revertOneAction();
|
||||||
|
|
||||||
|
check.in("revert 3 actions", check => {
|
||||||
|
check.equals(ship.getValue("hull"), 0, "hull=0");
|
||||||
|
check.equals(battle.log.count(), 0, "log count=0");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
|
@ -1,416 +1,436 @@
|
||||||
module TK.SpaceTac {
|
import { iarray, ichainit, ifilter, iforeach, imap, imaterialize } from "../common/Iterators"
|
||||||
/**
|
import { RandomGenerator } from "../common/RandomGenerator"
|
||||||
* A turn-based battle between fleets
|
import { RObjectContainer, RObjectId } from "../common/RObject"
|
||||||
*/
|
import { bool, flatten } from "../common/Tools"
|
||||||
export class Battle {
|
import { EndTurnAction } from "./actions/EndTurnAction"
|
||||||
// Grid for the arena
|
import { AIWorker } from "./ai/AIWorker"
|
||||||
grid?: IArenaGrid
|
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"
|
||||||
|
|
||||||
// Battle outcome, if the battle has ended
|
/**
|
||||||
outcome: BattleOutcome | null = null
|
* A turn-based battle between fleets
|
||||||
|
*/
|
||||||
|
export class Battle {
|
||||||
|
// Grid for the arena
|
||||||
|
grid?: IArenaGrid
|
||||||
|
|
||||||
// Statistics
|
// Battle outcome, if the battle has ended
|
||||||
stats: BattleStats
|
outcome: BattleOutcome | null = null
|
||||||
|
|
||||||
// Log of all battle events
|
// Statistics
|
||||||
log: BattleLog
|
stats: BattleStats
|
||||||
|
|
||||||
// List of fleets engaged in battle
|
// Log of all battle events
|
||||||
fleets: Fleet[]
|
log: BattleLog
|
||||||
|
|
||||||
// Container of all engaged ships
|
// List of fleets engaged in battle
|
||||||
ships: RObjectContainer<Ship>
|
fleets: Fleet[]
|
||||||
|
|
||||||
// List of playing ships, sorted by their initiative throw
|
// Container of all engaged ships
|
||||||
play_order: Ship[]
|
ships: RObjectContainer<Ship>
|
||||||
play_index = -1
|
|
||||||
|
|
||||||
// Current battle "cycle" (one cycle is one turn done for all ships in the play order)
|
// List of playing ships, sorted by their initiative throw
|
||||||
cycle = 0
|
play_order: Ship[]
|
||||||
|
play_index = -1
|
||||||
|
|
||||||
// List of deployed drones
|
// Current battle "cycle" (one cycle is one turn done for all ships in the play order)
|
||||||
drones = new RObjectContainer<Drone>()
|
cycle = 0
|
||||||
|
|
||||||
// Size of the battle area
|
// List of deployed drones
|
||||||
width: number
|
drones = new RObjectContainer<Drone>()
|
||||||
height: number
|
|
||||||
border = 50
|
|
||||||
ship_separation = 100
|
|
||||||
|
|
||||||
// Indicator that an AI is playing
|
// Size of the battle area
|
||||||
ai_playing = false
|
width: number
|
||||||
|
height: number
|
||||||
|
border = 50
|
||||||
|
ship_separation = 100
|
||||||
|
|
||||||
constructor(fleet1 = new Fleet(new Player("Attacker")), fleet2 = new Fleet(new Player("Defender")), width = 1808, height = 948) {
|
// Indicator that an AI is playing
|
||||||
this.grid = new HexagonalArenaGrid(50);
|
ai_playing = false
|
||||||
|
|
||||||
this.fleets = [fleet1, fleet2];
|
constructor(fleet1 = new Fleet(new Player("Attacker")), fleet2 = new Fleet(new Player("Defender")), width = 1808, height = 948) {
|
||||||
this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships));
|
this.grid = new HexagonalArenaGrid(50);
|
||||||
this.play_order = [];
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
|
|
||||||
this.log = new BattleLog();
|
this.fleets = [fleet1, fleet2];
|
||||||
this.stats = new BattleStats();
|
this.ships = new RObjectContainer(fleet1.ships.concat(fleet2.ships));
|
||||||
|
this.play_order = [];
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
this.fleets.forEach((fleet: Fleet) => {
|
this.log = new BattleLog();
|
||||||
fleet.setBattle(this);
|
this.stats = new BattleStats();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
postUnserialize() {
|
this.fleets.forEach((fleet: Fleet) => {
|
||||||
this.ai_playing = false;
|
fleet.setBattle(this);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
postUnserialize() {
|
||||||
* Property is true if the battle has ended
|
this.ai_playing = false;
|
||||||
*/
|
}
|
||||||
get ended(): boolean {
|
|
||||||
return bool(this.outcome);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a list of diffs to the game state, and add them to the log.
|
* Property is true if the battle has ended
|
||||||
*
|
*/
|
||||||
* This should be the main way to modify the game state.
|
get ended(): boolean {
|
||||||
*/
|
return bool(this.outcome);
|
||||||
applyDiffs(diffs: BaseBattleDiff[]): void {
|
}
|
||||||
let client = new BattleLogClient(this, this.log);
|
|
||||||
diffs.forEach(diff => client.add(diff));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a quick random battle, for testing purposes, or quick skirmish
|
* Apply a list of diffs to the game state, and add them to the log.
|
||||||
*/
|
*
|
||||||
static newQuickRandom(start = true, level = 1, shipcount = 5): Battle {
|
* This should be the main way to modify the game state.
|
||||||
let player1 = Player.newQuickRandom("Player", level, shipcount, true);
|
*/
|
||||||
let player2 = Player.newQuickRandom("Enemy", level, shipcount, true);
|
applyDiffs(diffs: BaseBattleDiff[]): void {
|
||||||
|
let client = new BattleLogClient(this, this.log);
|
||||||
|
diffs.forEach(diff => client.add(diff));
|
||||||
|
}
|
||||||
|
|
||||||
let result = new Battle(player1.fleet, player2.fleet);
|
/**
|
||||||
if (start) {
|
* Create a quick random battle, for testing purposes, or quick skirmish
|
||||||
result.start();
|
*/
|
||||||
}
|
static newQuickRandom(start = true, level = 1, shipcount = 5): Battle {
|
||||||
return result;
|
let player1 = Player.newQuickRandom("Player", level, shipcount, true);
|
||||||
}
|
let player2 = Player.newQuickRandom("Enemy", level, shipcount, true);
|
||||||
|
|
||||||
/**
|
let result = new Battle(player1.fleet, player2.fleet);
|
||||||
* Get the currently playing ship
|
if (start) {
|
||||||
*/
|
result.start();
|
||||||
get playing_ship(): Ship | null {
|
|
||||||
return this.play_order[this.play_index] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a ship by its ID.
|
|
||||||
*/
|
|
||||||
getShip(id: RObjectId | null): Ship | null {
|
|
||||||
if (id === null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return this.ships.get(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an iterator over all ships engaged in the battle
|
|
||||||
*/
|
|
||||||
iships(alive_only = false): Iterable<Ship> {
|
|
||||||
let result = ichainit(imap(iarray(this.fleets), fleet => iarray(fleet.ships)));
|
|
||||||
return alive_only ? ifilter(result, ship => ship.alive) : result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an iterator over ships allies of (or owned by) a player
|
|
||||||
*/
|
|
||||||
iallies(ship: Ship, alive_only = false): Iterable<Ship> {
|
|
||||||
return ifilter(this.iships(alive_only), iship => iship.fleet.player.is(ship.fleet.player));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an iterator over ships enemy of a player
|
|
||||||
*/
|
|
||||||
ienemies(ship: Ship, alive_only = false): Iterable<Ship> {
|
|
||||||
return ifilter(this.iships(alive_only), iship => !iship.fleet.player.is(ship.fleet.player));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a player is able to play
|
|
||||||
*
|
|
||||||
* This can be used by the UI to determine if player interaction is allowed
|
|
||||||
*/
|
|
||||||
canPlay(player: Player): boolean {
|
|
||||||
if (this.ended) {
|
|
||||||
return false;
|
|
||||||
} else if (this.playing_ship && player.is(this.playing_ship.fleet.player)) {
|
|
||||||
return this.playing_ship.isAbleToPlay(false);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create play order, performing an initiative throw
|
|
||||||
throwInitiative(gen: RandomGenerator = new RandomGenerator()): void {
|
|
||||||
var play_order: Ship[] = [];
|
|
||||||
|
|
||||||
// Throw each ship's initiative
|
|
||||||
this.fleets.forEach(function (fleet: Fleet) {
|
|
||||||
fleet.ships.forEach(function (ship: Ship) {
|
|
||||||
ship.throwInitiative(gen);
|
|
||||||
play_order.push(ship);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort by throw result
|
|
||||||
play_order.sort(function (ship1: Ship, ship2: Ship) {
|
|
||||||
return (ship2.play_priority - ship1.play_priority);
|
|
||||||
});
|
|
||||||
this.play_order = play_order;
|
|
||||||
this.play_index = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the number of turns before a specific ship plays (currently playing ship will return 0).
|
|
||||||
*
|
|
||||||
* Returns -1 if the ship is not in the play list.
|
|
||||||
*/
|
|
||||||
getPlayOrder(ship: Ship): number {
|
|
||||||
let index = this.play_order.indexOf(ship);
|
|
||||||
if (index < 0) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
let result = index - this.play_index;
|
|
||||||
return (result < 0) ? (result + this.play_order.length) : result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a ship in the play order list
|
|
||||||
*/
|
|
||||||
removeFromPlayOrder(idx: number): void {
|
|
||||||
this.play_order.splice(idx, 1);
|
|
||||||
if (idx <= this.play_index) {
|
|
||||||
this.play_index -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a ship from the play order list
|
|
||||||
*/
|
|
||||||
insertInPlayOrder(idx: number, ship: Ship): void {
|
|
||||||
this.play_order.splice(idx, 0, ship);
|
|
||||||
if (idx <= this.play_index) {
|
|
||||||
this.play_index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the currently playing ship
|
|
||||||
*/
|
|
||||||
setPlayingShip(ship: Ship): void {
|
|
||||||
let current = this.playing_ship;
|
|
||||||
if (current) {
|
|
||||||
current.playing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.play_index = this.play_order.indexOf(ship);
|
|
||||||
this.ai_playing = false;
|
|
||||||
|
|
||||||
current = this.playing_ship;
|
|
||||||
if (current) {
|
|
||||||
current.playing = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Defines the initial ship positions of all engaged fleets
|
|
||||||
placeShips(vertical = true): void {
|
|
||||||
if (vertical) {
|
|
||||||
this.placeFleetShips(this.fleets[0], this.width * 0.25, this.height * 0.5, 0, this.height);
|
|
||||||
this.placeFleetShips(this.fleets[1], this.width * 0.75, this.height * 0.5, Math.PI, this.height);
|
|
||||||
} else {
|
|
||||||
this.placeFleetShips(this.fleets[0], this.width * 0.5, this.height * 0.90, -Math.PI / 2, this.width);
|
|
||||||
this.placeFleetShips(this.fleets[1], this.width * 0.5, this.height * 0.10, Math.PI / 2, this.width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all ships within a given radius of a target
|
|
||||||
collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] {
|
|
||||||
return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ends the battle and sets the outcome
|
|
||||||
*/
|
|
||||||
endBattle(winner: Fleet | null) {
|
|
||||||
this.applyDiffs([new EndBattleDiff(winner, this.cycle)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the next playing ship
|
|
||||||
*/
|
|
||||||
getNextShip(): Ship {
|
|
||||||
return this.play_order[(this.play_index + 1) % this.play_order.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make an AI play the current ship
|
|
||||||
*
|
|
||||||
* This will run asynchronous work in background, until the playing ship is changed
|
|
||||||
*/
|
|
||||||
playAI(debug = false): boolean {
|
|
||||||
if (this.playing_ship && !this.ai_playing) {
|
|
||||||
this.ai_playing = true;
|
|
||||||
AIWorker.process(this, debug);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the battle
|
|
||||||
*
|
|
||||||
* This will call all necessary initialization steps (initiative, placement...)
|
|
||||||
*
|
|
||||||
* This should not put any diff in the log
|
|
||||||
*/
|
|
||||||
start(): void {
|
|
||||||
this.outcome = null;
|
|
||||||
this.cycle = 1;
|
|
||||||
this.placeShips();
|
|
||||||
iforeach(this.iships(), ship => ship.restoreInitialState());
|
|
||||||
this.throwInitiative();
|
|
||||||
this.setPlayingShip(this.play_order[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force current ship's turn to end, then advance to the next one
|
|
||||||
*/
|
|
||||||
advanceToNextShip(): void {
|
|
||||||
if (this.playing_ship) {
|
|
||||||
this.applyOneAction(EndTurnAction.SINGLETON.id);
|
|
||||||
} else if (this.play_order.length) {
|
|
||||||
this.setPlayingShip(this.play_order[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the initial ship positions for one fleet
|
|
||||||
*
|
|
||||||
* *x* and *y* are the center of the fleet formation
|
|
||||||
* *facing_angle* is the forward angle in radians
|
|
||||||
* *width* is the formation width
|
|
||||||
*/
|
|
||||||
private placeFleetShips(fleet: Fleet, x: number, y: number, facing_angle: number, width: number): void {
|
|
||||||
var side_angle = facing_angle + Math.PI * 0.5;
|
|
||||||
var spacing = width * 0.2;
|
|
||||||
var total_length = spacing * (fleet.ships.length - 1);
|
|
||||||
var dx = Math.cos(side_angle);
|
|
||||||
var dy = Math.sin(side_angle);
|
|
||||||
x -= dx * total_length * 0.5;
|
|
||||||
y -= dy * total_length * 0.5;
|
|
||||||
for (var i = 0; i < fleet.ships.length; i++) {
|
|
||||||
fleet.ships[i].setArenaPosition(x + i * dx * spacing, y + i * dy * spacing);
|
|
||||||
fleet.ships[i].setArenaFacingAngle(facing_angle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a drone to the battle
|
|
||||||
*/
|
|
||||||
addDrone(drone: Drone) {
|
|
||||||
this.drones.add(drone);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a drone from the battle
|
|
||||||
*/
|
|
||||||
removeDrone(drone: Drone) {
|
|
||||||
this.drones.remove(drone);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the list of area effects that are expected to apply on a given ship
|
|
||||||
*/
|
|
||||||
getAreaEffects(ship: Ship): [Ship | Drone, BaseEffect][] {
|
|
||||||
let drone_effects = this.drones.list().map(drone => {
|
|
||||||
// TODO Should apply filterImpactedShips from drone action
|
|
||||||
if (drone.isInRange(ship.arena_x, ship.arena_y)) {
|
|
||||||
return drone.effects.map((effect): [Ship | Drone, BaseEffect] => [drone, effect]);
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let ships_effects = this.ships.list().map(iship => {
|
|
||||||
return iship.getAreaEffects(ship).map((effect): [Ship | Drone, BaseEffect] => [iship, effect]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return flatten(drone_effects.concat(ships_effects));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform all battle checks to ensure the state is consistent
|
|
||||||
*
|
|
||||||
* Returns all applied diffs
|
|
||||||
*/
|
|
||||||
performChecks(): BaseBattleDiff[] {
|
|
||||||
let checks = new BattleChecks(this);
|
|
||||||
return checks.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply one action to the battle state
|
|
||||||
*
|
|
||||||
* At the end of the action, some checks will be applied to ensure the battle state is consistent
|
|
||||||
*/
|
|
||||||
applyOneAction(action_id: RObjectId, target?: Target): boolean {
|
|
||||||
let ship = this.playing_ship;
|
|
||||||
if (ship) {
|
|
||||||
let action = ship.actions.getById(action_id);
|
|
||||||
if (action) {
|
|
||||||
if (!target) {
|
|
||||||
target = action.getDefaultTarget(ship);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action.apply(this, ship, target)) {
|
|
||||||
this.performChecks();
|
|
||||||
|
|
||||||
if (!this.ended) {
|
|
||||||
this.applyDiffs([new ShipActionEndedDiff(ship, action, target)]);
|
|
||||||
|
|
||||||
if (ship.playing && ship.getValue("hull") <= 0) {
|
|
||||||
// Playing ship died during its action, force a turn end
|
|
||||||
this.applyOneAction(EndTurnAction.SINGLETON.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("Action not found on ship", action_id, ship);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("Cannot apply action - ship not playing", action_id, this);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Revert the last applied action
|
|
||||||
*
|
|
||||||
* This will remove diffs from the log, so pay attention to other log clients!
|
|
||||||
*/
|
|
||||||
revertOneAction(): void {
|
|
||||||
let client = new BattleLogClient(this, this.log);
|
|
||||||
while (!client.atStart() && !(client.getCurrent() instanceof ShipActionUsedDiff)) {
|
|
||||||
client.backward();
|
|
||||||
}
|
|
||||||
if (!client.atStart()) {
|
|
||||||
client.backward();
|
|
||||||
}
|
|
||||||
client.truncate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the currently playing ship
|
||||||
|
*/
|
||||||
|
get playing_ship(): Ship | null {
|
||||||
|
return this.play_order[this.play_index] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a ship by its ID.
|
||||||
|
*/
|
||||||
|
getShip(id: RObjectId | null): Ship | null {
|
||||||
|
if (id === null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return this.ships.get(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an iterator over all ships engaged in the battle
|
||||||
|
*/
|
||||||
|
iships(alive_only = false): Iterable<Ship> {
|
||||||
|
let result = ichainit(imap(iarray(this.fleets), fleet => iarray(fleet.ships)));
|
||||||
|
return alive_only ? ifilter(result, ship => ship.alive) : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an iterator over ships allies of (or owned by) a player
|
||||||
|
*/
|
||||||
|
iallies(ship: Ship, alive_only = false): Iterable<Ship> {
|
||||||
|
return ifilter(this.iships(alive_only), iship => iship.fleet.player.is(ship.fleet.player));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an iterator over ships enemy of a player
|
||||||
|
*/
|
||||||
|
ienemies(ship: Ship, alive_only = false): Iterable<Ship> {
|
||||||
|
return ifilter(this.iships(alive_only), iship => !iship.fleet.player.is(ship.fleet.player));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a player is able to play
|
||||||
|
*
|
||||||
|
* This can be used by the UI to determine if player interaction is allowed
|
||||||
|
*/
|
||||||
|
canPlay(player: Player): boolean {
|
||||||
|
if (this.ended) {
|
||||||
|
return false;
|
||||||
|
} else if (this.playing_ship && player.is(this.playing_ship.fleet.player)) {
|
||||||
|
return this.playing_ship.isAbleToPlay(false);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create play order, performing an initiative throw
|
||||||
|
throwInitiative(gen: RandomGenerator = new RandomGenerator()): void {
|
||||||
|
var play_order: Ship[] = [];
|
||||||
|
|
||||||
|
// Throw each ship's initiative
|
||||||
|
this.fleets.forEach(function (fleet: Fleet) {
|
||||||
|
fleet.ships.forEach(function (ship: Ship) {
|
||||||
|
ship.throwInitiative(gen);
|
||||||
|
play_order.push(ship);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by throw result
|
||||||
|
play_order.sort(function (ship1: Ship, ship2: Ship) {
|
||||||
|
return (ship2.play_priority - ship1.play_priority);
|
||||||
|
});
|
||||||
|
this.play_order = play_order;
|
||||||
|
this.play_index = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of turns before a specific ship plays (currently playing ship will return 0).
|
||||||
|
*
|
||||||
|
* Returns -1 if the ship is not in the play list.
|
||||||
|
*/
|
||||||
|
getPlayOrder(ship: Ship): number {
|
||||||
|
let index = this.play_order.indexOf(ship);
|
||||||
|
if (index < 0) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
let result = index - this.play_index;
|
||||||
|
return (result < 0) ? (result + this.play_order.length) : result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a ship in the play order list
|
||||||
|
*/
|
||||||
|
removeFromPlayOrder(idx: number): void {
|
||||||
|
this.play_order.splice(idx, 1);
|
||||||
|
if (idx <= this.play_index) {
|
||||||
|
this.play_index -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a ship from the play order list
|
||||||
|
*/
|
||||||
|
insertInPlayOrder(idx: number, ship: Ship): void {
|
||||||
|
this.play_order.splice(idx, 0, ship);
|
||||||
|
if (idx <= this.play_index) {
|
||||||
|
this.play_index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the currently playing ship
|
||||||
|
*/
|
||||||
|
setPlayingShip(ship: Ship): void {
|
||||||
|
let current = this.playing_ship;
|
||||||
|
if (current) {
|
||||||
|
current.playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.play_index = this.play_order.indexOf(ship);
|
||||||
|
this.ai_playing = false;
|
||||||
|
|
||||||
|
current = this.playing_ship;
|
||||||
|
if (current) {
|
||||||
|
current.playing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defines the initial ship positions of all engaged fleets
|
||||||
|
placeShips(vertical = true): void {
|
||||||
|
if (vertical) {
|
||||||
|
this.placeFleetShips(this.fleets[0], this.width * 0.25, this.height * 0.5, 0, this.height);
|
||||||
|
this.placeFleetShips(this.fleets[1], this.width * 0.75, this.height * 0.5, Math.PI, this.height);
|
||||||
|
} else {
|
||||||
|
this.placeFleetShips(this.fleets[0], this.width * 0.5, this.height * 0.90, -Math.PI / 2, this.width);
|
||||||
|
this.placeFleetShips(this.fleets[1], this.width * 0.5, this.height * 0.10, Math.PI / 2, this.width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all ships within a given radius of a target
|
||||||
|
collectShipsInCircle(center: Target, radius: number, alive_only = false): Ship[] {
|
||||||
|
return imaterialize(ifilter(this.iships(), ship => (ship.alive || !alive_only) && Target.newFromShip(ship).getDistanceTo(center) <= radius));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends the battle and sets the outcome
|
||||||
|
*/
|
||||||
|
endBattle(winner: Fleet | null) {
|
||||||
|
this.applyDiffs([new EndBattleDiff(winner, this.cycle)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next playing ship
|
||||||
|
*/
|
||||||
|
getNextShip(): Ship {
|
||||||
|
return this.play_order[(this.play_index + 1) % this.play_order.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an AI play the current ship
|
||||||
|
*
|
||||||
|
* This will run asynchronous work in background, until the playing ship is changed
|
||||||
|
*/
|
||||||
|
playAI(debug = false): boolean {
|
||||||
|
if (this.playing_ship && !this.ai_playing) {
|
||||||
|
this.ai_playing = true;
|
||||||
|
AIWorker.process(this, debug);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the battle
|
||||||
|
*
|
||||||
|
* This will call all necessary initialization steps (initiative, placement...)
|
||||||
|
*
|
||||||
|
* This should not put any diff in the log
|
||||||
|
*/
|
||||||
|
start(): void {
|
||||||
|
this.outcome = null;
|
||||||
|
this.cycle = 1;
|
||||||
|
this.placeShips();
|
||||||
|
iforeach(this.iships(), ship => ship.restoreInitialState());
|
||||||
|
this.throwInitiative();
|
||||||
|
this.setPlayingShip(this.play_order[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force current ship's turn to end, then advance to the next one
|
||||||
|
*/
|
||||||
|
advanceToNextShip(): void {
|
||||||
|
if (this.playing_ship) {
|
||||||
|
this.applyOneAction(EndTurnAction.SINGLETON.id);
|
||||||
|
} else if (this.play_order.length) {
|
||||||
|
this.setPlayingShip(this.play_order[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the initial ship positions for one fleet
|
||||||
|
*
|
||||||
|
* *x* and *y* are the center of the fleet formation
|
||||||
|
* *facing_angle* is the forward angle in radians
|
||||||
|
* *width* is the formation width
|
||||||
|
*/
|
||||||
|
private placeFleetShips(fleet: Fleet, x: number, y: number, facing_angle: number, width: number): void {
|
||||||
|
var side_angle = facing_angle + Math.PI * 0.5;
|
||||||
|
var spacing = width * 0.2;
|
||||||
|
var total_length = spacing * (fleet.ships.length - 1);
|
||||||
|
var dx = Math.cos(side_angle);
|
||||||
|
var dy = Math.sin(side_angle);
|
||||||
|
x -= dx * total_length * 0.5;
|
||||||
|
y -= dy * total_length * 0.5;
|
||||||
|
for (var i = 0; i < fleet.ships.length; i++) {
|
||||||
|
fleet.ships[i].setArenaPosition(x + i * dx * spacing, y + i * dy * spacing);
|
||||||
|
fleet.ships[i].setArenaFacingAngle(facing_angle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a drone to the battle
|
||||||
|
*/
|
||||||
|
addDrone(drone: Drone) {
|
||||||
|
this.drones.add(drone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a drone from the battle
|
||||||
|
*/
|
||||||
|
removeDrone(drone: Drone) {
|
||||||
|
this.drones.remove(drone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of area effects that are expected to apply on a given ship
|
||||||
|
*/
|
||||||
|
getAreaEffects(ship: Ship): [Ship | Drone, BaseEffect][] {
|
||||||
|
let drone_effects = this.drones.list().map(drone => {
|
||||||
|
// TODO Should apply filterImpactedShips from drone action
|
||||||
|
if (drone.isInRange(ship.arena_x, ship.arena_y)) {
|
||||||
|
return drone.effects.map((effect): [Ship | Drone, BaseEffect] => [drone, effect]);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ships_effects = this.ships.list().map(iship => {
|
||||||
|
return iship.getAreaEffects(ship).map((effect): [Ship | Drone, BaseEffect] => [iship, effect]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return flatten(drone_effects.concat(ships_effects));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform all battle checks to ensure the state is consistent
|
||||||
|
*
|
||||||
|
* Returns all applied diffs
|
||||||
|
*/
|
||||||
|
performChecks(): BaseBattleDiff[] {
|
||||||
|
let checks = new BattleChecks(this);
|
||||||
|
return checks.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply one action to the battle state
|
||||||
|
*
|
||||||
|
* At the end of the action, some checks will be applied to ensure the battle state is consistent
|
||||||
|
*/
|
||||||
|
applyOneAction(action_id: RObjectId, target?: Target): boolean {
|
||||||
|
let ship = this.playing_ship;
|
||||||
|
if (ship) {
|
||||||
|
let action = ship.actions.getById(action_id);
|
||||||
|
if (action) {
|
||||||
|
if (!target) {
|
||||||
|
target = action.getDefaultTarget(ship);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.apply(this, ship, target)) {
|
||||||
|
this.performChecks();
|
||||||
|
|
||||||
|
if (!this.ended) {
|
||||||
|
this.applyDiffs([new ShipActionEndedDiff(ship, action, target)]);
|
||||||
|
|
||||||
|
if (ship.playing && ship.getValue("hull") <= 0) {
|
||||||
|
// Playing ship died during its action, force a turn end
|
||||||
|
this.applyOneAction(EndTurnAction.SINGLETON.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Action not found on ship", action_id, ship);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Cannot apply action - ship not playing", action_id, this);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revert the last applied action
|
||||||
|
*
|
||||||
|
* This will remove diffs from the log, so pay attention to other log clients!
|
||||||
|
*/
|
||||||
|
revertOneAction(): void {
|
||||||
|
let client = new BattleLogClient(this, this.log);
|
||||||
|
while (!client.atStart() && !(client.getCurrent() instanceof ShipActionUsedDiff)) {
|
||||||
|
client.backward();
|
||||||
|
}
|
||||||
|
if (!client.atStart()) {
|
||||||
|
client.backward();
|
||||||
|
}
|
||||||
|
client.truncate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,23 @@
|
||||||
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);
|
|
||||||
|
|
||||||
cheats.win();
|
cheats.win();
|
||||||
|
|
||||||
check.equals(battle.ended, true, "ended");
|
check.equals(battle.ended, true, "ended");
|
||||||
check.same(nn(battle.outcome).winner, battle.fleets[0].id, "winner");
|
check.same(nn(battle.outcome).winner, battle.fleets[0].id, "winner");
|
||||||
check.equals(any(battle.fleets[1].ships, ship => ship.alive), false, "all enemies dead");
|
check.equals(any(battle.fleets[1].ships, ship => ship.alive), false, "all enemies dead");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("loses a battle", check => {
|
test.case("loses 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);
|
||||||
|
|
||||||
cheats.lose();
|
cheats.lose();
|
||||||
|
|
||||||
check.equals(battle.ended, true, "ended");
|
check.equals(battle.ended, true, "ended");
|
||||||
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");
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
|
@ -1,40 +1,43 @@
|
||||||
module TK.SpaceTac {
|
import { iforeach } from "../common/Iterators";
|
||||||
/**
|
import { first } from "../common/Tools";
|
||||||
* Cheat helpers for current battle
|
import { Battle } from "./Battle";
|
||||||
*
|
import { Player } from "./Player";
|
||||||
* May be used from the console to help development
|
|
||||||
*/
|
|
||||||
export class BattleCheats {
|
|
||||||
battle: Battle
|
|
||||||
player: Player
|
|
||||||
|
|
||||||
constructor(battle: Battle, player: Player) {
|
/**
|
||||||
this.battle = battle;
|
* Cheat helpers for current battle
|
||||||
this.player = player;
|
*
|
||||||
}
|
* May be used from the console to help development
|
||||||
|
*/
|
||||||
|
export class BattleCheats {
|
||||||
|
battle: Battle
|
||||||
|
player: Player
|
||||||
|
|
||||||
/**
|
constructor(battle: Battle, player: Player) {
|
||||||
* Make player win the current battle
|
this.battle = battle;
|
||||||
*/
|
this.player = player;
|
||||||
win(): void {
|
}
|
||||||
iforeach(this.battle.iships(), ship => {
|
|
||||||
if (!this.player.is(ship.fleet.player)) {
|
|
||||||
ship.setDead();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.battle.endBattle(this.player.fleet);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make player lose the current battle
|
* Make player win the current battle
|
||||||
*/
|
*/
|
||||||
lose(): void {
|
win(): void {
|
||||||
iforeach(this.battle.iships(), ship => {
|
iforeach(this.battle.iships(), ship => {
|
||||||
if (this.player.is(ship.fleet.player)) {
|
if (!this.player.is(ship.fleet.player)) {
|
||||||
ship.setDead();
|
ship.setDead();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.battle.endBattle(first(this.battle.fleets, fleet => !this.player.is(fleet.player)));
|
this.battle.endBattle(this.player.fleet);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Make player lose the current battle
|
||||||
|
*/
|
||||||
|
lose(): void {
|
||||||
|
iforeach(this.battle.iships(), ship => {
|
||||||
|
if (this.player.is(ship.fleet.player)) {
|
||||||
|
ship.setDead();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.battle.endBattle(first(this.battle.fleets, fleet => !this.player.is(fleet.player)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,106 +1,104 @@
|
||||||
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();
|
let ship2 = battle.fleets[1].addShip();
|
||||||
let ship2 = battle.fleets[1].addShip();
|
let checks = new BattleChecks(battle);
|
||||||
let checks = new BattleChecks(battle);
|
check.equals(checks.checkVictory(), [], "no victory");
|
||||||
check.equals(checks.checkVictory(), [], "no victory");
|
|
||||||
|
|
||||||
battle.cycle = 5;
|
battle.cycle = 5;
|
||||||
ship1.setDead();
|
ship1.setDead();
|
||||||
check.equals(checks.checkVictory(), [new EndBattleDiff(battle.fleets[1], 5)], "victory");
|
check.equals(checks.checkVictory(), [new EndBattleDiff(battle.fleets[1], 5)], "victory");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("fixes ship values", check => {
|
test.case("fixes ship values", check => {
|
||||||
let battle = new Battle();
|
let battle = new Battle();
|
||||||
let ship1 = battle.fleets[0].addShip();
|
let ship1 = battle.fleets[0].addShip();
|
||||||
let ship2 = battle.fleets[1].addShip();
|
let ship2 = battle.fleets[1].addShip();
|
||||||
let checks = new BattleChecks(battle);
|
let checks = new BattleChecks(battle);
|
||||||
check.equals(checks.checkShipValues(), [], "no value to fix");
|
check.equals(checks.checkShipValues(), [], "no value to fix");
|
||||||
|
|
||||||
ship1.setValue("hull", -4);
|
ship1.setValue("hull", -4);
|
||||||
TestTools.setAttribute(ship2, "shield_capacity", 48);
|
TestTools.setAttribute(ship2, "shield_capacity", 48);
|
||||||
ship2.setValue("shield", 60);
|
ship2.setValue("shield", 60);
|
||||||
check.equals(checks.checkShipValues(), [
|
check.equals(checks.checkShipValues(), [
|
||||||
new ShipValueDiff(ship1, "hull", 4),
|
new ShipValueDiff(ship1, "hull", 4),
|
||||||
new ShipValueDiff(ship2, "shield", -12),
|
new ShipValueDiff(ship2, "shield", -12),
|
||||||
], "fixed values");
|
], "fixed values");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("marks ships as dead, except the playing one", check => {
|
test.case("marks ships as dead, except the playing one", check => {
|
||||||
let battle = TestTools.createBattle(1, 2);
|
let battle = TestTools.createBattle(1, 2);
|
||||||
let [ship1, ship2, ship3] = battle.play_order;
|
let [ship1, ship2, ship3] = battle.play_order;
|
||||||
let checks = new BattleChecks(battle);
|
let checks = new BattleChecks(battle);
|
||||||
check.equals(checks.checkDeadShips(), [], "no ship to mark as dead");
|
check.equals(checks.checkDeadShips(), [], "no ship to mark as dead");
|
||||||
|
|
||||||
battle.ships.list().forEach(ship => ship.setValue("hull", 0));
|
battle.ships.list().forEach(ship => ship.setValue("hull", 0));
|
||||||
|
|
||||||
let result = checks.checkDeadShips();
|
let result = checks.checkDeadShips();
|
||||||
check.equals(result, [new ShipDeathDiff(battle, ship2)], "ship2 marked as dead");
|
check.equals(result, [new ShipDeathDiff(battle, ship2)], "ship2 marked as dead");
|
||||||
battle.applyDiffs(result);
|
battle.applyDiffs(result);
|
||||||
|
|
||||||
result = checks.checkDeadShips();
|
result = checks.checkDeadShips();
|
||||||
check.equals(result, [new ShipDeathDiff(battle, ship3)], "ship3 marked as dead");
|
check.equals(result, [new ShipDeathDiff(battle, ship3)], "ship3 marked as dead");
|
||||||
battle.applyDiffs(result);
|
battle.applyDiffs(result);
|
||||||
|
|
||||||
result = checks.checkDeadShips();
|
result = checks.checkDeadShips();
|
||||||
check.equals(result, [], "ship1 left playing");
|
check.equals(result, [], "ship1 left playing");
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("fixes area effects", check => {
|
test.case("fixes area effects", check => {
|
||||||
let battle = new Battle();
|
let battle = new Battle();
|
||||||
let ship1 = battle.fleets[0].addShip();
|
let ship1 = battle.fleets[0].addShip();
|
||||||
let ship2 = battle.fleets[1].addShip();
|
let ship2 = battle.fleets[1].addShip();
|
||||||
let checks = new BattleChecks(battle);
|
let checks = new BattleChecks(battle);
|
||||||
|
|
||||||
check.in("initial state", check => {
|
check.in("initial state", check => {
|
||||||
check.equals(checks.checkAreaEffects(), [], "effects diff");
|
check.equals(checks.checkAreaEffects(), [], "effects diff");
|
||||||
});
|
});
|
||||||
|
|
||||||
let effect1 = ship1.active_effects.add(new StickyEffect(new BaseEffect("e1")));
|
let effect1 = ship1.active_effects.add(new StickyEffect(new BaseEffect("e1")));
|
||||||
let effect2 = ship1.active_effects.add(new BaseEffect("e2"));
|
let effect2 = ship1.active_effects.add(new BaseEffect("e2"));
|
||||||
let effect3 = ship1.active_effects.add(new BaseEffect("e3"));
|
let effect3 = ship1.active_effects.add(new BaseEffect("e3"));
|
||||||
check.patch(battle, "getAreaEffects", (): [Ship, BaseEffect][] => [[ship1, effect3]]);
|
check.patch(battle, "getAreaEffects", (): [Ship, BaseEffect][] => [[ship1, effect3]]);
|
||||||
check.in("sticky+obsolete+missing", check => {
|
check.in("sticky+obsolete+missing", check => {
|
||||||
check.equals(checks.checkAreaEffects(), [
|
check.equals(checks.checkAreaEffects(), [
|
||||||
new ShipEffectRemovedDiff(ship1, effect2),
|
new ShipEffectRemovedDiff(ship1, effect2),
|
||||||
new ShipEffectAddedDiff(ship2, effect3)
|
new ShipEffectAddedDiff(ship2, effect3)
|
||||||
], "effects diff");
|
], "effects diff");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("applies vigilance actions", check => {
|
test.case("applies vigilance actions", check => {
|
||||||
let battle = new Battle();
|
let battle = new Battle();
|
||||||
let ship1 = battle.fleets[0].addShip();
|
let ship1 = battle.fleets[0].addShip();
|
||||||
ship1.setArenaPosition(100, 100);
|
ship1.setArenaPosition(100, 100);
|
||||||
TestTools.setShipModel(ship1, 10, 0, 5);
|
TestTools.setShipModel(ship1, 10, 0, 5);
|
||||||
let ship2 = battle.fleets[1].addShip();
|
let ship2 = battle.fleets[1].addShip();
|
||||||
ship2.setArenaPosition(1000, 1000);
|
ship2.setArenaPosition(1000, 1000);
|
||||||
TestTools.setShipModel(ship2, 10);
|
TestTools.setShipModel(ship2, 10);
|
||||||
TestTools.setShipPlaying(battle, ship1);
|
TestTools.setShipPlaying(battle, ship1);
|
||||||
|
|
||||||
let vig1 = ship1.actions.addCustom(new VigilanceAction("Vig1", { radius: 100, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(1)] }));
|
let vig1 = ship1.actions.addCustom(new VigilanceAction("Vig1", { radius: 100, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(1)] }));
|
||||||
let vig2 = ship1.actions.addCustom(new VigilanceAction("Vig2", { radius: 50, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(2)] }));
|
let vig2 = ship1.actions.addCustom(new VigilanceAction("Vig2", { radius: 50, filter: ActionTargettingFilter.ENEMIES }, { intruder_effects: [new DamageEffect(2)] }));
|
||||||
let vig3 = ship1.actions.addCustom(new VigilanceAction("Vig3", { radius: 100, filter: ActionTargettingFilter.ALLIES }, { intruder_effects: [new DamageEffect(3)] }));
|
let vig3 = ship1.actions.addCustom(new VigilanceAction("Vig3", { radius: 100, filter: ActionTargettingFilter.ALLIES }, { intruder_effects: [new DamageEffect(3)] }));
|
||||||
battle.applyOneAction(vig1.id);
|
battle.applyOneAction(vig1.id);
|
||||||
battle.applyOneAction(vig2.id);
|
battle.applyOneAction(vig2.id);
|
||||||
battle.applyOneAction(vig3.id);
|
battle.applyOneAction(vig3.id);
|
||||||
|
|
||||||
let checks = new BattleChecks(battle);
|
let checks = new BattleChecks(battle);
|
||||||
check.in("initial state", check => {
|
check.in("initial state", check => {
|
||||||
check.equals(checks.checkAreaEffects(), [], "effects diff");
|
check.equals(checks.checkAreaEffects(), [], "effects diff");
|
||||||
});
|
});
|
||||||
|
|
||||||
ship2.setArenaPosition(100, 160);
|
ship2.setArenaPosition(100, 160);
|
||||||
check.in("ship2 moved in range", check => {
|
check.in("ship2 moved in range", check => {
|
||||||
check.equals(checks.checkAreaEffects(), [
|
check.equals(checks.checkAreaEffects(), [
|
||||||
new ShipEffectAddedDiff(ship2, vig1.effects[0]),
|
new ShipEffectAddedDiff(ship2, vig1.effects[0]),
|
||||||
new VigilanceAppliedDiff(ship1, vig1, ship2),
|
new VigilanceAppliedDiff(ship1, vig1, ship2),
|
||||||
new ShipDamageDiff(ship2, 1, 0),
|
new ShipDamageDiff(ship2, 1, 0),
|
||||||
new ShipValueDiff(ship2, "hull", -1),
|
new ShipValueDiff(ship2, "hull", -1),
|
||||||
], "effects diff");
|
], "effects diff");
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
|
@ -1,164 +1,174 @@
|
||||||
module TK.SpaceTac {
|
import { ifirst, iforeach, imaterialize } from "../common/Iterators";
|
||||||
/**
|
import { RObjectContainer } from "../common/RObject";
|
||||||
* List of checks to apply at the end of an action, to ensure a correct battle state
|
import { any, first, flatten, keys } from "../common/Tools";
|
||||||
*
|
import { Battle } from "./Battle";
|
||||||
* This is useful when the list of effects simulated by an action was missing something
|
import { BaseBattleDiff } from "./diffs/BaseBattleDiff";
|
||||||
*
|
import { EndBattleDiff } from "./diffs/EndBattleDiff";
|
||||||
* To fix the state, new diffs will be applied
|
import { ShipEffectAddedDiff, ShipEffectRemovedDiff } from "./diffs/ShipEffectAddedDiff";
|
||||||
*/
|
import { ShipValueDiff } from "./diffs/ShipValueDiff";
|
||||||
export class BattleChecks {
|
import { StickyEffect } from "./effects/StickyEffect";
|
||||||
constructor(private battle: Battle) {
|
import { Ship } from "./Ship";
|
||||||
}
|
import { SHIP_VALUES } from "./ShipValue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply all the checks
|
* List of checks to apply at the end of an action, to ensure a correct battle state
|
||||||
*/
|
*
|
||||||
apply(): BaseBattleDiff[] {
|
* This is useful when the list of effects simulated by an action was missing something
|
||||||
let all: BaseBattleDiff[] = [];
|
*
|
||||||
let diffs: BaseBattleDiff[];
|
* To fix the state, new diffs will be applied
|
||||||
let loops = 0;
|
*/
|
||||||
|
export class BattleChecks {
|
||||||
|
constructor(private battle: Battle) {
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
/**
|
||||||
diffs = this.checkAll();
|
* Apply all the checks
|
||||||
|
*/
|
||||||
|
apply(): BaseBattleDiff[] {
|
||||||
|
let all: BaseBattleDiff[] = [];
|
||||||
|
let diffs: BaseBattleDiff[];
|
||||||
|
let loops = 0;
|
||||||
|
|
||||||
if (diffs.length > 0) {
|
do {
|
||||||
//console.log("Battle checks diffs", diffs);
|
diffs = this.checkAll();
|
||||||
this.battle.applyDiffs(diffs);
|
|
||||||
all = all.concat(diffs);
|
|
||||||
}
|
|
||||||
|
|
||||||
loops += 1;
|
if (diffs.length > 0) {
|
||||||
if (loops >= 1000) {
|
//console.log("Battle checks diffs", diffs);
|
||||||
console.error("Battle checks stuck in infinite loop", diffs);
|
this.battle.applyDiffs(diffs);
|
||||||
break;
|
all = all.concat(diffs);
|
||||||
}
|
}
|
||||||
} while (diffs.length > 0);
|
|
||||||
|
|
||||||
return all;
|
loops += 1;
|
||||||
}
|
if (loops >= 1000) {
|
||||||
|
console.error("Battle checks stuck in infinite loop", diffs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (diffs.length > 0);
|
||||||
|
|
||||||
/**
|
return all;
|
||||||
* Get a list of diffs to apply to fix the battle state
|
}
|
||||||
*
|
|
||||||
* This may not contain ALL the diffs needed, and should be called again while it returns diffs.
|
|
||||||
*/
|
|
||||||
checkAll(): BaseBattleDiff[] {
|
|
||||||
let diffs: BaseBattleDiff[] = [];
|
|
||||||
|
|
||||||
if (this.battle.ended) {
|
/**
|
||||||
return diffs;
|
* Get a list of diffs to apply to fix the battle state
|
||||||
}
|
*
|
||||||
|
* This may not contain ALL the diffs needed, and should be called again while it returns diffs.
|
||||||
|
*/
|
||||||
|
checkAll(): BaseBattleDiff[] {
|
||||||
|
let diffs: BaseBattleDiff[] = [];
|
||||||
|
|
||||||
diffs = this.checkAreaEffects();
|
if (this.battle.ended) {
|
||||||
if (diffs.length) {
|
return diffs;
|
||||||
return diffs;
|
|
||||||
}
|
|
||||||
|
|
||||||
diffs = this.checkShipValues();
|
|
||||||
if (diffs.length) {
|
|
||||||
return diffs;
|
|
||||||
}
|
|
||||||
|
|
||||||
diffs = this.checkDeadShips();
|
|
||||||
if (diffs.length) {
|
|
||||||
return diffs;
|
|
||||||
}
|
|
||||||
|
|
||||||
diffs = this.checkVictory();
|
|
||||||
if (diffs.length) {
|
|
||||||
return diffs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks victory conditions, to put an end to the battle
|
|
||||||
*/
|
|
||||||
checkVictory(): BaseBattleDiff[] {
|
|
||||||
if (this.battle.ended) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let fleets = this.battle.fleets;
|
|
||||||
if (any(fleets, fleet => !fleet.isAlive())) {
|
|
||||||
const winner = first(fleets, fleet => fleet.isAlive());
|
|
||||||
return [new EndBattleDiff(winner, this.battle.cycle)];
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that ship values stays in their allowed range
|
|
||||||
*/
|
|
||||||
checkShipValues(): BaseBattleDiff[] {
|
|
||||||
let result: BaseBattleDiff[] = [];
|
|
||||||
|
|
||||||
iforeach(this.battle.iships(true), ship => {
|
|
||||||
keys(SHIP_VALUES).forEach(valuename => {
|
|
||||||
let value = ship.getValue(valuename);
|
|
||||||
if (value < 0) {
|
|
||||||
result.push(new ShipValueDiff(ship, valuename, -value));
|
|
||||||
} else {
|
|
||||||
let maximum = ship.getAttribute(<any>(valuename + "_capacity"));
|
|
||||||
if (value > maximum) {
|
|
||||||
result.push(new ShipValueDiff(ship, valuename, maximum - value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that not-playing ships with no more hull are dead
|
|
||||||
*/
|
|
||||||
checkDeadShips(): BaseBattleDiff[] {
|
|
||||||
// We do one ship at a time, because the state of one ship may depend on another
|
|
||||||
let dying = ifirst(this.battle.iships(true), ship => !ship.playing && ship.getValue("hull") <= 0);
|
|
||||||
|
|
||||||
if (dying) {
|
|
||||||
return dying.getDeathDiffs(this.battle);
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the diffs to apply to a ship, if moving at a given location
|
|
||||||
*/
|
|
||||||
getAreaEffectsDiff(ship: Ship): BaseBattleDiff[] {
|
|
||||||
let result: BaseBattleDiff[] = [];
|
|
||||||
let expected = this.battle.getAreaEffects(ship);
|
|
||||||
let expected_hash = new RObjectContainer(expected.map(x => x[1]));
|
|
||||||
|
|
||||||
// Remove obsolete effects
|
|
||||||
ship.active_effects.list().forEach(effect => {
|
|
||||||
if (!(effect instanceof StickyEffect) && !expected_hash.get(effect.id)) {
|
|
||||||
result.push(new ShipEffectRemovedDiff(ship, effect));
|
|
||||||
result = result.concat(effect.getOffDiffs(ship));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add missing effects
|
|
||||||
expected.forEach(([source, effect]) => {
|
|
||||||
if (!ship.active_effects.get(effect.id)) {
|
|
||||||
result.push(new ShipEffectAddedDiff(ship, effect));
|
|
||||||
result = result.concat(effect.getOnDiffs(ship, source));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check area effects (remove obsolete ones, and add missing ones)
|
|
||||||
*/
|
|
||||||
checkAreaEffects(): BaseBattleDiff[] {
|
|
||||||
let ships = imaterialize(this.battle.iships(true));
|
|
||||||
return flatten(ships.map(ship => this.getAreaEffectsDiff(ship)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diffs = this.checkAreaEffects();
|
||||||
|
if (diffs.length) {
|
||||||
|
return diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
diffs = this.checkShipValues();
|
||||||
|
if (diffs.length) {
|
||||||
|
return diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
diffs = this.checkDeadShips();
|
||||||
|
if (diffs.length) {
|
||||||
|
return diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
diffs = this.checkVictory();
|
||||||
|
if (diffs.length) {
|
||||||
|
return diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks victory conditions, to put an end to the battle
|
||||||
|
*/
|
||||||
|
checkVictory(): BaseBattleDiff[] {
|
||||||
|
if (this.battle.ended) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let fleets = this.battle.fleets;
|
||||||
|
if (any(fleets, fleet => !fleet.isAlive())) {
|
||||||
|
const winner = first(fleets, fleet => fleet.isAlive());
|
||||||
|
return [new EndBattleDiff(winner, this.battle.cycle)];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that ship values stays in their allowed range
|
||||||
|
*/
|
||||||
|
checkShipValues(): BaseBattleDiff[] {
|
||||||
|
let result: BaseBattleDiff[] = [];
|
||||||
|
|
||||||
|
iforeach(this.battle.iships(true), ship => {
|
||||||
|
keys(SHIP_VALUES).forEach(valuename => {
|
||||||
|
let value = ship.getValue(valuename);
|
||||||
|
if (value < 0) {
|
||||||
|
result.push(new ShipValueDiff(ship, valuename, -value));
|
||||||
|
} else {
|
||||||
|
let maximum = ship.getAttribute(<any>(valuename + "_capacity"));
|
||||||
|
if (value > maximum) {
|
||||||
|
result.push(new ShipValueDiff(ship, valuename, maximum - value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that not-playing ships with no more hull are dead
|
||||||
|
*/
|
||||||
|
checkDeadShips(): BaseBattleDiff[] {
|
||||||
|
// We do one ship at a time, because the state of one ship may depend on another
|
||||||
|
let dying = ifirst(this.battle.iships(true), ship => !ship.playing && ship.getValue("hull") <= 0);
|
||||||
|
|
||||||
|
if (dying) {
|
||||||
|
return dying.getDeathDiffs(this.battle);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the diffs to apply to a ship, if moving at a given location
|
||||||
|
*/
|
||||||
|
getAreaEffectsDiff(ship: Ship): BaseBattleDiff[] {
|
||||||
|
let result: BaseBattleDiff[] = [];
|
||||||
|
let expected = this.battle.getAreaEffects(ship);
|
||||||
|
let expected_hash = new RObjectContainer(expected.map(x => x[1]));
|
||||||
|
|
||||||
|
// Remove obsolete effects
|
||||||
|
ship.active_effects.list().forEach(effect => {
|
||||||
|
if (!(effect instanceof StickyEffect) && !expected_hash.get(effect.id)) {
|
||||||
|
result.push(new ShipEffectRemovedDiff(ship, effect));
|
||||||
|
result = result.concat(effect.getOffDiffs(ship));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add missing effects
|
||||||
|
expected.forEach(([source, effect]) => {
|
||||||
|
if (!ship.active_effects.get(effect.id)) {
|
||||||
|
result.push(new ShipEffectAddedDiff(ship, effect));
|
||||||
|
result = result.concat(effect.getOnDiffs(ship, source));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check area effects (remove obsolete ones, and add missing ones)
|
||||||
|
*/
|
||||||
|
checkAreaEffects(): BaseBattleDiff[] {
|
||||||
|
let ships = imaterialize(this.battle.iships(true));
|
||||||
|
return flatten(ships.map(ship => this.getAreaEffectsDiff(ship)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,34 @@
|
||||||
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());
|
ship1a.level.forceLevel(3);
|
||||||
ship1a.level.forceLevel(3);
|
let ship1b = fleet1.addShip(new Ship());
|
||||||
let ship1b = fleet1.addShip(new Ship());
|
ship1b.level.forceLevel(4);
|
||||||
ship1b.level.forceLevel(4);
|
let fleet2 = new Fleet();
|
||||||
let fleet2 = new Fleet();
|
let ship2a = fleet2.addShip(new Ship());
|
||||||
let ship2a = fleet2.addShip(new Ship());
|
ship2a.level.forceLevel(6);
|
||||||
ship2a.level.forceLevel(6);
|
let ship2b = fleet2.addShip(new Ship());
|
||||||
let ship2b = fleet2.addShip(new Ship());
|
ship2b.level.forceLevel(8);
|
||||||
ship2b.level.forceLevel(8);
|
check.equals(ship1a.level.getExperience(), 300);
|
||||||
check.equals(ship1a.level.getExperience(), 300);
|
check.equals(ship1b.level.getExperience(), 600);
|
||||||
check.equals(ship1b.level.getExperience(), 600);
|
check.equals(ship2a.level.getExperience(), 1500);
|
||||||
check.equals(ship2a.level.getExperience(), 1500);
|
check.equals(ship2b.level.getExperience(), 2800);
|
||||||
check.equals(ship2b.level.getExperience(), 2800);
|
|
||||||
|
|
||||||
// draw
|
// draw
|
||||||
let outcome = new BattleOutcome(null);
|
let outcome = new BattleOutcome(null);
|
||||||
outcome.grantExperience([fleet1, fleet2]);
|
outcome.grantExperience([fleet1, fleet2]);
|
||||||
check.equals(ship1a.level.getExperience(), 345);
|
check.equals(ship1a.level.getExperience(), 345);
|
||||||
check.equals(ship1b.level.getExperience(), 645);
|
check.equals(ship1b.level.getExperience(), 645);
|
||||||
check.equals(ship2a.level.getExperience(), 1511);
|
check.equals(ship2a.level.getExperience(), 1511);
|
||||||
check.equals(ship2b.level.getExperience(), 2811);
|
check.equals(ship2b.level.getExperience(), 2811);
|
||||||
|
|
||||||
// win/lose
|
// win/lose
|
||||||
outcome = new BattleOutcome(fleet1);
|
outcome = new BattleOutcome(fleet1);
|
||||||
outcome.grantExperience([fleet1, fleet2]);
|
outcome.grantExperience([fleet1, fleet2]);
|
||||||
check.equals(ship1a.level.getExperience(), 480);
|
check.equals(ship1a.level.getExperience(), 480);
|
||||||
check.equals(ship1b.level.getExperience(), 780);
|
check.equals(ship1b.level.getExperience(), 780);
|
||||||
check.equals(ship2a.level.getExperience(), 1518);
|
check.equals(ship2a.level.getExperience(), 1518);
|
||||||
check.equals(ship2b.level.getExperience(), 2818);
|
check.equals(ship2b.level.getExperience(), 2818);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
|
@ -1,34 +1,36 @@
|
||||||
module TK.SpaceTac {
|
import { RObjectId } from "../common/RObject";
|
||||||
/**
|
import { flatten, sum } from "../common/Tools";
|
||||||
* Result of an ended battle
|
import { Fleet } from "./Fleet";
|
||||||
*
|
|
||||||
* This stores the winner, and the retrievable loot
|
|
||||||
*/
|
|
||||||
export class BattleOutcome {
|
|
||||||
// Indicates if the battle is a draw (no winner)
|
|
||||||
draw: boolean
|
|
||||||
|
|
||||||
// Victorious fleet
|
/**
|
||||||
winner: RObjectId | null
|
* Result of an ended battle
|
||||||
|
*
|
||||||
|
* This stores the winner, and the retrievable loot
|
||||||
|
*/
|
||||||
|
export class BattleOutcome {
|
||||||
|
// Indicates if the battle is a draw (no winner)
|
||||||
|
draw: boolean
|
||||||
|
|
||||||
constructor(winner: Fleet | null) {
|
// Victorious fleet
|
||||||
this.winner = winner ? winner.id : null;
|
winner: RObjectId | null
|
||||||
this.draw = winner ? false : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
constructor(winner: Fleet | null) {
|
||||||
* Grant experience to participating fleets
|
this.winner = winner ? winner.id : null;
|
||||||
*/
|
this.draw = winner ? false : true;
|
||||||
grantExperience(fleets: Fleet[]) {
|
}
|
||||||
fleets.forEach(fleet => {
|
|
||||||
let winfactor = (fleet.is(this.winner)) ? 0.03 : (this.draw ? 0.01 : 0.005);
|
/**
|
||||||
let enemies = flatten(fleets.filter(f => f !== fleet).map(f => f.ships));
|
* Grant experience to participating fleets
|
||||||
let difficulty = sum(enemies.map(enemy => 100 + enemy.level.getExperience()));
|
*/
|
||||||
fleet.ships.forEach(ship => {
|
grantExperience(fleets: Fleet[]) {
|
||||||
ship.level.addExperience(Math.floor(difficulty * winfactor));
|
fleets.forEach(fleet => {
|
||||||
ship.level.checkLevelUp();
|
let winfactor = (fleet.is(this.winner)) ? 0.03 : (this.draw ? 0.01 : 0.005);
|
||||||
});
|
let enemies = flatten(fleets.filter(f => f !== fleet).map(f => f.ships));
|
||||||
});
|
let difficulty = sum(enemies.map(enemy => 100 + enemy.level.getExperience()));
|
||||||
}
|
fleet.ships.forEach(ship => {
|
||||||
}
|
ship.level.addExperience(Math.floor(difficulty * winfactor));
|
||||||
|
ship.level.checkLevelUp();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +1,73 @@
|
||||||
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, {});
|
|
||||||
|
|
||||||
stats.addStat("Test", 1, true);
|
stats.addStat("Test", 1, true);
|
||||||
check.equals(stats.stats, { Test: [1, 0] });
|
check.equals(stats.stats, { Test: [1, 0] });
|
||||||
|
|
||||||
stats.addStat("Test", 1, true);
|
stats.addStat("Test", 1, true);
|
||||||
check.equals(stats.stats, { Test: [2, 0] });
|
check.equals(stats.stats, { Test: [2, 0] });
|
||||||
|
|
||||||
stats.addStat("Test", 1, false);
|
stats.addStat("Test", 1, false);
|
||||||
check.equals(stats.stats, { Test: [2, 1] });
|
check.equals(stats.stats, { Test: [2, 1] });
|
||||||
|
|
||||||
stats.addStat("Other Test", 10, true);
|
stats.addStat("Other Test", 10, true);
|
||||||
check.equals(stats.stats, { Test: [2, 1], "Other Test": [10, 0] });
|
check.equals(stats.stats, { Test: [2, 1], "Other Test": [10, 0] });
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("collects damage dealt", check => {
|
test.case("collects damage dealt", check => {
|
||||||
let stats = new BattleStats();
|
let stats = new BattleStats();
|
||||||
let battle = new Battle();
|
let battle = new Battle();
|
||||||
let attacker = battle.fleets[0].addShip();
|
let attacker = battle.fleets[0].addShip();
|
||||||
let defender = battle.fleets[1].addShip();
|
let defender = battle.fleets[1].addShip();
|
||||||
stats.processLog(battle.log, battle.fleets[0]);
|
stats.processLog(battle.log, battle.fleets[0]);
|
||||||
check.equals(stats.stats, {});
|
check.equals(stats.stats, {});
|
||||||
|
|
||||||
battle.log.add(new ShipDamageDiff(defender, 1, 3, 2));
|
battle.log.add(new ShipDamageDiff(defender, 1, 3, 2));
|
||||||
stats.processLog(battle.log, battle.fleets[0], true);
|
stats.processLog(battle.log, battle.fleets[0], true);
|
||||||
check.equals(stats.stats, { "Damage taken": [0, 1], "Damage shielded": [0, 3], "Damage evaded": [0, 2] });
|
check.equals(stats.stats, { "Damage taken": [0, 1], "Damage shielded": [0, 3], "Damage evaded": [0, 2] });
|
||||||
|
|
||||||
battle.log.add(new ShipDamageDiff(attacker, 2, 1, 3));
|
battle.log.add(new ShipDamageDiff(attacker, 2, 1, 3));
|
||||||
stats.processLog(battle.log, battle.fleets[0], true);
|
stats.processLog(battle.log, battle.fleets[0], true);
|
||||||
check.equals(stats.stats, { "Damage taken": [2, 1], "Damage shielded": [1, 3], "Damage evaded": [3, 2] });
|
check.equals(stats.stats, { "Damage taken": [2, 1], "Damage shielded": [1, 3], "Damage evaded": [3, 2] });
|
||||||
|
|
||||||
battle.log.add(new ShipDamageDiff(defender, 1, 1, 1));
|
battle.log.add(new ShipDamageDiff(defender, 1, 1, 1));
|
||||||
stats.processLog(battle.log, battle.fleets[0], true);
|
stats.processLog(battle.log, battle.fleets[0], true);
|
||||||
check.equals(stats.stats, { "Damage taken": [2, 2], "Damage shielded": [1, 4], "Damage evaded": [3, 3] });
|
check.equals(stats.stats, { "Damage taken": [2, 2], "Damage shielded": [1, 4], "Damage evaded": [3, 3] });
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("collects distance moved", check => {
|
test.case("collects distance moved", check => {
|
||||||
let stats = new BattleStats();
|
let stats = new BattleStats();
|
||||||
let battle = new Battle();
|
let battle = new Battle();
|
||||||
let attacker = battle.fleets[0].addShip();
|
let attacker = battle.fleets[0].addShip();
|
||||||
let defender = battle.fleets[1].addShip();
|
let defender = battle.fleets[1].addShip();
|
||||||
stats.processLog(battle.log, battle.fleets[0]);
|
stats.processLog(battle.log, battle.fleets[0]);
|
||||||
check.equals(stats.stats, {});
|
check.equals(stats.stats, {});
|
||||||
|
|
||||||
battle.log.add(new ShipMoveDiff(attacker, new ArenaLocationAngle(0, 0), new ArenaLocationAngle(10, 0)));
|
battle.log.add(new ShipMoveDiff(attacker, new ArenaLocationAngle(0, 0), new ArenaLocationAngle(10, 0)));
|
||||||
stats.processLog(battle.log, battle.fleets[0], true);
|
stats.processLog(battle.log, battle.fleets[0], true);
|
||||||
check.equals(stats.stats, { "Move distance (km)": [10, 0] });
|
check.equals(stats.stats, { "Move distance (km)": [10, 0] });
|
||||||
|
|
||||||
battle.log.add(new ShipMoveDiff(defender, new ArenaLocationAngle(10, 5), new ArenaLocationAngle(10, 63)));
|
battle.log.add(new ShipMoveDiff(defender, new ArenaLocationAngle(10, 5), new ArenaLocationAngle(10, 63)));
|
||||||
stats.processLog(battle.log, battle.fleets[0], true);
|
stats.processLog(battle.log, battle.fleets[0], true);
|
||||||
check.equals(stats.stats, { "Move distance (km)": [10, 58] });
|
check.equals(stats.stats, { "Move distance (km)": [10, 58] });
|
||||||
})
|
})
|
||||||
|
|
||||||
test.case("collects deployed drones", check => {
|
test.case("collects deployed drones", check => {
|
||||||
let stats = new BattleStats();
|
let stats = new BattleStats();
|
||||||
let battle = new Battle();
|
let battle = new Battle();
|
||||||
let attacker = battle.fleets[0].addShip();
|
let attacker = battle.fleets[0].addShip();
|
||||||
let defender = battle.fleets[1].addShip();
|
let defender = battle.fleets[1].addShip();
|
||||||
stats.processLog(battle.log, battle.fleets[0]);
|
stats.processLog(battle.log, battle.fleets[0]);
|
||||||
check.equals(stats.stats, {});
|
check.equals(stats.stats, {});
|
||||||
|
|
||||||
battle.log.add(new DroneDeployedDiff(new Drone(attacker)));
|
battle.log.add(new DroneDeployedDiff(new Drone(attacker)));
|
||||||
stats.processLog(battle.log, battle.fleets[0], true);
|
stats.processLog(battle.log, battle.fleets[0], true);
|
||||||
check.equals(stats.stats, { "Drones deployed": [1, 0] });
|
check.equals(stats.stats, { "Drones deployed": [1, 0] });
|
||||||
|
|
||||||
battle.log.add(new DroneDeployedDiff(new Drone(defender)));
|
battle.log.add(new DroneDeployedDiff(new Drone(defender)));
|
||||||
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] });
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
|
@ -1,64 +1,70 @@
|
||||||
module TK.SpaceTac {
|
import { any, iteritems } from "../common/Tools";
|
||||||
/**
|
import { BattleLog } from "./BattleLog";
|
||||||
* Statistics collection over a battle
|
import { BaseBattleShipDiff } from "./diffs/BaseBattleDiff";
|
||||||
*/
|
import { DroneDeployedDiff } from "./diffs/DroneDeployedDiff";
|
||||||
export class BattleStats {
|
import { ShipDamageDiff } from "./diffs/ShipDamageDiff";
|
||||||
stats: { [name: string]: [number, number] } = {}
|
import { ShipMoveDiff } from "./diffs/ShipMoveDiff";
|
||||||
|
import { Fleet } from "./Fleet";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a value to the collector
|
* Statistics collection over a battle
|
||||||
*/
|
*/
|
||||||
addStat(name: string, value: number, attacker: boolean) {
|
export class BattleStats {
|
||||||
if (!this.stats[name]) {
|
stats: { [name: string]: [number, number] } = {}
|
||||||
this.stats[name] = [0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attacker) {
|
/**
|
||||||
this.stats[name] = [this.stats[name][0] + value, this.stats[name][1]];
|
* Add a value to the collector
|
||||||
} else {
|
*/
|
||||||
this.stats[name] = [this.stats[name][0], this.stats[name][1] + value];
|
addStat(name: string, value: number, attacker: boolean) {
|
||||||
}
|
if (!this.stats[name]) {
|
||||||
}
|
this.stats[name] = [0, 0];
|
||||||
|
|
||||||
/**
|
|
||||||
* Get important stats
|
|
||||||
*/
|
|
||||||
getImportant(maxcount: number): { name: string, attacker: number, defender: number }[] {
|
|
||||||
// TODO Sort by importance
|
|
||||||
let result: { name: string, attacker: number, defender: number }[] = [];
|
|
||||||
iteritems(this.stats, (name, [attacker, defender]) => {
|
|
||||||
if (result.length < maxcount) {
|
|
||||||
result.push({ name: name, attacker: Math.round(attacker), defender: Math.round(defender) });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a battle log
|
|
||||||
*/
|
|
||||||
processLog(log: BattleLog, attacker: Fleet, clear = true) {
|
|
||||||
if (clear) {
|
|
||||||
this.stats = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
let n = log.count();
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
let diff = log.get(i);
|
|
||||||
if (diff instanceof BaseBattleShipDiff) {
|
|
||||||
let diff_ship = diff.ship_id;
|
|
||||||
let attacker_ship = any(attacker.ships, ship => ship.is(diff_ship));
|
|
||||||
if (diff instanceof ShipDamageDiff) {
|
|
||||||
this.addStat("Damage evaded", diff.evaded, attacker_ship);
|
|
||||||
this.addStat("Damage shielded", diff.shield, attacker_ship);
|
|
||||||
this.addStat("Damage taken", diff.hull, attacker_ship);
|
|
||||||
} else if (diff instanceof ShipMoveDiff) {
|
|
||||||
this.addStat("Move distance (km)", diff.getDistance(), attacker_ship);
|
|
||||||
} else if (diff instanceof DroneDeployedDiff) {
|
|
||||||
this.addStat("Drones deployed", 1, attacker_ship);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (attacker) {
|
||||||
|
this.stats[name] = [this.stats[name][0] + value, this.stats[name][1]];
|
||||||
|
} else {
|
||||||
|
this.stats[name] = [this.stats[name][0], this.stats[name][1] + value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get important stats
|
||||||
|
*/
|
||||||
|
getImportant(maxcount: number): { name: string, attacker: number, defender: number }[] {
|
||||||
|
// TODO Sort by importance
|
||||||
|
let result: { name: string, attacker: number, defender: number }[] = [];
|
||||||
|
iteritems(this.stats, (name, [attacker, defender]) => {
|
||||||
|
if (result.length < maxcount) {
|
||||||
|
result.push({ name: name, attacker: Math.round(attacker), defender: Math.round(defender) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a battle log
|
||||||
|
*/
|
||||||
|
processLog(log: BattleLog, attacker: Fleet, clear = true) {
|
||||||
|
if (clear) {
|
||||||
|
this.stats = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = log.count();
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
let diff = log.get(i);
|
||||||
|
if (diff instanceof BaseBattleShipDiff) {
|
||||||
|
let diff_ship = diff.ship_id;
|
||||||
|
let attacker_ship = any(attacker.ships, ship => ship.is(diff_ship));
|
||||||
|
if (diff instanceof ShipDamageDiff) {
|
||||||
|
this.addStat("Damage evaded", diff.evaded, attacker_ship);
|
||||||
|
this.addStat("Damage shielded", diff.shield, attacker_ship);
|
||||||
|
this.addStat("Damage taken", diff.hull, attacker_ship);
|
||||||
|
} else if (diff instanceof ShipMoveDiff) {
|
||||||
|
this.addStat("Move distance (km)", diff.getDistance(), attacker_ship);
|
||||||
|
} else if (diff instanceof DroneDeployedDiff) {
|
||||||
|
this.addStat("Drones deployed", 1, attacker_ship);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,36 @@
|
||||||
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);
|
|
||||||
|
|
||||||
cooldown.use();
|
cooldown.use();
|
||||||
check.equals(cooldown.canUse(), true);
|
check.equals(cooldown.canUse(), true);
|
||||||
|
|
||||||
cooldown.configure(2, 3);
|
cooldown.configure(2, 3);
|
||||||
check.equals(cooldown.canUse(), true);
|
check.equals(cooldown.canUse(), true);
|
||||||
|
|
||||||
cooldown.use();
|
cooldown.use();
|
||||||
check.equals(cooldown.canUse(), true);
|
check.equals(cooldown.canUse(), true);
|
||||||
|
|
||||||
cooldown.use();
|
cooldown.use();
|
||||||
check.equals(cooldown.canUse(), false);
|
check.equals(cooldown.canUse(), false);
|
||||||
|
|
||||||
cooldown.cool();
|
cooldown.cool();
|
||||||
check.equals(cooldown.canUse(), false);
|
check.equals(cooldown.canUse(), false);
|
||||||
|
|
||||||
cooldown.cool();
|
cooldown.cool();
|
||||||
check.equals(cooldown.canUse(), false);
|
check.equals(cooldown.canUse(), false);
|
||||||
|
|
||||||
cooldown.cool();
|
cooldown.cool();
|
||||||
check.equals(cooldown.canUse(), true);
|
check.equals(cooldown.canUse(), true);
|
||||||
|
|
||||||
cooldown.configure(1, 0);
|
cooldown.configure(1, 0);
|
||||||
check.equals(cooldown.canUse(), true);
|
check.equals(cooldown.canUse(), true);
|
||||||
|
|
||||||
cooldown.use();
|
cooldown.use();
|
||||||
check.equals(cooldown.canUse(), false);
|
check.equals(cooldown.canUse(), false);
|
||||||
|
|
||||||
cooldown.cool();
|
cooldown.cool();
|
||||||
check.equals(cooldown.canUse(), true);
|
check.equals(cooldown.canUse(), true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
|
@ -1,93 +1,91 @@
|
||||||
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
|
|
||||||
|
|
||||||
// Accumulated heat to dissipate (number of turns)
|
// Accumulated heat to dissipate (number of turns)
|
||||||
heat = 0
|
heat = 0
|
||||||
|
|
||||||
// Maximum number of uses allowed per turn before overheating (0 for unlimited)
|
// Maximum number of uses allowed per turn before overheating (0 for unlimited)
|
||||||
overheat = 0
|
overheat = 0
|
||||||
|
|
||||||
// Number of "end turn" needed to cooldown when overheated
|
// Number of "end turn" needed to cooldown when overheated
|
||||||
cooling = 1
|
cooling = 1
|
||||||
|
|
||||||
constructor(overheat = 0, cooling = 1) {
|
constructor(overheat = 0, cooling = 1) {
|
||||||
this.configure(overheat, cooling);
|
this.configure(overheat, cooling);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
return `Overheat ${this.overheat} / Cooldown ${this.cooling}`;
|
return `Overheat ${this.overheat} / Cooldown ${this.cooling}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the equipment can be used in regards to heat
|
* Check if the equipment can be used in regards to heat
|
||||||
*/
|
*/
|
||||||
canUse(): boolean {
|
canUse(): boolean {
|
||||||
return this.heat == 0;
|
return this.heat == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the equipment would overheat if used
|
* Check if the equipment would overheat if used
|
||||||
*/
|
*/
|
||||||
willOverheat(): boolean {
|
willOverheat(): boolean {
|
||||||
return this.overheat > 0 && this.uses + 1 >= this.overheat;
|
return this.overheat > 0 && this.uses + 1 >= this.overheat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the number of uses before overheating
|
* Check the number of uses before overheating
|
||||||
*/
|
*/
|
||||||
getRemainingUses(): number {
|
getRemainingUses(): number {
|
||||||
if (this.overheat) {
|
if (this.overheat) {
|
||||||
return (this.heat > 0) ? 0 : (this.overheat - this.uses);
|
return (this.heat > 0) ? 0 : (this.overheat - this.uses);
|
||||||
} else {
|
} else {
|
||||||
return Infinity;
|
return Infinity;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure the overheat and cooling
|
|
||||||
*/
|
|
||||||
configure(overheat: number, cooling: number) {
|
|
||||||
this.overheat = overheat;
|
|
||||||
this.cooling = Math.max(1, cooling);
|
|
||||||
this.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use the equipment, increasing the heat
|
|
||||||
*/
|
|
||||||
use(times = 1): void {
|
|
||||||
if (this.overheat) {
|
|
||||||
this.uses += times;
|
|
||||||
if (this.uses >= this.overheat) {
|
|
||||||
this.heat = this.cooling;
|
|
||||||
} else {
|
|
||||||
this.heat = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply one cooling-down step if necessary
|
|
||||||
*/
|
|
||||||
cool(steps = 1): void {
|
|
||||||
this.heat = Math.max(this.heat - steps, 0);
|
|
||||||
|
|
||||||
if (this.heat == 0) {
|
|
||||||
this.uses = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset the cooldown (typically at the end of turn)
|
|
||||||
*/
|
|
||||||
reset(): void {
|
|
||||||
this.uses = 0;
|
|
||||||
this.heat = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the overheat and cooling
|
||||||
|
*/
|
||||||
|
configure(overheat: number, cooling: number) {
|
||||||
|
this.overheat = overheat;
|
||||||
|
this.cooling = Math.max(1, cooling);
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the equipment, increasing the heat
|
||||||
|
*/
|
||||||
|
use(times = 1): void {
|
||||||
|
if (this.overheat) {
|
||||||
|
this.uses += times;
|
||||||
|
if (this.uses >= this.overheat) {
|
||||||
|
this.heat = this.cooling;
|
||||||
|
} else {
|
||||||
|
this.heat = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply one cooling-down step if necessary
|
||||||
|
*/
|
||||||
|
cool(steps = 1): void {
|
||||||
|
this.heat = Math.max(this.heat - steps, 0);
|
||||||
|
|
||||||
|
if (this.heat == 0) {
|
||||||
|
this.uses = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the cooldown (typically at the end of turn)
|
||||||
|
*/
|
||||||
|
reset(): void {
|
||||||
|
this.uses = 0;
|
||||||
|
this.heat = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,60 @@
|
||||||
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);
|
TestTools.setShipModel(ship, 100, 0, 10);
|
||||||
TestTools.setShipModel(ship, 100, 0, 10);
|
let weapon = new DeployDroneAction("testdrone", { power: 2 }, { deploy_distance: 300, drone_radius: 30, drone_effects: [new AttributeEffect("evasion", 15)] });
|
||||||
let weapon = new DeployDroneAction("testdrone", { power: 2 }, { deploy_distance: 300, drone_radius: 30, drone_effects: [new AttributeEffect("evasion", 15)] });
|
ship.actions.addCustom(weapon);
|
||||||
ship.actions.addCustom(weapon);
|
let engine = TestTools.addEngine(ship, 1000);
|
||||||
let engine = TestTools.addEngine(ship, 1000);
|
|
||||||
|
|
||||||
TestTools.actionChain(check, battle, [
|
TestTools.actionChain(check, battle, [
|
||||||
[ship, weapon, Target.newFromLocation(150, 50)], // deploy out of effects radius
|
[ship, weapon, Target.newFromLocation(150, 50)], // deploy out of effects radius
|
||||||
[ship, engine, Target.newFromLocation(110, 50)], // move out of effects radius
|
[ship, engine, Target.newFromLocation(110, 50)], // move out of effects radius
|
||||||
[ship, engine, Target.newFromLocation(130, 50)], // move in effects radius
|
[ship, engine, Target.newFromLocation(130, 50)], // move in effects radius
|
||||||
[ship, weapon, Target.newFromShip(ship)], // recall
|
[ship, weapon, Target.newFromShip(ship)], // recall
|
||||||
[ship, weapon, Target.newFromLocation(130, 70)], // deploy in effects radius
|
[ship, weapon, Target.newFromLocation(130, 70)], // deploy in effects radius
|
||||||
], [
|
], [
|
||||||
check => {
|
check => {
|
||||||
check.equals(ship.active_effects.count(), 0, "active effects");
|
check.equals(ship.active_effects.count(), 0, "active effects");
|
||||||
check.equals(ship.getValue("power"), 10, "power");
|
check.equals(ship.getValue("power"), 10, "power");
|
||||||
check.equals(battle.drones.count(), 0, "drone count");
|
check.equals(battle.drones.count(), 0, "drone count");
|
||||||
},
|
},
|
||||||
check => {
|
check => {
|
||||||
check.equals(ship.active_effects.count(), 0, "active effects");
|
check.equals(ship.active_effects.count(), 0, "active effects");
|
||||||
check.equals(ship.getValue("power"), 8, "power");
|
check.equals(ship.getValue("power"), 8, "power");
|
||||||
check.equals(battle.drones.count(), 1, "drone count");
|
check.equals(battle.drones.count(), 1, "drone count");
|
||||||
},
|
},
|
||||||
check => {
|
check => {
|
||||||
check.equals(ship.active_effects.count(), 0, "active effects");
|
check.equals(ship.active_effects.count(), 0, "active effects");
|
||||||
check.equals(ship.getValue("power"), 7, "power");
|
check.equals(ship.getValue("power"), 7, "power");
|
||||||
check.equals(battle.drones.count(), 1, "drone count");
|
check.equals(battle.drones.count(), 1, "drone count");
|
||||||
},
|
},
|
||||||
check => {
|
check => {
|
||||||
check.equals(ship.active_effects.count(), 1, "active effects");
|
check.equals(ship.active_effects.count(), 1, "active effects");
|
||||||
check.equals(ship.getValue("power"), 6, "power");
|
check.equals(ship.getValue("power"), 6, "power");
|
||||||
check.equals(battle.drones.count(), 1, "drone count");
|
check.equals(battle.drones.count(), 1, "drone count");
|
||||||
},
|
},
|
||||||
check => {
|
check => {
|
||||||
check.equals(ship.active_effects.count(), 0, "active effects");
|
check.equals(ship.active_effects.count(), 0, "active effects");
|
||||||
check.equals(ship.getValue("power"), 8, "power");
|
check.equals(ship.getValue("power"), 8, "power");
|
||||||
check.equals(battle.drones.count(), 0, "drone count");
|
check.equals(battle.drones.count(), 0, "drone count");
|
||||||
},
|
},
|
||||||
check => {
|
check => {
|
||||||
check.equals(ship.active_effects.count(), 1, "active effects");
|
check.equals(ship.active_effects.count(), 1, "active effects");
|
||||||
check.equals(ship.getValue("power"), 6, "power");
|
check.equals(ship.getValue("power"), 6, "power");
|
||||||
check.equals(battle.drones.count(), 1, "drone count");
|
check.equals(battle.drones.count(), 1, "drone count");
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.case("builds a textual description", check => {
|
test.case("builds a textual description", check => {
|
||||||
let drone = new Drone(new Ship());
|
let drone = new Drone(new Ship());
|
||||||
check.equals(drone.getDescription(), "While deployed:\n• do nothing");
|
check.equals(drone.getDescription(), "While deployed:\n• do nothing");
|
||||||
|
|
||||||
drone.effects = [
|
drone.effects = [
|
||||||
new DamageEffect(5),
|
new DamageEffect(5),
|
||||||
new AttributeEffect("evasion", 1)
|
new AttributeEffect("evasion", 1)
|
||||||
]
|
]
|
||||||
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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
|
@ -1,63 +1,70 @@
|
||||||
module TK.SpaceTac {
|
import { ifilter, imaterialize } from "../common/Iterators"
|
||||||
/**
|
import { RObject, RObjectId } from "../common/RObject"
|
||||||
* Drones are static objects that apply effects in a circular zone around themselves.
|
import { DeployDroneAction } from "./actions/DeployDroneAction"
|
||||||
*/
|
import { ArenaLocation } from "./ArenaLocation"
|
||||||
export class Drone extends RObject {
|
import { Battle } from "./Battle"
|
||||||
// ID of the owning ship
|
import { BaseEffect } from "./effects/BaseEffect"
|
||||||
owner: RObjectId
|
import { Ship } from "./Ship"
|
||||||
|
import { Target } from "./Target"
|
||||||
|
|
||||||
// Code of the drone
|
/**
|
||||||
code: string
|
* Drones are static objects that apply effects in a circular zone around themselves.
|
||||||
|
*/
|
||||||
|
export class Drone extends RObject {
|
||||||
|
// ID of the owning ship
|
||||||
|
owner: RObjectId
|
||||||
|
|
||||||
// Location in arena
|
// Code of the drone
|
||||||
x = 0
|
code: string
|
||||||
y = 0
|
|
||||||
radius = 0
|
|
||||||
|
|
||||||
// Effects to apply
|
// Location in arena
|
||||||
effects: BaseEffect[] = []
|
x = 0
|
||||||
|
y = 0
|
||||||
|
radius = 0
|
||||||
|
|
||||||
// Action that triggered that drone
|
// Effects to apply
|
||||||
parent: DeployDroneAction | null = null;
|
effects: BaseEffect[] = []
|
||||||
|
|
||||||
constructor(owner: Ship, code = "drone") {
|
// Action that triggered that drone
|
||||||
super();
|
parent: DeployDroneAction | null = null;
|
||||||
|
|
||||||
this.owner = owner.id;
|
constructor(owner: Ship, code = "drone") {
|
||||||
this.code = code;
|
super();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
this.owner = owner.id;
|
||||||
* Return the current location of the drone
|
this.code = code;
|
||||||
*/
|
}
|
||||||
get location(): ArenaLocation {
|
|
||||||
return new ArenaLocation(this.x, this.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a textual description of this drone
|
* Return the current location of the drone
|
||||||
*/
|
*/
|
||||||
getDescription(): string {
|
get location(): ArenaLocation {
|
||||||
let effects = this.effects.map(effect => "• " + effect.getDescription()).join("\n");
|
return new ArenaLocation(this.x, this.y);
|
||||||
if (effects.length == 0) {
|
}
|
||||||
effects = "• do nothing";
|
|
||||||
}
|
|
||||||
return `While deployed:\n${effects}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a location is in range
|
* Get a textual description of this drone
|
||||||
*/
|
*/
|
||||||
isInRange(x: number, y: number): boolean {
|
getDescription(): string {
|
||||||
return Target.newFromLocation(x, y).getDistanceTo(this) <= this.radius;
|
let effects = this.effects.map(effect => "• " + effect.getDescription()).join("\n");
|
||||||
}
|
if (effects.length == 0) {
|
||||||
|
effects = "• do nothing";
|
||||||
/**
|
|
||||||
* Get the list of affected ships.
|
|
||||||
*/
|
|
||||||
getAffectedShips(battle: Battle): Ship[] {
|
|
||||||
let ships = ifilter(battle.iships(), ship => ship.alive && ship.isInCircle(this.x, this.y, this.radius));
|
|
||||||
return imaterialize(ships);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return `While deployed:\n${effects}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a location is in range
|
||||||
|
*/
|
||||||
|
isInRange(x: number, y: number): boolean {
|
||||||
|
return Target.newFromLocation(x, y).getDistanceTo(this) <= this.radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of affected ships.
|
||||||
|
*/
|
||||||
|
getAffectedShips(battle: Battle): Ship[] {
|
||||||
|
let ships = ifilter(battle.iships(), ship => ship.alive && ship.isInCircle(this.x, this.y, this.radius));
|
||||||
|
return imaterialize(ships);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,43 +1,46 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("ExclusionAreas", test => {
|
import { ArenaLocationAngle } from "./ArenaLocation";
|
||||||
test.case("constructs from a ship or battle", check => {
|
import { Battle } from "./Battle";
|
||||||
let battle = new Battle();
|
import { ExclusionAreas } from "./ExclusionAreas";
|
||||||
battle.border = 17;
|
|
||||||
battle.ship_separation = 31;
|
|
||||||
let ship1 = battle.fleets[0].addShip();
|
|
||||||
ship1.setArenaPosition(12, 5);
|
|
||||||
let ship2 = battle.fleets[1].addShip();
|
|
||||||
ship2.setArenaPosition(43, 89);
|
|
||||||
|
|
||||||
let exclusion = ExclusionAreas.fromBattle(battle);
|
testing("ExclusionAreas", test => {
|
||||||
check.equals(exclusion.hard_border, 17);
|
test.case("constructs from a ship or battle", check => {
|
||||||
check.equals(exclusion.effective_obstacle, 31);
|
let battle = new Battle();
|
||||||
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]);
|
battle.border = 17;
|
||||||
|
battle.ship_separation = 31;
|
||||||
|
let ship1 = battle.fleets[0].addShip();
|
||||||
|
ship1.setArenaPosition(12, 5);
|
||||||
|
let ship2 = battle.fleets[1].addShip();
|
||||||
|
ship2.setArenaPosition(43, 89);
|
||||||
|
|
||||||
exclusion = ExclusionAreas.fromBattle(battle, [ship1], 120);
|
let exclusion = ExclusionAreas.fromBattle(battle);
|
||||||
check.equals(exclusion.hard_border, 17);
|
check.equals(exclusion.hard_border, 17);
|
||||||
check.equals(exclusion.effective_obstacle, 120);
|
check.equals(exclusion.effective_obstacle, 31);
|
||||||
check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]);
|
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]);
|
||||||
|
|
||||||
exclusion = ExclusionAreas.fromBattle(battle, [ship2], 10);
|
exclusion = ExclusionAreas.fromBattle(battle, [ship1], 120);
|
||||||
check.equals(exclusion.hard_border, 17);
|
check.equals(exclusion.hard_border, 17);
|
||||||
check.equals(exclusion.effective_obstacle, 31);
|
check.equals(exclusion.effective_obstacle, 120);
|
||||||
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]);
|
check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]);
|
||||||
|
|
||||||
exclusion = ExclusionAreas.fromShip(ship1);
|
exclusion = ExclusionAreas.fromBattle(battle, [ship2], 10);
|
||||||
check.equals(exclusion.hard_border, 17);
|
check.equals(exclusion.hard_border, 17);
|
||||||
check.equals(exclusion.effective_obstacle, 31);
|
check.equals(exclusion.effective_obstacle, 31);
|
||||||
check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]);
|
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]);
|
||||||
|
|
||||||
exclusion = ExclusionAreas.fromShip(ship2, 99);
|
exclusion = ExclusionAreas.fromShip(ship1);
|
||||||
check.equals(exclusion.hard_border, 17);
|
check.equals(exclusion.hard_border, 17);
|
||||||
check.equals(exclusion.effective_obstacle, 99);
|
check.equals(exclusion.effective_obstacle, 31);
|
||||||
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]);
|
check.equals(exclusion.obstacles, [new ArenaLocationAngle(43, 89)]);
|
||||||
|
|
||||||
exclusion = ExclusionAreas.fromShip(ship2, 10, false);
|
exclusion = ExclusionAreas.fromShip(ship2, 99);
|
||||||
check.equals(exclusion.hard_border, 17);
|
check.equals(exclusion.hard_border, 17);
|
||||||
check.equals(exclusion.effective_obstacle, 31);
|
check.equals(exclusion.effective_obstacle, 99);
|
||||||
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]);
|
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5)]);
|
||||||
})
|
|
||||||
})
|
exclusion = ExclusionAreas.fromShip(ship2, 10, false);
|
||||||
}
|
check.equals(exclusion.hard_border, 17);
|
||||||
|
check.equals(exclusion.effective_obstacle, 31);
|
||||||
|
check.equals(exclusion.obstacles, [new ArenaLocationAngle(12, 5), new ArenaLocationAngle(43, 89)]);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -1,96 +1,101 @@
|
||||||
module TK.SpaceTac {
|
import { ifilter, imap, imaterialize } from "../common/Iterators"
|
||||||
/**
|
import { cmp, contains, sorted } from "../common/Tools"
|
||||||
* Helper for working with exclusion areas (areas where a ship cannot go)
|
import { arenaDistance, ArenaLocation } from "./ArenaLocation"
|
||||||
*
|
import { Battle } from "./Battle"
|
||||||
* There are three types of exclusion:
|
import { Ship } from "./Ship"
|
||||||
* - Hard border exclusion, that prevents a ship from being too close to the battle edges
|
import { Target } from "./Target"
|
||||||
* - 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
|
|
||||||
*/
|
|
||||||
export class ExclusionAreas {
|
|
||||||
xmin: number
|
|
||||||
xmax: number
|
|
||||||
ymin: number
|
|
||||||
ymax: number
|
|
||||||
active: boolean
|
|
||||||
hard_border = 50
|
|
||||||
hard_obstacle = 100
|
|
||||||
effective_obstacle = this.hard_obstacle
|
|
||||||
obstacles: ArenaLocation[] = []
|
|
||||||
|
|
||||||
constructor(width: number, height: number) {
|
/**
|
||||||
this.xmin = 0;
|
* Helper for working with exclusion areas (areas where a ship cannot go)
|
||||||
this.xmax = width - 1;
|
*
|
||||||
this.ymin = 0;
|
* There are three types of exclusion:
|
||||||
this.ymax = height - 1;
|
* - Hard border exclusion, that prevents a ship from being too close to the battle edges
|
||||||
this.active = width > 0 && height > 0;
|
* - 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
|
||||||
|
*/
|
||||||
|
export class ExclusionAreas {
|
||||||
|
xmin: number
|
||||||
|
xmax: number
|
||||||
|
ymin: number
|
||||||
|
ymax: number
|
||||||
|
active: boolean
|
||||||
|
hard_border = 50
|
||||||
|
hard_obstacle = 100
|
||||||
|
effective_obstacle = this.hard_obstacle
|
||||||
|
obstacles: ArenaLocation[] = []
|
||||||
|
|
||||||
/**
|
constructor(width: number, height: number) {
|
||||||
* Build an exclusion helper from a battle.
|
this.xmin = 0;
|
||||||
*/
|
this.xmax = width - 1;
|
||||||
static fromBattle(battle: Battle, ignore_ships: Ship[] = [], soft_distance = 0): ExclusionAreas {
|
this.ymin = 0;
|
||||||
let result = new ExclusionAreas(battle.width, battle.height);
|
this.ymax = height - 1;
|
||||||
result.hard_border = battle.border;
|
this.active = width > 0 && height > 0;
|
||||||
result.hard_obstacle = battle.ship_separation;
|
}
|
||||||
let obstacles = imap(ifilter(battle.iships(true), ship => !contains(ignore_ships, ship)), ship => ship.location);
|
|
||||||
result.configure(imaterialize(obstacles), soft_distance);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an exclusion helper for a ship.
|
* Build an exclusion helper from a battle.
|
||||||
*
|
*/
|
||||||
* If *ignore_self* is True, the ship will itself not be included in exclusion areas.
|
static fromBattle(battle: Battle, ignore_ships: Ship[] = [], soft_distance = 0): ExclusionAreas {
|
||||||
*/
|
let result = new ExclusionAreas(battle.width, battle.height);
|
||||||
static fromShip(ship: Ship, soft_distance = 0, ignore_self = true): ExclusionAreas {
|
result.hard_border = battle.border;
|
||||||
let battle = ship.getBattle();
|
result.hard_obstacle = battle.ship_separation;
|
||||||
if (battle) {
|
let obstacles = imap(ifilter(battle.iships(true), ship => !contains(ignore_ships, ship)), ship => ship.location);
|
||||||
return ExclusionAreas.fromBattle(battle, ignore_self ? [ship] : [], soft_distance);
|
result.configure(imaterialize(obstacles), soft_distance);
|
||||||
} else {
|
return result;
|
||||||
return new ExclusionAreas(0, 0);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure the areas for next check calls.
|
* Build an exclusion helper for a ship.
|
||||||
*/
|
*
|
||||||
configure(obstacles: ArenaLocation[], soft_distance: number) {
|
* If *ignore_self* is True, the ship will itself not be included in exclusion areas.
|
||||||
this.obstacles = obstacles;
|
*/
|
||||||
this.effective_obstacle = Math.max(soft_distance, this.hard_obstacle);
|
static fromShip(ship: Ship, soft_distance = 0, ignore_self = true): ExclusionAreas {
|
||||||
}
|
let battle = ship.getBattle();
|
||||||
|
if (battle) {
|
||||||
/**
|
return ExclusionAreas.fromBattle(battle, ignore_self ? [ship] : [], soft_distance);
|
||||||
* Keep a location outside exclusion areas, when coming from a source.
|
} else {
|
||||||
*
|
return new ExclusionAreas(0, 0);
|
||||||
* It will return the furthest location on the [source, location] segment, that is not inside an exclusion
|
|
||||||
* area.
|
|
||||||
*/
|
|
||||||
stopBefore(location: ArenaLocation, source: ArenaLocation): ArenaLocation {
|
|
||||||
if (!this.active) {
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
|
|
||||||
let target = Target.newFromLocation(location.x, location.y);
|
|
||||||
|
|
||||||
// Keep out of arena borders
|
|
||||||
target = target.keepInsideRectangle(this.xmin + this.hard_border, this.ymin + this.hard_border,
|
|
||||||
this.xmax - this.hard_border, this.ymax - this.hard_border,
|
|
||||||
source.x, source.y);
|
|
||||||
|
|
||||||
// Apply collision prevention
|
|
||||||
let obstacles = sorted(this.obstacles, (a, b) => cmp(arenaDistance(a, source), arenaDistance(b, source), true));
|
|
||||||
obstacles.forEach(s => {
|
|
||||||
let new_target = target.moveOutOfCircle(s.x, s.y, this.effective_obstacle, source.x, source.y);
|
|
||||||
if (target != new_target && arenaDistance(s, source) < this.effective_obstacle) {
|
|
||||||
// Already inside the nearest ship's exclusion area
|
|
||||||
target = Target.newFromLocation(source.x, source.y);
|
|
||||||
} else {
|
|
||||||
target = new_target;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new ArenaLocation(target.x, target.y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the areas for next check calls.
|
||||||
|
*/
|
||||||
|
configure(obstacles: ArenaLocation[], soft_distance: number) {
|
||||||
|
this.obstacles = obstacles;
|
||||||
|
this.effective_obstacle = Math.max(soft_distance, this.hard_obstacle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep a location outside exclusion areas, when coming from a source.
|
||||||
|
*
|
||||||
|
* It will return the furthest location on the [source, location] segment, that is not inside an exclusion
|
||||||
|
* area.
|
||||||
|
*/
|
||||||
|
stopBefore(location: ArenaLocation, source: ArenaLocation): ArenaLocation {
|
||||||
|
if (!this.active) {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = Target.newFromLocation(location.x, location.y);
|
||||||
|
|
||||||
|
// Keep out of arena borders
|
||||||
|
target = target.keepInsideRectangle(this.xmin + this.hard_border, this.ymin + this.hard_border,
|
||||||
|
this.xmax - this.hard_border, this.ymax - this.hard_border,
|
||||||
|
source.x, source.y);
|
||||||
|
|
||||||
|
// Apply collision prevention
|
||||||
|
let obstacles = sorted(this.obstacles, (a, b) => cmp(arenaDistance(a, source), arenaDistance(b, source), true));
|
||||||
|
obstacles.forEach(s => {
|
||||||
|
let new_target = target.moveOutOfCircle(s.x, s.y, this.effective_obstacle, source.x, source.y);
|
||||||
|
if (target != new_target && arenaDistance(s, source) < this.effective_obstacle) {
|
||||||
|
// Already inside the nearest ship's exclusion area
|
||||||
|
target = Target.newFromLocation(source.x, source.y);
|
||||||
|
} else {
|
||||||
|
target = new_target;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ArenaLocation(target.x, target.y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,168 +1,174 @@
|
||||||
module TK.SpaceTac {
|
import { RObjectId } from "../common/RObject";
|
||||||
testing("Fleet", test => {
|
import { testing } from "../common/Testing";
|
||||||
test.case("get average level", check => {
|
import { Battle } from "./Battle";
|
||||||
var fleet = new Fleet();
|
import { Fleet } from "./Fleet";
|
||||||
check.equals(fleet.getLevel(), 0);
|
import { Ship } from "./Ship";
|
||||||
|
import { StarLocationType } from "./StarLocation";
|
||||||
|
import { Universe } from "./Universe";
|
||||||
|
|
||||||
fleet.addShip(new Ship());
|
testing("Fleet", test => {
|
||||||
fleet.addShip(new Ship());
|
test.case("get average level", check => {
|
||||||
fleet.addShip(new Ship());
|
var fleet = new Fleet();
|
||||||
|
check.equals(fleet.getLevel(), 0);
|
||||||
|
|
||||||
fleet.ships[0].level.forceLevel(2);
|
fleet.addShip(new Ship());
|
||||||
fleet.ships[1].level.forceLevel(4);
|
fleet.addShip(new Ship());
|
||||||
fleet.ships[2].level.forceLevel(7);
|
fleet.addShip(new Ship());
|
||||||
check.equals(fleet.getLevel(), 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("adds and removes ships", check => {
|
fleet.ships[0].level.forceLevel(2);
|
||||||
let fleet1 = new Fleet();
|
fleet.ships[1].level.forceLevel(4);
|
||||||
let fleet2 = new Fleet();
|
fleet.ships[2].level.forceLevel(7);
|
||||||
|
check.equals(fleet.getLevel(), 4);
|
||||||
|
});
|
||||||
|
|
||||||
let ship1 = fleet1.addShip();
|
test.case("adds and removes ships", check => {
|
||||||
check.equals(fleet1.ships, [ship1]);
|
let fleet1 = new Fleet();
|
||||||
check.equals(fleet2.ships, []);
|
let fleet2 = new Fleet();
|
||||||
|
|
||||||
let ship2 = new Ship();
|
let ship1 = fleet1.addShip();
|
||||||
check.equals(fleet1.ships, [ship1]);
|
check.equals(fleet1.ships, [ship1]);
|
||||||
check.equals(fleet2.ships, []);
|
check.equals(fleet2.ships, []);
|
||||||
|
|
||||||
fleet2.addShip(ship2);
|
let ship2 = new Ship();
|
||||||
check.equals(fleet1.ships, [ship1]);
|
check.equals(fleet1.ships, [ship1]);
|
||||||
check.equals(fleet2.ships, [ship2]);
|
check.equals(fleet2.ships, []);
|
||||||
|
|
||||||
fleet1.addShip(ship2);
|
fleet2.addShip(ship2);
|
||||||
check.equals(fleet1.ships, [ship1, ship2]);
|
check.equals(fleet1.ships, [ship1]);
|
||||||
check.equals(fleet2.ships, []);
|
check.equals(fleet2.ships, [ship2]);
|
||||||
|
|
||||||
fleet1.removeShip(ship1, fleet2);
|
fleet1.addShip(ship2);
|
||||||
check.equals(fleet1.ships, [ship2]);
|
check.equals(fleet1.ships, [ship1, ship2]);
|
||||||
check.equals(fleet2.ships, [ship1]);
|
check.equals(fleet2.ships, []);
|
||||||
|
|
||||||
fleet1.removeShip(ship1);
|
fleet1.removeShip(ship1, fleet2);
|
||||||
check.equals(fleet1.ships, [ship2]);
|
check.equals(fleet1.ships, [ship2]);
|
||||||
check.equals(fleet2.ships, [ship1]);
|
check.equals(fleet2.ships, [ship1]);
|
||||||
|
|
||||||
fleet1.removeShip(ship2);
|
fleet1.removeShip(ship1);
|
||||||
check.equals(fleet1.ships, []);
|
check.equals(fleet1.ships, [ship2]);
|
||||||
check.equals(fleet2.ships, [ship1]);
|
check.equals(fleet2.ships, [ship1]);
|
||||||
});
|
|
||||||
|
|
||||||
test.case("changes location, only using jumps to travel between systems", check => {
|
fleet1.removeShip(ship2);
|
||||||
let fleet = new Fleet();
|
check.equals(fleet1.ships, []);
|
||||||
let universe = new Universe();
|
check.equals(fleet2.ships, [ship1]);
|
||||||
let system1 = universe.addStar();
|
});
|
||||||
let system2 = universe.addStar();
|
|
||||||
let jump1 = system1.addLocation(StarLocationType.WARP);
|
|
||||||
let jump2 = system2.addLocation(StarLocationType.WARP);
|
|
||||||
jump1.setJumpDestination(jump2);
|
|
||||||
jump2.setJumpDestination(jump1);
|
|
||||||
let other1 = system1.addLocation(StarLocationType.PLANET);
|
|
||||||
universe.updateLocations();
|
|
||||||
|
|
||||||
let result = fleet.move(other1);
|
test.case("changes location, only using jumps to travel between systems", check => {
|
||||||
check.in("cannot move from nowhere", check => {
|
let fleet = new Fleet();
|
||||||
check.equals(result, false);
|
let universe = new Universe();
|
||||||
check.equals(fleet.location, null);
|
let system1 = universe.addStar();
|
||||||
});
|
let system2 = universe.addStar();
|
||||||
|
let jump1 = system1.addLocation(StarLocationType.WARP);
|
||||||
|
let jump2 = system2.addLocation(StarLocationType.WARP);
|
||||||
|
jump1.setJumpDestination(jump2);
|
||||||
|
jump2.setJumpDestination(jump1);
|
||||||
|
let other1 = system1.addLocation(StarLocationType.PLANET);
|
||||||
|
universe.updateLocations();
|
||||||
|
|
||||||
fleet.setLocation(other1);
|
let result = fleet.move(other1);
|
||||||
check.in("force set to other1", check => {
|
check.in("cannot move from nowhere", check => {
|
||||||
check.equals(fleet.location, other1.id);
|
check.equals(result, false);
|
||||||
});
|
check.equals(fleet.location, null);
|
||||||
|
|
||||||
result = fleet.move(jump2);
|
|
||||||
check.in("other1=>jump2", check => {
|
|
||||||
check.equals(result, false);
|
|
||||||
check.equals(fleet.location, other1.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
result = fleet.move(jump1);
|
|
||||||
check.in("other1=>jump1", check => {
|
|
||||||
check.equals(result, true);
|
|
||||||
check.equals(fleet.location, jump1.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
result = fleet.move(jump2);
|
|
||||||
check.in("jump1=>jump2", check => {
|
|
||||||
check.equals(result, true);
|
|
||||||
check.equals(fleet.location, jump2.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
result = fleet.move(other1);
|
|
||||||
check.in("jump2=>other1", check => {
|
|
||||||
check.equals(result, false);
|
|
||||||
check.equals(fleet.location, jump2.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
result = fleet.move(jump1);
|
|
||||||
check.in("jump2=>jump1", check => {
|
|
||||||
check.equals(result, true);
|
|
||||||
check.equals(fleet.location, jump1.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("registers presence in locations, and keeps track of visited locations", check => {
|
|
||||||
let fleet = new Fleet();
|
|
||||||
let universe = new Universe();
|
|
||||||
let star = universe.addStar();
|
|
||||||
let loc1 = star.addLocation(StarLocationType.PLANET);
|
|
||||||
let loc2 = star.addLocation(StarLocationType.PLANET);
|
|
||||||
let loc3 = star.addLocation(StarLocationType.PLANET);
|
|
||||||
universe.updateLocations();
|
|
||||||
|
|
||||||
function checks(desc: string, fleets1: Fleet[], fleets2: Fleet[], fleets3: Fleet[], visited: RObjectId[]) {
|
|
||||||
check.in(desc, check => {
|
|
||||||
check.equals(loc1.fleets, fleets1, "loc1 fleets");
|
|
||||||
check.equals(loc2.fleets, fleets2, "loc2 fleets");
|
|
||||||
check.equals(loc3.fleets, fleets3, "loc3 fleets");
|
|
||||||
check.equals(fleet.visited, visited, "visited");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
checks("initial", [], [], [], []);
|
|
||||||
|
|
||||||
fleet.setLocation(loc1);
|
|
||||||
checks("first move to loc1", [fleet], [], [], [loc1.id]);
|
|
||||||
|
|
||||||
fleet.setLocation(loc1);
|
|
||||||
checks("already in loc1", [fleet], [], [], [loc1.id]);
|
|
||||||
|
|
||||||
fleet.setLocation(loc2);
|
|
||||||
checks("first move to loc2", [], [fleet], [], [loc2.id, loc1.id]);
|
|
||||||
|
|
||||||
fleet.setLocation(loc3);
|
|
||||||
checks("first move to loc3", [], [], [fleet], [loc3.id, loc2.id, loc1.id]);
|
|
||||||
|
|
||||||
fleet.setLocation(loc2);
|
|
||||||
checks("go back to loc2", [], [fleet], [], [loc2.id, loc3.id, loc1.id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("checks if a fleet is alive", check => {
|
|
||||||
let battle = new Battle();
|
|
||||||
let fleet = battle.fleets[0];
|
|
||||||
check.equals(fleet.isAlive(), false);
|
|
||||||
|
|
||||||
let ship1 = fleet.addShip();
|
|
||||||
check.equals(fleet.isAlive(), true);
|
|
||||||
|
|
||||||
let ship2 = fleet.addShip();
|
|
||||||
check.equals(fleet.isAlive(), true);
|
|
||||||
|
|
||||||
ship1.setDead();
|
|
||||||
check.equals(fleet.isAlive(), true);
|
|
||||||
|
|
||||||
ship2.setDead();
|
|
||||||
check.equals(fleet.isAlive(), false);
|
|
||||||
|
|
||||||
let ship3 = fleet.addShip();
|
|
||||||
check.equals(fleet.isAlive(), true);
|
|
||||||
|
|
||||||
let ship4 = fleet.addShip();
|
|
||||||
ship4.critical = true;
|
|
||||||
check.equals(fleet.isAlive(), true);
|
|
||||||
|
|
||||||
ship4.setDead();
|
|
||||||
check.equals(fleet.isAlive(), false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
fleet.setLocation(other1);
|
||||||
|
check.in("force set to other1", check => {
|
||||||
|
check.equals(fleet.location, other1.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
result = fleet.move(jump2);
|
||||||
|
check.in("other1=>jump2", check => {
|
||||||
|
check.equals(result, false);
|
||||||
|
check.equals(fleet.location, other1.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
result = fleet.move(jump1);
|
||||||
|
check.in("other1=>jump1", check => {
|
||||||
|
check.equals(result, true);
|
||||||
|
check.equals(fleet.location, jump1.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
result = fleet.move(jump2);
|
||||||
|
check.in("jump1=>jump2", check => {
|
||||||
|
check.equals(result, true);
|
||||||
|
check.equals(fleet.location, jump2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
result = fleet.move(other1);
|
||||||
|
check.in("jump2=>other1", check => {
|
||||||
|
check.equals(result, false);
|
||||||
|
check.equals(fleet.location, jump2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
result = fleet.move(jump1);
|
||||||
|
check.in("jump2=>jump1", check => {
|
||||||
|
check.equals(result, true);
|
||||||
|
check.equals(fleet.location, jump1.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("registers presence in locations, and keeps track of visited locations", check => {
|
||||||
|
let fleet = new Fleet();
|
||||||
|
let universe = new Universe();
|
||||||
|
let star = universe.addStar();
|
||||||
|
let loc1 = star.addLocation(StarLocationType.PLANET);
|
||||||
|
let loc2 = star.addLocation(StarLocationType.PLANET);
|
||||||
|
let loc3 = star.addLocation(StarLocationType.PLANET);
|
||||||
|
universe.updateLocations();
|
||||||
|
|
||||||
|
function checks(desc: string, fleets1: Fleet[], fleets2: Fleet[], fleets3: Fleet[], visited: RObjectId[]) {
|
||||||
|
check.in(desc, check => {
|
||||||
|
check.equals(loc1.fleets, fleets1, "loc1 fleets");
|
||||||
|
check.equals(loc2.fleets, fleets2, "loc2 fleets");
|
||||||
|
check.equals(loc3.fleets, fleets3, "loc3 fleets");
|
||||||
|
check.equals(fleet.visited, visited, "visited");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checks("initial", [], [], [], []);
|
||||||
|
|
||||||
|
fleet.setLocation(loc1);
|
||||||
|
checks("first move to loc1", [fleet], [], [], [loc1.id]);
|
||||||
|
|
||||||
|
fleet.setLocation(loc1);
|
||||||
|
checks("already in loc1", [fleet], [], [], [loc1.id]);
|
||||||
|
|
||||||
|
fleet.setLocation(loc2);
|
||||||
|
checks("first move to loc2", [], [fleet], [], [loc2.id, loc1.id]);
|
||||||
|
|
||||||
|
fleet.setLocation(loc3);
|
||||||
|
checks("first move to loc3", [], [], [fleet], [loc3.id, loc2.id, loc1.id]);
|
||||||
|
|
||||||
|
fleet.setLocation(loc2);
|
||||||
|
checks("go back to loc2", [], [fleet], [], [loc2.id, loc3.id, loc1.id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("checks if a fleet is alive", check => {
|
||||||
|
let battle = new Battle();
|
||||||
|
let fleet = battle.fleets[0];
|
||||||
|
check.equals(fleet.isAlive(), false);
|
||||||
|
|
||||||
|
let ship1 = fleet.addShip();
|
||||||
|
check.equals(fleet.isAlive(), true);
|
||||||
|
|
||||||
|
let ship2 = fleet.addShip();
|
||||||
|
check.equals(fleet.isAlive(), true);
|
||||||
|
|
||||||
|
ship1.setDead();
|
||||||
|
check.equals(fleet.isAlive(), true);
|
||||||
|
|
||||||
|
ship2.setDead();
|
||||||
|
check.equals(fleet.isAlive(), false);
|
||||||
|
|
||||||
|
let ship3 = fleet.addShip();
|
||||||
|
check.equals(fleet.isAlive(), true);
|
||||||
|
|
||||||
|
let ship4 = fleet.addShip();
|
||||||
|
ship4.critical = true;
|
||||||
|
check.equals(fleet.isAlive(), true);
|
||||||
|
|
||||||
|
ship4.setDead();
|
||||||
|
check.equals(fleet.isAlive(), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,151 +1,156 @@
|
||||||
module TK.SpaceTac {
|
import { RObject, RObjectId } from "../common/RObject"
|
||||||
/**
|
import { add, any, remove } from "../common/Tools"
|
||||||
* A fleet of ships, all belonging to the same player
|
import { Battle } from "./Battle"
|
||||||
*/
|
import { Player } from "./Player"
|
||||||
export class Fleet extends RObject {
|
import { Ship } from "./Ship"
|
||||||
// Fleet owner
|
import { StarLocation, StarLocationType } from "./StarLocation"
|
||||||
player: Player
|
|
||||||
|
|
||||||
// Fleet name
|
/**
|
||||||
name: string
|
* A fleet of ships, all belonging to the same player
|
||||||
|
*/
|
||||||
|
export class Fleet extends RObject {
|
||||||
|
// Fleet owner
|
||||||
|
player: Player
|
||||||
|
|
||||||
// List of ships
|
// Fleet name
|
||||||
ships: Ship[]
|
name: string
|
||||||
|
|
||||||
// Current fleet location
|
// List of ships
|
||||||
location: RObjectId | null = null
|
ships: Ship[]
|
||||||
|
|
||||||
// Visited locations (ordered by last visited)
|
// Current fleet location
|
||||||
visited: RObjectId[] = []
|
location: RObjectId | null = null
|
||||||
|
|
||||||
// Current battle in which the fleet is engaged (null if not fighting)
|
// Visited locations (ordered by last visited)
|
||||||
battle: Battle | null = null
|
visited: RObjectId[] = []
|
||||||
|
|
||||||
// Amount of credits available
|
// Current battle in which the fleet is engaged (null if not fighting)
|
||||||
credits = 0
|
battle: Battle | null = null
|
||||||
|
|
||||||
// Create a fleet, bound to a player
|
// Amount of credits available
|
||||||
constructor(player = new Player()) {
|
credits = 0
|
||||||
super();
|
|
||||||
|
|
||||||
this.player = player;
|
// Create a fleet, bound to a player
|
||||||
this.name = player ? player.name : "Fleet";
|
constructor(player = new Player()) {
|
||||||
this.ships = [];
|
super();
|
||||||
}
|
|
||||||
|
|
||||||
jasmineToString(): string {
|
this.player = player;
|
||||||
return `${this.name} [${this.ships.map(ship => ship.getName()).join(",")}]`;
|
this.name = player ? player.name : "Fleet";
|
||||||
}
|
this.ships = [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
jasmineToString(): string {
|
||||||
* Set the owner player
|
return `${this.name} [${this.ships.map(ship => ship.getName()).join(",")}]`;
|
||||||
*/
|
}
|
||||||
setPlayer(player: Player): void {
|
|
||||||
this.player = player;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a location as visited
|
* Set the owner player
|
||||||
*/
|
*/
|
||||||
setVisited(location: StarLocation): void {
|
setPlayer(player: Player): void {
|
||||||
remove(this.visited, location.id);
|
this.player = player;
|
||||||
this.visited.unshift(location.id);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move the fleet to another location, checking that the move is physically possible
|
* Set a location as visited
|
||||||
*
|
*/
|
||||||
* Returns true on success
|
setVisited(location: StarLocation): void {
|
||||||
*/
|
remove(this.visited, location.id);
|
||||||
move(to: StarLocation): boolean {
|
this.visited.unshift(location.id);
|
||||||
if (!this.location) {
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source = to.universe.locations.get(this.location);
|
/**
|
||||||
if (!source) {
|
* Move the fleet to another location, checking that the move is physically possible
|
||||||
return false;
|
*
|
||||||
}
|
* Returns true on success
|
||||||
|
*/
|
||||||
if (source.star != to.star) {
|
move(to: StarLocation): boolean {
|
||||||
// Need to jump, check conditions
|
if (!this.location) {
|
||||||
if (source.type != StarLocationType.WARP || source.jump_dest != to) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setLocation(to);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the current location of the fleet, without condition
|
|
||||||
*/
|
|
||||||
setLocation(location: StarLocation): void {
|
|
||||||
if (this.location) {
|
|
||||||
let previous = location.universe.locations.get(this.location);
|
|
||||||
if (previous) {
|
|
||||||
previous.removeFleet(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.location = location.id;
|
|
||||||
this.setVisited(location);
|
|
||||||
location.addFleet(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a ship this fleet
|
|
||||||
*/
|
|
||||||
addShip(ship = new Ship(null, `${this.name} ${this.ships.length + 1}`)): Ship {
|
|
||||||
if (ship.fleet && ship.fleet != this) {
|
|
||||||
remove(ship.fleet.ships, ship);
|
|
||||||
}
|
|
||||||
add(this.ships, ship);
|
|
||||||
ship.fleet = this;
|
|
||||||
if (this.battle) {
|
|
||||||
this.battle.ships.add(ship);
|
|
||||||
}
|
|
||||||
return ship;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the ship from this fleet, transferring it to another fleet
|
|
||||||
*/
|
|
||||||
removeShip(ship: Ship, fleet = new Fleet()): void {
|
|
||||||
if (ship.fleet === this) {
|
|
||||||
fleet.addShip(ship);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the current battle
|
|
||||||
setBattle(battle: Battle | null): void {
|
|
||||||
this.battle = battle;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the average level of this fleet
|
|
||||||
getLevel(): number {
|
|
||||||
if (this.ships.length === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sum = 0;
|
|
||||||
this.ships.forEach((ship: Ship) => {
|
|
||||||
sum += ship.level.get();
|
|
||||||
});
|
|
||||||
var avg = sum / this.ships.length;
|
|
||||||
return Math.floor(avg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the fleet is considered alive (at least one ship alive, and no critical ship dead)
|
|
||||||
*/
|
|
||||||
isAlive(): boolean {
|
|
||||||
if (any(this.ships, ship => ship.critical && !ship.alive)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return any(this.ships, ship => ship.alive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let source = to.universe.locations.get(this.location);
|
||||||
|
if (!source) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.star != to.star) {
|
||||||
|
// Need to jump, check conditions
|
||||||
|
if (source.type != StarLocationType.WARP || source.jump_dest != to) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setLocation(to);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current location of the fleet, without condition
|
||||||
|
*/
|
||||||
|
setLocation(location: StarLocation): void {
|
||||||
|
if (this.location) {
|
||||||
|
let previous = location.universe.locations.get(this.location);
|
||||||
|
if (previous) {
|
||||||
|
previous.removeFleet(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.location = location.id;
|
||||||
|
this.setVisited(location);
|
||||||
|
location.addFleet(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a ship this fleet
|
||||||
|
*/
|
||||||
|
addShip(ship = new Ship(null, `${this.name} ${this.ships.length + 1}`)): Ship {
|
||||||
|
if (ship.fleet && ship.fleet != this) {
|
||||||
|
remove(ship.fleet.ships, ship);
|
||||||
|
}
|
||||||
|
add(this.ships, ship);
|
||||||
|
ship.fleet = this;
|
||||||
|
if (this.battle) {
|
||||||
|
this.battle.ships.add(ship);
|
||||||
|
}
|
||||||
|
return ship;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the ship from this fleet, transferring it to another fleet
|
||||||
|
*/
|
||||||
|
removeShip(ship: Ship, fleet = new Fleet()): void {
|
||||||
|
if (ship.fleet === this) {
|
||||||
|
fleet.addShip(ship);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the current battle
|
||||||
|
setBattle(battle: Battle | null): void {
|
||||||
|
this.battle = battle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the average level of this fleet
|
||||||
|
getLevel(): number {
|
||||||
|
if (this.ships.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum = 0;
|
||||||
|
this.ships.forEach((ship: Ship) => {
|
||||||
|
sum += ship.level.get();
|
||||||
|
});
|
||||||
|
var avg = sum / this.ships.length;
|
||||||
|
return Math.floor(avg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the fleet is considered alive (at least one ship alive, and no critical ship dead)
|
||||||
|
*/
|
||||||
|
isAlive(): boolean {
|
||||||
|
if (any(this.ships, ship => ship.critical && !ship.alive)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return any(this.ships, ship => ship.alive);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,34 @@
|
||||||
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";
|
||||||
// Random generator to use
|
import { ShipModel } from "./models/ShipModel";
|
||||||
random: RandomGenerator;
|
import { Player } from "./Player";
|
||||||
|
import { ShipGenerator } from "./ShipGenerator";
|
||||||
|
|
||||||
constructor(random = RandomGenerator.global) {
|
// Generator of balanced ships to form a fleet
|
||||||
this.random = random;
|
export class FleetGenerator {
|
||||||
}
|
// Random generator to use
|
||||||
|
random: RandomGenerator;
|
||||||
|
|
||||||
/**
|
constructor(random = RandomGenerator.global) {
|
||||||
* Generate a fleet of a given level
|
this.random = random;
|
||||||
*/
|
}
|
||||||
generate(level: number, player?: Player, ship_count = 3, upgrade = false): Fleet {
|
|
||||||
var fleet = new Fleet(player);
|
|
||||||
var ship_generator = new ShipGenerator(this.random);
|
|
||||||
|
|
||||||
let models = this.random.sample(ShipModel.getDefaultCollection(), ship_count);
|
/**
|
||||||
|
* Generate a fleet of a given level
|
||||||
|
*/
|
||||||
|
generate(level: number, player?: Player, ship_count = 3, upgrade = false): Fleet {
|
||||||
|
var fleet = new Fleet(player);
|
||||||
|
var ship_generator = new ShipGenerator(this.random);
|
||||||
|
|
||||||
range(ship_count).forEach(i => {
|
let models = this.random.sample(ShipModel.getDefaultCollection(), ship_count);
|
||||||
var ship = ship_generator.generate(level, models[i] || null, upgrade);
|
|
||||||
ship.name = ship.model.name;
|
|
||||||
fleet.addShip(ship);
|
|
||||||
});
|
|
||||||
|
|
||||||
return fleet;
|
range(ship_count).forEach(i => {
|
||||||
}
|
var ship = ship_generator.generate(level, models[i] || null, upgrade);
|
||||||
}
|
ship.name = ship.model.name;
|
||||||
|
fleet.addShip(ship);
|
||||||
|
});
|
||||||
|
|
||||||
|
return fleet;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,148 +1,154 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { SkewedRandomGenerator } from "../common/RandomGenerator";
|
||||||
testing("GameSession", test => {
|
import { RObjectContainer } from "../common/RObject";
|
||||||
/**
|
import { testing } from "../common/Testing";
|
||||||
* Compare two sessions
|
import { nn } from "../common/Tools";
|
||||||
*/
|
import { Fleet } from "./Fleet";
|
||||||
function compare(session1: GameSession, session2: GameSession) {
|
import { GameSession } from "./GameSession";
|
||||||
test.check.equals(session1, session2);
|
import { StarLocation, StarLocationType } from "./StarLocation";
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
testing("GameSession", test => {
|
||||||
* Apply deterministic game steps
|
/**
|
||||||
*/
|
* Compare two sessions
|
||||||
function applyGameSteps(session: GameSession): void {
|
*/
|
||||||
var battle = nn(session.getBattle());
|
function compare(session1: GameSession, session2: GameSession) {
|
||||||
battle.advanceToNextShip();
|
test.check.equals(session1, session2);
|
||||||
// TODO Make some fixed moves (AI?)
|
}
|
||||||
battle.endBattle(battle.fleets[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
test.case("serializes to a string", check => {
|
/**
|
||||||
var session = new GameSession();
|
* Apply deterministic game steps
|
||||||
session.startQuickBattle();
|
*/
|
||||||
|
function applyGameSteps(session: GameSession): void {
|
||||||
|
var battle = nn(session.getBattle());
|
||||||
|
battle.advanceToNextShip();
|
||||||
|
// TODO Make some fixed moves (AI?)
|
||||||
|
battle.endBattle(battle.fleets[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// Dump and reload
|
test.case("serializes to a string", check => {
|
||||||
var dumped = session.saveToString();
|
var session = new GameSession();
|
||||||
var loaded_session = GameSession.loadFromString(dumped);
|
session.startQuickBattle();
|
||||||
|
|
||||||
// Check equality
|
// Dump and reload
|
||||||
compare(loaded_session, session);
|
var dumped = session.saveToString();
|
||||||
|
var loaded_session = GameSession.loadFromString(dumped);
|
||||||
|
|
||||||
// Apply game steps
|
// Check equality
|
||||||
applyGameSteps(session);
|
compare(loaded_session, session);
|
||||||
applyGameSteps(loaded_session);
|
|
||||||
|
|
||||||
// Check equality after game steps
|
// Apply game steps
|
||||||
compare(loaded_session, session);
|
applyGameSteps(session);
|
||||||
});
|
applyGameSteps(loaded_session);
|
||||||
|
|
||||||
test.case("generates a quick battle", check => {
|
// Check equality after game steps
|
||||||
var session = new GameSession();
|
compare(loaded_session, session);
|
||||||
session.startQuickBattle();
|
});
|
||||||
|
|
||||||
check.same(session.getBattle(), session.player.fleet.battle, "battle");
|
test.case("generates a quick battle", check => {
|
||||||
check.same(session.player.fleet, session.fleet, "fleet");
|
var session = new GameSession();
|
||||||
check.same(nn(session.getBattle()).fleets[0], session.fleet, "attacker fleet");
|
session.startQuickBattle();
|
||||||
});
|
|
||||||
|
|
||||||
test.case("applies battle outcome to bound player", check => {
|
check.same(session.getBattle(), session.player.fleet.battle, "battle");
|
||||||
let session = new GameSession();
|
check.same(session.player.fleet, session.fleet, "fleet");
|
||||||
check.equals(session.getBattle(), null);
|
check.same(nn(session.getBattle()).fleets[0], session.fleet, "attacker fleet");
|
||||||
|
});
|
||||||
|
|
||||||
let location1 = new StarLocation();
|
test.case("applies battle outcome to bound player", check => {
|
||||||
let location2 = new StarLocation(location1.star);
|
let session = new GameSession();
|
||||||
session.universe.locations = new RObjectContainer([location1, location2]);
|
check.equals(session.getBattle(), null);
|
||||||
|
|
||||||
// Victory case
|
let location1 = new StarLocation();
|
||||||
location1.encounter = new Fleet();
|
let location2 = new StarLocation(location1.star);
|
||||||
session.player.fleet.setLocation(location1);
|
session.universe.locations = new RObjectContainer([location1, location2]);
|
||||||
check.notequals(session.getBattle(), null);
|
|
||||||
check.notequals(location1.encounter, null);
|
|
||||||
|
|
||||||
let battle = nn(session.getBattle());
|
// Victory case
|
||||||
battle.endBattle(session.player.fleet);
|
location1.encounter = new Fleet();
|
||||||
session.setBattleEnded();
|
session.player.fleet.setLocation(location1);
|
||||||
check.notequals(session.getBattle(), null);
|
check.notequals(session.getBattle(), null);
|
||||||
check.equals(location1.encounter, null);
|
check.notequals(location1.encounter, null);
|
||||||
|
|
||||||
// Defeat case
|
let battle = nn(session.getBattle());
|
||||||
location2.encounter = new Fleet();
|
battle.endBattle(session.player.fleet);
|
||||||
session.player.fleet.setLocation(location2);
|
session.setBattleEnded();
|
||||||
check.notequals(session.getBattle(), null);
|
check.notequals(session.getBattle(), null);
|
||||||
check.notequals(location2.encounter, null);
|
check.equals(location1.encounter, null);
|
||||||
|
|
||||||
battle = nn(session.getBattle());
|
// Defeat case
|
||||||
battle.endBattle(null);
|
location2.encounter = new Fleet();
|
||||||
session.setBattleEnded();
|
session.player.fleet.setLocation(location2);
|
||||||
check.notequals(session.getBattle(), null);
|
check.notequals(session.getBattle(), null);
|
||||||
check.notequals(location2.encounter, null);
|
check.notequals(location2.encounter, null);
|
||||||
});
|
|
||||||
|
|
||||||
test.case("generates a new campaign", check => {
|
battle = nn(session.getBattle());
|
||||||
let session = new GameSession();
|
battle.endBattle(null);
|
||||||
|
session.setBattleEnded();
|
||||||
|
check.notequals(session.getBattle(), null);
|
||||||
|
check.notequals(location2.encounter, null);
|
||||||
|
});
|
||||||
|
|
||||||
session.startNewGame(false);
|
test.case("generates a new campaign", check => {
|
||||||
check.notequals(session.player, null);
|
let session = new GameSession();
|
||||||
check.equals(session.player.fleet.ships.length, 0);
|
|
||||||
check.equals(session.player.fleet.credits, 0);
|
|
||||||
check.equals(session.universe.stars.length, 50);
|
|
||||||
check.equals(session.getBattle(), null);
|
|
||||||
check.equals(session.start_location.shop, null);
|
|
||||||
check.equals(session.start_location.encounter, null);
|
|
||||||
check.equals(session.start_location.encounter_gen, true);
|
|
||||||
|
|
||||||
session.setCampaignFleet();
|
session.startNewGame(false);
|
||||||
check.equals(session.player.fleet.ships.length, 2);
|
check.notequals(session.player, null);
|
||||||
check.equals(session.player.fleet.credits, 0);
|
check.equals(session.player.fleet.ships.length, 0);
|
||||||
check.equals(session.player.fleet.location, session.start_location.id);
|
check.equals(session.player.fleet.credits, 0);
|
||||||
});
|
check.equals(session.universe.stars.length, 50);
|
||||||
|
check.equals(session.getBattle(), null);
|
||||||
|
check.equals(session.start_location.shop, null);
|
||||||
|
check.equals(session.start_location.encounter, null);
|
||||||
|
check.equals(session.start_location.encounter_gen, true);
|
||||||
|
|
||||||
test.case("can revert battle", check => {
|
session.setCampaignFleet();
|
||||||
let session = new GameSession();
|
check.equals(session.player.fleet.ships.length, 2);
|
||||||
let star = session.universe.addStar();
|
check.equals(session.player.fleet.credits, 0);
|
||||||
let loc1 = star.addLocation(StarLocationType.PLANET);
|
check.equals(session.player.fleet.location, session.start_location.id);
|
||||||
loc1.clearEncounter();
|
});
|
||||||
let loc2 = star.addLocation(StarLocationType.PLANET);
|
|
||||||
loc2.encounter_random = new SkewedRandomGenerator([0], true);
|
|
||||||
session.universe.updateLocations();
|
|
||||||
|
|
||||||
session.fleet.setLocation(loc1);
|
test.case("can revert battle", check => {
|
||||||
check.in("init in loc1", check => {
|
let session = new GameSession();
|
||||||
check.equals(session.getBattle(), null, "bound battle");
|
let star = session.universe.addStar();
|
||||||
check.equals(session.fleet.location, loc1.id, "fleet location");
|
let loc1 = star.addLocation(StarLocationType.PLANET);
|
||||||
check.equals(session.player.hasVisitedLocation(loc2), false, "visited");
|
loc1.clearEncounter();
|
||||||
});
|
let loc2 = star.addLocation(StarLocationType.PLANET);
|
||||||
|
loc2.encounter_random = new SkewedRandomGenerator([0], true);
|
||||||
|
session.universe.updateLocations();
|
||||||
|
|
||||||
session.fleet.setLocation(loc2);
|
session.fleet.setLocation(loc1);
|
||||||
check.in("move to loc2", check => {
|
check.in("init in loc1", check => {
|
||||||
check.notequals(session.getBattle(), null, "bound battle");
|
check.equals(session.getBattle(), null, "bound battle");
|
||||||
check.equals(session.fleet.location, loc2.id, "fleet location");
|
check.equals(session.fleet.location, loc1.id, "fleet location");
|
||||||
check.equals(session.player.hasVisitedLocation(loc2), true, "visited");
|
check.equals(session.player.hasVisitedLocation(loc2), false, "visited");
|
||||||
});
|
|
||||||
let enemy = loc2.encounter;
|
|
||||||
|
|
||||||
session.revertBattle();
|
|
||||||
check.in("reverted", check => {
|
|
||||||
check.equals(session.getBattle(), null, "bound battle");
|
|
||||||
check.equals(session.fleet.location, loc1.id, "fleet location");
|
|
||||||
check.equals(session.player.hasVisitedLocation(loc2), true, "visited");
|
|
||||||
});
|
|
||||||
|
|
||||||
session.fleet.setLocation(loc2);
|
|
||||||
check.in("move to loc2 again", check => {
|
|
||||||
check.notequals(session.getBattle(), null, "bound battle");
|
|
||||||
check.equals(session.fleet.location, loc2.id, "fleet location");
|
|
||||||
check.equals(session.player.hasVisitedLocation(loc2), true, "visited");
|
|
||||||
check.same(nn(session.getBattle()).fleets[1], nn(enemy), "same enemy");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*test.case("can generate lots of new games", check => {
|
|
||||||
range(20).forEach(() => {
|
|
||||||
let session = new GameSession();
|
|
||||||
session.startNewGame();
|
|
||||||
check.equals(session.universe.stars.length, 50);
|
|
||||||
});
|
|
||||||
});*/
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
session.fleet.setLocation(loc2);
|
||||||
|
check.in("move to loc2", check => {
|
||||||
|
check.notequals(session.getBattle(), null, "bound battle");
|
||||||
|
check.equals(session.fleet.location, loc2.id, "fleet location");
|
||||||
|
check.equals(session.player.hasVisitedLocation(loc2), true, "visited");
|
||||||
|
});
|
||||||
|
let enemy = loc2.encounter;
|
||||||
|
|
||||||
|
session.revertBattle();
|
||||||
|
check.in("reverted", check => {
|
||||||
|
check.equals(session.getBattle(), null, "bound battle");
|
||||||
|
check.equals(session.fleet.location, loc1.id, "fleet location");
|
||||||
|
check.equals(session.player.hasVisitedLocation(loc2), true, "visited");
|
||||||
|
});
|
||||||
|
|
||||||
|
session.fleet.setLocation(loc2);
|
||||||
|
check.in("move to loc2 again", check => {
|
||||||
|
check.notequals(session.getBattle(), null, "bound battle");
|
||||||
|
check.equals(session.fleet.location, loc2.id, "fleet location");
|
||||||
|
check.equals(session.player.hasVisitedLocation(loc2), true, "visited");
|
||||||
|
check.same(nn(session.getBattle()).fleets[1], nn(enemy), "same enemy");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*test.case("can generate lots of new games", check => {
|
||||||
|
range(20).forEach(() => {
|
||||||
|
let session = new GameSession();
|
||||||
|
session.startNewGame();
|
||||||
|
check.equals(session.universe.stars.length, 50);
|
||||||
|
});
|
||||||
|
});*/
|
||||||
|
});
|
||||||
|
|
|
@ -1,205 +1,215 @@
|
||||||
module TK.SpaceTac {
|
import { NAMESPACE } from ".."
|
||||||
/**
|
import { iforeach } from "../common/Iterators"
|
||||||
* A game session, binding a universe and a player
|
import { RandomGenerator } from "../common/RandomGenerator"
|
||||||
*
|
import { Serializer } from "../common/Serializer"
|
||||||
* This represents the current state of game
|
import { Battle } from "./Battle"
|
||||||
*/
|
import { Fleet } from "./Fleet"
|
||||||
export class GameSession {
|
import { FleetGenerator } from "./FleetGenerator"
|
||||||
// "Hopefully" unique session id
|
import { PersonalityReactions } from "./PersonalityReactions"
|
||||||
id: string
|
import { Player } from "./Player"
|
||||||
|
import { StarLocation } from "./StarLocation"
|
||||||
|
import { Universe } from "./Universe"
|
||||||
|
|
||||||
// Game universe
|
/**
|
||||||
universe: Universe
|
* A game session, binding a universe and a player
|
||||||
|
*
|
||||||
|
* This represents the current state of game
|
||||||
|
*/
|
||||||
|
export class GameSession {
|
||||||
|
// "Hopefully" unique session id
|
||||||
|
id: string
|
||||||
|
|
||||||
// Current connected player
|
// Game universe
|
||||||
player: Player
|
universe: Universe
|
||||||
|
|
||||||
// Personality reactions
|
// Current connected player
|
||||||
reactions: PersonalityReactions
|
player: Player
|
||||||
|
|
||||||
// Starting location
|
// Personality reactions
|
||||||
start_location: StarLocation
|
reactions: PersonalityReactions
|
||||||
|
|
||||||
// Indicator that the session is the primary one
|
// Starting location
|
||||||
primary = true
|
start_location: StarLocation
|
||||||
|
|
||||||
// Indicator of spectator mode
|
// Indicator that the session is the primary one
|
||||||
spectator = false
|
primary = true
|
||||||
|
|
||||||
// Indicator that introduction has been watched
|
// Indicator of spectator mode
|
||||||
introduced = false
|
spectator = false
|
||||||
|
|
||||||
constructor() {
|
// Indicator that introduction has been watched
|
||||||
this.id = RandomGenerator.global.id(20);
|
introduced = false
|
||||||
this.universe = new Universe();
|
|
||||||
this.player = new Player();
|
|
||||||
this.reactions = new PersonalityReactions();
|
|
||||||
this.start_location = new StarLocation();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
constructor() {
|
||||||
* Get the currently played fleet
|
this.id = RandomGenerator.global.id(20);
|
||||||
*/
|
this.universe = new Universe();
|
||||||
get fleet(): Fleet {
|
this.player = new Player();
|
||||||
return this.player.fleet;
|
this.reactions = new PersonalityReactions();
|
||||||
}
|
this.start_location = new StarLocation();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an indicative description of the session (to help identify game saves)
|
* Get the currently played fleet
|
||||||
*/
|
*/
|
||||||
getDescription(): string {
|
get fleet(): Fleet {
|
||||||
let level = this.player.fleet.getLevel();
|
return this.player.fleet;
|
||||||
let ships = this.player.fleet.ships.length;
|
}
|
||||||
return `Level ${level} - ${ships} ships`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load a game state from a string
|
/**
|
||||||
static loadFromString(serialized: string): GameSession {
|
* Get an indicative description of the session (to help identify game saves)
|
||||||
var serializer = new Serializer(TK.SpaceTac);
|
*/
|
||||||
return <GameSession>serializer.unserialize(serialized);
|
getDescription(): string {
|
||||||
}
|
let level = this.player.fleet.getLevel();
|
||||||
|
let ships = this.player.fleet.ships.length;
|
||||||
|
return `Level ${level} - ${ships} ships`;
|
||||||
|
}
|
||||||
|
|
||||||
// Serializes the game state to a string
|
// Load a game state from a string
|
||||||
saveToString(): string {
|
static loadFromString(serialized: string): GameSession {
|
||||||
var serializer = new Serializer(TK.SpaceTac);
|
var serializer = new Serializer(NAMESPACE);
|
||||||
return serializer.serialize(this);
|
return <GameSession>serializer.unserialize(serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Serializes the game state to a string
|
||||||
* Generate a real single player game (campaign)
|
saveToString(): string {
|
||||||
*
|
var serializer = new Serializer(NAMESPACE);
|
||||||
* If *fleet* is false, the player fleet will be empty, and needs to be set with *setCampaignFleet*.
|
return serializer.serialize(this);
|
||||||
*/
|
}
|
||||||
startNewGame(fleet = true, story = false): void {
|
|
||||||
this.universe = new Universe();
|
|
||||||
this.universe.generate();
|
|
||||||
|
|
||||||
this.start_location = this.universe.getStartLocation();
|
/**
|
||||||
this.start_location.clearEncounter();
|
* Generate a real single player game (campaign)
|
||||||
this.start_location.removeShop();
|
*
|
||||||
|
* If *fleet* is false, the player fleet will be empty, and needs to be set with *setCampaignFleet*.
|
||||||
|
*/
|
||||||
|
startNewGame(fleet = true, story = false): void {
|
||||||
|
this.universe = new Universe();
|
||||||
|
this.universe.generate();
|
||||||
|
|
||||||
this.player = new Player();
|
this.start_location = this.universe.getStartLocation();
|
||||||
|
this.start_location.clearEncounter();
|
||||||
|
this.start_location.removeShop();
|
||||||
|
|
||||||
this.reactions = new PersonalityReactions();
|
this.player = new Player();
|
||||||
|
|
||||||
if (fleet) {
|
this.reactions = new PersonalityReactions();
|
||||||
this.setCampaignFleet(null, story);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if (fleet) {
|
||||||
* Set the initial campaign fleet, null for a default fleet
|
this.setCampaignFleet(null, story);
|
||||||
*
|
|
||||||
* If *story* is true, the main story arc will be started.
|
|
||||||
*/
|
|
||||||
setCampaignFleet(fleet: Fleet | null = null, story = true) {
|
|
||||||
if (fleet) {
|
|
||||||
this.player.setFleet(fleet);
|
|
||||||
} else {
|
|
||||||
let fleet_generator = new FleetGenerator();
|
|
||||||
this.player.fleet = fleet_generator.generate(1, this.player, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.player.fleet.setLocation(this.start_location);
|
|
||||||
|
|
||||||
if (story) {
|
|
||||||
this.player.missions.startMainStory(this.universe, this.player.fleet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a new "quick battle" game
|
|
||||||
*/
|
|
||||||
startQuickBattle(with_ai: boolean = false, level?: number, shipcount?: number): void {
|
|
||||||
this.player = new Player();
|
|
||||||
this.universe = new Universe();
|
|
||||||
|
|
||||||
let battle = Battle.newQuickRandom(true, level || RandomGenerator.global.randInt(1, 10), shipcount);
|
|
||||||
this.player.setFleet(battle.fleets[0]);
|
|
||||||
this.player.setBattle(battle);
|
|
||||||
|
|
||||||
this.reactions = new PersonalityReactions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get currently played battle, null when none is in progress
|
|
||||||
*/
|
|
||||||
getBattle(): Battle | null {
|
|
||||||
return this.player.getBattle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the main fleet's location
|
|
||||||
*/
|
|
||||||
getLocation(): StarLocation {
|
|
||||||
return this.universe.getLocation(this.player.fleet.location) || new StarLocation();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the end of current battle.
|
|
||||||
*
|
|
||||||
* This will reset the fleet, grant experience, and create loot.
|
|
||||||
*
|
|
||||||
* The battle will still be bound to the session (exitBattle or revertBattle should be called after).
|
|
||||||
*/
|
|
||||||
setBattleEnded() {
|
|
||||||
let battle = this.getBattle();
|
|
||||||
|
|
||||||
if (battle && battle.ended && battle.outcome) {
|
|
||||||
// Generate experience
|
|
||||||
battle.outcome.grantExperience(battle.fleets);
|
|
||||||
|
|
||||||
// Reset ships status
|
|
||||||
iforeach(battle.iships(), ship => ship.restoreInitialState());
|
|
||||||
|
|
||||||
// If the battle happened in a star location, keep it informed
|
|
||||||
let location = this.universe.getLocation(this.player.fleet.location);
|
|
||||||
if (location) {
|
|
||||||
location.resolveEncounter(battle.outcome);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exit the current battle unconditionally, if any
|
|
||||||
*
|
|
||||||
* This does not apply retreat penalties, or battle outcome, only unbind the battle from current session
|
|
||||||
*/
|
|
||||||
exitBattle(): void {
|
|
||||||
this.player.setBattle(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Revert current battle, and put the player's fleet to its previous location, as if the battle never happened
|
|
||||||
*/
|
|
||||||
revertBattle(): void {
|
|
||||||
this.exitBattle();
|
|
||||||
|
|
||||||
let previous_location = this.universe.getLocation(this.fleet.visited[1]);
|
|
||||||
if (previous_location) {
|
|
||||||
this.fleet.setLocation(previous_location);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the session has an universe to explore (campaign mode)
|
|
||||||
*/
|
|
||||||
hasUniverse(): boolean {
|
|
||||||
return this.universe.stars.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if initial fleet creation has been done.
|
|
||||||
*/
|
|
||||||
isFleetCreated(): boolean {
|
|
||||||
return this.player.fleet.ships.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if campaign introduction has been watched
|
|
||||||
*/
|
|
||||||
isIntroViewed(): boolean {
|
|
||||||
return this.introduced;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the initial campaign fleet, null for a default fleet
|
||||||
|
*
|
||||||
|
* If *story* is true, the main story arc will be started.
|
||||||
|
*/
|
||||||
|
setCampaignFleet(fleet: Fleet | null = null, story = true) {
|
||||||
|
if (fleet) {
|
||||||
|
this.player.setFleet(fleet);
|
||||||
|
} else {
|
||||||
|
let fleet_generator = new FleetGenerator();
|
||||||
|
this.player.fleet = fleet_generator.generate(1, this.player, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player.fleet.setLocation(this.start_location);
|
||||||
|
|
||||||
|
if (story) {
|
||||||
|
this.player.missions.startMainStory(this.universe, this.player.fleet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new "quick battle" game
|
||||||
|
*/
|
||||||
|
startQuickBattle(with_ai: boolean = false, level?: number, shipcount?: number): void {
|
||||||
|
this.player = new Player();
|
||||||
|
this.universe = new Universe();
|
||||||
|
|
||||||
|
let battle = Battle.newQuickRandom(true, level || RandomGenerator.global.randInt(1, 10), shipcount);
|
||||||
|
this.player.setFleet(battle.fleets[0]);
|
||||||
|
this.player.setBattle(battle);
|
||||||
|
|
||||||
|
this.reactions = new PersonalityReactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get currently played battle, null when none is in progress
|
||||||
|
*/
|
||||||
|
getBattle(): Battle | null {
|
||||||
|
return this.player.getBattle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the main fleet's location
|
||||||
|
*/
|
||||||
|
getLocation(): StarLocation {
|
||||||
|
return this.universe.getLocation(this.player.fleet.location) || new StarLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the end of current battle.
|
||||||
|
*
|
||||||
|
* This will reset the fleet, grant experience, and create loot.
|
||||||
|
*
|
||||||
|
* The battle will still be bound to the session (exitBattle or revertBattle should be called after).
|
||||||
|
*/
|
||||||
|
setBattleEnded() {
|
||||||
|
let battle = this.getBattle();
|
||||||
|
|
||||||
|
if (battle && battle.ended && battle.outcome) {
|
||||||
|
// Generate experience
|
||||||
|
battle.outcome.grantExperience(battle.fleets);
|
||||||
|
|
||||||
|
// Reset ships status
|
||||||
|
iforeach(battle.iships(), ship => ship.restoreInitialState());
|
||||||
|
|
||||||
|
// If the battle happened in a star location, keep it informed
|
||||||
|
let location = this.universe.getLocation(this.player.fleet.location);
|
||||||
|
if (location) {
|
||||||
|
location.resolveEncounter(battle.outcome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exit the current battle unconditionally, if any
|
||||||
|
*
|
||||||
|
* This does not apply retreat penalties, or battle outcome, only unbind the battle from current session
|
||||||
|
*/
|
||||||
|
exitBattle(): void {
|
||||||
|
this.player.setBattle(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revert current battle, and put the player's fleet to its previous location, as if the battle never happened
|
||||||
|
*/
|
||||||
|
revertBattle(): void {
|
||||||
|
this.exitBattle();
|
||||||
|
|
||||||
|
let previous_location = this.universe.getLocation(this.fleet.visited[1]);
|
||||||
|
if (previous_location) {
|
||||||
|
this.fleet.setLocation(previous_location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the session has an universe to explore (campaign mode)
|
||||||
|
*/
|
||||||
|
hasUniverse(): boolean {
|
||||||
|
return this.universe.stars.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if initial fleet creation has been done.
|
||||||
|
*/
|
||||||
|
isFleetCreated(): boolean {
|
||||||
|
return this.player.fleet.ships.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if campaign introduction has been watched
|
||||||
|
*/
|
||||||
|
isIntroViewed(): boolean {
|
||||||
|
return this.introduced;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,207 +1,225 @@
|
||||||
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";
|
||||||
|
|
||||||
function simpleWeaponCase(distance = 10, ship_ap = 5, weapon_ap = 3, engine_distance = 5): [Ship, MoveFireSimulator, BaseAction] {
|
testing("MoveFireSimulator", test => {
|
||||||
let ship = new Ship();
|
|
||||||
TestTools.setShipModel(ship, 100, 0, ship_ap);
|
|
||||||
TestTools.addEngine(ship, engine_distance);
|
|
||||||
let action = new TriggerAction("weapon", { power: weapon_ap, range: distance });
|
|
||||||
let simulator = new MoveFireSimulator(ship);
|
|
||||||
return [ship, simulator, action];
|
|
||||||
}
|
|
||||||
|
|
||||||
test.case("finds a suitable engine to make an approach", check => {
|
function simpleWeaponCase(distance = 10, ship_ap = 5, weapon_ap = 3, engine_distance = 5): [Ship, MoveFireSimulator, BaseAction] {
|
||||||
let ship = new Ship();
|
let ship = new Ship();
|
||||||
let simulator = new MoveFireSimulator(ship);
|
TestTools.setShipModel(ship, 100, 0, ship_ap);
|
||||||
check.equals(simulator.findEngine(), null, "no engine");
|
TestTools.addEngine(ship, engine_distance);
|
||||||
let engine1 = TestTools.addEngine(ship, 100);
|
let action = new TriggerAction("weapon", { power: weapon_ap, range: distance });
|
||||||
engine1.configureCooldown(1, 1);
|
let simulator = new MoveFireSimulator(ship);
|
||||||
check.same(simulator.findEngine(), engine1, "one engine");
|
return [ship, simulator, action];
|
||||||
let engine2 = TestTools.addEngine(ship, 120);
|
}
|
||||||
engine2.configureCooldown(1, 1);
|
|
||||||
check.same(simulator.findEngine(), engine1, "two engines, choose first one");
|
|
||||||
ship.actions.storeUsage(engine1);
|
|
||||||
check.same(simulator.findEngine(), engine2, "first engine overheated, choose second one");
|
|
||||||
ship.actions.storeUsage(engine2);
|
|
||||||
check.equals(simulator.findEngine(), null, "both engines overheated");
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("fires directly when in range", check => {
|
test.case("finds a suitable engine to make an approach", check => {
|
||||||
let [ship, simulator, action] = simpleWeaponCase();
|
let ship = new Ship();
|
||||||
let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null));
|
let simulator = new MoveFireSimulator(ship);
|
||||||
|
check.equals(simulator.findEngine(), null, "no engine");
|
||||||
|
let engine1 = TestTools.addEngine(ship, 100);
|
||||||
|
engine1.configureCooldown(1, 1);
|
||||||
|
check.same(simulator.findEngine(), engine1, "one engine");
|
||||||
|
let engine2 = TestTools.addEngine(ship, 120);
|
||||||
|
engine2.configureCooldown(1, 1);
|
||||||
|
check.same(simulator.findEngine(), engine1, "two engines, choose first one");
|
||||||
|
ship.actions.storeUsage(engine1);
|
||||||
|
check.same(simulator.findEngine(), engine2, "first engine overheated, choose second one");
|
||||||
|
ship.actions.storeUsage(engine2);
|
||||||
|
check.equals(simulator.findEngine(), null, "both engines overheated");
|
||||||
|
});
|
||||||
|
|
||||||
check.same(result.success, true, 'success');
|
test.case("fires directly when in range", check => {
|
||||||
check.same(result.need_move, false, 'need_move');
|
let [ship, simulator, action] = simpleWeaponCase();
|
||||||
check.same(result.need_fire, true, 'need_fire');
|
let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null));
|
||||||
check.same(result.can_fire, true, 'can_fire');
|
|
||||||
check.same(result.total_fire_ap, 3, 'total_fire_ap');
|
|
||||||
|
|
||||||
check.equals(result.parts, [
|
check.same(result.success, true, 'success');
|
||||||
{ action: action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3, possible: true }
|
check.same(result.need_move, false, 'need_move');
|
||||||
]);
|
check.same(result.need_fire, true, 'need_fire');
|
||||||
});
|
check.same(result.can_fire, true, 'can_fire');
|
||||||
|
check.same(result.total_fire_ap, 3, 'total_fire_ap');
|
||||||
|
|
||||||
test.case("can't fire when in range, but not enough AP", check => {
|
check.equals(result.parts, [
|
||||||
let [ship, simulator, action] = simpleWeaponCase(10, 2, 3);
|
{ action: action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3, possible: true }
|
||||||
let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null));
|
]);
|
||||||
check.same(result.success, true, 'success');
|
});
|
||||||
check.same(result.need_move, false, 'need_move');
|
|
||||||
check.same(result.need_fire, true, 'need_fire');
|
|
||||||
check.same(result.can_fire, false, 'can_fire');
|
|
||||||
check.same(result.total_fire_ap, 3, 'total_fire_ap');
|
|
||||||
|
|
||||||
check.equals(result.parts, [
|
test.case("can't fire when in range, but not enough AP", check => {
|
||||||
{ action: action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3, possible: false }
|
let [ship, simulator, action] = simpleWeaponCase(10, 2, 3);
|
||||||
]);
|
let result = simulator.simulateAction(action, new Target(ship.arena_x + 5, ship.arena_y, null));
|
||||||
});
|
check.same(result.success, true, 'success');
|
||||||
|
check.same(result.need_move, false, 'need_move');
|
||||||
|
check.same(result.need_fire, true, 'need_fire');
|
||||||
|
check.same(result.can_fire, false, 'can_fire');
|
||||||
|
check.same(result.total_fire_ap, 3, 'total_fire_ap');
|
||||||
|
|
||||||
test.case("moves straight to get within range", check => {
|
check.equals(result.parts, [
|
||||||
let [ship, simulator, action] = simpleWeaponCase();
|
{ action: action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 3, possible: false }
|
||||||
let result = simulator.simulateAction(action, new Target(ship.arena_x + 15, ship.arena_y, null));
|
]);
|
||||||
check.same(result.success, true, 'success');
|
});
|
||||||
check.same(result.need_move, true, 'need_move');
|
|
||||||
check.same(result.can_end_move, true, 'can_end_move');
|
|
||||||
check.equals(result.move_location, new Target(ship.arena_x + 5, ship.arena_y, null));
|
|
||||||
check.equals(result.total_move_ap, 1);
|
|
||||||
check.same(result.need_fire, true, 'need_fire');
|
|
||||||
check.same(result.can_fire, true, 'can_fire');
|
|
||||||
check.same(result.total_fire_ap, 3, 'total_fire_ap');
|
|
||||||
|
|
||||||
let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0];
|
test.case("moves straight to get within range", check => {
|
||||||
check.equals(result.parts, [
|
let [ship, simulator, action] = simpleWeaponCase();
|
||||||
{ action: move_action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 1, possible: true },
|
let result = simulator.simulateAction(action, new Target(ship.arena_x + 15, ship.arena_y, null));
|
||||||
{ action: action, target: new Target(ship.arena_x + 15, ship.arena_y, null), ap: 3, possible: true }
|
check.same(result.success, true, 'success');
|
||||||
]);
|
check.same(result.need_move, true, 'need_move');
|
||||||
});
|
check.same(result.can_end_move, true, 'can_end_move');
|
||||||
|
check.equals(result.move_location, new Target(ship.arena_x + 5, ship.arena_y, null));
|
||||||
|
check.equals(result.total_move_ap, 1);
|
||||||
|
check.same(result.need_fire, true, 'need_fire');
|
||||||
|
check.same(result.can_fire, true, 'can_fire');
|
||||||
|
check.same(result.total_fire_ap, 3, 'total_fire_ap');
|
||||||
|
|
||||||
test.case("scans a circle for move targets", check => {
|
let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0];
|
||||||
let simulator = new MoveFireSimulator(new Ship());
|
check.equals(result.parts, [
|
||||||
|
{ action: move_action, target: new Target(ship.arena_x + 5, ship.arena_y, null), ap: 1, possible: true },
|
||||||
|
{ action: action, target: new Target(ship.arena_x + 15, ship.arena_y, null), ap: 3, possible: true }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
let result = simulator.scanCircle(50, 30, 10, 1, 1);
|
test.case("scans a circle for move targets", check => {
|
||||||
check.equals(imaterialize(result), [
|
let simulator = new MoveFireSimulator(new Ship());
|
||||||
new Target(50, 30)
|
|
||||||
]);
|
|
||||||
|
|
||||||
result = simulator.scanCircle(50, 30, 10, 2, 1);
|
let result = simulator.scanCircle(50, 30, 10, 1, 1);
|
||||||
check.equals(imaterialize(result), [
|
check.equals(imaterialize(result), [
|
||||||
new Target(50, 30),
|
new Target(50, 30)
|
||||||
new Target(60, 30)
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
result = simulator.scanCircle(50, 30, 10, 2, 2);
|
result = simulator.scanCircle(50, 30, 10, 2, 1);
|
||||||
check.equals(imaterialize(result), [
|
check.equals(imaterialize(result), [
|
||||||
new Target(50, 30),
|
new Target(50, 30),
|
||||||
new Target(60, 30),
|
new Target(60, 30)
|
||||||
new Target(40, 30)
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
result = simulator.scanCircle(50, 30, 10, 3, 4);
|
result = simulator.scanCircle(50, 30, 10, 2, 2);
|
||||||
check.equals(imaterialize(result), [
|
check.equals(imaterialize(result), [
|
||||||
new Target(50, 30),
|
new Target(50, 30),
|
||||||
new Target(55, 30),
|
new Target(60, 30),
|
||||||
new Target(45, 30),
|
new Target(40, 30)
|
||||||
new Target(60, 30),
|
]);
|
||||||
new Target(50, 40),
|
|
||||||
new Target(40, 30),
|
|
||||||
new Target(50, 20)
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("accounts for exclusion areas for the approach", check => {
|
result = simulator.scanCircle(50, 30, 10, 3, 4);
|
||||||
let [ship, simulator, action] = simpleWeaponCase(100, 5, 1, 50);
|
check.equals(imaterialize(result), [
|
||||||
ship.setArenaPosition(300, 200);
|
new Target(50, 30),
|
||||||
let battle = new Battle();
|
new Target(55, 30),
|
||||||
battle.fleets[0].addShip(ship);
|
new Target(45, 30),
|
||||||
let ship1 = battle.fleets[0].addShip();
|
new Target(60, 30),
|
||||||
let moveaction = nn(simulator.findEngine());
|
new Target(50, 40),
|
||||||
(<any>moveaction).safety_distance = 30;
|
new Target(40, 30),
|
||||||
battle.ship_separation = 30;
|
new Target(50, 20)
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
check.same(simulator.getApproach(moveaction, Target.newFromLocation(350, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED);
|
test.case("accounts for exclusion areas for the approach", check => {
|
||||||
check.same(simulator.getApproach(moveaction, Target.newFromLocation(400, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED);
|
let [ship, simulator, action] = simpleWeaponCase(100, 5, 1, 50);
|
||||||
check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(400, 200));
|
ship.setArenaPosition(300, 200);
|
||||||
|
let battle = new Battle();
|
||||||
|
battle.fleets[0].addShip(ship);
|
||||||
|
let ship1 = battle.fleets[0].addShip();
|
||||||
|
let moveaction = nn(simulator.findEngine());
|
||||||
|
(<any>moveaction).safety_distance = 30;
|
||||||
|
battle.ship_separation = 30;
|
||||||
|
|
||||||
ship1.setArenaPosition(420, 200);
|
check.same(simulator.getApproach(moveaction, Target.newFromLocation(350, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED);
|
||||||
|
check.same(simulator.getApproach(moveaction, Target.newFromLocation(400, 200), 100), ApproachSimulationError.NO_MOVE_NEEDED);
|
||||||
|
check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(400, 200));
|
||||||
|
|
||||||
check.patch(simulator, "scanCircle", () => iarray([
|
ship1.setArenaPosition(420, 200);
|
||||||
new Target(400, 200),
|
|
||||||
new Target(410, 200),
|
|
||||||
new Target(410, 230),
|
|
||||||
new Target(420, 210),
|
|
||||||
new Target(480, 260),
|
|
||||||
]));
|
|
||||||
check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(410, 230));
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("moves to get in range, even if not enough AP to fire", check => {
|
check.patch(simulator, "scanCircle", () => iarray([
|
||||||
let [ship, simulator, action] = simpleWeaponCase(8, 3, 2, 5);
|
new Target(400, 200),
|
||||||
let result = simulator.simulateAction(action, new Target(ship.arena_x + 18, ship.arena_y, null));
|
new Target(410, 200),
|
||||||
check.same(result.success, true, 'success');
|
new Target(410, 230),
|
||||||
check.same(result.need_move, true, 'need_move');
|
new Target(420, 210),
|
||||||
check.same(result.can_end_move, true, 'can_end_move');
|
new Target(480, 260),
|
||||||
check.equals(result.move_location, new Target(ship.arena_x + 10, ship.arena_y, null));
|
]));
|
||||||
check.equals(result.total_move_ap, 2);
|
check.equals(simulator.getApproach(moveaction, Target.newFromLocation(500, 200), 100), new Target(410, 230));
|
||||||
check.same(result.need_fire, true, 'need_fire');
|
});
|
||||||
check.same(result.can_fire, false, 'can_fire');
|
|
||||||
check.same(result.total_fire_ap, 2, 'total_fire_ap');
|
|
||||||
|
|
||||||
let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0];
|
test.case("moves to get in range, even if not enough AP to fire", check => {
|
||||||
check.equals(result.parts, [
|
let [ship, simulator, action] = simpleWeaponCase(8, 3, 2, 5);
|
||||||
{ action: move_action, target: new Target(ship.arena_x + 10, ship.arena_y, null), ap: 2, possible: true },
|
let result = simulator.simulateAction(action, new Target(ship.arena_x + 18, ship.arena_y, null));
|
||||||
{ action: action, target: new Target(ship.arena_x + 18, ship.arena_y, null), ap: 2, possible: false }
|
check.same(result.success, true, 'success');
|
||||||
]);
|
check.same(result.need_move, true, 'need_move');
|
||||||
});
|
check.same(result.can_end_move, true, 'can_end_move');
|
||||||
|
check.equals(result.move_location, new Target(ship.arena_x + 10, ship.arena_y, null));
|
||||||
|
check.equals(result.total_move_ap, 2);
|
||||||
|
check.same(result.need_fire, true, 'need_fire');
|
||||||
|
check.same(result.can_fire, false, 'can_fire');
|
||||||
|
check.same(result.total_fire_ap, 2, 'total_fire_ap');
|
||||||
|
|
||||||
test.case("does nothing if trying to move in the same spot", check => {
|
let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0];
|
||||||
let [ship, simulator, action] = simpleWeaponCase();
|
check.equals(result.parts, [
|
||||||
let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0];
|
{ action: move_action, target: new Target(ship.arena_x + 10, ship.arena_y, null), ap: 2, possible: true },
|
||||||
let result = simulator.simulateAction(move_action, new Target(ship.arena_x, ship.arena_y, null));
|
{ action: action, target: new Target(ship.arena_x + 18, ship.arena_y, null), ap: 2, possible: false }
|
||||||
check.equals(result.success, false);
|
]);
|
||||||
check.equals(result.need_move, false);
|
});
|
||||||
check.equals(result.need_fire, false);
|
|
||||||
check.equals(result.parts, []);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("does not move if already in range, even if in the safety margin", check => {
|
test.case("does nothing if trying to move in the same spot", check => {
|
||||||
let [ship, simulator, action] = simpleWeaponCase(100);
|
let [ship, simulator, action] = simpleWeaponCase();
|
||||||
let result = simulator.simulateAction(action, new Target(ship.arena_x + 97, ship.arena_y, null), 5);
|
let move_action = ship.actions.listAll().filter(action => action instanceof MoveAction)[0];
|
||||||
check.equals(result.success, true);
|
let result = simulator.simulateAction(move_action, new Target(ship.arena_x, ship.arena_y, null));
|
||||||
check.equals(result.need_move, false);
|
check.equals(result.success, false);
|
||||||
result = simulator.simulateAction(action, new Target(ship.arena_x + 101, ship.arena_y, null), 5);
|
check.equals(result.need_move, false);
|
||||||
check.equals(result.success, true);
|
check.equals(result.need_fire, false);
|
||||||
check.equals(result.need_move, true);
|
check.equals(result.parts, []);
|
||||||
check.equals(result.move_location, new Target(ship.arena_x + 6, ship.arena_y));
|
});
|
||||||
});
|
|
||||||
|
|
||||||
test.case("simulates the results on a fake battle, to provide a list of expected diffs", check => {
|
test.case("does not move if already in range, even if in the safety margin", check => {
|
||||||
let battle = TestTools.createBattle();
|
let [ship, simulator, action] = simpleWeaponCase(100);
|
||||||
let ship = battle.fleets[0].ships[0];
|
let result = simulator.simulateAction(action, new Target(ship.arena_x + 97, ship.arena_y, null), 5);
|
||||||
let enemy = battle.fleets[1].ships[0];
|
check.equals(result.success, true);
|
||||||
ship.setArenaPosition(100, 100);
|
check.equals(result.need_move, false);
|
||||||
enemy.setArenaPosition(300, 100);
|
result = simulator.simulateAction(action, new Target(ship.arena_x + 101, ship.arena_y, null), 5);
|
||||||
TestTools.setShipModel(ship, 1, 1, 3);
|
check.equals(result.success, true);
|
||||||
TestTools.setShipModel(enemy, 2, 1);
|
check.equals(result.need_move, true);
|
||||||
let engine = TestTools.addEngine(ship, 80);
|
check.equals(result.move_location, new Target(ship.arena_x + 6, ship.arena_y));
|
||||||
let weapon = TestTools.addWeapon(ship, 5, 1, 150);
|
});
|
||||||
let simulator = new MoveFireSimulator(ship);
|
|
||||||
let result = simulator.simulateAction(weapon, Target.newFromShip(enemy), 5);
|
|
||||||
let diffs = simulator.getExpectedDiffs(nn(ship.getBattle()), result);
|
|
||||||
check.equals(diffs, [
|
|
||||||
new ShipActionUsedDiff(ship, engine, Target.newFromLocation(155, 100)),
|
|
||||||
new ShipValueDiff(ship, "power", -1),
|
|
||||||
new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(155, 100), engine),
|
|
||||||
new ShipActionUsedDiff(ship, weapon, Target.newFromShip(enemy)),
|
|
||||||
new ShipValueDiff(ship, "power", -1),
|
|
||||||
new ProjectileFiredDiff(ship, weapon, Target.newFromShip(enemy)),
|
|
||||||
new ShipDamageDiff(enemy, 2, 1, 0, 5),
|
|
||||||
new ShipValueDiff(enemy, "shield", -1),
|
|
||||||
new ShipValueDiff(enemy, "hull", -2),
|
|
||||||
new ShipDeathDiff(battle, enemy),
|
|
||||||
new EndBattleDiff(battle.fleets[0], 0)
|
|
||||||
]);
|
|
||||||
|
|
||||||
check.equals(enemy.getValue("hull"), 2);
|
test.case("simulates the results on a fake battle, to provide a list of expected diffs", check => {
|
||||||
check.equals(enemy.getValue("hull"), 2);
|
let battle = TestTools.createBattle();
|
||||||
});
|
let ship = battle.fleets[0].ships[0];
|
||||||
});
|
let enemy = battle.fleets[1].ships[0];
|
||||||
}
|
ship.setArenaPosition(100, 100);
|
||||||
|
enemy.setArenaPosition(300, 100);
|
||||||
|
TestTools.setShipModel(ship, 1, 1, 3);
|
||||||
|
TestTools.setShipModel(enemy, 2, 1);
|
||||||
|
let engine = TestTools.addEngine(ship, 80);
|
||||||
|
let weapon = TestTools.addWeapon(ship, 5, 1, 150);
|
||||||
|
let simulator = new MoveFireSimulator(ship);
|
||||||
|
let result = simulator.simulateAction(weapon, Target.newFromShip(enemy), 5);
|
||||||
|
let diffs = simulator.getExpectedDiffs(nn(ship.getBattle()), result);
|
||||||
|
check.equals(diffs, [
|
||||||
|
new ShipActionUsedDiff(ship, engine, Target.newFromLocation(155, 100)),
|
||||||
|
new ShipValueDiff(ship, "power", -1),
|
||||||
|
new ShipMoveDiff(ship, ship.location, new ArenaLocationAngle(155, 100), engine),
|
||||||
|
new ShipActionUsedDiff(ship, weapon, Target.newFromShip(enemy)),
|
||||||
|
new ShipValueDiff(ship, "power", -1),
|
||||||
|
new ProjectileFiredDiff(ship, weapon, Target.newFromShip(enemy)),
|
||||||
|
new ShipDamageDiff(enemy, 2, 1, 0, 5),
|
||||||
|
new ShipValueDiff(enemy, "shield", -1),
|
||||||
|
new ShipValueDiff(enemy, "hull", -2),
|
||||||
|
new ShipDeathDiff(battle, enemy),
|
||||||
|
new EndBattleDiff(battle.fleets[0], 0)
|
||||||
|
]);
|
||||||
|
|
||||||
|
check.equals(enemy.getValue("hull"), 2);
|
||||||
|
check.equals(enemy.getValue("hull"), 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,211 +1,220 @@
|
||||||
module TK.SpaceTac {
|
import { NAMESPACE } from ".."
|
||||||
/**
|
import { ichainit, iforeach, imap, irepeat, istep } from "../common/Iterators"
|
||||||
* Error codes for approach simulation
|
import { cfilter, duplicate, first, minBy, nn } from "../common/Tools"
|
||||||
*/
|
import { BaseAction } from "./actions/BaseAction"
|
||||||
export enum ApproachSimulationError {
|
import { MoveAction } from "./actions/MoveAction"
|
||||||
NO_MOVE_NEEDED,
|
import { arenaDistance } from "./ArenaLocation"
|
||||||
NO_VECTOR_FOUND,
|
import { Battle } from "./Battle"
|
||||||
}
|
import { BaseBattleDiff } from "./diffs/BaseBattleDiff"
|
||||||
|
import { Ship } from "./Ship"
|
||||||
|
import { Target } from "./Target"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single action in the sequence result from the simulator
|
* Error codes for approach simulation
|
||||||
*/
|
*/
|
||||||
export type MoveFirePart = {
|
export enum ApproachSimulationError {
|
||||||
action: BaseAction
|
NO_MOVE_NEEDED,
|
||||||
target: Target
|
NO_VECTOR_FOUND,
|
||||||
ap: number
|
}
|
||||||
possible: boolean
|
|
||||||
}
|
/**
|
||||||
|
* A single action in the sequence result from the simulator
|
||||||
/**
|
*/
|
||||||
* A simulation result
|
export type MoveFirePart = {
|
||||||
*/
|
action: BaseAction
|
||||||
export class MoveFireResult {
|
target: Target
|
||||||
// Simulation success, false only if no route can be found
|
ap: number
|
||||||
success = false
|
possible: boolean
|
||||||
// Ideal successive parts to make the full move+fire
|
}
|
||||||
parts: MoveFirePart[] = []
|
|
||||||
// Simulation complete (both move and fire are possible)
|
/**
|
||||||
complete = false
|
* A simulation result
|
||||||
|
*/
|
||||||
need_move = false
|
export class MoveFireResult {
|
||||||
can_move = false
|
// Simulation success, false only if no route can be found
|
||||||
can_end_move = false
|
success = false
|
||||||
total_move_ap = 0
|
// Ideal successive parts to make the full move+fire
|
||||||
move_location = new Target(0, 0, null)
|
parts: MoveFirePart[] = []
|
||||||
|
// Simulation complete (both move and fire are possible)
|
||||||
need_fire = false
|
complete = false
|
||||||
can_fire = false
|
|
||||||
total_fire_ap = 0
|
need_move = false
|
||||||
fire_location = new Target(0, 0, null)
|
can_move = false
|
||||||
};
|
can_end_move = false
|
||||||
|
total_move_ap = 0
|
||||||
/**
|
move_location = new Target(0, 0, null)
|
||||||
* Utility to simulate a move+fire action.
|
|
||||||
*
|
need_fire = false
|
||||||
* This is also a helper to bring a ship in range to fire a weapon.
|
can_fire = false
|
||||||
*/
|
total_fire_ap = 0
|
||||||
export class MoveFireSimulator {
|
fire_location = new Target(0, 0, null)
|
||||||
ship: Ship;
|
};
|
||||||
|
|
||||||
constructor(ship: Ship) {
|
/**
|
||||||
this.ship = ship;
|
* Utility to simulate a move+fire action.
|
||||||
}
|
*
|
||||||
|
* This is also a helper to bring a ship in range to fire a weapon.
|
||||||
/**
|
*/
|
||||||
* Find an engine action, to make an approach
|
export class MoveFireSimulator {
|
||||||
*
|
ship: Ship;
|
||||||
* This will return the first available engine, in the definition order
|
|
||||||
*/
|
constructor(ship: Ship) {
|
||||||
findEngine(): MoveAction | null {
|
this.ship = ship;
|
||||||
let actions = cfilter(this.ship.actions.listAll(), MoveAction);
|
}
|
||||||
return first(actions, action => this.ship.actions.getCooldown(action).canUse());
|
|
||||||
}
|
/**
|
||||||
|
* Find an engine action, to make an approach
|
||||||
/**
|
*
|
||||||
* Check that a move action can reach a given destination
|
* This will return the first available engine, in the definition order
|
||||||
*/
|
*/
|
||||||
canMoveTo(action: MoveAction, target: Target): boolean {
|
findEngine(): MoveAction | null {
|
||||||
let checked = action.checkLocationTarget(this.ship, target);
|
let actions = cfilter(this.ship.actions.listAll(), MoveAction);
|
||||||
return checked != null && checked.x == target.x && checked.y == target.y;
|
return first(actions, action => this.ship.actions.getCooldown(action).canUse());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an iterator for scanning a circle
|
* Check that a move action can reach a given destination
|
||||||
*/
|
*/
|
||||||
scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterable<Target> {
|
canMoveTo(action: MoveAction, target: Target): boolean {
|
||||||
let rcount = nr ? 1 / (nr - 1) : 0;
|
let checked = action.checkLocationTarget(this.ship, target);
|
||||||
return ichainit(imap(istep(0, irepeat(rcount, nr - 1)), r => {
|
return checked != null && checked.x == target.x && checked.y == target.y;
|
||||||
let angles = Math.max(1, Math.ceil(na * r));
|
}
|
||||||
return imap(istep(0, irepeat(2 * Math.PI / angles, angles - 1)), a => {
|
|
||||||
return new Target(x + r * radius * Math.cos(a), y + r * radius * Math.sin(a))
|
/**
|
||||||
});
|
* Get an iterator for scanning a circle
|
||||||
}));
|
*/
|
||||||
}
|
scanCircle(x: number, y: number, radius: number, nr = 6, na = 30): Iterable<Target> {
|
||||||
|
let rcount = nr ? 1 / (nr - 1) : 0;
|
||||||
/**
|
return ichainit(imap(istep(0, irepeat(rcount, nr - 1)), r => {
|
||||||
* Find the best approach location, to put a target in a given range.
|
let angles = Math.max(1, Math.ceil(na * r));
|
||||||
*
|
return imap(istep(0, irepeat(2 * Math.PI / angles, angles - 1)), a => {
|
||||||
* Return null if no approach vector was found.
|
return new Target(x + r * radius * Math.cos(a), y + r * radius * Math.sin(a))
|
||||||
*/
|
});
|
||||||
getApproach(action: MoveAction, target: Target, radius: number, margin = 0): Target | ApproachSimulationError {
|
}));
|
||||||
let dx = target.x - this.ship.arena_x;
|
}
|
||||||
let dy = target.y - this.ship.arena_y;
|
|
||||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
/**
|
||||||
|
* Find the best approach location, to put a target in a given range.
|
||||||
if (distance <= radius) {
|
*
|
||||||
return ApproachSimulationError.NO_MOVE_NEEDED;
|
* Return null if no approach vector was found.
|
||||||
} else {
|
*/
|
||||||
if (margin && radius > margin) {
|
getApproach(action: MoveAction, target: Target, radius: number, margin = 0): Target | ApproachSimulationError {
|
||||||
radius -= margin;
|
let dx = target.x - this.ship.arena_x;
|
||||||
}
|
let dy = target.y - this.ship.arena_y;
|
||||||
let factor = (distance - radius) / distance;
|
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
let candidate = new Target(this.ship.arena_x + dx * factor, this.ship.arena_y + dy * factor);
|
|
||||||
if (this.canMoveTo(action, candidate)) {
|
if (distance <= radius) {
|
||||||
return candidate;
|
return ApproachSimulationError.NO_MOVE_NEEDED;
|
||||||
} else {
|
} else {
|
||||||
let candidates: [number, Target][] = [];
|
if (margin && radius > margin) {
|
||||||
iforeach(this.scanCircle(target.x, target.y, radius), candidate => {
|
radius -= margin;
|
||||||
if (this.canMoveTo(action, candidate)) {
|
}
|
||||||
candidates.push([candidate.getDistanceTo(this.ship.location), candidate]);
|
let factor = (distance - radius) / distance;
|
||||||
}
|
let candidate = new Target(this.ship.arena_x + dx * factor, this.ship.arena_y + dy * factor);
|
||||||
});
|
if (this.canMoveTo(action, candidate)) {
|
||||||
|
return candidate;
|
||||||
if (candidates.length) {
|
} else {
|
||||||
return minBy(candidates, ([distance, candidate]) => distance)[1];
|
let candidates: [number, Target][] = [];
|
||||||
} else {
|
iforeach(this.scanCircle(target.x, target.y, radius), candidate => {
|
||||||
return ApproachSimulationError.NO_VECTOR_FOUND;
|
if (this.canMoveTo(action, candidate)) {
|
||||||
}
|
candidates.push([candidate.getDistanceTo(this.ship.location), candidate]);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
if (candidates.length) {
|
||||||
/**
|
return minBy(candidates, ([distance, candidate]) => distance)[1];
|
||||||
* Simulate a given action on a given valid target.
|
} else {
|
||||||
*/
|
return ApproachSimulationError.NO_VECTOR_FOUND;
|
||||||
simulateAction(action: BaseAction, target: Target, move_margin = 0): MoveFireResult {
|
}
|
||||||
let result = new MoveFireResult();
|
}
|
||||||
let ap = this.ship.getValue("power");
|
}
|
||||||
|
}
|
||||||
// Move or approach needed ?
|
|
||||||
let move_target: Target | null = null;
|
/**
|
||||||
let move_action: MoveAction | null = null;
|
* Simulate a given action on a given valid target.
|
||||||
result.move_location = Target.newFromShip(this.ship);
|
*/
|
||||||
if (action instanceof MoveAction) {
|
simulateAction(action: BaseAction, target: Target, move_margin = 0): MoveFireResult {
|
||||||
let corrected_target = action.applyReachableRange(this.ship, target, move_margin);
|
let result = new MoveFireResult();
|
||||||
corrected_target = action.applyExclusion(this.ship, corrected_target);
|
let ap = this.ship.getValue("power");
|
||||||
if (corrected_target) {
|
|
||||||
result.need_move = target.getDistanceTo(this.ship.location) > 0;
|
// Move or approach needed ?
|
||||||
move_target = corrected_target;
|
let move_target: Target | null = null;
|
||||||
}
|
let move_action: MoveAction | null = null;
|
||||||
move_action = action;
|
result.move_location = Target.newFromShip(this.ship);
|
||||||
} else {
|
if (action instanceof MoveAction) {
|
||||||
move_action = this.findEngine();
|
let corrected_target = action.applyReachableRange(this.ship, target, move_margin);
|
||||||
if (move_action) {
|
corrected_target = action.applyExclusion(this.ship, corrected_target);
|
||||||
let approach_radius = action.getRangeRadius(this.ship);
|
if (corrected_target) {
|
||||||
let approach = this.getApproach(move_action, target, approach_radius, move_margin);
|
result.need_move = target.getDistanceTo(this.ship.location) > 0;
|
||||||
if (approach instanceof Target) {
|
move_target = corrected_target;
|
||||||
result.need_move = true;
|
}
|
||||||
move_target = approach;
|
move_action = action;
|
||||||
} else if (approach != ApproachSimulationError.NO_MOVE_NEEDED) {
|
} else {
|
||||||
result.need_move = true;
|
move_action = this.findEngine();
|
||||||
result.can_move = false;
|
if (move_action) {
|
||||||
result.success = false;
|
let approach_radius = action.getRangeRadius(this.ship);
|
||||||
return result;
|
let approach = this.getApproach(move_action, target, approach_radius, move_margin);
|
||||||
}
|
if (approach instanceof Target) {
|
||||||
}
|
result.need_move = true;
|
||||||
}
|
move_target = approach;
|
||||||
if (move_target && arenaDistance(move_target, this.ship.location) < 0.000001) {
|
} else if (approach != ApproachSimulationError.NO_MOVE_NEEDED) {
|
||||||
result.need_move = false;
|
result.need_move = true;
|
||||||
}
|
result.can_move = false;
|
||||||
|
result.success = false;
|
||||||
// Check move AP
|
return result;
|
||||||
if (result.need_move && move_target && move_action) {
|
}
|
||||||
result.total_move_ap = move_action.getPowerUsage(this.ship, move_target);
|
}
|
||||||
result.can_move = ap > 0;
|
}
|
||||||
result.can_end_move = result.total_move_ap <= ap;
|
if (move_target && arenaDistance(move_target, this.ship.location) < 0.000001) {
|
||||||
result.move_location = move_target;
|
result.need_move = false;
|
||||||
// TODO Split in "this turn" part and "next turn" part if needed
|
}
|
||||||
result.parts.push({ action: move_action, target: move_target, ap: result.total_move_ap, possible: result.can_move });
|
|
||||||
|
// Check move AP
|
||||||
ap -= result.total_move_ap;
|
if (result.need_move && move_target && move_action) {
|
||||||
}
|
result.total_move_ap = move_action.getPowerUsage(this.ship, move_target);
|
||||||
|
result.can_move = ap > 0;
|
||||||
// Check action AP
|
result.can_end_move = result.total_move_ap <= ap;
|
||||||
if (action instanceof MoveAction) {
|
result.move_location = move_target;
|
||||||
result.success = result.need_move && result.can_move;
|
// TODO Split in "this turn" part and "next turn" part if needed
|
||||||
} else {
|
result.parts.push({ action: move_action, target: move_target, ap: result.total_move_ap, possible: result.can_move });
|
||||||
result.need_fire = true;
|
|
||||||
result.total_fire_ap = action.getPowerUsage(this.ship, target);
|
ap -= result.total_move_ap;
|
||||||
result.can_fire = result.total_fire_ap <= ap;
|
}
|
||||||
result.fire_location = target;
|
|
||||||
result.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire });
|
// Check action AP
|
||||||
result.success = true;
|
if (action instanceof MoveAction) {
|
||||||
}
|
result.success = result.need_move && result.can_move;
|
||||||
|
} else {
|
||||||
result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire);
|
result.need_fire = true;
|
||||||
|
result.total_fire_ap = action.getPowerUsage(this.ship, target);
|
||||||
return result;
|
result.can_fire = result.total_fire_ap <= ap;
|
||||||
}
|
result.fire_location = target;
|
||||||
|
result.parts.push({ action: action, target: target, ap: result.total_fire_ap, possible: (!result.need_move || result.can_end_move) && result.can_fire });
|
||||||
/**
|
result.success = true;
|
||||||
* Apply a move-fire simulation result, and predict the diffs it will apply on a battle
|
}
|
||||||
*
|
|
||||||
* The original battle passed as parameter will be duplicated, and not altered
|
result.complete = (!result.need_move || result.can_end_move) && (!result.need_fire || result.can_fire);
|
||||||
*/
|
|
||||||
getExpectedDiffs(battle: Battle, simulation: MoveFireResult): BaseBattleDiff[] {
|
return result;
|
||||||
let sim_battle = duplicate(battle, TK.SpaceTac);
|
}
|
||||||
let sim_ship = nn(sim_battle.getShip(this.ship.id));
|
|
||||||
let results: BaseBattleDiff[] = [];
|
/**
|
||||||
simulation.parts.forEach(part => {
|
* Apply a move-fire simulation result, and predict the diffs it will apply on a battle
|
||||||
let diffs = part.action.getDiffs(sim_ship, battle, part.target);
|
*
|
||||||
results = results.concat(diffs);
|
* The original battle passed as parameter will be duplicated, and not altered
|
||||||
sim_battle.applyDiffs(diffs);
|
*/
|
||||||
|
getExpectedDiffs(battle: Battle, simulation: MoveFireResult): BaseBattleDiff[] {
|
||||||
diffs = sim_battle.performChecks();
|
let sim_battle = duplicate(battle, NAMESPACE);
|
||||||
results = results.concat(diffs);
|
let sim_ship = nn(sim_battle.getShip(this.ship.id));
|
||||||
});
|
let results: BaseBattleDiff[] = [];
|
||||||
return results;
|
simulation.parts.forEach(part => {
|
||||||
}
|
let diffs = part.action.getDiffs(sim_ship, battle, part.target);
|
||||||
}
|
results = results.concat(diffs);
|
||||||
|
sim_battle.applyDiffs(diffs);
|
||||||
|
|
||||||
|
diffs = sim_battle.performChecks();
|
||||||
|
results = results.concat(diffs);
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { SkewedRandomGenerator } from "../common/RandomGenerator";
|
||||||
testing("NameGenerator", test => {
|
import { testing } from "../common/Testing";
|
||||||
test.case("generates unique names", check => {
|
import { NameGenerator } from "./NameGenerator";
|
||||||
var random = new SkewedRandomGenerator([0.48, 0.9, 0.1]);
|
|
||||||
var gen = new NameGenerator(["a", "b", "c"], random);
|
|
||||||
|
|
||||||
check.equals(gen.getName(), "b");
|
testing("NameGenerator", test => {
|
||||||
check.equals(gen.getName(), "c");
|
test.case("generates unique names", check => {
|
||||||
check.equals(gen.getName(), "a");
|
var random = new SkewedRandomGenerator([0.48, 0.9, 0.1]);
|
||||||
check.equals(gen.getName(), null);
|
var gen = new NameGenerator(["a", "b", "c"], random);
|
||||||
});
|
|
||||||
});
|
check.equals(gen.getName(), "b");
|
||||||
}
|
check.equals(gen.getName(), "c");
|
||||||
|
check.equals(gen.getName(), "a");
|
||||||
|
check.equals(gen.getName(), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
module TK.SpaceTac {
|
import { RandomGenerator } from "../common/RandomGenerator";
|
||||||
// A unique name generator
|
import { acopy } from "../common/Tools";
|
||||||
export class NameGenerator {
|
|
||||||
// List of available choices
|
|
||||||
private choices: string[];
|
|
||||||
|
|
||||||
// Random generator to use
|
// A unique name generator
|
||||||
private random: RandomGenerator;
|
export class NameGenerator {
|
||||||
|
// List of available choices
|
||||||
|
private choices: string[];
|
||||||
|
|
||||||
constructor(choices: string[], random: RandomGenerator = new RandomGenerator()) {
|
// Random generator to use
|
||||||
this.choices = acopy(choices);
|
private random: RandomGenerator;
|
||||||
this.random = random;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a new unique name from available choices
|
constructor(choices: string[], random: RandomGenerator = new RandomGenerator()) {
|
||||||
getName(): string | null {
|
this.choices = acopy(choices);
|
||||||
if (this.choices.length === 0) {
|
this.random = random;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var index = this.random.randInt(0, this.choices.length - 1);
|
// Get a new unique name from available choices
|
||||||
var result = this.choices[index];
|
getName(): string | null {
|
||||||
this.choices.splice(index, 1);
|
if (this.choices.length === 0) {
|
||||||
return result;
|
return null;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var index = this.random.randInt(0, this.choices.length - 1);
|
||||||
|
var result = this.choices[index];
|
||||||
|
this.choices.splice(index, 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +1,34 @@
|
||||||
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
|
*
|
||||||
*
|
* 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 = ""
|
|
||||||
|
// Aggressive 1 / Poised -1
|
||||||
// Aggressive 1 / Poised -1
|
aggressive = 0
|
||||||
aggressive = 0
|
|
||||||
|
// Funny 1 / Serious -1
|
||||||
// Funny 1 / Serious -1
|
funny = 0
|
||||||
funny = 0
|
|
||||||
|
// Heroic 1 / Coward -1
|
||||||
// Heroic 1 / Coward -1
|
heroic = 0
|
||||||
heroic = 0
|
|
||||||
|
// Optimistic 1 / Pessimistic -1
|
||||||
// Optimistic 1 / Pessimistic -1
|
optimistic = 0
|
||||||
optimistic = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +1,70 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("PersonalityReactions", test => {
|
import { Battle } from "./Battle";
|
||||||
function apply(pool: ReactionPool): PersonalityReaction | null {
|
import { ShipDamageDiff } from "./diffs/ShipDamageDiff";
|
||||||
let reactions = new PersonalityReactions();
|
import { BUILTIN_REACTION_POOL, PersonalityReaction, PersonalityReactionConversation, PersonalityReactions, ReactionPool } from "./PersonalityReactions";
|
||||||
return reactions.check(new Player(), null, null, null, pool);
|
import { Player } from "./Player";
|
||||||
}
|
import { Ship } from "./Ship";
|
||||||
|
|
||||||
class FakeReaction extends PersonalityReactionConversation {
|
testing("PersonalityReactions", test => {
|
||||||
ships: Ship[]
|
function apply(pool: ReactionPool): PersonalityReaction | null {
|
||||||
constructor(ships: Ship[]) {
|
let reactions = new PersonalityReactions();
|
||||||
super([]);
|
return reactions.check(new Player(), null, null, null, pool);
|
||||||
this.ships = ships;
|
}
|
||||||
}
|
|
||||||
static cons(ships: Ship[]): FakeReaction {
|
|
||||||
return new FakeReaction(ships);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test.case("fetches ships from conditions", check => {
|
class FakeReaction extends PersonalityReactionConversation {
|
||||||
let reaction = apply({});
|
ships: Ship[]
|
||||||
check.equals(reaction, null);
|
constructor(ships: Ship[]) {
|
||||||
|
super([]);
|
||||||
|
this.ships = ships;
|
||||||
|
}
|
||||||
|
static cons(ships: Ship[]): FakeReaction {
|
||||||
|
return new FakeReaction(ships);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let s1 = new Ship(null, "S1");
|
test.case("fetches ships from conditions", check => {
|
||||||
let s2 = new Ship(null, "S2");
|
let reaction = apply({});
|
||||||
|
check.equals(reaction, null);
|
||||||
|
|
||||||
reaction = apply({
|
let s1 = new Ship(null, "S1");
|
||||||
a: [() => [s1, s2], 1, [[() => 1, FakeReaction.cons]]],
|
let s2 = new Ship(null, "S2");
|
||||||
});
|
|
||||||
check.equals(reaction, new FakeReaction([s1, s2]));
|
|
||||||
})
|
|
||||||
|
|
||||||
test.case("applies weight on conditions", check => {
|
reaction = apply({
|
||||||
let s1 = new Ship(null, "S1");
|
a: [() => [s1, s2], 1, [[() => 1, FakeReaction.cons]]],
|
||||||
let s2 = new Ship(null, "S2");
|
});
|
||||||
|
check.equals(reaction, new FakeReaction([s1, s2]));
|
||||||
|
})
|
||||||
|
|
||||||
let reaction = apply({
|
test.case("applies weight on conditions", check => {
|
||||||
a: [() => [s1], 1, [[() => 1, FakeReaction.cons]]],
|
let s1 = new Ship(null, "S1");
|
||||||
b: [() => [s2], 0, [[() => 1, FakeReaction.cons]]],
|
let s2 = new Ship(null, "S2");
|
||||||
});
|
|
||||||
check.equals(reaction, new FakeReaction([s1]));
|
|
||||||
|
|
||||||
reaction = apply({
|
let reaction = apply({
|
||||||
a: [() => [s1], 0, [[() => 1, FakeReaction.cons]]],
|
a: [() => [s1], 1, [[() => 1, FakeReaction.cons]]],
|
||||||
b: [() => [s2], 1, [[() => 1, FakeReaction.cons]]],
|
b: [() => [s2], 0, [[() => 1, FakeReaction.cons]]],
|
||||||
});
|
});
|
||||||
check.equals(reaction, new FakeReaction([s2]));
|
check.equals(reaction, new FakeReaction([s1]));
|
||||||
})
|
|
||||||
|
|
||||||
test.case("checks for friendly fire", check => {
|
reaction = apply({
|
||||||
let condition = BUILTIN_REACTION_POOL['friendly_fire'][0];
|
a: [() => [s1], 0, [[() => 1, FakeReaction.cons]]],
|
||||||
let battle = new Battle();
|
b: [() => [s2], 1, [[() => 1, FakeReaction.cons]]],
|
||||||
let player = new Player();
|
});
|
||||||
battle.fleets[0].setPlayer(player);
|
check.equals(reaction, new FakeReaction([s2]));
|
||||||
let ship1a = battle.fleets[0].addShip();
|
})
|
||||||
let ship1b = battle.fleets[0].addShip();
|
|
||||||
let ship2a = battle.fleets[1].addShip();
|
|
||||||
let ship2b = battle.fleets[1].addShip();
|
|
||||||
|
|
||||||
check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship1a, 50, 10)), [], "self shoot");
|
test.case("checks for friendly fire", check => {
|
||||||
check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship1b, 50, 10)), [ship1b, ship1a]);
|
let condition = BUILTIN_REACTION_POOL['friendly_fire'][0];
|
||||||
check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship2a, 50, 10)), [], "enemy shoot");
|
let battle = new Battle();
|
||||||
check.equals(condition(player, battle, ship2a, new ShipDamageDiff(ship2a, 50, 10)), [], "other player event");
|
let player = new Player();
|
||||||
})
|
battle.fleets[0].setPlayer(player);
|
||||||
})
|
let ship1a = battle.fleets[0].addShip();
|
||||||
}
|
let ship1b = battle.fleets[0].addShip();
|
||||||
|
let ship2a = battle.fleets[1].addShip();
|
||||||
|
let ship2b = battle.fleets[1].addShip();
|
||||||
|
|
||||||
|
check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship1a, 50, 10)), [], "self shoot");
|
||||||
|
check.equals(condition(player, battle, ship1a, new ShipDamageDiff(ship1b, 50, 10)), [ship1b, ship1a]);
|
||||||
|
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");
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -1,106 +1,113 @@
|
||||||
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
|
||||||
* Reactions to external events according to personalities.
|
export type ReactionPool = { [code: string]: ReactionConfig }
|
||||||
*
|
|
||||||
* This allows for a more "alive" world, as characters tend to speak to react to events.
|
|
||||||
*
|
|
||||||
* This object will store the previous reactions to avoid too much recurrence, and should be global to a whole
|
|
||||||
* game session.
|
|
||||||
*/
|
|
||||||
export class PersonalityReactions {
|
|
||||||
done: string[] = []
|
|
||||||
random = RandomGenerator.global
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for a reaction.
|
* Reactions to external events according to personalities.
|
||||||
*
|
*
|
||||||
* This will return a reaction to display, and add it to the done list
|
* This allows for a more "alive" world, as characters tend to speak to react to events.
|
||||||
*/
|
*
|
||||||
check(player: Player, battle: Battle | null = null, ship: Ship | null = null, event: BaseBattleDiff | null = null, pool: ReactionPool = BUILTIN_REACTION_POOL): PersonalityReaction | null {
|
* This object will store the previous reactions to avoid too much recurrence, and should be global to a whole
|
||||||
let codes = difference(keys(pool), this.done);
|
* game session.
|
||||||
|
*/
|
||||||
|
export class PersonalityReactions {
|
||||||
|
done: string[] = []
|
||||||
|
random = RandomGenerator.global
|
||||||
|
|
||||||
let candidates = nna(codes.map((code: string): [string, Ship[], ReactionProfile[]] | null => {
|
/**
|
||||||
let [condition, chance, profiles] = pool[code];
|
* Check for a reaction.
|
||||||
if (this.random.random() <= chance) {
|
*
|
||||||
let involved = condition(player, battle, ship, event);
|
* This will return a reaction to display, and add it to the done list
|
||||||
if (involved.length > 0) {
|
*/
|
||||||
return [code, involved, profiles];
|
check(player: Player, battle: Battle | null = null, ship: Ship | null = null, event: BaseBattleDiff | null = null, pool: ReactionPool = BUILTIN_REACTION_POOL): PersonalityReaction | null {
|
||||||
} else {
|
let codes = difference(keys(pool), this.done);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (candidates.length > 0) {
|
let candidates = nna(codes.map((code: string): [string, Ship[], ReactionProfile[]] | null => {
|
||||||
let [code, involved, profiles] = this.random.choice(candidates);
|
let [condition, chance, profiles] = pool[code];
|
||||||
let primary = involved[0];
|
if (this.random.random() <= chance) {
|
||||||
let weights = profiles.map(([evaluator, _]) => evaluator(primary.personality));
|
let involved = condition(player, battle, ship, event);
|
||||||
let action_number = this.random.weighted(weights);
|
if (involved.length > 0) {
|
||||||
if (action_number >= 0) {
|
return [code, involved, profiles];
|
||||||
this.done.push(code);
|
|
||||||
let reaction_constructor = profiles[action_number][1];
|
|
||||||
return reaction_constructor(involved);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* One kind of personality reaction: saying something out loud
|
|
||||||
*/
|
|
||||||
export class PersonalityReactionConversation {
|
|
||||||
messages: { interlocutor: Ship, message: string }[]
|
|
||||||
constructor(messages: { interlocutor: Ship, message: string }[]) {
|
|
||||||
this.messages = messages;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Standard reaction pool
|
|
||||||
*/
|
|
||||||
export const BUILTIN_REACTION_POOL: ReactionPool = {
|
|
||||||
friendly_fire: [cond_friendly_fire, 1, [
|
|
||||||
[traits => 1, ships => new PersonalityReactionConversation([
|
|
||||||
{ interlocutor: ships[0], message: "Hey !!! Watch where you're shooting !" },
|
|
||||||
{ interlocutor: ships[1], message: "Sorry mate..." },
|
|
||||||
])]
|
|
||||||
]]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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[] {
|
|
||||||
if (battle && ship && event) {
|
|
||||||
if (event instanceof ShipDamageDiff && player.is(ship.fleet.player) && !ship.is(event.ship_id)) {
|
|
||||||
let hurt = battle.getShip(event.ship_id);
|
|
||||||
return (hurt && player.is(hurt.fleet.player)) ? [hurt, ship] : [];
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
let [code, involved, profiles] = this.random.choice(candidates);
|
||||||
|
let primary = involved[0];
|
||||||
|
let weights = profiles.map(([evaluator, _]) => evaluator(primary.personality));
|
||||||
|
let action_number = this.random.weighted(weights);
|
||||||
|
if (action_number >= 0) {
|
||||||
|
this.done.push(code);
|
||||||
|
let reaction_constructor = profiles[action_number][1];
|
||||||
|
return reaction_constructor(involved);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One kind of personality reaction: saying something out loud
|
||||||
|
*/
|
||||||
|
export class PersonalityReactionConversation {
|
||||||
|
messages: { interlocutor: Ship, message: string }[]
|
||||||
|
constructor(messages: { interlocutor: Ship, message: string }[]) {
|
||||||
|
this.messages = messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard reaction pool
|
||||||
|
*/
|
||||||
|
export const BUILTIN_REACTION_POOL: ReactionPool = {
|
||||||
|
friendly_fire: [cond_friendly_fire, 1, [
|
||||||
|
[traits => 1, ships => new PersonalityReactionConversation([
|
||||||
|
{ interlocutor: ships[0], message: "Hey !!! Watch where you're shooting !" },
|
||||||
|
{ interlocutor: ships[1], message: "Sorry mate..." },
|
||||||
|
])]
|
||||||
|
]]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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[] {
|
||||||
|
if (battle && ship && event) {
|
||||||
|
if (event instanceof ShipDamageDiff && player.is(ship.fleet.player) && !ship.is(event.ship_id)) {
|
||||||
|
let hurt = battle.getShip(event.ship_id);
|
||||||
|
return (hurt && player.is(hurt.fleet.player)) ? [hurt, ship] : [];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,41 @@
|
||||||
module TK.SpaceTac {
|
import { testing } from "../common/Testing";
|
||||||
testing("Player", test => {
|
import { Player } from "./Player";
|
||||||
test.case("keeps track of visited locations", check => {
|
import { StarLocationType } from "./StarLocation";
|
||||||
let player = new Player();
|
import { Universe } from "./Universe";
|
||||||
let universe = new Universe();
|
|
||||||
let star1 = universe.addStar();
|
|
||||||
let star2 = universe.addStar();
|
|
||||||
let loc1a = star1.addLocation(StarLocationType.PLANET);
|
|
||||||
let loc1b = star1.addLocation(StarLocationType.PLANET);
|
|
||||||
let loc2a = star2.addLocation(StarLocationType.PLANET);
|
|
||||||
let loc2b = star2.addLocation(StarLocationType.PLANET);
|
|
||||||
universe.updateLocations();
|
|
||||||
|
|
||||||
function checkVisited(s1 = false, s2 = false, v1a = false, v1b = false, v2a = false, v2b = false) {
|
testing("Player", test => {
|
||||||
check.same(player.hasVisitedSystem(star1), s1);
|
test.case("keeps track of visited locations", check => {
|
||||||
check.same(player.hasVisitedSystem(star2), s2);
|
let player = new Player();
|
||||||
check.same(player.hasVisitedLocation(loc1a), v1a);
|
let universe = new Universe();
|
||||||
check.same(player.hasVisitedLocation(loc1b), v1b);
|
let star1 = universe.addStar();
|
||||||
check.same(player.hasVisitedLocation(loc2a), v2a);
|
let star2 = universe.addStar();
|
||||||
check.same(player.hasVisitedLocation(loc2b), v2b);
|
let loc1a = star1.addLocation(StarLocationType.PLANET);
|
||||||
}
|
let loc1b = star1.addLocation(StarLocationType.PLANET);
|
||||||
|
let loc2a = star2.addLocation(StarLocationType.PLANET);
|
||||||
|
let loc2b = star2.addLocation(StarLocationType.PLANET);
|
||||||
|
universe.updateLocations();
|
||||||
|
|
||||||
checkVisited();
|
function checkVisited(s1 = false, s2 = false, v1a = false, v1b = false, v2a = false, v2b = false) {
|
||||||
|
check.same(player.hasVisitedSystem(star1), s1);
|
||||||
|
check.same(player.hasVisitedSystem(star2), s2);
|
||||||
|
check.same(player.hasVisitedLocation(loc1a), v1a);
|
||||||
|
check.same(player.hasVisitedLocation(loc1b), v1b);
|
||||||
|
check.same(player.hasVisitedLocation(loc2a), v2a);
|
||||||
|
check.same(player.hasVisitedLocation(loc2b), v2b);
|
||||||
|
}
|
||||||
|
|
||||||
player.fleet.setLocation(loc1b);
|
checkVisited();
|
||||||
checkVisited(true, false, false, true, false, false);
|
|
||||||
|
|
||||||
player.fleet.setLocation(loc1a);
|
player.fleet.setLocation(loc1b);
|
||||||
checkVisited(true, false, true, true, false, false);
|
checkVisited(true, false, false, true, false, false);
|
||||||
|
|
||||||
player.fleet.setLocation(loc2a);
|
player.fleet.setLocation(loc1a);
|
||||||
checkVisited(true, true, true, true, true, false);
|
checkVisited(true, false, true, true, false, false);
|
||||||
|
|
||||||
player.fleet.setLocation(loc2a);
|
player.fleet.setLocation(loc2a);
|
||||||
checkVisited(true, true, true, true, true, false);
|
checkVisited(true, true, true, true, true, false);
|
||||||
});
|
|
||||||
});
|
player.fleet.setLocation(loc2a);
|
||||||
}
|
checkVisited(true, true, true, true, true, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,78 +1,84 @@
|
||||||
/// <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
|
|
||||||
|
|
||||||
// Bound fleet
|
// Bound fleet
|
||||||
fleet: Fleet
|
fleet: Fleet
|
||||||
|
|
||||||
// Active missions
|
// Active missions
|
||||||
missions = new ActiveMissions()
|
missions = new ActiveMissions()
|
||||||
|
|
||||||
// Create a player, with an empty fleet
|
// Create a player, with an empty fleet
|
||||||
constructor(name = "Player", fleet?: Fleet) {
|
constructor(name = "Player", fleet?: Fleet) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.fleet = fleet || new Fleet(this);
|
this.fleet = fleet || new Fleet(this);
|
||||||
|
|
||||||
this.fleet.setPlayer(this);
|
this.fleet.setPlayer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a quick random player, with a fleet, for testing purposes
|
// Create a quick random player, with a fleet, for testing purposes
|
||||||
static newQuickRandom(name: string, level = 1, shipcount = 4, upgrade = false): Player {
|
static newQuickRandom(name: string, level = 1, shipcount = 4, upgrade = false): Player {
|
||||||
let player = new Player(name);
|
let player = new Player(name);
|
||||||
let generator = new FleetGenerator();
|
let generator = new FleetGenerator();
|
||||||
player.fleet = generator.generate(level, player, shipcount, upgrade);
|
player.fleet = generator.generate(level, player, shipcount, upgrade);
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the fleet for this player
|
* Set the fleet for this player
|
||||||
*/
|
*/
|
||||||
setFleet(fleet: Fleet): void {
|
setFleet(fleet: Fleet): void {
|
||||||
this.fleet = fleet;
|
this.fleet = fleet;
|
||||||
fleet.setPlayer(this);
|
fleet.setPlayer(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a cheats object
|
* Get a cheats object
|
||||||
*/
|
*/
|
||||||
getCheats(): BattleCheats | null {
|
getCheats(): BattleCheats | null {
|
||||||
let battle = this.getBattle();
|
let battle = this.getBattle();
|
||||||
if (battle) {
|
if (battle) {
|
||||||
return new BattleCheats(battle, this);
|
return new BattleCheats(battle, this);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if the player has visited at least one location in a given system.
|
|
||||||
*/
|
|
||||||
hasVisitedSystem(system: Star): boolean {
|
|
||||||
return intersection(this.fleet.visited, system.locations.map(loc => loc.id)).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return true if the player has visited a given star location.
|
|
||||||
*/
|
|
||||||
hasVisitedLocation(location: StarLocation): boolean {
|
|
||||||
return contains(this.fleet.visited, location.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get currently played battle, null when none is in progress
|
|
||||||
getBattle(): Battle | null {
|
|
||||||
return this.fleet.battle;
|
|
||||||
}
|
|
||||||
setBattle(battle: Battle | null): void {
|
|
||||||
this.fleet.setBattle(battle);
|
|
||||||
this.missions.checkStatus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the player has visited at least one location in a given system.
|
||||||
|
*/
|
||||||
|
hasVisitedSystem(system: Star): boolean {
|
||||||
|
return intersection(this.fleet.visited, system.locations.map(loc => loc.id)).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the player has visited a given star location.
|
||||||
|
*/
|
||||||
|
hasVisitedLocation(location: StarLocation): boolean {
|
||||||
|
return contains(this.fleet.visited, location.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get currently played battle, null when none is in progress
|
||||||
|
getBattle(): Battle | null {
|
||||||
|
return this.fleet.battle;
|
||||||
|
}
|
||||||
|
setBattle(battle: Battle | null): void {
|
||||||
|
this.fleet.setBattle(battle);
|
||||||
|
this.missions.checkStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,46 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("Range", test => {
|
import { IntegerRange } from "./Range";
|
||||||
test.case("can work with proportional values", check => {
|
|
||||||
var range = new Range(1, 5);
|
|
||||||
|
|
||||||
function checkProportional(range: Range, value1: number, value2: number) {
|
testing("Range", test => {
|
||||||
check.equals(range.getProportional(value1), value2);
|
test.case("can work with proportional values", check => {
|
||||||
check.equals(range.getReverseProportional(value2), value1);
|
var range = new Range(1, 5);
|
||||||
}
|
|
||||||
|
|
||||||
checkProportional(range, 0, 1);
|
function checkProportional(range: Range, value1: number, value2: number) {
|
||||||
checkProportional(range, 1, 5);
|
check.equals(range.getProportional(value1), value2);
|
||||||
checkProportional(range, 0.5, 3);
|
check.equals(range.getReverseProportional(value2), value1);
|
||||||
checkProportional(range, 0.4, 2.6);
|
}
|
||||||
|
|
||||||
check.equals(range.getProportional(-0.25), 1);
|
checkProportional(range, 0, 1);
|
||||||
check.equals(range.getProportional(1.8), 5);
|
checkProportional(range, 1, 5);
|
||||||
|
checkProportional(range, 0.5, 3);
|
||||||
|
checkProportional(range, 0.4, 2.6);
|
||||||
|
|
||||||
check.equals(range.getReverseProportional(0), 0);
|
check.equals(range.getProportional(-0.25), 1);
|
||||||
check.equals(range.getReverseProportional(6), 1);
|
check.equals(range.getProportional(1.8), 5);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
testing("IntegerRange", test => {
|
check.equals(range.getReverseProportional(0), 0);
|
||||||
test.case("can work with proportional values", check => {
|
check.equals(range.getReverseProportional(6), 1);
|
||||||
var range = new IntegerRange(1, 5);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
check.equals(range.getProportional(0), 1);
|
testing("IntegerRange", test => {
|
||||||
check.equals(range.getProportional(0.1), 1);
|
test.case("can work with proportional values", check => {
|
||||||
check.equals(range.getProportional(0.2), 2);
|
var range = new IntegerRange(1, 5);
|
||||||
check.equals(range.getProportional(0.45), 3);
|
|
||||||
check.equals(range.getProportional(0.5), 3);
|
|
||||||
check.equals(range.getProportional(0.75), 4);
|
|
||||||
check.equals(range.getProportional(0.8), 5);
|
|
||||||
check.equals(range.getProportional(0.99), 5);
|
|
||||||
check.equals(range.getProportional(1), 5);
|
|
||||||
|
|
||||||
check.equals(range.getReverseProportional(1), 0);
|
check.equals(range.getProportional(0), 1);
|
||||||
check.equals(range.getReverseProportional(2), 0.2);
|
check.equals(range.getProportional(0.1), 1);
|
||||||
check.equals(range.getReverseProportional(3), 0.4);
|
check.equals(range.getProportional(0.2), 2);
|
||||||
check.equals(range.getReverseProportional(4), 0.6);
|
check.equals(range.getProportional(0.45), 3);
|
||||||
check.equals(range.getReverseProportional(5), 0.8);
|
check.equals(range.getProportional(0.5), 3);
|
||||||
});
|
check.equals(range.getProportional(0.75), 4);
|
||||||
});
|
check.equals(range.getProportional(0.8), 5);
|
||||||
}
|
check.equals(range.getProportional(0.99), 5);
|
||||||
|
check.equals(range.getProportional(1), 5);
|
||||||
|
|
||||||
|
check.equals(range.getReverseProportional(1), 0);
|
||||||
|
check.equals(range.getReverseProportional(2), 0.2);
|
||||||
|
check.equals(range.getReverseProportional(3), 0.4);
|
||||||
|
check.equals(range.getReverseProportional(4), 0.6);
|
||||||
|
check.equals(range.getReverseProportional(5), 0.8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,82 +1,80 @@
|
||||||
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
|
|
||||||
|
|
||||||
// Maximal value
|
// Maximal value
|
||||||
max = 0
|
max = 0
|
||||||
|
|
||||||
// Create a range of values
|
// Create a range of values
|
||||||
constructor(min: number, max: number | null = null) {
|
constructor(min: number, max: number | null = null) {
|
||||||
this.set(min, max);
|
this.set(min, max);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change the range
|
// Change the range
|
||||||
set(min: number, max: number | null = null) {
|
set(min: number, max: number | null = null) {
|
||||||
this.min = min;
|
this.min = min;
|
||||||
if (max === null) {
|
if (max === null) {
|
||||||
this.max = this.min;
|
this.max = this.min;
|
||||||
} else {
|
} else {
|
||||||
this.max = max;
|
this.max = max;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get a proportional value (give 0.0-1.0 value to obtain a value in range)
|
|
||||||
getProportional(cursor: number): number {
|
|
||||||
if (cursor <= 0.0) {
|
|
||||||
return this.min;
|
|
||||||
} else if (cursor >= 1.0) {
|
|
||||||
return this.max;
|
|
||||||
} else {
|
|
||||||
return (this.max - this.min) * cursor + this.min;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the value of the cursor that would give this proportional value (in 0.0-1.0 range)
|
|
||||||
getReverseProportional(expected: number): number {
|
|
||||||
if (expected <= this.min) {
|
|
||||||
return 0;
|
|
||||||
} else if (expected >= this.max) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return (expected - this.min) / (this.max - this.min);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a value is in the range
|
|
||||||
isInRange(value: number): boolean {
|
|
||||||
return value >= this.min && value <= this.max;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a proportional value (give 0.0-1.0 value to obtain a value in range)
|
||||||
// Range of integer values
|
getProportional(cursor: number): number {
|
||||||
//
|
if (cursor <= 0.0) {
|
||||||
// This differs from Range in that it adds space in proportional values to include the 'max'.
|
return this.min;
|
||||||
// Typically, using Range for integers will only yield 'max' for exactly 1.0 proportional, not for 0.999999.
|
} else if (cursor >= 1.0) {
|
||||||
// This fixes this behavior.
|
return this.max;
|
||||||
//
|
} else {
|
||||||
// As this rounds values to integer, the 'reverse' proportional is no longer a bijection.
|
return (this.max - this.min) * cursor + this.min;
|
||||||
export class IntegerRange extends Range {
|
|
||||||
getProportional(cursor: number): number {
|
|
||||||
if (cursor <= 0.0) {
|
|
||||||
return this.min;
|
|
||||||
} else if (cursor >= 1.0) {
|
|
||||||
return this.max;
|
|
||||||
} else {
|
|
||||||
return Math.floor((this.max - this.min + 1) * cursor + this.min);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getReverseProportional(expected: number): number {
|
|
||||||
if (expected <= this.min) {
|
|
||||||
return 0;
|
|
||||||
} else if (expected > this.max) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return (expected - this.min) * 1.0 / (this.max - this.min + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value of the cursor that would give this proportional value (in 0.0-1.0 range)
|
||||||
|
getReverseProportional(expected: number): number {
|
||||||
|
if (expected <= this.min) {
|
||||||
|
return 0;
|
||||||
|
} else if (expected >= this.max) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return (expected - this.min) / (this.max - this.min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a value is in the range
|
||||||
|
isInRange(value: number): boolean {
|
||||||
|
return value >= this.min && value <= this.max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Range of integer values
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
// This fixes this behavior.
|
||||||
|
//
|
||||||
|
// As this rounds values to integer, the 'reverse' proportional is no longer a bijection.
|
||||||
|
export class IntegerRange extends Range {
|
||||||
|
getProportional(cursor: number): number {
|
||||||
|
if (cursor <= 0.0) {
|
||||||
|
return this.min;
|
||||||
|
} else if (cursor >= 1.0) {
|
||||||
|
return this.max;
|
||||||
|
} else {
|
||||||
|
return Math.floor((this.max - this.min + 1) * cursor + this.min);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getReverseProportional(expected: number): number {
|
||||||
|
if (expected <= this.min) {
|
||||||
|
return 0;
|
||||||
|
} else if (expected > this.max) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return (expected - this.min) * 1.0 / (this.max - this.min + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,195 +1,210 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("Ship", test => {
|
import { nn } from "../common/Tools";
|
||||||
test.case("creates a full name", check => {
|
import { BaseAction } from "./actions/BaseAction";
|
||||||
let ship = new Ship();
|
import { ToggleAction } from "./actions/ToggleAction";
|
||||||
check.equals(ship.getName(false), "Ship");
|
import { Battle } from "./Battle";
|
||||||
check.equals(ship.getName(true), "Level 1 Ship");
|
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";
|
||||||
|
|
||||||
ship.model = new ShipModel("test", "Hauler");
|
testing("Ship", test => {
|
||||||
check.equals(ship.getName(false), "Hauler");
|
test.case("creates a full name", check => {
|
||||||
check.equals(ship.getName(true), "Level 1 Hauler");
|
let ship = new Ship();
|
||||||
|
check.equals(ship.getName(false), "Ship");
|
||||||
|
check.equals(ship.getName(true), "Level 1 Ship");
|
||||||
|
|
||||||
ship.name = "Titan-W12";
|
ship.model = new ShipModel("test", "Hauler");
|
||||||
check.equals(ship.getName(false), "Titan-W12");
|
check.equals(ship.getName(false), "Hauler");
|
||||||
check.equals(ship.getName(true), "Level 1 Titan-W12");
|
check.equals(ship.getName(true), "Level 1 Hauler");
|
||||||
|
|
||||||
ship.level.forceLevel(3);
|
ship.name = "Titan-W12";
|
||||||
check.equals(ship.getName(false), "Titan-W12");
|
check.equals(ship.getName(false), "Titan-W12");
|
||||||
check.equals(ship.getName(true), "Level 3 Titan-W12");
|
check.equals(ship.getName(true), "Level 1 Titan-W12");
|
||||||
});
|
|
||||||
|
|
||||||
test.case("moves in the arena", check => {
|
ship.level.forceLevel(3);
|
||||||
let ship = new Ship(null, "Test");
|
check.equals(ship.getName(false), "Titan-W12");
|
||||||
let engine = TestTools.addEngine(ship, 50);
|
check.equals(ship.getName(true), "Level 3 Titan-W12");
|
||||||
|
});
|
||||||
|
|
||||||
check.equals(ship.arena_x, 0);
|
test.case("moves in the arena", check => {
|
||||||
check.equals(ship.arena_y, 0);
|
let ship = new Ship(null, "Test");
|
||||||
check.equals(ship.arena_angle, 0);
|
let engine = TestTools.addEngine(ship, 50);
|
||||||
|
|
||||||
ship.setArenaFacingAngle(1.2);
|
check.equals(ship.arena_x, 0);
|
||||||
ship.setArenaPosition(12, 50);
|
check.equals(ship.arena_y, 0);
|
||||||
|
check.equals(ship.arena_angle, 0);
|
||||||
|
|
||||||
check.equals(ship.arena_x, 12);
|
ship.setArenaFacingAngle(1.2);
|
||||||
check.equals(ship.arena_y, 50);
|
ship.setArenaPosition(12, 50);
|
||||||
check.nears(ship.arena_angle, 1.2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("applies permanent effects of ship model on attributes", check => {
|
check.equals(ship.arena_x, 12);
|
||||||
let model = new ShipModel();
|
check.equals(ship.arena_y, 50);
|
||||||
let ship = new Ship(null, null, model);
|
check.nears(ship.arena_angle, 1.2);
|
||||||
|
});
|
||||||
|
|
||||||
check.patch(model, "getEffects", () => [
|
test.case("applies permanent effects of ship model on attributes", check => {
|
||||||
new AttributeEffect("power_capacity", 4),
|
let model = new ShipModel();
|
||||||
new AttributeEffect("power_capacity", 5),
|
let ship = new Ship(null, null, model);
|
||||||
]);
|
|
||||||
|
|
||||||
ship.updateAttributes();
|
check.patch(model, "getEffects", () => [
|
||||||
check.equals(ship.getAttribute("power_capacity"), 9);
|
new AttributeEffect("power_capacity", 4),
|
||||||
});
|
new AttributeEffect("power_capacity", 5),
|
||||||
|
]);
|
||||||
|
|
||||||
test.case("repairs hull and recharges shield", check => {
|
ship.updateAttributes();
|
||||||
var ship = new Ship(null, "Test");
|
check.equals(ship.getAttribute("power_capacity"), 9);
|
||||||
|
});
|
||||||
|
|
||||||
TestTools.setAttribute(ship, "hull_capacity", 120);
|
test.case("repairs hull and recharges shield", check => {
|
||||||
TestTools.setAttribute(ship, "shield_capacity", 150);
|
var ship = new Ship(null, "Test");
|
||||||
|
|
||||||
check.equals(ship.getValue("hull"), 0);
|
TestTools.setAttribute(ship, "hull_capacity", 120);
|
||||||
check.equals(ship.getValue("shield"), 0);
|
TestTools.setAttribute(ship, "shield_capacity", 150);
|
||||||
|
|
||||||
ship.restoreHealth();
|
check.equals(ship.getValue("hull"), 0);
|
||||||
|
check.equals(ship.getValue("shield"), 0);
|
||||||
|
|
||||||
check.equals(ship.getValue("hull"), 120);
|
ship.restoreHealth();
|
||||||
check.equals(ship.getValue("shield"), 150);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("checks if a ship is able to play", check => {
|
check.equals(ship.getValue("hull"), 120);
|
||||||
let battle = new Battle();
|
check.equals(ship.getValue("shield"), 150);
|
||||||
let ship = battle.fleets[0].addShip();
|
});
|
||||||
ship.setValue("hull", 10);
|
|
||||||
|
|
||||||
check.equals(ship.isAbleToPlay(), false);
|
test.case("checks if a ship is able to play", check => {
|
||||||
check.equals(ship.isAbleToPlay(false), true);
|
let battle = new Battle();
|
||||||
|
let ship = battle.fleets[0].addShip();
|
||||||
|
ship.setValue("hull", 10);
|
||||||
|
|
||||||
ship.setValue("power", 5);
|
check.equals(ship.isAbleToPlay(), false);
|
||||||
|
check.equals(ship.isAbleToPlay(false), true);
|
||||||
|
|
||||||
check.equals(ship.isAbleToPlay(), true);
|
ship.setValue("power", 5);
|
||||||
check.equals(ship.isAbleToPlay(false), true);
|
|
||||||
|
|
||||||
ship.setDead();
|
check.equals(ship.isAbleToPlay(), true);
|
||||||
|
check.equals(ship.isAbleToPlay(false), true);
|
||||||
|
|
||||||
check.equals(ship.isAbleToPlay(), false);
|
ship.setDead();
|
||||||
check.equals(ship.isAbleToPlay(false), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("checks if a ship is inside a given circle", check => {
|
check.equals(ship.isAbleToPlay(), false);
|
||||||
let ship = new Ship();
|
check.equals(ship.isAbleToPlay(false), false);
|
||||||
ship.arena_x = 5;
|
});
|
||||||
ship.arena_y = 8;
|
|
||||||
|
|
||||||
check.equals(ship.isInCircle(5, 8, 0), true);
|
test.case("checks if a ship is inside a given circle", check => {
|
||||||
check.equals(ship.isInCircle(5, 8, 1), true);
|
let ship = new Ship();
|
||||||
check.equals(ship.isInCircle(5, 7, 1), true);
|
ship.arena_x = 5;
|
||||||
check.equals(ship.isInCircle(6, 9, 1.7), true);
|
ship.arena_y = 8;
|
||||||
check.equals(ship.isInCircle(5, 8.1, 0), false);
|
|
||||||
check.equals(ship.isInCircle(5, 7, 0.9), false);
|
|
||||||
check.equals(ship.isInCircle(12, -4, 5), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("restores as new at the end of battle", check => {
|
check.equals(ship.isInCircle(5, 8, 0), true);
|
||||||
let ship = new Ship();
|
check.equals(ship.isInCircle(5, 8, 1), true);
|
||||||
TestTools.setShipModel(ship, 10, 20, 5);
|
check.equals(ship.isInCircle(5, 7, 1), true);
|
||||||
ship.setValue("hull", 5);
|
check.equals(ship.isInCircle(6, 9, 1.7), true);
|
||||||
ship.setValue("shield", 15);
|
check.equals(ship.isInCircle(5, 8.1, 0), false);
|
||||||
ship.setValue("power", 2);
|
check.equals(ship.isInCircle(5, 7, 0.9), false);
|
||||||
ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 12));
|
check.equals(ship.isInCircle(12, -4, 5), false);
|
||||||
ship.updateAttributes();
|
});
|
||||||
let action1 = new BaseAction("action1");
|
|
||||||
ship.actions.addCustom(action1);
|
|
||||||
let action2 = new ToggleAction("action2");
|
|
||||||
ship.actions.addCustom(action2);
|
|
||||||
ship.actions.toggle(action2, true);
|
|
||||||
let action3 = new ToggleAction("action3");
|
|
||||||
ship.actions.addCustom(action3);
|
|
||||||
|
|
||||||
check.in("before", check => {
|
test.case("restores as new at the end of battle", check => {
|
||||||
check.equals(ship.getValue("hull"), 5, "hull");
|
let ship = new Ship();
|
||||||
check.equals(ship.getValue("shield"), 15, "shield");
|
TestTools.setShipModel(ship, 10, 20, 5);
|
||||||
check.equals(ship.getValue("power"), 2, "power");
|
ship.setValue("hull", 5);
|
||||||
check.equals(ship.active_effects.count(), 1, "effects count");
|
ship.setValue("shield", 15);
|
||||||
check.equals(ship.getAttribute("power_capacity"), 3, "power capacity");
|
ship.setValue("power", 2);
|
||||||
check.equals(ship.actions.isToggled(action2), true, "action 2 activation");
|
ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("power_capacity", 3), 12));
|
||||||
check.equals(ship.actions.isToggled(action3), false, "action 3 activation");
|
ship.updateAttributes();
|
||||||
});
|
let action1 = new BaseAction("action1");
|
||||||
|
ship.actions.addCustom(action1);
|
||||||
|
let action2 = new ToggleAction("action2");
|
||||||
|
ship.actions.addCustom(action2);
|
||||||
|
ship.actions.toggle(action2, true);
|
||||||
|
let action3 = new ToggleAction("action3");
|
||||||
|
ship.actions.addCustom(action3);
|
||||||
|
|
||||||
ship.restoreInitialState();
|
check.in("before", check => {
|
||||||
|
check.equals(ship.getValue("hull"), 5, "hull");
|
||||||
check.in("after", check => {
|
check.equals(ship.getValue("shield"), 15, "shield");
|
||||||
check.equals(ship.getValue("hull"), 10, "hull");
|
check.equals(ship.getValue("power"), 2, "power");
|
||||||
check.equals(ship.getValue("shield"), 20, "shield");
|
check.equals(ship.active_effects.count(), 1, "effects count");
|
||||||
check.equals(ship.getValue("power"), 5, "power");
|
check.equals(ship.getAttribute("power_capacity"), 3, "power capacity");
|
||||||
check.equals(ship.active_effects.count(), 0, "effects count");
|
check.equals(ship.actions.isToggled(action2), true, "action 2 activation");
|
||||||
check.equals(ship.getAttribute("power_capacity"), 5, "power capacity");
|
check.equals(ship.actions.isToggled(action3), false, "action 3 activation");
|
||||||
check.equals(ship.actions.isToggled(action2), false, "action 2 activation");
|
|
||||||
check.equals(ship.actions.isToggled(action3), false, "action 3 activation");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("lists active effects", check => {
|
|
||||||
let ship = new Ship();
|
|
||||||
check.equals(ship.getEffects(), []);
|
|
||||||
|
|
||||||
let effect1 = new AttributeEffect("evasion", 4);
|
|
||||||
check.patch(ship.model, "getEffects", () => [effect1]);
|
|
||||||
check.equals(ship.getEffects(), [effect1]);
|
|
||||||
|
|
||||||
let effect2 = new AttributeLimitEffect("evasion", 2);
|
|
||||||
ship.active_effects.add(new StickyEffect(effect2, 4));
|
|
||||||
check.equals(ship.getEffects(), [effect1, effect2]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("gets a textual description of an attribute", check => {
|
|
||||||
let ship = new Ship();
|
|
||||||
check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering");
|
|
||||||
|
|
||||||
check.patch(ship, "getUpgrades", () => [
|
|
||||||
{ code: "Base", effects: [new AttributeEffect("evasion", 3)] },
|
|
||||||
{ code: "Up1", effects: [new AttributeEffect("shield_capacity", 1)] },
|
|
||||||
{ code: "Up2", effects: [new AttributeEffect("shield_capacity", 1), new AttributeEffect("evasion", 1)] }
|
|
||||||
]);
|
|
||||||
check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1");
|
|
||||||
|
|
||||||
ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("evasion", 3)));
|
|
||||||
check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1\nSticky effect: limit to 3");
|
|
||||||
|
|
||||||
ship.active_effects.remove(ship.active_effects.list()[0]);
|
|
||||||
ship.active_effects.add(new AttributeEffect("evasion", -1));
|
|
||||||
check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1\nActive effect: -1");
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("produces death diffs", check => {
|
|
||||||
let battle = TestTools.createBattle(1);
|
|
||||||
let ship = nn(battle.playing_ship);
|
|
||||||
|
|
||||||
check.equals(ship.getDeathDiffs(battle), [
|
|
||||||
new ShipValueDiff(ship, "hull", -1),
|
|
||||||
new ShipDeathDiff(battle, ship),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let effect1 = ship.active_effects.add(new AttributeEffect("shield_capacity", 2));
|
|
||||||
let effect2 = ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("evasion", 1)));
|
|
||||||
let action1 = ship.actions.addCustom(new ToggleAction("weapon1", { power: 3 }));
|
|
||||||
let action2 = ship.actions.addCustom(new ToggleAction("weapon2", { power: 3 }));
|
|
||||||
ship.actions.toggle(action2, true);
|
|
||||||
|
|
||||||
check.equals(ship.getDeathDiffs(battle), [
|
|
||||||
new ShipEffectRemovedDiff(ship, effect1),
|
|
||||||
new ShipAttributeDiff(ship, "shield_capacity", {}, { cumulative: 2 }),
|
|
||||||
new ShipEffectRemovedDiff(ship, effect2),
|
|
||||||
new ShipAttributeDiff(ship, "evasion", {}, { limit: 1 }),
|
|
||||||
new ShipActionToggleDiff(ship, action2, false),
|
|
||||||
new ShipValueDiff(ship, "hull", -1),
|
|
||||||
new ShipDeathDiff(battle, ship),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
ship.restoreInitialState();
|
||||||
|
|
||||||
|
check.in("after", check => {
|
||||||
|
check.equals(ship.getValue("hull"), 10, "hull");
|
||||||
|
check.equals(ship.getValue("shield"), 20, "shield");
|
||||||
|
check.equals(ship.getValue("power"), 5, "power");
|
||||||
|
check.equals(ship.active_effects.count(), 0, "effects count");
|
||||||
|
check.equals(ship.getAttribute("power_capacity"), 5, "power capacity");
|
||||||
|
check.equals(ship.actions.isToggled(action2), false, "action 2 activation");
|
||||||
|
check.equals(ship.actions.isToggled(action3), false, "action 3 activation");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("lists active effects", check => {
|
||||||
|
let ship = new Ship();
|
||||||
|
check.equals(ship.getEffects(), []);
|
||||||
|
|
||||||
|
let effect1 = new AttributeEffect("evasion", 4);
|
||||||
|
check.patch(ship.model, "getEffects", () => [effect1]);
|
||||||
|
check.equals(ship.getEffects(), [effect1]);
|
||||||
|
|
||||||
|
let effect2 = new AttributeLimitEffect("evasion", 2);
|
||||||
|
ship.active_effects.add(new StickyEffect(effect2, 4));
|
||||||
|
check.equals(ship.getEffects(), [effect1, effect2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("gets a textual description of an attribute", check => {
|
||||||
|
let ship = new Ship();
|
||||||
|
check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering");
|
||||||
|
|
||||||
|
check.patch(ship, "getUpgrades", () => [
|
||||||
|
{ code: "Base", effects: [new AttributeEffect("evasion", 3)] },
|
||||||
|
{ code: "Up1", effects: [new AttributeEffect("shield_capacity", 1)] },
|
||||||
|
{ code: "Up2", effects: [new AttributeEffect("shield_capacity", 1), new AttributeEffect("evasion", 1)] }
|
||||||
|
]);
|
||||||
|
check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1");
|
||||||
|
|
||||||
|
ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("evasion", 3)));
|
||||||
|
check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1\nSticky effect: limit to 3");
|
||||||
|
|
||||||
|
ship.active_effects.remove(ship.active_effects.list()[0]);
|
||||||
|
ship.active_effects.add(new AttributeEffect("evasion", -1));
|
||||||
|
check.equals(ship.getAttributeDescription("evasion"), "Damage points that may be evaded by maneuvering\n\nBase: +3\nUp2: +1\nActive effect: -1");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.case("produces death diffs", check => {
|
||||||
|
let battle = TestTools.createBattle(1);
|
||||||
|
let ship = nn(battle.playing_ship);
|
||||||
|
|
||||||
|
check.equals(ship.getDeathDiffs(battle), [
|
||||||
|
new ShipValueDiff(ship, "hull", -1),
|
||||||
|
new ShipDeathDiff(battle, ship),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let effect1 = ship.active_effects.add(new AttributeEffect("shield_capacity", 2));
|
||||||
|
let effect2 = ship.active_effects.add(new StickyEffect(new AttributeLimitEffect("evasion", 1)));
|
||||||
|
let action1 = ship.actions.addCustom(new ToggleAction("weapon1", { power: 3 }));
|
||||||
|
let action2 = ship.actions.addCustom(new ToggleAction("weapon2", { power: 3 }));
|
||||||
|
ship.actions.toggle(action2, true);
|
||||||
|
|
||||||
|
check.equals(ship.getDeathDiffs(battle), [
|
||||||
|
new ShipEffectRemovedDiff(ship, effect1),
|
||||||
|
new ShipAttributeDiff(ship, "shield_capacity", {}, { cumulative: 2 }),
|
||||||
|
new ShipEffectRemovedDiff(ship, effect2),
|
||||||
|
new ShipAttributeDiff(ship, "evasion", {}, { limit: 1 }),
|
||||||
|
new ShipActionToggleDiff(ship, action2, false),
|
||||||
|
new ShipValueDiff(ship, "hull", -1),
|
||||||
|
new ShipDeathDiff(battle, ship),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
899
src/core/Ship.ts
|
@ -1,441 +1,462 @@
|
||||||
/// <reference path="../common/RObject.ts" />
|
import { RandomGenerator } from "../common/RandomGenerator"
|
||||||
|
import { RObject, RObjectContainer } from "../common/RObject"
|
||||||
module TK.SpaceTac {
|
import { bool, cfilter, flatten, keys, sum } from "../common/Tools"
|
||||||
/**
|
import { ActionList } from "./actions/ActionList"
|
||||||
* A single ship in a fleet
|
import { BaseAction } from "./actions/BaseAction"
|
||||||
*/
|
import { ToggleAction } from "./actions/ToggleAction"
|
||||||
export class Ship extends RObject {
|
import { ArenaLocationAngle } from "./ArenaLocation"
|
||||||
// Ship model
|
import { Battle } from "./Battle"
|
||||||
model: ShipModel
|
import { BaseBattleDiff } from "./diffs/BaseBattleDiff"
|
||||||
|
import { ShipDeathDiff } from "./diffs/ShipDeathDiff"
|
||||||
// Fleet this ship is a member of
|
import { ShipEffectRemovedDiff } from "./diffs/ShipEffectAddedDiff"
|
||||||
fleet: Fleet
|
import { ShipValueDiff } from "./diffs/ShipValueDiff"
|
||||||
|
import { AttributeEffect } from "./effects/AttributeEffect"
|
||||||
// Level of this ship
|
import { AttributeLimitEffect } from "./effects/AttributeLimitEffect"
|
||||||
level = new ShipLevel()
|
import { AttributeMultiplyEffect } from "./effects/AttributeMultiplyEffect"
|
||||||
|
import { BaseEffect } from "./effects/BaseEffect"
|
||||||
// Name of the ship, null if unimportant
|
import { StickyEffect } from "./effects/StickyEffect"
|
||||||
name: string | null
|
import { Fleet } from "./Fleet"
|
||||||
|
import { ShipModel, ShipUpgrade } from "./models/ShipModel"
|
||||||
// Flag indicating if the ship is alive
|
import { Personality } from "./Personality"
|
||||||
alive: boolean
|
import { Player } from "./Player"
|
||||||
|
import { ShipLevel } from "./ShipLevel"
|
||||||
// Flag indicating that the ship is mission critical (escorted ship)
|
import { ShipAttributes, ShipValues, SHIP_VALUES, SHIP_VALUES_DESCRIPTIONS } from "./ShipValue"
|
||||||
critical = false
|
import { Target } from "./Target"
|
||||||
|
|
||||||
// Position in the arena
|
/**
|
||||||
arena_x: number
|
* A single ship in a fleet
|
||||||
arena_y: number
|
*/
|
||||||
|
export class Ship extends RObject {
|
||||||
// Facing direction in the arena
|
// Ship model
|
||||||
arena_angle: number
|
model: ShipModel
|
||||||
|
|
||||||
// Available actions
|
// Fleet this ship is a member of
|
||||||
actions = new ActionList()
|
fleet: Fleet
|
||||||
|
|
||||||
// Active effects (sticky, self or area)
|
// Level of this ship
|
||||||
active_effects = new RObjectContainer<BaseEffect>()
|
level = new ShipLevel()
|
||||||
|
|
||||||
// Ship attributes
|
// Name of the ship, null if unimportant
|
||||||
attributes = new ShipAttributes()
|
name: string | null
|
||||||
|
|
||||||
// Ship values
|
// Flag indicating if the ship is alive
|
||||||
values = new ShipValues()
|
alive: boolean
|
||||||
|
|
||||||
// Personality
|
// Flag indicating that the ship is mission critical (escorted ship)
|
||||||
personality = new Personality()
|
critical = false
|
||||||
|
|
||||||
// Boolean set to true if the ship is currently playing its turn
|
// Position in the arena
|
||||||
playing = false
|
arena_x: number
|
||||||
|
arena_y: number
|
||||||
// Priority in current battle's play_order (used as sort key)
|
|
||||||
play_priority = 0
|
// Facing direction in the arena
|
||||||
|
arena_angle: number
|
||||||
// Create a new ship inside a fleet
|
|
||||||
constructor(fleet: Fleet | null = null, name: string | null = null, model = new ShipModel()) {
|
// Available actions
|
||||||
super();
|
actions = new ActionList()
|
||||||
|
|
||||||
this.fleet = fleet || new Fleet();
|
// Active effects (sticky, self or area)
|
||||||
this.name = name;
|
active_effects = new RObjectContainer<BaseEffect>()
|
||||||
this.alive = true;
|
|
||||||
|
// Ship attributes
|
||||||
this.arena_x = 0;
|
attributes = new ShipAttributes()
|
||||||
this.arena_y = 0;
|
|
||||||
this.arena_angle = 0;
|
// Ship values
|
||||||
|
values = new ShipValues()
|
||||||
this.model = model;
|
|
||||||
|
// Personality
|
||||||
this.updateAttributes();
|
personality = new Personality()
|
||||||
this.actions.updateFromShip(this);
|
|
||||||
|
// Boolean set to true if the ship is currently playing its turn
|
||||||
this.fleet.addShip(this);
|
playing = false
|
||||||
}
|
|
||||||
|
// Priority in current battle's play_order (used as sort key)
|
||||||
/**
|
play_priority = 0
|
||||||
* Return the current location and angle of this ship
|
|
||||||
*/
|
// Create a new ship inside a fleet
|
||||||
get location(): ArenaLocationAngle {
|
constructor(fleet: Fleet | null = null, name: string | null = null, model = new ShipModel()) {
|
||||||
return new ArenaLocationAngle(this.arena_x, this.arena_y, this.arena_angle);
|
super();
|
||||||
}
|
|
||||||
|
this.fleet = fleet || new Fleet();
|
||||||
/**
|
this.name = name;
|
||||||
* Returns the name of this ship
|
this.alive = true;
|
||||||
*/
|
|
||||||
getName(level = true): string {
|
this.arena_x = 0;
|
||||||
let name = this.name || this.model.name;
|
this.arena_y = 0;
|
||||||
return level ? `Level ${this.level.get()} ${name}` : name;
|
this.arena_angle = 0;
|
||||||
}
|
|
||||||
|
this.model = model;
|
||||||
// Returns true if the ship is able to play
|
|
||||||
// If *check_ap* is true, ap_current=0 will make this function return false
|
this.updateAttributes();
|
||||||
isAbleToPlay(check_ap: boolean = true): boolean {
|
this.actions.updateFromShip(this);
|
||||||
var ap_checked = !check_ap || this.getValue("power") > 0;
|
|
||||||
return this.alive && ap_checked;
|
this.fleet.addShip(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set position in the arena
|
/**
|
||||||
// This does not consumes action points
|
* Return the current location and angle of this ship
|
||||||
setArenaPosition(x: number, y: number) {
|
*/
|
||||||
this.arena_x = x;
|
get location(): ArenaLocationAngle {
|
||||||
this.arena_y = y;
|
return new ArenaLocationAngle(this.arena_x, this.arena_y, this.arena_angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set facing angle in the arena
|
/**
|
||||||
setArenaFacingAngle(angle: number) {
|
* Returns the name of this ship
|
||||||
this.arena_angle = angle;
|
*/
|
||||||
}
|
getName(level = true): string {
|
||||||
|
let name = this.name || this.model.name;
|
||||||
// String repr
|
return level ? `Level ${this.level.get()} ${name}` : name;
|
||||||
jasmineToString(): string {
|
}
|
||||||
return this.getName();
|
|
||||||
}
|
// Returns true if the ship is able to play
|
||||||
|
// If *check_ap* is true, ap_current=0 will make this function return false
|
||||||
// Make an initiative throw, to resolve play order in a battle
|
isAbleToPlay(check_ap: boolean = true): boolean {
|
||||||
throwInitiative(gen: RandomGenerator): void {
|
var ap_checked = !check_ap || this.getValue("power") > 0;
|
||||||
this.play_priority = gen.random() * this.attributes.initiative.get();
|
return this.alive && ap_checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Set position in the arena
|
||||||
* Return the player that plays this ship
|
// This does not consumes action points
|
||||||
*/
|
setArenaPosition(x: number, y: number) {
|
||||||
getPlayer(): Player {
|
this.arena_x = x;
|
||||||
return this.fleet.player;
|
this.arena_y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Set facing angle in the arena
|
||||||
* Check if a player is playing this ship
|
setArenaFacingAngle(angle: number) {
|
||||||
*/
|
this.arena_angle = angle;
|
||||||
isPlayedBy(player: Player): boolean {
|
}
|
||||||
return player.is(this.fleet.player);
|
|
||||||
}
|
// String repr
|
||||||
|
jasmineToString(): string {
|
||||||
/**
|
return this.getName();
|
||||||
* Get the battle this ship is currently engaged in
|
}
|
||||||
*/
|
|
||||||
getBattle(): Battle | null {
|
// Make an initiative throw, to resolve play order in a battle
|
||||||
return this.fleet.battle;
|
throwInitiative(gen: RandomGenerator): void {
|
||||||
}
|
this.play_priority = gen.random() * this.attributes.initiative.get();
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Get the list of activated upgrades
|
/**
|
||||||
*/
|
* Return the player that plays this ship
|
||||||
getUpgrades(): ShipUpgrade[] {
|
*/
|
||||||
return this.model.getActivatedUpgrades(this.level.get(), this.level.getUpgrades());
|
getPlayer(): Player {
|
||||||
}
|
return this.fleet.player;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Refresh the actions and attributes from the bound model
|
/**
|
||||||
*/
|
* Check if a player is playing this ship
|
||||||
refreshFromModel(): void {
|
*/
|
||||||
this.updateAttributes();
|
isPlayedBy(player: Player): boolean {
|
||||||
this.actions.updateFromShip(this);
|
return player.is(this.fleet.player);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the ship model
|
* Get the battle this ship is currently engaged in
|
||||||
*/
|
*/
|
||||||
setModel(model: ShipModel): void {
|
getBattle(): Battle | null {
|
||||||
this.model = model;
|
return this.fleet.battle;
|
||||||
this.level.clearUpgrades();
|
}
|
||||||
this.refreshFromModel();
|
|
||||||
}
|
/**
|
||||||
|
* Get the list of activated upgrades
|
||||||
/**
|
*/
|
||||||
* Toggle an upgrade
|
getUpgrades(): ShipUpgrade[] {
|
||||||
*/
|
return this.model.getActivatedUpgrades(this.level.get(), this.level.getUpgrades());
|
||||||
activateUpgrade(upgrade: ShipUpgrade, on: boolean): void {
|
}
|
||||||
if (on && (upgrade.cost || 0) > this.getAvailableUpgradePoints()) {
|
|
||||||
return;
|
/**
|
||||||
}
|
* Refresh the actions and attributes from the bound model
|
||||||
this.level.activateUpgrade(upgrade, on);
|
*/
|
||||||
this.refreshFromModel();
|
refreshFromModel(): void {
|
||||||
}
|
this.updateAttributes();
|
||||||
|
this.actions.updateFromShip(this);
|
||||||
/**
|
}
|
||||||
* Get the number of upgrade points available
|
|
||||||
*/
|
/**
|
||||||
getAvailableUpgradePoints(): number {
|
* Change the ship model
|
||||||
let upgrades = this.getUpgrades();
|
*/
|
||||||
return this.level.getUpgradePoints() - sum(upgrades.map(upgrade => upgrade.cost || 0));
|
setModel(model: ShipModel): void {
|
||||||
}
|
this.model = model;
|
||||||
|
this.level.clearUpgrades();
|
||||||
/**
|
this.refreshFromModel();
|
||||||
* Add an event to the battle log, if any
|
}
|
||||||
*/
|
|
||||||
addBattleEvent(event: BaseBattleDiff): void {
|
/**
|
||||||
var battle = this.getBattle();
|
* Toggle an upgrade
|
||||||
if (battle && battle.log) {
|
*/
|
||||||
battle.log.add(event);
|
activateUpgrade(upgrade: ShipUpgrade, on: boolean): void {
|
||||||
}
|
if (on && (upgrade.cost || 0) > this.getAvailableUpgradePoints()) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a ship value
|
|
||||||
*/
|
|
||||||
getValue(name: keyof ShipValues): number {
|
|
||||||
return this.values[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a ship value
|
|
||||||
*/
|
|
||||||
setValue(name: keyof ShipValues, value: number, relative = false): void {
|
|
||||||
if (relative) {
|
|
||||||
value += this.values[name];
|
|
||||||
}
|
|
||||||
this.values[name] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a ship attribute's current value
|
|
||||||
*/
|
|
||||||
getAttribute(name: keyof ShipAttributes): number {
|
|
||||||
if (!this.attributes.hasOwnProperty(name)) {
|
|
||||||
console.error(`No such ship attribute: ${name}`);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return this.attributes[name].get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the action points counter
|
|
||||||
* This should be called once at the start of a battle
|
|
||||||
* If no value is provided, the attribute power_capacity will be used
|
|
||||||
*/
|
|
||||||
private initializePower(value: number | null = null): void {
|
|
||||||
if (value === null) {
|
|
||||||
value = this.getAttribute("power_capacity");
|
|
||||||
}
|
|
||||||
this.setValue("power", value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called at the start of battle, to restore a pristine condition on the ship
|
|
||||||
*/
|
|
||||||
restoreInitialState() {
|
|
||||||
this.alive = true;
|
|
||||||
|
|
||||||
this.actions.updateFromShip(this);
|
|
||||||
this.active_effects = new RObjectContainer();
|
|
||||||
|
|
||||||
this.updateAttributes();
|
|
||||||
this.restoreHealth();
|
|
||||||
this.initializePower();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the ship is inside a given circular area
|
|
||||||
*/
|
|
||||||
isInCircle(x: number, y: number, radius: number): boolean {
|
|
||||||
let dx = this.arena_x - x;
|
|
||||||
let dy = this.arena_y - y;
|
|
||||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
return distance <= radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the distance to another ship
|
|
||||||
*/
|
|
||||||
getDistanceTo(other: Ship): number {
|
|
||||||
return Target.newFromShip(this).getDistanceTo(Target.newFromShip(other));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the diffs needed to apply changes to a ship value
|
|
||||||
*/
|
|
||||||
getValueDiffs(name: keyof ShipValues, value: number, relative = false): BaseBattleDiff[] {
|
|
||||||
let result: BaseBattleDiff[] = [];
|
|
||||||
let current = this.values[name];
|
|
||||||
|
|
||||||
if (relative) {
|
|
||||||
value += current;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO apply range limitations
|
|
||||||
|
|
||||||
if (current != value) {
|
|
||||||
result.push(new ShipValueDiff(this, name, value - current));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Produce diffs needed to put the ship in emergency stasis
|
|
||||||
*/
|
|
||||||
getDeathDiffs(battle: Battle): BaseBattleDiff[] {
|
|
||||||
let result: BaseBattleDiff[] = [];
|
|
||||||
|
|
||||||
// Remove active effects
|
|
||||||
this.active_effects.list().forEach(effect => {
|
|
||||||
if (!(effect instanceof StickyEffect)) {
|
|
||||||
result.push(new ShipEffectRemovedDiff(this, effect));
|
|
||||||
}
|
|
||||||
result = result.concat(effect.getOffDiffs(this));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Deactivate toggle actions
|
|
||||||
this.getToggleActions(true).forEach(action => {
|
|
||||||
result = result.concat(action.getSpecificDiffs(this, battle, Target.newFromShip(this)));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Put all values to 0
|
|
||||||
keys(SHIP_VALUES).forEach(value => {
|
|
||||||
result = result.concat(this.getValueDiffs(value, 0));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mark as dead
|
|
||||||
result.push(new ShipDeathDiff(battle, this));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the death status on this ship
|
|
||||||
*/
|
|
||||||
setDead(): void {
|
|
||||||
let battle = this.getBattle();
|
|
||||||
if (battle) {
|
|
||||||
let events = this.getDeathDiffs(battle);
|
|
||||||
battle.applyDiffs(events);
|
|
||||||
} else {
|
|
||||||
console.error("Cannot set ship dead outside of battle", this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update attributes, taking into account model's permanent effects and active effects
|
|
||||||
*/
|
|
||||||
updateAttributes(): void {
|
|
||||||
// Reset attributes
|
|
||||||
keys(this.attributes).forEach(attr => this.attributes[attr].reset());
|
|
||||||
|
|
||||||
// Apply attribute effects
|
|
||||||
this.getEffects().forEach(effect => {
|
|
||||||
if (effect instanceof AttributeEffect) {
|
|
||||||
this.attributes[effect.attrcode].addModifier(effect.value);
|
|
||||||
} else if (effect instanceof AttributeMultiplyEffect) {
|
|
||||||
this.attributes[effect.attrcode].addModifier(undefined, effect.value);
|
|
||||||
} else if (effect instanceof AttributeLimitEffect) {
|
|
||||||
this.attributes[effect.attrcode].addModifier(undefined, undefined, effect.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fully restore hull and shield, at their maximal capacity
|
|
||||||
*/
|
|
||||||
restoreHealth(): void {
|
|
||||||
if (this.alive) {
|
|
||||||
this.setValue("hull", this.getAttribute("hull_capacity"));
|
|
||||||
this.setValue("shield", this.getAttribute("shield_capacity"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get actions from the ship model
|
|
||||||
*/
|
|
||||||
getModelActions(): BaseAction[] {
|
|
||||||
return this.model.getActions(this.level.get(), this.level.getUpgrades());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get permanent effects from the ship model
|
|
||||||
*/
|
|
||||||
getModelEffects(): BaseEffect[] {
|
|
||||||
return this.model.getEffects(this.level.get(), this.level.getUpgrades());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator over all effects active for this ship.
|
|
||||||
*
|
|
||||||
* This combines the permanent effects from ship model, with sticky and area effects.
|
|
||||||
*/
|
|
||||||
getEffects(): BaseEffect[] {
|
|
||||||
return this.getModelEffects().concat(
|
|
||||||
this.active_effects.list().map(effect => (effect instanceof StickyEffect) ? effect.base : effect)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterator over toggle actions
|
|
||||||
*/
|
|
||||||
getToggleActions(only_active = false): ToggleAction[] {
|
|
||||||
let result = cfilter(this.actions.listAll(), ToggleAction);
|
|
||||||
if (only_active) {
|
|
||||||
result = result.filter(action => this.actions.isToggled(action));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the effects that this ship has on another ship (which may be herself)
|
|
||||||
*/
|
|
||||||
getAreaEffects(ship: Ship): BaseEffect[] {
|
|
||||||
let toggled = this.getToggleActions(true);
|
|
||||||
let effects = toggled.map(action => {
|
|
||||||
if (bool(action.filterImpactedShips(this, this.location, Target.newFromShip(ship), [ship]))) {
|
|
||||||
return action.effects;
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return flatten(effects);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a textual description of an attribute, and the origin of its value
|
|
||||||
*/
|
|
||||||
getAttributeDescription(attribute: keyof ShipAttributes): string {
|
|
||||||
let result = SHIP_VALUES_DESCRIPTIONS[attribute];
|
|
||||||
|
|
||||||
let diffs: string[] = [];
|
|
||||||
let limits: string[] = [];
|
|
||||||
|
|
||||||
function addEffect(base: string, effect: BaseEffect) {
|
|
||||||
if (effect instanceof AttributeEffect && effect.attrcode == attribute) {
|
|
||||||
diffs.push(`${base}: ${effect.value > 0 ? "+" + effect.value.toString() : effect.value}`);
|
|
||||||
} else if (effect instanceof AttributeLimitEffect && effect.attrcode == attribute) {
|
|
||||||
limits.push(`${base}: limit to ${effect.value}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.getUpgrades().forEach(upgrade => {
|
|
||||||
if (upgrade.effects) {
|
|
||||||
upgrade.effects.forEach(effect => addEffect(upgrade.code, effect));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.active_effects.list().forEach(effect => {
|
|
||||||
if (effect instanceof StickyEffect) {
|
|
||||||
addEffect("Sticky effect", effect.base);
|
|
||||||
} else {
|
|
||||||
addEffect("Active effect", effect);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let sources = diffs.concat(limits).join("\n");
|
|
||||||
return sources ? (result + "\n\n" + sources) : result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.level.activateUpgrade(upgrade, on);
|
||||||
|
this.refreshFromModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of upgrade points available
|
||||||
|
*/
|
||||||
|
getAvailableUpgradePoints(): number {
|
||||||
|
let upgrades = this.getUpgrades();
|
||||||
|
return this.level.getUpgradePoints() - sum(upgrades.map(upgrade => upgrade.cost || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an event to the battle log, if any
|
||||||
|
*/
|
||||||
|
addBattleEvent(event: BaseBattleDiff): void {
|
||||||
|
var battle = this.getBattle();
|
||||||
|
if (battle && battle.log) {
|
||||||
|
battle.log.add(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a ship value
|
||||||
|
*/
|
||||||
|
getValue(name: keyof ShipValues): number {
|
||||||
|
return this.values[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a ship value
|
||||||
|
*/
|
||||||
|
setValue(name: keyof ShipValues, value: number, relative = false): void {
|
||||||
|
if (relative) {
|
||||||
|
value += this.values[name];
|
||||||
|
}
|
||||||
|
this.values[name] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a ship attribute's current value
|
||||||
|
*/
|
||||||
|
getAttribute(name: keyof ShipAttributes): number {
|
||||||
|
if (!this.attributes.hasOwnProperty(name)) {
|
||||||
|
console.error(`No such ship attribute: ${name}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.attributes[name].get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the action points counter
|
||||||
|
* This should be called once at the start of a battle
|
||||||
|
* If no value is provided, the attribute power_capacity will be used
|
||||||
|
*/
|
||||||
|
private initializePower(value: number | null = null): void {
|
||||||
|
if (value === null) {
|
||||||
|
value = this.getAttribute("power_capacity");
|
||||||
|
}
|
||||||
|
this.setValue("power", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called at the start of battle, to restore a pristine condition on the ship
|
||||||
|
*/
|
||||||
|
restoreInitialState() {
|
||||||
|
this.alive = true;
|
||||||
|
|
||||||
|
this.actions.updateFromShip(this);
|
||||||
|
this.active_effects = new RObjectContainer();
|
||||||
|
|
||||||
|
this.updateAttributes();
|
||||||
|
this.restoreHealth();
|
||||||
|
this.initializePower();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the ship is inside a given circular area
|
||||||
|
*/
|
||||||
|
isInCircle(x: number, y: number, radius: number): boolean {
|
||||||
|
let dx = this.arena_x - x;
|
||||||
|
let dy = this.arena_y - y;
|
||||||
|
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
return distance <= radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the distance to another ship
|
||||||
|
*/
|
||||||
|
getDistanceTo(other: Ship): number {
|
||||||
|
return Target.newFromShip(this).getDistanceTo(Target.newFromShip(other));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the diffs needed to apply changes to a ship value
|
||||||
|
*/
|
||||||
|
getValueDiffs(name: keyof ShipValues, value: number, relative = false): BaseBattleDiff[] {
|
||||||
|
let result: BaseBattleDiff[] = [];
|
||||||
|
let current = this.values[name];
|
||||||
|
|
||||||
|
if (relative) {
|
||||||
|
value += current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO apply range limitations
|
||||||
|
|
||||||
|
if (current != value) {
|
||||||
|
result.push(new ShipValueDiff(this, name, value - current));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produce diffs needed to put the ship in emergency stasis
|
||||||
|
*/
|
||||||
|
getDeathDiffs(battle: Battle): BaseBattleDiff[] {
|
||||||
|
let result: BaseBattleDiff[] = [];
|
||||||
|
|
||||||
|
// Remove active effects
|
||||||
|
this.active_effects.list().forEach(effect => {
|
||||||
|
if (!(effect instanceof StickyEffect)) {
|
||||||
|
result.push(new ShipEffectRemovedDiff(this, effect));
|
||||||
|
}
|
||||||
|
result = result.concat(effect.getOffDiffs(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deactivate toggle actions
|
||||||
|
this.getToggleActions(true).forEach(action => {
|
||||||
|
result = result.concat(action.getSpecificDiffs(this, battle, Target.newFromShip(this)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Put all values to 0
|
||||||
|
keys(SHIP_VALUES).forEach(value => {
|
||||||
|
result = result.concat(this.getValueDiffs(value, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark as dead
|
||||||
|
result.push(new ShipDeathDiff(battle, this));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the death status on this ship
|
||||||
|
*/
|
||||||
|
setDead(): void {
|
||||||
|
let battle = this.getBattle();
|
||||||
|
if (battle) {
|
||||||
|
let events = this.getDeathDiffs(battle);
|
||||||
|
battle.applyDiffs(events);
|
||||||
|
} else {
|
||||||
|
console.error("Cannot set ship dead outside of battle", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update attributes, taking into account model's permanent effects and active effects
|
||||||
|
*/
|
||||||
|
updateAttributes(): void {
|
||||||
|
// Reset attributes
|
||||||
|
keys(this.attributes).forEach(attr => this.attributes[attr].reset());
|
||||||
|
|
||||||
|
// Apply attribute effects
|
||||||
|
this.getEffects().forEach(effect => {
|
||||||
|
if (effect instanceof AttributeEffect) {
|
||||||
|
this.attributes[effect.attrcode].addModifier(effect.value);
|
||||||
|
} else if (effect instanceof AttributeMultiplyEffect) {
|
||||||
|
this.attributes[effect.attrcode].addModifier(undefined, effect.value);
|
||||||
|
} else if (effect instanceof AttributeLimitEffect) {
|
||||||
|
this.attributes[effect.attrcode].addModifier(undefined, undefined, effect.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully restore hull and shield, at their maximal capacity
|
||||||
|
*/
|
||||||
|
restoreHealth(): void {
|
||||||
|
if (this.alive) {
|
||||||
|
this.setValue("hull", this.getAttribute("hull_capacity"));
|
||||||
|
this.setValue("shield", this.getAttribute("shield_capacity"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get actions from the ship model
|
||||||
|
*/
|
||||||
|
getModelActions(): BaseAction[] {
|
||||||
|
return this.model.getActions(this.level.get(), this.level.getUpgrades());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get permanent effects from the ship model
|
||||||
|
*/
|
||||||
|
getModelEffects(): BaseEffect[] {
|
||||||
|
return this.model.getEffects(this.level.get(), this.level.getUpgrades());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator over all effects active for this ship.
|
||||||
|
*
|
||||||
|
* This combines the permanent effects from ship model, with sticky and area effects.
|
||||||
|
*/
|
||||||
|
getEffects(): BaseEffect[] {
|
||||||
|
return this.getModelEffects().concat(
|
||||||
|
this.active_effects.list().map(effect => (effect instanceof StickyEffect) ? effect.base : effect)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator over toggle actions
|
||||||
|
*/
|
||||||
|
getToggleActions(only_active = false): ToggleAction[] {
|
||||||
|
let result = cfilter(this.actions.listAll(), ToggleAction);
|
||||||
|
if (only_active) {
|
||||||
|
result = result.filter(action => this.actions.isToggled(action));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the effects that this ship has on another ship (which may be herself)
|
||||||
|
*/
|
||||||
|
getAreaEffects(ship: Ship): BaseEffect[] {
|
||||||
|
let toggled = this.getToggleActions(true);
|
||||||
|
let effects = toggled.map(action => {
|
||||||
|
if (bool(action.filterImpactedShips(this, this.location, Target.newFromShip(ship), [ship]))) {
|
||||||
|
return action.effects;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return flatten(effects);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a textual description of an attribute, and the origin of its value
|
||||||
|
*/
|
||||||
|
getAttributeDescription(attribute: keyof ShipAttributes): string {
|
||||||
|
let result = SHIP_VALUES_DESCRIPTIONS[attribute];
|
||||||
|
|
||||||
|
let diffs: string[] = [];
|
||||||
|
let limits: string[] = [];
|
||||||
|
|
||||||
|
function addEffect(base: string, effect: BaseEffect) {
|
||||||
|
if (effect instanceof AttributeEffect && effect.attrcode == attribute) {
|
||||||
|
diffs.push(`${base}: ${effect.value > 0 ? "+" + effect.value.toString() : effect.value}`);
|
||||||
|
} else if (effect instanceof AttributeLimitEffect && effect.attrcode == attribute) {
|
||||||
|
limits.push(`${base}: limit to ${effect.value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getUpgrades().forEach(upgrade => {
|
||||||
|
if (upgrade.effects) {
|
||||||
|
upgrade.effects.forEach(effect => addEffect(upgrade.code, effect));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.active_effects.list().forEach(effect => {
|
||||||
|
if (effect instanceof StickyEffect) {
|
||||||
|
addEffect("Sticky effect", effect.base);
|
||||||
|
} else {
|
||||||
|
addEffect("Active effect", effect);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let sources = diffs.concat(limits).join("\n");
|
||||||
|
return sources ? (result + "\n\n" + sources) : result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("ShipGenerator", test => {
|
import { ShipModel } from "./models/ShipModel";
|
||||||
test.case("can use ship model", check => {
|
import { ShipGenerator } from "./ShipGenerator";
|
||||||
var gen = new ShipGenerator();
|
|
||||||
var model = new ShipModel("test", "Test");
|
testing("ShipGenerator", test => {
|
||||||
var ship = gen.generate(3, model, false);
|
test.case("can use ship model", check => {
|
||||||
check.same(ship.model, model);
|
var gen = new ShipGenerator();
|
||||||
check.same(ship.level.get(), 3);
|
var model = new ShipModel("test", "Test");
|
||||||
});
|
var ship = gen.generate(3, model, false);
|
||||||
});
|
check.same(ship.model, model);
|
||||||
}
|
check.same(ship.level.get(), 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,49 +1,51 @@
|
||||||
module TK.SpaceTac {
|
import { RandomGenerator } from "../common/RandomGenerator";
|
||||||
/**
|
import { ShipModel } from "./models/ShipModel";
|
||||||
* Generator of random ship
|
import { Ship } from "./Ship";
|
||||||
*/
|
|
||||||
export class ShipGenerator {
|
|
||||||
// Random number generator used
|
|
||||||
random: RandomGenerator
|
|
||||||
|
|
||||||
constructor(random = RandomGenerator.global) {
|
/**
|
||||||
this.random = random;
|
* Generator of random ship
|
||||||
}
|
*/
|
||||||
|
export class ShipGenerator {
|
||||||
|
// Random number generator used
|
||||||
|
random: RandomGenerator
|
||||||
|
|
||||||
/**
|
constructor(random = RandomGenerator.global) {
|
||||||
* Generate a ship of a givel level.
|
this.random = random;
|
||||||
*
|
}
|
||||||
* If *upgrade* is true, random levelling options will be chosen
|
|
||||||
*/
|
|
||||||
generate(level: number, model: ShipModel | null = null, upgrade = true): Ship {
|
|
||||||
if (!model) {
|
|
||||||
// Get a random model
|
|
||||||
model = ShipModel.getRandomModel(level, this.random);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = new Ship(null, null, model);
|
/**
|
||||||
|
* Generate a ship of a givel level.
|
||||||
result.level.forceLevel(level);
|
*
|
||||||
if (upgrade) {
|
* If *upgrade* is true, random levelling options will be chosen
|
||||||
let iteration = 0;
|
*/
|
||||||
while (iteration < 100) {
|
generate(level: number, model: ShipModel | null = null, upgrade = true): Ship {
|
||||||
iteration += 1;
|
if (!model) {
|
||||||
|
// Get a random model
|
||||||
let points = result.getAvailableUpgradePoints();
|
model = ShipModel.getRandomModel(level, this.random);
|
||||||
let upgrades = model.getAvailableUpgrades(result.level.get()).filter(upgrade => {
|
|
||||||
return (upgrade.cost || 0) <= points && !result.level.hasUpgrade(upgrade);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (upgrades.length > 0) {
|
|
||||||
let upgrade = this.random.choice(upgrades);
|
|
||||||
result.activateUpgrade(upgrade, true);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let result = new Ship(null, null, model);
|
||||||
|
|
||||||
|
result.level.forceLevel(level);
|
||||||
|
if (upgrade) {
|
||||||
|
let iteration = 0;
|
||||||
|
while (iteration < 100) {
|
||||||
|
iteration += 1;
|
||||||
|
|
||||||
|
let points = result.getAvailableUpgradePoints();
|
||||||
|
let upgrades = model.getAvailableUpgrades(result.level.get()).filter(upgrade => {
|
||||||
|
return (upgrade.cost || 0) <= points && !result.level.hasUpgrade(upgrade);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (upgrades.length > 0) {
|
||||||
|
let upgrade = this.random.choice(upgrades);
|
||||||
|
result.activateUpgrade(upgrade, true);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,71 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("ShipLevel", test => {
|
import { ShipLevel } from "./ShipLevel";
|
||||||
test.case("level up from experience points", check => {
|
|
||||||
let level = new ShipLevel();
|
|
||||||
check.equals(level.get(), 1);
|
|
||||||
check.equals(level.getNextGoal(), 100);
|
|
||||||
check.equals(level.getUpgradePoints(), 0);
|
|
||||||
|
|
||||||
level.addExperience(60); // 60
|
testing("ShipLevel", test => {
|
||||||
check.equals(level.get(), 1);
|
test.case("level up from experience points", check => {
|
||||||
check.equals(level.checkLevelUp(), false);
|
let level = new ShipLevel();
|
||||||
|
check.equals(level.get(), 1);
|
||||||
|
check.equals(level.getNextGoal(), 100);
|
||||||
|
check.equals(level.getUpgradePoints(), 0);
|
||||||
|
|
||||||
level.addExperience(70); // 130
|
level.addExperience(60); // 60
|
||||||
check.equals(level.get(), 1);
|
check.equals(level.get(), 1);
|
||||||
check.equals(level.checkLevelUp(), true);
|
check.equals(level.checkLevelUp(), false);
|
||||||
check.equals(level.get(), 2);
|
|
||||||
check.equals(level.getNextGoal(), 300);
|
|
||||||
check.equals(level.getUpgradePoints(), 3);
|
|
||||||
|
|
||||||
level.addExperience(200); // 330
|
level.addExperience(70); // 130
|
||||||
check.equals(level.get(), 2);
|
check.equals(level.get(), 1);
|
||||||
check.equals(level.checkLevelUp(), true);
|
check.equals(level.checkLevelUp(), true);
|
||||||
check.equals(level.get(), 3);
|
check.equals(level.get(), 2);
|
||||||
check.equals(level.getNextGoal(), 600);
|
check.equals(level.getNextGoal(), 300);
|
||||||
check.equals(level.getUpgradePoints(), 5);
|
check.equals(level.getUpgradePoints(), 3);
|
||||||
|
|
||||||
level.addExperience(320); // 650
|
level.addExperience(200); // 330
|
||||||
check.equals(level.get(), 3);
|
check.equals(level.get(), 2);
|
||||||
check.equals(level.checkLevelUp(), true);
|
check.equals(level.checkLevelUp(), true);
|
||||||
check.equals(level.get(), 4);
|
check.equals(level.get(), 3);
|
||||||
check.equals(level.getNextGoal(), 1000);
|
check.equals(level.getNextGoal(), 600);
|
||||||
check.equals(level.getUpgradePoints(), 7);
|
check.equals(level.getUpgradePoints(), 5);
|
||||||
});
|
|
||||||
|
|
||||||
test.case("forces a given level", check => {
|
level.addExperience(320); // 650
|
||||||
let level = new ShipLevel();
|
check.equals(level.get(), 3);
|
||||||
check.equals(level.get(), 1);
|
check.equals(level.checkLevelUp(), true);
|
||||||
|
check.equals(level.get(), 4);
|
||||||
|
check.equals(level.getNextGoal(), 1000);
|
||||||
|
check.equals(level.getUpgradePoints(), 7);
|
||||||
|
});
|
||||||
|
|
||||||
level.forceLevel(10);
|
test.case("forces a given level", check => {
|
||||||
check.equals(level.get(), 10);
|
let level = new ShipLevel();
|
||||||
});
|
check.equals(level.get(), 1);
|
||||||
|
|
||||||
test.case("manages upgrades", check => {
|
level.forceLevel(10);
|
||||||
let up1 = { code: "test1" };
|
check.equals(level.get(), 10);
|
||||||
let up2 = { code: "test2" };
|
});
|
||||||
|
|
||||||
let level = new ShipLevel();
|
test.case("manages upgrades", check => {
|
||||||
check.equals(level.getUpgrades(), []);
|
let up1 = { code: "test1" };
|
||||||
check.equals(level.hasUpgrade(up1), false);
|
let up2 = { code: "test2" };
|
||||||
|
|
||||||
level.activateUpgrade(up1, true);
|
let level = new ShipLevel();
|
||||||
check.equals(level.getUpgrades(), ["test1"]);
|
check.equals(level.getUpgrades(), []);
|
||||||
check.equals(level.hasUpgrade(up1), true);
|
check.equals(level.hasUpgrade(up1), false);
|
||||||
|
|
||||||
level.activateUpgrade(up1, true);
|
level.activateUpgrade(up1, true);
|
||||||
check.equals(level.getUpgrades(), ["test1"]);
|
check.equals(level.getUpgrades(), ["test1"]);
|
||||||
check.equals(level.hasUpgrade(up1), true);
|
check.equals(level.hasUpgrade(up1), true);
|
||||||
|
|
||||||
level.activateUpgrade(up1, false);
|
level.activateUpgrade(up1, true);
|
||||||
check.equals(level.getUpgrades(), []);
|
check.equals(level.getUpgrades(), ["test1"]);
|
||||||
check.equals(level.hasUpgrade(up1), false);
|
check.equals(level.hasUpgrade(up1), true);
|
||||||
|
|
||||||
level.activateUpgrade(up1, true);
|
level.activateUpgrade(up1, false);
|
||||||
level.activateUpgrade(up2, true);
|
check.equals(level.getUpgrades(), []);
|
||||||
check.equals(level.getUpgrades(), ["test1", "test2"]);
|
check.equals(level.hasUpgrade(up1), false);
|
||||||
level.clearUpgrades();
|
|
||||||
check.equals(level.getUpgrades(), []);
|
level.activateUpgrade(up1, true);
|
||||||
});
|
level.activateUpgrade(up2, true);
|
||||||
});
|
check.equals(level.getUpgrades(), ["test1", "test2"]);
|
||||||
}
|
level.clearUpgrades();
|
||||||
|
check.equals(level.getUpgrades(), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,119 +1,121 @@
|
||||||
module TK.SpaceTac {
|
import { imap, irange, isum } from "../common/Iterators";
|
||||||
/**
|
import { acopy, add, contains, remove } from "../common/Tools";
|
||||||
* Level and experience system for a ship, with enabled upgrades.
|
import { ShipUpgrade } from "./models/ShipModel";
|
||||||
*/
|
|
||||||
export class ShipLevel {
|
|
||||||
private level = 1
|
|
||||||
private experience = 0
|
|
||||||
private upgrades: string[] = []
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current level
|
* Level and experience system for a ship, with enabled upgrades.
|
||||||
*/
|
*/
|
||||||
get(): number {
|
export class ShipLevel {
|
||||||
return this.level;
|
private level = 1
|
||||||
}
|
private experience = 0
|
||||||
|
private upgrades: string[] = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current experience points
|
* Get current level
|
||||||
*/
|
*/
|
||||||
getExperience(): number {
|
get(): number {
|
||||||
return this.experience;
|
return this.level;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the activated upgrades
|
* Get the current experience points
|
||||||
*/
|
*/
|
||||||
getUpgrades(): string[] {
|
getExperience(): number {
|
||||||
return acopy(this.upgrades);
|
return this.experience;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the next experience goal to reach, to gain one level
|
* Get the activated upgrades
|
||||||
*/
|
*/
|
||||||
getNextGoal(): number {
|
getUpgrades(): string[] {
|
||||||
return isum(imap(irange(this.level), i => (i + 1) * 100));
|
return acopy(this.upgrades);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force experience gain, to reach a given level
|
* Get the next experience goal to reach, to gain one level
|
||||||
*/
|
*/
|
||||||
forceLevel(level: number): void {
|
getNextGoal(): number {
|
||||||
while (this.level < level) {
|
return isum(imap(irange(this.level), i => (i + 1) * 100));
|
||||||
this.forceLevelUp();
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force a level up
|
* Force experience gain, to reach a given level
|
||||||
*/
|
*/
|
||||||
forceLevelUp(): void {
|
forceLevel(level: number): void {
|
||||||
let old_level = this.level;
|
while (this.level < level) {
|
||||||
|
this.forceLevelUp();
|
||||||
this.addExperience(this.getNextGoal() - this.experience);
|
|
||||||
this.checkLevelUp();
|
|
||||||
|
|
||||||
if (old_level >= this.level) {
|
|
||||||
// security against infinite loops
|
|
||||||
throw new Error("No effective level up");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for level-up
|
|
||||||
*
|
|
||||||
* Returns true if level changed
|
|
||||||
*/
|
|
||||||
checkLevelUp(): boolean {
|
|
||||||
let changed = false;
|
|
||||||
while (this.experience >= this.getNextGoal()) {
|
|
||||||
this.level++;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add experience points
|
|
||||||
*/
|
|
||||||
addExperience(points: number): void {
|
|
||||||
this.experience += points;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get upgrade points given by current level
|
|
||||||
*
|
|
||||||
* This does not deduce activated upgrades usage
|
|
||||||
*/
|
|
||||||
getUpgradePoints(): number {
|
|
||||||
return this.level > 1 ? (1 + 2 * (this.level - 1)) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (De)Activate an upgrade
|
|
||||||
*
|
|
||||||
* This does not check the upgrade points needed
|
|
||||||
*/
|
|
||||||
activateUpgrade(upgrade: ShipUpgrade, active: boolean): void {
|
|
||||||
if (active) {
|
|
||||||
add(this.upgrades, upgrade.code);
|
|
||||||
} else {
|
|
||||||
remove(this.upgrades, upgrade.code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if an upgrade is active
|
|
||||||
*/
|
|
||||||
hasUpgrade(upgrade: ShipUpgrade): boolean {
|
|
||||||
return contains(this.upgrades, upgrade.code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all activated upgrades
|
|
||||||
*/
|
|
||||||
clearUpgrades(): void {
|
|
||||||
this.upgrades = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force a level up
|
||||||
|
*/
|
||||||
|
forceLevelUp(): void {
|
||||||
|
let old_level = this.level;
|
||||||
|
|
||||||
|
this.addExperience(this.getNextGoal() - this.experience);
|
||||||
|
this.checkLevelUp();
|
||||||
|
|
||||||
|
if (old_level >= this.level) {
|
||||||
|
// security against infinite loops
|
||||||
|
throw new Error("No effective level up");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for level-up
|
||||||
|
*
|
||||||
|
* Returns true if level changed
|
||||||
|
*/
|
||||||
|
checkLevelUp(): boolean {
|
||||||
|
let changed = false;
|
||||||
|
while (this.experience >= this.getNextGoal()) {
|
||||||
|
this.level++;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add experience points
|
||||||
|
*/
|
||||||
|
addExperience(points: number): void {
|
||||||
|
this.experience += points;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upgrade points given by current level
|
||||||
|
*
|
||||||
|
* This does not deduce activated upgrades usage
|
||||||
|
*/
|
||||||
|
getUpgradePoints(): number {
|
||||||
|
return this.level > 1 ? (1 + 2 * (this.level - 1)) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (De)Activate an upgrade
|
||||||
|
*
|
||||||
|
* This does not check the upgrade points needed
|
||||||
|
*/
|
||||||
|
activateUpgrade(upgrade: ShipUpgrade, active: boolean): void {
|
||||||
|
if (active) {
|
||||||
|
add(this.upgrades, upgrade.code);
|
||||||
|
} else {
|
||||||
|
remove(this.upgrades, upgrade.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an upgrade is active
|
||||||
|
*/
|
||||||
|
hasUpgrade(upgrade: ShipUpgrade): boolean {
|
||||||
|
return contains(this.upgrades, upgrade.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all activated upgrades
|
||||||
|
*/
|
||||||
|
clearUpgrades(): void {
|
||||||
|
this.upgrades = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,48 @@
|
||||||
module TK.SpaceTac {
|
import { testing } from "../common/Testing";
|
||||||
testing("ShipAttribute", test => {
|
import { ShipAttribute } from "./ShipValue";
|
||||||
test.case("applies cumulative, multiplier and limit", check => {
|
|
||||||
let attribute = new ShipAttribute();
|
|
||||||
check.equals(attribute.get(), 0, "initial");
|
|
||||||
|
|
||||||
attribute.addModifier(4);
|
testing("ShipAttribute", test => {
|
||||||
check.in("+4", check => {
|
test.case("applies cumulative, multiplier and limit", check => {
|
||||||
check.equals(attribute.get(), 4, "effective value");
|
let attribute = new ShipAttribute();
|
||||||
});
|
check.equals(attribute.get(), 0, "initial");
|
||||||
|
|
||||||
attribute.addModifier(2);
|
attribute.addModifier(4);
|
||||||
check.in("+4 +2", check => {
|
check.in("+4", check => {
|
||||||
check.equals(attribute.get(), 6, "effective value");
|
check.equals(attribute.get(), 4, "effective value");
|
||||||
});
|
|
||||||
|
|
||||||
attribute.addModifier(undefined, 20);
|
|
||||||
check.in("+4 +2 +20%", check => {
|
|
||||||
check.equals(attribute.get(), 7, "effective value");
|
|
||||||
});
|
|
||||||
|
|
||||||
attribute.addModifier(undefined, 5);
|
|
||||||
check.in("+4 +2 +20% +5%", check => {
|
|
||||||
check.equals(attribute.get(), 8, "effective value");
|
|
||||||
check.equals(attribute.getMaximal(), Infinity, "maximal value");
|
|
||||||
});
|
|
||||||
|
|
||||||
attribute.addModifier(undefined, undefined, 6);
|
|
||||||
check.in("+4 +2 +20% +5% lim6", check => {
|
|
||||||
check.equals(attribute.get(), 6, "effective value");
|
|
||||||
check.equals(attribute.getMaximal(), 6, "maximal value");
|
|
||||||
});
|
|
||||||
|
|
||||||
attribute.addModifier(undefined, undefined, 4);
|
|
||||||
check.in("+4 +2 +20% +5% lim6 lim4", check => {
|
|
||||||
check.equals(attribute.get(), 4, "effective value");
|
|
||||||
check.equals(attribute.getMaximal(), 4, "maximal value");
|
|
||||||
});
|
|
||||||
|
|
||||||
attribute.addModifier(undefined, undefined, 10);
|
|
||||||
check.in("+4 +2 +20% +5% lim6 lim4 lim10", check => {
|
|
||||||
check.equals(attribute.get(), 4, "effective value");
|
|
||||||
check.equals(attribute.getMaximal(), 4, "maximal value");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
attribute.addModifier(2);
|
||||||
|
check.in("+4 +2", check => {
|
||||||
|
check.equals(attribute.get(), 6, "effective value");
|
||||||
|
});
|
||||||
|
|
||||||
|
attribute.addModifier(undefined, 20);
|
||||||
|
check.in("+4 +2 +20%", check => {
|
||||||
|
check.equals(attribute.get(), 7, "effective value");
|
||||||
|
});
|
||||||
|
|
||||||
|
attribute.addModifier(undefined, 5);
|
||||||
|
check.in("+4 +2 +20% +5%", check => {
|
||||||
|
check.equals(attribute.get(), 8, "effective value");
|
||||||
|
check.equals(attribute.getMaximal(), Infinity, "maximal value");
|
||||||
|
});
|
||||||
|
|
||||||
|
attribute.addModifier(undefined, undefined, 6);
|
||||||
|
check.in("+4 +2 +20% +5% lim6", check => {
|
||||||
|
check.equals(attribute.get(), 6, "effective value");
|
||||||
|
check.equals(attribute.getMaximal(), 6, "maximal value");
|
||||||
|
});
|
||||||
|
|
||||||
|
attribute.addModifier(undefined, undefined, 4);
|
||||||
|
check.in("+4 +2 +20% +5% lim6 lim4", check => {
|
||||||
|
check.equals(attribute.get(), 4, "effective value");
|
||||||
|
check.equals(attribute.getMaximal(), 4, "maximal value");
|
||||||
|
});
|
||||||
|
|
||||||
|
attribute.addModifier(undefined, undefined, 10);
|
||||||
|
check.in("+4 +2 +20% +5% lim6 lim4 lim10", check => {
|
||||||
|
check.equals(attribute.get(), 4, "effective value");
|
||||||
|
check.equals(attribute.getMaximal(), 4, "maximal value");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,155 +1,155 @@
|
||||||
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 = {
|
||||||
"initiative": "Capacity to play before others in a battle",
|
[P in (keyof ShipValues | keyof ShipAttributes)]: string
|
||||||
"hull": "Physical structure of the ship",
|
}
|
||||||
"shield": "Shield around the ship that may absorb damage",
|
|
||||||
"power": "Power available to supply the equipments",
|
export const SHIP_VALUES_DESCRIPTIONS: ShipValuesMapping = {
|
||||||
"hull_capacity": "Maximal Hull value before the ship risks collapsing",
|
"initiative": "Capacity to play before others in a battle",
|
||||||
"shield_capacity": "Maximal Shield value to protect the hull from damage",
|
"hull": "Physical structure of the ship",
|
||||||
"power_capacity": "Maximal Power value to use equipment",
|
"shield": "Shield around the ship that may absorb damage",
|
||||||
"evasion": "Damage points that may be evaded by maneuvering",
|
"power": "Power available to supply the equipments",
|
||||||
}
|
"hull_capacity": "Maximal Hull value before the ship risks collapsing",
|
||||||
|
"shield_capacity": "Maximal Shield value to protect the hull from damage",
|
||||||
export const SHIP_VALUES_NAMES: ShipValuesMapping = {
|
"power_capacity": "Maximal Power value to use equipment",
|
||||||
"initiative": "initiative",
|
"evasion": "Damage points that may be evaded by maneuvering",
|
||||||
"hull": "hull",
|
}
|
||||||
"shield": "shield",
|
|
||||||
"power": "power",
|
export const SHIP_VALUES_NAMES: ShipValuesMapping = {
|
||||||
"hull_capacity": "hull capacity",
|
"initiative": "initiative",
|
||||||
"shield_capacity": "shield capacity",
|
"hull": "hull",
|
||||||
"power_capacity": "power capacity",
|
"shield": "shield",
|
||||||
"evasion": "evasion",
|
"power": "power",
|
||||||
}
|
"hull_capacity": "hull capacity",
|
||||||
|
"shield_capacity": "shield capacity",
|
||||||
/**
|
"power_capacity": "power capacity",
|
||||||
* A ship attribute is a number resulting of a list of modifiers.
|
"evasion": "evasion",
|
||||||
*/
|
}
|
||||||
export class ShipAttribute {
|
|
||||||
// Current value
|
/**
|
||||||
private current = 0
|
* A ship attribute is a number resulting of a list of modifiers.
|
||||||
|
*/
|
||||||
// Modifiers
|
export class ShipAttribute {
|
||||||
private cumulatives: number[] = []
|
// Current value
|
||||||
private multipliers: number[] = []
|
private current = 0
|
||||||
private limits: number[] = []
|
|
||||||
|
// Modifiers
|
||||||
/**
|
private cumulatives: number[] = []
|
||||||
* Get the current value
|
private multipliers: number[] = []
|
||||||
*/
|
private limits: number[] = []
|
||||||
get(): number {
|
|
||||||
return this.current;
|
/**
|
||||||
}
|
* Get the current value
|
||||||
|
*/
|
||||||
/**
|
get(): number {
|
||||||
* Get the maximal value enforced by limit modifiers, Infinity for unlimited
|
return this.current;
|
||||||
*/
|
}
|
||||||
getMaximal(): number {
|
|
||||||
if (this.limits.length > 0) {
|
/**
|
||||||
return min(this.limits);
|
* Get the maximal value enforced by limit modifiers, Infinity for unlimited
|
||||||
} else {
|
*/
|
||||||
return Infinity;
|
getMaximal(): number {
|
||||||
}
|
if (this.limits.length > 0) {
|
||||||
}
|
return min(this.limits);
|
||||||
|
} else {
|
||||||
/**
|
return Infinity;
|
||||||
* Reset all modifiers
|
}
|
||||||
*/
|
}
|
||||||
reset(): void {
|
|
||||||
this.cumulatives = [];
|
/**
|
||||||
this.multipliers = [];
|
* Reset all modifiers
|
||||||
this.limits = [];
|
*/
|
||||||
this.update();
|
reset(): void {
|
||||||
}
|
this.cumulatives = [];
|
||||||
|
this.multipliers = [];
|
||||||
/**
|
this.limits = [];
|
||||||
* Add a modifier
|
this.update();
|
||||||
*/
|
}
|
||||||
addModifier(cumulative?: number, multiplier?: number, limit?: number): void {
|
|
||||||
if (typeof cumulative != "undefined") {
|
/**
|
||||||
this.cumulatives.push(cumulative);
|
* Add a modifier
|
||||||
}
|
*/
|
||||||
if (typeof multiplier != "undefined") {
|
addModifier(cumulative?: number, multiplier?: number, limit?: number): void {
|
||||||
this.multipliers.push(multiplier);
|
if (typeof cumulative != "undefined") {
|
||||||
}
|
this.cumulatives.push(cumulative);
|
||||||
if (typeof limit != "undefined") {
|
}
|
||||||
this.limits.push(limit);
|
if (typeof multiplier != "undefined") {
|
||||||
}
|
this.multipliers.push(multiplier);
|
||||||
this.update();
|
}
|
||||||
}
|
if (typeof limit != "undefined") {
|
||||||
|
this.limits.push(limit);
|
||||||
/**
|
}
|
||||||
* Remove a modifier
|
this.update();
|
||||||
*/
|
}
|
||||||
removeModifier(cumulative?: number, multiplier?: number, limit?: number): void {
|
|
||||||
if (typeof cumulative != "undefined") {
|
/**
|
||||||
remove(this.cumulatives, cumulative);
|
* Remove a modifier
|
||||||
}
|
*/
|
||||||
if (typeof multiplier != "undefined") {
|
removeModifier(cumulative?: number, multiplier?: number, limit?: number): void {
|
||||||
remove(this.multipliers, multiplier);
|
if (typeof cumulative != "undefined") {
|
||||||
}
|
remove(this.cumulatives, cumulative);
|
||||||
if (typeof limit != "undefined") {
|
}
|
||||||
remove(this.limits, limit);
|
if (typeof multiplier != "undefined") {
|
||||||
}
|
remove(this.multipliers, multiplier);
|
||||||
this.update();
|
}
|
||||||
}
|
if (typeof limit != "undefined") {
|
||||||
|
remove(this.limits, limit);
|
||||||
/**
|
}
|
||||||
* Update the current value
|
this.update();
|
||||||
*/
|
}
|
||||||
private update(): void {
|
|
||||||
let value = sum(this.cumulatives);
|
/**
|
||||||
if (this.multipliers.length) {
|
* Update the current value
|
||||||
value = Math.round(value * (1 + sum(this.multipliers) / 100));
|
*/
|
||||||
}
|
private update(): void {
|
||||||
if (this.limits.length) {
|
let value = sum(this.cumulatives);
|
||||||
value = Math.min(value, min(this.limits));
|
if (this.multipliers.length) {
|
||||||
}
|
value = Math.round(value * (1 + sum(this.multipliers) / 100));
|
||||||
this.current = value;
|
}
|
||||||
}
|
if (this.limits.length) {
|
||||||
}
|
value = Math.min(value, min(this.limits));
|
||||||
|
}
|
||||||
/**
|
this.current = value;
|
||||||
* Set of ShipAttribute for a ship
|
}
|
||||||
*/
|
}
|
||||||
export class ShipAttributes {
|
|
||||||
// Initiative (capacity to play first)
|
/**
|
||||||
initiative = new ShipAttribute()
|
* Set of ShipAttribute for a ship
|
||||||
// Maximal hull value
|
*/
|
||||||
hull_capacity = new ShipAttribute()
|
export class ShipAttributes {
|
||||||
// Maximal shield value
|
// Initiative (capacity to play first)
|
||||||
shield_capacity = new ShipAttribute()
|
initiative = new ShipAttribute()
|
||||||
// Damage evasion
|
// Maximal hull value
|
||||||
evasion = new ShipAttribute()
|
hull_capacity = new ShipAttribute()
|
||||||
// Maximal power value
|
// Maximal shield value
|
||||||
power_capacity = new ShipAttribute()
|
shield_capacity = new ShipAttribute()
|
||||||
}
|
// Damage evasion
|
||||||
|
evasion = new ShipAttribute()
|
||||||
/**
|
// Maximal power value
|
||||||
* Set of simple values for a ship
|
power_capacity = new ShipAttribute()
|
||||||
*/
|
}
|
||||||
export class ShipValues {
|
|
||||||
hull = 0
|
/**
|
||||||
shield = 0
|
* Set of simple values for a ship
|
||||||
power = 0
|
*/
|
||||||
}
|
export class ShipValues {
|
||||||
|
hull = 0
|
||||||
/**
|
shield = 0
|
||||||
* Static attributes and values object for property queries
|
power = 0
|
||||||
*/
|
}
|
||||||
export const SHIP_ATTRIBUTES = new ShipAttributes();
|
|
||||||
export const SHIP_VALUES = new ShipValues();
|
/**
|
||||||
|
* Static attributes and values object for property queries
|
||||||
/**
|
*/
|
||||||
* Type guards
|
export const SHIP_ATTRIBUTES = new ShipAttributes();
|
||||||
*/
|
export const SHIP_VALUES = new ShipValues();
|
||||||
export function isShipValue(key: string): key is keyof ShipValues {
|
|
||||||
return SHIP_VALUES.hasOwnProperty(key);
|
/**
|
||||||
}
|
* Type guards
|
||||||
export function isShipAttribute(key: string): key is keyof ShipAttributes {
|
*/
|
||||||
return SHIP_ATTRIBUTES.hasOwnProperty(key);
|
export function isShipValue(key: string): key is keyof ShipValues {
|
||||||
}
|
return SHIP_VALUES.hasOwnProperty(key);
|
||||||
|
}
|
||||||
|
export function isShipAttribute(key: string): key is keyof ShipAttributes {
|
||||||
|
return SHIP_ATTRIBUTES.hasOwnProperty(key);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,44 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("Shop", test => {
|
import { Mission } from "./missions/Mission";
|
||||||
test.case("generates secondary missions", check => {
|
import { Player } from "./Player";
|
||||||
let universe = new Universe();
|
import { Shop } from "./Shop";
|
||||||
universe.generate(4);
|
import { StarLocation } from "./StarLocation";
|
||||||
let start = universe.getStartLocation();
|
import { Universe } from "./Universe";
|
||||||
|
|
||||||
let shop = new Shop();
|
testing("Shop", test => {
|
||||||
check.equals((<any>shop).missions.length, 0);
|
test.case("generates secondary missions", check => {
|
||||||
|
let universe = new Universe();
|
||||||
|
universe.generate(4);
|
||||||
|
let start = universe.getStartLocation();
|
||||||
|
|
||||||
let result = shop.getMissions(start, 4);
|
let shop = new Shop();
|
||||||
check.equals(result.length, 4);
|
check.equals((<any>shop).missions.length, 0);
|
||||||
check.equals((<any>shop).missions.length, 4);
|
|
||||||
|
|
||||||
let oresult = shop.getMissions(start, 4);
|
let result = shop.getMissions(start, 4);
|
||||||
check.equals(oresult, result);
|
check.equals(result.length, 4);
|
||||||
|
check.equals((<any>shop).missions.length, 4);
|
||||||
|
|
||||||
result.forEach(mission => {
|
let oresult = shop.getMissions(start, 4);
|
||||||
check.equals(mission.main, false);
|
check.equals(oresult, result);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("assigns missions to a fleet", check => {
|
result.forEach(mission => {
|
||||||
let shop = new Shop();
|
check.equals(mission.main, false);
|
||||||
let player = new Player();
|
|
||||||
let mission = new Mission(new Universe());
|
|
||||||
(<any>shop).missions = [mission];
|
|
||||||
|
|
||||||
check.equals(shop.getMissions(new StarLocation(), 1), [mission]);
|
|
||||||
check.equals(player.missions.secondary, []);
|
|
||||||
|
|
||||||
shop.acceptMission(mission, player);
|
|
||||||
|
|
||||||
check.equals((<any>shop).missions, []);
|
|
||||||
check.equals(player.missions.secondary, [mission]);
|
|
||||||
check.same(mission.fleet, player.fleet);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
|
test.case("assigns missions to a fleet", check => {
|
||||||
|
let shop = new Shop();
|
||||||
|
let player = new Player();
|
||||||
|
let mission = new Mission(new Universe());
|
||||||
|
(<any>shop).missions = [mission];
|
||||||
|
|
||||||
|
check.equals(shop.getMissions(new StarLocation(), 1), [mission]);
|
||||||
|
check.equals(player.missions.secondary, []);
|
||||||
|
|
||||||
|
shop.acceptMission(mission, player);
|
||||||
|
|
||||||
|
check.equals((<any>shop).missions, []);
|
||||||
|
check.equals(player.missions.secondary, [mission]);
|
||||||
|
check.same(mission.fleet, player.fleet);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,51 +1,56 @@
|
||||||
module TK.SpaceTac {
|
import { RandomGenerator } from "../common/RandomGenerator";
|
||||||
/**
|
import { contains, remove } from "../common/Tools";
|
||||||
* A shop is a place to buy/sell equipments
|
import { Mission } from "./missions/Mission";
|
||||||
*/
|
import { MissionGenerator } from "./missions/MissionGenerator";
|
||||||
export class Shop {
|
import { Player } from "./Player";
|
||||||
// Average level of equipment
|
import { StarLocation } from "./StarLocation";
|
||||||
private level: number
|
|
||||||
|
|
||||||
// Random generator
|
/**
|
||||||
private random: RandomGenerator
|
* A shop is a place to buy/sell equipments
|
||||||
|
*/
|
||||||
|
export class Shop {
|
||||||
|
// Average level of equipment
|
||||||
|
private level: number
|
||||||
|
|
||||||
// Available missions
|
// Random generator
|
||||||
private missions: Mission[] = []
|
private random: RandomGenerator
|
||||||
|
|
||||||
constructor(level = 1) {
|
// Available missions
|
||||||
this.level = level;
|
private missions: Mission[] = []
|
||||||
this.random = new RandomGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
constructor(level = 1) {
|
||||||
* Get a list of available secondary missions
|
this.level = level;
|
||||||
*/
|
this.random = new RandomGenerator();
|
||||||
getMissions(around: StarLocation, max_count = 3): Mission[] {
|
}
|
||||||
while (this.missions.length < max_count) {
|
|
||||||
let generator = new MissionGenerator(around.star.universe, around, this.random);
|
|
||||||
let mission = generator.generate();
|
|
||||||
this.missions.push(mission);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.missions;
|
/**
|
||||||
}
|
* Get a list of available secondary missions
|
||||||
|
*/
|
||||||
/**
|
getMissions(around: StarLocation, max_count = 3): Mission[] {
|
||||||
* Assign a mission to a fleet
|
while (this.missions.length < max_count) {
|
||||||
*
|
let generator = new MissionGenerator(around.star.universe, around, this.random);
|
||||||
* Returns true on success
|
let mission = generator.generate();
|
||||||
*/
|
this.missions.push(mission);
|
||||||
acceptMission(mission: Mission, player: Player): boolean {
|
|
||||||
if (contains(this.missions, mission)) {
|
|
||||||
if (player.missions.addSecondary(mission, player.fleet)) {
|
|
||||||
remove(this.missions, mission);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return this.missions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign a mission to a fleet
|
||||||
|
*
|
||||||
|
* Returns true on success
|
||||||
|
*/
|
||||||
|
acceptMission(mission: Mission, player: Player): boolean {
|
||||||
|
if (contains(this.missions, mission)) {
|
||||||
|
if (player.missions.addSecondary(mission, player.fleet)) {
|
||||||
|
remove(this.missions, mission);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,36 +1,39 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("Star", test => {
|
import { Star } from "./Star";
|
||||||
test.case("lists links to other stars", check => {
|
import { StarLink } from "./StarLink";
|
||||||
var universe = new Universe();
|
import { Universe } from "./Universe";
|
||||||
universe.stars.push(new Star(universe, 0, 0, "Star A"));
|
|
||||||
universe.stars.push(new Star(universe, 1, 0, "Star B"));
|
|
||||||
universe.stars.push(new Star(universe, 0, 1, "Star C"));
|
|
||||||
universe.stars.push(new Star(universe, 1, 1, "Star D"));
|
|
||||||
universe.addLink(universe.stars[0], universe.stars[1]);
|
|
||||||
universe.addLink(universe.stars[0], universe.stars[3]);
|
|
||||||
|
|
||||||
var result = universe.stars[0].getLinks();
|
testing("Star", test => {
|
||||||
check.equals(result.length, 2);
|
test.case("lists links to other stars", check => {
|
||||||
check.equals(result[0], new StarLink(universe.stars[0], universe.stars[1]));
|
var universe = new Universe();
|
||||||
check.equals(result[1], new StarLink(universe.stars[0], universe.stars[3]));
|
universe.stars.push(new Star(universe, 0, 0, "Star A"));
|
||||||
|
universe.stars.push(new Star(universe, 1, 0, "Star B"));
|
||||||
|
universe.stars.push(new Star(universe, 0, 1, "Star C"));
|
||||||
|
universe.stars.push(new Star(universe, 1, 1, "Star D"));
|
||||||
|
universe.addLink(universe.stars[0], universe.stars[1]);
|
||||||
|
universe.addLink(universe.stars[0], universe.stars[3]);
|
||||||
|
|
||||||
check.equals(universe.stars[0].getLinkTo(universe.stars[1]), universe.starlinks[0]);
|
var result = universe.stars[0].getLinks();
|
||||||
check.equals(universe.stars[0].getLinkTo(universe.stars[2]), null);
|
check.equals(result.length, 2);
|
||||||
check.equals(universe.stars[0].getLinkTo(universe.stars[3]), universe.starlinks[1]);
|
check.equals(result[0], new StarLink(universe.stars[0], universe.stars[1]));
|
||||||
check.equals(universe.stars[1].getLinkTo(universe.stars[0]), universe.starlinks[0]);
|
check.equals(result[1], new StarLink(universe.stars[0], universe.stars[3]));
|
||||||
check.equals(universe.stars[1].getLinkTo(universe.stars[2]), null);
|
|
||||||
check.equals(universe.stars[1].getLinkTo(universe.stars[3]), null);
|
|
||||||
check.equals(universe.stars[2].getLinkTo(universe.stars[0]), null);
|
|
||||||
check.equals(universe.stars[2].getLinkTo(universe.stars[1]), null);
|
|
||||||
check.equals(universe.stars[2].getLinkTo(universe.stars[3]), null);
|
|
||||||
check.equals(universe.stars[3].getLinkTo(universe.stars[0]), universe.starlinks[1]);
|
|
||||||
check.equals(universe.stars[3].getLinkTo(universe.stars[1]), null);
|
|
||||||
check.equals(universe.stars[3].getLinkTo(universe.stars[2]), null);
|
|
||||||
|
|
||||||
let neighbors = universe.stars[0].getNeighbors();
|
check.equals(universe.stars[0].getLinkTo(universe.stars[1]), universe.starlinks[0]);
|
||||||
check.equals(neighbors.length, 2);
|
check.equals(universe.stars[0].getLinkTo(universe.stars[2]), null);
|
||||||
check.contains(neighbors, universe.stars[1]);
|
check.equals(universe.stars[0].getLinkTo(universe.stars[3]), universe.starlinks[1]);
|
||||||
check.contains(neighbors, universe.stars[3]);
|
check.equals(universe.stars[1].getLinkTo(universe.stars[0]), universe.starlinks[0]);
|
||||||
});
|
check.equals(universe.stars[1].getLinkTo(universe.stars[2]), null);
|
||||||
});
|
check.equals(universe.stars[1].getLinkTo(universe.stars[3]), null);
|
||||||
}
|
check.equals(universe.stars[2].getLinkTo(universe.stars[0]), null);
|
||||||
|
check.equals(universe.stars[2].getLinkTo(universe.stars[1]), null);
|
||||||
|
check.equals(universe.stars[2].getLinkTo(universe.stars[3]), null);
|
||||||
|
check.equals(universe.stars[3].getLinkTo(universe.stars[0]), universe.starlinks[1]);
|
||||||
|
check.equals(universe.stars[3].getLinkTo(universe.stars[1]), null);
|
||||||
|
check.equals(universe.stars[3].getLinkTo(universe.stars[2]), null);
|
||||||
|
|
||||||
|
let neighbors = universe.stars[0].getNeighbors();
|
||||||
|
check.equals(neighbors.length, 2);
|
||||||
|
check.contains(neighbors, universe.stars[1]);
|
||||||
|
check.contains(neighbors, universe.stars[3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
368
src/core/Star.ts
|
@ -1,198 +1,202 @@
|
||||||
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";
|
||||||
|
|
||||||
// Available names for star systems
|
// A star system
|
||||||
static NAMES_POOL = [
|
export class Star {
|
||||||
"Alpha Prime",
|
|
||||||
"Bright Skies",
|
|
||||||
"Costan Sector",
|
|
||||||
"Duncan's Legacy",
|
|
||||||
"Ethiopea",
|
|
||||||
"Fringe Space",
|
|
||||||
"Gesurd Deep",
|
|
||||||
"Helios",
|
|
||||||
"Justice Enclave",
|
|
||||||
"Kovak Second",
|
|
||||||
"Lumen Circle",
|
|
||||||
"Manoa Society",
|
|
||||||
"Neptune's Record",
|
|
||||||
"Ominous Murmur",
|
|
||||||
"Pirate's Landing",
|
|
||||||
"Quasuc Effect",
|
|
||||||
"Roaring Thunder",
|
|
||||||
"Safe Passage",
|
|
||||||
"Time Holes",
|
|
||||||
"Unknown Territory",
|
|
||||||
"Vulcan Terror",
|
|
||||||
"Wings Aurora",
|
|
||||||
"Xenos Trading",
|
|
||||||
"Yu's Pride",
|
|
||||||
"Zoki's Hammer",
|
|
||||||
"Astral Tempest",
|
|
||||||
"Burned Star",
|
|
||||||
"Crystal Bride",
|
|
||||||
"Death Star",
|
|
||||||
"Ether Bending",
|
|
||||||
"Forgotten Realm",
|
|
||||||
"Galactic Ring",
|
|
||||||
"Hegemonia",
|
|
||||||
"Jorgon Trails",
|
|
||||||
"Kemini System",
|
|
||||||
"Light Rain",
|
|
||||||
"Moons Astride",
|
|
||||||
"Nubia's Sisters",
|
|
||||||
"Opium Hide",
|
|
||||||
"Paradise Quest",
|
|
||||||
"Quarter Horizon",
|
|
||||||
"Rising Dust",
|
|
||||||
"Silence of Forge",
|
|
||||||
"Titan Feet",
|
|
||||||
"Unicorn Fly",
|
|
||||||
"Violated Sanctuary",
|
|
||||||
"World's Repose",
|
|
||||||
"Xanthia's Travel",
|
|
||||||
"Yggdrasil",
|
|
||||||
"Zone of Ending",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Parent universe
|
// Available names for star systems
|
||||||
universe: Universe;
|
static NAMES_POOL = [
|
||||||
|
"Alpha Prime",
|
||||||
|
"Bright Skies",
|
||||||
|
"Costan Sector",
|
||||||
|
"Duncan's Legacy",
|
||||||
|
"Ethiopea",
|
||||||
|
"Fringe Space",
|
||||||
|
"Gesurd Deep",
|
||||||
|
"Helios",
|
||||||
|
"Justice Enclave",
|
||||||
|
"Kovak Second",
|
||||||
|
"Lumen Circle",
|
||||||
|
"Manoa Society",
|
||||||
|
"Neptune's Record",
|
||||||
|
"Ominous Murmur",
|
||||||
|
"Pirate's Landing",
|
||||||
|
"Quasuc Effect",
|
||||||
|
"Roaring Thunder",
|
||||||
|
"Safe Passage",
|
||||||
|
"Time Holes",
|
||||||
|
"Unknown Territory",
|
||||||
|
"Vulcan Terror",
|
||||||
|
"Wings Aurora",
|
||||||
|
"Xenos Trading",
|
||||||
|
"Yu's Pride",
|
||||||
|
"Zoki's Hammer",
|
||||||
|
"Astral Tempest",
|
||||||
|
"Burned Star",
|
||||||
|
"Crystal Bride",
|
||||||
|
"Death Star",
|
||||||
|
"Ether Bending",
|
||||||
|
"Forgotten Realm",
|
||||||
|
"Galactic Ring",
|
||||||
|
"Hegemonia",
|
||||||
|
"Jorgon Trails",
|
||||||
|
"Kemini System",
|
||||||
|
"Light Rain",
|
||||||
|
"Moons Astride",
|
||||||
|
"Nubia's Sisters",
|
||||||
|
"Opium Hide",
|
||||||
|
"Paradise Quest",
|
||||||
|
"Quarter Horizon",
|
||||||
|
"Rising Dust",
|
||||||
|
"Silence of Forge",
|
||||||
|
"Titan Feet",
|
||||||
|
"Unicorn Fly",
|
||||||
|
"Violated Sanctuary",
|
||||||
|
"World's Repose",
|
||||||
|
"Xanthia's Travel",
|
||||||
|
"Yggdrasil",
|
||||||
|
"Zone of Ending",
|
||||||
|
];
|
||||||
|
|
||||||
// Name of the system (unique in the universe)
|
// Parent universe
|
||||||
name: string;
|
universe: Universe;
|
||||||
|
|
||||||
// Location in the universe
|
// Name of the system (unique in the universe)
|
||||||
x: number;
|
name: string;
|
||||||
y: number;
|
|
||||||
|
|
||||||
// Radius of the star system
|
// Location in the universe
|
||||||
radius: number;
|
x: number;
|
||||||
|
y: number;
|
||||||
|
|
||||||
// List of points of interest
|
// Radius of the star system
|
||||||
locations: StarLocation[];
|
radius: number;
|
||||||
|
|
||||||
// Base level for encounters in this system
|
// List of points of interest
|
||||||
level: number;
|
locations: StarLocation[];
|
||||||
|
|
||||||
constructor(universe: Universe | null = null, x = 0, y = 0, name = "") {
|
// Base level for encounters in this system
|
||||||
this.universe = universe || new Universe();
|
level: number;
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.radius = 0.1;
|
|
||||||
this.locations = [new StarLocation(this, StarLocationType.STAR, 0, 0)];
|
|
||||||
this.level = 1;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
jasmineToString(): string {
|
constructor(universe: Universe | null = null, x = 0, y = 0, name = "") {
|
||||||
return `Star ${this.name}`;
|
this.universe = universe || new Universe();
|
||||||
}
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.radius = 0.1;
|
||||||
|
this.locations = [new StarLocation(this, StarLocationType.STAR, 0, 0)];
|
||||||
|
this.level = 1;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
jasmineToString(): string {
|
||||||
* Add a location of interest
|
return `Star ${this.name}`;
|
||||||
*/
|
}
|
||||||
addLocation(type: StarLocationType): StarLocation {
|
|
||||||
let result = new StarLocation(this, type);
|
|
||||||
this.locations.push(result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the distance to another star
|
/**
|
||||||
getDistanceTo(star: Star): number {
|
* Add a location of interest
|
||||||
var dx = this.x - star.x;
|
*/
|
||||||
var dy = this.y - star.y;
|
addLocation(type: StarLocationType): StarLocation {
|
||||||
|
let result = new StarLocation(this, type);
|
||||||
|
this.locations.push(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
return Math.sqrt(dx * dx + dy * dy);
|
// Get the distance to another star
|
||||||
}
|
getDistanceTo(star: Star): number {
|
||||||
|
var dx = this.x - star.x;
|
||||||
|
var dy = this.y - star.y;
|
||||||
|
|
||||||
// Generate the contents of this star system
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
generate(random = RandomGenerator.global): void {
|
}
|
||||||
var location_count = random.randInt(2 + Math.floor(this.level / 2), 3 + this.level);
|
|
||||||
if (this.name.length == 0) {
|
|
||||||
this.name = random.choice(Star.NAMES_POOL);
|
|
||||||
}
|
|
||||||
this.generateLocations(location_count, random);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate points of interest (*count* doesn't include the star and warp locations)
|
// Generate the contents of this star system
|
||||||
generateLocations(count: number, random = RandomGenerator.global): void {
|
generate(random = RandomGenerator.global): void {
|
||||||
while (count--) {
|
var location_count = random.randInt(2 + Math.floor(this.level / 2), 3 + this.level);
|
||||||
this.generateOneLocation(StarLocationType.PLANET, this.locations, this.radius * 0.2, this.radius * 0.6, random);
|
if (this.name.length == 0) {
|
||||||
}
|
this.name = random.choice(Star.NAMES_POOL);
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a warp location to another star (to be bound later)
|
|
||||||
generateWarpLocationTo(other: Star, random = RandomGenerator.global): StarLocation {
|
|
||||||
let fav_phi = Math.atan2(other.y - this.y, other.x - this.x);
|
|
||||||
var warp = this.generateOneLocation(StarLocationType.WARP, this.locations, this.radius * 0.75, this.radius * 0.85, random, fav_phi);
|
|
||||||
return warp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all direct links to other stars
|
|
||||||
getLinks(all = this.universe.starlinks): StarLink[] {
|
|
||||||
var result: StarLink[] = [];
|
|
||||||
|
|
||||||
all.forEach(link => {
|
|
||||||
if (link.first === this || link.second === this) {
|
|
||||||
result.push(link);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the link to another star, null if not found
|
|
||||||
getLinkTo(other: Star, all = this.universe.starlinks): StarLink | null {
|
|
||||||
var result: StarLink | null = null;
|
|
||||||
|
|
||||||
all.forEach(link => {
|
|
||||||
if (link.isLinking(this, other)) {
|
|
||||||
result = link;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the warp location to another star, null if not found
|
|
||||||
getWarpLocationTo(other: Star): StarLocation | null {
|
|
||||||
var result: StarLocation | null = null;
|
|
||||||
|
|
||||||
this.locations.forEach(location => {
|
|
||||||
if (location.type == StarLocationType.WARP && location.jump_dest && location.jump_dest.star == other) {
|
|
||||||
result = location;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the neighboring star systems (single jump accessible)
|
|
||||||
*/
|
|
||||||
getNeighbors(all = this.universe.starlinks): Star[] {
|
|
||||||
return nna(this.getLinks(all).map(link => link.getPeer(this)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a location is far enough from all other ones
|
|
||||||
private checkMinDistance(loc: StarLocation, others: StarLocation[]): boolean {
|
|
||||||
return others.every((iloc: StarLocation): boolean => {
|
|
||||||
return iloc.getDistanceTo(loc) > this.radius * 0.15;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a single location
|
|
||||||
private generateOneLocation(type: StarLocationType, others: StarLocation[], radius_min: number, radius_max: number, random: RandomGenerator, fav_phi: number | null = null): StarLocation {
|
|
||||||
do {
|
|
||||||
var phi = fav_phi ? (fav_phi + random.random() * 0.4 - 0.2) : (random.random() * Math.PI * 2);
|
|
||||||
var r = random.random() * (radius_max - radius_min) + radius_min;
|
|
||||||
var result = new StarLocation(this, type, r * Math.cos(phi), r * Math.sin(phi));
|
|
||||||
} while (!this.checkMinDistance(result, others));
|
|
||||||
|
|
||||||
this.locations.push(result);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.generateLocations(location_count, random);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate points of interest (*count* doesn't include the star and warp locations)
|
||||||
|
generateLocations(count: number, random = RandomGenerator.global): void {
|
||||||
|
while (count--) {
|
||||||
|
this.generateOneLocation(StarLocationType.PLANET, this.locations, this.radius * 0.2, this.radius * 0.6, random);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a warp location to another star (to be bound later)
|
||||||
|
generateWarpLocationTo(other: Star, random = RandomGenerator.global): StarLocation {
|
||||||
|
let fav_phi = Math.atan2(other.y - this.y, other.x - this.x);
|
||||||
|
var warp = this.generateOneLocation(StarLocationType.WARP, this.locations, this.radius * 0.75, this.radius * 0.85, random, fav_phi);
|
||||||
|
return warp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all direct links to other stars
|
||||||
|
getLinks(all = this.universe.starlinks): StarLink[] {
|
||||||
|
var result: StarLink[] = [];
|
||||||
|
|
||||||
|
all.forEach(link => {
|
||||||
|
if (link.first === this || link.second === this) {
|
||||||
|
result.push(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the link to another star, null if not found
|
||||||
|
getLinkTo(other: Star, all = this.universe.starlinks): StarLink | null {
|
||||||
|
var result: StarLink | null = null;
|
||||||
|
|
||||||
|
all.forEach(link => {
|
||||||
|
if (link.isLinking(this, other)) {
|
||||||
|
result = link;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the warp location to another star, null if not found
|
||||||
|
getWarpLocationTo(other: Star): StarLocation | null {
|
||||||
|
var result: StarLocation | null = null;
|
||||||
|
|
||||||
|
this.locations.forEach(location => {
|
||||||
|
if (location.type == StarLocationType.WARP && location.jump_dest && location.jump_dest.star == other) {
|
||||||
|
result = location;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the neighboring star systems (single jump accessible)
|
||||||
|
*/
|
||||||
|
getNeighbors(all = this.universe.starlinks): Star[] {
|
||||||
|
return nna(this.getLinks(all).map(link => link.getPeer(this)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a location is far enough from all other ones
|
||||||
|
private checkMinDistance(loc: StarLocation, others: StarLocation[]): boolean {
|
||||||
|
return others.every((iloc: StarLocation): boolean => {
|
||||||
|
return iloc.getDistanceTo(loc) > this.radius * 0.15;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a single location
|
||||||
|
private generateOneLocation(type: StarLocationType, others: StarLocation[], radius_min: number, radius_max: number, random: RandomGenerator, fav_phi: number | null = null): StarLocation {
|
||||||
|
do {
|
||||||
|
var phi = fav_phi ? (fav_phi + random.random() * 0.4 - 0.2) : (random.random() * Math.PI * 2);
|
||||||
|
var r = random.random() * (radius_max - radius_min) + radius_min;
|
||||||
|
var result = new StarLocation(this, type, r * Math.cos(phi), r * Math.sin(phi));
|
||||||
|
} while (!this.checkMinDistance(result, others));
|
||||||
|
|
||||||
|
this.locations.push(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,40 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { testing } from "../common/Testing";
|
||||||
testing("StarLink", test => {
|
import { Star } from "./Star";
|
||||||
test.case("checks link intersection", check => {
|
import { StarLink } from "./StarLink";
|
||||||
var star1 = new Star(null, 0, 0);
|
|
||||||
var star2 = new Star(null, 0, 1);
|
|
||||||
var star3 = new Star(null, 1, 0);
|
|
||||||
var star4 = new Star(null, 1, 1);
|
|
||||||
var link1 = new StarLink(star1, star2);
|
|
||||||
var link2 = new StarLink(star1, star3);
|
|
||||||
var link3 = new StarLink(star1, star4);
|
|
||||||
var link4 = new StarLink(star2, star3);
|
|
||||||
var link5 = new StarLink(star2, star4);
|
|
||||||
var link6 = new StarLink(star3, star4);
|
|
||||||
var links = [link1, link2, link3, link4, link5, link6];
|
|
||||||
links.forEach((first: StarLink) => {
|
|
||||||
links.forEach((second: StarLink) => {
|
|
||||||
if (first !== second) {
|
|
||||||
var expected = (first === link3 && second === link4) ||
|
|
||||||
(first === link4 && second === link3);
|
|
||||||
check.same(first.isCrossing(second), expected);
|
|
||||||
check.same(second.isCrossing(first), expected);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.case("gets the peer of a given sector", check => {
|
testing("StarLink", test => {
|
||||||
var star1 = new Star(null, 0, 0);
|
test.case("checks link intersection", check => {
|
||||||
var star2 = new Star(null, 0, 1);
|
var star1 = new Star(null, 0, 0);
|
||||||
var star3 = new Star(null, 0, 1);
|
var star2 = new Star(null, 0, 1);
|
||||||
var link1 = new StarLink(star1, star2);
|
var star3 = new Star(null, 1, 0);
|
||||||
|
var star4 = new Star(null, 1, 1);
|
||||||
check.same(link1.getPeer(star1), star2);
|
var link1 = new StarLink(star1, star2);
|
||||||
check.same(link1.getPeer(star2), star1);
|
var link2 = new StarLink(star1, star3);
|
||||||
check.equals(link1.getPeer(star3), null);
|
var link3 = new StarLink(star1, star4);
|
||||||
});
|
var link4 = new StarLink(star2, star3);
|
||||||
|
var link5 = new StarLink(star2, star4);
|
||||||
|
var link6 = new StarLink(star3, star4);
|
||||||
|
var links = [link1, link2, link3, link4, link5, link6];
|
||||||
|
links.forEach((first: StarLink) => {
|
||||||
|
links.forEach((second: StarLink) => {
|
||||||
|
if (first !== second) {
|
||||||
|
var expected = (first === link3 && second === link4) ||
|
||||||
|
(first === link4 && second === link3);
|
||||||
|
check.same(first.isCrossing(second), expected);
|
||||||
|
check.same(second.isCrossing(first), expected);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
|
test.case("gets the peer of a given sector", check => {
|
||||||
|
var star1 = new Star(null, 0, 0);
|
||||||
|
var star2 = new Star(null, 0, 1);
|
||||||
|
var star3 = new Star(null, 0, 1);
|
||||||
|
var link1 = new StarLink(star1, star2);
|
||||||
|
|
||||||
|
check.same(link1.getPeer(star1), star2);
|
||||||
|
check.same(link1.getPeer(star2), star1);
|
||||||
|
check.equals(link1.getPeer(star3), null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,49 +1,49 @@
|
||||||
module TK.SpaceTac {
|
import { Star } from "./Star";
|
||||||
// An hyperspace link between two star systems
|
|
||||||
export class StarLink {
|
|
||||||
// Stars
|
|
||||||
first: Star;
|
|
||||||
second: Star;
|
|
||||||
|
|
||||||
constructor(first: Star, second: Star) {
|
// An hyperspace link between two star systems
|
||||||
this.first = first;
|
export class StarLink {
|
||||||
this.second = second;
|
// Stars
|
||||||
}
|
first: Star;
|
||||||
|
second: Star;
|
||||||
|
|
||||||
// Check if this links bounds the two stars together, in either way
|
constructor(first: Star, second: Star) {
|
||||||
isLinking(first: Star, second: Star) {
|
this.first = first;
|
||||||
return (this.first === first && this.second === second) || (this.first === second && this.second === first);
|
this.second = second;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the length of a link
|
// Check if this links bounds the two stars together, in either way
|
||||||
getLength(): number {
|
isLinking(first: Star, second: Star) {
|
||||||
return this.first.getDistanceTo(this.second);
|
return (this.first === first && this.second === second) || (this.first === second && this.second === first);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this link crosses another
|
// Get the length of a link
|
||||||
isCrossing(other: StarLink): boolean {
|
getLength(): number {
|
||||||
if (this.first === other.first || this.second === other.first || this.first === other.second || this.second === other.second) {
|
return this.first.getDistanceTo(this.second);
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
var ccw = (a: Star, b: Star, c: Star): boolean => {
|
|
||||||
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
||||||
};
|
|
||||||
var cc1 = ccw(this.first, other.first, other.second);
|
|
||||||
var cc2 = ccw(this.second, other.first, other.second);
|
|
||||||
var cc3 = ccw(this.first, this.second, other.first);
|
|
||||||
var cc4 = ccw(this.first, this.second, other.second);
|
|
||||||
return cc1 !== cc2 && cc3 !== cc4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the other side of the link, for a given side
|
// Check if this link crosses another
|
||||||
getPeer(star: Star): Star | null {
|
isCrossing(other: StarLink): boolean {
|
||||||
if (star === this.first) {
|
if (this.first === other.first || this.second === other.first || this.first === other.second || this.second === other.second) {
|
||||||
return this.second;
|
return false;
|
||||||
} else if (star === this.second) {
|
|
||||||
return this.first;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var ccw = (a: Star, b: Star, c: Star): boolean => {
|
||||||
|
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
||||||
|
};
|
||||||
|
var cc1 = ccw(this.first, other.first, other.second);
|
||||||
|
var cc2 = ccw(this.second, other.first, other.second);
|
||||||
|
var cc3 = ccw(this.first, this.second, other.first);
|
||||||
|
var cc4 = ccw(this.first, this.second, other.second);
|
||||||
|
return cc1 !== cc2 && cc3 !== cc4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the other side of the link, for a given side
|
||||||
|
getPeer(star: Star): Star | null {
|
||||||
|
if (star === this.first) {
|
||||||
|
return this.second;
|
||||||
|
} else if (star === this.second) {
|
||||||
|
return this.first;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,41 @@
|
||||||
module TK.SpaceTac.Specs {
|
import { SkewedRandomGenerator } from "../common/RandomGenerator";
|
||||||
testing("StarLocation", test => {
|
import { testing } from "../common/Testing";
|
||||||
test.case("removes generated encounters that lose", check => {
|
import { nn } from "../common/Tools";
|
||||||
var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0);
|
import { Fleet } from "./Fleet";
|
||||||
var fleet = new Fleet();
|
import { StarLocation, StarLocationType } from "./StarLocation";
|
||||||
fleet.addShip();
|
|
||||||
location.encounter_random = new SkewedRandomGenerator([0]);
|
|
||||||
var battle = nn(location.enterLocation(fleet));
|
|
||||||
|
|
||||||
check.notequals(location.encounter, null);
|
testing("StarLocation", test => {
|
||||||
check.notequals(battle, null);
|
test.case("removes generated encounters that lose", check => {
|
||||||
|
var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0);
|
||||||
|
var fleet = new Fleet();
|
||||||
|
fleet.addShip();
|
||||||
|
location.encounter_random = new SkewedRandomGenerator([0]);
|
||||||
|
var battle = nn(location.enterLocation(fleet));
|
||||||
|
|
||||||
battle.endBattle(fleet);
|
check.notequals(location.encounter, null);
|
||||||
check.notequals(location.encounter, null);
|
check.notequals(battle, null);
|
||||||
|
|
||||||
location.resolveEncounter(nn(battle.outcome));
|
battle.endBattle(fleet);
|
||||||
check.equals(location.encounter, null);
|
check.notequals(location.encounter, null);
|
||||||
});
|
|
||||||
|
|
||||||
test.case("leaves generated encounters that win", check => {
|
location.resolveEncounter(nn(battle.outcome));
|
||||||
var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0);
|
check.equals(location.encounter, null);
|
||||||
var fleet = new Fleet();
|
});
|
||||||
fleet.addShip();
|
|
||||||
location.encounter_random = new SkewedRandomGenerator([0]);
|
|
||||||
var battle = nn(location.enterLocation(fleet));
|
|
||||||
|
|
||||||
check.notequals(location.encounter, null);
|
test.case("leaves generated encounters that win", check => {
|
||||||
check.notequals(battle, null);
|
var location = new StarLocation(undefined, StarLocationType.PLANET, 0, 0);
|
||||||
|
var fleet = new Fleet();
|
||||||
|
fleet.addShip();
|
||||||
|
location.encounter_random = new SkewedRandomGenerator([0]);
|
||||||
|
var battle = nn(location.enterLocation(fleet));
|
||||||
|
|
||||||
battle.endBattle(location.encounter);
|
check.notequals(location.encounter, null);
|
||||||
check.notequals(location.encounter, null);
|
check.notequals(battle, null);
|
||||||
|
|
||||||
location.resolveEncounter(nn(battle.outcome));
|
battle.endBattle(location.encounter);
|
||||||
check.notequals(location.encounter, null);
|
check.notequals(location.encounter, null);
|
||||||
});
|
|
||||||
});
|
location.resolveEncounter(nn(battle.outcome));
|
||||||
}
|
check.notequals(location.encounter, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,181 +1,189 @@
|
||||||
/// <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
|
|
||||||
|
// Type of location
|
||||||
// Type of location
|
type: StarLocationType
|
||||||
type: StarLocationType
|
|
||||||
|
// Location in the star system
|
||||||
// Location in the star system
|
x: number
|
||||||
x: number
|
y: number
|
||||||
y: number
|
|
||||||
|
// Absolute location in the universe
|
||||||
// Absolute location in the universe
|
universe_x: number
|
||||||
universe_x: number
|
universe_y: number
|
||||||
universe_y: number
|
|
||||||
|
// Destination for jump, if its a WARP location
|
||||||
// Destination for jump, if its a WARP location
|
jump_dest: StarLocation | null
|
||||||
jump_dest: StarLocation | null
|
|
||||||
|
// Fleets present at this location (excluding the encounter for now)
|
||||||
// Fleets present at this location (excluding the encounter for now)
|
fleets: Fleet[] = []
|
||||||
fleets: Fleet[] = []
|
|
||||||
|
// Enemy encounter
|
||||||
// Enemy encounter
|
encounter: Fleet | null = null
|
||||||
encounter: Fleet | null = null
|
encounter_gen = false
|
||||||
encounter_gen = false
|
encounter_random = RandomGenerator.global
|
||||||
encounter_random = RandomGenerator.global
|
|
||||||
|
// Shop to buy/sell equipment
|
||||||
// Shop to buy/sell equipment
|
shop: Shop | null = null
|
||||||
shop: Shop | null = null
|
|
||||||
|
constructor(star = new Star(), type: StarLocationType = StarLocationType.PLANET, x: number = 0, y: number = 0) {
|
||||||
constructor(star = new Star(), type: StarLocationType = StarLocationType.PLANET, x: number = 0, y: number = 0) {
|
super();
|
||||||
super();
|
|
||||||
|
this.star = star;
|
||||||
this.star = star;
|
this.type = type;
|
||||||
this.type = type;
|
this.x = x;
|
||||||
this.x = x;
|
this.y = y;
|
||||||
this.y = y;
|
this.universe_x = this.star.x + this.x;
|
||||||
this.universe_x = this.star.x + this.x;
|
this.universe_y = this.star.y + this.y;
|
||||||
this.universe_y = this.star.y + this.y;
|
this.jump_dest = null;
|
||||||
this.jump_dest = null;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Get the universe containing this location
|
||||||
* Get the universe containing this location
|
*/
|
||||||
*/
|
get universe(): Universe {
|
||||||
get universe(): Universe {
|
return this.star.universe;
|
||||||
return this.star.universe;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Add a shop in this location
|
||||||
* Add a shop in this location
|
*/
|
||||||
*/
|
addShop(level = this.star.level) {
|
||||||
addShop(level = this.star.level) {
|
this.shop = new Shop(level);
|
||||||
this.shop = new Shop(level);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Remove a potential shop in this location
|
||||||
* Remove a potential shop in this location
|
*/
|
||||||
*/
|
removeShop(): void {
|
||||||
removeShop(): void {
|
this.shop = null;
|
||||||
this.shop = null;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Add a fleet to the list of fleets present in this system
|
||||||
* Add a fleet to the list of fleets present in this system
|
*/
|
||||||
*/
|
addFleet(fleet: Fleet): void {
|
||||||
addFleet(fleet: Fleet): void {
|
if (add(this.fleets, fleet)) {
|
||||||
if (add(this.fleets, fleet)) {
|
this.enterLocation(fleet);
|
||||||
this.enterLocation(fleet);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Remove a fleet from the list of fleets present in this system
|
||||||
* Remove a fleet from the list of fleets present in this system
|
*/
|
||||||
*/
|
removeFleet(fleet: Fleet): void {
|
||||||
removeFleet(fleet: Fleet): void {
|
remove(this.fleets, fleet);
|
||||||
remove(this.fleets, fleet);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Check if the location is clear of encounter
|
||||||
* Check if the location is clear of encounter
|
*/
|
||||||
*/
|
isClear(): boolean {
|
||||||
isClear(): boolean {
|
return this.encounter_gen && this.encounter === null;
|
||||||
return this.encounter_gen && this.encounter === null;
|
}
|
||||||
}
|
|
||||||
|
// Set the jump destination of a WARP location
|
||||||
// Set the jump destination of a WARP location
|
setJumpDestination(jump_dest: StarLocation): void {
|
||||||
setJumpDestination(jump_dest: StarLocation): void {
|
if (this.type === StarLocationType.WARP) {
|
||||||
if (this.type === StarLocationType.WARP) {
|
this.jump_dest = jump_dest;
|
||||||
this.jump_dest = jump_dest;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Call this when first probing a location to generate the possible encounter
|
||||||
// Call this when first probing a location to generate the possible encounter
|
// Returns the encountered fleet, null if no encounter happens
|
||||||
// Returns the encountered fleet, null if no encounter happens
|
tryGenerateEncounter(): Fleet | null {
|
||||||
tryGenerateEncounter(): Fleet | null {
|
if (!this.encounter_gen) {
|
||||||
if (!this.encounter_gen) {
|
this.encounter_gen = true;
|
||||||
this.encounter_gen = true;
|
|
||||||
|
if (this.encounter_random.random() < 0.8) {
|
||||||
if (this.encounter_random.random() < 0.8) {
|
this.setupEncounter();
|
||||||
this.setupEncounter();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return this.encounter;
|
||||||
return this.encounter;
|
}
|
||||||
}
|
|
||||||
|
// Call this when entering a location to generate the possible encounter
|
||||||
// Call this when entering a location to generate the possible encounter
|
// *fleet* is the player fleet, entering the location
|
||||||
// *fleet* is the player fleet, entering the location
|
// Returns the engaged battle, null if no encounter happens
|
||||||
// Returns the engaged battle, null if no encounter happens
|
enterLocation(fleet: Fleet): Battle | null {
|
||||||
enterLocation(fleet: Fleet): Battle | null {
|
let encounter = this.tryGenerateEncounter();
|
||||||
let encounter = this.tryGenerateEncounter();
|
if (encounter) {
|
||||||
if (encounter) {
|
let battle = new Battle(fleet, encounter);
|
||||||
let battle = new Battle(fleet, encounter);
|
battle.start();
|
||||||
battle.start();
|
return battle;
|
||||||
return battle;
|
} else {
|
||||||
} else {
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Get the distance to another location
|
||||||
// Get the distance to another location
|
getDistanceTo(other: StarLocation): number {
|
||||||
getDistanceTo(other: StarLocation): number {
|
var dx = this.x - other.x;
|
||||||
var dx = this.x - other.x;
|
var dy = this.y - other.y;
|
||||||
var dy = this.y - other.y;
|
|
||||||
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
return Math.sqrt(dx * dx + dy * dy);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Clear an encounter, when the encountered fleet has been defeated
|
||||||
* Clear an encounter, when the encountered fleet has been defeated
|
*/
|
||||||
*/
|
clearEncounter() {
|
||||||
clearEncounter() {
|
this.encounter_gen = true;
|
||||||
this.encounter_gen = true;
|
this.encounter = null;
|
||||||
this.encounter = null;
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Forces the setup of an encounter
|
||||||
* Forces the setup of an encounter
|
*/
|
||||||
*/
|
setupEncounter() {
|
||||||
setupEncounter() {
|
this.encounter_gen = true;
|
||||||
this.encounter_gen = true;
|
|
||||||
|
let fleet_generator = new FleetGenerator(this.encounter_random);
|
||||||
let fleet_generator = new FleetGenerator(this.encounter_random);
|
let variations: [number, number][];
|
||||||
let variations: [number, number][];
|
if (this.star.level == 1) {
|
||||||
if (this.star.level == 1) {
|
variations = [[this.star.level, 2]];
|
||||||
variations = [[this.star.level, 2]];
|
} else if (this.star.level <= 3) {
|
||||||
} else if (this.star.level <= 3) {
|
variations = [[this.star.level, 2], [this.star.level - 1, 3]];
|
||||||
variations = [[this.star.level, 2], [this.star.level - 1, 3]];
|
} else if (this.star.level <= 6) {
|
||||||
} else if (this.star.level <= 6) {
|
variations = [[this.star.level, 3], [this.star.level - 1, 4], [this.star.level + 1, 2]];
|
||||||
variations = [[this.star.level, 3], [this.star.level - 1, 4], [this.star.level + 1, 2]];
|
} else {
|
||||||
} else {
|
variations = [[this.star.level, 4], [this.star.level - 1, 5], [this.star.level + 1, 3], [this.star.level + 3, 2]];
|
||||||
variations = [[this.star.level, 4], [this.star.level - 1, 5], [this.star.level + 1, 3], [this.star.level + 3, 2]];
|
}
|
||||||
}
|
let [level, enemies] = this.encounter_random.choice(variations);
|
||||||
let [level, enemies] = this.encounter_random.choice(variations);
|
this.encounter = fleet_generator.generate(level, new Player("Enemy"), enemies, true);
|
||||||
this.encounter = fleet_generator.generate(level, new Player("Enemy"), enemies, true);
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
/**
|
* Resolves the encounter from a battle outcome
|
||||||
* Resolves the encounter from a battle outcome
|
*/
|
||||||
*/
|
resolveEncounter(outcome: BattleOutcome) {
|
||||||
resolveEncounter(outcome: BattleOutcome) {
|
if (this.encounter && outcome.winner && !this.encounter.is(outcome.winner)) {
|
||||||
if (this.encounter && outcome.winner && !this.encounter.is(outcome.winner)) {
|
this.clearEncounter();
|
||||||
this.clearEncounter();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|