Sharpword - A Wordle Clone Using C#
I created this game to evaluate my ability to perform simple animations using Windows Form.
- Download source - 104.4 KB
- Download SharpWordExe - 47 KB
- Download the latest code from GitHub
- Download the latest version of EXE from Github
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 valuelstAlphabet[i]
toNotinTheWord
if thelstAlphabet[i]
matches withAnswerWord[i]
then
setlstHashCheckedAlpha[i]
to true so that we know that this position has been checked
and set the result toCorrectSpot
. -
loop through all of the characters in the
lstAlphabet
again.
if thelstAlphabet[i]
match with anyAnswerWord[n]
and
lstHashCheckedAlpha[n]
is false then
set the result toInCorrectSpot
.
public Boolean IsCorrect(Word AnswerWord)
{
int i = 0;
Boolean IsCorrect = true;
if(lstAlphabet.Count != AnswerWord.GetWordAsString().Length )
{
IsCorrect = false;
}
/* Check all of the Alphabet in lstAlphabet
If there is at least one Alphabet
That the result is not AlphaResult.CorrectSpot
This function return false, otherwise return true.
*/
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.
//Unit/Integreation Test No5. (We also have other test)
[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 callsOperation()
after theOperation()
method was executed, unblocks the UI input.Operation(String KeyData)
- This method acceptsKeyData
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 thecurrentwordindex
from the_lstGuessWord
object.PreviousGuessWord
as its name suggests, returns the previousGuess
word.MaxWordGuessAllow
number of the word allows the player toGuess
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(); // Render in case Incorrect answer
void RemoveChar(int Row, int Col);
void SetTheme(Theme pTheme);
Boolean IsFinishProcessing();
void BlockInput(); // When UI is rendering we don't accept input
void UnBlockInput(); // After UI has finished rendering, we accept input.
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 aPanel
object that containsplnKeys
andpnlTitles
.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 theLabels
, then letpnlTitles
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 theTheme
object according to the parameterIsDarkTheme
.
//We have Dictionary to map the character and the RoundLabel
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;
//Loop thought row 0 to 2
for (i = 0; i < arrKey.Length; i++)
{
PreviousKey = null;
//Loop thought all character in each row
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;
}
}
// > and Enter act the same
if (cKey == ">")
{
cKey = "Enter";
KeyWidth = 100;
}
// < and ? act the same
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)
{
//The latest character in a row
if(LblKey.Left + LblKey.Width > MaxWidth)
{
MaxWidth = LblKey.Left + LblKey.Width;
// Adjust the width
}
}
}
}
pnlKeys.Height = PreviousKey.Top + PreviousKey.Height + SpaceBetweenY;
pnlKeys.Width = MaxWidth;
}
public void CreateTiles()
{
int i;
int j;
// Loop thought all of the Row
for (i = 0; i < _Game.MaxWordGuessAllow; i++)
{
// Loop thoguht all of the character in the row;
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);
// Add to DicButton
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();
//Loop thought all of the label in DicButton
//To set the position
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)
{
//Just set the value in case of DarkMode
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
{
// Light mode.
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;
}
// Set BackColor to each panel
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;
//Loop thought all of the label to set ForColor and BackColor
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;
// Set BackColorButton, ForeColor according to the result;
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()
{
/*
If timer.Interval is precious this value supposed to be
1000.
We use 1285 instead because it tooks about 1.285 seconds
for a timer to tick 40 times using interval 25.
*/
int Millisecond = 1285;
Transitions.Transition tran =
new Transitions.Transition(new TransitionType_EaseInEaseOut(Millisecond));
//Just use method add
//The parameter are object, propertyname, destination of the property value.
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; //Walking
if (lblNonTran.Left >= DestinationLeft) //Reach destination
{
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 theLeft
,Top
,Width
, andHeight
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));
//Keep Original position
int OriLeft = plabel.Left;
int OriTop = plabel.Top;
int OriHeight = plabel.Height;
int OriWidth = plabel.Width;
//Change size and position a little bit
plabel.Left += 5;
plabel.Top += 5;
plabel.Height -= 10;
plabel.Width -= 10;
plabel.Text = ptext;
plabel.ForeColor = Color.Black;
plabel.Tag = OriLeft;
// use TransitionExtend object to change
// the size and position back within a specific time.
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)
{
// After Transition has completed try to Adjust position.
Label lbl = (Label)((TransitionExtend)sender).Tag;
try
{
AdjustLeftProperty(lbl, (int)lbl.Tag);
}catch (Exception ex)
{
//Do nothing
}
}
private static void AdjustLeftProperty(Label plabel, int Left)
{
if (plabel.InvokeRequired)
{
//Handle in case the non UI Thread need to set
//the property of the control.
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++) // 3 Rows
{
for (j = 0; j <= 4; j++) // All of Label in each Row
{
Label LTemp = DicButton[GetLableID(i, j)];
LTemp.Visible = false; // Hide
DrawLabel(e.Graphics, LTemp); // Draw it on panel
}
}
Rectangle rPosition = lblAnswer.ClientRectangle;
rPosition.X += lblAnswer.Left;
rPosition.Y += lblAnswer.Top;
// Draw lblAnswer on Panel
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)
{
//Get label at Current index.
Label label = this.labelList[indexBtnMove];
TransitionExtend transition = new TransitionExtend
(new TransitionType_EaseInEaseOut(TranstitionTime));
//Set properties Top, BackColor on transition object
//For Top property we use the current position - 20 to make to go upper.
transition.add(label, "Top", label.Top - 20);
transition.add(label, "BackColor", Color.Teal);
//To make label goes down we set label.Top + 5
TransitionExtend tBack = new TransitionExtend
(new TransitionType_EaseInEaseOut(TranstitionTime));
tBack.add(label, "Top", label.Top + 5);
//To make label goes back up to the original position.
TransitionExtend tBack2 = new TransitionExtend
(new TransitionType_EaseInEaseOut(450));
tBack2.add(label, "Top", label.Top);
//transition->tback->tback2
//Go up -> Go Down ->Go to the orginal position.
transition.Childs = new List<TransitionExtend>();
transition.Childs.Add(tBack);
tBack.Childs = new List<TransitionExtend>();
tBack.Childs.Add(tBack2);
transition.run();
indexBtnMove++;
//If we finished the latest Label
if (indexBtnMove >= this.labelList.Count)
{
iCountLoop++;
indexBtnMove = 0;
//If we reached the last loop then
if (iCountLoop > NumberofLoop)
{
Timer thisTimer = (Timer)sender;
thisTimer.Enabled = false;
Complete?.Invoke(this, new EventArgs());
//Complete event.
}
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), // destination for upper-left point of original
new Point(100, 0), // destination for upper-right point of original
new Point(0, 100)};// destination for lower-left point of original
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 newBitmap
. - Calculate the
NewDes
point. - Set
pp.DrawImage
property,NewDes
(pp
isDoubleBufferedPanel
that we use). - Call
pp.Invalidate()
so that thePaint_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 ), // destination for upper-left
// point of original
new Point(L.Left + L.Width, L.Top ), // destination for
// upper-right point of
// original
new Point(L.Left , L.Top + L.Height)};// destination for lower-left
// point of original
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[0].Y;
}
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; //It is a time to flip.
}
if (NewDes[0].Y <= FirstY)
{
if (IsShowBackCard)
{
LoopCount = 0;
L.Visible = true;
Application.DoEvents();
// If CurrentLabel is not the last label yet
if (CurrentarrLabelSwapIndex < labelList.Count - 1)
{
System.Threading.Thread.Sleep(MilisecondSleepBetwenPile);
IsShowBackCard = false;
CurrentarrLabelSwapIndex++;
}
else
{
IsProcessing = false; //Finish because it is the last label
}
}
}
}
} catch (Exception ex)
{
throw;
}
finally {
pp.Paint -= Paint_SwapLabel; // Clear the event handler
}
Complete?.Invoke(this, new EventArgs()); //Raise event that it already finish.
return;
}
private void Paint_SwapLabel(object sender, PaintEventArgs e)
{
//The actual method that draw
try
{
SharpWord.UI.DoubleBufferedPanel panel =
(SharpWord.UI.DoubleBufferedPanel)sender;
e.Graphics.Clear(panel.BackColor);
e.Graphics.DrawImage(panel.DrawImage, panel.NewDes);
}catch (Exception ex)
{
//Do nothing in case of image is swapping too fast,
//it might throw an exception
}
}
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