C# - OOP Buttons Guessing Game
Overview
This is a graphical guesstimate game. Each new game a random number of yellow 'Buttons' are drawn on the game grid. The red vertical partition line is also drawn on a random vertical grid line. This partition line isn't moveable. Mouse movement over the blue selection triangles (to the right of the grid) causes a black horizontal partion line to be drawn on the selected horizontal grid line. When mouse hovering over one of the rightmost triangles, some of the yellow 'Buttons' will become black 'Buttons' and some will become red 'Buttons'. Clicking on one of the rightmost triangles selects the current amounts of red 'Buttons' and black 'Buttons'. The aim of the game is to estimate which of the triangles you must click on to select more black 'Buttons' than red 'Buttons'. The real skill in the game is not only to win, but also to win by the narrowest margin possible...
This is an application developed using standard OOP techniques, namely encapsulation and custom events. The game core is entirely encapsulated within an extended Panel, where the GDI+ graphical elements are rendered on to the panel. Standard Panel events are used to detect mouse movement and clicks. Two custom events send feedback to the Form when the game is ended, one to enable the New Game button, and the other to provide the result String which is then displayed in a Label.
The GamePanel 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 Buttons_cs
{
class GamePanel : Panel
{
//custom events
public event EventHandler enableNewButton;
public event EventHandler<String> showScoreLine;
//graphical elements, stored as GraphicsPaths
//for use with mouse movement And clicks
private List<GraphicsPath> topTriangles = new List<GraphicsPath>();
private List<GraphicsPath> rightTriangles = new List<GraphicsPath>();
//game play variables
private int topIndex = -1;
private int rightIndex = -1;
private int oldRightIndex = -1;
//used for storing locations of random graphical buttons
private List<Point> buttons = new List<Point>();
private Random r = new Random();
private bool locked = false;
public GamePanel(){
this.Size = new Size(570, 570);
this.DoubleBuffered = true;
newGame();
}
/// <summary>
/// Randomly places a random amount of buttons in random positions
/// Randomly positions the vertical divider line
/// </summary>
public void newGame()
{
rightIndex = -1;
locked = false;
buttons = new List<Point>();
int n = r.Next(500, 1001);
for (int c = 1; c <= n; c++)
{
int x = r.Next(0, 37);
int y = r.Next(0, 37);
if (!buttons.Contains(new Point(x, y)))
{
buttons.Add(new Point(x, y));
}
else
{
c -= 1;
}
}
int[] buttonCounts = Enumerable.Range(15, 7).Select((x) => buttons.Where((p) => p.X < x && p.Y < 17).Count() + buttons.Where((p) => p.X > x && p.Y > 17).Count()).ToArray();
topIndex = 15 + Array.IndexOf(buttonCounts, buttonCounts.Max());
this.Invalidate();
}
/// <summary>
/// Detects clicking on the triangles to the right of the grid
/// Locks the game and calls the Paint event.
/// </summary>
/// <param name="e"></param>
protected override void OnMouseDown(System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
if (rightTriangles.FindIndex((gp) => gp.IsVisible(e.X, e.Y)) != -1)
{
locked = true;
enableNewButton?.Invoke(this, new EventArgs());
this.Cursor = Cursors.Default;
this.Invalidate();
}
}
base.OnMouseDown(e);
}
/// <summary>
/// Detects movement on the triangles to the right of the grid
/// causing different amounts of buttons to be rendered red and black
/// depending on their position relative to the partition lines
/// </summary>
/// <param name="e"></param>
protected override void OnMouseMove(System.Windows.Forms.MouseEventArgs e)
{
//topIndex = topTriangles.FindIndex(Function(gp) gp.IsVisible(e.X, e.Y))
if (!locked)
{
rightIndex = rightTriangles.FindIndex((gp) => gp.IsVisible(e.X, e.Y));
if (rightIndex != -1)
{
this.Cursor = Cursors.Hand;
}
else
{
this.Cursor = Cursors.Default;
}
if (rightIndex != oldRightIndex)
{
this.Invalidate();
}
}
base.OnMouseMove(e);
}
/// <summary>
/// The paint event dynamically renders the panel
/// drawing both the fixed elements and the dynamic user defined parts
/// </summary>
/// <param name="e"></param>
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
//the grid is drawn in these two for, next loops
for (int x = 15; x <= 555; x += 15)
{
e.Graphics.DrawLine(Pens.LightGray, x, 15, x, 555);
}
for (int y = 15; y <= 555; y += 15)
{
e.Graphics.DrawLine(Pens.LightGray, 15, y, 555, y);
}
//the buttons are drawn on the grid in one of the three colours
//and documented as they are drawn
string[] buttonCounts = new string[buttons.Count];
for (int x = 0; x < buttons.Count; x++)
{
e.Graphics.FillEllipse(((rightIndex == -1) ? Brushes.Yellow : (((buttons[x].X < (topIndex + 1) && buttons[x].Y < (rightIndex + 1)) || (buttons[x].X > (topIndex + 1) && buttons[x].Y > (rightIndex + 1))) ? Brushes.Red : (((buttons[x].X > (topIndex + 1) && buttons[x].Y < (rightIndex + 1)) || (buttons[x].X < (topIndex + 1) && buttons[x].Y > (rightIndex + 1))) ? Brushes.Black : Brushes.Yellow))), new Rectangle(((buttons[x].X + 1) * 15) - 5, ((buttons[x].Y + 1) * 15) - 5, 10, 10));
buttonCounts[x] = (rightIndex == -1) ? "g" : (((buttons[x].X < (topIndex + 1) && buttons[x].Y < (rightIndex + 1)) || (buttons[x].X > (topIndex + 1) && buttons[x].Y > (rightIndex + 1))) ? "r" : (((buttons[x].X > (topIndex + 1) && buttons[x].Y < (rightIndex + 1)) || (buttons[x].X < (topIndex + 1) && buttons[x].Y > (rightIndex + 1))) ? "b" : "g"));
}
//these two for, next loops draws the triangles above and to the right of the grid
Point[] triangle1 =
{
new Point(0, 0),
new Point(15, 0),
new Point(8, 10),
new Point(0, 0)
};
for (int x = 30; x <= 540; x += 15)
{
e.Graphics.FillPolygon(Brushes.SteelBlue, Array.ConvertAll(triangle1, (p) => new Point(p.X + x - 8, p.Y + 5)));
if (topTriangles.Count < 35)
{
GraphicsPath gp = new GraphicsPath();
gp.AddPolygon(Array.ConvertAll(triangle1, (p) => new Point(p.X + x - 8, p.Y + 5)));
topTriangles.Add(gp);
}
}
Point[] triangle2 =
{
new Point(0, 8),
new Point(10, 0),
new Point(10, 15),
new Point(0, 8)
};
for (int y = 30; y <= 540; y += 15)
{
e.Graphics.FillPolygon(Brushes.SteelBlue, Array.ConvertAll(triangle2, (p) => new Point(p.X + 555, p.Y + y - 8)));
if (rightTriangles.Count < 35)
{
GraphicsPath gp = new GraphicsPath();
gp.AddPolygon(Array.ConvertAll(triangle2, (p) => new Point(p.X + 555, p.Y + y - 8)));
rightTriangles.Add(gp);
}
}
//this draws the vertical partition line
if (topIndex != -1)
{
int x = Convert.ToInt32(topTriangles[topIndex].GetLastPoint().X);
e.Graphics.DrawLine(Pens.Red, x, 15, x, 555);
}
//this draws the changeable horizontal selecting partition line
if (rightIndex != -1)
{
int y = Convert.ToInt32(rightTriangles[rightIndex].PathPoints[0].Y);
e.Graphics.DrawLine(Pens.Black, 15, y, 555, y);
//if game over...
if (locked)
{
int red = buttonCounts.Count((s) => s == "r");
int black = buttonCounts.Count((s) => s == "b");
showScoreLine?.Invoke(this, string.Format("Red: {0}, Black: {1}. You " + ((red > black) ? "lose" : ((red == black) ? "draw" : "win")), red, black));
}
}
base.OnPaint(e);
}
}
}
Conclusion
This is another example that shows C# and GDI+ are a good choice of 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 Tangram Shapes 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