Tracking Games of Kelly Pool

Julian Ferrone
I wrote a fully client-side app to track my Kelly Pool games using HTML, JavaScript, and CSS.
Published: April 11, 2026

Sydney is a damn expensive city and none of my mates drink Either by choice or because they’ve got the Asian flush genes. It’s a shame, there’s a lot of artistry in wines, rums, whiskeys, tequilas, cocktails, etc., that I can’t share with them—not to mention the simple pleasure of an Aperol spritz during a hot summer’s day., so the billiards hall is our go-to hangout spot. Typically we’ll play Kelly pool for an hour or so, then chow down on as much protein as we can stomach at the unlimited Korean BBQ buffet nearby.

But there’s a huge problem. The last time we went out, the devs of the app we use to keep score managed to make the already bad ads even worse—engaging in all sorts of dark pattern shenanigans like hiding or moving the “close button” ad.

And that’s saying nothing of the utterly spamtastic ads that get displayed—the first time I cracked open the app in a while, I got hit with an ad that promised me that “Your PDF viewer has been infected with a virus !!1!1!!”.,

So I decided to write my own app to track our games, purely out of spite And also because, according to one of my friends, software is “dark magic” and it always amuses me how easy I find it. On the other hand he’s a hardware guy (civil engineer by trade) and that doesn’t make much sense to me. So….

I whipped up a Python implementation in a few hours, but it was usable only on computers, being a command-line REPL style app. When my mates and I play, we only have our phones, so I needed to write something that would work on mobile.

It didn’t seem worth learning Kotlin just to write one Android app, plus the inevitable hurdles when it came to publishing the app officially (or sideloading it) made that even less appetising.

Luckily, there’s a universal app framework I can use—the web browser Generally speaking, I’m not a huge fan of this approach. I’ve been burned by too many slow Electron apps—looking at you, Microslop Teams—and in general I’d prefer to write apps with native GUI frameworks, on the principle of conserving memory usage.But in cases like these, I don’t mind. It’s a tiny app (a paltry 33 kB which I could shrink even further with a minifier if I thought it necessary), so the memory usage isn’t a problem, and the convenience of distributing and using the app vastly outweighs my usual concerns.!

Now, this is not the first time I’ve written a client-side app, and it’s not even the first time I’ve written one that’s packaged as a web app, but it is the first time I’ve written one in HTML, JS, and CSS—nothing fancy, just the most barebones, plain, vanilla web app I can muster.

What is Kelly Pool? #

Kelly pool is a game for 2 to 15 people on a pool ball.

Now, it turns out that I play a modified ruleset for Kelly pool. I had no idea of this until I searched up the rules on Wikipedia, but whatever, this is the game my father taught me and these are the rules I wrote this app for:

  1. Each player gets allocated 1–7 pool balls randomly, depending on how many people are playing. These balls are to be kept secret from the other players. The 8-ball can be allocated and doesn’t need to be sunk last like in traditional pool—it’s just a normal ball in this game.
  2. Players rotate in the same order at all times. I recommend following the name order in the app: left to right, then top to bottom.
  3. The aim is to be the last player to have an allocated ball on the table. This doesn’t include balls that aren’t allocated to anyone.
  4. If you lose all of your allocated ball numbers, you are eliminated from the game.
  5. Play continues until all but 1 player has been eliminated.

My mates and I play by pub rules when the cue ball is sunk; next player:

  1. Gets 2 shots.

  2. Must place the cue ball on the head string Technical term for “the white line where you put the cue ball when you’re about to break”. Search for an image of the anatomy of a pool table, it’ll make sense..

  3. Must shoot the cue ball towards the foot rail The far end of the table, where the 1–15 balls are racked..

But you can negotiate foul terms with your friends and family however you like.

If you want the quote unquote “real rules”, you can always check Wikipedia, but I think my variant is fun and easier to play.

You also can’t play the Wikipedia rules using my app.

Writing the App #

Project Constraints #

I had a couple constraints when I wrote this app.

Firstly, like I mentioned before, I wanted it to be easily accessible on mobile, which is what killed off my Python implementation and drew me to writing a web app.

Given I was going to write a web-app, I also wanted to continue experimenting with different ways to write them fully client-side. I do love writing Rust, but the WASM approach in Forsp didn’t feel very ergonomic given I had to keep schlepping inputs and results back and forth between the main thread and the WASM in the web worker.

There are a lot of GUI frameworks in Rust, including some that work in WASM, but none of them seem particularly stable at the moment so I nixed this idea—I’m not dead set against this approach, I’m just waiting until the ecosystem is a bit more developed, although maybe I’ll try it another time anyways.

HTML, JS, and CSS it was.

On a microarchitectural note, I chose to unify the JS and CSS inline into the HTML file to further shrink the app into a single file. That way, users can download the HTML file and run it themselves, 100% offline. This did complicate development somewhat. The formatting of my CSS and JS is a bit off in places since I wrote the full thing in one file—it runs completely fine, it’s just aesthetically unappealing.I may set up a proper build pipeline for this in future where I keep the source code for the HTML, CSS, and JS in separate files, but minify them and unify them into one index.html file before deploying it to my website.

This is a meaningless optimization, but it also cuts down the number of server requests to get the app running from 3 to 1.

A Selection of Commentated Code Snippets in No Particular Order #

I thought I had a rather clever idea for the HTML structure—rather than programmatically create whole screens at once, I created a few divs to act as various screens and dialogs, but kept them all hidden until I needed them:

<div id="app">
    <div id="playerDetails">
    <!-- Where user picks number of players and balls per player -->
    </div>
    <div id="playerNames" data-invisible="true">
    <!-- User allocates names to players -->
    </div>
    <div id="showAllocation" data-invisible="true">
    <!-- Show each player their allocated balls -->
    </div>
    <div id="game" data-invisible="true">
    <!-- Tracks the game in progress -->
    </div>
</div>

And then whenever I want to show a particular app screen, I have a function to set all the other divs to have the attribute data-invisible set to true:

function showAppScreen(screenId) {
    const app = document.getElementById('app');
    const children = Array.from(app.children);
    children.forEach((appScreen) => {
        if (appScreen.id === screenId) {
            appScreen.removeAttribute('data-invisible');
        } else {
            appScreen.setAttribute('data-invisible', true)
        }
    });
}

Along with a little sprinkle of CSS to hide said invisible div`:

div[data-invisible=true] {
    display: none;
}

Originally I was using the hidden attribute to hide the other app screens, but that interacted bizarrely with the flex items of the divs under #app container div—they didn’t end up actually hidden, just off-screen, so you could scroll to them. I swapped to the custom data attribute approach to manually control turning the display off on the other divs instead.


I wanted to save game data on a user session basis, which meant saving data to sessionStorage, but I wanted to use some unsupported objects—Maps and Sets—which meant I needed to whip up a custom wrapper.

Thankfully, JSON.stringify (JSON.parse), offers a replacer (reviver) argument which lets us customise how we serialise (deserialise) values to (from) strings, which is the only type that we can store in sessionStorage.

I convert Maps and Sets into tagged objects:

function customReplacer(key, value) {
    if (value instanceof Map) {
        return {
            __type: "Map", 
            __value: [...value.entries()]
        };
    }
    if (value instanceof Set) {
        return {
            __type: "Set", 
            __value: [...value]
        };
    }
    // All other values pass through unchanged
    return value;
}

And can re-parse in the parsed JSON objects to convert back into Maps and Sets:

function customReviver(key, value) {
    if (value && value.__type === "Map") {
        return new Map(value.__value);
    }
    if (value && value.__type === "Set") {
        return new Set(value.__value);
    }
    // All other values pass through unchanged
    return value;
}
function setSessionState(key, value) {
    const str = JSON.stringify(value, customReplacer);
    sessionStorage.setItem(key, str);
}

function getSessionState(key) {
    const value = sessionStorage.getItem(key);
    const parsed = JSON.parse(value, customReviver);
    return parsed
}

There’s still some improvements that can be made to this.

Originally, I had written three separate session state getter functions—getSessionState, getSessionStateMap, getSessionStateSet—but while I was writing this article, I thought to myself that “if I were smarter about how I stringify Maps and Sets, I wouldn’t need those three functions” and then I ended up rolling up my sleeves and writing the type tagging logic. The act of writing about the code I’m writing, and then showing it, often shows me new ways to make it better.It’s an unintended consequence, but useful, and I’ve noticed it happening before.Part of the effect is rubber-ducking, where explaining it forces me to look at my code in new ways.I think the other part of it is that I don’t want to embarrass myself by showing ugly code.On a slightly unrelated note, I’ve heard that livestreaming is another great way to be productive if you’re working on your own projects, as the observers, even if imaginary, encourage you to keep working.If you don’t have your own panopticon, store-bought is fine.

The still-standing improvement is that currently, I don’t save the current screen to the sessionStorage—so if you reload the page, you’ll lose your place and it sends you to the initial screen. It would be nice to fix this.

However, fixing this would probably require saving quite a bit more state about what UI screen we’re on.

A lesson for next time.

TL;DR: Just don’t reload the page, it’ll be fine.


What happens if, during the middle of a game, you forget what balls you have You could always just not care. One of my friends forgets every time we play (and refuses to remind himself). He regularly loses games by potting his own balls.? I already had the code to show players their allocated balls before the game was started, I just needed to extend it a bit so they could check during the game.

This ended up being a doozy and a half.

Problem: after showing a player their allocated balls, I need to dynamically change the callback on the next button to distinguish between a few different scenarios:

  1. If we’re showing all the players their balls, we need to show them one at a time, moving onto the game screen when the last player has seen their allocation
  2. If a player is checking their allocation in-game, we need to show them their allocated balls, then return to the game screen when they’re done

I of course also want to change the text on the next button to say something along the lines of either “pass to next player” or “start the game” or “return to game” depending on where the button will take the player.

Solution: Closure A closure is a function with a reference to an environment—it’s said to “close over” this environment, hence the name—that allows the function to interact with non-local variables (variables not provided as function arguments). You can think of the environment as adding statefulness to the function. shenanigans! I keep the button text and callback information gated behind a function of shape () => { callback: callback, dialogText: dialogText, nextButtontext: nextButtontext }, the result of which provide the necessary details for the next button’s text, the dialog confirmation text, and the callback to call when the user confirms they’re done checking their allocation.

When I need to show a player their allocated balls screen, I call a function setShowAllocationScreen with an interface like so:

function setShowAllocationScreen(player, nextButtonClosure)

player is the player’s name (which we use for showing their name on the GUI), and nextButtonClosure is the nextButtonClosure argument.

I’m still not sure whether this was a clever solution—it seems to have only added to the spaghetti jank hell I put myself in, but it works.

When I want to show all the users their allocated balls, I wrap up that functionality by providing a closure over the state nextPlayerIndex which gets incremented every time we show a new player their allocated balls. When we’ve run through all the players, we change the button text and callback to change the app screen to the game div, which shows the current state of the game: players still in the game and sunken/unsunken balls.

function nextButtonAllPlayers() {
    const players = getSessionState('players');
    let nextPlayerIndex = 0;

    function inner() {
        nextPlayerIndex++;

        const nextPlayer = players[nextPlayerIndex];
        const startGame = (nextPlayerIndex === players.length);

        const nextButtonText = (startGame) 
            ? 'Start game' 
            : `Pass to ${nextPlayer}`;
        const callback = (startGame) 
            ? () => {
                renderBalls();
                renderPlayers();
                showAppScreen('game');
            } 
            : () => setShowAllocationScreen(nextPlayer, inner)
        const dialogText = (startGame) 
            ? 'Are you ready to start the game?' 
            : `Are you ready to pass to ${nextPlayer}?`;

        return {
            nextButtonText: nextButtonText, 
            callback: callback,
            dialogText: dialogText,
        };
    };
    return inner;
}

On the other hand, when I render the game screen, each player needs to get a button that will let them check their allocated balls, then immediately return to the game screen. I provide a stateless lambda to setShowAllocationScreen which does this:

() => return {
    nextButtonText: "Return to game",
    dialogText: "Are you ready to return to the game?",
    callback: () => showAppScreen('game');
});

I originally started off using the built-in window.alert and window.confirm functions to show pop-up dialog boxes to the user—whether to alert them that they’ve put in incorrect inputs (i.e., missing or duplicate player names), or to tell them whose ball was just sunk, or who won the game.

But the dialog boxes those functions produce are ugly and unstyleable.

Lesson learned: I wrote some custom dialog divs and used showModal instead. That way I could style the buttons to have fat padding—all the easier to use on mobile.

Try It Out #

You can play using the web app!

Or watch the demo video below:

Thoughts #

Overall I’d call this experiment a success—I’ve now got a portable app I can use to keep track of my Kelly pool games whenever I hang out with my mates.

In terms of the code itself, I think I need to find some better affordances for working with JavaScript—I got myself mired in a bit of a ball of callback mud.

But ideally, I want to use zero dependencies, I just want to write vanilla JavaScript. Maybe I need to write something lightweight I can use across other apps.

Probably it’d look like an interpreter: a mini VM reading inputs from the player, adding events to a queue (saved in sessionState) to process, and updating the UI. I probably should’ve done this from the start—it would’ve helped me deal with the spaghetti and keep things modular—but this is the first time I’ve ever tried writing an app this way and I didn’t think of it until just now. More rubber-ducking in action.

Another bit of polish I could add to this project would be to add the minifier and auto-packaging pipeline I mentioned before—I would write HTML, CSS, and JavaScript in their own files (with automatic formatters set-up, too) then have some scripts minify them and compile them into 1 HTML file as a packaged up output.

I really wish there were a better way to write these apps—I keep trying to stave off the itch to learn Common Lisp, but I can’t help myself wanting to write a Lisp library that’ll output HTML, JS, and CSS all at once now…