diff --git a/.eslintrc b/.eslintrc index 1548530..99b2701 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,6 @@ { - "extends": "@kenan" + "extends": "@kenan", + "rules": { + "no-var": 0 + } } diff --git a/.renovaterc.json b/.renovaterc.json index 0fc502f..57650bd 100644 --- a/.renovaterc.json +++ b/.renovaterc.json @@ -1,5 +1,3 @@ { - "extends": [ - "@kenan" - ] + "extends": ["@kenan/renovate-config"] } diff --git a/index.js b/index.js index 162e771..6047094 100644 --- a/index.js +++ b/index.js @@ -4,20 +4,52 @@ var assign = require('lodash.assign'); var map = require('lodash.map'); var reduce = require('lodash.reduce'); +/** @typedef {[rating: number, rd: number, vol: number]} Opponent */ + +/** + * @typedef {object} ScaledOpponent + * @property {number} muj + * @property {number} phij + * @property {number} gphij + * @property {number} emmp + * @property {number} score +*/ + +/** + * @param {number} rating + * @param {number} rd + * @param {{ rating: number; }} options + */ function scale(rating, rd, options) { var mu = (rating - options.rating) / 173.7178; var phi = rd / 173.7178; return { mu: mu, phi: phi }; } +/** + * @param {number} phi + * @returns {number} + */ function g(phi) { return 1 / Math.sqrt(1 + 3 * Math.pow(phi, 2) / Math.pow(Math.PI, 2)); } +/** + * @param {number} mu + * @param {number} muj + * @param {number} phij + * @returns {number} + */ function e(mu, muj, phij) { return 1 / (1 + Math.exp(-g(phij) * (mu - muj))); } +/** + * @param {number} mu + * @param {readonly Opponent[]} opponents + * @param {{ rating: number; }} options + * @returns {ScaledOpponent[]} + */ function scaleOpponents(mu, opponents, options) { return map(opponents, function(opp) { var scaled = scale(opp[0], opp[1], options); @@ -31,21 +63,41 @@ function scaleOpponents(mu, opponents, options) { }); } +/** + * @param {readonly ScaledOpponent[]} opponents + * @returns {number} + */ function updateRating(opponents) { return 1 / reduce(opponents, function(sum, opp) { return sum + Math.pow(opp.gphij, 2) * opp.emmp * (1 - opp.emmp); }, 0); } +/** + * @param {number} v + * @param {readonly ScaledOpponent[]} opponents + * @returns {number} + */ function computeDelta(v, opponents) { return v * reduce(opponents, function(sum, opp) { return sum + opp.gphij * (opp.score - opp.emmp); }, 0); } +/** + * @param {number} phi + * @param {number} v + * @param {number} delta + * @param {number} a + * @param {{ tau: number; }} options + */ function volF(phi, v, delta, a, options) { var phi2 = Math.pow(phi, 2); var d2 = Math.pow(delta, 2); + + /** + * @param {number} x + */ return function(x) { var ex = Math.exp(x); var a2 = phi2 + v + ex; @@ -55,12 +107,21 @@ function volF(phi, v, delta, a, options) { }; } +/** + * @param {number} sigma + * @param {number} phi + * @param {number} v + * @param {number} delta + * @param {{ tau: number; }} options + * @returns {number} + */ function computeVolatility(sigma, phi, v, delta, options) { // 5.1 var a = Math.log(Math.pow(sigma, 2)); var f = volF(phi, v, delta, a, options); // 5.2 + /** @type {number} */ var b; if (Math.pow(delta, 2) > Math.pow(phi, 2) + v) { b = Math.log(Math.pow(delta, 2) - Math.pow(phi, 2) - v); @@ -79,6 +140,7 @@ function computeVolatility(sigma, phi, v, delta, options) { // 5.4 while (Math.abs(b - a) > 0.000001) { + /** @type {number} */ var c = a + (a - b) * fa / (fb - fa); var fc = f(c); @@ -98,10 +160,21 @@ function computeVolatility(sigma, phi, v, delta, options) { return Math.exp(a / 2); } +/** + * @param {number} sigmap + * @param {number} phi + * @returns {number} + */ function phiStar(sigmap, phi) { return Math.sqrt(Math.pow(sigmap, 2) + Math.pow(phi, 2)); } +/** + * @param {number} phis + * @param {number} mu + * @param {number} v + * @param {readonly ScaledOpponent[]} opponents + */ function newRating(phis, mu, v, opponents) { var phip = 1 / Math.sqrt(1 / Math.pow(phis, 2) + 1 / v); var mup = mu + Math.pow(phip, 2) * reduce(opponents, function(sum, opp) { @@ -110,18 +183,32 @@ function newRating(phis, mu, v, opponents) { return { mu: mup, phi: phip }; } +/** + * @param {number} mup + * @param {number} phip + * @param {{ rating: number; }} options + */ function unscale(mup, phip, options) { var rating = 173.7178 * mup + options.rating; var rd = 173.7178 * phip; return { rating: rating, rd: rd }; } +/** + * @param {number} rating + * @param {number} rd + * @param {number} sigma + * @param {readonly Opponent[]} opponents + * @param {{ rating?: number; tau?: number; }} [options] + * @returns {{ rating: number; rd: number; vol: number; }} + */ function rate(rating, rd, sigma, opponents, options) { - options = assign({}, { rating: 1500, tau: 0.5 }, options || {}); + /** @type {{ rating: number; tau: number; }} */ + var opts = assign({}, { rating: 1500, tau: 0.5 }, options || {}); // Step 2 - var scaled = scale(rating, rd, options); - var scaledOpponents = scaleOpponents(scaled.mu, opponents, options); + var scaled = scale(rating, rd, opts); + var scaledOpponents = scaleOpponents(scaled.mu, opponents, opts); // Step 3 var v = updateRating(scaledOpponents); @@ -130,7 +217,7 @@ function rate(rating, rd, sigma, opponents, options) { var delta = computeDelta(v, scaledOpponents); // Step 5 - var sigmap = computeVolatility(sigma, scaled.phi, v, delta, options); + var sigmap = computeVolatility(sigma, scaled.phi, v, delta, opts); // Step 6 var phis = phiStar(sigmap, scaled.phi); @@ -138,9 +225,7 @@ function rate(rating, rd, sigma, opponents, options) { // Step 7 var updated = newRating(phis, scaled.mu, v, scaledOpponents); - var unscaled = unscale(updated.mu, updated.phi, options); - unscaled.vol = sigmap; - return unscaled; + return assign({}, unscale(updated.mu, updated.phi, opts), { vol: sigmap }); } module.exports = rate; diff --git a/package.json b/package.json index a939fd5..b21d88d 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,11 @@ }, "scripts": { "release": "semantic-release", - "pretest": "npm run -s lint", + "type-check": "tsc", + "type-coverage": "type-coverage --at-least 100 --detail --strict", + "pretest": "npm run -s type-check && npm run -s type-coverage", "test": "tape test/index.js", + "posttest": "npm run -s lint", "lint": "eslint index.js test/index.js bench/index.js" }, "dependencies": { @@ -36,6 +39,13 @@ "@kenan/renovate-config": "^1.5.1", "@semantic-release/changelog": "^6.0.1", "@semantic-release/git": "^10.0.1", + "@tsconfig/node12": "^1.0.9", + "@types/almost-equal": "^1.1.0", + "@types/lodash.assign": "^4.2.6", + "@types/lodash.isfunction": "^3.0.6", + "@types/lodash.map": "^4.6.13", + "@types/lodash.reduce": "^4.6.6", + "@types/tape": "^4.13.2", "almost-equal": "^1.1.0", "beautify-benchmark": "^0.2.4", "benchmark": "^2.1.4", @@ -44,6 +54,8 @@ "glicko2": "^0.8.6", "lodash.isfunction": "^3.0.9", "semantic-release": "^18.0.0", - "tape": "^5.3.2" + "tape": "^5.3.2", + "type-coverage": "^2.19.0", + "typescript": "^4.5.2" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2821318 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node12/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noEmit": true + }, + "include": ["*.js", "test/**/*.js"] +}