Stratadox /
PuzzleSolver
| 1 | <?php declare(strict_types=1); |
||
| 2 | |||
| 3 | namespace Stratadox\PuzzleSolver\Puzzle\Sudoku; |
||
| 4 | |||
| 5 | use Stratadox\PuzzleSolver\Moves; |
||
| 6 | use function assert; |
||
| 7 | |||
| 8 | final class Sudoku |
||
| 9 | { |
||
| 10 | /** @var int[][]|null[][] */ |
||
| 11 | private $numbers; |
||
| 12 | /** @var bool[][] */ |
||
| 13 | private $rows = []; |
||
| 14 | /** @var bool[][] */ |
||
| 15 | private $cols = []; |
||
| 16 | /** @var bool[][][] */ |
||
| 17 | private $boxes = []; |
||
| 18 | /** @var int[]|null */ |
||
| 19 | private $firstEmptyPosition; |
||
| 20 | |||
| 21 | public function __construct(array ...$numbers) |
||
| 22 | { |
||
| 23 | $this->numbers = $numbers; |
||
| 24 | foreach ($numbers as $r => $row) { |
||
| 25 | foreach ($row as $c => $number) { |
||
| 26 | if (null !== $number) { |
||
| 27 | $this->rows[$r][$number] = true; |
||
| 28 | $this->cols[$c][$number] = true; |
||
| 29 | $this->boxes[$r / 3][$c / 3][$number] = true; |
||
| 30 | } elseif (null === $this->firstEmptyPosition) { |
||
| 31 | $this->firstEmptyPosition = [$r, $c]; |
||
| 32 | } |
||
| 33 | } |
||
| 34 | } |
||
| 35 | } |
||
| 36 | |||
| 37 | public function isFull(): bool |
||
| 38 | { |
||
| 39 | return null === $this->firstEmptyPosition; |
||
| 40 | } |
||
| 41 | |||
| 42 | public function possibilitiesForNextEmptyPosition(): Moves |
||
| 43 | { |
||
| 44 | if (null === $this->firstEmptyPosition) { |
||
| 45 | return Moves::none(); |
||
| 46 | } |
||
| 47 | [$r, $c] = $this->firstEmptyPosition; |
||
| 48 | return Entry::allPossibilitiesFor($r, $c)->filterWith(function (Entry $entry) { |
||
|
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
| 49 | return $this->allows($entry); |
||
| 50 | }); |
||
| 51 | } |
||
| 52 | |||
| 53 | // For immutable sudoku puzzle implementations |
||
| 54 | public function withFilledIn(Entry ...$entries): self |
||
| 55 | { |
||
| 56 | $numbers = $this->numbers; |
||
| 57 | foreach ($entries as $entry) { |
||
| 58 | assert($this->allows($entry)); |
||
| 59 | $numbers[$entry->row()][$entry->column()] = $entry->number(); |
||
| 60 | } |
||
| 61 | return new self(...$numbers); |
||
| 62 | } |
||
| 63 | |||
| 64 | public function __toString(): string |
||
| 65 | { |
||
| 66 | $sudoku = "\n+-------+-------+-------+ \n"; |
||
| 67 | foreach ($this->numbers as $r => $row) { |
||
| 68 | $sudoku .= '| '; |
||
| 69 | foreach ($row as $c => $value) { |
||
| 70 | $sudoku .= ($value ?: ' ') . ' '; |
||
| 71 | if ($c % 3 === 2) { |
||
| 72 | $sudoku .= '| '; |
||
| 73 | } |
||
| 74 | } |
||
| 75 | $sudoku .= "\n"; |
||
| 76 | if ($r % 3 === 2) { |
||
| 77 | $sudoku .= "+-------+-------+-------+ \n"; |
||
| 78 | } |
||
| 79 | } |
||
| 80 | return $sudoku; |
||
| 81 | } |
||
| 82 | |||
| 83 | private function allows(Entry $entry): bool |
||
| 84 | { |
||
| 85 | return null === $this->numbers[$entry->row()][$entry->column()] |
||
| 86 | && $this->canAddInRow($entry->row(), $entry->number()) |
||
| 87 | && $this->canAddInColumn($entry->column(), $entry->number()) |
||
| 88 | && $this->canAddInRegion($entry->row(), $entry->column(), $entry->number()); |
||
| 89 | } |
||
| 90 | |||
| 91 | private function canAddInRow(int $row, int $number): bool |
||
| 92 | { |
||
| 93 | return !isset($this->rows[$row][$number]); |
||
| 94 | } |
||
| 95 | |||
| 96 | private function canAddInColumn(int $column, int $number): bool |
||
| 97 | { |
||
| 98 | return !isset($this->cols[$column][$number]); |
||
| 99 | } |
||
| 100 | |||
| 101 | private function canAddInRegion(int $row, int $column, int $number): bool |
||
| 102 | { |
||
| 103 | return !isset($this->boxes[$row / 3][$column / 3][$number]); |
||
| 104 | } |
||
| 105 | } |
||
| 106 |