diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 70bf5e4..d7931e4 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -4,6 +4,12 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@anwinged/predictor": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@anwinged/predictor/-/predictor-0.2.1.tgz", + "integrity": "sha512-817M9xiPesxLtvUH/qZNs3EBNw5HBOR/W8T3HYLqyNvMRCEJ/h86uJJB5BW+FzZc+mQsVQcRX+NI8pSOBr3jwg==", + "dev": true + }, "@babel/code-frame": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.0.tgz", diff --git a/package.json b/package.json index bb9bdb9..7425261 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "version": "1.0.0", "description": "Homepage", "devDependencies": { + "@anwinged/predictor": "^0.2.1", "@babel/core": "^7.8.0", "@babel/plugin-proposal-class-properties": "^7.8.0", "@babel/plugin-transform-runtime": "^7.8.0", diff --git a/source/_articles/2019-05-01-predictor.md b/source/_articles/2019-05-01-predictor.md index a588622..3d8cc19 100644 --- a/source/_articles/2019-05-01-predictor.md +++ b/source/_articles/2019-05-01-predictor.md @@ -8,17 +8,130 @@ scripts: - /static/predictor.js --- -Правила игры простые. Робот пытается предсказать, что выберет человек: чет или нечет. -Если предсказание удалось, робот получает очко, а счет уменьшается на единицу. -Если же предугадать выбор не удалось, то очко достается человеку, а счет увеличивается. -Игрок победит, если доберется до 50 очков. Но когда счет опустится до -50, победа будет -за железякой. +В студенческое время я наткнулся на интересную статью об [игре "Чет-нечет"](game) +на домашней страничке пользователя [ltwood](ltwood). -Чтобы выбирать вариант с клавиатуры, кликните внутри серой рамки, -а потом пользуйтесь клавишами "1" - нечет или "2" - чет. +Правила очень простые. Игрок загадывает один вариант из двух: "чет" или "нечет", +а оппонент пытается угадать выбор игрока. Если угадать не удалось, то очко получает +загадавший, а если угадать получилось - то угадывающий. Кто первым наберет 20 очков, +тот и молодец! -- [Код гадалки](https://github.com/anwinged/homepage/blob/master/source/_assets/predictor/demo.vue) +Кажется, что в этой игре все случайно. Случайно загадывается число, потом случайно +второй игрок пытается угадать что же было загадано. Я очень сильно удивился, когда +попробовал поиграть в эту игру с программой и за десять попыток так ни разу и не выиграл. + +Парадокс в том, что мы _думаем_ что загадываем числа случайно. На самом деле все не так, +и последовательность загаданных чисел не случайна. + +Исходного кода оригинальной гадалки в открытом доступе нет, есть только [описание алгоритма](algo), +по которому я сделал свою реализацию на TypeScrypt. + +## Демоверсия + +Попробуйте набрать 50 очков и выиграть. Чтобы выбирать вариант с клавиатуры, +кликните внутри серой рамки, а потом пользуйтесь клавишами "1" - нечет или "2" - чет. ---
+ +--- + +## Как Это работает + +Математически алгоритм на [странице](algo) сайта ltwood. +Я рассмотрю простой пример, чтобы показать принцип. + +В основе алгоритма находится популяция "демонов" - автоматов, которые на основании ходов +игрока и предсказанных значениях выдают новое предсказание. Демонами управляет +супервайзер. Задача супервайзера в том, чтобы опросить всех демонов, выбрать ответ +от одного их них, а после получения ответа игрока пометить тех, кто выдал правильный ответ. + +Алгоритм состоит из двух шагов: + +- предсказать следующих ход игрока; +- учесть реальный ход игрока, добавив веса тому демону, который предугадал ход. + +Рассмотрим работу на примере одного демона. + +Пусть у нас есть демон, который смотрит на последний хода игрока +и на свое последнее предсказание. + +Строим два вектора: + +- `[<1 ход демона>, <1 ход игрока>, 0]` +- `[<1 ход демона>, <1 ход игрока>, 1]` + +В самом начале, когда у демона нет никакой информации о ходах игрока, эти векторы +будут выглядеть как `[0]` и `[1]`. Но с накоплением данных, они всегда будут каждый +по 5 элементов. + +После чего смотрим, который из таких наборов в прошлом приносил победу чаще, +и соответственно выбираем или вариант с 0, или с 1. + +После получения действительного хода игрока, мы увеличиваем вес того набора, +который оказался верным. И далее снова предсказываем ход. + +Теперь с числами. + +#### Ход 1 + +У демона нет информации, наборы `[0]` и `[1]` равнозначны, выбираем `[0]`, +а значит предсказываем ход игрока 0. + +Игрок загадывал 1. Обновляем веса: + +``` +[0] = 0 +[1] = 1 +``` + +#### Ход 2 + +Строим векторы на основе последних ходов: + +``` +0: [0, 1, 0] +1: [0, 1, 1] +``` + +Для этих векторов тоже еще нет весов, так что снова выбираем первый, предсказываем 0. + +Игрок снова выбрал 1. Обновляем веса (помним, что еще были прошлые вектора из одного элемента): + +``` +[0] = 0 +[1] = 1 +[0, 1, 0] = 0 +[0, 1, 1] = 1 +``` + +#### Ход 3 + +Картина такая же, как на втором ходу, но отличие в том, что у нас есть веса с прошлого хода: + +``` +0: [0, 1, 0] - 0 +1: [0, 1, 1] - 1 +``` + +Выбираем вариант 1, игрок снова выбирает 1. Предсказание удалось! + +## Расширение алгоритма + +Это был самый элементарный вариант. Понятно, что на таком далеко не уедешь, +и никого не обыграешь. Чтобы хорошо предугадывать ходы игроков, используется +несколько демонов с разной величиной просматриваемой истории. Следит за ними +"супервайзер", который ведет для каждого демона рейтинг. На основе этого рейтинга +выбираются ответы тех демонов, которые были наиболее успешны в своих предсказаниях. + +## Ссылки + +- [Код гадалки](repo) +- [Описание алгоритма](algo) +- [Описание игры у ltwood](game) + +[ltwood]: https://sites.google.com/site/ltwood/ +[game]: https://sites.google.com/site/ltwood/projects/heshby +[algo]: https://sites.google.com/site/ltwood/projects/heshby/algorithm +[repo]: https://github.com/anwinged/predictor diff --git a/source/_assets/predictor/Daemon.js b/source/_assets/predictor/Daemon.js deleted file mode 100644 index f81deb2..0000000 --- a/source/_assets/predictor/Daemon.js +++ /dev/null @@ -1,145 +0,0 @@ -const DEFAULT_EPSILON = 0.01; - -/** - * @param {Number[]} steps - * - * @returns {String} - */ -function create_key(steps) { - return steps.join(':'); -} - -class Daemon { - /** - * @type {Number} - */ - base; - - /** - * @type {Number} - */ - humanCount; - - /** - * @type {Number} - */ - robotCount; - - /** - * @type {Number} - */ - epsilon; - - /** - * @type {Object} - */ - weights = {}; - - /** - * @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 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/_assets/predictor/Journal.js b/source/_assets/predictor/Journal.js deleted file mode 100644 index 86584bd..0000000 --- a/source/_assets/predictor/Journal.js +++ /dev/null @@ -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; diff --git a/source/_assets/predictor/Move.js b/source/_assets/predictor/Move.js deleted file mode 100644 index 292735c..0000000 --- a/source/_assets/predictor/Move.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Represents one game move. - */ -class Move { - /** - * @param {Number} human - * @param {Number} robot - */ - constructor(human, robot) { - this.human = human; - this.robot = robot; - } -} - -export default Move; diff --git a/source/_assets/predictor/Predictor.js b/source/_assets/predictor/Predictor.js deleted file mode 100644 index fe3b397..0000000 --- a/source/_assets/predictor/Predictor.js +++ /dev/null @@ -1,90 +0,0 @@ -import Daemon from './Daemon'; -import Journal from './Journal'; -import Supervisor from './Supervisor'; - -const DEFAULT_CONFIG = { - base: 2, - supervisor_epsilon: 0.01, - daemons: [ - { 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 }, - ], -}; - -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 = this._createDaemons(config.daemons); - this.supervisor = new Supervisor(daemons, config.supervisor_epsilon); - } - - /** - * @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); - this.journal.makeMove(value, prediction); - 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/_assets/predictor/Supervisor.js b/source/_assets/predictor/Supervisor.js deleted file mode 100644 index faaa0cd..0000000 --- a/source/_assets/predictor/Supervisor.js +++ /dev/null @@ -1,102 +0,0 @@ -const DEFAULT_EPSILON = 0.01; - -class Supervisor { - /** - * @type {{daemon: Daemon, rate: Number}[]} - */ - daemons = []; - - /** - * @type {Number} - */ - epsilon; - - /** - * @param {Daemon[]} daemons - * @param {Number} epsilon - */ - constructor(daemons, epsilon = DEFAULT_EPSILON) { - if (!daemons || daemons.length === 0) { - throw Error('Empty daemon list'); - } - this.daemons = daemons.map(daemon => ({ - daemon: daemon, - rate: 0, - })); - this.epsilon = epsilon; - } - - /** - * @param {Journal} journal - * - * @returns {Number} - */ - predict(journal) { - const predictions = this._createPredictions(journal); - const ordered = this._sortPredictions(predictions); - - return ordered[0].value; - } - - /** - * @param {Journal} journal - * @param {Number} humanValue - */ - adjust(journal, humanValue) { - const predictions = this._createPredictions(journal); - for (const prediction of predictions) { - if (prediction.value === humanValue) { - prediction.daemon.rate += this._getAdjustmentWeight( - journal.length - ); - } - prediction.daemon.daemon.adjust(journal, humanValue); - } - } - - /** - * @param {Journal} journal - * - * @returns {Array} - * - * @private - */ - _createPredictions(journal) { - return this.daemons.map(daemon => ({ - daemon: daemon, - power: daemon.daemon.power, - rate: daemon.rate, - value: daemon.daemon.predict(journal), - })); - } - - /** - * @param {Array} predictions - * - * @returns {Array} - * - * @private - */ - _sortPredictions(predictions) { - return predictions.sort((result1, result2) => { - const rateDiff = result2.rate - result1.rate; - if (Math.abs(rateDiff) > 0.000001) { - return rateDiff; - } - return result1.power - result2.power; - }); - } - - /** - * @param {Number} stepNumber - * - * @returns {Number} - * - * @private - */ - _getAdjustmentWeight(stepNumber) { - return Math.pow(1 + this.epsilon, stepNumber); - } -} - -export default Supervisor; diff --git a/source/_assets/predictor/demo.vue b/source/_assets/predictor/demo.vue index 1d964f4..4a0fdbb 100644 --- a/source/_assets/predictor/demo.vue +++ b/source/_assets/predictor/demo.vue @@ -39,7 +39,7 @@