12.7. Mahjong Tiles: Outlining A Polymorphic Solution

polymorphism, Mahjong, tiles
Time: 00:05:23 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides: PDF, PPTX
Review

The difficulty of establishing and using polymorphism is on par with other C++ and object-oriented concepts. However, polymorphism is rarely beneficial in small or simple programs, making it challenging to demonstrate its value in an introductory programming text. We can better appreciate its benefits by describing an authentic problem and its solution. To accomplish this, the tile example excerpts and simplifies a small part of a Mahjong solitaire program.

"Mahjong is a tile-based game that was developed in the 19th century in China and has spread throughout the world." Originally a three- or four-person game, it has become a popular computer solitaire game with numerous variations (see, https://www.free-play-mahjong.com for one example). The following UML class diagram, code fragments, and descriptions are adapted and simplified from a Mahjong solitaire program.

The Mahjong Tile Classes

polymorphism, UML, class diagram, Mahjong, tiles

Mahjong consists of 144 tile objects saved in a custom data structure reflecting each tile's position on a three-dimensional game board. The implementation described here instantiates the tiles from ten related classes representing seven different kinds of tiles - one class represents three kinds of tiles, and three classes support inheritance and polymorphism, but the program never instantiates them. However, each tile's position in the data structure and on the game board is independent of their type. Therefore, it's easiest to upcast and represent them collectively in the data structure and program functions as Tile pointers.

In this simplified example, polymorphism enables two essential operations. First, the program must draw each tile separately. The Tile draw function draws a blank tile, and the subclass draw functions add tile-specific detail, making the relative execution order of these functions significant. Second, to play the game, the player selects the tiles two at a time, and if they "match," the program removes them from the board. What it means to "match" depends on the tile's type and, sometimes, on saved member data. Polymorphism allows the program to test for a match with a single, simple function call. We can solve both problems without polymorphism, but solutions based on branching statements (such as switches or if-else ladders) are larger, more cumbersome, and more complex than polymorphic solutions.

A UML class diagram illustrating the inheritance relationships connecting the Mahjong tile classes.
Mahjong tiles UML class diagram. Each class in the tile inheritance hierarchy has numerous responsibilities, but this simplified example focuses only on the polymorphic drawing and matching operations. Click on a class symbol to jump to its corresponding code outline.
The Tile class The RankTile Class The CharacterTile class The WhiteDragonTile The PictureTile class The CircleTile class The BambooTile class The SeasonTile class The FlowerTile class The Bamboo1Tile class

Partial MahJong Tile Implementations

polymorphism, Mahjong, tiles, matches, draw, typeid, downcast

The draw functions use the host operating system's or implementing language's native drawing functions, making the functions non-portable and placing them beyond the scope of an introductory text. Nevertheless, even the partial implementations presented here authentically illustrate numerous polymorphic concepts introduced in the current chapter. The tile classes outlined here include abridged draw functions to demonstrate C++'s polymorphic syntax and function-calling sequences. Conversely, the matches functions are complete.

Tile ClassUsing The Tile Class
class Tile
{
    public:
        virtual bool matches(Tile* t)
        {
            if (this == t)			// (a)
                return false;

            if (t == nullptr)			// (b)
                return false;

            return typeid(*this) == typeid(*t);	// (c)
        }


        virtual void draw()
        {
            // draw a blank tile
        }
};
Tile* t1 = first selected tile;
Tile* t2 = second selected tile;

if (t1->matches(t2))
{
    remove(t1);
    remove(t2);
}
void draw_board()
{
    for (Tile t : board)
        t->draw();
}
The Tile class. During gameplay, the player selects two tiles, and the program removes them from the game board if they "match." The Tile class matches function performs three tile comparison operations. The function arranges the comparison sequence in a case analysis pattern: Comparison (a) executes quickly and prevents a tile from matching itself. Comparison (b) prevents null pointer runtime errors and protects comparison (c). The WhiteDragonTile and the PictureTile and its subclasses, directly inherit the Tile matches function; the other classes override but chain to it.
  1. The program removes two matching tiles, implying that a valid match requires two distinct tiles. This test rejects matching a tile with itself: t1->matches(t1), preventing (c) from returning a false match.
  2. No tile matches the nullptr. The test also prevents a runtime error in (c) if the program inadvertently passes a nullptr to typeid.
  3. typeid is a function returning an object describing a data type, including structures and classes. If it returns the same object, the two tiles are instances of the same class. Please note that the function dereferences both pointers before calling typeid and that == forms a Boolean-valued expression, matching the function's return type.

 

class RankTile : public Tile
{
    private:
        int rank;

    public:
        RankTile(int r) : rank(r) {}

        virtual bool matches(Tile* t)
        {
            return Tile::matches(t) && rank == ((RankTile *)t)->rank;
        }
};
The RankTile class. The Mahjong program does not instantiate the RankTile class, but does instantiate its subclasses: CircleTime and BambooTile. The subclasses decorate their faces with varying numbers of circles or bamboo sticks, with the number of decorations corresponding to their rank. Both classes inherit the RankTile matches function without overriding it.

Breaking the matches function into sub-expressions makes it easier to understand. For brevity, ellipses replace previously evaluated sub-expressions.

 

class CircleTile : public RankTile
{
    public:
        CircleTile(int rank) : RankTile(rank) {}

        virtual void draw()
        {
            Tile::draw();	// draw a blank tile
            // draw "rank" circles on the tile
        }
};
class BambooTile : public RankTile
{
    public:
        BambooTile(int rank) : RankTile(rank) {}

        virtual void draw()
        {
            Tile::draw();	// draw a blank tile
            // draw "rank" bamboo sticks on the tile
        }
};
The CircleTile and BambooTile classes. The CircleTile and RankTile classes share several features. Their constructors chain to the RankTile constructor, passing the tile's rank (the number of circles or bamboo sticks on the tile) as an argument, and both inherit and use the RankTile matches function. Furthermore, both classes inherit but override the Tile draw function, demonstrating that inheritance can skip one or more "generations." The overloaded draw functions call the Tile draw function to draw a blank tile before drawing the decorations on it.

 

class CharacterTile : public Tile
{
    private:
        char symbol;

    public:
        CharacterTile(char c) : symbol(c) {}

        virtual bool matches(Tile* t)
        {
            return Tile::matches(t) && symbol == ((CharacterTile *)t)->symbol;
        }

        virtual void draw()
        {
            Tile::draw();	// draw a blank tile
            // draw "symbol" on the tile
        }
};
The CharacterTile class. The CharacterTile class represents three kinds of tiles: character, wind, and two dragons (red and green). The draw function chains to the Tile draw to draw a blank tile before adding the Chinese characters with a wide (16-bit) Unicode character. The White Dragon is a special case drawn graphically. The CharacterTile matches function is similar to the RankTile function, allowing us to decompose it into sub-expressions in the same way, replacing the sub-expressions with ellipses as the description progresses.

 

class WhiteDragonTile : public Tile
{
    public:
        virtual void draw()
        {
            Tile::draw();	// draw a blank tile
            // White dragon decoration on the tile
        }
};
The WhiteDragon class. The WhiteDragon is a special case not represented by a more general class. It inherits and uses the Tile matches function but has a unique and specialized draw function. Its draw function chains to the Tile draw function, painting a blank tile before drawing the WhiteDragon's blue frame.

 

class PictureTile : public Tile
{
    private:
        string name;
        Image picture;

    public:
        PictureTile(string s) : name(s)
        {
            picture = load(name);
        }

        virtual void draw()
        {
            Tile::draw();	// draw a blank tile
            // draw "picture" on the tile
        }
};
 
 
class SeasonTile : public PictureTile
{
    public:
        SeasonTile(string s) : PictureTile(s) {}
};
class FlowerTile : public PictureTile
{
    public:
        FlowerTile(string s) : PictureTile(s) {}
};
class Bamboo1Tile : public PictureTile
{
    public:
        Bamboo1Tile() : PictureTile("Sparrow") {}
};
The PictureTile classes. The Mahjong program doesn't instantiate the PictureTile class; instead, it uses it to generalize tiles decorated with a digital image. The PictureTile constructor loads the image from a file, and the draw function paints the image. Consolidating these operations in a superclass eliminates duplicate code and the image variable in the subclasses.

Given that the subclasses (SeasonTile, FlowerTile, and BambooTile) inherit the draw function from PictureTile and the matches function from Tile, why are they necessary? The Mahjong rules state that any two season tiles match, and the program can remove them in pairs. The rule is the same for flower and bamboo tiles. However, a season and a flower tile do not match; only tiles of the same class match. The Tile matches function enforces this rule with the expression typeid(*this) == typeid(*t).

Downloadable Mahjong Tile Classes

polymorphism, tiles.h

tiles.cpp includes a main function, making the example compilable and runnable. It demonstrates the C++ syntax for creating various tile object, upcasting them to Tile pointers, and calling the virtual functions.

ViewDownloadComments
tiles.cpp tiles.cpp Abridged tile classes demonstrating the constructors, matches, and draw functions implementing polymorphism.