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 instance. This will raise a question of how to identify the Outcome object associated with a Bet instance.

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

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

The Roulette Bet Design – Complex section will provide detailed design for the Bet class. The Roulette Bet Design – Simple section will provide some advice for an alternative design based on @dataclass definitions.

In Roulette Bet Deliverables we’ll enumerate the deliverables for this chapter.

Roulette Bet Analysis

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

The general scenario is to have the Player object construct a number of Bet instances. The Wheel object is spun to select a winning Bin instance. Once a winning bin has been chosen, each of the Bet objects will be checked to see if the hoped-for Outcome instance is in the actual set of Outcome instances in the winning Bin object.

Each winning Bet instance must have an Outcome instance that can be found in the winning Bin object. The winning bets will adds money to the Player object. All other bets are not in the winning Bin object; they are losers, which removes money from the Player object.

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

Design Decision – Create or Locate an Outcome

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

We looked at this issue in Additional Outcome Design Thoughts. We’ll revisit this design topic in some more depth here.

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 object includes an Outcome object and an amount. The Outcome object includes a name and the payout odds. We don’t want to repeat payout odds when creating an Outcome object to create a Bet object. This violates the Don’t Repeat Yourself (DRY) principle.

We want to get a complete Outcome object from 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 identify some global object that can maintain the collection of Outcome instances for use by the Player object when building Bet instances.

  • We need to create the global object that builds the collection of distinct Outcome instances. This sounds a lot like the BinBuilder class.

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 to provide a mapping from name to complete Outcome instance. 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. An alternative to a collection is 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 class, we can see that this fits the overall Factory design for creating Outcome instances.

However, the BinBuilder class doesn’t – currently – have a handy mapping to support looking up an Outcome object based on the name of an outcome. Is this the right place to do the lookup?

It would look like this:

BinBuilder as Factory

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

We could also make the case that it would fee in the the Wheel class. It would look like this:

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 class.

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

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

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

    Here’s what it might look like in Python.

    class BinBuilder:
        ...
        def save(self, outcome: Outcome, bin: int, wheel: Wheel) -> None:
            self.all_outcomes[outcome.name] = outcome
            wheel.add(bin, outcome)
    
        def getOutcome(self, name):
            return self.all_outcomes[name]
        ...
    
  • Access the Wheel object. A better choice is to get Outcome objects from the Wheel. To do this, we’d have to do several things.

    First, we expand the Wheel class to keep a simple dict of the various Outcome instances created by a BinBuilder object. This dict would be updated by the Wheel.add() method.

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

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

    class Wheel:
        ...
        def add(self, bin: int, outcome: Outcome) -> None:
            self.all_outcomes[outcome.name] = outcome
            self.bins[bin].add(outcome)
    
        def getOutcome(self, name):
            return self.all_outcomes[name]
        ...
    

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 class is an essential part of the game of Roulette. It showed up in our initial noun analysis. The BinBuilder class was an implementation convenience to separate the one-time construction of the Bin instances from the overall work of the Wheel object.

Since the Wheel class is part of the problem, as well as part of the solution, it seems better to augment the Wheel class to keep track of our individual Outcome objects by name.

In the next sections, the questions and answers will look at some additional design considerations. After that, we’ll look at two versions of the design. The complex version will build all of the methods; the simpler version will rely on @dataclass.

Roulette Bet Questions and Answers

Why not update each Outcome instance with the amount of the bet on that outcome?

We are isolating the static definition of the Outcome objects from the presence or absence of an amount wagered. Note that an Outcome object is shared by the wheel’s Bin instances, and the available betting spaces on a Table instance, and possibly even the Player class. Also, if we have multiple Player objects, 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 an Outcome instance. Neither the name nor the odds change.

The odds associated with an outcome can’t change; this is a fundamental principle of casino gambling. An outcome may be disabled by certain game states, but the payout must be well known to the players.

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. 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.

This points up another reason why we know a Bet instance is distinct from the associated Outcome object. A Bet instance changes state; initially a bet is active, in some games they can be deactivated, eventually they are winners or losers.

We don’t need all of this state-change machinery for simulating Roulette. We will, however, see more complex bets when simulating Craps.

Roulette Bet Design – Complex

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: int, outcome: Outcome) → None
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=amount, outcome=outcome)"

Return type

str

Roulette Bet Design – Simple

A simpler variation on the Bet class can be based on @dataclass.

See above, in the fields section, the two fields required.

The default methods created by the @dataclass decorator should work perfectly.

The __str__() method will have to be written based on the description above, under methods.

This should pass all of the unit tests described in the Roulette Bet Deliverables section.

Wheel Redesign

We’ll need to update the Wheel class to have the following method. This will return an Outcome instance given the string name of the outcome. This works by maintaining a dict of Outcome objects using the name attribute as a key. This is built incrementally as each Bin added to the Wheel instance.

Wheel.getOutcome(str: name) → Outcome
Parameters

name (str) – the name of an Outcome

Returns

the Outcome object

Return type

Outcome

This should raise an exception if the string isn’t the name of a known Outcome.

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.

Looking Forward

Once we have a useful definition of bets we have to work out how to place them, and decide if they are winners or losers. In the next chapter, we’ll look at the Table class as a container for active Bet instances.