Typing Game
Source
behavior game init log 'Game installed' set $level to 0 set $statsArea to first <[hs-stats-area] /> set $gameplayArea to first <[hs-gameplay-area] /> set $completed to the first in $gameplayArea set $nextCharacterContainerContainer to the next from $completed set $nextCharacterContainer to the first in $nextCharacterContainerContainer set $allRemaining to the next from $nextCharacterContainerContainer getNextCharacter() end def getNextCharacter get $allRemaining.innerText get result.replaceAll("\\n", "") get result[0] set $nextCharacter to result get $allRemaining.innerText.replace($nextCharacter, "") set $allRemaining.innerText to the result put $nextCharacter into $nextCharacterContainer end def nextLevel if $timerOn then recordLastTime($level) end set $level to $level + 1 if $level is 1 setupLevel("hunt cheesy smelly hurried dependent punch whole sleep uninterested beds night private") exit end if $level is 2 setupLevel("accessible optimal bare married defiant receipt incandescent needy rigid spiky fair") exit end if $level is 3 setupLevel("argument face idea loutish wreck soup maniacal test daily art slip rotten equable") exit end if $level is 4 setupLevel("brainy receive chew cute bump rapid peace railway visit lovely") exit end put "You win!" into $gameplayArea exit end def recordLastTime(levelNum) get Date.now() - $levelStartTime get it / 1000 -- seconds get it / 60 -- minutes set totalTimeForLevel1 to it get $levelCharacters.length / 5 -- standard word size get it / totalTimeForLevel1 get Math.round(it) put the result into <[hs-raw-wpm] /> end def setupLevel(words) put "" into $completed put "" into $nextCharacterContainer set $levelCharacters to words put $levelCharacters into $allRemaining getNextCharacter() set $levelStartTime to Date.now() set $timerOn to true end on every keydown if event.ctrlKey then exit end -- let ctrl+r thru -- don't respond to special keys being pressed like shift, ctrl, etc. if event.key.length is greater than 1 then exit end halt the event if event.key is $nextCharacter trigger correctCharacter on $nextCharacterContainer set $completed.innerHTML to $completed.innerHTML + $nextCharacter getNextCharacter() if $nextCharacter is undefined nextLevel() end else $nextCharacterContainer.classList.add("!text-red-600") $nextCharacterContainer.classList.add("!-left-px") wait for transitionend or correctCharacter from $nextCharacterContainer or 1s if the result's type is 'transitionend' wait a tick $nextCharacterContainer.classList.remove("!-left-px") $nextCharacterContainer.classList.add("!left-px") wait for transitionend or correctCharacter from $nextCharacterContainer or 1s end -- Always wipe out all the "bad character" indicators $nextCharacterContainer.classList.remove("!text-red-600") $nextCharacterContainer.classList.remove("!left-px") $nextCharacterContainer.classList.remove("!-left-px") end end on focus remove .hidden from the $gameplayArea remove .hidden from the $statsArea add .hidden to the firstin me end on blur add .hidden to the $gameplayArea add .hidden to the $statsArea get the first
in me remove .hidden from it remove .hidden from the first in it end end
Game paused. Click here to play.
Logbook
Wed Feb 21 11:05:13 PST 2024
Today's prompt for the RC Creative Coding group was:
Go to an extreme, move back to a more comfortable place
I chewed on it while I ate my lunch. I was reading an article about feedback loops the night before and I thought to play with that idea. I'm not sure how I jumped from there to typing games. But the vision in my head was of a typing game with a goal, and that goal being in a feedback loop with your current typing speed. The faster you typed, the faster the target speed would get. So in order to continue to hit your goal, you'd probably want a consistent pace, to keep the goal consistent.
Since I took time for lunch, I only gave myself some 40 minutes to work on this which included writing out this idea.
I had never made a typing game before. I assumed I would take a some large list of words, and randomize them, and ask the user to type them in order, tracking their progress. I decided to start there and add my concept of speed tracking and goals only after. I didn't want to take too much time planning.
I didn't want to think about where my random words were coming from, so I just searched online for a generator.
For some reason, Hyperscript didn't love when I put my script blog inside another element. Apparently it only respected top level script tags? This didn't work, but the "FancyScriptTag" outside of the details element did work correctly?
<details>
<summary>Code</summary>
<FancyScriptTag type="text/hyperscript">
{`log 'Online'`}
</FancyScriptTag>
</details>
It was relatively quick to see the progress of typing by manipulation of the text nodes. This first version was confusing since there was some whitespace issues. I played with the CSS whitespace mode to see if I could match reality to how I felt the whitespace should work. I fixed the whitespace problem not with CSS but by not putting newlines between the span elements.
I implemented outlines and an underline on the next letter and a consistent color scheme that I liked quite a bit. Then I did some renaming.
Fri Feb 23 02:59:06 PM PST 2024
Based on some positive feedback from the RC game dev interest group, I made a few adjustments to the page and to the instructions. First I made the code hidden by default, with a button to unhide it.
install, and that could only happen by putting it higher in the document order. I thought about alternatively using the CSS order property, but that seemed like a lot of hubub.
I had second thoughts about the button, however. My main call to action was to play the game, not to see the source code! Instead I entirely hid the code.
Next I wanted to work on the instructions. Why have the instructions outside the box of the game? This isn't 1985 with game manuals sold in jewel cases alongside cartridges. So I put the instructions inside the box.
I ran into a very annoying bug in my code to do with my extensive use of innerText, where new lines were swapped for <br /> when I set the current innerText back to itself with minor modifications. That was mixed with another confusing bug where the visibility and display CSS values were affecting my game code. Because I was using DOM manipulations, somehow using display:none; caused innerText to not see and manipulate items. I wasn't sure how to deal with that. Maybe I should switch to the less confusing, simpler innerHTML?
Sat Feb 24 09:19:08 PM PST 2024
After my challenges with newlines, I decided to ensure all my words in the source file were on one line so that no newlines were introduced and thus no surprising <br /> appeared. I also applied white-space: pre; to the span which held the next letter to type, as that prevented it from disappearing when it only had a space in it. Additionally, I added a monospace font to my site so that each space character would take up a consistent amount of horizontal room. This also had the effect of differentiating the instructions from the typing game which felt right.
Next I made a bit of a progression system. Instead of having the instructions outside the game, I had the player type the instructions themselves. When they finished with that, they'd automatically progress to the next stage.
I wanted some visual feedback when I typed the wrong letter, so I made a small CSS animation. This required an interesting dive into both Hyperscript and Tailwind documentation. If I were not using either of these tools, or only one of them, it would have been simpler, but instead I had to add a Tailwind class, then wait for the transitionend event, and then remove it. Perhaps a Hyperscript afficionado could tell me how to do it in a more straightforward manner.
I also had to adjust my keydown handler to not take the press of a shift key or control key as a mistaken character. And i found that if I hit the space bar twice quickly, I'd screw up Hyperscript's event synchronization, so I disabled that by saying on every keydown instead of just on keydown. This worked swimmingly.
Finally I had complete-feeling basis for a typing game. So I felt comfortable stepping forward towards my lofty ideas about feedback loops. First I researched a bit about how typing speed calculators usually handle typing speed. It would be simple to simply measure the amount of time between when the player began typing a particular word and finished typing that word, disregarding spaces, and then average those times together, but that felt strange given the random nature of word lengths. Perhaps there was some ideal or average word length, and that's what most "words-per-minute" measurements were based on?
The search "how is words per minute calculated" yielded many articles. I started with the Wikipedia entry. As I'd predicted (maybe I already knew this in the depths of my subconscious? I'm not usually this clever), the article confirmed that, "the definition of each "word" is often standardized to be five characters or keystrokes long in English." A detailed article on how to calculate "Raw WPM" at 100utils.com was illuminating as well. Also, the page itself was amazing with an unironic marquee and a lovely textured background image. I was thankful for the math being spelled out so dilligently, and I was doubly thankful that the site pointed out the importance of penalizing errors in typing speeds. The site called the calculation which included error penalties "Net WPM".
The Raw WPM calculation was spelled out as "all keystrokes divided by 5, further divided by the total time".
The Net WPM was "all keystrokes divided by 5, minus the number of uncorrected errors left, all divided by the total time".
The article also pointed out that accuracy alone may be an interesting metric, which was simply the percentage of the number of correct keystrokes divided by the total keystrokes.
Note that when I said keystrokes, I meant characters on the keyboard. I did not mean to propose that when the shift key is pressed that be counted as a separate keystroke. Maybe "characters" would be better, or as the 100utils people say, "typed entities." Either way, I think you catch my meaning.
I wanted to measure a rolling average WPM so that it would update as the player changed their speed in real time. I'd use the change in the rolling average over time as a slope, and I'd set a goal based on the trajectory of that slope. So, if the player typed faster in the current second than the last second, they'd be encouraged to go faster still in the next second via an even higher goal.
Mon Feb 26 09:58:02 AM PST 2024
I started by timing the total amount of time it took to complete each stage. Then I thought I'd count how many characters were in the stage, and divide that by five, then divide that by the time. That wouldn't be the rolling average I wanted yet, but it would be a good start. All the pieces on the game board.
That seemed to work well. I guess the only verification I could make on the math was my own beliefs about my typing speed, which seemed to hover between 75 and 100 (raw) WPM depending on my focus.
I was eager to make a chart, but I determined that the chart wouldn't be too interesting until I had a rolling average, and thus more data points to put on that chart. Right now my chart would only have one data point after every level. So I changed to a rolling average. Up to this point I was logging the WPM to the console, so I first changed that to display in the play area itself.
Fri Mar 1 04:46:20 PM PST 2024
At Game Dev Interest Group at RC someone made the comment that the visual indication was too subtle. At the time, the indicator was a quick color tween from the normal amber to red. I agreed it was likely too little contrast especially if someone had colorblindness. I had the idea to add some motion to the indicator as well. Yannick suggested that motion be only horizontal, to suggest the letter was shaking its head "no" at the player. So I tried that out quickly. I tried out Tailwind's transition-transform.
Because I was transforming two properties, I had to specify both of them in one rule. If I used two transition- rules, the latter overrode the former. So I had to use transition-[color,transform] to allow both to work.
Unfortunately, to get the transition-transform to work, I had to set the span to inline-block which set me back to some of my previous whitespace related woes. Now the space character shifted around depending on where it was.
Sat Mar 2 03:58:33 PM PST 2024
After much play and feeling annoyed, I realized I could avoid the use of inline-block if instead of using transform, I used position: relative; and left: -1; to shake the character. It worked smoothly!
Backlinks
Keywords
- No keywords