Skip to content

Instantly share code, notes, and snippets.

@lexi-lambda
Last active September 5, 2022 19:37
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lexi-lambda/701f1f1282401059f13a4220e8178ba4 to your computer and use it in GitHub Desktop.
Save lexi-lambda/701f1f1282401059f13a4220e8178ba4 to your computer and use it in GitHub Desktop.

KSSU splits timer GUI challenge

This document presents an informal “GUI framework benchmark” along the lines of TodoMVC: it is a relatively simple, well-defined problem that can be used to illustrate the strengths and weaknesses of different GUI paradigms. However, unlike TodoMVC, this problem is specifically designed to stress test state management in multi-stage modal flows, where modifications to the application state can be complex but must not be committed immediately.

The problem in question is to write a speedrunning timer for Kirby Super Star Ultra’s True Arena boss rush mode. This is a completely meaningless problem to most people, but that’s okay—this document does not assume any familiarity with KSSU or with speedrunning timers more generally. However, I do want to go over a couple basics to provide some context on the problem being solved:

  • The True Arena is a videogame boss rush mode where the objective is to beat a series of bosses as quickly as possible.

  • You always fight the same 10 bosses, but the first 6 are fought in a random order each time. The final 4 bosses are always fought in the same order.

  • Time is tracked using an in-game timer that stops between bosses. This is convenient, since it means a player can stop between each boss to enter their time.

    • Times are tracked in a XXX:YY:ZZ format, where XXX is minutes, YY is seconds, and ZZ is hundredths of a second.
  • It is desirable to keep track of a player’s best time per boss. This is useful because the game only displays the running total in the in-game timer, and since the first 6 bosses are fought in a random order, it can be tricky for a player to know if they are ahead or behind their average time partway into a given attempt.

With that out of the way, let me describe the basics of what the application should do. As a player attempting this speedrun, I want an application that automatically supports entering these times and running the relevant calculations. I want it to show me what my best times are for each boss, and during a run, I want to know if I’m ahead or behind my personal best (and by how much). The full details are spelled out in the sections below, but here are a couple additional general points of note:

  • The application itself should be “graphical”, but my definition of a graphical application is quite loose. It can be a truly graphical application that runs in a native windowing system, it can be a web app, or it can even be an ncurses-style terminal application. I don’t really care, so long as the interface generally resembles what I describe.

  • To keep the problem simple, I don’t care about persistence at all. On application startup, you can assume the user has not yet entered any times, and while times should be saved in-memory between runs, once the application is closed, it’s fine to discard all of the stored records.

Overview

The application consists of two panels: a splits display (on the left), and an interactive menu (on the right). When the application starts up, it should look something like this:

┌─────────────────────────┬────────────────────────────────────────────────────┐
│ Splits                  │ Menu                                               │
│                         │                                                    │
│ Boss 1           (none) │ > Start Run                                        │
│ Boss 2          0:20:47 │   Quit                                             │
│ Boss 3           (none) │                                                    │
│ Boss 4           (none) │                                                    │
│ Boss 5           (none) │                                                    │
│ Boss 6           (none) │                                                    │
├─────────────────────────┤                                                    │
│ Boss 7           (none) │                                                    │
│ Boss 8           (none) │                                                    │
│ Boss 9           (none) │                                                    │
│ Boss 10          (none) │                                                    │
├─────────────────────────┤                                                    │
│ sum of best      (none) │                                                    │
│                         │                                                    │
└─────────────────────────┴────────────────────────────────────────────────────┘

The left hand side displays the best times per boss and a total. Since times may not exist if the user hasn’t gotten to that boss yet, some of the times may display (none), but once the player has completed the boss rush once, all of the times should be filled in.

On the right hand side is the menu. This menu is not particularly complex or interesting, but it should be simple to navigate. The menu should be navigable using the keyboard, since a user will want to quickly enter times, but having mouse support would be nice to have as well.

Active Run

Once a run is started from the main menu, the application switches into a different mode, the active run mode. The main screen in that mode should look like this:

┌────────────────────────────────────┬─────────────────────────────────────────┐
│ Splits                             │ Menu                                    │
│                                    │                                         │
│ Boss 1           (none)            │ > Enter Time                            │
│ Boss 2          0:20:45   +0:00:02 │   Cancel Run (& save new bests)         │
│ Boss 3           (none)            │   Discard Run (& undo bests)            │
│ Boss 4           (none)            │                                         │
│ Boss 5           (none)            │                                         │
│ Boss 6           (none)            │                                         │
├────────────────────────────────────┤                                         │
│ Boss 7           (none)            │                                         │
│ Boss 8           (none)            │                                         │
│ Boss 9           (none)            │                                         │
│ Boss 10          (none)            │                                         │
├────────────────────────────────────┤                                         │
│ sum of best      (none)            │                                         │
│                                    │                                         │
└────────────────────────────────────┴─────────────────────────────────────────┘

The left panel should expand to show a new column, which represents the differences between the player’s best run and their current time, per boss. For the first six, this can be a tad confusing, since the diffs can occur in any order, but that’s okay for this simple example. (A more sophisticated implementation might want to dynamically reorder the list based on the bosses seen so far on the current run.)

The primary action in this mode is the Enter Time menu item, which allows the user to enter the current time displayed on their in-game timer, as the name implies. If the user has not yet completed five bosses, this should proceed to the Boss Selection menu, otherwise it should proceed directly to the Time Entry screen for the relevant boss.

On a bad run, it is likely the user will want to abort the run early and start over, but if they had any new personal best times for a certain boss, they should be able to save them. The Cancel Run option does that, and it returns to the main menu with any new bests “committed”.

If a run has gone awry because the entered times are somehow invalid, the Discard Run option returns to the main menu and discards any new personal bests. It would be nice to offer a finer-grained way to fix improper times, but this is fine to start.

Boss Selection

When a user selects Enter Time during a run, but they have not yet defeated five bosses, the application should prompt the user to ask which boss they want to log a time for:

┌────────────────────────────────────┬─────────────────────────────────────────┐
│ Splits                             │ Menu                                    │
│                                    │                                         │
│ Boss 1           (none)            │ > Boss 1                                │
│ Boss 2          0:20:45   +0:00:02 │   Boss 3                                │
│ Boss 3           (none)            │   Boss 4                                │
│ Boss 4           (none)            │   Boss 5                                │
│ Boss 5           (none)            │   Boss 6                                │
│ Boss 6           (none)            │                                         │
├────────────────────────────────────┤                                         │
│ Boss 7           (none)            │                                         │
│ Boss 8           (none)            │                                         │
│ Boss 9           (none)            │                                         │
│ Boss 10          (none)            │                                         │
├────────────────────────────────────┤                                         │
│ sum of best      (none)            │                                         │
│                                    │                                         │
└────────────────────────────────────┴─────────────────────────────────────────┘

Note that the menu should not display any bosses the user has already logged a time for.

Once a boss has been selected, this should proceed to the Time Entry screen.

Time Entry

The most important piece of functionality for the application is being able to actually enter times. This screen looks like this:

┌────────────────────────────────────┬─────────────────────────────────────────┐
│ Splits                             │ Menu                                    │
│                                    │                                         │
│ Boss 1           (none)            │ Entering time for: Boss 4               │
│ Boss 2          0:20:45   +0:00:02 │                                         │
│ Boss 3           (none)            │ > ___:__:__                             │
│ Boss 4           (none)            │                                         │
│ Boss 5           (none)            │   Save Time                             │
│ Boss 6           (none)            │   Cancel                                │
├────────────────────────────────────┤                                         │
│ Boss 7           (none)            │                                         │
│ Boss 8           (none)            │                                         │
│ Boss 9           (none)            │                                         │
│ Boss 10          (none)            │                                         │
├────────────────────────────────────┤                                         │
│ sum of best      (none)            │                                         │
│                                    │                                         │
└────────────────────────────────────┴─────────────────────────────────────────┘

The user interface should provide a time entry field that supports the XXX:YY:ZZ format used in-game, and it should only allow numbers. If the user selects Save Time, the time for that boss should be saved, and the application should return to the main Active Run menu, unless that was the time for the last boss, in which case the run is finished, and it should return to the main menu. If the user selects Cancel, the application should return to the main Active Run menu without saving any times.

Remember that the user enters their times as a running, cumulative total (since that’s what’s displayed on screen), but boss times must be tracked per-boss. Therefore, you’ll have to do some simple time arithmetic to calculate the relevant diff for each individual boss.

Technical requirements

In terms of concrete specifications, that’s everything you need to worry about. There’s nothing else! The quality of the implementation is scored on the following (qualitative) metrics:

  • Minimal boilerplate. Ideally, individual components should be locally-contained and “snap together” automatically without top-level boilerplate needed to route events to the right places or integrate state updates.

  • Single source of truth for state. State should not need to be manually transferred between cells except when necessary due to the user performing an action that commits temporary state.

  • Separation of rendering logic from state updates. Rendering logic should ideally be a pure function from the current state to some notion of a view. If some imperative drawing logic is required, that is alright, but it should not mutate the application state. Likewise, code that handles state updates should not need to manually trigger repaints.

  • Component encapsulation and reuse. The “menu” component is used frequently, and ideally it should be possible to write a generic menu component that can be embedded any place in the application without needing to duplicate code. Some sort of menu “component” that can be “instantiated” multiple times for each screen is preferable to a single component that keeps track of its current options as a piece of mutable state.

  • Declarative navigation. Navigating between application states should be done via some explicit notion of state transition, not by manually adding/removing individual components from a single, shared container. Ideally, the state machine should be represented declaratively, and the component hierarchy should be a function of the current navigation state. It’s okay if a state transition results in imperative manipulation of the component hierarchy, but this should be kept as an implementation detail from the perspective of the state processing code.

  • Separation of concerns with support for local reasoning. As much as possible, related pieces of logic should be locally contained such that modifying them only requires altering a single place in the code. Modifying or extending a single part of the application should not require changing distant pieces of code.

    For example, adding a new subscreen to the application should only require modifying the new screen’s direct parent. It should not require changes to the entire hierarchy of components above the one being added, and it definitely shouldn’t touch any sibling components (except as necessary to ensure sharing of state).

    Similarly, routing of events should be as local as possible. Even if events are internally propagated through the entire component hierarchy, components should not need to worry about events they do not directly care about, even if their children do.

Most of these things are subjective, so obviously, use your best judgment. The ultimate goal is to implement the specified behavior in a way that is as modular, testable, extensible, and understandable as practically possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment