Compare commits

...

7 Commits

Author SHA1 Message Date
94f6fd3ae9 Fix main lib location 2020-03-22 17:44:59 +03:00
49bb3da4bb Add history for predictor moves 2020-03-22 14:38:05 +03:00
9e06a1c630 Write more tests 2020-03-22 13:30:37 +03:00
eb66b6904d Restore tests 2020-03-22 12:40:58 +03:00
4a18dc5808 Fix spellcheck 2020-03-22 10:48:29 +03:00
54a5b0683d Add circle ci badge 2020-03-22 10:20:33 +03:00
e3ea017315 Add junit report 2020-03-22 10:12:05 +03:00
16 changed files with 358 additions and 229 deletions

View File

@ -8,7 +8,9 @@ jobs:
- checkout - checkout
- run: npm ci - run: npm ci
- run: npm run format-check - run: npm run format-check
- run: npm run test - run: npm run test:junit-report
- store_test_results:
path: test-results
build_and_publish: build_and_publish:
docker: docker:

1
.gitignore vendored
View File

@ -3,5 +3,6 @@
built/ built/
coverage/ coverage/
node_modules/ node_modules/
test-results/
var/ var/
.npmrc .npmrc

View File

@ -1,16 +1,18 @@
# Электронная гадалка # Электронная гадалка
[![CircleCI](https://circleci.com/gh/anwinged/predictor/tree/master.svg?style=svg)](https://circleci.com/gh/anwinged/predictor/tree/master)
[Демоверсия][demo] [Демоверсия][demo]
Алгоритм, который противостоит человеку, и на основе ходов пытается предсказать Алгоритм, который противостоит человеку, и на основе ходов пытается предсказать
следующих ход человека. следующих ход человека.
Игрок загаывает один из двух вариантов, а робот пытается его угадать. Игрок загадывает один из двух вариантов, а робот пытается его угадать.
Если программе удалось угадать, то игрок теряет очко. Если программе удалось угадать, то игрок теряет очко.
Если же программа не смогла предсказать выбор человека, то игрок зарабатывает очко. Если программа не смогла предсказать выбор человека, то игрок зарабатывает очко.
Алгоритм реализован на основе [описания][algorithm]. В процессе реализации алгоритм слегка изменился. Алгоритм реализован на основе [описания][algorithm]. В процессе реализации алгоритм слегка изменился.
В отличие от описания, здесь можно допольнительно указать количество вариантов. В отличие от описания, здесь можно дополнительно указать количество вариантов.
С двумя вариантами будет игра "Чет - нечет", а с тремя - "Камень, ножницы, бумага". С двумя вариантами будет игра "Чет - нечет", а с тремя - "Камень, ножницы, бумага".
Интересно то, что программу сложно обыграть. Игрок пытается обставить робота, но все равно Интересно то, что программу сложно обыграть. Игрок пытается обставить робота, но все равно
@ -34,7 +36,7 @@ const prediction = predictor.pass(1);
// Получение текущего счета // Получение текущего счета
const score = predictor.score; const score = predictor.score;
// Получение количества сделаннх ходов // Получение количества сделанных ходов
const sc = predictor.stepCount(); const sc = predictor.stepCount();
``` ```
@ -53,7 +55,7 @@ const sc = predictor.stepCount();
tools/build-docker tools/build-docker
tools/npm run build tools/npm run build
## Тестиирование ## Тестирование
tools/npm run test tools/npm run test

53
package-lock.json generated
View File

@ -1119,6 +1119,12 @@
"supports-color": "^5.3.0" "supports-color": "^5.3.0"
} }
}, },
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=",
"dev": true
},
"check-error": { "check-error": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
@ -1953,6 +1959,12 @@
"which": "^1.2.9" "which": "^1.2.9"
} }
}, },
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=",
"dev": true
},
"crypto-browserify": { "crypto-browserify": {
"version": "3.12.0", "version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@ -3576,6 +3588,17 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"md5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
"dev": true,
"requires": {
"charenc": "~0.0.1",
"crypt": "~0.0.1",
"is-buffer": "~1.1.1"
}
},
"md5.js": { "md5.js": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -3917,6 +3940,30 @@
} }
} }
}, },
"mocha-junit-reporter": {
"version": "1.23.3",
"resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.23.3.tgz",
"integrity": "sha512-ed8LqbRj1RxZfjt/oC9t12sfrWsjZ3gNnbhV1nuj9R/Jb5/P3Xb4duv2eCfCDMYH+fEu0mqca7m4wsiVjsxsvA==",
"dev": true,
"requires": {
"debug": "^2.2.0",
"md5": "^2.1.0",
"mkdirp": "~0.5.1",
"strip-ansi": "^4.0.0",
"xml": "^1.0.0"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
}
},
"move-concurrently": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -6195,6 +6242,12 @@
"typedarray-to-buffer": "^3.1.5" "typedarray-to-buffer": "^3.1.5"
} }
}, },
"xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=",
"dev": true
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -4,13 +4,14 @@
"description": "", "description": "",
"author": "Anton Vakhrushev", "author": "Anton Vakhrushev",
"license": "MIT", "license": "MIT",
"main": "built/predictor.js", "main": "dist/predictor.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/anwinged/predictor.git" "url": "https://github.com/anwinged/predictor.git"
}, },
"scripts": { "scripts": {
"test": "mocha", "test": "mocha",
"test:junit-report": "mocha --reporter mocha-junit-reporter --reporter-options mochaFile=./test-results/results.xml",
"coverage": "nyc mocha", "coverage": "nyc mocha",
"build:dev": "webpack", "build:dev": "webpack",
"build": "webpack --env.production", "build": "webpack --env.production",
@ -25,6 +26,7 @@
"@types/node": "^13.9.2", "@types/node": "^13.9.2",
"chai": "^4.2.0", "chai": "^4.2.0",
"mocha": "^7.1.1", "mocha": "^7.1.1",
"mocha-junit-reporter": "^1.23.3",
"nyc": "^15.0.0", "nyc": "^15.0.0",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"ts-loader": "^6.2.1", "ts-loader": "^6.2.1",

View File

@ -1,49 +1,42 @@
import Journal from './Journal'; import Journal from './Journal';
const DEFAULT_EPSILON = 0.01;
function create_key(steps: number[]): string { function create_key(steps: number[]): string {
return steps.join(':'); return steps.join(':');
} }
class Daemon { class Daemon {
/** static DEFAULT_EPSILON = 0.01;
* @type {Number}
*/
base;
/** private readonly thisId: string;
* @type {Number}
*/
humanCount;
/** private readonly base: number;
* @type {Number}
*/
robotCount;
/** private readonly humanCount: number;
* @type {Number}
*/
epsilon;
/** private readonly robotCount: number;
* @type {Object}
*/ private readonly epsilon: number;
weights = {};
private weights: { [key: string]: number } = {};
constructor( constructor(
id: string,
base: number, base: number,
humanCount: number, humanCount: number,
robotCount: number, robotCount: number,
epsilon: number = DEFAULT_EPSILON epsilon: number = Daemon.DEFAULT_EPSILON
) { ) {
this.thisId = id;
this.base = base; this.base = base;
this.humanCount = humanCount; this.humanCount = humanCount;
this.robotCount = robotCount; this.robotCount = robotCount;
this.epsilon = epsilon; this.epsilon = epsilon;
} }
get id(): string {
return this.thisId;
}
get power(): number { get power(): number {
return this.humanCount + this.robotCount; return this.humanCount + this.robotCount;
} }
@ -62,69 +55,35 @@ class Daemon {
return proposals.indexOf(maxWeight); return proposals.indexOf(maxWeight);
} }
/** adjust(journal: Journal, humanValue: number): void {
* @param {Journal} journal
* @param {Number} humanValue
*/
adjust(journal, humanValue) {
const steps = this._getStepSlice(journal); const steps = this._getStepSlice(journal);
const adjustmentWeight = this._getAdjustmentWeight(journal.length); const adjustmentWeight = this._getAdjustmentWeight(journal.length);
this._adjustWeight([...steps, humanValue], adjustmentWeight); this._adjustWeight([...steps, humanValue], adjustmentWeight);
} }
/** getWeights() {
* @param {Journal} journal return { ...this.weights };
* }
* @returns {Number[]}
*/ private _getStepSlice(journal: Journal): number[] {
private _getStepSlice(journal) {
return journal.getLastMovements(this.humanCount, this.robotCount); return journal.getLastMovements(this.humanCount, this.robotCount);
} }
/** private _getAdjustmentWeight(stepNumber: number): number {
* @param {Number} stepNumber
*
* @returns {Number}
*
* @private
*/
private _getAdjustmentWeight(stepNumber) {
return Math.pow(1 + this.epsilon, stepNumber); return Math.pow(1 + this.epsilon, stepNumber);
} }
/**
* @param {Number[]} steps
*
* @returns {Number}
*
* @private
*/
private _getWeight(steps: number[]): number { private _getWeight(steps: number[]): number {
const key = create_key(steps); const key = create_key(steps);
const weight = this.weights[key]; return key in this.weights ? this.weights[key] : 0;
return weight as number;
} }
/** private _setWeight(steps: number[], value: number): void {
* @param {Number[]} steps
* @param {Number} value
*
* @returns {Number}
*
* @private
*/
private _setWeight(steps, value) {
const key = create_key(steps); const key = create_key(steps);
this.weights[key] = value; this.weights[key] = value;
} }
/** private _adjustWeight(steps: number[], weight: number): void {
* @param {Number[]} steps
* @param {Number} weight
*
* @private
*/
private _adjustWeight(steps, weight) {
const currentWeight = this._getWeight(steps); const currentWeight = this._getWeight(steps);
const newWeight = currentWeight + weight; const newWeight = currentWeight + weight;
this._setWeight(steps, newWeight); this._setWeight(steps, newWeight);

View File

@ -2,13 +2,20 @@
* Represents one game move. * Represents one game move.
*/ */
class Move { class Move {
public human: number; private readonly itsHuman: number;
private readonly itsRobot: number;
public robot: number;
constructor(human: number, robot: number) { constructor(human: number, robot: number) {
this.human = human; this.itsHuman = human;
this.robot = robot; this.itsRobot = robot;
}
get human() {
return this.itsHuman;
}
get robot() {
return this.itsRobot;
} }
} }

View File

@ -2,15 +2,35 @@ import Daemon from './Daemon';
import Journal from './Journal'; import Journal from './Journal';
import Supervisor from './Supervisor'; import Supervisor from './Supervisor';
const DEFAULT_CONFIG = { interface DaemonConfig {
id?: string;
human: number;
robot: number;
epsilon?: number;
}
interface PredictorConfig {
base: number;
supervisor_epsilon: number;
daemons: DaemonConfig[];
}
interface HistoryRecord {
score: number;
move: [number, number];
rates: { [id: string]: number };
weights: { [id: string]: any };
}
const DEFAULT_CONFIG: PredictorConfig = {
base: 2, base: 2,
supervisor_epsilon: 0.01, supervisor_epsilon: 0.01,
daemons: [ daemons: [
{ human: 2, robot: 2, epsilon: 0.01 }, { human: 2, robot: 2 },
{ human: 3, robot: 3, epsilon: 0.01 }, { human: 3, robot: 3 },
{ human: 4, robot: 4, epsilon: 0.01 }, { human: 4, robot: 4 },
{ human: 5, robot: 5, epsilon: 0.01 }, { human: 5, robot: 5 },
{ human: 6, robot: 6, epsilon: 0.01 }, { human: 6, robot: 6 },
], ],
}; };
@ -18,73 +38,70 @@ export default class Predictor {
/** /**
* @type {Number} * @type {Number}
*/ */
base; readonly base: number;
/** /**
* @type {Number} * @type {Number}
*/ */
score; score: number;
/** /**
* @type {Journal} * @type {Journal}
*/ */
journal; journal: Journal;
/** /**
* @type {Supervisor} * @type {Supervisor}
*/ */
supervisor; supervisor: Supervisor;
/** history: HistoryRecord[];
* @param {Object} config
*/ constructor(config: PredictorConfig = DEFAULT_CONFIG) {
constructor(config = DEFAULT_CONFIG) {
this.base = config.base; this.base = config.base;
this.score = 0; this.score = 0;
this.journal = new Journal(); this.journal = new Journal();
const daemons = this._createDaemons(config.daemons); const daemons = this._createDaemons(config.daemons);
this.supervisor = new Supervisor(daemons, config.supervisor_epsilon); this.supervisor = new Supervisor(daemons, config.supervisor_epsilon);
this.history = [];
} }
/** pass(humanValue: number): number {
* @param {Number|String} humanValue if (humanValue < 0 || humanValue >= this.base) {
*
* @returns {Number}
*/
pass(humanValue) {
const value = parseInt(humanValue, 10);
if (value < 0 || value >= this.base) {
throw new Error(`Passed value must be in [0, ${this.base})`); throw new Error(`Passed value must be in [0, ${this.base})`);
} }
const prediction = this.supervisor.predict(this.journal); const prediction = this.supervisor.predict(this.journal);
this.score += prediction === value ? -1 : 1; this.score += prediction === humanValue ? -1 : 1;
this.supervisor.adjust(this.journal, value); this.supervisor.adjust(this.journal, humanValue);
this.journal.makeMove(value, prediction); this.journal.makeMove(humanValue, prediction);
this.history.push({
score: this.score,
move: [humanValue, prediction],
rates: this.supervisor.rates(),
weights: this.supervisor.weights(),
});
return prediction; return prediction;
} }
/** showHistory(deep: number): HistoryRecord[] {
* @param {Object} daemonConfigs return this.history.slice(-deep);
* }
* @returns {Daemon[]}
* private _createDaemons(daemonConfigs: DaemonConfig[]): Daemon[] {
* @private
*/
_createDaemons(daemonConfigs) {
return daemonConfigs.map(config => { return daemonConfigs.map(config => {
const epsilon = config.epsilon || Daemon.DEFAULT_EPSILON;
return new Daemon( return new Daemon(
config.id ||
`daemon-${config.human}-${config.robot}-${epsilon}`,
this.base, this.base,
config.human, config.human,
config.robot, config.robot,
config.epsilon || 0.01 epsilon
); );
}); });
} }
/** stepCount(): number {
* @returns {Number}
*/
stepCount() {
return this.journal.length; return this.journal.length;
} }
} }

View File

@ -1,19 +1,34 @@
import Journal from './Journal'; import Journal from './Journal';
import Daemon from './Daemon'; import Daemon from './Daemon';
const DEFAULT_EPSILON = 0.01; interface DaemonRate {
daemon: Daemon;
rate: number;
}
interface Prediction {
daemonRate: DaemonRate;
rate: number;
power: number;
value: number;
}
class Supervisor { class Supervisor {
daemons: { daemon: Daemon; rate: number }[] = []; static DEFAULT_EPSILON = 0.01;
daemonRates: DaemonRate[] = [];
readonly epsilon: number; readonly epsilon: number;
constructor(daemons: Daemon[], epsilon: number = DEFAULT_EPSILON) { constructor(
daemons: Daemon[],
epsilon: number = Supervisor.DEFAULT_EPSILON
) {
if (!daemons || daemons.length === 0) { if (!daemons || daemons.length === 0) {
throw Error('Empty daemon list'); throw Error('Empty daemon list');
} }
this.daemons = daemons.map(daemon => ({ this.daemonRates = daemons.map(daemon => ({
daemon: daemon, daemon: daemon,
rate: 0, rate: 0,
})); }));
@ -21,11 +36,6 @@ class Supervisor {
this.epsilon = epsilon; this.epsilon = epsilon;
} }
/**
* @param {Journal} journal
*
* @returns {Number}
*/
predict(journal: Journal): number { predict(journal: Journal): number {
const predictions = this._createPredictions(journal); const predictions = this._createPredictions(journal);
const ordered = this._sortPredictions(predictions); const ordered = this._sortPredictions(predictions);
@ -33,47 +43,45 @@ class Supervisor {
return ordered[0].value; return ordered[0].value;
} }
/** adjust(journal: Journal, humanValue: number) {
* @param {Journal} journal
* @param {Number} humanValue
*/
adjust(journal: Journal, humanValue) {
const predictions = this._createPredictions(journal); const predictions = this._createPredictions(journal);
for (const prediction of predictions) { for (const prediction of predictions) {
if (prediction.value === humanValue) { if (prediction.value === humanValue) {
prediction.daemon.rate += this._getAdjustmentWeight( prediction.daemonRate.rate += this._getAdjustmentWeight(
journal.length journal.length
); );
} }
prediction.daemon.daemon.adjust(journal, humanValue); prediction.daemonRate.daemon.adjust(journal, humanValue);
} }
} }
/** rates() {
* @param {Journal} journal const result = {};
* this.daemonRates.forEach(r => {
* @returns {Array} result[r.daemon.id] = r.rate;
* });
* @private return result;
*/ }
private _createPredictions(journal: Journal) {
return this.daemons.map(daemon => ({ weights() {
daemon: daemon, const result = {};
power: daemon.daemon.power, this.daemonRates.forEach(r => {
rate: daemon.rate, result[r.daemon.id] = r.daemon.getWeights();
value: daemon.daemon.predict(journal), });
return result;
}
private _createPredictions(journal: Journal): Prediction[] {
return this.daemonRates.map(daemonRate => ({
daemonRate: daemonRate,
power: daemonRate.daemon.power,
rate: daemonRate.rate,
value: daemonRate.daemon.predict(journal),
})); }));
} }
/** private _sortPredictions(predictions: Prediction[]) {
* @param {Array} predictions return predictions.sort((result1: Prediction, result2: Prediction) => {
*
* @returns {Array}
*
* @private
*/
private _sortPredictions(predictions) {
return predictions.sort((result1, result2) => {
const rateDiff = result2.rate - result1.rate; const rateDiff = result2.rate - result1.rate;
if (Math.abs(rateDiff) > 0.000001) { if (Math.abs(rateDiff) > 0.000001) {
return rateDiff; return rateDiff;

View File

@ -3,50 +3,61 @@ import { expect } from 'chai';
import Daemon from '../src/Daemon'; import Daemon from '../src/Daemon';
import Journal from '../src/Journal'; import Journal from '../src/Journal';
import Move from '../src/Move';
// it('Get prediction for beginning', function() { describe('Daemon', function() {
// const m = new Journal(); it('Get prediction for beginning', function() {
// const d = new Daemon(2, 1, 1); const daemon = new Daemon('d1', 2, 1, 1);
// const predicted = d.predict(m); expect('d1').to.equals(daemon.id);
// expect(predicted).to.equals(0);
// });
it('Can get power', function() { const predicted = daemon.predict(new Journal());
const d = new Daemon(2, 5, 8); expect(0).to.equals(predicted);
expect(d.power).to.eqls(13); });
it('Can get power', function() {
const d = new Daemon('d1', 2, 5, 8);
expect(13).to.eqls(d.power);
});
it('Can adjust', function() {
const journal = new Journal([new Move(0, 0), new Move(0, 0)]);
const daemon = new Daemon('d1', 2, 1, 1, 0.1);
daemon.adjust(journal, 1);
expect({ '0:0:1': 1.1 ** 2 }).to.eqls(daemon.getWeights());
});
it('Daemon 1-1', function() {
const journal = new Journal();
const daemon = new Daemon('d1', 2, 1, 1, 0.1);
const steps = [
{
prediction: 0,
human: 1,
},
{
prediction: 0,
human: 1,
},
{
prediction: 1,
human: 1,
},
{
prediction: 0,
human: 1,
},
{
prediction: 1,
human: 1,
},
];
steps.forEach(step => {
const prediction = daemon.predict(journal);
expect(prediction).to.eqls(step.prediction);
daemon.adjust(journal, step.human);
journal.makeMove(step.human, step.prediction);
});
});
}); });
// it('Daemon 1-1', function() {
// const m = new Journal();
// const d = new Daemon(2, 1, 1);
//
// const steps = [
// {
// prediction: 0,
// human: 1,
// },
// {
// prediction: 0,
// human: 1,
// },
// {
// prediction: 1,
// human: 1,
// },
// {
// prediction: 0,
// human: 1,
// },
// {
// prediction: 1,
// human: 1,
// },
// ];
//
// steps.forEach(step => {
// const prediction = d.predict(m);
// expect(prediction).to.eqls(step.prediction);
// d.adjust(m, step.human);
// m.makeMove(step.human, step.prediction);
// });
// });

View File

@ -4,29 +4,34 @@ import { expect } from 'chai';
import Journal from '../src/Journal'; import Journal from '../src/Journal';
import Move from '../src/Move'; import Move from '../src/Move';
it('Create with empty constructor', function() { describe('Journal', function() {
const m = new Journal(); it('Create with empty constructor', function() {
expect(m.getLastMovements(5, 5)).to.eqls([]); const journal = new Journal();
}); expect(journal.length).to.equals(0);
expect(journal.getLastMovements(5, 5)).to.eqls([]);
});
it('Constructor with human steps', function() { it('Constructor with human steps', function() {
const m = new Journal([new Move(1, 1)]); const journal = new Journal([new Move(1, 1)]);
expect(m.getLastMovements(5, 5)).to.eqls([1, 1]); expect(journal.length).equals(1);
}); expect(journal.getLastMovements(5, 5)).to.eqls([1, 1]);
});
it('Make steps', function() { it('Make steps', function() {
const m = new Journal(); const journal = new Journal();
m.makeMove(1, 0); journal.makeMove(1, 0);
expect(m.getLastMovements(5, 5)).to.eqls([0, 1]); journal.makeMove(1, 1);
}); expect(journal.length).to.equals(2);
expect(journal.getLastMovements(2, 2)).to.eqls([0, 1, 1, 1]);
});
it('Get slice', function() { it('Get slice', function() {
const m = new Journal([ const m = new Journal([
new Move(1, 1), new Move(1, 1),
new Move(0, 1), new Move(0, 1),
new Move(0, 1), new Move(0, 1),
new Move(1, 0), new Move(1, 0),
]); ]);
expect(m.getLastMovements(2, 2)).to.eqls([1, 0, 0, 1]);
expect(m.getLastMovements(2, 2)).to.eqls([1, 0, 0, 1]); });
}); });

17
tests/PredictorTest.ts Normal file
View File

@ -0,0 +1,17 @@
import { it, describe } from 'mocha';
import { expect } from 'chai';
import Predictor from '../src/Predictor';
describe('Predictor', function() {
it('Get prediction for one daemon state', function() {
const predictor = new Predictor({
base: 2,
supervisor_epsilon: 0.01,
daemons: [{ robot: 1, human: 1, epsilon: 0.01 }],
});
const predicted = predictor.pass(1);
expect(predicted).to.equals(0);
expect(predictor.stepCount()).to.equals(1);
});
});

35
tests/SupervisorTest.ts Normal file
View File

@ -0,0 +1,35 @@
import { it, describe } from 'mocha';
import { expect } from 'chai';
import Supervisor from '../src/Supervisor';
import Daemon from '../src/Daemon';
import Journal from '../src/Journal';
describe('Supervisor', function() {
it('Get prediction for one daemon state', function() {
const supervisor = new Supervisor(
[new Daemon('d1', 2, 1, 1, 0.1)],
0.1
);
const journal = new Journal();
const human1 = 1;
const predicted1 = supervisor.predict(journal);
expect(0).to.equals(predicted1, 'First prediction for empty journal');
journal.makeMove(human1, predicted1);
supervisor.adjust(journal, human1);
const human2 = 1;
const predicted2 = supervisor.predict(journal);
expect(1).to.equals(
predicted2,
`Second prediction for (${human1}, ${predicted1})`
);
journal.makeMove(human2, predicted1);
supervisor.adjust(journal, human2);
expect({ d1: 1.1 ** 2 }).to.eqls(supervisor.rates());
});
});

View File

@ -4,13 +4,18 @@ set -eu
source .env source .env
TTY=
if [ -t 1 ] ; then
TTY=--tty
fi
docker run \ docker run \
--rm \ --rm \
--interactive \ --interactive \
--tty \ ${TTY} \
--init \ --init \
--user "$(id -u):$(id -g)" \ --user "$(id -u):$(id -g)" \
--volume "$PWD:/srv/app" \ --volume "$PWD:/app" \
--workdir /srv/app \ --workdir /app \
${NODE_IMAGE} \ ${NODE_IMAGE} \
node "$@" node "$@"

View File

@ -9,14 +9,19 @@ CONTAINER_CACHE_DIR=/tmp/.npm
mkdir -p ${HOST_CACHE_DIR} mkdir -p ${HOST_CACHE_DIR}
TTY=
if [ -t 1 ] ; then
TTY=--tty
fi
docker run \ docker run \
--rm \ --rm \
--interactive \ --interactive \
--tty \ ${TTY} \
--init \ --init \
--user "$UID:$(id -g)" \ --user "$UID:$(id -g)" \
--volume "$PWD:/srv/app" \ --volume "$PWD:/app" \
--env npm_config_cache="${CONTAINER_CACHE_DIR}" \ --env npm_config_cache="${CONTAINER_CACHE_DIR}" \
--workdir /srv/app \ --workdir /app \
${NODE_IMAGE} \ ${NODE_IMAGE} \
npm "$@" npm "$@"

View File

@ -10,7 +10,7 @@ docker run \
--tty \ --tty \
--init \ --init \
--user "$(id -u):$(id -g)" \ --user "$(id -u):$(id -g)" \
--volume "$PWD:/srv/app" \ --volume "$PWD:/app" \
--workdir /srv/app \ --workdir /app \
${NODE_IMAGE} \ ${NODE_IMAGE} \
./node_modules/.bin/tsc "$@" ./node_modules/.bin/tsc "$@"