This is a clone of a well-known Wordle game which I created to evaluate my ability to perform simple animations using Windows Form.

Introduction
This is a clone of Josh Wardle, a Welsh software engineer, and his well-known Wordle game. I created this game to evaluate my ability to perform simple animations using Windows Form.
How to play

File Structure
Game
The game business rule belongs here.
UI
Everything relates to creating and rendering UI.
Utility
Relate to the path and serialize the object.

The Concept
All of the Game logic is in the Game folder.
Alphabet.cs
This class is used to store the character and the result of the Word
object. The Alphabet
object itself does not have the ability to verify the result. The possibility of the result is CorrectSpot
, WrongSpot
, NoninThWord
.
Word.cs
This class holds a list of alphabets, and its most significant method is IsCorrect ()
. If all of the alphabets in the list are accurate, this method returns true
; otherwise, it returns false
.
The InCorrect()
method assigns the result to the Alphabet
object in the lstAlphabet
in addition to determining whether the result is accurate. We must keep the result in Alphabet
so that the UI object may use it to render the tile.
First I would like to talk about the incorrect result. Here is the first version I created. The Answer is AMPLY.

As you see, the program renders the first P using yellow as an indication that the P alphabet exists in an answer but it is not in the correct position.
The problem is according to the Wordle rule if the answer only has one P, and the second P is already in the correct position. The first P cannot be yellow, it cannot be counted as "Incorrect position", it must be counted as "Not in the word".
I got informed about this bug, so I fixed it. This is an example of a correct program.

- P is yellow because it exits in an answer but is in the incorrect position.
- The first P is gray, and the second P is green because
the second P is in the correct position. - The first and the third P is gray, the second P is green.
Here is the algorithm
In this case, I will use a variable AnswerWord as an answer
and I will lstAlphabet as a list that contains what the user enters.
-
Create a variable List lstHasCheckedAlpha
to store if the
character in the answer has been checked.
-
loop through all of the characters in the lstAlphabet
.
set default value lstAlphabet[i]
to NotinTheWord
if the lstAlphabet[i]
matches with AnswerWord[i]
then
set lstHashCheckedAlpha[i]
to true so that we know that this position has been checked
and set the result to CorrectSpot
.
-
loop through all of the characters in the lstAlphabet
again.
if the lstAlphabet[i]
match with any AnswerWord[n]
and
lstHashCheckedAlpha[n]
is false then
set the result to InCorrectSpot
.
public Boolean IsCorrect(Word AnswerWord)
{
int i = 0;
Boolean IsCorrect = true;
if(lstAlphabet.Count != AnswerWord.GetWordAsString().Length )
{
IsCorrect = false;
}
for (i = 0; i < lstAlphabet.Count ; i++)
{
Alphabet AlphaAnswer = AnswerWord.lstAlphabet[i];
lstAlphabet[i].Result = AlphaResult.NotinTheWord;
if(lstAlphabet [i].Character == AlphaAnswer.Character )
{
lstAlphabet[i].Result = AlphaResult.CorrectSpot;
} else
{
if(AnswerWord.IsItContainChar (lstAlphabet [i].Character ))
{
lstAlphabet[i].Result = AlphaResult.WrongSpot;
}
}
if(lstAlphabet [i].Result == AlphaResult.NotinTheWord ||
lstAlphabet [i].Result == AlphaResult.WrongSpot )
{
IsCorrect = false;
}
}
return IsCorrect;
}
I will also use these two images as an example.

In the first step, the program cannot find any matching alphabet. In the second step, it found out that P is the incorrect position.
In the first step, the program found that the Alphabet at positions 1, 3, and 4 matches the Answer. So it set the result to Alphabet 1, 3, 4 to CorrectPosition (Green color) and it also set that the position 1, 3, 4 has checked. So it will not allow checking again (You see black color)
The second step, check P with M, Y so it set the result to NotinTheWord. It checks E with M, Y so it set the result to NotinTheWord.
[TestMethod]
public void TestAnswer05()
{
String filePath = WordFilePath;
ISharpWordUI UI = new SharpWord.UI.MockUI();
SharpWord.Game.SharpWordGame game = new SharpWord.Game.SharpWordGame(UI, filePath);
game.SetWordAnswerForTestingPurpose(@"AMPLY");
int iCurrentIndex = game.CurrentWordIndex;
Assert.IsTrue(iCurrentIndex == 0);
AnswerResultEnum Result = AnswerResultEnum.InTheWordListButNotCorrect;
SharpWordGame.GameStateEnum GameState = GameStateEnum.Playing;
Answer(game, "POINT");
iCurrentIndex = game.CurrentWordIndex;
Result = game.LatestResult;
GameState = game.GameState;
System.Collections.Generic.List<Alphabet> lstAlpha = game.PreviousGuessWord.lstAlphabet;
Assert.IsTrue(lstAlpha[0].Result == AlphaResult.WrongSpot);
Assert.IsTrue(lstAlpha[1].Result == AlphaResult.NotinTheWord );
Assert.IsTrue(lstAlpha[2].Result == AlphaResult.NotinTheWord);
Assert.IsTrue(lstAlpha[3].Result == AlphaResult.NotinTheWord);
Assert.IsTrue(lstAlpha[4].Result == AlphaResult.NotinTheWord);
Assert.IsTrue (game.GameState == GameStateEnum.Playing);
Answer(game, "APPLY");
lstAlpha = game.PreviousGuessWord.lstAlphabet;
Assert.IsTrue(lstAlpha[0].Result == AlphaResult.CorrectSpot );
Assert.IsTrue(lstAlpha[1].Result == AlphaResult.NotinTheWord);
Assert.IsTrue(lstAlpha[2].Result == AlphaResult.CorrectSpot );
Assert.IsTrue(lstAlpha[3].Result == AlphaResult.CorrectSpot );
Assert.IsTrue(lstAlpha[4].Result == AlphaResult.CorrectSpot);
Assert.IsTrue(game.GameState == GameStateEnum.Playing);
Answer(game, "PUPPY");
lstAlpha = game.PreviousGuessWord.lstAlphabet;
Assert.IsTrue(lstAlpha[0].Result == AlphaResult.NotinTheWord);
Assert.IsTrue(lstAlpha[1].Result == AlphaResult.NotinTheWord);
Assert.IsTrue(lstAlpha[2].Result == AlphaResult.CorrectSpot);
Assert.IsTrue(lstAlpha[3].Result == AlphaResult.NotinTheWord);
Assert.IsTrue(lstAlpha[4].Result == AlphaResult.CorrectSpot);
Assert.IsTrue(game.GameState == GameStateEnum.Playing);
Answer(game, "AMPLY");
lstAlpha = game.PreviousGuessWord.lstAlphabet;
Assert.IsTrue(game.GameState == GameStateEnum.Finished);
}
SharpWordGame.cs
This class contains a list of Words and game information and ISharpWordUI
.
These are important methods of this class:
EnterChar(String KeyData)
- This method accepts the value from the keyboard, then checks if the UI was not blocked, blocks the UI input and then calls Operation()
after the Operation()
method was executed, unblocks the UI input. Operation(String KeyData)
- This method accepts KeyData
and then handles the game logic.
- If it is the Back key, remove
Char
then returns. - If it is a RETURN key,
SubmitAnswer()
then returns. - If it reaches this point, it means it is neither Back nor Return, then program Add Character to the word.
LoadListWord()
- This method will load the list of the word from the file path. InitialGame()
- Load the list of Word from the file then set the answer, initial _lstGuessWord
then calls the UI object to render the game. CheckAnswer()
to check if the answer is correct.
These are the important properties:
CurrentGuessWord
returns a Word object.
Behind the scenes, it returns the currentwordindex
from the _lstGuessWord
object. PreviousGuessWord
as its name suggests, returns the previous Guess
word. MaxWordGuessAllow
number of the word allows the player to Guess
before the game is over. MaxCharInWord
number of characters in the word. CurrentWordIndex
we have a _lstGuessWord
to contain all of the words object.
This property contains the current index of the word.
This is the game state.
public enum GameStateEnum
{
Playing,
Finished
}
Statistics.cs
This class is responsible for keeping the information on the number of times the Player Wins/Loss, then calculating the percentage.

This image shows a sequence diagram between UI and the Game
object.
The UI
The code in the UI parts took more than 90% of the time I developed this project because the Windows Form is not a CSS, so you cannot just flip an image by writing five lines of code.
ISharpWordUI.cs
This is an interface that provides the methods that the actual UI object needs to implement. These are the methods:
void SetGame(SharpWordGame pGame);
void CreateTiles();
void CreateKeyBoard();
void CreateBoard();
void RenderWin();
void RenderLost();
void RenderIncorrectRow(int pRowIndexIncorrect);
void RenderCurrentWord(String str);
void RenderKeyBoard(Dictionary<Char, AlphaResult> pDicTriecChar);
void RenderAttemptWord();
void RemoveChar(int Row, int Col);
void SetTheme(Theme pTheme);
Boolean IsFinishProcessing();
void BlockInput();
void UnBlockInput();
void ClearUI();
void ShowStatistics(Statistics statis);
Boolean IsInputBlocked();
WinFormUI.cs
This class is the UI class that implements the ISharpWordUI
interface.
These are three important objects in this class:
pnlMain
is a Panel
object that contains plnKeys
and pnlTitles
. plnKeys
is used for rendering the virtual keyboard. pnlTitles
is used for rendering the tiles of the game.

CreateTitles()
is a method this class uses for rendering the tiles, we simply create arrays of the Labels
, then let pnlTitles
add them as controls. CreateKeyBoard()
- This method also uses an array of labels as a keyboard key. RenderTheme()
- This program supports dark and light modes, and we use this method to render the game's appearance. GetTheme()
returns the Theme
object according to the parameter IsDarkTheme
.
Dictionary<String, RoundLabel> DicKeyBoard =
new Dictionary<String, RoundLabel>();
String[] arrKey = { "QWERTYUIOP", "ASDFGHJKL", ">ZXCVBNM<" };
public void CreateKeyBoard()
{
pnlKeys = new Panel();
DicKeyBoard = new Dictionary<String, RoundLabel>();
int i;
int j;
int SpaceBetweenX = 5;
int SpaceBetweenY = 5;
Label PreviousKey = null;
int MaxWidth = 0;
for (i = 0; i < arrKey.Length; i++)
{
PreviousKey = null;
for (j = 0; j < arrKey[i].Length; j++)
{
String cKey = arrKey[i][j].ToString();
RoundLabel LblKey = new RoundLabel();
LblKey.Font = lblTemplateKey.Font;
LblKey.TextAlign = lblTemplateKey.TextAlign;
LblKey.AutoSize = lblTemplateKey.AutoSize;
int KeyWidth = lblTemplateTile.Width;
int KeyLeft = 0;
if (i == 1 && j == 0)
{
KeyLeft = lblTemplateTile.Width / 2;
}
else
{
if (PreviousKey == null)
{
KeyLeft = (j * (LblKey.Width + SpaceBetweenX) +
SpaceBetweenX);
}
else
{
KeyLeft = PreviousKey.Left +
PreviousKey.Width + SpaceBetweenX;
}
}
if (cKey == ">")
{
cKey = "Enter";
KeyWidth = 100;
}
if (cKey == "<")
{
cKey = "?";
KeyWidth = MaxWidth - KeyLeft;
}
LblKey.Text = cKey.ToString();
LblKey.Width = KeyWidth;
LblKey.Height = lblTemplateKey.Height;
LblKey.Top = (i * (LblKey.Height + SpaceBetweenY) +
SpaceBetweenY);
LblKey.Left = KeyLeft;
LblKey.Visible = true;
LblKey.Click += LblKey_Click;
PreviousKey = LblKey;
DicKeyBoard.Add(LblKey.Text, LblKey);
pnlKeys.Controls.Add(LblKey);
if(j== arrKey [i].Length -1)
{
if(LblKey.Left + LblKey.Width > MaxWidth)
{
MaxWidth = LblKey.Left + LblKey.Width;
}
}
}
}
pnlKeys.Height = PreviousKey.Top + PreviousKey.Height + SpaceBetweenY;
pnlKeys.Width = MaxWidth;
}
public void CreateTiles()
{
int i;
int j;
for (i = 0; i < _Game.MaxWordGuessAllow; i++)
{
for (j = 0; j < this.MaxWordLength; j++)
{
Label labelTile = new Label();
labelTile.Height = lblTemplateTile.Height;
labelTile.Width = lblTemplateTile.Width;
labelTile.Font = lblTemplateTile.Font;
labelTile.FlatStyle = FlatStyle.Flat;
labelTile.BorderStyle = BorderStyle.FixedSingle;
labelTile.TextAlign = ContentAlignment.MiddleCenter;
labelTile.Visible = true;
labelTile.Text = "";
labelTile.Name = GetLableID(i,j);
DicButton.Add(labelTile.Name, labelTile);
}
}
this.pnlTiles = new DoubleBufferedPanel();
SetDoubleBuffered(this.pnlTiles);
this.pnlTiles.Controls.Clear();
int HeightOffset = 8;
int WidthOffset = 8;
List<Label> lstLbl = DicButton.Values.ToList();
for (i = 0; i < lstLbl.Count; i++)
{
String Name = lstLbl[i].Name;
int iTop = int.Parse(Name.Substring(0, 2));
int iLeft = int.Parse(Name.Substring(2, 2));
lstLbl[i].Top = iTop * (lstLbl[i].Height + HeightOffset) +
HeightOffset * 2;
lstLbl[i].Left = iLeft * (lstLbl[i].Width + WidthOffset) + WidthOffset;
this.pnlTiles.Controls.Add(lstLbl[i]);
}
Label lastLbl = lstLbl[lstLbl.Count - 1];
this.pnlTiles.Width = lastLbl.Left + lastLbl.Width + WidthOffset;
this.pnlTiles.Height = lastLbl.Top + lastLbl.Height + HeightOffset;
lblAnswer = new RoundLabel();
lblAnswer.AutoSize = false;
lblAnswer.Text = _Game.WWordAnswer.GetWordAsString();
lblAnswer.Top = 100;
lblAnswer.Width = 160;
lblAnswer.AutoSize = false;
lblAnswer.Height = 80;
lblAnswer.TextAlign = ContentAlignment.MiddleCenter;
lblAnswer.Left = (this.pnlTiles.Width - lblAnswer.Width) / 2;
lblAnswer.Visible = false;
lblAnswer.BringToFront();
this.pnlTiles.Controls.Add(lblAnswer);
}
private Theme GetTheme(Boolean IsDarkTheme)
{
Theme theme = new Theme();
if (IsDarkTheme)
{
theme.TileNormalBackColor = Color.White;
theme.TileNormalForeColor = Color.FromArgb(18, 18, 18);
theme.LabelAnswerBackColor = Color.FromArgb(18, 18, 18);
theme.LabelAnswerForeColor = Color.White;
theme.TileCorrectBackColor = Color.FromArgb(106, 170, 100);
theme.TileCorrectForeColor = Color.White;
theme.TileNotExistBackColor = Color.FromArgb(58, 58, 60);
theme.TileNotExistForeColor = Color.White;
theme.TileNotCorrectPositionBackColor = Color.FromArgb(201, 180, 88);
theme.TileNotCorrectPositionForeColor = Color.White;
theme.KeyForeColor = Color.Black;
theme.KeyBackColor = Color.FromArgb(211, 214, 218);
theme.BoardBackColor = Color.FromArgb(18, 18, 19);
theme.BoardForeColor = Color.White;
theme.ButtonBackColor = Color.White;
theme.ButtonForeColor = Color.Black;
theme.PopupFormBackColor = Color.FromArgb(20,18,20);
theme.IsFormCaptionDarkMode = true;
}
else
{
theme.TileNormalBackColor = Color.White;
theme.TileNormalForeColor = Color.Black;
theme.LabelAnswerBackColor = Color.FromArgb(18, 18, 18);
theme.LabelAnswerForeColor = Color.White;
theme.TileCorrectBackColor = Color.FromArgb(83, 141, 78);
theme.TileCorrectForeColor = Color.White;
theme.TileNotExistBackColor = Color.FromArgb(120, 124, 126);
theme.TileNotExistForeColor = Color.White;
theme.TileNotCorrectPositionBackColor = Color.FromArgb(201, 180, 88);
theme.TileNotCorrectPositionForeColor = Color.White;
theme.KeyForeColor = Color.Black;
theme.KeyBackColor = Color.FromArgb(211, 214, 218);
theme.BoardBackColor = Color.White;
theme.BoardForeColor = Color.Black;
theme.ButtonBackColor = Color.Black;
theme.ButtonForeColor = Color.White;
theme.PopupFormBackColor = Color.FromArgb (240,240,240);
theme.IsFormCaptionDarkMode = false;
}
return theme;
}
public void RenderTheme()
{
if(pnlMain ==null)
{
return;
}
this.pnlMain.BackColor = _CurrentTheme.BoardBackColor;
this.pnlTiles.BackColor = _CurrentTheme.BoardBackColor;
this.pnlKeys.BackColor = _CurrentTheme.BoardBackColor;
Form.BackColor = pnlMain.BackColor;
lblAnswer._BackColor = _CurrentTheme.LabelAnswerBackColor;
lblAnswer.Font = lblTemplateTile.Font;
lblAnswer.ForeColor = _CurrentTheme.LabelAnswerForeColor;
Color BackColorButton = Color.White;
Color ForeColor = Color.Black;
Color BorderColor = Color.Black;
int i;
int j;
for (i = 0; i < _Game.MaxWordGuessAllow; i++)
{
for (j = 0; j < this.MaxWordLength; j++)
{
Label labelTile = DicButton[GetLableID(i,j)];
labelTile.ForeColor = _CurrentTheme.TileNormalForeColor;
labelTile.BackColor = _CurrentTheme.TileNormalBackColor;
}
}
for (i = 0; i < this._Game.lstGuessWord.Count; i++)
{
BorderStyle borderStyle = BorderStyle.FixedSingle;
for (j = 0; j < this._Game.lstGuessWord[i].lstAlphabet.Count; j++)
{
if (this._Game.lstGuessWord[i].GetWordAsString().Length <
this.MaxWordLength)
{
BackColorButton = _CurrentTheme.TileNormalBackColor;
ForeColor = _CurrentTheme.TileNormalForeColor;
}
else
{
BackColorButton = Color.White;
ForeColor = Color.White;
borderStyle = BorderStyle.None;
switch (this._Game.lstGuessWord[i].lstAlphabet[j].Result)
{
case AlphaResult.CorrectSpot:
BackColorButton = _CurrentTheme.TileCorrectBackColor;
ForeColor = _CurrentTheme.TileCorrectForeColor;
break;
case AlphaResult.WrongSpot:
BackColorButton =
_CurrentTheme.TileNotCorrectPositionBackColor;
ForeColor =
_CurrentTheme.TileNotCorrectPositionForeColor;
break;
case AlphaResult.NotinTheWord:
BackColorButton = _CurrentTheme.TileNotExistBackColor;
ForeColor = _CurrentTheme.TileNotExistForeColor;
break;
default:
throw new Exception("Wrong value");
}
}
Label labelTile = DicButton[GetLableID(i,j)];
labelTile.BackColor = BackColorButton;
labelTile.BorderStyle = borderStyle;
labelTile.ForeColor = ForeColor;
}
}
for (i = 0; i < arrKey.Length; i++)
{
for (j = 0; j < arrKey[i].Length; j++)
{
String cKey = arrKey[i][j].ToString();
if (cKey == ">")
{
cKey = "Enter";
}
if (cKey == "<")
{
cKey = "?";
}
RoundLabel labelKey = new RoundLabel();
labelKey = DicKeyBoard[cKey];
labelKey.ForeColor = _CurrentTheme.KeyForeColor;
labelKey._BackColor = _CurrentTheme.KeyBackColor;
}
}
Utility.Utility.MakeFormCaptionToBeDarkMode
(this.Form, _CurrentTheme.IsFormCaptionDarkMode);
}
MainUI.cs
This class contains a UI object and a game
object, it acts like glue between those two objects.
Render Animation
This project uses Transitions.dll to help with the rendering part. The Transitions.dll has a Transition
class which can help us make a smooth animation.
We use Transitions
because we need to find the rate of change of a parameter over time.
Supposing we would like to move a label that its left property is 10, then we need to move its left position to 40 within 5 seconds.
In each second, the left property will be increased by 6, after 5 seconds have passed, the left property would be 40 as we expected.
The problem is it will not look so smooth because the rate of change is the same in each step.
In nature when the object moves, it does not move at the same rate. If our program moves the object at the same rate, it will look strange.
This site has information on how Transition works https://easings.net/. It uses CSS as an example but it also provides us with a Math
function.
TransitionExtend.cs
This is a class that inherits from Transitions.Transition
class. We use this class to set the property of the object, the property of the object will keep being updated until it reaches the goal automatically.
The DLL we used is Transistion.dll. You can check for more information at https://github.com/UweKeim/dot-net-transitions.

This is an example of the code that uses Transitions to move the label vs the non-transitions moving. You can look into the code in frmSampleTransitions.cs.
Timer timerMove = new Timer();
private int DestinationLeft = 0;
private int StepSize = 10;
private int NumberofStep = 40;
int iCount = 0;
Utility.TimeMeasure timeMeasure = null;
private void btnRun_Click(object sender, EventArgs e)
{
timeMeasure = new Utility.TimeMeasure();
timeMeasure.Start();
lblTran.Left = 30;
lblNonTran.Left = 30;
DestinationLeft = lblTran.Left + (StepSize * NumberofStep);
StartTranMoving();
StartNonTranMoving();
}
private void StartTranMoving()
{
int Millisecond = 1285;
Transitions.Transition tran =
new Transitions.Transition(new TransitionType_EaseInEaseOut(Millisecond));
tran.add(lblTran, "Left", DestinationLeft);
tran.run();
}
private void StartNonTranMoving()
{
if (timerMove != null)
{
timerMove.Enabled = false;
}
timerMove = new Timer();
timerMove.Enabled = true;
timerMove.Interval = 25;
timerMove.Tick += TimerMove_Tick;
}
private void TimerMove_Tick(object sender, EventArgs e)
{
lblNonTran.Left += StepSize;
if (lblNonTran.Left >= DestinationLeft)
{
timerMove.Enabled = false;
timeMeasure.Finish();
this.Text = "Time takes (seconds) " + timeMeasure.TimeTakes.TotalSeconds;
}
}
Render Tiles When the Character is Entered
We use TransitionHelper.PopLabel()
method. The logic in this method is:
- Keep the Original position of the label to the variables (the name is
OriLeft
, OriTop
, OriHeight
, OriWidth
) - Decrease the size of the label by moving it to the bottom right position a little bit and also decrease its size
- Use the
TransitionExtend
object to update the Left
, Top
, Width
, and Height
properties to their original values.
public static void PopLabel(Label plabel, String ptext,
Color pforecolor, int iTransactionTime)
{
plabel.ForeColor = pforecolor;
TransitionExtend t = new TransitionExtend
(new TransitionType_EaseInEaseOut(iTransactionTime));
int OriLeft = plabel.Left;
int OriTop = plabel.Top;
int OriHeight = plabel.Height;
int OriWidth = plabel.Width;
plabel.Left += 5;
plabel.Top += 5;
plabel.Height -= 10;
plabel.Width -= 10;
plabel.Text = ptext;
plabel.ForeColor = Color.Black;
plabel.Tag = OriLeft;
t.add(plabel, "Left", OriLeft);
t.add(plabel, "Top", OriTop);
t.add(plabel, "Width", OriWidth);
t.add(plabel, "Height", OriHeight);
t.Tag = plabel;
t.run();
t.TransitionCompletedEvent += T_TransitionCompletedEvent;
}
private static void
T_TransitionCompletedEvent(object sender, Transition.Args e)
{
Label lbl = (Label)((TransitionExtend)sender).Tag;
try
{
AdjustLeftProperty(lbl, (int)lbl.Tag);
}catch (Exception ex)
{
}
}
private static void AdjustLeftProperty(Label plabel, int Left)
{
if (plabel.InvokeRequired)
{
plabel.Invoke(new Action<Label, int>(AdjustLeftProperty), plabel,Left);
}
else
{
plabel.Left = Left;
}
}
Render Tiles When the Answer Is Incorrect
We use the RenderShake()
method. Just loop through all of the labels that we use for tiles, then update the Left
value and also use Sleep
the thread for 2 milliseconds every time the labels are moving.
public void RenderShake(int pRowIndexIncorrect)
{
List<Label> lstB = new List<Label>();
int i;
int j;
int iValueChange = 1;
int iLoop = 0;
int[] arrLeft = new int[5];
for (i = 0; i <= 4; i++)
{
lstB.Add(DicButton[GetLableID(pRowIndexIncorrect,i)]);
arrLeft[i] = DicButton[GetLableID(pRowIndexIncorrect, i)].Left;
}
for (iLoop = 0; iLoop < 4; iLoop++)
{
for (i = 1; i <= 10; i++)
{
for (j = 0; j < lstB.Count; j++)
{
lstB[j].Left += iValueChange;
}
if (i % 2 == 0)
{
System.Threading.Thread.Sleep(2);
Application.DoEvents();
}
}
iValueChange *= -1;
}
for (i = 0; i <= 4; i++)
{
lstB[i].Left = arrLeft[i];
}
}
Render Tiles When the Player Lost

This is a thing we would like to show. But we cannot use a RoundLabel
to show due to its limit to display the corner color correctly in case the RoundLabel
is on top of the other control.

This picture shows a problem with RoundLabel
control.
What we need to do is still need to have a RoundLabel
control. We named it lblAnswer
, but we don't have the intention to show it.
We hide the first 3 rows of label titles and the lblAnswer
. Then draw the images of those 15 label titles, then draw the rectangle object using the information from lblAnswer
.
We do it in Panel1_Paint
event.
private void Panel1_Paint(object sender, PaintEventArgs e)
{
if (IsLost)
{
int i;
int j;
for (i = 0; i <= 2; i++)
{
for (j = 0; j <= 4; j++)
{
Label LTemp = DicButton[GetLableID(i, j)];
LTemp.Visible = false;
DrawLabel(e.Graphics, LTemp);
}
}
Rectangle rPosition = lblAnswer.ClientRectangle;
rPosition.X += lblAnswer.Left;
rPosition.Y += lblAnswer.Top;
using (var graphicsPath = lblAnswer._getRoundRectangle(rPosition))
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using (var brush = new SolidBrush(lblAnswer._BackColor))
e.Graphics.FillPath(brush, graphicsPath);
using (var pen = new Pen(lblAnswer._BackColor, 1.0f))
e.Graphics.DrawPath(pen, graphicsPath);
TextRenderer.DrawText(e.Graphics, lblAnswer.Text,
lblAnswer.Font, rPosition, lblAnswer.ForeColor);
}
return;
}
}
Render Tiles When the Player Won
We use SwapLabel.DanceLabel()
methods. This method uses a timer to trigger the Ti_TickV2()
method.
Ti_TickV2()
selects the current tile from indexBtnMove
field, then uses TransitionExtend
to change the Top
and BackColor
properties of the label, then indexBtnMove++
so that next time this method will move the next tile.
public void DanceLabel(List<Label> pLabelList,
int pTranstitionTime, int pNumberofLoop)
{
labelList = pLabelList;
NumberofLoop = pNumberofLoop;
TranstitionTime = pTranstitionTime;
Timer Ti = new Timer();
Ti.Interval = 200;
Ti.Tick += Ti_TickV2;
Ti.Enabled = true;
}
int iCountLoop = 0;
int indexBtnMove = 0;
int NumberofLoop = 1;
int TranstitionTime = 200;
private void Ti_TickV2(object sender, EventArgs e)
{
Label label = this.labelList[indexBtnMove];
TransitionExtend transition = new TransitionExtend
(new TransitionType_EaseInEaseOut(TranstitionTime));
transition.add(label, "Top", label.Top - 20);
transition.add(label, "BackColor", Color.Teal);
TransitionExtend tBack = new TransitionExtend
(new TransitionType_EaseInEaseOut(TranstitionTime));
tBack.add(label, "Top", label.Top + 5);
TransitionExtend tBack2 = new TransitionExtend
(new TransitionType_EaseInEaseOut(450));
tBack2.add(label, "Top", label.Top);
transition.Childs = new List<TransitionExtend>();
transition.Childs.Add(tBack);
tBack.Childs = new List<TransitionExtend>();
tBack.Childs.Add(tBack2);
transition.run();
indexBtnMove++;
if (indexBtnMove >= this.labelList.Count)
{
iCountLoop++;
indexBtnMove = 0;
if (iCountLoop > NumberofLoop)
{
Timer thisTimer = (Timer)sender;
thisTimer.Enabled = false;
Complete?.Invoke(this, new EventArgs());
}
return;
}
}
Render Tiles Flipping
We use SwapLabel.SwapNotUsingTimer()
. The concept of this method is just draw the label using DrawImage()
, the second argurment of this method looks like this:
Point[] destinationPoints = {
new Point(0, 0),
new Point(100, 0),
new Point(0, 100)};
With each step that we draw, we will calculate the position of the y-axis so that we can decrease the size of the image until its height < 0.
Then, we will change its back color and increase the size of the image until it reaches its original size.
Step 1-6 is to calculate the position of the label.
We need to draw. Step 7 is the actual step that draws the label image.
- Store
FirstY= Label.Top
for the first loop. - Hide the label.
- Use
label.DrawToBitmap()
to create a new Bitmap
. - Calculate the
NewDes
point. - Set
pp.DrawImage
property, NewDes
(pp
is DoubleBufferedPanel
that we use). - Call
pp.Invalidate()
so that the Paint_SwapLabel()
method will be called. - In
Paint_SwapLabel()
method, it will read the information from the panel, then draw the image.

public void SwapNotUsingTimer(SharpWord.UI.DoubleBufferedPanel pp,
List<Label> pLabelList, int pMilisecondThreadSleep)
{
pp.Paint += Paint_SwapLabel;
try
{
labelList = pLabelList;
if (pMilisecondThreadSleep == -1)
{
pMilisecondThreadSleep = 15;
}
int MilisecondSleepBetwenPile = pMilisecondThreadSleep * 15;
int MilisecondThreadSleepBackCard =
Convert.ToInt32(pMilisecondThreadSleep * 1.5);
CurrentarrLabelSwapIndex = 0;
Boolean IsProcessing = true;
iYChange = 2;
int iTemp = 0;
while (IsProcessing)
{
if (IsShowBackCard)
{
System.Threading.Thread.Sleep(MilisecondThreadSleepBackCard );
}
else
{
System.Threading.Thread.Sleep(pMilisecondThreadSleep);
}
Application.DoEvents();
Label L = labelList[CurrentarrLabelSwapIndex];
LabelAttribute NewAttribute = (LabelAttribute)L.Tag;
int inumerator = L.Height / iYChange;
iTemp++;
iTemp = 0;
if (LoopCount == 0)
{
L.Visible = false;
}
Bitmap b = new Bitmap(L.Width, L.Height);
L.DrawToBitmap(b, new Rectangle(0, 0, b.Width, b.Height));
Image image = b;
LoopCount++;
Point[] NewDes = {
new Point(L.Left , L.Top ),
new Point(L.Left + L.Width, L.Top ),
new Point(L.Left , L.Top + L.Height)};
if (IsShowBackCard)
{
NewDes[0].Y = L.Top + (iYChange * (inumerator - LoopCount));
L.BackColor = NewAttribute.BackColor;
L.ForeColor = NewAttribute.ForeColor;
}
else
{
NewDes[0].Y = L.Top + (iYChange * LoopCount);
}
if (LoopCount == 1)
{
FirstY = L.Top;
}
NewDes[1].Y = NewDes[0].Y;
if (IsShowBackCard)
{
NewDes[2].Y = L.Top + L.Height -
(iYChange * (inumerator - LoopCount));
}
else
{
NewDes[2].Y = L.Top + L.Height - (iYChange * LoopCount);
}
pp.DrawImage = image;
pp.NewDes = NewDes;
pp.Invalidate();
if (NewDes[0].Y > NewDes[2].Y)
{
IsShowBackCard = true;
}
if (NewDes[0].Y <= FirstY)
{
if (IsShowBackCard)
{
LoopCount = 0;
L.Visible = true;
Application.DoEvents();
if (CurrentarrLabelSwapIndex < labelList.Count - 1)
{
System.Threading.Thread.Sleep(MilisecondSleepBetwenPile);
IsShowBackCard = false;
CurrentarrLabelSwapIndex++;
}
else
{
IsProcessing = false;
}
}
}
}
} catch (Exception ex)
{
throw;
}
finally {
pp.Paint -= Paint_SwapLabel;
}
Complete?.Invoke(this, new EventArgs());
return;
}
private void Paint_SwapLabel(object sender, PaintEventArgs e)
{
try
{
SharpWord.UI.DoubleBufferedPanel panel =
(SharpWord.UI.DoubleBufferedPanel)sender;
e.Graphics.Clear(panel.BackColor);
e.Graphics.DrawImage(panel.DrawImage, panel.NewDes);
}catch (Exception ex)
{
}
}
Testing
You can run a unit or integration test on a test project. There are not many test methods here because most of the code in this project relates to the UI and animation which makes it difficult to automate tests.

Known Issues
I tried searching but couldn't find any examples of flipping images and other animations, so I created my own function.
I try to use Timer
, Thread.Sleep()
, Application.DoEvents()
, then tuning then changing the value and seeing the result.
The thing is, each of these methods has drawbacks, and the code appears complicated.
I will be more than happy if you use this code, but I also hope you discover a more effective technique if you need to flip an image in your application.
Point of interest
One of the challenges with developing this project is that you must test with the real Wordle to learn the requirements, and you are only allowed to play it once per day when you try to test a particular case, like the case where there are duplicate letters. However, you are not allowed to select the world you need to test.
Reference Code
Reference
History
- 19th November, 2022: Initial version
- 22nd November, 2022: Initial version
Hi. My name is Krirk Srithaweewath, I am a C# developer from Thailand.
I always create puzzle games on my free time.