<filename>advent_of_code/solvers/day_15.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Puzzle Solver for Advent of Code 2015 Day 15
Author: <NAME>
Copyright: MIT license

Description (https://adventofcode.com/2015/day/15):
--- Day 15: Science for Hungry People ---

  Today, you set out on the task of perfecting your milk-dunking cookie recipe.
  All you have to do is find the right balance of ingredients.

  Your recipe leaves room for exactly 100 teaspoons of ingredients. You make a
  list of the remaining ingredients you could use to finish the recipe (your
  puzzle input) and their properties per teaspoon:
    capacity (how well it helps the cookie absorb milk)
    durability (how well it keeps the cookie intact when full of milk)
    flavor (how tasty it makes the cookie)
    texture (how it improves the feel of the cookie)
    calories (how many calories it adds to the cookie)

  You can only measure ingredients in whole-teaspoon amounts accurately, and
  you have to be accurate so you can reproduce your results in the future. The
  total score of a cookie can be found by adding up each of the properties
  (negative totals become 0) and then multiplying together everything except
  calories.

  For instance, suppose you have these two ingredients:
    Butterscotch: capacity -1, durability -2, flavor 6, texture 3, calories 8
    Cinnamon: capacity 2, durability 3, flavor -2, texture -1, calories 3

    Then, choosing to use 44 teaspoons of butterscotch and 56 teaspoons of
    cinnamon (because the amounts of each ingredient must add up to 100) would
    result in a cookie with the following properties:
      A capacity of 44*-1 + 56*2 = 68
      A durability of 44*-2 + 56*3 = 80
      A flavor of 44*6 + 56*-2 = 152
      A texture of 44*3 + 56*-1 = 76

    Multiplying these together (68 * 80 * 152 * 76, ignoring calories for now)
    results in a total score of 62842880, which happens to be the best score
    possible given these ingredients. If any properties had produced a negative
    total, it would have instead become zero, causing the whole score to
    multiply to zero.

  Given the ingredients in your kitchen and their properties, what is the total
  score of the highest-scoring cookie you can make?
    Answer: 18965440

--- Day 15: Part Two ---

  Your cookie recipe becomes wildly popular! Someone asks if you can make
  another recipe that has exactly 500 calories per cookie (so they can use it
  as a meal replacement). Keep the rest of your award-winning process the same
  (100 teaspoons, same ingredients, same scoring system).

  For example, given the ingredients above, if you had instead selected 40
  teaspoons of butterscotch and 60 teaspoons of cinnamon (which still adds to
  100), the total calorie count would be 40*8 + 60*3 = 500. The total score
  would go down, though: only 57600000, the best you can do in such trying
  circumstances.

  Given the ingredients in your kitchen and their properties, what is the total
  score of the highest-scoring cookie you can make with a calorie total of 500?
    Answer: 15862900
"""

# Standard Library Imports
from collections import namedtuple
from itertools import combinations_with_replacement as combinations
import re

# Application-specific Imports
from advent_of_code.solvers import solver


# Stores metadata about a recipe ingredient
Ingredient = namedtuple(
    typename='Ingredient',
    field_names='capacity durability flavor texture calories',
)


class Solver(solver.AdventOfCodeSolver):
    """Advent of Code 2015 Day 15: Science for Hungry People

    Attributes:
        puzzle_input (list): A list of instructions for solving the puzzle
        puzzle_title (str): Name of the Advent of Code puzzle
        solved_output (str): A template string for solution output
    """

    def __init__(self, *args):
        solver.AdventOfCodeSolver.__init__(self, *args)
        self._solved_output = '\n'.join((
            'The highest-scoring with these cookie ingredients has {0} points.',
            'The highest-scoring cookie with 500 calories has {1} points.',
        ))

    def _parse_input(self):
        """Parses recipe ingredients and associated metadata

        Args: None
        Returns:
            dict: Item names mapped to Ingredient namedtuples
        """
        ingredient_pattern = r'(\w+)' + ''.join([r'[^\d-]+(-?\d+)'] * 5)
        parser = re.compile(ingredient_pattern)
        pantry = {}
        for line in self.puzzle_input.splitlines():
            ingredient = parser.match(line)
            if ingredient is None:
                continue
            item = ingredient.group(1)
            pantry[item] = Ingredient(
                capacity=int(ingredient.group(2)),
                durability=int(ingredient.group(3)),
                flavor=int(ingredient.group(4)),
                texture=int(ingredient.group(5)),
                calories=int(ingredient.group(6)),
            )
        return pantry

    @staticmethod
    def _get_max_score(recipe, pantry):
        """Calculates the highest score possible for the recipe

        Args:
            recipe (list): Combination of ingredients for making the cookie
            pantry (dict): Item names mapped to Ingredient namedtuples
        Returns:
            int: Highest possible score for the given cookie recipe
        """
        score = max(0, sum(pantry[item].capacity for item in recipe))
        score *= max(0, sum(pantry[item].durability for item in recipe))
        score *= max(0, sum(pantry[item].flavor for item in recipe))
        score *= max(0, sum(pantry[item].texture for item in recipe))
        return score

    def _get_alt_score(self, recipe, pantry):
        """Calculates the highest score if the recipe has exactly 500 calories

        Args:
            recipe (list): Combination of ingredients for making the cookie
            pantry (dict): Item names mapped to Ingredient namedtuples
        Returns:
            int: Highest possible score for a 500 calory cookie recipe
        """
        calories = sum(pantry[ingredient].calories for ingredient in recipe)
        return self._get_max_score(recipe, pantry) if calories == 500 else -1

    def _solve_puzzle_parts(self):
        """Solves each part of a Advent of Code 2015 puzzle

        Args: None
        Returns:
            tuple: Pair of solutions for the two parts of the puzzle
        """
        pantry = self._parse_input()
        teaspoons = 100
        max_score, alt_score = 0, 0
        for recipe in combinations(pantry.keys(), teaspoons):
            max_score = max(max_score, self._get_max_score(recipe, pantry))
            alt_score = max(alt_score, self._get_alt_score(recipe, pantry))
        return max_score, alt_score

    def run_test_cases(self):
        """Runs a series of inputs and compares against expected outputs

        Args: None
        Returns: None
        """
        item = '{0}: capacity {1}, durability {2}, flavor {3}'
        item += ', texture {4}, calories {5}'
        input1 = (
            item.format('Butterscotch', -1, -2, 6, 3, 8),
            item.format('Cinnamon', 2, 3, -2, -1, 3),
        )
        input2 = input1 + (item.format('Sugar', 1, 1, 1, 1, 1),)
        input3 = input1 + (item.format('Boogers', 2, 2, 2, 2, 2),)
        test_cases = (
            solver.TestCase('\n'.join(input1), 62842880, 57600000),
            solver.TestCase('\n'.join(input2), 105187500, 65014560),
            solver.TestCase('\n'.join(input3), 1600000000, 130975000),
        )
        for test_case in test_cases:
            self._run_test_case(test_case)

<filename>advent_of_code/solvers/day_15.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Puzzle Solver for Advent of Code 2015 Day 15
Author: <NAME>
Copyright: MIT license

Description (https://adventofcode.com/2015/day/15):
--- Day 15: Science for Hungry People ---

  Today, you set out on the task of perfecting your milk-dunking cookie recipe.
  All you have to do is find the right balance of ingredients.

  Your recipe leaves room for exactly 100 teaspoons of ingredients. You make a
  list of the remaining ingredients you could use to finish the recipe (your
  puzzle input) and their properties per teaspoon:
    capacity (how well it helps the cookie absorb milk)
    durability (how well it keeps the cookie intact when full of milk)
    flavor (how tasty it makes the cookie)
    texture (how it improves the feel of the cookie)
    calories (how many calories it adds to the cookie)

  You can only measure ingredients in whole-teaspoon amounts accurately, and
  you have to be accurate so you can reproduce your results in the future. The
  total score of a cookie can be found by adding up each of the properties
  (negative totals become 0) and then multiplying together everything except
  calories.

  For instance, suppose you have these two ingredients:
    Butterscotch: capacity -1, durability -2, flavor 6, texture 3, calories 8
    Cinnamon: capacity 2, durability 3, flavor -2, texture -1, calories 3

    Then, choosing to use 44 teaspoons of butterscotch and 56 teaspoons of
    cinnamon (because the amounts of each ingredient must add up to 100) would
    result in a cookie with the following properties:
      A capacity of 44*-1 + 56*2 = 68
      A durability of 44*-2 + 56*3 = 80
      A flavor of 44*6 + 56*-2 = 152
      A texture of 44*3 + 56*-1 = 76

    Multiplying these together (68 * 80 * 152 * 76, ignoring calories for now)
    results in a total score of 62842880, which happens to be the best score
    possible given these ingredients. If any properties had produced a negative
    total, it would have instead become zero, causing the whole score to
    multiply to zero.

  Given the ingredients in your kitchen and their properties, what is the total
  score of the highest-scoring cookie you can make?
    Answer: 18965440

--- Day 15: Part Two ---

  Your cookie recipe becomes wildly popular! Someone asks if you can make
  another recipe that has exactly 500 calories per cookie (so they can use it
  as a meal replacement). Keep the rest of your award-winning process the same
  (100 teaspoons, same ingredients, same scoring system).

  For example, given the ingredients above, if you had instead selected 40
  teaspoons of butterscotch and 60 teaspoons of cinnamon (which still adds to
  100), the total calorie count would be 40*8 + 60*3 = 500. The total score
  would go down, though: only 57600000, the best you can do in such trying
  circumstances.

  Given the ingredients in your kitchen and their properties, what is the total
  score of the highest-scoring cookie you can make with a calorie total of 500?
    Answer: 15862900
"""

# Standard Library Imports
from collections import namedtuple
from itertools import combinations_with_replacement as combinations
import re

# Application-specific Imports
from advent_of_code.solvers import solver


# Stores metadata about a recipe ingredient
Ingredient = namedtuple(
    typename='Ingredient',
    field_names='capacity durability flavor texture calories',
)


class Solver(solver.AdventOfCodeSolver):
    """Advent of Code 2015 Day 15: Science for Hungry People

    Attributes:
        puzzle_input (list): A list of instructions for solving the puzzle
        puzzle_title (str): Name of the Advent of Code puzzle
        solved_output (str): A template string for solution output
    """

    def __init__(self, *args):
        solver.AdventOfCodeSolver.__init__(self, *args)
        self._solved_output = '\n'.join((
            'The highest-scoring with these cookie ingredients has {0} points.',
            'The highest-scoring cookie with 500 calories has {1} points.',
        ))

    def _parse_input(self):
        """Parses recipe ingredients and associated metadata

        Args: None
        Returns:
            dict: Item names mapped to Ingredient namedtuples
        """
        ingredient_pattern = r'(\w+)' + ''.join([r'[^\d-]+(-?\d+)'] * 5)
        parser = re.compile(ingredient_pattern)
        pantry = {}
        for line in self.puzzle_input.splitlines():
            ingredient = parser.match(line)
            if ingredient is None:
                continue
            item = ingredient.group(1)
            pantry[item] = Ingredient(
                capacity=int(ingredient.group(2)),
                durability=int(ingredient.group(3)),
                flavor=int(ingredient.group(4)),
                texture=int(ingredient.group(5)),
                calories=int(ingredient.group(6)),
            )
        return pantry

    @staticmethod
    def _get_max_score(recipe, pantry):
        """Calculates the highest score possible for the recipe

        Args:
            recipe (list): Combination of ingredients for making the cookie
            pantry (dict): Item names mapped to Ingredient namedtuples
        Returns:
            int: Highest possible score for the given cookie recipe
        """
        score = max(0, sum(pantry[item].capacity for item in recipe))
        score *= max(0, sum(pantry[item].durability for item in recipe))
        score *= max(0, sum(pantry[item].flavor for item in recipe))
        score *= max(0, sum(pantry[item].texture for item in recipe))
        return score

    def _get_alt_score(self, recipe, pantry):
        """Calculates the highest score if the recipe has exactly 500 calories

        Args:
            recipe (list): Combination of ingredients for making the cookie
            pantry (dict): Item names mapped to Ingredient namedtuples
        Returns:
            int: Highest possible score for a 500 calory cookie recipe
        """
        calories = sum(pantry[ingredient].calories for ingredient in recipe)
        return self._get_max_score(recipe, pantry) if calories == 500 else -1

    def _solve_puzzle_parts(self):
        """Solves each part of a Advent of Code 2015 puzzle

        Args: None
        Returns:
            tuple: Pair of solutions for the two parts of the puzzle
        """
        pantry = self._parse_input()
        teaspoons = 100
        max_score, alt_score = 0, 0
        for recipe in combinations(pantry.keys(), teaspoons):
            max_score = max(max_score, self._get_max_score(recipe, pantry))
            alt_score = max(alt_score, self._get_alt_score(recipe, pantry))
        return max_score, alt_score

    def run_test_cases(self):
        """Runs a series of inputs and compares against expected outputs

        Args: None
        Returns: None
        """
        item = '{0}: capacity {1}, durability {2}, flavor {3}'
        item += ', texture {4}, calories {5}'
        input1 = (
            item.format('Butterscotch', -1, -2, 6, 3, 8),
            item.format('Cinnamon', 2, 3, -2, -1, 3),
        )
        input2 = input1 + (item.format('Sugar', 1, 1, 1, 1, 1),)
        input3 = input1 + (item.format('Boogers', 2, 2, 2, 2, 2),)
        test_cases = (
            solver.TestCase('\n'.join(input1), 62842880, 57600000),
            solver.TestCase('\n'.join(input2), 105187500, 65014560),
            solver.TestCase('\n'.join(input3), 1600000000, 130975000),
        )
        for test_case in test_cases:
            self._run_test_case(test_case)

#!/usr/bin/env python # -*- coding: utf-8 -*- Puzzle Solver for Advent of Code 2015 Day 15 Author: <NAME> Copyright: MIT license  Description (https://adventofcode.com/2015/day/15): --- Day 15: Science for Hungry People ---    Today, you set out on the task of perfecting your milk-dunking cookie recipe.   All you have to do is find the right balance of ingredients.    Your recipe leaves room for exactly 100 teaspoons of ingredients. You make a   list of the remaining ingredients you could use to finish the recipe (your   puzzle input) and their properties per teaspoon:     capacity (how well it helps the cookie absorb milk)     durability (how well it keeps the cookie intact when full of milk)     flavor (how tasty it makes the cookie)     texture (how it improves the feel of the cookie)     calories (how many calories it adds to the cookie)    You can only measure ingredients in whole-teaspoon amounts accurately, and   you have to be accurate so you can reproduce your results in the future. The   total score of a cookie can be found by adding up each of the properties   (negative totals become 0) and then multiplying together everything except   calories.    For instance, suppose you have these two ingredients:     Butterscotch: capacity -1, durability -2, flavor 6, texture 3, calories 8     Cinnamon: capacity 2, durability 3, flavor -2, texture -1, calories 3      Then, choosing to use 44 teaspoons of butterscotch and 56 teaspoons of     cinnamon (because the amounts of each ingredient must add up to 100) would     result in a cookie with the following properties:       A capacity of 44*-1 + 56*2 = 68       A durability of 44*-2 + 56*3 = 80       A flavor of 44*6 + 56*-2 = 152       A texture of 44*3 + 56*-1 = 76      Multiplying these together (68 * 80 * 152 * 76, ignoring calories for now)     results in a total score of 62842880, which happens to be the best score     possible given these ingredients. If any properties had produced a negative     total, it would have instead become zero, causing the whole score to     multiply to zero.    Given the ingredients in your kitchen and their properties, what is the total   score of the highest-scoring cookie you can make?     Answer: 18965440  --- Day 15: Part Two ---    Your cookie recipe becomes wildly popular! Someone asks if you can make   another recipe that has exactly 500 calories per cookie (so they can use it   as a meal replacement). Keep the rest of your award-winning process the same   (100 teaspoons, same ingredients, same scoring system).    For example, given the ingredients above, if you had instead selected 40   teaspoons of butterscotch and 60 teaspoons of cinnamon (which still adds to   100), the total calorie count would be 40*8 + 60*3 = 500. The total score   would go down, though: only 57600000, the best you can do in such trying   circumstances.    Given the ingredients in your kitchen and their properties, what is the total   score of the highest-scoring cookie you can make with a calorie total of 500?     Answer: 15862900 # Standard Library Imports # Application-specific Imports # Stores metadata about a recipe ingredient Advent of Code 2015 Day 15: Science for Hungry People      Attributes:         puzzle_input (list): A list of instructions for solving the puzzle         puzzle_title (str): Name of the Advent of Code puzzle         solved_output (str): A template string for solution output Parses recipe ingredients and associated metadata          Args: None         Returns:             dict: Item names mapped to Ingredient namedtuples Calculates the highest score possible for the recipe          Args:             recipe (list): Combination of ingredients for making the cookie             pantry (dict): Item names mapped to Ingredient namedtuples         Returns:             int: Highest possible score for the given cookie recipe Calculates the highest score if the recipe has exactly 500 calories          Args:             recipe (list): Combination of ingredients for making the cookie             pantry (dict): Item names mapped to Ingredient namedtuples         Returns:             int: Highest possible score for a 500 calory cookie recipe Solves each part of a Advent of Code 2015 puzzle          Args: None         Returns:             tuple: Pair of solutions for the two parts of the puzzle Runs a series of inputs and compares against expected outputs          Args: None         Returns: None