Статистика созданных 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.