C# - OOP Tangram Shapes Game


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#)


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
VB.Net - Totris