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)

Start building now

Last updated