The game of Mah Jongg is played with a deck of tiles with three suits with ranks from one to nine. There are four sets of these 27 tiles. Additionally there are four copies of the four winds and three dragons. This gives a deck of 136 tiles.
The three suits are dots, bamboo and “characters” (wan, 万 simplified or 萬 traditional). The ranks are the numbers one to nine. [One story is that the dots are chinese coins, the “bamboo” are stacks of 100 coins that and the “wan” represents 10,000 coins.] Since these tiles have ranks, they can form a variety of interesting combinations including matches and sequences.
The winds and dragons are collectively called “honors”. There are four winds: East (東), South (南), West (西), and North (北). There are three dragons: White (白), Red (中), and Green (發). These honors tiles don’t have ranks, merely names. Since there are four of each, these tiles can only participate in matching; there’s no sequence of winds combination.
In some variations of the game there are also jokers, seasons and flowers. We’ll leave these out of our analysis for the moment.
We can define a parent class of Tile, and two subclasses: SuitTile and HonorTile. These have slightly different attributes.
The SuitTile class has suit and rank information.
The HonorTile class merely has a unique name.
The superclass can define a basic comparison function, __eq__(), that compares self.getName() to other.getName() to see if the other tile has the same name. For SuitTile, the name includes rank and suit.
The SuitTile class, however, needs to define methods for __lt__(), __le__(), __gt__() and __ge__() to compare rank and suit.
The HonorTile class can simply return False for the various __lt__() and __gt__(). The implementation of __ge__() and __le__() must simply use __eq__().
If we use the names "Bamboo", "Character" and "Dots", this makes the suits occur alphabetically in front of the honors without any further special processing. If, on the other hand, we want to use Unicode characters for the suits, we should add an additional sort key to the Tile that can be overridden by SuitTile and HonorsTile to force a particular sort order.
Note that the two ranks of one and nine have special status among the suit tiles. These are called terminals; ranks two through eight are called simples. Currently, we don’t have a need for this distinction.
Build The Tile Class Hierarchy. First, build the tile class hierarchy. This includes the Tile, SuitTile, HonorTile classes. Write a short test that will be sure that the equality tests work correctly among tiles.
You hould also define a Mah Jongg Wall class which holds the initial set of 136 tiles. We can create additional subclasses to add as many as a dozen more tiles to include jokers, flowers and seasons.
Create the set of 136 tiles. This is four copies of the following tiles:
The wall is nearly identical with a deck of playing cards. See Advanced Class Definition Exercises for more guidance on this class design.
Build The Wall Class. Design and implement the Wall. Write a short test that will be sure that it shuffles and deals tiles properly.
A winning Mah Jongg hand generally as 14 tiles in five scoring sets. Exactly one of these stes must be a pair. The remaining sets are generally four groups of three tiles.
Under some circumstances, there can one ore more 4-of-a-kind sets, and the hand will also be larger. This can happen when you are holding three-of-a-kind and draw the fourth. Your hand must be extended by one tile to become 15 tiles in size. Clearly, this can only happen four times, leading to an upper limit of a four groups of four tiles.
There are four varieties of set:
The most common winning hands have 14 tiles: 4 sets of three and a pair.
A Mah Jongg Hand object, then, is a list of Tiles. This class needs a method, mahjongg() that returns True is the hand is a winning hand. The evaluation is rather complex, because a tile can participate in a number of sets, and several alternative interpretations may be necessary to determine the appropriate use for a given tile.
Consider a hand with 2, 2, 2, 3, 4 of bamboo. This is either a set of three 2’s and a non-scoring 3 and 4, or it is a pair of 2’s and a sequence of 2, 3, 4.
The mahjongg() method, then must create five TileSet objects, assigning individual Tiles to the TileSets until all of the Tiles find a home. The hand is a winning hand if all sets are full, there are five sets, and one set is a pair.
We’ll cover the design of the TileSet classes in this section, and return to the design of the Hand class in the next section.
The Set Class Hierachy. We can create a class hierarchy around the four varieties of TileSet: pairs, threes, fours and straights. A PairSet holds two of a kind: both of the tiles have the same name or the same suit and rank. A ThreeSet and FourSet are similar, but have a different expectation for being full. A SequenceSet holds three suit tiles of the same suit with adjacent ranks. Since we will sort the tiles into ascending order, this set will be built in ascending order, making the comparison rules slightly simpler.
We’ll define a TileSet superclass to hold a sequence of Tiles. We will be able to add new tiles to a TileSet, as well as check to see if a tile could possibly belong to a TileSet. Finally, we can check to see if the TileSet is full. The superclass, TileSet, is abstract and returns NotImplemented for the full() method. The sublasses will override this methods with specific rules appropriate to the kind of set.
The superclass canContain() method returns True if the list is empty; it returns False if the list is full. Otherwise it compares the new tile against the last tile in the list to see if they are equal. Since most of the subclasses must match exactly, this rule will be used.
The sequence subclass must override this to compare suit and rank correctly.
An important note about the fallback() method is that the stack that will be given as an argument in tileStack is part of the Hand, and is maintainted by doing tileStack.pop(0) to get the first tile, and tileStack.insert( 0, aTile ) to push a tile back onto the front of the hand of tiles.
We’ll need the following four subclasses of TileSet.
Specializes TileSet for sets of four matching tiles. The full() method returns True when there are four elements in the list.
The fallback() method pushes the set’s tiles onto the given tileStack; it returns a new ThreeSet with the first tile from the tileStack.
Specializes TileSet for sets of three matching tiles. The full() method returns True when there are three elements in the list.
The fallback() method pushes the set’s tiles onto the given tileStack; it returns a new SequenceSet with the first tile from the tileStack.
Specializes TileSet for sets of three tiles of the same suit and ascending rank.
The canContain() returns True for an empty list of tiles, False for a full list of tiles, otherwise it compares the suit and rank of the last tile in the list with the new tile to see if the suits match and the new tile’s rank is one more than the last tile in the list.
The full() method returns True when there are three elements in the list.
The fallback() method pushes the set’s tiles onto the given tileStack ; it returns a new PairSet with the first tile from the tileStack.
The full() method returns True when there are two elements in the list.
The fallback() method is inherited from the superclass method in TileSet; this method returns None, since there is no fallback from a pair.
This subclass also returns True for the pair() method.
The idea of these class definitions is that the Hand can attempt to use a FourSet to collect a group of tiles. If this doesn’t work out, we put the tiles back into the hand, and try a ThreeSet. If this doesn’t work out, we put the tiles back and try a SequenceSet. The last resort is to try a PairSet. There is no fallback after a pair set, and the hand cannot be a winner.
A Mah Jongg Hand object, then, is a list of Tiles The mahjongg() creates an assignment of individual TileSets It checks these sets to see if all of them are full, if there are five of them and if one of the five is a pair. If so, it returns True because the hand is a winning hand.
If we sort the tiles by name or suit, we can more effectively assign tiles to sets. The first step in the mahjongg() method is to sort the tiles into order. Then the tiles can be broken into sets based on what matches between the tiles.
Hand Scoring
The mahjongg() function examines a hand to determine if the tiles can be assigned to five scoring sets, one of which is a pair.
The allFull() function checks three conditions: all TileSets are full, there are five TileSets, and is one TileSet is a pair. The first test, all TileSets are full, can be done with the built-in all() function, looking something like the following all( s.full() for s in sets ).
Examine All Tiles
The examine() method requires a non-empty stack of candidate TileSets, created by the mahjongg() method. It assigns all of the remaining tiles beginning with the top-most candidate TileSet. Initially, the entire hand is examined. After each retry, some number of tiles will have been pushed back into the hand for re-examination.
Retry a TileSet Assignment
The retry() method requires at least one TileSet in the assignments. This will pop that TileSet, pushing the tiles back into the hand. It will then use the popped TileSet‘s fallback() method to get another flavor of TileSet to try.
The following test case is typical.
Bamboo: 2, 2, 2, 3, 4, 5, 5, 5
Dots: 2, 2, 2
Green Dragon |times| 3
In this case, we will attempt to put the “2 Bamboo” tiles into a TileSet of four. No other tile will fill this TileSet. After looking at the remaining tiles, we’ll pop that incomplete TileSet, and put them into a TileSet of three. This will be full, so we’ll move on.
The “3 Bamboo” will be put into a set of four. No other tile can fill this set, so we’ll pop it, and put the “3 Bamboo” into a set of three. Again, no other tile can fill this set, so we’ll pop that, and fill back to a Sequence. This set will be filled with the 4 and 5.
The remaining two “5 Bamboo” tiles will be put into a set of four (which won’t be filled), a set of three (which won’t be filled), a straight (which won’t be filled) and finally a pair.
The three “2 Dots” tiles will be put into a set of file (which won’t be filled) and a set of three. The fate awaits the three green dragon tiles.
The final set stack will have a three set, a straight, a pair, and two three sets. This meets the rules for five full sets, one of which is a pair.
def testHand1():
t1= [ SuitTile( 2, "Bamboo" ), SuitTile( 2, "Bamboo" ),
SuitTile( 2, "Bamboo" ), SuitTile( 3, "Bamboo" ),
SuitTile( 4, "Bamboo" ), SuitTile( 5, "Bamboo" ),
SuitTile( 5, "Bamboo" ), SuitTile( 5, "Bamboo" ),
SuitTile( 2, "Dot" ), SuitTile( 2, "Dot" ),
SuitTile( 2, "Dot" ), HonorsTile( "Green" ),
HonorsTile( "Green" ), HonorsTile( "Green" ), ]
h1= Hand( *t1 )
print h1.mahjongg()
More Complex. The following test case is a little more difficult.
Bamboo: 2, 2, 2, 2, 3, 4
Green Dragon |times| 3
Red Dragon |times| 3
North Wind |times| 2
The initial run of four “2 Bamboo” tiles will be put into a set of four.
The next “3 Bamboo” and “4 Bamboo” tiles will be put into a four set (which won’t be filled), a three set and straight. None if this will be successful.
We then pop the initial set of four “2 Bamboo” tiles and replace that with a set of three. The “2 Bamboo” will be tried in a set of four, and a set of three before it winds up in a sequence. This sequence will allow the “3 Bamboo” and the “4 Bamboo” to be added.
The remaining honors will be tried against four sets and then three sets before the hand is found to be valid.
Another Test. Here’s a challenging test case with two groups of tiles that require multiple retries.
Here’s a summary of the hand.
Bamboo: 2, 2, 2, 3, 4, 5, 5, 5
Dots: 2, 2, 2, 2, 3, 4
Here’s the test fixture.
def testHand2():
t2= [ SuitTile( 2, "Bamboo" ), SuitTile( 2, "Bamboo" ),
SuitTile( 2, "Bamboo" ), SuitTile( 3, "Bamboo" ),
SuitTile( 4, "Bamboo" ), SuitTile( 5, "Bamboo" ),
SuitTile( 5, "Bamboo" ), SuitTile( 5, "Bamboo" ),
SuitTile( 2, "Dot" ), SuitTile( 2, "Dot" ),
SuitTile( 2, "Dot" ), SuitTile( 2, "Dot" ),
SuitTile( 3, "Dot" ), SuitTile( 4, "Dot" ), ]
h2= Hand( *t2 )
print h2.mahjongg()
Ideally, your overall unit test looks something like the following.
import unittest
class TestHand_1(unittest.TestCase):
def setUp( self ):
Create the hand
def testHand1_should_mahjongg( self ):
self.assert_( h1.mahjongg() )
self.assertEqual( str(h1.sets[0]), "ThreeSet['2B', '2B', '2B']" )
self.assertEqual( str(h1.sets[1]), "SequenceSet['3B', '4B', '5B']" )
self.assertEqual( str(h1.sets[2]), "PairSet['5B', '5B']" )
self.assertEqual( str(h1.sets[3]), "ThreeSet['2D', '2D', '2D']" )
self.assertEqual( str(h1.sets[4]), "ThreeSet['Green', 'Green', 'Green']" )
class TestHand_2(unittest.TestCase):
def setUp( self ):
Create the hand
def testHand2_should_mahjongg( self ):
self.assert_( h2.mahjongg() )
... check individual TileSets
if __name__ == "__main__":
unittest.main()
A set of nine interesting test cases can be built around the following set of tiles: 3 × 1’s, 2, 3, 4, 5, 6, 7, 8, and 3 × 9’s all of the same suit. Adding any number tile of the same suit to this set of 13 will create a winning hand. Develop a test function that iterates through the nine possible hands and prints the results.
A hand has a point value, based on the mixture of TileSets. This point value is used to resolve the amount owed to the winner by the losers in the game. There is a subtlety to this evaluation that we have to gloss over, and that is the rules about for concealed and exposed or melded TileSets. For now, we will assume that all TileSets are concealed.
We need to expand our definition of SuitTile. There are two different score values for SuitTiles: the terminals (one and nine) have one score, and the simples (two through eight) have a different score. This will lead to two subclasses of SuitTile: TerminalSuitTile and SimpleSuitTile.
A winning hand has a base value of 20 points plus points assigned for each of the four scoring sets and the pair.
| TileSet | Simples | Terminals or Honors |
| SequenceSet | 0 | 0 |
| ThreeSet | 4 | 8 |
| FourSet | 16 | 32 |
The PairSet is typically worth zero points. However, the following kinds of pairs can add points to a hand.
There are a few more ways to add points, all related to the mechanics of play, not to the hand itself.
Update the Tile Class Hierarchy. You will need to add two new subclass of SuitTile: TerminalSuitTile and SimpleSuitTile.
You will want to upgrade Wall to correctly generate the various HonorsTile, TerminalSuitTile and SimpleSuitTile instances.
You may also want to create a Generator for tiles. A function similar to the following can make programs somewhat easier to read.
def tile( *args ):
"""tile(name) -> HonorsTile
tile( rank, suit ) -> SuitTile
"""
if len(args) == 1:
return HonorsTile( *args )
elif args[0] in ( 1, 9 ):
return TerminalSuitTile( *args )
else:
return SimpleSuitTile( *args )
Update the TileSet Class Hierarchy. You will need to add at least one new method to the TileSet classes.
Examine the first Tile of the TileSet to see if it is simple() or not, and return the proper number of points.
The two wind parameters aren’t used for most TileSet subclasses.
In the case of PairSet, however, the first Tile must be checked against two rules. If prevailingWind is the same as myWind and the same as the tile’s name, this is worth 4 points. If the tile’s lucky() method is True (a dragon, or one of the two winds), then the value is 2 points.
Update the Hand Class. You’ll want to add at least one new method to the Hand class.
You will want to revise your unit tests, also, to reflect these changes. You’ll also need to add additional unit tests to check the number of points in each hand.
Test Cases. For the first test cases in the previous Some Test Cases, here are the scores.
| Set | Points |
| Winning | 20 |
| ThreeSet[‘2B’, ‘2B’, ‘2B’] | 4 |
| StraightSet[‘3B’, ‘4B’, ‘5B’] | 0 |
| PairSet[‘5B’, ‘5B’] | 0 |
| ThreeSet[‘2D’, ‘2D’, ‘2D’] | 4 |
| ThreeSet[‘Green’, ‘Green’, ‘Green’] | 8 |
| Points | 36 |
For the second test cases in Some Test Cases, here are the scores. The assumption here is that we’re not sitting at North, and we’re not playing the final four hands (where the prevailing wind is North.)
| Set | Points |
| Winning | 20 |
| ThreeSet[‘2B’, ‘2B’, ‘2B’] | 4 |
| StraightSet[‘2B’, ‘3B’, ‘4B’] | 0 |
| ThreeSet[‘Green’, ‘Green’, ‘Green’] | 8 |
| ThreeSet[‘Red’, ‘Red’, ‘Red’] | 8 |
| PairSet[‘N’, ‘N’] | 0 |
| Points | 36 |
For the third test cases in Some Test Cases, here are the scores.
| Set | Points |
| Winning | 20 |
| ThreeSet[‘2B’, ‘2B’, ‘2B’] | 4 |
| StraightSet[‘3B’, ‘4B’, ‘5B’] | 0 |
| PairSet[‘5B’, ‘5B’] | 0 |
| ThreeSet[‘2D’, ‘2D’, ‘2D’] | 4 |
| StraightSet[‘2D’, ‘3D’, ‘4D’] | 0 |
| Points | 28 |
Be sure to add a test case with lucky tiles (dragons or winds) as the pair.
The point value for a hand can be doubled a number of times for a variety of rare achievements. Most of these rules of these are additional properties of TileSets that are summarized by the Hand.
Each non-pair TileSet that contains lucky tiles is worth 1 double (2 ×). In the case of the player’s wind being the prevailing wind, a TileSet of this wind is worth 2 doubles (4 ×)
A hand of four ThreeSet or FourSet (i.e., no SequenceSet) merits a double. Depending on how the hand was played and how many of these triples were concealed or melded, the hand can have a second double, or possibly even pay the house limit, something we’ll look into in Limit Hands. For now, we are ignoring these mechanics of play issues, and will simply double the score if there are no SequenceSets.
A hand that has three consecutive SequenceSets in the same suit is doubled. There are many rule variations on this theme, including same-rank sequences from all three suits, same-rank ThreeSet s or FourSets from all three suits. We’ll focus on the three-consecutive rule for now.
If the hand is worth exactly 20 points (it is all SequenceSets and an unlucky PairSet), then it merits one double for being all non-scoring sets.
There are six different consistency tests. These are exclusive and at most one of these conditions will be true.
There are a few more ways to add doubles, all related to the mechanics of play, not to the hand itself.
Update TileSet Class Hierarchy. You’ll need to add the following functions to the TileSet classes.
For the ThreeSet or FourSet subclasses, this returns True for a complete set with lucky tiles (dragons, the prevailing wind or the player’s wind.)
Other subclasses of TileSet return False.
Returns True for a ThreeSet or FourSet. Other subclasses of TileSet return False.
This is used to see if a hand is all threes or fours, which merits a double. The name “triplet” isn’t literally true; a literally true function name would be cumbersome.
Returns the suit of a SequenceSet. For this class only, it should be based on the suit() method described below.
Other subclasses of of TileSet return None.
Returns the lowest rank of a SequenceSet.
Other subclasses of of TileSet return None.
Returns True if the TileSet contains no simple tiles. This is an all terminals and honors TileSet.
This is not the opposite of allSimple(). A SequenceSet could have a mixture of Terminal and Simple tiles, so there are three cases: allSimple, noSimple and a mixed bag.
Update Hand Class. You’ll need to add the following functions to the Hand classes.
Returns 1 or 4 after checking for the following conditions:
The sum of the double functions is the total number of doubles for the hand. This is given by code something like the following.
doubles = hand.luckySets( wind_prevail, wind_me ) + hand.groups() + hand.sequences() + hand.noPoints() + hand.consistency()
final_score = 2**doubles * base_points
An amazing hand of all one suit with three consecutive sequences leads to 5 doubles, 32 × the base number of points.
, where d is the number
of doubles and p is the base number of points.The total score is often rounded to the nearest 10, as well as limited to 500 or less to produce a final score. This final score is used to settle up the payments at the end of the game.
At the end of a hand of Mah Jongg, the winner is paid based on the final score of the hand. Generally, the final score is limited to 500 points. There are, however, some extraordinary hands which simply score this limit amount. These conditions are checked first; if none of these are true, then the normal hand scoring is performed.
An interesting limit hand is the Nine Gates hand, which is 3?1’s, 2, 3, 4, 5, 6, 7, 8, and 3?9’s all of the same suit. Any other tile of this suit will create a winning hand that pays the limit. Just considering the hand outside the mechanics of play, it would get four doubles because it is all one suit, plus the possibility of an additional double for consecutive sequences. The Nine Gates hand is only a limit hand if the player draws it as a completely concealed hand.
There are a few other limit hands, including all concealed triplets, or being dealt a winning hand. These, however, depend on the mechanics of play, not the hand itself.
Update Set Class Hierarchy. You’ll want to add wind() and dragon() methods to the TileSet hierarchy. These return True if all Tiles in the TileSet are a wind or a dragon, respectively.
Update Hand Class. You can add six additional methods to Hand to check for each of these limit hands.
The final step is to update the finalScore() to check for limit hands prior to computing points and doubles.