Skip to content

Commit c7e2248

Browse files
Added python3 script and edited readme
1 parent 3826a64 commit c7e2248

File tree

2 files changed

+242
-1
lines changed

2 files changed

+242
-1
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
# uno
2-
A python program that compares uno strategies.
2+
A python3 program that compares uno strategies.
3+
4+
If you want to build your own strategy just write a function that takes
5+
the current state and a hand as arguments and returns an action. Don't
6+
forget to add your function as an argument to the compare_strategies function call.

uno.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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

Comments
 (0)