We recently added power-of-2 kicker scoring to the poker hand Petri net model—and then removed it. The encoding itself is sound and the technique is genuinely useful. But embedding it in the poker net was the wrong application. This post is about the technique, what we learned, and where it actually belongs.
The idea is to assign exponential weights so that any single higher-priority item outweighs all lower-priority items combined. This is a well-known trick in computer science—it's the same principle behind bitmasks, Unix file permissions (read=4, write=2, execute=1), and binary-encoded feature flags.
For poker, we wanted to rank all 52 cards with a total ordering where rank dominates and suit breaks ties. The formula:
weight = rank_power x 4 + suit_value
Each rank gets a power of 2:
| Rank | Rank Power |
|---|---|
| A | 4096 (2^12) |
| K | 2048 (2^11) |
| Q | 1024 (2^10) |
| J | 512 (2^9) |
| T | 256 (2^8) |
| 9 | 128 (2^7) |
| ... | ... |
| 2 | 1 (2^0) |
Suits get tiebreaker values (spade=3, heart=2, diamond=1, club=0). Multiplying rank power by 4 leaves room for the suit value without overlap. Every card maps to a unique integer: A-spade = 16387, A-heart = 16386, down to 2-club = 4.
The key property: binary dominance. A single King (2048 x 4 = 8192) outscores all cards Queen and below combined. This means you can sum the weights for any set of cards and the result preserves lexicographic comparison. No sorting, no iteration—just a number.
This is why naive linear weights fail. If you assign A=13, K=12, Q=11 and sum them, a hand with {A, K, 5, 4, 3} scores 37 while {A, Q, J, T, 9} scores 51. Linear scoring says the second hand wins. Poker says the King beats the Queen. Exponential weights fix this by construction.
There's a deeper property here. When we assign weight 2^n to each item, the sum of any subset is unique—that's just binary representation. But look at what it means.
Take a hand with {K, T, 5, 2}, using rank powers only:
K = 2¹¹ = 2048
T = 2⁸ = 256
5 = 2³ = 8
2 = 2⁰ = 1
─────────────
Sum = 2313
Now write 2313 in binary: 0100100001001. Each bit position maps to a rank. And there are our places again:
The bits are the places. The 1s are the tokens. The binary representation of the score is the Petri net marking.
This isn't a decoding trick—it's the same structure viewed two different ways. A Petri net marking is a vector of token counts across places. When every place holds 0 or 1 tokens, that vector is a bit string. And a bit string is a binary number. So the accumulated score, the bit string, and the marking are three notations for the same object.
The constraint matters: this works for identity recovery—determining which items are present. Each place holds at most one token. If a place could hold 2 or more tokens, a single bit can't represent it and the encoding would need more bits per place. But for set membership—"which cards are in this hand?"—one bit per place is exactly right. The number 2313 doesn't just rank the hand. It is the hand, written in a format where humans can read the bits and machines can compare with a single integer operation.
We encoded this in the poker hand Petri net using:
kicker_score place that accumulated the total weight (initial = 0)hc_A-heart, hc_K-spade, ...), one per cardkicker_scoreWhen a card was in the hand, its place had a token, enabling the corresponding hc_* transition. It fired once—consuming the card token—and deposited the card's universal weight into kicker_score. Cards not in the hand never fired. The consuming arcs made each transition self-limiting.
Mechanically, it worked. The transitions fired, tokens accumulated, and the resulting kicker_score correctly ranked hands. Tests passed. You could look at two hands' kicker scores and determine the winner.
From the revert PR:
The fundamental problem is that Petri nets track state as token counts in places, not as externally-interpreted numeric values. Accumulating weighted tokens into a single place works mechanically—the net fires and tokens move—but the resulting number only has meaning through external interpretation. The net itself has no way to compare two kicker scores or use the accumulated value to influence firing. It's just a number sitting in a place.
This is the core issue. The poker hand model detects pairs, straights, and flushes through structural properties of the net—token patterns across places that enable or inhibit transitions. Those detections participate in the net's behavior. A pair is detected because two card tokens enable a pair transition. A flush is detected because five suited tokens enable a flush transition. The net does the classification.
Kicker scoring doesn't work this way. The kicker_score place just accumulates a number. Nothing in the net reads that number. No transition is enabled or disabled by it. No arc weight depends on it. The model needs a separate interpreter to extract meaning from the token count—you have to decompose the sum back into powers of 2 to recover which cards contributed.
That's not modeling. That's bookkeeping bolted onto the side.
A Petri net model should be self-describing: the structure of places, transitions, and arcs encodes the rules. If you need a decoder ring to read meaning out of a token count, you're not modeling the domain in the net—you're using the net as a storage medium for an external computation. The 52 extra transitions and 104 extra arcs added complexity without adding behavioral insight.
The encoding is still valuable. It's just misapplied when the consumer is the net itself. The right setting is where the consumer is external and the Petri net is generating output for it.
In an event-sourced system built on a Petri net, transitions emit events and external projections interpret them. If a projection needs to rank items by priority, the net can deposit exponentially-weighted tokens into an output place as part of the transition's effect. The projection reads the accumulated value and uses it directly for sorting or comparison. The net produces the value; the projection consumes it. Each side does what it's good at.
When criteria have strict priority order (safety > performance > cost), exponential weights encode the hierarchy. A Petri net modeling a decision workflow could accumulate scores as items pass through evaluation stages. The final score in an output place encodes the full priority ranking as a single integer. An external dashboard or API reads the score without needing to replay the evaluation logic.
Consider a Petri net that models resource requests at different priority tiers. Using exponential weights on arcs that deposit tokens into a priority_score place, the net can produce a value that an external scheduler reads to determine allocation order. The scheduler doesn't need to know the priority structure—it just sorts by score.
As we saw above, the score in binary is the place vector. This makes power-of-2 sums useful whenever a Petri net needs to communicate which subset of items was selected to an external consumer—not just how many, but exactly which ones. One place, one integer, full recovery. The external system reads the bits to reconstruct the set without replaying any transitions.
The lesson is about the boundary between the net and the world.
Petri nets are good at modeling concurrent, discrete behavior through structure. Places represent state. Transitions represent events. Arcs define preconditions and effects. The topology is the logic. When you need to add behavior, the right instinct is to add structure—new places, new transitions, new arcs that encode the rules.
But encoding isn't behavior. Assigning clever weights to arcs doesn't make the net do anything new—it makes the net store something for someone else to read. That's fine, as long as you're clear about the boundary. The net generates the score; an external system interprets it.
The trouble with the poker kicker implementation was that there was no external system. The score accumulated into a place that nothing read. We were encoding information the net couldn't use.
Future applications should respect this boundary: use exponential weights when the net is producing output for external consumption, not when the net is trying to reason about the result internally. The encoding is a communication tool, not a computation tool.
View the poker hand model (without kicker scoring): pilot.pflow.xyz/poker-hand
This topic is covered in depth in Chapter 13: Exponential Weights of Petri Nets as a Universal Abstraction.