nhiknee

nhi's notes projects

Karmel

  • web
  • PWA
  • games
play it

01What it is

Think NYT Games but with numbers instead of letters. The app is named after a similar math game I played in 7th grade, and it exists because I refused to let the postfix expression algorithm I wrote in CSE12 exist in vain.

The Karmel daily puzzle on a phone: a target of 58, a part-built expression, and a grid of nine digits

02How it's built

How are the expressions evaluated?

Converting the input into postfix order is what handles the precedence and the parentheses. Side note: while writing this project, I was reminded that stacks truly are the most adorable data structure. So literal, so petite, so cute.

infix( 5 + 2 × 9 ) × 8

postfix5 2 9 × + 8 ×

answer184

Getting bored with this explanation? Skip to the non-computer-science-y part

Does every board have a verified solution?

Yes, I make the intern solve it by hand.

The completed path must be a Hamiltonian path on the 3x3 king graph: it visits every square once and moves like a king in chess.

The script that builds the daily puzzles generates a board and a target, hands the pair to the solver, and rejects the puzzle if the solver returns nothing.

The solver picks any one path, which fixes the order of the nine digits. The paths are tried in a shuffled but fixed order, so the pick is not biased toward one corner of the board. An interval dynamic program then covers every operator and every set of parentheses over that order, as illustrated below.

How the solver takes one path apart The game's screen, drawn small: the label TARGET, the number 20 under it, and a two by two board reading 5 and 2 over 8 and 9, with arrows through the tiles in the order the path walks them: 5, then 2, then 9, then 8. The real board is 3x3; this example is cut to 2x2 to simplify the demo. Below, under the title DP table for fixed path 5 2 9 8, every run of those four digits, shortest first, each labelled with how many answers it can produce. The four runs on the chain also carry a sample of those answers, with the one the chain uses circled: 5 in the run 5, 3 in the run 5 2, 12 in the run 5 2 9, and, in the target's own blue because it is the target, 20 in the whole run. A string joins them down the left, and beside each step is the operator it used and the new digit it folded in: minus 2, then plus 9, then plus 8. Written out, 5 minus 2 plus 9 plus 8 is 20. A caption under the table reads: Proof that a path to the target 20 exists! TARGET 20 5 2 8 9 The real board is 3x3. This example is cut to 2x2 to simplify the demo. DP Table For Fixed Path 5 2 9 8 5 2 answers 5 120 2 1 answer 9 2 answers 8 2 answers 5 2 11 possible answers 3 6 7 10 25 60 118 122 240 … 2 9 7 possible answers 9 8 12 possible answers 5 2 9 75 possible answers … 12 15 16 19 23 27 34 51 54 55 63 69 90 102 … 2 9 8 56 possible answers 5 2 9 8 604 possible answers 20 23 24 25 26 27 30 31 32 34 35 39 40 42 43 46 47 50 51 54 55 … − 2 + 9 + 8 Proof that a path to the target 20 exists!

So how many of those tables would you have to build? In principle 784, one per possible Hamiltonian path on the 3x3 king graph. Not 362,880, which is what nine digits would come to if the order were free — (yes, this is a human-placed em dash) the adjacency narrows it down to just 784. Big savings! Yahoo!

In practice the code builds one (1) table. Mega savings! Yeehaw! Across a year's worth of games, all 300+ boards were solved on the very first path tried. That is because nine digits in nine squares are more flexible than they look: every target from 1 to 100, which is the range the game draws its targets from, has a path that lands on it, in 71 milliseconds.

How one board reaches target values from 1 to 100
  • 5 2 9 1 7 8 3 6 4

    1=5+2+9+7+1+3+6-8×4

  • 5 2 9 1 7 8 3 6 4

    2=2+5+1+7+9-8+4-6×3

  • 5 2 9 1 7 8 3 6 4

    3=9+2+5+1+7-8-4-6-3

  • and every target between 3 and 25
  • 5 2 9 1 7 8 3 6 4

    25=1+5+2+9+7+8-4-6+3

  • and every target between 25 and 99
  • 5 2 9 1 7 8 3 6 4

    99=7+5+2+9×8+4+6+1×3

The cost of one of those runs is O(n³ · C²). A run is any unbroken stretch of the path, and you pick one by choosing where it starts and where it stops. Two choices, each one of n spots, so about n² runs. They are the rows in the table above: the path 5 2 9 8 gives ten of them, from 5 on its own up to the whole 5 2 9 8. Each run then gets broken in two, and there are n places to break it, so n² runs times n places is n³ breaks to work through. Every break pairs what the left side can reach against what the right side can reach. n is nine (because the board is 3x3) and it is not going to change, so C, the cap on how many possible answers a run is allowed to keep, is the variable I can control.

What is the tech stack?

This is built entirely on a web stack using JavaScript/React. It ships as a PWA, so it "installs" straight from the browser on Android and on iOS. Players do not need to touch the app store.

Installing Karmel from Safari on iOS in four steps: tap the share button beside the address bar, tap More at the end of the share sheet, choose Add to Home Screen, then tap Add.

Umami handles the product metrics, Sentry the error tracing and alerts.

03Delight engineering

When you solve the puzzle, the animation retraces the route you took across the board. The leaderboard shows everyone's path, so you get to see that someone hit the same target in a way you would never have thought of. Streaks give you a reason to come back tomorrow. The weekly recap turns seven small, forgettable moments into one story you can see.

The double-parenthesis key wraps what you already typed instead of tacking one on the end, because that is how players build an expression, with the parentheses coming as an afterthought. Making players follow the keyboard's layout instead is counterintuitive, and fixing it afterward is phone-throwing frustrating.

I also set it up as a PWA so it lives on a home screen instead of being a website in a tab, and wrote custom bottom sheets that move the way a native one does. A big part of making this app feel good, and therefore work well, is making it behave like it belongs to the phone rather than to a browser. Loading skeletons hold the shape of whatever is coming, so the board does not jump around while you are already reaching for it. The downside of a web-only stack is that it leaves me without haptic feedback or notifications. Darn.

04Edge cases I choose to live with

Why is Gmail the only sign-in option?

Sign-in via email links was previously available, but because the link needs to open in the same browser as the game, the feature would not work for any player who uses the app as an installed PWA. Perhaps I will offer regular email and password sign-in in the future. Heavy on the perhaps.

What if a player uses a huge factorial, like (9!)!?

A woman laughing, captioned NO! NO! NO! DON'T DO THAT!

A factorial that big will not hang the game, but no number will be returned as the running total. 9! is 362,880, and multiplying that many times runs past the largest number JavaScript can hold, so the game reports that expression as uncalculable.

Perhaps there is a better and faster way of calculating these egregiously large expressions; they must have taught it in CSE13.

You may also like

← back to projects