В предыдущих постах — 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.
Но из графика, очевидно, что весовой коэффициента zeroWeightMC
= 16384 и количество экспериментов 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.