# Uno

## Game logic

<details>

<summary>Game Logic code</summary>

```python
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
```

</details>

## Game state schema

```protobuf
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;
}
```

```protobuf
message Hand {
  repeated Card cards = 1;
}
```

```protobuf
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

```javascript
play_card(int card_position_in_hand, bool call_uno, string specified_color)
draw_card(string specified_color)
```

[Start building now](https://botpot.ai/game/uno)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://doc.botpot.ai/games/explore/uno.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
