acutesoftware/virtual-AI-simulator

View on GitHub
vais/battle.py

Summary

Maintainability
F
4 days
Test Coverage
# battle.py   
import os
import random  

import character

rules_file = os.getcwd() + os.sep + 'data' + os.sep + 'battle.rules'

class BattleRules(object):
    """
    Manages the parsing of rules for a battle by reading the .rules file
    """
    def __init__(self, rules_file):
        self.rules_file = rules_file
        self.all_rules = {}
        with open(rules_file, 'r') as f:
            for line in f:
                if line[0] != '#' and line.strip() != '' and line:
                    line_parts = line.split('=')
                    self.all_rules[line_parts[0].strip(' ')] = line_parts[1].strip(' ').strip('\n')
                    
    def __str__(self):
        res  = 'Battle Rules : ' + self.rules_file + '\n'
        for k, v in self.all_rules.items(): 
            res += 'Rule ' + k + ':' + v + '\n'
        return res
    

    
class BattleSimulator(object):
    """
    class to handle multiple simulation runs of Battles 
    between characters
    """
    def __init__(self, c1, c2, traits, rules, num_fights = 1000):
        self.c1 = c1
        self.c2 = c2
        self.num_fights = num_fights
        self.num_c1 = 0
        self.num_c2 = 0
        self.traits = traits
        self.rules = rules
        self.winner = 'No battles have been done'
        self.run_simulation()
    
    def __str__(self):
        res = 'After ' + str(self.num_fights) + ' fights ' + self.winner + ' wins!\n'
        res += self.c1.name + ' = ' + str(self.num_c1) + ' (' + str(round(self.num_c1*100/self.num_fights)) + '%)\n'
        res += self.c2.name + ' = ' + str(self.num_c2) + ' (' + str(round(self.num_c2*100/self.num_fights)) + '%)\n'
        
        return res
     
    def run_simulation(self):
        """
        runs the simulation
        """
        for _ in range(self.num_fights):
            # restore health between each fight
            self.c1.stats['Health'] = self.c1.stats['max_health']
            self.c2.stats['Health'] = self.c2.stats['max_health']
            
            # run the Battles
            b = Battle(self.c1, self.c2, self.traits, self.rules, print_console='No')
            #print(b)
            if b.status == self.c1.name:
                self.num_c1 += 1
            else:
                self.num_c2 += 1
                
        # tag winner        
        if self.num_c1 > self.num_c2:
            self.winner = self.c1.name
        else:
            self.winner = self.c2.name
    
class Battle(object):
    """
    manages a fight between 2 rpg characters
    """
    def __init__(self, char1, char2, traits, rules, print_console='Yes'):
        self.c1 = char1
        self.c2 = char2
        self.log = []
        self.traits = traits
        self.rules = rules
        self.status = self.fight(100, print_console)
        
    def __str__(self):
        res  = 'Battle Status : ' + self.status + ' Wins\n'
        res += 'Character 1 =  ' + self.c1.name + '\n'
        res += 'Character 2 =  ' + self.c2.name + '\n'
        return res
        
    
    def fight(self, moves, print_console):
        """
        runs a series of fights - TODO switch order 
        of who attacks first, as this has an effect 
        on win rate over 1000 fights
        """
        for _ in range(1, moves):
            #if i == 1 and random.randint(1,100) > 50:   # randomly choose who moves first
            # player 1
            result, dmg = self.calc_move(self.c1)
            self.show_message(self.c1, self.c2, result, dmg, print_console)
            self.take_damage(self.c2, dmg)
            if self.is_character_dead(self.c2):
                #print(self.c2.name + ' has died')
                return self.c1.name
                
            # player 2
            result, dmg = self.calc_move(self.c2)
            self.show_message(self.c2, self.c1, result, dmg, print_console)
            self.take_damage(self.c1, dmg)
            if self.is_character_dead(self.c1):
                #print(self.c1.name + ' has died')
                return self.c2.name
    
    def take_damage(self, c, dmg):
        """
        wrapper to apply damage taken to a character
        """
        if c.name == self.c1.name:
            self.c1.stats['Health'] = self.c1.stats['Health'] - dmg
        else:
            self.c2.stats['Health'] = self.c2.stats['Health'] - dmg
        
    
    def show_message(self, c_attack, c_defend, result, dmg, print_console='Yes'):
        """
        function to wrap the display of the battle messages
        """
        perc_health_att = '[' + str(round((c_attack.stats['Health']*100) / c_attack.stats['max_health'] )) + '%]'
        perc_health_def = '[' + str(round((c_defend.stats['Health']*100) / c_defend.stats['max_health'] )) + '%]'
        if result == 'Miss':
            txt = c_attack.name + ' ' + perc_health_att.rjust(6) + ' miss ' + c_defend.name + ' ' + perc_health_def.rjust(6)
        elif result == 'Crit':
            txt = c_attack.name + ' ' + perc_health_att.rjust(6) + ' CRIT ' + c_defend.name + ' ' + perc_health_def.rjust(6)
            txt +=  ' for ' + str(dmg)   
        else:
            txt = c_attack.name + ' ' + perc_health_att.rjust(6) + ' hits ' + c_defend.name + ' ' + perc_health_def.rjust(6) 
            txt +=  ' for ' + str(dmg)   
        
        if print_console == 'Yes':
            print(txt)
            
    def calc_move(self, c):
        """
        calculate the amount of damage down using the battle.rules file
        WARNING - shitty eval functions - careful with user input.
        TODO = max_hit = parser.parse(self.rules['hit_max']).to_pyfunc()
        """
        
        # first get local variables the same as battle.rules file so parser can interpret.
        AGI = c.stats['AGI']
        INT = c.stats['INT']
        STR = c.stats['STR']
        #STA = c.stats['STA']
        #print('calc move : self.rules.all_rules[hit_min] = ', self.rules.all_rules['hit_min'])
        #print('calc move : self.rules.all_rules[hit_max] = ', self.rules.all_rules['hit_max'])
        #print('calc move :  = self.rules.all_rules[hit_limit]', self.rules.all_rules['hit_limit'])
        hit_min   = float(self.rules.all_rules['hit_min'])
        
        hit_AGI_mult   = float(self.rules.all_rules['hit_AGI_mult'])
        hit_INT_mult   = float(self.rules.all_rules['hit_INT_mult'])
        hit_AGI_add   = float(self.rules.all_rules['hit_AGI_add'])
        hit_INT_add   = float(self.rules.all_rules['hit_INT_add'])
        hit_overall_add   = float(self.rules.all_rules['hit_overall_add'])
        
        #  hit_max = round(INT/2) + round(AGI/2) + 1
        hit_max = round(INT*(hit_INT_mult + hit_INT_add)) + round(AGI*(hit_AGI_mult + hit_AGI_add)) + hit_overall_add
        
        hit_limit = float(self.rules.all_rules['hit_limit']) 
        if hit_max > hit_limit:
            hit_max = hit_limit
        chance_hit = random.randint(hit_min,hit_max)
        
        dmg_min   = float(self.rules.all_rules['dmg_min'])
        #dmg_max   = eval(self.rules.all_rules['dmg_max'])
        calc_agi = round((AGI + float(self.rules.all_rules['dmg_AGI_add'])) * float(self.rules.all_rules['dmg_AGI_mult']))
        calc_int = round((INT + float(self.rules.all_rules['dmg_INT_add'])) * float(self.rules.all_rules['dmg_INT_mult']))
        calc_str = round((STR + float(self.rules.all_rules['dmg_STR_add'])) * float(self.rules.all_rules['dmg_STR_mult'])) 
        #print('TESTING CALC_MOVE : calc_agi', calc_agi, 'calc_int',calc_int ,'calc_str',calc_str )
        dmg_max = round((calc_agi + calc_int + calc_str) * float(self.rules.all_rules['dmg_overall_mult']) + float(self.rules.all_rules['dmg_overall_add']))

        #print('hit_min  =',hit_min  , 'hit_max = ',hit_max  )
        #print('dmg_min  =',dmg_min  , 'dmg_max = ',dmg_max  )
        amount_dmg = random.randint(dmg_min, dmg_max)
        
        if chance_hit > float(self.rules.all_rules['shot_crit_greater_than']):
            return 'Crit', amount_dmg * float(self.rules.all_rules['dmg_mult_crit'])
        elif chance_hit < float(self.rules.all_rules['shot_hit_greater_than']):
            return 'Miss', amount_dmg * float(self.rules.all_rules['dmg_mult_miss'])
        else:
            return 'Hit', amount_dmg * float(self.rules.all_rules['dmg_mult_hit'])
            
    def is_character_dead(self, c):
        """
        check to see if a character is dead
        """
        if c.stats['Health'] < 1:
            return True
        else:
            return False