Mah Jongg Hands

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.

Tile Class Hierarchy

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__().

class Tile
__init__(self, name)
Build this tile from the given name.
__str__(self) → string
Returns the Tile.getName() value.
__eq__(self, other) → boolean
This is simply self.getName() == other.getName().
__ne__(self, other) → boolean
This is simply self.getName() != other.getName().
getSuit(self) → string
Returns NotImplemented; each subclass must override this.
getRank(self) → string
Returns NotImplemented; each subclass must override this.
getName(self) → string
Returns the tile’s name.
class SuitTile(Tile)
__init__(self, rank, suit)
Initializes a tile with rank and suit instead of name.
getSuit(self) → string
Returns this tile’s suit
getRank(self) → string
Returns this tile’s rank
getName(self) → string
Returns this tile’s full name, including suit and rank.
__lt__(self, other) → boolean
Compares rank and suit.
__le__(self, other) → boolean
Compares rank and suit.
__gt__(self, other) → boolean
Compares rank and suit.
__ge__(self, other) → boolean
Compares rank and suit.
class HonorTile(Tile)
getSuit(self) → string
Returns None.
getRank(self) → string
Returns None.
getName(self) → string
Returns this tile’s full name.
__lt__(self, other) → boolean
Returns False.
__le__(self, other) → boolean
Returns the value of HonorTile.__eq__().
__gt__(self, other) → boolean
Returns False.
__ge__(self, other) → boolean
Returns the value of HonorTile.__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.

Wall Class

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.

class Wall
__init__(self)

Create the set of 136 tiles. This is four copies of the following tiles:

  • The twenty-seven combinations of each suit (dot, bamboo, and character) and each rank (one through nine).
  • The seven honor tiles (east, south, west, north, red, white, green).
__str__(self) → string
Display information about the wall.
shuffle(self)
Shuffles the wall tiles.
deal(self) → Tile
Return the next undealt tile. This will not enumerate through all of the tiles. Generally, six tiles will remain undealt.

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.

TileSet Class Hierarchy

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:

  • Pair. Two matching tiles; either honor tiles with the same name, or suit tiles with the same rank and suit.
  • Three of a kind. Three matching tiles.
  • Sequence of three in a row of the same suit. Only suit tiles can participate in a sequence. The same suit is an essential feature.
  • Four of a kind.

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.

class TileSet
__init__(self)
Create a new, empty set of Tiles.
__str__(self) → string
Representation of the contents of this set.
canContain(self, aTile) → boolean

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.

add(self, aTile)
The add() method appends the new tile to the internal list. A pop() function can remove the last tile appended to the list.
full(self) → boolean
The superclass will return NotImplemented. Each subclass must override this.
fallback(self, tileStack) → TileSet instance
The superclass fallback() pushes all of the tiles from the TileSet back onto the given stack. The superclass version pushes the tiles and then returns None. Each subclass must override this to return a different fallback TileSet instance.
pair(self) → boolean
The superclass pair() method returns False. The PairSet subclass must override this to return True.

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.

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

class ThreeSet(TileSet)

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.

class SequenceSet(TileSet)

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.

class PairSet(TileSet)

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.

Hand Class

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.

  1. Sort Tiles. Sort the tiles by name (or suit) and by rank for suit tiles where the suit matches. We will treat the hand of tiles as a tile stack, popping and pushing tiles from position 0 using pop(0) and insert(0,tile).
  2. Stack of TileSets. The candidate set definition is a stack of TileSet objects. Create an empty list to be used as the candidate stack. Create a new, empty FourSet and push this onto the top of the candidate stack.
  3. Examine Tiles. Use the examine() function to examine the tiles of the hand, assigning tiles to TileSets in the candidate stack. When this operation is complete, we may have a candidate assignment that will contain a number of TileSets, some of which are full, and some are incomplete. We may also have an empty stack because we have run out of fallback TileSets.
  4. While Not A Winner. While we have TileSets in the candidate stack, use the allFull() to see if all TileSets are full, there are five sets, and there is exactly one pair. If we do not have five full TileSets and a single pair, then we must fallback to another subclass of TileSet.
    1. Retry. Use the retry() method to pop the last candidate TileSet, and use that TileSet‘s fallback() to create a different TileSet for examination. Save this TileSet in n.
    2. Any More Assignments? If the result of retry() is None, there are no more fallbacks; we can return False.
    3. Examine Tiles. Append the TileSet, n, returned by retry() to the candidate stack. Use the examine() function to examine the tiles of the hand, assigning tiles to TileSets. When this operation is complete, we may have a candidate assignment that will contain a number of TileSets, some of which are full, and some are incomplete.
  5. Winner? If we finish the loop normally, it means we have a candidate set assignment which has five full sets, one of which is a pair. For some hands, there can be multiple winners; however, we won’t continue the examination to locate additional winning assignments.

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.

  1. While More Tiles. If the tile stack in the Hand is empty, we are done, all tiles have been assigned to Sets.
    1. Next Tile. Pop the next unexamined tile from the tile stack, assigning it to the variable t.
    2. Topmost Set Full? If the topmost set on the set stack is full, push a new, empty FourSet onto the top of the set stack. (This is also a handy place to use a print statement to watch the progress of the evaluation.)
    3. Topmost Set Can Contain? If the top-most TileSet can contain Tile t, add this tile to the set. We’re done examining this tile.
    4. Topmost Set Can’t Contain. Put the tile t back into the stack of tiles to be examined. Use the retry() function to pop the TileSet from the stack, and fallback to another subclass of TileSet.
    5. Another Retry? If the result of the retry() is None, we’ve run out of alternatives, return from this function. Otherwise, append the new TileSet created by retry() to the stack of candidate sets.

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.

  1. Pop. Pop the top-most TileSet from the TileSet stack, assign it to s. Call s fallback() method to get a new top-most TileSet, assign this to n.
  2. Out Of Fallbacks? While the TileSet stack is not empty and n is None, there was no fallback.
    1. Pop Another. Pop the top-most set from the set stack, assign it to s. Call s fallback() method to get a new top-most TileSet, assign this to n.
  3. Done? If n is None and the set stack is empty, the hand is incomplete and we are out of fallback sets. Otherwise, append n to the stack of TileSets.

Some Test Cases

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.

Hand Scoring - Points

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.

  • A pair of dragons is worth 2 points.
  • A pair of winds associated with your seat at the table is worth 2 points.
  • A full game consists of four rounds. Each round has a prevailing wind. Within each round, each of the players will be the dealer. A pair of the round’s prevailing winds is worth 2 points.
  • A double wind pair occurs when your seat’s wind is also the prevailing wind. A pair of this wind is worth 4 points.

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.

class TileSet
points(self, prevailingWind, myWind) → integer

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.

class Hand
points(self, prevailingWind, myWind) → integer
Compute the total number of points for a hand.
pointReport(self, prevailingWind, myWind)
Print a small scorecard for the hand, showing each set and the points awarded.

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.

Hand Scoring - Doubles

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.

  1. If the hand is all terminals and honors (no simples), it is doubled.
  2. If each set in the hand has one terminal or an honor in it, the hand is doubled. A hand could have four SequenceSet s, each of which begins with one or ends with nine, and a pair of honors or terminals to qualify for this double.
  3. If the hand is all simples (no terminals or honors), it is doubled.
  4. If all of the SuitTiles are of the same suit, and all other tiles are HonorsTiles, this is doubled.
  5. If all of the SuitTiles are of the same suit, and there are no HonorsTiles, this is doubled four times (16?).
  6. If the hand contains TileSets of all three dragons, and one of those sets is a PairSet, this is called the Little Three Dragons, and the hand’s points are doubled.

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.

class TileSet
lucky(self, prevailingWind, myWind) → boolean

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.

triplet(self) → boolean

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.

sequenceSuit(self) → string

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.

sequenceRank(self) → int

Returns the lowest rank of a SequenceSet.

Other subclasses of of TileSet return None.

allSimple(self) → boolean
Returns True if the TileSet contains only simple tiles.
noSimple(self) → boolean

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.

oneTermHonor(self) → boolean
Returns True if the TileSet contains one terminal or honor. Since we only have a simple() function, this means there is one non-simple Tile in the TileSet.
suit(self) → string
If all tiles have the same value for Tile.getSuit(), return that value. If there is a mixture of suits, or suits of None, return None.
bigDragon(self) → boolean

For the ThreeSet class or FourSet class, return True if all tiles are Dragons.

Other subclasses of TileSet return False.

littleDragon(self) → boolean

For the PairSet class, return True if all tiles are Dragons.

Other subclasses of TileSet return False.

Update Hand Class. You’ll need to add the following functions to the Hand classes.

class Hand
luckySets(self, prevailingWind, myWind) → int
Returns the number of lucky sets. This function also checks for the double wind conditions where prevailWind is the same as myWind and one of the TileSets has this condition and throws an additional doubling in for this.
groups(self) → int
Returns 1 if all TileSets have the triple() property True.
sequences(self) → int
Returns 1 if three of the TileSets have the same value for sequenceSuit(), and the values for sequenceRank() are 1, 4, and 7.
noPoints(self) → int
Returns 1 all of the TileSets are worth zero points.
consistency(self) → int

Returns 1 or 4 after checking for the following conditions:

  • If allSimple() is true for all TileSets, return 1.
  • If noSimple() is true for all TileSets, return 1.
  • If oneTermHonor() is true for all TileSets, return 1.
  • If every TileSet has the same value for suit() and there is no TileSet where suit() is None, return 4.
  • If every TileSet has the same value for suit() or the value for suit() is None, return 1 .
  • If there are two TileSets for which bigDragon() is true, and one TileSet for which littleDragon() is true, return 1.

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.

class Hand
doubleReport(self)
Prints a small scorecard for the hand, showing each double that was awarded. You can then write a scoreCard() which produces the pointReport(), the doubleReport() and the final score of 2^{d} \times p, 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.

Limit Hands

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.

  • The Big Three Dragons hand has three TileSets for which the bigDragon() function is true.
  • The Little Four Winds hand has three ThreeSets or FourSetss for which the wind() function is true and a PairSet for which wind() is true.
  • The Big Four Winds hand has four ThreeSets or FourSets s for which the wind() function is true.
  • The All Honors hand has all TileSets composed of HonorsTiles; these will all have either wind() or dragon() true.
  • The All Terminals hand has all TileSets composed of TerminalSuitTiles.
  • An additional hand that pays the limit also breaks many of the rules for a winning hand. This is the Thirteen Orphans hand, which is one each of the various terminals and honors: three dragons, four winds, three one’s, three nine’s and any other of the thirteen terminal and honor tiles. This requires a special-case test in Hand that short-cuts all of the evaluation algorithm.

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.

Table Of Contents

Previous topic

What Can be Computed?

Next topic

Chess Game Notation

This Page