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.
@@ -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
+ }
@@ -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
+ }