Files
shikaku/ios/Shikaku/Core/ShikakuModels.swift
2026-05-01 09:21:43 -04:00

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!")
}
}