# Let's play Judgement!

In this challenge, you will be making a 3 bot to play the card game judgement!

Sandbox

# Rules

Judgement is a hand based game similar to games like rummy. In each round, 7 cards are dealt to each player, a trump suit is chosen (trump suits are chosen sequentially from the list Spades/Hearts/Diamonds/Clubs/No Trump), and based on these cards, the trump suit, and the play order, each player must make a guess as to how many hands they will win.

The first player to play in a particular hand can play any card they want. From then on, any subsequent player must play a card of the same suit as the first player if they have it, otherwise, they can play any card.

Once every player has played a card, the winner of the hand is determined as follows:

• If a trump card has been played, the highest trump card wins
• Otherwise, the highest card in the suit played by the first player wins

(As in most card games, the ordering is 2,3,4,5,6,7,8,9,10,J,Q,K,A)

The winner of a given hand starts the next hand. Once everyone's cards are depleted, each player counts the number of hands they have won. If it is equal to their guess, they earn 10 + the number of guessed hands. Otherwise, they earn nothing.

The winner of the game is the player with the highest scores after several rounds.

# Challenge

I've built the game controller here. Everything in this section is present in the link, but I'll include it here for completeness.

### A submission must include 5 parts:

"name"


A name for the bot.

def make_bet(self, turn_order, trump):
...
return hands_to_make


This function takes in turn_order, which is a list of the names of all bots in order of when they will play, and trump, which is a string representing the trump suit for the round. It must return hands_to_make, which is the number of hands the bot expects to win this round. self.handwill be automatically updated to contain your current hand.

play_card(self, current_board):
...
return card_played


This function takes in the list of cards played so far this hand in the form ("bot_name", Card), and returns card_played, which is the card from your hand to play.

update_round(self, current_scores, rounds_left):
...
return None


This function gets called right before every round (including at the very beginning for initializing you bot). It takes current_score which is a dictionary of bot_name: current_score, and rounds_left, which is the number of rounds left to be played.

update_play(self, final_board, winner):
...
return None


This function gets called right after each hand. final_board is the list of cards played that hand in the form ("bot_name", Card), and winner is a string indicating which bot won the hand.

The classes have the following specs

Card:

• self.suit accesses the suit (S,H,D, or C)
• self.rank accesses the rank (int from 2 -> 14)
• self.get_rank() accesses the rank in str form (2-10,J,Q,K,A)

Bot:

• self.hand accesses the hand (List[Card])
• self.name accesses the name of the bot. Alternatively, str(self) works as well.
• self.all_bets accesses a dictionary of bets placed by all bots.
• You can store other information you want to store by create self. class variables.

As a basic example, consider the following bot:

# Basic Bot

def basic_make_bet(self, turn_order, trump):
return len(self.hand)

def basic_play_card(self, current_board):
if current_board:
for card in self.hand:
if card.suit == current_board.suit:
return card
return self.hand

def basic_update_round(self, current_scores, rounds_left):
pass

def basic_update_play(self, final_board, winner):
pass

basic_bot = ("basic", basic_make_bet, basic_play_card, basic_update_round, basic_update_play)


The Colab link above contains an easy to use template for submissions, so I recommend using that! I will also try to update the Colab link with new bots as soon as they are posted.

# Requirements

• You can only use libraries available in Google Colab. These are the libraries present in the Anaconda distribution of Python 3.
• self.hand, the variable to access to see one's hand, must be treated as read only. Modifying the variable will mess up the game, so please avoid this.
• Bots must run in a reasonable amount of time. I'm not going to provide any strict bounds but I'll remove any serious offenders.
• The competition will be run June 6th, and the winner will receive a bounty + a check mark.
• To judge the bots, I'll pick random groupings of up to 7 bots and play the game thousands of times, and evaluate the average score of the bots per game.

# Current Averages

Each bot has played a total of 100 rounds. This table should be updated semi-regularly. (PlanBot is currently too slow to run 10000 rounds)

Bot Average
PlanBot 6.71
Conservative 6.30
BasicCardStrat 4.77
Half 4.28
Random 4.15
sans. 3.19
Basic 0.00
• Can we get access to other bots' bets? That's very important for many intelligent strategies. May 13 at 15:13
• @Spitemaster Fixed, check revised question/code. May 13 at 15:14
• @AaronMiller Thank you for the edit! Looks really nice. May 13 at 17:56
• I know a variant of this under the name "Estimation". In the variant, a different number of cards are dealt on each round. You get one point per trick plus a bonus of 10 if you correctly estimated the number of tricks that you would win.
– Neil
May 13 at 18:47
• @Neil I was actually planning on doing a different number of cards per round, but in Sandbox, this variant was complicated enough, so I left it. May 13 at 18:48

# Conservative Bot

This bot always bets 0, and tries its best to get rid of the biggest cards it has.


def conservative_make_bet(self, turn_order, trump):
self.trump = trump
return 0
def conservative_play_card(self,current_board):
suit_count = {s:len([c for c in self.hand if c.suit == s]) for s in ["S","H","D","C"]}
if current_board == []:
if all(card.suit == self.trump for card in self.hand):
return min(self.hand, key=lambda c: c.rank)
return min([c for c in self.hand if c.suit != self.trump], key = lambda c: c.rank)
trump_on_board = any(c.suit == self.trump for c in current_board)
valid_cards = [c for c in self.hand if c.suit == current_board.suit]
if valid_cards:
if trump_on_board:
return max(valid_cards, key = lambda c: c.rank)
winning_card = max([c for c in current_board if c.suit == current_board.suit], key = lambda c: c.rank)
losing_cards = [c for c in valid_cards if c.rank < winning_card.rank]
if losing_cards:
return max(losing_cards, key = lambda c: c.rank)
return min(valid_cards, key = lambda c: c.rank)
if trump_on_board:
winning_card = max([c for c in current_board if c.suit == self.trump], key = lambda c: c.rank)
small_trumps = [c for c in self.hand if c.rank < winning_card.rank and c.suit == self.trump]
if any(card.suit != self.trump for card in self.hand) and not small_trumps:
return max([c for c in self.hand if c.suit != self.trump], key = lambda c: c.rank * 8 + suit_count[c.suit])
if small_trumps:
return max(small_trumps, key = lambda c: c.rank)
return min(self.hand, key = lambda c: c.rank)
if all(card.suit == self.trump for card in self.hand):
return min(self.hand, key = lambda c: c.rank)
return max([c for c in self.hand if c.suit != self.trump], key = lambda c: c.rank * 8 + suit_count[c.suit])
def conservative_update_round(self, current_scores, rounds_left):
pass
def conservative_update_play(self, final_board, winner):
pass

conservative_bot = ("Conservative", conservative_make_bet,conservative_play_card,conservative_update_round,conservative_update_play)



# Half

def half_make_bet(self, _, trump):
self.trump = trump
self.wins = 0
self.bet = len([i for i in self.hand if i.rank > 8]) // 2
return self.bet

def half_play_card(self, current_board):
if not current_board:
return min(self.hand,key=lambda card: card.rank)
valid = [i for i in self.hand if i.suit == current_board.suit]
if valid:
return max(valid,key = lambda i: i.rank)
if self.wins > self.bet:
return self.hand
if self.bet - self.wins >= len([i for i in self.hand if i.rank > 8 or i.suit == self.trump]):
return max(self.hand, key = lambda i: i.rank + 1000 * (i.suit == self.trump))
return min(self.hand, key = lambda i: i.rank + 1000 * (i.suit == self.trump))

def half_update_round(self, a, b):
pass

def half_update_play(self, a, w):
self.wins += w == self.name

half_bot = ("Half", half_make_bet, half_play_card, half_update_round, half_update_play)

• @DonThousand "other than the template and Bot classes." That's exactly the problem. People shouldn't have to go digging around your source code to understand the problem. Write your problem spec like docs - what type is each function? What arguments does each function take? etc. May 13 at 12:49
• @DonThousand If only Python had interfaces... That's why JS is better for challenges like this - object literals are the way to go. Anyway, you should probably write the problem spec again including docs for the Bot and Card classes. May 13 at 12:59
• You don't update self.wins in your update_play function, so Half always tries to win. May 13 at 14:50

## PlanBot

Plans one step ahead and then simulates a random game. Tries to sabotage its opponents once it goes bust.

    import random

def valid_plays(hand, suit):
if suit is None:
return list(range(len(hand)))
follows_suit = [i for i, card in enumerate(hand) if card.suit == suit]
if follows_suit:
return follows_suit
return list(range(len(hand)))

def pick_random(hand, suit):
if suit is None:
return random.randrange(len(hand))
follows_suit = [i for i, card in enumerate(hand) if card.suit == suit]
if follows_suit:
return random.choice(follows_suit)
return random.randrange(len(hand))

def simulate_random_game(hands, trump, starting_player, current_trick):
tricks_won =  * len(hands)
current_trick = list(current_trick)
current_suit = current_trick.suit if current_trick else None
while hands[starting_player]:
i = 0
while len(current_trick) < len(hands):
player = (starting_player + i) % len(hands)
hand = hands[player]
card_index = pick_random(hand, current_suit)
card = hand[card_index]
current_suit = current_suit or card.suit
hand[card_index] = hand[-1]
hand.pop()
current_trick.append(card)
i += 1
winner_offset = max(enumerate(current_trick), key=lambda t: (t.suit == trump, t.suit == current_trick.suit, t.rank))
current_suit = None
starting_player = (starting_player + winner_offset) % len(hands)
tricks_won[starting_player] += 1
current_trick.clear()
return tricks_won

def dirichlet_score(counts):
sampled_counts = [random.gammavariate(n, 1) for n in counts]
return max(sum(i * n for i, n in enumerate(counts_prime)) / sum(counts_prime) for counts_prime in (counts, sampled_counts))

INITIAL_DECK = Deck().cards

def as_tuple(card):
return (card.suit, card.rank)

num_attempts = 4
order = sorted(range(len(hand_sizes)), key=lambda i: len(out[i]), reverse=True)
for attempt in range(num_attempts):
result = [[] for _ in hand_sizes]
failed = False
deck = INITIAL_DECK
for i in order:
o = out[i]
n = hand_sizes[i]
if not n:
result[i] = []
continue
deck = [card for card in deck if as_tuple(card) not in dead_cards]
if o and attempt != (num_attempts - 1):
possible_cards = [card for card in deck if card.suit not in o]
else:
possible_cards = deck
if len(possible_cards) < n:
failed = True
break
hand = random.sample(possible_cards, n)
result[i] = hand
dead_cards |= {as_tuple(card) for card in hand}
if failed:
continue
return result

def plan_make_bet(self, turn_order, trump):
scores = [0 for _ in range(8)]
self.tricks_won = {bot.name: 0 for bot in turn_order}
self.turn_order = [bot.name for bot in turn_order]
self.my_index = next(i for i, bot in enumerate(turn_order) if bot == self)
self.hand_sizes = {bot.name: 7 for bot in turn_order}
self.trump = trump
hand_sizes = [7 * (i != self.my_index) for i, _ in enumerate(turn_order)]
self.dead_cards = {as_tuple(card) for card in self.hand}
self.out = {bot.name: set() for bot in turn_order}
for _ in range(1500):
hands = deal([[] for _ in turn_order], self.dead_cards, hand_sizes)
hands[self.my_index] = list(self.hand)
tricks_won = simulate_random_game(hands, trump, 0, [])[self.my_index]
scores[tricks_won] += 1
bet = max(enumerate(scores), key=lambda t: (10 + t) * t)
self.bet = bet
return self.bet

def plan_play_card(self, current_trick):
hand_sizes = dict(self.hand_sizes)
for bot, card in current_trick:
hand_sizes[bot] -= 1
hand_sizes[self.name] = 0
current_suit = current_trick.suit if current_trick else None
for bot, card in current_trick[1:]:
if card.suit != current_suit:
out = [self.out[bot] for bot in self.turn_order]
plays = list(valid_plays(self.hand, current_suit))
bust = ((self.tricks_won[self.name] + len(self.hand)) < self.bet) or (self.tricks_won[self.name] > self.bet)
if len(plays) == 1:
return self.hand[plays]
if bust:
a = 1. / (len(self.turn_order) - 1)
outcomes = [[a] * (len(self.turn_order) - 1) for _ in plays]
else:
outcomes = [[1, 1] for _ in plays]
hand_sizes = [hand_sizes[bot] for bot in self.turn_order]
current_trick = [card for _, card in current_trick]
all_bets = {k.name: v for k, v in self.all_bets.items()}
for _ in range(1500):
current_trick_copy = list(current_trick)
my_hand = hands[self.my_index] = list(self.hand)
scores = [dirichlet_score(counts) for counts in outcomes]
chosen_play = scores.index(max(scores))
current_trick_copy.append(my_hand[chosen_play])
my_hand[chosen_play] = my_hand[-1]
my_hand.pop()
tricks_won = simulate_random_game(hands, self.trump, (self.my_index + 1) % len(hands), current_trick_copy)
if bust:
outcome = -(1 + sum(self.tricks_won[bot] + trick_count == all_bets[bot] for bot, trick_count in zip(self.turn_order, tricks_won)))
else:
outcome = (self.tricks_won[self.name] + tricks_won[self.my_index]) == self.bet
outcomes[chosen_play][outcome] += 1
best_play = max(enumerate(plays), key=lambda t: sum(outcomes[t]))
return self.hand[best_play]

def plan_update_round(self, current_scores, rounds_left):
pass

def plan_update_play(self, final_trick, winner):
for bot in self.hand_sizes:
self.hand_sizes[bot] -= 1
for bot, card in final_trick[1:]:
winner = max(final_trick, key=lambda t: (t.suit == self.trump, t.suit == final_trick.suit, t.rank))
self.tricks_won[winner] += 1

plan_bot = ("PlanBot", plan_make_bet, plan_play_card, plan_update_round, plan_update_play)

• Added! However, this bot is currently taking around 2 seconds a round, which is on the border of being too slow. If you could try to make some small optimizations, that would be much appreciated. May 16 at 17:24
• @Don_Thousand OK I'll try later. Are you using PyPy? May 17 at 8:38
• sadly, colab doesn't have it. May 17 at 13:52
• @user1502040 you could maybe try rewriting some bits in numpy to speed it up May 17 at 17:27

# sans.

sans has arrived to pass Judgement. However, being sans, he aims low and plays lazily.

def sans_make_bet(self,turn_order,trump):
return 1

def sans_play_card(self,current_board):
if current_board:
for card in self.hand:
if card.suit == current_board.suit:
return card
return self.hand[-1]

def sans_update_round(self, current_scores, rounds_left):
pass
def sans_update_play(self, final_board, winner):
pass

sans = ("sans.", sans_make_bet, sans_play_card, sans_update_round, sans_update_play)

• Added to the controller! May 13 at 20:34
• @DonThousand I think you forgot to add it to bot_info_list in the Main Program section of the controller. May 14 at 0:11
• I thought I did? Let me fix that May 14 at 0:59
• You should call this Crab May 14 at 4:28
• @Ausername Initially, I was going to call it crab, but I decided that crab is more about making things worse for everybody by picking low numbers, but this doesn't really affect anybody else. Plus, sans? Judgement? It's just perfect. May 14 at 12:14

# BasicCardStrat

def bcs_make_bet(self, turn_order, trump):
self.wins = 0
self.trump = trump
self.played = []
winners_by_suit = {"C": 14, "D": 14, "H": 14, "S": 14}
for card in self.hand:
if winners_by_suit[card.suit] == card.rank:
winners_by_suit[card.suit] -= 1
winner_count = 56 - winners_by_suit["C"] - winners_by_suit["D"] - winners_by_suit["H"] - winners_by_suit["S"]
threshold = 3 + len(turn_order)
face_count = sum(1 for x in self.hand if x.rank > threshold) - winner_count
trump_count = sum(1 for x in self.hand if x.rank <= threshold and x.suit == self.trump)
self.bet = int(winner_count * 0.75 + face_count * 0.33 + trump_count * 0.5)
return self.bet

def bcs_play_card(self, current_board):
# Check if we're winning more than projected
winners_by_suit = {"C": 14, "D": 14, "H": 14, "S": 14}
for card in self.hand:
if winners_by_suit[card.suit] == card.rank:
winners_by_suit[card.suit] -= 1
winner_count = 52 - winners_by_suit["C"] - winners_by_suit["D"] - winners_by_suit["H"] - winners_by_suit["S"]
face_count = sum(1 for x in self.hand if x.rank > 10) - winner_count
trump_count = sum(1 for x in self.hand if x.rank <= 10 and x.suit == self.trump)
ahead = int(winner_count * 0.75 + face_count * 0.33 + trump_count * 0.5) + self.wins - self.bet

# Determine which cards are guaranteed winners (barring trump)
winners_by_suit = {"C": 14, "D": 14, "H": 14, "S": 14}
played_by_suit = {"C": 0, "D": 0, "H": 0, "S": 0}
all_played = sorted(self.played, key=lambda x: -x.rank)
for card in all_played:
played_by_suit[card.suit] += 1
if winners_by_suit[card.suit] == card.rank:
winners_by_suit[card.suit] -= 1
if not current_board:
winners = [x for x in self.hand if winners_by_suit[x.suit] == x.rank]
if len(winners):
return next((x for x in winners if x.suit == self.trump), winners)
return max(self.hand, key=lambda x: x.rank - (15 if x.suit == self.trump else 0))
# Get rid of others' trump
trump_cards = [x for x in self.hand if x.suit == self.trump]
if trump_cards:
return min(trump_cards, key=lambda x: x.rank)
return min(self.hand, key=lambda x: x.rank + played_by_suit[x.suit])
valid = [x for x in self.hand if x.suit == lead_suit]
is_trumped = lead_suit != self.trump and any(x.suit == self.trump for x in current_board)
current_winner = max([x.rank for (_,x) in current_board if (x.suit == lead_suit and not is_trumped) or (x.suit == self.trump)])

# If I have more winners than tricks I want to take, try to slough one
if valid:
if is_trumped:
current_winner = 15
return max(valid, key=lambda x: -x.rank if x.rank > current_winner else x.rank)
if is_trumped:
trump_cards = [x for x in self.hand if x.suit == self.trump and x.rank < current_winner]
if trump_cards:
return max(trump_cards, key=lambda x: x.rank)
return max(self.hand, key=lambda x: x.rank - (15 if x.suit == self.trump else 0))

if not is_trumped:
if valid:
# If I want to win and I have a winner for this suit, play it.
winner = next((x for x in valid if winners_by_suit[x.suit] == x.rank), None)
if winner:
return winner
# Play my lowest winning card
winners = [x for x in valid if (x.rank > current_winner)]
if len(winners):
return min(winners, key=lambda x: x.rank)
return min(valid, key=lambda x: x.rank)
winners = [x for x in self.hand if x.suit == self.trump]
if len(winners):
return min(winners, key=lambda x: x.rank)
if is_trumped:
# Have to follow suit if possible
if len(valid):
return min(valid, key=lambda x: x.rank)
winners = [x for x in self.hand if x.rank > current_winner and x.suit == self.trump]
if len(winners):
return min(winners, key=lambda x: x.rank)

# Finally, play my worst card
if valid:
return min(valid, key=lambda x: x.rank)
return min(self.hand, key=lambda x: x.rank + (15 if x.suit == self.trump else 0))

def bcs_update_round(self, a, b):
pass

def bcs_update_play(self, final_board, winner):
self.played += [x for x in final_board]
if self.name == winner:
self.wins += 1

bcs_bot = ("BasicCardStrat", bcs_make_bet, bcs_play_card, bcs_update_round, bcs_update_play)


Some very simple card game strategy. Play winners, try to win with trump, slough when needed. Feel free to adapt this into other bots.

• if winner := next((x for x in self.hand if x.rank > current_winner and x.suit == self.trump), None): return min(winner, key=lambda x: x.rank) May 13 at 18:40
• What is the expected functionality here? winner is a Card, which is not iterable. May 13 at 18:41
• @DonThousand Oops, winner should be a list instead of getting the next one, so that it's sortable. I think I fixed all of them. May 13 at 18:56
• I've been working on fixing your code for a bit now, and it's still not working. How about you save a version of the controller locally, and fix up the code until it works? May 13 at 19:02
• @DonThousand Will do; it'll be a bit. May 13 at 19:05

# Random Bot

Simulates a random game and guesses the number of hands it wins in that random game. It always chooses a random card.

from random import sample

def make_random_bot():

def pick_random(hand, suit):
if suit is None:
return sample(hand, 1)
shuffled = sample(hand, len(hand))
for card in shuffled:
if card.suit == suit:
return card
return shuffled

def simulate_random_game(hands, trump):
hands = [[c for c in hand] for hand in hands]
hands_won =  * len(hands)

last_won = 0
for h in range(7):
last_suit = None
cards = []
for i in range(len(hands)):
player = (i + last_won) % len(hands)
card = pick_random(hands[player], last_suit)
last_suit = card.suit
hands[player].remove(card)
cards.append(card)
winner_i = max(range(i), key=lambda i: (cards[i].suit == trump, cards[i].suit == cards.suit, cards[i].rank))
last_won = (winner_i + last_won) % len(hands)
hands_won[last_won] += 1
return hands_won

def make_bet(self,turn_order,trump):
cards = list(set(Deck().cards) - set(self.hand))

other_cards = sample(cards, k=7*(len(turn_order) - 1))
hands = []
i = 0
for bot in turn_order:
if bot == self:
hands.append(self.hand)
else:
hands.append(other_cards[i * 7: (i+1)*7])
i += 1
result = simulate_random_game(hands, trump)

return result[turn_order.index(self)]

def play_card(self,current_board):

hand = sample(self.hand, k=len(self.hand))

if current_board:
for card in hand:
if card.suit == current_board.suit:
return card

return hand

def nop(self, a, b):
pass

return ('Random', make_bet, play_card, nop, nop)

random_bot = make_random_bot()
$$$$

• Added to controller! May 15 at 3:54
• @TheNumberOne I think you have a bug in simulate_random_game` where you reset the current suit after every card played instead of just at the start of the trick. May 16 at 10:48
• Thank you @user1502040 ^-^ May 17 at 16:45