Source code for splendor.splendor.gym.envs.utils

"""
Collection of useful utility functions used in the implementation of SplendorEnv.
"""

from typing import Dict, List

import numpy as np
from numpy.typing import NDArray

from splendor.splendor.constants import MAX_TIER_CARDS, NUMBER_OF_TIERS, RESERVED
from splendor.splendor.splendor_model import SplendorState
from splendor.splendor.types import ActionType

from .actions import ALL_ACTIONS, Action, ActionEnum, CardPosition


def _valid_position(state: SplendorState, position: CardPosition) -> bool:
    """
    check if the given card position is a valid position in the given state.
    useful for validating that a position of a card can be purchased/reserved.
    """
    if position.tier not in range(NUMBER_OF_TIERS):
        return False
    if (
        position.card_index not in range(MAX_TIER_CARDS)
        or state.board.dealt[position.tier][position.card_index] is None
    ):
        return False
    return True


def _valid_reserved_position(
    state: SplendorState, position: CardPosition, agent_index: int
) -> bool:
    """
    check if the given reserved card position is a valid position in the given state.
    useful for validating that a position of a reserved card can be purchased.
    """
    return (
        position.reserved_index in range(len(state.agents[agent_index].cards[RESERVED]))
        and state.agents[agent_index].cards[RESERVED][position.reserved_index]
    )


[docs] def build_action( action_index: int, state: SplendorState, agent_index: int, ) -> Dict: """ Construct the action to be taken from it's action index in the ALL_ACTION list. :return: the corresponding action to the action_index, in the format required by SplendorGameRule. :note: when using this function for building a buying action the function doesn't takes into account the wildcard gems (yellow) and the owned cards for the conclusion of the returned_gems - this can lead to a broken state where a player have a negative amount of gems... """ if action_index not in range(len(ALL_ACTIONS)): raise ValueError(f"The action {action_index} isn't a valid action") action = ALL_ACTIONS[action_index] noble = ( state.board.nobles[action.noble_index] if action.noble_index is not None and action.noble_index in range(len(state.board.nobles)) else None ) card = ( state.board.dealt[action.position.tier][action.position.card_index] if action.position and _valid_position(state, action.position) else None ) reserved_card = ( state.agents[agent_index].cards[RESERVED][action.position.reserved_index] if action.position and _valid_reserved_position(state, action.position, agent_index) else None ) match action.type: case ActionEnum.PASS: action_to_execute = { "type": "pass", "noble": noble, } case ActionEnum.COLLECT_SAME: action_to_execute = { "type": "collect_same", "noble": noble, "collected_gems": action.collected_gems, "returned_gems": action.returned_gems, } case ActionEnum.COLLECT_DIFF: action_to_execute = { "type": "collect_diff", "noble": noble, "collected_gems": action.collected_gems, "returned_gems": action.returned_gems, } case ActionEnum.RESERVE: action_to_execute = { "type": "reserve", "noble": noble, "card": card, "collected_gems": action.collected_gems, "returned_gems": action.returned_gems, } case ActionEnum.BUY_AVAILABLE: if card is None: # this might happen when buying a card but with a # wrong index (there is no card at that position). raise ValueError( f"Can't build action {action} since there is not card to buy!" ) returned_gems = card.cost action_to_execute = { "type": "buy_available", "noble": noble, "card": card, "returned_gems": returned_gems, } case ActionEnum.BUY_RESERVE: if reserved_card is None: # this might happen when buying a reserved card but with a # wrong index. raise ValueError( f"Can't build action {action} since there is not card to buy!" ) returned_gems = reserved_card.cost action_to_execute = { "type": "buy_reserve", "noble": noble, "card": reserved_card, "returned_gems": returned_gems, } case _: raise ValueError( f"Unknown action type: {action.type} of the action {action}" ) return action_to_execute
[docs] def create_action_mapping( legal_actions: List[ActionType], state: SplendorState, agent_index: int ) -> Dict[int, ActionType]: """ Create the mapping between action indices to legal actions. This would be in use by both SplendorEnv & by the PPO agent. """ mapping = { ALL_ACTIONS.index( Action.to_action_element(legal_action, state, agent_index) ): legal_action for legal_action in legal_actions } return mapping