Uno
Game logic
Game Logic code
import random
def init_game(game_settings):
player_count = int(game_settings["Player Count"])
state = {
'scores': [0] * player_count
}
state = __initialize_round(state)
return state
def __initialize_round(state):
deck = __initalize_deck()
#Flip over first card on the deck and add to discard pile, cannot be a wild or wild draw four card
first_card = deck.pop()
#Set up empty hands
hands = []
player_count = len(state['scores']) # scores is the only thing that persists across rounds
for i in range(player_count):
hand = {
'cards': []
}
#Deal each player 7 cards
for x in range(7):
hand['cards'].append(deck.pop())
hands.append(hand)
#Randomly decide who goes first/who is the "dealer"
state['player_turn_idx'] = random.randint(0, player_count - 1)
state['players_hands'] = hands
state['draw_pile'] = deck
state['discard_pile'] = [first_card]
state['direction'] = "CLOCKWISE"
return state
def __initalize_deck():
def make_cards(color, type, card_number, count):
cards = []
for i in range(count):
cards.append({
'color': color,
'type': type,
'card_number': card_number
})
return cards
standard_colors = ['BLUE', 'GREEN', 'RED', 'YELLOW']
deck = []
#Skip, reverse, and draw Cards, 2 of each standard color
for card_color in standard_colors:
deck.extend(make_cards(card_color, 'SKIP', -1, 2))
deck.extend(make_cards(card_color, 'REVERSE', -1, 2))
deck.extend(make_cards(card_color, 'DRAWTWO', -1, 2))
#4 Wild Cards and wild draw four cards
deck.extend(make_cards('UNDEFINED', 'WILD', -1, 4))
deck.extend(make_cards('UNDEFINED', 'WILDDRAWFOUR', -1, 4))
#Cards numbered 0-9 in each standard color
for number in range(10):
for card_color in standard_colors:
#only one 0 of each color, all other numbers have 2 of the same card in the deck
if number == 0:
deck.extend(make_cards(card_color, 'NUMBERED', number, 1))
else:
deck.extend(make_cards(card_color, 'NUMBERED', number, 2))
random.shuffle(deck)
# Make sure last card is a normal card (we will use pop, so last card is drawn first)
for i in range(len(deck)):
if deck[i]['type'] == 'NUMBERED':
result = deck[0:i]
result.extend(deck[i + 1:])
result.append(deck[i])
return result
# shouldn't happen
return deck
def __can_be_played(state, player_idx, card):
def can_non_wilddrawfour_be_played(top_card, card): # assumes card is not a wildDrawFour
if card['type'] == 'WILD':
return True
if card['color'] == top_card['color']:
return True
if card['type'] == top_card['type']:
if card['type'] == 'NUMBERED':
return card['card_number'] == top_card['card_number']
return True
return False
top_discard_card = state['discard_pile'][-1]
if card['type'] == 'WILDDRAWFOUR':
# For simplicity, no bluffing so you can't play this if you have another playable card
for c in state['players_hands'][player_idx]['cards']:
if c['type'] == 'WILDDRAWFOUR':
continue
if can_non_wilddrawfour_be_played(top_discard_card, c):
raise InvalidActionError("If another card is playable, you cannot play the wild draw 4 card.")
return True
return can_non_wilddrawfour_be_played(top_discard_card, card)
def draw_card(state, specified_color = None):
player_idx = state['player_turn_idx']
__safe_draw(state, player_idx, 1)
cards = state['players_hands'][player_idx]['cards']
drawn_card = cards[-1]
if (__can_be_played(state, player_idx, drawn_card)):
state = __carry_out_action(state, player_idx, len(cards) - 1, specified_color)
else:
state['player_turn_idx'] = __get_next_index(state, player_idx, 1)
state = __handle_end_of_round(state, player_idx)
return state
def __safe_draw(state, player_idx, count):
"""
The <player_idx> player tries to draw <count> number of cards from the draw_pile.
Returns True if it can be done and False if there are not enough cards in the pile.
"""
for i in range(count):
if len(state['draw_pile']) > 0:
drawn_card = state['draw_pile'].pop();
state['players_hands'][player_idx]['cards'].append(drawn_card)
else:
return False
return True
def __get_next_index(state, player_idx, offset):
return (player_idx+offset) % len(state['players_hands']) if state['direction'] == "CLOCKWISE" else (player_idx - offset) % len(state['players_hands'])
def __carry_out_action(state, player_idx, card_index, specified_color = None):
current_cards = state['players_hands'][__get_next_index(state, player_idx, 0)]['cards']
card = current_cards.pop(card_index)
#Reverse the direction
if card['type'] == 'REVERSE':
state['direction'] = 'COUNTERCLOCKWISE' if state['direction'] == 'CLOCKWISE' else 'CLOCKWISE'
#update turn
if len(state['players_hands']) != 2:
state['player_turn_idx'] = __get_next_index(state, player_idx, 1)
#Skips next player's turn
elif card['type'] == 'SKIP':
state['player_turn_idx'] = __get_next_index(state, player_idx, 2)
#Gives next player 2 cards and skips the next player's turn
elif card['type'] == 'DRAWTWO':
next_player_index = __get_next_index(state, player_idx, 1)
__safe_draw(state, next_player_index, 2)
state['player_turn_idx'] = __get_next_index(state, player_idx, 2)
#Changes the color to any color
elif card['type'] == 'WILD':
card['color'] = specified_color
#update turn
state['player_turn_idx'] = __get_next_index(state, player_idx, 1)
#Changes the color to any color, gives next player 4 colors, and skips the next player's turn
elif card['type'] == 'WILDDRAWFOUR':
card['color'] = specified_color
next_player_index = __get_next_index(state, player_idx, 1)
__safe_draw(state, next_player_index, 4)
state['player_turn_idx'] = __get_next_index(state, player_idx, 2)
#Normal numbered card does nothing special
else:
#update turn
state['player_turn_idx'] = __get_next_index(state, player_idx, 1)
state['discard_pile'].append(card)
return state
def play_card(state, card_position_in_hand, call_uno, specified_color = None):
player_idx = state['player_turn_idx']
cards = state['players_hands'][player_idx]['cards']
if card_position_in_hand >= len(cards) or card_position_in_hand < 0:
raise InvalidActionError("Not a valid card in the player's hand")
selected_card = cards[card_position_in_hand]
if (__can_be_played(state, player_idx, selected_card)):
state = __carry_out_action(state, player_idx, card_position_in_hand, specified_color)
if len(state['players_hands'][player_idx]['cards']) == 1:
if not call_uno:
__safe_draw(state, player_idx, 4)
else:
raise InvalidActionError("This card is not playable -- choose a card with the same color, word, or number as the top discarded card")
state = __handle_end_of_round(state, player_idx)
return state
def __handle_end_of_round(state, player_idx):
# A round ends when one player has no cards left or when the draw pile is empty
# Each player p gets a penalty <Sp> based on how many cards they have left when
# the round ends. The player with the minimum penalty min(Sp) is the winner of the round.
# Thus if a player has no cards, he is the winner of the round.
# The winner(s) gets a score that is the sum of (Sp - min(Sp)) and is evenly distributed
# across the winners if the winners have the same minimum score.
# Then shuffle and redeal the cards
if len(state['players_hands'][player_idx]['cards']) > 0 and len(state['draw_pile']) > 0:
return state
def value_of_card(card):
if card['type'] == 'NUMBERED':
return card['card_number']
elif card['type'] == 'DRAWTWO' or card['type'] == 'REVERSE' or card['type'] == 'SKIP':
return 20
else:
return 50
scores = []
for i in range(len(state['players_hands'])):
scores.append(sum([value_of_card(card) for card in state['players_hands'][i]['cards']]))
min_score = min(scores)
winner_indices = []
sum_of_penalty = 0
for i in range(len(scores)):
if min_score == scores[i]:
winner_indices.append(i)
else:
sum_of_penalty += (scores[i] - min_score)
winner_score = int(sum_of_penalty / len(winner_indices))
for winner_index in winner_indices:
state['scores'][winner_index] += winner_score
if max(state['scores']) > 300:
return state
return __initialize_round(state)
def get_game_result(state):
#determines whether a player has reached at least 300 points and if so, declare them the winner
scores = state['scores']
for i in range(len(scores)):
if scores[i] >= 300:
return {
"game_result": "Winner",
"winner_idx": i
}
return {
"game_result": "NoWinnerYet"
}
def get_player_states(state):
hand_count = []
for i in range(len(state['players_hands'])):
hand_count.append(len(state['players_hands'][i]['cards']))
player_states = []
for i in range(len(state['players_hands'])):
player_states.append({
'player_turn_idx': state['player_turn_idx'],
'player_hand': state['players_hands'][i],
'players_hand_count': hand_count,
'last_discarded': state['discard_pile'][-1],
'direction': state['direction'],
'scores': state['scores'],
'draw_pile_count': len(state['draw_pile'])
})
return player_states
Game state schema
message State {
// Required field to indicate the player who should be making the next move
// Values = 0 or 1
int32 player_turn_idx = 1;
// Each player can only see their own hands. They can see the number of cards
// in the other player's hands, but not their card type.
Hand player_hand = 2;
// An array of the number of cards in each player's hands, including yourself.
repeated int32 players_hand_count = 3;
// The top of the deck. What you can play is based on this.
Card last_discarded = 4;
// Direction of the turns, can be "CLOCKWISE" or "COUNTERCLOCKWISE".
// Clockwise means the player indices increment, and counterclockwise means decrement.
// Playing the reverse card will flip the direction.
string direction = 5;
}
message Hand {
repeated Card cards = 1;
}
message Card {
enum Color {
BLACK = 0;
BLUE = 1;
GREEN = 2;
RED = 3;
YELLOW = 4;
// WILD cards may have this as undefined initially
// When WILD cards are played, players specify their color
UNDEFINED = 5;
}
enum Type {
NUMBERED = 0;
WILD = 1;
DRAWTWO = 2;
WILDDRAWFOUR = 3;
REVERSE = 4;
SKIP = 5;
}
Color color = 1;
Type type = 2;
int32 card_number = 3;
}
Action schema
play_card(int card_position_in_hand, bool call_uno, string specified_color)
draw_card(string specified_color)
Last updated