diff --git a/lib/predictor.js b/lib/predictor.js index 6f7339d..94226a0 100644 --- a/lib/predictor.js +++ b/lib/predictor.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("predictor",[],t):"object"==typeof exports?exports.predictor=t():e.predictor=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=5)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n2&&void 0!==arguments[2]?arguments[2]:u;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.weights={},this.humanCount=t,this.robotCount=n,this.epsilon=r}return r(e,[{key:"predict",value:function(e){var t=this._getStepSlice(e),n=this._getWeight([].concat(o(t),[0]));return this._getWeight([].concat(o(t),[1]))>n?1:0}},{key:"adjust",value:function(e,t){var n=this._getStepSlice(e),r=this._getAdjustmentWeight(e.length);this._adjustWeight([].concat(o(n),[t]),r)}},{key:"_getStepSlice",value:function(e){return e.getLastMovements(this.humanCount,this.robotCount)}},{key:"_getAdjustmentWeight",value:function(e){return Math.pow(1+this.epsilon,e)}},{key:"_getWeight",value:function(e){var t=i(e),n=this.weights[t];return void 0===n?0:n}},{key:"_setWeight",value:function(e,t){var n=i(e);this.weights[n]=t}},{key:"_adjustWeight",value:function(e,t){var n=this._getWeight(e)+t;this._setWeight(e,n)}},{key:"power",get:function(){return this.humanCount+this.robotCount}}]),e}();t.default=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:o;if(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.daemons=[],!t)throw Error("Empty daemon list");this.daemons=t.map(function(e){return{daemon:e,rate:0}}),this.epsilon=n}return r(e,[{key:"predict",value:function(e){var t=this._createPredictions(e);return this._sortPredictions(t)[0].value}},{key:"adjust",value:function(e,t){var n=this._createPredictions(e),r=!0,o=!1,u=void 0;try{for(var i,a=n[Symbol.iterator]();!(r=(i=a.next()).done);r=!0){var s=i.value;s.value===t&&(s.daemon.rate+=this._getAdjustmentWeight(e.length)),s.daemon.daemon.adjust(e,t)}}catch(e){o=!0,u=e}finally{try{!r&&a.return&&a.return()}finally{if(o)throw u}}}},{key:"_createPredictions",value:function(e){return this.daemons.map(function(t){return{daemon:t,power:t.daemon.power,rate:t.rate,value:t.daemon.predict(e)}})}},{key:"_sortPredictions",value:function(e){return e.sort(function(e,t){var n=t.rate-e.rate;return Math.abs(n)>1e-6?n:e.power-t.power})}},{key:"_getAdjustmentWeight",value:function(e){return Math.pow(1+this.epsilon,e)}}]),e}();t.default=u},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:[];!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.moves=[],this.moves=t}return o(e,[{key:"makeMove",value:function(e,t){this.moves.push(new i.default(e,t))}},{key:"getLastMovements",value:function(e,t){var n=this.moves.map(function(e){return e.human}),r=this.moves.map(function(e){return e.robot});return[].concat(r.slice(-t),n.slice(-e))}},{key:"length",get:function(){return this.moves.length}}]),e}();t.default=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:s;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.score=0,this.journal=new o.default;var n=t.daemons.map(function(e){return new i.default(e.human,e.robot,e.epsilon||.01)});this.supervisor=new u.default(n,t.supervisor_epsilon)}return r(e,[{key:"pass",value:function(e){var t=this.supervisor.predict(this.journal);return this.score+=t===e?-1:1,this.supervisor.adjust(this.journal,e),this.journal.makeMove(e,t),t}},{key:"stepCount",value:function(){return this.journal.length}}]),e}();t.default=c},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r,o=n(4),u=(r=o)&&r.__esModule?r:{default:r};t.default=u.default}])}); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("predictor",[],t):"object"==typeof exports?exports.predictor=t():e.predictor=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:r})},n.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=5)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:o;if(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.daemons=[],!t||0===t.length)throw Error("Empty daemon list");this.daemons=t.map(function(e){return{daemon:e,rate:0}}),this.epsilon=n}return r(e,[{key:"predict",value:function(e){var t=this._createPredictions(e);return this._sortPredictions(t)[0].value}},{key:"adjust",value:function(e,t){var n=this._createPredictions(e),r=!0,o=!1,i=void 0;try{for(var u,a=n[Symbol.iterator]();!(r=(u=a.next()).done);r=!0){var s=u.value;s.value===t&&(s.daemon.rate+=this._getAdjustmentWeight(e.length)),s.daemon.daemon.adjust(e,t)}}catch(e){o=!0,i=e}finally{try{!r&&a.return&&a.return()}finally{if(o)throw i}}}},{key:"_createPredictions",value:function(e){return this.daemons.map(function(t){return{daemon:t,power:t.daemon.power,rate:t.rate,value:t.daemon.predict(e)}})}},{key:"_sortPredictions",value:function(e){return e.sort(function(e,t){var n=t.rate-e.rate;return Math.abs(n)>1e-6?n:e.power-t.power})}},{key:"_getAdjustmentWeight",value:function(e){return Math.pow(1+this.epsilon,e)}}]),e}();t.default=i},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});t.default=function e(t,n){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.human=t,this.robot=n}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r,o=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:[];!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.moves=[],this.moves=t}return o(e,[{key:"makeMove",value:function(e,t){this.moves.push(new u.default(e,t))}},{key:"getLastMovements",value:function(e,t){var n=this.moves.map(function(e){return e.human}),r=this.moves.map(function(e){return e.robot});return[].concat(r.slice(-t),n.slice(-e))}},{key:"length",get:function(){return this.moves.length}}]),e}();t.default=a},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n3&&void 0!==arguments[3]?arguments[3]:i;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.weights={},this.base=t,this.humanCount=n,this.robotCount=r,this.epsilon=o}return r(e,[{key:"predict",value:function(e){for(var t=this._getStepSlice(e),n=[],r=0;r0&&void 0!==arguments[0]?arguments[0]:s;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),this.base=t.base,this.score=0,this.journal=new i.default;var n=this._createDaemons(t.daemons);this.supervisor=new u.default(n,t.supervisor_epsilon)}return r(e,[{key:"pass",value:function(e){var t=parseInt(e,10);if(t<0||t>=this.base)throw new Error("Passed value must be in [0, "+this.base+")");var n=this.supervisor.predict(this.journal);return this.score+=n===t?-1:1,this.supervisor.adjust(this.journal,t),this.journal.makeMove(t,n),n}},{key:"_createDaemons",value:function(e){var t=this;return e.map(function(e){return new o.default(t.base,e.human,e.robot,e.epsilon||.01)})}},{key:"stepCount",value:function(){return this.journal.length}}]),e}();t.default=c},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r,o=n(4),i=(r=o)&&r.__esModule?r:{default:r};t.default=i.default}])}); \ No newline at end of file diff --git a/package.json b/package.json index 3cdfa7c..1ea642e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "jest": { "collectCoverage": true, "collectCoverageFrom": [ - "**/source/*.js" + "**/source/**/*.js", + "!**/source/build.js", + "!**/source/index.js" ], "testMatch": [ "**/tests/**/*.js" diff --git a/source/Daemon.js b/source/Daemon.js index 52659bb..f81deb2 100644 --- a/source/Daemon.js +++ b/source/Daemon.js @@ -1,61 +1,145 @@ const DEFAULT_EPSILON = 0.01; +/** + * @param {Number[]} steps + * + * @returns {String} + */ function create_key(steps) { - return steps.join(''); + return steps.join(':'); } -export default class Daemon { +class Daemon { + /** + * @type {Number} + */ + base; + + /** + * @type {Number} + */ humanCount; + + /** + * @type {Number} + */ robotCount; + + /** + * @type {Number} + */ epsilon; + + /** + * @type {Object} + */ weights = {}; - constructor(humanCount, robotCount, epsilon = DEFAULT_EPSILON) { + /** + * @param {Number} base + * @param {Number} humanCount + * @param {Number} robotCount + * @param {Number} epsilon + */ + constructor(base, humanCount, robotCount, epsilon = DEFAULT_EPSILON) { + this.base = base; this.humanCount = humanCount; this.robotCount = robotCount; this.epsilon = epsilon; } + /** + * @returns {Number} + */ get power() { return this.humanCount + this.robotCount; } + /** + * @param {Journal} journal + * + * @returns {Number} + */ predict(journal) { const steps = this._getStepSlice(journal); - const w0 = this._getWeight([...steps, 0]); - const w1 = this._getWeight([...steps, 1]); - return w1 > w0 ? 1 : 0; + const proposals = []; + for (let i = 0; i < this.base; ++i) { + proposals[i] = this._getWeight([...steps, i]); + } + + const maxWeight = Math.max(...proposals); + + return proposals.indexOf(maxWeight); } + /** + * @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); } + /** + * @param {Journal} journal + * + * @returns {Number[]} + */ _getStepSlice(journal) { return journal.getLastMovements(this.humanCount, this.robotCount); } + /** + * @param {Number} stepNumber + * + * @returns {Number} + * + * @private + */ _getAdjustmentWeight(stepNumber) { return Math.pow(1 + this.epsilon, stepNumber); } + /** + * @param {Number[]} steps + * + * @returns {Number} + * + * @private + */ _getWeight(steps) { const key = create_key(steps); const weight = this.weights[key]; return weight === undefined ? 0 : weight; } + /** + * @param {Number[]} steps + * @param {Number} value + * + * @returns {Number} + * + * @private + */ _setWeight(steps, value) { const key = create_key(steps); this.weights[key] = value; } + /** + * @param {Number[]} steps + * @param {Number} weight + * + * @private + */ _adjustWeight(steps, weight) { const currentWeight = this._getWeight(steps); const newWeight = currentWeight + weight; this._setWeight(steps, newWeight); } } + +export default Daemon; diff --git a/source/Journal.js b/source/Journal.js index 350fe08..86584bd 100644 --- a/source/Journal.js +++ b/source/Journal.js @@ -1,16 +1,32 @@ import Move from './Move'; -export default class Journal { +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); @@ -20,7 +36,12 @@ export default class Journal { ); } + /** + * @returns {Number} + */ get length() { return this.moves.length; } } + +export default Journal; diff --git a/source/Move.js b/source/Move.js index c4c032d..292735c 100644 --- a/source/Move.js +++ b/source/Move.js @@ -1,14 +1,15 @@ -export default class Move { +/** + * Represents one game move. + */ +class Move { + /** + * @param {Number} human + * @param {Number} robot + */ constructor(human, robot) { - this._human = human ? 1 : 0; - this._robot = robot ? 1 : 0; - } - - get human() { - return this._human; - } - - get robot() { - return this._robot; + this.human = human; + this.robot = robot; } } + +export default Move; diff --git a/source/Predictor.js b/source/Predictor.js index a689371..fe3b397 100644 --- a/source/Predictor.js +++ b/source/Predictor.js @@ -1,8 +1,9 @@ +import Daemon from './Daemon'; import Journal from './Journal'; import Supervisor from './Supervisor'; -import Daemon from './Daemon'; const DEFAULT_CONFIG = { + base: 2, supervisor_epsilon: 0.01, daemons: [ { human: 2, robot: 2, epsilon: 0.01 }, @@ -14,24 +15,47 @@ const DEFAULT_CONFIG = { }; export default class Predictor { + /** + * @type {Number} + */ + base; + + /** + * @type {Number} + */ score; + + /** + * @type {Journal} + */ journal; + + /** + * @type {Supervisor} + */ supervisor; + /** + * @param {Object} config + */ constructor(config = DEFAULT_CONFIG) { + this.base = config.base; this.score = 0; this.journal = new Journal(); - const daemons = config.daemons.map(daemonConfig => { - return new Daemon( - daemonConfig.human, - daemonConfig.robot, - daemonConfig.epsilon || 0.01 - ); - }); + const daemons = this._createDaemons(config.daemons); this.supervisor = new Supervisor(daemons, config.supervisor_epsilon); } - pass(value) { + /** + * @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 === value ? -1 : 1; this.supervisor.adjust(this.journal, value); @@ -39,6 +63,27 @@ export default class Predictor { return prediction; } + /** + * @param {Object} daemonConfigs + * + * @returns {Daemon[]} + * + * @private + */ + _createDaemons(daemonConfigs) { + return daemonConfigs.map(config => { + return new Daemon( + this.base, + config.human, + config.robot, + config.epsilon || 0.01 + ); + }); + } + + /** + * @returns {Number} + */ stepCount() { return this.journal.length; } diff --git a/source/Supervisor.js b/source/Supervisor.js index 50ca98a..faaa0cd 100644 --- a/source/Supervisor.js +++ b/source/Supervisor.js @@ -1,11 +1,22 @@ const DEFAULT_EPSILON = 0.01; -export default class Supervisor { +class Supervisor { + /** + * @type {{daemon: Daemon, rate: Number}[]} + */ daemons = []; + + /** + * @type {Number} + */ epsilon; + /** + * @param {Daemon[]} daemons + * @param {Number} epsilon + */ constructor(daemons, epsilon = DEFAULT_EPSILON) { - if (!daemons) { + if (!daemons || daemons.length === 0) { throw Error('Empty daemon list'); } this.daemons = daemons.map(daemon => ({ @@ -15,6 +26,11 @@ export default class Supervisor { this.epsilon = epsilon; } + /** + * @param {Journal} journal + * + * @returns {Number} + */ predict(journal) { const predictions = this._createPredictions(journal); const ordered = this._sortPredictions(predictions); @@ -22,6 +38,10 @@ export default class Supervisor { return ordered[0].value; } + /** + * @param {Journal} journal + * @param {Number} humanValue + */ adjust(journal, humanValue) { const predictions = this._createPredictions(journal); for (const prediction of predictions) { @@ -34,6 +54,13 @@ export default class Supervisor { } } + /** + * @param {Journal} journal + * + * @returns {Array} + * + * @private + */ _createPredictions(journal) { return this.daemons.map(daemon => ({ daemon: daemon, @@ -43,6 +70,13 @@ export default class Supervisor { })); } + /** + * @param {Array} predictions + * + * @returns {Array} + * + * @private + */ _sortPredictions(predictions) { return predictions.sort((result1, result2) => { const rateDiff = result2.rate - result1.rate; @@ -53,7 +87,16 @@ export default class Supervisor { }); } + /** + * @param {Number} stepNumber + * + * @returns {Number} + * + * @private + */ _getAdjustmentWeight(stepNumber) { return Math.pow(1 + this.epsilon, stepNumber); } } + +export default Supervisor; diff --git a/source/index.js b/source/index.js index 0f69324..88dbb1d 100644 --- a/source/index.js +++ b/source/index.js @@ -6,6 +6,7 @@ new Vue({ el: '#app', data: { predictor: new Predictor({ + base: 3, daemons: [ { human: 3, robot: 3 }, { human: 4, robot: 4 }, @@ -15,11 +16,11 @@ new Vue({ }, methods: { click(v) { - const value = v ? 1 : 0; + const value = parseInt(v, 10); this.pass(value); }, press(evt) { - const value = evt.key === '1' ? 0 : 1; + const value = parseInt(evt.key, 10) - 1; this.pass(value); }, pass(value) { diff --git a/tests/DaemonTest.js b/tests/DaemonTest.js index 0dc8ba7..80279a3 100644 --- a/tests/DaemonTest.js +++ b/tests/DaemonTest.js @@ -1,21 +1,21 @@ -import Daemon from '../source/Daemon'; import expect from 'expect'; -import History from '../source/Journal'; +import Daemon from '../source/Daemon'; +import Journal from '../source/Journal'; test('Get prediction for beginning', function() { - const m = new History(); - const d = new Daemon(1, 1); + 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(5, 8); + const d = new Daemon(2, 5, 8); expect(d.power).toEqual(13); }); test('Daemon 1-1', function() { - const m = new History(); - const d = new Daemon(1, 1); + const m = new Journal(); + const d = new Daemon(2, 1, 1); const steps = [ { @@ -40,11 +40,10 @@ test('Daemon 1-1', function() { }, ]; - steps.forEach((step, index) => { + steps.forEach(step => { const prediction = d.predict(m); expect(prediction).toEqual(step.prediction); - d.adjust(m, step.human, index + 1); + d.adjust(m, step.human); m.makeMove(step.human, step.prediction); - console.log('Step', index + 1, d); }); }); diff --git a/web/index.html b/web/index.html index 8b1bdf8..e074b0b 100644 --- a/web/index.html +++ b/web/index.html @@ -11,6 +11,7 @@
+