Last active
September 25, 2019 09:29
-
-
Save freddi301/531eebf581b292db51bee06f3717036b to your computer and use it in GitHub Desktop.
Type Faster
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<style> | |
.type-chars { | |
font-family: monospace; | |
font-size: 30px; | |
} | |
</style> | |
</head> | |
<body> | |
<textarea id="text" class="type-chars" cols="70" rows="4" readonly></textarea> | |
<textarea id="type" class="type-chars" cols="70" rows="4" autofocus></textarea> | |
<div> | |
<h4>train characters<h4> | |
<textarea id="characters" cols="70" rows="10"></textarea> | |
</div> | |
<pre id="score"></pre> | |
<script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/bundles/Rx.min.js"></script> | |
<script> | |
const trainCharactersElement = document.getElementById('characters'); | |
const textElement = document.getElementById('text'); | |
const typeElement = document.getElementById('type'); | |
const SPECIAL_CHARS = ['Alt', 'Shift', 'AltGr', 'Backspace']; | |
const COMBINATION_LENGTH = 4; | |
const text = new Rx.BehaviorSubject; | |
text.forEach(value => textElement.value = value || ""); | |
const type = new Rx.BehaviorSubject; | |
type.forEach(value => typeElement.value = value || ""); | |
function getExpectedCharacter() { | |
return text.getValue()[type.getValue().length] | |
} | |
function wrongChar(expectedChar, gotChar) { | |
if (~trainCharacters.getValue().indexOf(expectedChar)) getComboScore(expectedChar).mistakes += 3; | |
if (~trainCharacters.getValue().indexOf(gotChar)) getComboScore(gotChar).mistakes += 1; | |
} | |
function rightChar(char) { | |
type.next(typeElement.value + char); | |
} | |
const charSubject = new Rx.Subject; | |
typeElement.addEventListener('keydown', e => { | |
if (~SPECIAL_CHARS.indexOf(e.key)) return; | |
const expectedChar = getExpectedCharacter(); | |
if (e.key != expectedChar) { | |
wrongChar(expectedChar, e.key); | |
e.preventDefault(); | |
} else { | |
rightChar(e.key); | |
charSubject.next(e.key); | |
e.preventDefault(); | |
} | |
}); | |
const exerciseComplete = new Rx.Subject; | |
typeElement.addEventListener('keyup', e => { | |
if (type.getValue() == text.getValue()) { | |
exerciseComplete.next(); | |
} | |
}); | |
const scorePerCombination = {}; | |
function startExercise() { | |
text.next(genExercise()); | |
type.next(""); | |
const charTimestamps = charSubject.map(char => ({ time: new Date, char })); | |
const charDuration = charTimestamps | |
.scan((memo, current) => ({ last: current, duration: { char: current.char, duration: current.time - memo.last.time } }), { last: { time: new Date } }) | |
.skip(1).pluck('duration').filter(d => d.char != " "); | |
const charCombination = charDuration | |
.scan((memo, current) => { | |
memo.queue.unshift(current); | |
memo.queue = memo.queue.slice(0, COMBINATION_LENGTH); | |
memo.combinations = []; | |
let last = { char: "", duration: 0 }; | |
memo.queue.forEach(item => memo.combinations.push(last = { char: item.char + last.char, duration: last.duration + item.duration })); | |
return memo; | |
}, { queue: [] }).skip(1).pluck('combinations') | |
charCombination.forEach(combos => { | |
combos.forEach(combo => { | |
const current = getComboScore(combo.char); | |
current.times += 1; | |
current.duration += combo.duration; | |
current.average = current.duration / current.times; | |
}); | |
}); | |
} | |
function getComboScore(char) { | |
if (!scorePerCombination[char.length]) scorePerCombination[char.length] = {}; | |
if (!scorePerCombination[char.length][char]) { | |
scorePerCombination[char.length][char] = { char, times: Math.random(), duration: Math.random(), average: Math.random(), mistakes: Math.random() }; | |
} | |
return scorePerCombination[char.length][char]; | |
} | |
function extract(stats) { | |
return _.flatten(Object.values(stats)).map(_.property('char')) | |
} | |
exerciseComplete.forEach(startExercise); | |
const trainCharacters = new Rx.BehaviorSubject; | |
trainCharacters.next("qwertyuiopasdfghjklzxcvbnm"); | |
trainCharacters.forEach(value => trainCharactersElement.value = value); | |
trainCharactersElement.addEventListener('input', e => trainCharacters.next(trainCharactersElement.value)); | |
trainCharacters.forEach(chars => Array.from(chars).forEach(getComboScore)); | |
function genExercise () { | |
const lessTyped = _.mapValues(scorePerCombination, board => _.orderBy(Object.values(board), 'times', 'asc').slice(0,1)); | |
const mostMistaken = _.mapValues(scorePerCombination, board => _.orderBy(Object.values(board), 'mistakes', 'desc').slice(0,1)); | |
const slowest = _.mapValues(scorePerCombination, board => _.orderBy(Object.values(board), 'average', 'desc').slice(0,1)); | |
const random1 = _.orderBy(Object.values(scorePerCombination["1"] || {}), 'times', 'asc').slice(0,5).map(_.property('char')).join(''); | |
const random2 = _.orderBy(Object.values(scorePerCombination["1"] || {}), 'average', 'asc').slice(0,5).map(_.property('char')).join(''); | |
const random3 = _.orderBy(Object.values(scorePerCombination["2"] || {}), 'times', 'asc').slice(0,3).map(_.property('char')).join(''); | |
const random4 = _.orderBy(Object.values(scorePerCombination["2"] || {}), 'average', 'asc').slice(0,3).map(_.property('char')).join(''); | |
// return _.shuffle(trainCharacters.getValue()).join(''); | |
return extract(lessTyped) | |
.concat(extract(slowest)) | |
.concat(extract(mostMistaken)) | |
.concat(random1) | |
.concat(random2) | |
.concat(random3) | |
.concat(random4) | |
.join(" "); | |
}; | |
const scoreElement = document.getElementById('score'); | |
exerciseComplete.forEach(() => { scoreElement.innerText = JSON.stringify(scorePerCombination, null, 2); }); | |
exerciseComplete.next(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
preview