← Home
petri-net weighted-arcs poker encoding technique

Exponential Weights in Petri Nets: What Worked, What Didn't, and What's Next

In February 2026 we added power-of-2 kicker scoring to the poker hand Petri net model (c892720, then a per-card weight in 1f18db7), and on February 15 we reverted it. The rank encoding is sound and useful. Embedding it in the poker net was the wrong application — and, as we found only on re-reading it for this revision, the per-card version we actually shipped didn't preserve the ordering it was meant to preserve.

The Encoding

Assign exponential weights so that any single higher-priority item outweighs all lower-priority items combined. It's an old trick — the same principle as bitmasks, Unix file permissions (read=4, write=2, execute=1), and binary feature flags.

For poker, we wanted a total ordering on all 52 cards where rank dominates and suit breaks ties. The formula in 1f18db7:

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, so every card maps to a unique integer: A-spade = 16387, A-heart = 16386, down to 2-club = 4.

The property we wanted is binary dominance, and it holds for the rank powers alone: 2^n > 2^(n−1) + … + 2^0, so a King (2048) outweighs Queen through Two combined (2047). Sum the rank powers of any set of distinct ranks and the totals compare the way poker compares high cards, lexicographically — no sorting, just a number. Distinct matters: two Queens sum to 2048, same as one King, so this only works once pairs and trips have been classified and you are comparing what's left.

Binary Dominance

Linear weights fail on exactly this. With A=13, K=12, Q=11 and so on down to 2=1, the hand {A, K, 5, 4, 3} sums to 34 and {A, Q, J, T, 9} to 51. Linear scoring says the second hand wins; poker says the King beats the Queen. Rank powers give 4096+2048+8+4+2 = 6158 against 4096+1024+512+256+128 = 6016, the right answer by construction.

The per-card weight we shipped does not keep that guarantee. Each card's suit adds up to 3, so five cards can add up to 15 on top of the rank part, while two hands that first differ at a low rank can be only 4 apart after the ×4. Take 8♣ 7♣ 6♣ 5♣ 2♦ against 8♠ 7♠ 6♠ 4♠ 3♥ — both high card, neither a flush nor a straight. Poker gives it to the first hand (5 beats 4). The rank powers agree: 121 against 118. The per-card weights disagree: 256+128+64+32+5 = 485 against 259+131+67+19+10 = 486. The suit values carry into the rank bits and the sum stops being lexicographic. That is our arithmetic on the formula above, not a re-run of the reverted code; the one kicker matchup visible in the revert, A-K-Q-J-9 against A-K-Q-J-8, differs by 256 at the 9-versus-8 card, far more than suits can carry, so it would not have exposed this.

The Score Is the State

When each item gets weight 2^n, the sum of any subset is unique — that's just binary representation. 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

Write 2313 in binary: 0100100001001. Each bit position maps to a rank, and there are our places again:

Binary Place Vector

The bits are the places and the 1s are the tokens: the binary representation of the score is a marking, of a net with one place per rank. 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 rather than a decoding trick on top of one.

Two qualifications. This is the 13-place rank view, not the 52-place card net we actually built, and with the per-card ×4 + suit weights the bits stop lining up, for the carry reason above. And it works for identity recovery — which items are present — only while each place holds at most one token. If a place could hold 2 or more, one bit can't represent it and the encoding needs more bits per place. For set membership, "which ranks are in this hand?", one bit per place is exactly right: 2313 doesn't just rank the hand, it is the hand, in a format a human can read bit by bit and a machine can compare in one integer operation.

What We Built

The model builder (buildPokerHandModel in petri-pilot) got a kicker_score place, initially empty, and 52 detection transitions (hc_A-heart, hc_K-spade, …), one per card. Each hc_* transition consumed its card's token — and, per the revert PR, a token from a card_counter guard place — and deposited the card's weight into kicker_score along a weighted output arc. A card in the hand enabled its transition, which fired once and was then disabled because its input token was gone; cards not in the hand never fired.

Mechanically, it worked: transitions fired, tokens accumulated, and the commits shipped with tests and a "High Card Kicker Battle" preset (the A-K-Q-J-9 against A-K-Q-J-8 matchup above), where comparing the two kicker_score values does pick the right winner. We have not re-run those tests for this revision, and the matchup we can see is not one where the suit term overturns the rank order.

Why We Removed It

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.

That is the core issue. The poker hand model detects pairs, straights and flushes through structure — a pair_<rank>_<suits> transition for each pair of same-rank cards, a flush_* transition per suited pattern, a straight_<start> per run — and each fires because the right card tokens are present. Those detections participate in the net's behavior; the net does the classification.

The kicker_score place just accumulated a number. Nothing in the net read it: no transition was enabled or disabled by it, no arc weight depended on it, and recovering which cards contributed meant decomposing the sum back into powers of 2 outside the net — bookkeeping bolted onto the side rather than modeling.

A Petri net model should be self-describing: the places, transitions and arcs encode the rules. If you need a decoder ring to read meaning out of a token count, the net has become a storage medium for an external computation. Fifty-two extra transitions, with their input and weighted output arcs, added complexity without adding behavior.

Where This Technique Belongs

The encoding is misapplied when the consumer is the net itself. It belongs where the consumer is external and the net is producing output for it — and, to be plain, none of the following has been built; they are where we would expect it to fit, not worked cases. In an event-sourced system on a Petri net, transitions emit events and projections interpret them, so a projection that ranks items by priority could read an exponentially weighted output place and sort on it directly: the net produces the value, the projection consumes it. Criteria with a strict priority order (safety > performance > cost) have the same shape — a decision workflow could accumulate stage scores into an output place that a dashboard or scheduler sorts on without replaying the evaluation or knowing the priority structure. And because the score in binary is the place vector, a power-of-2 sum is a compact way for a net to tell an external consumer exactly which subset of items was selected, not just how many, in one integer. Each of these inherits the precondition from the poker case: distinct items, at most one token per place, and no tiebreak term that can carry.

For ranking inside the net, the later attempt used structure rather than weights, with a caveat. ZK Hold'em builds a hand-analysis net whose drain counts are hand-picked in ranking order, and the ODE equilibrium (the incidence reduction) hands the hand-category order back, straight flush down to high card, as 1/drains. The ranking goes in with the drains rather than being discovered, but the drain transitions are the mechanism rather than bookkeeping: the net's own dynamics produce the order.

The boundary

Petri nets are good at modeling concurrent, discrete behavior through structure. Places are state, transitions are events, arcs are preconditions and effects; the topology is the logic. When you need new behavior, the right instinct is new structure. Clever arc weights don't make a net do anything new — they make it 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 kicker implementation was that there was no external system. The score accumulated into a place that nothing read, so we were encoding information the net couldn't use — and, it turns out, encoding it slightly wrong.

Open question

The drain-based ranking orders hand categories. It says nothing about kickers: two high-card hands, or two pairs of Kings, land on the same equilibrium value. Whether kicker comparison can be made structural — something that enables or inhibits a transition, rather than a number in a place — is not something we've built or shown. The revert PR called kicker comparison "inherently ordinal and contextual in a way that doesn't map to token accumulation," which may be right, but it's an assertion, not a result.

What Changed

For readers of the February version:

  1. Dominance scoped to distinct ranks. The post said a single King (8192 with the ×4) outscores all cards Queen and below combined, and that summing card weights preserves lexicographic comparison. With the suit term both are false — all lower cards together are 32818, one of each lower rank up to 8221 — and the pair of high-card hands above (485 vs 486) shows the per-card sum ordering them wrong. The guarantee holds for rank powers over distinct ranks; the post now says so.
  2. Linear example arithmetic. {A, K, 5, 4, 3} under A=13 … 2=1 is 34, not 37.
  3. What was built. Linked the commits and the model builder, added the card_counter guard the revert PR describes, dropped the unverified "104 extra arcs", and narrowed "tests passed" to what the tests covered.
  4. Links. The book chapter moved from Chapter 13 to Chapter 15 and the old URL returns 404. Added the later drain-based ranking (ZK Hold'em, incidence reduction). The application sections now say they are unbuilt.

View the poker hand model (without kicker scoring): pilot.pflow.xyz/poker-hand

This topic is covered in depth in Chapter 15: Exponential Weights and Scoring Systems of Petri Nets as a Universal Abstraction.

×

Follow on Mastodon