HenryRLee / PokerHandEvaluator

Poker-Hand-Evaluator: An efficient poker hand evaluation algorithm and its implementation, supporting 7-card poker and Omaha poker evaluation
Apache License 2.0
370 stars 79 forks source link

Add Java implementation #5

Open HenryRLee opened 5 years ago

HenryRLee commented 5 years ago

Add Java implementation, using the same algorithm, similar to the C++ implementation.

Create a directory named java and add Java source code in it.

hellraiserinchief commented 4 years ago

I would like to pick this up. [ also there is a typo, its should be 'add java code' in the description above ]

HenryRLee commented 4 years ago

Thank you. @hellraiserinchief

HenryRLee commented 4 years ago

Hi @hellraiserinchief, I've just updated the Contributing file, which specified the code style.

Feel free to ask me if you have any questions related to the implementation. Good luck!

HenryRLee commented 4 years ago

I have some advice/guide that may help you with this implementation: You can start implementing a 5-card or a 7-card evaluator only, with unit tests, and make a PR for it. Then make consequential PRs for the rest of the evaluators.

It might not be easy to write test codes fully cover all the possible hands (enumerating all possibilities like the C++ implementation, which is using Cactus Kev's evaluator as a reference). To make it easier, you can just write test code only choosing a few examples in each rank category. For example, choose a few hands from Straight Flush, then a few from Four of a Kind, etc.

hellraiserinchief commented 4 years ago

Makes sense, I will get stated on a 5-card evaluator and take things from there.

hellraiserinchief commented 4 years ago

Hi, seems like we are just returning the ranks using the hash tables after some simple operations. Can you please let me know how these hash tables were created in the first place? I would like to learn how we generated these hash tables.

HenryRLee commented 4 years ago

Sure, @hellraiserinchief.

The basic idea of the algorithm is, given a hand of more than 5 cards, it will find its strength value in a hash table. Splitting the hands by whether it is a flush, will reduce the total size of the hash table. So there will be two hash tables: flush[8192] covering all 13-bit binaries, and noflush5[6175] (5-card evaluator only) covering all 13-bit quiaries (base 5 numbers). You can find out why in this algorithm documentation.

The way I generated the flush[8192] and the noflush5[6175], was not through calculation. I simply enumerated all possible hands, got the correct value from Cactus Kev's evaluator, ran my hash function, and finally assigned the value to the corresponding slot in the hash table. This part is boring, you can copy these two tables if you like.

As for the other tables, I can give you a little bit more code walk first. The entry is the method evaluate_5cards:

int evaluate_5cards(int a, int b, int c, int d, int e)
{
  int suit_hash = 0;
  int suit_binary[4] = {0};
  unsigned char quinary[13] = {0};
  int hash;

  suit_hash += suitbit_by_id[a];
  suit_hash += suitbit_by_id[b];
  suit_hash += suitbit_by_id[c];
  suit_hash += suitbit_by_id[d];
  suit_hash += suitbit_by_id[e];

  if (suits[suit_hash])
  {
    suit_binary[a & 0x3] |= binaries_by_id[a];
    suit_binary[b & 0x3] |= binaries_by_id[b];
    suit_binary[c & 0x3] |= binaries_by_id[c];
    suit_binary[d & 0x3] |= binaries_by_id[d];
    suit_binary[e & 0x3] |= binaries_by_id[e];

    return flush[suit_binary[suits[suit_hash]-1]];
  }

  quinary[(a >> 2)]++;
  quinary[(b >> 2)]++;
  quinary[(c >> 2)]++;
  quinary[(d >> 2)]++;
  quinary[(e >> 2)]++;

  hash = hash_quinary(quinary, 13, 5);

  return noflush5[hash];
}

The first part is generating a suit hash and looking it up in a table named suits. The suits table will tell us whether the hand is a flush, and if it is, it will tell us which suit the flush is in. The table has five different values: 0, 1, 2, 3, 4. Value 0 indicates it not a flush. Value 1 indicates it is a flush in clubs, 2 for diamonds, 3 for hearts and 4 for spades.

Actually, the suit table is not a necessary part of the algorithm. Maybe the following code would be easier to understand, which is how I originally coded the algorithm.

int evaluate_5cards(int a, int b, int c, int d, int e) {
  int suit_counter[4] = {0};

  // The last two bits is the suit of the card, this will count how many cards are in each suit.
  suit_counter[a & 0x3]++;
  suit_counter[b & 0x3]++;
  suit_counter[c & 0x3]++;
  suit_counter[d & 0x3]++;
  suit_counter[e & 0x3]++;

  for (int i = 0; i < 4; i++) {
    if (suit_counter[i] >= 5) { // One of the suit has more than five cards, making it a flush
      int suit_binary[4] = {0};
      // Dividing by 4 will get rid of the suit information, giving us the rank of the card
      suit_binary[a & 0x3] |= 1 << (a / 4);
      suit_binary[b & 0x3] |= 1 << (b / 4);
      suit_binary[c & 0x3] |= 1 << (c / 4);
      suit_binary[d & 0x3] |= 1 << (d / 4);
      suit_binary[e & 0x3] |= 1 << (e / 4);
      return flush[suit_binary[i]];
    }
  }

  unsigned char quinary[13] = {0};
  quinary[(a >> 2)]++;
  quinary[(b >> 2)]++;
  quinary[(c >> 2)]++;
  quinary[(d >> 2)]++;
  quinary[(e >> 2)]++;

  int hash = hash_quinary(quinary, 13, 5);

  return noflush5[hash];
}

In case you'd still like to know how the suits table generated, it is basically similar to the flush and noflush table, run the hash function, and assign the correct value to the corresponding slot...

Then we can take a look at the hash_quinary method, which used a table called dp[5][14][10]:

int hash_quinary(unsigned char q[], int len, int k)
{
  int sum = 0;

  for (int i=0; i<len; i++)
  {
    sum += dp[q[i]][len-i-1][k];

    k -= q[i];

    if (k <= 0)
    {
      break;
    }
  }

  return sum;
}

The table dp comes from dynamic programming calculation. I once provided the code in #1 and #2. Again, the algorithm behind that is in the third chapter of this documentation. If you find any part of the doc hard to understand, feel free to ask me.

Have fun coding!

hellraiserinchief commented 4 years ago

Wow!! This super helpful. Thanks for taking time out to explain this in such detail.

Mkerian10 commented 3 years ago

Have the implementation complete. Adding benchmarking and tests, as well as some other helper methods.

@HenryRLee would you be able to dump the eval values for each hand into a file for me? I don't want to mess with setting up the cpp build and this seemed like a decent way to do so. This way for testing it'll just compare the values to a file instead of setting up another known evaluator into the repo.

HenryRLee commented 3 years ago

Hi @Mkerian10, thanks for contributing.

Yes, having a language-independent test data makes sense. I'll generate such test data for 5, 6 and 7 card hands.

Mkerian10 commented 3 years ago

@HenryRLee glad to hear it.

For now I have eval5-9 implemented. If you want me to implement Omaha I could but I don't have an interest in it so I don't plan to currently.

Outside of that I want to expand some more evaluations of equity, Preflop equity vs. a full range is very simple to figure out. I'm looking at implementing more solutions but I'm sure you're more in touch with poker programming so if you know more about this stuff feel free to let me know.

HenryRLee commented 3 years ago

Hi @Mkerian10, I created a pull request, adding 5-card hands test data #29. You are welcome to review it.

I actually generated test data for 6 and 7 card hands, too. But the file sizes are too large to fit in Github. So it's likely I'll only provide a subset of them instead of providing all possible hands. But the format should be consistent.

Mkerian10 commented 3 years ago

Awesome, let me know when you merge it and I'll add the tests and can push my branch.

HenryRLee commented 3 years ago

Hi @Mkerian10, just added some random test data for 6-card and 7-card hands. Also merged the PR.

Looking forward to your changes.

Mkerian10 commented 3 years ago

I never forked the repo so can I just be listed as a contributor and I'll submit the pr. All tests are passing.

HenryRLee commented 3 years ago

Hi @Mkerian10. Actually I don't have control of deciding who can be on contributor list. Github manages that. But I am happy to help you to have you listed in the contributors list.

The easiest way is to create a pull request, I review the pull request, and then merge it. But usually you have to fork the repository in order to create a pull request. (You can delete the fork after the PR is merged anyway.)

There are other ways, though they are not easy. For example you can send me the patch and I apply the patch using your name and email address. And then there are a number of approaches: either I push these commits to the master branch directly, or I can push them to another branch, so that you can create a pull request. However, if I push the commits directly, you still need to have some interactions with this repo in order to have these commits counted towards your name, e.g. star the repo, fork the repo, or create a new issue (because Github doesn't want me to create a commit using Linus Torvalds' name, and then say Linus is a contributor of my repo).

You may find this official document useful. In particular, you need to follow the section that how to have your commits counted as your contribution.

Mkerian10 commented 3 years ago

For some reason I thought I needed permissions but my IntelliJ was being rude last night. Restarted and it fixed it.