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.
Files changed (39) hide show
  1. package/.env.example +1 -0
  2. package/.vscode/settings.json +4 -4
  3. package/Dockerfile +10 -0
  4. package/README.md +49 -49
  5. package/cloudbuild.yaml +27 -0
  6. package/constants/tournamentManagerAbi.json +208 -208
  7. package/game-logic/index.js +6 -5
  8. package/game-logic/v1.4/constants.js +120 -120
  9. package/game-logic/v1.4/index.js +1366 -1353
  10. package/game-logic/v1.5/index.js +8 -8
  11. package/game-logic/v1.6/constants.js +129 -129
  12. package/game-logic/v1.6/index.js +1406 -1402
  13. package/game-logic/v1.7/constants.js +147 -0
  14. package/game-logic/v1.7/index.js +1389 -0
  15. package/index.js +13 -6
  16. package/package.json +26 -22
  17. package/schemas/team.json +208 -203
  18. package/scripts/balancing/createCSV.js +126 -0
  19. package/scripts/balancing/fixTrainingGotchis.js +260 -0
  20. package/scripts/balancing/processSims.js +230 -0
  21. package/scripts/balancing/sims.js +278 -0
  22. package/scripts/balancing/v1.7/class_combos.js +44 -0
  23. package/scripts/balancing/v1.7/setTeamPositions.js +105 -0
  24. package/scripts/balancing/v1.7/training_gotchis.json +20162 -0
  25. package/scripts/balancing/v1.7/trait_combos.json +10 -0
  26. package/scripts/balancing/v1.7.1/class_combos.js +44 -0
  27. package/scripts/balancing/v1.7.1/setTeamPositions.js +122 -0
  28. package/scripts/balancing/v1.7.1/training_gotchis.json +22402 -0
  29. package/scripts/balancing/v1.7.1/trait_combos.json +10 -0
  30. package/scripts/data/team1.json +200 -200
  31. package/scripts/data/team2.json +200 -200
  32. package/scripts/data/tournaments.json +66 -66
  33. package/scripts/runBattle.js +15 -15
  34. package/scripts/validateBattle.js +70 -64
  35. package/scripts/validateTournament.js +101 -101
  36. package/utils/contracts.js +12 -12
  37. package/utils/errors.js +29 -29
  38. package/utils/transforms.js +88 -47
  39. package/utils/validations.js +39 -39
@@ -0,0 +1,278 @@
1
+ require('dotenv').config()
2
+ const path = require('path')
3
+ const crypto = require('crypto')
4
+ const { Storage } = require('@google-cloud/storage')
5
+ const storage = new Storage()
6
+ const classes = ['Ninja','Enlightened','Cleaver','Tank','Cursed','Healer', 'Mage', 'Troll']
7
+
8
+ /**
9
+ * Create teams from all the possible trait combinations from the class combination
10
+ * @param {Array} classCombo The class combinations
11
+ * @param {Array} classTraitCombos The trait combinations for each class
12
+ * @param {Array} powerLevels The power levels
13
+ * @param {Array} trainingGotchis The training gotchis
14
+ * @param {Boolean} useTraitSets If false then exhaustive search is done, if true then only the trait sets are used
15
+ * @returns {Object} A team object
16
+ */
17
+ const createTeamIndexes = (classCombos, classTraitCombos, powerLevels, trainingGotchis, useTraitSets) => {
18
+
19
+ const teams = []
20
+
21
+ if (!useTraitSets) {
22
+ classCombos.forEach(classCombo => {
23
+ powerLevels.forEach(powerLevel => {
24
+ // Loop over each class in the classCombo
25
+ for(let i = 0; i < classCombo.length; i++) {
26
+ classTraitCombos[classCombo[i] - 1].forEach(traitSet1 => {
27
+ for (let j = i + 1; j < classCombo.length; j++) {
28
+ classTraitCombos[classCombo[j] - 1].forEach(traitSet2 => {
29
+ for (let k = j + 1; k < classCombo.length; k++) {
30
+ classTraitCombos[classCombo[k] - 1].forEach(traitSet3 => {
31
+ for (let l = k + 1; l < classCombo.length; l++) {
32
+ classTraitCombos[classCombo[l] - 1].forEach(traitSet4 => {
33
+ for (let m = l + 1; m < classCombo.length; m++) {
34
+ classTraitCombos[classCombo[m] - 1].forEach(traitSet5 => {
35
+ const team = [];
36
+
37
+ [traitSet1, traitSet2, traitSet3, traitSet4, traitSet5].forEach((traitSet, index) => {
38
+ const gotchiName = `${powerLevel} ${traitSet} ${classes[classCombo[index] - 1]}`
39
+ const gotchi = trainingGotchis.find(gotchi => {
40
+ return gotchi.name === gotchiName
41
+ })
42
+
43
+ if (!gotchi) throw new Error(`Gotchi not found: "${gotchiName}"`)
44
+
45
+ team.push(gotchi.id)
46
+ })
47
+
48
+ teams.push(team)
49
+ })
50
+ }
51
+ })
52
+ }
53
+ })
54
+ }
55
+ })
56
+ }
57
+ })
58
+ }
59
+ })
60
+ })
61
+ } else {
62
+ classCombos.forEach(classCombo => {
63
+ powerLevels.forEach(powerLevel => {
64
+ // Loop over how many trait sets there are in the classTraitCombos array
65
+ classTraitCombos[0].forEach((x, i) => {
66
+ const team = []
67
+ classCombo.forEach(classIndex => {
68
+ const gotchiName = `${powerLevel} ${classTraitCombos[classIndex - 1][i]} ${classes[classIndex - 1]}`
69
+ const gotchi = trainingGotchis.find(gotchi => {
70
+ return gotchi.name === gotchiName
71
+ })
72
+
73
+ if (!gotchi) throw new Error(`Gotchi not found: "${gotchiName}"`)
74
+
75
+ team.push(gotchi.id)
76
+ })
77
+ teams.push(team)
78
+ })
79
+ })
80
+ })
81
+ }
82
+
83
+
84
+
85
+ return teams
86
+ }
87
+
88
+ /**
89
+ * Creates an in game team object from a team index
90
+ * @param {Array} teamIndex An array of gotchi ids
91
+ * @returns {Object} An in game team object
92
+ */
93
+ const createTeamFromTeamIndex = (teamIndex, trainingGotchis, setTeamPositions) => {
94
+ const team = {
95
+ formation: {
96
+ front: [null, null, null, null, null],
97
+ back: [null, null, null, null, null],
98
+ },
99
+ leader: null,
100
+ name: null,
101
+ owner: null
102
+ }
103
+
104
+ // Put all in the back row for now
105
+ team.formation.back = teamIndex.map(gotchiId => {
106
+ const gotchi = trainingGotchis.find(gotchi => gotchi.id === gotchiId)
107
+
108
+ if (!gotchi) throw new Error(`Gotchi not found with id: "${gotchiId}"`)
109
+
110
+ return gotchi
111
+ })
112
+
113
+ team.leader = team.formation.back[0].id
114
+ team.name = `${team.formation.back[0].name[0]} ${teamIndex}` // e.g. "M 1,2,3,4,5"
115
+ team.owner = '0x0000000000000000000000000000000000000000'
116
+
117
+ // Set the team positions for each gotchi being in the front or back row
118
+ setTeamPositions(team)
119
+
120
+ return team
121
+ }
122
+
123
+ const getGotchisSimNameFromTeam = (team) => {
124
+ const names = [];
125
+
126
+ [0,1,2,3,4].forEach(i => {
127
+ const gotchi = team.formation.back[i] || team.formation.front[i]
128
+ const position = !!team.formation.back[i] ? 'B' : 'F'
129
+
130
+ const nameParts = gotchi.name.split(' ')
131
+ // Return e.g. "R|++++|1_B"
132
+ names.push(`${nameParts[0][0]}|${nameParts[1]}|${classes.indexOf(nameParts[2]) + 1}_${position}`)
133
+ })
134
+ return names
135
+ }
136
+
137
+ const runSims = async (simsVersion, gameLogicVersion, simsPerMatchup) => {
138
+ const trainingGotchis = require(`./${simsVersion}/training_gotchis.json`)
139
+ const classCombos = require(`./${simsVersion}/class_combos.js`)
140
+ const classTraitCombos = require(`./${simsVersion}/trait_combos.json`)
141
+ const setTeamPositions = require(`./${simsVersion}/setTeamPositions`)
142
+ const gameLogic = require("../../game-logic")[gameLogicVersion].gameLoop
143
+
144
+ const attackingPowerLevels = ['Godlike']
145
+ const defendingPowerLevels = ['Godlike', 'Mythical', 'Legendary']
146
+
147
+ const attackingTeamIndexes = createTeamIndexes(classCombos, classTraitCombos, attackingPowerLevels, trainingGotchis, true)
148
+
149
+ // console.log(`Running sims for ${attackingTeamIndexes.length} attacking teams`)
150
+
151
+ const defendingTeamIndexes = createTeamIndexes(classCombos, classTraitCombos, defendingPowerLevels, trainingGotchis, true)
152
+
153
+ // console.log(`Against ${defendingTeamIndexes.length} defending teams`)
154
+
155
+ // Which attacking team are we running the sims on?
156
+ // If running on Cloud Run, use the task index
157
+ // If running locally, use the command line argument or default to 0
158
+ const attackingTeamIndex = process.env.CLOUD_RUN_TASK_INDEX ? parseInt(process.env.CLOUD_RUN_TASK_INDEX) : parseInt(process.argv[2]) || 0
159
+
160
+ // Get the attacking team
161
+ const attackingTeam = createTeamFromTeamIndex(attackingTeamIndexes[attackingTeamIndex], trainingGotchis, setTeamPositions)
162
+ const gotchiSimNames = getGotchisSimNameFromTeam(attackingTeam)
163
+ // Run the sims for each defending team
164
+ const results = {
165
+ id: attackingTeamIndex,
166
+ slot1: gotchiSimNames[0],
167
+ slot2: gotchiSimNames[1],
168
+ slot3: gotchiSimNames[2],
169
+ slot4: gotchiSimNames[3],
170
+ slot5: gotchiSimNames[4],
171
+ wins: 0,
172
+ draws: 0,
173
+ losses: 0
174
+ }
175
+
176
+ defendingPowerLevels.forEach(powerLevel => {
177
+ results[`wins${powerLevel}`] = 0
178
+ results[`draws${powerLevel}`] = 0
179
+ results[`losses${powerLevel}`] = 0
180
+ })
181
+
182
+ console.time(`Sims for ${attackingTeam.name}`)
183
+
184
+ defendingTeamIndexes.forEach((defendingTeamIndex, i) => {
185
+ const defendingTeam = createTeamFromTeamIndex(defendingTeamIndex, trainingGotchis, setTeamPositions)
186
+
187
+ let matchupWins = 0
188
+ let matchupDraws = 0
189
+ let matchupLosses = 0
190
+
191
+ // Run the sims
192
+ Array(simsPerMatchup).fill(null).forEach(() => {
193
+ // Quit early if result is already determined
194
+ if (matchupWins >= simsPerMatchup / 2 ||
195
+ matchupLosses >= simsPerMatchup / 2 ||
196
+ matchupDraws >= simsPerMatchup / 2) return
197
+
198
+ const logs = gameLogic(attackingTeam, defendingTeam, crypto.randomBytes(32).toString('hex'))
199
+
200
+ if (logs.result.winner === 1) matchupWins++
201
+ if (logs.result.winner === 0) matchupDraws++
202
+ if (logs.result.winner === 2) matchupLosses++
203
+ })
204
+
205
+ // Get first letter of oppeonent team name to determine power level
206
+ const powerLevel = defendingPowerLevels.find(name => name[0] === defendingTeam.name[0])
207
+
208
+ if (matchupWins >= simsPerMatchup / 2) {
209
+ results.wins++
210
+ results[`wins${powerLevel}`]++
211
+ }
212
+
213
+ if (matchupDraws >= simsPerMatchup / 2) {
214
+ results.draws++
215
+ results[`draws${powerLevel}`]++
216
+ }
217
+
218
+ if (matchupLosses >= simsPerMatchup / 2) {
219
+ results.losses++
220
+ results[`losses${powerLevel}`]++
221
+ }
222
+ })
223
+
224
+ console.timeEnd(`Sims for ${attackingTeam.name}`)
225
+
226
+ // Check total wins, draws, losses to make sure they add up to number of defending teams
227
+ const totalMatchups = results.wins + results.draws + results.losses
228
+ if (totalMatchups !== defendingTeamIndexes.length) {
229
+ throw new Error(`Total matchups (${totalMatchups}) does not match number of defending teams (${defendingTeamIndexes.length})`)
230
+ }
231
+
232
+ if (process.env.CLOUD_RUN_JOB && process.env.SIMS_BUCKET) {
233
+ // Save as JSON to GCS
234
+ try {
235
+ await storage.bucket(process.env.SIMS_BUCKET).file(`${process.env.CLOUD_RUN_EXECUTION}_${attackingTeamIndex}.json`).save(JSON.stringify(results))
236
+ } catch (err) {
237
+ throw err
238
+ }
239
+
240
+ } else {
241
+ console.log('Results', results)
242
+
243
+ // Test saving to GCS
244
+ if (false) {
245
+ process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join(__dirname, '../../keyfile.json')
246
+ try {
247
+ await storage.bucket(process.env.SIMS_BUCKET).file(`avg-sims-znw68_${attackingTeamIndex}.json`).save(JSON.stringify(results))
248
+ } catch (err) {
249
+ throw err
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ module.exports = runSims
256
+
257
+ // If running from the command line, run the sims
258
+ // 1st argument is the attacking team index
259
+ // 2nd argument is the sims version
260
+ // 3rd argument is the game logic version
261
+ // 4th argument is the number of sims per matchup
262
+ // node scripts/balancing/sims.js 0 v1.7.1 v1.7 3 true
263
+ if (require.main === module) {
264
+ const simsVersion = process.env.SIMS_VERSION || process.argv[3] || 'v1.7'
265
+ const gameLogicVersion = process.env.GAME_LOGIC_VERSION || process.argv[4] || 'v1.7'
266
+ const simsPerMatchup = parseInt(process.env.SIMS_PER_MATCHUP) || parseInt(process.argv[5]) || 3
267
+ const useAvg = process.env.USE_AVG || process.argv[6] === 'true' || false
268
+
269
+ runSims(simsVersion, gameLogicVersion, simsPerMatchup, useAvg)
270
+ .then(() => {
271
+ console.log('Done')
272
+ process.exit(0)
273
+ })
274
+ .catch((err) => {
275
+ console.error(err)
276
+ process.exit(1)
277
+ })
278
+ }
@@ -0,0 +1,44 @@
1
+ const generateClassCombinations = () => {
2
+ // Generate combinations of the 8 classes for the 4 non-leader spots
3
+ const combinations = []
4
+
5
+ for (let i = 1; i <= 8; i++) {
6
+ for (let j = 1; j <= 8; j++) {
7
+ for (let k = 1; k <= 8; k++) {
8
+ for (let l = 1; l <= 8; l++) {
9
+ combinations.push([i, j, k, l].sort())
10
+ }
11
+ }
12
+ }
13
+ }
14
+
15
+ // Remove duplicate combinations, so [1,1,1,1] is allowed but [1,1,1,2] and [1,1,2,1] are duplicates
16
+ // Keep as numbers for now, convert to strings to remove duplicates, then convert back to numbers
17
+ const uniqueCombinations = [...new Set(combinations.map((combination) => combination.join('')))].map((combination) => combination.split('').map((number) => parseInt(number)))
18
+
19
+ return uniqueCombinations
20
+ }
21
+
22
+ const getCombinationsForALeader = (leaderClass) => {
23
+ const combinations = generateClassCombinations()
24
+
25
+ const combinationsForALeader = combinations.map((combination) => {
26
+ return [leaderClass, ...combination]
27
+ })
28
+
29
+ return combinationsForALeader
30
+ }
31
+
32
+ const getAllClassCombos = () => {
33
+ const allClassCombos = []
34
+
35
+ for (let i = 1; i <= 8; i++) {
36
+ allClassCombos.push(...getCombinationsForALeader(i))
37
+ }
38
+
39
+ return allClassCombos
40
+ }
41
+
42
+ const allClassCombos = getAllClassCombos()
43
+
44
+ module.exports = allClassCombos
@@ -0,0 +1,105 @@
1
+ const trainingGotchis = require('./training_gotchis.json')
2
+
3
+ const getFrontRowScore = (gotchiId, leaderId) => {
4
+ const gotchi = trainingGotchis.find(gotchi => gotchi.id === gotchiId)
5
+ const leader = trainingGotchis.find(gotchi => gotchi.id === leaderId)
6
+
7
+ if (gotchi.name.includes(' avg ')) {
8
+
9
+ if (gotchi.specialId === 2) {
10
+ // Enlightened are the best up front
11
+ return 6
12
+ } else if (leader.specialId === 6 && gotchi.specialId === 6) {
13
+ // Healer with a healer leader are the second best up front
14
+ return 5
15
+ } else if (gotchi.specialId === 5) {
16
+ // Cursed are the third best up front
17
+ return 4
18
+ } else if (gotchi.specialId === 6) {
19
+ // Healers do ok
20
+ return 2
21
+ } else if (gotchi.specialId === 7) {
22
+ // Mages always go to the back
23
+ return 0
24
+ } else {
25
+ return 1
26
+ }
27
+ }
28
+
29
+ // High health gotchis do well up front
30
+ const isHighHealth = gotchi.nrg < 50
31
+
32
+ // High armor gotchis do well up front
33
+ const isLowAgg = gotchi.agg < 50
34
+
35
+ // High evasion gotchis do well up front
36
+ const isHighSpk = gotchi.spk >= 50
37
+
38
+ const isLowBrn = gotchi.brn < 50
39
+
40
+ // Create a score from 0 to 6 based on how much they favour the front
41
+
42
+ let score = 0
43
+
44
+ if (gotchi.specialId === 2) score +=2 // Enlightened
45
+ if (isHighHealth) score++
46
+ if (isLowBrn) score++
47
+ if (isLowAgg) score++
48
+ if (isHighSpk) score++
49
+
50
+ return score
51
+ }
52
+
53
+ module.exports = (team) => {
54
+ // All gotchis are currently in the back row
55
+ // Get the score for each gotchi for how much they favour the front row
56
+ const teamFrontRowScores = team.formation.back.map((gotchi) => getFrontRowScore(gotchi.id, team.leader));
57
+
58
+ [0,1,2,3,4].forEach((i) => {
59
+ const gotchi = team.formation.back[i]
60
+ const score = teamFrontRowScores[i]
61
+
62
+ // If score is >= 3 then move to front row
63
+ if (score >= 3) {
64
+ team.formation.front[i] = gotchi
65
+ team.formation.back[i] = null
66
+ }
67
+ })
68
+
69
+ // If you have 5 gotchis in the front then send the lowest 2 to the back
70
+ const frontGotchis = team.formation.front.filter(gotchi => gotchi)
71
+ if (frontGotchis.length === 5) {
72
+ // Sort the gotchis by score in ascending order (lowest score first)
73
+ const orderedGotchis = JSON.parse(JSON.stringify(frontGotchis)).sort((a, b) => getFrontRowScore(a.id, team.leader) - getFrontRowScore(b.id, team.leader));
74
+
75
+ // Loop through the front row and the first 2 gotchis that have a score of either orderedGotchis[0] or orderedGotchis[1] move to the back
76
+ let hasMoved = 0
77
+ team.formation.front.forEach((gotchi, i) => {
78
+ if (hasMoved < 2 && (gotchi.id === orderedGotchis[0].id || gotchi.id === orderedGotchis[1].id)) {
79
+ team.formation.back[i] = gotchi
80
+ team.formation.front[i] = null
81
+
82
+ hasMoved++
83
+ }
84
+ })
85
+ }
86
+
87
+ // If you have 5 gotchis in the back then send the highest 2 to the front
88
+ const backGotchis = team.formation.back.filter(gotchi => gotchi)
89
+ if (backGotchis.length === 5) {
90
+ // Sort the gotchis by score in descending order (highest score first)
91
+ const orderedGotchis = JSON.parse(JSON.stringify(backGotchis)).sort((a, b) => getFrontRowScore(b.id, team.leader) - getFrontRowScore(a.id, team.leader));
92
+
93
+ // Loop through the back row and the first 2 gotchis that have a score of either orderedGotchis[0] or orderedGotchis[1] move to the front
94
+ let hasMoved = 0
95
+ team.formation.back.forEach((gotchi, i) => {
96
+ if (hasMoved < 2 && (gotchi.id === orderedGotchis[0].id || gotchi.id === orderedGotchis[1].id)) {
97
+ team.formation.front[i] = gotchi
98
+ team.formation.back[i] = null
99
+
100
+ hasMoved++
101
+ }
102
+ })
103
+ }
104
+ }
105
+