gotchi-battler-game-logic 1.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/.vscode/settings.json +5 -0
- package/README.md +50 -0
- package/constants/tournamentManagerAbi.json +209 -0
- package/game-logic/index.js +6 -0
- package/game-logic/v1.4/constants.js +121 -0
- package/game-logic/v1.4/index.js +1354 -0
- package/game-logic/v1.5/constants.js +124 -0
- package/game-logic/v1.5/index.js +1378 -0
- package/game-logic/v1.6/constants.js +130 -0
- package/game-logic/v1.6/index.js +1403 -0
- package/index.js +6 -0
- package/package.json +22 -0
- package/schemas/team.json +204 -0
- package/scripts/data/team1.json +201 -0
- package/scripts/data/team2.json +201 -0
- package/scripts/data/tournaments.json +67 -0
- package/scripts/output/.gitkeep +0 -0
- package/scripts/runBattle.js +16 -0
- package/scripts/validateBattle.js +64 -0
- package/scripts/validateTournament.js +101 -0
- package/utils/contracts.js +13 -0
- package/utils/errors.js +30 -0
- package/utils/transforms.js +48 -0
- package/utils/validations.js +40 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": 1,
|
|
4
|
+
"name": "Test Tournament 1",
|
|
5
|
+
"gameLogicVersion": "v1.5"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"id": 2,
|
|
9
|
+
"name": "Test Tournament 2",
|
|
10
|
+
"gameLogicVersion": "v1.5"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"id": 4,
|
|
14
|
+
"name": "1000 DAI Bounty Tournament",
|
|
15
|
+
"gameLogicVersion": "v1.5"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": 5,
|
|
19
|
+
"name": "2000 DAI Bounty Tournament",
|
|
20
|
+
"gameLogicVersion": "v1.5"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": 7,
|
|
24
|
+
"name": "10k GHST Rarity Farming Tournament",
|
|
25
|
+
"gameLogicVersion": "v1.5"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": 8,
|
|
29
|
+
"name": "30k GHST Rarity Farming Tournament",
|
|
30
|
+
"gameLogicVersion": "v1.5"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": 9,
|
|
34
|
+
"name": "60k GHST Rarity Farming Tournament",
|
|
35
|
+
"gameLogicVersion": "v1.5"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": 10,
|
|
39
|
+
"name": "Test Tournament 3",
|
|
40
|
+
"gameLogicVersion": "v1.5"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": 11,
|
|
44
|
+
"name": "1000 DAI Bounty Tournament",
|
|
45
|
+
"gameLogicVersion": "v1.4"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": 12,
|
|
49
|
+
"name": "1000 DAI Bounty Tournament",
|
|
50
|
+
"gameLogicVersion": "v1.5"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": 13,
|
|
54
|
+
"name": "RF8 10K GHST Tournament",
|
|
55
|
+
"gameLogicVersion": "v1.5"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": 14,
|
|
59
|
+
"name": "RF8 40K GHST Tournament",
|
|
60
|
+
"gameLogicVersion": "v1.6"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": 15,
|
|
64
|
+
"name": "RF8 100K GHST Tournament",
|
|
65
|
+
"gameLogicVersion": "v1.6"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const battle = require('..')
|
|
4
|
+
|
|
5
|
+
// Edit these json files to test different battles
|
|
6
|
+
// NOTE: Only the in-game stats (speed, health, crit etc..) are used in the game logic
|
|
7
|
+
const team1 = require('./data/team1.json')
|
|
8
|
+
const team2 = require('./data/team2.json')
|
|
9
|
+
|
|
10
|
+
const results = battle(team1, team2, "82807311112923564712218359337695919195403960526804010606215202651499586140469")
|
|
11
|
+
|
|
12
|
+
const timestamp = new Date().getTime()
|
|
13
|
+
const resultsFilename = `results-${timestamp}.json`
|
|
14
|
+
fs.writeFileSync(path.join(__dirname, 'output', resultsFilename), JSON.stringify(results, null, '\t'))
|
|
15
|
+
|
|
16
|
+
console.log(`Results written to ${path.join(__dirname, 'output', resultsFilename)}`)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const axios = require('axios')
|
|
2
|
+
const { GameError, ValidationError } = require('../utils/errors')
|
|
3
|
+
const { logToInGameTeams } = require('../utils/transforms')
|
|
4
|
+
const { compareLogs } = require('../utils/validations')
|
|
5
|
+
const gameVersions = require('../game-logic')
|
|
6
|
+
|
|
7
|
+
const main = async (battleId, seed, gameLogicVersion) => {
|
|
8
|
+
|
|
9
|
+
if (!gameLogicVersion) gameLogicVersion = gameVersions.current
|
|
10
|
+
if (!gameVersions[gameLogicVersion]) throw new Error('Invalid game logic version')
|
|
11
|
+
|
|
12
|
+
const gameLoop = gameVersions[gameLogicVersion].gameLoop
|
|
13
|
+
|
|
14
|
+
const res = await axios.get(`https://storage.googleapis.com/gotchi-battler-live_battles/v1/${battleId}.json`)
|
|
15
|
+
|
|
16
|
+
if (!res || !res.data || !res.data.layout) {
|
|
17
|
+
console.error('Battle not found')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Transform the logs to in-game teams
|
|
22
|
+
const teams = logToInGameTeams(res.data)
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Run the game loop
|
|
26
|
+
const logs = await gameLoop(teams[0], teams[1], seed)
|
|
27
|
+
|
|
28
|
+
// Validate the results
|
|
29
|
+
compareLogs(res.data, logs)
|
|
30
|
+
|
|
31
|
+
return logs
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof GameError) {
|
|
34
|
+
console.error('Errored game logs: ', error.logs)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = main
|
|
42
|
+
|
|
43
|
+
// node scripts/validateBattle.js 4d0f3c5c-08a0-42db-bd34-dee44300685a 82807311112923564712218359337695919195403960526804010606215202651499586140469
|
|
44
|
+
if (require.main === module) {
|
|
45
|
+
const battleId = process.argv[2]
|
|
46
|
+
const seed = process.argv[3]
|
|
47
|
+
const gameLogicVersion = process.argv[4]
|
|
48
|
+
|
|
49
|
+
main(battleId, seed, gameLogicVersion)
|
|
50
|
+
.then(() => {
|
|
51
|
+
console.log('Results from game logic match the logs ✅')
|
|
52
|
+
console.log('Done')
|
|
53
|
+
process.exit(0)
|
|
54
|
+
})
|
|
55
|
+
.catch((error) => {
|
|
56
|
+
if (error instanceof ValidationError) {
|
|
57
|
+
console.error('Results from game logic do not match the logs ❌')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.error('Error: ', error.message)
|
|
61
|
+
|
|
62
|
+
process.exit(1)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const axios = require('axios')
|
|
4
|
+
const { getTournamentContract } = require('../utils/contracts')
|
|
5
|
+
const { ValidationError } = require('../utils/errors')
|
|
6
|
+
const tournaments = require('./data/tournaments.json')
|
|
7
|
+
const validateBattle = require('./validateBattle')
|
|
8
|
+
|
|
9
|
+
const main = async (tournamentId) => {
|
|
10
|
+
|
|
11
|
+
console.log(`Getting tournament config for id ${tournamentId}...`)
|
|
12
|
+
|
|
13
|
+
const tournamentConfig = tournaments.find(t => `${t.id}` === `${tournamentId}`)
|
|
14
|
+
|
|
15
|
+
if (!tournamentConfig || !tournamentConfig.gameLogicVersion) {
|
|
16
|
+
console.error('Tournament config not found with id:', tournamentId)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log(`Found, using game logic version "${tournamentConfig.gameLogicVersion}" ✅`)
|
|
21
|
+
console.log(`Getting tournament data for id: ${tournamentId}...`)
|
|
22
|
+
|
|
23
|
+
const tournament = await axios.get(`https://gotchi-battler-backend-blmom6tkla-ew.a.run.app/api/v1/tournaments/${tournamentId}`)
|
|
24
|
+
|
|
25
|
+
if (!tournament || !tournament.data || !tournament.data.address) {
|
|
26
|
+
console.error('Tournament not found with id:', tournamentId)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const onchainAddress = tournament.data.address
|
|
31
|
+
|
|
32
|
+
const brackets = await axios.get(`https://gotchi-battler-backend-blmom6tkla-ew.a.run.app/api/v1/tournaments/${tournamentId}/brackets`)
|
|
33
|
+
|
|
34
|
+
if (!brackets || !brackets.data || !brackets.data.length) {
|
|
35
|
+
console.error('Brackets not found')
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`Tournament data for id ${tournamentId} found ✅`)
|
|
40
|
+
console.log(`Validating "${tournament.data.name}"...`)
|
|
41
|
+
console.log('(This process can take up to 20 minutes for large tournaments)')
|
|
42
|
+
for (const bracket of brackets.data) {
|
|
43
|
+
console.log(`Validating "${bracket.name}"...`)
|
|
44
|
+
for (const round of bracket.rounds) {
|
|
45
|
+
for (const battle of round.battles) {
|
|
46
|
+
// If battle is a BYE then skip
|
|
47
|
+
if (!battle.team1Id || !battle.team2Id) {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get seed for the battle
|
|
52
|
+
const tournamentContract = getTournamentContract(onchainAddress)
|
|
53
|
+
const seed = await tournamentContract.roundSeeds(round.roundStage)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await validateBattle(battle.id, seed.toString(), tournamentConfig.gameLogicVersion)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof ValidationError) {
|
|
59
|
+
console.error(`Battle ${battle.id} failed validation ❌`)
|
|
60
|
+
console.error(`Seed: "${seed.toString()}"`)
|
|
61
|
+
|
|
62
|
+
// Write original logs to file
|
|
63
|
+
const originalLogsFilename = `${battle.id}-originalLogs.json`
|
|
64
|
+
fs.writeFileSync(path.join(__dirname, 'output', originalLogsFilename), JSON.stringify(error.originalLogs, null, '\t'))
|
|
65
|
+
|
|
66
|
+
// Write new logs to file
|
|
67
|
+
const newLogsFilename = `${battle.id}-newLogs.json`
|
|
68
|
+
fs.writeFileSync(path.join(__dirname, 'output', newLogsFilename), JSON.stringify(error.newLogs, null, '\t'))
|
|
69
|
+
|
|
70
|
+
console.error(`Original logs written to ${path.join(__dirname, 'output', originalLogsFilename)}`)
|
|
71
|
+
console.error(`New logs written to ${path.join(__dirname, 'output', newLogsFilename)}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
}
|
|
78
|
+
console.log(`Round ${round.roundStage} validated ✅`)
|
|
79
|
+
}
|
|
80
|
+
console.log(`"${bracket.name}" validated ✅`)
|
|
81
|
+
}
|
|
82
|
+
console.log(`"${tournament.data.name}" validated ✅`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = main
|
|
86
|
+
|
|
87
|
+
// node scripts/validateTournament.js 15
|
|
88
|
+
if (require.main === module) {
|
|
89
|
+
const tournamentId = process.argv[2]
|
|
90
|
+
|
|
91
|
+
main(tournamentId)
|
|
92
|
+
.then(() => {
|
|
93
|
+
console.log('Done')
|
|
94
|
+
process.exit(0)
|
|
95
|
+
})
|
|
96
|
+
.catch((error) => {
|
|
97
|
+
console.error(error)
|
|
98
|
+
|
|
99
|
+
process.exit(1)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const { ethers } = require('ethers')
|
|
2
|
+
const tournamentAbi = require('../constants/tournamentManagerAbi.json')
|
|
3
|
+
|
|
4
|
+
const getTournamentContract = (address) => {
|
|
5
|
+
const provider = new ethers.providers.JsonRpcProvider('https://polygon-rpc.com')
|
|
6
|
+
const contract = new ethers.Contract(address, tournamentAbi, provider)
|
|
7
|
+
|
|
8
|
+
return contract
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
getTournamentContract
|
|
13
|
+
}
|
package/utils/errors.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class GameError extends Error {
|
|
2
|
+
constructor(msg, logs) {
|
|
3
|
+
super(msg)
|
|
4
|
+
this.name = 'GameError'
|
|
5
|
+
|
|
6
|
+
if(logs) {
|
|
7
|
+
this.logs = logs
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class ValidationError extends Error {
|
|
13
|
+
constructor(msg, originalLogs, newLogs) {
|
|
14
|
+
super(msg)
|
|
15
|
+
this.name = 'ValidationError'
|
|
16
|
+
|
|
17
|
+
if(originalLogs) {
|
|
18
|
+
this.originalLogs = originalLogs
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if(newLogs) {
|
|
22
|
+
this.newLogs = newLogs
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
GameError,
|
|
29
|
+
ValidationError
|
|
30
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const logToInGameTeams = (originalLog) => {
|
|
2
|
+
// Deep copy the log to avoid modifying the original log
|
|
3
|
+
const log = JSON.parse(JSON.stringify(originalLog))
|
|
4
|
+
|
|
5
|
+
const teams = [];
|
|
6
|
+
|
|
7
|
+
[0, 1].forEach((teamIndex) => {
|
|
8
|
+
teams.push({
|
|
9
|
+
formation: {
|
|
10
|
+
front: log.layout.teams[teamIndex].rows[0].slots.map((slot) => {
|
|
11
|
+
if (slot.isActive) {
|
|
12
|
+
const gotchi = log.gotchis.find((gotchi) => gotchi.id === slot.id)
|
|
13
|
+
|
|
14
|
+
if (!gotchi) {
|
|
15
|
+
throw new Error(`Gotchi not found: ${slot.id}`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return gotchi
|
|
19
|
+
} else {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
}),
|
|
23
|
+
back: log.layout.teams[teamIndex].rows[1].slots.map((slot) => {
|
|
24
|
+
if (slot.isActive) {
|
|
25
|
+
const gotchi = log.gotchis.find((gotchi) => gotchi.id === slot.id)
|
|
26
|
+
|
|
27
|
+
if (!gotchi) {
|
|
28
|
+
throw new Error(`Gotchi not found: ${slot.id}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return gotchi
|
|
32
|
+
} else {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
leader: log.layout.teams[teamIndex].leaderId,
|
|
38
|
+
name: log.layout.teams[teamIndex].name,
|
|
39
|
+
owner: log.layout.teams[teamIndex].owner
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return teams
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
logToInGameTeams
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { ValidationError } = require('./errors')
|
|
2
|
+
|
|
3
|
+
const compareLogs = (originalLogs, newLogs) => {
|
|
4
|
+
// Check winner, loser and numOfTurns properties
|
|
5
|
+
if (originalLogs.result.winner !== newLogs.result.winner) {
|
|
6
|
+
throw new ValidationError(`Winner mismatch: ${originalLogs.result.winner} !== ${newLogs.result.winner}`, originalLogs, newLogs)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (originalLogs.result.loser !== newLogs.result.loser) {
|
|
10
|
+
throw new ValidationError(`Loser mismatch: ${originalLogs.result.loser} !== ${newLogs.result.loser}`, originalLogs, newLogs)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (originalLogs.result.numOfTurns !== newLogs.result.numOfTurns) {
|
|
14
|
+
throw new ValidationError(`numOfTurns mismatch: ${originalLogs.result.numOfTurns} !== ${newLogs.result.numOfTurns}`, originalLogs, newLogs)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Validate winningTeam array
|
|
18
|
+
originalLogs.result.winningTeam.forEach((gotchi) => {
|
|
19
|
+
// Check id, name and health properties
|
|
20
|
+
const gotchi2 = newLogs.result.winningTeam.find((gotchi2) => gotchi2.id === gotchi.id)
|
|
21
|
+
|
|
22
|
+
if (!gotchi2) {
|
|
23
|
+
throw new ValidationError(`Gotchi not found in winningTeam: ${gotchi.id}`, originalLogs, newLogs)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (gotchi.name !== gotchi2.name) {
|
|
27
|
+
throw new ValidationError(`Gotchi name mismatch: ${gotchi.name} !== ${gotchi2.name}`, originalLogs, newLogs)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (gotchi.health !== gotchi2.health) {
|
|
31
|
+
throw new ValidationError(`Gotchi health mismatch: ${gotchi.health} !== ${gotchi2.health}`, originalLogs, newLogs)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
compareLogs
|
|
40
|
+
}
|