##### Writing a Solitaire TUI with Lenses and Brick
###### 2017-12-02
┌───────────── Solitaire ──────────────┐
│┌──┐│┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐│┌  ┐│ Score:   0
││λ=││┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐│7♠││    │
│└──┘│┌──┐┌──┐┌──┐┌──┐┌──┐│K♥│└──┘│└  ┘│ Moves:   0
│┌──┐│┌──┐┌──┐┌──┐┌──┐│J♣│└──┘    │┌  ┐│
││3♠││┌──┐┌──┐┌──┐│6♦│└──┘        │    │ [New]
│┌──┐│┌──┐┌──┐│9♣│└──┘            │└  ┘│
││3♥││┌──┐│Q♠│└──┘                │┌  ┐│ [Undo]
│┌──┐││4♠│└──┘                    │    │
││7♦││└──┘                        │└  ┘│
│└──┘│                            │┌  ┐│
│    │                            │    │
│    │                            │└  ┘│
│    │                            │    │
└──────────────────────────────────────┘


I’d wanted to write an implementation of Solitaire a.k.a. Patience, Klondike, etc in Haskell ever since I learned about brick, a library for programming terminal user interfaces (TUIs). I liked it because, as the docs say, it

…exposes a declarative API. Unlike most GUI toolkits which require you to write a long and tedious sequence of “create a widget, now bind an event handler”, brick just requires you to describe your interface using a set of declarative combinators. Then you provide a function to transform your application state when input or other kinds of events arrive.

The other component of this project involved learning about lenses. Lenses are a Template Haskell solution to the record problem, which concerns the difficulty of reading from, writing to, and editing in-place deeply-nested record variables. Although Haskell is an immutable language, sometimes in-place modification is simply too convenient to abandon. Lenses are an elegant set of combinators for working around this.

# Application Overview

This essay will be a high-level architecture of the game, but the code itself is decently commented, and only spans one Main.hs and four small helper libaries.

## Brick

As discussed above, brick lets us define

• an app :: App State Event () application state object, and
• an appEvent :: State -> Event e -> EventM () (Next State) event handler and that’s almost entirely it. There’s a bit more business for styling and click region detection, but the core of the game takex place in the event loop within appEvent.

In the above, some keys halt the game, but most of them continue the game either

• with the state s as-is, or
• with the state s modified by some function (newGame, undoMove, or doMove).

## Rules of Solitaire

Before we continue let’s just speak briefly about Solitaire.

+-------+----------------------+
| Stock |         |            |
+-------+ Tableau | Foundation |
| Waste |         |            |
+-------+----------------------+

• Cards start either facedown in the stock or in seven piles of lengths 1, 2,.. in the tableau.
• The stock is always facedown, but can be dealt three at a time to the
• The piles in the tableau are splayed downwards, and start with only their top card visible.
• Nothing starts in the foundation, but cards can accumulate there face-up.

Cards can be moved like so:

+-------+----------------------+
| Stock |         |            |
|   ^   |        <--           |
+-- | --+ Tableau | Foundation |
|   v   |        -->           |
| Waste ->        |            |
+-------+----------------------+


Some more rules:

• in the tableau only a King can go on an empty pile, but any card can go on any other card as long as it has a different color and is of exactly one rank less.
• in the foundation only an Ace can go on an empty pile, and any card can go on a foundation pile as long as it matches the base suit and is of exactly one rank more.

I’m not sure Solitaire is a very interesting game to play, but abstracting the core ideas of cards, displaycards, piles, lists of piles, and operations between them was a lot of fun.

# Custom Types

I think Haskell is fairly readable, so it might be best to just look at the CardTypes.hs source. But just as a quick overview, we define:

• a Card (rank and suit),
• a DCard, a display-card which wraps a Card and contains a preference for being displayed face-up or face-down
• a Pile, which is a list of DCards with an opinion on what sort of card can go at its base (for example, only a King, or only an Ace) as well as a preference for its cards being displayed stacked or splayed out.
• a GSt, a game state which wraps the stock, waste, tableau, and foundation, as well as containing the current score, the elapsed move count, a random seed, and a history of prior fields and scores.

## Show instances

We can make our own type instance of a few of the above custom typeclasses by defining what it means to Show a Rank or Suit.

## Lenses 101

We want to define our record fields with underscores like so:

So that the Lens library can, at compile time, create functions like card or facedir which can be called on DCard objects, like so:

where (^.) is a getter and .~ is a setter (sorta). For more read the lens tutorial.

By the same convention, a deeply-nested object could be accessed with

which makes it super easy for us to just pass around the Field or the GSt gamestate and modify it at any level. Thanks, Lenses!

# Output

Just as we wrote a set of abstract data types above which can be composed into flexible Piles, etc., we want to write a set of abstract render functions which can be composed to draw a Pile, or a DCard, or whatever. Brick wants us to define our app like so:

where drawUI :: GSt -> [Widget ()] handles every part of the program, from the field to the score counters. It is a pure function of the game state and doesn’t need callbacks or promises or event handlers at all, except that we can name certain regions so that, when clicked, Brick handles a BrickEvent Ext (Vty.EvMouseDown col row _ _) where extents = map extentName $findClickedExtents (col,row) lets us interpet the observed column and row and get a list of clicked extents. We can report a named extent by wrapping it in reporteExtent ExtentName. Brick provides some primitive combinators for stacking widgets (rectangles) next to (<+>) or above (<=>) each other, as well as some primitive widgets for displaying text (str :: String -> Widget ()), wrapping widgets in styled borders, ( withBorderStyle unicodeRounded$ borderWithLabel (str "title") \$ myWidget), etc. As before, the code is fairly readable, so I’ll just cover some interesting mechanics here briefly before moving on.

## Custom Borderstyles

A typical card looks like this: a string 7♦ wrapped in a unicodeRounded border:

┌──┐
│7♦│
└──┘


but we want to be able to draw custom border too, in the case of our empty piles:

┌  ┐

└  ┘


Brick lets us define custom borderstyles like so:

Where those unicode 0x.... codes are just various box-drawing characters, and the bsVertical and bsHorizontal codes are (intentionally) spaces.

## Piles

Once we have a drawCard function, we can stack the cards by cropping their bottom or right borders as necessary, with more of the card cropped if it is meant to be face-down than if it is meant to be face-up. For example,

stacked face-up  |  stacked face-down
┌──┐             │┌──┐
│3♥│             │┌──┐
┌──┐             ││4♠│
│7♦│             │└──┘
└──┘             │


Otherwise, Render.hs is mostly composing existing Brick primitives in easy ways.

# Input

OK, here’s where the complexity of the game begins to shine through.

In computer solitaire, we typically expect to be able to click on a card and it will, it possible, move to the next open position. Thus, whenever a card gets clicked on, we should be able to figure out the next valid pile it could be moved to and move it there.

We can do so with lenses – and we can define our own lenses with independent getters and setters to make doing so easier.

For example, here’s a lens which writes to or reads from the stock. Its type is stockL :: Lens' Field [DCard], and it either reads and returns a list of DCards or accepts a list of DCards and writes them to the stock. The syntax is

Actually, let’s use this opportunity to flip the cards at read/write-time. We can use each to iterate over each of the returned or processed objects and apply some transformation with (.~).

Perfect. In Movement.hs you can see custom lenses for the stock, the waste, as well as two lens generators for the tableau and foundation which instead a) return Piles instead of [DCard]s, and b) accept an integer index for which tableau pile / foundation pile to return. They are of type tableLN :: Int -> Lens' Field Pile, where N is a convention for indexed generators.

## doMove

Eventually we want to be able to, upon reading a list of extents from a clicked region, continue with our game by calling

We’ll write hasWon :: GSt -> Bool later, but for now let’s write doMove :: GSt -> [Ext] -> GSt, which tries to move the clicked card and, if successful, returns a changed GSt with incremented moves ticker, mutated score, and augmented history.

Here we can see chained

operator chaining for the first time, which is pretty snazzy. Here, doMove expects a tryMove function which will return not just the new field, but a score mutator (+5, -10, id, etc.).

## Extents

Before we write tryMove, we should talk about Extents and what they look like in practice. They end up being lists of clicked extents where the innermost extents are first. We can wrap our stock, waste, etc. in StockX, WasteX, etc. extent labels, and we can report the DCard directly with a DCX wrapper. Later we’ll use the ActionX wrapper to report something of type Action.

Finally, we can use IdX Int as a wrapper for a row/col index.

For now let’s write tryMove by pattern-matching on the reported extents. Each region will have a different shape so we should be able to striate our regions fairly easily.

• [StockX]: reporting just an empty StockX region means there are no cards, so we should refresh it from the waste.

• [_, StockX]: reporting a non-empty stock means we want to take three cards from the stock and move them to the waste.

• [DCX dc, IdX 0, WasteX]: reporting from the top of the waste stack means we should try to move it. We don’t pattern-match on just any index in the waste since none of the others are actionable.

• [DCX dc, Idx row, FoundX]: reporting from any row in the foundation means we should try and move its topmost card.

• [DCX dc, Idx row, Id col, TableX]: reporting from the tableau means we expect a row and column index, which tells us where in the tableau structure to try and read from. Uniquely, we can read a card or a stack of cards at a time from the tableau and move them all as a unit, as long as the stack of cards doesn’t leave the tableau.

One more thing to do before we can write tryMove:

## Movement Lenses

Eventually, we want to be able to write (pseudocode below):

these newLocation and oldLocation lenses will have to be deduced from context.

Let’s get a list of our tableau and foundation lenses:

These are almost of type Lens' Field Pile, but not quite. We define them differently here because they aren’t setters or getters yet. If we defined them as Lens' types, they’d be polymorphic, and the process of evaluating them thru a filter cond ls mechanism would solidify them as setters, when ideally we want to be able to later turn around and use them as getters.

Each location can provide its own set of candidate lenses (usually either inFoundation++inTableau or inTableau, but it will depend) and evaluate them through findSpot, which takes a list of lenses and a card and a field and returns the index of the first matching lens, if possible.

canPlace is a manual bit of pattern-matching which lives in Utils.hs and runs through the types of piles and types of cards to provide a true/false.

In practice it is convenient to provide two helpers to findSpot:

We can use the first in canMove, and the second in mkMove. canMove tells us whether there is a spot for a card to go elsewhere in the field, and mkMoveL will, assuming there is a spot, return both a lens to that spot and the piletype of the spot the card can go to.

OK, let’s write tryMove. If you don’t like lenses, the above was the worst of it.

## tryMove

We expect tryMove to have the form:

### tryMove [StockX] Moving from the Stock (i)

Let’s write the tryMove [StockX] function first, since it is the simplest:

Even for Haskell, this is pretty esoteric. we read a load from the waste using the wasteL custom lens, reverse it, and prepend it (++) to the stock by using the in-place mutation operator (%~), while overwriting the waste with an empty list (.~). We return that new field f' and a scoreFn id, which keeps the score as-is.

### tryMove [_, StockX] Moving from the Stock (ii)

This is pretty similar to the last function, except that we are reading from the stock and writing to the waste instead. We drop 3 and take 3 at a time. Remember that the need to flip our cards over is handled innately in the stockL and wasteL lenses!

### tryMove [DCX dc, IdX 0, WasteX] Moving from the Waste

We’ve already solved the hardest subproblem, that if determining whether or not a card can move anywhere else in the field. So we can just use canMove rowIndex displaycard field to decide whether or not to try to evaluate f'. If we do, it lazily evaluates mkMoveL, which returns the moveL lens which we can use to write one card to the location in question. We also use the computed PileType to inform our scoring mutator.

### tryMove [DCX dc, IdX row, FoundX] Moving from the Foundation

This is starting to feel familiar; we know how to move one card at a time. The only difficulty comes when it’s time to move from the tableau; we could be moving one card or more at a time.

### tryMove [DCX dc, IdX row, Idx col, TableX] Moving from the Tableau

OK, this was definitely the most difficult one, But we’ve built ourselves a nice set of primitives, so that flipping the underlying card, or dropping n cards from the tableau row in question become pretty readable.

This is great! A well-formed tryMove mean we can write doMove, and the core of our game is finished.

# Final Touches

Now that we know how to use lenses, a lot of the remaining functions are pretty simple:

Now we can finally finish appEvent` and our app is done!

# Conclusions

I think a good UI framework is one where the bulk of the difficulty of writing the app is the internal logic of the underlying app itself, not fighting with the framework. Brick fit right into my functional understanding of frameworks like React or Angular, and although there was some learning curve for lenses, the final product is, like all Haskell, surprisingly short and readable.

If you’ve never played with Haskell before, I think Brick is an excellent place to start. I encourage you to download and play Solitiare if you’re interested in getting a feel for the ecosystem, or just reading through some of the code if you’re interested in how a comparatively large app feels at a low level.

• View the source code on Github.