I had a lot of fun building out this doc and figuring out all the pieces to put in place for it to work. Read on if you want to learn more under the scenes! (And as a bonus, check out the
The doc is supported by a few tables:
: the 52 cards in play in the game. Each card can be undealt in the deck, a community card on the table, or a pocket card in a player's hand
: the active players in a game, each having a balance and a hand comprised of hole and community cards
: the 13 ranks of cards, each having an ordinal value for comparison
: a list of valid blinds configurations for the game (defaults to $0.25/$0.50)
: an internal table that contains a single row with state such as the dealer, active player, etc. as well as many buttons that enable the functionality of the game
Most of the logic in the game is encoded in the
table. Using a table as opposed to lots of canvas formulas is great because it lets us scope game logic within the "@STATE" reference and more importantly, we can format columns allowing them to always return the correctly-typed values (canvas formulas can break down on empty lists and cause formula errors).
The most important button here is the "Action Performed" button, which actually pushes a number of other buttons depending on the state of the game. For instance, if the conditions for a new game have been met, this button will end up shuffling the deck, assigning the dealer/small blinds/big blinds role to the next players, betting blinds for them, and so on.
The cards in this game come from an excellent, free source of high-quality card graphics
. These were all uploaded to Coda into Images columns displayed at a large size.
The screen consists of a few important components:
The two pocket cards on the left are a view of all the cards filtered down to those of the current player. To get these to be lined up horizontally, they are grouped up top by the column. The same is done for the community cards, where they're grouped on a column representing their ranking in the dealing order
A set of controls applicable to the current player (also usable if "God Mode" is enabled). Raising is supported by setting the raise interval and adjusting the bet using the plus/minus buttons
A view of game players containing their role, balance, and the amount bet in the current round
Once all the 5 community cards are dealt, each player has 7 cards they can use to form their best hand, which in Texas Hold'em is always comprised of 5 cards. This hand is computed within a view on the
table into the
column, which is then sorted by rank.
In order to determine the best hand among all players, each hand is transformed into a value that can be lexicographically sorted by Coda with the standard Sort() function. To achieve this, every card has a "Sort Value" based on its ranking; for instance, a 2 has a Sort Value of 0, while a Queen is an "A", a King is a "B", and so on (this works because letters come after numbers in the ASCII table).
Building on top of sort values, each hand is then codified into a sortable string used to determine the best hand. This is done in the following format:
<hand type>-<values / kickers>
For instance, a Four of a Kind of Aces with a 2 of Spades kicker would be represented as
here. This would beat out a hand that's threes full of twos, which would be represented as
. On the other hand, the weakest possible hand in the game here in terms of ranking would be
(unsuited 2, 3,
4, 5, 7, 8, 9
, with the five highest cards making up the hand).
Determining the best hand is done in the "Cards Ranking" column, with a big "SwitchIf" formula that looks at the various conditions making up the strongest hands and works down from Royal/Straight Flushes to High Cards.
If there is no better hand, this simply determined by taking the 5 best cards by rank, sorting in descending order:
First, we define the "Ranks Unique" column, which simply contains a list of all the deduplicated ranks in the hand:
From there, the goal is to only find ranks that are in the hand at least two times. It's tempting to write a
formula that counts out ranks where the count is at least 2, but the formula language's limitations currently prevent writing nested expression-parameter formulas that need to reference both inner and outer
s. To work around this, we define a "Player Counts" column on the Ranks table, which creates a list with the number of times the card appears for each player:
Now, we can simply, use Nth() to filter down ranks where the card appears twice or more for the current player:
The "Cards Ranking" for this then contains three subsequent kicker cards.
Determining a two pair is very similar to the single pair caseーwe just check that there are at least two pairs.
Three of a Kind
Similar to determining pairs, we just check for ranks that are repeated three times:
Figuring these out is a bit tricky. The easiest solution I could think of was to see if we can find any of the rank sort values within the ordered sequence of values:
For instance, suppose there is a 6-high straight with an 8 and 9 (sort values
). This formula would capture the index of this straight via the third
, which would then get mapped back into the Sort Value "4". We can then resolve this value back into the row reference to the actual rank using the following lookup formula:
To determine flushes, we use a similar technique to finding pairs and trips, except using a "Player Counts" column on the Suits table:
Of course, there can only be one flush among seven cards and it's straightforward to determine the rank of the flush by looking at the last sorted card matching the flush suit
A full house is comprised of a set and a pair, so first off, we can conditionally determine that there is a full house if there are at least two pairs and at least one three-of-a-kind. That makes the set:
Four of a Kind
Figuring out quads is similar to how pairs and trips are detected:
The formula for matching a straight flush is exactly the same as that of the straight, except instead of looking at "Ranks Unique", we filter the input down to only cards matching the flush suit.
A royal flush is just a straight flush with a rank of A.
So, what would it take to make this into a robust, cheat-proof poker platform? For the most part, Coda would need to support the following:
A stronger set of data flow and permissions, implemented at the model layer.
This is tough to achieve since Coda docs are meant to be entirely loaded in memory so that they work well offline, so the current way to accomplish this is using
. Used here, this approach would result in 10 docs (a master doc, plus one for each of the seats), and it would probably be too slow (due to all the syncing) to work fast enough for such a game
Some form of automated testing.
Complex docs such as this one become pretty brittle: it's easy to change the behavior of one formula and inadvertently break the behavior of another one without knowing, especially when the current state of the doc affects which formula branches are executed. Having some kind of unit tests for modeling the behavior of the game would make the process of converging onto a perfect implementation much easier
Custom view types.
Currently, views of data are limited to the ones explicitly shipped with the product. These cover a lot of use cases (for instance, calendars, Gantt charts, etc.) but the doc would really shine if users could build their own views of data, for instance, one that looks like an actual poker table with "rows" rendered as seats/players around it
. The formulas written for detecting certain poker hands need to be able to count the number of duplicates of each rank or suit, for instance, to determine pairs, full houses, flushes, etc. This is currently done by projecting counts out onto the Ranks/Suits tables, since nested expression formulas can only refer to the innermost CurrentValue. This may contribute negatively to the performance of the doc
. Shuffling cards currently works by generating a random number for each row and then sorting by it, which ends up being pretty slow (5-10 seconds) since each additional card shuffled results in a recalculation of the entire doc. But, a single Random() call does not provide enough entropy to shuffle all the cards in one go, as there are 52! possible ways to shuffle the deck. Additional formulas could make this step faster