Hello friends, Let’s see how we can create a Sudoku puzzle game step by
step using Windows Form and C# DotNet in Visual Studio without using any
complex/advance codes neither external libraries.
Prerequisites
- Basic c# programming
knowledge
- Basic windows form
application development knowledge
- Visual studio
2010+
Step 1.
Start a new Windows Form Project in Visual Studio which will provide you
the default form template and add a panel as shows in the below
screenshot.
Step 2.
Add new class for sudoku cells which by inheriting the button class.
using System.Windows.Forms;
namespace sudoku
{
class SudokuCell : Button
{
public int Value { get; set; }
public bool IsLocked { get; set; }
public int X { get; set; }
public int Y { get; set; }
public void Clear()
{
this.Text = string.Empty;
this.IsLocked = false;
}
}
}
Add Code in the code behind for the form in order to generate sudoku cells in the panel and click events for each cells (You can go to the code behind by right click on the form and select view code menu).
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace sudoku
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
createCells();
}
SudokuCell[,] cells = new SudokuCell[9, 9];
private void createCells()
{
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
// Create 81 cells for with styles and locations based on the index
cells[i, j] = new SudokuCell();
cells[i, j].Font = new Font(SystemFonts.DefaultFont.FontFamily, 20);
cells[i, j].Size = new Size(40, 40);
cells[i, j].ForeColor = SystemColors.ControlDarkDark;
cells[i, j].Location = new Point(i * 40, j * 40);
cells[i, j].BackColor = ((i / 3) + (j / 3)) % 2 == 0 ? SystemColors.Control : Color.LightGray;
cells[i, j].FlatStyle = FlatStyle.Flat;
cells[i, j].FlatAppearance.BorderColor = Color.Black;
cells[i, j].X = i;
cells[i, j].Y = j;
// Assign key press event for each cells
cells[i, j].KeyPress += cell_keyPressed;
panel1.Controls.Add(cells[i, j]);
}
}
}
private void cell_keyPressed(object sender, KeyPressEventArgs e)
{
var cell = sender as SudokuCell;
// Do nothing if the cell is locked
if (cell.IsLocked)
return;
int value;
// Add the pressed key value in the cell only if it is a number
if (int.TryParse(e.KeyChar.ToString(), out value))
{
// Clear the cell value if pressed key is zero
if (value == 0)
cell.Clear();
else
cell.Text = value.ToString();
cell.ForeColor = SystemColors.ControlDarkDark;
}
}
}
}
Let's run the application and see if the sudoku cells loads
correctly
Step 3.
Add code to load values in the cells. We have the logic to find valid
numbers for each cell.
public Form1()
{
InitializeComponent();
createCells();
startNewGame();
}
private void startNewGame()
{
loadValues();
}
private void loadValues()
{
// Clear the values in each cells
foreach (var cell in cells)
{
cell.Value = 0;
cell.Clear();
}
// This method will be called recursively
// until it finds suitable values for each cells
findValueForNextCell(0, -1);
}
Random random = new Random();
private bool findValueForNextCell(int i, int j)
{
// Increment the i and j values to move to the next cell
// and if the columsn ends move to the next row
if (++j > 8)
{
j = 0;
// Exit if the line ends
if (++i > 8)
return true;
}
var value = 0;
var numsLeft = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// Find a random and valid number for the cell and go to the next cell
// and check if it can be allocated with another random and valid number
do
{
// If there is not numbers left in the list to try next,
// return to the previous cell and allocate it with a different number
if (numsLeft.Count < 1)
{
cells[i, j].Value = 0;
return false;
}
// Take a random number from the numbers left in the list
value = numsLeft[random.Next(0, numsLeft.Count)];
cells[i, j].Value = value;
// Remove the allocated value from the list
numsLeft.Remove(value);
}
while (!isValidNumber(value, i, j) || !findValueForNextCell(i, j));
// TDO: Remove this line after testing
cells[i, j].Text = value.ToString();
return true;
}
private bool isValidNumber(int value, int x, int y)
{
for (int i = 0; i < 9; i++)
{
// Check all the cells in vertical direction
if (i != y && cells[x, i].Value == value)
return false;
// Check all the cells in horizontal direction
if (i != x && cells[i, y].Value == value)
return false;
}
// Check all the cells in the specific block
for (int i = x - (x % 3); i < x - (x % 3) + 3; i++)
{
for (int j = y - (y % 3); j < y - (y % 3) + 3; j++)
{
if (i != x && j != y && cells[i, j].Value == value)
return false;
}
}
return true;
}
We have the code to be removed as all the values won’t be shown to the
player. It was included just to make sure the values are valid and proper while testing.
Let's run the application and see if the values loads correctly
Now we can remove the code for showing the values in the cell.
Step 4.
Add code to show values in random based cells in order to work as hints
private void startNewGame()
{
loadValues();
//Show values of 45 cells as hint
showRandomValuesHints(45);
}
Random random = new Random();
private void showRandomValuesHints(int hintsCount)
{
// Show value in random cells
// The hints count is based on the level player choose
for (int i = 0; i < hintsCount; i++)
{
var rX = random.Next(9);
var rY = random.Next(9);
// Style the hint cells differently and
// lock the cell so that player can't edit the value
cells[rX, rY].Text = cells[rX, rY].Value.ToString();
cells[rX, rY].ForeColor = Color.Black;
cells[rX, rY].IsLocked = true;
}
}
Let's run the application and see if the hints loads correctly
As shown in the screenshot the hints are in black font colour and the
user inputs are slightly light coloured (marked with red in the image).
Step 5.
Now let’s add the following controls in the form.
- button for checking if the inputs are valid and if the player wins
- button for clearing all the inputs
- radio buttons for choosing the levels
- button for starting new game
·
Now let’s add click events for the check input button which will do
check if the game ends too.
private void checkButton_Click(object sender, EventArgs e)
{
var wrongCells = new List<SudokuCell>();
// Find all the wrong inputs
foreach (var cell in cells)
{
if (!string.Equals(cell.Value.ToString(), cell.Text))
{
wrongCells.Add(cell);
}
}
// Check if the inputs are wrong or the player wins
if (wrongCells.Any())
{
// Highlight the wrong inputs
wrongCells.ForEach(x => x.ForeColor = Color.Red);
MessageBox.Show("Wrong inputs");
}
else
{
MessageBox.Show("You Wins");
}
}
Add events for the clear and start new game button.
private void clearButton_Click(object sender, EventArgs e)
{
foreach (var cell in cells)
{
// Clear the cell only if it is not locked
if (cell.IsLocked == false)
cell.Clear();
}
}
private void newGameButton_Click(object sender, EventArgs e)
{
startNewGame();
}
Modify the start game method to consider what level the player has chosen.
private void startNewGame()
{
loadValues();
var hintsCount = 0;
// Assign the hints count based on the
// level player chosen
if (beginnerLevel.Checked)
hintsCount = 45;
else if (IntermediateLevel.Checked)
hintsCount = 30;
else if (AdvancedLevel.Checked)
hintsCount = 15;
showRandomValuesHints(hintsCount);
}
Let’s run the application and see the check, clear and new game button works
Enjoy Coding…!!!
Hola! Esto ha sido muy útil, te agradezco mucho, como incluirias un botón que muestre una pista aleatoria??? Es algo que intento hacer y no encuentro la forma
ReplyDeleteComo puede ver en el bloque Código del Paso 3, el método "loadValues ()" llama a otro método "findValueForNextCell (int i, int j)", que es recursivo. Ese método se llama para cada bloque en el rompecabezas del 0 al 80, y toma un número aleatorio y verifica si romperá la regla usando el método "isValidNumber (int value, int x, int y)". Si no es así, pasa al siguiente bloque, o bien, pasa al bloque anterior y toma otro número aleatorio allí. Estas cosas ocurren una y otra vez hasta que encuentra números válidos para todos los bloques. (Si no usamos estas lógicas, podemos terminar con algunos puntos muertos en el rompecabezas que hacen que el rompecabezas sea irresoluble - Todo esto sucede en milisegundos :-) )
Delete
DeleteAs you can see in the Step 3 Code block, the method "loadValues()" calls another method "findValueForNextCell(int i, int j)", which is a recursive one. That method is been called against each blocks in the puzzle from 0th to 80th, and takes a random number and checks if it will break the rule using the method "isValidNumber(int value, int x, int y)". If it it won't, move to the next block, or else move to the previous block and takes another random number there. These stuffs happens over and over until it finds valid numbers for all blocks. (if we don't use these logics we may end up with some dead locks in the puzzle which makes the puzzle unsolvable - All this happens in milliseconds)
Thanks for sharing this. I enjoyed typing it up as you explained the sections - I always re-type code as it gives me a chance to understand each line as I go.
DeleteHowever, I found a problem with the code that checks to see if the numbers entered match the generated numbers. The application reports "Wrong inputs" even though the data is valid. For example:
8, 2,*9, 3, 5, 6, 7,*4,*1
*6,*3, 4,*1,*7, 8, 9,*5, 2
5, 1,*7, 4, 2, 9,*8, 3,*6
4, 6, 5, 9,*1, 7,*3, 2,*8
1,*9, 2,*6, 8, 3, 4, 7,*5
*3, 7,*8,*5, 4, 2,*1,*6,*9
*7,*4,*6, 2, 9, 1,*5, 8, 3
9, 5, 3,*8,*6,*4,*2,*1,*7
2,*8, 1,*7, 3,*5, 6,*9, 4
(* = locked cell)
my data:
0,5 = 1
2,5 = 2
0,8 = 2
2,8 = 1
Application data:
0,5 = 2
2,5 = 1
0,8 = 1
2,8 = 2
I will have a think about how this can be fixed. My guess is to ignore the random generated values and only check locked and user entered data validity?
thank you
ReplyDelete