1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
use constants::*;
use rustc_hash::{FxBuildHasher, FxHashMap};
use crate::{
constants::{INT_RANKS, PRIMES},
evaluate::{
eval::Eval,
poker_type::FiveCard,
utils::{self, BitSequence},
},
};
/// Stores information about looking up poker hands.
///
/// There are two hash tables, one for hands where the cards are suited (flushes
/// and straight flushes) and one for hands where the cards are not suited (the
/// rest of the poker hands). Both tables are indexed by a hand's prime product.
///
/// For example, the worst possible hand is 23457 (unsuited). The prime product
/// of these ranks is 2 * 3 * 5 * 7 * 13 = 2730. The evaluation implementation
/// first checks to make sure the hand is not suited, then indexes into the
/// unsuited lookup to find that `unsuited_lookup\[2730\]` is equal
/// to `Meta::HighCard { hand_rank: HandRank(7462), high_rank: Rank::Seven }`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LookupTable {
pub flush_lookup: FxHashMap<i32, Eval<FiveCard>>,
pub unsuited_lookup: FxHashMap<i32, Eval<FiveCard>>,
}
impl LookupTable {
pub fn new() -> Self {
let mut table = Self {
flush_lookup: FxHashMap::with_capacity_and_hasher(6175, FxBuildHasher),
unsuited_lookup: FxHashMap::with_capacity_and_hasher(1287, FxBuildHasher),
};
table.flushes_straights_high_cards();
table.multiples();
table
}
/// Calculate the metadata for flushes, straights, high cards, and straight
/// flushes.
fn flushes_straights_high_cards(&mut self) {
const NOT_STRAIGHTS: [i16; 1277] = constants::not_straights();
// Now we have `STRAIGHTS` (our constant) and `not_straights` (const-
// calculated). Using these, we can consider both sets as if the ranks
// they encode are suited
// - `STRAIGHT` hands suited become straight flushes
// - `not_straight` hands become flushes
// We can also consider them unsuited:
// - `STRAIGHT` hands are just straights
// - `not_straight` hands are high-card hands (pairs, etc. not possible)
// Let's first work with the `STRAIGHTS`.
// If suited, we start with the best hand, a royal flush. This corresponds to a
// value of 1
let mut rank_suited = 1;
// If unsuited, we start with the best possible straight, which is one worse
// (1+) the worst (max) flush
let mut rank_unsuited = WORST_FLUSH + 1;
// These are recycled and hold the rank of the highest card of the hand and the
// prime product
let mut high_rank: i16;
let mut prime_product: i32;
// Straight flushes and straights
for straight in STRAIGHTS {
// We get the prime product using the bits
prime_product = utils::prime_product_from_rank_bits(straight);
// We also obtain the highest rank from the bits
high_rank = utils::high_rank_from_rank_bits_five(straight);
// Into the flush table we map the prime product to a straight flush
// the has our current `rank_suited` value and the highest card's rank
self.flush_lookup
.insert(prime_product, Eval::new(rank_suited, high_rank));
// Into the unsuited table, we map the same prime product to a straight
// with our current `rank_unsuited` value and the highest card's rank
self.unsuited_lookup
.insert(prime_product, Eval::new(rank_unsuited, high_rank));
// We increment our values as in the next loop we consider the next-worse hand.
rank_suited = rank_suited.wrapping_add(1);
rank_unsuited = rank_unsuited.wrapping_add(1);
}
// Now, we work with our `not_straights`.
// If suited, we have a flush, which is starts just worse than the worst full
// house
rank_suited = WORST_FULL_HOUSE.wrapping_add(1);
// If unsuited, we start just worse than the worst pair
rank_unsuited = WORST_PAIR.wrapping_add(1);
// Flushes and high cards
// We reverse `not_straights` before looping because we generated the worst
// hands first, but we want to start mapping from the best hands
for bits in NOT_STRAIGHTS.into_iter().rev() {
// Get the prime product from the bits
prime_product = utils::prime_product_from_rank_bits(bits);
// Get the highest card's rank
high_rank = utils::high_rank_from_rank_bits_five(bits);
// In the flush table, map the prime product to a flush
self.flush_lookup
.insert(prime_product, Eval::new(rank_suited, high_rank));
// In the unsuited table, map it to a high card hand
self.unsuited_lookup
.insert(prime_product, Eval::new(rank_unsuited, high_rank));
// Increment our values to consider the next worst hand
rank_suited = rank_suited.wrapping_add(1);
rank_unsuited = rank_unsuited.wrapping_add(1);
}
}
/// Calculate metadata for all hands where at least one rank is repeated.
fn multiples(&mut self) {
// We want backwards ranks so we can consider the best/highest card ranks first
// Reusable product holder
let mut product;
// Four of a kind
// Given a four of a kind hand, we know one rank is repeated four times, and one
// extra card, the kicker, is left, which can be one of the other eleven
// card ranks. We start our rank at just worse than the worst straight
// flush
let mut rank = WORST_STRAIGHT_FLUSH + 1;
// First, select our rank that there will be 4x
for quad in INT_RANKS.rev() {
// Then filter out our 4x rank so we can consider each possible kicker
let kickers = INT_RANKS.rev().filter(|&kicker| kicker != quad);
for k in kickers {
// Get the prime product by hand, using `pow` if/when the card is present more
// than once
product = PRIMES[quad as usize].wrapping_pow(4) // 4x the quad card
.wrapping_mul(PRIMES[k as usize]); // 1x the kicker
// Map the product to the appropriate hand
self.unsuited_lookup
.insert(product, Eval::new(rank, 1i16.wrapping_shl(quad as u32)));
rank = rank.wrapping_add(1);
}
}
// Full house
// We have one three of a kind (trips) and one (pair)
rank = WORST_FOUR_OF_A_KIND + 1;
// We select our trips rank...
for trips in INT_RANKS.rev() {
// ...and select our pair rank
let pair_ranks = INT_RANKS.rev().filter(|&pr| pr != trips);
for pr in pair_ranks {
// And we calculate the prime product using power of 3 for the 3x rank and power
// of 2 for the 2x rank
product = PRIMES[trips as usize].wrapping_pow(3) // 3x trips
.wrapping_mul(PRIMES[pr as usize].wrapping_pow(2)); // 2x pair
let mut rank_flags = 1i16.wrapping_shl(trips as u32) | 1i16.wrapping_shl(pr as u32);
if pr > trips {
rank_flags |= 0b1000_0000_0000_0000u16 as i16;
}
self.unsuited_lookup
.insert(product, Eval::new(rank, rank_flags));
rank = rank.wrapping_add(1);
}
}
// Three of a kind
// One 3x rank and two unique kickers
rank = WORST_STRAIGHT + 1;
for trips in INT_RANKS.rev() {
let kickers = INT_RANKS
.rev()
.filter(|&kicker| kicker != trips)
.collect::<Vec<_>>();
// We want every combination of two kickers
let generator = utils::const_combos::<_, 2>(&kickers);
for [c1, c2] in generator {
// Calculate our prime product with power of 3 for the trips and simply
// multiply in the two kickers' primes
product = PRIMES[trips as usize].wrapping_pow(3) // 3x trips
.wrapping_mul(PRIMES[c1 as usize]) // 1x first kicker
.wrapping_mul(PRIMES[c2 as usize]); // 1x second kicker
self.unsuited_lookup
.insert(product, Eval::new(rank, 1i16.wrapping_shl(trips as u32)));
rank = rank.wrapping_add(1);
}
}
// Two pair
// Two unique 2x cards and one unique kicker
rank = WORST_THREE_OF_A_KIND + 1;
// We want want every combination of two card ranks together to consider as our
// two pair ranks
let bw = INT_RANKS.rev().collect::<Vec<_>>();
let two_pairs_combos = utils::const_combos::<_, 2>(&bw);
for [pair1, pair2] in two_pairs_combos {
// Our kickers are any rank that isn't equal to one of our two pair ranks
let kickers = INT_RANKS
.rev()
.filter(|&kicker| kicker != pair1 && kicker != pair2);
for kicker in kickers {
// Product is power of two for our two pair ranks, multiplied by kicker
product = PRIMES[pair1 as usize].wrapping_pow(2) // 2x first pair
.wrapping_mul(PRIMES[pair2 as usize].wrapping_pow(2)) // 2x second pair
.wrapping_mul(PRIMES[kicker as usize]); // 1x kicker
self.unsuited_lookup.insert(
product,
Eval::new(
rank,
1i16.wrapping_shl(pair1 as u32) | 1i16.wrapping_shl(pair2 as u32),
),
);
rank = rank.wrapping_add(1);
}
}
// Pair
// 1 pair rank and three unique kickers
rank = WORST_TWO_PAIR + 1;
for pair_rank in INT_RANKS.rev() {
let kickers = INT_RANKS
.rev()
.filter(|&kicker| kicker != pair_rank)
.collect::<Vec<_>>();
// We want every combination of three unique ranks that aren't equal to our pair
// rank
let kicker_combos = utils::const_combos::<_, 3>(&kickers);
for kicker_combo in kicker_combos {
let k1 = kicker_combo[0] as usize;
let k2 = kicker_combo[1] as usize;
let k3 = kicker_combo[2] as usize;
// Our product is the pair rank's prime to the power of two times the kickers'
// primes
product = PRIMES[pair_rank as usize].wrapping_pow(2) // 2x pair
.wrapping_mul(PRIMES[k1]) // 1x first kicker
.wrapping_mul(PRIMES[k2]) // 1x second kicker
.wrapping_mul(PRIMES[k3]); // 1x third kicker
self.unsuited_lookup.insert(
product,
Eval::new(rank, 1i16.wrapping_shl(pair_rank as u32)),
);
rank = rank.wrapping_add(1);
}
}
// And we're done! Phew!
}
}
impl Default for LookupTable {
fn default() -> Self { Self::new() }
}
pub mod constants {
use super::*;
// These are the worst hand ranks for each of the poker hands
pub const WORST_STRAIGHT_FLUSH: i16 = 10;
pub const WORST_FOUR_OF_A_KIND: i16 = 166;
pub const WORST_FULL_HOUSE: i16 = 322;
pub const WORST_FLUSH: i16 = 1599;
pub const WORST_STRAIGHT: i16 = 1609;
pub const WORST_THREE_OF_A_KIND: i16 = 2467;
pub const WORST_TWO_PAIR: i16 = 3325;
pub const WORST_PAIR: i16 = 6185;
pub const WORST_HIGH_CARD: i16 = 7462;
/// Statically calculated bit straights
pub const STRAIGHTS: [i16; 10] = [
0b1_1111_0000_0000, // 7936 => TJQKA
0b0_1111_1000_0000, // 3968 => 9TJQK
0b0_0111_1100_0000, // 1984 => 89TJQ
0b0_0011_1110_0000, // 992 => 789TJ
0b0_0001_1111_0000, // 496 => 6789T
0b0_0000_1111_1000, // 248 => 56789
0b0_0000_0111_1100, // 124 => 45678
0b0_0000_0011_1110, // 62 => 34567
0b0_0000_0001_1111, // 31 => 23456
0b1_0000_0000_1111, // 4111 => A2345
];
// Bit sequences that are not in the list above
pub const fn not_straights() -> [i16; 1277] {
let mut arr = [0; 1277];
let mut generator = BitSequence::new(0b11111);
let mut i = 0;
let mut cur = 0;
while i < 1286 {
let bits = generator.get_next();
let mut not_straight = true;
let mut j = 0;
while j < STRAIGHTS.len() {
let straight = STRAIGHTS[j];
// If the bits XOR a straight is 0, then it **is** a s traight, so we don't add
// it to our vector
if bits ^ straight == 0 {
not_straight = false;
break;
}
j += 1;
}
if not_straight {
arr[cur] = bits;
cur += 1;
}
i += 1;
}
arr
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use super::*;
#[test]
fn five_card_table_contains_all_entries() {
let table = LookupTable::new();
let mut hand_ranks = HashSet::new();
let LookupTable {
flush_lookup,
unsuited_lookup,
} = table;
flush_lookup.values().for_each(|&eval| {
assert!(hand_ranks.insert(eval.hand_rank()));
});
unsuited_lookup.values().for_each(|&eval| {
assert!(hand_ranks.insert(eval.hand_rank()));
});
assert_eq!(hand_ranks.len(), WORST_HIGH_CARD as usize);
let mut v = hand_ranks.into_iter().collect::<Vec<_>>();
v.sort();
let w = (1..=WORST_HIGH_CARD).collect::<Vec<_>>();
assert_eq!(v, w);
}
}