CSC110 / lectures / Building a Simulation / events.py
events.py
Raw
"""Lecture 30 (Events)

This is the code for Event and its subclasses from lecture, with the following modifications:

- I included a solution implementation for GenerateOrdersEvent.handle_events, but I strongly
  suggest trying to implement it yourself first for practice!!
- I added a __str__ method implementation for each Event subclass. This isn't technically necessary
  for the simulation, but because print(event) calls event.__str__(), these methods make
  printing the events in the run_simulation loop look nicer. :)
"""
from __future__ import annotations

import datetime
import random

from entities import Order
from food_delivery_system import FoodDeliverySystem


class Event:
    """An abstract class representing an event in a food delivery simulation.

    Instance Attributes:
        - timestamp: the start time of the event
    """
    timestamp: datetime.datetime

    def __init__(self, timestamp: datetime.datetime) -> None:
        """Initialize this event with the given timestamp."""
        self.timestamp = timestamp

    def handle_event(self, system: FoodDeliverySystem) -> list[Event]:
        """Mutate the given food delivery system to process this event.

        (NEW) Return a new list of new events created by processing
        this event.
        """
        raise NotImplementedError


class NewOrderEvent(Event):
    """An event representing when a customer places an order at a vendor."""
    # Private Instance Attributes:
    #   _order: the new order to be added to the FoodDeliverySystem
    _order: Order

    def __init__(self, order: Order) -> None:
        """Initialize a NewOrderEvent for the given order."""
        self._order = order   # This initializes self._order

        Event.__init__(self, order.start_time)   # This initializes self.timestamp

        # This works, but is not the best practice
        # self.timestamp = order.start_time

    def handle_event(self, system: FoodDeliverySystem) -> list[Event]:
        """Mutate system by placing an order.

        (NEW) Return a new list of new events created by processing
        this event.
        """
        # NOTE: we should modify this code in case system.place_order returns False!
        # What might we do in that case?
        # system.place_order(self._order)
        # future_complete_time = self.timestamp + datetime.timedelta(minutes=1)
        # complete_event = CompleteOrderEvent(future_complete_time, self._order)
        # return [complete_event]

        success = system.place_order(self._order)

        if success:
            completion_time = self.timestamp + datetime.timedelta(minutes=10)
            return [CompleteOrderEvent(completion_time, self._order)]
        else:
            # Try to place the order again in 5 minutes
            self._order.start_time = self.timestamp + datetime.timedelta(minutes=5)
            return [NewOrderEvent(self._order)]

    def __str__(self) -> str:
        """Return a string representation for this event.

        Useful if we want to call print on the event.
        """
        return f'{self.timestamp}: New order from {self._order.customer.name} for {self._order.vendor.name}'


###############################################################################
# Lecture Exercise
###############################################################################
class CompleteOrderEvent(Event):
    """An event representing when an order is delivered to a customer by a courier."""
    # Private Instance Attributes:
    #   _order: the order to be completed by this event
    _order: Order

    def __init__(self, timestamp: datetime.datetime, order: Order) -> None:
        Event.__init__(self, timestamp)
        self._order = order

    def handle_event(self, system: FoodDeliverySystem) -> list[Event]:
        """Mutate the system by recording that the order has been delivered to the customer.

        (NEW) Return a new list of new events created by processing
        this event.
        """
        system.complete_order(self._order, self.timestamp)
        return []  # Trigger no new events

        # Other possibilities:
        # Place an order at the same restaurant in the future
        # Place 5 orders at a competing restaurant

    def __str__(self) -> str:
        """Return a string representation for this event.

        Useful if we want to call print on the event.
        """
        return f'{self.timestamp}: Order from {self._order.customer.name} was ' \
               f'completed by {self._order.courier.name}'


###############################################################################
# Lecture Exercise (Event for randomly generating orders)
###############################################################################
class GenerateOrdersEvent(Event):
    """An event that causes a random generation of new orders.

    Private Representation Invariants:
        - self._duration > 0
    """
    # Private Instance Attributes:
    #   - _duration: the number of hours to generate orders for
    _duration: int

    def __init__(self, timestamp: datetime.datetime, duration: int) -> None:
        """Initialize this event with timestamp and the duration in hours.

        Preconditions:
            - duration > 0
        """
        Event.__init__(self, timestamp)
        self._duration = duration

    def handle_event(self, system: FoodDeliverySystem) -> list[Event]:
        """Generate new orders for this event's timestamp and duration."""
        # Conceptual idea (simple)
        # new_events = []
        #
        # for i in range(0, self._duration):
        #     new_event = NewOrderEvent(...)
        #     new_events.append(new_event)
        #
        # return new_events

        # Actual implementation
        customers = system.get_customers()
        vendors = system.get_vendors()

        events = []  # Event accumulator

        current_time = self.timestamp
        end_time = self.timestamp + datetime.timedelta(hours=self._duration)

        while current_time < end_time:
            # Create a randomly-generated Order called new_order.
            # Note the use of random.choice, which returns a random element from its argument list
            customer = random.choice(customers)
            vendor = random.choice(vendors)
            food_items = {}  # This is a simple version
            new_order = Order(customer=customer, vendor=vendor, food_items=food_items,
                              start_time=current_time)

            new_order_event = NewOrderEvent(new_order)
            events.append(new_order_event)

            # Update current_time
            current_time = current_time + datetime.timedelta(minutes=random.randint(1, 60))

        return events

    def __str__(self) -> str:
        """Return a string representation for this event.

        Useful if we want to call print on the event.
        """
        return f'{self.timestamp}: Generating new orders (up to {self._duration} hours)'