import Foundation enum PuzzleGenerationError: Error, LocalizedError { case unsupportedSize(Int) case failed(size: Int, attempts: Int) var errorDescription: String? { switch self { case .unsupportedSize(let size): return "Grid size \(size) is not supported." case .failed(let size, let attempts): return "Puzzle generation failed for \(size)x\(size) after \(attempts) attempts." } } } struct SeededRandomNumberGenerator: RandomNumberGenerator { private var state: UInt64 init(seed: UInt64) { self.state = seed == 0 ? 0x9E3779B97F4A7C15 : seed } mutating func next() -> UInt64 { state &+= 0x9E3779B97F4A7C15 var z = state z = (z ^ (z >> 30)) &* 0xBF58476D1CE4E5B9 z = (z ^ (z >> 27)) &* 0x94D049BB133111EB return z ^ (z >> 31) } mutating func randomDouble() -> Double { let value = next() >> 11 return Double(value) / Double(1 << 53) } mutating func weightedLowIndex(count: Int) -> Int { guard count > 1 else { return 0 } // The Python version sampled a beta distribution to favor smaller rectangles. // This power curve keeps the same bias without pulling in a statistics library. let fraction = pow(randomDouble(), 2.2) return min(Int(fraction * Double(count)), count - 1) } } enum ShikakuGenerator { static let maximumAttempts = 200 private static let maximumRandomSeed: UInt64 = 2_147_483_648 static func generate(size: Int, seed requestedSeed: UInt64? = nil) throws -> ShikakuPuzzle { guard size > 1 else { throw PuzzleGenerationError.unsupportedSize(size) } let seed = requestedSeed ?? UInt64.random(in: 0...maximumRandomSeed) var rng = SeededRandomNumberGenerator(seed: seed) for _ in 0.. ShikakuPuzzle? { var grid = Array(repeating: Array(repeating: -1, count: size), count: size) var rects: [SolutionRect] = [] var uncovered = (0..= 0, col0 >= 0, row1 < size, col1 < size else { continue } if rectangleIsUncovered(row0: row0, col0: col0, row1: row1, col1: col1, grid: grid) { candidates.append(SolutionRect(row: row0, col: col0, height: height, width: width)) } } } } } if candidates.isEmpty { guard absorbIsolatedCell(start, size: size, grid: &grid, rects: &rects, uncovered: &uncovered, rng: &rng) else { return nil } continue } let keyedCandidates = candidates.map { rect in (rect: rect, tieBreak: rng.randomDouble()) } let sortedCandidates = keyedCandidates .sorted { lhs, rhs in if lhs.rect.area == rhs.rect.area { return lhs.tieBreak < rhs.tieBreak } return lhs.rect.area < rhs.rect.area } .map(\.rect) let chosen = sortedCandidates[rng.weightedLowIndex(count: sortedCandidates.count)] let rectID = rects.count for cell in chosen.cells { grid[cell.row][cell.col] = rectID } rects.append(chosen) uncovered.removeAll { grid[$0.row][$0.col] != -1 } } guard (0.. Bool { for row in row0...row1 { for col in col0...col1 where grid[row][col] != -1 { return false } } return true } private static func absorbIsolatedCell( _ cell: GridPoint, size: Int, grid: inout [[Int]], rects: inout [SolutionRect], uncovered: inout [GridPoint], rng: inout SeededRandomNumberGenerator ) -> Bool { var neighbors = [ GridPoint(row: cell.row - 1, col: cell.col), GridPoint(row: cell.row + 1, col: cell.col), GridPoint(row: cell.row, col: cell.col - 1), GridPoint(row: cell.row, col: cell.col + 1) ] neighbors.shuffle(using: &rng) for neighbor in neighbors { guard (0..