93d98e04 by Barry

Finally have AI working. Started to add feral skills.

1 parent 840d3f95
No preview for this file type
......@@ -4,6 +4,7 @@ import pickle
import math
import collections
import logging
import operator
def log(message):
try:
......@@ -134,15 +135,20 @@ PhysicalDamageTypes = {
}
FeralSkills = {
'Airy Shield': {'fp_cost': 0, 'type': 'Enhancing', 'sub_type': 'arrow shield', 'shadows': 'ignore', 'range': None, 'aoe': False, 'spell': 'arrow shield'},
'Binding Wave': {'type': 'Enfeebling', 'shadows': 'ignore', 'range': 15, 'aoe': True, 'spell': 'bind'},
'Dire Straight': {'type': 'Physical', 'shadows': 'wipe', 'range': None, 'aoe': False},
'Dismemberment': {'type': 'Piercing', 'shadows': 'absorb', 'range': None, 'aoe': False}, # causes the monster to lose a body part
'Earthshatter': {'type': 'Piercing', 'shadows': 'wipe', 'range': None, 'aoe': True},
'Eyes On Me': {'type': 'Magical', 'sub_type': 'Dark', 'mp_cost': 112, 'shadows': 'absorb', 'range': 13, 'aoe': False},
'Hypnosis': {'type': 'Enfeebling', 'sub_type': 'sleep', 'shadows': 'ignore', 'range': 'gaze', 'aoe': True},
'Magic Barrier': {'type': 'Magical', 'sub_type': 'Dark', 'mp_cost': 29, 'shadows': 'absorb', 'range': None, 'aoe': True},
'Sinker Drill': {'type': 'Physical', 'sub_type': 'Piercing', 'shadows': 'absorb', 'range': None, 'aoe': False},
# 'Airy Shield': {'fp_cost': 0, 'type': 'Enhancing', 'sub_type': 'arrow shield', 'shadows': 'ignore', 'range': None, 'aoe': False, 'spell': 'arrow shield'},
# 'Binding Wave': {'type': 'Enfeebling', 'shadows': 'ignore', 'range': 15, 'aoe': True, 'spell': 'bind'},
# 'Dire Straight': {'type': 'Physical', 'shadows': 'wipe', 'range': None, 'aoe': False},
# 'Dismemberment': {'type': 'Piercing', 'shadows': 'absorb', 'range': None, 'aoe': False}, # causes the monster to lose a body part
# 'Earthshatter': {'type': 'Piercing', 'shadows': 'wipe', 'range': None, 'aoe': True},
# 'Eyes On Me': {'type': 'Magical', 'sub_type': 'Dark', 'mp_cost': 112, 'shadows': 'absorb', 'range': 13, 'aoe': False},
# 'Hypnosis': {'type': 'Enfeebling', 'sub_type': 'sleep', 'shadows': 'ignore', 'range': 'gaze', 'aoe': True},
# 'Magic Barrier': {'type': 'Magical', 'sub_type': 'Dark', 'mp_cost': 29, 'shadows': 'absorb', 'range': None, 'aoe': True},
# 'Sinker Drill': {'type': 'Physical', 'sub_type': 'Piercing', 'shadows': 'absorb', 'range': None, 'aoe': False},
'Accuracy +15%': {'fp_cost': 23, 'effect': 'acc', 'amount': 15, 'is_percent': True, 'expires_seconds': 0, 'levels': False},
'Accuracy +30%': {'fp_cost': 30, 'effect': 'acc', 'amount': 30, 'is_percent': True, 'expires_seconds': 0, 'levels': False},
'Accuracy Bonus': {'fp_cost': 30, 'effect': 'acc', 'amount': 15, 'is_percent': True, 'expires_seconds': 0, 'levels': True},
'AGI +25': {'fp_cost': 17, 'effect': 'agi', 'amount': 25, 'is_percent': False, 'expires_seconds': 0, 'levels': False},
}
class Spells:
......@@ -193,15 +199,17 @@ class Spells:
'Banish III': {'mp_cost': 96, 'casting_time': 3, 'recast_time': 45, 'magic_acc_stat': 'mnd', 'spell_type': 't3', 'damage_type': 'light', 'jobs': {Jobs.WHM: 65} },
}
def get_spell_list(self, level, job = None, sub_job = None):
def get_spell_list(self, mp, level, job = None, support_job = None):
available_spells = []
for spell_name, spell in self.SpellList.iteritems():
if job and job in spell['jobs'] and spell['jobs'][job] <= level:
if spell_name not in available_spells:
available_spells.append(spell_name)
if sub_job and sub_job in spell['jobs'] and spell['jobs'][sub_job] <= level / 2:
if spell['mp_cost'] < mp:
available_spells.append(spell_name)
if support_job and support_job in spell['jobs'] and spell['jobs'][support_job] <= level / 2:
if spell_name not in available_spells:
available_spells.append(spell_name)
if spell['mp_cost'] < mp:
available_spells.append(spell_name)
return sorted(available_spells)
def get_spell_data(self, spell):
......@@ -224,8 +232,9 @@ Families = {
'max_fp': 55,
'available_main_job': [Jobs.WAR, Jobs.DRG, Jobs.DRK, Jobs.PLD],
'available_support_job': [Jobs.WAR, Jobs.DRG, Jobs.DRK, Jobs.PLD],
'innate_feral_skills': ['Sinker Drill', 'Dire Straight', 'Dismemberment', 'Earthshatter'],
'innate_feral_skills': [],#'Sinker Drill', 'Dire Straight', 'Dismemberment', 'Earthshatter'],
'target_magic_damage_adjustment': 1.00,
'feral_skills_conversion': ['Accuracy +15%'],
'type': 'Arcana',
'strong_vs': ['Dark'],
'charmable': False,
......@@ -272,6 +281,7 @@ Families = {
'available_support_job': [Jobs.BLM, Jobs.RDM, Jobs.WAR],
'innate_feral_skills': ['Binding Wave', 'Magic Barrier', 'Hypnosis', 'Eyes On Me', 'Airy Shield'],
'type': 'Demon',
'feral_skills_conversion': ['Accuracy +15%'],
'traits': ['magic defence bonus +25%'],
'target_magic_damage_adjustment': 0.75,
'charmable': False,
......@@ -316,6 +326,7 @@ Monsters = {
'Mechanical Menace': {
'family': 'acrolith',
'zone': ['abyssea - uleguerand'],
'feral_skills_conversion': [],
'hp': 20,
'mp': 20,
'weapon_base_damage': 18
......@@ -323,95 +334,108 @@ Monsters = {
'Floating Eye': {
'family': 'ahriman',
'zone': ['ranguemont pass'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15,
'weapon_base_damage': 5,
'image_url': 'http://vignette1.wikia.nocookie.net/ffxi/images/9/94/Floating_Eye.JPG/revision/latest?cb=20070704142439'
},
'Bat Eye': {
'family': 'ahriman',
'zone': ['ranguemont pass', 'beaucedine glacier'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15,
'weapon_base_damage': 5,
'image_url': 'http://vignette3.wikia.nocookie.net/ffxi/images/4/40/Bat_Eye.jpg/revision/latest?cb=20060909124134',
},
'Evil Eye': {
'family': 'ahriman',
'zone': ['castle zvahl baileys', 'castle zvahl keep', 'xarcabard'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15,
'weapon_base_damage': 5,
},
'Morbid Eye': {
'family': 'ahriman',
'zone': ['castle zvahl baileys', 'castle zvahl keep'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Deadly Iris': {
'family': 'ahriman',
'zone': ['castle zvahl keep'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Ahriman': {
'family': 'ahriman',
'zone': ['castle zvahl baileys'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Fachan': {
'family': 'ahriman',
'zone': ['uleguerand range'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Gawper': {
'family': 'ahriman',
'zone': ['beaucedine glacier (s)'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Menacing Eye': {
'family': 'ahriman',
'zone': ['xarcabard (s)'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Ogler': {
'family': 'ahriman',
'zone': ['castle zvahl keep (s)', 'castle zvahl baileys (s)'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Smolenkos': {
'family': 'ahriman',
'zone': ['uleguerand range'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Doom Lens': {
'family': 'ahriman',
'zone': ['castle zvahl keep (s)', 'castle zvahl baileys (s)'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
},
'Scowlenkos': {
'family': 'ahriman',
'zone': ['uleguerand range'],
'feral_skills_conversion': [],
'hp': 15,
'mp': 24,
'weapon_base_damage': 15
'weapon_base_damage': 5
}
}
......@@ -477,13 +501,14 @@ class Pankration:
while support_job == main_job:
support_job = random.choice(family['available_support_job'])
feral_skills = []
skills = random.sample(family['innate_feral_skills'], 3)
if random.randint(1, 100) < SKILL_PERCENT:
feral_skills.append(skills[0])
if random.randint(1, 100) < SKILL_PERCENT:
feral_skills.append(skills[1])
if random.randint(1, 100) < SKILL_PERCENT:
feral_skills.append(skills[2])
# TODO: Add support back in for innate feral skills
# skills = random.sample(family['innate_feral_skills'], 3)
# if random.randint(1, 100) < SKILL_PERCENT:
# feral_skills.append(skills[0])
# if random.randint(1, 100) < SKILL_PERCENT:
# feral_skills.append(skills[1])
# if random.randint(1, 100) < SKILL_PERCENT:
# feral_skills.append(skills[2])
dicipline_level = 1
monster = Monster(monster_name, family_name, family, hp, mp, level,
......@@ -556,7 +581,7 @@ class Monster:
self.mp = self.get_mp()
self.wins = 0
self.losses = 0
self.battle_memory = []
self.battle_memory = {}
def __str__(self):
try:
......@@ -608,15 +633,18 @@ class Monster:
def add_xp(self, exp_to_add):
self.exp += exp_to_add
original_level = self.level
for i in range(self.level, 51):
if self.exp >= 200 * i:
if i > self.level:
# We leveled up!
self.level = i
return (True, "Congratulations your monster leveled up!\n\nStart level: {} New Level: {}".format(self.level, i))
else:
break
return (False,None)
if original_level > self.level:
return (True, "Congratulations your monster leveled up!\n\nStart level: {} New Level: {}".format(original_level, self.level))
else:
return (False,None)
def get_current_posture(self):
return TemperamentPosture[self.temperament_posture]
......@@ -695,12 +723,16 @@ class Monster:
# Get the stat for the spell and base magic accuracy for that spells MA
magic_acc_stat = spell_data['magic_acc_stat']
if magic_acc_stat == 'int':
print("DSTAT - INT")
dSTAT = self.get_intelligence() - enemy.get_intelligence()
elif magic_acc_stat == 'mnd':
print("DSTAT - INT")
dSTAT = self.get_mind() - enemy.get_mind()
elif magic_acc_stat == 'chr':
print("DSTAT - INT")
dSTAT = self.get_charisma() - enemy.get_charisma()
elif magic_acc_stat == 'agi':
print("DSTAT - INT")
dSTAT = self.get_agility() - enemy.get_agility()
magic_hit_rate = 50 - 0.5 * (enemy.get_magic_evasion(spell_data['damage_type']) - dSTAT)
......@@ -709,6 +741,8 @@ class Monster:
magic_hit_rate = 5
elif magic_hit_rate > 95:
magic_hit_rate = 95
print("DSTAT {}\nMagic Evasion: {}\nMagic hit rate: {}".format(dSTAT, enemy.get_magic_evasion(spell_data['damage_type']), magic_hit_rate))
return magic_hit_rate
def get_pdif(self, base_damage, defense, attack_type, level_difference):
......@@ -905,13 +939,48 @@ class Monster:
# Step 0: Am I already casting a spell? Is it time to calc damage from that?
# Step 1: Is magic possible?
spell_list = Spells().get_spell_list(self.level, job=self.main_job, sub_job=self.sub_job)
# Step 2: Do i have a memory of this monster from past battles? Does it have any elemental weakness?
spell_list = Spells().get_spell_list(self.mp, self.level, job=self.main_job, support_job=self.support_job)
best_spell_name = None
if spell_list:
if monster.family_name in self.battle_memory:
battles = self.battle_memory[monster.family_name]
# this assumes spells are: 'spells':{'Fire': 100, 'Stone II': 500}
if 'spells' in battles:
# if i have tried all spells then i know the right spell, otherwise try
# one of the ones that i don't know
if len(battles['spells']) == len(spell_list):
# get the sorted spell list by best average damage against this monster.
for i in max(battles['spells'].iteritems(), key=operator.itemgetter(1)):
best_spell_name = i
break
else:
untried_spells = []
for i in spell_list:
if i not in battles['spells']:
untried_spells.append(i)
best_spell_name = random.choice(untried_spells)
else:
best_spell_name = random.choice(spell_list)
else:
best_spell_name = random.choice(spell_list)
# Step 3: What status effects is the monster currently under? What are the effects(DOTS) we can apply?
spell_damage = 0
if best_spell_name:
print("BEST SPELL NAME: {}".format(best_spell_name))
spell_damage = self.get_magic_base_damage(best_spell_name, monster)
# TODO: Make this follow the behavior in temprament and select the right behavior.
if self.tp >= 1000:
physical_damage = self.get_physical_ws_damage(monster.get_base_defense(), 'melee', [1.0, 1.5, 4.0], [], monster.level - self.level, monster.get_vitality())
else:
physical_damage = self.get_physical_base_damage(monster.get_base_defense(), 'melee', monster.level - self.level, monster.get_vitality())
# find out the avg damage from each type of attack and sort them
return (best_spell_name, spell_damage, physical_damage)
# Step 4: find out the avg damage from each type of attack and sort them
# find out if we need to do defensive actions given the amount of life left in the enemy vs how much damage it does.
# We should also store how hard the AI has hit previously and adjust accordingly (Store previous average damage per hit). IE project how much damage we will
......@@ -934,38 +1003,45 @@ class Monster:
resisted = False
damage = 0
self.get_best_action(monster)
######
# TODO: Build some type of AI / testing to find the most powerful attack and use that (magic, melee, or ranged)
######
spell = 'Fire'
if self.is_a_magic_hit(spell, monster):
damage = self.get_magic_base_damage(spell, monster)
if damage == 0:
resisted = True
log("MAGIC DAMAGE: {}".format(damage))
is_a_hit = True
spell_data = Spells().get_spell_data(spell)
self.mp -= spell_data['mp_cost']
if self.mp < 0:
self.mp = 0
return AttackResponse(is_a_hit, damage, is_a_crit, spell, resisted)
if self.is_a_hit(monster):
# TODO: Make this follow the behavior in temprament and select the right behavior.
if self.tp >= 1000:
damage = self.get_physical_ws_damage(monster.get_base_defense(), 'melee', [1.0, 1.5, 4.0], [], monster.level - self.level, monster.get_vitality())
log("WS DAMAGE: {}".format(damage))
self.tp = 0
# intialize the monster memory for this family name if it isn't already set.
if monster.family_name not in self.battle_memory:
self.battle_memory[monster.family_name] = {'spells': {}}
action_result = self.get_best_action(monster)
print("AI RESULTS: {}".format(action_result))
# TODO: Add a nice response object from the action choice to make this code pretty. (no tuple)
if action_result[1] > action_result[2]:
spell = action_result[0]
if self.is_a_magic_hit(spell, monster):
damage = self.get_magic_base_damage(spell, monster)
if damage == 0:
resisted = True
log("MAGIC DAMAGE: {}".format(damage))
is_a_hit = True
spell_data = Spells().get_spell_data(spell)
self.mp -= spell_data['mp_cost']
if self.mp < 0:
self.mp = 0
# if the spell already exists then average the two damage amounts.
if spell in self.battle_memory[monster.family_name]['spells']:
self.battle_memory[monster.family_name]['spells'][spell] = damage + (self.battle_memory[monster.family_name]['spells'][spell]) / 2
else:
damage = self.get_physical_base_damage(monster.get_base_defense(), 'melee', monster.level - self.level, monster.get_vitality())
print("STD DAMAGE: {}".format(damage))
self.tp += 64
is_a_hit = True
# Returns the amount of damage and if it is a crit
return AttackResponse(is_a_hit, damage, is_a_crit, spell, resisted)
self.battle_memory[monster.family_name]['spells'][spell] = damage
return AttackResponse(is_a_hit, damage, is_a_crit, spell, resisted)
else:
if self.is_a_hit(monster):
# TODO: Make this follow the behavior in temprament and select the right behavior.
if self.tp >= 1000:
damage = self.get_physical_ws_damage(monster.get_base_defense(), 'melee', [1.0, 1.5, 4.0], [], monster.level - self.level, monster.get_vitality())
log("WS DAMAGE: {}".format(damage))
self.tp = 0
else:
damage = self.get_physical_base_damage(monster.get_base_defense(), 'melee', monster.level - self.level, monster.get_vitality())
print("STD DAMAGE: {}".format(damage))
self.tp += 64
is_a_hit = True
# Returns the amount of damage and if it is a crit
return AttackResponse(is_a_hit, damage, is_a_crit, spell, resisted)
def is_a_magic_hit(self, spell, monster):
hit_rate = self.get_magic_hit_rate(spell, monster)
......@@ -1135,7 +1211,7 @@ class Arena:
if __name__ == "__main__":
#print Families
print("Spell List: \n{}\n".format(Spells().get_spell_list(20, job=Jobs.BLM, sub_job=Jobs.WHM)))
print("Spell List: \n{}\n".format(Spells().get_spell_list(100, 10, job=Jobs.RDM, support_job=Jobs.WAR)))
hunt_response = None
p = Pankration()
print("Zones: \n\n{}".format('\n'.join(p.list_zones())))
......@@ -1150,12 +1226,12 @@ if __name__ == "__main__":
print(hunt_response.message)
monster = hunt_response.monster
monster.set_monster_name('Kickass 1')
monster.set_status_effect('int', 100, False, None)
print("The Soul Plate Shows: \n\n{}".format(monster))
# print(monster.set_strategy(4, 3))
# print(monster.set_strategy(1, 1))
# print(monster.set_strategy(1, 2))
monster.add_xp(22900)
#monster.add_xp(22900)
# phy_damage = monster.get_physical_base_damage(100, 'melee', -15)
# print("Phys Attack: {}".format(phy_damage))
# print(monster.get_base_defense())
......@@ -1176,7 +1252,8 @@ if __name__ == "__main__":
print("1 The Soul Plate Shows: \n\n{}".format(monster))
print("2 The Soul Plate Shows: \n\n{}".format(monster2))
print("MP m1: {}\nMP m2: {}".format(monster.get_mp(), monster2.get_mp()))
monster2.add_xp(16900)
#print(monster2.add_xp(16900))
#monster2.set_status_effect('int', 100, False, None)
phy_damage = monster2.attack(monster)
print("Phys Attack: {}".format(phy_damage))
......@@ -1201,12 +1278,13 @@ if __name__ == "__main__":
print("{} {} {} against {} for {}.".format(action.attacker.get_monster_name(), action.message, action.spell, action.target.get_monster_name(), action.damage))
if isinstance(action, DefeatAction):
print("{} {} {} gains {} xp.".format(action.target.get_monster_name(), action.message, action.attacker.get_monster_name(), action.xp))
action.attacker.hp = 500
#print(action.attacker.add_xp(action.xp))
print("{} {}".format(action.target.hp, action.attacker.hp))
fighting = False
break
print("\n\nhp: {} {}\nmp: {} {}\n\n".format(monster.hp, monster2.hp, monster.mp, monster2.mp))
print("\n{} {}% - {} {}%\n".format(monster.get_monster_name(), monster.get_hp_percent(), monster2.get_monster_name(), monster2.get_hp_percent()))
print("\n{} Memory: {}\n\n{} Memory: {}\n\n".format(monster.get_monster_name(), monster.battle_memory, monster2.get_monster_name(), monster2.battle_memory))
......