155 lines
4.4 KiB
Swift
155 lines
4.4 KiB
Swift
import Foundation
|
|
|
|
enum GameStatus: Equatable {
|
|
case playing
|
|
case solved
|
|
case error
|
|
}
|
|
|
|
@MainActor
|
|
final class ShikakuGame: ObservableObject {
|
|
static let supportedSizes = [5, 7, 10, 15, 20, 25]
|
|
|
|
@Published private(set) var puzzle: ShikakuPuzzle
|
|
@Published private(set) var playerRects: [PlayerRect] = []
|
|
@Published private(set) var errorRectIDs: Set<PlayerRect.ID> = []
|
|
@Published private(set) var message = "Ready"
|
|
@Published private(set) var status: GameStatus = .playing
|
|
|
|
private var startDate = Date()
|
|
private var solvedElapsed: TimeInterval?
|
|
|
|
var size: Int {
|
|
puzzle.size
|
|
}
|
|
|
|
var currentSeed: UInt64 {
|
|
puzzle.seed
|
|
}
|
|
|
|
init(size: Int = 5) {
|
|
self.puzzle = Self.makePuzzle(size: size)
|
|
self.startDate = Date()
|
|
}
|
|
|
|
func newPuzzle(size requestedSize: Int? = nil, seed requestedSeed: UInt64? = nil) {
|
|
let nextSize = requestedSize ?? size
|
|
puzzle = Self.makePuzzle(size: nextSize, seed: requestedSeed)
|
|
playerRects = []
|
|
errorRectIDs = []
|
|
message = "Ready"
|
|
status = .playing
|
|
startDate = Date()
|
|
solvedElapsed = nil
|
|
}
|
|
|
|
func clear() {
|
|
playerRects = []
|
|
errorRectIDs = []
|
|
message = "Cleared"
|
|
status = .playing
|
|
|
|
if let solvedElapsed {
|
|
startDate = Date().addingTimeInterval(-solvedElapsed)
|
|
}
|
|
|
|
solvedElapsed = nil
|
|
}
|
|
|
|
func placeRect(_ rect: PlayerRect) {
|
|
guard rect.area > 1 else {
|
|
errorRectIDs = []
|
|
status = .playing
|
|
message = "Drag over at least 2 cells"
|
|
return
|
|
}
|
|
|
|
let newCells = Set(rect.cells)
|
|
playerRects.removeAll { !Set($0.cells).isDisjoint(with: newCells) }
|
|
playerRects.append(rect)
|
|
errorRectIDs = []
|
|
status = .playing
|
|
runAutoCheck()
|
|
}
|
|
|
|
func removeRect(at cell: GridPoint) {
|
|
let oldCount = playerRects.count
|
|
playerRects.removeAll { $0.contains(cell) }
|
|
|
|
if playerRects.count != oldCount {
|
|
errorRectIDs = []
|
|
status = .playing
|
|
message = "Ready"
|
|
solvedElapsed = nil
|
|
}
|
|
}
|
|
|
|
func checkSolution() {
|
|
let result = ShikakuValidation.verify(size: size, playerRects: playerRects, clues: puzzle.clues)
|
|
|
|
if result.isSolved {
|
|
markSolved()
|
|
errorRectIDs = []
|
|
message = "Solved in \(formattedElapsed())"
|
|
} else {
|
|
status = .error
|
|
errorRectIDs = invalidRectIDs()
|
|
message = result.message
|
|
}
|
|
}
|
|
|
|
func formattedElapsed(at date: Date = Date()) -> String {
|
|
let elapsed = solvedElapsed ?? max(0, date.timeIntervalSince(startDate))
|
|
let minutes = Int(elapsed) / 60
|
|
let seconds = Int(elapsed) % 60
|
|
return String(format: "%d:%02d", minutes, seconds)
|
|
}
|
|
|
|
private func runAutoCheck() {
|
|
guard status != .solved else {
|
|
return
|
|
}
|
|
|
|
let result = ShikakuValidation.verify(size: size, playerRects: playerRects, clues: puzzle.clues)
|
|
if result.isSolved {
|
|
markSolved()
|
|
message = "Solved in \(formattedElapsed())"
|
|
} else {
|
|
status = .playing
|
|
message = "Ready"
|
|
}
|
|
}
|
|
|
|
private func markSolved() {
|
|
if solvedElapsed == nil {
|
|
solvedElapsed = Date().timeIntervalSince(startDate)
|
|
}
|
|
status = .solved
|
|
}
|
|
|
|
private func invalidRectIDs() -> Set<PlayerRect.ID> {
|
|
Set(playerRects.compactMap { rect in
|
|
let clueValues = rect.cells.compactMap { puzzle.clues[$0] }
|
|
if clueValues.count != 1 || clueValues.first != rect.area {
|
|
return rect.id
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
private static func makePuzzle(size: Int, seed requestedSeed: UInt64? = nil) -> ShikakuPuzzle {
|
|
do {
|
|
return try ShikakuGenerator.generate(size: size, seed: requestedSeed)
|
|
} catch {
|
|
assertionFailure("Falling back after puzzle generation error: \(error)")
|
|
return fallbackPuzzle(size: size)
|
|
}
|
|
}
|
|
|
|
private static func fallbackPuzzle(size: Int) -> ShikakuPuzzle {
|
|
let rect = SolutionRect(row: 0, col: 0, height: size, width: size)
|
|
let clue = GridPoint(row: size / 2, col: size / 2)
|
|
return ShikakuPuzzle(size: size, clues: [clue: rect.area], solution: [rect], seed: 0)
|
|
}
|
|
}
|