dominicprice / endplay

A suite of tools for generation and analysis of bridge deals. Read the documentation at https://endplay.readthedocs.io
MIT License
22 stars 5 forks source link

How to Count Distribution Points? #10

Open BSalita opened 2 years ago

BSalita commented 2 years ago

I'd like to count points for a hand's distribution. Default would use the traditional system (1,2,3 for doubleton, singleton, void). I don't see any existing method for doing so. Surely this is a common need. What am I missing? I created some functions as a work-around.

def dist_points_suit(suit, points=[3,2,1]+[0]*10):
    return points[len(suit)]

def dist_points(hand):
    return sum([dist_points_suit(suit) for suit in [hand.clubs, hand.diamonds, hand.hearts, hand.spades]])

def constraints(deal):
    points = hcp(deal.west)+dist_points(deal.west)-dist_points_suit(deal.west.spades)
    return points >= 13 and points <= 15 and len(deal.west.spades) <= 4 and len(deal.west.hearts) >= 5 and len(deal.west.diamonds) < 5 and len(deal.west.clubs) < 5

`

dominicprice commented 2 years ago

Agreed, this should be included in the evaluate module. I think your approach for shortage points is good, and then in the evaluate namespace there can be various definitions for different standard ways of calculating distribution, e.g something like:

dp_shortage = [3,2,1] + [0]*10 # Counting shortages
dp_length = [0]*4 + list(range(9)) # Counting long suits
dp_mixed = [s+l for s,l in zip(dp_shortage, dp_length)] # For calculating total points

def dist_points(o: Union[SuitHolding, Hand], scale):
    if isinstance(o, SuitHolding):
        return scale[len(o)]
    else:
        return sum(dist_points(o[s]) for s in Denom.suits())

def total_points(hand: Hand, hcp_scale, dist_scale):
    return hcp(hand, hcp_scale) + dist_points(hand, dist_scale)

Thoughts?

BSalita commented 2 years ago

Issues:

  1. My example mislead you. There's 14 possible suit lengths (0-13) so [3,2,1]+[0]11 and [0]5 + list(range(9)).
  2. Short suit distribution points are naively counted [3,2,1] before a trump fit is agreed upon and [5,3,1] after.
  3. Short suit dist points should not add in trump suit or arguably partner's long suit. Compensate in constraints() or pass an arg?
  4. Total points are naively hcp+dist_points but not so for K, QX, JX.
dominicprice commented 2 years ago
  1. Yes of course :P. Best in the actual package file to explicitly write out the list anyway.
  2. Can be fixed by shortage_points_trump and shortage_points_notrump scale?
  3. An optional exclude parameter which accepts a suit/list of suits would be good as this is common enough
  4. This one is harder and I have often had lengthy discussions with various partners about exactly how much these holdings are worth. It can often depend on the bidding too -- Qx in a suit partner has bid is more useful than a side suit. There is the endplay.evaluate.cccc function which is based on the original Kaplan evaluation (without the Rubens modification) method which takes into account all of these sorts of things and a myriad of other consideration too and in my opinion provides the most accurate calculation of hand strength, so perhaps the total_points function is not actually necessary over a simple estimation of how the high card points and distribution combine.
BSalita commented 2 years ago
  1. shortage_points_notrump perhaps should be named shortage_points_trump_fit?
  2. Provide multiple hand evaluation methods which make API use simpler; those commonly used (explainable), advanced (Kaplan), eventually neural net weights (probably most accurate), roll-your-own.
dominicprice commented 2 years ago

I'll make the names nice and unambiguous. I think for now, I will add in the dist_points function and a few common scales but definitely with the optional parameter so that a custom one can be defined. I'll put in the 'naïve' total points function for now but I'd like to keep the issue open to discuss a few different implementations as hand evaluation methods are definitely useful. I have a flight tomorrow so I'll put it together then and push it in the afternoon.

BSalita commented 2 years ago

All good. Bon voyage.

dominicprice commented 2 years ago

Implementation is pushed adding dist_points and total_points. Actions is running the test suite now and then (assuming all green) will push to pypi.

The compromise I have come up with for now is a protect_honours flag for total_points, which discounts honours which would drop if the opponents played a suit from the top, so any K, Qx, Jxx. This also includes the queen in e.g. KQ which is not ideal but is simpler to implement.

Another option is to have a exclude_holding parameter which contains a list of holdings which shouldn't count towards total points. The trouble with this is twofold: (a) some holdings may just be worth less, not 0 and (b) this requires some way to represent a generic small card. Its possible to add a Rank.Rx entry to the enum --- internally each suit holding is represented by a 16-bit integer where each bit represents a different rank, so there are three spare bits to play around with. However, this might cause some unintuitive behaviour. Another option would be to have an extra parameter smallest_significant_card or something like that, and the function would treat all cards below this as equal.

Either way, it might just be easier to define ones own my_total_points function on a per-usecase basis rather than have a standard total_points function which tries to solve every possible situation. I am happy to expand the evaluate module with more functions if you come up with any evaluation methods you think might be useful.