working ios game
This commit is contained in:
209
ios/Shikaku/Views/ShikakuGridView.swift
Normal file
209
ios/Shikaku/Views/ShikakuGridView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user