1// User
  2let lineMaxCharacterLength = 40;
  3let lineCount = 4;
  4
  5// System
  6let threeKListLength = top_threek_english_words.length;
  7let lineData = [];
  8let userCursorPosition = 0;
  9let isShiftPressed = false;
 10let runStartTime = new Date();
 11let runStarted = false;
 12let runErrors = [];
 13let writtenLetters = [];
 14
 15function pickRandomWordFromList() {
 16  return top_threek_english_words[
 17    Math.round(Math.random() * (threeKListLength - 1))
 18  ];
 19}
 20
 21function generateLines() {
 22  let generatedLineData = [];
 23  for (let lineIndex = 0; lineIndex < lineCount; lineIndex++) {
 24    let currentLineData = "";
 25    let maxCharacterLengthReached = false;
 26    while (!maxCharacterLengthReached) {
 27      let newCurrentLineData = currentLineData.concat(
 28        pickRandomWordFromList() + " "
 29      );
 30      if (newCurrentLineData.length > lineMaxCharacterLength) {
 31        maxCharacterLengthReached = true;
 32      } else {
 33        currentLineData = newCurrentLineData;
 34      }
 35    }
 36    generatedLineData.push(currentLineData);
 37  }
 38  return generatedLineData;
 39}
 40
 41function getCharacterFromIndex(lineData, index) {
 42  lineData;
 43  let currentCharacterId = 0;
 44  for (let lineIndex in lineData) {
 45    let currentLineString = lineData[lineIndex];
 46    for (let currentCharacterIndex in currentLineString) {
 47      if (index == currentCharacterId) {
 48        return currentLineString[currentCharacterIndex];
 49      }
 50      currentCharacterId++;
 51    }
 52  }
 53  return "Not found.";
 54}
 55
 56function getCharacterCount(lineData) {
 57  let currentCharacterId = 0;
 58  for (let lineIndex in lineData) {
 59    let currentLineString = lineData[lineIndex];
 60    for (let currentCharacterIndex in currentLineString) {
 61      currentCharacterId++;
 62    }
 63  }
 64  return currentCharacterId;
 65}
 66
 67function getWordCount(lineData) {
 68  let wordCount = 0;
 69  for (let lineIndex in lineData) {
 70    let currentLineString = lineData[lineIndex];
 71    wordCount += currentLineString.split(" ").length;
 72  }
 73  return wordCount;
 74}
 75
 76function formatForHTML(stringToFormat) {
 77  return stringToFormat.replaceAll(" ", "&nbsp;");
 78}
 79
 80function renderLineDataToScreen() {
 81  let linesContainerDiv = document.getElementById("lines-container");
 82  linesContainerDiv.innerHTML = "";
 83  let currentCharacterId = 0;
 84  for (let lineIndex in lineData) {
 85    let lineDiv = document.createElement("div");
 86    lineDiv.className = "line-div";
 87    let currentLineString = lineData[lineIndex];
 88    for (let currentCharacterIndex in currentLineString) {
 89      let characterDiv = document.createElement("div");
 90      characterDiv.id = "character-div-" + currentCharacterId;
 91      characterDiv.className = "character-div";
 92      if (runErrors.includes(currentCharacterId)) {
 93        characterDiv.className += " character-div-error";
 94      }
 95      if (writtenLetters.includes(currentCharacterId)) {
 96        characterDiv.className += " character-div-written";
 97      }
 98      characterDiv.innerHTML = formatForHTML(
 99        currentLineString[currentCharacterIndex]
100      );
101      lineDiv.appendChild(characterDiv);
102      currentCharacterId++;
103    }
104    linesContainerDiv.appendChild(lineDiv);
105  }
106}
107
108function updateCursorPosition() {
109  let cursorElement = document.getElementById("cursor-div");
110  cursorElement.innerHTML = formatForHTML(
111    getCharacterFromIndex(lineData, userCursorPosition)
112  );
113  let targetBoudingRect = {
114    left: "-50",
115    top: "50",
116    right: "-50",
117    bottom: "50",
118  };
119  try {
120    targetBoudingRect = document
121      .getElementById("character-div-" + userCursorPosition)
122      .getBoundingClientRect();
123  } catch {
124    console.log("unsafe behavior but I don't care");
125  }
126  cursorElement.style.top = targetBoudingRect.top + "px";
127  cursorElement.style.left = targetBoudingRect.left + "px";
128  cursorElement.style.width =
129    targetBoudingRect.right - targetBoudingRect.left + "px";
130  cursorElement.style.height =
131    targetBoudingRect.bottom - targetBoudingRect.top + "px";
132}
133
134function updateStats() {
135  let statusContainer = document.getElementById("status-container");
136  let characterCount = getCharacterCount(lineData);
137  let wordCount = getWordCount(lineData);
138  let avgTypedWords = wordCount * (userCursorPosition / characterCount);
139  let timeSinceStart = new Date() - runStartTime;
140  if (runStarted) {
141    statusContainer.innerHTML = `
142            <div id="status-wpm">WPM: ${Math.round(
143              (avgTypedWords / (timeSinceStart / 1000)) * 60
144            )}</div>
145            <div id="status-errors">Errors: ${runErrors.length}</div>
146            <div id="status-accuracy">Accuracy: ${Math.round(
147              ((characterCount - runErrors.length) / characterCount) * 100
148            )}%</div>
149            <div id="status-time">Time: ${String(
150              Math.floor(Math.floor(timeSinceStart / 1000) / 60)
151            ).padStart(2, "0")}:${String(
152      Math.floor(timeSinceStart / 1000) % 60
153    ).padStart(2, "0")}.${Math.floor((timeSinceStart % 1000) / 100)}</div>
154  `;
155  }
156}
157
158function initialiseTest() {
159  lineData = generateLines();
160  renderLineDataToScreen();
161}
162
163document.addEventListener("keyup", function (event) {
164  if (event.keyCode == 16) {
165    isShiftPressed = false;
166  }
167});
168
169function advanceCursor() {
170  if (userCursorPosition == 0) {
171    runStartTime = new Date();
172    runStarted = true;
173  }
174  userCursorPosition++;
175}
176
177document.addEventListener("keydown", function (event) {
178  let characterTyped = String.fromCharCode(event.keyCode);
179  let isCapsLockOn = event.getModifierState("CapsLock");
180  if (event.keyCode == 8) {
181    if (userCursorPosition != 0) {
182      userCursorPosition--;
183      var index = runErrors.indexOf(userCursorPosition);
184      if (index !== -1) {
185        runErrors.splice(index, 1);
186      }
187      index = writtenLetters.indexOf(userCursorPosition);
188      if (index !== -1) {
189        writtenLetters.splice(index, 1);
190      }
191    }
192  }
193  if (event.keyCode == 27) {
194    initialiseTest();
195    runStartTime = new Date();
196    runStarted = false;
197    runErrors = [];
198    writtenLetters = [];
199    userCursorPosition = 0;
200  }
201  if (event.keyCode == 109 || event.keyCode == 54) {
202    characterTyped = "-";
203  }
204  if (event.keyCode == 16) {
205    isShiftPressed = true;
206  }
207  if (
208    !((!isCapsLockOn && isShiftPressed) || (isCapsLockOn && !isShiftPressed))
209  ) {
210    characterTyped = characterTyped.toLowerCase();
211  }
212  let pendingCharacter = getCharacterFromIndex(lineData, userCursorPosition);
213  if (pendingCharacter == characterTyped) {
214    writtenLetters.push(userCursorPosition);
215    advanceCursor();
216  } else {
217    if (![20, 16, 8, 27].includes(event.keyCode)) {
218      runErrors.push(userCursorPosition);
219      advanceCursor();
220    }
221  }
222  if (userCursorPosition == getCharacterCount(lineData)) {
223    runStarted = false;
224  }
225  updateCursorPosition();
226  renderLineDataToScreen();
227});
228
229initialiseTest();
230setTimeout(updateCursorPosition, 100);
231setInterval(updateStats, 100);