diff --git a/ace_eval_py.h b/ace_eval_py.h new file mode 100644 index 0000000..48f667f --- /dev/null +++ b/ace_eval_py.h @@ -0,0 +1,280 @@ +#include +#define Card uint32_t + + +#define ACEHAND 5 +static inline Card ACE_makecard(int i){return 1<<(2*(i%13)+6)|1<<(i/13);} +#define ACE_addcard(h,c) h[c&7]+=c,h[3]|=c +#define ACE_evaluate(h) E((h)) +#define ACE_rank(r) ((r)>>28) + +/* Mini poker hand evaluator. + * Takes a hand of 5-7 cards, + * returns a 32 bit int representing its rank. + * + * Cards are stored in a 32 bit word which has the following (implied) structure: + struct card{ + unsigned num_A:2; + unsigned num_K:2; + unsigned num_Q:2; + //.... + unsigned num_2:2; + unsigned spare:2; + unsigned spade:1; + unsigned heart:1; + unsigned diamond:1; + unsigned club:1; + }; +*/ + +/* + compressor: turn 26 bit-pairs into 13 bits +*/ +#define DECOMPRESS2 +Card C,i,X; +//#define c(a)for(X=a,i=C=0;X;X/=4}C|=(X&1)<>6)|a>>21; + //?out=V[a&255]+V[(a>>8)&255]; + Card out=0; + a>>=6; //dump low bits + out|=a&0x1; + out|=(a&0x4)>>1; + out|=(a&0x10)>>2; + out|=(a&0x40)>>3; + out|=(a&0x100)>>4; + out|=(a&0x400)>>5; + out|=(a&0x1000)>>6; + out|=(a&0x4000)>>7; + out|=(a&0x10000)>>8; + out|=(a&0x40000)>>9; + out|=(a&0x100000)>>10; + out|=(a&0x400000)>>11; + out|=(a&0x1000000)>>12; + out|=(a&0x4000000)>>13; + return out; +} +#else +#ifdef DECOMPRESS2 +Card compress(Card a){ + a=(a|(a>>1))&0x33333333; + a=(a|(a>>2))&0x0f0f0f0f; + a=(a|(a>>4))&0x00ff00ff; + a=(a|(a>>8))&0x0000ffff; + return a>>3; +} +#else + +/* uint16_t bits[] = {0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15}; */ +/* Card compress(Card x){ */ +/* uint16_t a = x>>6&1; */ +/* uint16_t b = bits[x>>8&0x55]<<1; */ +/* uint16_t c = bits[x>>16&0x55]<<5; */ +/* uint16_t d = bits[x>>24&0x55]<<9; */ +/* return a|b|c|d; */ +/* } */ + +uint16_t bits[] = {0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15, 14, 15, 12, 13, 12, 13, 14, 15, 14, 15, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15, 14, 15, 12, 13, 12, 13, 14, 15, 14, 15, 0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15, 14, 15, 12, 13, 12, 13, 14, 15, 14, 15, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15, 14, 15, 12, 13, 12, 13, 14, 15, 14, 15, 16, 17, 16, 17, 18, 19, 18, 19, 16, 17, 16, 17, 18, 19, 18, 19, 20, 21, 20, 21, 22, 23, 22, 23, 20, 21, 20, 21, 22, 23, 22, 23, 16, 17, 16, 17, 18, 19, 18, 19, 16, 17, 16, 17, 18, 19, 18, 19, 20, 21, 20, 21, 22, 23, 22, 23, 20, 21, 20, 21, 22, 23, 22, 23, 24, 25, 24, 25, 26, 27, 26, 27, 24, 25, 24, 25, 26, 27, 26, 27, 28, 29, 28, 29, 30, 31, 30, 31, 28, 29, 28, 29, 30, 31, 30, 31, 24, 25, 24, 25, 26, 27, 26, 27, 24, 25, 24, 25, 26, 27, 26, 27, 28, 29, 28, 29, 30, 31, 30, 31, 28, 29, 28, 29, 30, 31, 30, 31, 16, 17, 16, 17, 18, 19, 18, 19, 16, 17, 16, 17, 18, 19, 18, 19, 20, 21, 20, 21, 22, 23, 22, 23, 20, 21, 20, 21, 22, 23, 22, 23, 16, 17, 16, 17, 18, 19, 18, 19, 16, 17, 16, 17, 18, 19, 18, 19, 20, 21, 20, 21, 22, 23, 22, 23, 20, 21, 20, 21, 22, 23, 22, 23, 24, 25, 24, 25, 26, 27, 26, 27, 24, 25, 24, 25, 26, 27, 26, 27, 28, 29, 28, 29, 30, 31, 30, 31, 28, 29, 28, 29, 30, 31, 30, 31, 24, 25, 24, 25, 26, 27, 26, 27, 24, 25, 24, 25, 26, 27, 26, 27, 28, 29, 28, 29, 30, 31, 30, 31, 28, 29, 28, 29, 30, 31, 30, 31, 0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15, 14, 15, 12, 13, 12, 13, 14, 15, 14, 15, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15, 14, 15, 12, 13, 12, 13, 14, 15, 14, 15, 0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 0, 1, 0, 1, 2, 3, 2, 3, 0, 1, 0, 1, 2, 3, 2, 3, 4, 5, 4, 5, 6, 7, 6, 7, 4, 5, 4, 5, 6, 7, 6, 7, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15, 14, 15, 12, 13, 12, 13, 14, 15, 14, 15, 8, 9, 8, 9, 10, 11, 10, 11, 8, 9, 8, 9, 10, 11, 10, 11, 12, 13, 12, 13, 14, 15, 14, 15, 12, 13, 12, 13, 14, 15, 14, 15, 16, 17, 16, 17, 18, 19, 18, 19, 16, 17, 16, 17, 18, 19, 18, 19, 20, 21, 20, 21, 22, 23, 22, 23, 20, 21, 20, 21, 22, 23, 22, 23, 16, 17, 16, 17, 18, 19, 18, 19, 16, 17, 16, 17, 18, 19, 18, 19, 20, 21, 20, 21, 22, 23, 22, 23, 20, 21, 20, 21, 22, 23, 22, 23, 24, 25, 24, 25, 26, 27, 26, 27, 24, 25, 24, 25, 26, 27, 26, 27, 28, 29, 28, 29, 30, 31, 30, 31, 28, 29, 28, 29, 30, 31, 30, 31, 24, 25, 24, 25, 26, 27, 26, 27, 24, 25, 24, 25, 26, 27, 26, 27, 28, 29, 28, 29, 30, 31, 30, 31, 28, 29, 28, 29, 30, 31, 30, 31, 16, 17, 16, 17, 18, 19, 18, 19, 16, 17, 16, 17, 18, 19, 18, 19, 20, 21, 20, 21, 22, 23, 22, 23, 20, 21, 20, 21, 22, 23, 22, 23, 16, 17, 16, 17, 18, 19, 18, 19, 16, 17, 16, 17, 18, 19, 18, 19, 20, 21, 20, 21, 22, 23, 22, 23, 20, 21, 20, 21, 22, 23, 22, 23, 24, 25, 24, 25, 26, 27, 26, 27, 24, 25, 24, 25, 26, 27, 26, 27, 28, 29, 28, 29, 30, 31, 30, 31, 28, 29, 28, 29, 30, 31, 30, 31, 24, 25, 24, 25, 26, 27, 26, 27, 24, 25, 24, 25, 26, 27, 26, 27, 28, 29, 28, 29, 30, 31, 30, 31, 28, 29, 28, 29, 30, 31, 30, 31, 32, 33, 32, 33, 34, 35, 34, 35, 32, 33, 32, 33, 34, 35, 34, 35, 36, 37, 36, 37, 38, 39, 38, 39, 36, 37, 36, 37, 38, 39, 38, 39, 32, 33, 32, 33, 34, 35, 34, 35, 32, 33, 32, 33, 34, 35, 34, 35, 36, 37, 36, 37, 38, 39, 38, 39, 36, 37, 36, 37, 38, 39, 38, 39, 40, 41, 40, 41, 42, 43, 42, 43, 40, 41, 40, 41, 42, 43, 42, 43, 44, 45, 44, 45, 46, 47, 46, 47, 44, 45, 44, 45, 46, 47, 46, 47, 40, 41, 40, 41, 42, 43, 42, 43, 40, 41, 40, 41, 42, 43, 42, 43, 44, 45, 44, 45, 46, 47, 46, 47, 44, 45, 44, 45, 46, 47, 46, 47, 32, 33, 32, 33, 34, 35, 34, 35, 32, 33, 32, 33, 34, 35, 34, 35, 36, 37, 36, 37, 38, 39, 38, 39, 36, 37, 36, 37, 38, 39, 38, 39, 32, 33, 32, 33, 34, 35, 34, 35, 32, 33, 32, 33, 34, 35, 34, 35, 36, 37, 36, 37, 38, 39, 38, 39, 36, 37, 36, 37, 38, 39, 38, 39, 40, 41, 40, 41, 42, 43, 42, 43, 40, 41, 40, 41, 42, 43, 42, 43, 44, 45, 44, 45, 46, 47, 46, 47, 44, 45, 44, 45, 46, 47, 46, 47, 40, 41, 40, 41, 42, 43, 42, 43, 40, 41, 40, 41, 42, 43, 42, 43, 44, 45, 44, 45, 46, 47, 46, 47, 44, 45, 44, 45, 46, 47, 46, 47, 48, 49, 48, 49, 50, 51, 50, 51, 48, 49, 48, 49, 50, 51, 50, 51, 52, 53, 52, 53, 54, 55, 54, 55, 52, 53, 52, 53, 54, 55, 54, 55, 48, 49, 48, 49, 50, 51, 50, 51, 48, 49, 48, 49, 50, 51, 50, 51, 52, 53, 52, 53, 54, 55, 54, 55, 52, 53, 52, 53, 54, 55, 54, 55, 56, 57, 56, 57, 58, 59, 58, 59, 56, 57, 56, 57, 58, 59, 58, 59, 60, 61, 60, 61, 62, 63}; +Card compress(Card x){ + uint16_t a = x>>6&1; + uint16_t b = bits[x>>8&0x555]<<1; + uint16_t c = bits[x>>20&0x555]<<7; + return a|b|c; +} + + +#endif +#endif +/* Add a card to the hand with the `A` macro: + The hand is stored as an array of 5 ints. The low 3 bits are used to select a suit, + (h[0]=spade,h[1]=club,h[2]=diamond,h[4]=heart). + The cards are added to the suit - since only one suit bit is set, 3 of the low 8 bits + will contain a count of the cards in that suit (that's why the spare is where it is). + h[3] has a single bit set for each card present, used for straight detection +*/ +#define A(h,c)h[c&7]+=c,h[3]|=c + +/* a few globals + */ + +/* The evaluator function:*/ +Card E(Card h[]){ + /*variables: + a: the sum of all suits. counts the ranks in paralell. + But there are only 2 bits used to store each rank, so 4 of a kind will overflow. + We fix that by subtracting h[3] which has a 1 for each rank actually in the hand. + So now every 2-bit field holds the count-1 for that rank. + e: evens - it has a bit set in any rank which has a 1(pair) or 3(quad). + o: odds - it has a bit for every 2(set) or 3(quad). + t: type: will hold the type of hand: + 9= stfl. straight flush. + 7= quad. 4 of a kind + 6= boat. full house + 5= flsh. flush + 4= run. straight. + 3= set. 3 of a kind + 2= 2pr 2 pair + 1= pair. + 0= hi-c. high card. + v: value - the cards that determine the hand value. (eg: pair aces vs pair kings) + k: kicker - will hold the tiebreak card(s) (eg: KKA21 vs KKQ21) + */ /* *h */ + Card count=h[0]+h[1]+h[2]+h[4]-(h[3]&-16L); + Card evens=0x55555540&count; + Card odds =0xAAAAAA80&count; + Card result = 0; + Card value; + Card kicker =h[3]; + Card temp; + + +/* Quad detector: the value `v=e&o/2` will be non-zero only if a rank has both + the even and odd bit set, meaning its count-1 is 3. + The while loop clears all except the top bit of the remaining to find the kicker `k`. + Type is stored in `t` + */ + if(value=evens&odds/2){ + kicker=h[3]^value; + while(temp=kicker&kicker-1) + kicker=temp; + return 7<<28|compress(value)<<13|compress(kicker); + } + +/* Full House detector: + The first line catches 2 sets (odds counter has 2 bits set). + It separates the bits into high set, in `value` and the pair in `k` + The the second line catches a set plus one or two pairs. + It clears one bit from the pairs field if needed when setting `k` + (since AAAKKQQ ranks the same as AAAKKQJ) +*/ + else if(value=odds&odds-1){ + value/=2; + kicker=(odds/2)^value; + return 6<<28|compress(value)<<13|compress(kicker); + + } + else if (evens&&odds) { + result=6; + value=odds/2; + temp = evens&evens-1; + kicker= (temp)?temp:evens; + return 6<<28|compress(value)<<13|compress(kicker); + } + +/* All the other hands fall here. + `h[3]` is in `k`, it will be used to detect straights. + (it holds a bit for each unique value and a bit for each unique suit) +*/ + else{ + /* Look for flushes. + remember that for suit X=1,2,4,8: h[X&7] holds a 3-bit card count, + starting at bit 0,1,2,3 respectively + subtract 1 from the count, store in `C` + If C>4, we have a 5 card flush. `t` is 5. + overwrite `k` with the flush suit, since a plain straight won't beat this, but a + straight flush will. + */ + + if ((count=(h[0]>>3)&7)>4) { kicker=h[0]; result=5;} + else if ((count=h[1]&7)>4) { kicker=h[1]; result=5;} + else if ((count=(h[2]>>1)&7)>4) { kicker=h[2]; result=5;} + else if ((count=(h[4]>>2)&7)>4) { kicker=h[4]; result=5;} + + /* for (i=0;i<4;i++){ + int idx = (1<>i; + count &= 7; + if(count>=5){ + kicker=h[idx]; + result=5; + break; + } + } + */ + // printf("#%d %08x %08x %d\n",C,k,h[X&7],X); + + + // printf("#%d %08x %d %d\n",C,k,X,i); + +/* Now the straight detector. + clear the suit bits from a, then copy down the high bit (ace) + to the ones position so we can catch 5-high straights. +*/ + kicker&=-64; + value=kicker|(kicker>>26)&16; + +/* The next line zeros value unless there are at least 5 cards in a row. + `t` will be 4 for straights, 9 for straight flushes. + For a 6 or 7 card straight, there will be multiple consecutive bits set in value: + `value&=~(value/4)` clears all but the highest. +*/ + value&=value*4; + value&=value*4; + value&=value*4; + value&=value*4; + if(value){ + result+=4; + value&=~(value/4); + return result<<28|compress(value)<<13; + } + //k^value has 0 bits, i does not matter +/* finish up the pure flush processing: 't' is only set for flush, + store the high 5 cards in `k` and `value`, + by clearing low bit until the card count `C` is 5. + (done after straight detection to avoid calling AK98765 in same suit a plain flush.) + ((i will be 0 for cases below here)) + */ +// else if(i=t){for(i=(h[v&7]&63)/v;i-->5;)k&=k-1;v=k;} //k^v has 0 bits, i does not matter + else if (result){ + while(count-->5){ + kicker&=kicker-1; //k^v has 0 bits, i does not matter + } + return result<<28|compress(kicker)<<13; //|0 + } +/* three of a kind: + two sets are a full house, caught above. so if there is any bit left in 'odds', + it is a set. v=o/2 shifts the value bit into the right place +*/ + else if(value=odds/2) { + result=3; //v has 1 bit, k^v has 4 bits, i is 0 so we can clear 2 + kicker^=value; + kicker&=kicker-1; + kicker&=kicker-1; + return 3<<28|compress(value)<<13|compress(kicker); + } +/* Pairs: a bit set in evens is a pair. we might have 1,2, or 3 of them. + `o` will be set if there is more than one, `i` will be set if there are 3. + `v` is set to the top 1 or 2 cards. 't' is 1 or 2. + */ + else if (evens){ + temp=evens&evens-1; + if (temp&temp-1){ + kicker^=temp; + kicker&=kicker-1; + return 2<<28|compress(temp)<<13|compress(kicker); + } + else{ + kicker^=evens; + kicker&=kicker-1; + kicker&=kicker-1; + return 1+(temp>0)<<28|compress(evens)<<13|compress(kicker); + } + } +/* for all hands except 4 of a kind and full house, + we have left the primary cards which determine the hand's type in 'value' + and `a` holds all the cards (except a == v for flushes and straights) + set k to the kickers by findig all in a not in v (a^v) + then clear the extra 2. (or 1 if i is non zero b/c there was a 3rd pair). + */ +// printf("#%08x %08x %08x %d\n",value,k,k^value,i); + } + +/* + build the final result. for high card + 4 bits for the type 0..9, 13 bits for the value cards, 13 for the kicker. + */ + kicker&=kicker-1; + kicker&=kicker-1; + return 0|0<<13|compress(kicker); +} diff --git a/ace_functions.pyx b/ace_functions.pyx new file mode 100644 index 0000000..93f2263 --- /dev/null +++ b/ace_functions.pyx @@ -0,0 +1,222 @@ +# distutils: define_macros=CYTHON_TRACE_NOGIL=1 +# cython: boundscheck=False +# cython: wraparound=False + +# cython file with key functions for evaluating hands and running matchups +# needs to be compiled by running ACE_Cython.ace_setup + +from libc.stdint cimport uint32_t as Card, uint16_t +from libc.stdlib cimport rand, RAND_MAX +import itertools +cimport numpy as np +import numpy as np +from scipy.special import comb + +cdef extern from "ace_eval_py.h": + Card ACE_makecard(Card i) + Card ACE_evaluate(Card* h) + void ACE_addcard(Card* h, Card c) + +cdef ceval_hand(Card[:] hand_in): + # evaluate a 7-card poker hand and return a value + # the bigger value hand is the winner + # uses external C functions from AShelly ACE_eval + cdef Card h[5] #TODO: !!! why only five? + cdef Py_ssize_t i + h[:] = [0, 0, 0, 0, 0] + cdef Card* h_ptr = &h[0] + for i in range(len(hand_in)): + ACE_addcard(h_ptr, hand_in[i]) + cdef Card v = ACE_evaluate(h_ptr) + return v + +def eval_hand(hand_in): + # convenience function to allow for accessing hand evaluation from python + # takes an iterable containing the cards as an input + cdef Card[:] h = np.zeros(7, dtype=np.uint32) + cdef Py_ssize_t i + cdef Card c + for i, c in enumerate(hand_in): + h[i] = c + return ceval_hand(h) + +cdef ceval_hand_batch(np.ndarray[Card, ndim=2] hands_in): + # currently not used; idea was to make a version that does all a player's hands at once + # trying to make a function to take a list of player hands in and evaluate here + # ideas is to see if it speeds up the evaluation by making fewer calls from the python layer + + cdef np.ndarray ranks = np.zeros(1000000) + + for i in range(1000000): + ranks[i] = ceval_hand(hands_in[i,:]) + + return ranks + +def init_deck(): + # initializes a deck using the 32-bit integer system + # returns: list of integers comprising a deck + # 2 through A, hearts, clubs, diamonds, spades + + deck = list() + for i in range(52): + deck.append(ACE_makecard(i)) + return deck + +cdef shuffle_me(Card[:] deck, Py_ssize_t len_deck): + # shuffles a deck memoryview + + cdef int r + cdef Py_ssize_t i + cdef Card temp + for i in range(len_deck-1, 0, -1): + r= ((rand())/RAND_MAX*i) + temp = deck[i] + deck[i]=deck[r] + deck[r]=temp + +def make_card(card): + return ACE_makecard(card) + +cdef chand_rank(Card r): + return r>>28 + +def hand_rank(r): + return r>>28 + +def check_accuracy(list deck): + # loop through all possible seven-card combinations in the deck, determine their rank + # return the counts of ranks for comparison to documented frequencies in order to check the accuracy + + cdef Card[:] freq = np.zeros(10, dtype=np.uint32) + cdef Card v + cdef Card r + cdef Card[:] hand = np.zeros(7, dtype=np.uint32) + cdef Py_ssize_t i = 0 + for h in itertools.combinations(deck, 7): + for i in range(7): + hand[i] = h[i] + v = ceval_hand(hand) + r = chand_rank(v) + freq[r] += 1 + print('Processed {} MM hands'.format(sum(freq)/1e6)) + return freq + +def matchup(hands, deck, board=()): + ''' + runs through all possible combinations of hands v. each other + + hands: list of hand tuples + deck: list of remaining card ints in the deck + board: tuple for the cards already on the board + + returns: two numpy arrays: one with win percentages + for each hand in order passed, and the other + with tie percentages + ''' + cdef Card hands_played = comb(len(deck), 5-len(board)) + cdef Py_ssize_t num_players = len(hands) + cdef np.ndarray hand_ranks = np.zeros((hands_played, num_players), dtype=np.uint32) + cdef Card[:,:] hand_ranks_view = hand_ranks + cdef Card[:] h = np.zeros(7, dtype=np.uint32) + cdef Py_ssize_t i, c, j + cdef Py_ssize_t len_board = len(board) + cdef Py_ssize_t num_hands = len(hands) + cdef Card[:,:] hands_sim = np.zeros((num_hands, 2), dtype=np.uint32) + + for i in range(num_hands): + hands_sim[i,0] = hands[i][0] + hands_sim[i,1] = hands[i][1] + + for c in range(len_board): + h[2+c] = board[c] + + for s, b in enumerate(itertools.combinations(deck, 5-len(board))): + for c in range(5-len_board): + h[2+len_board+c] = b[c] + for i in range(num_hands): + h[0] = hands_sim[i, 0] + h[1] = hands_sim[i, 1] + hand_ranks_view[s, i] = ceval_hand(h) + + return get_results(hand_ranks, num_players, hands_played) + + +def sim_matchup(list hands, list deck, tuple board=(), int random_opponents=1, int num_sims=1000000): + ''' + Simulates hands and returns the winners and ties. + + hands: list of hand tuples + deck: list of remaining card ints in the deck + board: tuple for the cards already on the board + random_opponents: int for the number of random opponents to play agaginst + num_sims: int for number of sims to perform + + returns: two numpy arrays: one with win percentages + for each hand in order passed, and the other + with tie percentages + ''' + + cdef Py_ssize_t num_players = len(hands) + random_opponents + cdef Py_ssize_t len_deck = len(deck) + cdef Py_ssize_t num_cards_needed = 5-len(board) + 2*random_opponents + cdef Py_ssize_t counter = 0 + cdef Py_ssize_t num_hands = len(hands) + cdef Py_ssize_t len_board = len(board) + + cdef np.ndarray hand_ranks = np.zeros((num_sims, num_players), dtype=np.uint32) + cdef Card[:,:] hand_ranks_view = hand_ranks + cdef Py_ssize_t o + cdef Py_ssize_t c + cdef Py_ssize_t s, i + + cdef Card[:,:] hands_sim = np.zeros((num_players, 2), dtype=np.uint32) + for i in range(num_hands): + hands_sim[i,0] = hands[i][0] + hands_sim[i,1] = hands[i][1] + + cdef Card[:] cdeck = np.zeros(len_deck, dtype=np.uint32) + cdef Card[:] h = np.zeros(7, dtype=np.uint32) + + for c in range(len_deck): + cdeck[c] = deck[c] + + for c in range(len_board): + h[2+c] = board[c] + + shuffle_me(cdeck, len_deck) + for s in range(num_sims): + if (len_deck-counter) < num_cards_needed: + shuffle_me(cdeck, len_deck) + counter=0 + + for o in range(random_opponents): + hands_sim[num_hands + o, 0] = cdeck[counter+2*o] + hands_sim[num_hands + o, 1] = cdeck[counter+1+2*o] + + for c in range(5-len_board): + h[2+len_board+c] = cdeck[counter+2*random_opponents+c] + + counter += num_cards_needed + + for i in range(num_players): + h[0] = hands_sim[i,0] + h[1] = hands_sim[i,1] + hand_ranks_view[s,i] = ceval_hand(h) + + return get_results(hand_ranks, num_players, num_sims) + +cdef get_results(np.ndarray ranks, Py_ssize_t num_players, int num_sims): + # helper function for matchup and sim_matchup that + # takes a numpy array of hand ranks (rows) and players (columns) and + # calculates the wins and ties (where value is greater than or equal to others) + + cdef np.ndarray winners = np.zeros((num_sims, num_players), dtype=np.uint32) + cdef np.ndarray ties = np.zeros((num_sims, num_players), dtype=np.uint32) + cdef np.ndarray m = ranks.max(axis=1) + cdef np.ndarray a = np.tile(m, (num_players,1)).T + cdef np.ndarray w = (ranks==a) + cdef tuple w_ind = np.nonzero(np.tile(w.sum(axis=1)==1,(num_players,1)).T & w) + winners[w_ind] = 1 + cdef tuple t_ind = np.nonzero(np.tile(w.sum(axis=1)!=1,(num_players,1)).T & w) + ties[t_ind] = 1 + return winners.sum(axis=0)/num_sims, ties.sum(axis=0)/num_sims diff --git a/ace_setup.py b/ace_setup.py new file mode 100644 index 0000000..0e87e8f --- /dev/null +++ b/ace_setup.py @@ -0,0 +1,13 @@ +import setuptools +from distutils.core import setup +from Cython.Build import cythonize +from Cython.Compiler.Options import get_directive_defaults +import numpy +from Cython.Compiler import Options + +Options.generate_cleanup_code=False + +setup(ext_modules=cythonize("ace_functions.pyx", annotate=True), + include_dirs=[numpy.get_include()], build_dir="build", + script_args=['build'], options={'build': {'build_lib': '.'}, }, + ) diff --git a/ace_test_py.py b/ace_test_py.py new file mode 100644 index 0000000..878f275 --- /dev/null +++ b/ace_test_py.py @@ -0,0 +1,68 @@ +# this is a file to test the ace_functions cython integration for time and accuracy + +from ace_functions import init_deck, hand_rank, check_accuracy, eval_hand +import time + +card_strings = ['2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh', 'Ah', + '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc', 'Ac', + '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd', 'Ad', + '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks', 'As'] + +card_dict = {s: i for s, i in zip(card_strings, range(0, 52))} +rank_dict = {0: "High Card", + 1: "One Pair", + 2: "Two Pair", + 3: "Three of a Kind", + 4: "Straight", + 5: "Flush", + 6: "Full House", + 7: "Four of a Kind", + 8: "unused", + 9: "Straight Flush" + } + +deck = init_deck() +my_hand = deck[:7] + +# check some 7-card hands +sf = eval_hand(my_hand) +print(rank_dict[hand_rank(sf)]) +sf1 = eval_hand(deck[1:8]) +print(rank_dict[hand_rank(sf1)]) + +# check some 5-card hands +quads = eval_hand([deck[0], deck[13], deck[26], deck[39], deck[12]]) +print(rank_dict[hand_rank(quads)]) +flush = eval_hand([deck[0], deck[1], deck[5], deck[11], deck[12]]) +print(rank_dict[hand_rank(flush)]) + +# check a 7-card hand from strings +hand_string = ['Ah', 'Kh', 'Qh', 'Th', 'Jc', '2c', '8c'] +hand = [deck[card_dict[c]] for c in hand_string] +v = rank_dict[hand_rank(eval_hand(hand))] +print(v) + +# check the accuracy of all 7-card combos +start = time.time() +freq = check_accuracy(deck) +print('Seconds to run = {}'.format(time.time() - start)) + +for i in range(10): + print(rank_dict[i] + ': ' + str(freq[i])) + +# printed results can be compared to +# https://en.wikipedia.org/wiki/Poker_probability#Frequency_of_7-card_poker_hands +# note that straight flush is royal flush + straight flush + # Hand Frequency + # Royal flush 4,324 + # Straight flush (excl. royal flush) 37,260 + # Four of a kind 224,848 + # Full house 3,473,184 + # Flush 4,047,644 + # Straight 6,180,020 + # Three of a kind 6,461,620 + # Two pair 31,433,400 + # One pair 58,627,800 + # No pair 23,294,460 + # Total 133,784,560 + diff --git a/ace_utils.py b/ace_utils.py new file mode 100644 index 0000000..78229b1 --- /dev/null +++ b/ace_utils.py @@ -0,0 +1,195 @@ +# contains convenience functions, global variable definitions, and visualization functions + +from ACE_Cython.ace_functions import init_deck, hand_rank, matchup, sim_matchup +import time +import random +import numpy as np + + +CARD_STRINGS = ['2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh', 'Ah', + '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc', 'Ac', + '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd', 'Ad', + '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks', 'As'] + +CARD_DICT = {s: i for s, i in zip(CARD_STRINGS, range(0, 52))} +FULL_DECK = init_deck() + +rank_dict = {0: "High Card", + 1: "One Pair", + 2: "Two Pair", + 3: "Three of a Kind", + 4: "Straight", + 5: "Flush", + 6: "Full House", + 7: "Four of a Kind", + 8: "unused", + 9: "Straight Flush" + } + + +def deal_hands(number_of_hands, deck): + """ + Deal random hands from a deck. + :param number_of_hands: Number of hands to deal + :param deck: Deck to deal from + :return: List of hands and deck list with the cards removed. + """ + random.shuffle(deck) + hands = list() + for h in range(number_of_hands): + hands.append((deck.pop(), deck.pop())) + return hands, deck + + +def remove_from_deck(deck, hand_list): + """ + Take an existing deck and remove cards from it. + :param deck: list of ints deck to remove cards from + :param hand_list: list of card tuples to remove + :return: copy of deck with cards removed + """ + new_deck = deck[:] + for h in hand_list: + for c in h: + new_deck.remove(c) + return new_deck + + +def _card_to_string(c_in): + return CARD_STRINGS[FULL_DECK.index(c_in)] + + +def _string_to_card(c_in): + return FULL_DECK[CARD_DICT[c_in]] + + +def cards_to_string(c_in): + """ + Turn the cards into human-readable format. + + :param c_in: list, tuple, or string of card integers + :return: same object as input, converted to human-readable + """ + if isinstance(c_in, int): + return _card_to_string(c_in) + + if isinstance(c_in, tuple): + return tuple(map(_card_to_string, c_in)) + + if isinstance(c_in, list): + return [cards_to_string(c) for c in c_in] + + +def string_to_cards(cards, is_board=False): + """ + Convert human readable cards to the ints needed by the program. + + :param cards: string of cards, list of strings, or list of tuples of strings + format examples: 'AhKh7c7s', ['Ah', 'Kh', '7c', '7s'], [('Ah','Kh'), ('7c', '7s')] + broken up into pairs representing a hand (split in the order passed if not tuples) + :param is_board: if True, it is a board; represents hands if False + :return: if is_board=False, returns list 2-length hand tuples + if is_board=True, returns a board tuple + """ + if isinstance(cards, str): + card_list = [cards[i*2:i*2+2] for i in range(len(cards)//2)] + elif isinstance(cards, list): + if isinstance(cards[0], tuple): + card_list = [c for h in cards for c in h] + else: + card_list = cards + elif isinstance(cards, tuple): + card_list = list(cards) + else: + raise TypeError + + if not is_board: + c_iter = iter(card_list) + hand_list = list() + for a, b in zip(c_iter, c_iter): + hand_list.append((_string_to_card(a), _string_to_card(b))) + return hand_list + else: + return tuple(map(_string_to_card, card_list)) + + +def unique_starting_hands(offset=0): + """ + Create a list of unique starting hands that represent + all 169 mathematically different hands (suited, unsuited, pairs) + + :param offset: lets you adjust the suit (offset=13) + + :return: list of hand tuples + """ + + hand_list = list() + for i in range(13): + for j in range(i+1, 13): + hand_list.append((FULL_DECK[i+offset], FULL_DECK[j+offset])) + + for i in range(13): + for j in range(i+13, 26): + hand_list.append((FULL_DECK[i+offset], FULL_DECK[j+offset])) + + return hand_list + + +def matchup_from_string(cards, board=(), random_opponents=None, num_sims=None): + """ + Run a matchup of 2 or more hands to determine win and tie percentages. + Can be run deterministically or with simulations. + + :param cards: string of cards, list of strings, or list of tuples of strings + format examples: 'AhKh7c7s', ['Ah', 'Kh', '7c', '7s'], [('Ah','Kh'), ('7c', '7s')] + broken up into pairs representing a hand (split in the order passed if not tuples) + :param board: cards already on the board, same formats as cards: string, list, or tuple + :param random_opponents: int for number of random opponents to play against; increase # of hands + :param num_sims: int for number of simulations to perform + + :return: two tuples: wins, ties; both of length # of hands + items represent the % of each hand winning or tying, in order of hands passed + will likely sum to more than one since multiple hands tie + """ + + hand_list = string_to_cards(cards) + board = string_to_cards(board, is_board=True) + + if random_opponents and num_sims: + return sim_matchup(hand_list, remove_from_deck(FULL_DECK, hand_list + [board]), board, + random_opponents, num_sims) + else: + return matchup(hand_list, remove_from_deck(FULL_DECK, hand_list + [board]), board) + +# TODO: put in a utility that does hand ranges + + +if __name__ == '__main__': + # initialize a deck, deal two random hands, look at results + d = init_deck() + h1, d1 = deal_hands(2, d) + print(cards_to_string(h1)) + print(matchup(h1, d1)) + + # all possible matchups without a board, different input formats + w1, t1 = matchup_from_string('AsAhKsKh') + w2, t2 = matchup_from_string(['As', 'Ah', 'Ks', 'Kh']) + w3, t3 = matchup_from_string([('As', 'Ah'), ('Ks', 'Kh')]) + + # more opponents + w4, t4 = matchup_from_string('AsAhKsKh8c8d') + + # all possible matchups with a board, different input formats + w5, t5 = matchup_from_string('AsAhKsKh', 'Kc7s7h') + w6, t6 = matchup_from_string('AsAhKsKh', ('Kc', '7s', '7h')) + w7, t7 = matchup_from_string('AsAhKsKh', ['Kc', '7s', '7h']) + + # simulate a matchup that includes a random opponent + w8, t8 = matchup_from_string(['As', 'Ah'], (), 1, 1000000) + # simulate a matchup with a known opponent and a random opponent + w9, t9 = matchup_from_string(['As', 'Ah', 'Ks', 'Kh'], (), 1, 1000000) + + # simulate with board + # TODO: probably could just run all possible combinations) + w10, t10 = matchup_from_string(['As', 'Ah', 'Ks', 'Kh'], 'Kc7c7s', 1, 1000000) + w11, t11 = matchup_from_string(['As', 'Ah', 'Ks', 'Kh'], ('Kc', '7c', '7s'), 1, 1000000) diff --git a/starting_hands.py b/starting_hands.py new file mode 100644 index 0000000..0de9d21 --- /dev/null +++ b/starting_hands.py @@ -0,0 +1,34 @@ +# file to test all possible starting hands against n opponents +# for comparison: https://cs.indiana.edu/~kapadia/nofoldem/ +# https://www.mathematrucker.com/poker/matchups_proof.php + +from ACE_Cython.ace_utils import * +import datetime as dt +import pandas as pd +import time + +if __name__ == '__main__': + + hand_list = unique_starting_hands() + hand_strings = cards_to_string(hand_list) + wins_df = pd.DataFrame(data=None, index=hand_strings) + ties_df = pd.DataFrame(data=None, index=hand_strings) + + for opp in range(1, 10): + start_time = time.time() + wins_list = list() + ties_list = list() + for h in hand_list: + w, t = sim_matchup([h], remove_from_deck(FULL_DECK, [h]), random_opponents=opp, num_sims=2000000) + wins_list.append(w) + ties_list.append(t) + + wins_df[str(opp)] = [x[0] for x in wins_list] + ties_df[str(opp)] = [x[0] for x in ties_list] + + print('{} Done {} opponents.'.format(dt.datetime.now(), opp)) + print('Elapsed time for loop {} is {}'.format(opp, time.time() - start_time)) + # export them each loop just in case it fails + wins_df.to_csv('wins3.csv') + ties_df.to_csv('ties3.csv') +