Roulette Bet Class

In addition to the design of the Bet class, this chapter also presents some additional questions and answers on the nature of an object, identity and state change. This continues some of the ideas from Design Decision – Object Identity.

In Roulette Bet Analysis we’ll look at the details of a Bet. This will raise a question of how to identify the Outcome associated with a Bet.

We’ll look at object identiy in Design Decision – Create or Locate an Outcome.

We’ll provide some additional details in Roulette Bet Questions and Answers.

The Roulette Bet Design section will provide detailed design for the Bet class. In Roulette Bet Deliverables we’ll enumerate the deliverables for this chapter.

Roulette Bet Analysis

A Bet is an amount that the player has wagered on a specific Outcome. This class has the responsibilty for maintaining an association between an amount, an Outcome, and a specific Player.

The general scenario is to have the Player construct a number of Bet instances. The Wheel is spun to select a winning Bin. Then each of the Bet objects will be checked to see if the hoped-for Outcome is in the actual set of Outcomes in the winning Bin.

Each winning Bet has an Outcome that matches one in the winning Bin. The winning bets will return money to the Player. All other bets aren’t in the winning Bin; they are losers, which removes the money from the Player.

We have a design decision to make. Do we create a fresh Outcome object with each Bet or do we locate an existing Outcome object?

Design Decision – Create or Locate an Outcome

Building a Bet involves two parts: an Outcome and an amount. The amount is just a number. The Outcome, however, includes two parts: a name and payout odds.

We looked at this issue in Looking Forward. We’ll revisit this design topic in some more depth here. The bottom line is this.

We don’t want to create an Outcome object as part of constructing a Bet object. Here’s what it might look like to place a $25 bet on Red:

Bad Idea

my_bet= Bet(Outcome("red", 1), 25)

The Bet includes an Outcome and an amount. The Outcome includes a name and the payout odds.

One unfortunate feature of this is that we have repeated the odds when creating an Outcome object. This violates the DRY principle.

We want to get a complete Outcome from just the name of the outcome. The will prevent repeating the odds information.

Problem. How do we locate an existing Outcome object?

Do we use a collection or a global variable? Or is there some other approach?

Forces. There are several parts to this design.

  • We need to pick a collection. When we look at the collections (see Design Decision – Choosing A Collection) we can see that a Map from name to complete Outcome instance is ideal. This helps us associate a Bet with an Outcome given just the name of the Outcome.
  • We need to identify some global object that builds the collection of distinct Outcomes.
  • We need to identify some global object that can maintain the collection of Outcomes for use by the Player in building Bets.

If the builder and maintainer are the same object, then things would be somewhat simpler because all the responsibilities would fall into a single place.

We have several choices for the kind of global object we would use.

  • Variable. We can define a variable which is a global map from name to Outcome instance. This could be an instance of the built-in dict class or some other mapping object. It could be an instance of a class we’ve designed that maps names to Outcome instances.

    A truly variable global is a dangerous thing. An immutable global object, however, is a useful idea.

    We might have this:

    Global Mapping

    >>> some_map["Red"]
    Outcome('Red', 1)
    
  • Function. We can define a Factory function which will produce an Outcome instance as needed.

    Factory Function

    >>> some_factory("Red")
    Outcome('Red', 1)
    
  • Class. We can define class-level methods for emitting an instance of Outcome based on a name. We could, for example, add methods to the Outcome class which retrieved instances from a class-level mapping.

    Class Method

    >>> Outcome.getInstance("Red")
    Outcome('Red', 1)
    

After creating the BinBuilder, we can see that this fits the overall Factory design for creating Outcome instances.

However, the class:BinBuilder doesn’t – currently – have a handy mapping so that we can look up an Outcome based on the name of the outcome. Is this the right place to do the lookup?

It would look like this:

BinBuilder as Factory

>>> theBinBuilder.getOutcome("Red")
Outcome('Red', 1)

What about the Wheel?

Wheel as Factory

>>> theWheel.getOutcome("Red")
Outcome('Red', 1)

Alternative Solutions. We have a number of potential ways to gather all Outcome objects that were created by the BinBuilder.

  • Clearly, the BinBuilder can create the mapping from name to each distinct Outcome. To do this, we’d have to do several things.

    First, we expand the BinBuilder to keep a simple Map of the various Outcomes that are being assigned via the Wheel.add() method.

    Second, we would have to add specific Outcome getters to the BinBuilder. We could, for example, include a getOutcome() method that returns an Outcome based on its name.

    Here’s what it might look like in Python.

    class BinBuilder( object ):
        ...
        def apply( self, outcome, bin, wheel ):
            self.all_outcomes.add( outcome )
            wheel.add( bin, outcome )
        def getOutcome( self, name ):
            ...
    
  • Access the Wheel. A better choice is to get Outcome obects from the Wheel. To do this, we’d have to do several things.

    First, we expand the Wheel to keep a simple Map of the various Outcomes created by the BinBuilder. This Map would be maintained by the Wheel.add().

    Second, we would have to add specific Outcome getters to the Wheel. We could, for example, include a getOutcome() method that returns an Outcome based on its name.

    We might write a method function like the following to Wheel.

    class Wheel( object ):
        ...
        def add( self, bin, outcome ):
            self.all_outcomes.add( outcome )
            ...
        def getOutcome( self, name ):
            return set( [ oc for oc in self.all_outcomes if oc.name.lower().contains( name.lower() ) ] )
    

Solution. The allocation of responsibility seems to be a toss-up. We can see that the amount of programming is almost identical. This means that the real question is one of clarity: which allocation more clearly states our intention?

The Wheel is a first-class part of the game of Roulette. It showed up in our initial noun analysis. The BinBuilder was an implementation convenience to separate the one-time construction of the Bins from the overall work of the Wheel.

Since Wheel is a first-class part of the problem, we should augment the Wheel to keep track of our individual Outcome objects by name.

Roulette Bet Questions and Answers

Why not update each Outcome with the amount of the bet?

We are isolating the static definition of the Outcome from the presence or absence of an amount wagered. Note that the Outcome is shared by the wheel’s Bin s, and the available betting spaces on the Table, possibly even the Player class. Also, if we have multiple Player, then we need to distinguish bets placed by the individual players.

Changing a field’s value has an implication that the thing has changed state. In Roulette, there isn’t any state change in the definition of an Outcome. However, when we look at Craps, we will see that changes in the game’s state will enable and disable whole sets of Outcomes.

Does an individual bet really have unique identity? Isn’t it just anonymous money?

Yes, the money is anonymous. In a casino, the chips all look alike. However, the placement of the bet, really does have unique identity. A Bet is owned by a particular player, it lasts for a specific duration, it has a final outcome of won or lost. When we want to create summary statistics, we could do this by saving the individual Bet objects. We could update each Bet with a won or lost indicator, then we can total the wins and losses.

This points up another reason why we know a Bet is an object in its own right: it changes state. A bet that has been placed can change to a bet that was won or a bet that was lost.

Roulette Bet Design

class Bet

Bet associates an amount and an Outcome. In a future round of design, we can also associate a Bet with a Player.

Fields

Bet.amountBet

The amount of the bet.

Bet.outcome

The Outcome on which the bet is placed.

Constructors

Bet.__init__(self, amount, outcome)
Parameters:
  • amount (int) – The amount of the bet.
  • outcome (Outcome) – The Outcome we’re betting on.

Create a new Bet of a specific amount on a specific outcome.

For these first exercises, we’ll omit the Player. We’ll come back to this class when necessary, and add that capability back in to this class.

Methods

Bet.winAmount(self) → int
Returns:amount won
Return type:int

Uses the Outcome‘s winAmount to compute the amount won, given the amount of this bet. Note that the amount bet must also be added in. A 1:1 outcome (e.g. a bet on Red) pays the amount bet plus the amount won.

Bet.loseAmount(self) → int
Returns:amount lost
Return type:int

Returns the amount bet as the amount lost. This is the cost of placing the bet.

Bet.__str__(self) → str
Returns:string representation of this bet with the form "amount on outcome"
Return type:str

Returns a string representation of this bet. Note that this method will delegate the much of the work to the __str__() method of the Outcome.

Bet.__repr__(self) → str
Returns:string representation of this bet with the form "Bet(amount, outcome)"
Return type:str

Roulette Bet Deliverables

There are four deliverables for this exercise. The new classes will have Python docstrings.

  • The expanded Wheel class which creates a mapping of string name to Outcome.
  • Expanded unit tests of Wheel that confirm that the mapping is being built correctly.
  • The Bet class.
  • A class which performs a unit test of the Bet class. The unit test should create a couple instances of Outcome, and establish that the winAmount() and loseAmount() methods work correctly.