commit ebf938c48cc8f8f15cb62226c240620a102c6e8c Author: Rory Healy Date: Sat Jun 8 21:22:26 2024 +1000 Initial commit. Note - this is the original assignment submission. No modifications have been made. diff --git a/part1.py b/part1.py new file mode 100644 index 0000000..0e73155 --- /dev/null +++ b/part1.py @@ -0,0 +1,217 @@ +# Title: Project 3 - Group scoring +# Author: Rory Healy +# Date created - 17th May 2019 +# Date modified - 30th May 2019 + +def is_n_of_a_kind(cards): + '''Takes a list of cards and returns a Boolean value based on if those + cards are said to be "N-of-a-kind".''' + + # Analyse the numbers of the cards to determine validity. + numbers_of_cards = [] + for i in range(len(cards)): + numbers_of_cards.append(cards[i][:-1]) + if numbers_are_same(numbers_of_cards): + return True + return False + +def numbers_are_same(numbers): + '''Takes a list of strings, "numbers", and returns True if all the numbers + in the list of strings are the same, and False otherwise.''' + original_number = numbers[0] + for i in range(len(numbers)): + if numbers[i] != original_number: + return False + return True + +def is_run(cards): + '''Determines if a set of cards are said to be a "valid run" and returns a + corresponding Boolean value.''' + + # Extracts the suits of all the cards in the list. + suits_of_cards = [] + colour_of_cards = '' + red_cards = 'DH' + for i in range(len(cards)): + suits_of_cards.append(cards[i][-1]) + for card in suits_of_cards: + if card in red_cards: + colour_of_cards += 'R' + else: + colour_of_cards += 'B' + + # Extracts the numbers of the cards in the list. + numbers_of_cards = [] + for i in range(len(cards)): + numbers_of_cards.append(cards[i][:-1]) + + # Return True/False based on the validity of the suits and numbers. + if suits_are_alternating(colour_of_cards) and \ + numbers_are_monotonic(numbers_of_cards): + return True + return False + +def suits_are_alternating(suits): + '''Takes a string "suits" and returns True if the suits of the string are + alternating, and False otherwise.''' + for i in range(1, len(suits)): + if suits[i] == suits[i - 1]: + return False + return True + +def numbers_are_monotonic(numbers): + '''Takes a list of strings of numbers and returns True if the numbers in + the list are monotonic (i.e. strictly increasing or strictly decreasing), + where aces can represent any number.''' + + # Replaces Aces with their numerical value, based on the values of the + # cards adjacent to the Ace. + for i in range(len(numbers)): + if numbers[i] == 'A': + if (i == 0) or (i == (len(numbers) - 1)): + return False + + # The value the a is actually taking depends on whether the cards + # are increasing or decreasing in value, so this step is necessary + # for dealing with Aces. + if int(numbers[0]) < int(numbers[-1]): + a_actually_is = int(numbers[i - 1]) + 1 + elif int(numbers[0]) > int(numbers[-1]): + a_actually_is = int(numbers[i + 1]) + 1 + elif int(numbers[0]) == int(numbers[-1]): + return False + + # Validating that the ace represents the correct number. + a_should_represent = (int(numbers[i + 1]) + int(numbers[i - 1])) \ + /2 + if a_should_represent != a_actually_is: + return False + else: + numbers.insert(i, str(a_actually_is)) + numbers.remove('A') + + # Checks if the numbers are consecutive. + for i in range(len(numbers)): + if int(numbers[0]) == int(numbers[-1]): + return False + + # For runs that are ascending. + elif int(numbers[0]) < int(numbers[-1]): + if i == 0: + if int(numbers[0]) != (int(numbers[1]) - 1): + return False + + elif i == (len(numbers) - 1): + if int(numbers[i]) != (int(numbers[i - 1]) + 1): + return False + + elif int(numbers[i]) != (int(numbers[i - 1]) + 1): + return False + + # For runs that are descending. + elif int(numbers[0]) > int(numbers[-1]): + if i == 0: + if int(numbers[0]) != (int(numbers[1]) + 1): + return False + + elif i == (len(numbers) - 1): + if int(numbers[i]) != (int(numbers[i - 1]) - 1): + return False + + elif int(numbers[i]) != (int(numbers[i - 1]) - 1): + return False + + # If the run passes all of these cases, then it must be a valid run. + return True + +def convert_0jqk(cards): + '''Takes a list of cards and converts special numbers (0, J, Q, K) to + their respective values (10, 11, 12, 13) respectively.''' + + # Replaces letters with numbers. + for i in range(len(cards)): + # Replaces Kings with their numerical value - 13. + if cards[i][0] == 'K': + suit = cards[i][1] + cards[i] = '13' + suit + + # Replaces Queens with their numerical value - 12. + elif cards[i][0] == 'Q': + suit = cards[i][1] + cards[i] = '12' + suit + + # Replaces Jacks with their numerical value - 11. + elif cards[i][0] == 'J': + suit = cards[i][1] + cards[i] = '11' + suit + + # Replaces '0', representing 10, with its numerical value. + elif cards[i][0] == '0': + suit = cards[i][1] + cards[i] = '10' + suit + return cards + +def factorial(num): + '''Calculates the factorial of the number and returns it.''' + if num == 1: + return 1 + else: + return num * factorial(num - 1) + +def order_a_list(cards): + '''Orders a list of cards based their numerical value with Aces last.''' + ordered_list = [] + numbers_of_cards = [] + aces = [] + for i in range(len(cards)): + if cards[i][:-1] == 'A': + aces.append('A') + else: + numbers_of_cards.append(cards[i][:-1]) + + numbers_of_cards.sort(key=int) + numbers_of_cards += aces + for number in numbers_of_cards: + for card in cards: + if number == card[:-1]: + if card in ordered_list: + continue + ordered_list.append(number + card[-1]) + return ordered_list + +def comp10001go_score_group(cards): + '''Takes a list of cards (each a 2-element string, where the first letter + is the card value and the second letter is the card suit), and returns the + calculated score of that set of cards as an integer.''' + + score = 0 + cards = convert_0jqk(cards) + cards = order_a_list(cards) + + # Given a list, identifies an n-of-a-kind case or a run. + if len(cards) > 1: + for i in range(2, len(cards) + 1): + # Handles the case where all of the cards are n-of-a-kind. + if is_n_of_a_kind(cards): + score += int(cards[0][:-1]) * factorial(len(cards)) + return score + + # Handles the case where only some of the cards are n-of-a-kind + # and the rest aren't. + elif not is_n_of_a_kind(cards[0:i]): + score += int(cards[0][:-1]) * factorial(i - 1) + value = int(cards[0][:-1]) + for j in range(i): + if int(cards[0][:-1]) == value: + cards.pop(0) + + # Any leftover cards have their values taken from the total score. + if len(cards) > 0: + for card in cards: + value = card[:-1] + if value == 'A': + value = 20 + else: + value = int(value) + score -= value + return score diff --git a/part2.py b/part2.py new file mode 100644 index 0000000..764a0a4 --- /dev/null +++ b/part2.py @@ -0,0 +1,180 @@ +# Title: Project 3 - Group validation +# Author: Rory Healy +# Date created - 23rd May 2019 +# Date modified - 30th May 2019 + +def is_n_of_a_kind(cards): + '''Takes a list of cards and returns a Boolean value based on if those + cards are said to be "N-of-a-kind".''' + + # Analyse the numbers of the cards to determine validity. + numbers_of_cards = [] + for i in range(len(cards)): + numbers_of_cards.append(cards[i][:-1]) + if numbers_are_same(numbers_of_cards): + return True + return False + +def numbers_are_same(numbers): + '''Takes a list of strings, "numbers", and returns True if all the numbers + in the list of strings are the same, and False otherwise.''' + original_number = numbers[0] + for i in range(len(numbers)): + if numbers[i] != original_number: + return False + return True + +def is_run(cards, aces): + '''Takes two lists of cards (represented as strings), cards and aces, and + returns a list of the lists of runs that are possible with all the given + cards.''' + + return_list = [] + possible_runs = [] + original_cards_len = len(cards) + for i in range(1, len(cards)): + current_card = cards[0] + current_run = [] + if len(possible_runs) > 0: + # This is to prevent any IndexErrors due to the use of a while + # loop within a for loop. + for run in possible_runs: + if len(run) + len(cards) != original_cards_len: + break + else: + continue + break + n = 1 + while n < len(cards): + if len(cards) == 1: + break + # This will check that the cards form a run. + elif int(current_card[:-1]) == int(cards[i][:-1]) - n: + current_run.append(cards[i]) + cards.remove(cards[i]) + n += 1 + if len(current_run) > 0: + current_run.append(current_card) + possible_runs.append(current_run) + cards.remove(current_card) + + # Numerically sorts the runs. + for i in range(len(possible_runs)): + current_run = order_a_list(possible_runs[i]) + possible_runs.pop(i) + possible_runs.insert(i, current_run) + + # Inserts any aces where necessary. + for run in possible_runs: + for i in range(len(run)): + if i == len(run): + break + elif int(run[i][:-1]) != (int(run[i + 1][:-1]) - 1): + if len(aces) > 0: + run.insert(i + 1, aces[0]) + aces.pop(0) + break + else: + for card in run: + cards.append(card) + possible_runs.remove(run) + break + + # Checks for alternating suits. + for run in possible_runs: + if not suits_are_alternating(run): + for card in run: + cards.append(card) + run.remove(card) + + # Returns all the runs and leftover cards. + return_list.append(possible_runs) + return_list.append(cards) + return_list.append(aces) + return return_list + +def suits_are_alternating(suits): + '''Takes a string "suits" and returns True if the suits of the string are + alternating, and False otherwise.''' + for i in range(1, len(suits)): + if suits[i] == suits[i - 1]: + return False + return True + +def convert_0jqk(cards): + '''Takes a list of cards and converts special numbers (0, J, Q, K) to + their respective values (10, 11, 12, 13) respectively.''' + + # Replaces letters with numbers. + for i in range(len(cards)): + # Replaces Kings with their numerical value - 13. + if cards[i][0] == 'K': + suit = cards[i][1] + cards[i] = '13' + suit + + # Replaces Queens with their numerical value - 12. + elif cards[i][0] == 'Q': + suit = cards[i][1] + cards[i] = '12' + suit + + # Replaces Jacks with their numerical value - 11. + elif cards[i][0] == 'J': + suit = cards[i][1] + cards[i] = '11' + suit + + # Replaces '0', representing 10, with its numerical value. + elif cards[i][0] == '0': + suit = cards[i][1] + cards[i] = '10' + suit + return cards + +def order_a_list(cards): + '''Orders a list of cards based their numerical value with Aces last.''' + ordered_list = [] + numbers_of_cards = [] + aces = [] + for i in range(len(cards)): + if cards[i][:-1] == 'A': + aces.append('A') + else: + numbers_of_cards.append(cards[i][:-1]) + + numbers_of_cards.sort(key=int) + numbers_of_cards += aces + for number in numbers_of_cards: + for card in cards: + if number == card[:-1]: + if card in ordered_list: + continue + ordered_list.append(number + card[-1]) + return ordered_list + +def comp10001go_valid_groups(groups): + '''Takes a list of lists of cards (stored as strings) and returns True or + False depending on if it matches the conditions of valiidty as specified + in the rules.''' + + # Handles empty lists. + if not groups: + return True + + for group in groups: + + # Converts all cards in the group to actual numbers (excluding aces) + group = convert_0jqk(group) + group = order_a_list(group) + + # Checks if the group is either an n-of-a-kind or a run + if not is_n_of_a_kind(group): + return False + + elif len(group) == 1: + aces = [] + for card in group: + if card[0] == 'A': + aces.append(card) + group.remove(card) + runs = is_run(group, aces) + if len(runs[0]) == 0 and len(group) != 1: + return False + return True diff --git a/part3.py b/part3.py new file mode 100644 index 0000000..9507ac1 --- /dev/null +++ b/part3.py @@ -0,0 +1,350 @@ +# Title: Project 3 - Group validation +# Author: Rory Healy +# Date created - 27th May 2019 +# Date modified - 30th May 2019 + +def convert_0jqk(cards): + '''Takes a list of cards and converts special numbers (0, J, Q, K) to + their respective values (10, 11, 12, 13) respectively.''' + + # Replaces letters with numbers. + for i in range(len(cards)): + # Replaces Kings with their numerical value - 13. + if cards[i][0] == 'K': + suit = cards[i][1] + cards[i] = '13' + suit + + # Replaces Queens with their numerical value - 12. + elif cards[i][0] == 'Q': + suit = cards[i][1] + cards[i] = '12' + suit + + # Replaces Jacks with their numerical value - 11. + elif cards[i][0] == 'J': + suit = cards[i][1] + cards[i] = '11' + suit + + # Replaces '0', representing 10, with its numerical value. + elif cards[i][0] == '0': + suit = cards[i][1] + cards[i] = '10' + suit + return cards + +def convert_10111213(cards): + '''Takes a list of cards and converts values (such as 10, 11, 12 and 13) to + their respective card representation (0, J, Q, K).''' + + for i in range(len(cards)): + # Replaces 13 with its value - K. + if cards[i][:-1] == '13': + suit = cards[i][-1] + cards[i] = 'K' + suit + + # Replaces 12 with its value - Q. + elif cards[i][:-1] == '12': + suit = cards[i][-1] + cards[i] = 'Q' + suit + + # Replaces 11 with its value - J. + elif cards[i][:-1] == '11': + suit = cards[i][-1] + cards[i] = 'J' + suit + + # Replaces 10 with its value - 0. + elif cards[i][:-1] == '10': + suit = cards[i][-1] + cards[i] = '0' + suit + return cards + +def suits_are_alternating(suits): + '''Takes a list of cards and returns True if the suits of the cards are + alternating, and False otherwise.''' + for i in range(1, len(suits)): + if suits[i][1] == suits[i - 1][1]: + return False + return True + +def order_a_list(cards): + '''Orders a list of cards based their numerical value with Aces last.''' + ordered_list = [] + numbers_of_cards = [] + aces = [] + for i in range(len(cards)): + if cards[i][:-1] == 'A': + aces.append('A') + else: + numbers_of_cards.append(cards[i][:-1]) + + numbers_of_cards.sort(key=int) + numbers_of_cards += aces + for number in numbers_of_cards: + for card in cards: + if number == card[:-1]: + if card in ordered_list: + continue + ordered_list.append(number + card[-1]) + return ordered_list + +def comp10001go_play(discard_history, player_no, hand): + '''Takes a list of lists, "discard_history", of the previous cards played + by each of the 4 players, an integer "player_no", and a list of cards held + by the player "hand".''' + + # Handles the case where there is only 1 card left. + if len(hand) == 1: + return hand[0] + + # Copies the hand to a list that can be changed without affecting the hand. + hhand = [] + for card in hand: + hhand.append(card) + + # Sorts the hand by numerical order, and also sorts the discard pile for + # the current player by numerical order. + hhand = convert_0jqk(hhand) + hhand = order_a_list(hhand) + + current_discard = [] + for turn in discard_history: + current_discard.append(turn[player_no]) + current_discard = convert_0jqk(current_discard) + current_discard = order_a_list(current_discard) + + # Extracts the numbers and suits from the discarded pile. + discarded_numbers = [] + discarded_suits = [] + red_cards = 'DH' + + for card in current_discard: + discarded_numbers.append(card[:-1]) + discarded_suits.append(card[-1]) + + discarded_colours = '' + for card in discarded_suits: + if card in red_cards: + discarded_colours += 'R' + else: + discarded_colours += 'B' + + # Preferences n-of-a-kind scoring to maximise points by returning the card + # that gives the highest scoring n-of-a-kind. + potential_return_cards = [] + for i in range(len(hhand)): + current_card_number = hhand[i][:-1] + for number in discarded_numbers: + if current_card_number == number and number != 'A': + potential_return_cards.append(hand[i]) + + # A frequency list of the values of the cards in the discard pile is + # created, then sorted by the frequency. The card with the highest + # frequency is always preferred over the card with the highest value due to + # the growth of x!. + freq_dict = {} + for number in discarded_numbers: + if number not in freq_dict.keys(): + freq_dict[number] = 1 + else: + freq_dict[number] += 1 + + # Generates a list of tuple of items where the values and keys are swapped. + dict_items = [] + for item in freq_dict.items(): + key, value = item + dict_items.append((value, key)) + + # Sorts the tuples by frequency then flips the tuples again. + dict_items = sorted(dict_items, reverse=True) + sorted_items = [] + for item in dict_items: + value, key = item + sorted_items.append((key, value)) + + # Selects the card with the highest frequency from the list of potential + # cards that could be returned. + for item in sorted_items: + key, value = item + for card in potential_return_cards: + if key == card: + return card + + # If there aren't any cards that fit in a run, this returns the lowest + # valued card to minimise the points subtracted from singleton cards. + hand_numbers = [] + for i in range(len(hhand)): + if hhand[i][:-1] == 'A': + continue + hand_numbers.append(int(hhand[i][:-1])) + + for i in range(len(hand)): + if hhand[i][:-1] == 'A': + continue + elif int(hhand[i][:-1]) == min(hand_numbers): + return hand[i] + +def is_run(cards, aces): + '''Takes two lists of cards (represented as strings), cards and aces, and + returns a list of the lists of runs that are possible with all the given + cards.''' + + return_list = [] + possible_runs = [] + original_cards_len = len(cards) + for i in range(1, len(cards)): + current_card = cards[0] + current_run = [] + if len(possible_runs) > 0: + # This is to prevent any IndexErrors due to the use of a while + # loop within a for loop. + for run in possible_runs: + if len(run) + len(cards) != original_cards_len: + break + else: + continue + break + n = 1 + while n < len(cards): + if len(cards) == 1: + break + # This will check that the cards form a run. + elif int(current_card[:-1]) == int(cards[i][:-1]) - n: + current_run.append(cards[i]) + cards.remove(cards[i]) + n += 1 + if len(current_run) > 0: + current_run.append(current_card) + possible_runs.append(current_run) + cards.remove(current_card) + + # Numerically sorts the runs. + for i in range(len(possible_runs)): + current_run = order_a_list(possible_runs[i]) + possible_runs.pop(i) + possible_runs.insert(i, current_run) + + # Inserts any aces where necessary. + for run in possible_runs: + for i in range(len(run)): + if i == len(run) - 1: + break + elif int(run[i][:-1]) != (int(run[i + 1][:-1]) - 1): + if len(aces) > 0: + run.insert(i + 1, aces[0]) + aces.pop(0) + break + else: + for card in run: + cards.append(card) + possible_runs.remove(run) + break + + # Checks for alternating suits. + for run in possible_runs: + if not suits_are_alternating(run): + for a_card in run: + card = a_card + cards.append(a_card) + possible_runs.remove(run) + + # Returns all the runs and leftover cards. + return_list.append(possible_runs) + return_list.append(cards) + return_list.append(aces) + return return_list + +def comp10001go_group(discard_history, player_no): + '''Takes a list of the lists of cards played by each player each turn, + "discard_history", and an integer "player_no", and returns a list of lists + of cards based on the discard history of the player that could be used in + scoring.''' + + # Extracts the cards that the player has discarded, and orders them into a# + # list. + discarded_cards = [] + discarded_numbers = [] + + for turn in discard_history: + discarded_cards.append(turn[player_no]) + discarded_cards = convert_0jqk(discarded_cards) + discarded_cards = order_a_list(discarded_cards) + + for card in discarded_cards: + number = card[:-1] + discarded_numbers.append(number) + + # Identifies n-of-a-kind cards. + freq_dict = {} + for number in discarded_numbers: + if number not in freq_dict.keys(): + freq_dict[number] = 1 + else: + freq_dict[number] += 1 + + # Generates a list of tuple of items where the values and keys are swapped. + dict_items = [] + for item in freq_dict.items(): + key, value = item + dict_items.append((value, key)) + + # Sorts the tuples by frequency then flips the tuples again. + dict_items = sorted(dict_items, reverse=True) + sorted_items = [] + for item in dict_items: + value, key = item + sorted_items.append([key, value]) + + # Using "sorted_items", we then need to identify cards who's number occurs + # more than once and removes them from "discarded_cards" and add them to a + # list to be returned. + return_list = [] + n_of_a_kind_list = [] + for cards in sorted_items: + if cards[1] > 1: + for card in discarded_cards: + if card[0] == 'A': + continue + elif int(card[:-1]) == int(cards[0]): + n_of_a_kind_card = card + n_of_a_kind_list.append(n_of_a_kind_card) + for card in n_of_a_kind_list: + for cards in discarded_cards: + if card == cards: + discarded_cards.remove(cards) + if len(n_of_a_kind_list) > 1: + return_list.append(n_of_a_kind_list) + + # Using the cards leftover from the previous check, we need to identify + # runs in the list of discarded cards. To do this, the aces are extracted + # and stored as a seperate list, then the first list is checked to see if a + # run is able to be made and if so, then it is added to the returned list. + ace_list = [] + for card in discarded_cards: + if card[0] == 'A': + ace_list.append(card) + for card in ace_list: + for discard in discarded_cards: + if discard == card: + discarded_cards.remove(card) + + runs = is_run(discarded_cards, ace_list) + if len(runs[0]) > 0: + for run in runs[0]: + for card in run: + for discard in discarded_cards: + if discard == card: + discarded_cards.remove(card) + return_list.append(run) + + # Checks for singletons and adds them to a list to be added to the return + # list. + if len(discarded_cards) > 0: + for card in discarded_cards: + return_list.append([card]) + if len(runs[2]) > 0: + for card in runs[2]: + return_list.append([card]) + + for lst in return_list: + tmp = convert_10111213(lst) + return_list.insert(return_list.index(lst), tmp) + return_list.remove(lst) + return return_list diff --git a/project03 b/project03 new file mode 100644 index 0000000..98f69d9 --- /dev/null +++ b/project03 @@ -0,0 +1,197 @@ +-------------------------------------------------------------------------------- +Part 1 - Valid table + +Write a function comp10001huxxy_valid_table() which takes a single argument: + +- groups, a list of lists of cards (each a 2-element string, where the first + letter is the card value and the second letter is the card suit, e.g. '3H' for + the 3 of Hearts), where each list of cards represents a single group on the + table, and the combined list of lists represents the combined groups played to + the table. + +Your function should return a bool, which evaluates whether the table state is +valid or not. Recall from the rules of the game that the table is valid if all +groups are valid, where a group can take one of the following two forms: + +- an N-of-a-kind (i.e. three or more cards of the same value), noting that in + the case of a 3-of-a-kind, each card must have a unique suit (e.g. ['2S', + '2S', '2C'] is not a valid 3-of-a-kind, as the Two of Spades has been played + twice), and if there are 4 or more cards, all suits must be present. +- a run (i.e. a group of 3 or more cards, starting from the lowest-valued card, + and ending with the highest-valued card, forming a continuous sequence in + terms of value, and alternating in colour; note that the specific ordering of + cards in the list is not significant, i.e. ['2C', '3D', '4S'] and ['4S', '2C', + '3D'] both make up the same run. + +Example function calls are as follows: + + >>> comp10001huxxy_valid_table([]) + True + >>> comp10001huxxy_valid_table([['AC']]) + False + >>> # run too short + >>> comp10001huxxy_valid_table([['AC', '2S']]) + False + >>> # run doesn't alternate in colour + >>> comp10001huxxy_valid_table([['AC', '2S', '3H']]) + False + >>> # values not adjacent + >>> comp10001huxxy_valid_table([['AC', '2S', '4H']]) + False + >>> comp10001huxxy_valid_table([['AC', '2H', '3S']]) + True + >>> # test unsorted run + >>> comp10001huxxy_valid_table([['3C', 'AS', '2H']]) + True + >>> comp10001huxxy_valid_table([['0C', 'JH', 'QS', 'KH', '9D']]) + True + >>> # n-of-kind too short + >>> comp10001huxxy_valid_table([['2C', '2H']]) + False + >>> # same suit twice for 3-of-kind + >>> comp10001huxxy_valid_table([['2C', '2H', '2C']]) + False + >>> # same suit twice for 4-of-kind + >>> comp10001huxxy_valid_table([['2C', '2H', '2S', '2C']]) + False + >>> comp10001huxxy_valid_table([['2C', '2H', '2S']]) + True + >>> comp10001huxxy_valid_table([['2C', '2H', '2S', '2D']]) + True + >>> comp10001huxxy_valid_table([['2C', '2H', '2S', '2D', '2S']]) + True + >>> comp10001huxxy_valid_table([['2C', '2H', '2S', '2D', '2S'], + ['0D', '9C', '8H']]) + True + +-------------------------------------------------------------------------------- +Part 2 - Group validation + +Write a function comp10001go_valid_groups() which takes a single argument: + +- groups, a list of groups, each of which is a list of cards (following the same + definition as Part 1) + +Your function should return a Boolean indicating whether all groups are valid or +not (i.e. a singleton card, a valid N-of-a-kind or a valid run). Note that the +function may be used to validate a grouping of partial discards or the full set +of discards, i.e. the total number of cards in groups will be between 0 and 10. + +Example function calls are as follows: + + >>> comp10001go_valid_groups([['KC', 'KH', 'KS', 'KD'], ['2C']]) + True + >>> comp10001go_valid_groups([['KC', 'KH', 'KS', 'AD'], ['2C']]) + False + >>> comp10001go_valid_groups([['KC', 'KH', 'KS', 'KD'], ['2C', '3H']]) + False + >>> comp10001go_valid_groups([]) + True + +-------------------------------------------------------------------------------- +Part 3 - Play and Group! + +The third question requires that you implement the two functions that are called +in the tournament: (1) comp10001_play, which is used to select a discard over +the 10 turns of a game; and (2) comp10001_group, which is used to group the +discards into groups for scoring. We combine these together into a single +question in Grok as a means of validating that you have a complete player that +is qualified to enter the tournament. Note that in each case, we provide only a +single test case (and no hidden test cases) for two reasons: (1) there are very +few game states where there is a single possible option to either play a discard +or group the discards, and testing relies on there only being one possible +output; and (2) the real testing occurs in simulation mode in the actual +tournament, in the form of random games against other players. On validation of +implementations of each of the two functions, you will be given the option to +submit your player to the tournament. + +First, write a function comp10001go_play() which takes three arguments: + +- discard_history, a list of lists of four cards, each representing the discards + from each of the four players in preceding turns (up to 9 turns) in sequence + of player number (i.e. the first element in each list of four cards is for + Player 0, the second is for Player 1, etc.). Note that the list is sequenced + based on the turns, i.e. the first list of four cards corresponds to the first + turn, and the last list of four cards corresponds to the last turn. +- player_no, an integer between 0 and 3 inclusive, indicating which player is + being asked to play. player_no can also be used to determine the discards for + that player from discard_history by indexing within each list of four cards. +- hand, a list of cards held by the player. + +Your function should return a single card to discard from hand. + +An example function call is as follows: + + >>> comp10001go_play([['0S', 'KH', 'AC', '3C'], ['JH', 'AD', 'QS', '5H'], + ['9C', '8S', 'QH', '9S'], ['8C', '9D', '0D', 'JS'], + ['5C', 'AH', '5S', '4C'], ['8H', '2D', '6C', '2C'], + ['8D', '4D', 'JD', 'AS'], ['0H', '6S', '2H', 'KC'], + ['KS', 'KD', '7S', '6H']], 3, ['QC']) + 'QC' + +Second, write a function comp10001go_group() which takes two arguments: + +- discard_history, a list of lists of four cards, each representing the discards + from each of the four players in preceding turns in sequence of player number + (i.e., the first element in each list of four cards is for Player 0, the + second is for Player 1, etc.). Note that the list is sequenced based on the + turns, i.e. the first list of four cards corresponds to the first turn, and + the last list of four cards corresponds to the last turn. Additionally note + that the number of turns contained in discard_history will always be 10. +- player_no, an integer between 0 and 3 inclusive, indicating which player is + being asked to play. player_no can also be used to determine the discards for + that player from discard_history by indexing within each list of four cards. + +Your function should return a list of lists of cards based on the discard +history of player_no, to use in scoring the player. Note that the grouping of +cards represented by the output must be valid (i.e. each list of cards must be a +singleton card, or a valid N-of-a-kind or run), but that the ordering of cards +within groups, and the ordering of groups is not significant. + +An example function call is as follows: + + >>> comp10001go_group([['0S', 'KH', 'AC', '3C'], ['JH', 'AD', 'QS', '5H'], + ['9C', '8S', 'QH', '9S'], ['8C', '9D', '0D', 'JS'], + ['5C', 'AH', '5S', '4C'], ['8H', '2D', '6C', '2C'], + ['8D', '4D', 'JD', 'AS'], ['0H', '6S', '2H', 'KC'], + ['KS', 'KD', '7S', '6H'], ['JC', 'QD', '4H', 'QC']], 3) + [['3C'], ['5H'], ['9S'], ['JS'], ['4C'], + ['2C'], ['AS'], ['KC'], ['6H'], ['QC']] + +-------------------------------------------------------------------------------- +Part 4 - Optimal grouping + +The final question is for bonus marks, and is deliberately quite a bit harder +than the four basic questions (and the number of marks on offer is, as always, +deliberately not commensurate with the amount of effort required). Only attempt +this is you have completed the earlier questions, and are up for a challenge! + +Write a function comp10001go_best_partitions() which takes a single argument: + +- cards, a list of up to 10 cards + +Your function should return a list of list of lists of cards, representing the +groupings of cards that score the most points from cards. Note that the ordering +of the groupings is not significant, and neither is the ordering of the groups +within a grouping, nor the order of cards within a group. + +One area of particular focus with this question is efficiency: there are strict +time limits associated with running your code over each example, that you must +work within, or you will fail the test. Good luck! + +Example function calls are as follows: + + >>> comp10001go_best_partitions(['0H', '8S', '6H', 'AC', '0S', + 'JS', '8C', '7C', '6D', 'QS']) + [[['AC'], ['0H', '0S'], ['JS'], ['8S', '8C'], ['7C'], ['6H', '6D'], ['QS']]] + >>> comp10001go_best_partitions(['9D', '2S', '4D', '4H', '6D', + 'AH', '2C', 'JH', '3C', '9H']) + [[['4D', '4H'], ['6D'], ['AH'], ['2S', '2C'], + ['JH'], ['3C'], ['9D', '9H']]] + >>> comp10001go_best_partitions(['3C', '5H', '9S', 'JS', '4C', + '2C', 'AS', 'KC', '6H', 'QC']) + [[['3C'], ['5H'], ['9S'], ['JS'], ['4C'], ['2C'], ['AS'], ['KC'], ['6H'], ['QC']]] + >>> comp10001go_best_partitions(['0D', 'AS', '5C', '8H', + 'KS', 'AH', 'QH', 'AC']) + [[['AS', '5C', '8H', 'AH'], ['0D', 'KS', 'QH', 'AC']], + [['0D', 'AS', 'KS', 'QH'], ['5C', '8H', 'AH', 'AC']]] diff --git a/project03-rubric.pdf b/project03-rubric.pdf new file mode 100755 index 0000000..4b1f087 Binary files /dev/null and b/project03-rubric.pdf differ diff --git a/project03-sample-solutions.py b/project03-sample-solutions.py new file mode 100644 index 0000000..02e9d5e --- /dev/null +++ b/project03-sample-solutions.py @@ -0,0 +1,1076 @@ +# ------------------------------------------------------------------------------ +# Part 1 - Valid table + +VALUE = 0 +VALUES = {'A': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, + '9': 9, '0': 10, 'J': 11, 'Q': 12, 'K': 13} +SUIT = COLOUR = 1 +COLOURS = {'D': True, 'H': True, 'C': False, 'S': False} +N_SUITS = 4 + +def comp10001huxxy_valid_table(groups): + """ Returns a boolean indicating whether list of groups `groups` + (containing card strings) represents a valid table. + """ + # Checks each group for validity conditions + for group in groups: + if not valid_group(group): + return False + return True + +def valid_group(group): + """ Returns a boolean indicating whether list of card strings `group` + represents a valid group: either a run or n-of-a-kind. + """ + # Groups must be either empty or of length three or greater + if len(group) == 0: + return True + elif len(group) < 3: + return False + else: + return valid_run(group) or valid_n_of_a_kind(group) + +def valid_run(group): + """ Returns a boolean indicating whether list of card strings `group` + (which is of a valid length) represents a valid run. + """ + # Generates list of cards in format (value: int, is_red: bool) so that it + # can be sorted by card value + cards = [] + for card in group: + curr_value = VALUES[card[VALUE]] + curr_colour = COLOURS[card[SUIT]] + cards.append((curr_value, curr_colour)) + cards.sort() + + # Processes cards one-by-one, checking for violation of run group rules + prev_card = cards[0] + for card in cards[1:]: + # Value increases by 1 for adjacent cards in the run + if card[VALUE] != prev_card[VALUE] + 1 or \ + card[COLOUR] == prev_card[COLOUR]: + return False + prev_card = card + return True + +def valid_n_of_a_kind(group): + """ Returns a boolean indicating whether list of card strings `group` + (which is of a valid length) represents a valid n-of-a-kind group. + """ + # Checks that each card has the same value, while adding suits to set + value = group[0][VALUE] + suits = {group[0][SUIT]} + for card in group[1:]: + if card[VALUE] != value: + return False + suits.add(card[SUIT]) + + # Checks that a group of four or less cards contains no duplicate suits + if len(group) <= N_SUITS: + return len(suits) == len(group) + # Checks that a group of more than four contains one of each suit + else: + return len(suits) == N_SUITS + +# ------------------------------------------------------------------------------ +# Part 2 - Group validation + +# Sample solution 1 + +from math import factorial + +# index of value of a card +VALUE = 0 + +# index of suit of a card +SUIT = 1 + +# value of Ace +ACE = 'A' + +# dictionary of scores of individual cards +card_score = { + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9, + '0': 10, + 'J': 11, + 'Q': 12, + 'K': 13, + ACE: 20, + } + +# suits which are red +RED_SUITS = 'HD' + +# suits which are black +BLACK_SUITS = 'SC' + +# card colours +RED = 1 +BLACK = 2 + +# minimum no. of cards in an n-of-a-kind set +MIN_CARDS_NKIND = 2 + +# minimum no. of non-Ace cards in a run +MIN_NONACE_RUN = 2 + +# minimum no. cards in a run +MIN_RUN = 3 + +def is_ace(card): + """Boolean evaluation of whether `card` is an Ace""" + return card[VALUE] == ACE + + +def get_score(card): + """return the score of `card`, based on its value""" + return card_score[card[VALUE]] + + +def get_colour(card): + """Return the colour of `card` (`RED` or `BLACK`)""" + if card[SUIT] in RED_SUITS: + return RED + else: + return BLACK + + +def comp10001go_score_group(cards): + """Validate/score a group of cards (order unimportant), supplied as a + list of cards (each a string); return the positive score of the group if + valid, and negative score otherwise. Note, assumes that all cards are + valid, and unique.""" + + # construct sorted list of values of cards (ignore suit for now) + values = sorted([get_score(card) for card in cards]) + + # CASE 1: N-of-a-kind if all cards of same value, at least + # `MIN_CARDS_NKIND` cards in total, and not Aces + if (len(set(values)) == 1 and len(cards) >= MIN_CARDS_NKIND + and not is_ace(cards[0])): + return factorial(len(cards)) * card_score[cards[0][VALUE]] + + # construct sorted list of non-Ace cards + nonace_cards = sorted([card for card in cards if not is_ace(card)], + key=lambda x: get_score(x)) + + # construct list of Ace cards + ace_cards = list(set(cards) - set(nonace_cards)) + + # run must have at least `MIN_NONACE_RUN` non-Ace cards in it + if len(nonace_cards) >= MIN_NONACE_RUN: + + is_run = True + prev_val = prev_colour = None + score = 0 + + # iterate through cards to make sure they form a run + for card in nonace_cards: + + # CASE 1: for the first card in `nonace_cards`, nothing to + # check for + if prev_val is None: + score = prev_val = get_score(card) + prev_colour = get_colour(card) + + # CASE 2: adjacent to previous card in value + elif get_score(card) - prev_val == 1: + + # CASE 2.1: alternating colour, meaning continuation of run + if get_colour(card) != prev_colour: + prev_val = get_score(card) + prev_colour = get_colour(card) + score += prev_val + # CASE 2.2: not alternating colour, meaning invalid run + else: + is_run = False + break + + # CASE 3: repeat value, meaning no possibility of valid run + elif get_score(card) == prev_val: + is_run = False + break + + # CASE 4: gap in values, in which case check to see if can be + # filled with Ace(s) + else: + gap = get_score(card) - prev_val - 1 + + gap_filled = False + # continue until gap filled + while is_run and gap and len(ace_cards) >= gap: + + gap_filled = False + + # search for an Ace of appropriate colour, and remove + # from list of Aces if found (note that it doesn't matter + # which Ace is used if multiple Aces of same colour) + for i, ace in enumerate(ace_cards): + if get_colour(ace) != prev_colour: + ace_cards.pop(i) + prev_val += 1 + prev_colour = get_colour(ace) + score += prev_val + gap -= 1 + gap_filled = True + break + + if not gap_filled: + is_run = False + + if is_run and gap_filled and get_colour(card) != prev_colour: + prev_val = get_score(card) + prev_colour = get_colour(card) + score += prev_val + else: + is_run = False + + if is_run and len(cards) >= MIN_RUN and not ace_cards: + return score + + return -sum(values) + + +def comp10001go_valid_groups(groups): + for cards in groups: + if not cards or (len(cards) > 1 + and comp10001go_score_group(cards) < 0): + return False + return True + +# Sample solution 2 + +import math + +SUIT_TO_COLOUR = dict(zip('HDCS', 'RRBB')) +VALUE_STRING_TO_VALUE = dict(zip('A234567890JQK', range(1, 14))) +VALUE_TO_VALUE_STRING = {v: k for k, v in VALUE_STRING_TO_VALUE.items()} + +class Card: + def __init__(self, card_string): + if isinstance(card_string, tuple): + card_string = VALUE_TO_VALUE_STRING[card_string[0]] + card_string[1] + self.value_str = card_string[0] + self.value = VALUE_STRING_TO_VALUE[self.value_str] + self.suit = card_string[1] + self.colour = SUIT_TO_COLOUR[self.suit] + self.inv_colour = 'R' if self.colour == 'B' else 'B' + self.orphan_value = -20 if self.is_ace() else -self.value + + def __eq__(self, other): + return self.value_str == other.value_str and self.suit == other.suit + + def __repr__(self): + return f'Card(\'{self.value_str}{self.suit}\')' + + def __str__(self): + return f'{self.value_str}{self.suit}' + + def is_ace(self): + return self.value_str == 'A' + + def is_black(self): + return self.colour == 'B' + + def is_king(self): + return self.value_str == 'K' + + def is_red(self): + return self.colour == 'R' + + +def construct_n_of_a_kind(cards): + # Early bail if we don't have enough cards. + if len(cards) < 2: + return None + + # Ensure that all of the cards have the same value and are not an Ace. + value = None + for card in cards: + if card.is_ace(): + return None + elif value is None: + value = card.value + elif card.value != value: + return None + + # Return the cards as is. + return list(cards) + + +def construct_run(cards): + # Early bail if we don't have enough cards. + if len(cards) < 3: + return None + + # Partition the cards into Aces and non-Aces. + non_aces = [] + aces_by_colour = {'B': [], 'R': []} + for card in cards: + if card.is_ace(): + aces_by_colour[card.colour].append(card) + else: + non_aces.append(card) + + # Ensure we have enough non-Aces. + if len(non_aces) < 2: + return None + + # Sort the non-Aces by value. + non_aces.sort(key=lambda card: card.value) + + # Attempt to construct a valid run from the avaialble cards. + prev = non_aces.pop(0) + run = [prev] + while non_aces: + top = non_aces[0] + + # Check for a normal valid transition. + if prev.value + 1 == top.value and prev.colour == top.inv_colour: + run.append(non_aces.pop(0)) # Consume the current card in the run. + prev = top + else: + # Check if we can do an Ace insertion. + aces = aces_by_colour[prev.inv_colour] + if aces and not prev.is_king(): # Can't go higher than a King for Ace insertion. + ace = aces.pop(0) # Consume the next Ace. + run.append(ace) + prev = Card((prev.value + 1, ace.suit)) + else: + # We did not find a valid transition. + return None + + # If we have any aces left over, we do not have a valid run. + if aces_by_colour['B'] or aces_by_colour['R']: + return None + + return run + + +def score_n_of_a_kind(cards): + return cards[0].value * math.factorial(len(cards)) + + +def score_orphans(cards): + return sum(map(lambda card: card.orphan_value, cards)) + + +def score_run(cards): + return sum(range(cards[0].value, cards[-1].value + 1)) + +def comp10001go_valid_groups(group_strings): + # Convert the card strings to Card objects. + card_groups = [list(map(Card, card_strings)) for card_strings in group_strings] + + # Attempt to shape each of the groups of cards into the three allowed shapes. + for cards in card_groups: + if construct_n_of_a_kind(cards) is not None: + pass + elif construct_run(cards) is not None: + pass + elif len(cards) == 1: + pass + else: + return False + + # If all groups of cards were one of the three allowed shapes, we're good. + return True + +# ------------------------------------------------------------------------------ +# Part 3 - Play and Group! + +# Sample solution 1 + +from math import factorial + +# index of value of a card +VALUE = 0 + +# index of suit of a card +SUIT = 1 + +# value of Ace +ACE = 'A' + +# dictionary of scores of individual cards +card_score = { + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9, + '0': 10, + 'J': 11, + 'Q': 12, + 'K': 13, + ACE: 20, + } + +# suits which are red +RED_SUITS = 'HD' + +# suits which are black +BLACK_SUITS = 'SC' + +# card colours +RED = 1 +BLACK = 2 + +# minimum no. of cards in an n-of-a-kind set +MIN_CARDS_NKIND = 2 + +# minimum no. of non-Ace cards in a run +MIN_NONACE_RUN = 2 + +# minimum no. cards in a run +MIN_RUN = 3 + + + +def is_ace(card): + """Boolean evaluation of whether `card` is an Ace""" + return card[VALUE] == ACE + + +def get_score(card): + """return the score of `card`, based on its value""" + return card_score[card[VALUE]] + + +def get_colour(card): + """Return the colour of `card` (`RED` or `BLACK`)""" + if card[SUIT] in RED_SUITS: + return RED + else: + return BLACK + + +def comp10001go_score_group(cards): + """Validate/score a group of cards (order unimportant), supplied as a + list of cards (each a string); return the positive score of the group if + valid, and negative score otherwise. Note, assumes that all cards are + valid, and unique.""" + + # construct sorted list of values of cards (ignore suit for now) + values = sorted([get_score(card) for card in cards]) + + # CASE 1: N-of-a-kind if all cards of same value, at least + # `MIN_CARDS_NKIND` cards in total, and not Aces + if (len(set(values)) == 1 and len(cards) >= MIN_CARDS_NKIND + and not is_ace(cards[0])): + return factorial(len(cards)) * card_score[cards[0][VALUE]] + + # construct sorted list of non-Ace cards + nonace_cards = sorted([card for card in cards if not is_ace(card)], + key=lambda x: get_score(x)) + + # construct list of Ace cards + ace_cards = list(set(cards) - set(nonace_cards)) + + # run must have at least `MIN_NONACE_RUN` non-Ace cards in it + if len(nonace_cards) >= MIN_NONACE_RUN: + + is_run = True + prev_val = prev_colour = None + score = 0 + + # iterate through cards to make sure they form a run + for card in nonace_cards: + + # CASE 1: for the first card in `nonace_cards`, nothing to + # check for + if prev_val is None: + score = prev_val = get_score(card) + prev_colour = get_colour(card) + + # CASE 2: adjacent to previous card in value + elif get_score(card) - prev_val == 1: + + # CASE 2.1: alternating colour, meaning continuation of run + if get_colour(card) != prev_colour: + prev_val = get_score(card) + prev_colour = get_colour(card) + score += prev_val + # CASE 2.2: not alternating colour, meaning invalid run + else: + is_run = False + break + + # CASE 3: repeat value, meaning no possibility of valid run + elif get_score(card) == prev_val: + is_run = False + break + + # CASE 4: gap in values, in which case check to see if can be + # filled with Ace(s) + else: + gap = get_score(card) - prev_val - 1 + + gap_filled = False + # continue until gap filled + while is_run and gap and len(ace_cards) >= gap: + + gap_filled = False + + # search for an Ace of appropriate colour, and remove + # from list of Aces if found (note that it doesn't matter + # which Ace is used if multiple Aces of same colour) + for i, ace in enumerate(ace_cards): + if get_colour(ace) != prev_colour: + ace_cards.pop(i) + prev_val += 1 + prev_colour = get_colour(ace) + score += prev_val + gap -= 1 + gap_filled = True + break + + if not gap_filled: + is_run = False + + if is_run and gap_filled and get_colour(card) != prev_colour: + prev_val = get_score(card) + prev_colour = get_colour(card) + score += prev_val + else: + is_run = False + + if is_run and len(cards) >= MIN_RUN and not ace_cards: + return score + + return -sum(values) + + +def comp10001go_valid_groups(groups): + for cards in groups: + if not cards or (len(cards) > 1 + and comp10001go_score_group(cards) < 0): + return False + return True + + +def comp10001go_score_groups(groups): + score = 0 + for group in groups: + score += comp10001go_score_group(group) + return score + + +def comp10001go_randplay(discard_history, player_no, hand): + + from random import shuffle + + shuffle(hand) + + # for first turn, select lowest card + return hand[0] + + +def comp10001go_play(discard_history, player_no, hand): + + # for first turn, select lowest card + if not discard_history: + return sorted(hand, key=lambda x: get_score(x))[0] + + # for subseuquent rounds, select card which maximises optimal score + else: + return sorted(hand, key=lambda x: get_score(x))[0] + + + +def comp10001go_group(discard_history, player_no): + + # construct list of discards from `discard_history` + discards = [] + for turn in discard_history: + discards.append(turn[player_no]) + + return [[card] for card in discards] + +# Sample solution 2 +SUIT_TO_COLOUR = dict(zip('HDCS', 'RRBB')) +VALUE_STRING_TO_VALUE = dict(zip('A234567890JQK', range(1, 14))) +VALUE_TO_VALUE_STRING = {v: k for k, v in VALUE_STRING_TO_VALUE.items()} + + +class Card: + def __init__(self, card_string): + if isinstance(card_string, tuple): + card_string = VALUE_TO_VALUE_STRING[card_string[0]] + card_string[1] + self.value_str = card_string[0] + self.value = VALUE_STRING_TO_VALUE[self.value_str] + self.suit = card_string[1] + self.colour = SUIT_TO_COLOUR[self.suit] + self.inv_colour = 'R' if self.colour == 'B' else 'B' + self.orphan_value = -20 if self.is_ace() else -self.value + + def __eq__(self, other): + return self.value_str == other.value_str and self.suit == other.suit + + def __repr__(self): + return f'Card(\'{self.value_str}{self.suit}\')' + + def __str__(self): + return f'{self.value_str}{self.suit}' + + def is_ace(self): + return self.value_str == 'A' + + def is_black(self): + return self.colour == 'B' + + def is_king(self): + return self.value_str == 'K' + + def is_red(self): + return self.colour == 'R' + +def comp10001go_play(discard_history, player_no, hand): + # Convert the card strings to Card objects. + discard_history = [list(map(Card, card_strings)) for card_strings in discard_history] + hand = list(map(Card, hand)) + + # Greedily discard the smallest valued card in my hand. + hand.sort(key=lambda card: card.orphan_value) + + return str(hand[0]) + +def comp10001go_group(discard_history, player_no): + return [[discards[player_no]] for discards in discard_history] + +# ------------------------------------------------------------------------------ +# Part 4 - Optimal grouping + +# Sample solution 1 + +from math import factorial + +# index of value of a card +VALUE = 0 + +# index of suit of a card +SUIT = 1 + +# value of Ace +ACE = 'A' + +# dictionary of scores of individual cards +card_score = { + '2': 2, + '3': 3, + '4': 4, + '5': 5, + '6': 6, + '7': 7, + '8': 8, + '9': 9, + '0': 10, + 'J': 11, + 'Q': 12, + 'K': 13, + ACE: 20, + } + +# suits which are red +RED_SUITS = 'HD' + +# suits which are black +BLACK_SUITS = 'SC' + +# card colours +RED = 1 +BLACK = 2 + +# minimum no. of cards in an n-of-a-kind set +MIN_CARDS_NKIND = 2 + +# minimum no. of non-Ace cards in a run +MIN_NONACE_RUN = 2 + +# minimum no. cards in a run +MIN_RUN = 3 + + + +def is_ace(card): + """Boolean evaluation of whether `card` is an Ace""" + return card[VALUE] == ACE + + +def get_score(card): + """return the score of `card`, based on its value""" + return card_score[card[VALUE]] + + +def get_colour(card): + """Return the colour of `card` (`RED` or `BLACK`)""" + if card[SUIT] in RED_SUITS: + return RED + else: + return BLACK + + +def comp10001go_score_group(cards): + """Validate/score a group of cards (order unimportant), supplied as a + list of cards (each a string); return the positive score of the group if + valid, and negative score otherwise. Note, assumes that all cards are + valid, and unique.""" + + # construct sorted list of values of cards (ignore suit for now) + values = sorted([get_score(card) for card in cards]) + + # CASE 1: N-of-a-kind if all cards of same value, at least + # `MIN_CARDS_NKIND` cards in total, and not Aces + if (len(set(values)) == 1 and len(cards) >= MIN_CARDS_NKIND + and not is_ace(cards[0])): + return factorial(len(cards)) * card_score[cards[0][VALUE]] + + # construct sorted list of non-Ace cards + nonace_cards = sorted([card for card in cards if not is_ace(card)], + key=lambda x: get_score(x)) + + # construct list of Ace cards + ace_cards = list(set(cards) - set(nonace_cards)) + + # run must have at least `MIN_NONACE_RUN` non-Ace cards in it + if len(nonace_cards) >= MIN_NONACE_RUN: + + is_run = True + prev_val = prev_colour = None + score = 0 + + # iterate through cards to make sure they form a run + for card in nonace_cards: + + # CASE 1: for the first card in `nonace_cards`, nothing to + # check for + if prev_val is None: + score = prev_val = get_score(card) + prev_colour = get_colour(card) + + # CASE 2: adjacent to previous card in value + elif get_score(card) - prev_val == 1: + + # CASE 2.1: alternating colour, meaning continuation of run + if get_colour(card) != prev_colour: + prev_val = get_score(card) + prev_colour = get_colour(card) + score += prev_val + # CASE 2.2: not alternating colour, meaning invalid run + else: + is_run = False + break + + # CASE 3: repeat value, meaning no possibility of valid run + elif get_score(card) == prev_val: + is_run = False + break + + # CASE 4: gap in values, in which case check to see if can be + # filled with Ace(s) + else: + gap = get_score(card) - prev_val - 1 + + gap_filled = False + # continue until gap filled + while is_run and gap and len(ace_cards) >= gap: + + gap_filled = False + + # search for an Ace of appropriate colour, and remove + # from list of Aces if found (note that it doesn't matter + # which Ace is used if multiple Aces of same colour) + for i, ace in enumerate(ace_cards): + if get_colour(ace) != prev_colour: + ace_cards.pop(i) + prev_val += 1 + prev_colour = get_colour(ace) + score += prev_val + gap -= 1 + gap_filled = True + break + + if not gap_filled: + is_run = False + + if is_run and gap_filled and get_colour(card) != prev_colour: + prev_val = get_score(card) + prev_colour = get_colour(card) + score += prev_val + else: + is_run = False + + if is_run and len(cards) >= MIN_RUN and not ace_cards: + return score + + return -sum(values) + + +def comp10001go_valid_groups(groups): + for cards in groups: + if not cards or (len(cards) > 1 + and comp10001go_score_group(cards) < 0): + return False + return True + + +def comp10001go_score_groups(groups): + score = 0 + for group in groups: + score += comp10001go_score_group(group) + return score + + +def comp10001go_randplay(discard_history, player_no, hand): + + from random import shuffle + + shuffle(hand) + + # for first turn, select lowest card + return hand[0] + + + +def comp10001go_partition(cards): + + # BASE CASE 1: no cards, so no grouping to make + if len(cards) == 0: + return [] + + # BASE CASE 2: single card, so make a singleton group + if len(cards) == 1: + return [[cards]] + + # RECURSIVE CASE + out = [] + first = cards[0] + for sub_partition in comp10001go_partition(cards[1:]): + + # insert `first` in each of the subpartition's groups + for n, subpart in enumerate(sub_partition): + out.append(sub_partition[:n] + [[first] + subpart] + sub_partition[n+1:]) + + # put `first` in its own subpart + out.append([[first]] + sub_partition) + return out + + +def comp10001go_best_partitions(cards): + + # generate and score all valid card groups from `cards` + valid_groups = [(part, comp10001go_score_groups(part)) for part in comp10001go_partition(cards) + if comp10001go_valid_groups(part)] + + if valid_groups: + first_group, best_score = valid_groups[0] + best_groups = [first_group] + for group, score in valid_groups[1:]: + if score > best_score: + best_groups = [group] + best_score = score + elif score == best_score: + best_groups.append(group) + return best_groups + +# Sample solution 2 +import itertools +import math + +SUIT_TO_COLOUR = dict(zip('HDCS', 'RRBB')) +VALUE_STRING_TO_VALUE = dict(zip('A234567890JQK', range(1, 14))) +VALUE_TO_VALUE_STRING = {v: k for k, v in VALUE_STRING_TO_VALUE.items()} + + +class Card: + def __init__(self, card_string): + if isinstance(card_string, Card): + card_string = str(card_string) + elif isinstance(card_string, tuple): + card_string = VALUE_TO_VALUE_STRING[card_string[0]] + card_string[1] + self.value_str = card_string[0] + self.value = VALUE_STRING_TO_VALUE[self.value_str] + self.suit = card_string[1] + self.colour = SUIT_TO_COLOUR[self.suit] + self.inv_colour = 'R' if self.colour == 'B' else 'B' + self.orphan_value = -20 if self.is_ace() else -self.value + + def __hash__(self): + return hash(str(self)) + + def __eq__(self, other): + return self.value_str == other.value_str and self.suit == other.suit + + def __repr__(self): + return f'Card(\'{self.value_str}{self.suit}\')' + + def __str__(self): + return f'{self.value_str}{self.suit}' + + def is_ace(self): + return self.value_str == 'A' + + def is_black(self): + return self.colour == 'B' + + def is_king(self): + return self.value_str == 'K' + + def is_red(self): + return self.colour == 'R' + + +def construct_n_of_a_kind(cards): + # Early bail if we don't have enough cards. + if len(cards) < 2: + return None + + # Ensure that all of the cards have the same value and are not an Ace. + value = None + for card in cards: + if card.is_ace(): + return None + elif value is None: + value = card.value + elif card.value != value: + return None + + # Return the cards as is. + return list(cards) + + +def construct_run(cards): + # Early bail if we don't have enough cards. + if len(cards) < 3: + return None + + # Partition the cards into Aces and non-Aces. + non_aces = [] + aces_by_colour = {'B': [], 'R': []} + for card in cards: + if card.is_ace(): + aces_by_colour[card.colour].append(card) + else: + non_aces.append(card) + + # Ensure we have enough non-Aces. + if len(non_aces) < 2: + return None + + # Sort the non-Aces by value. + non_aces.sort(key=lambda card: card.value, reverse=True) + + # Attempt to construct a valid run from the avaialble cards. + prev = non_aces.pop() + run = [prev] + while non_aces: + top = non_aces[-1] + + # Check for a normal valid transition. + if prev.value + 1 == top.value and prev.colour == top.inv_colour: + run.append(non_aces.pop()) # Consume the current card in the run. + prev = top + else: + # Check if we can do an Ace insertion. + aces = aces_by_colour[prev.inv_colour] + if aces and not prev.is_king(): # Can't go higher than a King for Ace insertion. + ace = aces.pop() # Consume the next ace. + run.append(ace) + prev = Card((prev.value + 1, ace.suit)) + else: + # We did not find a valid transition. + return None + + # If we have any aces left over, we do not have a valid run. + if aces_by_colour['B'] or aces_by_colour['R']: + return None + + return run + + +def score_n_of_a_kind(cards): + return cards[0].value * math.factorial(len(cards)) + + +def score_orphans(cards): + return sum(map(lambda card: card.orphan_value, cards)) + + +def score_run(cards): + return sum(range(cards[0].value, cards[-1].value + 1)) + + +def score_group(cards): + if len(cards) == 1: + return score_orphans(cards) + + grouped_cards = construct_n_of_a_kind(cards) + if grouped_cards is not None: + return score_n_of_a_kind(grouped_cards) + + grouped_cards = construct_run(cards) + if grouped_cards is not None: + return score_run(grouped_cards) + + assert False, 'Should not be possible' + + +def is_valid_group(cards): + if len(cards) == 1: + return True + elif construct_n_of_a_kind(cards) is not None: + return True + elif construct_run(cards) is not None: + return True + else: + return False + + +def _generate_partitions(cards, groups): + if len(cards) == 0: + yield frozenset(groups) + return + + for k in range(1, len(cards) + 1): + for combination in map(frozenset, itertools.combinations(cards, k)): + if not is_valid_group(combination): + continue + remaining_cards = cards - combination + groups.append(combination) + yield from _generate_partitions(remaining_cards, groups) + groups.pop() + + +def generate_partitions(cards): + seen = set() + for partition in _generate_partitions(frozenset(cards), []): + if partition in seen: + continue + seen.add(partition) + yield list(map(list, partition)) + + +def comp10001go_best_partitions(card_strings): + cards = set(map(Card, card_strings)) + + max_score = float('-inf') + max_partitions = [] + for partition in generate_partitions(cards): + score = sum(map(score_group, partition)) + if score > max_score: + max_score = score + max_partitions = [partition] + elif score == max_score: + max_partitions.append(partition) + + return [[list(map(str, cards)) for cards in partition] for partition in max_partitions] diff --git a/project03.pdf b/project03.pdf new file mode 100755 index 0000000..a0b8f0a Binary files /dev/null and b/project03.pdf differ