dominicprice / endplay

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

Example of endplay code that executes card play which results in double dummy results #52

Open LGTrader opened 5 months ago

LGTrader commented 5 months ago

Sorry up front for opening an issue that I would normally ask in a forum so if there's a better way to approach this question please let me know.

I've been learning python and using endplay to possibly improve my duplicate bridge play. One example of using the library that I haven't found or figured out on my own is how to get endplay to show the card play for a specific hand and contract that matches a double dummy result.

Is there an example of this sort of code using endplay?

At a high level I understand that I could make random card choices and then use (I think) analyse_play or solve_board and look to minimize future tricks to the opponent but that feels a little heavy handed so I thought I'd ask before I start writing code.

Thanks for considering this question.

dominicprice commented 5 months ago

Hi LGTrader,

Here is a small example solution to your problem:

from endplay import *

d = Deal("N:AJ975.4.987.AQ54 K642.J53.KT52.73 T8.A976.J43.JT62 Q3.KQT82.AQ6.K98")

table = calc_dd_table(d)
#     ♣  ♦  ♥  ♠ NT
#  N  8  5  4  7  6
#  S  8  5  4  7  6
#  E  4  7  9  4  5
#  W  5  8  9  6  7

# let's see how west makes 9 tricks in hearts
d.first = Player.south
d.trump = Denom.hearts

d1 = d.copy()
sequence = []
while len(d1.curhand) > 0:
    # solve board returns (card, tricks) tuples in decreasing number of tricks, so the
    # first element is the optimal one. as we don't care about the number of tricks (
    # we already worked it out in the double dummy table) or any other cards we discard
    # all these values by assigning them to _
    (card, _), *_ = solve_board(d1)
    # add the optimal card to the play sequence
    sequence.append(card)
    # play the card so that the next iteration analyses the next player's move
    d1.play(card)
print(sequence)

# confirm it is a double dummy sequence
assert all(x == 9 for x in analyse_play(d, sequence))

If you have any questions feel free to let me know :). I'm afraid I only really get time on weekends to help out though!

LGTrader commented 5 months ago

Dominic, THANKS! Excellent starting example. I rearranged a couple of things to make the output more readable to me and then tested a number of contracts. They all work and they are all executing DD paths to completion to that's great.

Clearly endplay knows whats going on during play, but imagine that where I display the play of each trick that I also wanted to document who lead the first card and who won the trick. How do I gather that information?

I think it would be helpful to add more examples like this to the package.

And I totally understand only having time on weekends. I spent most of my life in that state!

from endplay import *
Watch_Play = False

d = Deal("N:AJ975.4.987.AQ54 K642.J53.KT52.73 T8.A976.J43.JT62 Q3.KQT82.AQ6.K98")
print(d)
print()

table = calc_dd_table(d)
#     ♣  ♦  ♥  ♠ NT
#  N  8  5  4  7  6
#  S  8  5  4  7  6
#  E  4  7  9  4  5
#  W  5  8  9  6  7

table.pprint()
print()

# let's see how west makes 9 tricks in hearts
d.first = Player.west
d.trump = Denom.hearts
dd_tricks = 8

d1 = d.copy()
sequence = []

while len(d1.curhand) > 0:
    # solve board returns (card, tricks) tuples in decreasing number of tricks, so the
    # first element is the optimal one. as we don't care about the number of tricks (
    # we already worked it out in the double dummy table) or any other cards we discard
    # all these values by assigning them to _
    (card, _), *_ = solve_board(d1)
    # add the optimal card to the play sequence
    sequence.append(card)
    if Watch_Play:
        print(d1.curplayer, ' ', d1.curtrick)
        print(card)
    # play the card so that the next iteration analyses the next player's move
    d1.play(card)
    #d1.pprint()

    #print()

# Print the original deal
print()
d.pprint()
print()

# Review the card play order
print()
print('Order the cards were played')
print()
for i in range(0,13):
    print(sequence[4*i + 0], sequence[4*i + 1], sequence[4*i + 2], sequence[4*i + 3])

# Show that deal d1 is now empty - all cards played
print()
d1.pprint()
print()

#print(sequence)
print()

# Demonstrate that all steps were optimum
print(analyse_play(d, sequence))

# confirm it is a double dummy sequence
assert all(x == dd_tricks for x in analyse_play(d, sequence))
dominicprice commented 5 months ago

Hi, No worries! The player on lead to the current trick is always stored in deal.first (the player who is next to play a card is stored in deal.curplayer). The player who won the previous trick will be the player who leads the next trick, so you can just check the value of deal.first after the last card to the trick is played. You could do, for example:

from dataclasses import dataclass

from endplay import *

@dataclass
class DoubleDummyTrick:
    cards: list[Card]
    first: Player
    winner: Player

    def __str__(self):
        res = []
        for i, player in enumerate(Player.iter_from(self.first)):
            prefix = "*" if player == self.winner else " "
            res += [f"{prefix}{player.abbr}: {self.cards[i]}"]
        return " ".join(res)

def get_double_dummy_line(deal: Deal, trump: Denom, declarer: Player):
    deal = deal.copy()  # copy so we don't mutate the input deal
    deal.trump = trump
    deal.first = declarer.lho  # first card is played by declarer's lho

    tricks: list[DoubleDummyTrick] = []
    for _ in range(13):
        # initialise trick with no cards and a default value for winner which will
        # be updated at the end
        trick = DoubleDummyTrick([], deal.first, Player.north)
        for _ in range(4):
            (card, _), *_ = solve_board(deal)
            trick.cards.append(card)
            deal.play(card)
        # the person on lead after the trick is played is the player who won the last trick
        trick.winner = deal.first
        tricks.append(trick)

    return tricks

d = Deal("N:AJ975.4.987.AQ54 K642.J53.KT52.73 T8.A976.J43.JT62 Q3.KQT82.AQ6.K98")
line = get_double_dummy_line(d, Denom.hearts, Player.west)
for trick in line:
    print(trick)

# Output:
# *N: ♠A  E: ♠2  S: ♠8  W: ♠3
#  N: ♠5 *E: ♠K  S: ♠T  W: ♠Q
#  E: ♦2  S: ♦3 *W: ♦Q  N: ♦7
#  W: ♦6  N: ♦8 *E: ♦K  S: ♦4
#  E: ♦5  S: ♦J *W: ♦A  N: ♦9
#  W: ♣8  N: ♣4  E: ♣3 *S: ♣T
#  S: ♣2  W: ♣9 *N: ♣Q  E: ♣7
#  N: ♣A *E: ♥3  S: ♣6  W: ♣K
#  E: ♥5 *S: ♥A  W: ♥2  N: ♥4
#  S: ♥6  W: ♥8  N: ♣5 *E: ♥J
#  E: ♠4  S: ♥7 *W: ♥T  N: ♠7
# *W: ♥Q  N: ♠9  E: ♠6  S: ♥9
# *W: ♥K  N: ♠J  E: ♦T  S: ♣J

Here I've made a function which calculates a double dummy line, the dataclass is just a little convenience class to store the results for each trick with a __str__ method so that it prints nicely, displaying the cards in the order they were played (so the first entry is by the player on lead) and an asterix before the player who wins that trick.

LGTrader commented 5 months ago

Thanks. That's very informative and addresses an additional question I had with

deal.first = declarer.lho

The example runs fine here so again I think this sort of thing would help newer users of the library if it's in the examples package.

My personal use is to hopefully become better at card play. I'm relatively new to duplicate bridge. My partner says my bidding is better than many of his long-term partners but my card play needs work.