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. 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
Output example
This code will convert the battle logs into a structured format that is easy to read. An example output looks like this:
Output structure
As you can see above, the output consists of 7 columns. The columns contain:
- The round and action number
- The action initiator (for example which units is performing an attack)
- Which action/event is occurring
- The action/event target
- Damage/Healing value
- The hit chance, if relevant
- The RNG result, has to be less than the hit chance to be successful.
Usage
If you want to use the code with Trinket.io, just copy the entire code supplied below into the text field on the left side:
Scroll to the bottom and insert your battle id:
Then press Run at the top of the page:
You can also run the code offline. Python installations often contain the required libraries, so it should hopefully be straightforward to use.
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")
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.
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:
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.
Then follows the definitions of a lot of methods for the class to use while printing the battle events:
These three methods are
- A method that returns a string with the round and action number
- A method that results an empty string with the same width as the round string
- A method that prints the header of the log, with information about the players and match configuration, and then the column name string
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.
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:
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:
This class works like a function. When we initialize it, it automatically prints everything.
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.
Best wishes
@Kalkulus
Thanks for that, it will be interesting to test a few things.
Thank you for your witness vote!
Have a !BEER on me!
To Opt-Out of my witness beer program just comment STOP below
View or trade
BEER
.Hey @ifhy, here is a little bit of
BEER
from @isnochys for you. Enjoy it!Learn how to earn FREE BEER each day by staking your
BEER
.Cheers! I hope its useful.
Thanks for sharing! - @mango-juice
Hey, I remembered I used to know some simple excel formulas like gather and add bunch of ratings on school subjects and make a column for the average, so then another to say like "if above > this or below < this" it would be approved or not and display by color or throw to charts (not necessary), in this case sounds like it could be applied similarly to display if its a hit or a miss.
But anyway, don't take the work for it because Im saying... Thats indeed too nerd. Im sure you do that to exercise and improve your own knowledge and code skills. Nice work!