gotchi-battler-game-logic 1.0.0 → 2.0.0
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/.env.example +1 -0
- package/.vscode/settings.json +4 -4
- package/Dockerfile +10 -0
- package/README.md +49 -49
- package/cloudbuild.yaml +27 -0
- package/constants/tournamentManagerAbi.json +208 -208
- package/game-logic/index.js +6 -5
- package/game-logic/v1.4/constants.js +120 -120
- package/game-logic/v1.4/index.js +1366 -1353
- package/game-logic/v1.5/index.js +8 -8
- package/game-logic/v1.6/constants.js +129 -129
- package/game-logic/v1.6/index.js +1406 -1402
- package/game-logic/v1.7/constants.js +147 -0
- package/game-logic/v1.7/index.js +1389 -0
- package/index.js +13 -6
- package/package.json +26 -22
- package/schemas/team.json +208 -203
- package/scripts/balancing/createCSV.js +126 -0
- package/scripts/balancing/fixTrainingGotchis.js +260 -0
- package/scripts/balancing/processSims.js +230 -0
- package/scripts/balancing/sims.js +278 -0
- package/scripts/balancing/v1.7/class_combos.js +44 -0
- package/scripts/balancing/v1.7/setTeamPositions.js +105 -0
- package/scripts/balancing/v1.7/training_gotchis.json +20162 -0
- package/scripts/balancing/v1.7/trait_combos.json +10 -0
- package/scripts/balancing/v1.7.1/class_combos.js +44 -0
- package/scripts/balancing/v1.7.1/setTeamPositions.js +122 -0
- package/scripts/balancing/v1.7.1/training_gotchis.json +22402 -0
- package/scripts/balancing/v1.7.1/trait_combos.json +10 -0
- package/scripts/data/team1.json +200 -200
- package/scripts/data/team2.json +200 -200
- package/scripts/data/tournaments.json +66 -66
- package/scripts/runBattle.js +15 -15
- package/scripts/validateBattle.js +70 -64
- package/scripts/validateTournament.js +101 -101
- package/utils/contracts.js +12 -12
- package/utils/errors.js +29 -29
- package/utils/transforms.js +88 -47
- package/utils/validations.js +39 -39
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const trainingGotchis = require('./v1.7/training_gotchis.json')
|
|
3
|
+
|
|
4
|
+
const specials = [
|
|
5
|
+
{
|
|
6
|
+
"id": 1,
|
|
7
|
+
"class": "Ninja",
|
|
8
|
+
"name": "Spectral strike",
|
|
9
|
+
"cooldown": 0,
|
|
10
|
+
"leaderPassive": "Sharpen blades"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": 2,
|
|
14
|
+
"class": "Enlightened",
|
|
15
|
+
"name": "Meditate",
|
|
16
|
+
"cooldown": 0,
|
|
17
|
+
"leaderPassive": "Cloud of Zen"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": 3,
|
|
21
|
+
"class": "Cleaver",
|
|
22
|
+
"name": "Cleave",
|
|
23
|
+
"cooldown": 2,
|
|
24
|
+
"leaderPassive": "Frenzy"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": 4,
|
|
28
|
+
"class": "Tank",
|
|
29
|
+
"name": "Taunt",
|
|
30
|
+
"cooldown": 0,
|
|
31
|
+
"leaderPassive": "Fortify"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": 5,
|
|
35
|
+
"class": "Cursed",
|
|
36
|
+
"name": "Curse",
|
|
37
|
+
"cooldown": 0,
|
|
38
|
+
"leaderPassive": "Spread the fear"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": 6,
|
|
42
|
+
"class": "Healer",
|
|
43
|
+
"name": "Blessing",
|
|
44
|
+
"cooldown": 0,
|
|
45
|
+
"leaderPassive": "Cleansing Aura"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": 7,
|
|
49
|
+
"class": "Mage",
|
|
50
|
+
"name": "Thunder",
|
|
51
|
+
"cooldown": 2,
|
|
52
|
+
"leaderPassive": "Channel the coven"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": 8,
|
|
56
|
+
"class": "Troll",
|
|
57
|
+
"name": "Devestating Smash",
|
|
58
|
+
"cooldown": 2,
|
|
59
|
+
"leaderPassive": "Clan momentum"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
const classes = ['Ninja','Enlightened','Cleaver','Tank','Cursed','Healer', 'Mage', 'Troll']
|
|
64
|
+
const powerLevels = ['Godlike', 'Mythical', 'Legendary', 'Rare', 'Uncommon', 'Common', 'Garbage']
|
|
65
|
+
|
|
66
|
+
// This is copied/hacked from the mapGotchi function in the backend
|
|
67
|
+
const fixTraits = (gotchi) => {
|
|
68
|
+
const traitMaps = {
|
|
69
|
+
speed: {
|
|
70
|
+
baseFormula: 100,
|
|
71
|
+
multiplier: 1,
|
|
72
|
+
traitKey: 0,
|
|
73
|
+
isNegative: false
|
|
74
|
+
},
|
|
75
|
+
health: {
|
|
76
|
+
baseFormula: 'brs*0.75',
|
|
77
|
+
multiplier: 12,
|
|
78
|
+
traitKey: 0,
|
|
79
|
+
isNegative: true
|
|
80
|
+
},
|
|
81
|
+
crit: {
|
|
82
|
+
baseFormula: 0,
|
|
83
|
+
multiplier: 0.5,
|
|
84
|
+
traitKey: 1,
|
|
85
|
+
isNegative: false
|
|
86
|
+
},
|
|
87
|
+
armor: {
|
|
88
|
+
baseFormula: 0,
|
|
89
|
+
multiplier: 2,
|
|
90
|
+
traitKey: 1,
|
|
91
|
+
isNegative: true
|
|
92
|
+
},
|
|
93
|
+
evade: {
|
|
94
|
+
baseFormula: 0,
|
|
95
|
+
multiplier: 0.3,
|
|
96
|
+
traitKey: 2,
|
|
97
|
+
isNegative: false
|
|
98
|
+
},
|
|
99
|
+
resist: {
|
|
100
|
+
baseFormula: 0,
|
|
101
|
+
multiplier: 1,
|
|
102
|
+
traitKey: 2,
|
|
103
|
+
isNegative: true
|
|
104
|
+
},
|
|
105
|
+
magic: {
|
|
106
|
+
baseFormula: 'brs*0.25',
|
|
107
|
+
multiplier: 5,
|
|
108
|
+
traitKey: 3,
|
|
109
|
+
isNegative: false
|
|
110
|
+
},
|
|
111
|
+
physical: {
|
|
112
|
+
baseFormula: 'brs*0.25',
|
|
113
|
+
multiplier: 5,
|
|
114
|
+
traitKey: 3,
|
|
115
|
+
isNegative: true
|
|
116
|
+
},
|
|
117
|
+
accuracy: {
|
|
118
|
+
baseFormula: 50,
|
|
119
|
+
multiplier: 0.5,
|
|
120
|
+
traitKey: 45,
|
|
121
|
+
isNegative: false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const traitValue = (trait) => {
|
|
126
|
+
return trait < 50 ? 50 - trait : trait - 50 + 1
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const onchainVals = [
|
|
130
|
+
gotchi.nrg,
|
|
131
|
+
gotchi.agg,
|
|
132
|
+
gotchi.spk,
|
|
133
|
+
gotchi.brn,
|
|
134
|
+
gotchi.eyc,
|
|
135
|
+
gotchi.eys
|
|
136
|
+
]
|
|
137
|
+
// Convert trait value to in-game value
|
|
138
|
+
const traitValues = onchainVals.map(x => { return traitValue(x) })
|
|
139
|
+
|
|
140
|
+
// Map traits
|
|
141
|
+
for(const trait in traitMaps) {
|
|
142
|
+
const traitMap = traitMaps[trait]
|
|
143
|
+
const onchainVal = onchainVals[traitMap.traitKey]
|
|
144
|
+
|
|
145
|
+
let base = traitMap.baseFormula
|
|
146
|
+
|
|
147
|
+
// If baseFormula is a string and contains a * then it is a formula
|
|
148
|
+
if (typeof traitMap.baseFormula === 'string' && traitMap.baseFormula.includes('*')) {
|
|
149
|
+
const formula = traitMap.baseFormula.split('*')
|
|
150
|
+
|
|
151
|
+
if (!gotchi[formula[0]]) throw new Error('Trait not found: ', formula[0])
|
|
152
|
+
|
|
153
|
+
base = Math.round(Number(gotchi[formula[0]]) * Number(formula[1]))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let newTrait
|
|
157
|
+
if (trait !== 'accuracy') {
|
|
158
|
+
if (traitMap.isNegative) {
|
|
159
|
+
newTrait = onchainVal < 50 ? Math.round(base + (traitValues[traitMap.traitKey] * traitMap.multiplier)) : base
|
|
160
|
+
} else {
|
|
161
|
+
newTrait = onchainVal < 50 ? base : Math.round(base + (traitValues[traitMap.traitKey] * traitMap.multiplier))
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
newTrait = base + ((traitValues[4] + traitValues[5]) * traitMap.multiplier)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (newTrait !== gotchi[trait]) gotchi[trait] = newTrait
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const addAvgGotchs = () => {
|
|
172
|
+
const statsToOverwrite = ["speed", "health", "crit", "armor", "evade", "resist", "accuracy"]
|
|
173
|
+
|
|
174
|
+
const avgGotchis = [];
|
|
175
|
+
|
|
176
|
+
// Add magic gotchis
|
|
177
|
+
['magic', 'physical'].forEach((brainValue) => {
|
|
178
|
+
powerLevels.forEach((powerLevel) => {
|
|
179
|
+
classes.forEach((className) => {
|
|
180
|
+
// Find all gotchis with the same class and power level
|
|
181
|
+
// Names are in the format "Godlike ++++ Ninja"
|
|
182
|
+
const gotchis = trainingGotchis.filter(gotchi => gotchi.name.includes(powerLevel) && gotchi.name.includes(className))
|
|
183
|
+
|
|
184
|
+
// Copy over one of the gotchis
|
|
185
|
+
const avgGotchi = JSON.parse(JSON.stringify(gotchis[0]))
|
|
186
|
+
|
|
187
|
+
// Add an id
|
|
188
|
+
avgGotchi.id = trainingGotchis.length + avgGotchis.length + 1
|
|
189
|
+
|
|
190
|
+
// Overwrite the name
|
|
191
|
+
avgGotchi.name = `${powerLevel} avg-${brainValue} ${className}`
|
|
192
|
+
|
|
193
|
+
// Overwrite the stats
|
|
194
|
+
statsToOverwrite.forEach((stat) => {
|
|
195
|
+
const total = gotchis.reduce((acc, gotchi) => acc + gotchi[stat], 0)
|
|
196
|
+
avgGotchi[stat] = Math.round(total / gotchis.length)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Get the highest/lowest magic value
|
|
200
|
+
const magicValues = gotchis.map(gotchi => gotchi.magic)
|
|
201
|
+
const highestMagic = Math.max(...magicValues)
|
|
202
|
+
const lowestMagic = Math.min(...magicValues)
|
|
203
|
+
|
|
204
|
+
// Get the highest/lowest physical value
|
|
205
|
+
const physicalValues = gotchis.map(gotchi => gotchi.physical)
|
|
206
|
+
const highestPhysical = Math.max(...physicalValues)
|
|
207
|
+
const lowestPhysical = Math.min(...physicalValues)
|
|
208
|
+
|
|
209
|
+
// If this is a magic gotchi then set the magic value to the highest
|
|
210
|
+
if (brainValue === 'magic') {
|
|
211
|
+
avgGotchi.magic = highestMagic
|
|
212
|
+
avgGotchi.physical = lowestPhysical
|
|
213
|
+
avgGotchi.attack = 'magic'
|
|
214
|
+
} else {
|
|
215
|
+
avgGotchi.magic = lowestMagic
|
|
216
|
+
avgGotchi.physical = highestPhysical
|
|
217
|
+
avgGotchi.attack = 'physical'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
avgGotchis.push(avgGotchi)
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
trainingGotchis.push(...avgGotchis)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Sort the gotchis by id
|
|
229
|
+
trainingGotchis.sort((a, b) => a.id - b.id)
|
|
230
|
+
|
|
231
|
+
trainingGotchis.forEach(gotchi => {
|
|
232
|
+
// Add special to gotchi
|
|
233
|
+
const special = specials.find(special => special.id === gotchi.specialId)
|
|
234
|
+
|
|
235
|
+
if (!special) throw new Error(`Special not found for gotchi: "${gotchi.name}"`)
|
|
236
|
+
|
|
237
|
+
gotchi.special = special
|
|
238
|
+
|
|
239
|
+
// Fix names
|
|
240
|
+
// If name ends with a + or - then add the class name at the end
|
|
241
|
+
if (gotchi.name.endsWith('+') || gotchi.name.endsWith('-')) {
|
|
242
|
+
gotchi.name = `${gotchi.name} ${classes[gotchi.specialId - 1]}`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Remove extra fields
|
|
246
|
+
delete gotchi.tier
|
|
247
|
+
delete gotchi.class
|
|
248
|
+
delete gotchi.stats_brs
|
|
249
|
+
|
|
250
|
+
// Make sure the gotchi has the correct traits
|
|
251
|
+
fixTraits(gotchi)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Add the average gotchis
|
|
255
|
+
addAvgGotchs()
|
|
256
|
+
|
|
257
|
+
// Write the updated trainingGotchis to a new file
|
|
258
|
+
fs.writeFileSync('./training_gotchis1.json', JSON.stringify(trainingGotchis, null, '\t'))
|
|
259
|
+
|
|
260
|
+
// node scripts/balancing/fixTrainingGotchis.js
|
|
@@ -0,0 +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-1')
|
|
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
|
+
}
|