CodeBreaker’s Model

内容准备

MeetXCode.swift

import SwiftUI

@main
struct MeetXCodeApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView3()
        }
    }
}

ContentView3.swift

import SwiftUI

struct ContentView3: View {
    var body: some View {
        VStack{
            peges(colors: [Color.green, Color.blue, Color.blue, Color.red])
            peges(colors: [Color.green, Color.blue, Color.red, Color.red])
            peges(colors: [Color.yellow, Color.blue, Color.yellow, Color.green])
            peges(colors: [Color.yellow, Color.green, Color.yellow, Color.green])
        }.padding()
    }

    @ViewBuilder
    func peges(colors: Array<Color>)-> some View {
        HStack{
            ForEach(colors.indices, id: \.self) { index in
                RoundedRectangle(cornerRadius: 10).aspectRatio(1, contentMode: .fit)
                    .foregroundStyle(colors[index])
            }
            MatchMarkers3(matches: [Match3.exact, Match3.inexact, Match3.nomatch, Match3.exact])
        }
    }
}

#Preview {
    ContentView3()
}

MatchMarkers3.swift

import SwiftUI

enum Match3 {
    case nomatch
    case exact
    case inexact
}

struct MatchMarkers3: View {
    // var matches: [Match]
    var matches: Array<Match3>
    
    var body: some View {
        HStack {
            VStack {
                matchMarker(peg: 0)
                matchMarker(peg: 1)
            }
            VStack {
                matchMarker(peg: 2)
                matchMarker(peg: 3)
            }
        }
    }
    
    func isNotNomatch(matchItem: Match3)-> Bool {
        return matchItem != Match3.nomatch
    }
    
    func matchMarker(peg: Int) -> some View {
        let exactCount = self.matches.count(where: { matchItem in matchItem == Match3.exact})
        // 传递函数写法:
        // let foundCount = self.matches.count(where: isNotNomatch)
        // foundCount 改为内联写法:
        // let foundCount = self.matches.count(where: {(matchItem: Match) -> Bool in
        //      matchItem != Match.nomatch
        // })
        // 极限简化后写法用 $number 代表函数参数
        // let foundCount = matches.count(where: {$0 != Match.nomatch})
        // 终极简化
        let foundCount = matches.count{ $0 != Match3.nomatch }
        
        // 普通模式下 如果 exactCount > peg 则填充为黑色
        //          如果 foundCount > peg 边框为黑色当白钉子
        //          填充为白 边框为白则表示没有钉子
        return Circle().fill(exactCount > peg ? Color.primary : Color.clear)
            .strokeBorder(foundCount > peg ? Color.primary : Color.clear, lineWidth: 2)
            .aspectRatio(1, contentMode: ContentMode.fit)
    }
}

#Preview {
    // exactCount为1 foundCount为3 一个黑钉 两个白钉
    MatchMarkers3(matches: [Match3.exact, Match3.inexact, Match3.nomatch, Match3.inexact])
}

创建 CodeBreaker.swift

import Foundation

基本准备就绪

构建 CodeBreaker

CodeBreaker.swift

import SwiftUI

// 这里直接用了Color,其实与UI的东西解耦才更好,比如用字符串表示颜色
typealias Peg = Color

struct CodeBreaker {
    // 答案
    var masterCode: Code = Code(kind: Code.Kind.master)
    // 玩家猜
    var guess: Code = Code(kind: Code.Kind.guess)
    // 猜过的历史结果
    var attempts: [Code] = []
    
    // 能选的钉子
    let pegChoices: [Peg]

    // 默认参数构造函数
    init(pegChoices: [Peg] = [.green, .red, .blue, .yellow]) {
        self.pegChoices = pegChoices
        masterCode.randomize(from: pegChoices)
        print(masterCode)
    }
    
    mutating func attemptGuess() {
        var attempt = guess
        attempt.kind = Code.Kind.attempt(guess.match(against: masterCode))
        attempts.append(attempt)
    }
    
    mutating func changeGuessPeg(at index: Int) {
        let existingPeg = guess.pegs[index]
        if let indexOfExistingPegInPegChoics = pegChoices.firstIndex(of: existingPeg) {
            let newPeg = pegChoices[(indexOfExistingPegInPegChoics + 1) % pegChoices.count]
            guess.pegs[index] = newPeg
        } else {
            guess.pegs[index] = pegChoices.first ?? Code.missing
        }
    }
}

struct Code {
    var kind: Kind // Kind?
    // .clear 触控会消失 无法触控回调
    var pegs: [Peg] = Array(repeating: Code.missing, count: 4)
    
    static let missing: Peg = .clear

    enum Kind: Equatable {
        case master
        case guess
        case attempt([Match3])
        case unknow
    }
    
    var matches: [Match3] {
        switch self.kind {
        case .attempt(let matches): return matches
        default: return []
        }
    }
    
    mutating func randomize(from pegChoices: [Peg]) {
        for index in pegChoices.indices {
            pegs[index] = pegChoices.randomElement() ?? Code.missing
        }
    }
    
    func match(against otherCode: Code) -> [Match3] {
        var results: [Match3] = Array(repeating: Match3.nomatch, count: pegs.count)
        var pegsToMatch = otherCode.pegs
        
        for index in pegs.indices.reversed() {
            if pegsToMatch.count > index, pegsToMatch[index] == pegs[index] {
                results[index] = Match3.exact
                pegsToMatch.remove(at: index)
            }
        }
        
        for index in pegs.indices {
            if results[index] != Match3.exact {
                if let matchIndex = pegsToMatch.firstIndex(of: pegs[index]) {
                    results[index] = Match3.inexact
                    pegsToMatch.remove(at: matchIndex)
                }
            }
        }

        return results
    }
}

MatchMarker3.swift

import SwiftUI

enum Match3 {
    case nomatch
    case exact
    case inexact
}

struct MatchMarkers3: View {
    // var matches: [Match]
    var matches: Array<Match3>
    
    var body: some View {
        HStack {
            VStack {
                matchMarker(peg: 0)
                matchMarker(peg: 1)
            }
            VStack {
                matchMarker(peg: 2)
                matchMarker(peg: 3)
            }
        }
    }
    
    func isNotNomatch(matchItem: Match3)-> Bool {
        return matchItem != Match3.nomatch
    }
    
    func matchMarker(peg: Int) -> some View {
        let exactCount = self.matches.count(where: { matchItem in matchItem == Match3.exact})
        // 传递函数写法:
        // let foundCount = self.matches.count(where: isNotNomatch)
        // foundCount 改为内联写法:
        // let foundCount = self.matches.count(where: {(matchItem: Match) -> Bool in
        //      matchItem != Match.nomatch
        // })
        // 极限简化后写法用 $number 代表函数参数
        // let foundCount = matches.count(where: {$0 != Match.nomatch})
        // 终极简化
        let foundCount = matches.count{ $0 != Match3.nomatch }
        
        // 普通模式下 如果 exactCount > peg 则填充为黑色
        //          如果 foundCount > peg 边框为黑色当白钉子
        //          填充为白 边框为白则表示没有钉子
        return Circle().fill(exactCount > peg ? Color.primary : Color.clear)
            .strokeBorder(foundCount > peg ? Color.primary : Color.clear, lineWidth: 2)
            .aspectRatio(1, contentMode: ContentMode.fit)
    }
}

#Preview {
    // exactCount为1 foundCount为3 一个黑钉 两个白钉
    MatchMarkers3(matches: [Match3.exact, Match3.inexact, Match3.nomatch, Match3.inexact])
}

ContentView3.swift

import SwiftUI

struct ContentView3: View {
    @State var game = CodeBreaker(pegChoices: [.green, .red, .blue, .yellow]) // 自动推断类型
    
    var body: some View {
        VStack{
            view(for: game.masterCode)
            
            ScrollView {
                view(for: game.guess)
                ForEach(game.attempts.indices.reversed(), id: \.self) { index in
                    view(for: game.attempts[index])
                }
            }
        }.padding()
    }
    
    var guessButton: some View {
        Button("Guess") {
            withAnimation {
                game.attemptGuess()
            }
        }.font(.system(size: 80))
            .minimumScaleFactor(0.1)
    }
    
    @ViewBuilder
    func view(for code: Code)-> some View {
        HStack{
            ForEach(code.pegs.indices, id: \.self) { index in
                RoundedRectangle(cornerRadius: 10)
                    .overlay {
                        if code.pegs[index] == Code.missing {
                            RoundedRectangle(cornerRadius: 10).strokeBorder(Color.gray)
                        }
                    }
                    .contentShape(Rectangle()) // 为了解决在.clear下触控
                    .aspectRatio(1, contentMode: .fit)
                    .foregroundStyle(code.pegs[index])
                    .onTapGesture {
                        if code.kind == Code.Kind.guess {
                            game.changeGuessPeg(at: index)
                        }
                    }
            }
            
            MatchMarkers3(matches: code.matches).overlay {
                if code.kind == Code.Kind.guess {
                    guessButton
                }
            }
        }
    }
}

#Preview {
    ContentView3()
}