|
| 1 | +import random |
| 2 | +from collections import namedtuple, defaultdict |
| 3 | +from datetime import datetime |
| 4 | + |
| 5 | +COLORS = RED, YELLOW, BLUE, GREEN = ['R', 'Y', 'B', 'G'] |
| 6 | +BLACK = 'b' |
| 7 | +SKIP = 'S' |
| 8 | +DRAW_2 = '+2' |
| 9 | +COLOR_WISH = 'C' |
| 10 | +DRAW_4 = '+4' |
| 11 | +CHANGE_DIRECTION = 'CD' |
| 12 | +ARBITRARY_KIND = '_' |
| 13 | +CARDS_PER_PLAYER = 7 |
| 14 | +PUT = 'put' |
| 15 | +DRAW = 'draw' |
| 16 | + |
| 17 | +State = namedtuple("State", ["flipped_card", # The card that is currently flipped on the table |
| 18 | + "history", # The cards that were flipped/put so far |
| 19 | + "draw_counter", # counts how many cards the next user has to draw if he/she is not able to put a card. |
| 20 | + "nr_of_players", |
| 21 | + "player_index", # The index of the player whos turn it is |
| 22 | + "p_has_drawn", # Is set to true if the player has applied the draw action. If true, the player can either put a card or do nothing without having to draw a card. |
| 23 | + "color_wish", |
| 24 | + "player_cards"] # array of integers in which each int stands for the amount of cards a player at an index has. |
| 25 | +) |
| 26 | + |
| 27 | +Action = namedtuple("Action", ["type", "card", "color_wish"]) |
| 28 | + |
| 29 | +def generate_deck(): |
| 30 | + "Generates a shuffled deck of uno cards" |
| 31 | + deck = [] |
| 32 | + kinds = list(map(str, range(1, 10))) * 2 + ['0'] # Each number comes up twice per deck and color except 0 |
| 33 | + kinds += [SKIP, DRAW_2, CHANGE_DIRECTION] * 2 |
| 34 | + |
| 35 | + for c in COLORS: |
| 36 | + deck += map(lambda n: n + c, kinds) |
| 37 | + |
| 38 | + deck += [DRAW_4 + BLACK, COLOR_WISH + BLACK] * 4 |
| 39 | + random.shuffle(deck) |
| 40 | + return deck |
| 41 | + |
| 42 | +def deal_cards(nr_of_players, cards): |
| 43 | + """Deals the cards to the given nr_of_players and returns a list of hands |
| 44 | + as well as the remaining cards.""" |
| 45 | + return ([cards[i:i+CARDS_PER_PLAYER] for i in range(nr_of_players)], |
| 46 | + cards[nr_of_players * CARDS_PER_PLAYER:]) |
| 47 | + |
| 48 | +def has_won(hand): return len(hand) == 0 |
| 49 | + |
| 50 | +def card_color(card): return card[-1] |
| 51 | + |
| 52 | +def card_kind(card): return card[:-1] |
| 53 | + |
| 54 | +def draw(action, state, hands, cards, strategies): |
| 55 | + "Applys the draw action and returns a tuple: (state, hands, cards, strategies)." |
| 56 | + hand = hands[state.player_index] |
| 57 | + |
| 58 | + if state.p_has_drawn: |
| 59 | + # Player has drawn cards and is still not able to put one |
| 60 | + flipped_card, history = state.flipped_card, list(state.history) |
| 61 | + if card_color(flipped_card) == BLACK: |
| 62 | + flipped_card = ARBITRARY_KIND + state.color_wish |
| 63 | + history += [state.flipped_card] |
| 64 | + |
| 65 | + state = State(flipped_card, history, state.draw_counter, state.nr_of_players, |
| 66 | + (state.player_index + 1) % state.nr_of_players, False, '', list(state.player_cards)) |
| 67 | + return (state, hands, cards, strategies) |
| 68 | + |
| 69 | + # Player has to draw cards |
| 70 | + history = list(state.history) |
| 71 | + player_cards = list(state.player_cards) |
| 72 | + if len(cards) >= state.draw_counter: |
| 73 | + history = [] |
| 74 | + cards += state.history |
| 75 | + random.shuffle(cards) |
| 76 | + |
| 77 | + hand += cards[:state.draw_counter] #TODO: sort for better caching? |
| 78 | + player_cards[state.player_index] = len(hand) |
| 79 | + cards = cards[state.draw_counter:] |
| 80 | + state = State(state.flipped_card, history, 1, state.nr_of_players, state.player_index, |
| 81 | + True, state.color_wish, player_cards) |
| 82 | + return (state, hands, cards, strategies) |
| 83 | + |
| 84 | +def put(action, state, hands, cards, strategies): |
| 85 | + "Applys the put action and returns a tuple: (state, cards, strategies)" |
| 86 | + history = state.history + ([state.flipped_card] if card_kind(state.flipped_card) != ARBITRARY_KIND |
| 87 | + else []) |
| 88 | + hand = hands[state.player_index] |
| 89 | + flipped_card = action.card |
| 90 | + hand.remove(action.card) |
| 91 | + color_wish = '' |
| 92 | + player_index = (state.player_index + 1) % state.nr_of_players |
| 93 | + draw_counter = state.draw_counter |
| 94 | + player_cards = list(state.player_cards) |
| 95 | + player_cards[state.player_index] -= 1 |
| 96 | + |
| 97 | + if card_color(action.card) == BLACK: |
| 98 | + draw_counter += 4 if card_kind(action.card) == DRAW_4 else 0 |
| 99 | + color_wish = action.color_wish |
| 100 | + |
| 101 | + if card_kind(action.card) == DRAW_2: |
| 102 | + draw_counter += 2 |
| 103 | + |
| 104 | + if card_kind(action.card) == SKIP: |
| 105 | + player_index = (state.player_index + 2) % state.nr_of_players |
| 106 | + |
| 107 | + if card_kind(action.card) == CHANGE_DIRECTION: |
| 108 | + strategies.reverse() |
| 109 | + hands.reverse() |
| 110 | + player_cards.reverse() |
| 111 | + |
| 112 | + if card_kind(action.card) in [DRAW_2, DRAW_4]: |
| 113 | + draw_counter -= state.draw_counter % 2 # Needed to make up for the 1 that is inside the counter by default |
| 114 | + |
| 115 | + state = State(flipped_card, history, draw_counter, state.nr_of_players, player_index, |
| 116 | + False, color_wish, player_cards) |
| 117 | + |
| 118 | + return (state, hands, cards, strategies) |
| 119 | + |
| 120 | +def apply_action(action, state, hands, cards, strategies): |
| 121 | + "Applys an action to a state and returns a tuple: (state, cards)" |
| 122 | + return (draw(action, state, hands, cards, strategies) if action.type == DRAW else |
| 123 | + put(action, state, hands, cards, strategies)) |
| 124 | + |
| 125 | +def whatever_works(state, hand): |
| 126 | + "A strategy that that takes the first action of the possible ones that it finds" |
| 127 | + if state.draw_counter > 1: |
| 128 | + return Action(DRAW, '', '') |
| 129 | + for card in hand: |
| 130 | + if card_color(card) == BLACK: |
| 131 | + hand_colors = list(map(card_color, hand)) |
| 132 | + return Action(PUT, card, max(COLORS, key = lambda c: hand_colors.count(c))) |
| 133 | + if card_color(card) == state.color_wish: |
| 134 | + return Action(PUT, card, '') |
| 135 | + |
| 136 | + action = Action(PUT, card, '') |
| 137 | + if valid_action(action, state, hand): |
| 138 | + return action |
| 139 | + return Action(DRAW, '', '') |
| 140 | + |
| 141 | +def save_blacks_increase_counter(state, hand): |
| 142 | + "A strategy that tries to save the black cards but increases the draw counter if possible" |
| 143 | + hand_kinds = list(map(card_kind, hand)) |
| 144 | + hand_colors = list(map(card_color, hand)) |
| 145 | + color_wish = max(COLORS, key = lambda c: hand_colors.count(c)) |
| 146 | + |
| 147 | + if state.draw_counter > 1 and card_kind(state.flipped_card) in hand_kinds: |
| 148 | + # put +2/+4 on already put +2/+4 |
| 149 | + card = hand[hand_kinds.index(card_kind(state.flipped_card))] |
| 150 | + return Action(PUT, card, color_wish if card_color(card) == BLACK else '') |
| 151 | + |
| 152 | + for card in filter(lambda c: card_color(c) != BLACK, hand): |
| 153 | + # hold black cards back if possible |
| 154 | + action = Action(PUT, card, '') |
| 155 | + if valid_action(action, state, hand): |
| 156 | + return action |
| 157 | + |
| 158 | + if BLACK in hand_colors and state.draw_counter == 1: |
| 159 | + return Action(PUT, hand[hand_colors.index(BLACK)], max(COLORS, key = lambda c: hand_colors.count(c))) |
| 160 | + |
| 161 | + return Action(DRAW, '', '') |
| 162 | + |
| 163 | +def valid_action(action, state, hand): |
| 164 | + """Returns boolean whether an action is valid or not.""" |
| 165 | + |
| 166 | + if action.color_wish == BLACK: |
| 167 | + return False |
| 168 | + if action.type == PUT and action.card not in hand: |
| 169 | + return False |
| 170 | + if action.type == PUT and state.draw_counter > 1 and card_kind(action.card) != card_kind(state.flipped_card): |
| 171 | + # The player is trying to put a card even though he has to draw |
| 172 | + return False |
| 173 | + if action.type == PUT and card_color(action.card) == BLACK and not action.color_wish: |
| 174 | + # The player did not specify a color wish |
| 175 | + return False |
| 176 | + if (action.type == PUT and card_color(action.card) != BLACK and |
| 177 | + state.color_wish and state.color_wish != card_color(action.card)): |
| 178 | + # The previous player has wished for a certain color and the player is not delivering... |
| 179 | + return False |
| 180 | + if (action.type == PUT and card_color(action.card) != BLACK and |
| 181 | + card_kind(action.card) != card_kind(state.flipped_card) and |
| 182 | + card_color(action.card) != card_color(state.flipped_card) and |
| 183 | + card_color(action.card) != state.color_wish): |
| 184 | + # The player wants to put a card that's neither in the same color nor the same nr as the flipped card |
| 185 | + return False |
| 186 | + |
| 187 | + return True |
| 188 | + |
| 189 | +def uno(*strategies, verbose=False): |
| 190 | + "Plays a game of uno between 2 - 10 strategies." |
| 191 | + assert len(strategies) >= 2 and len(strategies) <= 10, "Uno is a game for 2 - 10 players" |
| 192 | + cards = generate_deck() |
| 193 | + strategies = list(strategies) |
| 194 | + hands, cards = deal_cards(len(strategies), cards) |
| 195 | + |
| 196 | + first_card = cards.pop() |
| 197 | + color_wish = random.choice(COLORS) if card_color(first_card) == BLACK else '' |
| 198 | + |
| 199 | + state = State(flipped_card = first_card, history = [], |
| 200 | + draw_counter = 1, nr_of_players = len(strategies), |
| 201 | + player_index = 0, p_has_drawn = False, color_wish = color_wish, |
| 202 | + player_cards = list(map(len, hands))) |
| 203 | + |
| 204 | + while not any(map(lambda h: has_won(h), hands)): |
| 205 | + |
| 206 | + next_action = strategies[state.player_index](state, hands[state.player_index]) |
| 207 | + |
| 208 | + if not valid_action(next_action, state, hands[state.player_index]): |
| 209 | + print("\n\n\nINVALID ACTION by --- {2} --- \nSTATE={0}\nACTION={1}\n\n\n".format(state, |
| 210 | + next_action, |
| 211 | + strategies[state.player_index].__name__)) |
| 212 | + break |
| 213 | + |
| 214 | + if verbose: |
| 215 | + print("\nHANDS:") |
| 216 | + for i in range(state.nr_of_players): |
| 217 | + print("{2}: {0} ---- {1} cards".format(hands[i], len(hands[i]), strategies[i].__name__)) |
| 218 | + |
| 219 | + new_state, hands, cards, strategies = apply_action(next_action, state, hands, cards, strategies) |
| 220 | + |
| 221 | + if verbose: |
| 222 | + print("\nSTATE:\n{0}\nACTION by --- {2} ---:\n{1}\n\nCARDS: {3}\n\n".format(state, next_action, strategies[state.player_index].__name__, cards)) |
| 223 | + input("Press enter to continue...") |
| 224 | + state = new_state |
| 225 | + |
| 226 | + return strategies[hands.index([])] |
| 227 | + |
| 228 | +def compare_strategies(*strategies, n=1000): |
| 229 | + "Simulates n games and prints out how often each strategy won." |
| 230 | + scoreboard = defaultdict(int) |
| 231 | + for k in range(n): |
| 232 | + scoreboard[uno(*strategies)] += 1 |
| 233 | + for (strategy, win_counter) in sorted(scoreboard.items(), key=lambda t: t[1], reverse=True): |
| 234 | + print("{0} won {1}%".format(strategy.__name__, (win_counter / float(n)) * 100)) |
| 235 | + |
| 236 | + |
| 237 | +compare_strategies(whatever_works, save_blacks_increase_counter) |
0 commit comments