From: Frode Øijord
Subject: Lisp and Poker
Date: 
Message-ID: <123nkl387ep7g7d@corp.supernews.com>
Hi,

I previously posted on this list to get some help regarding evaluation 
of poker hands. I tried this first in Python, but it turned out to be 
too slow. I therefore decided to give Lisp a try after getting some 
excellent feedback. I have now (with the help of feedback from 
comp.lang.lisp) written some poker code that sort of works, but since 
this is my first attempt at Lisp I'm afraid the implementation is rather 
crude. I'm hoping to get some input on the code style, and perhaps also 
some hints on efficiency. (I'm just doing this for fun by the way).

The overall idea is to reduce a poker hand to a single number. This is 
called a Ness enumeration. This approach is explained quite well here:

http://mywebpages.comcast.net/dness/poker.htm

Given this information I hope the code is self-explanatory.

There are some things in the code I like less than others. The function 
eval-hand is the heart of the algorithm. Once the characteristics vector 
of the hand is known, I would like to return it immediately. The 
handling of straigts is inelegant and probably very inefficient as well. 
I don't yet handle the case where straights are five-high properly either.

Here are a couple of examples of running the code:

CL-USER> (eval-hand (mapcar #' card '((0 0) (1 0) (2 0) (3 0) (4 0))))
(9 4 3 2 1 0)

This hand represents a straight flush of spades, six high. (2, 3, 4, 5, 
and 6 of spades). The return value is the so called characteristics 
vector of the hand. The leading 9 means that the hand is a 
straight-flush. (the value of a flush + the value of a straight). Then 
the denominations of the cards follow in decreasing order.

CL-USER> (eval-hand (mapcar #' card '((1 1) (1 0) (1 2) (3 0) (4 0))))
(3 1 4 3 12 11)

This one is a three of a kind. There are 3 1's (or rather 3's), then a 6 
and 5. The 12 and 11 at the end are filler. We need the filler because 
this characteristics vector will be reduced to a base 13 number with the 
values of the vector representing the digits, in order to effectively 
compare hands.

If you have a fast computer you can try to count all the possible hands 
and list the number in each category. I would not try this unless you 
can compile the code first. This is from SBCL:

* (load "poker-eval.lisp")

T
* (count-hands)

1302540
1098240
123552
54912
10200
5108
3744
624
40


-- 
Frode, SIM

"Any fool can write code that a computer can understand.
  Good programmers write code that humans can understand"

From: Frode Øijord
Subject: Re: Lisp and Poker
Date: 
Message-ID: <123nko7f8rulvc2@corp.supernews.com>
This is a multi-part message in MIME format.
--------------060808060902000402040106
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit

Here is the code by the way :)

-- 
Frode, SIM

"Any fool can write code that a computer can understand.
  Good programmers write code that humans can understand"

--------------060808060902000402040106
Content-Type: text/plain;
 name="poker-eval.lisp"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
 filename="poker-eval.lisp"


(defun deck ()
  (let ((cards nil))
    (dotimes (i 52)
      (push i cards))
    (nreverse cards)))

(defun card (tuple)
  (+ (first tuple) (* (second tuple) 13)))

(defun denom (card)
  (mod card 13))

(defun suit (card)
  (values (floor (/ card 13.0))))

(defun map-combinations (items n fun &optional (selected '()))
  (if (zerop n)
      (funcall fun selected)
      (loop for (item . rest) on items do
           (push item selected)
           (map-combinations rest (1- n) fun selected)
           (pop selected))))

(defun count-elements (hand)
  (let ((cards (remove-duplicates hand)))
    (loop for card in cards
       collect (list (count card hand) card))))

(defmacro equal-case (form &body clauses)
  (let ((value (gensym)))
    `(let ((,value ,form))
       (cond ,@(loop for (key . actions) in clauses
                  collect `((equal ,value ',key) ,@actions))))))

(defun digits->number (digits)
  (reduce (lambda (n digit) (+ (* n 13) digit)) digits
          :initial-value 0)) 

(defparameter *straights* 
  '("1111000000001"
    "1111100000000"
    "0111110000000"
    "0011111000000"
    "0001111100000"
    "0000111110000"
    "0000011111000"
    "0000001111100"
    "0000000111110"
    "0000000011111"))

(defun straightp (hand)
  (let ((counts (make-list 13 :initial-element 0)))
    (dolist (card hand)
      (incf (elt counts card)))
    
    (position-if (lambda (straight)
                   (string= (coerce (mapcar #'digit-char counts) 'string) straight))
                 *straights*)))


(defun eval-hand (hand)
  (let* ((denomcounts (sort (count-elements (mapcar #'denom hand)) #'> 
                            :key #'digits->number))
         (denoms (mapcar #'second denomcounts))
         (counts (mapcar #'first denomcounts))
         (cv nil))
    
    ;; does the hand contain duplicates?
    (equal-case counts
                ((2 1 1 1) (setf cv (append '(1) denoms '(12))))
                ((2 2 1) (setf cv (append '(2) denoms '(12 11))))
                ((3 1 1) (setf cv (append '(3) denoms '(12 11))))
                ((3 2) (setf cv (append '(6) denoms '(12 11 10))))
                ((4 1) (setf cv (append '(7) denoms '(12 11 10 9)))))

    (let* ((suitcounts (sort (count-elements (mapcar #'suit hand)) #'> 
                             :key #'digits->number))
           (suits (mapcar #'first suitcounts))
           (handtype 0))
      
      ;; is it a straight?
      (if (straightp (mapcar #'denom hand))
          (progn
            (setf handtype 4)
            (setf cv (append (list handtype) denoms))))
      
      ;; is it a flush?   
      (equal-case suits
                  ((5) (setf cv (append (list (+ 5 handtype)) denoms)))))

    ;; if it's none of the above it must be a highcard
    (if (null cv)
        (setf cv (append '(0) denoms)))
    cv))


(defun count-hands ()
  (let ((highcard 0)
        (pair 0)
        (two-pair 0)
        (trips 0)
        (straight 0)
        (flush 0)
        (full-house 0)
        (quads 0)
        (straight-flush 0))

    (map-combinations (deck) 5
                      #'(lambda (hand)
                          (case (car (eval-hand hand))
                            (0 (incf highcard))
                            (1 (incf pair))
                            (2 (incf two-pair))
                            (3 (incf trips))
                            (4 (incf straight))
                            (5 (incf flush))
                            (6 (incf full-house))
                            (7 (incf quads))
                            (9 (incf straight-flush)))))
  
    
    (values highcard
            pair
            two-pair
            trips
            straight
            flush
            full-house
            quads
            straight-flush)))


--------------060808060902000402040106--
From: Alexander Schmolck
Subject: Re: Lisp and Poker
Date: 
Message-ID: <yfshd50xao1.fsf@oc.ex.ac.uk>
Frode �ijord <·····@sim.no> writes:

> Here is the code by the way :)

Didn't look much at it, but the use of string= appears perverse -- is there a
reason why you don't just use EQUAL and lists?

Another thing, by posting the code after a "--\n" you make it awkward to quote
it since many clients will not quote sigs.

'as
From: Frode Øijord
Subject: Re: Lisp and Poker
Date: 
Message-ID: <443BF08D.8010407@sim.no>
Alexander Schmolck wrote:
> Frode �ijord <·····@sim.no> writes:
> 
>> Here is the code by the way :)
> 
> Didn't look much at it, but the use of string= appears perverse -- is there a
> reason why you don't just use EQUAL and lists?

Actually, when I look at it now, I don't really understand why I thought 
it was a good idea to make strings and then compare them, instead of 
comparing lists directly. So no, there is no (good) reason for this :)

Frode �ijord
From: Frode Øijord
Subject: Re: Lisp and Poker
Date: 
Message-ID: <443bf0a8$1@news.broadpark.no>
Alexander Schmolck wrote:
> Frode �ijord <·····@sim.no> writes:
> 
>> Here is the code by the way :)
> 
> Didn't look much at it, but the use of string= appears perverse -- is there a
> reason why you don't just use EQUAL and lists?

Actually, when I look at it now, I don't really understand why I thought 
it was a good idea to make strings and then compare them, instead of 
comparing lists directly. So no, there is no (good) reason for this :)

Frode �ijord
From: Joel Reymont
Subject: Re: Lisp and Poker
Date: 
Message-ID: <1144794987.459744.305110@j33g2000cwa.googlegroups.com>
Frode,

darcs get http://wagerlabs.com/cl-openpoker 

    Joel
From: Frode Øijord
Subject: Re: Lisp and Poker
Date: 
Message-ID: <443c42e0$1@news.broadpark.no>
Joel Reymont wrote:
> Frode,
> 
> darcs get http://wagerlabs.com/cl-openpoker 

I looked at cards.lisp, and it seems the hand evaluation code is much 
more complicated than it has to be. Before I started writing the 
evaluation code I spent some time researching how to best evaluate poker 
hands. The best way to do this is by Ness Enumeration, which is 
basically a mapping from the set of comb(52,5) hands into the integers 
[0...2598959]. It is very convenient to be able to define a hand by a 
single number when comparing hands or computing some statistics :)

Here's a link: http://mywebpages.comcast.net/dness/poker.htm

Frode �ijord
From: GP lisper
Subject: Re: Lisp and Poker
Date: 
Message-ID: <slrne3ojd9.7kj.spambait@phoenix.clouddancer.com>
On Wed, 12 Apr 2006 01:59:24 +0200, <·····@sim.no> wrote:
> Joel Reymont wrote:
>> darcs get http://wagerlabs.com/cl-openpoker 
>
> I looked at cards.lisp, and it seems the hand evaluation code is much 
> more complicated than it has to be.

> evaluation code I spent some time researching how to best evaluate poker 
> hands. The best way to do this is by Ness Enumeration, which is 
> basically a mapping from the set of comb(52,5) hands into the integers 
> [0...2598959]. It is very convenient to be able to define a hand by a 
> single number when comparing hands or computing some statistics :)

I agree that a simplification to a single number is rather valuable.
Since you have a lot of familiarity with the technique, can I inquire
as to the nature of the number mapping?  Is it such that numbers in
the neighborhood of a hand, say 124424, relate to hands in the
neighborhood of the actual cards, i.e. do full houses with the same
triple map to nearby numbers?

*** Free account sponsored by SecureIX.com ***
*** Encrypt your Internet usage with a free VPN account from http://www.SecureIX.com ***
From: Frode Øijord
Subject: Re: Lisp and Poker
Date: 
Message-ID: <443ca4f5$1@news.broadpark.no>
GP lisper wrote:
> On Wed, 12 Apr 2006 01:59:24 +0200, <·····@sim.no> wrote:
>> Joel Reymont wrote:
>>> darcs get http://wagerlabs.com/cl-openpoker 
>> I looked at cards.lisp, and it seems the hand evaluation code is much 
>> more complicated than it has to be.
> 
>> evaluation code I spent some time researching how to best evaluate poker 
>> hands. The best way to do this is by Ness Enumeration, which is 
>> basically a mapping from the set of comb(52,5) hands into the integers 
>> [0...2598959]. It is very convenient to be able to define a hand by a 
>> single number when comparing hands or computing some statistics :)
> 
> I agree that a simplification to a single number is rather valuable.
> Since you have a lot of familiarity with the technique, can I inquire
> as to the nature of the number mapping?  Is it such that numbers in
> the neighborhood of a hand, say 124424, relate to hands in the
> neighborhood of the actual cards, i.e. do full houses with the same
> triple map to nearby numbers?

I guess that would depend on what you mean with nearby. It is always the 
case that a better hand maps to a higher number, and if a hand is better 
than another, its number will be higher. If the numbers are equal, the 
hands are equal. The mapping is not dense though, so if what you are 
asking is if the highest full house and the next highest full house are 
represented by consecutive numbers the answer is no.

Frode �ijord