12.7. Mahjong Tiles: Outlining A Polymorphic Solution

Time: 00:05:23 | Download: Large, Large (CC), Small | Streaming, Streaming (CC) | Slides (PDF)
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 tiles 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

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 makes two essential operations possible. 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 their relative execution order 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 (like switches or if-else ladders) are larger and more cumbersome and 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

Mahjong Tile Classes

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. The following class outlines include the draw functions to illustrate their syntax and the sequence of their internal operations but otherwise focus on the more easily demonstrated matches functions.

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
        }
};
The Tile class. The Tile class matches function, inherited by WhiteDragonTile and PictureTile and its subclasses, performs three tile comparison operations. During gameplay, the player selects two tiles, and the program removes them from the game board if they "match."
Tile* t1 = first selected tile;
Tile* t2 = second selected tile;

if (t1->matches(t2))
{
    remove(t1);
    remove(t2);
}
  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 run-time 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, only its subclasses. RankTile generalizes tiles having a specified number of circles or bamboo sticks on their face. The circles and sticks are similar to the diamonds or clubs on a poker card or the spots on a domino.

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 "rank" circles on the tile
        }
};
class BambooTile : public RankTile
{
    public:
        BambooTile(int rank) : RankTile(rank) {}

        virtual void draw()
        {
            Tile::draw();
            // 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) to it, and both inherit and use the RankTile matches function. Furthermore, both classes define a draw function that chains to the Tile function before drawing rank number of circles or bamboo sticks on the tile's face.
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 "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();
            // 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 to paint 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 "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 PictureTile and its subclasses inherit and use the Tile class matches function. The PictureTile constructor's parameter is the file name where the program loads the picture painted on the tile. The name Image is a placeholder for a system-dependent type storing an image format (.jpg or .png). The draw function chains to the File draw function to create a blank tile before painting the image on it. The subclasses pass the file name to the PictureTile constructor but inherit and use its draw function.

Downloadable Tiles Classes

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.

ViewDownload
tiles.cpp tiles.cpp