Darrien's technical blog

Documenting the technical stuff I do in my spare time

Typeracer through the ages

Hey folks. I’m Darrien Glasser, the main dev on the Typeracer project. The project has been going for almost a year and has been through some significant changes while doing its best to keep the core idea intact.

Typeracer is a terminal typing game where the goal is to type out a bit of text from a passage as fast as you can.

Recently we cut the 2.0.0 branch off and released it to the general public, and today we’re going to take a quick at where it started and how it has progressed over time.


Notes

Before we begin - it’s worth noting we were waiting on tui-rs, our terminal rendering library to add PartialEq and Clone on the Text object in a number of places very early on.

There are a number of early commits that reference either a local tui repo, or a git repo usually on my personal GitHub which no longer exists.

If you are building these commits, consider using:

1
tui = "0.6.2"

To ensure they build properly.


The beginning

Let’s look back at history, before there was even a release. The first working commit: 3476353d45c7dd330d33cac40567587d7b6d90b6

Typeracer in the very beginning

You’re going to notice this is looks kind of similar to the current Typeracer interface. It’s got 2 boxes. Type in one, show the passage in the other.

The first noticeable difference though is that it only works in a fully maximized terminal (dimensions about 170x44).

Some other things to notice:

  • It doesn’t tell you when you’re complete
  • It doesn’t tell you if you made a mistake
  • It only has the one passage shown
  • There was a strange backspace bug

Backspace would delete the character before it but wouldn’t move back. This would stay with the project for a while.

It was also tiny!

1
2
3
4
5
6
7
8
9
[root@79284cf5d498 terminal-typeracer]# tokei
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 Rust                    1           87           77            3            7
 TOML                    1           10            9            0            1
-------------------------------------------------------------------------------
 Total                   2           97           86            3            8
-------------------------------------------------------------------------------

I don’t think we’ll ever see <100 lines of code after this point.

There isn’t much more exciting about this, so let’s move on.


Pre-First release

We still hadn’t cut off a first release, but we were getting there! The first commit that had a nice semblance of a UI.

It also calculated WPM, but poorly.

Commit: 4e6f7e711c560d0df0a463c01063f4d349d93106

4e6f7e711c560d0df0a463c01063f4d349d93106

For folks who are not WPM aficionados, modern WPM is not calculated via the literal number of words typed every minute, but rather 5 letters is considered a word.

This was later fixed and made the default behavior, although you are still free to switch back to the old behavior at any time with typeracer --legacy-wpm

We’re at about 300 lines of code now depending on how you count it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[root@79284cf5d498 terminal-typeracer]# tokei
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 Markdown                1           32           32            0            0
 Rust                    1          286          227           27           32
 Shell                   1            6            3            1            2
 TOML                    1           15           12            1            2
-------------------------------------------------------------------------------
 Total                   4          339          274           29           36
-------------------------------------------------------------------------------

First release: version-0.9

At this point, we’d added in the ability to read the simple lang-pack format

The lang-pack version used was 0.1 which is the same as the current one, except it didn’t have any git stuff in it.

We used reqwest to download the latest lang pack and jeez it was bulky. The finalized binary size (post stripping) was 4.5 megabytes and we only had about 500 lines of code!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[root@79284cf5d498 terminal-typeracer]# tokei
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 Markdown                1           39           39            0            0
 Rust                    5          558          475           26           57
 TOML                    1           25           14            6            5
-------------------------------------------------------------------------------
 Total                   7          622          528           32           62
-------------------------------------------------------------------------------

The UI is mostly the same except there was this UI for when the latest lang pack was downloaded.

lang pack UI

You’ll note that the title of the UI was “checking bounds” (bug) which was because I also added a bounds checking UI that would stop the user from moving forward if the screen wasn’t big enough (AKA 170x40). It took a while for this to get removed.

Also the backspace bug still haunted me :(

backspace bug


v1.0.1, the real first release

Look at that UI :D

v1.0.1 complete passage UI

Really quite nice and simple. This was also the first release that added generalized CLI args.

There was only one arg. The new kid on the block was --read-text which let you take a passage as a CLI arg.

v1.0.0 had a bug that would make Typeracer panic if you didn’t run without --read-text making this the first official release.

We’re at roughly 600 lines of code now too!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[root@79284cf5d498 terminal-typeracer]# tokei
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 Makefile                1            8            7            0            1
 Markdown                1           44           44            0            0
 Rust                    5          620          530           27           63
 TOML                    1           28           15            7            6
-------------------------------------------------------------------------------
 Total                   8          700          596           34           70
-------------------------------------------------------------------------------

v1.0.5 Readline basics

Jumping ahead a little bit, because the next few releases I was far too excited about the project, and pushing out releases very fast.

v1.0.5 added the ability to skip passages, and the C-u delete all letters readline binding. It would also display the shortcuts on the bottom of the screen before they started typing.

readline bindings and shortcuts

By now we had also switched the WPM algorithm to the standard 5 chars == 1 word rather than 1 word == 1 word. The --legacy-wpm option existed to revert if desired.


v1.10.0 Ringbuffers for days

That’s right, we didn’t go straight to v1.1.0, we made the cardinal sin of going to 1.10.0. I did it, and I’d do it again 😀

We had made some exciting progress since v1.0.5.

v1.10.0 general ui

You’ll see there was an error indicator, and errors now showed up in the same bright red as they do today, but the real exciting newest feature was history.

You can go back up to 20 passages in the past using a super efficient ringbuffer like data structure.

This was the last release to use reqwest to fetch lang packs. It was also around this time I got a lot of help from Jeffas.

With one of the many things he did, he removed that darned backspace bug. To get around it we asked tui to completely remove the cursor and emulate it by manually highlighting the letter the user was on.

I appreciate all my contributors, but he put in quite a bit of work around this time!

Also at this point we had broken the 1k LoC boundary.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[root@79284cf5d498 terminal-typeracer]# tokei
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 Makefile                1           21           16            0            5
 Markdown                1           75           75            0            0
 Rust                   11         1436         1146          130          160
 Shell                   2           30           18            4            8
 TOML                    1           59           42            9            8
-------------------------------------------------------------------------------
 Total                  16         1621         1297          143          181
-------------------------------------------------------------------------------

v1.1.1 Friendship with reqwest ended, now libgit is my best friend

At this point, we moved away from downloading tarballs in releases of data packs to using libgit to manage language packs.

This was more configurable for us, and also cut stripped binary size to 1.8MiB. It also dropped build times by a huge amount.

Originally the lang pack was stored in the same repo as Typeracer, but after a single iteration, we moved it away to keep git activity of the two separate, and to keep downloaded size down.

Downloading with libgit

Checking bounds title still persisted in the downloader UI at this point.

The upgrade path to everything before this release was a little bugged thanks to my lack of experience with libgit.

For 1.0.9, 1.10.0, or 1.1.0 to run, 1.0.8 had to be run first.

We do our best not to ever break the user experience, so this slipped right on by.

This release also unified a bunch of our keypresses: e.g. C-c is always exit or back, C-n is always next passage, and more.


v1.2.1 Configurability time

It was about time this program got a config file. It didn’t really do a lot yet, but it was there in case you wanted it.

You could configure the lang pack main repo to point to one of your own git repos if you wanted. Nice :D

We also hit about 1500+ lines of code too! We’re getting to be a medium sized project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[root@79284cf5d498 terminal-typeracer]# tokei
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 Makefile                1           22           16            1            5
 Markdown                1          133          133            0            0
 Rust                   13         1832         1484          149          199
 Shell                   2           31           19            4            8
 TOML                    1           55           40            8            7
-------------------------------------------------------------------------------
 Total                  18         2073         1692          162          219
-------------------------------------------------------------------------------

v1.2.2 Scaling time

I love this release, because for the first time in ages, the UI scales. Hacked together in a few hours back after my interview with Google, I put together a really simple version of CSS scaling rules and a styles.css (styles.rs) that takes values based on screen dimensions to scale the UI.

With it, now we have a UI that nicely scales to screen size.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    β”ŒLarry Wall (Programming Perl)───────────────────────┐  β”ŒStats──────┐
    β”‚The three chief virtues of a programmer are:        β”‚  β”‚WPM   24   β”‚
    β”‚Laziness, Impatience and Hubris.                    β”‚  β”‚           β”‚
    β”‚                                                    β”‚  β”‚Error 0    β”‚
    β”‚                                                    β”‚  β”‚           β”‚
    β”‚                                                    β”‚  β”‚           β”‚
    β”‚                                                    β”‚  β”‚           β”‚
    β”‚                                                    β”‚  β”‚           β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚           β”‚
    β”ŒType out passage here───────────────────────────────┐  β”‚           β”‚
    β”‚three                                               β”‚  β”‚           β”‚
    β”‚                                                    β”‚  β”‚           β”‚
    β”‚                                                    β”‚  β”‚           β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

   ^C exit  ^R restart passage ^N next passage  ^P previous passage  ^U cl

It’s so tiny :D

I did not get the Google job though, but I guess this is a nice consolation.


v1.4.3 QoL features, colors and combos

These sets of releases are some of my favorites because they had some really simple, but nice to have features.

One of the simplest is combo: get a combo of 60 (do not mess up for 60 consecutive keystrokes) and the boxes change color to a fun green color. The shade is a little different if you hadn’t made a mistake yet.

We also introduced instant death mode (make a mistake and you lose):

instant death

This was also around the time I made it so words would disappear from the main box as you typed them (and set as default). It ensured the user could always type the full passage and made it so your eyes didn’t have to dart around when typing.

I had a lot of fun making it!

Oh and we’ve also gone past the 2kLoC achievement too!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[root@79284cf5d498 terminal-typeracer]# tokei
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 Makefile                1           22           16            1            5
 Markdown                5          305          305            0            0
 Rust                   16         2283         1887          152          244
 Shell                   2           31           19            4            8
 TOML                    1           57           41            8            8
-------------------------------------------------------------------------------
 Total                  25         2698         2268          165          265
-------------------------------------------------------------------------------

The most important thing though is that the title in the repo installer finally no longer said Checking bounds.

no more bounds


v1.6.0 Full wide character support

Rust may support UTF-8 nicely, but it turns out you can do terrible things very easily anyway, because the unicode standard is interesting.

One of the folks using Typeracer was interested in trying to use it to type out Chinese passages.

Well you know what, why not? It was the beginning of the COVID pandemic in America and I wasn’t going outside.

Wide character support also lets me do terrible things like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    β”ŒUser input─────────────────────────────────────────┐  β”ŒStats─────┐
    β”‚πŸ‘ͺπŸ‘ͺ                                               β”‚  β”‚WPM   0   β”‚
    β”‚                                                   β”‚  β”‚          β”‚
    β”‚                                                   β”‚  β”‚Error 0   β”‚
    β”‚                                                   β”‚  β”‚Combo 0   β”‚
    β”‚                                                   β”‚  β”‚Acc   0%  β”‚
    β”‚                                                   β”‚  β”‚          β”‚
    β”‚                                                   β”‚  β”‚          β”‚
    β”‚                                                   β”‚  β”‚          β”‚
    β”‚                                                   β”‚  β”‚          β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚          β”‚
    β”ŒType out passage here──────────────────────────────┐  β”‚          β”‚
    β”‚                                                   β”‚  β”‚          β”‚
    β”‚                                                   β”‚  β”‚          β”‚
    β”‚                                                   β”‚  β”‚          β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

If you did this in an earlier release though, it would have crashed and burned.

Note this is impossible to align properly on by website using the code formatter. It looks good in a terminal though, I promise.


v1.7.0 Stop - it’s graph time πŸ’ƒ

This is the first release where I retained data across runs. Using SQLite it was surprisingly painless.

I had a number of requests for adding a graph of how users had done over time, so here it was.

Graph time

You’ll note the days shown are not conventional dates, but rather how many days back from your last play.

I didn’t want to show based on the current date since it would really skew results.

The numbers could easily show “real” dates, but British people would get mad at my American dates, and if I picked ISO dates, Americans would be confused, so days since last play it is.


v2.0.0

The final release (for now) put out just a few days ago. Now with support for multiple lang packs, you can use the main lang pack and all its quotes without forking it, adding your own stuff, and then having to stay in sync with the main pack.

multi pack

We also set up some real SQL migration using refinery, and made it real easy to add new game modes.

The final code count comes out to almost 4k LoC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[root@79284cf5d498 terminal-typeracer]# tokei
-------------------------------------------------------------------------------
 Language            Files        Lines         Code     Comments       Blanks
-------------------------------------------------------------------------------
 Makefile                1           22           16            1            5
 Markdown                5          339          339            0            0
 Rust                   29         4170         3442          237          491
 Shell                   2           31           19            4            8
 SQL                     2           52           52            0            0
 TOML                    1           67           45           11           11
-------------------------------------------------------------------------------
 Total                  40         4681         3913          253          515
-------------------------------------------------------------------------------

Weighing in as a solid mid-sized project.

Wrap up

I had a great time implementing all of this. It was a superb opportunity to learn Rust and I made a program I really enjoy using, and hopefully other folks do to!

Share on: