Click here to Skip to main content
14,299,664 members

Ms. Wordbler - A Cute Little Warbler!

Rate this:
5.00 (7 votes)
Please Sign up or sign in to vote.
5.00 (7 votes)
11 Apr 2019CPOL
Word-making game!

Introduction

This is a word-making game; this is a Winforms project in C#.NET with .NET framework 4.5.2. IDE used was Visual Studio 2017.

The application is built to be resolution independent. Design-time resolution was 1680x1050. It is tested with resolutions of 1366x768, 1280x720, 800x600. Tested okay, however graphics quality degrades with lower resolution (as expected).

Letter, tile, cell – all are used interchangeably in the article; they mean the same.

Background

There are various word-making games including crosswording logic around the world. This one borrows some ideas from some of them. The purpose is to experiment on logic development about how to practically think of data structures and algorithms and how to find structural patterns in strings. This is purely for educational purposes with some fun inside it.

How the Game Works

  1. The game starts with asking for the number of players. There may be at best 4 players and at least 2 players. Players need to enter their names and choose mascots (drag and drop mascots in the individual mascot boxes).
  2. Which player starts the game is decided next. Each player is randomly distributed one letter from the bag. If any player gets a blank tile, then s/he starts the game. Else the player whose letter is closest to ‘A’ will start the game. After the first player is decided, all the letters are returned to the bag.
  3. Next the racks are loaded with 7 random letters from the bag.
  4. A Player drags and drops individual letters on the board. The first word must cross through the central star (*) tile.
  5. After the player thinks the word is complete, s/he presses the DONE button. At this point, a messagebox appears with scoring details. If all coined words are valid, then s/he scores. Else the letters are taken back to his/her rack and turn moves to the next player.
  6. Turn moves to the next player (clockwise) and the game is carried on as such.
  7. A player can exchange any number of letters from the bag. An exchange forfeits his/her current turn. To exchange, hold the CTRL button and click on the letters to exchange (the tile turns into crimson colour), then press the EXCHANGE button.
  8. A player can forfeit a turn if s/he can’t make a word. This is not needed though, as the game automatically forfeits the current turn if the word is not valid.
  9. If a player drags a blank tile, the tile value should be set when it is dropped on the board. The tile cell could be identified by pink colour. The value remains like that for the rest of the game. However, the tile will always carry a 0-score.
  10. If a valid word is made that crosses through premium cells (2W, 2L, 3W, 3L), then after the scoring those special tiles become regular tiles – they will not augment any future words that pass through them. This is signified by changing their special background colour to grey.
  11. If a player mistakenly drops a letter on a wrong cell of the board, s/he can take it back by pressing CTRL+Z. For the current turn, the player can press CTRL+Z as many times as needed. This will take the letters back to the rack in reverse order of dragging.
  12. At any point, 6 consecutive fails (wrong word, forfeit, exchange) will end the game. In that case, summation of the face-values of the remaining letters in each player’s rack is deducted from his/her total score.
  13. If there is no more letter in the bag, then
    1. If one of the players could use all his/her letters, then the sum of other players’ letters is added to his score and deducted from theirs.
    2. Else face-values of the remaining letters in each player's rack are summed up and each player’s score is deducted by the individual sum.
  14. Extended forms of a word are allowed. For example, if a player makes a word 'END'. then another player can extend it to 'ENDING', another player can extend it to 'ENDINGS'. All are valid words.

  15. After that, the player with the highest score wins the game.

A Glimpse of the Project Structure

Forms

SelectPlayer

This is the start-up form for the game. It doesn’t have to do much with the main logic. It just adds a little glamour touch to the game.

Image 1

In this form, the players need to enter their name and drag a mascot to their mascot boxes.

Wordbler

This is the main form where the game is played. It accommodates the board, the racks, player’s names, legends, fail counts, mascots, scores and buttons.

On loading, the form displays all the available players’ racks. The letter bag shakes and distributes a letter randomly to each of the players. The letter decides which player should start the game. To remind, the player who draws a letter closest to ‘A’, or draws a blank tile starts the game.

After the first player is decided the letters are taken bag and then 7 letters are randomly distributed to each of the players. The rack of the starting player is enabled, and others are disabled. The player drags the letters on the board.

Image 2

There are three buttons – ‘Done’, ‘Exchange’, and ‘Pass’. If the player wants to submit the word for scoring, then s/he presses ‘Done’. If the player wants to exchange letters, then s/he presses ‘Exchange’ (for exchanging, the letters need to be selected by clicking on the letters while holding the CTRL-key down). If a player decides to pass, s/he presses ‘Pass’.

Using the menu, the legends can be visible or hidden. The premium contents (3L, 2L, 3W, 2W) are hidden or displayed according to toggle.

Image 3

Board with Special Cells Indicators

The game moves on with turns.

WildCardChooser

Image 5When the player drags the wildcard (designated by the orchid-colour tile) on the board, this form shows up and asks the user to choose a letter for it. If the wording is successful, then the wildcard retains that letter on the board. However, a wildcard carries 0-point which will always be 0 till the end of the game. The wild card will be identified by the pink-colour.

DisplayScoreDetails

When a player clicks on any mascot, this form displays the scoring history of the player who owns the mascot. It loops through the detailed history and displays it according to turns.

Scoreboard

 

 

 

 

 

 

 
About

The ‘About’ box – displays information about the project.

Classes

Globals

Contains global and static variables needed by different modules of the project.

BoardCell

Contains the (X, Y) coordinate of the board-cell where a letter is dropped. It also contains information about any premium content (2W, 3W, 2L, 3L) that boad-cell might have before the drop. It contains the letter that was dropped on the cell. These properties are populated through the constructor.

RackCell

Holds the details of a rack cell when the player starts dragging a letter from the rack. It records the player number, the cell number and the letter that was on that cell. These properties are populated through a copy constructor, a regular constructor and an ‘Add’ method.

Letter

Holds a letter and its position. The letter might be an exchanging letter, a drawn letter, or a letter in the bag. The position is the index of the letter in the bag. These two properties are populated through the constructor.

IndividualScore

This class contains the mechanism for scoring each individual letter of a word. It contains the letter, the axis of the letter on the board, any premium content (2W, 3W, 2L, 3L) the board might have, and the score for the letter. The letters have their face-value according to pre-defined rules.

ValidWordWithScore

Contains the whole word that was coined in the current turn, individual letter score details of the word (using a list of ‘IndividualScore’ object), the axis on the board where this word started.

TurnsWithScores

Contains turn number, detailed score (which is an exact copy of the message that is displayed when ‘Done’ button is pressed), and valid words of the turn. All the properties are populated through constructor.

PlayerDetails

Contains details of the players (name, mascot), their total score and their historical scoring details (using the ‘TurnsWithScores’ object) of the current game. Name and mascot are populated through constructors.  Total score and historical score details are public properties – hence populated through the object instance.

WindowScaler

This class facilitates the automatic resizing of the elements of the forms according to different screen resolutions. The game is resolution-independent; this class contains the core calculation for the purpose. The detailed explanation for this class is beyond the scope of this article. Please refer to here.

Game Board Design

Every control of the board is named 0-based to avoid any confusion.

Racks

For player 1, there are 7 tiles which are basically 7 label controls. These tiles are placed inside a panel control.

Image 7These are named as p0l0, p0l1 …. p0l6. ‘p’ is for ‘player’, ‘l’ is for label. 0-based indexing is used as the naming convention which provides mapping benefits that will be discussed later.

Similarly tiles for other players are named. E.g.: p1l0, p1l1 etc.

There are 4 panels for 4 players, and 7x4 = 28 label controls for all the players.

Board

The board is designed in the following manner.

Moving from left to right is treated as X-axis, moving from top to bottom is the Y-axis. This is also 0-based which also provides mapping benefits that will be discussed later.

For the first row, the labels are designed in the following manner. ‘l’ is for label.

l0_0, l1_0, l2_0, …..... l14_0.

Image 8

The intention of such naming is each label maps to the corresponding cell in the matrix. For example, l4_12 corresponds to the cell [4,12] in the matrix. It is worthwhile mentioning that, the matrix is the actual calculative means to validate the words.

Loading (Showing) Players

The design has all the four players in place; initially they are all hidden. It is just the visibility logic that displays them according to the number of players. This is done in the ‘LoadPlayers’ method. It loops through all the available players (Players.Count) and makes their corresponding rack, mascot and score controls visible.

Control ctl;
for (int i = 0; i < Players.Count; i++)
{
    ctl = Controls.Find($"rack{i}", true)[0]; // E.g.: rack0.
    ctl.Visible = true;
……………………………………………..
……………………………………………..

Resolving First Player

The first player is decided by randomly drawing a letter from the bag for each player. It is loaded on the first cell of each player’s rack. The player who draws a letter closest to ‘A’, or draws a blank tile, will start the game. This is done in the ‘ResolveFirstPlayer’ method.

The following segment first checks if anybody has drawn a blank tile. If anybody draws a blank tile, then it simply returns from here with the player number. S/he will be the first player.

// If any player draws a blank tile, s/he will be the first to start.
// Visibility confirms if the player is available. Just to remind, if 3 players were to play,
// then p3l0 will not be visible, hence will be out of context and consideration.

if (p0l0.Visible && string.IsNullOrEmpty(p0l0.Text))
    return 0;
if (p1l0.Visible && string.IsNullOrEmpty(p1l0.Text))
    return 1;
if (p2l0.Visible && string.IsNullOrEmpty(p2l0.Text))
    return 2;
if (p3l0.Visible && string.IsNullOrEmpty(p3l0.Text))
    return 3;

If nobody draws a blank tile, then the next step is to decide who draws a letter closest to ‘A’. This is simple – just take the distances and decide which one is the lowest.

We start with a higher value (999). The maximum distance might be (Z-A) = 25. So, any number above 25 should do. It is a simple arithmetic – any distance that is lower than the preceding is assigned as the current minimum. Eventually the player with the minimum distance is obtained. This is a single sweep, hence is O(n).

int min = 999, distance;                       // Start with a higher number.
int minWinner = -1;                            // Any number other than 0,1,2,3 will do.

for (int i = 0; i < Players.Count; i++)        // Loop through all players.
{
    ctl = Controls.Find($"p{i}l0", true)[0];   // E.g.: p2l0.
    distance = ctl.Text.ToCharArray()[0] - 'A';

    if (distance < min)        // E.g.: if text of p2l0 is 'E', then distance is 4.
    {
        min = distance;        // then make min the current distance,
        minWinner = i;         // and change the minWinner to the current player.
    }
}

If more than one player draws the same letter that is closest to ‘A’, then the one who is earliest in the clockwise turn wins the toss.

Activating/Deactivating Players

This is simple – just loop through all the players, activate the current player and deactivate others. The player number is passed as the parameter. Only this player will be enabled, others will be disabled.

This is accomplished through the ‘ActivateDeactivatePlayers’ method. The enabling and visibility is toggled through the Boolean check if the current player is the current loop variable.

private void ActivateDeactivatePlayers(int player)
{
    ……………………………………………..
    ……………………………………………..
    // Loop through all players.
    for (int i = 0; i < Players.Count; i++)
    {
        ……………………………………………..
        ……………………………………………..
        // If current player's turn, then enable name and rack.
        rackCtl.Enabled = scoreCtl.Enabled = i == player;

        // And make the green button visible.
        turnButtonCtl.Visible = i == player;
    }

This can be a single statement anyway, just separated them for understanding. Otherwise this could be as simple as:

for (int i = 0; i < Players.Count; i++)
{
    ……………………………………………..
    ……………………………………………..
    rackCtl.Enabled = scoreCtl.Enabled = turnButtonCtl.Visible = i == player;
}

Loading Letters in Bag

The number of each letter in the bag is decided according to pre-defined rules. For example, there should be exactly 4 ‘D’s in the bag – no more, no less. This is accomplished by looping through 65 (ASCII for ‘A’) to 90 (ASCII for ‘Z’). For each ASCII value, the corresponding letter is added to the letter bag sequentially. To remind, it doesn’t matter at this point whether they are sequential or not as they are picked randomly during the game.

The two blank tiles are manually added in the last two indices.

public void LoadLetterBag()
{
    for (int letter = 65, i = 0; letter < 91; letter++)
        switch (letter)
        {
            case 65:    // Should be 9 'A'
                for (int count = 0; count < 9; count++)
                    LetterBag[i++] = (char)letter;
                break;
            case 66:    // Should be 2 'B'
            case 67:    // Should be 2 'C'
            case 70:    // Should be 2 'F'
            case 72:    // Should be 2 'H'
            case 77:    // Should be 2 'M'
            case 80:    // Should be 2 'P'
            case 86:    // Should be 2 'V'
            case 87:    // Should be 2 'W'
            case 89:    // Should be 2 'Y'
                for (int count = 0; count < 2; count++)
                     LetterBag[i++] = (char)letter;
                break;
            ……………………………………………..
            ……………………………………………..
            ……………………………………………..
        }
    LetterBag[98] = LetterBag[99] = ' ';        // Two blank tiles.
}

Image 9

Loading Letters in Racks

Letters are loaded through random letter picking from the bag (the ‘LetterBag’ array). A random index is obtained. If the letter in that index is null (‘\0’), then the loop continues, and another index is obtained.

private void LoadSingleLetter(int player, int cellNum)
{
    ………………………….
    ………………………….
    while (true)
    {
        index = Rnd.Next(0, NUM_LETTERS_IN_BAG);
        c = LetterBag[index];
        if (c == '\0')
            continue;
    ………………………….
    ………………………….
    LetterBag[index] = '\0';    // The letter at the index is taken. Hence make it empty.
    break;
    }
}

Image 10

It might be noted that Rnd.Next() has the lower bound inclusive and the upper bound exclusive. That means, the randomizer picks up to one less than the upper bound given to it. Our 100-letter array has the lower bound 0 and upper bound 100 (NUM_LETTERS_IN_BAG); so, the randomizer correctly picks an index between 0 and 99 inclusive. It might be noted that NUM_LETTERS_IN_BAG is a global variable that changes with time and denotes the current upper bound.

Drag-Drop

The game uses drag-drop events for placing letters on the board. When a mouse starts dragging a letter from the rack, that letter fires a mouse-down event. When the letter is dragged over a cell on the board, that board cell fires drag-enter event. When the mouse finally drops the letter on the board cell, then that cell fires a drag-drop event.

Now let’s ponder into the details of what happens at each event.

Drag Start

Suppose a letter is being dragged from the first cell of the first player’s rack.

Image 11

Following is the ‘MouseDown’ event of the first cell of rack of the first player. The first line adds the letter in a variable ‘Source’. The second line starts a drag-drop operation with the letter that was on that cell.

private void p0l0_MouseDown(object sender, MouseEventArgs e)
{
    string letter = AddSource(sender as Label);
    (sender as Label).DoDragDrop(letter, DragDropEffects.Copy);           
}

The generalized ‘AddSource’ method intersects the label to get the player’s number, the cell of his/her rack where the mouse was down, and the letter that was on that cell. If the label’s name is ‘p0l3’, then the player’s number is 0 (first player), the cell number is 3.

private string AddSource(Label sender)
{
    string letter = sender.Text;
    string name = sender.Name;

    int player = Convert.ToInt16(name.Substring(1, 1));
    int cellInRack = Convert.ToInt16(name.Substring(name.IndexOf("l") + 1));
    Source.Add(player, cellInRack, letter);
    return letter;
}

This is a very easy string operation. This is a common method that is called by all the 28 mouse-down events (4 players x 7 cells each).

In summary, this method keeps track of the source cell details where a mouse drag starts.

Drag Enter

Drag enter occurs when the player moves the dragging mouse on a cell in the board. This simply instructs that, after the drag-drop operation completes, the letter is to be copied from the source.

private void l0_0_DragEnter(object sender, DragEventArgs e)
{
    e.Effect = DragDropEffects.Copy;
}

There needs to be 225 ‘DragEnter’ events for all the 225 cells (15 x 15) of the board. And each event needs to put the same line. Unfortunately, there is no mechanism to assign them at once to avoid 225 such events.

Dropping

Drag drop occurs when the player finally releases the mouse button on a cell on the board.

Image 12

private void l0_0_DragDrop(object sender, DragEventArgs e)
{
    ProcessDragDrop(sender as Label, e);
}

The event calls the generalized method ’ProcessDragDrop’.

private void ProcessDragDrop(Label boardCell, DragEventArgs e)
{
    string name = boardCell.Name;
    int x = Convert.ToInt16(name.Substring(1, name.IndexOf("_") - 1));
    int y = Convert.ToInt16(name.Substring(name.IndexOf("_") + 1));
    Control rackCell = Controls.Find($"p{Source.Player}l{Source.Cell}", true)[0];
    ……………………………………………………
    ……………………………………………………
}

This method segregates the label (where the mouse was released) to obtain the axis. Also, it obtains the source label (rackCell) from where the mouse drag started.

For example, if the board cell is ‘l12_14’, then axis of the cell where mouse was released is: x = 12, y = 14.

Next, the following line checks if there was any content in the source cell. As seen from the message, this is only possible when a player forgetfully tries to drag the same letter that s/he dragged already in the current turn.

if (rackCell.Text.ToCharArray().GetUpperBound(0) == -1)
{
    MessageBox.Show("Can't drag this tile. It was already dragged before; there is nothing in it.");
    return;
}

After that, the following line checks if the cell where the letter was dropped already contains another letter in it.

else if (matrix[x, y] != '\0')
    MessageBox.Show("Occupied. Can't place here.");

Then it checks if the dragged letter is the blank tile. Just to remind, there are two blank tiles in the bag. If it is a blank tile, then it allows the player to choose a letter for the tile (WildCardChooser form). That other form sets the chosen wildcard as entered by the user.

if (Source.Letter == ' ')
{
    WildCardChooser card = new WildCardChooser(this);
    card.Top = Cursor.Position.X;
    card.Left = Cursor.Position.Y;
    card.ShowDialog();

    if (WildCard == '\0')
    {
        MessageBox.Show("You didn’t choose any letter for the wild card. Choose a letter for the wild card or proceed with another letter in your rack.");
        return;
    }

    // This contains special contents (if any) of the board before dragging. E.g.: 2W, 3L etc.
    string specialCellContent = boardCell.Text;
    RackCellList.Push(new RackCell(Source.Player, Source.Cell, WildCard));
    boardCell.Text = WildCard.ToString();
    BoardCellList.Push(new BoardCell(x, y, specialCellContent, WildCard.ToString(), boardCell.BackColor));
    rackCell.Text = string.Empty;

    // After putting the wildcard letter, set the global variable to NULL to be used for the second wildcard.
    WildCard = '\0';
}

RackCellList’ is a stack that contains the dragged cell details (in the rack) in the current turn. ‘BoardCellList’ is a stack that contains the cell details of the board where the mouse drops the letters. The stacks serve three purposes.

  1. Assist with ‘Ctrl+Z’ if the user mistakenly drops the letter(s) in wrong cells.
  2. Assist in scoring.
  3. Assist in Reverting back the letters if the word is invalid.

Lastly, if it is a regular letter (any letter from ‘A’ to ‘Z’) drag-drop, then it simply pushes the letter to respective stacks.

// This contains special contents (if any) of the board before dragging. E.g.: 2W, 3L etc.
string specialCellContent = boardCell.Text;
boardCell.Text = (string)e.Data.GetData(DataFormats.Text);
BoardCellList.Push(new BoardCell(x, y, specialCellContent, boardCell.Text, boardCell.BackColor));
matrix[x, y] = boardCell.Text.ToCharArray()[0];

RackCellList.Push(new RackCell(Source));
rackCell.Text = string.Empty;

In summary, there are 225 drag-enter events and 225 drag-drop events for the 225 cells (15 x 15) of the board.

Exchange of Letters

Selecting Letters

A player can exchange some or all his/her letters in the current turn (given that there are at least 7 letters in the bag). This is the situation where it seems impossible to make a valid word with the letters and the player wants to try luck with different letters. After the exchange the current turn is forfeited, and the next player carries on.

The exchange is facilitated first by selecting the letters, then pressing the ‘Exchange’ button. If the user presses CTRL-key, then clicks on a letter, that letter is added in a list of ‘Letters’. The letter background turns into Crimson to mean it was selected. If a letter is already selected (Crimson), and the player clicks on that letter while pressing CTRL-key, then that reverts the selection – the letter is removed from the list and the tile colour is set back to normal control colour (SystemColors.Control).

Image 13

List<Letters> ExchangingLetters = new List<Letters>();
private readonly Color EXCHANGE_LETTER_CELL_COLOR = Color.Crimson;

private void AddLettersToExchangeTotheList(Label label)
{
    // If the label's back colour is already crimson, then it removes the letter from the list.
    if (label.BackColor == EXCHANGE_LETTER_CELL_COLOR)
    {
        // Change the colour back to system control colour.
        label.BackColor = SystemColors.Control;

        // Remove the letter from the exchanging list.
        ExchangingLetters.Remove(ExchangingLetters.Find(x => x.Letter == label.Text.ToCharArray()[0]));
    }
    else
    {
        int pos = Convert.ToInt16(label.Name.Substring(3));
        ExchangingLetters.Add(new Letters(label.Text.ToCharArray()[0], pos));
        label.BackColor = EXCHANGE_LETTER_CELL_COLOR;
    }
}

Exchanging Letters

When the player presses ‘Exchange’, the letters in the list ‘ExchangingLetters’ are returned to the bag, new letters are brought in and the crimson colour is replaced with regular control colour. New random letters are drawn which are randomly selected. (Don’t blame the game if the same letters are returned. I remember an episode from Mr. Bean where he tried to return a food item but got the same item brought back to his table :)).

Before an exchange, there is a minor check if the user placed any letter on the board in the current turn. Placing letters on board and opting for exchange are mutually exclusive – they cannot happen together. In that case it displays a message and doesn’t exchange. If the player really opts for exchange, then the letters first should be taken back to rack (CTRL+Z). Whether the player placed letters on the board is checked by the two lists maintained for the movements. Logically checking either will do, as both will have the same numbers of entries.

if (RackCellList.Count > 0 || BoardCellList.Count > 0)
{
    MessageBox.Show("You already dragged letters on the board. Can't exchange now. First return the letters (CTRL+Z).");
    return;
}

If this check passes, then the letter(s) are flown back to the bag.

foreach (Letters l in ExchangingLetters)
{
    ctl = Controls.Find($"p{CurrentPlayer}l{l.Pos}", true)[0];      // E.g.: p2l0.

    // Obtain absolute location of the rack's cell from the top left of the window.
    cellAbsLoc = ctl.FindForm().PointToClient(ctl.Parent.PointToScreen(ctl.Location));

    lblFlyingLetter.Text = ctl.ToString();

    // Change the rack cell back colour to normal.
    ctl.BackColor = SystemColors.Control;
    ctl.Text = string.Empty;
    FlyLetter(cellAbsLoc, FlyingLetterInitialPosOnBag);

    LetterBag[l.Pos] = l.Letter;
    lblLettersRemaining.Text = $"Letters remaining: {++NUM_LETTERS_IN_BAG}";
    Application.DoEvents();
}

Image 14

After the letters are returned to the bag, new letters are brought in the empty cells from the bag.

foreach (Letters l in ExchangingLetters)
    LoadSingleLetter(CurrentPlayer, l.Pos);

Image 15

Finally, the list of exchanging letters (‘ExchangingLetters’) is cleared off.

ExchangingLetters.Clear(); // Clear the exchanging letters list.

Animation Effects

There are two main animation effects in three possible scenarios. One is bag-shaking, the other one is flying the letters across the bags, racks and board.

There is only one possible scenario of bag-shaking. Whenever a new set of letters are to be drawn from the bag, then the bag shakes to mean letter(s) are going to be drawn from the bag.

The three possible scenarios for the letter flights:

  1. Fly from bag to rack.
  2. Fly from rack to bag.
  3. Fly from board to rack.

Bag-shaking

This is very simple. A timer is used to tick every 50 milliseconds, moving the bag up and down. During the ticks it forces the thread to sleep 10 milliseconds (THREAD_SLEEP=10) to make the movement visible. If this sleep is not induced, then it would shake lightning fast; so the player(s) won’t be able to perceive the shaking effect.

pbLetterBag.Top += SHAKE_PIXELS;
SHAKE_PIXELS = -SHAKE_PIXELS;
Thread.Sleep(THREAD_SLEEP);
Application.DoEvents();

Image 16

pbLetterBag’ is the image control containing the bag. On first tick it moves down 20 pixels (SHAKE_PIXELS=20). Then ‘SHAKE_PIXELS’ negates itself; it becomes (-20) now. In the next tick it moves up (pbLetterBag.Top += SHAKE_PIXELS). In the next tick it becomes (-(-20) = +20) (SHAKE_PIXELS = -SHAKE_PIXELS), so it moves down. And things keep going like this for a while.

After the bag shakes a certain number of times (MAX_SHAKE_COUNT=13), then the timer stops, and the bag is at rest.

Fly Letters between the Bag, Board and Racks

The flight of letters is a simple animation that flies a label in between the letter bag, rack cells, and board cells. It creates a simple visual effect for the letter movement. Metaphorically this can be explained as a source airport, a destination airport, an aeroplane and a passenger. A label control is used as the aeroplane. The letter that needs to fly is the passenger of the plane. Source is the location from where it takes off, destination is where it lands on.

The method ‘FlyLetter’ is the control tower of the flying logic. It receives two parameters from the caller – the source and the destination.

The method first calculates the axis difference from destination to source. It has a pre-defined frame count in the configuration file (configurable). Then it determines how many pixels to fly at each direction by dividing the difference by the frame count (e.g. 25 frames). It rounds up to 4th decimal (e.g.: 25.2153).

// Determine axis distance (destination - source).
diff = new Point(dest.X - source.X, dest.Y - source.Y);

// Determine stepping needed by the letter - how many pixels to fly through x and y axes.
SteppingX = Math.Round(diff.X / MAX_ANIMATION_FRAMES, 4);
SteppingY = Math.Round(diff.Y / MAX_ANIMATION_FRAMES, 4);

Image 17

What is the need for the double-precision round value? What if just integer round value was taken? Let’s clarify that through an example.

Suppose the flying label (the aeroplane) is initially at (750, 50). Let’s say the stepping X = -10.48, stepping y = 1.52.

The first frame takes the label to: (750 - 10.48, 50 + 1.52) = (739.52, 51.52).

Because pixels need to be integers, so this is rounded to (740,52).

However, we recorded the last double-precision location in a global variable. We use that last-recorded double-precision location to calculate the next move.

Hence, the next location is (739.52 – 10.48, 51.52 + 1.52) = (729.12, 53.04).

The rounded pixel values for this is (729, 53).

Now let’s see what happens if integer steppings (rounded) are used.

The first frame is (740, 12).

The next frame after rounding is (740 – 10.48, 52 + 1.52) = (729.52, 53.52) = (730, 54).

See, this is not the same as (729, 53), which is calculatedly correct and more precise location. If the latter is used then the tile would move at flat rate and would fly the label outside the destination, then will drop it there. A little jerk of animation would be visible. Metaphorically, the airplane would fly beyond runway and will need a second attempt to land correctly.

A For-Loop facilitates the fly – it loops for the maximum frame count (MAX_ANIMATION_FRAMES=25), at each step it calculates the next location of the tile, flies it there and waits a few moments (THREAD_SLEEP=10) so the player can perceive the flight.

for (int i = 0; i < MAX_ANIMATION_FRAMES; i++)
{
    lastXOfFlyingLabel += SteppingX;
    lastYOfFlyingLabel += SteppingY;

    // Convert the double-precision to int, as pixel needs to be int.
    lblFlyingLetter.Left = (int)lastXOfFlyingLabel;

    // Convert the double-precision to int, as pixel needs to be int.
    lblFlyingLetter.Top = (int)lastYOfFlyingLabel;

    // Unless forced, the effect will not be visible.
    Application.DoEvents();

    // Allow delay else this will be too fast to see.
    Thread.Sleep(THREAD_SLEEP);
}

Bag Shrinking, i.e., Array Shrinking

After a turn is complete (either successful or failure), the array needs to shrink. This is a popular interview question asked by interviewers.

Let’s think of what would happen if we leave it as such after the moves. The array would eventually contain more and more NULL (\0) characters (remember, each time letters are drawn from the bag, the corresponding entry is nullified to mean the letter is taken). This makes the randomizer difficult to find a letter eventually; it becomes a bottleneck slowly.

Image 18

For a hundred cell-array this might not be recognizable, but if we consider millions of cells in the array, then down the line it becomes a bottleneck for the processor to find a non-blank cell.

The logic used here is a single sweep of the array; the sweep proceeds from both ways – from the front as well as the back, hence O(n). The logic is as following:

  1. Start from the beginning of the array. Let’s call the index indicator ‘pos’.
  2. As soon as a NULL cell is found (the letter of that cell was drawn in the current turn), then mark that position. Let’s name it ‘pos’.
    1. Now start traversing from the end.
    2. As soon as a non-NULL cell is found, then mark that position. Let’s name it ‘lastPickPos’.
    3. Copy the letter of the ‘lastPickPos’ to ‘pos’.
    4. Nullify the letter of ‘lastPickPos’.
  3. Loop until the ‘pos’  crosses ‘lastPickPos’. At this point all the NULL values are compromised (moved to the end).
  4. Now trim the trailing NULLs. This shrinks the array.

Image 19

private void ShrinkLetterBag()
{
    int len = LetterBag.GetUpperBound(0); // Initial length of the bag.
    int lastPickPos = len; // Last index is the length of the array.

    // Start traversing from the beginning.
    for (int pos = 0; pos < lastPickPos; pos++)      // Start traversing from the beginning.
    {
        if (LetterBag[pos] == '\0')                  // If a NULL is detected
        {
            while (lastPickPos > pos && LetterBag[lastPickPos] == '\0')
                lastPickPos--;
            LetterBag[pos] = LetterBag[lastPickPos]; // Copy that last character to the beginning NULL index.
            LetterBag[lastPickPos] = '\0';           // Nullify the last pick index.
        }
    }
    NUM_LETTERS_IN_BAG = lastPickPos;
    Array.Resize(ref LetterBag, NUM_LETTERS_IN_BAG);
}

The trimming (Array.Resize) is actually not needed as the randomizer deals with upper bound only which points next to the last non-null character in the array.

Undoing Moves

Players can undo the letter drags during a turn. To undo they need to simply press CTRL+Z as many times as needed. This is facilitated through the following logic.

Each time a letter is being dragged from a cell, a ‘mouse down’ event is triggered from that cell. In the event, the details of the cell (player, position of the cell on the rack and the letter that was in the cell) are added to a variable of type ‘RackCell’.

private string AddSource(Label sender)
{
    string letter = sender.Text;
    string name = sender.Name;

    int player = Convert.ToInt16(name.Substring(1, 1));
    int cellInRack = Convert.ToInt16(name.Substring(name.IndexOf("l") + 1));
    Source.Add(player, cellInRack, letter);
    return letter;
}

After the letter is dropped on a cell on the board, that cell triggers a ‘drag drop’ event. This event adds those details to a list (stack).

Stack<RackCell> RackCellList = new Stack<RackCell>();
……………………………
……………………………
RackCellList.Push(new RackCell(Source));

Why stack is chosen over other data structures for this? You guessed right. CTRL+Z needs to work in the reverse manner of the drag-drops. The last action is to be reversed first when CTRL+Z is pressed.

The CTRL+Z is facilitated by overriding the form’s default key-stroke processor ‘ProcessCmdKey’.

protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{ …………………………… …………………………… }

CTRL+Z is comprehended by bitwise OR of CTRL-key with ‘Z’.

if (keyData == (Keys.Control | Keys.Z))
{ …………………………… …………………………… }

If it is a CTRL+Z, then simply perform the following actions:

  1. Pop the details of the rack cell.
  2. Pop the details of the board cell.
  3. Set the rack-cell’s text to what it was before dragging.
  4. Set the board-cell’s text to what it was before dropping.
  5. Set the corresponding cell in the matrix to NULL.

Removing the entry from the stacks is done automatically by the pop() operation.

Validity of a Word

To be a valid word, it needs to pass the following.

  1. It should have a valid orientation.
  2. It should have a valid adjacency.
  3. If it is the first word, then it should cross through the central star tile.
  4. For other times, the word should cross through or touch existing letters on the board.

1) Valid Orientation

Validity of orientation is confirmed by the ‘ValidOrientation’ method.

A word can have either horizontal or vertical orientation. First it checks if it is a single drop – the player dropped a single letter only. In this case it tries to infer the intended orientation by checking with the existing words on the board. This is simple – just check if there is a letter to the top or to the bottom of the current letter – in that case, the inferred direction is vertical. On the other hand, if there is a letter to the left or to the right of the letter, then this is a horizontal orientation.

int x = boardCells[0].X;
int y = boardCells[0].Y;
int yBelow = y + 1;
int yAbove = y - 1;
int xLeft = x - 1;
int xRight = x + 1;

// If there is a letter above or below the singly-placed letter, then the inferred direction is vertical.
if ((yAbove >= 0 && matrix[x, yAbove] != '\0') || (yBelow < GRID_SIZE && matrix[x, yBelow] != '\0'))
    CurrentWordDirection = WordDirectionEnum.Vertical;
……………………………
……………………………

For example, if a single letter ‘A’ is dragged on the board, it finds a letter ‘C’ above it, hence it inferred the intended orientation as vertical.

Image 20

If the player dragged multiple letters on the board, then it first determines the orientation by checking the first two letters.

// If this is a vertical drop to the previous drop, then mark the direction as vertical.
if (boardCells[0].X == boardCells[1].X && Math.Abs(boardCells[0].Y - boardCells[1].Y) >= 1)
    CurrentWordDirection = WordDirectionEnum.Vertical;

// If this is a horizontal drop to the previous drop, then mark the direction as horizontal.
else if (boardCells[0].Y == boardCells[1].Y && Math.Abs(boardCells[0].X - boardCells[1].X) >= 1)
    CurrentWordDirection = WordDirectionEnum.Horizontal;

Then it loops through the rest of the letters and checks if the rest of the letters follow the same orientation or not. If all follow the same orientation, then it confirms that by returning true. Otherwise, it returns false.

WordDirectionEnum dir = WordDirectionEnum.None;
for (int i = 1; i < boardCells.Count - 1; i++)
{
    // Check orientation of each subsequent drops.
    if (boardCells[i].X == boardCells[i + 1].X && Math.Abs(boardCells[i].Y - boardCells[i + 1].Y) >= 1)
        dir = WordDirectionEnum.Vertical;
    else if (boardCells[i].Y == boardCells[i + 1].Y && Math.Abs(boardCells[i].X - boardCells[i + 1].X) >= 1)
        dir = WordDirectionEnum.Horizontal;

    // If this orientation is not the same as the first, then this must be an abnormal orientation.
    if (dir != CurrentWordDirection)
    {
        MessageBox.Show("Abnormal orientation detected. Letters must be placed horizontally or vertically. Cannot proceed!"
            , Properties.Resources.APP_TITLE, MessageBoxButtons.OK, MessageBoxIcon.Information);
        CurrentWordDirection = WordDirectionEnum.None;
        return false;
    }
}

For example, if the player placed ‘A’, ‘T’, ‘E’ and ‘R’ on the board, then it first infers the direction as vertical by checking the first two letters ‘A’ and ‘T’. Then the loop continues through the rest of the letters ‘E’ and ‘R’ and expects that they maintain the same orientation.

Image 21

If not, then this is an invalid orientation. For example:

Image 22

Ideally this would not happen as the player is not likely to play random. But care should be taken for everything, however odd that might be.

2) Valid Adjacency:

To make a valid word, the letters need to be adjacent either by themselves or with existing letters on the board.

Let’s try to explain the adjacency check for a horizontally directed word. First it sorts the letters from left to right.

// Clone the list sorted from top to bottom.
List<BoardCell> boardCells = boardCellList.Select(a => a).OrderBy(b => b.X).ToList();

Then it loops through them from left to right. If there is more than one cell gap between two consecutive letters, then it checks if there is any existing letter in those gap-cells.

for (int i = 0; i < boardCells.Count - 1; i++)
{
    // If the distance between the consecutive letters are more than a cell, that would mean
    // the cells in between must contain existing letters. Else this would be deemed invalid placement.
    if (boardCells[i + 1].X - boardCells[i].X > 1)
        // Start traversing from the next cell of the left cell, walk towards the left cell of the right cell.
        for (int j = boardCells[i].X + 1; j < boardCells[i + 1].X; j++)
            // If that cell is empty, means this is an invalid placement.
            if (matrix[j, boardCells[i].Y] == '\0')
            {
                MessageBox.Show("Letters don't have valid horizontal adjacency. Cannot proceed!"
                    , Properties.Resources.APP_TITLE, MessageBoxButtons.OK, MessageBoxIcon.Information);
                return false;
            }
}

For example, let’s say there are existing words ACT, STAY, SOOTH, HOE, OR, PAT on the board and the player tries to make SCATTERS through them. The player placed S, A, T, S on the board. Horizontally S and A has more than one cell gap in between. Hence there must be existing letters in between (which is C). Then there are three cells gap between T and S, which must have letters in between (which are T, E, R). Hence this is a valid adjacency.

Image 23

An invalid adjacency might be the following:

Image 24

3) Crossing Through Central Tile for the First Word

This is very simple. The star-tile location is (7, 7). It is just a matter of looping through all the placed letters and see if any of then has the location (7, 7). The logic is applied in the method ‘FirstWordThroughCentralStarTile’.

foreach (BoardCell c in boardCellList)  // Loop through all the cells.
    // If any of them contains (7, 7) - the central tile, then return true.
    if (c.X == 7 && c.Y == 7)
        return true;

MessageBox.Show("The first word should pass through the central star tile. Use CTRL+Z to return the letters, then place through the star."
    , Properties.Resources.APP_TITLE, MessageBoxButtons.OK, MessageBoxIcon.Information);

// If none of them contains (7, 7), then return false.
return false;

4) Crossing (or touching) existing words

Check for crossing can be done in two ways.

  1. Look at the surrounding tiles of the word on the board and see if any of them is grey coloured (SystemColors.ControlDarkDark). If any of the surrounding cells is grey, then that would mean the current word crossed through (or touched) an existing word. If none of the surrounding tiles of any letters of the current word is grey, that would mean the player placed the word isolated, hence would be deemed invalid placement (words should cross each other).
  2. Look at the surrounding cells of the matrix and see if any of them is non-NULL. A non-NULL would mean there is an existing valid word on the board, hence the current word placement is valid (as it touches or crosses through an existing word). Else, this is invalid.

Either one can be implemented. In this project the second option is chosen. The first one checks UI element colour, hence would require more time. Also, the first approach is a shortcut, while the second one is more thought-provoking in terms of coding through a 2D character array. Further, it is faster as it uses the character matrix, not any UI element.

The check is done in the method ‘CurrentWordCrossedThroughExistingWord’. The method returns true if the word crossed through or touched an existing word on the board. It accepts four parameters, the list of letters that the player dragged on the board, the character matrix, the current player and the current turn. The last two parameters are needed to record the move; they don’t play any role in the logic for checking cross through.

There can be two orientations  – horizontal, and vertical. In this part, only the horizontal orientation is explained; the vertical one can be understood through this.

First, the letters are sorted from left to right.

// Take a clone of the stack and order from left to right.
letters = letters.Select(a => a).OrderBy(a => a.X).ToList();

Then It checks the left of the first letter – if there is any letter there, then that would mean the word crossed through (or touched) an existing word on the board. It also needs to make sure that this check doesn’t fall outside the bounds (x >= 0), as that would generate runtime error.

x = letters.First().X - 1;              // Check left of the first letter.
y = letters.First().Y;
if (x >= 0)                             // If not falling out of grid.
    if (matrix[x, y] != '\0')           // If there is a non-NULL character in the cell.
        return true;                    // Then return true.

For example, if CATER is already on the board, and the player makes CAP by placing A and P, then it checks if there is a letter to the left of ‘A’ (which is true).

Image 25

If there is no letter to the left of the first letter, then check if there is any letter to the right of the last letter. This also needs to make sure that it doesn’t fall outside the boundary (x < GRID_SIZE).

x = letters.Last().X + 1;               // Check right of the last letter.
y = letters.Last().Y;
if (x < GRID_SIZE)                      // If not falling out of grid.
    if (matrix[x, y] != '\0')           // If there is a non-NULL character in the cell.
        return true;                    // Then return true.

For example, if POT is already on the board, and the player makes CAP by placing C and A, then it checks if there is a letter to the right of ‘A’ (which is true).

Image 26

If none of the above two could determine the truth, then the next step is to loop through all the letters and check if any of the above and below cells has a letter – obviously, if a letter is found, that would mean the current word crossed through (or touched) an existing word on the board. Again, this needs to make sure that the check doesn’t fall outside the bounds (y >= 0, y < GRID_SIZE).

foreach (BoardCell c in letters)
{
    x = c.X;
    y = c.Y - 1;                        // Check top.
    if (y >= 0)                         // If not falling out of grid.
        if (matrix[x, y] != '\0')       // If there is a non-NULL character in the cell.
            return true;                // Then return true.

    x = c.X;
    y = c.Y + 1;                        // Check bottom.
    if (y < GRID_SIZE)                  // If not falling out of grid.
        if (matrix[x, y] != '\0')       // If there is a non-NULL character in the cell.
            return true;                // Then return true.
}

For example, If there are existing words on the board (PAN, NO and ONE) and the player wants to coin the word CANOPY, then we have C, A, P, Y at our hand. We check each of the letters’ top and bottom cells to see if there is any letter there. Since this is false (none of C, A, P, Y has a letter above or beneath), so it is still not decided if it touched any existing word. Remember, we our using our BoardCellList which have C, A, P and Y only. The BoardCellList doesn’t have a clue of N and O which are in between and that is what we are going to discover next.

Image 27

If the truth about crossing still could not be determined, then the last thing to check is if there are existing letters in between the letters. Start with the right cell of the leftmost cell, walk towards the left cell of the rightmost cell, and see if there are existing letters in all the cells in between. The caveat here is, we have to check only the letters that are not in the current list.

for (x = letters.First().X + 1, y = letters.First().Y; x < letters.Last().X; x++)
    // If this is not already in the current player's cell list.
    if (letters.FirstOrDefault(a => a.X == x && a.Y == y) == null)
        if (matrix[x, y] != '\0')       // If there is a non-NULL character in the cell.
            return true;                // Then return true.

For example, let’s examine with the previous example. We start walking from A until P (remember, player placed C, A, P, Y) and see there is any letter in between. First found A, but since A is in the current BoardCellList, so this is not an existing letter. We proceed on and found N. Since we found N (i.e., a non-NULL character), we can straight away proclaim that we have found the truth – the word crossed through (or touched) an existing letter. We don’t need to proceed further as any other letter should have been already verified beforehand.

Image 28

If the truth still could not be determined and we reached this point, then this would mean the intended word didn’t cross through or touched any existing letter on the board; hence it records the situation and returns false.

MessageBox.Show("The word didn't cross through, or touched another word. Not a valid placement."
    , Properties.Resources.APP_TITLE, MessageBoxButtons.OK, MessageBoxIcon.Information);
Players[currentPlayer].ScoreDetails.Add(new TurnsWithScores(currentTurn, $"Disjoint word coined.", null));
return false;

The check for vertical word is similar to this, so that is not explained.

Scoring

Scoring logic is a bit complex, so are the data structures used for the purpose. There are two main steps – loading the dictionary and validating the created word.

Loading Dictionary

For scoring, obviously a dictionary is needed. An open source JSON dictionary from here is used for the purpose (which as 172,819 words).

[
    "aa",
    "aah",
    "aahed",
    "aahing",
    "aahs",
    "aal",
    "aalii",
    "aaliis",
    "aals",
    ……………………………….
    ……………………………….
    "zymurgy",
    "zyzzyva",
    "zyzzyvas"
]

It may be noted that this is an array of strings. At the very beginning of the game the words from the dictionary are loaded in ‘Words’ which is a JToken list. First we obtain the entire dictionary in a string variable ‘jsonWords’, then that is deserialized in a JArray variable named ‘json’. Finally, the JArray is converted to a JToken list.

private static List<JToken> Words;
string jsonWords;
using (StreamReader reader = new StreamReader(WORD_FILE_NAME))
    jsonWords = reader.ReadToEnd();
JArray json = (JArray)JsonConvert.DeserializeObject(jsonWords);
Words = json.ToList();

Valid Main Word

First, we check if the main coined word is valid or not. The validation is delegated to the method ‘CheckValidity. If it is a horizontal word, then it moves first to the beginning of the word.

while (--x >= 0)
    // First walk towards the left until you reach the beginning of the word that is already on the board.
    if (matrix[x, y] == '\0') break;

// Keep a track of where (x, y) this word started on the board.
startX = ++x;
startY = y;

For example, if the player placed A and P on the board (there is an existing word CATER), then we start with A and walk towards left until we reach the beginning of the intended word (CAP). Remember BoardCellList has A and P only, it doesn’t have any slightest idea of C.

Image 29

Then we start walking to the end of the word. During the walk it also checks if this is a blank tile, or if there is a premium content. Also, it keeps recording the score details for each individual letter. The ‘GetPreimumContent’ method simply checks the current board cell content if there was a premium content (2W, 3W, 2L, 3L) there. The ‘CheckIfBlankTile’ method checks in the global blank tiles list if the current cell has a blank tile there. Gradually it walks along the word to reach the end. Thus, it obtains the complete word.

string premium;
bool blankTile;

// Now walk towards right until you reach the end of the word that is already on the board.
for (int i = 0; x < GRID_SIZE; x++, i++)
{
    if (matrix[x, y] == '\0') break;
    chars[i] = matrix[x, y];
    premium = GetPreimumContent(x, y, boardCellList);
    blankTile = CheckIfBlankTile(x, y);
    score.Add(new IndividualScore(blankTile ? ' ' : chars[i], premium, new Point(x, y)));
}

string str = new string(chars);
str = str.Trim('\0');

Then it checks if the word is in the dictionary. If it there, then it further checks if the word is not already coined by any player (including the current player) in the same location. It may be noted that same word can be coined more than once, however they must not be in the same location.

if (Words.IndexOf(str) == -1)   // If the word is not found in the dictionary,
    return false;               // then return negative.

If the word is found in the dictionary, then we must check if it is the same word coined at the same place earlier. If it is, then this is an invalid word.

foreach (PlayerDetails p in Players)
{
    foreach (TurnsWithScores t in p.ScoreDetails)
        if (t.ValidWords != null)
            foreach (ValidWordWithScore v in t.ValidWords)
                if (v.Word == str.ToUpper() && v.Axis.X == x && v.Axis.Y == y)
                {
                    existingWord = true;
                    return false;
                }
}

It may be noted that we have used a Boolean ‘out’ variable ‘existingWord’. This flag determines whether this word should be added to the invalid word list or not. If it is an existing word on the board, then we don’t add it in the invalid word list as it is just an existing word, but not invalid.

If it is a valid word, then it is added to the valid word list with detailed score for each letter, along with the starting position of the word.

validWords.Add(new ValidWordWithScore(str, score, new Point(x, y))); // Add it to the valid list.

After the validity is confirmed, then we need to check if this crossed through (or touched) an existing word (if it is not the first word). This is discussed earlier.

Valid Secondary Words

Finally, it is time to check if secondary words are coined along with the main word. If other words are created along with the main word, then those should also be valid words.

Let’s explain through a horizontally placed word ‘PASCAL’. Let’s say there are existing words on the board – LAMB and LA. The player dragged P, S, C, A, L on the board.

Image 30

The player made the main word PASCAL correctly. However, looking at the board, s/he made another word AS which also needs to be a valid word (which is a valid word anyway).

To do this, we walk through P, S, C, A, L (the player’s dragged letters) and see if any word is coined vertically through them. ‘P’ doesn’t have any letter above or below it; so, it is passed. Next, we find AS; this is a valid word which is just born (not an existing word). So, this word (AS) is awarded to the current player. Gradually we walk up to the last letter (C, A, L) and check for each them.

// Take a clone of the letters that were placed on the board in sorted order from left to right.
List<BoardCell> boardCells = boardCellList.Select(a => a).OrderBy(b => b.X).ToList();
int y = boardCells[0].Y;
int yBelow = y + 1;
int yAbove = y - 1;

// Check if there are letters below or above the horizontally placed word.
// The bottom row to check could be less than or equal to the last row (GRID_SIZE).
// The top row to check could be greater than or equal to the first row (0).
//if (y + 1 < GRID_SIZE)
for (int i = 0, x; i < boardCells.Count; i++)
{
    x = boardCells[i].X;
    if ((yBelow < GRID_SIZE && matrix[x, yBelow] != '\0') || (yAbove >= 0 && matrix[x, yAbove] != '\0'))
        CheckIfValidWord(x, y, WordDirectionEnum.Vertical, matrix, validWords, invalidWords, boardCellList);
}

Reverting

A revert happens when the word is not valid. If the player makes more than one word, then all of them should be valid words as well. If at least one word is invalid, then the revert happens – letters are taken back to the rack and turn moves to the next player.

A couple of synchronous actions takes place.

For each letter that was drawn on the board –

1. Get the location of the letter on the board.
2. Get the location of the letter on the rack where it was staying before dragging.

rackCell = RackCellList.Pop();
boardCell = BoardCellList.Pop();

rackCellCtl = Controls.Find($"p{rackCell.Player}l{rackCell.Cell}", true)[0]; // E.g.: p2l4
rackCellAbsLoc = rackCellCtl.FindForm().PointToClient(rackCellCtl.Parent.PointToScreen(rackCellCtl.Location));

boardCellCtl = Controls.Find($"l{boardCell.X}_{boardCell.Y}", true)[0];      // E.g.: l4_12
boardCellAbsLoc = boardCellCtl.FindForm().PointToClient(boardCellCtl.Parent.PointToScreen(boardCellCtl.Location));

3. If any of the letters was a blank tile, then –
        a. Remove the corresponding entry from the global list ‘BlankTileRackLocations’.
        b. Remove the corresponding entry from ‘BlankTiles’ list.
        c. Set the flying letter text to the blank letter (metaphorically, check in the passenger, and board the plane).
        d. Set the rack cell back colour to blank tile colour (orchid).

4. Else, set the flying letter text to the letter on the board cell.

if (BlankTiles.Count > 0)
{
    RackCell c = BlankTiles.FirstOrDefault(a => a.Player == rackCell.Player && a.Cell == rackCell.Cell);
    if (c != null)
    {
        // Remove the entry from the global 'BlankTilesLocation' if the location matches.
        BlankTileRackLocations.Remove(BlankTileRackLocations.FirstOrDefault(a => a.X == boardCell.X && a.Y == boardCell.Y));

        lblFlyingLetter.Text = " ";
        BlankTiles.Remove(c);
        rackCellCtl.BackColor = BACK_COLOR_FOR_BLANK_TILE;
    }
}
else lblFlyingLetter.Text = boardCellCtl.Text;

5. If it is the central star cell, then set the font of the board cell to the bigger one (48).

if (boardCell.X == 7 && boardCell.Y == 7)
    boardCellCtl.Font = STAR_CELL_FONT;

6. If premium markers are toggled on, then show the marker (2L, 3L, 2W, 3W) on the board cell (if it was a premium cell).
7. Else just show the premium colour on the cell (if it was a premium cell).

// If premPut back any special content like 2L, 3W etc.
if (PremimumIdentifierToggle)
    boardCellCtl.Text = boardCell.PremiumContent.ToString();
else boardCellCtl.Text = "";                                            // Premium colour only.

8. If this rack cell was a blank tile, then change the board cell back colour to what it was before dragging the blank tile on it.

BlankTileOnBoard blankTile = BoardBlankTiles.FirstOrDefault(a => a.Cell.X == boardCell.X && a.Cell.Y == boardCell.Y);
if (blankTile != null)
{
    boardCellCtl.BackColor = blankTile.Colour;
    BoardBlankTiles.Remove(blankTile);
}

9. Fly the letter back to the rack.
10. Make the corresponding cell in the matrix to NULL (‘\0’).

FlyLetter(boardCellAbsLoc, rackCellAbsLoc);
rackCellCtl.Text = lblFlyingLetter.Text;

// Clear the corresponding cell in the matrix to enable next drag-drop on it.
Matrix[boardCell.X, boardCell.Y] = '\0';

Score Tracking

Each player’s score details are recorded at each turn – whether the wordings were correct, or if it was a pass or if invalid words were coined. This is what is called ‘traceability’ – our work should be traceable and honest in every sphere of life.

To understand the scoring track let’s have a look at the following DGML graph.

The ‘PlayerDetails’ class has an object of ‘TurnsWithScores’ which has an object of ‘DetailedScore’.

At each incident (valid wording, invalid wording, pass) the turn is recorded with valid words (simple string), current turn number and detailed score of that turn (if valid wording). If there are invalid words in the current wording, then the detailed score is null.

The detailed score contains the axis information of the current word, the total score and the actual valid word that was formulated in this turn. It may be noted that ‘TurnsWithScores’ is a list of ‘DetailedScore’; there may be many valid words in the current turn and each one of them will have its own history recorded.

Image 31

It is just a matter of keeping the right data in the right place.

If there is a failure, then it is easy – just add the turn number, the invalid words and the valid words in the current player’s score details.

Players[currentPlayer].ScoreDetails.Add(new TurnsWithScores(currentTurn, $"Invalid word(s):
     {string.Join(", ", invalidWords)}.{Environment.NewLine}{Environment.NewLine}", validWords));

If there is a success, then it is easy as well – just add to the current player’s score details:

  1. the turn number,
  2. the scoring details of each valid word that is coined,
  3. and individual letter score of each valid word.
Players[currentPlayer].ScoreDetails.Add(new TurnsWithScores(currentTurn, str.ToString(), validWords));

Following is a snap of the objects.

Player[0] name: Mehedi, total score is stored in ‘TotalScore’.

Two words so far for this player.

In turn 1, the ‘DetailedScore’ of the valid word keeps the exact message that was displayed.

Score breakdown is visible in the ‘Score’ object. The valid word that was coined is recorded in ‘Word’. The breakdown of the scores is recorded in ‘Score’ object. It keeps the axis of the letter, the premium content if any, the letter-score and the letter itself. The blank tile is a space and doesn’t have a score value.

Image 32

Game End

There are three ways that the game might end.

  1. The game reaches the maximum consecutive fail count (6).
  2. There is no more letter in the bag and the current player’s rack is empty.
  3. There is no more letter in the bag and there are remaining letters in all the players’ racks.

Reaching maximum fail count

If there are 6 consecutive fails (forfeits, exchanges or failed words), then the game ends. If the game ends this way, then the face-value of the remaining letters in each player’s rack is deducted from his/her total score. This is a simple logic – loop through all letters of the player, sum up the face-values, deduct from the score. This is done in the method ‘EndGame()’.

After deduction, whoever scores the highest wins the game.

No more letter in bag, rack is empty

This situation arises when the bag is empty and there is no more letter in the current player’s rack (s/he successfully used up all the letters). In this case two things happen.

  1. the face-values of all the letters in other players’ racks are added to the score of the current player.
  2. For the remaining players, each of their scores is reduced by the remaining letter score totals of his/her own rack.

After this simple arithmetic the highest scorer is the winner.

No more letter in bag, racks are not empty

This situation arises when the bag is empty and there are remaining letters in the current player’s rack (and in other player’s racks as well). It might be noted that in this scenario, other players will have 7 letters remaining while the current player might have less than or equal to 7 letters. This is because bag might become empty while refilling the current player’s rack.

For example, say the current player placed 5 letters in the current turn (2 remaining in the rack), then a refill is opted; say the bag has 3 letters only, hence the current player will have (2+3) = 5 letters after refilling. Hence the condition triggers – the bag is empty and there are remaining letters in all the players’ racks.

In this situation, the same logic for reaching maximum fail count applies – all the players’ scores are reduced by summation of their own remaining letter-scores.

Glitches

I would be grateful if you kindly put in comments if any found. I shall fix in a later release.

Limitations

The two animation effects (bag-shaking and letter flying) occur one after another. However, it involves a lot of thread sleeps and forcing the process to complete. At times based on the processor speed, these two effects might not sync in properly.

Future Works

A lot of work can be done on the project.

  1. Introduce timed challenge.
  2. Play against computer. This would involve anagramming algorithms and sophisticated AI approaches.
  3. While there are a lot of dances around the board, why not add some music :)?
  4. Provide a better mechanism for syncing the two animation effects properly.
  5. Add timed challenges.
  6. Extend the game to different Unicode languages. Create rules and scores for different Unicode languages.
  7. To link with Google dictionary web service (I don’t know if there is any; haven’t done any research on it yet – there are some other paid services though). The API can be called to fetch the meaning when a word is coined. For example, personally I didn’t know there is a valid word ‘AA’ that has a meaning ‘basaltic lava forming very rough, jagged masses with a light frothy texture’ (– from Google). I just tried it in the game and it was correct (it was just a fluke). So, it would be better to know the meaning (even if some of the attempt might be pure flukes).
  8. There is possibility of logic overlaps – I believe some of the logic or data structures are duplicated for a different purpose. These can be identified and resolved by making them generic.
  9. This is not completely object-oriented. Focus was more on building up logic, hence it rather became procedural. Different portions of the code could be more streamlined and aligned with elegant and updated OOP concepts and design patterns.
  10. Instead of mouse drag-drop, use keyboard to copy the letter from the rack and simply mouse-click on the cell where to drop it. This might save time?
  11. Flying letters back to rack by undoing (CTRL+Z) is left as an exercise for the reader. Let’s try it.
  12. Mascot drag-drop in the initial mascot selection is a one-time operation only; there is no UNDO option. This is left as an exercise to the reader to apply (CTRL+Z) for taking back a mascot and choosing another.
  13. If more than one player draws the same letter that is closest to ‘A’, then perform a redraw instead of awarding the start-up to the first player encountered clockwise with the closest letter.

Rationales Summary

When to shrink the bag

Question may arise why the bag is shrunk at the end of the current turn or a forfeit? Why doesn’t it shrink immediately after a letter draw? The reason is the exchange. An exchange returns the selected letters in the same index of the bag from where they were drawn. So, if the bag is shrunk immediately after drawing, then the returning letters have no space to move in. That would be a hectic chaos and end of the world (end of the game).

Use of Stack, Queue, List

These are already explained in the relevant sections. Only one queue is used; a list could be used otherwise. Also, an existing list or stack could be borrowed for the queue ‘DrawnLetters’; this one is only used for determining the first player.

Use of four green buttons

These again can be accomplished with one button only which moves according to turns. When it is clicked the corresponding score details can be displayed. This is a simple exercise left for the reader.

Points of Interest

This project works with timers. Timer is a thread-independent component. Means, it runs on its own where other activities can take place simultaneously. However, the opposite was needed for the animation; other threads needed to stop/pause until the timer finishes. For example, the letters should not fly until the bag-shaking timer finishes shaking bag (i.e., until that timer stops).

Another point of interest is a control’s absolute position. The rack cells and the board cells are inside panels. If their axis is used straight away, then that is measured relative to the container panels, not relative to the window. That is why an absolute location needs to be in place. For example:

// Obtain absolute location of the rack's cell from the top left of the window.
cellAbsLoc = ctl.FindForm().PointToClient(ctl.Parent.PointToScreen(ctl.Location));

Acknowledgements

Acknowledging various sites and organizations for using their images as mascots including Ubisoft (for using Prince of Persia, and Beyond Good and Evil mascot), Microsoft (for Assassin’s Creed mascot), Pyro and Eidos (for Green Beret mascot), Universal Pictures (for Gru and Agnes mascot), Nintendo (for Mario mascot), Walt Disney (for Bolt, Up, and Zootopia mascot), Universal Pictures (for Mr. Peabody and Sherman mascot), 20th Century Fox (for Yoda mascot), and Universal Studios (for Tiger Boo mascot).

The mascots are some random picks from moments of my life.

Disclaimer

The project is named after ‘warbler’ which is a cute little bird with cute little tweets - encouraging the players to tweet (cute) little words. It (title) also complements Mr. Crossworder; each of them stands on their own merits. There is no gender bias anymore. We are even :)

The project uses a couple of images and one gif. These made the size of the project and the executable a little bigger. My apologies for a little more bandwidth requirement for downloading the project and the executable. I didn’t want to sacrifice the glamour touches for one-time requirement for high download bandwidth.

I couldn't test the edge cases for game-ending. If anybody plays till the end and finds any glitches, I would be grateful if you kindly mention it in the comments.

The project is purely for educational purposes that experiments on logic development and encourages to think of data structures and algorithms for practical purposes. It also works on logic development for finding structural patterns in strings. Various aspects like wording, scoring, board, forfeits, rules are borrowed from various word-making games (e.g. scrabble, dabble, typo, quiddler, qwirkle etc.) and crossword rules.

Funny Pothole

When the game starts and if you commit 6 consecutive fails (just press 'pass' 6 times), then a winner will still be declared according to the regular game-ending rules. No rule says that, not a single word placed on the board would mean nobody can win. Although this is funny, but this relates to a deep philosophical insight of life - some people in the world can score (or earn) without moving a muscle (never mind, joking).

References

Open-source dictionary

Absolute location of a control

Ctrl modifier with mouse click

Online image editor

ASCII chart

A complete word puzzle game in C# .NET

A responsive design technique for Winforms

Bag image

Firecrackers image

Inventor’s image

Mr. Crossworder

Agnes's image

Bolt's image

Gru's image

Prince of Persia image

Mario Man image

Carl Fredrickson image

Assassin's Creed image

Tom & Jerry image

Sherman's image

Yoda image

Zootopia image

Tiger Boo image

Jade's image

Russell's image

Green Beret's image

Firecrackers image

Green button

Scissors icon

History

11 April 2019: First release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mehedi Shams
Software Developer
Bangladesh Bangladesh
A software developer mainly in .NET technologies and SQL Server. Love to code and learn.

Comments and Discussions

 
QuestionExcellent Work! Pin
Matias Lopez30-May-19 7:43
memberMatias Lopez30-May-19 7:43 
AnswerRe: Excellent Work! Pin
Mehedi Shams30-May-19 13:00
memberMehedi Shams30-May-19 13:00 
QuestionExcellent article Pin
dmjm-h12-Apr-19 12:46
memberdmjm-h12-Apr-19 12:46 
AnswerRe: Excellent article Pin
Mehedi Shams13-Apr-19 17:18
memberMehedi Shams13-Apr-19 17:18 
SuggestionAvoid Application.DoEvents() Pin
Rene Balvert12-Apr-19 0:44
memberRene Balvert12-Apr-19 0:44 
GeneralRe: Avoid Application.DoEvents() Pin
Mehedi Shams12-Apr-19 2:54
memberMehedi Shams12-Apr-19 2:54 
QuestionSnippets format Pin
Nelek10-Apr-19 19:09
protectorNelek10-Apr-19 19:09 
AnswerRe: Snippets format Pin
Mehedi Shams12-Apr-19 2:42
memberMehedi Shams12-Apr-19 2:42 
GeneralRe: Snippets format Pin
Nelek12-Apr-19 2:46
protectorNelek12-Apr-19 2:46 
GeneralRe: Snippets format Pin
Mehedi Shams13-Apr-19 17:11
memberMehedi Shams13-Apr-19 17:11 
GeneralRe: Snippets format Pin
Nelek14-Apr-19 8:55
protectorNelek14-Apr-19 8:55 
GeneralRe: Snippets format Pin
Nelek12-Apr-19 2:47
protectorNelek12-Apr-19 2:47 
QuestionCopyright? Pin
  Forogar  2-Apr-19 10:03
professional  Forogar  2-Apr-19 10:03 
AnswerRe: Copyright? Pin
Mehedi Shams2-Apr-19 11:33
memberMehedi Shams2-Apr-19 11:33 
GeneralRe: Copyright? Pin
  Forogar  3-Apr-19 2:37
professional  Forogar  3-Apr-19 2:37 
QuestionIt is good that you put effors and time to create an application Pin
Издислав Издиславов2-Apr-19 5:55
memberИздислав Издиславов2-Apr-19 5:55 
AnswerRe: It is good that you put effors and time to create an application Pin
Mehedi Shams2-Apr-19 11:30
memberMehedi Shams2-Apr-19 11:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 11 Apr 2019

Stats

5.7K views
248 downloads
4 bookmarked