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 thePlayer
object when buildingBet
instances.We need to create the global object that builds the collection of distinct
Outcome
instances. This sounds a lot like theBinBuilder
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-indict
class to provide a mapping from name to completeOutcome
instance. It could be an instance of a class we’ve designed that maps names toOutcome
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 theOutcome
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 distinctOutcome
instance. To do this, we’d have to do several things.First, we expand the
BinBuilder
class to keep a simple Map of the variousOutcome
instances that are being assigned via theWheel.add()
method.Second, we would have to add specific
Outcome
instance getters to theBinBuilder
class. We could, for example, include agetOutcome()
method that returns anOutcome
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 getOutcome
objects from theWheel
. To do this, we’d have to do several things.First, we expand the
Wheel
class to keep a simple dict of the variousOutcome
instances created by aBinBuilder
object. This dict would be updated by theWheel.add()
method.Second, we would have to add specific
Outcome
getter functions to theWheel
class. We could, for example, include agetOutcome()
method that returns anOutcome
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 anOutcome
object is shared by the wheel’sBin
instances, and the available betting spaces on aTable
instance, and possibly even thePlayer
class. Also, if we have multiplePlayer
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 individualBet
objects.This points up another reason why we know a
Bet
instance is distinct from the associatedOutcome
object. ABet
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 anOutcome
. In a future round of design, we can also associate aBet
with 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
’swinAmount
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 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
Wheel
class which creates a mapping of string name toOutcome
.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 ofOutcome
, and establish that thewinAmount()
andloseAmount()
methods work correctly.