Overview


"The tangram (Chinese: 七巧板; pinyin: qīqiǎobǎn; literally: "seven boards of skill") is a dissection puzzle consisting of seven flat shapes, called tans, which are put together to form shapes." (Wikipedia: https://en.wikipedia.org/wiki/Tangram)

In this version of the game, there are the regular seven 'tans' or pieces, which are comprised of three different sized right-angled triangles, a square, and a parallelogram. There are five different 'target' shapes which are drawn in outline on the form as guidance. You can drag the shapes to a new location, and by double-clicking, rotate the shape 45 degrees clockwise. Conversely, double-clicking while holding down a Shift button on your keyboard will rotate the selected shape 45 degrees anti-clockwise. The 'target' shapes are selected via a menu strip. There is no testing to see if you've won, except your own judgement. Simply put, if you can arrange the shapes (in any order, and with any rotation) within the 'target' shape outline, you've won.

This is a fairly simple application that uses OOP techniques, GDI+ and Control Regions to create an enjoyable, usable and quite challenging game, which requires skills and shapes perception that feature in MENSA tests.







The Piece Class



using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
namespace tanGram_shape_game_cs
{
    class piece : PictureBox
    {
 
        /// <summary>
        /// API function and Constants used to detect Shift keydown
        /// </summary>
        /// <param name="vkey"></param>
        /// <returns></returns>
        [System.Runtime.InteropServices.DllImport("user32", EntryPoint = "GetAsyncKeyState", ExactSpelling = true, CharSet = System.Runtime.InteropServices.CharSet.Ansi, SetLastError = true)]
        private static extern short GetAsyncKeyState(int vkey);
        private const int VK_LSHIFT = 0xA0;
        private const int VK_RSHIFT = 0xA1;
 
        private byte[] ppts = new byte[] { Convert.ToByte(PathPointType.Line), Convert.ToByte(PathPointType.Line), Convert.ToByte(PathPointType.Line), Convert.ToByte(PathPointType.Line), Convert.ToByte(PathPointType.Line) };
        private string[] directions = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
 
        /// <summary>
        /// Points used for resetting shape to initial shape
        /// </summary>
        private static Point ptA = new Point(25, 75);
        private static Point ptB = new Point(225, 75);
        private static Point ptC = new Point(225, 275);
        private static Point ptD = new Point(25, 275);
        private static Point ptE = new Point(75, 225);
        private static Point ptF = new Point(125, 175);
        private static Point ptG = new Point(175, 125);
        private static Point ptH = new Point(125, 275);
        private static Point ptI = new Point(175, 225);
        private static Point ptJ = new Point(225, 175);
 
        private string[] pt = { "E", "S", "", "W", "", "N", "SE" };
        private List<Point[]> rp = new List<Point[]>()
                                {
                                    new Point[] {ptA, ptF, ptD, ptA},
                                    new Point[] {ptB, ptF, ptA, ptB},
                                    new Point[] {ptB, ptJ, ptI, ptG, ptB},
                                    new Point[] {ptI, ptF, ptG, ptI},
                                    new Point[] {ptF, ptI, ptH, ptE, ptF},
                                    new Point[] {ptD, ptE, ptH, ptD},
                                    new Point[] {ptJ, ptC, ptH, ptJ}
                                };
 
 
        /// <summary>
        /// Current region points
        /// </summary>
        private Point[] _piecePoints;
        private Point[] piecePoints
        {
            get
            {
                return _piecePoints;
            }
            set
            {
                _piecePoints = value;
            }
        }
 
        /// <summary>
        /// Used to detect doubleclicks
        /// </summary>
        private int _lastClicked;
        private int lastClicked
        {
            get
            {
                return _lastClicked;
            }
            set
            {
                _lastClicked = value;
            }
        }
 
        /// <summary>
        /// Text label drawn on shape
        /// </summary>
        private int _pieceNumber;
        private int pieceNumber
        {
            get
            {
                return _pieceNumber;
            }
            set
            {
                _pieceNumber = value;
            }
        }
 
        /// <summary>
        /// Orientation of shape
        /// </summary>
        private string _pointsTo;
        private string PointsTo
        {
            get
            {
                return _pointsTo;
            }
            set
            {
                _pointsTo = value;
            }
        }
 
        /// <summary>
        /// Resets game
        /// </summary>
        /// <param name="pn"></param>
        public void initialize(int pn)
        {
            this.lastClicked = Environment.TickCount;
            this.Location = new Point(25, 75);
            this.PointsTo = pt[pn - 1];
            this.pieceNumber = pn;
            this.piecePoints = Array.ConvertAll(rp[pn - 1], (p) => new Point(p.X - 25, p.Y - 75));
            this.reShape();
        }
 
        /// <summary>
        /// Reshapes piece and resets initial colour
        /// </summary>
        public void reShape()
        {
            this.DoubleBuffered = true;
            this.Region = new Region(new GraphicsPath(piecePoints, ppts.Take(piecePoints.Count()).ToArray()));
            this.BackColor = Color.SteelBlue;
            this.Invalidate();
        }
 
        /// <summary>
        /// Paints piece border and text label
        /// </summary>
        /// <param name="pe"></param>
        protected override void OnPaint(System.Windows.Forms.PaintEventArgs pe)
        {
 
            if (DesignMode)
            {
                return;
            }
 
            pe.Graphics.DrawPolygon(new Pen(((this.BackColor == Color.SteelBlue) ? Color.White : Color.Black), 3), this.piecePoints);
 
            SizeF textSize = pe.Graphics.MeasureString(this.pieceNumber.ToString(), this.Parent.Font);
            if (this.pieceNumber == 3 || this.pieceNumber == 5)
            {
                Point p = measurement.findCentre(this.piecePoints, textSize);
 
                int minX = measurement.findOffsetX(this.piecePoints);
                int minY = measurement.findOffsetY(this.piecePoints);
 
                pe.Graphics.DrawString(this.pieceNumber.ToString(), this.Parent.Font, ((this.BackColor == Color.SteelBlue) ? Brushes.White : Brushes.Black), minX + p.X, minY + p.Y);
            }
            else
            {
                Point p = measurement.findTriangleOffset(this.PointsTo, this.piecePoints);
                p.X -= Convert.ToInt32(textSize.Width / 2.0);
                p.Y -= Convert.ToInt32(textSize.Height / 2.0);
 
                pe.Graphics.DrawString(this.pieceNumber.ToString(), this.Parent.Font, ((this.BackColor == Color.SteelBlue) ? Brushes.White : Brushes.Black), p.X, p.Y);
 
            }
 
            base.OnPaint(pe);
        }
 
        private const int HT_CAPTION = 0x2;
        private const int WM_NCLBUTTONDOWN = 0xA1;
 
        /// <summary>
        /// Handles rotation (doubleclicks) and dragging
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseDown(System.Windows.Forms.MouseEventArgs e)
        {
            if (Environment.TickCount - this.lastClicked < 250)
            {
                this.lastClicked = Environment.TickCount;
                Point l = this.Location;
 
                int minX = measurement.findOffsetX(this.piecePoints);
                int minY = measurement.findOffsetY(this.piecePoints);
                l.Offset(new Point(minX, minY));
 
                bool shifting = GetAsyncKeyState(VK_LSHIFT) < 0 || GetAsyncKeyState(VK_RSHIFT) < 0;
 
                this.piecePoints = rotation.RotateAll(this.piecePoints, shifting);
                minX = measurement.findOffsetX(this.piecePoints);
                minY = measurement.findOffsetY(this.piecePoints);
 
                this.piecePoints = Array.ConvertAll(this.piecePoints, (p) => new Point(p.X + -minX, p.Y + -minY));
                this.Region = new Region(new GraphicsPath(piecePoints, ppts.Take(this.piecePoints.Count()).ToArray()));
 
                this.Location = l;
 
                int index = Array.IndexOf(directions, this.PointsTo);
                if (shifting)
                {
                    if (index <= 0)
                    {
                        this.PointsTo = directions.Last();
                    }
                    else
                    {
                        this.PointsTo = directions[index - 1];
                    }
                }
                else
                {
                    this.PointsTo = directions[(index + 1) % directions.Length];
                }
 
                this.Invalidate();
 
            }
            else
            {
                this.lastClicked = Environment.TickCount;
            }
 
            foreach (PictureBox box in this.FindForm().Controls.OfType<piece>())
            {
                if (!(box == this))
                {
                    box.BackColor = Color.SteelBlue;
                }
                box.Invalidate();
            }
 
            this.BackColor = Color.FromArgb(192, 255, 192);
 
            if (e.Button == MouseButtons.Left)
            {
                foreach (PictureBox box in this.FindForm().Controls.OfType<piece>())
                {
                    box.SuspendLayout();
                }
                this.BringToFront();
                this.Capture = false;
                Message m = Message.Create(this.Handle, WM_NCLBUTTONDOWN, (IntPtr)HT_CAPTION, IntPtr.Zero);
                base.WndProc(ref m);
 
                foreach (PictureBox box in this.FindForm().Controls.OfType<piece>())
                {
                    box.ResumeLayout();
                }
            }
 
            base.OnMouseDown(e);
        }
 
 
    }
}




Conclusion


This is another example that shows C# and GDI+ are a good choice for technologies when writing this sort of desktop game...



Other Resources


VB.Net TechNet version
Download here (VB.NET and C#)




Articles related to game programming


VB.Net - WordSearch
VB.Net - Vertex
VB.Net - Perspective
VB.Net - MasterMind
VB.Net - OOP BlackJack
VB.Net - Numbers Game
VB.Net - HangMan
Console BlackJack - VB.Net | C#
TicTacToe - VB.Net | C#
OOP Sudoku - VB.Net | C#
OctoWords VB.Net | C#
OOP Buttons Guessing Game VB.Net | C#
VB.Net - Three-card Monte
VB.Net - Split Decisions
VB.Net - Pascal's Pyramid
VB.Net - Random Maze Games
(Office) Wordsearch Creator
VB.Net - Event Driven Programming - LockWords Game
C# - Crack the Lock