BacktrackSolver::solve()   A
last analyzed

Complexity

Conditions 5
Paths 8

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 17
c 0
b 0
f 0
rs 9.6111
cc 5
nc 8
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace CoenMooij\Sudoku\Solver;
6
7
use CoenMooij\Sudoku\Exception\UnsolvableException;
8
use CoenMooij\Sudoku\Puzzle\Grid;
9
use CoenMooij\Sudoku\Puzzle\Location;
10
11
/**
12
 * Informed depth first search.
13
 */
14
class BacktrackSolver implements GridSolverInterface
15
{
16
    private const DIRECTION_FORWARDS = true;
17
    private const DIRECTION_BACKWARDS = false;
18
19
    /**
20
     * @var Grid
21
     */
22
    private $grid;
23
24
    /**
25
     * @var int[][][]
26
     */
27
    private $possibleValues;
28
29
    /**
30
     * @var bool[][]
31
     */
32
    private $presetValues;
33
34
    /**
35
     * @var bool
36
     */
37
    private $direction;
38
39
    /**
40
     * @var int
41
     */
42
    private $row;
43
44
    /**
45
     * @var int
46
     */
47
    private $column;
48
49
    public function solve(Grid $grid): Grid
50
    {
51
        $this->initialize($grid);
52
53
        while ($this->locationIsValid()) {
54
            if (!$this->isPresetValue()) {
55
                $this->direction === self::DIRECTION_FORWARDS
56
                    ? $this->handleForwardsIteration()
57
                    : $this->handleBackwardsIteration();
58
            }
59
            $this->nextLocation();
60
        }
61
        if (!$this->reachedEndOfGrid()) {
62
            throw new UnsolvableException();
63
        }
64
65
        return $grid;
66
    }
67
68
    private function handleForwardsIteration(): void
69
    {
70
        $this->findAllPossibleValuesForCurrentLocation();
71
        if ($this->currentLocationHasPossibleValues()) {
72
            $this->fillCurrentLocationWithNextPossibleValue();
73
        } else {
74
            $this->grid->empty($this->getCurrentLocation());
75
            $this->direction = self::DIRECTION_BACKWARDS;
76
        }
77
    }
78
79
    private function handleBackwardsIteration(): void
80
    {
81
        if ($this->currentLocationHasPossibleValues()) {
82
            $this->fillCurrentLocationWithNextPossibleValue();
83
            $this->direction = self::DIRECTION_FORWARDS;
84
        } else {
85
            $this->grid->empty($this->getCurrentLocation());
86
        }
87
    }
88
89
    private function initialize(Grid $grid): void
90
    {
91
        $this->direction = self::DIRECTION_FORWARDS;
92
        $this->column = 0;
93
        $this->row = 0;
94
        $this->grid = $grid;
95
        $this->possibleValues = [];
96
        $this->presetValues = [];
97
        $this->initializePresetValues();
98
    }
99
100
    private function initializePresetValues(): void
101
    {
102
        for ($row = 0; $row < 9; $row++) {
103
            for ($column = 0; $column < 9; $column++) {
104
                $location = new Location($row, $column);
105
                if ($this->grid->isEmpty($location)) {
106
                    $this->presetValues[$row][$column] = false;
107
                } else {
108
                    $this->presetValues[$row][$column] = true;
109
                }
110
            }
111
        }
112
    }
113
114
    private function currentLocationHasPossibleValues(): bool
115
    {
116
        return !empty($this->possibleValues[$this->row][$this->column]);
117
    }
118
119
    private function findAllPossibleValuesForCurrentLocation(): void
120
    {
121
        $possibilities = $this->grid->getAllPossibilitiesFor(new Location($this->row, $this->column));
122
        shuffle($possibilities);
123
        $this->possibleValues[$this->row][$this->column] = $possibilities;
124
    }
125
126
    private function nextLocation(): void
127
    {
128
        if ($this->direction === self::DIRECTION_FORWARDS) {
129
            $this->row = $this->column === 8 ? $this->row + 1 : $this->row;
130
            $this->column = $this->column === 8 ? 0 : $this->column + 1;
131
        } else {
132
            $this->row = $this->column === 0 ? $this->row - 1 : $this->row;
133
            $this->column = $this->column === 0 ? 8 : $this->column - 1;
134
        }
135
    }
136
137
    private function fillCurrentLocationWithNextPossibleValue(): void
138
    {
139
        $this->grid->set($this->getCurrentLocation(), $this->possibleValues[$this->row][$this->column][0]);
140
        array_shift($this->possibleValues[$this->row][$this->column]);
141
    }
142
143
    private function getCurrentLocation(): Location
144
    {
145
        return new Location($this->row, $this->column);
146
    }
147
148
    private function isPresetValue(): bool
149
    {
150
        return !empty($this->presetValues[$this->row][$this->column]);
151
    }
152
153
    private function locationIsValid(): bool
154
    {
155
        return $this->row >= 0 && $this->row < 9;
156
    }
157
158
    private function reachedEndOfGrid(): bool
159
    {
160
        return $this->row > 8;
161
    }
162
}
163