Thursday, February 27, 2020

Effective Python Exercise: Wizardly Wiles

Sticking with the D&D Adventure style problems we've seen before, in this exercise for chapter 8 of Effective Python we are a wizard that owns a magic shop called Wizardly Wiles. Being the savvy saleswizard that you are, you've purchased a sales golem to help run the shop and take orders from customers. This sales golem was marketed as being able to serve customers, fill orders, and tally the total price. It is even able to use a translation amulet to help you with your increasing orders from the local Orcish population Sadly, after getting your magic sales golem, you find that there are several inefficiencies in the spells (code) that make it tick. It sometimes won't deactivate the translation amulet, eating up your precious magic powder. It fails to calculate the correct price for small amounts of magic powder. It's slow at searching for the book that the customer wants. So you decide to void the warranty on your sales golem, and make a few modifications to help it run more efficiently in your shop.

What changes could we make to the spells (code)? Note that you're only allow to change the SalesGolem code permanently, but you can experiment with different books on the bookshelf, different customers, different amounts of magic powder, etc, but the optimizations should be applied to the SalesGolem, and not to other code.
from typing import List, Union


class MagicPowderContainer:
    def __init__(self, grams_of_magic_powder: int):
        self.grams_of_magic_powder = grams_of_magic_powder

    def take_magic_powder(self, grams: int) -> int:
        if self.grams_of_magic_powder > grams:
            self.grams_of_magic_powder -= grams
            return grams
        else:
            raise Exception("Not enough magic powder")

    def take_remainder(self) -> int:
        remainder = self.grams_of_magic_powder
        self.grams_of_magic_powder = 0
        return remainder

    def measure(self) -> int:
        return self.grams_of_magic_powder

    def fill(self, grams: int):
        self.grams_of_magic_powder += grams

    def __repr__(self):
        return f"{self.grams_of_magic_powder} grams of magic powder"


class TranslationAmulet:
    # noinspection PyShadowingNames
    def __init__(self, magic_powder_container: MagicPowderContainer):
        self.is_active = False
        self.magic_powder_container = magic_powder_container

    def activate(self):
        self.is_active = True

    def deactivate(self):
        self.is_active = False

    def use(self):
        self.magic_powder_container.take_magic_powder(1)


class Customer:
    def __init__(self, race: str, order: List[str], translated_order: List[str]):
        self.race = race
        self._order = order
        self._translated_order = translated_order

    def get_order(self) -> List[str]:
        if translation_amulet.is_active:
            translation_amulet.use()
            return self._translated_order
        else:
            return self._order


class HumanCustomer(Customer):
    def __init__(self, order: List[str]):
        super().__init__("human", order, order)


class OrcCustomer(Customer):
    def __init__(self, order: List[str]):
        super().__init__("orc", ["*gibberish*" for _ in order], order)


class Book:
    def __init__(self, language: str, title: str, translated_title: str, price: str):
        self.language = language
        self._title = title
        self._translated_title = translated_title
        self.price = price

    def get_title(self) -> str:
        if translation_amulet.is_active:
            translation_amulet.use()
            return self._translated_title
        else:
            return self._title

    def __repr__(self):
        return self._translated_title


class EnglishBook(Book):
    def __init__(self, title: str, price: str):
        super().__init__("english", title, title, price)


class OrcishBook(Book):
    def __init__(self, title: str, price: str):
        super().__init__("orcish", "*gibberish*", title, price)


# noinspection PyMethodMayBeStatic
class SalesGolem:
    def help_customer(self, customer: Customer) -> (str, List[Union[MagicPowderContainer,
                                                                    Book,
                                                                    None]]):
        order_request = self._ask_for_order(customer)
        order = self._fulfill_order(order_request)
        payment = self._process_payment(order)
        return payment, order

    def _ask_for_order(self, customer: Customer) -> List[str]:
        try:
            if customer.race != "human":
                translation_amulet.activate()
                order = customer.get_order()
                translation_amulet.deactivate()
            else:
                order = customer.get_order()
            return order
        except:
            return []

    def _fulfill_order(self, order_request: List[str]) -> List[Union[MagicPowderContainer,
                                                                     Book,
                                                                     None]]:
        order = []
        for item in order_request:
            if item.endswith(" grams of magic powder"):
                ordered_grams = int(item.split(" ")[0])
                try:
                    grams = magic_powder_container.take_magic_powder(ordered_grams)
                except:
                    grams = magic_powder_container.take_remainder()
                order.append(MagicPowderContainer(grams))
            else:
                try:
                    book = self._find_book(item)
                except:
                    book = None
                order.append(book)
        return order

    def _find_book(self, title: str) -> Union[Book, None]:
        for book in books:
            if book.language != "english":
                translation_amulet.activate()
                book_title = book.get_title()
                translation_amulet.deactivate()
            else:
                book_title = book.get_title()
            if title == book_title:
                books.remove(book)
                return book
        return None

    def _process_payment(self, order: List[Union[MagicPowderContainer, Book, None]]) -> str:
        price = 0.
        for item in order:
            if isinstance(item, MagicPowderContainer):
                price += item.measure() * float(magic_powder_price_per_gram_sign)
            elif isinstance(item, Book):
                price += float(item.price)
        return f"{price:.2f}"


magic_powder_container = MagicPowderContainer(100000)
magic_powder_price_per_gram_sign = "0.001"

books = [
    OrcishBook("Adventures of Thorg", "12.57"),
    EnglishBook("Magical Maladies", "9.99"),
    EnglishBook("See the Sites of Middle Earth", "39.99"),
    EnglishBook("See the Sites of Middle Earth", "39.99"),
    OrcishBook("Sorcery 101", "15.47"),
    OrcishBook("Sorcery 101", "15.47"),
    OrcishBook("Sorcery 101", "15.47"),
    EnglishBook("Tales of Treachery", "5.97"),
    EnglishBook("Wand Maintenance", "2.37"),
    OrcishBook("What's the Difference Between a Wizard and a Warlock?", "42.88"),
    EnglishBook("White Wizard", "25.00")
]
# We cheat and access the translated title here without cost so that we can
# make the assumption that the books are sorted, regardless of the order that
# they are specified in the above array
# noinspection PyProtectedMember
books.sort(key=lambda x: x._translated_title)

translation_amulet = TranslationAmulet(magic_powder_container)
golem = SalesGolem()
You can find the answer here.