gotchi-battler-game-logic 2.0.7 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +4 -4
- package/Dockerfile +9 -9
- package/README.md +49 -49
- package/cloudbuild.yaml +27 -27
- package/constants/tournamentManagerAbi.json +208 -208
- package/game-logic/index.js +6 -6
- package/game-logic/v1.4/constants.js +114 -114
- package/game-logic/v1.4/index.js +1366 -1366
- package/game-logic/v1.6/constants.js +123 -123
- package/game-logic/v1.6/index.js +1406 -1406
- package/game-logic/v1.7/constants.js +142 -140
- package/game-logic/v1.7/helpers.js +595 -593
- package/game-logic/v1.7/index.js +802 -795
- package/index.js +12 -12
- package/package.json +26 -26
- package/schemas/team.json +349 -343
- package/scripts/balancing/createCSV.js +126 -126
- package/scripts/balancing/fixTrainingGotchis.js +155 -259
- package/scripts/balancing/processSims.js +229 -229
- package/scripts/balancing/sims.js +278 -278
- package/scripts/balancing/v1.7/class_combos.js +43 -43
- package/scripts/balancing/v1.7/setTeamPositions.js +105 -105
- package/scripts/balancing/v1.7/training_gotchis.json +20161 -20161
- package/scripts/balancing/v1.7/trait_combos.json +9 -9
- package/scripts/balancing/v1.7.1/class_combos.js +43 -43
- package/scripts/balancing/v1.7.1/setTeamPositions.js +122 -122
- package/scripts/balancing/v1.7.1/training_gotchis.json +22401 -22401
- package/scripts/balancing/v1.7.1/trait_combos.json +9 -9
- package/scripts/balancing/v1.7.2/class_combos.js +44 -0
- package/scripts/balancing/v1.7.2/setTeamPositions.js +122 -0
- package/scripts/balancing/v1.7.2/training_gotchis.json +22402 -0
- package/scripts/balancing/v1.7.2/trait_combos.json +10 -0
- package/scripts/data/team1.json +213 -213
- package/scripts/data/team2.json +200 -200
- package/scripts/data/tournaments.json +66 -66
- package/scripts/{runBattle.js → runLocalBattle.js} +18 -18
- package/scripts/runRealBattle.js +52 -0
- package/scripts/simRealBattle.js +121 -0
- package/scripts/validateBattle.js +74 -70
- package/scripts/validateTournament.js +101 -101
- package/utils/contracts.js +12 -12
- package/utils/errors.js +29 -29
- package/utils/mapGotchi.js +119 -0
- package/utils/transforms.js +89 -88
- package/utils/validations.js +39 -39
- package/debug.log +0 -2
- package/game-logic/v1.6/debug.log +0 -1
- package/game-logic/v1.7/debug.log +0 -3
- package/scripts/data/debug.log +0 -2
- package/scripts/debug.log +0 -1
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
require('dotenv').config()
|
|
2
|
-
const fs = require('fs')
|
|
3
|
-
const path = require('path')
|
|
4
|
-
const { Storage } = require('@google-cloud/storage')
|
|
5
|
-
const storage = new Storage()
|
|
6
|
-
|
|
7
|
-
if (!process.env.CLOUD_RUN_JOB) {
|
|
8
|
-
// Use key file locally
|
|
9
|
-
process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join(__dirname, '../../keyfile.json')
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const downloadAndCombineResults = async (executionId, numOfTasks) => {
|
|
13
|
-
const results = []
|
|
14
|
-
|
|
15
|
-
// Download all the files in batches of 100
|
|
16
|
-
for (let i = 0; i < numOfTasks; i += 100) {
|
|
17
|
-
const batch = []
|
|
18
|
-
|
|
19
|
-
for (let j = 0; j < 100; j++) {
|
|
20
|
-
if (i + j < numOfTasks) {
|
|
21
|
-
batch.push(storage.bucket(process.env.SIMS_BUCKET).file(`${executionId}_${i + j}.json`).download())
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const batchResults = await Promise.all(batch)
|
|
26
|
-
results.push(...batchResults)
|
|
27
|
-
|
|
28
|
-
console.log(`Downloaded ${i + batchResults.length} files`)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const combinedResults = []
|
|
32
|
-
|
|
33
|
-
for (const result of results) {
|
|
34
|
-
combinedResults.push(JSON.parse(result.toString()))
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return combinedResults
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const classes = ['Ninja','Enlightened','Cleaver','Tank','Cursed','Healer', 'Mage', 'Troll']
|
|
41
|
-
|
|
42
|
-
const calculateKpis = (data) => {
|
|
43
|
-
// Extract wins values
|
|
44
|
-
const wins = data.map(item => item.wins);
|
|
45
|
-
|
|
46
|
-
// Calculate the mean of wins
|
|
47
|
-
const winsMean = wins.reduce((sum, value) => sum + value, 0) / wins.length;
|
|
48
|
-
|
|
49
|
-
// Calculate the wins per slot
|
|
50
|
-
const winsPerSlot = winsMean / 5
|
|
51
|
-
|
|
52
|
-
// Calculate the standard deviation of wins
|
|
53
|
-
const winsVariance = wins.reduce((sum, value) => sum + Math.pow(value - winsMean, 2), 0) / (wins.length - 1);
|
|
54
|
-
const winsStdev = Math.sqrt(winsVariance)
|
|
55
|
-
|
|
56
|
-
// Calculate Interquartile Range (IQR)
|
|
57
|
-
wins.sort((a, b) => a - b)
|
|
58
|
-
const q1 = wins[Math.floor(wins.length / 4)]
|
|
59
|
-
const q3 = wins[Math.floor(wins.length * 3 / 4)]
|
|
60
|
-
const iqr = q3 - q1
|
|
61
|
-
|
|
62
|
-
// Analyse top and bottom quartiles
|
|
63
|
-
const topQuartile = data.filter(item => item.wins >= q3)
|
|
64
|
-
const bottomQuartile = data.filter(item => item.wins <= q1)
|
|
65
|
-
const topQuartileWins = topQuartile.map(item => item.wins)
|
|
66
|
-
const bottomQuartileWins = bottomQuartile.map(item => item.wins)
|
|
67
|
-
const topQuartileWinsMean = topQuartileWins.reduce((sum, value) => sum + value, 0) / topQuartileWins.length
|
|
68
|
-
const bottomQuartileWinsMean = bottomQuartileWins.reduce((sum, value) => sum + value, 0) / bottomQuartileWins.length
|
|
69
|
-
|
|
70
|
-
// Non leader global KPIs
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const results = {
|
|
74
|
-
winsMean: winsMean.toFixed(2),
|
|
75
|
-
winsPerSlot: winsPerSlot.toFixed(2),
|
|
76
|
-
winsStdev: winsStdev.toFixed(2),
|
|
77
|
-
iqr: iqr.toFixed(2),
|
|
78
|
-
topQuartileWinsMean: topQuartileWinsMean.toFixed(2),
|
|
79
|
-
bottomQuartileWinsMean: bottomQuartileWinsMean.toFixed(2),
|
|
80
|
-
classes: {}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
classes.forEach((className, i) => {
|
|
85
|
-
/**
|
|
86
|
-
* Get leader KPIs
|
|
87
|
-
* Leaders are in slot 1
|
|
88
|
-
*/
|
|
89
|
-
|
|
90
|
-
// Names are in format "G|avg|1_B"
|
|
91
|
-
// G is power level, avg is trait combo, 1 is class number, B is formation position
|
|
92
|
-
const leaderWins = data.filter(item => item.slot1.includes(`${i + 1}`)).map(item => item.wins)
|
|
93
|
-
|
|
94
|
-
const leaderWinsMean = leaderWins.reduce((sum, value) => sum + value, 0) / leaderWins.length;
|
|
95
|
-
const leaderWinsVariance = leaderWins.reduce((sum, value) => sum + Math.pow(value - leaderWinsMean, 2), 0) / (leaderWins.length - 1);
|
|
96
|
-
const leaderWinsStdev = Math.sqrt(leaderWinsVariance)
|
|
97
|
-
|
|
98
|
-
// Calculate Interquartile Range (IQR)
|
|
99
|
-
leaderWins.sort((a, b) => a - b)
|
|
100
|
-
const leaderQ1 = leaderWins[Math.floor(leaderWins.length / 4)]
|
|
101
|
-
const leaderQ3 = leaderWins[Math.floor(leaderWins.length * 3 / 4)]
|
|
102
|
-
const leaderIqr = leaderQ3 - leaderQ1
|
|
103
|
-
|
|
104
|
-
// Analyse top and bottom quartiles
|
|
105
|
-
const leaderTopQuartile = data.filter(item => item.slot1.includes(`${i + 1}`) && item.wins >= leaderQ3)
|
|
106
|
-
const leaderBottomQuartile = data.filter(item => item.slot1.includes(`${i + 1}`) && item.wins <= leaderQ1)
|
|
107
|
-
const leaderTopQuartileWins = leaderTopQuartile.map(item => item.wins)
|
|
108
|
-
const leaderBottomQuartileWins = leaderBottomQuartile.map(item => item.wins)
|
|
109
|
-
const leaderTopQuartileWinsMean = leaderTopQuartileWins.reduce((sum, value) => sum + value, 0) / leaderTopQuartileWins.length
|
|
110
|
-
const leaderBottomQuartileWinsMean = leaderBottomQuartileWins.reduce((sum, value) => sum + value, 0) / leaderBottomQuartileWins.length
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get non-leader KPIs
|
|
114
|
-
* Non leaders are in slots 2, 3, 4, 5
|
|
115
|
-
*/
|
|
116
|
-
|
|
117
|
-
// Get the teams where the class is in the non-leader slots
|
|
118
|
-
const nonLeaderTeams = data.filter(item => item.slot2.includes(`${i + 1}`) || item.slot3.includes(`${i + 1}`) || item.slot4.includes(`${i + 1}`) || item.slot5.includes(`${i + 1}`))
|
|
119
|
-
|
|
120
|
-
const totalNonLeaderWins = nonLeaderTeams.reduce((sum, item) => {
|
|
121
|
-
const team = [item.slot2, item.slot3, item.slot4, item.slot5]
|
|
122
|
-
const occurrences = team.filter(slot => slot.includes(`${i + 1}`)).length
|
|
123
|
-
return sum + item.wins / 5 * occurrences
|
|
124
|
-
}, 0)
|
|
125
|
-
|
|
126
|
-
// Get total non-leader occurrences
|
|
127
|
-
const totalOccurrences = nonLeaderTeams.reduce((sum, item) => {
|
|
128
|
-
const team = [item.slot2, item.slot3, item.slot4, item.slot5]
|
|
129
|
-
return sum + team.filter(slot => slot.includes(`${i + 1}`)).length
|
|
130
|
-
}, 0)
|
|
131
|
-
|
|
132
|
-
// Calulate win per non-leader occurance
|
|
133
|
-
const winsPerOccurance = totalNonLeaderWins / totalOccurrences
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
results.classes[className] = {
|
|
137
|
-
leader: {
|
|
138
|
-
winsMean: leaderWinsMean.toFixed(2),
|
|
139
|
-
winsMeanVsGlobal: (leaderWinsMean - winsMean).toFixed(2),
|
|
140
|
-
winsMeanVsGlobalPercentage: ((leaderWinsMean - winsMean) / winsMean * 100).toFixed(2),
|
|
141
|
-
winsStdev: leaderWinsStdev.toFixed(2),
|
|
142
|
-
winsStdevVsGlobal: (leaderWinsStdev - winsStdev).toFixed(2),
|
|
143
|
-
winsStdevVsGlobalPercentage: ((leaderWinsStdev - winsStdev) / winsStdev * 100).toFixed(2),
|
|
144
|
-
iqr: leaderIqr.toFixed(2),
|
|
145
|
-
iqrVsGlobal: (leaderIqr - iqr).toFixed(2),
|
|
146
|
-
iqrVsGlobalPercentage: ((leaderIqr - iqr) / iqr * 100).toFixed(2),
|
|
147
|
-
topQuartileWinsMean: leaderTopQuartileWinsMean.toFixed(2),
|
|
148
|
-
topQuartileWinsMeanVsGlobal: (leaderTopQuartileWinsMean - topQuartileWinsMean).toFixed(2),
|
|
149
|
-
topQuartileWinsMeanVsGlobalPercentage: ((leaderTopQuartileWinsMean - topQuartileWinsMean) / topQuartileWinsMean * 100).toFixed(2),
|
|
150
|
-
bottomQuartileWinsMean: leaderBottomQuartileWinsMean.toFixed(2),
|
|
151
|
-
bottomQuartileWinsMeanVsGlobal: (leaderBottomQuartileWinsMean - bottomQuartileWinsMean).toFixed(2),
|
|
152
|
-
bottomQuartileWinsMeanVsGlobalPercentage: ((leaderBottomQuartileWinsMean - bottomQuartileWinsMean) / bottomQuartileWinsMean * 100).toFixed(2)
|
|
153
|
-
},
|
|
154
|
-
nonLeader: {
|
|
155
|
-
winsPerOccurance: winsPerOccurance.toFixed(2),
|
|
156
|
-
winsPerOccuranceVsGlobal: (winsPerOccurance - winsPerSlot).toFixed(2),
|
|
157
|
-
winsPerOccuranceVsGlobalPercentage: ((winsPerOccurance - winsPerSlot) / winsPerSlot * 100).toFixed(2)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
return results
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const main = async (executionId, numOfTasks, gameLogicVersion) => {
|
|
166
|
-
const gameLogicConstants = require(`../../game-logic/${gameLogicVersion}/constants.js`)
|
|
167
|
-
|
|
168
|
-
const data = await downloadAndCombineResults(executionId, numOfTasks)
|
|
169
|
-
|
|
170
|
-
const availablePowerLevels = {
|
|
171
|
-
'G': 'Godlike',
|
|
172
|
-
'M': 'Mythical',
|
|
173
|
-
'L': 'Legendary',
|
|
174
|
-
'R': 'Rare',
|
|
175
|
-
'U': 'Uncommon',
|
|
176
|
-
'C': 'Common'
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Find power levels from data
|
|
180
|
-
// slot1 is in the format "G|avg|1_B" where the first character is the power level
|
|
181
|
-
// Find the unique first characters in slot1
|
|
182
|
-
const powerLevels = Array.from(new Set(data.map(item => item.slot1[0])))
|
|
183
|
-
|
|
184
|
-
// Calculate KPIs for each power level
|
|
185
|
-
const results = {
|
|
186
|
-
metadata: {
|
|
187
|
-
gameLogicVersion: gameLogicVersion,
|
|
188
|
-
gameLogicConstants: gameLogicConstants,
|
|
189
|
-
powerLevels: powerLevels.map(powerLevel => availablePowerLevels[powerLevel])
|
|
190
|
-
},
|
|
191
|
-
results: {}
|
|
192
|
-
}
|
|
193
|
-
powerLevels.forEach((powerLevel) => {
|
|
194
|
-
const powerLevelData = data.filter(item => item.slot1[0] === powerLevel)
|
|
195
|
-
results.results[availablePowerLevels[powerLevel]] = calculateKpis(powerLevelData)
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
if(process.env.CLOUD_RUN_JOB) {
|
|
199
|
-
const bucket = storage.bucket('gotchi-battler-sims-v1-7-
|
|
200
|
-
|
|
201
|
-
// Get number of files in the bucket
|
|
202
|
-
const [files] = await bucket.getFiles()
|
|
203
|
-
const numOfFiles = files.length
|
|
204
|
-
|
|
205
|
-
// Write the results to a file
|
|
206
|
-
await bucket.file(`${numOfFiles + 1}.json`).save(JSON.stringify(results, null, 2))
|
|
207
|
-
} else {
|
|
208
|
-
// Write to /scripts/balancing/output/<executionId>.json
|
|
209
|
-
fs.writeFileSync(path.join(__dirname, '/output/', `${executionId}.json`), JSON.stringify(results, null, 2))
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
module.exports = main
|
|
214
|
-
|
|
215
|
-
// node scripts/balancing/processSims.js avg-sims-99hzc 7920 v1.7
|
|
216
|
-
if (require.main === module) {
|
|
217
|
-
const executionId = process.env.EXECUTION_ID || process.argv[2]
|
|
218
|
-
const numOfTasks = parseInt(process.env.NUM_OF_TASKS) || parseInt(process.argv[3])
|
|
219
|
-
const gameLogicVersion = process.env.GAME_LOGIC_VERSION || process.argv[4]
|
|
220
|
-
|
|
221
|
-
main(executionId, numOfTasks, gameLogicVersion)
|
|
222
|
-
.then(() => {
|
|
223
|
-
console.log('Done')
|
|
224
|
-
process.exit(0)
|
|
225
|
-
})
|
|
226
|
-
.catch((error) => {
|
|
227
|
-
console.error(error)
|
|
228
|
-
process.exit(1)
|
|
229
|
-
})
|
|
1
|
+
require('dotenv').config()
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { Storage } = require('@google-cloud/storage')
|
|
5
|
+
const storage = new Storage()
|
|
6
|
+
|
|
7
|
+
if (!process.env.CLOUD_RUN_JOB) {
|
|
8
|
+
// Use key file locally
|
|
9
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join(__dirname, '../../keyfile.json')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const downloadAndCombineResults = async (executionId, numOfTasks) => {
|
|
13
|
+
const results = []
|
|
14
|
+
|
|
15
|
+
// Download all the files in batches of 100
|
|
16
|
+
for (let i = 0; i < numOfTasks; i += 100) {
|
|
17
|
+
const batch = []
|
|
18
|
+
|
|
19
|
+
for (let j = 0; j < 100; j++) {
|
|
20
|
+
if (i + j < numOfTasks) {
|
|
21
|
+
batch.push(storage.bucket(process.env.SIMS_BUCKET).file(`${executionId}_${i + j}.json`).download())
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const batchResults = await Promise.all(batch)
|
|
26
|
+
results.push(...batchResults)
|
|
27
|
+
|
|
28
|
+
console.log(`Downloaded ${i + batchResults.length} files`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const combinedResults = []
|
|
32
|
+
|
|
33
|
+
for (const result of results) {
|
|
34
|
+
combinedResults.push(JSON.parse(result.toString()))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return combinedResults
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const classes = ['Ninja','Enlightened','Cleaver','Tank','Cursed','Healer', 'Mage', 'Troll']
|
|
41
|
+
|
|
42
|
+
const calculateKpis = (data) => {
|
|
43
|
+
// Extract wins values
|
|
44
|
+
const wins = data.map(item => item.wins);
|
|
45
|
+
|
|
46
|
+
// Calculate the mean of wins
|
|
47
|
+
const winsMean = wins.reduce((sum, value) => sum + value, 0) / wins.length;
|
|
48
|
+
|
|
49
|
+
// Calculate the wins per slot
|
|
50
|
+
const winsPerSlot = winsMean / 5
|
|
51
|
+
|
|
52
|
+
// Calculate the standard deviation of wins
|
|
53
|
+
const winsVariance = wins.reduce((sum, value) => sum + Math.pow(value - winsMean, 2), 0) / (wins.length - 1);
|
|
54
|
+
const winsStdev = Math.sqrt(winsVariance)
|
|
55
|
+
|
|
56
|
+
// Calculate Interquartile Range (IQR)
|
|
57
|
+
wins.sort((a, b) => a - b)
|
|
58
|
+
const q1 = wins[Math.floor(wins.length / 4)]
|
|
59
|
+
const q3 = wins[Math.floor(wins.length * 3 / 4)]
|
|
60
|
+
const iqr = q3 - q1
|
|
61
|
+
|
|
62
|
+
// Analyse top and bottom quartiles
|
|
63
|
+
const topQuartile = data.filter(item => item.wins >= q3)
|
|
64
|
+
const bottomQuartile = data.filter(item => item.wins <= q1)
|
|
65
|
+
const topQuartileWins = topQuartile.map(item => item.wins)
|
|
66
|
+
const bottomQuartileWins = bottomQuartile.map(item => item.wins)
|
|
67
|
+
const topQuartileWinsMean = topQuartileWins.reduce((sum, value) => sum + value, 0) / topQuartileWins.length
|
|
68
|
+
const bottomQuartileWinsMean = bottomQuartileWins.reduce((sum, value) => sum + value, 0) / bottomQuartileWins.length
|
|
69
|
+
|
|
70
|
+
// Non leader global KPIs
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
const results = {
|
|
74
|
+
winsMean: winsMean.toFixed(2),
|
|
75
|
+
winsPerSlot: winsPerSlot.toFixed(2),
|
|
76
|
+
winsStdev: winsStdev.toFixed(2),
|
|
77
|
+
iqr: iqr.toFixed(2),
|
|
78
|
+
topQuartileWinsMean: topQuartileWinsMean.toFixed(2),
|
|
79
|
+
bottomQuartileWinsMean: bottomQuartileWinsMean.toFixed(2),
|
|
80
|
+
classes: {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
classes.forEach((className, i) => {
|
|
85
|
+
/**
|
|
86
|
+
* Get leader KPIs
|
|
87
|
+
* Leaders are in slot 1
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
// Names are in format "G|avg|1_B"
|
|
91
|
+
// G is power level, avg is trait combo, 1 is class number, B is formation position
|
|
92
|
+
const leaderWins = data.filter(item => item.slot1.includes(`${i + 1}`)).map(item => item.wins)
|
|
93
|
+
|
|
94
|
+
const leaderWinsMean = leaderWins.reduce((sum, value) => sum + value, 0) / leaderWins.length;
|
|
95
|
+
const leaderWinsVariance = leaderWins.reduce((sum, value) => sum + Math.pow(value - leaderWinsMean, 2), 0) / (leaderWins.length - 1);
|
|
96
|
+
const leaderWinsStdev = Math.sqrt(leaderWinsVariance)
|
|
97
|
+
|
|
98
|
+
// Calculate Interquartile Range (IQR)
|
|
99
|
+
leaderWins.sort((a, b) => a - b)
|
|
100
|
+
const leaderQ1 = leaderWins[Math.floor(leaderWins.length / 4)]
|
|
101
|
+
const leaderQ3 = leaderWins[Math.floor(leaderWins.length * 3 / 4)]
|
|
102
|
+
const leaderIqr = leaderQ3 - leaderQ1
|
|
103
|
+
|
|
104
|
+
// Analyse top and bottom quartiles
|
|
105
|
+
const leaderTopQuartile = data.filter(item => item.slot1.includes(`${i + 1}`) && item.wins >= leaderQ3)
|
|
106
|
+
const leaderBottomQuartile = data.filter(item => item.slot1.includes(`${i + 1}`) && item.wins <= leaderQ1)
|
|
107
|
+
const leaderTopQuartileWins = leaderTopQuartile.map(item => item.wins)
|
|
108
|
+
const leaderBottomQuartileWins = leaderBottomQuartile.map(item => item.wins)
|
|
109
|
+
const leaderTopQuartileWinsMean = leaderTopQuartileWins.reduce((sum, value) => sum + value, 0) / leaderTopQuartileWins.length
|
|
110
|
+
const leaderBottomQuartileWinsMean = leaderBottomQuartileWins.reduce((sum, value) => sum + value, 0) / leaderBottomQuartileWins.length
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get non-leader KPIs
|
|
114
|
+
* Non leaders are in slots 2, 3, 4, 5
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
// Get the teams where the class is in the non-leader slots
|
|
118
|
+
const nonLeaderTeams = data.filter(item => item.slot2.includes(`${i + 1}`) || item.slot3.includes(`${i + 1}`) || item.slot4.includes(`${i + 1}`) || item.slot5.includes(`${i + 1}`))
|
|
119
|
+
|
|
120
|
+
const totalNonLeaderWins = nonLeaderTeams.reduce((sum, item) => {
|
|
121
|
+
const team = [item.slot2, item.slot3, item.slot4, item.slot5]
|
|
122
|
+
const occurrences = team.filter(slot => slot.includes(`${i + 1}`)).length
|
|
123
|
+
return sum + item.wins / 5 * occurrences
|
|
124
|
+
}, 0)
|
|
125
|
+
|
|
126
|
+
// Get total non-leader occurrences
|
|
127
|
+
const totalOccurrences = nonLeaderTeams.reduce((sum, item) => {
|
|
128
|
+
const team = [item.slot2, item.slot3, item.slot4, item.slot5]
|
|
129
|
+
return sum + team.filter(slot => slot.includes(`${i + 1}`)).length
|
|
130
|
+
}, 0)
|
|
131
|
+
|
|
132
|
+
// Calulate win per non-leader occurance
|
|
133
|
+
const winsPerOccurance = totalNonLeaderWins / totalOccurrences
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
results.classes[className] = {
|
|
137
|
+
leader: {
|
|
138
|
+
winsMean: leaderWinsMean.toFixed(2),
|
|
139
|
+
winsMeanVsGlobal: (leaderWinsMean - winsMean).toFixed(2),
|
|
140
|
+
winsMeanVsGlobalPercentage: ((leaderWinsMean - winsMean) / winsMean * 100).toFixed(2),
|
|
141
|
+
winsStdev: leaderWinsStdev.toFixed(2),
|
|
142
|
+
winsStdevVsGlobal: (leaderWinsStdev - winsStdev).toFixed(2),
|
|
143
|
+
winsStdevVsGlobalPercentage: ((leaderWinsStdev - winsStdev) / winsStdev * 100).toFixed(2),
|
|
144
|
+
iqr: leaderIqr.toFixed(2),
|
|
145
|
+
iqrVsGlobal: (leaderIqr - iqr).toFixed(2),
|
|
146
|
+
iqrVsGlobalPercentage: ((leaderIqr - iqr) / iqr * 100).toFixed(2),
|
|
147
|
+
topQuartileWinsMean: leaderTopQuartileWinsMean.toFixed(2),
|
|
148
|
+
topQuartileWinsMeanVsGlobal: (leaderTopQuartileWinsMean - topQuartileWinsMean).toFixed(2),
|
|
149
|
+
topQuartileWinsMeanVsGlobalPercentage: ((leaderTopQuartileWinsMean - topQuartileWinsMean) / topQuartileWinsMean * 100).toFixed(2),
|
|
150
|
+
bottomQuartileWinsMean: leaderBottomQuartileWinsMean.toFixed(2),
|
|
151
|
+
bottomQuartileWinsMeanVsGlobal: (leaderBottomQuartileWinsMean - bottomQuartileWinsMean).toFixed(2),
|
|
152
|
+
bottomQuartileWinsMeanVsGlobalPercentage: ((leaderBottomQuartileWinsMean - bottomQuartileWinsMean) / bottomQuartileWinsMean * 100).toFixed(2)
|
|
153
|
+
},
|
|
154
|
+
nonLeader: {
|
|
155
|
+
winsPerOccurance: winsPerOccurance.toFixed(2),
|
|
156
|
+
winsPerOccuranceVsGlobal: (winsPerOccurance - winsPerSlot).toFixed(2),
|
|
157
|
+
winsPerOccuranceVsGlobalPercentage: ((winsPerOccurance - winsPerSlot) / winsPerSlot * 100).toFixed(2)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return results
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const main = async (executionId, numOfTasks, gameLogicVersion) => {
|
|
166
|
+
const gameLogicConstants = require(`../../game-logic/${gameLogicVersion}/constants.js`)
|
|
167
|
+
|
|
168
|
+
const data = await downloadAndCombineResults(executionId, numOfTasks)
|
|
169
|
+
|
|
170
|
+
const availablePowerLevels = {
|
|
171
|
+
'G': 'Godlike',
|
|
172
|
+
'M': 'Mythical',
|
|
173
|
+
'L': 'Legendary',
|
|
174
|
+
'R': 'Rare',
|
|
175
|
+
'U': 'Uncommon',
|
|
176
|
+
'C': 'Common'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Find power levels from data
|
|
180
|
+
// slot1 is in the format "G|avg|1_B" where the first character is the power level
|
|
181
|
+
// Find the unique first characters in slot1
|
|
182
|
+
const powerLevels = Array.from(new Set(data.map(item => item.slot1[0])))
|
|
183
|
+
|
|
184
|
+
// Calculate KPIs for each power level
|
|
185
|
+
const results = {
|
|
186
|
+
metadata: {
|
|
187
|
+
gameLogicVersion: gameLogicVersion,
|
|
188
|
+
gameLogicConstants: gameLogicConstants,
|
|
189
|
+
powerLevels: powerLevels.map(powerLevel => availablePowerLevels[powerLevel])
|
|
190
|
+
},
|
|
191
|
+
results: {}
|
|
192
|
+
}
|
|
193
|
+
powerLevels.forEach((powerLevel) => {
|
|
194
|
+
const powerLevelData = data.filter(item => item.slot1[0] === powerLevel)
|
|
195
|
+
results.results[availablePowerLevels[powerLevel]] = calculateKpis(powerLevelData)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if(process.env.CLOUD_RUN_JOB) {
|
|
199
|
+
const bucket = storage.bucket('gotchi-battler-sims-v1-7-2')
|
|
200
|
+
|
|
201
|
+
// Get number of files in the bucket
|
|
202
|
+
const [files] = await bucket.getFiles()
|
|
203
|
+
const numOfFiles = files.length
|
|
204
|
+
|
|
205
|
+
// Write the results to a file
|
|
206
|
+
await bucket.file(`${numOfFiles + 1}.json`).save(JSON.stringify(results, null, 2))
|
|
207
|
+
} else {
|
|
208
|
+
// Write to /scripts/balancing/output/<executionId>.json
|
|
209
|
+
fs.writeFileSync(path.join(__dirname, '/output/', `${executionId}.json`), JSON.stringify(results, null, 2))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = main
|
|
214
|
+
|
|
215
|
+
// node scripts/balancing/processSims.js avg-sims-99hzc 7920 v1.7
|
|
216
|
+
if (require.main === module) {
|
|
217
|
+
const executionId = process.env.EXECUTION_ID || process.argv[2]
|
|
218
|
+
const numOfTasks = parseInt(process.env.NUM_OF_TASKS) || parseInt(process.argv[3])
|
|
219
|
+
const gameLogicVersion = process.env.GAME_LOGIC_VERSION || process.argv[4]
|
|
220
|
+
|
|
221
|
+
main(executionId, numOfTasks, gameLogicVersion)
|
|
222
|
+
.then(() => {
|
|
223
|
+
console.log('Done')
|
|
224
|
+
process.exit(0)
|
|
225
|
+
})
|
|
226
|
+
.catch((error) => {
|
|
227
|
+
console.error(error)
|
|
228
|
+
process.exit(1)
|
|
229
|
+
})
|
|
230
230
|
}
|