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.