Copyright © 2008 Steven F. Lott
This work is licensed under a Creative Commons License. You are free to copy, distribute, display, and perform the work under the following conditions:
Attribution. You must give the original author, Steven F. Lott, credit.
Noncommercial. You may not use this work for commercial purposes.
No Derivative Works. You may not alter, transform, or build upon this work.
For any reuse or distribution, you must make clear to others the license terms of this work.
6/16/2008
Table of Contents
unittest Testingdoctest TestingJUnit TestingEpydoc Documentationjavadoc DocumentationList of Tables
List of Examples
instanceofplayer ModuleList of Equations
Table of Contents
The present letter is a very long one, simply because I had no leisure to make it shorter.
The coffee-shop answer is to provide the beginning designer with a sequence of interesting and moderately complex exercises in OO design.
Some software developers find themselves stalled when trying to do object-oriented (OO) design. As programmers, they've understood the syntax of a programming language, and pieced together small examples. However, it is often difficult to take the next step to becoming a designer. Because this transition from guided learning of language features to self-directed design work is often ignored, programmers are left to struggle through their first design projects without appropriate skills or support. While it is critically important to examine examples of good design, a finished product doesn't reveal the author's decision-making process that created the design.
The most notable consequence of this skills gap is the creation of software that is far more complex than necessary to effectively solve a given problem. This, in turn, leads to software with high maintenance costs stemming from the low quality. It also leads to an unfair indictment of OO technology; this is usually voiced as “we tried OO programming and it failed.”
As programming team leaders, educators and consultants, we find that software development training is focused on the programming tools, but does not expose the process of creating a design. In the building trades, we would neither expect nor allow apprentice plumbers to design the sanitary sewage system for an urban office building. Yet, in too many Information Technology (IT) departments, software developers are expected to leap from basic training in their tools to application design.
To continue this rant, we also find that some managers are entrusted with significant projects, but are uncomfortable with OO design on modern high-performance hardware. They tend to focus their design energies on the kinds of software architectures that were appropriate when the enterprise owned a single computer, when 64 megabytes of memory was all the enterprise would ever need, and centralized disk storage was charged back to end user departments at a rate of pennies per track per month. In some organizations, there are a few enduring symptoms of this mind set in some of the ways that “end-user computing” is separated from “enterprise computing”; we relegate everything non-mainframe to second class status.
Management discomfort with OO technology surfaces in many ways. One shocking comment was that “no application needs more than six classes.” A consequence of this management attitude is an unrealistic expectation for schedule and the evolution of the deliverables.
The intent of this book is to help the beginning designer by giving you a sequence of interesting and moderately complex exercises in OO design. This book can also help managers develop a level of comfort with the process of OO software development. The applications we will build are a step above trivial, and will require some careful thought and design. Further, because the applications are largely recreational in nature, they are interesting and engaging. This book allows the reader to explore the processes and artifacts of OO design before project deadlines make good design seem impossible.
We hope to prevent managers from saying the following: “We had a good design, but were forced to compromise it to meet our schedule.” As consultants, we find this to be a sad statement of management's emphasis of one near-term goal over long-term value. In one case, this was the result of a series of poorly-informed management decisions compounded on weak design skills. One of the root causes was the inability of the designers and managers to agree to a suitable course of action when a new kind of requirement made devastating changes to an existing design. We believe that more informed managers would have made a decision that created better long-term value.
Our primary audience is programmers who are new to OO programming. Have you found your first exposure to objects to be more distasteful than empowering? Why does this happen? Some instructors launch into extensive presentations on object orientation before getting to the language fundamentals, leaving students lost as to how they will accomplish the noble and lofty goals of OO. Other instructors leave OO for last, exposing the procedural side of the language first, and treating objects as a kind of add-on. This leaves students feeling that objects are optional. Additionally, some very skilled instructors are not skilled developers, and will often show examples that don't reflect currently accepted best practices.
Our audience has an exposure to the language, but needs more time to understand objects and object-orientation. We want to provide exercises that have four key features: just complex enough to require careful design work, just fun enough to be engaging, easy enough that results are available immediately, and can be built in simple stages.
In our effort to support the beginning student, we'll provide a few additional details on language features. We'll mark these as “Tips” for the new programmer, and qualify the tip by language. For more advanced students, these tips will be review material. We will not provide a thorough background in any programming language. The student is expected to know the basics of the language and tools.
Helpful additional skills include using one of the various unit test and documentation frameworks available. We've included information in the appendices.
Instructors are always looking for classroom projects that are engaging, comprehensible, and focus on perfecting language skills. Many real-world applications require considerable explanation of the problem domain; the time spent reviewing background information detracts from the time available to do the relevant programming. While all application programming requires some domain knowledge, the idea behind these exercises is to pick a domain that many people know a little bit about. This allows an instructor to use some or all of these exercises without wasting precious classroom time on incidental details required to understand the problem.
This book assumes an introductory level of skill in an OO programming language. We provide specific examples in Java (at least version 1.4) and Python (at least version 2.5). Student skills we expect include the following.
java.util package. For Python programmers,
this means the built-in sequence and mapping types.Optionally, some experience with a unit testing framework.
See the appendices for supplemental exercises if you aren't
familiar with Python's unittest or
doctest or Java's
JUnit.
Optionally, some experience writing formal documentation.
For Java programmers, this means javadoc comments. For Python
programmers, this often means Epydoc or a
similar documentation package. See the appendices for supplemental
exerises if you aren't familiar with formal, deliverable
documentation.
This book presents a series of exercises to build simulations of the common, popular casino table games: Roulette, Craps and Blackjack. Each simulation can be extended to include variations on the player's betting system. With a simple statistical approach, we can show the realistic expectations for any betting system. Each of these games has a separate part in this book. Each part consists of a number of individual exercises to build the entire simulation. The completed project results in an application that can provide simple tabular results that shows the average losses expected from each betting strategy.
The interesting degree of freedom in each of the simulations is the player's betting strategy. The design will permit easy adaptation and maintenance of the player's strategies. The resulting application program can be extended by inserting additional betting systems, which allows exploration of what (if any) player actions can minimize the losses.
For Roulette, we proceed slowly, building up the necessary application one class at a time. Since this is the simplest game, the individual classes reflect that simplicity. We focus on isolation of responsibilities, creating a considerable number of classes. The idea is to build skills in object design by applying those skills to a number of classes.
The first chapter of the part provides details on the game of Roulette and the problem that the simulation solves. The second chapter is an overview of the solution, setting out the highest-level design for the application software. This chapter includes a technique for doing a “walk-through” of the design to be confident that the design will actually solve the problem.
Each of the remaining sixteen chapters is a design and programming exercise to be completed by the student. Plus or minus a Frequently Asked Questions (FAQ) section, each chapter has the same basic structure: an overview of the components being designed, some design details, and a summary of the deliverables to be built. The overview section presents some justification and rationale for the design. This material should help the student understand why the particular design was chosen. The design section provides a more detailed specification of the class or classes to be built. This will include some technical information on Java or Python implementation techniques.
For Craps, we build on the design patterns from Roulette. Craps, however, is a stateful game, so there is a more sophisticated design to handle the interactions between dice, game state and player. We exploit the State design pattern to show how the design pattern can be applied to this simple situation.
The first chapter is background information on the game of Craps, and the problem that the simulation solves. The second chapter is an overview of the solution, setting out the highest-level design for the application software. This chapter also provides a “walk-through” of the design.
Each of the remaining eleven chapters is an exercise to be completed by the student. Each chapter has the same basic structure: an overview of the component being designed, some design details, and a summary of the deliverables to be built.
The most sophisticated game that we cover here is Blackjack. The game states are more sophisticated than Craps since the available betting opportunities can change with split hands. Further, the player has two kinds of choices: betting opportunities, plus play opportunities. This makes the player's strategy considerably more complex. In casino gift shops, you can buy small summary cards that enumerate all possible game states and responses. The more advanced student can tackle these sophisticated betting strategies. For the less advanced student we will simplify the strategies down to some simpler conditions.
The first two chapters are background information on the game of Blackjack, the problem that the simulation solves, and an overview of the solution, setting out the highest-level design for the application software. Each of the remaining six chapters is an exercise to be completed by the student. Since this is more advanced material, and builds on previous work, this part has many simple deliverables compressed into the individual chapters.
Casino table games may seem like an odd choice of subject matter for programming exercises. We find that casino games have a number of advantages for teaching OO design and OO programming.
Casino games have an almost ideal level of complexity. If they were too simple, the house edge would be too obvious and people would not play them. If they were too complex, people would not enjoy them as simple recreation. Years (centuries?) of experience in the gaming industry has fine-tuned the table games to fit nicely with the limits of our human intellect.
Simulation of discrete phenomena lies at the origin of OO programming. We have found it easier to motivate, explain and justify OO design when solving simulation problems. The student can then leverage this insight into other applications of OO programming for more common transactional applications.
The results are sophisticated but easy to interpret. Probability theory has been applied by others to develop precise expectations for each game. These simulations should produce results consistent with the known probabilities. This book will skim over the probability theory in order to focus on the programming. For a few exercises, the theoretical results will be provided to serve as checks on the correctness of the student's work.
This book does not endorse casino gaming. Indeed, one of the messages of this book is that all casino games are biased against the player. Even the most casual study of the results of the exercises will allow the student to see the magnitude of the house edge in each of the games presented.
We have to adopt a style for each of the languages we're presenting. We won't present a complete set of coding standards; we will omit a number of issues that should be standardized. Some IT shops have documents they call “coding standards”, but are little more than descriptive style guides. What follows is not this kind of style guide; instead, it is some justification of the style we use for the examples in this book.
Just to continune this rant, we find that source code examples speak louder than any gratuitously detailed “specification” of the desired style. We find that some IT organizations waste time trying to write definitions of the preferred style. A good example trumps the description of the example. In particular, as consultants, we are often asked to provide standards to an inexperienced team of programmers. While the programmers only look at the examples (often cutting and pasting them), some managers prefer to spend money on empty verbiage peripheral to the useful example.
We use Java-centric terminology -- “field” and “method” -- throughout the book. Occaisionally, we will emphasize the differences between Java and Python by using the Python terms “attribute”, “instance variable” or “method function”.
We avoid using complex prefixes for variable names. In particular,
we find prefixes to be little more than visual clutter.
For example, an integer parameter with the amount of a bet might be
called pi_amount where the prefix indicates the scope
(p for a parameter) and type
(i for an integer).
This style of name is only appropriate for primitive types, and doesn't address complex data structures well at all. How does one name a parameter that is a LinkedList of Sets of Outcomes? In Java programs, the variables are formally declared, therefore, we find that we don't need additional cues for their data type.
In some cases, prefixes are used to denote the scope of an
instance variables. Variable names might include a cryptic one-letter
prefix like “f” to denote an instance variable; sometimes
programmers will use “my” or “the” as an
English-like prefix. We prefer to reduce clutter. In Java, we have the
qualifier this. available to
disambiguate parameter from instance variable references. In Python,
instance variables are always qualified, typically by
self., making the scope very
clear.
Code examples will be minimal, and will include Python and Java. Here is a Python example.
Example 1. Typical Python Example
combo = { }
for i in range(1,7):
for j in range(1,7):
roll= i+j
combo.setdefault( roll, 0 )
combo[roll] += 1
for n in range(2,13):
print "%d %.2f%%" % ( n, combo[n]/36.0 )
The output from the above program will be shown as follows:
2 0.03% 3 0.06% 4 0.08% 5 0.11% 6 0.14% 7 0.17% 8 0.14% 9 0.11% 10 0.08% 11 0.06% 12 0.03% Tool completed successfully
We will use the following type styles for references to a specific
Class, method, or
variable.
Most of the design specifications will provide Java-style method and variable descriptions. Python doesn't use type specifications, and Python programmers will have to translate the Java specifications into Python by removing the type names.
There will be design tips, and warnings, in the material for each exercise. These reflect considerations and lessons learned that aren't typically clear to starting OO designers.
We would like to thank Chuck Pyrak for putting us up to this. His idea of a One Room Schoolhouse to teach Java to an audience at multiple skill levels was a great idea. Additionally, our colleagues who collaborated through BLOKI brought infinte wisdom and insight to a complex and difficult project.
Thanks to Dion Dock for detailed, thoughtful comments.
Table of Contents
We'll set our goal by presenting several elements that make up a complete problem statement: a context in which the problem arises, the problem, the forces that influence the choice of solution, the solution that balances the forces, and some consequences of the chosen solution.
Based on the problem statement, we'll present the high-level use case that this software implements. The use case is almost too trivial to bother defining. However, we have seen many projects run aground because they lacked even the most rudimentary description of the actor, the system and how the system helps the actor create value.
We will summarize the approach to the solution, describing the overall strategy that we will follow. This is a kind of overall design pattern that we'll use to establish some areas of responsibility.
We will also describe the technical foundations. In this case, they are not terribly complex, but this is an important part of describing any software solution, no matter how simple.
We will dance around the methodology issue. Our intent is not to sell a particular methodology, but to provide some perspective on how we broke the work into manageable pieces.
Finally, we'll present some important parts of getting started on the solution. These are more specific, technical considerations that define common aspects of our approach.
Context. Our context is the “classic” casino table games played against the house, including Roulette, Craps and Blackjack. We want to explore the consequences of various betting strategies for these casino games. Questions include “How well does the Cancellation strategy work?” “How well does the Martingale strategy works for the Come Line odds bet in Craps?” “How well does this Blackjack strategy I found on the Internet compare with the strategy card I bought in the gift shop?”
A close parallel to this is exploring variations in rules and how these different rules have an influence on outcomes. Questions include “What should we do with the 2x and 10x odds offers in Craps?” “How should we modify our play for a single-deck Blackjack game with 6:5 blackjack odds?”
Our context does not include exploring or designing new casino games. Our context also excludes multi-player games like poker. We would like to be able to include additional against-the-house games like Pai Gow Poker, Caribbean Stud Poker, and Baccarat.
Problem. Our problem is to answer the following question: For a given game, what player strategies produce the best results?
Forces. There are a number of forces that influence our choice of solution. First, we want an application that is relatively simple to build. Instead of producing an interactive user interface, we will produce raw data and statistical summaries. If we have little interaction, a command-line interface will work perfectly. We can have the user specify a player strategy and the application respond with a presentation of the results. If the results are tab-delimited, they can be pasted into a spreadsheet for further analysis.
Another force that influences our choice of solution is the need to be platform and language agnostic. In this case, we have selected an approach that works well on POSIX-compliant operating systems (i.e., Linux, MacOS, and all of the proprietary UNIX variants), and also works on non-compliant operating systems (i.e., all of the Windows versions). We have chosen two OO languages that work identically on both platform families: Java and Python.
We also need to strike a balance between interesting programming, probability theory and statistics. On one hand, the simplicity of these games means that complete analyses have been done using probability theory. However, that's not a very interesting programming exercise, so we will ignore the pure probability theory route in favor of learning OO design and programming.
Another force is the desire to reflect actual game play. While a long-running simulation of thousands of invidual cycles of play will approach the theoretical results, people typically don't spend more than a few hours at a table game. If, for example, a Roulette wheel is spun once each minute, a player is unlikely to see more that 480 spins in an 8-hour evening at a casino. Additionally, many players have a fixed budget, and the betting is confined by table limits. Finally, we need to address the subject of “money management”: a player may elect to stop playing when they are ahead. This structures our statistical analysis: we must simulate sessions of play that are limited in time, the amount lost and the amount won.
Solution. Our overall goal is to produce an application that allows us to experiment with different casino game betting strategies. We'll build a simple, command-line simulator that provides a reliable, accurate model of the game. We need to be able to easily pick one of a variety of player betting strategies, play a number of simulated rounds of the game, and produce a statistical summary of the results of that betting strategy.
Consequences. One of the most important consequences of our solution is that we will build an application into which new player betting strategies can be inserted. Clever gamblers invent new strategies all the time. We will not know all of the available strategies in advance, so we will not be able to fully specify all of the various design details in advance. Instead, we will find ourselves reworking some parts of the solution, to support a new player betting strategy. This forces us to take an agile approach to the design and implementation.
We have a single, small use case. We have some opinions on what makes a useful use case in our Soapbox on Use Cases. There is a single actor, the “investigator”. The actor's goal is to see the expected results of using a particular strategy for a particular game. The typical scenario is the following.
Procedure 1.1. Basic Use Case
Actor
Specifies which game and betting strategy to test. The strategy may need additional parameters, like an initial budget, or stake. The game may require additional parameters, like betting limits.
System
Responds with a statistical summary of the outcomes after a fixed number of cycles (spins, or throws or hands). The number of cycles needs to be small (on the order of 200, to reflect only a few hours of play).
This system use case is embedded in an overall cycle of investigation that forms a kind of business use case. From this overall view, the actor's goal is to find an optimal strategy for a given game. The procedure includes the following steps.
Procedure 1.2. Business Use Case
Actor
Researches alternative strategies. Uses IDE to build new classes.
IDE
Creates new classes for the simulator.
Actor
Uses the Basic Use Case. Runs the simulator with selection of game and strategy.
Simulator
Responds with statistical results.
Actor
Evaluates the results. Uses a spreadsheet or other tool for analysis and visualization.
We are addressing parts of this larger business use case. While not describing the detailed “how-to” of using the IDE to build the new classes, we will address the design of those new classes. Additionally, we won't address how to analyze the results.
From reading the problem and use case information, we can identify at least the following four general elements to our application.
When we look at common design patterns, the Model-View-Control pattern often helps to structure applications. A more sophisticated, transactional application may require a more complex structure. However, in this case, the game, the player, and the statistics are the model. The command line selection of player and the reporting of raw data is the view. The overall control component creates the various objects to start the simulation.
While interesting, we will not pursue the design of a general-purpose simulation framework. Nor will we use any of the available general frameworks. While these are handy and powerful tools, we want to focus on developing application software “from scratch” as a learning exercise.
Our solution will depend heavily on desktop integration: the actor will use their IDE to create a strategy and build a new version of the application program. Once the application is built, the actor can run the application from the command line, collecting the output file. The statistical results file can be analyzed using a spreadsheet application. There are at least three separate application programs involved: the IDE (including editor and compiler), the simulator, the spreadsheet used for analysis.
A typical execution of the simulator will look like the following example.
Example 1.1. Sample Java Execution
javacasino.MainCrapsSim
--Dplayer.name="Player1326"
>details.log
We are intentionally limiting our approach to a simple command-line application using the default language libraries. Avoiding additional libraries assures a “lowest-common denominator” multi-platform application. For Java, this standard is the J2SE set of libraries; we won't use any J2EE extensions. For Python, it is the base installation.
There are a number of more technical considerations that we will expand in the section called “Deliverables”. These include the use of an overall simulation framework and an approach for unit testing.
Among the topics this book deals with in a casual -- possibly misleading -- manner are probability and statitics. Experts will spot a number of gaps in our exposition. For example, there isn't a compelling need for simulation of the simpler games of Craps and Roulette, since they can be completely analyzed. However, our primary objective is to study programming, not casino games, therefore we don't mind solving known problems again. We are aware that our statistical analysis has a number of deficiencies. We will avoid any deeper investigation into statistics.
We want to focus on technical skills; we won't follow any particular software development methodology too closely. We hesitate to endorse a specific methodology; doing so inevitably alienates readers who embrace a different methodology. To continue this rant, we find that almost everyone has an existing notion of the proper way to organize software development work. This leads to the common practice of customizing methodologies, in most cases without a deep background in the methodology or the changes being made.
We prefer to lift up a few techniques which have a great deal of benefit.
Incremental Development. Each chapter is a "sprint" that produces some collection of deliverables. Each part is a complete release.
Unit Testing. We don't dwell on test-driven development, but each chapter explicitly requires unit tests for the classes built.
Embedded Documentation. We provide appendices on how to use Epydoc or javadoc to create usable API documents.
The exercises are presented as if we are doing a kind of iterative design with very, very small deliverables. We present the exercises like this for a number of reasons.
First, we find that beginning designers work best with immediate feedback on their design decisions. While we present the design in considerable detail, we do not present the final code. Programmers new to OO design will benefit from repeated exposure to the transformation of problem statement through design to code.
Second, for iterative or agile methodologies, this presentation parallels the way software is developed. A project manager may use larger collections of deliverables. However, the actual creation of functional source eventually decomposes into classes, fields and methods. For project managers, this exposition will help them see where and how rework can occur; giving them a chance to plan for the kind of learning that occur in most projects.
Third, for project teams using a strict waterfall methodology -- with all design work completed before any programming work -- the book can be read in a slightly different order. From each exercise chapter, read only the overview and design sections. From that information, integrate the complete design. Then proceed through the deliverables sections of each chapter, removing duplicates and building only the final form of the deliverables based on the complete design. This will show how design rework arises as part of a waterfall methodology.
We try to embrace a variety of deliverables in addition to working source code. In particular, we expect unit test classes in addition to the functional classes. Further, we expect Javadoc comments or Python docstrings in each class. Additionally, we will need to write some demonstration classes in order to determine the exact sequence of random numbers that are created from a specific seed; these are not deliverable as part of the final application, but will be necessary to construct proper unit tests.
This section addresses a number of methodology or process topics:
Quality, in general
Rework
Technical Decision-Making
Reuse
Design Patterns
Our approach to overall quality assurance is relatively simple. We feel that a focus on unit testing and documetation covers most of the generally accepted quality factors. The Software Engineering Institute (SEI) published a quality measures taxonomy. While officially "legacy", it still provides an exhaustive list of quality attributes. These are broadly grouped into five categories. Our approach covers most of those five categories reasonably well.
Need Satisfaction. Does the software meet the need? We start with a problem statement, define the use case, and then write software which is narrowly focused on the actor's needs. By developing our application in small increments, we can ask ourself at each step, "Does this meet the actor's needs?" It's fairly easy to keep a software development project focused when we have use cases to describe our goals.
Performance. We don't address this specifically in this book. However, the presence of extensive unit tests allows us to alter the implemention of classes to change the overall performance of our application. As long as the resulting class still passes the unit tests, we can develop numerous alternative implementations to optimize speed, memory use, input/output, or any other resource.
Maintenance. Software is something that is frequently changed. It changes when we uncover bugs. More commonly, it changes when our understanding of the problem, the actor or the use case changes. In many cases, our initial solution merely clarifies the actor's thinking, and we have to alter the software to reflect a deeper understanding of the problem.
Maintenance is just another cycle of the iterative approach we've chosen in this book. We pick a feature, create or modify classes, and then create or modify the unit tests. In the case of bug fixing, we often add unit tests to demonstrate the bug, and then fix our classes to pass the revised unit tests.
Adaptation. Adaptation refers to our need to adapt our software to changes in the environment. The environment includes interfaces, the operating system or platform, even the number of users is part of the environment. When we address issues of interoperability with other software, portability to new operating systems, scalability for more users, we are addressing adaptation issues.
We chose Python and Java to avoid having interoperability and portability issues — these platforms give admirable support for many scalability issues. Generally, a well-written piece of software can be reused. While this book doesn't focus on reuse, Java and Python are biased toward writing reusable software.
Organizational. There are some organizational quality factors: cost of ownership and productivity of the developers creating it. We don't address these directly. Our approach, however, of developing software incrementally often leads to good developer productivity.
Our approach (Incremental, Unit Testing, Embedded Documentation) assures high quality in four of the five quality areas. Incremental development is a way to focus on need satisfaction. Unit testing helps us optimize resource use, and do maintenance well. Our choices of tools and platforms help us address adaptation.
The organizational impact of these techniques isn't so clear. It is easy to mis-manage a team and turn incremental development into a quagmire of too much planning for too little delivered software. It is all too common to declare that the effort spent writing unit test code is "wasted".
Ultimately, this is a book on OO design. How people organize themselves to build software is beyond our scope.
In the section called “Problem Statement”, we described the problem. In the section called “Solution Approach”, we provided an overview of the solution. The following parts will guide you through an incremental design process; a process that involves learning and exploring. This means that we will coach you to build classes and then modify those classes based on lessons learned during later steps in the design process. See our Soapbox on Rework for an opinion on the absolute necessity for design rework.
We don't simply present a completed design. We feel that it is very important follow a realistic problem-solving trajectory so that beginning designers are exposed to the decisions involved in creating a complete design. In our experience, all problems involve a considerable amount of “learn as you go”. We want to reflect this in our series of exercises. In many respects, a successful OO design is one that respects the degrees of ignorance that people have when starting to build software. We will try to present the exercises in a way that teaches the reader how to manage ignorance and still develop valuable software.
Many of the chapters will include some lengthy design decisions that appear to be little more than hand-wringning over nuances. While this is true to an extent, we need to emphasize our technique for doing appropriate hand-wringing over OO design. We call it “Looking For The Big Simple”, and find that managers don't often permit the careful enumeration of all the alternatives and the itemization of the pros and cons of each choice. We have worked with managers who capriciously play their “schedule” or “budget” trump cards, stopping useful discussion of alternatives. This may stem from a fundamental discomfort with the technology, and a consequent discomfort of appearing lost in front of team members and direct reports. Our suggestion in this book can be summarized as follows:
Good OO design comes from a good process for technical decision-making.
First, admit what we don't know, and then take steps to reduce our degrees of ignorance.
Which means not saying “work smarter not harder” unless we also provide the time and budget to actually get smarter. The learning process, as with all things, must be planned and managed. Our lesson learned from Blaise Pascal is that a little more time spent on design can result in considerable simplification, which will reduce development and maintenance costs.
It's also important to note that no one in the real world is omniscient. Some of the exercises include intentional dead-ends. As a practical matter, we can rarely foresee all of the consequences of a design decision.
While there is a great deal of commonality among the three games, the exercises do not start with an emphasis on constructing a general framework. We find that too much generalization and too much emphasis on reuse is not appropriate for beginning object designers. See Soapbox on Reuse for an opinion on reuse. Additionally, we find that projects that begin with too-lofty reuse goals often fail to deliver valuable solutions in a timely fashion. We prefer not to start out with a goal that amounts to boiling the ocean to make a pot of tea.
These exercises will refer to several of the “Gang of Four” design patterns in Gamma95. The Design Patterns book is not a prerequisite; we use it as reference material to provide additional insight into the design patterns used here. We feel that use of common design patterns significantly expands the programmer's repertoire of techniques. We note where they are appropriate, and provide some guidance in their implementation.
In addition, we reference several other design patterns which are not as well documented. These are, in some cases, patterns of bad design more than patterns of good design.
Each chapter defines the classes to be built and the unit testing that is expected. A third deliverable is merely implied. The purpose of each chapter is to write the source files for one or more classes, the source files for one or more unit tests, and assure that a minimal set of API documentation is available.
Source Files. The source files are the most important deliverable. In effect, this is the working application program. Generally, you will be running this application from within your Integrated Development Environment (IDE). You may want to create a stand-alone program.
In the case of Java, we might also deliver the collection of class files. Additionally, we might bundle the class files into an executable JAR file. The source is the principle deliverable; anyone should be able to produce class and JAR files using readily available tools.
In the case of Python, it's the packages of
.py files. There really isn't much more to deliver.
The interested student might want to look at the Python
distutils and setuptools
to create a distribution kit, or possibly a Python
egg file.
Unit Test Files. The deliverables section of each chapter summarizes the unit testing that is expected, in addition to the classes to be built. We feel that unit testing is a critical skill, and emphasize it throughout the inividual exercises. We don't endorse a particular technology for implementing the unit tests. There are several approaches to unit testing that are in common use.
Formal Unit Tests. For formal testing of some class, X,
we create a separate class, TestX, which
creates instances of X and exercises those
instances to be sure they work. In Java, this is often done with
JUnit. In Python, the
unittest module is the mechanism for doing
formal unit tests. Additionally, many Python developers also use
the doctest module to assure that the
sample code in the docstrings is actually correct. We cover these
technologies in the appendices.
Informal Unit Tests. This is often done by include a main program in the class definition file. We'll show two examples of this. By following the standard unit test naming conventions, these informal tests can be easily upgraded to more formal JUnit or PyUnit tests.
In Python, an informal is done by including the following block of
code section in a module file. Each test method must have a name that
starts with “test” to be compatible with the Python
PyUnit module.
Example 1.2. Informal Python Unit Test
def testX():
test procedure X goes here
def testY():
test procedure Y goes here
if __name__ == "__main__":
testX()
testY()
In Java, this is done by putting public static void main(String[] args); in the class source file. Each test method must have
a name that starts with “test” to be compatible with the
Java JUnit package.
Example 1.3. Informal Java Unit Test
class SomeClass {
public static void main( String[] args ) {
SomeClass x= new SomeClass();
x.testX();
x.testY();
}
public void testX() {
test procedure X goes here
}
public void testY() {
test procedure Y goes here
}
}
Documentation. The job isn't over until the paperwork is done. In the case of Java and Python, the internal documentation is generally built from specially formatted blocks of comments within the source itself. Java programmers can use the javadoc tool to create documentation from the program source. Python programmers can use Epydoc to create similar documentation.
This part describes the game of Roulette. Roulette is a stateless game with numerous bets and a very simple process for game play.
The chapters of this part present the details on the game, an overview of the solution, and a series of sixteen exercises to build a complete simulation of the game, plus a variety of betting strategies. Each exercise chapter builds at least one class, plus unit tests; in some cases, this includes rework of previous deliverables.
Table of Contents
Table of Contents
In the first section we will present a summary of the game of Roulette as played in most American casinos.
We will follow this with a review the various bets available on the Roulette table in some depth. The definition of the various bets is an interesting programming exercise, and the first four exercise chapters will focus on this.
Finally, we will describe some common betting strategies that we will simulate. The betting strategies are interesting and moderately complex algorithms for changing the amount that is used for each bet in an attempt to recoup losses.
The game of Roulette centers around a wheel with thirty-eight numbered bins. The numbers include 0, 00 (double zero), 1 through 36. The table has a surface marked with spaces on which players can place bets. The spaces include the 38 numbers, plus a variety of additional bets, which will be detailed below. After the bets are placed by the players, the wheel is spun by the house, a small ball is dropped into the spinning wheel; when the wheel stops spinning, the ball will come to rest in one of the thirty-eight numbered bins, defining the winning number. The winning number and all of the related winning bets are paid off; the losing bets are collected. Roulette bets are all paid off using odds, which will be detailed with each of the bets, below.
The numbers from 1 to 36 are colored red and black in an arbitrary pattern. They fit into various ranges, as well as being even or odd, which defines many of the winning bets related to a given number. The numbers 0 and 00 are colored green, they fit into none of the ranges, and are considered to be neither even nor odd. There are relatively few bets related to the zeroes. The geometry of the betting locations on the table defines the relationships between number bets.
There are slight variations in Roulette between American and European casinos. We'll focus strictly on the American version.
There are a variety of bets available on the Roulette table. Each bet has a payout, which is stated as n:1 where n is the multiplier that defines the amount won based on the amount bet.
A $5 bet at 2:1 will win $10. After you are paid, there will be $15 sitting on the table, your original $5 bet, plus your $10 additional winnings.
Not all games state their odds using this convention. Some games state the odds as “2 for 1”. This means that the total left on the table after the bets are paid will be two times the original bet. So a $5 bet will win $5, there will be $10 sitting on the table.
The table is divided into two classes of bets. The “inside” bets are the 38 numbers and small groups of numbers; these bets all have relatively high odds. The “outside” bets are large groups of numbers, and have relatively low odds. If you are new to casino gambling, see Odds and Payouts for more information on odds and why they are offered.
The following bets are the “outside” bets. Each of these involves a group of twelve to eighteen related numbers. None of these outside bets includes 0 or 00. The only way to bet on 0 or 00 is to place a straight bet on the number itself, or use the five-number combination bet.
Perhaps because Roulette is a relatively simple game, elaborate betting systems have evolved around it. Searches on the Internet turn up a many copies of the same basic descriptions for a number of betting systems. Our purpose is not to uncover the actual history of these systems, but to exploit them for simple OO design exercises. Feel free to research additional betting systems or invent your own.
The Martingale system starts with a base wagering amount, w, and a count of the number of losses, c, initially 0. Each loss doubles the bet; any given spin will place an amount of w*2 c on a 1:1 proposition (for example, red). When a bet wins, the loss count is reset to zero; resetting the bet to the base amount, w. This assures that a single win will recoup all losses.
Note that the casinos effectively prevent successful use of this system by imposing a table limit. At a $10 Roulette table, the limit may be as low as $1,000. A Martingale bettor who lost six times in a row would be facing a $640 bet, and after the seventh loss, their next bet would exceed the table limit. At that point, the player is unable to recoup all of their losses. Seven losses in a row is only a 1 in 128 probability; making this a relatively likely situation.
Another system is to wait until some number of losses have elapsed. For example, wait until the wheel has spun seven reds in a row, and then bet on black. This can be combined with the Martingale system to double the bet on each loss as well as waiting for seven reds before betting on black.
Another betting system is called the 1-3-2-6 system. The idea is to avoid the doubling of the bet at each loss and running into the table limit. Rather than attempt to recoup all losses in a single win, this system looks to recoup all losses by waiting for four wins in a row. The sequence of numbers (1, 3, 2 and 6) are the multipliers to use when placing bets after winning. At each loss, the sequence resets to the multiplier of 1. At each win, the multiplier is advanced through the sequence. After one win, the bet is now 3w. After a second win, the bet is reduced to 2w, and the winnings of 4w are “taken down” or removed from play. In the event of a third win, the bet is advanced to 6w. Should there be a fourth win, the player has doubled their money, and the sequence resets.
Another method for tracking the lost bets is called the Cancellation system or the Labouchere system. The player starts with a betting budget allocated as a series of numbers. The usual example is 1, 2, 3, 4, 5, 6, 7, 8, 9. Each bet is sum of the first and last numbers in the last. In this case 1+9 is 10. At a win, cancel the two numbers used to make the bet. In the event of all the numbers being cancelled, reset the sequence of numbers and start again. For each loss, however, add the amount of the bet to the end of the sequence as a loss to be recouped.
Here's an example of the cancellation system using 1, 2, 3, 4, 5, 6, 7, 8, 9.
Bet 1+9. A win. Cancel 1 and 9 leaving 2, 3, 4, 5, 6, 7, 8.
Bet 2+8. A loss. Add 10 leaving 2, 3, 4, 5, 6, 7, 8, 10.
Bet 2+10. A loss. Add 12 leaving 2, 3, 4, 5, 6, 7, 8, 10, 12.
Bet 2+12. A win. Cancel 2 and 12 leaving 3, 4, 5, 6, 7, 8, 10.
Next bet will be 3+10.
A player could use the Fibonacci Sequence to structure a series of bets in a kind of cancellation system. The Fibonacci Sequence is 1, 1, 2, 3, 5, 8, 13, .... At each loss, the sum of the previous two bets -- the next number in the sequence -- becomes the new bet amount. In the event of a win, the last two numbers in the sequence are removed. This allows the player to easily track our accumulated losses, with bets that could recoup those losses through a series of wins.
The first section is a survey of the classes gleaned from the general problem statement in Problem Statement as well as the problem details in Roulette Details. This survey is drawn from a quick overview of the key nouns in these sections.
Given this survey of the candidate classes, the second section is a walkthrough of the possible design that will refine the definitions, and give us some assurance that we have a reasonable architecture. We will make some changes to the preliminary class list, revising and expanding on our survey.
We will also include a number of questions and answers about this preliminary design information. This should help clarify the design presentation and set the stage for the various development exercises in the chapters that follow.
To provide a starting point for the development effort, we have
to identify the objects and define their responsibilities. The central
principle behind the allocation of responsibility is
encapsulation; we do this by attempting to
isolate the information or isolate the
processing that must be done. Encapsulation assures that
the methods of a class are the exclusive users of the fields of that
class. It also makes each class very loosely coupled with other
classes; this permits change without a ripple through the application.
For example, each Outcome contains both the
name and the payout odds. That way each Outcome
can be used to compute a winning amount, and no other element of the
simulation needs to share the odds information or the payout
calculation.
In a few cases, we have looked forward to anticipate some future
considerations. One such consideration is the house
“rake”, also known as the
“vigorish”,
“vig”, or
commission. In some games, the house makes a 5%
deduction from some payouts. This complexity is best isolated in the
Outcome class. Roulette doesn't have any need
for a rake, since the presence of the 0 and 00 on the wheel gives the
house a little over 5% edge on each bet. We'll design our class so
that this can be added later when we implement Craps.
In reading the background information and the problem statement, we noticed a number of nouns that seemed to be important parts of the game we are simulating.
| Wheel |
| Bet |
| Bin |
| Table |
| Red |
| Black |
| Green |
| Number |
| Odds |
| Player |
| House |
One common development milestone is to be able to develop a class model in the Unified Modeling Language (UML) to describe the relationships among the various nouns in the problem statement. Building (and interpreting) this model takes some experience with OO programming. In this first part, we'll avoid doing extensive modeling. Instead we'll simply identify some basic design principles. We'll focus in on the most important of these nouns and describe the kinds of classes that you will build.
The following table summarizes some of the classes and responsibilities that we can identify from the problem statement. This is not the complete list of classes we need to build. As we work through the exercises, we'll discover additional classes and rework some of these preliminary classes more than once.
| Class | Responsibilities | Collaborations |
|---|---|---|
Outcome | A name for the bet and the payout odds. This isolates the calculation of the payout amount. Example: "Red", "1:1". | Collected by Wheel into the bins that
reflect the bets that win; collected by
Table into the available bets for the
Player; used by Game
to compute the amount won from the amount that was bet. |
Wheel | Selects the Outcomes that win. This
isolates the use of a random number generator to select
Outcomes; and it encapsulates the set of
winning Outcomes that are associated with
each individual number on the wheel. Example: the “1”
bin has the following winning Outcomes:
“1”, “Red”, “Odd”,
“Low”, “Column 1”, “Dozen
1-12”, “Split 1-2”, “Split 1-4”,
“Street 1-2-3”, “Corner 1-2-4-5”,
“Five Bet”, “Line 1-2-3-4-5-6”,
“00-0-1-2-3”, “Dozen 1”,
“Low” and “Column 1”. | Collects the Outcomes into bins; used
by the overall Game to get a next set of
winning Outcomes. |
Table | A collection of bets placed on
Outcomes by a
Player. This isolates the set of possible
bets and the management of the amounts currently at risk on each
bet. This also serves as the interface between the
Player and the other elements of the
game. | Collects the Outcomes; used by
Player to place a bet amount on a specific
Outcome; used by
Game to compute the amount won from the
amount that was bet. |
Player | Places bets on Outcomes, updates the
stake with amounts won and lost. | Uses Table to place bets on
Outcomes; used by
Game to record wins and losses. |
Game | Runs the game: gets bets from Player,
spins Wheel, collects losing bets, pays
winning bets. This encapsulates the basic sequence of play into a
single class. | Uses Wheel,
Table, Outcome,
Player. The overall statistical analysis
will play a finite number of games and collect the final value of
the Player's stake. |
The class Player has the most important
responsibility in the application, since we expect to update the
algorithms this class uses to place different kinds of bets. Clearly,
we need to cleanly encapsulate the Player, so
that changes to this class have no ripple effect in other classes of
the application.
A good preliminary task is to review these responsibilities to confirm that a complete cycle of play is possible. This will help provide some design details for each class. It will also provide some insight into classes that may be missing from this overview.
A good way to structure this task is to do a Class-Reponsibility-Collaborators (CRC) walkthrough. As preparation, get some 5" x 8" notecards. On each card, write down the name of a class, the responsibilities and the collaborators. Leave plenty of room around the responsibilities and collaborators to write notes. We've only identified five classes, so far, but others always show up during the walkthrough.
During the walkthrough, we identify areas of responsibility, allocate them to classes of objects and define any collaborating objects. An area of responsibility is a thing to do, a piece of information, a result. Sometimes a big piece of responsibility can be broken down into smaller pieces, and those smaller pieces assigned to other classes. There are a lot of reasons for decomposing, the purpose of this book is to explore many of them in depth. Therefore, we won't justify any of our suggestions until later in the book. For now, follow along closely to get a sense of where the exercises will be leading.
The basic processing outline is the responsibility of the
Game class. To start, locate the
Game card.
Our preliminary note was that this class “Runs the game.” The responsibilities section has a summary of four steps involved in running the game.
The first step is “gets bets from
Player.” Find the
Player card.
Does a Player collaborate with a
Game to place bets? If not, update the
cards as necessary to include this.
One of the responsibilities of a
Player is to place bets. The step in the
responsibility statement is merely “Places bets on
Outcomes.” Looking at the classes,
we note that the Table contains the amounts
placed on the Bets. Fix the collaboration information on the
Player to name the
Table class. Find the
Table card.
Does a Table collaborate with a
Player to accept the bets? If not, update
the cards as necessary to include this.
What card has responsibility for the amount of the bet? It
looks like Table. We note one small
problem: the Table contains the
collection of amounts bet on
Outcomes. What class contains the
individual “amount bet on an
Outcome?” This class appears to be
missing. We'll call this new class Bet and
start a new card. We know one responsibility is to hold the amount
bet on a particular Outcome. We know three
collaborators: the amount is paired with an
Outcome, all of the
Bets are collected by a
Table, and the Bets
are created by a Player. We'll update all
of the existing cards to name their collaboration with
Bet.
What card has responsibility for keeping all of the
Bets? Does Table
list that as a responsibility? We should update these cards to
clarify this collaboration.
You should continue this tour, working your way through spinning
the Wheel to get a list of winning
Outcomes. From there, the
Game can get all of the
Bets from the Table and
see which are based on winning Outcomes and
which are based on losing Outcomes. The
Game can notify the
Player of each losing
Bet, and notify the
Player of each winning
Bet, using the Outcome
to compute the winning amount.
This walkthrough will give you an overview of some of the interactions among the objects in the working application. You may uncover additional design ideas from this walkthrough. The most important outcome of the walkthrough is a clear sense of the responsibilities and the collaborations required to create the necessary application behavior.
Table of Contents
In addition to defining the fundamental
Outcome on which all gambling is based, this
chapter provides a sidebar discussion on the notion of object identity
and object equality. This is important because we will be dealing with a
number of individual Outcome objects, and we need
to be sure we can test for equality of two
different objects. This is different from the test for
identity which asks if we have two references to
the same object.
The first class we will tackle is a small class that encapsulates each outcome. This class will contain the name of the outcome as a String, and the odds that are paid as an integer. We will use these objects when placing a bet and also when defining the Roulette wheel.
There will be several hundred instances of this class on a given
Roulette table. The bins on the wheel, similarly, collect various
Outcomes together. The minimum set of
Outcome instances we will need are the 38
numbers, Red and Black. The other instances add details to our
simulation.
In Roulette, the amount won is a simple multiplication of the
amount bet and the odds. In other games, however, there may be a more
complex calculation because the house keeps 5% of the winnings, called
the “rake”. While it is not part of Roulette, it is good
to have our Outcome class designed to cope with
these more complex payout rules.
Also, we know that other casino games, like Craps, are stateful.
An Outcome may change the game state. We can
foresee reworking this class to add in the necessary features to
change the state of the game.
While we are also aware that some odds are not stated as x:1, we won't include these other kinds of odds in this initial design. Since all Roulette odds are x:1, we'll simply assume that the denominator is always 1. We can forsee reworking this class to handle more complex odds, but we don't need to handle the other cases yet.
Outcome contains a single outcome on
which a bet can be placed. In Roulette, each spin of the wheel has a
number of Outcomes. For example, the
“1” bin has the following winning
Outcomes: “1”, “Red”,
“Odd”, “Low”, “Column 1”,
“Dozen 1-12”, “Split 1-2”, “Split
1-4”, “Street 1-2-3”, “Corner
1-2-4-5”, “Five Bet”, “Line
1-2-3-4-5-6”, “00-0-1-2-3”, “Dozen
1”, “Low” and “Column 1”.
String name ;Holds the name of the Outcome.
Examples include "1",
"Red".
int odds ;Holds the payout odds for this
Outcome. Most odds are stated as 1:1 or
17:1, we only keep the numerator (17) and assume the denominator
is 1.
Outcome(String name,
int odds);Sets the local name and odds from the parameter name and odds.
int winAmount(int amount);Multiplies this Outcome's odds by
the given amount. The product is returned.
An easy-to-read String output method is also very handy.
This should return a String representation of the
name and the odds. A form that looks like 1-2 Split
(17:1) works nicely. In Java, this is done as
follows.
public String toString();In Python, the following declaration is used.
def __str__(self):For some tips on how to do this, see Formatting in Java. Python formatting is done more
simply, using the %
operator.
There are two deliverables for this exercise. Both will have Javadoc comments or Python docstrings.
The Outcome class.
A class which performs a unit test of the
Outcome class. The unit test should create
a three instances of Outcome, two of which
have the same name. It should use a number of individual tests to
establish that two Outcome with the same
name will test true for equality, have the same hash code, and
establish that the winAmount method works
correctly.
Table of Contents
This chapter will present the design for the
Bin class. In addition to that, we'll examine the
collection classes available in Java and Python. We'll also present
present some questions and answers on this particular class and the
process of creating a design.
The Roulette wheel has 38 bins, identified with a number and a
color. Each of these bins defines a number of closely related winning
Outcomes. At this time, we won't enumerate each
of the 38 Bins of the wheel and each of the
winning Outcomes (from two to fourteen in each
Bin); we'll save that for a later exercise. At
this time, we'll define the Bin class, and use
this class to contain a number of Outcome
objects.
Two of the Bins have relatively few
Outcomes. Specifically, the 0
Bin only contains the basic “0”
Outcome and the “00-0-1-2-3”
Outcome. The 00 Bin,
similarly, only contains the basic “00”
Outcome and the “00-0-1-2-3”
Outcome.
The other 36 Bins contain the straight
bet, split bets, street bet, corner bets, line bets and various
outside bets (column, dozen, even or odd, red or black, high or low)
that will win if this Bin is selected. Each
number bin has from 12 to 14 individual winning
Outcomes.
Some Outcomes, like red or black, occur
in as many as 18 individual Bins. Other
Outcomes, like the straight bet numbers, each
occur in only a single Bin. We will have to be
sure that our Outcome objects are shared
appropriately by the Bins.
Since a Bin is just a collection of
individual Outcome objects, we have to select a
collection class to contain the objects. In Java we have five basic
choices, with some variations based on performance needs. In Python,
we have two basic choices.
The Java collections framework offers us five basic choices for defining an object that is a container for other objects.
Simple Arrays. We can always declare a collection of
Outcome objects as follows:
Outcome[] bin1 = { Outcome("Red",1),
Outcome("1",35) };. This does almost
everything that we need it to do. Since our relationship
between a Bin and the collection of
Outcomes is relatively fixed, this kind
of collection has some appeal.
java.util.Vector. With a Vector, we have the
flexibility of easy addition of an
Outcome to the collection. We also have
the power of an Iterator to visit every
member of the collection.
A Vector has an implication of
non-sequential access to the elements. In our case, we won't be
using this; most of our collections will be visited in order, as
we determine which individual bets on the table match the
Bin's collection of winning
Outcomes.
java.util.Set. A Set offers easy
addition of elements, an Iterator to
visit the elements. Additionally, it assures that each element
occurs only once. The semantics of a mathematical set are
precisely what we want for each Bin on
a wheel: it is an unordered collection of elements, each
element occurs at most one time.
The Set is appealing
because the definition precisely matches our need. Additionally,
it is possible that a Set
performs faster than a Vector or
List. The reason it can be faster
is that it does not have to keep the elements in a particular
order.
java.util.List. A List offers easy
addition of elements, an Iterator to
visit the elements. Like a Vector, a
List keeps the elements in a
particular order.
A List has an implication
of strictly sequential access to the elements. In our case, this
is generally true. Most of our collections will be built in
order and visited in order, as we determine which individual
bets on the table match the Bin's
collection of winning Outcomes. The
order, however, isn't significant, so this feature is of
relatively little value.
java.util.Map. A map is used to associate a key with a value. This
key-value pair is called an entry in the
map. We don't need this feature at all. A map does more than
we need for representing the bins on a wheel.
Having looked at the fundamental collection varieties, we will
elect to use a Set. Our next step is
to choose a concrete implementation. There are three
alternatives.
java.util.HashSet. This uses a hash code to manage the
elements in the set. The hash code can be computed almost
instantly, allowing for rapid insert and retrieval. The hash
code is mapped to buckets in an internal table. However, a
large amount of storage is wasted by the need for empty
buckets in the internal table. This trades off storage for
speed.
java.util.LinkedHashSet. This adds a doubly-linked list that preserves the
insertion order. This is not appropriate for what we are
doing, as the insertion order is largely irrelevant.
java.util.TreeSet. This uses a binary red-black tree
algorithm to store the elements of the set. The insertion time
depends on the logarithm of the size of the tree, but the
storage is minimized. This seems like the ideal candidate for
representation of each Bin.
There are three basic Python types that are a containers for other objects.
Immutable Sequence or tuple. A tuple is immutable sequence of objects.
This is the kind of collection we need, since the elements of
a Bin don't change.
Mutable Sequence or list. A list is a sequence of objects that can be changed. While handy for the initial construction of the bin, this isn't really useful because the contents of a bin don't change once they have been enumerated.
Mapping or dictionary. A dictionary associated a key with a value.
This key-value pair is called an item in
the map. We don't need this feature at all. A map does more
than we need for representing a
Bin.
Having looked at the fundamental collection varieties, we will
elect to use a tuple.
Bin contains a collection of
Outcomes which reflect the winning bets that
are paid for a particular bin on a Roulette wheel. In Roulette, each
spin of the wheel has a number of Outcomes.
Example: A spin of 1, selects the “1” bin with the
following winning Outcomes: “1”,
“Red”, “Odd”, “Low”,
“Column 1”, “Dozen 1-12”, “Split
1-2”, “Split 1-4”, “Street 1-2-3”,
“Corner 1-2-4-5”, “Five Bet”, “Line
1-2-3-4-5-6”, “00-0-1-2-3”, “Dozen
1”, “Low” and “Column 1”. These are
collected into a single Bin.
Set outcomes ;Holds the connection of individual
Outcomes.
For a discussion on the type declaration used for
outcome, see Deferred Binding in
Java.
In addition to the default constructor that makes an empty
Bin, Java programmers may find it helpful to
create a two additional constructors to initialize the collection
from other collections.
Bin();Creates an empty Bin.
Outcomes can be added to it later.
Bin(Outcome[] outcomes);Creates an empty Bin using the
this() statement to invoke the
default constructor. It then loads that collection using
elements of the given array.
Bin(Collection outcomes);Creates an empty Bin using the
this() statement to invoke the
default constructor. It then loads that collection using an
iterator over the given
Collection. This relies on the
fact that all classes that implement
Collection will provide the
iterator; the constructor can convert
the elements of the input collection to a proper
Set.
The array constructor will initialize the
outcomes Set from
an array of Outcomes. This allows the
following kind of constructor call from another class.
Example 5.1. Java Bin Construction
Outcome five= new Outcome( "00-0-1-2-3", 6 );
Outcome[] ocZero = {
new Outcome("0", 35 ), five };
Outcome[] ocZeroZero = {
new Outcome("00", 35 ), five };
Bin zero= new Bin( ocZero );
Bin zerozero= bew Bin( ocZeroZero );
void add(Outcome outcome);Adds an Outcome to this
Bin. This can be used by a builder to
construct all of the bets in this Bin.
Since this class is really just a façade over the underlying
collection object, this method can simply delegate the real work
to the underlying collection.
public String toString();An easy-to-read String output method is also very handy.
This should return a String representation of the
list of Outcomes in this
Bin.
For some tips on how to implement this, see Formatting in Java.
Bin contains a collection of
Outcomes which reflect the winning bets that
are paid for a particular bin on a Roulette wheel. In Roulette, each
spin of the wheel has a number of Outcomes.
Example: A spin of 1, selects the “1” bin with the
following winning Outcomes: “1”,
“Red”, “Odd”, “Low”,
“Column 1”, “Dozen 1-12”, “Split
1-2”, “Split 1-4”, “Street 1-2-3”,
“Corner 1-2-4-5”, “Five Bet”, “Line
1-2-3-4-5-6”, “00-0-1-2-3”, “Dozen
1”, “Low” and “Column 1”. These are
collected into a single Bin.
Python programmers should provide an initializer that uses the
* modifier so that all of the
individual arguments appear as a single list within the
initializer.
def __init__(self, *outcomes):This constructor can be used as follows.
def add(self, outcome):Adds an Outcome to this
Bin. This can be used by a builder to
construct all of the bets in this Bin.
Since this class is really just a façade over the underlying
collection object, this method can simply delegate the real work
to the underlying collection. Note that a tuple is
immutable; unlike a list it does not have an
append method. Instead, a
tuple is constructed by using the “+”
operator which creates a new tuple from two input tuples.
def __str__(self):An easy-to-read String output method is also very handy.
This should return a String representation of the
list of Outcomes in this
Bin. A handy technique for displaying
collections is the following. This maps the
str function to each element of
self.outcomes, then joins the resulting
string with “,” separators.
There are two deliverables for this exercise. Both will have Javadoc comments or Python docstrings.
The Bin class.
A class which performs a unit test of the
Bin class. The unit test should create
several instances of Outcome, two instances
of Bin and establish that
Bins can be constructed from the
Outcomes.
Programmers who are new to OO techniques
are sometimes confused when reusing individual
Outcome instances. This unit test is a good
place to examine the ways in which object
references are shared. A single
Outcome object can be referenced by several
Bins. We will make increasing use of this
in later sections.
Table of Contents
This chapter builds on the previous two chapters, creating a more
complete composite object from the Outcome and
Bin classes we have already defined. We'll
introduce some basic subclass constructor techniques, also.
The wheel has two responsibilities: it is a container for the
Bins and it picks one of the
Bins at random. We'll also have to look at how
to initialize the various Bins.
Container Implementation. Since the Wheel is 38
Bins, it is a collection. We can review our
survey of the Java collections in Java Collections and the Python collections in
Python
Collections for some guidance here. In
this case, the choice of Bin will be selected
by a random numeric index. For Java programmers, this makes the
java.util.Vector very appealing. Python
programmers will find that a List
will do very nicely.
One consequence of using a Vector is that
we have to choose an index for the various
Bins. While each Bin
contains a variety of individual Outcomes, the
single number Outcome is unique to each
Bin. The numbers 1 to 36 become the index to
their respective Bins. We have a small problem,
however, with 0 and 00: we need two separate indexes. While 0 is a
valid index into a Vector, what do we do with
00?
Since the index of the Bin doesn't have
any significance at all, we can assign the Bin
that has the 00 Outcome to position 37 in the
Vector. This gives us a unique place for all 38
Bins.
Random Selection. In order for the Wheel to select a
Bin at random, we'll need a random number
from 0 to 37 that we can use an an index. The random number
generator in java.util.Random does everything
we need. We can use the generator's public int nextInt(int n); method to return integers.
Python programmers can use the random
module. This module offers a choice function
which picks a random value from a sequence. This is ideal for
returning a randomly selected Bin from our list
of Bins. For some cautions on casual use of
modules in Python, see Using Python Modules.
Initialization. Each instance of Bin has a list of
Outcomes. The 0 and 00
Bins only have two
Outcomes. The other numbers have anywhere
from twelve to fourteen Outcomes.
Constructing the entire collection of Bins
would be a tedious undertaking. We'll apply a common
OO design technique of deferred binding and
work on that algorithm later, after we have the basic design for the
Wheel finished.
Wheel contains the 38 individual bins on
a Roulette wheel, plus a random number generator. It can select a
Bin at random, simulating a spin of the
Roulette wheel.
For those new to Java, see Java Subclass Constructors.
For those new to Python, see Python Subclass Constructors.
Java: java.util.Vector bins = new Vector( 38 );
Python: bins = 38*[None]
Contains the individual Bin
instances.
Java: java.util.Random rng = new java.util.Random();
Python: rng = random.Random()
Generates the next random number, used to select a
Bin from the bins
collection.
Wheel();Creates a new wheel with 38 empty
Bins. It will also create a new random
number generator instance.
At the present time, this does not do the full
initialization of the Bins. We'll rework
this in a future exercise.
addOutcome(int number,
Outcome outcome);Adds the given Outcome to the
Bin with the given number.
next();Generates a random number between 0 and 37, and returns
the randomly selected Bin.
Java: Be sure to note that the
java.util.Random
nextInt method uses the size of the
bins collection to return values from 0 to
the size of the collection. Typically there are 38 values,
numbered from 0 to 37.
Python: the choice function of the
random module will select one of the
available Bins from the
bins list.
In later chapters, we'll revisit ways to test this method. For now, it returns a random result that's difficult to unit test.
Bin getBin(int aBin);Returns the given Bin from the
internal collection.
There are three deliverables for this exercise. The new class and the unit test will have Javadoc comments or Python docstrings.
The Wheel class.
A class which performs a unit test of building the
Wheel class. The unit test should create
several instances of Outcome, two instances
of Bin, and an instance of
Wheel. The unit test should establish that
Bins can be added to the
Wheel.
A class which performs a demonstration of selecting random
values from the Wheel class. Since the
Bins will be returned in a random order,
this is difficult to call a formal test. The demonstration should
include the following.
Create several instances of
Outcome.
Create two instances of Bin that
use the available Outcomes.
Create one instance of Wheel that
uses the two Bins.
A number of calls to the next
method should return randomly selected
Bins.
Note that the sequence of random numbers is fixed by the
seed value. The default constructor for a random number generator
creates a seed based on the system clock. Your unit test can set a
particular seed value for the random number generator within the
instance of Wheel; this will deliver a
fixed sequence of numbers that can be used to get a consistent
result.
Table of Contents
This is a rather long chapter, which describes a number of design
considerations surrounding Outcome,
Bin and Wheel. The second
section presents a number of algorithms to group
Outcomes based on the geometry of the table.
These algorithms are presented at a high level, leaving much of the
detailed design to the student.
It is clear that enumerating each Outcome
in the 38 Bins is a tedious undertaking. Most
Bins contain about fourteen individual
Outcomes. It is often helpful to create a class
that is used to build an instance of another class. This is a design
pattern sometimes called a Builder.
We'll design an object that builds the various
Bins and assigns them to the
Wheel. This will fill the need left open in the
design of the
Wheel class.
Additionally, we note that the complex algorithms to construct
the Bins are only tangential to the operation
of the Wheel object. While not essential to the
design of the Wheel class, we find it is often
helpful to segregate these rather complex builder methods into a
separate class.
The BinBuilder class will have a method
that enumerates the contents of each of the 36 number
Bins, building the individual
Outcome instances. We can then assign these
Outcome objects to the
Bins of a Wheel
instance. We will use a number of steps to create the various types of
Outcomes, and depend on the
Wheel to assign each
Outcome object to the correct
Bin.
Looking at the layout of a
Roulette table gives us a number of geometric rules for
determining the various Outcomes that are
combinations of individual numbers. These rules apply to the numbers
from one to thirty-six. A different -- and much simpler -- set of
rules applies to 0 and 00. First, we'll survey the table geometry,
then we'll develop specific algorithms for each kind of bet.
Split Bets. Each number is adjacent to two, three or four other numbers. The four corners (1, 3, 34, and 36) only participate in two split bets. The center column of numbers (5, 8, 11, ..., 32) each participate in four split bets. The remaining “edge” numbers participate in three split bets. While this is moderately complex, the bulk of the layout (from 4 to 32) can be handled with a simple rule to distinguish the center column from the edge columns. The ends (1, 2, 3, 34, 35 and 36) are a little more complex.
Street Bets. Each number is a member of one of the twelve street bets.
Corner Bets. Each number is a member of one, two or four corner bets. As with split bets, the bulk of the layout can be handled with a simple rule to distinguish the column, and hence the “corners”. A number in the center column (5, 8, 11, ..., 32) is a member of four corners. At the ends, 1, 3, 34, and 36, are members of just one corner. All of the remaining numbers are along an edge and are members of two corners.
Line Bets. Six numbers comprise a line; each number is a member of one or two lines. The ends (1, 2, 3 and 34, 35, 36) are each part of a single line. The remaining 10 rows are each part of two lines.
Dozen Bets. Each number is a member of one of the three dozens. The three ranges are from 1 to 12, 13 to 24 and 25 to 36, making it very easy to associate numbers and ranges.
Column Bets. Each number is a member of one of the three columns. Each of the columns has a number numeric relationship. The values are 3c+1, 3c+2, and 3c+3, where c varies from 0 to 11.
The Even-Money Bets. These include Red, Black, Even, Odd, High, Low. Each
number has three of the six possible even money
Outcomes. An easy way to handle these is
to create the 6 individual Outcome
instances. For each number, n, individual
if-statements can be used to determine which of the
Outcome objects are associated with the
given Bin.
The Bins for zero and double zero can
easily be enumerated. Each bin has a straight number bet
Outcome, plus the Five Bet
Outcome (00-0-1-2-3, which pays 6:1).
One other thing we'll probably want are handy names for the
various kinds of odds. While can define an
Outcome as Outcome( "Number 1",
35 ), this is a little opaque. A slightly nicer form
is Outcome( "Number 1", RouletteGame.StraightBet
). These are specific to a game, not general features
of all Outcomes. We haven't designed the game
yet, but we can provide a stub class with these these outcome odds
definitions.
Straight bet Outcomes are the easiest to
generate.
Procedure 7.1. Generating Straight Bets
For All Numbers. For each number, n, from 1 to 36:
Create Outcome. Create an Outcome from the
number, n, with odds of 35:l.
Assign to Bin. Assign this new Outcome to
Bin n.
Split bet Outcomes are more complex
because of the various cases: corners, edges and
down-the-middle.
We note that there are two kinds of split bets: left-right pairs and up-down pairs. The left-right pairs all have the form (n, n+1). The up-down paris have the form (n, n+3). We can look at the number 5 as being part of 4 different pairs: (4,4+1), (5,5+1), (2,2+3), (5,5+3). The corner number 1 is part of 2 split bets: (1,1+1), (1,1+3).
We can generate the “left-right” split bets by iterating through the left two columns; the numbers 1, 4, 7, ..., 34 and 2, 5, 8, ..., 35.
Procedure 7.2. Generating Split Bets
For All Rows. For each row, r, from 0 to 11:
First Column Number. Compute number, n as 3r+1. This will create values 1, 4, 7, ..., 34.
Column 1-2 Split. Create a “n,
n+1” split
Outcome with odds of 17:1.
Assign to Bins. Associate this object with two
Bins: n and
n+1.
Second Column Number. Compute number, n as 3r+2. This will create values 2, 5, 8, ..., 35.
Column 2-3 Split. Create a “n,
n+1” split
Outcome.
Assign to Bins. Associate this object to two
Bins: n and
n+1.
Similarly, for the numbers 1 through 33, we can generate the
“up-down” split bets. For each number,
n, we generate a “n,
n+3” split bet. This
Outcome belongs to two
Bins: n and
n+3.
Street bet Outcomes are very
simple.
We can generate the street bets by iterating through the twelve rows of the layout.
Procedure 7.3. Generating Street Bets
For All Rows. For each row, r, from 0 to 11:
First Column Number. Compute number, n as 3r+1. This will create values 1, 4, 7, ..., 34.
Street. Create a “n,
n+1, n+2”
street Outcome with odds of
11:1.
Assign to Bins. Associate this object to three
Bins: n,
n+1, n+2.
Corner bet Outcomes are as complex as
split bets because of the various cases: corners, edges and
down-the-middle.
Eacb corner has four numbers, n,
n+1, n+3,
n+4. We can generate the corner bets by iterating
through the numbers 1, 4, 7, ..., 31 and 2, 5, 8, ..., 32. For each
number, n, we generate four corner bets:
“n, n+1,
n+3, n+4” corner bet.
This Outcome object belongs to four
Bins.
We generate corner bets by iterating through the lines between rows and columns. There are two lines between the three columns, and 11 lines between the 12 rows. The column lines are to the right of the first two columns. Similarly the row lines are below the first 11 rows.
Procedure 7.4. Generating Corner Bets
For All Lines Between Rows. For each line below a row, r, from 0 to 10:
First Column Number. Compute number, n as 3r+1. This will create values 1, 4, 7, ..., 31.
Column 1-2 Corner. Create a “n,
n+1, n+3,
n+4” corner
Outcome withs odds of 8:1.
Assign to Bins. Associate this object to four
Bins: n,
n+1, n+3,
n+4.
Second Column Number. Compute number, n as 3r+2. This will create values 2, 5, 8, ..., 32.
Column 2-3 Corner. Create a “n,
n+1, n+3,
n+4” corner
Outcome withs odds of 8:1.
Assign to Bins. Associate this object to four
Bins: n,
n+1, n+3,
n+4.
Line bet Outcomes are similar to street
bets. However, these are based around the 11 lines between the 12
rows.
For lines s numbered 0 to 10, the numbers
on the line bet can be computed as follows: 3s+1,
3s+2, 3s+3,
3s+4, 3s+5,
3s+6. This Outcome object
belongs to six individual Bins.
Procedure 7.5. Generating Line Bets
For All Lines Between Rows. For each row, r, from 0 to 10:
First Column Number. Compute number, n as 3r+1. This will create values 1, 4, 7, ..., 31.
Line. Create a “n,
n+1, n+2,
n+3, n+4,
n+5” line
Outcome withs odds of 5:1.
Assign to Bins. Associate this object to six
Bins: n,
n+1, n+2,
n+3, n+4,
n+5.
Dozen bet Outcomes require enumerating
all twelve numbers in each of three groups.
Procedure 7.6. Generating Dozen Bets
For All Dozens. For each dozen, d, from 0 to 2:
Create Dozen. Create an Outcome for dozen
d+1 with odds of 2:1.
For All Numbers. For each number, m, from 0 to 11:
Assign to Bin. Associate this object to
Bin
12d+m+1.
Column bet Outcomes require enumerating
all twelve numbers in each of three groups. While the outline of the
algorithm is the same as the dozen bets, the enumeration of the
individual numbers in the inner loop is slightly different.
Procedure 7.7. Generating Column Bets
For All Columns. For each column, c, from 0 to 2:
Create Column. Create an Outcome for column
c+1 with odds of 2:1.
For All Rows. For each row, r, from 0 to 11:
Assign to Bin. Associate this object to
Bin
3r+c+1.
The even money bet Outcomes are
relatively easy to generate.
Procedure 7.8. Generating Even-Money Bets
Create the Red outcome, with odds of 1:1.
Create the Black outcome, with odds of 1:1.
Create the Even outcome, with odds of 1:1.
Create the Odd outcome, with odds of 1:1.
Create the High outcome, with odds of 1:1.
Create the Low outcome, with odds of 1:1.
For All Numbers. For each number, n, from 1 to 36:
Low? If n is between 1 and 18,
associate the low Outcome with
Bin n.
High? Otherwise, n is between 19 and
36, associate the high Outcome with
Bin n.
Even? If n % 2 is zero, associate the
even Outcome with
Bin n.
Odd? Otherwise, n % 2 is 1, associate
the odd Outcome with
Bin n.
Red? If n is one of 1, 3, 5, 7, 9, 12,
14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, or 36, associate
the red Outcome with
Bin n.
Black? Otherwise, associate the black
Outcome with
Bin n.
BinBuilder creates the
Outcomes for all of the 38 individual
Bin on a Roulette wheel.
void buildBins(Wheel aWheel);Creates the Outcome instances and
uses the addOutcome method to place
each Outcome in the appropropriate
Bin of
aWheel.
There should be nine separate methods to generate the straight bets, split bets, street bets, corner bets, line bets, dozen bets and column bets, even money bets and the special case of zero and double zero. Each of the methods will be relatively simple and easy to unit test. Details are provided in Algorithms.
There are three deliverables for this exercise. The new classes will have Javadoc comments or Python docstrings.
The BinBuilder class.
A class which performs a unit test of the
BinBuilder class. The unit test invoke each
of the various methods that create Outcome
instances.
Rework the unit test of the Wheel
class. The unit test should create and initialize a
Wheel. It can use the
getBin method to check selected
Bins for the correct
Outcomes.
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 On Object Identity
A Bet is an amount that the player has
wagered on a specific Outcome. This class has
the responsibilty for maintaining the association between an amount,
an Outcome, and a specific
Player.
This is the classic record declaration: a passive association among data elements. The only methods we can see are three pairs of getters and setters to get and set each of the three attributes.
The general scenario is to have a 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 they
are winners or losers. A winning Bet has an
Outcome that matches one in the winning
Bin; winners will return money to the
Player. All other bets are losers, which remove
the money from the Player.
Building Bets. In the long run, we'll need to build a variety of bets.
Building a Bet involves two parts: an
Outcome and an amount. There are several
places to get Outcomes. First, we can create an Outcome object as
part of constructing a Bet. Here's what it might look like in
Java.
Bet myBet= new Bet( new Outcome("red",2), 25 )A better choice is to get Outcome obects
from the Wheel. To do this, we'd have to make
sure that we saved Outcomes from the
BinBuilder. We'd also have add specific
Outcome getters to the
Wheel. We could, for example, include a
getBlack method that returns an instance of
the black Outcome. In Python, for example, we
might have a method function like the following.
class Wheel( object ):
...
def getBlack( self ):
return self.blackA third approach is to keep a Map (in Python, a dictionary) that
maps an Outcome's string name to the relevant
Outcome object. Our
BinBuilder can accumulate this
Map, and we can get
Outcomes out of the Map with a getter method
like the following.
Outcome getOutcome( String name ) {
return outcomeMap.get(name);
}For now, we'll build Outcomes manually in
order to produce a good test of the Bet
class.
Bet associates an amount and an
Outcome. In a future round of design, we can
also associate a it with a Player.
Bet(int amount,
Outcome outcome);Initialize the instance variables of this bet.
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.
int winAmount();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.
int loseAmount();Returns the amount bet as the amount lost. This is the cost of placing the bet.
Java: public String toString();
Python: def __str__(self):
Returns a string representation of this bet. Note that
this method will delegate the real work to the
toString (or
__str__) method of the
Outcome (and the
Player, when that is added).
There are two deliverables for this exercise. The new classes will have Javadoc comments or Python docstrings.
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.
Table of Contents
This section provides the design for the
Table to hold the bets. It also introduces the
concepts behind exception handling, and the proper role of
exceptions.
The Table has the responsibility to keep
the Bets created by the
Player. Additionally, the house imposes
table limits on the minimum amount that must be
bet and the maximum that can be bet. Clearly, the
Table has all the information required to
evaluation these conditions.
Casinos prevent the Martingale betting system from working by imposing a table limit on each game. To cover the cost of operating the table game, the casino also imposes a minimum bet. Typically, the maximum is a multiplier of the minimum bet, often in the range of 10 to 50; a table with a $5 minimum might have a $200 limit, a $10 minimum may have only a $300 limit.
It isn't clear where the responsibility lies for determining
winning and losing bets. The money placed on
Bets on the Table is
“at risk” of being lost. If the bet is a winner, the
house pays the Player an amount based on the
Outcome's odds and the
Bet's amount. If the bet is a loser, the amount
of the Bet is forfeit by the
Player. Looking forward to stateful games like
Craps, we'll place the responsibility for determining winners and
losers with the game, and not with the Table
object.
Our second open question is the timing of the payment for the bet from the player's stake. In a casino, the payment happens when the bet is placed on the table. In our Roulette simulation, this is a subtlety that doesn't have any practical consequences. We could deduct the money as part of bet creation, or we could deduct the money as part of resolving the spin of the wheel. In other games, however, there may several events and several opportunities for additional bets. For example, splitting a hand in blackjack, or placing additional odds bets in Craps. Because we can't allow a player to bet more than their stake, we should deduct the payment as the bet is created.
A consequence of this is a change to our definition of the
Bet class. We don't need to compute the amount
that is lost. Rather than deduct the money when the bet resolved,
we've decided that the money is deducted from the
Player's stake as part of creating the
Bet. This will become part of the design of
Player and Bet.
Looking forward a little, a stateful game like Craps will
introduce a subtle distinction that may be appropriate for a future
subclass of Table. When the game is in the
point off state, some of the bets on the table are not allowed, and
others become inactive. When the game is in the point on state, all
bets are allowed and active. In Craps parlance, some bets are
“not working” or “working” depending on the
game state. This does not apply to the version of
Table that will support Roulette.
Container Implementation. A Table is a collection of
Bets. We need to choose a concrete class for
the collection of the bets. We can review our survey of the Java
collections in Java Collections and the Python collections in
Python
Collections for some guidance here. In
this case, the bets are placed in no particular order, and are
simply visited in an arbitrary order for resolution. We can use a
Java LinkedList for this. Since the number of
bets varies, we can't use a Python
tuple; a
list will do.
Table Limits. Table limits can be checked by providing a public method
isValid that compares the total of a new
prospective amount plus all existing Bets to
the table limit. This can be used by the
Player to evaluate each potential bet prior
to creating it.
In the unlikely event of the Player
object creating an illegal Bet, we can also
throw (or raise) an exception to indicate that we have a design error
that was not detected via unit testing. This should be a subclass of
Exception that has enough information to debug
the problem with the Player that attempted to
place the illegal bet.
Additionally, the game can check the overall state of a
Player's Bets to be sure
that the table minimum is met. We'll need to provide a public method
isValid that is used by the game. In the
event of the minimum not being met, there are serious design issues,
and an exception should be thrown. Generally, this situation arises
because of a bug where the Player should have
declined to bet rather than placing incorrect bets that don't meet the
table minimum.
Bet Resolution. An important consideration is the collaboration between
Table and some potential game class for
resolving bets. The Table has the collection
of Bets, each of which has a specific
Outcome. The Wheel
selects a Bin, which has a collection of
Outcomes. All bets with a winning outcome
will be resolved as a winner.
Because some games are stateful, and the winning and losing bets depend on game state, we will defer the details of the collaboration design until we get to the Game class. For now, we'll simply collect the Bets.
Adding and Removing Bets. A Table contains
Bets. Instances of Bet
are added by a Player. Later,
Bets will be removed from the
Table by the Game.
When a bet is resolved, it must be deleted. Some games, like
Roulette resolve all bets with each spin. Other games, like Craps,
involve multiple rounds of placing and resolving some bets, and
leaving other bets in play.
For Bet deletion to work, we have to provide a method to remove a bet. When we look at Game and bet resolution we'll return to bet deletion. It's import not to over-design this class at this time; we will often add features as we develop designs for additional use cases.
There are two classes to design: the
InvalidBet exception and the
Table class.
InvalidBet is thrown when the
Player attempts to place a bet which exceeds
the table's limit.
InvalidBet extends Exception {
}
This class simply inherits all features of the superclass.
In Python, the declaration must have a body, and the
pass statement is used for this
purpose.
class InvalidBet ( Exception ):
pass
Table contains all the
Bets created by the
Player. A table also has a betting limit, and
the sum of all of a player's bets must be less than or equal to this
limit. We assume a single Player in the
simulation.
int limit ;This is the table limit. The sum of a
Player's bets must be less than or
equal to this limit.
List bets ;This is a LinkedList of the
Bets currently active. These will
result in either wins or losses to the
Player.
boolean isValid(Bet bet);Validates this bet. If the sum of all bets is less than
or equal to the table limit, then the bet is valid, return
true. Otherwise, return
false.
void placeBet(Bet bet)
throws InvalidBet;Adds this bet to the list of working bets. If the sum of
all bets is greater than the table limit, then an exception
should be thrown (Java) or raised (Python). This is a rare
circumstance, and indicates a bug in the
Player more than anything else.
ListIterator iterator();Returns a ListIterator over the
list of bets. This gives us the freedom to change the
representation from LinkedList to any
other Collection with no impact
to other parts of the application.
In Python, we can return an iterator over the available
list of Bet instances. The traditional
Python approach is to return the list object itself, rather
than an iterator over the list. With the introduction of the
generators in Python 2.3, however, it
can be slightly more flexible to return an iterator rather
than the list object.
Note that we need to be able remove Bets from the table.
Consequently, we have to update the list, which requires that
we use a ListIterator, not an
Iterator.
Java: public String toString();
Python: def __str__(self):
reports on all of the currently placed bets.
There are three deliverables for this exercise. Each of these will have complete Javadoc comments or Python docstring comments.
An InvalidBet exception class. This
is a simple subclass of Exception.
The Table class.
A class which performs a unit test of the
Table class. The unit test should create at
least two instances of Bet, and establish
that these Bets are managed by the table
correctly.
Table of Contents
Between Player and
Game, we have a
chicken-and-egg design problem.
In this chapter, we'll describe the design for
Game in detail. However, in order to create the
deliverables, we have to create a version of
Player that we can use just to get started. In
the long run, we'll need to create a sophisticated hierarchy of players.
Rather than digress too far, we'll create a simple player,
Pssenger57 (they always bet on black), which will
be the basis for further design in later chapters.
The RouletteGame's responsibility is to
cycle through the various steps of the game procedure, getting bets
from the player, spinning the wheel and resolving the bets. This is an
active class that makes use of the classes we
have built so far. The hallmark of an active class is longer or more
complex methods. This is distinct from most of the classes we have
considered so far, which have relatively trivial methods that are
little more than getters and setters of instance variables.
The sequence of operations in one round of the game is the following.
Procedure 10.1. A Single Round of Roulette
Place Bets
Notify the Player to create
Bets. The real work of placing bets is
delegated to the Player class. Note that
the money is committed at this point; they player's stake should
be reduced as part of creating a
Bet.
Spin Wheel
Get the next spin of the Wheel,
giving the winning Bin,
w.
Resolve All Bets
For each Bet,
b placed by the
Player:
Winner?
If Bet
b's Outcome is in
the winning Bin,
w, then notify the
Player that Bet
b was a winner and update the
Player's stake.
Loser?
If Bet
b's Outcome is not
in the winning Bin,
w, then notify the
Player that Bet
b was a loser. This allows the
Player to update the betting amount for
the next round.
Matching Algorithm. This class has the responsibility for matching the collection
of Outcomes in the Bin
of the Wheel with the collection of
Outcomes of the Bets
on the Table. We have two collections that
must be matched: Bin and
Table. We'll need to structure a loop or
nested loops to compare individual elements from these two
collections.
We could use a loop to visit each Outcome
in the winning Bin. For each
Outcome, we would then visit each of the
Bets contained by the
Table. A Bet's
Outcome that matches the
Bin's Outcome is a
winner and is paid off. The other bets are losers. This involves two
nested loops: one to visit the winning Outcomes
of a Bin and one to visit the
Bets of a Table.
The alternative is to visit each Bet
contained by the Table. Since the winning
Bin is defined by a
Set, we can exploit set membership
methods to test for presence or absence of an
Bet's Outcome in that
Bin's Set. If
the Bin contains the
Outcome, the Bet is a
winner; otherwise the Bet is a loser. This only
requires a single loop to visit the Bets of a
Table.
Player Interface. The Game collaborates with
Player. We have a “chicken and
egg” problem in decomposing the relationship between these
classes. We note that the Player is really a
complete hierarchy of subclasses, each of which provides a different
betting strategy. For the purposes of making the
Game work, we can develop our unit tests with
a stub for Player that simply places a single
kind of Bet. We'll call this player
“Passenger57” because it always bets on Black. In
several future exercises, we'll revisit this design to make more
sophisticated players.
For some additional design considerations, see the sidebar Additional Design Considerations. This provides some more advanced game options that our current design can be made to support. We'll leave this as an exercise for the more advanced student.
There are two classes to design: a stub
Player that we can use for testing, and the
actual Game.
PlayerStub constructs a
Bet based on the
Outcome named
"Black". This is a very persistent
player.
We'll need a source for the Black outcome. We have several
choices; we looked at these in Building Bets. First, we can
build the Black outcome from scratch as a distinct object. This
works well for a stub that will only participate in a unit test.
However, it's annoying to have to specify the odds each time.
Second, we can interrogate the Wheel or
BinBuilder for a specific
Outcome object. Third, we can generalize this
approach by adding a Map to the Wheel, and
getting Outcome objects from the Map.
For now, we can create the black
Outcome manually.
In the long run, we'll have to define a
Player superclass, and make this class a
proper subclass of Player. However, our focus
now is on getting the Game designed and
built.
Outcome black = null;This is the outcome on which this player focuses their
betting. For now, we'll build this Outcome the hard way. In
the long run, we should get this from some souce
(Game, Wheel or
Table) using a well-known bet
name.
Table table ;Used to place individual bets.
PlayerStub(Table aTable);Constructs the Player with a
specific table for placing bets. This also creates the
“black” Outcome. This is
saved in the black variable for use in
creating bets.
void placeBets();Updates the Table with the
various bets. This version creates a
Bet instance from the
black Outcome. It
uses Table
placeBet to place that bet.
void win(Bet theBet);Notification from the Game that
the Bet was a winner. The amount of
money won is available via theBet
winAmount.
void lose(Bet theBet);Notification from the Game that
the Bet was a loser.
Game manages the sequence of actions
that defines the game of Roulette. This includes notifying the
Player to place bets, spinning the
Wheel and resolving the
Bets actually present on the
Table.
Wheel theWheel ;Contains the wheel that returns a randomly selected
Bin of
Outcomes.
Table theTable ;Contains the bets placed by the player.
Player thePlayer ;Places bets on the table.
We based the Roulette constructor on a design that allows any of the fields to be replaced. This is the Strategy design pattern. Each of these objects is a replaceable strategy, and can be changed by the client that uses this game.
Additionally, we specifically do not include the
Player instance in the constructor. The
Game exists independently of any
particular Player, and we defer binding
the Player and
Game until we are gathering statistical
samples.
Game(Wheel aWheel,
Table aTable);Constructs a new Game, using a
given Wheel and
Table.
void cycle(Player aPlayer);This will execute a single cycle of play with a given
Player. It will call
thePlayer
placeBets to get bets. It will call
theWheel next to
get the next winning Bin. It will then
call theTable's
bets to get an
Iterator over the
Bets. Stepping through this
Iterator returns the individual
Bet objects. If the winning
Bin contains the
Outcome, call the
thePlayer win,
otherwise call the thePlayer
lose.
There are three deliverables for this exercise. The stub does not need documentation, but the other classes do need complete Javadoc or Python docstrings.
The Passenger57 class. We will rework
this design later. This class always places a bet on Black. Since
this is simply used to test Game, it
doesn't deserve a very sophisticated unit test of its own. It will
be replaced in a future exercise.
The RouletteGame class.
A class which performs a demonstration of the
Game class. This demo program creates the
Wheel, the stub
Passenger57 and the
Table. It creates the
Game object and cycles a few times. Note
that the Wheel returns random results,
making a formal test rather difficult. We'll address this
testability issue in the next chapter.
Table of Contents
This chapter presents some design rework and implementation rework for testability purposes. While testability is very important, new programmers can be slowed to a crawl by the mechanics of building test drivers and test cases. We prefer to emphasize the basic design considerations first, and address testability as a feature to be added to a working class.
Additionally, we'll address some issues in construction of class instances and an idealized structure for the main procedure.
We have been studiously ignoring an elephant standing in the saloon. This is the problem of testing an application that includes a random number generator (RNG). There are two questions raised:
How can we develop formalized unit tests when we can't predict the random outcomes? This is a serious testability issue in randomized simulations. This question also arises when considering interactive applications, particularly for performance tests of web applications where requests are received at random intervals.
Are the numbers really random? This is a more subtle issue, and is only relevant for more serious applications. Cryptographic, statistical or actuarial applications may care about the randomness of random numbers. This is a large subject, and well beyond the scope of this book. We'll just assume that our random number generator is good enough.
We'll address this first issue by developing some scaffolding
that permits controlled testing. There are three approaches to
replacing the random behavior with something more controlled. One
approach is to subclass Wheel to create a more
testable version without the random number generator. An alternative
to changing wheel is to define a subclass of
java.util.Random (or Python's
random) that isn't actually random. A third
approach is to record the sequence of random numbers actually
generated from a particular seed value and use this to define the
exected test results. For more information on this third alternative,
see On Random Numbers.
Good testability is achieved when there are no changes to the
target software. For this reason, we don't want to have two versions
of Wheel, one for testing and one for normal
operations.
Instead of having two versions of Wheel, it's slightly better to
have a random number generator that creates a known sequence with
which we can test. To get this known sequence, we have a choice
between creating a non-random subclass of
java.util.Random or controlling the seed for
the random number generator used by Wheel. Both
will produce a known sequence of non-random values.
One consequence of either of these decisions is that we have to
make the random number generator in Wheel more
visible. Our favorite approach to making something more visible is to
assign an object through an official interface method or possible as
part of the constructor. In the case of Wheel,
we'd jave our overall simulation or test assign an appropriate number
generating object to the instance of Wheel
rather than have Wheel privately create a
generator.
When we are doing testing, we can associate a
Wheel with an instance of
NonRandom, or an instance of
Random initialized with a known seed. For
actual use, we can then associate a Wheel with
an instance of java.util.Random; this will
either use the RNG's default constructor to assure
unpredictability.
We'll need an additional constructor for
Wheel that allows us to provide an
appropriately initialized generator. Our current constructor secretly
creates an instance of a RNG, using the
RNG's default constructor. We'll need an additional
method that provides an RNG, already initialized with
an appropriate value.
For Python programmers, this is handled as a second parameter with a default value.
Note that, consistent with our principle of deferred
binding, we don't have to choose exactly which implementation
we will use. By allowing Wheel to take either
an instance of Random initialized with a given
seed or an instance of a new subclass,
NonRandom, we have given ourselves the
flexibility to choose either implementation. We'll provide
specifications for NonRandom, but using a fixed
seed for Random is seen by some as
simpler.
| Q: | Why are we making the random number generator more visible? Isn't object design about encapsulation? |
| A: | Encapsulation isn't the same thing as “information hiding”. For some people, the information hiding concept can be a useful way to begin to learn about encapsulation. However, information hiding is misleading because it is often taken to exremes. In this case, we want to encapsulate the bins of the wheel and the procedure for selecting the winning bin into a single object. However, the exact random-number generator (RNG) is a separate component, allowing us to bind any suitable RNG. Consider the situation where we are generating random numbers for a cryptographic application. In this case, the built-in random number generator may not be random enough. In this case, we may have a third-party Super-Random-Generator that should replace the built-in generator. We would prefer to minimize the changes required to introduce this new class. Our initial design has isolated the changes to the
We do, however, have an alternative. We can change the
top-level This is why we feel that constructors should be made very visible using the various design patterns for Factories and Builders. Further, we look at the main method as a kind of master Builder that assembles the objects that comprise the current execution of our application. See our Why does the
Looking ahead, we will have additional notes on this topic
as we add the |
| Q: | If setting the seed works so well, why make a non-random subclass? |
| A: | While setting the seed is an excellent method for setting up a unit test, not everyone is comfortable with random number generators. The presence of an arbitrary but predictable sequence of values looks too much like luck for some Quality Assurance (QA) managers. While the algorithms for both Python and Java random number generators are published and well-understood, this isn't always sufficiently clear and convincing for non-mathematicians. It's often seems simpler to build a non-random subclass than to control the existing class. From another point of view, creating a subclass of a built-in class is a necessary skill. The random number generators are easy classes to extend. Extending the abstract collection classes is also a very useful skill, but somewhat more complex than extending the random number generator. |
We'll present the design modification for
Wheel first. This will be followed by design
information for NonRandom in both Java and
Python.
Wheel contains the 38 individual bins
on a Roulette wheel, plus a random number generator. It can select a
Bin at random, simulating a spin of the
Roulette wheel.
Note that we will be rewriting these methods to change their implementation. The definitions of the methods don't change, but the implementations can (and do) change. This is the real strength of OO design. Once we have a well-defined interface (defined by the public methods and attributes), we are free to change the implementation as necessary.
This kind of rework — making a fundamental change to the implementation without touching the interface — is essential to successful OO programming.
Java: java.util.Vector bins = new Vector( 38 );
Python: bins = 38*[None]
Contains the individual Bin
instances.
Java: java.util.Random rng = new java.util.Random();
Python: rng = random.Random()
Generates the next random number, used to select a
Bin from the bins
collection.
Wheel();This will also create a new random number generator instance. It will then use the other constructor to do the rest of the initialization.
Wheel(Random rng);Creates a new wheel with 38 empty
Bins. It will use the given random
number generator instance.
addOutcome(int number,
Outcome outcome);Adds the given Outcome to the
Bin with the given number.
next();Generates a random number between 0 and 37, and returns
the randomly selected Bin.
Java: Be sure to note that the
java.util.Random
nextInt method uses the size of the
bins collection to return values from 0 to
the size of the collection. Typically there are 38 values,
numbered from 0 to 37.
Python: the choice function will
select one of the available Bins from
the bins list.
Bin getBin(int aBin);Returns the given Bin from the
internal collection.
We need a controlled kind of random number generation for
testing purposes. We'll define a class
NonRandom that extends
java.util.Random, but provides a more
testable sequence of values. One approach is to simply count through
a range of integer vales.
The API documentation gives us the advice
that the protected int next(int bits); generates the next pseudorandom number. It says
that any subclass should override this, as this is used by all other
methods.
NonRandom();This is the default constructor, and will initialize
value to zero.
NonRandom(long seed);This constructor ignores the seed value. It initializes
value to zero.
Nonrandom module provides the same
interface as random.Random, but merely counts
through a range of integer vales.
The API documentation gives us the advice
that a subclass should override the
random(), seed(),
getstate(),
setstate() and
jumpahead() methods. However, we can safely
ignore all but the random() method
itself.
def NonRandom(self):This is the default constructor, and will initialize
value to zero.
def NonRandom(self, seed):This constructor ignores the seed value. It initializes
value to zero.
def random(self):This method is the heart of the generator. In our case, we simply add 1 to the value and return (value % 38)/38.0 . This provides a predicable sequence of values that is suitable for our testing.
The return value does two things. First, it computest the remainder when the value is divided by 38. This will be a value from 0 to 37. Then it divides this by 38 to return a floating-point number in the half-open range [0.0, 1.0), as required by the API. This means that 0.0 is included, but 1.0 is not included.
There are five deliverables for this exercise. All of these deliverables need appropriate Javadoc comments or Python docstrings.
A modified design for Wheel. This add
the second constructor that allows us to pass in a particular
random number generator to Wheel. Also,
there will be considerable rewriting of the original Wheel.
The NonRandom class.
A demonstration program of the new wheel and the
NonRandom class (or the
Random class with a known seed) that shows
the results of a dozen spins of the non-random wheel. In the case
of seeding Random, the spins while be in an
arbitrary order, but will be the same each time the program is
run.
Revised unit tests for Wheel based on
the fixed sequence of responses from the non-random number
generator.
Revised unit tests for Game based on this revised version of
Wheel based on the fixed sequence of
responses from the non-random number generator.
Table of Contents
The variations on Player, all of which
reflect different betting strategies, is the heart of this application.
In Chapter 10, Roulette Game Class, we roughed out a stub class for
Player. In this chapter, we will complete that
design. We will also expand on it to implement the Matingale betting
strategy.
We have now built enough infrastructure that we can begin to add a variety of players and see how their betting strategies work. Each player is betting algorithm that we will evaluate by looking at the player's stake to see how much they win, and how long they play before they run out of time or go broke.
The Player has the responsibility to
create bets and manage the amount of their stake. To create bets, the
player must create legal bets from known
Outcomes and stay within table limits. To
manage their stake, the player must deduct money when creating a bet,
accept winnings or pushes, report on the current value of the stake,
and leave the table when they are out of money.
We have an interface that was roughed out as part of the design
of Game and Table. In
designing Game, we put a
placeBets method in
Player to place all bets. We expected the
Player to create Bets
and use the placeBet method of
Table class to save all of the individual
Bets.
In an earlier exercise, we built a stub version of
Player in order to test
Game. See Passenger57
Class. When we finish creating the
final superclass, Player, we will also revise
our Passenger57 to be a subclass of
Player, and rerun our unit tests to be sure
that our more complete design still handles the basic test cases
correctly.
Our objective is to have a new abstract class,
Player, with two new concrete subclasses: a
revision to Passenger57 and a new player that
follows the Martingale betting system.
We'll defer some of the design required to collect detailed measurements for statistical analysis. In this first release, we'll simply place bets.
There are four design issues tied up in
Player: tracking stake, keeping within table
limits, leaving the table, and creating bets. We'll tackle them in
separate subsections.
Tracking the Stake. One of the more important features we need to add to
Player are the methods to track the player's
stake. The initial value of the stake is the player's budget. There
are two significant changes to the stake.
Each bet placed will deduct the bet amount from the
Player's stake. We are stopped from placing
bets when our stake is less than the table minimum.
Each win will credit the stake. The
Outcome will compute this amount for
us.
Additionally, a push will put the original bet amount back. This is a kind of win with no odds applied.
We'll have to design an interface that will create
Bets, reducing the stake. and will be used by
Game to notify the
Player of the amount won.
Additionally, we will need a method to reset the stake to the starting amount. This will be used as part of data collection for the overall simulation.
Table Limits. Once we have our superclass, we can then define the
Martingale player as a subclass. This player
doubles their bet on every loss, and resets their bet to a base
amount on every win. In the event of a long sequence of losses, this
player will have their bets rejected as over the table limit. This
raises the question of how the table limit is represented and how
conformance with the table limit is assured. We put a preliminary
design in place in Roulette Table Class. There are several places where we could
isolate this responsibility.
The Player stops placing bets when
they are over the Table limit. In this
case, we will be delegating responsibility to the
Player hierarchy. In a casino, a sign is
posted on the table, and both players and casino staff enforce
this rule. This can be modeled by providing a method in
Table that simply returns the table limit
for use by the Player to keep bets within
the limit.
The Table provides a “valid
bet” method. This reflects a more general situation where a
stateful game has bets that change. In Craps, for example, most
bets cannot be placed until a point is established.
The Table throws an “illegal
bet” exception when an illegal bet is placed. While
permissable, this kind of use for exceptions pushes the envelope
on clarity and simplicity. One viewpoint is that exceptions should
be reserved for situations that are truly unexpected. In this
case, we expect to run into the table limit situation fairly often
using Martigale betting.
We recommend the second choice: adding a
isValid method to the
Table class. This has the consequence of
allocating responsibility to Table, and permits
us to have more advanced games where some bets are not allowed during
some game states. It also obligates Player to
validate each bet with the Table. It also means
that at some point, the player may be unable to place a legal
bet.
We could also implement this by adding to the responsibilities
of the existing placeBet method. For example,
we could return true if the bet was accepted, and
false if the bet violated the limits or other game
state rules. We prefer to isolate responsibility and create a second
method rather than pile too much into a single method.
Leaving the Table. In enumerating the consequences of checking for legal bets, we
also uncovered the issue of the Player
leaving the game. We can identify a number of possible reasons for
leaving: out of money, out of time, won enough, and unwilling to
place a legal bet. Since this decision is private to the
Player, we need a way of alerting the
Game that the Player
is finished placing bets.
There are three mechanisms for alerting the
Game that the Player is
finished placing bets.
Expand the responsibilities of the
placeBets to also indicate if the player
wishes to continue or is withdrawing from the game. While most
table games require bets on each round, it is possible to step up
to a table and watch play before placing a bet. This is one
classic strategy for winning at blackjack: one player sits at the
table, placing small bets and counting cards, while a confederate
places large bets only when the deck is favorable. We really have
three player conditions: watching, betting and finished playing.
It becomes complex trying to bundle all this extra responsibility
into the placeBets method.
Add another method to Player that the
Game can use to determine if the
Player will continue or stop playing. This
can be used for a player who is placing no bets while waiting; for
example, a player who is waiting for the Roulette wheel to spin
red seven times in a row before betting on black.
The Player can throw an exception
when they are done playing. This is an exceptional situation: it
occurs exactly once in each simulation. However, it is a
well-defined condition, and doesn't deserve to be called
“exceptional”. It is merely a terminating condition
for the game.
We recommend adding a method to Player to
indicate when Player is done playing. This
gives the most flexibility, and it permits Game
to cycle until the player withdraws from the game.
A consequence of this decision is to rework the
Game class to allow the player to exit. This is
relatively small change to interrogate the
Player before asking the player to place
bets.
In this case, these were situations which we didn't discover
during the initial design. It helped to have some experience with
the classes in order to determine the proper allocation of
responsibilities. While design walkthroughs are helpful, an
alternative is a “prototype”, a piece of software that
is incomplete and can be disposed of. The earlier exercise created a
version of Game that was incomplete, and a
version of PlayerStub that will have to be
disposed of.
Creating Bets from Outcomes. Generally, a Player will have a few
Outcomes on which they are betting. Many
systems are similar to the Martingale system, and place bets on only
one of the Outcomes. These
Outcome objects are usually created during
player initialization. From these Outcomes,
the Player can create the individual
Bet instances based on their betting
strategy.
We'll design the base class of Player and
a specific subclass, Martingale. This will give
us a working player that we can test with.
Player places bets in Roulette. This an
abstract class, with no actual body for the
placeBets method. However, this class does
implement the basic win method used by all
subclasses.
int stake ;The player's current stake. Initialized to the player's starting budget.
int roundsToGo ;The number of rounds left to play. Initialized by the overall simulation control to the maximum number of rounds to play. In Roulette, this is spins. In Craps, this is the number of throws of the dice, which may be a large number of quick games or a small number of long-running games. In Craps, this is the number of cards played, which may be large number of hands or small number of multi-card hands.
Table table ;Used to place individual
Bets.
boolean playing();Returns true while the player is
still active.
void placeBets();Updates the Table with the
various Bets.
When designing the Table, we
decided that we needed to deduct the amount of a bet from the
stake when the bet is created. See the Table Overview for
more information.
void win(Bet theBet);Notification from the Game that
the Bet was a winner. The amount of
money won is available via theBet
winAmount.
void lose(Bet theBet);Notification from the Game that
the Bet was a loser. Note that the
amount was already deducted from the stake when the bet was
created.
Martingale is a
Player who places bets in Roulette. This
player doubles their bet on every loss and resets their bet to a
base amount on each win.
int lossCount ;The number of losses. This is the number of times to double the bet.
int betMultiple ;The the bet multiplier, based on the number of losses. This starts at 1, and is reset to 1 on each win. It is doubled in each loss. This is always equal to 2lossCount.
void placeBets();Updates the Table with a bet on
black. The amount bet is
2lossCount,
which is the value of betMultiple.
void win(Bet theBet);Uses the superclass win method
to update the stake with an amount won. This method then
resets lossCount to zero, and resets
betMultiple to 1.
void lose(Bet theBet);Increments lossCount by
1 and doubles
betMultiple.
There are six deliverables for this exercise. The new classes must have Javadoc comments or Python docstrings.
The Player abstract superclass. Since
this class doesn't have a body for the
placeBets, it can't be unit tested
directly.
A revised Passenger57 class. This
version will be a proper subclass of
Player, but still place bets on black until
the stake is exhausted. The existing unit test for
Passenger57 should continue to work
correctly after these changes.
The Martingale subclass of
Player.
A unit test class for Martingale.
This test should synthesize a fixed list of
Outcomes, Bins, and
calls a Martingale instance with various
sequences of reds and blacks to assure that the bet doubles
appropriately on each loss, and is reset on each win.
A revised Game class. This will check
the player's playing method before
calling placeBets, and do nothing if the
player withdraws. It will also call the player's
win and lose
methods for winning and losing bets.
A unit test class for the revised
Game class. Using a non-random generator
for Wheel, this should be able to confirm
correct operation of the Game for a number
of bets.
Table of Contents
This section starts to address the overall “main program” that pulls the various classes together to create a finished application. Additionally, we'll also address some issues in how Java handles collections of primitive data types like integers.
We can now use our application to generate some more usable results. We can perform a number of simulation runs and evaluate the long-term prospects for the Martingale betting system. We want to know a few things about the game:
How long can we play with a given budget? In other words, how many spins before we've lost our stake.
How much we can realistically hope to win? How large a streak can we hope for? How far ahead can we hope to get before we should quit?
The Simulator will be an active class
with a number of responsibilities
Create the Wheel,
Table and Game
objects.
Simulate a number of sessions (typically 100), saving the maximum stake and length of each session.
For each session: initialize the
Player and Game,
cycle the game a number of times, collect the size of the
Player's stake after each cycle.
Write a final summary of the results.
At this point, we'll need for formalize some definitions, just to be crystal clear on the responisbility allocation.
Simulation Terms
A single cycle of betting and bet resolution. This depends on a single random event: a spin of the wheel or a throw of the dice. Also known as a round of play.
One or more cycles. The session begins with a player having their full stake. A session ends when the play elects to leave or can no longer participate. A player may elect to leave because of elapsed time (typically 250 cycles), or they have won a statistically significant amount. A player can no longer participate when their stake is too small to make the minimum bet for the table.
Some games have intermediate groupings of events between an individual cycles and an entire session. Blackjack has hands, where a number of player decisions and a number of random events contribute to the payoff. Craps has a game, which starts with the dice roll when the point is off, and ends when the point is made or the shooter gets Craps; consequently, any number of individual dice rolls can make up a game. Some bets are placed on the overall game, while others are placed on individual dice rolls.
The sequence of operations for the simulator looks like this.
Procedure 13.1. Controlling the Simulation
Empty List of Maxima. Create an empty maxima list. This is the maximum stake at the end of each session.
Empty List of Durations. Create an empty durations list. This is the duration of each session, measured in the number of cycles of play before the player withdrew or ran out of money.
For All Sessions. For each of 100 sessions:
Empty List of Stake Details. Create an empty list to hold the history of stake values for this session. This is raw data that we will summarize into two metrics for the session: maximum stake and duration. We could also have two simple variables to record the maximum stake and count the number of spins for the duratioon. However, the list allows us to gather other statistics, like maximum win or maximum loss.
While The Player Is Active
One Cycle. Play one cycle of the game. See the definition in Roulette Game Class.
Save Detail. Save the player's current stake in the list of stake values for this session. The alternative is to update the maximum to be the larger of the current stake and the maximum, and increment the duration.
Get Maximum. Get the maximum stake from the list of stake values. Save the maximum stake metric in the maxima list.
Get Duration. Get the length of the list of stake values. Save the duration metric in the durations list.
Statistical Description of Maxima. Compute the average and standard deviation of the values in the maxima list.
Statistical Description of Durations. Compute the average and standard deviation of values in the durations list.
Both this overall Simulator and the
Game collaborate with the
Player. The Simulator's
collaboration, however, initalizes the Player
and then monitor's the changes to the Player's
stake. We have to design two interfaces for this collaboration.
Player Initialization. The Simulator will initialize a
Player for 250 cycles of play, assuming about
one cycle each minute, and about four hours of patience. We will
also initialize the player with a generous budget of the table
limit, 100 betting units. For a $10 table, this is $1,000
bankroll.
Currently, the Player class is designed
to play one session and stop when their duration is reached or their
stake is reduced to zero. We have two alternatives for reinitializing
the Player at the beginning of each
session.
Provide some setters that allow a
client class like this overall simulator control to reset the
stake and roundsToGo values
of a Player.
Provide a Factory that
allows a client class to create new, freshly initialized instances
of Player.
While the first solution is quite simple, there are some
advantages to creating a PlayerFactory. If we
create an Abstract Factory, we have a
single place that creates all Players. Further,
when we add new player subclasses, we introduce these new subclasses
by creating a new subclass of the factory. In this case, however, only
the main program creates instances of Player,
reducing the value of the factory. While design of the factory is a
good exercise, we can scrape by with adding setter methods to the
Player class.
Player Interrogation. The Simulator will interrogate the
Player after each cycle and capture the
current stake. An easy way to manage this detailed data is to create
a List that contains the stake at the
end of each cycle. The length of this list and the maximum value in
this list are the two metrics the Simulator
gathers for each session.
Our list of maxima and durations are created sequentially during
the session and summarized sequentially at the end of the session. A
Java LinkedList will do everything we need. For
a deeper discussion on the alternatives available in the collections
framework, see Java Collections.
Statistical Summary. The Simulator will interrogate the
Player after each cycle and capture the
current stake. We don't want the sequence of stakes for each cycle,
however, we want a summary of all the cycles in the session. We can
save the length of the sequence as well as the maximum of the
sequence to determine these two aggregate performance parameters for
each session. Our objective is to run several session simulations to
get averages and a standard deviations for duration and maximum
stake. This means that the Simulator needs to
retain these statistical samples. We will defer the detailed design
of the statistical processing, and simply keep the duration and
maximum values in Lists for this first round of design.
Simulator exercises the Roulette
simulation with a given Player placing bets. It
reports raw statistics on a number of sessions of play.
int initDuration = 250;The duration value to use when initializing a
Player for a session.
int initStake = 100;The stake value to use when initializing a
Player for a session.
int samples ;The number of game cycles to simulate. A default value of 50 makes sense.
List durations ;A list of lengths of time the
Player remained in the game. Each session
of play producrs a duration metric, which are collected into
this list.
List maxima ;A list of maximum stakes for each
Player. Each session of play producrs a
maximum stake metric, which are collected into this list.
Player thePlayer ;The betting strategy we are simulating.
Game theGame ;The casino game we are simulating.
Simulator(Game game,
Player player);Saves the Player and
Game instances so we can gather
statistics on the performance of the player's betting
strategy.
List session();Executes a single game session. The
Player is initialized with their initial
stake and initial cycles to go. An empty
List of stake values is created.
The session loop executes until the
Player playing
returns false. This loop executes the
Game cycle; then
it gets the stake from the Player and
appends this amount to the List
of stake values. The List of
individual stake values is returned as the result of the session
of play.
gather();Executes the number of games sessions in
samples. Each game session returns a
List of stake values. When the
session is over (either the play reached their time limit or
their stake was spent), then the length of the session
List and the maximum value in the
session List are the resulting
duration and maximum metrics. These two metrics are appended to
the durations list and the maxima
list.
A client class will either display the durations and maxima raw metrics or produce statistical summaries.
There are five deliverables for this exercise. Each of these classes needs complete Javadoc or Python docstring comments.
Revision to the Player class to add
two new methods: one will set the stake and the
other will set the roundsToGo.
void setStake(int initStake);void setRounds(int initRounds);The Simulator class.
The expected outcomes from the non-random wheel can be
rather complex to predict. Because of this, one of the
deliverables is a demonstration program that enumerates the actual
sequence of non-random spins. From this we can derive the sequence
of wins and losses, and the sequence of
Player bets. This will allow us to predict
the final outcome from a single session.
A unit test of the Simulator class
that uses the non-random generator to produce the predictable
sequence of spins and bets.
A main application class that creates the necessary objects,
runs the Simulator's
gather method, and writes the available
outputs to System.out In this case, it will
print the list of maxima, and the list of session lengths. This
raw data can be redirected to a file, loaded into a spreadsheet
and analyzed.
Table of Contents
This section introduces an additional specialization of the Martingale strategy. Additionally, we'll also address some issues in how an overall application is composed of individual class instances. Adding this new subclass should be a small change to the main application class.
The SevenReds player waits for seven red wins in a row before
betting black. This is a subclass of Player. We
can create a subclass of our main Simulator to
use this new SevenReds class.
We note that our previous players
(Passenger57 and
Martingale) are stateless: they place the same
bets over and over until they are cleaned out or their playing session
ends. This SevenReds player has two states:
waiting and betting. In the waiting state, they are simply counting
the number of reds. In the betting state, they have seen seven reds
and are now playing the Martingale system on black. We will defer
serious analysis of this stateful betting until
some of the more sophisticated subclasses of
Player. For now, we will simply use an integer
to count the number of reds.
Currently, the player is not informed of the final outcome
unless they place a bet. We designed the Game
to evaluate the Bet instances and notify the
Player of just their
Bets that were wins or losses. We will need to
add a method to Player to be given the overall
list of winning Outcomes even when the
Player has not placed a bet.
Once we have updated the design of Game
to notify Player, we can add the new
SevenReds class. Note that we are can intoduce
each new betting strategy via creation of new subclasses. A relatively
straightforward update to our simulation main program allows us to use
these new subclasses. The previously working subclasses are left in
place, allowing graceful evolution by adding features with minimal
rework of existing classes.
In addition to waiting for the wheel to spin seven reds, we will
also follow the Martingale betting system to track our wins and
losses, assuring that a single win will recoup all of our losses. This
makes SevenReds a further specialization of
Martingale. We will be using the basic features
of Martingale, but doing additional processing
to determine if we should place a bet or not.
Introducing a new subclass should be done by upgrading the main
program. See Soapbox on
Composition for comments on the ideal structure
for a main program. Additionally, see our Why does the
Game class run the sequence of steps?
Isn't that the responsibility of some “main
program?” FAQ
entry and Why are we making the
random number generator more visible? Isn't object design about
encapsulation? FAQ entry for more
discussion.
SevenReds is a
Martingale player who places bets in Roulette.
This player waits until the wheel has spun red seven times in a row
before betting black.
int redCount ;The number of reds yet to go. This starts at
7, is reset to 7 on each
non-red outcome, and decrements by 1 on each
red outcome.
Note that we inherit betMultiple. This
is initially 1, doubles with each loss and is
reset to one on each win.
There are five deliverables from this exercise. The new classes will require complete Javadoc comments or Python docstrings.
A revision to the Player to add the
following method. The superclass version doesn't do anything with
this information. Some subclasses, however, will process
this.
void winners(Bin outcomes);The SevenReds subclass of
Player.
A revision to the Game class. This
will call the winners with the winning
Bin instance before paying off the
bets.
A unit test of the SevenReds class.
This test should synthesize a fixed list of
Outcomes, Bins, and
calls a SevenReds instance with various
sequences of reds and blacks. One test cases can assure that no
bet is placed until 7 reds have been seen. Another test case can
assure that the bets double (following the Martingale betting
strategy) on each loss.
A main application class that creates the necessary objects,
runs the Simulator's
gather method, and writes the available
outputs to System.out In this case, it will
simply print the list of maxima, and the list of session
lengths.
Table of Contents
This section presents two ordinary statistical algorithms: mean and standard deviation. In one sense, this section could be treated as optional. Instead of having the simulator present useful summary statistics, raw data can be put into a spreadsheet for creating the statistics.
However, the process of designing these statistical processing classes is very interesting. This chapter examines some ways to add features to the built-in collection classes.
Currently, the Simulator class collects
two Lists. One has the length of a
session, the other has the maximum stake during a session. We need to
create some descriptive statistics to summarize these stakes and
session lengths.
We will design a Statistics class with
responsibility to retain and summarize a list of numbers and produce
the average (also known as the mean) and standard deviation. The
Simulator can then use this this
Statistics class to get an average of the
maximum stakes from the list of session-level measures. The
Simulator can also apply this
Statistics class to the list of session
durations to see an average length of game play before going
broke.
We can encapsulate this statistical processing in two ways: we
could extend an existing class or we could delegate the statistical
functions to a separate class. If we extend the library
java.util.List, we can add the needed
statistical summary features; given this new class, we can replace the
original List of sample values with a
StatisticalList that both saves the values and
computes descriptive statistics. In addition to delegation, we have
two choices for implementation of this extended version of
List, giving us three alternative
designs.
Extend LinkedList. In this case, we
are simply adding two new methods to the existing
LinkedList implementation. However, our new
methods would not apply to ArrayList or
Vector.
Extend AbstractList. However, in
doing this we would need to provide the implementation details of
the list structure itself. In effect, we would reimplement
LinkedList or
ArrayList.
Create a separate class that computes statistics on a
List given as an argument to the
methods of the statistical class. In this case, our statistical
methods will work with any subclass that implements the interface
List. This can be applied to any of
LinkedList,
ArrayList or
Vector.
Alternative three -- a separate statistical class -- has the
most reuse potential. We'll create a simple class with two methods:
mean and stdev to
compute the mean and standard deviation of a
List of Integer
objects. This can be used in a variety of contexts, and allows us the
freedom to switch List implementation
classes without any other changes.
The detailed algorithms for mean and standard deviation are provided in Statistical Algorithms.
Since our processes are relative simple, stateless algorithms,
we don't need any instance variables. Consequently, we'll never need
to create an instance of this class. As with
Java.lang.Math, all of these methods can be
declared as static, making them features of the class itself.
For those programmers new to statistics, this section covers the Sigma operator, Σ.
The Σ operator has three parts to it. Below it is a bound variable, i and the starting value for the range, written as i=0. Above it is the ending value for the range, usually something like n. To the right is some function to execute for each value of the bound variable. In this case, a generic function, f(i). This is read as “sum f(i) for i in the range 0 to n”.
One common definition of Σ uses a closed range, including the
end values of 0 and n. However, since this is not
a helpful definition for software, we will define Σ to use a half-open
interval. It has exactly n elements, including 0
and n-1; mathematically,
.
Consequently, we prefer the following notation, but it is not often used. Since statistical and mathematical texts often used 1-based indexing, some care is required when translating formulae to programming languages that use 0-based indexing.
Our two statistical algorithms have a form more like the following function. In this we are applying some function, f(), to each value, xi of an array. When computing the mean, there is no function. When computing standard deviation, the function involves subtracting and multiplying.
We can transform this definition directly into a for loop that
sets the bound variable to all of the values in the range, and does
some processing on each value of the List of Integers. This is the
Java implementation of Sigma. This computes two values, the sum,
sum and the number of elements,
n.
Example 15.1. Java Sigma Iteration
int sum= 0;
for( int i= 0; i != theList.size(); ++i ) {
int xi= ((Integer)theList.get(i)).intValue();
// int fxi = processing of xi
sum += fxi;
}
int n= theList.size();
This is the Python implemention of Sigma. This computes two
values, the sum, sum and the number of elements,
n.
Example 15.2. Python Sigma Iteration
sum= 0 for i in range(len(theList)):xi= theList[i]
# fxi = processing of xi
sum += fxi n= len(theList)
More advanced Python programmers may be aware that there are
several ways to shorten this loop down to a single expression using
the sum function as well as list displays.
In the usual mathematical notation, an integer index, i is used. In Java or Python it isn't necessary to use the formal integer index. Instead, an iterator can be used to visit each element of the list, without actually using an explicit numeric counter. In Java, the use of an iterator is shown below.
Example 15.3. Java Sample Values by Iterator
int sum= 0;
for( Iterator i= theList.iterator(); i.hasNext(); ) {
int xi= ((Integer)i.next()).intValue();
// int fxi = processing of xi
sum += fxi;
}
int n= theList.size();
In Python, the iterator is implied, and the processing simplifies to the following.
Example 15.4. Python Sample Values by Iterator
for xi in theList:# fxi = processing of xi
sum += fxi n= len(theList)
These iterator-based formulations can be slightly faster when
using a LinkedList because this class is
optimized for exactly this kind of sequential access.
Computing the mean of a list of values is relatively simple. The mean is the sum of the values divided by the number of values in the list. Since the statistical formula is so closely related to the actual loop, we'll provide the formula, followed by an overview of the code.
The definition of the Σ mathematical operator leads us to the following method for computing the mean:
Procedure 15.1. Computing Mean
intialize sum, s, to zero
for each value, i, in the range 0 to the number of values in the list, n:
add element xi to s
return s divided by the number of elements, n
The standard deviation can be done a few ways, but we'll use the formula shown below. This computes a deviation measurement as the square of the difference between each sample and the mean. The sum of these measurements is then divided by the number of values times the number of degrees of freedom to get a standardized deviation measurement. Again, the formula summarizes the loop, so we'll show the formula followed by an overview of the code.
The definition of the Σ mathematical operator leads us to the following method for computing the standard deviation:
Procedure 15.2. Computing Standard Deviation
compute the mean, m
intialize sum, s, to zero
for each value, xi in the list:
compute the difference from the mean, d as xi - m
add d2 to s. This is typically done as d*d in Java, since there is no “squared” operator. In Python, this can be d**2.
compute the variance, v, as s divided by (n - 1). This n - 1 value reflects the statistical notion of “degrees of freedom”, which is beyond the scope of this book.
return the square root of the variance, v.
For programmers new to Java, the
java.lang.Math contains the
Math.sqrt, which computes square roots.
For programmers new to Python, the math
module contains the math.sqrt, which computes
square roots.
Programmers who have the Python numeric
module installed will find that this module provides some
alternatives to the designs shown in this section. Since this module
is optional, we won't cover it any any depth here. However, it can
simplify the methods for computing mean and standard
deviation.
IntegerStatistics computes several simple
descriptive statistics of Integer values in a
List.
There are three deliverables for this exercise. These classes will include the complete Javadoc comments or Python dostring.
The IntegerStatistics class.
A unit test of the IntegerStatistics
class that some simple LinkedList,
ArrayList, and
Vector test data. The results can be
checked with a spreadsheet.
An update to the overall Simulator
that gets uses an IntegerStatistics object
to compute the mean and standard deviation of the peak stake. It
also computest the mean and standard deviation of the length of
each session of play.
Here is some standard deviation unit test data, some intermediate results and the correct answers given to 6 significant digits. Your answers should be the same to the precision shown.
| Sample Value |
|---|
| 9 |
| 8 |
| 5 |
| 9 |
| 9 |
| 4 |
| 5 |
| 8 |
| 10 |
| 7 |
| 8 |
| 8 |
| sum | 90 |
| mean | 7.5 |
| sum d*d | 608.668 |
| stdev | 1.88293 |
Table of Contents
This section will introduce a simple subclass of
Player who bets at random.
One possible betting strategy is to bet completely randomly. This serves as an interesting benchmark for other betting strategies.
We'll write a subclass of Player which
steps through all of the bets available on the
Wheel, selecting one or more of the available
outcomes at random. This Player, like others,
will have a fixed initial stake and a limited amount of time to
play.
The Wheel class can provide an
Iterator over the collection of
Bin instances. We could revise
Wheel to provide a
binIterator method that we can use to return
all of the Bins. From each
Bin, we will need an iterator we can use to
return all of the Outcomes.
To collect a list of all possible
Outcomes, we would use the following
algorithm:
Procedure 16.1. Locating all Outcomes
Empty List of Outcomes. Create an empty list of all
Outcomes,
allOC.
Get Bin Iterator. Get the Iterator from the Wheel
that lists all Bins.
For Each Bin
Get Outcome Iterator. Get the Iterator that lists all
Outcomes.
For Each Outcome
Save Outcome. Save a reference to each
Outcome in the list of all
known outcomes, allOC.
To place a random bet, we would use the following algorithm:
PlayerRandom is a
Player who places bets in Roulette. This player
makes random bets around the layout.
PlayerRandom(Table aTable,
Random aGenerator);This uses the super construct to
invoke the superclass constructor using the
Table. Then it saves the random number
generator. This could be an instance of either
Random or
NonRandom.
For those new to Java, see Java Subclass Constructors. For those new to Python, see Python Subclass Constructors.
There are five deliverables from this exercise. The new classes need Javadoc comments or Python docstrings.
Updates to the Bin to return an
iterator over available Outcomes
Updates to the Wheel to return an
iterator over available Bins
The PlayerRandom class.
A unit test of the PlayerRandom
class. This should use the NonRandom number generator to iterate
through all possible Outcomes.
An update to the overall Simulator
that uses the PlayerRandom.
Table of Contents
This section will describe a player who has a complex internal state. We will also digress on the way the states can be modeled using a group of polymorphic classes. This section also has an advanced exercise that shows some alternative implementations for the state objects, using the Singleton design pattern.
On the Internet, we found descriptions of a betting system called the “1-3-2-6” system. This system looks to recoup losses by waiting for four wins in a row. The sequence of numbers (1, 3, 2 and 6) are the multipliers to use when placing bets after winning. At each loss, the sequence resets to the multiplier of 1. At each win, the multiplier is advanced. After one win, the bet is now 3x. After a second win, the bet is reduced to 2x, and the winnings of 4x are “taken down” or removed from play. In the event of a third win, the bet is advanced to 6x. Should there be a fourth win, the player has doubled their money, and the sequence resets.
This betting system makes our player more stateful than in
previous betting systems. When designing
SevenReds, we noted that this player was
stateful, but deferred any more serious design consideration until
now.
In this case, the description of the betting system seems to identify four states: no wins, one win, two wins, and three wins. In each of these states, we have specific bets to place, and state transition rules that tell us what to do next. The following table summarizes the states, the bets and the transition rules.
| Next State | |||
|---|---|---|---|
| State | Bet | On Win | On Loss |
| No Wins | 1 | One Win | No Wins |
| One Win | 3 | Two Wins | No Wins |
| Two Wins | 2 | Three Wins | No Wins |
| Three Wins | 6 | No Wins | No Wins |
When we are in a given state, the table gives us the amount to bet in the Bet column. If this bet wins, we transition to the state named in the Win column, otherwise, we transition to the state named in the Lost column. We always start in the No Wins state.
We can exploit the State design pattern to design this more sophisticated player. This pattern suggests that we design a hierarchy of classes to represent these four states. Each state will have a slightly different bet amount, and a different next state when a bet wins. Each individual state class will be relatively simple, but we will have isolated the processing unique to each state into separate classes.
One of the consequences of the State design pattern is that it obligates us to
define the interface between the Player and the
object that holds the Player's current state.
It seems best to have the state object follow our table and provide
three methods: currentBet,
nextWon, and
nextLost. The Player
can use these methods of the state object to place bets and pick a new
state.
The state's currentBet method will
construct a Bet from an
Outcome that the Player
keeps, and the multiplier that is unique to the state. As the state
changes, the multiplier moves between 1, 3, 2 and 6.
The state's nextWon method constructs a
new state object based on the state transition table when the last bet
was a winner.
The state's nextLost method constructs
a new state based on the state transition table when the last bet was
a loser. In this case, all of the various states create a new instance
of the NoWins object, resetting the multiplier
to 1 and starting the sequence over again.
| Q: | Why code the state as objects? |
| A: | The reason for encoding the states as objects is to encapsulate the information or behavior associated with that state. In this case, we have both the bet amount and the rules for transition to next state. While simple, these are still unique to each state. Since this is a book on design, we feel compelled to present the best design. In games like blackjack, the player's state may have much more complex information or behavior. In those games, this design pattern will be very helpful. In this one case only, the design pattern appears to be over-engineered. We will use this same design pattern to model the state changes in the Craps game itself. In the case of the Craps game, there is additional information as well as behavior changes. When the state changes, bets are won or lost, bets are working or not, and outcomes are allowed or prohibited. |
| Q: | Isn't it simpler to code the state as a number? We can just increment when we win, and reset to zero when we lose. |
| A: | The answer to all “isn't it simpler” questions is “yes, but...”. In this case, the full answer is “yes, but what happens when you add a state or the states become more complex?” This question arises frequently in OO
programming. Variations on this question include “Why is
this an entire object?” and “Isn't an object
over-engineering this primitive type?” See Why is
Our goal in OO design is to isolate responsibility. First, and most important, we can unambiguously isolate the responsibilities for each individual state. Second, we find that it is very common that only one state changes, or new states get added. Given these two conditions, the best object model is separate state objects. |
| Q: | Doesn't this create a vast number of state objects? |
| A: | Yes. There are two usual follow-up questions: “Aren't all those objects a lot of memory overhead?” or “...a lot of processing overhead?” Since Java and Python remove unused objects, the old state definitions are cleaned up by the garbage collection thread. A few tests will demonstrate that the Java memory management is efficient and reliable. Object creation is an overhead that we can control. One common approach is to use the Singleton design pattern. In this case, this should be appropriate because we only want a single instance of each of these state classes. Note that using the Singleton design pattern doesn't change
any interfaces except the initialization of the
|
| Q: | Is Polymorphism necessary? |
| A: | In some design patterns, like State and Command, it is essential that all subclasses have the same interface and be uniform, indistinguishable, almost anonymous instances. Because of this polymorphic property, the objects can be invoked in a completely uniform way. In our exercise, we will design a number of different states for the player. Each state has the same interface. The actual values for the instance variables and the actual operation implemented by a subclass method will be unique. Since the interfaces are uniform, however, we can trust all state objects to behave properly. There are numerous designs where polymorphism doesn't matter at all. In many cases, the anonymous uniformity of subclasses isn't relevant. When we move on to example other casino games, we will see many examples of non-polymorphic class hierarchies. This will be due to the profound differences between the various games and their level of interaction with the players. |
There are a number of design elements: the superclass for the
states, plus the individual state classes, and the actual subclass of
Player that uses these states.
This version of the design will use a simpler design that constructs individual state objects on every state change. Once this is working, it can be replaced with a proper Singleton design to see how much that improves performance.
Player1326State is the superclass for
all of the states in the 1-3-2-6 betting system.
Player1326 player ;The player who is currently in this state. This player
will be used to provide the Outcome on
which we will bet.
Player1326State(Player1326 player);The constructor for this class saves the
Player1326 which will be used to
provide the Outcome on which we will
bet.
abstract Bet currentBet();Constructs a new Bet from the
player's outcome information. Each subclass
has a different multiplier used when creating this bet.
In Java, this method can be declared as abstract. Each subclass will provide a unique implementation for this method.
In Python, this method should raise the
NotImplementedException. This is a big
debugging aid, it helps us locate subclasses which did not
provide a method body. This raise statement is the functional
equivalent of the Java abstract declaration.
abstract Player1326State nextWon();Constructs the new
Player1326State instance to be used
when the bet was a winner.
In Java, this method can be declared as abstract. Each subclass will provide a unique implementation for this method.
In Python, this method should raise the
NotImplementedException.
Player1326State nextLost();Constructs the new
Player1326State instance to be used
when the bet was a loser. This method is the same for each
subclass: it creates a new instance of
Player1326NoWins. It is not abstract,
but actually defined in the superclass to assure that it is
available for each subclass.
Player1326NoWins defines the bet and
state transition rules in the 1-3-2-6 betting system. When there are
no wins, the base bet value of 1 is used.
Player1326NoWins defines the bet and
state transition rules in the 1-3-2-6 betting system. When there is
one wins, the base bet value of 3 is used.
Player1326NoWins defines the bet and
state transition rules in the 1-3-2-6 betting system. When there are
two wins, the base bet value of 2 is used.
Player1326NoWins defines the bet and
state transition rules in the 1-3-2-6 betting system. When there are
three wins, the base bet value of 6 is used.
Player1326 follows the 1-3-2-6 betting
system. The player has a preferred Outcome,
an even money bet like red, black, even, odd, high or low. The
player also has a current betting state that determines the current
bet to place, and what next state applies when the bet has won or
lost.
Outcome outcome ;This is the player's preferred outcome.
Player1326State myState ;This is the current state of the 1-3-2-6 betting system. It will be an instance of one of the four states: No Wins, One Win, Two Wins or Three Wins.
Player1326();Initializes the state and the outcome. The
myState is set to the initial state of an
instance o0f Player1326NoWins. The
outcome is set to some even money
proposition, for example
"Black".
void placeBets();Updates the Table with a bet
created by the current state. This method delegates the bet
creation to myState
currentBet.
void win(Bet bet);Uses the superclass method to update the stake with an
amount won. Uses the current state to determine what the next
state will be by calling myState's
nextWon method and saving the new
state in myState
void lose(Bet bet);Uses the current state to determine what the next state
will be. This method delegates the next state decision to
myState's
nextLost, method saving the result in
myState.
There are eight deliverables for this exercise. Additionally, there is an optional, more advanced design exercise in a separate seciotn.
The five classes that make up the
Player1326State class hierarchy.
The Player1326 class.
A unit test of the Player1326 class.
This test should synthesize a fixed list of
Outcomes, Bins, and
calls a Player1326 instance with various
sequences of reds and blacks. There are 16 different sequences of
four winning and losing bets. These range from four losses in a
row to four wins in a row.
An update to the overall Simulator
that uses the Player1326.
The object creation for each state change can make this particular player rather slow. Using the Singleton design pattern can make this application run faster. The difference in performance is small for this application, but can be huge for other applications. We note that the Singleton design pattern assures that only a single instance of a given class exists, guaranteeing a single centralized object which can have a number of consequences. First, it can prevent problems from duplicate use of precious resources. Second, it can improve performance and reduce memory consumption.
The general idea behind the Singleton design pattern is to assure that only a single instance of a given class ever exists. This is done by avoiding the use of an explicit constructor. Instead, a static method is provided by the class that will return the one-and-only instance of the object. Because of the technical differences in Java and Python, we'll cover each implementation in some detail.
The deliverables for this exercise are revisions to the
Player1326State class hierarchy to implement
the Singleton design pattern. This
will not change any of the existing unit tests, demonstrations or
application programs.
To implement the Singleton
design pattern for the entire state class hierarchy, we must add a
static variable, theInstance, initially
null, to each individual subclass of
Player1326State. Additionally, we have to add
a getInstance method to each subclass.
Finally, we can make the constructor
protected in the superclass to
prevent any other class from using it via the
new operator.
Note that we can't inherit a single superclass definition of
theInstance variable or
getInstance method. Being static, these
would be shared by all objects of all four subclasses. We would only
have a single Player1326State object, and it
would be the initially constructed object. Consequently, we need to
have a distinct instance variable and method in each subclass, so
that an instance of each distinct subclass gets created.
This aspect of this class hierarchy has to be included by a tedious cut-and-paste process. It might be nice to specify that this piece of programming is copied out of a template into each class, assuring consistent implementation of the Singleton design pattern. There are a few automated approaches to this, including tools for aspect-oriented programming and literate programming. Both of these are areas for further investigation, well outside the scope of this book.
The Player1326 constructor would be
changed to use the getInstance method of
Player1326NoWins to create the initial state.
We have to remove the creation a new instance via
new Player1326NoWins() and replace
this with
Player1326NoWins.getInstance().
Each state's nextWon and
nextLost methods would have to use the
getInstance method to get the next state
instance. By removing all of the individual
new operations, we assure that
objects are only made available through
getInstance, controlling the number of
instances actually created.
We note that the compile-time binding in Java forces us to repeat these two static elements in each class. There is some administrative overhead in assuring that this aspect of the Singleton pattern is repeated in each subclass. In Python, however, the late binding makes this slightly simpler.
This class can be sped up by using the Singleton design pattern for the entire state class hierarchy. In Python, we have two choices for dealing with the various singleton instances. The most common solution is to create module-level variables at import time; Python assures that each module is only important once. A second solution is to implement a Java-like singleton design in the classes.
Because of the deferred binding in Python, we can easily have
each class contain references module-level variables that won't be
created until after the class is defined. The
variables don't exist when the class is defined, but they will exist
when the object methods are actually executed. Each class can then
expect a module-level variable with a name like
theNoWinsState. These four variables are created
by the module after the definitions of the classes.
The Java-style declaration means that we have a
theInstance variable which contains the
one-and-only instance for this class. Note that we have two choices
for handling the object creation. We can override the
__new__ method, or we can provide a
getInstance method. We'll focus on the
getInstance method because it's most like
Java.
For programmers new to Python, there are two small syntax
changes that make a variable or method part of the class and not
part of each individual instance. First, instance variables must be
created by __init__ and qualified with the
instance name (usually self). If, instead, we
simply place a variable in the class definition, this variable
belongs to the class, and therefore it belongs to every instance of
the class. Second, if we call a method using an object name, the
method is called with that given instance as the value for
self. If, instead, we call a method using the
class name, the method is called for the class as a whole.
Table of Contents
This section will describe a player who has a complex internal state that can be modeled using existing library classes.
One method for tracking the lost bets is called the “cancellation” system or the “Labouchere” system. The player starts with a betting budget allocated as a series of numbers. The usual example is 1, 2, 3, 4, 5, 6. Each bet is sum of the first and last numbers in the last. In our example, the end values of 1+6 leads the player to bet 7. When the player wins, they cancel the two numbers used to make the bet. In the event that all the numbers are cancelled, the player resets the sequence of numbers and starts again. For each loss, however, the player adds the amount of the bet to the end of the sequence; this is a loss to be recouped. This adds the loss to the amount bet to assure that the next winning bet both recoups the most recent loss and provides a gain. Multiple winning bets will recoup multiple losses, supplemented with small gains.
Here's an example of the cancellation system using 1, 2, 3, 4, 5, 6.
Bet 1+6. A win. Cancel 1 and 6 leaving 2, 3, 4, 5.
Bet 2+5. A loss. Add 7 leaving 2, 3, 4, 5, 7.
Bet 2+7. A loss. Add 9 leaving 2, 3, 4, 5, 7, 9.
Bet 2+9. A win. Cancel 2 and 9 leaving 3, 4, 5, 7.
Next bet will be 3+7.
State. The player's state is a list of individual bet amounts. This
list grows and shrinks; when it is empty, the player leaves the
table. We can keep a List of
individual bet amounts. The total bet will be the first and last
elements of this list. Wins will remove elements from the
collection; losses will add elements to the collection. Since we
will be accessing elements in an arbitrary order, we will want to
use an ArrayList. We can define the player's
state with a simple list of values.
PlayerCancellation uses the cancellation
betting system. This player allocates their available budget into a
sequence of bets that have an accelerating potential gain as well as
recouping any losses.
List sequence = new List();This List keeps the bet
amounts; wins are removed from this list and losses are appended
to this list. THe current bet is the first value plus the last
value.
Outcome outcome ;This is the player's preferred outcome.
PlayerCancellation();This uses the resetSequence
method to initalize the sequence of numbers used to establish
the bet amount. This also picks a suitable even money
Outcome, for example, black.
resetSequence();Puts the initial sequence of six
Integer instances into the
sequence variable. These
Integers are built from the values 1
through 6.
void placeBets();Creates a bet from the sum of the first and last values of
sequence. The first value is at index 0. The
last value's position depends on the size of the
List.
void win(Bet bet);Uses the superclass method to update the stake with an
amount won. It then removes the fist and last element from
sequence.
void lose(Bet bet);Uses the superclass method to update the stake with an
amount lost. It then appends the sum of the first and list
elements of sequence to the end of
sequence as a new
Integer value.
There are three deliverables for this exercise.
The PlayerCancellation class.
A unit test of the PlayerCancellation
class. This test should synthesize a fixed list of
Outcomes, Bins, and
calls a PlayerCancellation instance with
various sequences of reds and blacks. There are 16 different
sequences of four winning and losing bets. These range from four
losses in a row to four wins in a row. This should be sufficient
to exercise the class and see the changes in the bet
amount.
An update to the overall Simulator
that uses the PlayerCancellation.
Table of Contents
This section will describe a player who has an internal state that can be modeled using methods and simple values instead of state objects.
A player could use the Fibonacci Sequence to structure a series of bets in a kind of cancellation system. The Fibonacci Sequence is 1, 1, 2, 3, 5, 8, 13, .... At each loss, the sum of the previous two bets is used, which is the next number in the sequence. In the event of a win, the last two numbers in the sequence are removed. This allows us to easily track our accumulated losses with bets that could recoup those losses.
In order to compute the Fibonacci sequence, we need to retain the two previous bets as the player's state. In the event of a win, we can compute the previous number in the sequence as the difference in the two bets. In the event of a loss, we can update the two numbers to show the next step in the sequence. The player's state is just these two numeric values.
PlayerFibonacci uses the Fibonacci
betting system. This player allocates their available budget into a
sequence of bets that have an accelerating potential gain.
int recent = 1;This is the most recent bet amount.
int previous = 0;This is the bet amount previous to the most recent bet amount.
void win(Bet bet);Uses the superclass method to update the stake with an
amount won. This will go “backwards” in the
sequence. It updates recent and
previous as follows.
newRecent =
recent -
previous
previous =
recent -
newRecent
recent =
newRecent
void lose(Bet bet);Uses the superclass method to update the stake with an
amount lost. This will go “forwards” in the
sequence. It updates recent and
previous as follows.
newRecent =
recent +
previous
previous =
recent
recent =
newRecent
There are three deliverables for this exercise.
The PlayerFibonacci class.
A unit test of the PlayerFibonacci
class. This test should synthesize a fixed list of
Outcomes, Bins, and
calls a PlayerFibonacci instance with
various sequences of reds and blacks. There are 16 different
sequences of four winning and losing bets. These range from four
losses in a row to four wins in a row. This should be sufficient
to exercise the class and see the changes in the bet
amount.
An update to the overall Simulator
that uses the PlayerFibonacci.
The game of Roulette has given us an opportunity to build an application with a considerable number of classes and objects. It is comfortably large, but not complex; we have built tremendous fidelity to a real-world problem. Finally, this program produces moderately interesting simulation results.
We note that a great many of our design decisions were not easy to make without exploring a great deal of the overall application's design. There are two ways to do this exploration: design everything in advance or design just enough but remain tolerant of our own ignorance.
Experienced designers can often design an entire, complex application before building any software. The process of doing this design, however, is internally iterative. Some parts are designed in detail, with tolerance for future changes; then other parts are designed in detail and the two design elements reconciled.
For new designers, we can't give enough emphasis to the importance of creating a trial design, exploring the consequences of that design, and then doing rework of that design. Too often, we have seen trial designs finalized into deliverables with no opportunity for meaningful rework. In Chapter 11, Review of Testability, we presented one common kind of rework to support more complete testing. In Chapter 12, Player Class, we presented another kind of rework to advance a design from a stub to a complete implementation.
We also feel compelled to point out the distinction between
relatively active and passive classes in this design. We had several
passive classes, like Outcome,
Bet and Table, which had
few responsibilities beyond collecting a number of related attributes
and providing simple functions. We also had several complex, active
classes, like Game,
BinBuilder and all of the variations on
Player. These classes, typically, had fewer
attributes and more complex methods. In the middle of the spectrum is
the Wheel. We find this distinction to be an
enduring feature of OO design: there are
things and actors; the things
tend to be passive, acted upon by the actors. The overall system
behavior emerges from the collaboration among all of the objects in the
system; primarily -- but not exclusively -- the behavior of the active
classes.
This part describes parts of the more complex game of Craps. Craps is a game with two states and a number of state-change rules. It has a variety of kinds of bets, some of which are quite complex.
The chapters of this part presents the details on the game, an overview of the solution, and a series of eleven exercises to build a complete simulation of the game, with a variety of betting strategies. The exercises in this part are more advanced; unlike Part I, “Roulette”, we will often combine several classes into a single batch of deliverables.
There are several examples of rework in this part, some of them quite extensive. This kind of rework reflects three more advanced scenarios: refactoring to generalize and add features, renaming to rationalize the architecture, and refactoring to extract features. Each of these is the result of learning; they are design issues that can't easily be located or explained in advance.