Making a Simple Web Game with _hyperscript

By: Aiden White
Posted:
Last Edited:

I wanted to make a Wordle-like daily guessing game for long distance running fans like myself. The premise was: given a race result (i.e. meet, date, distance, time, and place), try to guess the name of the athlete. If the player didn't know or guessed incorrectly, they would get to see another clue until they ran out of guesses.

The game logic was simple, so perhaps the more interesting challenge was actually sourcing the athlete and result data. I will probably make a separate post about that in the future, but basically it involved GraphQL, Python, and SQLite.

So all the frontend needed to do was:
  1. Fetch the athlete & their results for the current date
  2. Display the first clue
  3. If the player guessed correctly, end the game
  4. Else, display the next clue unless the maximum number of guesses had been reached
It seemed like overkill to bring in an entire JavaScript framework for these simple requirements. It would make much more sense to implement it in vanilla JS or jQuery, but I wanted to try something new and interesting. Enter hyperscript.

For those unfamiliar with hyperscript, you can read all about it at hyperscript.org. In a nutshell:
"Hyperscript is a scripting language for doing front end web development. It is designed to make it very easy to respond to events and do simple DOM manipulation in code that is directly embedded on elements on a web page."
One of the advantages of hyperscript is its fantastic readability. That means when I inevitably abandon this project and return to it after a few months or years to make a bugfix or add a new feature, I can quickly understand how the code functions and what I was thinking when I wrote it.

So after taking a look at the hyperscript docs, I dove in. It was a bit difficult at first. It took some adjusting to be able to write what is essentially pseudocode, even coming from a language like Python. Other hyperscript users have said, and I would agree, that some of the time saved from hyperscript's readability is exchanged for the time it takes to pick up initially. But once I got going, I came to really enjoy hyperscript's simple syntax and DOM manipulation.

I created the game as a single-page application with only a couple other endpoints for fetching files with data. I did not use any backend framework for implementing routes, but rather served the static files directly with NGINX. In the <head> of the index.html doc, I created a <script type="text/hyperscript"> for initializing some global variables and functions. It looks like this:
init
set $guessNum to 0
set $maxGuesses to 6
set userDTF to Intl.DateTimeFormat().resolvedOptions()
make a Date called rawDate
set $shortDate to rawDate.toLocaleDateString('en-CA', {timezone: userDTF.timezone})
set $shortDate to $shortDate.split(',')[0]
set readableDate to rawDate.toLocaleDateString(userDTF.locale, 
    {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'})
put readableDate into #date

def showNextResult(data, i)
    make a <tr> called row
    make a <td> put data['results'][i]['discipline'] into it then put it into row
    make a <td> put data['results'][i]['date'] into it then put it at the end of row
    make a <td> put data['results'][i]['competition'].split(',')[0] into it then put it at the end of row
    make a <td> put data['results'][i]['country'] into it then put it at the end of row
    make a <td> put data['results'][i]['mark'] into it then put it at the end of row
    make a <td> put data['results'][i]['race'] into it then put it at the end of row
    make a <td> put data['results'][i]['place'] into it then put it at the end of row
    put row at the end of #resultsBody
end
The userDTF object is something I used for localization of dates and times. It also shows just how smoothly hyperscript integrates with JS APIs. The showNextResult() function is what adds a new row to the clue table by taking in the dict/map of all the results and the index of the result to show. It inserts table data elements with each value into a table row, then appends that row to the element with id #resultsBody, which is the main table. There are undoubtedly more clean/efficient ways to do this, but this is what I did.

I decided to fetch the data and display the first row with the body tag, so that things had already been initialized in the <head> and it would kick off as the rest of the page content loaded in:
<body _="
    init fetch `/json/${$shortDate}.json` as an Object 
    set $data to the result
    call showNextResult($data, 0)
">
I have to say, I absolutely love using fetch in hyperscript so that I don't have to deal with Promises. Sure, it may be more limited than using JS, but this is a simple application and it has everything we need. In another <script type="text/hyperscript"> at the bottom of <body>, I fetch some athlete names from a .csv so I can autocomplete names when a player is typing their guess:
init
fetch /athletes.csv as String
set $athletes to the result
set $athletes to $athletes.split('\n').map( \ x -> x.split(', ')[0])
Here is the autocomplete logic in the <input>:
on input
get #listDiv then set its innerHTML to ''
set searchTerm to my value
if searchTerm.length > 1
    set searchResults to $athletes.filter( \ name -> name.toLowerCase().includes(searchTerm.toLowerCase()))
    make a <ul/> called autocompleteList put it into #listDiv
    repeat for x in searchResults
        make a <li/> put x into it 
        set its @tabindex to '0'
        set its @script to 
        `
            on click set #nameInput.value to my innerHTML then get #listDiv set its innerHTML to ''
        `
        then put it at the end of autocompleteList
    end
end
I even put hyperscript on the autocomplete <li> elements as a click handler. Not dealing with event listeners makes me happy :)

All the rest of the game logic lives inside the <input type="submit"> button and requires little to no explanation:
on click or submit
if ($guessNum < $maxGuesses) and (#nameInput does not match .correct)
    increment $guessNum
    if #nameInput.value.toLowerCase() is $data['name'].toLowerCase()
        add .correct to #nameInput
        if $guessNum > 1
            put 'Solved in ' + $guessNum + ' guesses!' into #message 
        else 
            put 'Solved in ' + $guessNum + ' guess!' into #message 
        end
    else
        make a <input/>
        set its @value to #nameInput.value
        add .incorrect to it
        add @disabled to it then put it before #guesses 
        set #nameInput.value to ''
        if $maxGuesses == $guessNum
            put 'Out of guesses! <br> Answer: ' + $data['name'] into #message
        else
            if $maxGuesses - $guessNum == 1
                put 'Incorrect. 1 guess remaining' into #message 
                showNextResult($data, $guessNum)
            else
                put 'Incorrect. ' + ($maxGuesses - $guessNum) + ' guesses remaining' into #message 
                showNextResult($data, $guessNum)
            end
        end
    end
end
I have some ideas for a few more features like allowing a player to play previous days' games, but that's all for now. To play the game and see this hyperscript in action, visit athleteguesser.com.

I enjoyed trying out hyperscript and would not hesitate to use it in the future for simple scripting and DOM manipulation. It's also worth mentioning that hyperscript is made by Big Sky Software (the creators of HTMX), and they have a great community on Discord.

How to Setup HTTPS for your Google Domains with Google Cloud CLI

By: Aiden White
Posted:
Last Edited:

This website supports HTTPS connections. I wanted to make sure that any user logins or form submissions I implement in the future would be encrypted when communicating with the server. In order to achieve this, I needed to get a certificate authority (CA) to issue me a TLS (Transport Layer Security) certificate for the aidenwhite.com domain. Since I was already using Google Domains, I decided to use the free service Public CA from Google Trust Services (GTS). Public CA can be used via any Automatic Certificate Management Environment (ACME), and I recommend the free, open source tool called Certbot. Here's how I managed to set up TLS certificates at no additional cost using Google Domains, Google Cloud CLI, and Certbot:

1) Install Certbot on the server

In my case, I am using a Virtual Private Server (VPS) from Digital Ocean running Ubuntu 22.04. After SSH'ing onto the server, I install certbot with apt install.
apt install certbot

2) Install and Start Google Cloud CLI

In my case I curl'd and extracted the download, then ran the install script and the init command.
curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-441.0.0-linux-x86_64.tar.gz
tar -xf google-cloud-cli-441.0.0-linux-x86_64.tar.gz 
./google-cloud-sdk/install.sh
./google-cloud-sdk/bin/gcloud init

3) Get EAB key from Google Domains

Log into Google Domains in a browser, select the domain you want to configure, and go to Security. Scroll down to SSL/TLS Certificates and underneath Google Trust Services click "Get EAB key". Keep track of the EAB and HMAC keys that are generated. We will use these to connect to our Google Domains account while using Certbot.

4) Register ACME account

Run the following command on the server with Certbot installed. For server, see the Google documentation for more information. Basically, use "https://dv.acme-v02.api.pki.goog/directory" in production and "https://dv.acme-v02.test-api.pki.goog/directory" in staging.
certbot register \
    --email "EMAIL_ADDRESS" \
    --no-eff-email \
    --server "SERVER" \
    --eab-kid "EAB_KID" \
    --eab-hmac-key "EAB_HMAC_KEY"

5) Request Certificates

Run the following command on the server. For server, see above. For domains, enter a comma-separated list of domains for which you are requesting certificates, i.e. "aidenwhite.com, www.aidenwhite.com".
certbot certonly \
    --manual \
    --preferred-challenges "dns-01" \
    --server "SERVER" \
    --domains "DOMAINS"

7) Add DNS Record

Now Certbot will prompt you to publish a specific TXT record at a given hostname. Go to the DNS page in Google Domains and paste and save this record. Then go back to your terminal running Certbot on the server and press enter to make Certbot validate the DNS record.

8) Deploy Certificates

If Certbot was able to successfully validate your DNS record, it will notify you that a certificate and key have been created and give you their location. It will also tell you the date the certificate expires and directions for renewing the certificate, so make sure to save these for later. Then, all you need to do is let your web server know where to find your certificate and key. For example, I run NGINX so I go into my /etc/nginx/sites-enabled/aidenwhite.com.conf file and add the paths to my certificate and key. See below for an example. I also redirect any HTTP requests on port 80 to HTTPS on port 443.
server {
    # listen on port 80 (HTTP)
    listen 80;
    server_name aidenwhite.com www.aidenwhite.com;
    location / {
        # redirect any requests to the same URL but on HTTPS
        return 301 https://$host$request_uri;
    }
}

server {
    # listen on port 443 (HTTPS)
    listen 443 ssl;
    server_name aidenwhite.com www.aidenwhite.com;

    # location of the certificate & key
    ssl_certificate /etc/letsencrypt/live/aidenwhite.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/aidenwhite.com/privkey.pem;

...

Don't forget to restart NGINX to reflect these changes
systemctl restart nginx
That's it! Your site should now support HTTPS connections.

My GeoPandas Plot Went Viral on Reddit

By: Aiden White
Posted:
Last Edited:

Plot of the tree density in Manhattan

In December 2022 I entered a competition on datacamp that involved visualizing the tree density of Manhattan, NY. I decided to post the above plot I created with GeoPandas to the subreddit /r/dataisbeautiful, and to my surprise, it ended up receiving over 1 million views and 8,000+ upvotes. I got a lot of great feedback in the comments about how I could improve the plot, and I think that engagement may have contributed to reddit showing the post to more users. It just goes to show that if you're willing to be a bit vulnerable and put your work out there to the world, you can learn a lot.