In [ ]:
import numpy as np
import scipy
from scipy.stats import randint

In [ ]:
NUM_PLAYERS = 4
NUM_DEEDS = 18
NUM_DEED_TYPES = NUM_DEEDS // 3
NUM_TRANSIT = 9
NUM_TRANSIT_TYPES = NUM_TRANSIT // 3
NUM_ANTI_TRUST = 3
NUM_PAYDAY = 2
INFINITE_VALUE = 1000000
ANTI_TRUST_FINE = 20
MIN_BID = 5
RESORT_COST = 20
HOTEL_COST = 10

DEED_CARD = 0
TRANSIT_CARD = 1
ANTI_TRUST_CARD = 2
PAYDAY_CARD = 3

DEED_CARD_NAMES = [
    "Male - Maldives",
    "Mpumalanga - South Africa",
    "Dubai - United Arab Emirates",

    "Saint Elizabeth Parish - Jamaica",
    "Kamalame Cay - The Bahamas",
    "St Lucia - West Indies",
    
    "Ambergris Caye - Belize",
    "Valparaíso - Chile",
    "Fernando de Noronha - Brazil",
    
    "Siargao  - Philippines",
    "Sanya - China",
    "Phuket - Thailand",
    
    "Naples - Florida, USA",
    "Kauai - Hawaii, USA",
    "Laguna Beach - California, USA",
    
    "Capri - Italy",
    "Eze - France",
    "Santorini - Greece",
]

TRANSIT_CARD_NAMES = [
    "Hartsfield–Jackson Atlanta International Airport - USA",
    "Beijing Capital International Airport - China",
    "Dubai International Airport - Dubai",
    
    "Tanggula Railway Station - Tibet",
    "Shinjuku Station - Japan",
    "Grand Central Terminal - USA",
    
    "Port of Shanghai - China",
    "Port of Singapore - Singapore",
    "Port of Rotterdam - Netherlands",
]

In [ ]:
class Player(object):
    def __init__(self, name):
        self.cash = 150
        self.deeds = set()
        self.hotels = set()
        self.resorts = set()
        self.transit = set()
        self.name = name
        self.game_over = False
        
    def countDeedType(self, deed_type):
        count = 0
        for deed in self.deeds:
            if deedType(deed) == deed_type:
                count += 1
        return count
    
    def countTransitType(self, transit_type):
        count = 0
        for transit in self.transit:
            if transitType(transit) == transit_type:
                count += 1
        return count
    
    def addCard(self, card_type, card):
        if card_type == DEED_CARD:
            self.deeds.add(card)
        elif card_type == TRANSIT_CARD:
            self.transit.add(card)
        else:
            raise Exception("Oops",card,card_type)

    def gameOver(self):
        self.deeds = set()
        self.hotels = set()
        self.resorts = set()
        self.transit = set()
        self.name = "XX" + self.name + "XX"
        self.game_over = True

def deedType(deed):
    if deed >= NUM_DEEDS:
        raise Exception("Invalid card",deed)
    return deed // 3

def transitType(transit):
    if transit >= NUM_TRANSIT:
        raise Exception("Invalid card",transit)
    return transit // 3

def rentCost(deed,owner):
    deed_type = deedType(deed)
    if deed_type in owner.resorts:
        return 25
    if deed_type in owner.hotels:
        return 10
    owned = owner.countDeedType(deed_type)
    if owned == 0:
        return 0
    elif owned == 1:
        return 1
    elif owned == 2:
        return 2
    elif owned == 3:
        return 5
    else:
        print(self.deeds)
        raise Exception("Oops " + str(owned))

def transitCost(transit, owner):
    transit_type = transitType(transit)
    owned = owner.countTransitType(transit_type)
    if owned == 0:
        return 0
    elif owned == 1:
        return 2
    elif owned == 2:
        return 5
    elif owned == 3:
        return 10
    else:
        raise Exception("Oops",owned)

def incPlayer(i):
    return (i+1)%NUM_PLAYERS

def rebase_onto_card_type(card):
    if card < NUM_DEEDS:
        return card,DEED_CARD
    elif card < NUM_DEEDS+NUM_TRANSIT:
        return card-NUM_DEEDS,TRANSIT_CARD
    elif card < NUM_DEEDS+NUM_TRANSIT+NUM_ANTI_TRUST:
        return card-NUM_DEEDS-NUM_TRANSIT,ANTI_TRUST_CARD
    elif card < NUM_DEEDS+NUM_TRANSIT+NUM_ANTI_TRUST+NUM_PAYDAY:
        return card-NUM_DEEDS-NUM_TRANSIT-NUM_ANTI_TRUST,PAYDAY_CARD
    else:
        raise Exception("Oops",card)

def getCardName(card_type, card):
    if card_type == DEED_CARD:
        return DEED_CARD_NAMES[card]
    elif card_type == TRANSIT_CARD:
        return TRANSIT_CARD_NAMES[card]
    elif card_type == ANTI_TRUST_CARD:
        return "ANTI-TRUST/" + str(card)
    elif card_type == PAYDAY_CARD:
        return "PAYDAY/" + str(card)

In [ ]:
# All of the AI can be derived from one question: What is a deed worth?

def computeDeedValue(deed,buyer_index,players):
    player = players[buyer_index]
    already_owns = False
    if deed in player.deeds:
        deed_type = deedType(deed)
        if deed_type in player.hotels or deed_type in player.resorts:
            return INFINITE_VALUE # Can't get rid of these, so assume high value
        already_owns = True
        player.deeds.remove(deed)

    valueWithoutDeed = rentCost(deed,player)*8
    player.deeds.add(deed)
    valueWithDeed = rentCost(deed,player)*8
    if player.countDeedType(deedType(deed)) == 3:
        valueWithDeed = 7*8 # hack to account for the fact that now the player has the opportunity to buy a hotel.
    if not already_owns:
        player.deeds.remove(deed)
    return valueWithDeed - valueWithoutDeed

def computeTransitValue(transit, buyer_index, players):
    player = players[buyer_index]
    already_owns = False
    if transit in player.transit:
        transit_type = transitType(transit)
        already_owns = True
        player.transit.remove(transit)

    valueWithout = transitCost(transit,player)*8
    player.transit.add(transit)
    valueWith = transitCost(transit,player)*8
    if not already_owns:
        player.transit.remove(transit)
    return valueWith - valueWithout

def computeValue(card_type, card, buyer_index, players):
    if card_type == DEED_CARD:
        return computeDeedValue(card, buyer_index, players)
    elif card_type == TRANSIT_CARD:
        return computeTransitValue(card, buyer_index, players)
    else:
        raise Exception("Oops",card_type,card)

In [ ]:
np.random.seed(1)
players = []
for x in range(NUM_PLAYERS):
    players.append(Player("Player_" + str(x + 1)))

visit_cards = np.arange(NUM_DEEDS + NUM_TRANSIT + NUM_ANTI_TRUST + NUM_PAYDAY)
on_card = 0
np.random.shuffle(visit_cards)

def auction(card_type, card, seller, first_to_bid):
    top_bid = None
    player_bid_map = {}
    for x in range(NUM_PLAYERS):
        if x != seller:
            player_bid_map[x] = 0
    player_to_bid = first_to_bid
    while len(player_bid_map)>1:
        if player_to_bid not in player_bid_map:
            player_to_bid = incPlayer(player_to_bid)
            continue
            
        # TODO: Consider the value to the other players in the game
        value_to_player = min(players[player_to_bid].cash, computeValue(card_type, card, player_to_bid, players))
        if value_to_player < MIN_BID:
            # Fold
            del player_bid_map[player_to_bid]
        elif top_bid is not None and value_to_player <= top_bid:
            # Fold
            del player_bid_map[player_to_bid]
        else:
            # Bid
            top_bid = value_to_player
            player_bid_map[player_to_bid] = value_to_player
        player_to_bid = incPlayer(player_to_bid)
        
    if len(player_bid_map)==0:
        return # No one bought
    else:
        winning_player_index = list(player_bid_map)[0]
        winning_player_bid = player_bid_map[winning_player_index]
        if winning_player_bid == 0:
            return # No one bought
        players[winning_player_index].cash -= winning_player_bid
        players[winning_player_index].addCard(card_type,card)
        if seller:
            players[seller].cash += winning_player_bid
        print(players[winning_player_index].name,'bid',winning_player_bid,'for',getCardName(card_type,card))

for turn in range(100):
    for player_index in range(len(players)):
        player = players[player_index]

        if player.cash < 0:
            continue

        # Draw a card
        card = visit_cards[on_card]
        on_card += 1
        card,card_type = rebase_onto_card_type(card)
        print(player.name,'draws',getCardName(card_type,card))
        if card_type == PAYDAY_CARD:
            # Shuffle the visit deck
            on_card = 0
            np.random.shuffle(visit_cards)
            # When we shuffle, give each player some money
            print('Shuffle, everyone gets money!')
            for p in players:
                p.cash += 5
        elif card_type == ANTI_TRUST_CARD:
            # Anti-trust card.  Loop through player's deeds and try to sell one
            worst_deed_value = -1
            worst_deed = None
            for deed in player.deeds:
                value = computeDeedValue(deed,player_index,players)
                if value == INFINITE_VALUE:
                    continue
                if worst_deed_value == -1 or worst_deed_value > value:
                    worst_deed_value = value
                    worst_deed = deed
                    
            worst_transit_value = -1
            worst_transit = None
            for transit in player.transit:
                value = computeTransitValue(transit,player_index,players)
                if worst_transit_value == -1 or worst_transit_value > value:
                    worst_transit_value = value
                    worst_transit = transit

            #print(worst_deed,worst_deed_value,worst_transit,worst_transit_value)
            if worst_deed is None and worst_transit is None:
                if len(player.resorts)>0 or len(player.hotels)>0:
                    # Can't sell anything because of hotels/resorts, pay a fine
                    player.cash -= ANTI_TRUST_FINE
                    print(player.name,'charged anti-trust fine of',ANTI_TRUST_FINE)
                else:
                    print(player.name,'avoided anti-trust')
            else:
                if worst_transit is None or (worst_deed is not None and worst_transit_value > worst_deed_value):
                    # Sell the least valuable deed (TODO: Consider the value to other players)
                    player.deeds.remove(worst_deed)
                    print(player.name,'forced to auction',getCardName(DEED_CARD,worst_deed))
                    auction(DEED_CARD, worst_deed,player_index,incPlayer(player_index))
                else:
                    # Sell the least valuable transit (TODO: Consider the value to other players)
                    player.transit.remove(worst_transit)
                    print(player.name,'forced to auction',getCardName(TRANSIT_CARD,worst_transit))
                    auction(TRANSIT_CARD, worst_transit,player_index,incPlayer(player_index))
        elif card_type == TRANSIT_CARD:
            owned = False
            for p in players:
                if card in p.transit:
                    owned = True
                    if p == player:
                        continue # Landed on your own card
                    # Landed on someone else's card, need to pay a fee
                    transit_cost = transitCost(card,p)
                    player.cash -= transit_cost
                    p.cash += transit_cost
                    print(player.name,'charged',transit_cost,'transit fee on',card,'by',p.name)
            if not owned:
                auction(card_type,card,None,player_index)
        elif card_type == DEED_CARD:
            owned = False
            for p in players:
                if card in p.deeds:
                    owned = True
                    if p == player:
                        continue # Landed on your own card
                    # Landed on someone else's card, need to pay a fee
                    rent_cost = rentCost(card,p)
                    player.cash -= rent_cost
                    p.cash += rent_cost
                    print(player.name,'charged',rent_cost,'rent on',card,'by',p.name)
            if not owned:
                auction(card_type,card,None,player_index)
        else:
            raise Exception("Oops",card_type)

        if player.cash < 0 and player.game_over == False:
            # Player is knocked out of the game
            print(player.name,'is knocked out of the game')
            player.gameOver()
            
        # Check if game is over
        players_alive = 0
        for p in players:
            if p.cash >= 0:
                players_alive += 1
        if players_alive < 2:
            break

        if not player.game_over:
            # Opportunity to buy hotel/resort
            for deed_type in range(NUM_DEED_TYPES):
                if deed_type in player.resorts:
                    continue # Nothing more to buy
                elif deed_type in player.hotels:
                    # Opportunity to upgrade to resort
                    if player.cash > RESORT_COST + 10: # dummy ai: buy resort/hotel if you have 10 left over
                        print(player.name,'bought a resort on',deed_type)
                        player.cash -= RESORT_COST
                        player.hotels.remove(deed_type)
                        player.resorts.add(deed_type)
                elif player.countDeedType(deed_type)==3:
                    # Opportunity to buy hotel
                    if player.cash > HOTEL_COST + 10: # dummy ai: buy resort/hotel if you have 10 left over
                        print(player.name,'bought a hotel on',deed_type)
                        player.cash -= HOTEL_COST
                        player.hotels.add(deed_type)
        
    print("TURN",turn)
    for player in players:
        print(player.name,player.cash,player.deeds,player.hotels,player.resorts,player.transit)
    # Check if game is over
    players_alive = 0
    for p in players:
        if p.cash >= 0:
            players_alive += 1
    if players_alive < 2:
        print('GAME OVER')
        break

In [ ]: