Статистика созданных ChatGPT алгоритмов Expeсtimax и Monte Carlo для игры 2048

В предыдущих постах — iOS приложения игры 2048 в SwiftUI  с ChatGPT 4-o. Часть 1, iOS приложения игры 2048 в SwiftUI  с ChatGPT 4-o. Часть 2. Анимация и UI, iOS приложения игры 2048 в SwiftUI  с ChatGPT 4-o. Часть 3. ИИ, — я рассказала о том, как ChatGPT помог создать эффективные ИИ алгоритмы Expectimax  и Monte Carlo для игры 2048. Это стохастические алгоритмы, то есть их результаты — максимальное значение value плитки maxTile и счет score — случайные величины. Хотелось бы иметь экспериментальное распределение этих случайных величин в виде гистограмм для того, чтобы выбрать их оптимальные параметры.

Приложение Game2048ChatGPT было расширено c целью сохранения результатов многократных запусков алгоритмов Expectimax  и Monte Carlo  в базе данных (БД) SwiftData для последующего статистического анализа. При написании кода максимально использовался ИИ ChatGPT, который иногда, ломая все стереотипы программирования, предлагает очень оригинальные решения, и именно это помогло получить такой лаконичный и читабельный код для нашей статистической задачи. Этот код находится на GitHub.

Я не буду утомлять вас протоколом взаимодействия с ChatGPT, a сразу приведу результаты статистических исследований, которые оптимальным образом помогли настроить параметры ИИ алгоритмов  Expectimax  и Monte Carlo.

Вот распределение максимального значения  плитки maxTile и счета score для алгоритма Expectimax для различных весовых коэффициентов zeroWeight, который участвует в функции эвристической оценке evaluate() игровой доски. Оценка  evaluate() существенно влияет на выбор следующего хода в игре 2048.

На первом графике мы видим количество игр 2048 с ИИ Expectimax с определенными максимальными значениями плиток: 256, 512, 1024. 2048, 4096, 8092 для различных значений параметра весового коэффициента zeroWeight: 5.7,  8.7, 11.7 и 13.7.

На втором графике мы видим средний счет score в играх 2048 с ИИ Expectimax с определенными максимальными значениями плиток: 512, 1024. 2048, 4096, 8092 для различных значений параметра весового коэффициента zeroWeight: 5.7,  8.7, 11.7 и 13.7.

Надо сказать, что средние значения счета ( average of score) в зависимости от  весового коэффициента zeroWeight не сильно различаются, так что второй график фактически не дает никакой информации о предпочтении определенного значения этого коэффициента. 

Но из первого графика, очевидно, что весовой коэффициента zeroWeight =  11.7 дает значительно лучший результат. Мы практически всегда получаем плитку с максимальным значением 4096, a в редких случаях и 8092, не говоря уже о том, что в наших руках практически всегда ПОБЕДА — счет 2048.

Мы можем более подробно изучить статистические эксперименты, просматривая игры 2048 в списке.

Для того, чтобы иметь возможность проводить статистические исследования алгоритмов Expectimax и Monte Carlo, результаты игр 2048 записывались в базу данных SwiftData, а затем выводились с помощью Charts в виде гистограммы распределения.

У этих алгоритмов есть параметры, которые управляют результатами игры  2048: максимальным значением плитки maxTile и счетом score.

У алгоритма Expectimax это:

  • глубина depth просмотра ходов вперед, но её не удается менять в каких-то диапазонах, максимально возможное значение depth = 5, его и будем придерживаться,
  • весовой коэффициент zeroWeight, который  умножается на число плиток с нулевым значением value, и таким образом участвует в функции эвристической оценке evaluate() игровой доски, которая существенно влияет на выбор следующего хода в игре 2048:
// MARK: - evaluateBoard
    private func evaluateBoard (_ board: [[Tile]]) -> Double {
        let grid = board.map {$0.map{$0.value}}
        let emptyCells = board.flatMap { $0 }.filter { $0.value == 0 }.count
            
        let smoothWeight: Double = 0.1
            let monoWeight: Double = 1.0
        let emptyWeight: Double = Constants.zerosWeight // 5.7 8.7 11.7 13.7
            let maxWeight: Double = 1.0
            let maxTileCornerWeight = 1.0
            
            return monoWeight *  monotonicity(grid)
         //        + smoothWeight * smoothness(grid)
                 + emptyWeight * Double(emptyCells)
                 + maxWeight * Double(grid.flatMap { $0 }.max() ?? 2)
             //    + maxTileCornerWeight * maxTileInCorner(board)
                 + snakeHeuristic(grid)
        }

У алгоритма Monte Carlo это:

  • весовой коэффициент zeroWeightMC, который  умножается на число плиток с нулевым значением value, и таким образом участвует наряду со счетом score в эвристической оценке игровой доски, которая существенно влияет на выбор следующего хода в игре 2048,
  • число число экспериментов simulations,
  •  глубина depth просмотра ходов вперед при каждой симуляции.

Вот наша простейшая модель SwiftData данных для хранения результатов работы ИИ алгоритмв Expectimax:

import Foundation
import SwiftData

// MARK: - @Model TreeSearch
@Model final class TreeSearch: Codable {
    var algorithm: String
    var time: Date = Date.now
    var fourEstimate: Bool = false
    var zeroWeight: Double = 11.7
    var zerosBeginning: Int = 4
    var maxTile: Int = 2
    var score: Int = 0
    var moves: Int = 0
    
    init(algorithm: String, time: Date, fourEstimate: Bool, zeroWeight: Double, zerosBeginning: Int, maxTile: Int, score: Int, moves: Int) {
        self.algorithm = algorithm
        self.time = time
        self.fourEstimate = fourEstimate
        self.zeroWeight = zeroWeight
        self.zerosBeginning = zerosBeginning
        self.maxTile = maxTile
        self.score = score
        self.moves = moves
    }
    
    enum CodingKeys: String, CodingKey {
            case algorithm
            case time
            case fourEstimate
            case zeroWeight
            case zerosBeginning
            case maxTile
            case score
            case moves
        }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.algorithm = try container.decode(String.self, forKey: .algorithm)
        self.time = try container.decode(Date.self, forKey: .time)
        self.fourEstimate = 
                    try container.decode(Bool.self, forKey: .fourEstimate)
        self.zeroWeight = 
                    try container.decode(Double.self, forKey: .zeroWeight)
        self.zerosBeginning = 
                    try container.decode(Int.self, forKey: .zerosBeginning)
        self.maxTile = try container.decode(Int.self, forKey: .maxTile)
        self.score = try container.decode(Int.self, forKey: .score)
        self.moves = try container.decode(Int.self, forKey: .moves)
    }
    
    func encode(to encoder: Encoder) throws {
      // TODO: Handle encoding if you need to here
        var container = encoder.container(keyedBy: CodingKeys.self)
       
        try container.encode(algorithm, forKey: .algorithm)
        try container.encode(time, forKey: .time)
        try container.encode(fourEstimate, forKey: .fourEstimate)
        try container.encode(zeroWeight, forKey: .zeroWeight)
        try container.encode(zerosBeginning, forKey: .zerosBeginning)
        try container.encode(maxTile, forKey: .maxTile)
        try container.encode(score, forKey: .score)
        try container.encode(moves, forKey: .moves)
    }
}

А вот простейшая модель SwiftData данных для хранения результатов работы ИИ алгоритма Monte Carlo:

import Foundation
import SwiftData

// MARK: - @Model MonteCarloNew

@Model final class MonterCarloNew: Codable {

    var algorithm: String = "Monte Carlo"
    var time: Date = Date.now
    var numberSimulations: Int = 100
    var deep: Int = 10
    var limitZeros: Int = 4
    var maxTile: Int = 2
    var score: Int = 0
    var moves: Int = 0
    var zerosWeightMC : Int = 16384
    
    init(algorithm: String,time: Date, numberExperiments: Int, deep: Int, limitZeros: Int, maxTile: Int, score: Int, moves: Int, zerosWeightMC : Int) {
        self.algorithm = algorithm
        self.time = time
        self.numberSimulations = numberExperiments
        self.deep = deep
        self.limitZeros = limitZeros
        self.maxTile = maxTile
        self.score = score
        self.moves = moves
        self.zerosWeightMC = zerosWeightMC
    }
    
    enum CodingKeys: String, CodingKey {
            case algorithm
            case time
            case numberExperiments
            case deep
            case limitZeros
            case maxTile
            case score
            case moves
            case zerosWeightMC
        }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.algorithm = try container.decode(String.self, forKey: .algorithm)
        self.time = try container.decode(Date.self, forKey: .time)
        self.numberSimulations = 
                    try container.decode(Int.self, forKey: .numberExperiments)
        self.deep = try container.decode(Int.self, forKey: .deep)
        self.limitZeros = try container.decode(Int.self, forKey: .limitZeros)
        self.maxTile = try container.decode(Int.self, forKey: .maxTile)
        self.score = try container.decode(Int.self, forKey: .score)
        self.moves = try container.decode(Int.self, forKey: .moves)
        self.zerosWeightMC = 
                     try container.decode(Int.self, forKey: .zerosWeightMC)
    }
    
    func encode(to encoder: Encoder) throws {
      // TODO: Handle encoding if you need to here
        var container = encoder.container(keyedBy: CodingKeys.self)
       
        try container.encode(algorithm, forKey: .algorithm)
        try container.encode(time, forKey: .time)
        try container.encode(numberSimulations, forKey: .numberExperiments)
        try container.encode(deep, forKey: .deep)
        try container.encode(limitZeros, forKey: .limitZeros)
        try container.encode(maxTile, forKey: .maxTile)
        try container.encode(score, forKey: .score)
        try container.encode(moves, forKey: .moves)
        try container.encode(zerosWeightMC, forKey: .zerosWeightMC)
    }
}

Мы сделали SwiftData модели Codable, чтобы иметь возможность записывать содержимое БД SwiftData в файл и считывать его из файла. 

После того, как игра закончилась, мы записываем результат в БД.

  struct GameView: View {
    @State private var viewModel = GameViewModel()
    let tileSize: CGFloat = 80
    let padding: CGFloat = 8
    
    @State var isAIPlaying = false
    @State var selectedAlgorithm = Algorithm.Expectimax
    @State var timer = Timer.publish(every: 0.45, on: .main, in: .common).autoconnect()
    var body: some View {
        VStack { . . .}
        .onReceive(timer){ value in
          if isAIPlaying {
              if !viewModel.isGameOver {
                  if selectedAlgorithm == Algorithm.MonteCarloAsync {
                      viewModel.monteCarloAsyncAIMove()
                  } else if selectedAlgorithm == Algorithm.Expectimax1 {
                      viewModel.expectimaxAsyncAIMove()
                  } else {
                      viewModel.executeAIMove()
                  }
              } else {
                  isAIPlaying  = false
                  writeDB()               
              }
          }
        }
       // another modifiers  
     }    

private func writeDB(){
        //--------
        let grid = viewModel.tiles.map {$0.map{$0.value}}
        let maxTile = grid.flatMap { $0 }.max() ?? 2
        
        if selectedAlgorithm == Algorithm.MonteCarloAsync ||  
           selectedAlgorithm == Algorithm.MonteCarlo {
                сontext.insert(
                    MonterCarloNew(algorithm:selectedAlgorithm.rawValue,
                              time: Date.now,
                              numberExperiments: Constants.numberSimilations, 
                              deep: Constants.deep, 
                              limitZeros: Constants.limitZeros, 
                              maxTile:  maxTile, 
                              score: viewModel.score, 
                              moves: 0,
                              zerosWeightMC: Constants.zerosWeightMC))
            сontext.saveContext()
        } else {
            сontext.insert(
                     TreeSearch(algorithm: selectedAlgorithm.rawValue, 
                                time: Date.now, fourEstimate: true, 
                                zeroWeight: Constants.zerosWeight, 
                                zerosBeginning : Constants.zerosBeginning ,
                                maxTile:  maxTile, 
                                score: viewModel.score, 
                                moves: 0))
            сontext.saveContext()
        }
    //---------
    }

Используя данные, записанные в БД  SwiftData, создаем  ChartTreeView для статистической интерпретации результатов работы ИИ алгоритма Expectimax в виде гистограмм:

import SwiftUI
import Charts
import SwiftData

//-------------------------
struct MaxTileFrequencyExp: Identifiable {
    let maxTile: Int
    let zeroWeight: Double
    let count: Int
    var id: Int { maxTile.hashValue ^ zeroWeight.hashValue }
    var avrScore: Int = 0
    var animate: Bool = false
}

struct MaxTileZeroWeigtKey: Hashable {
    let maxTile: Int
    let zeroWeight: Double
}
//---------------------------
struct ChartTreeView: View {
    @Query(sort: \TreeSearch.time, order: .forward)  
                                          var expectimaxs: [TreeSearch]
    @State private var algorithm: String = "All"
    //---- animate ----
    @State var sampleAnalitics: [String: [MaxTileFrequencyExp]]  = [:]
    //---------------   
    var filteredExpectimaxs: [TreeSearch] {
        if algorithm == "All" { return expectimaxs }
        return expectimaxs.compactMap { item in
            return item.algorithm == algorithm ? item : nil
        }
    }
    
    var maxTileFrequencyByZeroWeight: [MaxTileFrequencyExp] {
        let groupedData = Dictionary(
            grouping: filteredExpectimaxs,
            by: { MaxTileZeroWeigtKey(maxTile: $0.maxTile, 
                                      zeroWeight: $0.zeroWeight) }
        )
        
        return groupedData.map { key, results in
            MaxTileFrequencyExp(
                maxTile: key.maxTile,
                zeroWeight: key.zeroWeight,
                count: results.count,
                avrScore: results.map{$0.score}.average() / 1000
            )
        }
        .sorted { $0.maxTile < $1.maxTile }
    }
 
    var groupedData: [String: [MaxTileFrequencyExp]] {
        Dictionary(grouping: maxTileFrequencyByZeroWeight, 
                         by: { String($0.zeroWeight)})
    }

    var marks:  [String] {
      Array(Set(maxTileFrequencyByZeroWeight.map { $0.maxTile }))
                         .map {String($0)}.sorted { Int($0)! < Int($1)! }
    }

    var body: some View {
         VStack {
           HStack {
             Text ("Algorithm:")
              Picker( selection: $algorithm, label: Text("")) {
                ForEach(["All","Expectimax", "ExpectiMAsync"], id: \.self) {
                 algorithm in
                            Text("\(algorithm)").tag("\(algorithm)")
                        }
                    }
                    .pickerStyle(.segmented)
                    .padding(.leading, 10)
                }
                .padding(.bottom)
             //--------------------------------------------------------------
              Section(header: Text("Number of MaxTile").font(.subheadline)) {
                    AnimatedChart
              }
              Section(header: Text("Average of score").font(.subheadline)) {
                    AnimatedChartAverage
              }
            //----------------
        }// VStack
            .onChange(of: algorithm) { oldValue, newValue in
                sampleAnalitics =  groupedData
                // Re - Animating View
                animateGraph(fromChange: true)
            }
            .padding()
    } // body
    
    private var AnimatedChart: some View {
       let max = sampleAnalitics.values.flatMap{$0}.map{$0.count}.max () ?? 0
       return Chart {
 ForEach( sampleAnalitics.keys.sorted {Double($0)! < Double($1)!}, id:\.self) 
        { element in
                ForEach(sampleAnalitics[element]!, id: \.maxTile) { stat in
                    BarMark(
                        x: .value("MaxTile", String(stat.maxTile)),
                        y: .value("Count",  stat.animate ? stat.count : 0)
                    )
                    .annotation (position: .top) {
                        Text(String(stat.count))
                            .foregroundColor(.black)
                            .font(.footnote)
                    }
                }
                .foregroundStyle(by: .value("Simulations", element))
                .position(by: .value("Simulations", element))
            }
        }
        // MARK: Customizing Y-Axis Length
        .chartYScale(domain: 0...(max ) )
        // MARK: Customizing X-Axis Length
        .chartXScale(domain: marks)
        .aspectRatio(1, contentMode: .fit)
        .padding()
        //----- animate -------
        .onAppear{
            animateGraph()
         } // onAppear
    }
    
  private var AnimatedChartAverage: some View {
    let max = sampleAnalitics.values.flatMap{$0}.map{$0.avrScore}.max () ?? 0
         return Chart {
ForEach( sampleAnalitics.keys.sorted {Double($0)! < Double($1)!} , id:\.self) 
        { element in
                 ForEach(sampleAnalitics[element]!, id: \.maxTile) { stat in
                     BarMark(
                       x: .value("MaxTile", String(stat.maxTile)),
                       y: .value("Count",  stat.animate ? stat.avrScore : 0)
                     )
                     .annotation (position: .top) {
                         Text(String(stat.avrScore))
                             .foregroundColor(.black)
                             .font(.footnote)
                     }
                 }
                 .foregroundStyle(by: .value("Simulations", element))
                 .position(by: .value("Simulations", element))
             }
         }
         // MARK: Customizing Y-Axis Length
         .chartYScale(domain: 0...(max ) )
         // MARK: Customizing X-Axis Length
         .chartXScale(domain: marks)
         .aspectRatio(1, contentMode: .fit)
         .padding()
    }
    //------- animate ----
    func animateGraph(fromChange: Bool = false) {
        sampleAnalitics = groupedData
        
        for  key in groupedData.keys.sorted(by: {Double($0)! < Double($1)!})  {
            if let bars =  groupedData [key] {
            for (index, _) in bars.enumerated() {
                
                // For Some Reason Delay is Not Working
                // Using DispatchQueue Delay
                DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * (fromChange ? 0.03 : 0.05)) {
                    withAnimation( fromChange ? .easeIn(duration: 0.8) : 
                    .interactiveSpring(response:0.8, 
                                       dampingFraction: 0.8, 
                                       blendDuration: 0.8))  {
                        sampleAnalitics[key]! [index].animate = true  
                    } // with
                } // DispatchQueue
            } // for
        } // if
      } // for
    }// func
    //---------------------
}

Пишем код для TreeSearchView, который отображает статистические результаты работы ИИ алгоритма Expectimax в виде списка экспериментов:

import SwiftUI
import SwiftData

struct TreeSearchView: View {
    @Environment(\.modelContext) private var context
    @Query(sort: \TreeSearch.time, order: .forward)  var items: [TreeSearch]
    var body: some View {
      NavigationStack {
        List {
          ForEach(groupByZeroWeight(items), id: \.0) { montes in
            Section(header: Text("\(String(montes.0))  \(montes.1.count)")) {
                 ForEach(montes.1) { item in
                     let s = String(format: "%2.2f",item.zeroWeight)
                     HStack{
                       Text( "**\(item.maxTile)**").foregroundColor(.purple)
                       Text(item.time.formatted(date: .numeric, 
                                                time: .shortened))
                       Text("**\(item.score)**").foregroundColor(.blue)
                       Text("**\(s)**").foregroundColor(.red)
                                   }
                               }
                           }
                       }
                   }
            .listStyle(.plain)
            .navigationTitle("Expectimax  (\(items.count))")
            .toolbar{
                ToolbarItem(placement: .topBarTrailing) { write}
                ToolbarItem(placement: .topBarLeading) {read}
            }
        } .task {
            if items.count == 0 {
                await asyncLoad()  // background actor
            }
        }
    }
    
    private func asyncLoad () async {  // actor
        let actor = LoadModelActor(modelContainer: context.container)
       // await actor.monteCarlosAsync (FilesJSON.monteCarlosFile)
        await actor.treeSearchAsync (FilesJSON.treeSearchFile)
    }
    
    func groupByZeroWeight(_ items: [TreeSearch]) -> [(Double, [TreeSearch])] {
        let grouped = Dictionary(grouping: items, by: { $0.zeroWeight })
            return grouped.sorted(by: { $0.key < $1.key })
        }
    
    var write: some View {
        Button("Write") {
            Task  {
                writeJSON ()
            }
        }
    }
    var read: some View {
        Button("Read") {
            Task  {
                readJSON ()
            }
        }
    }
    
    private func writeJSON () { . . .}
    
    private func readJSON () {. . .}

И получаем список экспериментов для ИИ алгоритма Expectimax с разными значениями :

Аналогичную гистограмму получаем для метода Monte Carlo, но здесь уже больше параметров настройки. Основными считаем количество экспериментов simulations и весовой коэффициент zeroWeightMC, который  умножается на число плиток с нулевым значением value.И гистограмма будет зависеть от этих параметров:

На графике мы видим количество игр 2048 с ИИ Monte Carlo с определенными максимальными значениями плиток: 512, 1024. 2048, 4096, 8092 для различных значений параметра весового коэффициента zeroWeightMC: 4096 и 16384 и количества экспериментов simulations: 150, 180 и 200.

Но из графика, очевидно, что весовой коэффициента zeroWeightMC16384 и количество экспериментов simulations = 180 дает значительно лучший результат: 46 победных игр (MaxTile >= 2048) из 50 против 42 победы для simulations = 150 и 41 победа для simulations = 200. Мы практически всегда (с большой вероятностью) получаем плитку с максимальным значением 2048, и достаточно часто плитку с максимальным значением 4096.

Используя данные, записанные в БД, создаем  View для статистической интерпретации результатов работы алгоритма Monte Carlo в виде гистограмм:

import SwiftUI
import Charts
import SwiftData

//-------------------------
struct MaxTileFrequency: Identifiable {
    let maxTile: Int
    let simulations: Int
    let count: Int
    var id: Int { maxTile.hashValue ^ simulations.hashValue }
    var animate: Bool = false
}

struct MaxTileSimulationKey: Hashable {
    let maxTile: Int
    let simulations: Int
}
//----------------------
struct ChartViewNew: View {
    @Query(sort: \MonterCarloNew.time, order: .forward)  var monteCarlos: [MonterCarloNew]
   
    @State private var zerosWeightMC: String = "All"
    //---- animate ----
    @State var sampleAnalitics: [Int: [MaxTileFrequency]]  = [:]
    //----------------
    
    var filteredMonteCarlos: [MonterCarloNew] {
        if zerosWeightMC == "All" { return monteCarlos }
            return monteCarlos.compactMap { item in
                return item.zerosWeightMC == Int(zerosWeightMC) ? item : nil
            }
        }
   
    var maxTileFrequencyBySimulations: [MaxTileFrequency] {
        let groupedData = Dictionary(
            grouping: filteredMonteCarlos,
            by: { MaxTileSimulationKey(maxTile: $0.maxTile, 
                                       simulations: $0.numberSimulations ) }
        )
        
        return groupedData.map { key, results in
            MaxTileFrequency(
                maxTile: key.maxTile,
                simulations: key.simulations,
                count: results.count
            )
        }
        .sorted { $0.maxTile < $1.maxTile }
    }
    
    var groupedData: [Int: [MaxTileFrequency]] {
        Dictionary(grouping: maxTileFrequencyBySimulations, 
                   by: { $0.simulations})
    }
   
    var marks:  [String] {
        Array(Set(maxTileFrequencyBySimulations.map { $0.maxTile })).map {String($0)}.sorted { Int($0)! < Int($1)! }
    }
    //-----
    
    var body: some View {
        VStack {
          VStack {
            Text ("Zeros Weight:")
            Picker( selection: $zerosWeightMC, label: Text("")) {
              ForEach(["All", "1024", "2048", "4096", "8192", "16384"],
                      id: \.self) { weightMC in
                        Text("\(weightMC)").tag("\(weightMC)")
                    }
                }
                .pickerStyle(.segmented)
                .padding()
            }
            AnimatedChart
        } // VStack
        .onChange(of: zerosWeightMC) { oldValue, newValue in
            sampleAnalitics =  groupedData
            // Re - Animating View
            animateGraph(fromChange: true)
        }
    } // body
    
    private var AnimatedChart: some View {
      let max = sampleAnalitics.values.flatMap{$0}.map{$0.count}.max () ?? 0
       return Chart {
          //  ForEach( groupedData.keys.sorted() , id:\.self) { simulation in
          ForEach( sampleAnalitics.keys.sorted() , id:\.self) { simulation in
                  
            //   ForEach(groupedData[simulation]!, id: \.maxTile) { item in
              ForEach(sampleAnalitics[simulation]!, id: \.maxTile) { item in
                  
                    BarMark(
                        x: .value("Max Tile", String(item.maxTile)),
                        y: .value("Count", item.animate ? item.count : 0)
                    )
                    .annotation (position: .top) {
                        Text(String(item.count))
                            .foregroundColor(.black)
                            .font(.footnote)
              }
          }
          .foregroundStyle(by:.value("Simulations", String(simulation)))
          .position(by: .value("Simulations", String(simulation)))    
        }
    }
        // MARK: Customizing Y-Axis Length
        .chartYScale(domain: 0...(max ) )
        // MARK: Customizing X-Axis Length
        .chartXScale(domain: marks)
        .chartLegend(.visible)
        .aspectRatio(1, contentMode: .fit)
        .padding()
       .onAppear{
           animateGraph()
        } // onAppear
    } // some View
    
    //------- animate ----
    func animateGraph(fromChange: Bool = false) {
        sampleAnalitics = groupedData
        
        for  key in groupedData.keys.sorted() {
            if let bars =  groupedData [key] {
            for (index, _) in bars.enumerated() {
                
                // For Some Reason Delay is Not Working
                // Using DispatchQueue Delay
                DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * (fromChange ? 0.03 : 0.05)) {
                    withAnimation( fromChange ? .easeIn(duration: 0.8) : .interactiveSpring(response:0.8, dampingFraction: 0.8,blendDuration: 0.8)){
                        sampleAnalitics[key]! [index].animate = true 
                    } // with
                } // DispatchQueue
            } // for
        } // if
      } // for
    }// func
    //---------------------
} // View

Пишем код для MonteCarloView, который отображает статистические результаты работы алгоритма Monte Carlo в виде списка:

import SwiftUI
import SwiftData

struct MonteCarloView: View {
    @Query(sort: \MonterCarloNew.time, order: .forward)  
                                                 var items: [MonterCarloNew]
    @Environment(\.modelContext) private var context
    @State private var zerosWeightMC: String = "All"
    
    var filteredItems: [MonterCarloNew] {
        if zerosWeightMC == "All" { return  items }
            return  items.compactMap { item in
                return item.zerosWeightMC == Int(zerosWeightMC) ? item : nil
            }
        }
    
    var body: some View {
        NavigationStack {
          VStack {
            Text ("Zeros Weight:")
            Picker("", selection: $zerosWeightMC) {
              ForEach(["All","1024", "2048", "4096", "8192","16384"], 
                                                 id: \.self) { weightMC in
                        Text("\(weightMC)").tag("\(weightMC)")
                    }
                }
                .pickerStyle(.segmented)
                .padding()
            }
        List {        // zerosWeightMC
         ForEach(groupByNExperiments(filteredItems), id: \.0) { montes in
            Section(header: Text("simulations = \(montes.0)  
                                   ZW = \(( montes.1.first?.zerosWeightMC)!) 
                                               \(montes.1.count)")) {
              ForEach(montes.1) { item in
               HStack{
                 Text( "**\(item.maxTile)**").foregroundColor(.purple)
                 Text(item.time.formatted(date: .numeric, time: .shortened))
                 Text("**\(item.score)**").foregroundColor(.blue)
                 Text("**\(item.zerosWeightMC)**").foregroundColor(.red)
               }
              }
             }
           }
         }
          .listStyle(.plain)
          .navigationTitle("Monte Carlo (\(items.count))")
          .toolbar{
                ToolbarItem(placement: .topBarTrailing) { write}
                ToolbarItem(placement: .topBarLeading) {read}
            } // toolbar
        } //  Navigation
        .task {
            if items.count == 0 {
                await asyncLoad()  // background actor
            }
        } // task
    } // body
    
    private func asyncLoad () async {  // actor
        let actor = LoadModelActor(modelContainer: context.container)
        await actor.monteCarlosAsync (FilesJSON.monteCarlosFile)
      //  await actor.treeSearchAsync (FilesJSON.treeSearchFile)
    }
    
    func groupByNExperiments(_ items: [MonterCarloNew]) -> 
                                               [(Int, [MonterCarloNew])] {
        let grouped = Dictionary(grouping: items, 
                                 by: { $0.numberSimulations})
            return grouped.sorted(by: { $0.key < $1.key })
        }
    
    var write: some View {
        Button("Write") {
            Task  {
                writeJSON ()
            }
        }
    }
    var read: some View {
        Button("Read") {
            Task  {
                readJSON ()
            }
        }
    }
    
    private func writeJSON () {. . . }
    private func readJSON () {. . . }
}

И получаем список экспериментов для ИИ алгоритма Monte Carlo с разными значениями параметров:

Заключение.

Приложение Game2048ChatGPT для статистического анализа ИИ алгоритмов Expectimax  и Monte Carlo  позволило подобрать оптимальные параметры:

  • для Monte Carlo число экспериментов simulations = 180, весовой коэффициент zeroWeightMC = 16 384,
  • для Expectimax весовой коэффициент zeroWeight = 11.7

Код находится на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *