11
\$\begingroup\$

This is a challenge to write bots to play the 1st and 2nd players in the following simple poker game.

Rules of the poker game

There are two players, A and B. Each antes $10 into the pot, and is dealt a card, which is a real number in the range [0, 1).

Player A goes first, and may pass or bet. If A passes, then there is a showdown; the cards are revealed, and whichever player had the higher card wins the pot.

If A bets, A chooses an amount \$b\$ to bet. \$b\$ must be an integer multiple of $1, in the range [$1, $50], and no greater than the amount of money A has at the time.

EDIT (19 Aug 2020): Moreover, \$b\$ must be no greater than the amount of money B has at the time, to enable B to go all-in to call, if B wants to.

A adds \$b\$ to the pot.

Then B may fold or call.

If B folds, A wins the pot with no showdown.

If B calls, B adds \$b\$ to the pot, and there is a showdown.

EDIT (19 Aug 2020) Note that B will always have enough money to call, as A is not allowed to bet so much that B would not have enough.

Rules of the tournament, matches and sessions

The bots which are the entries to this contest will compete in an all-play-all tournament consisting of matches. Every pair of entries go head to head in a match.

Each match has two contestants (call them X and Y). Each match consists of \$n\$ sessions, where \$n\$ is a number I will choose, depending on how many entries there are and how much time I feel like devoting to running the engine.

At the start of each session, the tournament controller gives each contestant $100. There then follow a series of games. The games in each match alternate games where X's A-bot plays Y's B-bot, and games where Y's A-bot plays X's B-bot. Each session will continue until either 50 games in the session have been played, or one contestant no longer has enough money to start a further game (specifically, to put a $10 ante into the pot).

Where a session contained \$g\$ games, and the winner gained an amount \$m\$ of money, that winner is awarded \$m/\sqrt{g}\$ points, and the loser loses the same amount of points. (The amount of points is higher, the lower \$g\$ is, so as to reward bots that consistently beat their opponents and thus win their opponent's entire stack quickly. However, I don't want very quick sessions to dominate the scoring too much, so I divide only by \$\sqrt{g}\$ and not by \$g\$.)

The winning bot is the one who won most points over the course of all the matches it played in the tournament (as described in the previous paragraph).

Interfaces of procedures in an entry

An entry should contain C procedures which have the following prototypes:

int a(const Bot *bot);
int b(const Bot *bot, const int aBet);

where types are defined as follows:

typedef float Card;
typedef long Money;
typedef Money (*AProcType)(const void* bot);
typedef int (*BProcType)(const void* bot, const Money aBet);

typedef struct Bot
{
    AProcType a;
    BProcType b;
    Card    card, opponentsPreviousCard;
    Money   money, opponentsMoney;
    float   f[50]; // scratch area for bots to use as they will
} Bot;

Where bot points to an entrant's bot, just before bot->a or bot->b is called, the card dealt to that bot and the amount of money it has are assigned to bot->card and bot->money.

If a game ended in a showdown, then, afterwards, each bot's card is assigned to the other bot's bot->opponentsPreviousCard. By contrast, if the game ended with one player folding, then the controller does not reveal the cards: instead, a negative value is assigned to bot->opponentsPreviousCard.

In my sandbox proposal for this KotH, I asked whether or not the controller should unconditionally reveal both cards to both bots. It got a comment that in online poker "the winner has the choice whether they show or hide their cards". Seeing as a bot can't possibly do worse by hiding its card than by revealing it, I have opted instead to never reveal the cards dealt in a game where one player folded.

The array f is provided to enable a bot to maintain state between games.

In a game where the bot bot is the A-player, the controller will call the function bot->a(bot).

0. <= bot->card < 1.0. a must return the amount (in $) the bot is to bet. If a returns 0 or a negative value, that means the bot will pass. Otherwise, the bot will bet the value returned by a, $50, or all the player's money, whichever is the smallest.

In a game where the bot bot is the B-player, the controller will call the function bot->b(bot, aBet) where the A-player has just bet an amount $aBet.

0. <= bot->card < 1.0. The controller calls bot->b only if both the following conditions are true:

  • aBet > 0 because if A had passed, B does not get to act.
  • bot->money >= aBet because, if A had bet but B could not afford to call, B must fold.

bot->b must return 0 if the bot is to fold, and any other value if the bot is to call.

X and Y will never be the same entry. So, even if you think each of your bots would be able to tell if its match-opponent is your other bot... it won't be.

My sandbox proposal for this KotH expressed the game in terms of dealing cards from a pack. In such a game, if cards were not returned to the pack, the value of each card would change depending on how many cards above it and below it had not yet been seen, which would depend on the play. The proposal got a comment that cards are returned to the pack after each round. But in that case the above effect does not occur. So the cards might as well be independent variates from the uniform distribution on the interval [0, 1).

Each entry's tournament score will be the sum of its match-scores. [Note that each entry is pitted against every other entry, so all entries play equal numbers of matches.]

Loopholes are forbidden, as is trying to cheat. No bot may try to read or write or tamper with anything external to it, including the controller or other bots. However, calls to rand (in reasonable quantities) are allowed.

EDIT Tue 11 Aug 20 to clarify that using rand is allowed, and to give direct read-access to the amount of the opponent's money.

The following is a controller, provided just so that entrants can test their bots. My actual controller might contain additional code as required.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>

// Return codes from playVonNeumannPokerGame
#define G_FOLD  0
#define G_SHOWDOWN  1
#define G_MAYNOTBOTHPLAY    2

#define ANTE    10
#define BET_LIMIT   50
#define INIT_STACK  100

typedef float Point, Card;
typedef long Index, Money, Stat;
typedef Money (*AProcType)(const void* bot);
typedef int (*BProcType)(const void* bot, const Money aBet);

typedef struct Bot
{
    AProcType a;
    BProcType b;
    Card    card, opponentsPreviousCard;
    Money   money;
    float   f[50]; // scratch area for bots to use as they will
} Bot;

#define GAME_NAME_MAX   31

typedef struct Entrant
{
    Bot *bot;
    char    name[GAME_NAME_MAX+1];
    Point   vp;
    Money   mny;
} Entrant, *PEntrant;

long    nEntrants;
Entrant *plr;

#define NSESSIONSPERMATCH 500
#define MAXNGAMESPERSESSION 50
unsigned long nGamesInTotal, prngSeed;

static void playVonNeumannPokerTournament();
static void playVonNeumannPokerMatch(PEntrant c1, PEntrant c2);
static long playVonNeumannPokerGame(PEntrant a, PEntrant b);
static void initBots();
static void tournament2Init(long nPlayers);
static void tournament2MatchPlayers(long *pi1, long *pi2);
static float fRand();
static int cmpByVP(const Entrant* e1, const Entrant* e2);

// <nEntrants> <seed>
int main(int argc, char** argv)
{
    sscanf_s(argv[1], "%ul", &nEntrants); // for public engine
    sscanf_s(argv[2], "%ul", &prngSeed);
    srand(prngSeed);
    playVonNeumannPokerTournament();
} // main

static void playVonNeumannPokerTournament()
{
    long    pi, pj;
    PEntrant    e;

    nGamesInTotal = 0;
    //nEntrants = sizeof(aProc)/sizeof(aProc[0]); // works only if engine includes bot data
    plr = (PEntrant)calloc(nEntrants, sizeof(Entrant));
    for(pi=0; pi<nEntrants; ++pi) // Initialise the entrants
    {
        e   = &plr[pi];
        e->vp   = 0;
    }
    initBots(); // Connect each entrant to its bot
    for(pj=1; pj<nEntrants; ++pj) // all-play-all tournament
        for(pi=0; pi<pj; ++pi)
            playVonNeumannPokerMatch(&plr[pi], &plr[pj]);
} // playVonNeumannPokerTournament

static void playVonNeumannPokerMatch(PEntrant c1, PEntrant c2)
{
    long    si, mgi=0, sgi, r;
    Point   win1, win2;

    c1->bot->opponentsPreviousCard = -1.0;
    c2->bot->opponentsPreviousCard = -1.0;

    for(si=0; si<NSESSIONSPERMATCH; ++si)
    {
        c1->mny = INIT_STACK;
        c2->mny = INIT_STACK;
        for(sgi=0; sgi<MAXNGAMESPERSESSION; ++sgi)
        {
            if(mgi&1) // c1 & c2 swap roles in the match's every game
                r   = playVonNeumannPokerGame(c2, c1); // c2 is A; c1 is B
            else // even-numbered game
                r   = playVonNeumannPokerGame(c1, c2); // c1 is A; c2 is B
            ++mgi;
            if(r==G_MAYNOTBOTHPLAY)
                break; // one player can't afford to continue the session
            if(r==G_SHOWDOWN)
            {
                c1->bot->opponentsPreviousCard = c2->bot->card;
                c2->bot->opponentsPreviousCard = c1->bot->card;
            }
            else
            {
                c1->bot->opponentsPreviousCard = -1.0;
                c2->bot->opponentsPreviousCard = -1.0;
            }
        }
        win1    = (c1->mny - INIT_STACK +0.0)/sqrt(sgi); // sgi must > 0. Take sqrt so as not to over-reward quick wins
        win2    = (c2->mny - INIT_STACK +0.0)/sqrt(sgi);
        c1->vp += win1;
        c2->vp += win2;
    } // for each session in the match
} // playVonNeumannPokerMatch

static long playVonNeumannPokerGame(PEntrant a, PEntrant b)
{
    _Bool   bCalls;
    Card    ax, bx;
    Money   aBet;
    long    r=G_SHOWDOWN;

    // Unless each of the game's players can afford their ante, they cannot play a game.
    if(a->mny < ANTE || b->mny < ANTE)
        return G_MAYNOTBOTHPLAY; // players may not both play
    a->bot->card = ax = fRand();
    b->bot->card = bx = fRand();
    a->bot->money = b->bot->opponentsMoney = a->mny;
    b->bot->money = a->bot->opponentsMoney = b->mny;
    // Call A's bot to find out how much money A wants to bet.
    aBet    = a->bot->a(a->bot);
    // But A may not bet more money than A has, nor yet more than the bet-limit
    aBet    = aBet < 0 ? 0 : a->mny < aBet ? a->mny : aBet;
    aBet    = aBet > BET_LIMIT ? BET_LIMIT : aBet;
    // EDIT 19 Aug 2020: A may not bet more money than B has.
    aBet    = aBet > b->mny ? b->mny : aBet;
    // [If B cannot afford to call, B must fold; there is no need to call B's bot in such a case. Otherwise,] call B's bot to find B's reply (fold or call)
    // Treat A passing as A betting 0 and B calling
    bCalls  = aBet < 1 ? 1 : b->mny < aBet ? 0 : b->bot->b(b->bot, aBet);
    if(!bCalls) // B folds
    {
        a->mny  += ANTE;
        b->mny  -= ANTE;
        r   = G_FOLD;
    }
    else if(ax>bx) // B calls A's bet; A wins the showdown
    {
        a->mny  += ANTE+aBet;
        b->mny  -= ANTE+aBet;
    }
    else // B calls A's bet; B wins the showdown
    {
        a->mny  -= ANTE+aBet;
        b->mny  += ANTE+aBet;
    }
    return r;
} // playVonNeumannPokerGame

/*#############################################################################
    Bots

    This section is subject to change, and has my copies of user-submitted code for bots' a- and b-procedures
###############################################################################
*/

// This bot is so naive, it never bluffs.
static Money naiveA(const Bot *bot)
{
    Card x=bot->card;

    return 50.*x-25.;
}

static int naiveB(const Bot *bot, const Money aBet)
{
    return bot->card>.5;
}

// This bot treats it like 3-card Kuhn poker
static Money kuhn3A(const Bot *bot)
{
    Card x=bot->card;
    Money m=bot->money;
    Money bet = 10;

    if(m<bet)
        bet = m;
    return 9.*x<1. || 3.*x>2. ? bet : 0;
}

static int kuhn3B(const Bot *bot, const Money aBet)
{
    return bot->money>=aBet && 9.*bot->card>5.;
}
typedef char *String;
static String botName[] = {"naive", "Kuhn3"};
static AProcType aProc[] = {naiveA, kuhn3A};
static BProcType bProc[] = {naiveB, kuhn3B};

static void initBots()
{
    Bot *pBot;
    long    i, j;

    for(i=0; i<nEntrants; ++i)
    {
        pBot = (Bot*)calloc(1, sizeof(Bot));
        pBot->a = aProc[i];
        pBot->b = bProc[i];
        for(j=0; j<50; ++j)
            pBot->f[j] = 0.0;
        plr[i].bot  = pBot;
        strncpy_s(plr[i].name, GAME_NAME_MAX+1, botName[i], GAME_NAME_MAX);
    }
} // initBots

static float fRand()
{
    float   r = rand();
    return r / RAND_MAX;
}

static int cmpByVP(const Entrant* e1, const Entrant* e2)
{
    return e2->vp > e1->vp ? 1 : -1; // map from floats to int +-1
}
\$\endgroup\$
  • \$\begingroup\$ Given that calls to rand modify the external state, is that disallowed? Also this doesn't give any way of accessing the opponents current money, but there is enough information to deduce it. Is this intentional? \$\endgroup\$ – rtpax Aug 10 at 18:34
  • 3
    \$\begingroup\$ I'll allow calls to rand (in reasonable quantities ;) ). And I'll provide a way to see how much money the opponent has. \$\endgroup\$ – Rosie F Aug 11 at 4:16
  • 4
    \$\begingroup\$ If B has $49 or less and A has $50 or more, A's only strategy is to bet $50 because B must fold. This should not be how it is - A's bet should be capped to whatever B has so that B has the options "fold" and "go all in". \$\endgroup\$ – Spitemaster Aug 11 at 20:36
3
\$\begingroup\$

LikeMe Bot

Here's a simple bot. It mostly just assumes that the other bot bets roughly like it does.

int likemea(const Bot *bot){
    // Always go big if we can't play again if we lose.
    if (bot->money < 10) return bot->money;
    // Force an all-in if there's a decent change we win.
    if (bot->card > 0.5 && bot->opponentsMoney <= 50) return bot->opponentsMoney;
    float max_pass = 0.5;
    float min_max_bet = 0.9;
    // Increase risk tolerance when in the lead.
    float lead = bot->money / (bot->opponentsMoney + 20);
    if (lead > 1){
        // Don't go crazy.
        lead = lead / 2 + 1;
        if (lead > 1.5) lead = 1.5;
        max_pass /= lead;
        min_max_bet /= lead;
    }
    if (bot->card < max_pass) return 0;
    if (bot->card > min_max_bet) return 50;
    return (int)((bot->card - max_pass) / (min_max_bet - max_pass) * 50);
}

int likemeb(const Bot *bot, const int aBet){
    // Get what I would have bet if I was a.
    int my_bet = likemea(bot);
    if (bot->money < 50){
        // If I'm being pushed all-in, assume the other bot is playing riskier.
        my_bet = (int)(my_bet * 1.2);
    }
    if (my_bet >= aBet) return aBet;
    return 0;
}

Please have mercy on my C. It's been a while, and I've never done much C anyway.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you for your comment to the question. I think you are right, and have accordingly added a rule. I'm pinging you here in case you'd like to amend your answer. \$\endgroup\$ – Rosie F Aug 19 at 7:46
  • \$\begingroup\$ @RosieF Thanks! I think it's still the right choice in this case, but others may not feel that way. :) \$\endgroup\$ – Spitemaster Aug 19 at 14:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.