CSC108H Assignment 4, Summer 2009

Introduction

The purpose of this assignment is to give you practice with object-oriented programming and to introduce you to design patterns.

Object-oriented Mindreader

In assignment 3, we completed a mindreader program. The computer tried to predict whether the user would choose heads or tails, by relying on previous user patterns. We had several types of objects: a string for the context; a dictionary for the memory; a tuple, list, or other object of your choosing for the heads-and-tails information. Many of these objects were used implicitly, in the sense that a string is only a context because we apply only operations related to contexts on such a string. We can be much more explicit about the objects we use, and increase flexibility and modularity, by using object-oriented programming. In this assignment, we will refactor our approach from assignment 3 into an object-oriented style. Along the way, we will speculate on the benefits of object-oriented programming, sometimes imagining that the current program is orders of magnitude larger than it is. Whenever you're able to make a comparison between the approach in assignment 3 and the object-oriented solution developed here, make a note of it in a file called oop.txt. In the text below, you will be asked to make other observations in this file as well. You will submit oop.txt for part of your grade.

Starter Code

Download the starter code containing the class statements for the classes you will implement. Save your work in mindreader.py.

HTCounter

We will use a class called HTCounter, whose objects will represent a number of heads and tails. These objects will be used as values in our memory dictionary. Create this class with the following methods.

How is using these objects as values in the memory dictionary more advantageous than your approach from assignment 3? Are there drawbacks to this more object-oriented approach? (Answer in oop.txt.)

Testing HTCounter

In a file named test_a4.py, write three Nose tests for your HTCounter class. Each test should instantiate a new object of the HTCounter class, (possibly) call some of its methods, and finally assert that the expected result holds. For example, one of your test cases might be that a newly created HTCounter object represents zero heads and zero tails. Be sure that each of your tests stresses a different ``category'' of situations. These are the only Nose tests we are asking you to write for this assignment.

ShiftBuffer

Our ShiftBuffer class will encapsulate the idea of a context. Internally, the shift buffer will still be represented as a string, but now the only access to that underlying representation is through the ShiftBuffer objects' methods. (In other words, now we are being explicit that a context is not just a string.) Create this class with the following methods. (Again, use leading underscores in your private instance variables.)

Here, we have wrapped a string object inside another object and then restricted what can be done with the encapsulated string. Why is this more advantageous than using the string directly? (Answer in oop.txt.)

MindReader

Our MindReader class manages the components of a mindreader game. Its objects hold references to a shift buffer, a memory (still represented as a dictionary), and a predictor object. We will see that the predictor object is responsible for actually providing the prediction algorithm used by the computer opponent. Create this class with the following methods.

Predictor Objects

Instead of embedding the prediction algorithm in the MindReader class itself, we have split it off into predictor objects. When we create a MindReader object, we will configure it with a given predictor object to dictate the prediction algorithm the computer should use. To create a predictor object, we instantiate any class that has a predict method. Any class with a predict method is said to conform to the predictor interface. We will create two such classes now.

Create a class named TimeSeries, with only a predict method. This method will take a context (a ShiftBuffer object) and a memory (a dictionary), in that order, and make a prediction based on the number of heads and tails that occur in memory under the given context. The algorithm should implement the strategy used by the computer opponent in assignment 3. That is, predict heads if heads is more common, predict tails if tails is more common, or otherwise predict randomly.

Now, create a class named MoreRecent, also with only a predict method. The predict method should have the same signature as predict in TimeSeries. This class will encapsulate a different prediction technique, as follows. If the current context string contains more heads, guess heads; if it contains more tails, guess tails; otherwise, guess randomly. Notice that we are not using the memory at all, even though it is passed as a parameter to this method.

Strategy Pattern

Object-oriented programming has benefited from catalogues of design patterns that capture its lessons learned and best practices. The patterns have proven useful in a wide variety of circumstances, and programmers refer to these patterns in order to take advantage of this shared collective wisdom.

Creating separate classes housing each prediction algorithm, and initializing MindReader objects with a reference to a predictor object, is an example of the strategy design pattern. We call each prediction algorithm a strategy. For the following discussion, assume that we have a large number of strategies, such as twenty.

There are two alternative approaches we could take to supporting multiple strategies.

  1. Hard-code them all in one big Mindreader class. We would have methods predict_time_series, predict_more_recent, and so on, having one method for each prediction algorithm. The get_prediction method would then use an if... elif... elif ... else construct to decide, based on a parameter, which algorithm to call.
  2. Use inheritance. For each prediction algorithm, we could create a new class by inheriting from MindReader and only providing a new get_prediction method. For example, we might have class TimeSeriesMindreader, inheriting from Mindreader and defining a get_prediction method that implements the prediction strategy of assignment 3.

In oop.txt, provide a small paragraph for each of these two alternatives, discussing their advantages and disadvantages and comparing them to the strategy pattern. For example:

Playing the Game

Take a look at the code in play.py. When you have created all of the classes above, this code can be used to play a game of Mindreader. It is very similar to the play function we gave you in assignment 3, except that the current version creates objects of user-defined classes instead of built-in Python types.

By changing one line in play.py, we can change the prediction algorithm from the one represented by TimeSeries to the one represented by MoreRecent. In oop.txt, explain how we would make this change.

Marking

These are the aspects of your work that we will focus on in the marking:

What to Hand In

Hand in the following files:

Remember that spelling of filenames, including case, counts: your files must be named exactly as above.