| View previous topic :: View next topic |
Author |
Message |
M_White
Joined: 05 Nov 2005 Posts: 40
|
Posted: Sat Dec 23, 2006 7:35 pm Post subject: |
|
|
| Gullanian wrote: | Sorry to dig up old threads!
I've written a 7 card hand evaluator and am trying to determine if anyone can beat it.
It can evaluate 14.5 million hands per second, on a 2.4ghz p4.
The thing about my one is that is takes any set of 7 integers (0-51) to represent a hand, and returns the strength of the hand as a comparable value to other hands. The integers passed in are UNORDERED, so 4,3,10,11,12,50,0 might be passed in and it would return the correct hand ranking. Is this what all of yours do as well? |
In mine, I pass a 64 bit integer where each card is represented by a bit.
Ordering kind of happens automatically.
On average it takes 66 CPU cycles to produce a 32 bit code that can be directly compared to other hands (strength) and also easily decoded to determine the type of hand and composition of the best 5 cards used.
On a 2.4 ghz CPU that should come out to 36 million hands per second.
I'm working on a completely table driven solution that should be MUCH faster, but it would also consume a lot more memory. |
|
| Back to top |
|
 |
M_White
Joined: 05 Nov 2005 Posts: 40
|
Posted: Wed Dec 27, 2006 7:49 pm Post subject: |
|
|
I've completed a state driven 7 card Eval table that can be used when speed is more important than memory consumption.
1 card states 52
2 card states 1326
3 card states 22100
4 card states 84448
5 card states 152607
6 card states 352443
The table consists of 31874856 integers.
Rank = Eval[Eval[Eval[Eval[Eval[Eval[Eval[52+c1]+c2]+c3]+c4]+c5]+c6]+c7]
Each state is a multiple of 52.
State = 0 represents some form of error, and all vectors from state 0 point back to state 0.
State_1 = Eval[52+Card_1]
EVal[State_n+Card_(n+1)] = State_(n+1)
Eval[State_6+Card_7] = Hand Rank for those 7 cards.
Total size of the Table is 121Mb |
|
| Back to top |
|
 |
terence Developer

Joined: 27 Sep 2004 Posts: 1153 Location: Edmonton
|
Posted: Thu Jan 04, 2007 5:12 pm Post subject: |
|
|
| M_White wrote: | I've completed a state driven 7 card Eval table that can be used when speed is more important than memory consumption.
1 card states 52
2 card states 1326
3 card states 22100
4 card states 84448
5 card states 152607
6 card states 352443
The table consists of 31874856 integers.
Rank = Eval[Eval[Eval[Eval[Eval[Eval[Eval[52+c1]+c2]+c3]+c4]+c5]+c6]+c7]
Each state is a multiple of 52.
State = 0 represents some form of error, and all vectors from state 0 point back to state 0.
State_1 = Eval[52+Card_1]
EVal[State_n+Card_(n+1)] = State_(n+1)
Eval[State_6+Card_7] = Hand Rank for those 7 cards.
Total size of the Table is 121Mb |
To let everyone know there is a discussion on this going on at twoplustwo:
http://forumserver.twoplustwo.com/showflat.php?Cat=0&Number=8513906&page=0&fpart=13&vc=1
M_White did you also post in that thread?
Terence. |
|
| Back to top |
|
 |
M_White
Joined: 05 Nov 2005 Posts: 40
|
Posted: Fri Jan 05, 2007 12:05 am Post subject: |
|
|
| terence wrote: | | M_White wrote: | I've completed a state driven 7 card Eval table that can be used when speed is more important than memory consumption.
1 card states 52
2 card states 1326
3 card states 22100
4 card states 84448
5 card states 152607
6 card states 352443
The table consists of 31874856 integers.
Rank = Eval[Eval[Eval[Eval[Eval[Eval[Eval[52+c1]+c2]+c3]+c4]+c5]+c6]+c7]
Each state is a multiple of 52.
State = 0 represents some form of error, and all vectors from state 0 point back to state 0.
State_1 = Eval[52+Card_1]
EVal[State_n+Card_(n+1)] = State_(n+1)
Eval[State_6+Card_7] = Hand Rank for those 7 cards.
Total size of the Table is 121Mb |
To let everyone know there is a discussion on this going on at twoplustwo:
http://forumserver.twoplustwo.com/showflat.php?Cat=0&Number=8513906&page=0&fpart=13&vc=1
M_White did you also post in that thread?
Terence. |
Yep, it's a small world. |
|
| Back to top |
|
 |
darse Lemur

Joined: 04 Oct 2004 Posts: 185 Location: Edmonton, Alberta
|
Posted: Tue Jan 09, 2007 7:28 pm Post subject: |
|
|
Here is a high-level explanation of the nested table approach, with an example. This is a description of the system I use, which I think is the same as the one in the 2+2 thread (there might be some minor differences, but they should be pretty close since it is "the natural thing to do", for some value of "natural").
Imagine six separate tables, corresponding to the number of cards we have currently processed. Each table holds an index value (or a pointer to a particular location) into the next table. It's like a network of pointers, expanding at each stage, but having a branching factor much less than 52 because each set of cards is sorted and reduced to only the essential information.
Let's suppose the first card is the 9h. It is represented as the 1-card hand "9h". The table entry for the 9h contains 52 indices (or pointers) corresponding to the 52 cards in the deck. The pointer for 9h[9h] might point to -1, indicating an error, since a legal hand can't have two 9h.
The second card is the Kc. The hand ID is the full hand sorted by rank and suit, so we currently have the hand "Kc9h". This entry in the second table is pointed to from two places in the first table, namely 9h[Kc] and Kc[9h]. There are 52 out-going pointers for this hand, two of which point to an error state.
The third card is the Kh. Kc9h[Kh] => KhKc9h. There are six different paths through the network leading to this state.
The fourth card is the Qc. KhKc9h[Qc] => KhKcQc9h. We retain the suit information because both hearts and clubs have a potential flush after 7 cards. If this card had been the Qd, then neither diamonds nor clubs could make a potential flush. They would be mapped to a "meta-suit" for no possible flush, which I will call "o" for "offsuit" or "other". Thus, the Qd would have produced KhKc9h[Qd] => KhKoQo9h.
The fifth card is the 2h. KhKcQc9h[2h] => KhKoQo9h2h. Clubs are now irrelevant with respect to possible flushes.
The sixth card is the 5h. KhKoQo9h2h[5h] => KhKoQo9h5h2h. Each entry of the sixth (and largest) table also has 52 out-going pointers, but instead of an index, they contain hand values.
The seventh card is the 7s. KhKoQo9h5h2h[7s] = value(KKQ97) which is 3844. If this had been the 7h, the table entry would contain the value of K9752f, which is 6367.
Note that a 6-card hand with five flush cards can ignore the offsuit card, as it cannot have a bearing on the final value. Thus, the large 6-card table can be reduced in size a little by using a "meta-rank" for "don't care", merging many cases into one. We cannot eliminate the smallest rank of a 6-card flush, however, because it might be part of a straight flush after the seventh card is known.
Note also that the sequential composition of hand IDs uses a lot less memory than more simplistic approaches. This can be further reduced if we dispense with the duplicate cards pointing to an error state, but the indexing gets trickier.
There are two main reasons this method is much faster for systematic enumeration tasks. First, we move most of the work out of the inner-most loops, and do it once instead of repeating it many times. Second, we ensure cache coherence to minimize the number of expensive accesses to main memory. For example, each fetch of an 8K block contains exactly the information that will be needed for the upcoming enumeration cases.
This technique might also be useful for hybrid Monte Carlo simulations over sets of cases, such as all possible river cards for a given 4-card board, or all possible 2-card hands in connection with a random board.
- Darse. |
|
| Back to top |
|
 |
Sparky2007
Joined: 11 Oct 2006 Posts: 9
|
Posted: Tue Jan 09, 2007 10:27 pm Post subject: |
|
|
Thanks alot for that explanation Darse. This technique makes alot of sense. I am in the process of converting this to java at the minute, I will let you all know how it performs during an enumeration of 7 card hands.
I assume from the 2+2 thread, that a mix between this (for enumeration) and the older evaluation functions would be a good idea. |
|
| Back to top |
|
 |
M_White
Joined: 05 Nov 2005 Posts: 40
|
Posted: Wed Jan 10, 2007 4:08 am Post subject: |
|
|
| darse wrote: | Here is a high-level explanation of the nested table approach, with an example. This is a description of the system I use, which I think is the same as the one in the 2+2 thread (there might be some minor differences, but they should be pretty close since it is "the natural thing to do", for some value of "natural").
- Darse. |
In your example you mention 8k blocks of memory.... is that just a number out of the blue or does the cache system access main memory in 8k chunks now?
Last time I ventured into the details of cache, I think it was only 16 bytes or so.. that was something like 20+ years ago. |
|
| Back to top |
|
 |
ChessMan
Joined: 23 Feb 2006 Posts: 76
|
Posted: Wed Jan 10, 2007 5:16 am Post subject: |
|
|
| Sparky2007 wrote: | Thanks alot for that explanation Darse. This technique makes alot of sense. I am in the process of converting this to java at the minute, I will let you all know how it performs during an enumeration of 7 card hands.
I assume from the 2+2 thread, that a mix between this (for enumeration) and the older evaluation functions would be a good idea. |
Please post your code when finished either here or at twoplustwo. I have Senzee's http://senzee.blogspot.com/2006/06/some-perfect-hash.html ported to Java if you want it. _________________ Windows XP Pro 32-bit
Prescott Pentium 4 (2.5.4 Build 176)
& AMD 3800+ X2 (2.5.8 Build 210)
1GB of RAM & 2GB of RAM |
|
| Back to top |
|
 |
alan

Joined: 27 Oct 2004 Posts: 972
|
Posted: Wed Jan 10, 2007 7:31 am Post subject: |
|
|
An evaluator generates a single integer representing hand strength, right?
How are flushes handled? It seems that you would need 1277 equivalence classes for flushes. Is this correct? |
|
| Back to top |
|
 |
Sparky2007
Joined: 11 Oct 2006 Posts: 9
|
Posted: Wed Jan 10, 2007 4:46 pm Post subject: |
|
|
I have converted the code to Java but when running it, I am getting an out of bounds exception at one point.
I am not an expert on the differences between c++ and java but I think my conversion of the following line may be incorrect.
| Code: | if (suit = holdcards[cardnum] & 0xf) { // find out what suit (if any) was significant
mainsuit = suit; // and remember it
} |
Is suit being assigned to holdcards[cardnum] & 0xf, or being compared to it? Also the problem may lie with the SWAP function. I have created a swap function as follows
| Code: |
public static void SWAP(int I, int J) {
if (workcards[I] < workcards[J]) {
workcards[I]^=workcards[J];
workcards[J]^=workcards[I];
workcards[I]^=workcards[J];
}
}
|
but I am pretty sure this can be converted to
| Code: |
Arrays.sort(workcards);
|
in java as this should sort the array fine.... I'm lost as to where else c++ differs as my code is compiling fine. |
|
| Back to top |
|
 |
darse Lemur

Joined: 04 Oct 2004 Posts: 185 Location: Edmonton, Alberta
|
Posted: Thu Jan 11, 2007 12:27 am Post subject: |
|
|
| alan wrote: | An evaluator generates a single integer representing hand strength, right?
How are flushes handled? It seems that you would need 1277 equivalence classes for flushes. Is this correct? |
Right. There are only 7462 distinct hand values, of which 1277 are flushes (ranks 5863-7140). There are only four hands in each flush class, compared to 1020 copies of each straight or high-card hand.
"Cactus Kev" explains this quite nicely on his 5-card eval webpage:
http://www.suffecool.net/poker/evaluator.html |
|
| Back to top |
|
 |
Sparky2007
Joined: 11 Oct 2006 Posts: 9
|
Posted: Thu Jan 11, 2007 1:54 am Post subject: |
|
|
I have converted the code to Java as best I understand the difference between the two languages but I am still unable to get through the entire process of assigning the 7 card hand values at the end of the process.
Hopefully some of you more familiar could give it a shot? |
|
| Back to top |
|
 |
alan

Joined: 27 Oct 2004 Posts: 972
|
Posted: Thu Jan 11, 2007 4:08 am Post subject: |
|
|
| Quote: |
Right. There are only 7462 distinct hand values, of which 1277 are flushes (ranks 5863-7140). There are only four hands in each flush class, compared to 1020 copies of each straight or high-card hand.
|
It seems to me that the goal of an evaluator is to compare strengths between two or more hands. In many cases, getting a range of values should be sufficient. Once you know that a hand makes a flush, you do not need to eliminate the possiblity of those other 1276 classes unless you are trying to compare the hand to another flush. Have you tried any optimizations along these lines?
Different games have different distributions of hand ranks. It might also be possible to optimize the ranking elimination to the game by looking at which hands are most likely.
I worked out the equivalence classes last night. This is what I got:
Straight Flush 10
Four of a Kind 13
Full House 13*12=156
Flush C(13, 5)-10=1277
Straight 10
Three of a Kind 13
Two Pair 11*(13*12)/2=858
Pair 13*C(11, 3)=2145
High C(13,5)-10=1277
Flushes, Pairs, and High hands seem like the most expensive of the bunch.
(BTW Darse, have you defended yet? I haven't been keeping up.) |
|
| Back to top |
|
 |
ChessMan
Joined: 23 Feb 2006 Posts: 76
|
Posted: Thu Jan 11, 2007 6:24 am Post subject: |
|
|
| Sparky2007 wrote: |
| Code: | if (suit = holdcards[cardnum] & 0xf) { // find out what suit (if any) was significant
mainsuit = suit; // and remember it
} |
|
The above is equivalent to
| Code: |
suit = holdcards[cardnum] & 0xf;
if(suit){
mainsuit = suit;
}
|
This code won't work in Java because Java has a boolean primitive type and conditional statements in Java must be of boolean type. "suit & 0xf" is actually a bitwise AND and so the result is some number.
Next is a Java conversion of the original code:
| Code: |
if ((suit = holdcards[cardnum] & 0xf) != 0) { // find out what suit (if any) was significant
mainsuit = suit; // and remember it
|
_________________ Windows XP Pro 32-bit
Prescott Pentium 4 (2.5.4 Build 176)
& AMD 3800+ X2 (2.5.8 Build 210)
1GB of RAM & 2GB of RAM |
|
| Back to top |
|
 |
thomas
Joined: 11 Jan 2007 Posts: 24
|
Posted: Thu Jan 11, 2007 10:43 am Post subject: |
|
|
I am new to this forum and also fairly new to poker AI, so my question may be a bit uninformed.
Could we not just precalculate all hand strengths?
((52 choose 7) + (52 choose 6) + (52 choose 5) + (52 choose 4) + (52 choose 3) + (52 choose 2)) / 4 = 39 259 047.8
I divide by four because I figure the different combinations of suits should yield the same strength.
If I calculate the result as a probability that the hand will win the game and then bin it into 256 discrete values, i.e. a byte I should be able to store the complete set in a bit lesds than 40 mb of memory.
Is there a flaw in my reasoning?
Thomas |
|
| Back to top |
|
 |
Sparky2007
Joined: 11 Oct 2006 Posts: 9
|
Posted: Thu Jan 11, 2007 4:41 pm Post subject: |
|
|
Thanks ChessMan, yes I have made that change, the java code creates the Id's now with no errors, but fails at the stage where the actual rankings are assigned at the end.
There must be a difference in the way the java code creates the ID's as they are not being created equally.
I found that a __int64 in C is a 64bit unsigned integer. In java there are no such things as unsigned integers. A long is a Java 64bit signed integer, so I think this is where the ID's are not being created correctly. |
|
| Back to top |
|
 |
Steve Brecher
Joined: 12 Jan 2007 Posts: 6
|
Posted: Fri Jan 12, 2007 1:03 am Post subject: |
|
|
| thomas wrote: | Could we not just precalculate all hand strengths?
((52 choose 7) + (52 choose 6) + (52 choose 5) + (52 choose 4) + (52 choose 3) + (52 choose 2)) / 4 = 39 259 047.8 |
There are two ways to obtain a hand evalution (plus a mixture of the two): calculation, i.e., use only the data in the representation of the hand, and table lookup. The problem to be solved by an implementer of a table lookup evaluator is how to quickly get from a representaton of a hand to the corresponding hand value in the table(s). One such solution has been mentioned above by M_White and outlined by that author (as mykey1961, same person) on the twoplustwo forum thread cited, and also a similar solution has been outlined above by darse.
It turns out (see twoplustwo thread) that the table lookup solution is blazingly fast if and only if it is not competing for the CPU data cache -- or more generally, for RAM access -- with code that is using/calling the evaluator; otherwise, calculation routines with perhaps some small tables are faster. | Quote: | | I divide by four because I figure the different combinations of suits should yield the same strength. | Yes, but before you throw away the suits you have to determine whether or not the hand is a flush or straight flush. That is a significant portion of the work of a calculation evaluator. | Quote: | | If I calculate the result as a probability that the hand will win the game and then bin it into 256 discrete values, i.e. a byte I should be able to store the complete set in a bit lesds than 40 mb of memory. | I'm not sure what the probability is to which you refer. Evaluators are used in comparing the strengths of hands to determine wins/ties/losses among a specified set of two or more hands. So I don't see how a probability could be associated with a hand out of the context of the other hands with which it is competing.
Last edited by Steve Brecher on Fri Jan 12, 2007 1:37 am; edited 1 time in total |
|
| Back to top |
|
 |
Steve Brecher
Joined: 12 Jan 2007 Posts: 6
|
Posted: Fri Jan 12, 2007 1:31 am Post subject: |
|
|
| Sparky2007 wrote: | | I found that a __int64 in C is a 64bit unsigned integer. In java there are no such things as unsigned integers. A long is a Java 64bit signed integer, so I think this is where the ID's are not being created correctly. | Note that __int64 is a Microsoft-specific extension. Other implementations may also provide 64-bit integers with other type designators, e.g., "long long". Also, __int64 is signed; "unsigned __int64" is the unsigned type.
In general -- i.e., without looking at the code -- the difference between signed and unsigned should not be significant if the most significant bit, the sign bit, is clear. Java does provide an unsigned right-shift operator, ">>>", which will not replicate the sign bit if it is set in the left operand.
[Edited to add note about __int64 vs. unsigned __int64.] |
|
| Back to top |
|
 |
Sparky2007
Joined: 11 Oct 2006 Posts: 9
|
Posted: Fri Jan 12, 2007 2:15 am Post subject: |
|
|
Hi Steve, yes I have noticed that using the >>> removes java's signed representation but unfortunately there must be some difference that I am not noticing. I would greatly appreciate if for testing could someone who has gotten the C++ version working, to maybe print out the hand ID's as they are created.
his would allow me to see where my code is working differently (hopefully) and if not, narrow the problem down to some other section.
Thanks alot everyone |
|
| Back to top |
|
 |
thomas
Joined: 11 Jan 2007 Posts: 24
|
Posted: Fri Jan 12, 2007 10:48 am Post subject: |
|
|
| Steve Brecher wrote: | | thomas wrote: | Could we not just precalculate all hand strengths?
((52 choose 7) + (52 choose 6) + (52 choose 5) + (52 choose 4) + (52 choose 3) + (52 choose 2)) / 4 = 39 259 047.8 |
There are two ways to obtain a hand evalution (plus a mixture of the two): calculation, i.e., use only the data in the representation of the hand, and table lookup. The problem to be solved by an implementer of a table lookup evaluator is how to quickly get from a representaton of a hand to the corresponding hand value in the table(s). One such solution has been mentioned above by M_White and outlined by that author (as mykey1961, same person) on the twoplustwo forum thread cited, and also a similar solution has been outlined above by darse.
It turns out (see twoplustwo thread) that the table lookup solution is blazingly fast if and only if it is not competing for the CPU data cache -- or more generally, for RAM access -- with code that is using/calling the evaluator; otherwise, calculation routines with perhaps some small tables are faster. | Quote: | | I divide by four because I figure the different combinations of suits should yield the same strength. | Yes, but before you throw away the suits you have to determine whether or not the hand is a flush or straight flush. That is a significant portion of the work of a calculation evaluator. | Quote: | | If I calculate the result as a probability that the hand will win the game and then bin it into 256 discrete values, i.e. a byte I should be able to store the complete set in a bit lesds than 40 mb of memory. | I'm not sure what the probability is to which you refer. Evaluators are used in comparing the strengths of hands to determine wins/ties/losses among a specified set of two or more hands. So I don't see how a probability could be associated with a hand out of the context of the other hands with which it is competing. |
dividing by four does not get rid of the suit information, it just gets rid of the different possibilites of assigning suits. I.e. Kh9h becomes Kc9c and Ks9s becomes Kc9c, so we are reducing the number of possible hands without losing information.
the main problem is to generate an index from a card hand. I think one should be able to do better than the previously mentioned approaches. One could view a hand as an array of booleans with length 52. We know exactly how many of these booleans are going to be true, i.e. the number of cards in the hand. So how can we minimally represent an array of length n with exactly k being true. The solution is ceil(log2(n choose k)) bits. This is a lot less than other encodings. (here n is 52 and k is the no. of cards for which we are making the table).
The problem is going from such an array representation of the hand to an actual integer index into the lookup array.
this is not making any simplifications with similar suits yet.
Thomas |
|
| Back to top |
|
 |
|