Visualizing Bracket City Puzzles
by Graham Marlow, puzzles , javascript
Lately I've been addicted to a new daily word puzzle game called Bracket City. It's unique among competitors because the game isn't about rearranging letters baked in hidden information, but rather solving hand-written, crossword-style clues.
I recommend giving the daily puzzle a shot before reading the rest of this article since it will help with visualizing the puzzle format. But as a quick rules summary:
- A Bracket City solution is a short phrase
- Certain words are substituted with clues, indicated via a pair of square brackets
- Clues can nest other clues
- You must solve the inner-most clues before you can solve the outer-most
Since Bracket City is basically a recursive crossword, the structure of a puzzle is easily mapped to a tree. And so, in classic programmer-brain fashion, I built a little app that turns a Bracket City puzzle into an interactive tree. Check it out: Bracket Viz.
How it works
I had a couple of realizations while working on this little project.
The first was recognizing how brilliant the Bracket City puzzle structure is. Not only does it spin the age-old crossword in a compelling way that feels fresh, but the actual mechanics for constructing a Bracket City puzzle are super simple. It's a win in all categories, excellence in design.[1]
The second realization was how easy it is to parse Bracket City puzzles into trees and render them via Svelte components. I haven't done much work with Svelte, but the ability to recursively render a component by simply self-referencing that component is incredibly expressive.
If you're unfamiliar with Svelte, don't worry! There's really not that much special Svelte stuff going on in my solution. Most of it is plain old JavaScript.
First thing's first: a class for nodes in our tree:
class Node {
constructor(text = '', children = []) {
this.text = text
this.children = children
}
}
Next, the parsing algorithm.
The basic strategy has a function read through the input string one character at a time. When a "[" is encountered, a new node is created. A couple variables track our position in the resulting tree:
currentNode
points to the most recent nodestack
holds a list of nodes in order
With currentNode
, we can easily append new child nodes to our position in the
tree. With stack
, we can exit the currentNode
and navigate upwards in the
tree to the node's parent.
Here's the algorithm in full:
const parsePuzzle = (raw) => {
// Initial output takes the form of a single node.
const root = new Node()
let currentNode = root
let stack = [root]
for (let i = 0; i < raw.length; i++) {
const char = raw[i]
if (char === '[') {
// Substitutions are marked with ??.
currentNode.text += '??'
const node = new Node()
currentNode.children.push(node)
stack.push(node)
// Update our currentNode context so that future nodes
// are appended to the most recent one.
currentNode = node
} else if (char === ']') {
if (stack.length > 1) {
// Closing brace encountered, so we can bump the
// currentNode context up the tree by a single node.
stack.pop()
currentNode = stack[stack.length - 1]
}
} else {
currentNode.text += char
}
}
// If we have any elements left over, there's a missing closing
// brace in the input.
if (stack.length > 1) {
return [false, root]
}
return [true, root]
}
The return result of the function denotes whether or not it was successful followed by the resulting tree, a simple form of error handling.
In Svelte, we can tie this algorithm together with an HTML textarea in a component like so:
<script>
import parsePuzzle from '$lib/parsePuzzle.js'
let puzzle = $state('')
let [_, tree] = $derived(parsePuzzle(puzzle))
$inspect(tree)
</script>
<textarea bind:value="{puzzle}"></textarea>
And using the tutorial puzzle as an example,
# raw input:
[where [opposite of clean] dishes pile up] or [exercise in a [game played with a cue ball]]
# tree:
Node(
"?? or ??",
[
Node(
"where ?? dishes pile up",
[
Node("opposite of clean", [])
]
),
Node(
"exercise in a ??",
[
Node("game played with a cue ball", [])
]
)
]
)
As the textarea is updated, $inspect
logs the resulting tree. We haven't yet
rendered the tree in the actual UI. Let's change that.
First, update the original component to include a new component named Tree
:
<script>
import parsePuzzle from '$lib/parsePuzzle.js'
import Tree from '$lib/components/Tree.svelte'
let puzzle = $state('')
let [success, tree] = $derived(parsePuzzle(puzzle))
</script>
<textarea bind:value="{puzzle}"></textarea>
{#if success}
<Tree nodes="{[tree]}" />
{:else}
<p>Error: brackets are unbalanced</p>
{/if}
Creating a new component to handle rendering the puzzle tree is not just to tidy up the code, it's to enable a bit of fancy self-referential Svelte behavior. Intro CS courses have taught us that tree structures map nicely to recursive algorithms and it's no different when we think about UI components in Svelte. Svelte allows components to import themselves as a form of recursive rendering.
Here's the Tree
component in full:
<script>
import Self from './Tree.svelte'
const { nodes } = $props()
</script>
{#each nodes as node}
<div>
<div>{node.text}</div>
<div class="ml-4">
{#if node.children.length > 0}
<Self nodes="{node.children}" />
{/if}
</div>
</div>
{/each}
How about that? A Svelte component can render itself by simply importing itself
as a regular old Svelte file. In the template content of the component, we
simply map over our list of nodes and render their text content. If a given node
has children, we use a Self
reference to repeat the same process from the
viewpoint of the children.
ml-4
applies left-margin to each of the children nodes, enabling stair-like
nesting throughout the tree. We never need to increment the margin in subsequent
child nodes because the document box model handles the hard work for us. Each
margin is relative to its container, which itself uses the same margin
indentation.
That about wraps it up! I added a couple extra features to the final version, namely the ability to show/hide individual nodes in the tree. I'll leave that as an exercise for the reader.
Well, there is one thing that is maybe questionable about the design of Bracket City. The layout of the puzzle makes you really want to solve the outer-most clue before the inner-most, if you know the answer. However the puzzle forces you to solve the inner-most clues first. This is a surprisingly controversial design choice! ↩︎