August 27, 2024

P97 - Sudoku.

Sudoku puzzles go like this:

Problem          Solution

..4|8..|.17	 934|825|617
67.|9..|...	 672|914|853
5.8|.3.|..4	 518|637|924
---+---+---	 ---+---+---
3..|74.|1..	 325|748|169
.69|...|78.	 469|153|782
..1|.69|..5	 781|269|435
---+---+---	 ---+---+---
1..|.8.|3.6	 197|582|346
...|..6|.91	 853|476|291
24.|..1|5..	 246|391|578

Every cell in the puzzle belongs to a (horizontal) row and a (vertical) column, as well as to one single 3×3 square. At the beginning, some of the cells carry a single-digit number between 1 and 9. The problem is to fill the missing cells with digits in such a way that every number between 1 and 9 appears exactly once in each row, in each column, and in each square.

kotlin

package org.kotlin99.misc

import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo
import org.junit.Test
import org.kotlin99.misc.Sudoku.*
import org.kotlin99.misc.Sudoku.Board.Companion.size
import org.kotlin99.misc.Sudoku.Board.Companion.squareSize
import org.kotlin99.misc.Sudoku.Companion.toBoard
import org.kotlin99.misc.Sudoku.Point
import java.util.*

@Suppress("unused") // Because this class is a "namespace".
class Sudoku {

    data class Board(private val cells: MutableList<Cell> = MutableList(size * size){ Cell() }) {
        private val positionedCells: List<PositionedCell>
            get() = cells.mapIndexed { i, cell -> PositionedCell(Point(i % size, i / size), cell) }

        fun solve(): Sequence<Board> {
            optimizeGuesses()

            if (cells.any { it.isNotFilled() && it.guesses.isEmpty() }) return emptySequence()
            if (cells.all { it.isFilled() }) return sequenceOf(this)

            return positionedCells.find { it.cell.isNotFilled() }!!.let {
                it.cell.guesses.asSequence().flatMap { guess ->
                    copy().set(it.point.x, it.point.y, Cell(guess)).solve()
                }
            }
        }

        private fun optimizeGuesses() {
            fun cell(point: Point) = this[point.x, point.y]

            fun List<Point>.removeGuesses(value: Int) =
                filter { cell(it).isNotFilled() }
                    .forEach { set(it.x, it.y, cell(it).removeGuess(value)) }

            positionedCells
                .filter { it.cell.isFilled() }
                .forEach {
                    it.point.row().removeGuesses(it.cell.value!!)
                    it.point.column().removeGuesses(it.cell.value)
                    it.point.square().removeGuesses(it.cell.value)
                }
        }

        private fun copy(): Board = Board(ArrayList(cells))

        operator fun get(x: Int, y: Int): Cell = cells[x + y * size]

        operator fun set(x: Int, y: Int, cell: Cell): Board {
            cells[x + y * size] = cell
            return this
        }

        override fun toString(): String {
            fun <T> List<T>.slicedBy(sliceSize: Int): List<List<T>> =
                if (size <= sliceSize) listOf(this)
                else listOf(take(sliceSize)) + drop(sliceSize).slicedBy(sliceSize)

            fun <T> List<T>.mapJoin(separator: String, f: (T) -> String) = joinToString(separator) { f(it) }

            return positionedCells.slicedBy(size * squareSize).mapJoin("\n---+---+---\n") { section ->
                section.slicedBy(size).mapJoin("\n") { row ->
                    row.slicedBy(squareSize).mapJoin("|") { slice ->
                        slice.mapJoin("") {
                            if (it.cell.isFilled()) it.cell.value.toString() else "."
                        }
                    }
                }
            }
        }

        companion object {
            const val size = 9
            const val squareSize = size / 3
        }
    }

    private data class PositionedCell(val point: Point, val cell: Cell)

    data class Point(val x: Int, val y: Int) {
        fun row() = 0.until(size).map { Point(it, y) }

        fun column() = 0.until(size).map { Point(x, it) }

        fun square(): List<Point> {
            fun rangeOfSquare(coordinate: Int): IntRange {
                return (coordinate / squareSize).let {
                    (it * squareSize).until((it + 1) * squareSize)
                }
            }
            return rangeOfSquare(y).flatMap { cellY ->
                rangeOfSquare(x).map { cellX ->
                    Point(cellX, cellY)
                }
            }
        }
    }

    data class Cell(val value: Int?, val guesses: List<Int>) {
        constructor(): this(null, 1.rangeTo(size).toList())
        constructor(value: Int): this(value, emptyList())

        fun isFilled() = value != null
        fun isNotFilled() = !isFilled()
        fun removeGuess(value: Int) = if (guesses.isEmpty()) this else copy(guesses = guesses - value)
    }

    companion object {
        fun String.toBoard(): Board {
            val cells = replace(Regex("[|\\-+\n]"), "").mapTo(ArrayList()) { c ->
                if (c == '.') Cell() else Cell(c.toString().toInt())
            }
            return Board(cells)
        }
    }
}

class P97Test {
    @Test fun `solve sudoku example from readme`() {
        val board = """
            |..4|8..|.17
            |67.|9..|...
            |5.8|.3.|..4
            |---+---+---
            |3..|74.|1..
            |.69|...|78.
            |..1|.69|..5
            |---+---+---
            |1..|.8.|3.6
            |...|..6|.91
            |24.|..1|5..
        """.trimMargin().toBoard()

        assertThat(board.solve().first(), equalTo("""
            |934|825|617
            |672|914|853
            |518|637|924
            |---+---+---
            |325|748|169
            |469|153|782
            |781|269|435
            |---+---+---
            |197|582|346
            |853|476|291
            |246|391|578
        """.trimMargin().toBoard()))
    }

    @Test fun `solve medium sudoku from dailysudoku-dot-com`() {
        // http://dailysudoku.com/sudoku/archive/2016/11/2016-11-6_solution.shtml
        val board = """
            |6..|...|2.3
            |...|4.3|8..
            |.3.|7..|..9
            |---+---+---
            |...|.2.|1..
            |49.|...|.65
            |..6|.9.|...
            |---+---+---
            |1..|..5|.8.
            |..9|6..|...
            |8.4|...|..2
        """.trimMargin().toBoard()

        assertThat(board.solve().first(), equalTo("""
            |641|859|273
            |927|413|856
            |538|762|419
            |---+---+---
            |785|326|194
            |492|187|365
            |316|594|728
            |---+---+---
            |163|245|987
            |279|638|541
            |854|971|632
        """.trimMargin().toBoard()))
    }

    @Test fun `solve hard sudoku from dailysudoku-dot-com`() {
        // http://dailysudoku.com/sudoku/archive/2016/11/2016-11-5_solution.shtml
        val board = """
            |.7.|..1|...
            |19.|6.5|...
            |84.|.7.|.9.
            |---+---+---
            |9..|..8|5..
            |5.7|...|8.1
            |..1|5..|..2
            |---+---+---
            |.5.|.2.|.19
            |...|9.4|.86
            |...|1..|.5.
        """.trimMargin().toBoard()

        assertThat(board.solve().first(), equalTo("""
            |275|891|634
            |193|645|728
            |846|372|195
            |---+---+---
            |964|218|573
            |527|439|861
            |381|567|942
            |---+---+---
            |658|723|419
            |712|954|386
            |439|186|257
        """.trimMargin().toBoard()))
    }

    @Test fun `solve very hard sudoku from dailysudoku-dot-com`() {
        // http://dailysudoku.com/sudoku/archive/2016/11/2016-11-7_solution.shtml
        val board = """
            |.1.|...|..4
            |7.9|..1|..5
            |..5|9.7|..6
            |---+---+---
            |3.4|...|.2.
            |...|3.2|...
            |.7.|...|9.3
            |---+---+---
            |9..|8.6|4..
            |2..|5..|3.1
            |1..|...|.6.
        """.trimMargin().toBoard()

        assertThat(board.solve().first(), equalTo("""
            |816|235|794
            |729|461|835
            |435|987|216
            |---+---+---
            |384|659|127
            |591|372|648
            |672|148|953
            |---+---+---
            |953|816|472
            |267|594|381
            |148|723|569
        """.trimMargin().toBoard()))
    }

    @Test fun `square coordinates of a point`() {
        assertThat(Point(1, 2).square(), equalTo(listOf(
            Point(0, 0), Point(1, 0), Point(2, 0),
            Point(0, 1), Point(1, 1), Point(2, 1),
            Point(0, 2), Point(1, 2), Point(2, 2)
        )))
        assertThat(Point(0, 8).square(), equalTo(listOf(
            Point(0, 6), Point(1, 6), Point(2, 6),
            Point(0, 7), Point(1, 7), Point(2, 7),
            Point(0, 8), Point(1, 8), Point(2, 8)
        )))
        assertThat(Point(7, 7).square(), equalTo(listOf(
            Point(6, 6), Point(7, 6), Point(8, 6),
            Point(6, 7), Point(7, 7), Point(8, 7),
            Point(6, 8), Point(7, 8), Point(8, 8)
        )))
    }

    @Test fun `convert string to board`() {
        val board = """
            |..4|8..|.17
            |67.|9..|...
            |5.8|.3.|..4
            |---+---+---
            |3..|74.|1..
            |.69|...|78.
            |..1|.69|..5
            |---+---+---
            |1..|.8.|3.6
            |...|..6|.91
            |24.|..1|5..
        """.trimMargin().toBoard()

        board.apply {
            assertThat(this[0, 0], equalTo(Cell()))
            assertThat(this[2, 0], equalTo(Cell(4)))
            assertThat(this[0, 2], equalTo(Cell(5)))
            assertThat(this[8, 7], equalTo(Cell(1)))
        }
    }

    @Test fun `convert board to string`() {
        val board = Board().apply {
            set(0, 0, Cell(9))
            set(4, 0, Cell(8))
            set(8, 0, Cell(7))
            set(0, 4, Cell(6))
            set(4, 4, Cell(5))
            set(8, 4, Cell(4))
            set(0, 8, Cell(3))
            set(4, 8, Cell(2))
            set(8, 8, Cell(1))
        }
        assertThat(board.toString(), equalTo("""
            |9..|.8.|..7
            |...|...|...
            |...|...|...
            |---+---+---
            |...|...|...
            |6..|.5.|..4
            |...|...|...
            |---+---+---
            |...|...|...
            |...|...|...
            |3..|.2.|..1
        """.trimMargin()))
    }
}
Be first to comment
Leave a reply