Puzzle Blast

Open in bitrigDownload Code
```swift
import SwiftUI

@main
struct PuzzleBlastApp: App {
    var body: some Scene {
        WindowGroup {
            PuzzleGameView()
        }
    }
}

struct PuzzleGameView: View {
    @State private var pieces: [PuzzlePiece] = []
    @State private var isExploded = false
    @State private var timeRemaining: Double = 30
    @State private var gameOver = false
    @State private var timerActive = false
    @State private var colorInverted = false
    
    let gridSize = 4
    let pieceSize: CGFloat = 80
    let symbols = ["star.fill", "heart.fill", "circle.fill", "square.fill", "triangle.fill", "diamond.fill", "hexagon.fill", "pentagon.fill"]
    
    var body: some View {
        ZStack {
            (colorInverted ? Color.white : Color.black).ignoresSafeArea()
            
            if !gameOver {
                VStack(spacing: 20) {
                    ZStack {
                        Text("Block Blast")
                            .font(.system(size: 44, weight: .heavy))
                            .foregroundStyle(colorInverted ? .black : .white)
                        
                        HStack {
                            Spacer()
                            Button {
                                shareContent()
                            } label: {
                                Image(systemName: "square.and.arrow.up")
                                    .font(.system(size: 22))
                                    .foregroundStyle(colorInverted ? .black : .white)
                            }
                            .padding(.trailing, 20)
                        }
                    }
                    .padding(.top, 60)
                    
                    Text("Reassemble the blocks in less than 30 seconds")
                        .font(.system(size: 16, weight: .medium))
                        .foregroundStyle((colorInverted ? Color.black : Color.white).opacity(0.8))
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 30)
                    
                    Text("Time: \(Int(timeRemaining))s")
                        .font(.system(size: 32, weight: .bold))
                        .foregroundStyle(colorInverted ? .black : .white)
                    
                    Spacer()
                    
                    ZStack {
                        ForEach(pieces) { piece in
                            PieceView(piece: piece, pieceSize: pieceSize)
                                .offset(x: piece.currentX, y: piece.currentY)
                                .gesture(
                                    isExploded ? DragGesture()
                                        .onChanged { value in
                                            if let index = pieces.firstIndex(where: { $0.id == piece.id }) {
                                                pieces[index].currentX = piece.dragStartX + value.translation.width
                                                pieces[index].currentY = piece.dragStartY + value.translation.height
                                            }
                                        }
                                        .onEnded { _ in
                                            if let index = pieces.firstIndex(where: { $0.id == piece.id }) {
                                                pieces[index].dragStartX = pieces[index].currentX
                                                pieces[index].dragStartY = pieces[index].currentY
                                                checkWin()
                                            }
                                        } : nil
                                )
                        }
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    
                    Spacer()
                    
                    if !isExploded {
                        Button {
                            explodePuzzle()
                        } label: {
                            Text("EXPLODE!")
                                .font(.system(size: 24, weight: .bold))
                                .foregroundStyle(.white)
                                .padding(.horizontal, 40)
                                .padding(.vertical, 16)
                                .background(Color.red)
                                .cornerRadius(12)
                        }
                        .padding(.bottom, 40)
                    }
                }
                .onAppear {
                    setupPuzzle()
                }
                .onChange(of: timerActive) { _, active in
                    if active {
                        startTimer()
                    }
                }
            } else {
                VStack(spacing: 30) {
                    Text("GAME OVER")
                        .font(.system(size: 60, weight: .bold))
                        .foregroundStyle(colorInverted ? .black : .white)
                        .transition(.opacity)
                    
                    Button {
                        resetGame()
                    } label: {
                        Text("PLAY AGAIN")
                            .font(.system(size: 24, weight: .bold))
                            .foregroundStyle(colorInverted ? .black : .white)
                            .padding(.horizontal, 40)
                            .padding(.vertical, 16)
                            .background(Color.green)
                            .cornerRadius(12)
                    }
                }
                .onAppear {
                    flashColors()
                }
            }
        }
        .animation(.easeInOut(duration: 1), value: gameOver)
    }
    
    func setupPuzzle() {
        pieces = []
        let colors: [Color] = [.red, .orange, .yellow, .green, .blue, .purple, .pink, .cyan]
        let symbolColorMap = Dictionary(uniqueKeysWithValues: zip(symbols, colors))
        
        var id = 0
        for row in 0..<gridSize {
            for col in 0..<gridSize {
                let x = CGFloat(col) * pieceSize - (CGFloat(gridSize - 1) * pieceSize / 2)
                let y = CGFloat(row) * pieceSize - (CGFloat(gridSize - 1) * pieceSize / 2)
                let symbol = symbols[id % symbols.count]
                let color = symbolColorMap[symbol] ?? .blue
                pieces.append(PuzzlePiece(
                    id: id,
                    correctX: x,
                    correctY: y,
                    currentX: x,
                    currentY: y,
                    symbol: symbol,
                    color: color
                ))
                id += 1
            }
        }
    }
    
    func resetGame() {
        gameOver = false
        isExploded = false
        timeRemaining = 30
        timerActive = false
        colorInverted = false
        setupPuzzle()
    }
    
    func flashColors() {
        var count = 0
        Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { timer in
            colorInverted.toggle()
            count += 1
            if count >= 10 {
                timer.invalidate()
            }
        }
    }
    
    func explodePuzzle() {
        isExploded = true
        let screenWidth = UIScreen.main.bounds.width
        let screenHeight = UIScreen.main.bounds.height
        let maxX = (screenWidth / 2) - (pieceSize / 2) - 20
        let maxY = (screenHeight / 2) - (pieceSize / 2) - 100
        
        withAnimation(.spring(response: 0.6, dampingFraction: 0.6)) {
            for index in pieces.indices {
                let randomX = CGFloat.random(in: -maxX...maxX)
                let randomY = CGFloat.random(in: -maxY...maxY)
                pieces[index].currentX = randomX
                pieces[index].currentY = randomY
                pieces[index].dragStartX = randomX
                pieces[index].dragStartY = randomY
            }
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
            timerActive = true
        }
    }
    
    func startTimer() {
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
            if timeRemaining > 0 {
                timeRemaining -= 0.1
            } else {
                timer.invalidate()
                gameOver = true
            }
        }
    }
    
    func checkWin() {
        let threshold: CGFloat = 30
        let allCorrect = pieces.allSatisfy { piece in
            abs(piece.currentX - piece.correctX) < threshold && abs(piece.currentY - piece.correctY) < threshold
        }
        if allCorrect {
            timerActive = false
        }
    }
    
    func shareContent() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
            guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                  let window = windowScene.windows.first(where: { $0.isKeyWindow }) else {
                return
            }
            
            let renderer = UIGraphicsImageRenderer(size: window.bounds.size)
            let screenshot = renderer.image { context in
                window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
            }
            
            let message = "I made this with the Bitrig App on my iPhone!\n\n"https://www.bitrig.app/share/wj06cdn1"
            
            let activityVC = UIActivityViewController(activityItems: [screenshot, message], applicationActivities: nil)
            
            if let rootVC = window.rootViewController {
                var topVC = rootVC
                while let presentedVC = topVC.presentedViewController {
                    topVC = presentedVC
                }
                
                activityVC.popoverPresentationController?.sourceView = window
                activityVC.popoverPresentationController?.sourceRect = CGRect(x: window.bounds.midX, y: window.bounds.midY, width: 0, height: 0)
                activityVC.popoverPresentationController?.permittedArrowDirections = []
                
                topVC.present(activityVC, animated: true)
            }
        }
    }
}

struct PuzzlePiece: Identifiable {
    let id: Int
    let correctX: CGFloat
    let correctY: CGFloat
    var currentX: CGFloat
    var currentY: CGFloat
    var dragStartX: CGFloat = 0
    var dragStartY: CGFloat = 0
    let symbol: String
    let color: Color
}

struct PieceView: View {
    let piece: PuzzlePiece
    let pieceSize: CGFloat
    
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(piece.color.gradient)
                .frame(width: pieceSize - 4, height: pieceSize - 4)
            
            Image(systemName: piece.symbol)
                .font(.system(size: 36))
                .foregroundStyle(.white)
        }
    }
}
```