Rewrite lib with typescript

This commit is contained in:
Anton Vakhrushev 2020-03-21 17:59:41 +03:00
parent 474dae9748
commit 4bdd38ec15
28 changed files with 2374 additions and 7718 deletions

View File

@ -1,4 +0,0 @@
{
"presets": ["env"],
"plugins": ["transform-class-properties"]
}

2
.env
View File

@ -1 +1 @@
NODE_IMAGE=node:10 NODE_IMAGE=node:12.16.1-alpine3.11

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
.idea/ .idea/
.nyc_output/
built/
coverage/ coverage/
dist/
node_modules/ node_modules/
var/ var/

5
.mocharc.json Normal file
View File

@ -0,0 +1,5 @@
{
"extension": ["ts"],
"spec": "tests/**/*.ts",
"require": "ts-node/register"
}

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
all: test build build-dev
build:
tools/npm run-script build
build-dev:
tools/npm run-script build:dev
format:
tools/npm run-script format
test:
tools/npm run test
coverage:
tools/npm run coverage
publish:
tools/npm publish --access public

File diff suppressed because one or more lines are too long

9453
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,35 @@
{ {
"name": "predictor", "name": "@anwinged/predictor",
"version": "1.0.0", "version": "0.1.0",
"description": "", "description": "",
"module": "lib/predictor.js", "author": "Anton Vakhrushev",
"license": "MIT",
"main": "built/predictor.js",
"repository": {
"type": "git",
"url": "https://github.com/anwinged/predictor.git"
},
"scripts": { "scripts": {
"test": "jest", "test": "mocha",
"coverage": "nyc mocha",
"build:dev": "webpack", "build:dev": "webpack",
"build": "WEBPACK_ENV=build webpack", "build": "WEBPACK_ENV=build webpack",
"start:dev": "webpack-dev-server", "format": "prettier --tab-width=4 --single-quote --trailing-comma es5 --write '{src,tests}/**/*.{ts,js}'",
"format": "prettier --tab-width=4 --single-quote --trailing-comma es5 --write '{source,tests}/**/*.js'",
"format-md": "prettier --write './*.md'" "format-md": "prettier --write './*.md'"
}, },
"author": "",
"license": "ISC",
"devDependencies": { "devDependencies": {
"babel": "^6.23.0", "@types/chai": "^4.2.11",
"babel-jest": "^22.4.4", "@types/mocha": "^7.0.2",
"babel-loader": "^7.1.5", "@types/node": "^13.9.2",
"babel-plugin-transform-class-properties": "^6.24.1", "chai": "^4.2.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"bable-loader": "0.0.1-security",
"css-loader": "^0.28.11",
"deepmerge": "^2.2.1", "deepmerge": "^2.2.1",
"jest": "^22.4.4", "mocha": "^7.1.1",
"prettier": "^1.18.2", "nyc": "^15.0.0",
"sass-loader": "^7.1.0", "prettier": "^1.19.1",
"style-loader": "^0.21.0", "ts-loader": "^6.2.1",
"vue": "^2.6.10", "ts-node": "^8.7.0",
"webpack": "^4.39.1", "typescript": "^3.8.3",
"webpack-cli": "^3.3.6", "webpack": "^4.42.0",
"webpack-dev-server": "^3.7.2" "webpack-cli": "^3.3.11"
},
"jest": {
"collectCoverage": true,
"collectCoverageFrom": [
"**/source/**/*.js",
"!**/source/build.js",
"!**/source/index.js"
],
"testMatch": [
"**/tests/**/*.js"
]
} }
} }

View File

@ -1,47 +0,0 @@
import Move from './Move';
class Journal {
/**
* @type {Move[]}
*/
moves = [];
/**
* @param {Move[]} moves
*/
constructor(moves = []) {
this.moves = moves;
}
/**
* @param {Number} human
* @param {Number} robot
*/
makeMove(human, robot) {
this.moves.push(new Move(human, robot));
}
/**
* @param {Number} humanCount
* @param {Number} robotCount
*
* @returns {Number[]}
*/
getLastMovements(humanCount, robotCount) {
const humanMoves = this.moves.map(m => m.human);
const robotMoves = this.moves.map(m => m.robot);
return [].concat(
robotMoves.slice(-robotCount),
humanMoves.slice(-humanCount)
);
}
/**
* @returns {Number}
*/
get length() {
return this.moves.length;
}
}
export default Journal;

View File

@ -1,32 +0,0 @@
import Vue from 'vue';
import './style.css';
import Predictor from './Predictor';
new Vue({
el: '#app',
data: {
predictor: new Predictor({
base: 3,
daemons: [
{ human: 3, robot: 3 },
{ human: 4, robot: 4 },
{ human: 5, robot: 5 },
],
}),
},
methods: {
click(v) {
const value = parseInt(v, 10);
this.pass(value);
},
press(evt) {
const value = parseInt(evt.key, 10) - 1;
this.pass(value);
},
pass(value) {
const prediction = this.predictor.pass(value);
const step = this.predictor.stepCount();
console.log('STEP', step, 'PREDICTED', prediction, 'PASS', value);
},
},
});

View File

@ -1,20 +0,0 @@
html, body {
width: 100vw;
}
.app {
display: block;
margin: 0 auto;
width: 200px;
text-align: center;
}
.score {
font-size: 300%;
margin: 2em auto 0.8em;
display: inline-block;
}
button {
display: inline-block;
}

View File

@ -1,11 +1,8 @@
import Journal from './Journal';
const DEFAULT_EPSILON = 0.01; const DEFAULT_EPSILON = 0.01;
/** function create_key(steps: number[]): string {
* @param {Number[]} steps
*
* @returns {String}
*/
function create_key(steps) {
return steps.join(':'); return steps.join(':');
} }
@ -35,37 +32,29 @@ class Daemon {
*/ */
weights = {}; weights = {};
/** constructor(
* @param {Number} base base: number,
* @param {Number} humanCount humanCount: number,
* @param {Number} robotCount robotCount: number,
* @param {Number} epsilon epsilon: number = DEFAULT_EPSILON
*/ ) {
constructor(base, humanCount, robotCount, epsilon = DEFAULT_EPSILON) {
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 power(): number {
* @returns {Number}
*/
get power() {
return this.humanCount + this.robotCount; return this.humanCount + this.robotCount;
} }
/** predict(journal: Journal): number {
* @param {Journal} journal
*
* @returns {Number}
*/
predict(journal) {
const steps = this._getStepSlice(journal); const steps = this._getStepSlice(journal);
const proposals = []; const proposals: number[] = [];
for (let i = 0; i < this.base; ++i) { for (let i = 0; i < this.base; ++i) {
proposals[i] = this._getWeight([...steps, i]); const weight = this._getWeight([...steps, i]);
proposals.push(weight);
} }
const maxWeight = Math.max(...proposals); const maxWeight = Math.max(...proposals);
@ -88,7 +77,7 @@ class Daemon {
* *
* @returns {Number[]} * @returns {Number[]}
*/ */
_getStepSlice(journal) { private _getStepSlice(journal) {
return journal.getLastMovements(this.humanCount, this.robotCount); return journal.getLastMovements(this.humanCount, this.robotCount);
} }
@ -99,7 +88,7 @@ class Daemon {
* *
* @private * @private
*/ */
_getAdjustmentWeight(stepNumber) { private _getAdjustmentWeight(stepNumber) {
return Math.pow(1 + this.epsilon, stepNumber); return Math.pow(1 + this.epsilon, stepNumber);
} }
@ -110,10 +99,10 @@ class Daemon {
* *
* @private * @private
*/ */
_getWeight(steps) { private _getWeight(steps: number[]): number {
const key = create_key(steps); const key = create_key(steps);
const weight = this.weights[key]; const weight = this.weights[key];
return weight === undefined ? 0 : weight; return weight as number;
} }
/** /**
@ -124,7 +113,7 @@ class Daemon {
* *
* @private * @private
*/ */
_setWeight(steps, value) { private _setWeight(steps, value) {
const key = create_key(steps); const key = create_key(steps);
this.weights[key] = value; this.weights[key] = value;
} }
@ -135,7 +124,7 @@ class Daemon {
* *
* @private * @private
*/ */
_adjustWeight(steps, weight) { 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);

28
src/Journal.ts Normal file
View File

@ -0,0 +1,28 @@
import Move from './Move';
class Journal {
moves: Move[] = [];
constructor(moves: Move[] = []) {
this.moves = moves;
}
makeMove(human: number, robot: number): void {
this.moves.push(new Move(human, robot));
}
getLastMovements(humanCount: number, robotCount: number): number[] {
const humanMoves = this.moves.map(m => m.human);
const robotMoves = this.moves.map(m => m.robot);
return [
...robotMoves.slice(-robotCount),
...humanMoves.slice(-humanCount),
];
}
get length(): number {
return this.moves.length;
}
}
export default Journal;

View File

@ -2,11 +2,11 @@
* Represents one game move. * Represents one game move.
*/ */
class Move { class Move {
/** public human: number;
* @param {Number} human
* @param {Number} robot public robot: number;
*/
constructor(human, robot) { constructor(human: number, robot: number) {
this.human = human; this.human = human;
this.robot = robot; this.robot = robot;
} }

View File

@ -1,28 +1,23 @@
import Journal from './Journal';
import Daemon from './Daemon';
const DEFAULT_EPSILON = 0.01; const DEFAULT_EPSILON = 0.01;
class Supervisor { class Supervisor {
/** daemons: { daemon: Daemon; rate: number }[] = [];
* @type {{daemon: Daemon, rate: Number}[]}
*/
daemons = [];
/** readonly epsilon: number;
* @type {Number}
*/
epsilon;
/** constructor(daemons: Daemon[], epsilon: number = DEFAULT_EPSILON) {
* @param {Daemon[]} daemons
* @param {Number} epsilon
*/
constructor(daemons, epsilon = 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.daemons = daemons.map(daemon => ({
daemon: daemon, daemon: daemon,
rate: 0, rate: 0,
})); }));
this.epsilon = epsilon; this.epsilon = epsilon;
} }
@ -31,7 +26,7 @@ class Supervisor {
* *
* @returns {Number} * @returns {Number}
*/ */
predict(journal) { predict(journal: Journal): number {
const predictions = this._createPredictions(journal); const predictions = this._createPredictions(journal);
const ordered = this._sortPredictions(predictions); const ordered = this._sortPredictions(predictions);
@ -42,7 +37,7 @@ class Supervisor {
* @param {Journal} journal * @param {Journal} journal
* @param {Number} humanValue * @param {Number} humanValue
*/ */
adjust(journal, 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) {
@ -61,7 +56,7 @@ class Supervisor {
* *
* @private * @private
*/ */
_createPredictions(journal) { private _createPredictions(journal: Journal) {
return this.daemons.map(daemon => ({ return this.daemons.map(daemon => ({
daemon: daemon, daemon: daemon,
power: daemon.daemon.power, power: daemon.daemon.power,
@ -77,7 +72,7 @@ class Supervisor {
* *
* @private * @private
*/ */
_sortPredictions(predictions) { private _sortPredictions(predictions) {
return predictions.sort((result1, result2) => { 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) {
@ -87,14 +82,7 @@ class Supervisor {
}); });
} }
/** private _getAdjustmentWeight(stepNumber: number): number {
* @param {Number} stepNumber
*
* @returns {Number}
*
* @private
*/
_getAdjustmentWeight(stepNumber) {
return Math.pow(1 + this.epsilon, stepNumber); return Math.pow(1 + this.epsilon, stepNumber);
} }
} }

View File

@ -1,49 +0,0 @@
import expect from 'expect';
import Daemon from '../source/Daemon';
import Journal from '../source/Journal';
test('Get prediction for beginning', function() {
const m = new Journal();
const d = new Daemon(2, 1, 1);
expect(d.predict(m)).toEqual(0);
});
test('Can get power', function() {
const d = new Daemon(2, 5, 8);
expect(d.power).toEqual(13);
});
test('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).toEqual(step.prediction);
d.adjust(m, step.human);
m.makeMove(step.human, step.prediction);
});
});

52
tests/DaemonTest.ts Normal file
View File

@ -0,0 +1,52 @@
import { it, describe } from 'mocha';
import { expect } from 'chai';
import Daemon from '../src/Daemon';
import Journal from '../src/Journal';
// 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(2, 5, 8);
expect(d.power).to.eqls(13);
});
// 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

@ -1,30 +0,0 @@
import Journal from '../source/Journal';
import Move from '../source/Move';
import expect from 'expect';
test('Create with empty constructor', function() {
const m = new Journal();
expect(m.getLastMovements(5, 5)).toEqual([]);
});
test('Constructor with human steps', function() {
const m = new Journal([new Move(1, 1)]);
expect(m.getLastMovements(5, 5)).toEqual([1, 1]);
});
test('Make steps', function() {
const m = new Journal();
m.makeMove(1, 0);
expect(m.getLastMovements(5, 5)).toEqual([0, 1]);
});
test('Get slice', function() {
const m = new Journal([
new Move(1, 1),
new Move(0, 1),
new Move(0, 1),
new Move(1, 0),
]);
expect(m.getLastMovements(2, 2)).toEqual([1, 0, 0, 1]);
});

32
tests/JourlanTest.ts Normal file
View File

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

View File

@ -1,5 +0,0 @@
#!/bin/bash
source .env
docker pull ${NODE_IMAGE}

16
tools/node Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -eu
source .env
docker run \
--rm \
--interactive \
--tty \
--init \
--user "$(id -u):$(id -g)" \
--volume "$PWD:/srv/app" \
--workdir /srv/app \
${NODE_IMAGE} \
node "$@"

View File

@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
set -eu
source .env source .env
HOST_CACHE_DIR=$PWD/var/docker-cache/.npm HOST_CACHE_DIR=$PWD/var/docker-cache/.npm
@ -14,10 +16,6 @@ docker run \
--init \ --init \
--user "$UID:$(id -g)" \ --user "$UID:$(id -g)" \
--volume "$PWD:/srv/app" \ --volume "$PWD:/srv/app" \
--volume "$HOME:$HOME" \
--volume "${HOST_CACHE_DIR}:${CONTAINER_CACHE_DIR}" \
--expose=9000 \
--publish=9000:9000 \
--env npm_config_cache="${CONTAINER_CACHE_DIR}" \ --env npm_config_cache="${CONTAINER_CACHE_DIR}" \
--workdir /srv/app \ --workdir /srv/app \
${NODE_IMAGE} \ ${NODE_IMAGE} \

16
tools/tsc Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -eu
source .env
docker run \
--rm \
--interactive \
--tty \
--init \
--user "$(id -u):$(id -g)" \
--volume "$PWD:/srv/app" \
--workdir /srv/app \
${NODE_IMAGE} \
./node_modules/.bin/tsc "$@"

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"outDir": "./built",
"allowJs": true,
"target": "es5",
"module": "commonjs",
"strictNullChecks": true,
"types": ["node", "mocha", "chai"]
},
"include": [
"./src/**/*"
]
}

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Predictor</title>
</head>
<body>
<main id="app" class="app" tabindex="0" v-on:keyup="press">
<span class="score">
{{ predictor.score }}
</span>
<div class="buttons">
<button value="0" v-on:click="click(0)">0</button>
<button value="1" v-on:click="click(1)">1</button>
<button value="2" v-on:click="click(2)">2</button>
</div>
</main>
<script src="dist/app.js"></script>
</body>
</html>

View File

@ -5,51 +5,43 @@ const baseConfig = {
module: { module: {
rules: [ rules: [
{ {
test: /\.js$/, test: /\.ts$/,
exclude: /node_modules/,
use: { use: {
loader: 'babel-loader', loader: 'ts-loader',
}, },
}, },
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
] ]
}, },
}; };
const buildConfig = { const buildConfig = {
mode: 'production', mode: 'production',
entry: path.resolve(__dirname, 'source/build.js'), entry: path.resolve(__dirname, 'src/index.ts'),
output: { output: {
filename: 'predictor.js', filename: 'predictor.min.js',
path: path.resolve(__dirname, 'lib'), path: path.resolve(__dirname, 'built'),
library: 'predictor', library: 'predictor',
libraryTarget: 'umd', libraryTarget: 'umd',
umdNamedDefine: true umdNamedDefine: true
}, },
resolve: {
extensions: ['.ts', '.js']
},
}; };
const devConfig = { const devConfig = {
mode: 'development', mode: 'development',
entry: path.resolve(__dirname, 'source/index.js'), entry: path.resolve(__dirname, 'src/index.ts'),
output: { output: {
filename: 'app.js', filename: 'predictor.js',
path: path.resolve(__dirname, 'web/dist'), path: path.resolve(__dirname, 'built'),
publicPath: 'dist/', library: 'predictor',
libraryTarget: 'umd',
umdNamedDefine: true
}, },
resolve: { resolve: {
alias: { extensions: ['.ts', '.js']
'vue$': 'vue/dist/vue.esm.js'
}
}, },
devServer: {
contentBase: path.resolve(__dirname, 'web'),
index: 'index.html',
host: "0.0.0.0",
port: 9000,
}
}; };
module.exports = merge( module.exports = merge(