Compare commits

..

No commits in common. "master" and "0.1.3" have entirely different histories.

16 changed files with 233 additions and 363 deletions

View File

@ -8,9 +8,7 @@ jobs:
- checkout
- run: npm ci
- run: npm run format-check
- run: npm run test:junit-report
- store_test_results:
path: test-results
- run: npm run test
build_and_publish:
docker:
@ -21,8 +19,7 @@ jobs:
- run: npm run build
- run: npm run build:dev
- run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc
- run: npm version --no-git-tag-version --force "${CIRCLE_TAG}"
- run: npm publish --access public
- run: npm publish --tag "${CIRCLE_TAG}" --access public
workflows:
version: 2

1
.gitignore vendored
View File

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

View File

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

53
package-lock.json generated
View File

@ -1119,12 +1119,6 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
@ -1959,12 +1953,6 @@
"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": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
@ -3588,17 +3576,6 @@
"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": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@ -3940,30 +3917,6 @@
}
}
},
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -6242,12 +6195,6 @@
"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": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

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

View File

@ -1,42 +1,49 @@
import Journal from './Journal';
const DEFAULT_EPSILON = 0.01;
function create_key(steps: number[]): string {
return steps.join(':');
}
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;
private readonly epsilon: number;
private weights: { [key: string]: number } = {};
/**
* @type {Object}
*/
weights = {};
constructor(
id: string,
base: number,
humanCount: number,
robotCount: number,
epsilon: number = Daemon.DEFAULT_EPSILON
epsilon: number = DEFAULT_EPSILON
) {
this.thisId = id;
this.base = base;
this.humanCount = humanCount;
this.robotCount = robotCount;
this.epsilon = epsilon;
}
get id(): string {
return this.thisId;
}
get power(): number {
return this.humanCount + this.robotCount;
}
@ -55,35 +62,69 @@ class Daemon {
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 adjustmentWeight = this._getAdjustmentWeight(journal.length);
this._adjustWeight([...steps, humanValue], adjustmentWeight);
}
getWeights() {
return { ...this.weights };
}
private _getStepSlice(journal: Journal): number[] {
/**
* @param {Journal} journal
*
* @returns {Number[]}
*/
private _getStepSlice(journal) {
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);
}
/**
* @param {Number[]} steps
*
* @returns {Number}
*
* @private
*/
private _getWeight(steps: number[]): number {
const key = create_key(steps);
return key in this.weights ? this.weights[key] : 0;
const weight = this.weights[key];
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);
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 newWeight = currentWeight + weight;
this._setWeight(steps, newWeight);

View File

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

View File

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

View File

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

View File

@ -3,61 +3,50 @@ import { expect } from 'chai';
import Daemon from '../src/Daemon';
import Journal from '../src/Journal';
import Move from '../src/Move';
describe('Daemon', function() {
it('Get prediction for beginning', function() {
const daemon = new Daemon('d1', 2, 1, 1);
expect('d1').to.equals(daemon.id);
const predicted = daemon.predict(new Journal());
expect(0).to.equals(predicted);
});
// it('Get prediction for beginning', function() {
// const m = new Journal();
// const d = new Daemon(2, 1, 1);
// const predicted = d.predict(m);
// expect(predicted).to.equals(0);
// });
it('Can get power', function() {
const d = new Daemon('d1', 2, 5, 8);
expect(13).to.eqls(d.power);
const d = new Daemon(2, 5, 8);
expect(d.power).to.eqls(13);
});
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,25 +4,20 @@ import { expect } from 'chai';
import Journal from '../src/Journal';
import Move from '../src/Move';
describe('Journal', function() {
it('Create with empty constructor', function() {
const journal = new Journal();
expect(journal.length).to.equals(0);
expect(journal.getLastMovements(5, 5)).to.eqls([]);
const m = new Journal();
expect(m.getLastMovements(5, 5)).to.eqls([]);
});
it('Constructor with human steps', function() {
const journal = new Journal([new Move(1, 1)]);
expect(journal.length).equals(1);
expect(journal.getLastMovements(5, 5)).to.eqls([1, 1]);
const m = new Journal([new Move(1, 1)]);
expect(m.getLastMovements(5, 5)).to.eqls([1, 1]);
});
it('Make steps', function() {
const journal = new Journal();
journal.makeMove(1, 0);
journal.makeMove(1, 1);
expect(journal.length).to.equals(2);
expect(journal.getLastMovements(2, 2)).to.eqls([0, 1, 1, 1]);
const m = new Journal();
m.makeMove(1, 0);
expect(m.getLastMovements(5, 5)).to.eqls([0, 1]);
});
it('Get slice', function() {
@ -32,6 +27,6 @@ describe('Journal', function() {
new Move(0, 1),
new Move(1, 0),
]);
expect(m.getLastMovements(2, 2)).to.eqls([1, 0, 0, 1]);
});
});

View File

@ -1,17 +0,0 @@
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);
});
});

View File

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

View File

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

View File

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