Sudoku

The idea for this started when I saw the v-for directive and I thought that instead of having to define 81 input fields for a Sudoku grid I could just declare a single input field and have it replicated 81 times in a grid.

sudoku grid

The html code is indeed pretty sparse:

<div class="wrapper">
    <input v-for="cell in cells" v-model="cell.num" class="cell" type="text" pattern="[1-9]" />
</div>
1
2
3

There is quite a lot going on in this single div and input field.

First, the CSS. The wrapping div is where the css grid is declared (no fallback as I am not trying to please anyone but me).

.wrapper {
    float: left;
    display: grid;
    grid-template-columns: repeat(9, 3em);
    grid-gap: .1em;
}
1
2
3
4
5
6

This ensures that the cells are laid out in rows of 9 columns with a small gap between them, there are 9 rows because there will be 81 cells - that is driven by the saize of the cells array in the Vue instance.

I decided to have the size of the grid columns and gap relative to the font size (3em in this case), and to have the font size relative to the size of the display to make sure that it scales to fit the display.

The body tag has font-size: 3.3vw; which means that the font is scaled to 3.3% of the viewport width. Since the grid columns are three times this width they will each be 10% of the the width including the grid gap and so the whole width is 90% of the viewport width. Thinking about mobile first.

I used a media query to flip this when the display is wider than it is high (landscape) so that you don't have to scroll down to see it.

@media screen and (orientation: landscape) {
    body {
        font-size:3.3vh;
	}
}
1
2
3
4
5

So when the screen is in landscape mode the font and the grid scales to the height of the viewport and each cell is 10% of the viewport height.

input {
    width: 3em;
    height: 3em;
    line-height: 3em;
    color: blue;
    text-align: center;
    border-color: darkgray;
    border-style: solid;
    border-width: 2px 2px 2px 2px;
    background: aliceblue;
    font-size: inherit;
}
1
2
3
4
5
6
7
8
9
10
11
12

For the cells themselves they scale again to the same size as the grid columns - three times the size of the font. Using line-height means that the text is vertically aligned inside the cell.

I wanted the cell to show up with a red background if anything is entered other than the numbers 1-9, hence pattern="[1-9] the regular expression in the html on the input field. If anything other than 1-9 is entered in the input the browser applies the invalid attribute which means it can be targeted in CSS

input:invalid {
    background: red;
}
1
2
3

But I also wanted to show that the grid is not just 81 identical squares. Sudoku also has 9 9x9 squares. At this point I did not understand about style and class binding in Vue so and the grid is always fixed so I went with an inelegant approach but one which taught me about the different stages of the Vue instance.

There are hooks into the lifecycle of the Vue object (instance) which can be targeted and additional code inserted. I wanted to be able to apply a beige background to the cells in the alternate 9x9 squares and I was going to do this by their id. Of course they don't have an id in the html since there is only one cell declared and Vue builds the 81 of them based on the size of the cells array.

I had to inject a unique id into the DOM for each of these inputs after Vue has created them. The lifecycle hook for this is mounted. So I wrote some very traditional JS to inject the ids

mounted: function () {
    var boxes = document.getElementsByClassName('cell');
    i = boxes.length;
    while (i--) {
        boxes[i].id = "cell" + i;
    }
}
1
2
3
4
5
6
7

This means that each of the elements with the class 'cell' gets an id cell0, cell1 and so on, and so in the CSS the appropriate cells can be targeted for the beige background (just showing the first three):

#cell3, #cell4, #cell5 {
    background: beige;
}
1
2
3

Looking back on it I am sure this can also be done using a flag in the cells array to apply a class to cells with the flag set to true, but I think there is still going to have to be a loop to go through and apply the flag to each of the 36 cells which need it, so this loop works perfectly well and I'm not sure whether another method would be any less code or easier to understand or quicker.

At this point I wanted to just pain the grid of 81 blank cells but not to type out an array with 81 elements in it (might as well have just defined the 81 inputs in html).

data: {
    cells: [
        {
            num: '',
        },
    ],
},
beforeMount: function () {
    i = 80;
    while (i--) {
        this.cells.push({
            num: '',
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Or something like that I hope (this is superceded later on so I am not using this code any more) - basically have to push 80 items into the array so that there is a total of 81 items in the array beforemount so that when Vue comes to execute the v-for directive to loop through the items in the cells array there are already 81 of them.

This is not however a Sudoku at this point. I did write a PHP script to generate a completed Sudoku - i.e. an array of 9x9 numbers where each row, column and square have the numbers 1-9 once only. It runs fast enough for me and was a lesson in itself. However I could not then really work out a good way of removing values to leave a sensible array of clues. It is easy enough to randomnly leave a certain number of clues left but this could leave all the clues in one corner and the Sudoku impossible to solve.

I was quite frankly sick of the Sudoku problem at this point and there are so many algorithms on github that have solved the generating problem I just decided to use one of them.

Here is my routine for generating the completed Sudoku in any case.

I decided to use this github repo because the output from it is a 9x9 multidimensional array with either null or a number as the value which fits nicely into my pattern.

array(9) {
  [0]=>
  array(9) {
    [0]=>
    NULL
    [1]=>
    NULL
    [2]=>
    int(1)
1
2
3
4
5
6
7
8
9

This means I can insert into the php script that I am using to generate the web page a php echo to fill in the Vue data object with the output I want from the Sudoku generator:

clues: [
    <?php
    require('sudoku.php');
        use AbcAeffchen\sudoku\Sudoku;
        $task = Sudoku::generate(9, Sudoku::MEDIUM);
        for ($i = 0; $i < 9 ; $i++){
            for ($j = 0; $j < 9 ; $j++){
                if($task[$i][$j]){
                    echo(" {num: '".$task[$i][$j]."', row: ".$i.", column: ".$j."},");
                }
            }
        }
    ?>
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14

The nested PHP for loops go through each dimension of the array incrementing the row and column and then where there is a number instead of null outputting a entry into the clues array in the javascript so that Vue has these clues to use.

The output looks like this:

 clues: [
 {num: '7', row: 0, column: 4}, {num: '5', row: 0, column: 7}, {num: '3', row: 0, column: 8}, {num: '1', row: 1, column: 1}, {num: '8', row: 1, column: 2}, {num: '2', row: 1, column: 6}, {num: '7', row: 1, column: 7}, {num: '9', row: 2, column: 6}, {num: '1', row: 3, column: 8}, {num: '4', row: 4, column: 0}, {num: '5', row: 4, column: 2}, {num: '1', row: 4, column: 3}, {num: '8', row: 4, column: 6}, {num: '2', row: 4, column: 8}, {num: '8', row: 5, column: 1}, {num: '3', row: 6, column: 0}, {num: '7', row: 7, column: 3}, {num: '6', row: 7, column: 5}, {num: '3', row: 8, column: 5}, {num: '1', row: 8, column: 7}, {num: '7', row: 8, column: 8},                
 ]
1
2
3

This now enables me to put these numbers into the appropriate cells in the grid by updating my beforemount function:

beforeMount: function () {
    i = 80;
    rowValue = 0;
    columnValue = 0;
    numValue = '';
    var column0 = [80,71,62,53,44,35,26,17,8];
    var column1 = [79,70,61,52,43,34,25,16,7];
    var column2 = [78,69,60,51,42,33,24,15,6];
    var column3 = [77,68,59,50,41,32,23,14,5];
    var column4 = [76,67,58,49,40,31,22,13,4];
    var column5 = [75,66,57,48,39,30,21,12,3];
    var column6 = [74,65,56,47,38,29,20,11,2];
    var column7 = [73,64,55,46,37,28,19,10,1];
    var column8 = [72,63,54,45,36,27,18,9,0];
    while (i--) {
        if(i < 73){
            rowValue = 1;
        }
        if(i < 64){
            rowValue = 2;
        }
        if(i < 55){
            rowValue = 3;
        }
        if(i < 46){
            rowValue = 4;
        }
        if(i < 37){
            rowValue = 5;
        }
        if(i < 28){
            rowValue = 6;
        }
        if(i < 19){
            rowValue = 7;
        }
        if(i < 10){
            rowValue = 8;
        }
        if(column0.includes(i)){
            columnValue = 0;
        }
        else if (column1.includes(i)){
            columnValue = 1;
        }
        else if (column2.includes(i)){
            columnValue = 2;
        }
        else if (column3.includes(i)){
            columnValue = 3;
        }
        else if (column4.includes(i)){
            columnValue = 4;
        }
        else if (column5.includes(i)){
            columnValue = 5;
        }
        else if (column6.includes(i)){
            columnValue = 6;
        }
        else if (column7.includes(i)){
            columnValue = 7;
        }
        else if (column8.includes(i)){
            columnValue = 8;
        }
        numValue = '';
        this.clues.forEach(function (clue, key) {
            if(clue.row == rowValue && clue.column == columnValue){
                numValue = clue.num;
            }
        });
        this.cells.push({
            num: numValue,
            row: rowValue,
            column: columnValue
        })
    }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

First of all I have to calculate based on the index in the cells array which row and column the cell is in, there may be cleverer ways to do this but I hard coded which cells are in which column (not a mathematician) and then can check whether I have a clue for that x,y position in the grid. I decided to keep the row and column reference in the cells array as well thinking that in the future this may be useful (I would like to indicate later on if possible that a number entered by the user is already on the grid so they can't use it and for this I'm thinking I will need to know what row and column I'm on - square too I imagine but have not got that far). At the moment there is no use for the row and column values in the array.

Last thing is to disable input for the cells where there is already a number clue in place. So I changed the mounted function to apply the disabled attribute to any cells with a value.

mounted: function () {
    var boxes = document.getElementsByClassName('cell');
    i = boxes.length;
    while (i--) {
        boxes[i].id = "cell" + i;
        if(boxes[i].value != ''){
            boxes[i].setAttribute("disabled", "");
        }
    }
}
1
2
3
4
5
6
7
8
9
10

And in the css change the font colour to dark grey for these cells:

input:disabled {
    color: darkgrey;
}
1
2
3

Would like to do...(now done!!)

  • give feedback when a number is entered which cannot be correct because it has already been used in the row, column, square
  • give feedback when the sudoku is completed correctly