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
Outcomeinstances for use by thePlayerobject when buildingBetinstances.We need to create the global object that builds the collection of distinct
Outcomeinstances. This sounds a lot like theBinBuilderclass.
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
Outcomeinstance. This could be an instance of the built-indictclass to provide a mapping from name to completeOutcomeinstance. It could be an instance of a class we’ve designed that maps names toOutcomeinstances.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
Outcomeinstance as needed.Factory Function
>>> some_factory("Red") Outcome('Red', 1)
Class. We can define class-level methods for emitting an instance of
Outcomebased on a name. We could, for example, add methods to theOutcomeclass 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
BinBuilderclass can create the mapping from name to each distinctOutcomeinstance. To do this, we’d have to do several things.First, we expand the
BinBuilderclass to keep a simple Map of the variousOutcomeinstances that are being assigned via theWheel.add()method.Second, we would have to add specific
Outcomeinstance getters to theBinBuilderclass. We could, for example, include agetOutcome()method that returns anOutcomeobject 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
Wheelobject. A better choice is to getOutcomeobjects from theWheel. To do this, we’d have to do several things.First, we expand the
Wheelclass to keep a simple dict of the variousOutcomeinstances created by aBinBuilderobject. This dict would be updated by theWheel.add()method.Second, we would have to add specific
Outcomegetter functions to theWheelclass. We could, for example, include agetOutcome()method that returns anOutcomeobject 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
Outcomeobjects from the presence or absence of an amount wagered. Note that anOutcomeobject is shared by the wheel’sBininstances, and the available betting spaces on aTableinstance, and possibly even thePlayerclass. Also, if we have multiplePlayerobjects, 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
Outcomeinstance. 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
Betis 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 individualBetobjects.This points up another reason why we know a
Betinstance is distinct from the associatedOutcomeobject. ABetinstance 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¶ Betassociates an amount and anOutcome. In a future round of design, we can also associate aBetwith aPlayer.
Constructors¶
-
Bet.__init__(self, amount: int, outcome: Outcome) → None -
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’swinAmountto 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 theOutcome.
-
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¶
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
Wheelclass which creates a mapping of string name toOutcome.Expanded unit tests of
Wheelthat confirm that the mapping is being built correctly.The
Betclass.A class which performs a unit test of the
Betclass. The unit test should create a couple instances ofOutcome, and establish that thewinAmount()andloseAmount()methods work correctly.