PFlem.com SudokuSharp
a C# learning space

Buy me a cup of coffee?

Let's Draw Numbers

Now that we have the basic framework of a Sudoku puzzle built and we add some navigation with mouse clicks and using the arrow keys on the keyboard, we can focus our attention to drawing numbers onto the screen. But first let's talk a little bit about those numbers. There are large numbers and small numbers. There are going to be two different colors of large numbers. We are going to have cells where the numbers cannot change and cells that we can edit and cells that have no visible number, but still have a value. If a cell does not have a large number showing, it's value is zero. Keeping this concepts in mind we can proceed.

Given & Solved Numbers

At the beginning of a puzzle there are clues that are "given". I cleverly call this type of number, Given. Then there are numbers that can be typed into blank cells (or selected from a context menu - spoiler alert) to solve a cell. Again, I cleverly named these numbers, Solved. In order to easily see Givens from Solved numbers, I use differnt colors for each. You could make the different fonts if you wanted. Again, you are the programmer and you do what works for you.

Candidates

Difficult Sudoku puzzles require that the user keeps track of possible numbers that can belong in a cell as a solution. I call these numbers, Candidates. In Sudoku Sharp and other Sudoku programs, candidates are much smaller than Givens and Solved numbers so that you can tell they are candidates. They also hold a specific spot in a cell. 1 is always in the upper left corner and 9 is always in the lower right corner.

In the image below you can see Givens (black), Solved (green), and Candidates (blue).

Candidates

Let's start with the large numbers. Givens in particular. Because you need to start with a given set of numbers to begin a Sudoku puzzle. Simple enough. Either we are going to create a puzzle from scratch by designing it. Or we could start with someone else's puzzle. Like one from a newspaper or Sudoku book, or like the page a day calendar that I have on my desk. So, first thing I want to do is to be able to highlight a cell and then type a number into that cell.

From the Navigation page, we built a method that recognizes when a key is let up (Form1_KeyUp). In this method we are going to add code that will recognize the numbers, either from the number pad or from the numbered keys at the top of the keyboard. At the end of the method add the following code. This will first test to see if the key is a number key and then set the cell's value to that number.

// Numbers from the keyboard or number pad
if ((e.KeyCode >= Keys.D0 && e.KeyCode <= Keys.D9) || (e.KeyCode >= Keys.NumPad0 && e.KeyCode <= Keys.NumPad9))
{
   string KeyValue = e.KeyCode.ToString();
   if (KeyValue.Length == 2) { KeyValue = KeyValue.Substring(1, 1); }
   if (KeyValue.Length == 7) { KeyValue = KeyValue.Substring(6, 1); }
   for (int i = 0; i <= 80; i++)
   {
      if (Cell[i].IsActive)
      {
         cellIndex = i;
         break;
      }
   }

   if (int.Parse(KeyValue) != 0)
   {
      if (!Cell[cellIndex].IsGiven)
      {
         Cell[cellIndex].CellValue = int.Parse(KeyValue);
      }
   }
   this.Refresh();
}
Once the cell's value is set, we need to refresh, or paint, the screen in order to show the number we just set. Add the following code in the OnPaint event. The colors are pulled from the Settings, so you will need to add some default colors there. See Painting The Board for reference. We also add some new brushes and some fonts.

// Set colors from default settings
Color BoxBackColor = Properties.Settings.Default.Boxes;
Color AlternateBoxColor = Properties.Settings.Default.AlternateBoxes;
Color GridBorderColor = Properties.Settings.Default.GridBorder;
Color GridLinesColor = Properties.Settings.Default.GridLines;
Color ActiveCellColor = Properties.Settings.Default.ActiveCellColor;
Color CurrentCellBorder = Properties.Settings.Default.CurrentCellBorder;
Color GivenColor = Properties.Settings.Default.GivenColor;
Color CandidateColor = Properties.Settings.Default.Candidates;
Color CandidateHighlight = Properties.Settings.Default.CandidateHighlight;
Color SolvedColor = Properties.Settings.Default.SolvedColor;


//Create some brushes
SolidBrush brshBackground = new SolidBrush(Color.Black);
SolidBrush brshBorder = new SolidBrush(GridBorderColor);
SolidBrush brshCandidate = new SolidBrush(CandidateColor);
SolidBrush brshCandidateHighlight = new SolidBrush(CandidateHighlight);
SolidBrush brshGivens = new SolidBrush(GivenColor);
SolidBrush brshSolved = new SolidBrush(SolvedColor);


// Create some fonts
Font fntArialLarge = new Font("Arial Rounded MT Bold", 30, FontStyle.Regular);
Font fntArialTiny = new Font("Arial Rounded MT Bold", 10, FontStyle.Regular);
Font fntGiven = new Font("Arial Rounded MT Bold", 30, FontStyle.Regular);
Font fntCandidate = new Font("Arial Rounded MT Bold", 10, FontStyle.Regular);


Then jump to the end of the OnPaint event and add the following code. Here is where the numbers actually get drawn on the screen. The g.DrawString() method in this code looks a little intimidating, but the DrawString() method is pretty simple. It takes the string you want to place on the screen, the font, a brush, and the X, Y coordinates of where to place it. I've added a calculation to figure out how to center the number in the cell for the X, Y coordinates.

// Draw the givens, solved, and candidate values on the grid.
int i = 0;
for (row = 1; row <= 9; row++)
{
   for (col = 1; col <= 9; col++)
   {
      if (Cell[i].CellValue != 0)
      {
         if (Cell[i].IsGiven)
         {
            g.DrawString(Cell[i].CellValue.ToString(), fntArialLarge, brshGivens, (Grid.xOffset + Grid.BorderWidth + ((col - 1) * (Grid.CellWidth + 1)) + 7), ((Grid.yOffset + Grid.BorderWidth + ((row - 1) * (Grid.CellHeight + 1))) + 3));
         }
         else
         {
            g.DrawString(Cell[i].CellValue.ToString(), fntArialLarge, brshSolved, (Grid.xOffset + Grid.BorderWidth + ((col - 1) * (Grid.CellWidth + 1)) + 7), ((Grid.yOffset + Grid.BorderWidth + ((row - 1) * (Grid.CellHeight + 1))) + 3));
         }
      }
      i += 1;
   }
}

One major step that we want to perform, now that we are going to start accessing the properties of the cell object is to initialize some properties.

private void InitializeCells()
{
   for (int i = 0; i <= 80; i++)
   {
      Cell[i] = new cell();
      Cell[i].CellValue = 0;
      Cell[i].Candidates = "123456789";
      Cell[i].CandidatePattern = "000000000";
      Cell[i].IsGiven = false;
      Cell[i].IsActive = false;
      Cell[i].IsCurrentCell = false;
      Cell[i].MarkerValue = 0;
   }
}

Run the application and you should be able to type numbers in each of the cells. By doing this you are actually assigning the cell value to the cell from the Form1_KeyUp method and then painting on the screen with the OnPaint event.

If you were to take all of the cell values starting in the upper left corner and reading left to right, top to bottom, one row at a time, you would end up with a string of digits that is 81 characters long. This includes cells that look blank since they have the cell value of zero. This will come in handy for the copy and paste code. Below is a string that we can use to copy and paste into our application and it is a valid Sudoku puzzle.

080000040205070003400390000020000700300601008004000060000018002700020601030000080

To do this we need to have access to the Windows clipboard. We're going to pretend for a momment that the only thing that could be on the clipboard is a string of 81 numeric characters. Then we need to come back and test and clean up strings. Add the PasteFromClipboard() code below to the end of your code (or wherever makes sense to you). This code uses the GetDataObject() to access the Windows clipboard and loads it into a string variable which gets parsed through one character at a time and sets the cell value with the number.

private void PasteFromClipboard()
{
   if (Clipboard.GetDataObject() != null)
   {
      string strClipboard = Clipboard.GetDataObject().GetData(DataFormats.Text).ToString();

      for (int p = 0; p < strClipboard.Length; p++)
      {
         string str = strClipboard.Substring(p, 1);

         Cell[p].CellValue = int.Parse(str);
         if (Cell[p].CellValue != 0) // A non-zero value should be flagged as given
         {
            Cell[p].IsGiven = true; // Is given
         }
         else
         {
            Cell[p].IsGiven = false; // Empty cell
         }
      }
   }
}

Next we need to call the Paste method from the menu item and the toolbar. From the Form1 [Design] tab, click on the Menu for Edit and then double click on the Paste menu item. And call the Paste method from here.

private void pasteToolStripMenuItem_Click(object sender, EventArgs e)
{
   PasteFromClipboard();
   this.Refresh();
}

And

private void pasteToolStripButton_Click(object sender, EventArgs e)
{
   PasteFromClipboard();
   this.Refresh();
}

Test out the new pasting capabilities and you could even start solving the way the application stands. But what happens if something different is on the clipboard when you go to paste into the puzzle. It could be disasterous. So, we need to clean up our act. Or our string.

Paste from clipboard Here is an example of what could be on the clipboard. If we remove the asterisks and pipes and periods (oh my!), we would have a valid Sudoku puzzle.

      *-----------*
      |19.|..5|..4|
      |.8.|..4|3..|
      |4..|...|.1.|
      |---+---+---|
      |...|.5.|9.8|
      |.7.|...|.4.|
      |5.3|.2.|...|
      |---+---+---|
      |.6.|...|..7|
      |..7|6..|.5.|
      |8..|7..|.29|
      *-----------*

To clean this up and make it a string of 81 characters, I use the following series of Replace() statements, updating the string through each itteration.

string strClipboard = Clipboard.GetDataObject().GetData(DataFormats.Text).ToString();
strClipboard = strClipboard.Replace(" ", "");
strClipboard = strClipboard.Replace("*", "");
strClipboard = strClipboard.Replace("-", "");
strClipboard = strClipboard.Replace("|", "");
strClipboard = strClipboard.Replace("+", "");
strClipboard = strClipboard.Replace("#", "");
strClipboard = strClipboard.Replace(".", "0");
strClipboard = strClipboard.Replace("\r\n", "");

What if there is something unknown on the clipboard; a paragraph of some text perhaps. We don't want to try pasting invalid data into a puzzle, so we have to test whatever it is on the clipboard to see if we can, in fact, paste it into our application. After stripping out known characters from the clipboard contents, we should only see numeric characters. If there is anything else at this point, we will reject what's on the clipboard as a valid puzzle and we should inform the user of what is going on.

The first test I'm going to do is to make sure we have at least 81 characters left in the string from the clipboard. The bold code below is our simple test to make sure we have enough characters to work with. The code to assign cell values is embedded within the test block of code. So if the test fails then cell values don't get assigned and nothing gets placed on the screen.

// Test to see if there are enough characters for a valid puzzle.
if (strClipboard.Length >= 81)
{

   for (int p = 0; p < strClipboard.Length; p++)
   {
      string str = strClipboard.Substring(p, 1);

      Cell[p].CellValue = int.Parse(str);
      if (Cell[p].CellValue != 0) // A non-zero value should be flagged as given
      {
         Cell[p].IsGiven = true; // Is given
      }
      else
      {
         Cell[p].IsGiven = false; // Empty cell
      }
   }
}

The next test is to make sure all 81 characters are numeric. A single number 81 digits long is a pretty big number and too big to try converting to an int data type, so I just step through the string one character at a time. Place the following code within the test for the number of characters. And wrap the code for assigning values so that it will only execute if the IsValidPuzzleString evaluates to true.

// Test to see if there are enough characters for a valid puzzle.
if (strClipboard.Length >= 81)
{
   strClipboard = strClipboard.Substring(0, 81);   // Strip off extra characters
   bool IsValidPuzzleString = false;               // Initialize a valid puzzle flag
   // loop through the puzzle string for non-numeric characters
   for (int p = 0; p < strClipboard.Length; p++)
   {
      string str = strClipboard.Substring(p, 1);   // Test one character at a time
      if (int.TryParse(str, out _))                // Test for number
      {
         IsValidPuzzleString = true;
      }
      else
      {
         // A non-numeric character should end the loop and warn the user
         MessageBox.Show("Clipboard does not contain a valid puzzle.", "Clipboard Error");
         IsValidPuzzleString = false;
         break;
      }
   }


   if (IsValidPuzzleString) // Paste the valid puzzle string to each cell value
   {

      int intCellsGiven = 0;
      for (int p = 0; p < strClipboard.Length; p++)
      {
         string str = strClipboard.Substring(p, 1);

         Cell[p].CellValue = int.Parse(str);
         if (Cell[p].CellValue != 0) // A non-zero value should be flagged as given
         {
            Cell[p].IsGiven = true; // Is given
            intCellsGiven += 1;
         }
         else
         {
            Cell[p].IsGiven = false; // Empty cell
         }
      }
   }
}

At this point you can copy the string from above and paste it into the application and then solve the puzzle. We don't have the capability to Undo any mistakes, but you can type over a cell value as long as it is not a given value. But the fact remains that there is room for mistakes. We'll get to the Undo and Redo programming in a bit. But first, we need to take care of putting Candidates on the board.

Before we put a candidate within a cell, we need to know if it should be there or not. In other words, you have candidates that are disqualified as a possible solution to the cell and therefore should not be drawn to the screen. Which means we not only need a method for drawing the numbers on the screen we need to have a way to maintain the candidates as the puzzle progresses towards a solution. That means, any changes to cells affect the candidates need to get updated. Whether it's solving a cell, undoing, redoing, or disqualifying a candidate

To handle this, I have made one method that updates (or sets) the candidates and then there are many places with the application where the SetCellCandidates is called.

Before we look at the code, let's talk about a couple of things. The SetCellCandidates deals with a couple of strings one is to maintain which candidates are put on the screen; Cell[].Candidates. And a bit pattern of 0s and 1s to tell if a particular candidate is valid (1) or disqualified (0); Cell[].CandidatePattern. The Candidate Pattern will be used in solving later on. Just put it on the back burner for now.

Cell[].Candidates is a string that can be 0 to 9 digits long. If the length is 0, then that cell should have a cell value between 1 & 9 and it is either a given value or a solved cell. This is our first test. If the cell is solved or given, we set the Candidate string to nothing and we set the Candidate Pattern to all zeros (000000000). Cells that have a cell value of 0 should have a string of candidates from 1 to 9 digits long. This is where we have to set the possible candidates. As candidates are eliminated, the candidates string gets shorter. We use the concept of Peers in this method quite a bit. With a cell value that is zero, we test to see if the Peer exists so we don't get an error while trying to access it. If it doesn't exist, we run the AssignCellPeers() method and then extract the Cell[].Peer string into a row, col, & box ID. Then it checks each cell within the row for a non-zero cell value and then removes that value as a candidate from all of the cells within that row. Then we rinse and repeat for each column and each box. Then based on the candidates we just set, we then set the candidate pattern.

public void SetCellCandidates()
{
   for (int i = 0; i <= 80; i++)
   {
      if (Cell[i].CellValue != 0) // Cell is either solved or given and has no candidates
      {
         Cell[i].Candidates = "";
         Cell[i].CandidatePattern = "000000000";
      }
      else
      {
         if (Cell[i].Peer == null)
         {
            AssignCellPeers();
         }
         // Extract the row, col, and box IDs from the cell peer value.
         int rowID = int.Parse(Cell[i].Peer.Substring(0, 1));
         int colID = int.Parse(Cell[i].Peer.Substring(1, 1));
         int boxID = int.Parse(Cell[i].Peer.Substring(2, 1));
         int rowPeer = 0;
         int colPeer = 0;
         int boxPeer = 0;
         // Removes candidates from peer cells where a non-zero cell value exists within the row.
         for (int x = 1; x <= 9; x++)
         {
            rowPeer = ((rowID * 9) - 10) + x;
            if (Cell[rowPeer].CellValue != 0)
            {
               Cell[i].Candidates = Cell[i].Candidates.Replace(Cell[rowPeer].CellValue.ToString(), "");
            }
         }
         // Removes cadidates from peer cells where a non-zero cell value exists within a column.
         for (int x = 1; x <= 9; x++)
         {
            colPeer = (colID - 1) + (x * 9) - 9;
            if (Cell[colPeer].CellValue != 0)
            {
               Cell[i].Candidates = Cell[i].Candidates.Replace(Cell[colPeer].CellValue.ToString(), "");
            }
         }
         // Removes candidates from peer cells where a non-zero cell value exists within a box.
         for (int row = 1; row <= 9; row += 3)
         {
            for (int col = 1; col <= 9; col += 3)
            {
               for (int rowModifier = 0; rowModifier <= 2; rowModifier++)
               {
                  for (int colModifier = 0; colModifier <= 2; colModifier++)
                  {
                     boxPeer = ((col + colModifier) - 1) + (((row + rowModifier) - 1) * 9);
                     if (Cell[boxPeer].Peer.Substring(2, 1) == boxID.ToString())
                     {
                        if (Cell[boxPeer].CellValue != 0)
                        {
                           Cell[i].Candidates = Cell[i].Candidates.Replace(Cell[boxPeer].CellValue.ToString(), "");
                        }
                     }
                  }
               }
            }
         }
         // Set candidate pattern string for use in solving.
         if (Cell[i].Candidates != null)
         {
            Cell[i].CandidatePattern = "";
            for (int x = 1; x <= 9; x++)
            {
               if (Cell[i].Candidates.Contains(x.ToString()))
               {
                  Cell[i].CandidatePattern += "1";
               }
               else
               {
                  Cell[i].CandidatePattern += "0";
               }
            }
          }
      }
   }
}

With the SetCellCandidates() code in place, we need to call it. The first place we're going to call it is in the Form1_Load().

private void Form1_Load(object sender, EventArgs e)
{
   Cell[0].IsActive = true;
   AssignCellPeers();
   SetCellCandidates();
   this.Invalidate();
}

In the Form1_KeyUp method, find the code towards the end and add the SetCandidates(); call. This will update the candidates as you solve cells.

if (int.Parse(KeyValue) != 0)
{
   if (!Cell[cellIndex].IsGiven)
   {
      Cell[cellIndex].CellValue = int.Parse(KeyValue);
      SetCellCandidates();
   }
}

We need to add some code to our Show Candidates menu item under the Tools menu. Click over to the Design tab and click on the Tools menu and then double click on the Show Candidates menu item and add the following code.

private void showCandidatesToolStripMenuItem_Click(object sender, EventArgs e)
{
   if (Grid.ShowCandidates)
   {
      Grid.ShowCandidates = false;
      showCandidatesToolStripMenuItem.Checked = false;
   }
   else
   {
      Grid.ShowCandidates = true;
      showCandidatesToolStripMenuItem.Checked = true;
   }
   this.Refresh();
}

Run the application, paste a puzzle into the application, click on the Tools and Show Candidates menu item. You should see something like the image below. Try entering some numbers and make sure your code is updating the candidates within the peer cells.

Grid with Candidates

For those of you that would like to try the puzzle, but get stuck, here's the solution.

Solution

That's all I want to cover with regards to painting/drawing numbers on the screen for now. It's time to move on to Strategic Highlighting. This will aid in solving puzzles.