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.
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.
A bone consists of two blocks that are flipped by 180 degrees around the middle. The following illustration shows such a 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.
private void CreateUpperdisc()
{
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;
Paintdisc(g, eDisc.eUpperDisc);
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 string
s 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.
public string GetColorString()
{
StringBuilder sb = new StringBuilder();
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());
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.
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));
m_blocks[1].GP.AddPath(m_blocks[0].GP, false);
m_blocks[1].GP.Transform(mat120);
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).
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.
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));
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:
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.
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.
public void InitBoard(int level)
{
...
m_stones = new Figure[10];
m_bones = new Figure[11];
m_frames = new Figure[18];
Block.Init(level);
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.
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.
public bool Rotate(eDisc disc, eDirection dir, bool bShow, EnigmaPuzzleDlg form)
{
...
RotateDisk(60.0f, disc, dir, bShow, form);
...
if (bShow)
{
CreateUpperDisk();
CreateLowerDisk();
form.Refresh();
}
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.
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.
public bool TurnDisk(float x1, float y1, float x2, float y2, EnigmaPuzzleDlg form)
{
eDisc disc = eDisc.eUpperDisc;
eDirection dir = eDirection.eLeft;
float cy;
if (y1 < MiddleY)
{
disc = eDisc.eUpperDisc;
cy = m_upperCenter.Y;
}
else
{
disc = eDisc.eLowerDisc;
cy = m_lowerCenter.Y;
}
x1 -= MiddleX;
x2 -= MiddleX;
y1 = -(y1 - cy);
y2 = -(y2 - cy);
float vx = x2 - x1;
float vy = y2 - y1;
if (Math.Sqrt(vx * vx + vy * vy) < 20)
{
return false;
}
double orient = x1 * y2 - y1 * x2;
if (orient > 0)
{
dir = eDirection.eLeft;
}
else
{
dir = eDirection.eRight;
}
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