""" This file contains the AI used in the game, which is called during decision making procedure. All AI procedure should ending up producing a valid option, never an invalid one. AI also possess information on the table to facilitate decision making. AI should output the card play as int, and the actual Card is played in the Player class AI possesses the table knowledge and the hand AI should not modify the player cards and table data. They are read only. """ import random import cards import math class BaseAI: """ A base class for AI implementation. """ def __init__(self, table_status, player=None): self.player = player self.table_status = table_status def connect_to_player(self, player): self.player = player def request_reshuffle(self): pass def make_a_bid(self): pass def call_partner(self): pass def make_a_play(self, sub_state): pass def update_memory(self): return def reset_memory(self): return def get_valid_plays(self, leading): all_plays = self.player.get_deck_values() possible_plays = None if leading: if not self.table_status['trump broken']: possible_plays = [card for card in all_plays if not cards.get_card_suit(card) == self.table_status['trump suit']] else: leading_suit = self.table_status['played cards'][self.table_status["leading player"]].suit() possible_plays = [card for card in all_plays if cards.get_card_suit(card) == leading_suit] if not possible_plays: return all_plays return possible_plays class RandomAI(BaseAI): def request_reshuffle(self): if random.randint(0, 1): return True return False def make_a_bid(self): """ :return: int - the bid """ if self.player: current_round_bid = self.table_status["bid"] // 10 current_suit_bid = self.table_status["bid"] % 10 bid_threshold = int(current_round_bid*1.5 + current_suit_bid*0.5) gen_bid = random.randint(0, bid_threshold) print(gen_bid) if gen_bid <= 1: if current_suit_bid == 5: return (current_round_bid+1)*10 + 1 else: return self.table_status["bid"]+1 def call_partner(self): """ :return: int - the card value """ player_cards = self.player.get_deck_values() other_cards = [] for i in range(4): for j in range(13): current_card = (i + 1) * 100 + j + 2 if current_card not in player_cards: other_cards.append(current_card) return random.choice(other_cards) def make_a_play(self, sub_state): """ :param sub_state: :return: int - card value """ if sub_state == 0: valid_plays = self.get_valid_plays(True) else: valid_plays = self.get_valid_plays(False) return random.choice(valid_plays) class VivianAI(RandomAI): def __init__(self, table_status, player=None): super().__init__(table_status, player=player) self.weigh1 = 0.15 self.weigh2 = 0.002 self.bid_weigh = 0.3 self.unplayed_cards = [] [self.unplayed_cards.append([i+2 for i in range(13)]) for _ in range(4)] def request_reshuffle(self): return True def make_a_bid(self): # TODO: execute estimate_wins only once # Be careful when getting max_bid as it is 0-index but suits are 1-index est_wins = self.estimate_wins() max_est = max(est_wins) max_bid = [math.ceil(est)-3 for est in est_wins] favourable_suit = [i+1 for i, est in enumerate(est_wins) if est == max_est] if len(favourable_suit) > 1: favourable_suit = random.choice(favourable_suit) else: favourable_suit = favourable_suit[0] bid_num = self.table_status["bid"] // 10 bid_suit = self.table_status["bid"] % 10 if bid_suit == favourable_suit: if bid_num < max_bid[favourable_suit]: return 10 + self.table_status["bid"] else: loss_reward = self.bid_weigh*((8-bid_num)-(max_bid[bid_suit-1]+1)) max_bid[favourable_suit-1] += int(loss_reward) next_bid_num = bid_num + 1 * (favourable_suit < bid_suit) if next_bid_num <= max_bid[favourable_suit-1]: return next_bid_num*10 + favourable_suit return 0 def call_partner(self): """ :return: int - the card value """ player_cards = self.player.get_deck_values() card_suits = [cards.get_card_suit(crd) for crd in player_cards] card_nums = [cards.get_card_number(crd) for crd in player_cards] trump_suit = self.table_status["bid"] % 10 trump_nums = [num for suit, num in zip(card_suits, card_nums) if suit == trump_suit] if 14 not in trump_nums: return trump_suit*100 + 14 if 13 not in trump_nums: return trump_suit*100 + 13 if 12 not in trump_nums: return trump_suit*100 + 12 suit_values = [] for i in range(4): suit_values.append(sum([num for suit, num in zip(card_suits, card_nums) if suit == i+1])) min_val = min(suit_values) weakest_suit = [i + 1 for i, val in enumerate(suit_values) if val == min_val] if len(weakest_suit) > 1: weakest_suit = random.choice(weakest_suit) else: weakest_suit = weakest_suit[0] all_nums = [i+2 for i in range(13)] weak_nums = [num for suit, num in zip(card_suits, card_nums) if suit == weakest_suit] [all_nums.remove(num) for num in weak_nums] return weakest_suit*100 + max(all_nums) def make_a_play(self, sub_state): """ :param sub_state: :return: int - card value """ # TODO: Recall last round and update memory # Get valid plays if sub_state == 0: valid_plays = self.get_valid_plays(True) else: valid_plays = self.get_valid_plays(False) n_cards = len(valid_plays) card_viability = [1] * n_cards card_nums = [cards.get_card_number(play) for play in valid_plays] card_suits = [cards.get_card_suit(play) for play in valid_plays] high_cards = [max(card_set) if card_set else 0 for card_set in self.unplayed_cards] suit_counts = [0] * 4 for i in range(4): suit_counts[i] = card_suits.count(i+1) non_empty_suits = [i+1 for i, count in enumerate(suit_counts) if count] suit_counts = [count for count in suit_counts if count] min_suit_count = min(suit_counts) low_suits = [suit for suit, counts in zip(non_empty_suits, suit_counts) if counts == min_suit_count] for i in range(n_cards): card_viability[i] += any([card_suits[i] == s for s in low_suits]) / min_suit_count # Leading-specific viability if sub_state == 0: for i in range(n_cards): card_viability[i] += any([valid_plays[i] == card for card in high_cards]) * 1.2 else: played_cards = [card.value if card else None for card in self.table_status["played cards"]] played_nums = [cards.get_card_number(card) if card else 0 for card in played_cards] played_suits = [cards.get_card_suit(card) if card else 0 for card in played_cards] trumped = any([suit == self.table_status['trump suit'] for suit in played_suits]) max_played_num = max(played_nums) max_trump_played = [num for suit, num in zip(played_suits, played_nums) if suit == self.table_status['trump suit']] if max_trump_played: max_trump_played = max(max_trump_played) else: max_trump_played = 1 for i in range(n_cards): if trumped and card_suits[i] != self.table_status['trump suit']: card_viability[i] *= card_nums[i]/7 card_viability[i] += (card_nums[i] < max_played_num) / card_nums[i] if card_suits[i] == self.table_status['trump suit'] and\ card_nums[i]>max_trump_played : card_viability[i] *= 2 / card_nums[i] best_viability = max(card_viability) best_cards = [play for viability, play in zip(card_viability, valid_plays) if viability == best_viability] return random.choice(best_cards) def update_memory(self): played_cards = [card.value for card in self.table_status["played cards"]] for val in played_cards: suit = cards.get_card_suit(val) num = cards.get_card_number(val) self.unplayed_cards[suit-1].remove(num) def reset_memory(self): self.unplayed_cards = [] [self.unplayed_cards.append([i+2 for i in range(13)]) for _ in range(4)] def estimate_wins(self): player_cards = self.player.get_deck_values() card_suits = [cards.get_card_suit(crd) for crd in player_cards] card_nums = [cards.get_card_number(crd) for crd in player_cards] n_cards = [] for i in range(4): n_cards.append(card_suits.count(i+1)) bids = [0] * 5 trump_points = [num-10 if num >= 10 else 0.001 for num in card_nums] non_trump_points = [self.calc_win_points(num, n_cards[suit-1]) if num > 10 else 0.001 for (num, suit) in zip(card_nums, card_suits)] for trump_call in range(5): for suit in range(4): valid_cards = [crd_suit == suit+1 for crd_suit in card_suits] if suit == trump_call: points = sum([pts for valid, pts in zip(valid_cards, trump_points) if valid]) bids[trump_call] += points*n_cards[suit] * self.weigh1 else: points = sum([pts for valid, pts in zip(valid_cards, non_trump_points) if valid]) bids[trump_call] += points*math.log(n_cards[suit]+1) * self.weigh2 return bids def calc_win_points(self, card_num, n_cards): """ Calculate the points which affects the bidding decision depending on which card is considered and the number of card available :param card_num: int 2-14 :param n_cards: int :return: float score """ num = max(0, card_num-10) if not n_cards: return 0 if num <= n_cards: return math.exp(n_cards-1)-1 return 19.167/n_cards