A document from MCS 275 Spring 2024, instructor Emily Dumas. You can also get the notebook file.

MCS 275 Spring 2024 Homework 3 Solutions

  • Course Instructor: Emily Dumas

Problem 2: Overloading the traffic signal

Put the code you write for this problem in hwk3prob2.py.

In the next cell, you'll see code for a class that represents a traffic signal controlling a single direction of traffic. The signal is a panel of three lights (one red, one yellow, one green), and exactly one of them is illuminated at any given time. The state of the light can be referred to by the illumated color, by a state number, or by the intent of the signal, which relate as shown below:

state color intent
0 red stop
1 yellow slow
2 green go

The class given below is functional, in that it supports setting, querying, and changing the state of the light. It can also display itself in various forms.

However, a couple of features are missing. After copying the template code below into hwk2prob2.py, modify it to add the following features:

  1. Boolean coercion: A traffic signal object should be considered True if it allows any flow of traffic (i.e. is green or yellow), and False otherwise. Thus the boolean value answers the question "is any movement allowed?"
  2. Equality testing: If A and B are traffic signal objects, then the equality test A==B should evaluate to True if the two signals are in the same state (i.e. color/intent) and False otherwise.

This means you should add some special methods to TrafficSignal. Do not modify or remove any existing methods or attributes of the template.

(Template class omitted; instead we just include the modified version below.)

Solution

Comment lines highlight the new methods.

Also, it turns out that there was a bug in the set_state method of the template caused by a typo (using s instead of i). This method was not called in any of the demonstration cells and isn't relevant to the problem. Still, it has been corrected here.

In [48]:
# MCS 275 Spring 2024 Homework 3 Solutions

class TrafficSignal:
    """
    Class representing the state of a 3-color traffic signal (AKA traffic light)
    controlling one direction of traffic.  It can be in three states:
    state 0 = red light = STOP = no traffic is allowed to pass the signal
    state 1 = yellow light = SLOW = traffic at or near signal can proceed, others stop
    state 2 = green light = GO = traffic can proceed normally
    """
    color_map = ("red","yellow","green")
    intent_map = ("stop","slow","go")
    def __init__(self,state=None,color=None,intent=None):
        """
        Initialize new traffic light according to a state number (0,1,2),
        color name (red,yellow,green), or intent (stop,slow,go).  Only one
        of the arguments can be given.  Initially red if no arguments given.
        """
        self.state = 0 # default red
        args_possible = [state,color,intent]
        args_given = [ a for a in args_possible if a is not None ]
        if not args_given:
            # No arguments, so keep default red.  We're done.
            return
        
        if len(args_given) > 1:
            raise ValueError("Can only specify one of `state`, `color`, `intent`.")
        
        # Now we know exactly one argument given.  Which one?
        if state is not None:
            self.state = state
        elif color is not None:
            try:
                self.state = self.color_map.index(color.lower())
            except ValueError:
                raise ValueError("Unrecognized color: "+str(color))
        elif intent is not None:
            try:
                self.state = self.intent_map.index(intent.lower())
            except ValueError:
                raise ValueError("Unrecognized intent: "+str(intent))

    def get_state(self):
        "get the current state number"
        return self.state                
                
    def get_color(self):
        "get the current color"
        return self.color_map[self.state]
                
    def get_intent(self):
        "get the current intent"
        return self.intent_map[self.state]
    
    def set_state(self,i):
        "set the current state number"
        if not isinstance(i,int):
            raise TypeError("State must be an integer")
        if i not in (0,1,2):  # previously read `if s not in (0,1,2):`
            raise ValueError("Invalid state {}".format(repr(i)))
        self.state = i
        
    def set_color(self,c):
        "set the light color"
        if not isinstance(c,str):
            raise TypeError("Color must be a string")
        if c.lower() not in self.color_map:
            raise ValueError("Invalid color {}".format(repr(c)))
        self.state = self.color_map.index(c.lower())

    def set_intent(self,t):
        "set the signal intent"
        if not isinstance(t,str):
            raise TypeError("Intent must be a string")
        if t.lower() not in self.intent_map:
            raise ValueError("Invalid intent {}".format(repr(t)))
        self.state = self.intent_map.index(t.lower())
                             
    def advance(self):
        "advance to the next state in the usual green-yellow-red-green-... cycle"
        self.state = (self.state-1)%3
        
    def show(self):
        "display with unicode characters"
        for i in range(3):
            print("\U0001F534\U0001F7E1\U0001F7E2\u26AB"[i if i==self.state else -1])

    def __str__(self):
        "Human-readable string"
        return "{}[{}={}={}]".format(self.__class__.__name__,self.state,self.get_color(),self.get_intent())

    def __repr__(self):
        "String for use in the REPL (evaluates to a copy of this object)"
        return "{}(state={})".format(self.__class__.__name__,self.state)

    #----------------------------------------------------------------------
    #----------------------------------------------------------------------
    #              START OF METHODS ADDED FOR PROBLEM 2
    #----------------------------------------------------------------------
    #----------------------------------------------------------------------
    
    def __eq__(self,other):
        "Equality of signals means same state"
        return self.state == other.state   # This is an int==int test
    
    def __bool__(self):
        "Boolean coercion indicates whether any movement is allowed"
        # Since built-in bool sends integer 0 to False and integers 1,2 to True
        # it happens that bool(self.state) is the same as the
        # requested behavior!
        return bool(self.state)

Demonstration that TrafficSignal behaves as requested

In [49]:
signalsA = [ TrafficSignal(state=i) for i in range(3) ]
boolsA = [ False, True, True ]

looks_good = True
for i in range(3):
    actual = bool(signalsA[i])
    expected = boolsA[i]
    agree = actual==expected
    looks_good = looks_good and agree
    print("{}: bool({} signal) is {}".format(
        "OK" if agree else "ERROR",
        signalsA[i].get_color(),
        actual,
    ))
print("\nBoolean coercion","is working \U0001F60A" if looks_good else "IS NOT WORKING CORRECTLY \U0001F6A9")
OK: bool(red signal) is False
OK: bool(yellow signal) is True
OK: bool(green signal) is True

Boolean coercion is working 😊
In [50]:
signalsA = [ TrafficSignal(state=i) for i in range(3) ]
signalsB = [ TrafficSignal(state=i) for i in range(3) ]
looks_good = True
for i in range(3):
    for j in range(3):
        actual = signalsA[i]==signalsB[j]
        expected = i==j
        agree = actual==expected
        looks_good = looks_good and agree
        print("{}: {}=={} evaluates to {}".format(
            "OK" if agree else "ERROR",
            signalsA[i].get_color(),
            signalsB[j].get_color(),
            actual,
        ))
print("\nEquality testing","is working \U0001F60A" if looks_good else "IS NOT WORKING CORRECTLY \U0001F6A9")
OK: red==red evaluates to True
OK: red==yellow evaluates to False
OK: red==green evaluates to False
OK: yellow==red evaluates to False
OK: yellow==yellow evaluates to True
OK: yellow==green evaluates to False
OK: green==red evaluates to False
OK: green==yellow evaluates to False
OK: green==green evaluates to True

Equality testing is working 😊

Problem 3: Slow start robot

Put the code you write for this problem in hwk3prob3.py.

This problem continues the work on Bot subclasses that we began in lecture by creating bots.py, and which you also worked on in Worksheet 3.

Create another subclass of Bot called SlowStartBot with the following behavior:

  • The constructor takes three arguments, in this order:
    1. position, a Point2 specifying the starting position
    2. direction, a Vector2 specifying the movement direction
    3. initial_delay, an integer indicating how "gradual" the acceleration will be
  • When .update() is called repeatedly, the robot should behave as follows:
    • At first, it sits still for initial_delay time steps
    • On the next time step, it moves by vector direction
    • Then it waits initial_delay-1 time steps (assuming that is positive)
    • Then it takes one step in direction direction
    • Then it waits initial_delay-2 time steps (assuming that is position)
    • This alternation of waiting and taking one step continues, with each waiting period being one time step shorter than the last one.
    • Once the waiting time has decreased to zero, the robot moves by direction on each time step thereafter

Thus, the class is used for a robot that initially spends a lot of time waiting around instead of moving, but gradually the pauses get shorter until it eventually moves on each time step.

The cell below shows an example of how the class might be instantiated.

In [51]:
import sys
d = "/home/ddumas/teaching/mcs275/public/samplecode/oop"
if d not in sys.path:
    sys.path.append(d)
In [52]:
from bots import *

class SlowStartBot(Bot):
    """
    Robot that initially pauses before each step, but each pause is
    shorter than the last until it eventually takes one step per update.
    """
    
    def __init__(self,position,direction,initial_delay):
        """
        Initialize bot with given position, direction of movement,
        and first pause duration
        """
        super().__init__(position)
        self.direction = direction
        self.initial_delay = initial_delay
        self.current_delay = initial_delay
        self.waited = 0
        self.state = "wait"
    
    def update(self):
        if self.state == "wait":
            self.waited += 1
            if self.waited == self.current_delay:
                # We're done with this pause
                # set next pause to be shorter
                self.current_delay -= 1
                # change to "go" mode
                self.state = "go"
        else:
            # state=="go"
            # take a step
            self.move_by(self.direction)
            # if we're still in the "slow start", switch
            # to wait mode for the next update.
            if self.current_delay:
                # reset wait count
                self.waited = 0
                self.state = "wait"
                
    # This method totally optional!
    def __str__(self):
        "Human-readable string"
        return "{}(pos={},dir={},state={},waited={})".format(
            self.__class__.__name__,
            self.position,
            self.direction,
            self.state,
            self.waited,
        )
        
    # This method totally optional!
    def __repr__(self):
        "Unambiguous string"
        return str(self)

Test instantiation

In [53]:
from plane import Point2, Vector2

S = SlowStartBot(position=Point2(6,4), direction=Vector2(-1,1), initial_delay=4)
In [54]:
S
Out[54]:
SlowStartBot(pos=Point2(6,4),dir=Vector2(-1,1),state=wait,waited=0)

The __str__ method we included above makes it easy to test the behavior as follows:

In [55]:
for _ in range(20):
    print(S)
    S.update()
SlowStartBot(pos=Point2(6,4),dir=Vector2(-1,1),state=wait,waited=0)
SlowStartBot(pos=Point2(6,4),dir=Vector2(-1,1),state=wait,waited=1)
SlowStartBot(pos=Point2(6,4),dir=Vector2(-1,1),state=wait,waited=2)
SlowStartBot(pos=Point2(6,4),dir=Vector2(-1,1),state=wait,waited=3)
SlowStartBot(pos=Point2(6,4),dir=Vector2(-1,1),state=go,waited=4)
SlowStartBot(pos=Point2(5,5),dir=Vector2(-1,1),state=wait,waited=0)
SlowStartBot(pos=Point2(5,5),dir=Vector2(-1,1),state=wait,waited=1)
SlowStartBot(pos=Point2(5,5),dir=Vector2(-1,1),state=wait,waited=2)
SlowStartBot(pos=Point2(5,5),dir=Vector2(-1,1),state=go,waited=3)
SlowStartBot(pos=Point2(4,6),dir=Vector2(-1,1),state=wait,waited=0)
SlowStartBot(pos=Point2(4,6),dir=Vector2(-1,1),state=wait,waited=1)
SlowStartBot(pos=Point2(4,6),dir=Vector2(-1,1),state=go,waited=2)
SlowStartBot(pos=Point2(3,7),dir=Vector2(-1,1),state=wait,waited=0)
SlowStartBot(pos=Point2(3,7),dir=Vector2(-1,1),state=go,waited=1)
SlowStartBot(pos=Point2(2,8),dir=Vector2(-1,1),state=go,waited=1)
SlowStartBot(pos=Point2(1,9),dir=Vector2(-1,1),state=go,waited=1)
SlowStartBot(pos=Point2(0,10),dir=Vector2(-1,1),state=go,waited=1)
SlowStartBot(pos=Point2(-1,11),dir=Vector2(-1,1),state=go,waited=1)
SlowStartBot(pos=Point2(-2,12),dir=Vector2(-1,1),state=go,waited=1)
SlowStartBot(pos=Point2(-3,13),dir=Vector2(-1,1),state=go,waited=1)

Revision history

  • 2024-02-01 Initial publication