Introduction
Hangman is two-player word guessing game; a player guesses a hidden word character-by-character given by another player within a predefined numbers of moves (guesses). Later, player punishes (tries to hang) former if character-guess is wrong and if guess is right, he is rewarded revealing the letter positions within a word. More about Hangman is given here. This implementation tries to solve this scenario with technologies tagged. I hope this will help a beginner to play around.
Using the Code
This is a small Windows Forms application and all event-fireup logic is contained in a single file (That's what Winform is famous for :D ). For UI design part, some containers (GroupBox
, Panel
s) are added statically and others (Button
s, Label
s) are dynamic (added at runtime). I'm reading words from a text file for simplicity, contained in our assembly. 2D graphics APIs are used to draw graphics on a panel so as to indicate current hang state. All other steps in my implementation are given below:
1. Initialization and Public Declarations
Upon controls rendering, buttons are added at runtime with letters (A, B,.. Z) as text registering their click events.
private void SelectWord()
{
private void AddButtons()
{
for (int i = (int)'A'; i <= (int)'Z'; i++)
{
Button b = new Button();
b.Text = ((char)i).ToString();
b.Parent = flowLayoutPanel1;
b.Font = new Font(FontFamily.GenericSansSerif, 20, FontStyle.Bold);
b.Size = new Size(40, 40);
b.BackColor = Color.LawnGreen;
b.Click += b_Click; }
flowLayoutPanel1.Enabled = false;
}
enum to contain all hang states:
enum HangState { None, Piller, Rope, Head, body, LeftHand, RightHand, LeftLeg, RightLeg }
Other declarations are in the code provided, viz. collections, graphics related variables, etc.
2. Read file containing words and select (randomize) a word to guess
File (Words.txt) containing some predefined words, is read from current directory (EXE location) and random word is chosen to start with.
private void SelectWord()
{
string filePath = Path.Combine(Path.GetDirectoryName
(System.Reflection.Assembly.GetExecutingAssembly().Location), "Words.txt");
using (TextReader tr = new StreamReader(filePath, Encoding.ASCII))
{
Random r = new Random();
var allWords = tr.ReadToEnd().Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
currentWord = allWords[r.Next(0, allWords.Length - 1)]; }
}
3. Adding labels for a word
Labels are added to groupbox container with some character (e.g. __) as text (so as to hide actual letter).
private void AddLabels()
{
groupBox1.Controls.Clear();
labels.Clear();
char[] wordChars = currentWord.ToCharArray();
int len = wordChars.Length;
int refer = groupBox1.Width / len;
for (int i = 0; i < len; i++)
{
Label l = new Label();
l.Text = DefaultChar;
l.Location = new Point(10 + i * refer, groupBox1.Height - 30);
l.Parent = groupBox1;
l.BringToFront();
labels.Add(l);
}
txtWordLen.Text = len.ToString();
txtGuessesLeft.Text = HangStateSize.ToString();
}
4. Implementing b_Click: handle letter clicks and adding graphics to a panel
We have to take all decisions (win, lose, wrong guess, right guess) upon button (containing letters) clicking, so the following code snippet is the heart.
void b_Click(object sender, EventArgs e)
{
Button b = (Button)sender;
char charClicked = b.Text.ToCharArray()[0];
b.Enabled = false;
if ((currentWord = currentWord.ToUpper()).Contains(charClicked))
{
lblInfo.Text = "Awesome!";
lblInfo.ForeColor = Color.Green;
char[] charArray = currentWord.ToCharArray();
for (int i = 0; i < currentWord.Length; i++)
{
if (charArray[i] == charClicked)
labels[i].Text = charClicked.ToString();
}
if (labels.Where(x => x.Text.Equals(DefaultChar)).Any())
return;
lblInfo.ForeColor = Color.Green;
lblInfo.Text = "Hurray! You win.";
flowLayoutPanel1.Enabled = false;
}
else
{
lblInfo.Text = "Boo..";
lblInfo.ForeColor = Color.Brown;
if (CurrentHangState != HangState.RightLeg)
CurrentHangState++;
txtGuessesLeft.Text = (HangStateSize - (int)CurrentHangState).ToString();
txtWrongguesses.Text += string.IsNullOrWhiteSpace(txtWrongguesses.Text)
? charClicked.ToString() : "," + charClicked;
panel1.Invalidate();
if (CurrentHangState == HangState.RightLeg)
{
lblInfo.Text = "You lose!";
lblInfo.ForeColor = Color.Red;
flowLayoutPanel1.Enabled = false;
for (int i = 0; i < currentWord.Length; i++)
{
if (labels[i].Text.Equals(DefaultChar))
{
labels[i].Text = currentWord[i].ToString();
labels[i].ForeColor = Color.Blue;
}
}
}
}
}
Now, we need to add hangman body parts (hand, leg, etc.) to panel graphics. This logic needs to be in panel Paint()
event handler. This will keep our hangman drawing refreshed, otherwise graphics will vanish upon Paint()
trigger (e.g. resize, minimize, etc).
private void panel1_Paint(object sender, PaintEventArgs e)
{
InitializeVars();
var g = e.Graphics;
if (CurrentHangState >= HangState.Piller)
{
g.DrawLine(p, new Point(pillerVerBottomX, pillerVerBootomY), new Point(pillerVerTopX, pillerVerTopY));
g.DrawLine(p, new Point(pillerHorRightX, pillerHorRightY), new Point(pillerHorLeftX, pillerHorLeftY));
}
if (CurrentHangState >= HangState.Rope)
{
g.DrawLine(pRope, new Point(ropeTopX, ropeTopY), new Point(ropeBottomX, ropeBottomY));
}
if (CurrentHangState >= HangState.Head)
{
g.DrawEllipse(pRope, new Rectangle(new Point(HeadBoundingRectX, ropeBottomY), new Size(diameter, diameter)));
g.FillRectangles(new SolidBrush(Color.Crimson),
new[] {
new Rectangle( new Point(HeadBoundingRectX + 10,
ropeBottomY + 10), new Size(6, 6)), new Rectangle( new Point(HeadBoundingRectX +
diameter - 10 - 6, ropeBottomY + 10), new Size(6, 6)), new Rectangle(new Point(ropeBottomX - 5/2,
ropeBottomY + diameter/2), new Size(5, 5)), new Rectangle(new Point(ropeBottomX - 10,
ropeBottomY + diameter/2 + 10), new Size(20, 5)) });
}
if (CurrentHangState >= HangState.body)
{
g.DrawEllipse(pRope, new Rectangle(new Point
(HeadBoundingRectX, bodyBoundingRectY), new Size(diameter, bodySize)));
}
if (CurrentHangState >= HangState.LeftHand)
{
g.DrawCurve(pRope,
new[] {
new Point(HeadBoundingRectX + 8, bodyBoundingRectY + 15),
new Point(HeadBoundingRectX - 30, bodyBoundingRectY + 30),
new Point(HeadBoundingRectX - 30, bodyBoundingRectY + 20),
new Point(HeadBoundingRectX + 5, bodyBoundingRectY + 25)
});
}
if (CurrentHangState >= HangState.RightHand)
{
g.DrawCurve(pRope,
new[] {
new Point(HeadBoundingRectX + diameter - 8, bodyBoundingRectY + 15),
new Point(HeadBoundingRectX + diameter + 30, bodyBoundingRectY + 30),
new Point(HeadBoundingRectX + diameter + 30, bodyBoundingRectY + 20),
new Point(HeadBoundingRectX + diameter - 5, bodyBoundingRectY + 25)
});
}
if (CurrentHangState >= HangState.LeftLeg)
{
g.DrawCurve(pRope,
new[] {
new Point(HeadBoundingRectX + 8, bodyBoundingRectY + bodySize - 15),
new Point(HeadBoundingRectX - 30, bodyBoundingRectY + bodySize - 5),
new Point(HeadBoundingRectX - 30, bodyBoundingRectY + bodySize),
new Point(HeadBoundingRectX + 5, bodyBoundingRectY + bodySize - 25)
});
}
if (CurrentHangState >= HangState.RightLeg)
{
g.DrawCurve(pRope,
new[] {
new Point(HeadBoundingRectX + diameter - 8, bodyBoundingRectY + bodySize - 15),
new Point(HeadBoundingRectX + diameter + 30, bodyBoundingRectY + bodySize - 5),
new Point(HeadBoundingRectX + diameter + 30, bodyBoundingRectY + bodySize),
new Point(HeadBoundingRectX + diameter - 5, bodyBoundingRectY + bodySize - 25)
});
}
}
Each graphics drawing is relative to the previous one, so you can use it in your code while changing some width/height parameters.
Points of Interest
This game is very simple at this level. But we can add more vocabularies domain, meaning for hint. Doing so definitely carries some academic significance (vocabulary learning) with fun.