Write a Program for winning sevens game

6

2

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

Agile_Eagle

Posted 2018-07-11T18:24:51.610

Reputation: 663

will you play the programs against each other until a winner is clear to a statistically significant level? – Jonah – 2018-07-12T02:38:51.630

@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. – Jonah – 2018-07-12T04:19:46.847

1

Let us continue this discussion in chat.

– Agile_Eagle – 2018-07-12T04:20:20.023

3https://www.pagat.com/layout/sevens.html (For anyone else that doesn't know the game and needs more detailed explanation.) – sundar - Reinstate Monica – 2018-07-12T10:50:57.297

1The 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. – user1502040 – 2018-07-13T23:53:45.567

Answers

2

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)

sundar - Reinstate Monica

Posted 2018-07-11T18:24:51.610

Reputation: 5 296

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!). – sundar - Reinstate Monica – 2018-07-14T19:38:52.403

I am ending this question now. keep the bounty for yourself – Agile_Eagle – 2018-07-17T17:40:21.750

2

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).

sundar - Reinstate Monica

Posted 2018-07-11T18:24:51.610

Reputation: 5 296

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 – Veskah – 2018-07-13T01:20:18.393

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. – sundar - Reinstate Monica – 2018-07-13T01:42:37.060

@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 – Agile_Eagle – 2018-07-13T10:52:16.027

@sundar I modified my controller take a look at it. Thanks!! – Agile_Eagle – 2018-07-13T10:53:27.520

@Veskah The player 2 advantage still seems to remain, even after fixing the bug in my code. It doesn't seem to be about the initial card shuffle either - just changing the order of the player1, player2 calls in the while loop changes which one has the advantage - the player called second wins most of the time (given same code for both "players"). Any ideas what this could be? Gist

– sundar - Reinstate Monica – 2018-07-13T15:20:06.437

@Agile_Eagle The return value is just for my convenience (when debugging in the REPL), the code also does the necessary appends and removes on the lists. – sundar - Reinstate Monica – 2018-07-13T15:22:35.037

1@sundar cool. I am going to test your programs against each other soon. – Agile_Eagle – 2018-07-13T15:23:50.593

2

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

user1502040

Posted 2018-07-11T18:24:51.610

Reputation: 2 196

0

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.

heather

Posted 2018-07-11T18:24:51.610

Reputation: 219

Can you please explain what the two inputs of the function are ? – Agile_Eagle – 2018-07-11T19:55:56.243

@Agile_Eagle the cards that have already been played and the cards the computer has. – heather – 2018-07-11T20:19:53.927

1Your chain of if-elif seem to be returning backwards cards unless I'm missing something about python lists. Should always be [Value, suit] – Veskah – 2018-07-11T22:22:12.920

@Veskah ah, yes, you're right! I'll fix that. Thanks! – heather – 2018-07-11T23:00:28.263

I cannot test this until someone else has submitted their program. Sorry ! – Agile_Eagle – 2018-07-12T04:13:32.013

@heather there is an error in your code. In this line: max_card_spades = max(dealt_spades) + 1, the error is ValueError: max() arg is an empty sequence. This means that if a spade is not dealt, your code will fail. Same is the case for other suits. – Agile_Eagle – 2018-07-12T05:18:46.557

@Veskah are you getting the same error as above ? – Agile_Eagle – 2018-07-12T15:46:49.323

@Agile_Eagle didn't try it, just saw the error visually. – Veskah – 2018-07-12T20:09:56.577

@heather are you going to fix the code – Agile_Eagle – 2018-07-13T10:40:22.620

@Agile_Eagle yes, probably over the weekend - I have been very busy over the last couple days; I apologize – heather – 2018-07-13T22:59:22.633

@Agile_Eagle it should hopefully be fixed now. Sorry about that! – heather – 2018-07-17T16:51:09.847

@heather I already have a winner https://codegolf.stackexchange.com/a/168517/81559

– Agile_Eagle – 2018-07-17T17:39:41.150

@Agile_Eagle that doesn't mean I shouldn't fix my code =) Anyway, you implied in the comment below that answer you were going to rerun it if I fixed my answer. I don't mind if you don't, though. – heather – 2018-07-17T17:41:35.063

@heather I am closing this competition. Sundar has won it – Agile_Eagle – 2018-07-17T18:10:33.673