Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
94f6fd3ae9 | |||
49bb3da4bb | |||
9e06a1c630 | |||
eb66b6904d | |||
4a18dc5808 | |||
54a5b0683d | |||
e3ea017315 | |||
28bde76ddd | |||
7a8b87530c | |||
4094badb20 |
@ -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:
|
||||||
@ -19,18 +21,22 @@ jobs:
|
|||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run build:dev
|
- run: npm run build:dev
|
||||||
- run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
- run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
|
||||||
- run: npm publish --tag "${CIRCLE_TAG}" --access public
|
- run: npm version --no-git-tag-version --force "${CIRCLE_TAG}"
|
||||||
|
- run: npm publish --access public
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
test_and_publish:
|
test_and_publish:
|
||||||
jobs:
|
jobs:
|
||||||
- test
|
- test:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
- build_and_publish:
|
- build_and_publish:
|
||||||
requires:
|
requires:
|
||||||
- test
|
- test
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /^\d+\.\d+\.\d+$/
|
only: /.*/
|
||||||
branches:
|
branches:
|
||||||
ignore: /.*/
|
ignore: /.*/
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,5 +3,6 @@
|
|||||||
built/
|
built/
|
||||||
coverage/
|
coverage/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
test-results/
|
||||||
var/
|
var/
|
||||||
.npmrc
|
.npmrc
|
||||||
|
12
README.md
12
README.md
@ -1,16 +1,18 @@
|
|||||||
# Электронная гадалка
|
# Электронная гадалка
|
||||||
|
|
||||||
|
[](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
53
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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);
|
||||||
|
17
src/Move.ts
17
src/Move.ts
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
@ -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
17
tests/PredictorTest.ts
Normal 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
35
tests/SupervisorTest.ts
Normal 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());
|
||||||
|
});
|
||||||
|
});
|
11
tools/node
11
tools/node
@ -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 "$@"
|
||||||
|
11
tools/npm
11
tools/npm
@ -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 "$@"
|
||||||
|
@ -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 "$@"
|
||||||
|
Loading…
Reference in New Issue
Block a user