153 lines
3.9 KiB
Swift
153 lines
3.9 KiB
Swift
import Foundation
|
|
|
|
struct GridPoint: Hashable, Sendable {
|
|
let row: Int
|
|
let col: Int
|
|
}
|
|
|
|
struct CellBounds: Hashable, Sendable {
|
|
let startRow: Int
|
|
let startCol: Int
|
|
let endRow: Int
|
|
let endCol: Int
|
|
|
|
var height: Int {
|
|
endRow - startRow + 1
|
|
}
|
|
|
|
var width: Int {
|
|
endCol - startCol + 1
|
|
}
|
|
|
|
var area: Int {
|
|
height * width
|
|
}
|
|
|
|
func contains(_ point: GridPoint) -> Bool {
|
|
startRow <= point.row && point.row <= endRow && startCol <= point.col && point.col <= endCol
|
|
}
|
|
|
|
var cells: [GridPoint] {
|
|
var result: [GridPoint] = []
|
|
result.reserveCapacity(area)
|
|
for row in startRow...endRow {
|
|
for col in startCol...endCol {
|
|
result.append(GridPoint(row: row, col: col))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
struct SolutionRect: Hashable, Sendable {
|
|
let row: Int
|
|
let col: Int
|
|
let height: Int
|
|
let width: Int
|
|
|
|
var area: Int {
|
|
height * width
|
|
}
|
|
|
|
var bounds: CellBounds {
|
|
CellBounds(startRow: row, startCol: col, endRow: row + height - 1, endCol: col + width - 1)
|
|
}
|
|
|
|
var cells: [GridPoint] {
|
|
bounds.cells
|
|
}
|
|
}
|
|
|
|
struct PlayerRect: Identifiable, Hashable, Sendable {
|
|
let id: UUID
|
|
let startRow: Int
|
|
let startCol: Int
|
|
let endRow: Int
|
|
let endCol: Int
|
|
|
|
init(id: UUID = UUID(), startRow: Int, startCol: Int, endRow: Int, endCol: Int) {
|
|
self.id = id
|
|
self.startRow = startRow
|
|
self.startCol = startCol
|
|
self.endRow = endRow
|
|
self.endCol = endCol
|
|
}
|
|
|
|
var bounds: CellBounds {
|
|
CellBounds(
|
|
startRow: min(startRow, endRow),
|
|
startCol: min(startCol, endCol),
|
|
endRow: max(startRow, endRow),
|
|
endCol: max(startCol, endCol)
|
|
)
|
|
}
|
|
|
|
var area: Int {
|
|
bounds.area
|
|
}
|
|
|
|
var cells: [GridPoint] {
|
|
bounds.cells
|
|
}
|
|
|
|
func contains(_ point: GridPoint) -> Bool {
|
|
bounds.contains(point)
|
|
}
|
|
}
|
|
|
|
struct ShikakuPuzzle: Sendable {
|
|
let size: Int
|
|
let clues: [GridPoint: Int]
|
|
let solution: [SolutionRect]
|
|
let seed: UInt64
|
|
}
|
|
|
|
struct ValidationResult: Equatable, Sendable {
|
|
let isSolved: Bool
|
|
let message: String
|
|
}
|
|
|
|
enum ShikakuValidation {
|
|
static func verify(size: Int, playerRects: [PlayerRect], clues: [GridPoint: Int]) -> ValidationResult {
|
|
var coverage: [GridPoint: Int] = [:]
|
|
|
|
for (index, rect) in playerRects.enumerated() {
|
|
for cell in rect.cells {
|
|
guard (0..<size).contains(cell.row), (0..<size).contains(cell.col) else {
|
|
return ValidationResult(isSolved: false, message: "A rectangle extends outside the grid")
|
|
}
|
|
|
|
if coverage[cell] != nil {
|
|
return ValidationResult(isSolved: false, message: "Cells overlap between two rectangles")
|
|
}
|
|
|
|
coverage[cell] = index
|
|
}
|
|
}
|
|
|
|
let totalCells = size * size
|
|
if coverage.count != totalCells {
|
|
return ValidationResult(isSolved: false, message: "Not all cells are covered (\(coverage.count)/\(totalCells))")
|
|
}
|
|
|
|
for rect in playerRects {
|
|
let clueValues = rect.cells.compactMap { clues[$0] }
|
|
|
|
if clueValues.isEmpty {
|
|
return ValidationResult(isSolved: false, message: "A rectangle contains no clue number")
|
|
}
|
|
|
|
if clueValues.count > 1 {
|
|
return ValidationResult(isSolved: false, message: "A rectangle contains more than one clue number")
|
|
}
|
|
|
|
let clueValue = clueValues[0]
|
|
if rect.area != clueValue {
|
|
return ValidationResult(isSolved: false, message: "A rectangle contains \(clueValue) but has \(rect.area) cells")
|
|
}
|
|
}
|
|
|
|
return ValidationResult(isSolved: true, message: "Solved!")
|
|
}
|
|
}
|