// ==UserScript==
// @name KG_ComplexityPanel
// @version 1.4.0
// @namespace klavogonki
// @author Silly_Sergio
// @description Добавляет панель прогноза сложности текста в заездах
// @include
http://klavogonki.ru/g/*// @grant none
// ==/UserScript==
function embed() {
var COMPLEXITY_TEXT = [
"СУПЕР РЕКОРДНЫЙ ТЕКСТ", // < 1
"РЕКОРДНЫЙ ТЕКСТ", // < 2
"очень лёгкий текст", // < 3
"лёгкий текст", // < 4
"нормальный текст", // < 5
"нормальный текст", // < 6
"текст сложней нормального",// < 7
"сложноватый текст", // < 8
"сложный текст", // < 9
"очень сложный текст", // < 10
"очень сложный текст", // < 11
"крайне сложный текст", // < 12
"крайне сложный текст", // < 13
"мега сложный текст", // < 14
"мега сложный текст", // < 15
"текст - жесть", // < 16
"текст - жесть", // < 17
"запредельная сложность" // < 18
];
// 2 letters combinations for the Russian language, ordered by their frequency. Only the first 791 most common combinations are included.
// The rest will get the worst coefficient.
var SYLLABLES = "тостнанопонеконираеналроовголипркаосерреотволаолтатьонваореллоеттеомодзавеилриатдеанлеогесдальитактиемасмеойсяныдообскмо" +
"инчежесеамазтрмикиависвиедбысоманнимарсьчтслегнурутвбовыдииевсизейивикпеокэтхооеадожчабемушеспаяихияудкрочсидневчиляылкузнсапаты" +
"дуульншиутнящежитсеесвыйрыопозекмнидаетуусичглужлуятлсвнийыеымезгабребтныхиражмыжадруюьсснлюпиечгднтбугрииахрярнабжнаюссучачьквл" +
"ньумчноиугапывцеплщизвбиепгиешсмашблсуцишазджддвошбактврукицагайртежпуялупклдлвуурытоязолытккенсубещысянушвшдыувцаютишиггуйсшьох" +
"хатяехядндчуьшзирьшкющибшлбягеясдьнквзземязыьюзуямзмрврмышиюкндснцухщайнявлнунбнюдркрсьефилкиоецпырдсычкущллржсрдяацзрзгузфоиаюб" +
"счргязлжеоксвягнижшнджьяащвтоойтвкзлипдкящощхнуефебщхишовдтлыршувпзяоюсдчьыбьмжуфаршмлпяхрмпквяхнгцоофяецыбсынхвсхеяыкищаивьдцоц" +
"якфрйднчьзрхыпычйчауафзбючмсюсяжзкэлпньтбъяюсюрлцущунюхулгеювмдттдеарюнщзьхлуабкмкгкэнчшдпеиыдэкъеыгтчйкммррячрплчъятттпштеуяржк" +
"ягжоьцуйэрьбптоупппкоэйшефифрчрбмьсшяяюрхерцлднрызлткцсбдммбчоймбхпьддцвфлфущнжчьгзжнфтмюньчэмьдьицкпсдшйокжуятцйцтюхмсцйлуивввч" +
"бвдчяцмряйхсэйдхщьюзаоккрзябыжмгсэтбсфюкбмвхюмнзмчэсмэжьчлбьяпьфлпффрэжбюлдбэдвцнвгчуэвгиулбяилмхтюцдздгшвоаюжьврфсжэвкэнжьодюсг" +
"съмцююдъэпрщлздэыягвюшббэфюглвчргсфтаэкззсггшпуфтщйруошмжсззпчмвтхфыюхгтзъааяшбюбдэгуцйеншбжгмлэыцкшйзхэмфнбвщжрфстэлштъпцбшзтжл" +
"иэнхйвшрыщьщжжтгкмжгйфтшэхзчтфвънлыивблхсзнэйбйабэлфэшхшьпэзюпкгжмпшбтзцйгчвгэмтйхбзмюйяэбтзфьзюшцюйфюйпкпмъюабцпюхьювкхгбщрхъвэ" +
"пэчмзшмзбчууфнцзкдлщчжхгфэыумщгюфгкюгшжюшюбгнъщоцлццфммхюикъкбцнпбнмцмйикьнплъюепфъюдфлрьхцсмшкчххюфьркфжтмдвжзэцрхкфчгхсщюэшсцд" +
"ччхчгпфбеэхдшээямжжэяугзшхвюйщфяжцээйюгфпвхпэожвчэтжцп";
// Complexity weight for each character. Russian letters and special characters use different formulas, so even if you
// see the same weights for some Russian letter and some special character, they will contribute different final weights.
var CHAR_WEIGHTS = {
"а": 105, "б": 142, "в": 119, "г": 140, "д": 125, "е": 105, "ж": 153, "з": 142, "и": 107, "й": 150,
"к": 122, "л": 117, "м": 122, "н": 108, "о": 100, "п": 126, "р": 118, "с": 115, "т": 110, "у": 127,
"ф": 155, "х": 157, "ц": 175, "ч": 145, "ш": 160, "щ": 180, "ъ": 200, "ы": 140, "ь": 137, "э": 183,
"ю": 167, "я": 135, "n": 0.3, " ": 0.3, ".": 2.0, ",": 2.5, "!": 4.0, "?": 3.0, ":": 4.0, ";": 4.5,
""": 7.0, "«": 5.0, "»": 5.0, "'": 9.0, "_": 8.0, "%": 8.0, "—": 5.0, "–": 5.0, "-": 5.0, "(": 5.0,
")": 5.0, "*": 7.0, "=": 7.0, "+": 7.0, "№": 5.0, "#": 9.0, "$": 9.0, "&": 9.0, "/": 9.0, "\": 10.0,
"|": 10.0, "<": 11.0, ">": 11.0, "@": 11.0, "[": 11.0, "]": 11.0, "^": 11.0, "~": 11.0, "`": 11.0, "{": 11.0,
"}": 11.0, "0": 5.0, "1": 5.0, "2": 5.0, "3": 5.0, "4": 5.0, "5": 5.0, "6": 5.0, "7": 5.0, "8": 5.0,
"9": 5.0
};
// Spectrum canvas settings
var CANVAS_HEIGHT = 12;
var CANVAS_OPACITY = 0.65;
// This is the upper threshold for the spectrum complexity. Everything up will be pure red.
var MAX_SPECTRUM_COMPLEXITY = 310;
// This is the bottom threshold for the spectrum complexity. Everything down will be pure green.
var MIN_SPECTRUM_COMPLEXITY = 120;
var SPECTRUM_COMPLEXITY_RANGE = MAX_SPECTRUM_COMPLEXITY - MIN_SPECTRUM_COMPLEXITY;
// If character is not found, then it will be treated as a special character with the given weight
var UNKNOWN_CHAR_WEIGHT = 15.0;
// All Russian letters, with the exception of the first one, in the word will get such default weight
var DEFAULT_LETTER_WEIGHT = 100;
// Characters for Russian language.
var ALPHABETICAL_CHARACTERS = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя";
// If letter is a capital, we add such a number to its weight
var CAPITAL_WEIGHT = 200.0;
// If the some word has number of capitals more than this value, then we stop apply CAPITAL_COEF, because user will likely use Caps key
var CAPS_LENGTH = 4;
// If character is not part of Russian letters, we multiply weight by this number
var SPECIAL_CHARACTER_COEF = 200.0;
// Whole word's weight will get coefficient multiplied depending on its size. Longer words are harder to type.
// If a word has only a single letter, then it will take longer to type it, because it doesn't have keys combination
// with other letters.
var WORD_LENGTH_COEF = [2.0, 1.0, 1.0, 1.0, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0];
// Dictionary with 2 letters combinations and their frequency orders. We use associative array for search, because it is much faster (if it is hot),
// than other approaches: String#indexOf, RegExp, Array#indexOf.
var dictionary = {};
var waitForText = setInterval(function () {
if (!game || !game.players) {
return;
}
clearInterval(waitForText);
if (!game.text) {
return;
}
prepareDictionary();
var complexityObject = getComplexity(game.text);
var complexity = complexityObject.complexity.toFixed(2);
var params = document.getElementById("params");
if (params) {
// We use wrapper only as a workaround to support KTS custom styles.
// KTS changes panel style by ID. We use "statistics" ID, which is also used by another panel.
// HTML 5 allows duplicate IDs. They just need to be in different subtrees. I don't like
// such approach, but I fear it's the only way for now, unless we ask KTS developer to
// include some lines into img/apply.js file.
var wrapper = document.createElement("div");
wrapper.id = "complexity-panel";
var elem = document.createElement("div");
elem.style.setProperty("background", "none repeat scroll 0 0 #F8F4E6", null);
elem.style.setProperty("margin-top", "10px", null);
elem.style.setProperty("margin-bottom", "10px", null);
elem.id = "statistics";
wrapper.appendChild(elem);
params.parentNode.insertBefore(wrapper, params.nextSibling);
elem.innerHTML = '<div class="r tl" title="v1.4"><div class="tr"><div class="bl"><div class="br"><div class="rc">' +
'<h4>Прогноз сложности</h4>' +
'<div><span style="font-size:17px;font-weight:bold;color:#333">' + (complexity >= 0 ? complexity.slice(0, -3) : '') + '</span>' +
'<span style="font-size:14px;font-weight:bold;color:#666">' + (complexity >= 0 ? '.' + complexity.slice(-2) : 'N/A')+ '</span>' +
' − ' + (complexity >= 0 ? getComplexityText(complexity) : 'неприменимо к данному заезду') +
'</div><div style="margin:10px 0 4px;background-color:white"id="spectrumCanvas"/>' +
'</div></div></div></div></div>';
// Add spectrum canvas to the panel
if (complexity >= 0) {
var spectrum = complexityObject.spectrum;
smoothArray(spectrum, 10);
var canvasDiv = document.getElementById("spectrumCanvas");
var canvasElem = getSpectrumCanvas(spectrum, canvasDiv.clientWidth, CANVAS_HEIGHT);
if (canvasElem != null) {
canvasDiv.appendChild(canvasElem);
}
}
}
}, 100);
function prepareDictionary() {
for (var i = 0; i < SYLLABLES.length; i += 2) {
dictionary[SYLLABLES.substr(i, 2)] = i / 2;
}
}
function getWordLengthCoef(length) {
return WORD_LENGTH_COEF[(length < WORD_LENGTH_COEF.length) ? length - 1 : WORD_LENGTH_COEF.length - 1];
}
function getComplexityText(complexity) {
var position = Math.floor(complexity);
return COMPLEXITY_TEXT[(position < COMPLEXITY_TEXT.length) ? position : COMPLEXITY_TEXT.length - 1];
}
// Return string complexity and its spectrum. Return -1 for the complexity, if it can't be applied to the string.
function getComplexity(str) {
var complexity = 0.0;
var wordLength = 0;
var wordComplexity = 0;
var capsLength = 0;
var prevChar = "";
var unknownCharNum = 0;
var spectrum = [];
var pos = 0;
var wordComplexityPerCharacter, j;
// Replace all "ё" occurrences to "е".
str = str.split("ё").join("е");
for (var i = 0; i < str.length; i++) {
var char = str.charAt(i);
var charLowerCase = char.toLowerCase();
var weight;
if (ALPHABETICAL_CHARACTERS.indexOf(charLowerCase) != -1) {
// Alphabetical character
wordLength++;
if (wordLength === 1) {
// First letter in a word will get weight depending on its frequency
weight = CHAR_WEIGHTS[charLowerCase];
} else {
// All the following letters in the word will get default weight. Their final weight will depend on the letters combination frequency.
weight = DEFAULT_LETTER_WEIGHT;
}
// All letters in the word, starting from the second, will apply coefficient depending on the 2 letters frequency
if (wordLength > 1) {
var n = dictionary[prevChar + charLowerCase];
if (n == null) {
n = -3.75; // This is minimum value, which can be achieved by the following formula
}
// Such formula returns 2 letters frequency in the decimal logarithmic scale, e.g. "0" - most common, "-1",
// if frequency is 10 times less. Formula is a polynomial interpolation based on the experimental selection.
var syllableDifficulty = -7e-9 * Math.pow(n, 3) + 7e-6 * Math.pow(n, 2) - 0.0059 * n;
// Increase weight based on the difficulty
weight *= Math.pow(1 - syllableDifficulty, 1.3);
}
// Capitals are harder to type
var isLowerCase = char === charLowerCase;
if (!isLowerCase) {
capsLength++;
if (capsLength < CAPS_LENGTH) {
weight += CAPITAL_WEIGHT;
}
} else {
capsLength = 0;
}
wordComplexity += weight;
prevChar = charLowerCase;
} else {
// Special character (not part of Russian letters)
// If we reached the end of the word, we need to calculate final word's weight depending on its length
if (wordLength > 0) {
wordComplexity *= getWordLengthCoef(wordLength);
complexity += wordComplexity;
wordComplexityPerCharacter = wordComplexity / wordLength;
for(j = 0; j < wordLength; j++) {
spectrum[pos++] = wordComplexityPerCharacter;
}
}
wordComplexity = 0;
capsLength = 0;
wordLength = 0;
weight = CHAR_WEIGHTS[charLowerCase];
if (weight == null) {
weight = UNKNOWN_CHAR_WEIGHT;
unknownCharNum++;
}
weight *= SPECIAL_CHARACTER_COEF;
complexity += weight;
spectrum[pos++] = weight;
}
}
// If text doesn't finish with the special character, we need to add last word's weight
if (wordLength > 0) {
wordComplexity *= getWordLengthCoef(wordLength);
complexity += wordComplexity;
wordComplexityPerCharacter = wordComplexity / wordLength;
for(j = 0; j < wordLength; j++) {
spectrum[pos++] = wordComplexityPerCharacter;
}
}
// Complexity is per character
if (str.length > 0) {
complexity /= str.length;
} else {
return {complexity: 0, spectrum: spectrum};
}
// if number of unknown characters is more than 10%, then complexity value is inconsistent
if (unknownCharNum / str.length > 0.1) {
return {complexity: -1, spectrum: spectrum};
}
// Divide by theoretical complexity minimum in order to start our reference frame for complexity from 1
complexity /= 160;
if (complexity < 1) {
complexity = 1;
}
complexity -= 1;
complexity *= 20; // Scale complexity to readable values, so ~10 is close to usual hard complexity
return {
complexity: complexity,
spectrum: spectrum
};
}
// Function smooths (blurs) 1D array using flat convolution buffer. Algorithm complexity is O(N + power).
function smoothArray(array, power) {
var smoothedValue = 0;
var fractions = [];
var coef = 2 * power + 1;
for (var i = 0; i < array.length + power; i++) {
if (i >= coef) {
smoothedValue -= fractions[i % coef];
}
if (i < array.length) {
var value = array[i];
smoothedValue += value;
fractions[i % coef] = value;
}
if (i >= power) {
var factor;
if (i < array.length) {
factor = Math.min(i + 1, coef);
} else {
factor = coef + array.length - i - 1;
}
array[i - power] = smoothedValue / factor;
}
}
}
// Function returns canvas with the given width and height. We paint spectrum on the canvas using values in the array.
// Lowest values will be green. Middle will be yellow. Highest will be red.
function getSpectrumCanvas(array, width, height) {
var canvas = document.createElement('canvas');
if (!canvas || !canvas.getContext) {
// We are very sorry for our dinosaurs. God bless them.
return null;
}
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
if(typeof G_vmlCanvasManager != 'undefined') {
canvas = G_vmlCanvasManager.initElement(canvas);
}
var ctx = canvas.getContext("2d");
// Too opaque spectrum burns eyes
ctx.globalAlpha = CANVAS_OPACITY;
// We need to scale to the given canvas width
ctx.scale(width / array.length, 1);
var r, g;
for (var i = 0; i < array.length; i++) {
// Apply the thresholds
var value = Math.min(Math.max(array[i], MIN_SPECTRUM_COMPLEXITY), MAX_SPECTRUM_COMPLEXITY);
// Scale complexity to 0 - 255 range
var complexity = Math.floor(255 * (value - MIN_SPECTRUM_COMPLEXITY) / SPECTRUM_COMPLEXITY_RANGE);
// Calculate red and green components. Blue will be always 0.
r = Math.min(complexity * 2, 255);
g = Math.min(2 * (255 - complexity), 255);
// Paint vertical line
ctx.strokeStyle="rgb(" + r + "," + g + ",0)";
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i, height);
ctx.stroke();
}
return canvas;
}
}
var inject = document.createElement("script");
inject.setAttribute("type", "text/javascript");
inject.appendChild(document.createTextNode("(" + embed + ")()"));
document.body.appendChild(inject);