working ios game

This commit is contained in:
2026-05-01 09:21:43 -04:00
commit 89b2e779f5
17 changed files with 2892 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
import SwiftUI
enum DrawingMode: String, CaseIterable, Identifiable {
case draw
case erase
var id: String {
rawValue
}
var title: String {
switch self {
case .draw:
return "Draw"
case .erase:
return "Erase"
}
}
var symbolName: String {
switch self {
case .draw:
return "rectangle"
case .erase:
return "eraser"
}
}
}
struct ShikakuGridView: View {
@ObservedObject var game: ShikakuGame
let mode: DrawingMode
let side: CGFloat
@State private var dragStart: GridPoint?
@State private var dragEnd: GridPoint?
@State private var lastErasedCell: GridPoint?
var body: some View {
Canvas { context, _ in
drawBoard(in: CGRect(x: 0, y: 0, width: side, height: side), context: &context)
}
.frame(width: side, height: side)
.contentShape(Rectangle())
.gesture(boardGesture(side: side))
.accessibilityLabel("Shikaku grid")
.accessibilityValue("\(game.size) by \(game.size)")
}
private func boardGesture(side: CGFloat) -> some Gesture {
DragGesture(minimumDistance: 0)
.onChanged { value in
guard let cell = cell(at: value.location, side: side) else {
return
}
switch mode {
case .draw:
if dragStart == nil {
dragStart = cell
}
dragEnd = cell
case .erase:
guard lastErasedCell != cell else {
return
}
game.removeRect(at: cell)
lastErasedCell = cell
dragStart = nil
dragEnd = nil
}
}
.onEnded { value in
defer {
dragStart = nil
dragEnd = nil
lastErasedCell = nil
}
guard mode == .draw,
let start = dragStart,
let end = cell(at: value.location, side: side) ?? dragEnd else {
return
}
game.placeRect(
PlayerRect(
startRow: start.row,
startCol: start.col,
endRow: end.row,
endCol: end.col
)
)
}
}
private func drawBoard(in rect: CGRect, context: inout GraphicsContext) {
let cellSide = rect.width / CGFloat(game.size)
let boardRect = CGRect(origin: .zero, size: rect.size)
context.fill(Path(boardRect), with: .color(ShikakuTheme.paper))
for (index, playerRect) in game.playerRects.enumerated() {
draw(playerRect, index: index, cellSide: cellSide, context: &context)
}
if let start = dragStart, let end = dragEnd, mode == .draw {
let preview = PlayerRect(startRow: start.row, startCol: start.col, endRow: end.row, endCol: end.col)
drawPreview(preview, cellSide: cellSide, context: &context)
}
drawGridLines(cellSide: cellSide, boardRect: boardRect, context: &context)
drawClues(cellSide: cellSide, context: &context)
}
private func draw(_ rect: PlayerRect, index: Int, cellSide: CGFloat, context: inout GraphicsContext) {
let pathRect = cgRect(for: rect.bounds, cellSide: cellSide).insetBy(dx: 2, dy: 2)
let path = Path(roundedRect: pathRect, cornerRadius: 6)
let hasError = game.errorRectIDs.contains(rect.id)
if hasError {
context.fill(path, with: .color(ShikakuTheme.errorFill))
context.stroke(path, with: .color(ShikakuTheme.errorStroke), lineWidth: 2.5)
} else {
let colorIndex = index % ShikakuTheme.rectFills.count
context.fill(path, with: .color(ShikakuTheme.rectFills[colorIndex]))
context.stroke(path, with: .color(ShikakuTheme.rectStrokes[colorIndex]), lineWidth: 2.5)
}
}
private func drawPreview(_ rect: PlayerRect, cellSide: CGFloat, context: inout GraphicsContext) {
let pathRect = cgRect(for: rect.bounds, cellSide: cellSide).insetBy(dx: 2, dy: 2)
let path = Path(roundedRect: pathRect, cornerRadius: 6)
let isInvalid = rect.area == 1
context.fill(path, with: .color(isInvalid ? ShikakuTheme.errorFill : ShikakuTheme.dragFill))
context.stroke(path, with: .color(isInvalid ? ShikakuTheme.errorStroke : ShikakuTheme.dragStroke), lineWidth: 2)
let fontSize = max(10, min(26, cellSide * 0.38))
let center = CGPoint(x: pathRect.midX, y: pathRect.midY)
let label = Text("\(rect.area)")
.font(.system(size: fontSize, weight: .bold, design: .rounded))
context.draw(
label.foregroundStyle(Color.black.opacity(0.55)),
at: CGPoint(x: center.x + 1, y: center.y + 1),
anchor: .center
)
context.draw(
label.foregroundStyle(Color.white.opacity(0.95)),
at: center,
anchor: .center
)
}
private func drawGridLines(cellSide: CGFloat, boardRect: CGRect, context: inout GraphicsContext) {
var gridPath = Path()
for index in 0...game.size {
let offset = CGFloat(index) * cellSide
gridPath.move(to: CGPoint(x: offset, y: 0))
gridPath.addLine(to: CGPoint(x: offset, y: boardRect.maxY))
gridPath.move(to: CGPoint(x: 0, y: offset))
gridPath.addLine(to: CGPoint(x: boardRect.maxX, y: offset))
}
context.stroke(gridPath, with: .color(ShikakuTheme.gridLine), lineWidth: 0.8)
context.stroke(Path(boardRect), with: .color(ShikakuTheme.gridBorder), lineWidth: 3)
}
private func drawClues(cellSide: CGFloat, context: inout GraphicsContext) {
let fontSize = max(14, min(24, cellSide * 0.36))
for (cell, value) in game.puzzle.clues {
let point = CGPoint(
x: (CGFloat(cell.col) + 0.5) * cellSide,
y: (CGFloat(cell.row) + 0.5) * cellSide
)
let text = Text("\(value)")
.font(.system(size: fontSize, weight: .bold, design: .rounded))
.foregroundStyle(ShikakuTheme.clue)
context.draw(text, at: point, anchor: .center)
}
}
private func cgRect(for bounds: CellBounds, cellSide: CGFloat) -> CGRect {
CGRect(
x: CGFloat(bounds.startCol) * cellSide,
y: CGFloat(bounds.startRow) * cellSide,
width: CGFloat(bounds.width) * cellSide,
height: CGFloat(bounds.height) * cellSide
)
}
private func cell(at location: CGPoint, side: CGFloat) -> GridPoint? {
guard location.x >= 0,
location.y >= 0,
location.x <= side,
location.y <= side else {
return nil
}
let cellSide = side / CGFloat(game.size)
let row = min(Int(location.y / cellSide), game.size - 1)
let col = min(Int(location.x / cellSide), game.size - 1)
return GridPoint(row: row, col: col)
}
}

View File

@@ -0,0 +1,41 @@
import SwiftUI
enum ShikakuTheme {
static let background = Color(red: 0.11, green: 0.10, blue: 0.13)
static let panel = Color(red: 0.16, green: 0.15, blue: 0.18)
static let paper = Color(red: 0.95, green: 0.93, blue: 0.88)
static let gridLine = Color(red: 0.55, green: 0.52, blue: 0.48)
static let gridBorder = Color(red: 0.20, green: 0.18, blue: 0.22)
static let clue = Color(red: 0.10, green: 0.10, blue: 0.12)
static let mutedText = Color(red: 0.67, green: 0.62, blue: 0.56)
static let primaryText = Color(red: 0.91, green: 0.89, blue: 0.84)
static let success = Color(red: 0.34, green: 0.78, blue: 0.48)
static let error = Color(red: 0.88, green: 0.28, blue: 0.28)
static let accent = Color(red: 0.49, green: 0.38, blue: 0.72)
static let dragFill = Color(red: 0.50, green: 0.70, blue: 0.95).opacity(0.30)
static let dragStroke = Color(red: 0.30, green: 0.55, blue: 0.90).opacity(0.90)
static let errorFill = Color(red: 0.90, green: 0.25, blue: 0.25).opacity(0.30)
static let errorStroke = Color(red: 0.80, green: 0.10, blue: 0.10).opacity(0.90)
static let rectFills: [Color] = [
Color(red: 0.36, green: 0.61, blue: 0.84).opacity(0.45),
Color(red: 0.42, green: 0.78, blue: 0.60).opacity(0.45),
Color(red: 0.90, green: 0.55, blue: 0.35).opacity(0.45),
Color(red: 0.76, green: 0.45, blue: 0.82).opacity(0.45),
Color(red: 0.88, green: 0.76, blue: 0.30).opacity(0.45),
Color(red: 0.45, green: 0.82, blue: 0.85).opacity(0.45),
Color(red: 0.90, green: 0.65, blue: 0.80).opacity(0.45),
Color(red: 0.55, green: 0.72, blue: 0.35).opacity(0.45)
]
static let rectStrokes: [Color] = [
Color(red: 0.20, green: 0.42, blue: 0.70),
Color(red: 0.22, green: 0.60, blue: 0.40),
Color(red: 0.75, green: 0.35, blue: 0.12),
Color(red: 0.56, green: 0.25, blue: 0.65),
Color(red: 0.70, green: 0.58, blue: 0.08),
Color(red: 0.22, green: 0.62, blue: 0.68),
Color(red: 0.80, green: 0.45, blue: 0.65),
Color(red: 0.35, green: 0.54, blue: 0.18)
]
}