Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game

EnigmaPuzzle

4.85/5 (52 votes)
17 Nov 2011CPOL14 min read 70.5K   3.8K  
Enigma Puzzle – a game as difficult as the Rubik's cube
Image 1

Introduction

Welcome to the Enigma Puzzle – a game as difficult as the Rubik's cube.

EnigmaPuzzle looks harmless but is very tricky. The playing area consists of two circular discs that are intersecting each other. On each disc, there are six stones alternating with six bones. The stones look like overweight triangles, the bones as malnourished rectangles.

Since the discs are intersecting, they share two stones and a bone. If a disc, let's say the upper one, is rotated by 60 degrees, then one stone and one bone that had previously also belonged to the lower disc are replaced by a new stone and new bone.

Game Concept

The game concept of Enigma is simple. The playing area consists, as mentioned, of two rotating discs, which partly overlap. The discs are surrounded by a frame that takes of the shape and color of the adjacent disc component.

The two discs are broken into small sections (stones and bones), which can be colored differently. The stones and bones are further subdivided. These parts can have different colors as well. The higher the level of the puzzle, the more colors are used.

By alternating rotations of the two discs by multiples of 60 degrees in random directions, the stones and bones mess up.

During a game, the computer rotates the two discs randomly to the left or to the right so that the parts come to lie at other positions. How often the computer should turn and if the rotations are displayed can be specified in the configuration. The goal of the game is of course to bring the discs back to the original positions by turning them. They can be turned by clicking on the arrow buttons or by mouse gestures.

There is a detailed manual (EnigmaPuzzle.pdf) in the download. The English version can be found in the \EnigmaPuzzle\en folder.

Background

This puzzle was invented by Douglas A. Engel and it consisted of two intersecting discs in a plastic holder. This is an adaptation for computers running a windows OS.

As I saw the puzzle for the first time several years ago in an issue of Spektrum der Wissenschaft (German version of Scientific American), I wanted to implement it for the computer. My first attempt with Turbo C failed because of the terrific performance. Now I've tried again and the program should run quickly on a not too old computer. It is the first game I wrote, since I usually develop (with C++) for the construction industry. The program was developed in Visual Studio 2010 with C# and uses .NET 4.0. Incidentally, it has no integrated solution algorithm (I don't know none).

There were a few problems that would be solved. First I had to be able to draw the board onto the screen. Therefore I tried it first on a piece of paper and used some fixed coordinates for the centers of the different circles. Then I had to do a lot of calculations of intersecting circles and lines, to determine the coordinates for one stone and one bone. I could then multiply these objects by means of rotation and translation, to fill the entire board. The frame was then only diligence work.

The next challenge was determining the positions of all the current stones and bones in order to determine whether the puzzle has been solved. The difficulty arose from the fact that the stones and bones can change the orientation when they migrate from the upper to the lower disc and vice versa. Further, there are often identical stones or bones which may be located on different positions. I solved the problem with a vector of the positions of the individual blocks on a disc and some complicated manipulations on the arrays when the discs are turned. The most complex part is the handling of the intersection area of the two discs. It would need a lot of words to explain that in detail. The best way to find out how it works is to debug the code and watch the arrays.

The Code

Most of the program is relatively simple and it consists of just a few classes. The most important are Block, Figure and Board.

The stones and bones are composed of different Block objects. On the board, there are arrays for all the Figure objects for the discs and the frame. The Board object also controls the whole game.

The program contains several comments, so it should be easy to understand the code.

The Class Block

The smallest unit on the board is a Block (e.g. the red area in the two pictures below). A block consists of a GraphicsPath for the shape, a color code for the fillcolor and a number for the framecolor. The Paint() method draws the block into the given Graphics object.

C#
public class Block
{
    public GraphicsPath GP { get; set; }
    public int Col { get; set; }
    public int Edge { get; set; }

    public Block()
    {
        GP = new GraphicsPath();
        Col = -1;
        Edge = -1;
    }

    public void Paint(Graphics g)
    {
        if (Col >= 0 && Col < m_colors.Count())
        {
            g.FillPath(m_colors[Col], GP);
        }
        if (Edge >= 0 && Edge < m_pens.Count())
        {
            g.DrawPath(m_pens[Edge], GP);
            g.DrawPath(m_pens[1], GP);
        }
    }
}

The module Blocks has a few static methods that create all the necessary Block objects and fill them with the original colors for a particular game level. The GraphicsPath of the blocks are built with the calculated coordinates and subsequently scaled by a transformation for the screen size. The blocks are always fixed on the board, only the colors of them are set for the current pattern.

Only the coordinates of the blocks of one stone and one bone are fixed in the source code. All the other blocks are created by skilled rotation and translation of the given one. This can be found in the static method Init(...) in the module Blocks.

The Cclass Figure

This class forms the stones and bones of the board. A stone is composed of three blocks that are rotated by 120 degrees around the center of the stone. The following image shows a stone, which are composed of a red, a yellow and a green block.

Stone

A bone consists of two blocks that are flipped by 180 degrees around the middle. The following illustration shows such a bone.

Bone

In addition to the blocks, a Figure object has also a counter for the orientation of the object. The counter indicates whether and how the Figure has been rotated relative to the original state. The value can be 0 (= original), 1 (rotated by 120 degrees) or 2 (rotated by 240 degrees). A change of orientation can occur when a Figure object is moved from the upper to the lower disc, or vice versa.

Such an object must, of course, be able to draw itself. This Is done by the Paint() method, which simply executes the Paint() method of all added Block objects. The current color of a block is controlled by the board.

The Class Board

This class is used to control the whole game. It provides members and methods that contain the current state of the board and make it possible that the two discs can be rotated. Each disc consists of twelve Figure objects (six stones and six bones).

If a game is active, ??the moves are stored as well, so the game can be saved and loaded again.

For not everything needs always to be redrawn, the three parts (upper disc, lower disc and the frame) are drawn into bitmaps. Only if something changes (e.g. turning a disc) this bitmap is created and drawn again. The frame must of course be created only once, because it always remains fixed.

Here an example of creating the bitmap for the upper disc. The method calls PaintDisc(...) which paints all the Figure objects of the disc using there Paint() method.

C#
private void CreateUpperdisc()
{
    // Create a bitmap
    if (m_upperdisc != null)
    {
        m_upperdisc.Dispose();
    }
    m_upperdisc = new Bitmap(m_w, m_h);
    Graphics g = Graphics.FromImage(m_upperdisc);
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

    // Paint the disc
    Paintdisc(g, eDisc.eUpperDisc);

    // Clean up
    g.Dispose();
}

In the class Board you will also find the methods that are needed for the rotation of the discs. There are methods that accomplish the graphical rotation (RotateDisc()) and it has a method that holds the logical state always up to date (Rotate()). This method was quite tricky, as all the possible transitions of the stones and bones have to be checked and handled.

An important method is GetColorString. This method provides a string with the color indices of all blocks on the board for the current state of the discs. This string can be compared with the color string in the original board. If both strings are equal, the puzzle is solved. This rather complicated procedure is necessary because there are always several solutions and it is not possible to rely only on the original and current position of the blocks.

C#
public string GetColorString()
{
    StringBuilder sb = new StringBuilder();

    // First the stones and bones of the upper disc
    for (int i = 0; i < 5; i++)
    {
        sb.Append(m_stones[m_upperStones[i]].GetColorString());
        sb.Append(m_bones[m_upperBones[i + 1]].GetColorString());
    }
    sb.Append(m_stones[m_upperStones[5]].GetColorString());
    sb.Append(m_bones[m_upperBones[0]].GetColorString());

    // .. and now the lower disc
    sb.Append(m_bones[m_lowerBones[1]].GetColorString());
    for (int i = 2; i < 6; i++)
    {
        sb.Append(m_stones[m_lowerStones[i]].GetColorString());
        sb.Append(m_bones[m_lowerBones[i]].GetColorString());
    }
           
    return sb.ToString();
}

The arrays "m_stones" and "m_bones" contain the Figure objects for the whole board. The arrays "m_upperStones" and "m_upperBones" contain the indices for the Figure object on a specific location in the upper disc. These arrays will also be used to paint the discs (see PaintDisc() method).

Using Blocks and Figures

First of all, the static method Init() in the module Blocks will create all the Block objects needed for the game. These Block objects will be held in a static array "m_blocks" and they are fixed until the level on the board changes.

The following code would create a stone by first creating a block and then rotating this block to get the other two blocks. The blocks 0, 1 and 2 in the array "m_blocks" build together the first stone.

C#
// Create the first sub-part of the first stone
m_blocks[0].GP.AddArc(new RectangleF(6.60254F, 20, 160, 160), 180, 21.31781F);
m_blocks[0].GP.AddArc(new RectangleF(-80, 70, 160, 160), 278.68219F, 21.31781F);
m_blocks[0].GP.AddLine(new PointF(40.00000F, 80.71797F), new PointF(28.86751F, 100));
m_blocks[0].GP.AddLine(new PointF(28.86751F, 100), new PointF(6.60254F, 100F));

Matrix mat120 = new Matrix();
mat120.RotateAt(120.0F, new PointF(28.86751F, 100));

// The second sub-part of the first stone (rotate the first by 120 degrees)
m_blocks[1].GP.AddPath(m_blocks[0].GP, false);
m_blocks[1].GP.Transform(mat120);

// The third sub-part of the first stone (rotate the second part by 120 degrees)
m_blocks[2].GP.AddPath(m_blocks[1].GP, false);
m_blocks[2].GP.Transform(mat120);

The first four lines would create the GraphicsPath for the first Block object in the "m_blocks" array consisting of two arcs (A and B) and two lines (C and D). This block would look like the following image if painted (without the letters of course).

Bone

To create the other two blocks of the first stone this base block is rotated by 120 degrees around the center of the stone which is where the two lines cross (point M in the image above).

In a similar way, the blocks for the first bone will be created and stored in "m_blocks" on positions 3 and 4.

C#
// The first sub-part of the first bone
m_blocks[3].GP.AddArc(new RectangleF(6.60254F, 20, 160, 160), 218.68218F, -17.36437F);
m_blocks[3].GP.AddArc(new RectangleF(-80, 70, 160, 160), 278.68219F, 21.31781F);
m_blocks[3].GP.AddLine(new PointF(40.00000F, 80.71797F), new PointF(46.60254F, 69.28203F));
m_blocks[3].GP.AddArc(new RectangleF(6.60254F, -80, 160, 160), 120, 21.31781F);

Matrix mat180 = new Matrix();
mat180.RotateAt(180.0F, new PointF(43.30127F, 75F));

// The second sub-part of the first stone (rotate the first part by 180 degrees)
m_blocks[4].GP.AddPath(m_blocks[3].GP, false);
m_blocks[4].GP.Transform(mat180);

These five basic blocks will then be copied six times and each time rotated by 60 degrees around the center of the upper disc (clockwise) to create all blocks for the upper disc. The blocks for the lower disc will be created by rotating the blocks of the upper disc anticlockwise around the center of the lower disc. After all that copying and rotating, there are some more blocks needed for the frame and then all objects for the board are ready. They would look like the following image:

Bone

In the class Board there are a few arrays that hold the 10 stones, the 11 bones and the 18 frame parts for the board.

C#
/// <summary>
/// Figures in the board (stones and bones)
/// </summary />
private Figure[] m_stones = new Figure[10];
private Figure[] m_bones = new Figure[11];
private Figure[] m_frames = new Figure[18];

In the method InitBoard() these arrays will be filled with the Figure objects so that they can be painted. The Figure objects are linked with the blocks for the initial positions. The arrays "m_stones" and "m_bones" will be used to create rotated images of the discs. They always represent the current graphical state of the game.

C#
public void InitBoard(int level)
{
    ...
    // Init the stones and bones
    m_stones = new Figure[10];
    m_bones = new Figure[11];
    m_frames = new Figure[18];

    // Build the blocks and color them
    Block.Init(level);

    // Build the stones and bones with the blocks
    m_bones[0] = new Figure();
    m_bones[0].AddBlock(28);
    m_bones[0].AddBlock(29);

    for (int i = 1; i < 6; i++)
    {
        m_bones[i] = new Figure();
        m_bones[i].AddBlock(5 * (i - 1) + 3);
        m_bones[i].AddBlock(5 * (i - 1) + 4);
    }
    for (int i = 0; i < 6; i++)
    {
        m_stones[i] = new Figure();
        m_stones[i].AddBlock(5 * i);
        m_stones[i].AddBlock(5 * i + 1);
        m_stones[i].AddBlock(5 * i + 2);
    }
    for (int i = 6; i < 11; i++)
    {
        m_bones[i] = new Figure();
        m_bones[i].AddBlock(5 * i);
        m_bones[i].AddBlock(5 * i + 1);

    }
    for (int i = 6; i < 10; i++)
    {
        m_stones[i] = new Figure();
        m_stones[i].AddBlock(5 * i + 2);
        m_stones[i].AddBlock(5 * i + 3);
        m_stones[i].AddBlock(5 * i + 4);
    }
    ...
}

The numbering in the arrays can be ignored and it could be any other order as well. But this way, the first bone in the array is the one just left above the intersection of the two circles and the other stones and bones in the arrays follow a line as if you would draw a number 8 over the board.

There are some more arrays which are filled in the InitBoard() method ("m_upperBones", "m_upperStones", "m_lowerBones", "m_lowerStones"). These arrays hold the current logical position of the stones and the bones and they are used to check if the puzzle has been solved.

A deeper look into the painting

With all these objects and arrays introduced in the last paragraph, the program can now paint the graphics and handle mouse gestures to turn the discs. In the OnPaint() method of the main form (EnigmaPuzzleDlg), only the prepared Bitmap objects ("m_b.Background", "m_b.LowerDisk" and "m_b.upperDisk") will be painted on the screen. To see the correct animation of disc turning it is important which disc is painted last. Always the last moved disc will be painted last.

C#
protected override void OnPaint(PaintEventArgs e)
{
    ...
    e.Graphics.DrawImageUnscaled(m_b.Background, 0, 0);
    if (m_b.RotDisk == Board.eDisc.eUpperDisc)
    {
        e.Graphics.DrawImageUnscaled(m_b.LowerDisk, 0, 0);
        e.Graphics.DrawImageUnscaled(m_b.UpperDisk, 0, 0);
    }
    else
    {
        e.Graphics.DrawImageUnscaled(m_b.UpperDisk, 0, 0);
        e.Graphics.DrawImageUnscaled(m_b.LowerDisk, 0, 0);
    }
}

But before the Bitmaps may be used, they have to be created. That is done in the methods CreateBackground(), CreateUppderDisk() and CreateLowerDisk(). The first call of these functions will normally come from the OnResize() method because the program will always fill the whole screen. After that, the calls to renew the Bitmaps will be initiated by the rotation functions RotateDisk() and by the Rotate() method which handles all rotations - graphically and logically.

C#
public bool Rotate(eDisc disc, eDirection dir, bool bShow, EnigmaPuzzleDlg form)
{
    ...
    
    // Graphically rotation
    RotateDisk(60.0f, disc, dir, bShow, form);

    // Logically rotation - adjust the stones and bones on the disks and take incount
    // that there are two stone and one bone that overlap
    ...
    
    // Create the bitmaps newly and show them 
    if (bShow)
    {
        CreateUpperDisk();
        CreateLowerDisk();
        form.Refresh();
    }

    // Check if the game has been solved
    if (GameActive)
    {
        ...
    }

    return false;
}

Whenever the user starts a game or when a disc should be turned as a reaction of an input, the Rotate() method will be called. This method then calls the RotateDisk() method which calls another, more complex RotateDisk() method to show the animation of the turning disk. These two functions look quite complex, but that's only because the handling of the animation of a swinging disc. It could be done as well without the second RotateDisk() function.

A big part of Rotate() is the handling of the logical state. That is not so complex as it looks like but all the possible rotations has to be handled. At the end of Rotate(), the bitmaps may be created newly and there is a check if the puzzle has been solved with the last turn.

Handling Mouse Gestures

The discs may be turned by clicking on one of the four little buttons with the arrows on it. But it is also possible to do it by a mouse gesture. The MouseDown and MouseUp event of the main form are used to get the coordinates of two points. The following picture shows two possible gestures - one from A to B and one from C to D. AB should lead to clockwise turn of the upper disc and CD should the same with the lower disc.

Gestures

The function TurnDisk() of the class Board handles the coordinates of two given points "(x1,y1)" and "(x2,y2)" and determines the disc and the direction of the rotation. In the picture above, there are for example the points A and B and they may have the coordinates (ax,ay) and (bx,by).

First in TurnDisk() there is check for which disk should be turned. That's done in a very simple way. Every move that starts above the middle line of the board (M on the picture above) belongs to the upper disc and everything below that line to the lower disc. The vertical starting point is "y1". The code for this can be found on the first few lines of TurnDisc(). The disc which will be turned defines also the vertical center for the turn "cy".

After that, there are some shifts of the coordinates for the two given points. The coordinates are moved in such a way that the center of the disc which has to be turned is at the coordinates (0,0). Here the variable "cy" is used to calculate the vertical shift. In the same operation, the screen coordinates (top-down) are converted to real coordinates (bottom-up).

The next step is to check if the move was long enough. A simple click (mouse down - mouse up) or a very short drag should not turn a disc. The length of the move can be calculated as the length of the vector between the two points "(x1,y1)" and "(x2,y2)". A minimum length of 20 pixels is expected.

C#
public bool TurnDisk(float x1, float y1, float x2, float y2, EnigmaPuzzleDlg form)
{
    eDisc disc = eDisc.eUpperDisc;
    eDirection dir = eDirection.eLeft;
    float cy;

    // Determine the disk - just look at the hor. middle of the board
    if (y1 < MiddleY)
    {
        disc = eDisc.eUpperDisc;
        cy = m_upperCenter.Y;
    }
    else
    {
        disc = eDisc.eLowerDisc;
        cy = m_lowerCenter.Y;
    }
                       
    // Move the coordinates so that the y-axle is in the middle of the board
    x1 -= MiddleX;
    x2 -= MiddleX;

    // Because 0/0 is upper left corner, we have to inverse the y-coordinate
    y1 = -(y1 - cy);
    y2 = -(y2 - cy);

    // If the drag is too short (length of the turning vector) - do nothing
    // Get the turning vector
    float vx = x2 - x1;
    float vy = y2 - y1;
    if (Math.Sqrt(vx * vx + vy * vy) < 20)
    {
        return false;
    }

    // Calc vector product to get the orientation
    double orient = x1 * y2 - y1 * x2;
    if (orient > 0)
    {
        dir = eDirection.eLeft;
    }
    else
    {
        dir = eDirection.eRight;
    }

    // Do the rotations
    Rotate(disc, dir, true, form);

    return true;
}

The last action in TurnDisk() is to determine the direction of the turn. There is a very simple way to do that. What is needed is the vector product of the two vectors "(x1,y1)" and "(x2,y2)". If this value is greater than zero, the disc has to be turned anticlockwise else clockwise.

Points of Interest

For me, almost everything was interesting, since I've never worked with GraphicsPath, Bitmap and the transformation with the Matrix class. Since I've never done game programming before, I'm sure there are a lot of things that could be done better or easier.

Multiple Languages

The program is implemented for two languages?? - German and English. In the Visual Studio solution, the forms have German as the default language. At runtime, the culture settings on the computer will be checked. If it has anything to do with "German" (CH, DE, AUT), then the program starts with a German user interface, if not, then it starts with an English one.

Setup

The setup program has been created with Inno-Setup compiler. The source file for the setup program can be found on the toplevel folder (EnigmaPuzzle.iss).

History

  • Version 1.0 - 11.11.2011
  • Version 1.1 - 17.11.2011 - Added setting to change from fullscreen to sizable window

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)