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

in Splinterlands2 months ago (edited)

Image created with Bing image generator

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

separator.png

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

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.
    separator.png

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:

image.png

Scroll to the bottom and insert your battle id:

image.png

Then press Run at the top of the page:

image.png

You can also run the code offline. Python installations often contain the required libraries, so it should hopefully be straightforward to use.

separator.png

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")

separator.png

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

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

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

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

image.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

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

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

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

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

separator.png

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

Best wishes
@Kalkulus

Sort:  

This post has been supported by @Splinterboost with a 20% upvote! Delagate HP to Splinterboost to Earn Daily HIVE rewards for supporting the @Splinterlands community!

Delegate HP | Join Discord

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


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!