|
| 1 | +# Design of the hand classification mechanism in Pokerbotic |
| 2 | + |
| 3 | +## Detection of N-of-a-kind |
| 4 | + |
| 5 | +Detection of N-of-a-kind, two pairs and full house is described [here](https://github.com/thecppzoo/pokerbotic/blob/master/design/What-is-noak-or-how-to-determine-pairs.md). |
| 6 | + |
| 7 | +## Detection of flush |
| 8 | + |
| 9 | +Flush detection happens at [Poker.h:78](https://github.com/thecppzoo/pokerbotic/blob/master/inc/ep/Poker.h#L78) the hand is filtered per each suit, and the built in for population count on the filtered set is called. This code assumes a hand can only have one suit in flush (or that the hand has up to 9 cards). |
| 10 | + |
| 11 | +## Detection of straights |
| 12 | + |
| 13 | +The straightforward way to detect straights, if the ranks would be consecutive bits (which we will call "packed representation") is this: |
| 14 | + |
| 15 | +```c++ |
| 16 | +unsigned straights(unsigned cards) { |
| 17 | + auto shifted1 = cards << 1; |
| 18 | + auto shifted2 = cards << 2; |
| 19 | + auto shifted3 = cards << 3; |
| 20 | + auto shifted4 = cards << 4; |
| 21 | + return cards & shifted1 & shifted2 & shifted3 & shifted4 |
| 22 | +} |
| 23 | +``` |
| 24 | +
|
| 25 | +By shifting and doing a conjuction at the end, the only bits in the result set to one are those that are succeeded by four consecutive bits set to one. Before accounting for the aces to work as "ace or one", there are possible improvements to be discussed: |
| 26 | +
|
| 27 | +### Checking for the presence of 5 or 10 |
| 28 | +
|
| 29 | +In a deck of 13 ranks starting with 2, all straights must have either the rank 5 or the rank ten. This has a probability of nearly a third; however, testing for this explicitly is performance disadvantageous. It seems the branch is fundamentaly not predictable by the processor, so, the penalty of misprediction overcompensates the benefit of early exit. In the code above, there are 8 binary operators and 4 compile-time constants, there is little budget for branch misprediction. Older versions of the code had this check until it was benchmarked to be a disadvantage. |
| 30 | +
|
| 31 | +### Checking for partial conjunctions |
| 32 | +
|
| 33 | +For the same reason, testing if any of the conjunctions is zero to return 0 early is not performance advantageous, confirmed through benchmarking. |
| 34 | +
|
| 35 | +### Addition chain |
| 36 | +
|
| 37 | +There is one improvement that benchmarks confirm: |
| 38 | +
|
| 39 | +```c++ |
| 40 | +unsigned straights(unsigned cards) { |
| 41 | + // assume the point of view from the bit position for tens. |
| 42 | + auto shift1 = cards >> 1; |
| 43 | + // in shift1 the bit for the rank ten now contains the bit for jacks |
| 44 | + auto tj = cards & shift1; |
| 45 | + auto shift2 = tj >> 2; |
| 46 | + // in shift2, the position for the rank ten now contains the conjunction of originals queen and king |
| 47 | + auto tjqk = tj & shift2; |
| 48 | + return tjqk & (cards >> 4); |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +This implementation (which does not take into account the ace duality) requires 6 binary operations and 3 constants and accomplishes the same thing as the straightforward implementation. Benchmarks confirm this taking roughly 3/4 of the time than the straightforward implementation. |
| 53 | + |
| 54 | +The key insight here is to view the detection of the straight as adding up to 5 starting with 1. The straightforward implementation does the equivalent of `1 + 1 + 1 + 1 + 1`, this new implementation does `auto two = 1 + 1; return two + two + 1`. This technique is to build an *addition chain*. This technique was inspired by the second chapter of the book ["From Mathematics To Generic Programming"](https://www.amazon.com/Mathematics-Generic-Programming-Alexander-Stepanov/dp/0321942043) |
| 55 | + |
| 56 | +Taking into account the dual rank of aces is simply to turn on the 'ones' if there is the ace, but this requires left shift to make room for it. This can be done at the beginning of the straight check, and its cost can be amortized by the compiler doing a conditional move early, meaning the result will be ready by the time it is used. |
| 57 | + |
| 58 | +There is one further complication in the code, which is that the engine uses the rank-array representation. Provided that the shifts are for 4, 8, 12, 16 bits instead of 1, 2, 3, 4 there isn't yet a difference. There are two needs for straights: |
| 59 | + |
| 60 | +1. Normal straights, in which the suit of the rank does not matter. This is accomplished by making the 13 rank counts as described in how to detect pairs, etc., and using the SWAR operation `greaterEqual<1>` prior to the straight code. Naturally, the straights don't incurr in an extra cost of doing popcounts because they are amortized in the necessary part of detection of pairs, three of a kind, etc., the `greaterEqual<N>(arg)` operation requires two constants and two or three assembler operations, depending on how the result is used, thus, for practical purposes have negligible cost compared to a packed rank representation. |
| 61 | +2. Straights to detect straight flush: Since the bits for |
| 62 | + |
| 63 | +We suspect our detection of straight code is maximal in terms of performance. |
0 commit comments