A tool for viewing Battle Logs in Splinterlands (Python, but no coding skills required)

View this thread on: d.buzz | hive.blog | peakd.com | ecency.com
·@kalkulus·
0.000 HBD
A tool for viewing Battle Logs in Splinterlands (Python, but no coding skills required)
![Image created with Bing image generator](https://files.peakd.com/file/peakd-hive/kalkulus/AKVVT4u2wddtfGJaChBSso3wNXwxUSUposA6Zw3X6Fw67rUoYK9eZhCZ8G8FaNL.png)

In this post I provide a Python program for viewing Splinterlands battle logs. Battle logs are useful for understanding what is going on in the battle. You can copy and run the code on your computer, or you can run it in an online python environment such a [Trinket.io](https://trinket.io/embed/python3). *Note, I have no prior experience with Trinket, I just found it to work when I searched for an online python environment for this post*


<center>![separator.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tmBVTkPE8GPjV9KGtgDkp1eB3TQ7yDVmYDYLjwAFvVvXBaKwoNL2yje7oAbW49gFCTM.png)</center>

# Output example

This code will convert the battle logs into a structured format that is easy to read. An example output looks like this:

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/23swiDwXPt48PiH3tNjRqyQiY97jSyi6VACFrRSyoQfhukhUaTX8GvybZxf8oXhvMrffz.png)

# Output structure

As you can see above, the output consists of 7 columns. The columns contain:
1. The round and action number
2. The action initiator (for example which units is performing an attack)
3. Which action/event is occurring
4. The action/event target
5. Damage/Healing value
6. The hit chance, if relevant
7. The RNG result, has to be less than the hit chance to be successful. 
<center>![separator.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tmBVTkPE8GPjV9KGtgDkp1eB3TQ7yDVmYDYLjwAFvVvXBaKwoNL2yje7oAbW49gFCTM.png)</center>
# Usage

If you want to use the code with [Trinket.io](https://trinket.io/embed/python3), just copy the entire code supplied below into the text field on the left side:

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tw9tret9g7tyAeEdzrsyfXUv2s9zUkV5GsY6yy2GTKeSCfL38ztUymnLtVNKuqkYLmD.png)

Scroll to the bottom and insert your battle id: 

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/244Kn7bCM6ts6KpRLsjVD8ZY9UTkz9jAJD6Ppvi5EAExsZiXPfYXPg4uvDMU4xk1yQSeg.png)

Then press *Run* at the top of the page:

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/EoKBgfwZGxxG4VVyxA59o3QRFW1tcjMpatz9RpsGS52hNsD8tRcUf1ayhZXyCKPzTrf.png)

You can also run the code offline. Python installations often contain the required libraries, so it should hopefully be straightforward to use. 
<center>![separator.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tmBVTkPE8GPjV9KGtgDkp1eB3TQ7yDVmYDYLjwAFvVvXBaKwoNL2yje7oAbW49gFCTM.png)</center>
# Python Code
```
from urllib.request import urlopen
from json import loads

class BattleLogParser:
	def __init__(self, battle_id):
		self.battle_id = battle_id
		self.url = f"https://api2.splinterlands.com/battle/result?id={battle_id}"
		with urlopen("https://api.splinterlands.io/cards/get_details") as response:
			self.carddata = loads(response.read())
		
		with urlopen(self.url) as response:
			self.data = loads(response.read())
			self.ruleset = self.data['ruleset']
			self.inactive= self.data['inactive']
			self.details = loads(self.data['details']) # Details is a string so we parse again
			self.team1 = self.details['team1']
			self.team2 = self.details['team2']
			self.team1_uids = [self.team1['summoner']['uid']]+[x['uid'] for x in self.team1['monsters']]
			self.team2_uids = [self.team2['summoner']['uid']]+[x['uid'] for x in self.team2['monsters']]
			self.team1_ids = [self.team1['summoner']['card_detail_id']]+[x['card_detail_id'] for x in self.team1['monsters']]
			self.team2_ids = [self.team2['summoner']['card_detail_id']]+[x['card_detail_id'] for x in self.team2['monsters']]
			self.player1 = self.data['player_1']
			self.player2 = self.data['player_2']
			self.pre_battle = self.details['pre_battle']
			
			self.names = {}
			for uid,c in zip(self.team1_uids, self.team1_ids):
				self.names[uid] = self.carddata[c-1]['name'] + " (blue)"
			for uid,c in zip(self.team2_uids, self.team2_ids):
				self.names[uid] = self.carddata[c-1]['name'] + " (red)"
			
			self.separator = "-"*124
			col1 = "Round"
			col2 = "Initiator"
			col3 = "Action" 
			col4 = "Target"
			col5 = "Value"
			col6 = "Hit chance"
			col7 = "RNG"
			self.columnNames = f"{col1:>7s} | {col2:>30s} | {col3:>16s} | {col4:>30s} | {col5} | {col6:>11s} | {col7:>5s}"

			self.printHeader()
			self.roundCount=1
			self.round = 1
			self.printPreBattle()
			rounds = self.details['rounds']
			
			for r in rounds:
				print(self.separator)
				print(self.columnNames)
				print(self.separator)
				self.printRound(r)
			
	def getRoundString(self):
		return f"{self.round:>3d}-{self.roundCount:<3d}"
		
	def getEmptyRoundString(self):
		return f"{'':>3s}-{'':>3s}"
	
	def printHeader(self):
		print(f"{self.separator}\nBattle {self.battle_id}, {self.player1} vs {self.player2}")
		print(f"Mana: {self.data['mana_cap']} Rules: {self.ruleset.replace('|', ' | ')}, Inactive splinters: {self.inactive}")
		print(self.separator)
		print(self.columnNames)
		print(self.separator)

	def printAction(self, action):
		keys = action.keys()
		if('initiator' in keys and "target" in keys):
			if("details" in keys):
				self.printActionWithInitiatorAndTargetAndDetails(action)
			else:
				self.printActionWithInitiatorAndTarget(action)
		elif('initiator' in keys and "group_state" in keys):
			self.printActionWithInitiatorAndGroupState(action)
		else:
			self.printActionWithoutInitiator(action)
	
	def printActionWithoutInitiator(self, a):
		if("damage" in a.keys()):
			print(f"{self.getRoundString()} | {'':>30s} | {a['type']:>16s} | {self.names[a['target']]:>30s} | {a['damage']:>5d} | {'':>11s} | {'':>5s}")
		else:
			print(f"{self.getRoundString()} | {'':>30s} | {a['type']:>16s} | {self.names[a['target']]:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")

	def printActionWithInitiatorAndTargetAndDetails(self, a):
		print(f"{self.getRoundString()} | {self.names[a['initiator']]:>30s} | {a['details']['name']:>16s} | {self.names[a['target']]:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")
		
	def printActionWithInitiatorAndTarget(self, a):
		# ~ print(list(a.keys()))
		if("damage" in a.keys()):
			if("hit_chance" in a.keys()):
				print(f"{self.getRoundString()} | {self.names[a['initiator']]:>30s} | {a['type']:>16s} | {self.names[a['target']]:>30s} | {a['damage']:>5d} | {a['hit_chance']:>11.2f} | {a['hit_val']:>5.3f}")
			else:
				print(f"{self.getRoundString()} | {self.names[a['initiator']]:>30s} | {a['type']:>16s} | {self.names[a['target']]:>30s} | {a['damage']:>5d} | {'':>11s} | {'':>5s}")
		else:
			print(f"{self.getRoundString()} | {self.names[a['initiator']]:>30s} | {a['type']:>16s} | {self.names[a['target']]:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")
		
	def printActionWithInitiatorAndGroupState(self, a):
		targets = [self.names[x['monster']] for x in a['group_state']]
		name = a['details']['name']
		tmp = ""
		if(name == "Summoner"):
			if("stats" in a['details'].keys()):
				stats = a['details']['stats']
				if(stats):
					v = list(stats.values())[0]
					sum_buff_name = f"{v:+} {list(stats.keys())[0]}"
					if(v > 0):
						targets = [self.names[x] for x in (self.team1_uids if a['initiator'] == self.team1_uids[0] else self.team2_uids)[1:]]
					else:
						targets = [self.names[x] for x in (self.team2_uids if a['initiator'] == self.team1_uids[0] else self.team1_uids)[1:]]
					print(f"{self.getRoundString()} | {self.names[a['initiator']]:>30s} | {sum_buff_name:>16s} | {targets[0]:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")
					for t in targets:
						print(f"{self.getEmptyRoundString()} | {'':>30s} | {'':>16s} | {t:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")
			if("ability" in a['details'].keys()):
				ability = a['details']['ability']
				targets = [self.names[x] for x in (self.team1_uids if a['initiator'] == self.team1_uids[0] else self.team2_uids)[1:]]
				print(f"{self.getRoundString()} | {self.names[a['initiator']]:>30s} | {ability:>16s} | {targets[0]:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")
				for t in targets:
					print(f"{self.getEmptyRoundString()} | {'':>30s} | {'':>16s} | {t:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")
		else:
			Type = a['type']
			if(Type in ("buff", "halving")):
				print(f"{self.getRoundString()} | {self.names[a['initiator']]:>30s} | {a['details']['name']:>16s} | {targets[0]:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")
				if(len(targets)>1):
					for t in targets[1:]:
						print(f"{self.getEmptyRoundString()} | {'':>30s} | {'':>16s} | {t:>30s} | {'':>5s} | {'':>11s} | {'':>5s}")
			elif(Type == "remove_buff"):
				remove_string = ('remove '+a['details']['name'])[:16]
				print(f"{self.getRoundString()} | {self.names[a['initiator']]:>30s} | {remove_string:>16s} | {'':>30s} | {'':>5s} | {'':>11s} | {'':>5s}")

			else:
				print("Unhandled:", a)
				input()
	
	def printPreBattle(self):
		self.roundCount = 1
		for a in self.pre_battle:
			self.printAction(a)
			self.roundCount += 1
			
	def printRound(self, Round):
		actions = Round['actions']
		self.round = Round['num']
		for ia, a in enumerate(actions):
			self.roundCount = ia
			self.printAction(a)

if __name__ == "__main__":
	# Can run with https://trinket.io/embed/python3
	BattleLogParser("sl_e27d4cc557498a74e15dd9a7b028753b")

```

<center>![separator.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tmBVTkPE8GPjV9KGtgDkp1eB3TQ7yDVmYDYLjwAFvVvXBaKwoNL2yje7oAbW49gFCTM.png)</center>

# Code explanation

*Note, there are likely many ways to optimize this code, and there may be bugs. Please leave a comment if you encounter a problem.*

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/EoAh3TNvcPACcPn4tAxpV7p4mFMMGDpm2DneJNKbia81Rz2DxqjnM8QQgH2K6rCiXEK.png)

The first part of the code contains import statements for two functions we need for downloading data from the Splinterlands API. We use *urlopen* and *loads* from the *urllib.request* and *json* libraries. 

The next part is the definition of the BattleLogParser class. The *__ init __* function is the initializer, which is called when we create a BattleLogParser object. We do multiple things in the initializer. 
- Download splinterlands card information. We need this to convert from unique card identifiers / ids to card names. The card information is stored in the *carddata* attribute.
- Download the battle log, store it in the *data* attribute, and extract+store various information from the battle log. This includes info about the ruleset, inactive splinters, teams etc. We also parse the unique card identifiers and card ids into lists for convenience. 

Next, we define the names of the cards. The names are stored in a dictionary where the key is the unique card id, and the value is the name:
![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/48JfFh67DRXzpRdK95ZXvNaKU42GHQtzu7WhdgdGdpPAxu7GQC3QMeibgJ2ymfh3rw.png)

Then we get to actually parsing the log. First I set up the column names, and then call the functions to print the actions. The battle rounds are stored in the details -> rounds field in the downloaded data. We store that in the *rounds* attribute, and then loop over that and call the *printRound* function along with a round header to make it look nice.  
![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tSzWX9gqBYp6peVs2pUs4tRjdVPeLSun8a3GjBiPGzBgRgTYXgXQpY2ceFJq1fwsZqP.png)

Then follows the definitions of a lot of methods for the class to use while printing the battle events:

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tw9kEoZ9PGbprun7ma7SExnxrLZgJRWoSVhReBynHn72NqTcjVXj8UeqEzKKKhsQwaQ.png)

These three methods are
1. A method that returns a string with the round and action number
2. A method that results an empty string with the same width as the round string
3. A method that prints the header of the log, with information about the players and match configuration, and then the column name string


![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tmm5sE3PC1rC1kNdPxhKet7MQ1CtGYxHEF7TVqd74VYf21b7ZHNg2P9ocsMDsT361fX.png)

Then follows a function that is called a bunch of times (image above). It takes an action, and the looks for the keys *initiator*, *target*, and possibly *group state*. The combination of these terms lets us identify which type of action it is. Depending on the type, we call different printing methods that parses the action correctly. The four methods for parsing the actions are somewhat ugly, but the purpose is simply to fill in the column fields we discussed earlier.

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/EoCpNqtJuqu3phDnuEZoAhrb2Ld5mR2pCMLEuJzEFDAPD1CijCa7i1Xzak7TwqL7cHA.png)


Finally, there are the two function *printPrebattle* and *printRound*. These do what their names suggest. The *printPrebattle* method looks over the events that occur in the phase where monsters apply their buffs/debuffs and possibly ambush events. The *printRound* method takes a round as argument, and then loops over the actions in the rounds and prints them. 

The very last part of the code looks like this:

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/23wN2KL3QXB4ZmS4jLD7cJ3Hitxw6k2Mz7gx2uTNY3ySevu2dLL8hU2LGcT1G9Pym87Px.png)

The first line here makes sure that we can import the BattleLogParser class into another python script without creating a BattleLogParser object. 

In the last line, we create an object for a splinterlands battle. The *"sl_..."* string is the battle id, which you can find in the url of a splinterlands battle. Simply copy everything after *id=* in the url:

![image.png](https://files.peakd.com/file/peakd-hive/kalkulus/23t7BMAtbczPEMUYYq91w6NgvGnPckHiQo9QLeGb1qKqhMHK6Rz2att46mo3H95822TU6.png)

This class works like a function. When we initialize it, it automatically prints everything. 

<center>![separator.png](https://files.peakd.com/file/peakd-hive/kalkulus/23tmBVTkPE8GPjV9KGtgDkp1eB3TQ7yDVmYDYLjwAFvVvXBaKwoNL2yje7oAbW49gFCTM.png)</center>


# Final words
I hope you found this post interesting, and that you find the battle log parser useful. If you have not yet joined Splinterlands please click the referral link below to get started.

[Join Splinterlands](https://splinterlands.com/?ref=kalkulus)

Best wishes
@Kalkulus 
👍 , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ,