6
\$\begingroup\$

Build a function in python that can win a Sevens game provided that there are only 2 players


Here are the instructions from Wikipedia:

All cards are dealt to the players, even if as a result some players have one card more than others. The owner of the seven of hearts begins by playing it. Similarly, the other three sevens may later be played as the first cards of their respective suits. After that, cards may be added in sequence down to the ace and up to the king. A player who cannot place a card passes.

You cannot pass if you have a card to play.

The one who gets the seven of hearts is the first to play.

The cards will be randomly distributed to the user and the computer.

The function will have two parameter:

  1. List of cards remaining to play for computer
  2. List cards that have been played

The syntax of a card is like this:

[value, suit]

For example [king, "clubs"]


The list of suits are:

  • clubs
  • spades
  • diamonds
  • hearts

The list of values are:

  • 1
  • 2
  • 3 ....
  • 11(jack)
  • 12(queen)
  • 13(king)

The remaining cards for player 1 will be stored in list named card_1_rem and the remaining cards for player 2 will be stored in list named card_2_rem

The cards that have been played will be stored in a list called played_cards


The function will have to append the played card to the list called played_cards and subtract the item for the list called card_1_rem or card_2_rem

The return of one function will be the input of the competitor

The one who finishes his cards first, wins.

Winning Condition


Step 1: Users will submit their programs.

Step 2: I will test the programs against each other.

Step 3: The first program to remain in first place (as measured by lowest total running score) for 50 consecutive games will be the winner. This will help smooth out the effects of luck. However, a maximum of 10000 games will be played, and if no player meets the first criterion, then the player with the lowest total score after 10000 games wins.

Step 4: All the results will be uploaded to a github repo(which I will soon make).


The loser has to count his points as follows

Ace : 1
2 : 2
3 : 3
4 : 4
5 : 5
6 : 6
7 : 7
8 : 8
9 : 9
10 : 10
jack : 10
queen : 10
king : 10

For example if a king and an ace remain in the loser's hand, he gets 11 point.

The objective is to minimize the points you have got.

Winner gets 0 points

IMP : Submissions are allowed only in the Python 3 language.

The controller code of this challenge is here(major update has been done): https://gist.github.com/Salil03/19a093554205b52d05dc7dc55992375a

\$\endgroup\$
  • \$\begingroup\$ will you play the programs against each other until a winner is clear to a statistically significant level? \$\endgroup\$ – Jonah Jul 12 '18 at 2:38
  • \$\begingroup\$ @Agile_Eagle I couldn't say without knowing the variance of that game. but if you graph the results over time you should be able to see the convergence. eg, say there are 5 programs competing. give each one a different color. let the y-axis be a competitors place, and let x be the number of rounds. at the beginning, the lines will jump up and down and intersect each other. eventually, all the lines will be parallel and stay that way. once that state is achieved, you'll know who the real winner is. \$\endgroup\$ – Jonah Jul 12 '18 at 4:19
  • 1
    \$\begingroup\$ Let us continue this discussion in chat. \$\endgroup\$ – Agile_Eagle Jul 12 '18 at 4:20
  • 3
    \$\begingroup\$ pagat.com/layout/sevens.html (For anyone else that doesn't know the game and needs more detailed explanation.) \$\endgroup\$ – sundar Jul 12 '18 at 10:50
  • 1
    \$\begingroup\$ The bots should ideally just return the card played; it should be the responsibility of the controller to check that the intended move is vaild and make the appropriate changes to the game state. \$\endgroup\$ – user1502040 Jul 13 '18 at 23:53
2
+50
\$\begingroup\$

Tactical

This ended up different enough that I felt it deserved a separate entry. This one calculates slightly smarter scores, looking not just at the next step but future choices for each player as well, based on the cards they hold. Seems to do a lot better than the "synergistic" version, better enough to beat the mysterious player2 advantage.

def tactical(cards_in_hand, played_cards):
    def list2dict(lst):
        d = {}
        for val, suit in lst:
            if suit in d:
                d[suit].append(val)
            else:
                d[suit] = [val]
        return d
    def play_card(card):
        cards_in_hand.remove(card)
        played_cards.append(card)

    hand = list2dict(cards_in_hand)
    if not played_cards:
        if 7 in hand['hearts']:
            play_card([7, 'hearts'])
        return (cards_in_hand, played_cards)
    table = list2dict(played_cards)

    playable_cards = {}
    for suit in hand:

        if suit not in table:
            if 7 in hand[suit]:
                # Do I hold the majority of the cards of this suit?
                suit_advantage = (len(hand[suit]) - 6.5)
                playable_cards[(7, suit)] = suit_advantage * 20
                if 6 in hand[suit] and 8 in hand[suit]:
                    # opponent can't immediately make use of this 
                    playable_cards[(7, suit)] += 20
            continue

        visible = set(table[suit] + hand[suit])
        opp_hand = set(range(1,14)) - visible

        highcard = max(table[suit]) + 1
        if highcard in hand[suit]:
            advantage = sum(c > highcard for c in hand[suit]) - sum(c > highcard for c in opp_hand)
            playable_cards[(highcard, suit)] = advantage * 10
            if highcard + 1 in opp_hand:
                playable_cards[(highcard, suit)] -= 20

        lowcard = min(table[suit]) - 1
        if lowcard in hand[suit]:
            advantage = sum(c < lowcard for c in hand[suit]) - sum(c < lowcard for c in opp_hand)
            playable_cards[(lowcard, suit)] = advantage * 10
            if lowcard - 1 in opp_hand:
                playable_cards[(lowcard, suit)] -= 20

    if not playable_cards:
        return (cards_in_hand, played_cards)

    best_card = max(playable_cards, key=playable_cards.get)
    #print(hand, "\n", table, "\n", best_card, ":", playable_cards[best_card])
    play_card(list(best_card))

    return (cards_in_hand, played_cards)
\$\endgroup\$
  • \$\begingroup\$ Thanks for the accept and the bounty, Agile_Eagle. Though, I still hope @heather or someone will post another competitive entry (and I'll happily transfer the bounty over if they win!). \$\endgroup\$ – sundar Jul 14 '18 at 19:38
  • \$\begingroup\$ I am ending this question now. keep the bounty for yourself \$\endgroup\$ – Agile_Eagle Jul 17 '18 at 17:40
2
\$\begingroup\$

Synergistic

I'm not really a Python guy, but wanted to give this a go. This builds the set of playable cards at each turn, and assigns each of them a simple static score. The card with the highest score is played (assuming any playable card exists).

def synergistic(cards_in_hand, played_cards):
    def list2dict(lst):
        d = {}
        for val, suit in lst:
            if suit in d:
                d[suit].append(val)
            else:
                d[suit] = [val]
        return d
    def play_card(card):
        cards_in_hand.remove(card)
        played_cards.append(card)

    hand = list2dict(cards_in_hand)
    if not played_cards:
        if 7 in hand['hearts']:
            play_card([7, 'hearts'])
        return (cards_in_hand, played_cards)
    table = list2dict(played_cards)

    playable_cards = {}
    for suit in hand:
        if 7 in hand[suit]:
            playable_cards[(7, suit)] = -1

        if suit not in table:
            continue
        visible = set(table[suit] + hand[suit])
        opp_hand = set(range(1,14)) - visible
        highcard = max(table[suit]) + 1

        if highcard in hand[suit]:
            if highcard+1 in opp_hand:
                playable_cards[(highcard, suit)] = 1
            else:
                playable_cards[(highcard, suit)] = 2

        lowcard = min(table[suit]) - 1
        if lowcard in hand[suit]:
            if lowcard - 1 in opp_hand:
                playable_cards[(lowcard, suit)] = 0
            else:
                playable_cards[(lowcard, suit)] = 1


    if not playable_cards:
        return (cards_in_hand, played_cards)

    best_card = list(max(playable_cards, key=playable_cards.get))
    #print(hand, "\n", table, "\n", best_card)
    play_card(best_card)

    return (cards_in_hand, played_cards)

By the way, the controller seemed to have several issues, including in score calculation and comparison. I made some changes to the controller here, please take a look and update your version if this seems right.

Two things I haven't fixed in the controller:

  • why is the loop condition (win2 <= 50) and (win1 <= 100) ? This should probably be symmetrical, it should exit the loop whenever either of the players has 100 consecutive wins.

  • trying some runs of the controller locally, with the same function for both players, Player 2 seems to win most of the time - it can't be inherent to the game since the initial 7H requirement would smooth that out (as @Veskah mentioned in the comments), so, yet undetected controller bugs? Or my player code somehow maintaining state and having a bias this way? Per-game, it's not like Player 2 dominates heavily (from the results output txt), but somehow the overall score per controller run ends up favouring player 2 much more than random (Player 1's total scores are often more than 2x that of Player 2).

\$\endgroup\$
  • \$\begingroup\$ Picking who is "first" or "second" player in a round should be inherently random based on who holds 7H so bot order shouldn't matter because you either play 7H or pass on Turn 1. If the 2nd Function consistently wins, seems like an RNG issue \$\endgroup\$ – Veskah Jul 13 '18 at 1:20
  • \$\begingroup\$ I just discovered a bug regarding that (my code uses any 7 card it has, instead of passing if it doesn't have 7H), so the problem might go away if I fix that. \$\endgroup\$ – sundar Jul 13 '18 at 1:42
  • \$\begingroup\$ @sundar why are you returning (cards_in_hand, played_cards) ? You just have to append it to a list and subtract it from a list \$\endgroup\$ – Agile_Eagle Jul 13 '18 at 10:52
  • \$\begingroup\$ @sundar I modified my controller take a look at it. Thanks!! \$\endgroup\$ – Agile_Eagle Jul 13 '18 at 10:53
  • 1
    \$\begingroup\$ @sundar cool. I am going to test your programs against each other soon. \$\endgroup\$ – Agile_Eagle Jul 13 '18 at 15:23
2
\$\begingroup\$

SearchBot

import random

suits = ["clubs", "diamonds", "hearts", "spades"]
suit_mul = 14
hearts = suit_mul * suits.index("hearts")

def evaluate(hand):
    return sum(min(c % suit_mul, 10) for c in hand)

def rollout(hand0, hand1, runs):
    sign = -1
    counts = [[0.] * 8 for _ in range(2)]
    def counts_index(card):
        return 2 * (card // suit_mul) + ((card % suit_mul) > 7)
    for card in hand0:
        counts[0][counts_index(card)] += 1
    for card in hand1:
        counts[1][counts_index(card)] += 1
    while True:
        if not hand1:
            return sign * evaluate(hand0)
        can_play = []
        for i, run in enumerate(runs):
            if run[0] == 8 or run[1] == 6:
                if run[1] != 6:
                    run[0] = 7
                if run[0] != 8:
                    run[1] = 7
            suit = suit_mul * i
            rank = run[0] - 1
            next_low = suit + rank
            if next_low in hand0:
                if next_low - 1 in hand0:
                    runs[i][0] -= 1
                    hand0.remove(next_low)
                    counts[0][counts_index(next_low)] -= 1
                    can_play = []
                    break
                can_play.append((next_low, 0, -1))
            rank = run[1] + 1
            next_high = suit + rank
            if next_high in hand0:
                if next_high + 1 in hand0:
                    runs[i][1] += 1
                    hand0.remove(next_high)
                    counts[0][counts_index(next_high)] -= 1
                    can_play = []
                    break
                can_play.append((next_high, 1, 1))
        if can_play:
            weights = [(a - 1) / (a + b - 1) if a + b - 1 > 0 else 0 for a, b in zip(*counts)]
            weighted = [(0 if t[0] % suit_mul == 7 else weights[counts_index(t[0])], t) for t in can_play]
            weight = sum(t[0] for t in weighted)
            total = random.uniform(0, weight)
            for (w, (card, index, direction)) in weighted:
                total -= w
                if total <= 0:
                    break
            hand0.remove(card)
            counts[0][counts_index(card)] -= 1
            runs[card // suit_mul][index] += direction
        hand0, hand1 = hand1, hand0
        counts[0], counts[1] = counts[1], counts[0]
        sign *= -1

def select_move(hand0, hand1, runs, n=40):
    if hearts + 7 in hand0:
        return hearts + 7
    if hearts + 7 in hand1:
        return
    can_play = []
    for i, run in enumerate(runs):
        suit = suit_mul * i
        rank = run[0] - 1
        next_low = suit + rank
        if next_low in hand0:
            if next_low - 1 in hand0:
                return next_low
            can_play.append((next_low, 0, -1))
        rank = run[1] + 1
        next_high = suit + rank
        if next_high in hand0:
            if next_high + 1 in hand0:
                return next_high
            can_play.append((next_high, 1, 1))
    if not can_play:
        return
    if len(can_play) == 1:
        return can_play[0][0]
    scores = [0 for _ in can_play]
    for i, (card, index, sign) in enumerate(can_play):
        hand0_copy = set(hand0)
        runs_copy = [list(r) for r in runs]
        hand0_copy.remove(card)
        runs_copy[card // suit_mul][index] += sign
        for j in range(n):
            scores[i] -= rollout(set(hand1), set(hand0_copy), [list(r) for r in runs_copy])
    return can_play[scores.index(max(scores))][0]


def search(cards_in_hand, played_cards):

    def play_card(c):
        if c is None:
            return
        suit = suits[c // suit_mul]
        rank = c % suit_mul
        for i, card in enumerate(cards_in_hand):
            if card[0] == rank and card[1] == suit:
                del cards_in_hand[i]
                played_cards.append([rank, suit])
                return
        assert(False)

    hand = set(suit_mul * suits.index(s) + v for v, s in cards_in_hand)
    played = set(suit_mul * suits.index(s) + v for v, s in played_cards)
    opponent_hand = (suit_mul * s + v for v in range(1, 14) for s in range(4))
    opponent_hand = set(c for c in opponent_hand if c not in hand and c not in played)
    runs = [[8, 6] for _ in range(4)]
    for i, run in enumerate(runs):
        suit = suit_mul * i
        while suit + run[0] - 1 in played:
            run[0] -= 1
        while suit + run[1] + 1 in played:
            run[1] += 1
    card = select_move(hand, opponent_hand, runs)
    play_card(card)
    return cards_in_hand, played_cards
\$\endgroup\$
0
\$\begingroup\$

This is in python 3, but I'm pretty sure it'd work in python 2 as well. It effectively works linearly through the best (as per my limited understanding of the game) possible options at each step - highest scoring card that can match those already played, a new seven, and finally lower scoring cards, finishing by returning None if none of the previous options were possible.

def computer_play(computer_cards, dealt_cards):
    #look at dealt_cards and sort computer_cards by suit
    dealt_hearts = []
    dealt_spades = []
    dealt_clubs = []
    dealt_diamonds = []
    for i in dealt_cards:
        if i[1] == 'hearts':
            dealt_hearts.append(i[0])
        elif i[1] == 'spades':
            dealt_spades.append(i[0])
        elif i[1] == 'clubs':
            dealt_clubs.append(i[0])
        else:
            dealt_diamonds.append(i[0])
    #look at each suit and the highest card that can be played in each
    max_card_hearts = max(dealt_hearts, default=0) + 1
    max_card_spades = max(dealt_spades, default=0) + 1
    max_card_clubs = max(dealt_clubs, default=0) + 1
    max_card_diamonds = max(dealt_diamonds, default=0) + 1
    #play highest overall card
    if ['hearts', max_card_hearts] in computer_cards:
        return [max_card_hearts, 'hearts']
    elif ['spades', max_card_spades] in computer_cards:
        return [max_card_spades, 'spades']
    elif ['clubs', max_card_clubs] in computer_cards:
        return [max_card_clubs, 'clubs']
    elif ['diamonds', max_card_diamonds] in computer_cards:
        return [max_card_diamonds, 'diamonds']
    else:
        #if no cards that match suits out, check for sevens
        suits = {'hearts':max_card_hearts, 'clubs':max_card_clubs, 'spades':max_card_spades, 'diamonds':max_card_diamonds}
        for i in suits.keys():
            if [7, i] in computer_cards:
                return [7, i]
        #if no sevens, check for lowest card that can be played
        for i in suits:
            if [i, suits[i]-2] in computer_cards:
                return [i, suits[i]-2]
        #if nothing can be played, pass
        return None

I'll probably make more updates to it as I debug. The comments hopefully make it fairly clear what's going on.

\$\endgroup\$
  • \$\begingroup\$ Can you please explain what the two inputs of the function are ? \$\endgroup\$ – Agile_Eagle Jul 11 '18 at 19:55
  • \$\begingroup\$ @Agile_Eagle the cards that have already been played and the cards the computer has. \$\endgroup\$ – heather Jul 11 '18 at 20:19
  • 1
    \$\begingroup\$ Your chain of if-elif seem to be returning backwards cards unless I'm missing something about python lists. Should always be [Value, suit] \$\endgroup\$ – Veskah Jul 11 '18 at 22:22
  • \$\begingroup\$ @Veskah ah, yes, you're right! I'll fix that. Thanks! \$\endgroup\$ – heather Jul 11 '18 at 23:00
  • \$\begingroup\$ I cannot test this until someone else has submitted their program. Sorry ! \$\endgroup\$ – Agile_Eagle Jul 12 '18 at 4:13

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.