dozingcat / CardsWithCats

Hearts and Spades card games
GNU General Public License v3.0
19 stars 2 forks source link

Enhancement to nil bidding #21

Closed gsiems closed 1 year ago

gsiems commented 1 year ago

I would like to propose an enhancement to the way the computer players bid nil, the goal being for the players to be more intentional about bidding nil.

The idea is to first determine if a nil bid would be a sane thing to do. If a player can reasonably expect to succeed in a nil bid then they should bid nil.

In testing the addition of the "can bid nil" functionality, 1,000 AI vs AI games were played where team 0 used the new functionality and team 1 did not.

Team 0 won 57.4% of the games and team 1 won 42.6% of the games.

The summary of the results are as follows:

Team Action Result Rounds
0 chose 1 failed to take tricks 71
0 chose 1 took tricks 270
0 chose nil failed to take nil 29
0 chose nil took nil 119
0 normal n/a 21755
0 normal nil failed to take nil 125
0 normal nil took nil 467
1 high spade nil failed to take nil 144
1 high spade nil took nil 161
1 normal n/a 21922
1 normal nil failed to take nil 127
1 normal nil took nil 482

Where the actions are defined as:

For the "chose 1" hands, the player enjoyed a 79% success rate so there was little penalty for bidding 1 in those instances. For the rounds where the player did not take any tricks their partner often took extra so the success rate for the team was even higher.

For the "chose nil" hands, the player enjoyed an 80% success rate.

For the "normal nil" hands, the player enjoyed an 79% success rate.

For the "high spade nil" hands, the success rate was only 52.8%.

From the above results it can be argued that:

  1. Refraining from bidding nil when a player has a jack and/or queen of spades is more successful (or at least less annoying) than bidding nil in those instances.

  2. Being more assertive about bidding nil, even when the hand could possibly take one or more tricks, is a more successful strategy than not doing so.

The proposed change involves adding a couple of functions to the lib/spades/spades_ai.dart file and updating the chooseBid function as follows:.

  bool _canBidNilSpades(List<Rank> ranks) {

    if (ranks.isEmpty) {
      return true;
    }

    if (ranks.length > 3) {
      return false;
    }

    if (ranks[0].index > Rank.ten.index) {
      return false;
    }

    return true;
  }

  bool _canBidNilNonSpades(List<Rank> ranks) {

    if (ranks.isEmpty) {
      return true;
    }

    if (ranks[0].index < Rank.ten.index) {
      return true;
    }

    int rankCount = ranks.length;

    /* The goal is to determine whether, even if the suit could potentially take
    one or more tricks, it is reasonably safe to bid nil anyhow. The thought
    being that gaining 100 points from a nil bid is better than only gaining 10
    or 20 points by not bidding nil. If a hand does have one or more high cards
    (Jack or higher) but there are enough low-enough rank cards in the suit to
    run the other players out of the suit before needing to play the high card(s)
    then bidding nil should be a safe, and even desirable, course of action. */

    // If there are any high cards then there needs to be at least 4 cards of the suit
    if (ranks[0].index > Rank.ten.index && rankCount < 4) {
      return false;
    }

    // If there are multiple high cards then there needs to be at least 5 cards of the suit
    if (rankCount > 1 && ranks[1].index > Rank.ten.index && rankCount < 5) {
      return false;
    }

    // If there is an ace then there needs to be at least 5 cards of the suit
    if (ranks[0].index == Rank.ace.index && rankCount < 5) {
      return false;
    }

    double rankSum = 0;
    for (int i = 0; i < rankCount; i++) {
      rankSum += ranks[i].index + 2;
    }
    double aveRank = rankSum / rankCount ;

    /* 8.0 seems to be a sweet spot. Setting the average too low and the player
    will almost never make an intentional nil bid and setting the average too
    high results in more instances of failing to realize the nil bid. */
    if (aveRank < 8.0) {
      return true;
    }

    return false;
  }

  int chooseBid(BidRequest req) {

    List<Rank> srSpades = sortedRanksInSuit(req.hand, Suit.spades);
    List<Rank> srHearts = sortedRanksInSuit(req.hand, Suit.hearts);
    List<Rank> srDiamonds = sortedRanksInSuit(req.hand, Suit.diamonds);
    List<Rank> srClubs = sortedRanksInSuit(req.hand, Suit.clubs);

    // Check first to see if the player can safely bid nil
    bool cbnS = _canBidNilSpades(srSpades);
    bool canBidNil = cbnS
      && _canBidNilNonSpades(srHearts)
      && _canBidNilNonSpades(srDiamonds)
      && _canBidNilNonSpades(srClubs);
    if ( canBidNil ) {
      return 0;
    }

    // Get the estimated tricks for each suit
    double estimatedTricks =  _estimatedTricksForSpades(srSpades)
      + _estimatedTricksForNonspades(srHearts)
      + _estimatedTricksForNonspades(srDiamonds)
      + _estimatedTricksForNonspades(srClubs);
    int bid = estimatedTricks.round();

    if (bid == 0) {
      // If it isn't considered safe to bid nil for spades then bid 1
      if (! cbnS) {
        return 1;
      }
      return 0;
    }

    // If this is the last bid and sum of bids is low, increase bid by up to 2.
    if (req.otherBids.length == req.rules.numPlayers - 1) {
      final sumOfBids = bid + req.otherBids.reduce((a, b) => a + b);
      int diff = req.rules.numberOfCardsPerPlayer - sumOfBids;
      if (diff > 2) {
        bid += (diff == 3) ? 1 : 2;
      }
    }
    return bid;
  }
dozingcat commented 1 year ago

This looks great, thanks! I should be able to add it for the next release. Or if you send a PR I'd be happy to merge it.

gsiems commented 1 year ago

Go ahead and just add it (I don't have any forks that are available for doing a pull from.)

dozingcat commented 1 year ago

(Finally) added in https://github.com/dozingcat/CardsWithCats/pull/26. Thanks again!