Skip to content

Instantly share code, notes, and snippets.

@verarong
Created April 29, 2021 14:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save verarong/9f44f13b8becd58be34a9a394a077d9e to your computer and use it in GitHub Desktop.
Save verarong/9f44f13b8becd58be34a9a394a077d9e to your computer and use it in GitHub Desktop.
fight
try:
from FlaskApp.FlaskApp import app, db, tools, schema
except:
import sys
sys.path.append('..')
from FlaskApp import app, db, schema
from FlaskApp.Mongo_Model import configs
from FlaskApp.utils import _sum_dict, debug
from itertools import cycle
from transitions import Machine
from enum import Enum, auto
import random
from functools import reduce
# ============================================= RandomTree ==========================================
#
class Node:
def __init__(self, layer_id, node_id, w, h, min_range, max_range, node_type="monster",
legal_buff=(1000, 1001, 1005, 1006, 1010, 1011, 1015, 1016, 1020, 1021)):
self.id = str(layer_id * 10 + node_id)
self.node_id = node_id
if node_type in ["boss", "thread"]:
self.loc = (int(w * 0.5), int(h * (layer_id + 0.5)))
else:
self.loc = (random.randint(int(w * (node_id + min_range)), int(w * (node_id + max_range))),
random.randint(int(h * (layer_id + min_range)), int(h * (layer_id + max_range))))
self.node_type = node_type
self.legal_buff = legal_buff
self.parents = []
def _generate_detail(self, node_type, node_params):
if node_type == 'monster':
return random.sample(node_params['monster_group'], random.randint(2, 3))
elif node_type == 'warden':
return random.sample(node_params['monster_group'], 2) + random.sample(node_params['warden_group'], 1)
elif node_type == 'boss':
return random.sample(node_params['monster_group'], 2) + random.sample(node_params['boss_group'], 1)
elif node_type == 'event':
return random.choice(node_params['event_group'])
elif node_type == 'box':
return random.choice(node_params['box_group'])
elif node_type == 'thread':
return node_params['thread']
elif node_type == 'campfire':
return 'campfire'
def add_parent(self, node):
if node not in self.parents:
self.parents.append(node)
def generate(self, node_params):
drop, drop_min, drop_max = node_params["drop"]
detail = self._generate_detail(self.node_type, node_params)
return {"type": self.node_type, "detail": detail, "drop": [drop, random.randint(drop_min, drop_max)],
"loc": self.loc, "parents": [node.id for node in self.parents],
"legal_buff": random.sample(self.legal_buff, 3) if self.node_type == 'warden' else []}
class Layer:
def __init__(self, layer_id, width, x, h, min_range, max_range, types, depth, thread=None):
self.layer_id = layer_id
self.terminal = True if layer_id == depth else False
self.pre = True if layer_id == depth - 1 else False
self.node_amount = 1 if self.terminal else random.randint(2, width)
self.w = int(x / self.node_amount)
self.h = h
self.thread = thread
self.layer = self._add_node(min_range, max_range, types)
self.size = len(self.layer)
def _add_node(self, min_range, max_range, types):
if self.terminal:
return [Node(self.layer_id, 0, self.w, self.h, min_range, max_range, "boss")]
elif self.thread and self.pre:
return [Node(self.layer_id, 0, self.w, self.h, min_range, max_range, "thread")]
else:
nodes_type = ["monster"] * (self.node_amount - 1) + [random.choice(types)]
random.shuffle(nodes_type)
return [Node(self.layer_id, i, self.w, self.h, min_range, max_range, x) for i, x in enumerate(nodes_type)]
def get_node(self, node_id):
return self.layer[node_id]
class RandomTree:
def __init__(self, depth, width, node_params, x=680, height=200, y=2000, min_range=0.3, max_range=0.7,
types=("warden", "event", "box", "campfire")):
self.depth = depth
self.width = width
self.x = x
self.y = y
self.h = height
self.node_params = node_params
self.thread = node_params["thread"] if "thread" in node_params else None
self.tree = self._generate(min_range, max_range, types)
self._get_path()
def _generate(self, min_range, max_range, types):
return [Layer(i, self.width, self.x, self.h, min_range, max_range, types, self.depth, self.thread)
for i in range(self.depth + 1)]
def _get_layer(self, layer_id):
return self.tree[layer_id]
def _get_node(self, layer_id, node_id):
return self._get_layer(layer_id).get_node(node_id)
def _get_path(self):
for layer_id in range(self.depth):
current_amount = self._get_layer(layer_id).size
next_amount = self._get_layer(layer_id + 1).size
current_parent = 0
for node_id in range(current_amount):
terminal = next_amount if node_id == current_amount - 1 else random.randint(current_parent,
max(1, next_amount - 2))
if terminal == next_amount:
self._get_node(layer_id, node_id).add_parent(
self._get_node(layer_id + 1, next_amount - 1))
for next_node_id in range(current_parent, min(terminal + 1, next_amount)):
self._get_node(layer_id, node_id).add_parent(
self._get_node(layer_id + 1, next_node_id))
current_parent = terminal
def generate(self):
return {node.id: node.generate(self.node_params) for layer in self.tree for node in layer.layer}
# ============================================= Fighter ==========================================
#
class FightSkill():
def __init__(self, skill_id, level=1):
self.skill_id = skill_id
self.level = level
self.cold = 0
self.name, self.cold_round, self.target, self.element, self.ratio, damage_per_level, self.attack_attribute_expand, \
expand_amount_per_level, buff, debuff = self._get_skill()
self.damage = damage_per_level * level
self.expand_amount = expand_amount_per_level * level
self.buff = buff if buff else None
self.debuff = debuff if debuff else None
def _get_skill(self):
return configs.get_values('talent', self.skill_id,
["name", "cold_round", "target", "element", "ratio", "damage_per_level",
"attack_attribute_expand", "expand_amount_pre_level", "buff", "debuff"])
def update_cold(self, x=-1):
self.cold = max(0, self.cold + x)
def bool_cold(self):
return True if self.cold == 0 else False
def generate_attack(self, attack_params):
attack = attack_params.copy()
attack["attack"] = self.ratio * attack["attack"] + self.damage
for x in ["attack", "gold_attack", "wood_attack", "water_attack", "fire_attack", "soil_attack"]:
if self.attack_attribute_expand == x:
x_ = x.split("_")[0]
attack[x_] = attack[x_] + self.expand_amount
if self.element:
total = sum(attack.values())
attack = {k: total if k == self.element else 0 for k, v in attack.items()}
self.update_cold(self.cold_round)
return attack
class FightBuff(object):
def __init__(self, buff_id):
self.buff_id = buff_id
self.round = 0
self.stack = 1
self.class_, self.bool_stack, self.max_stack, self.max_round, self.type, self.ratio, self.expand = self._get_buff()
def _get_buff(self):
return configs.get_values('buff', self.buff_id,
["class", "bool_stack", "max_stack", "round", "type", "ratio", "expand"])
def update_round(self, x=1):
self.round += x
if self.type == "hot":
return self.expand
elif self.type == "drug":
return -self.expand
return None
def update_stack(self, x=1):
if self.bool_stack and self.max_stack > self.stack:
self.stack += x
class Fighter(object):
def __init__(self, name="", attack=0, defence=0, life=0, speed=0, hit=0, dodge=0, crit=0, gold_attack=0,
wood_attack=0, water_attack=0, fire_attack=0, soil_attack=0, gold_resistance=0, wood_resistance=0,
water_resistance=0, fire_resistance=0, soil_resistance=0, all_resistance=0, ignore_defence=0,
ignore_resistance=0, hit_param=8888, dodge_param=8888, crit_param=8888, ignore_defence_param=8888,
ignore_resistance_param=8888, defence_param=2888, crit_multiply=2, buff=None, current_life=None,
base_hit=0.9, *args, **kwargs):
self.name = str(name)
self.current_life = int(min(current_life, life)) if current_life else int(life)
self.hit_param = hit_param
self.dodge_param = dodge_param
self.crit_param = crit_param
self.ignore_defence_param = ignore_defence_param
self.ignore_resistance_param = ignore_resistance_param
self.defence_param = defence_param
self.crit_multiply = crit_multiply
self.base_hit = base_hit
self.original = {"attack": int(attack),
"defence": int(defence),
"life": int(life),
"current_life": self.current_life,
"speed": int(speed),
"hit": int(hit),
"dodge": int(dodge),
"crit": int(crit),
"gold_attack": int(gold_attack),
"wood_attack": int(wood_attack),
"water_attack": int(water_attack),
"fire_attack": int(fire_attack),
"soil_attack": int(soil_attack),
"gold_resistance": int(gold_resistance),
"wood_resistance": int(wood_resistance),
"water_resistance": int(water_resistance),
"fire_resistance": int(fire_resistance),
"soil_resistance": int(soil_resistance),
"all_resistance": int(all_resistance),
"ignore_defence": int(ignore_defence),
"ignore_resistance": int(ignore_resistance)}
self.buff = []
if buff:
for x in buff:
self.add_buff(x)
self.buff_ratio = {}
self.buff_expand = {}
self._calculate_attribute()
def bool_alive(self):
return True if self.current_life > 0 else False
def resurrection(self):
self.current_life = self.original["life"]
def bool_dead(self):
return True if self.current_life <= 0 else False
def add_buff(self, buff_id):
non_exist = True
for x in self.buff:
if buff_id == x.buff_id and x.bool_stack == 1:
x.update_stack()
non_exist = False
elif buff_id == x.buff_id and x.max_round != 99999:
x.update_round(x.max_round)
non_exist = False
if non_exist:
self.buff.append(FightBuff(buff_id))
self._calculate_buff()
def buff_progress(self, round=1):
buff = []
for x in self.buff:
if x.round + round <= x.max_round:
life_change = x.update_round(round)
buff.append(x)
if life_change:
self.current_life = min(self.current_life + life_change, self.life)
self.buff = buff
self._calculate_buff()
def _calculate_buff(self):
buff_ratio = []
buff_expand = []
for x in self.buff:
if x.class_ == 0:
buff_ratio.append({x.type: x.ratio * x.stack})
buff_expand.append({x.type: x.expand * x.stack})
elif x.class_ == 1:
buff_ratio.append({x.type: -x.ratio * x.stack})
buff_expand.append({x.type: -x.expand * x.stack})
self.buff_ratio = reduce(_sum_dict, buff_ratio) if buff_ratio else {}
self.buff_expand = reduce(_sum_dict, buff_expand) if buff_expand else {}
self._calculate_attribute()
def _calculate_by_type(self, attribute_type):
return int(self.original.get(attribute_type, 0) * (
1 + self.buff_ratio.get(attribute_type, 0))) + self.buff_expand.get(attribute_type, 0)
def _calculate_attribute(self):
self.attack = {"attack": self._calculate_by_type("attack"),
"gold": self._calculate_by_type("gold_attack"),
"wood": self._calculate_by_type("wood_attack"),
"water": self._calculate_by_type("water_attack"),
"fire": self._calculate_by_type("fire_attack"),
"soil": self._calculate_by_type("soil_attack")}
all_resistance = self._calculate_by_type("all_resistance")
self.defence = {"defence": self.defence_param / (self._calculate_by_type("defence") + self.defence_param),
"gold": self._calculate_by_type("gold_resistance") + all_resistance,
"wood": self._calculate_by_type("wood_resistance") + all_resistance,
"water": self._calculate_by_type("water_resistance") + all_resistance,
"fire": self._calculate_by_type("fire_resistance") + all_resistance,
"soil": self._calculate_by_type("soil_resistance") + all_resistance}
self.life = self._calculate_by_type("life")
self.speed = self._calculate_by_type("speed")
self.hit = self._calculate_by_type("hit")
self.dodge = self._calculate_by_type("dodge")
self.crit = self._calculate_by_type("crit")
self.ignore_defence = self._calculate_by_type("ignore_defence")
self.ignore_resistance = self._calculate_by_type("ignore_resistance")
def _bool_hit(self, skill):
hit = self.hit
if skill and skill.attack_attribute_expand == "hit":
hit += skill.expand_amount
return random.random() <= (hit + self.hit_param * self.base_hit) / (hit + self.hit_param)
def _bool_crit(self, skill):
crit = self.crit
if skill and skill.attack_attribute_expand == "crit":
crit += skill.expand_amount
return random.random() <= crit / (crit + self.crit_param)
def _bool_ignore_defence(self, skill):
ignore_defence = self.ignore_defence
if skill and skill.attack_attribute_expand == "ignore_defence":
ignore_defence += skill.expand_amount
return random.random() <= ignore_defence / (ignore_defence + self.ignore_defence_param)
def _bool_ignore_resistance(self, skill):
ignore_resistance = self.ignore_resistance
if skill and skill.attack_attribute_expand == "ignore_resistance":
ignore_resistance += skill.expand_amount
return random.random() <= ignore_resistance / (ignore_resistance + self.ignore_resistance_param)
def _get_attack_state(self, skill):
state = "hit"
bool_ignore_defence = False
bool_ignore_resistance = False
if not self._bool_hit(skill):
return "miss", bool_ignore_defence, bool_ignore_resistance
if self._bool_crit(skill):
state = "crit"
if self._bool_ignore_defence(skill):
bool_ignore_defence = True
if self._bool_ignore_resistance(skill):
bool_ignore_resistance = True
return state, bool_ignore_defence, bool_ignore_resistance
def generate_attack(self, skill):
self.buff_progress()
state, bool_ignore_defence, bool_ignore_resistance = self._get_attack_state(skill)
attack_param = skill.generate_attack(self.attack) if skill else self.attack
if state == "crit":
attack_param["attack"] *= self.crit_multiply
buff = None
if skill and skill.buff:
buff = skill.buff
self.add_buff(buff)
debuff = skill.debuff if skill and skill.debuff else None
skill_id = skill.skill_id if skill else None
return self.name, skill_id, state, bool_ignore_defence, bool_ignore_resistance, attack_param, buff, debuff
def _get_harm_state(self, state):
if random.random() <= self.dodge / (self.dodge + self.dodge_param):
state = "dodge"
return state
def generate_harm(self, attack_name, skill_id, state, bool_ignore_defence, bool_ignore_resistance, attack_param,
buff, debuff):
state = self._get_harm_state(state)
if bool_ignore_defence:
harm = {'attack': attack_param['attack']}
else:
harm = {'attack': attack_param['attack'] * self.defence["defence"]}
element = ['gold', 'wood', 'water', 'fire', 'soil']
for i in element:
harm[i] = attack_param[i] if bool_ignore_resistance else max(0, attack_param[i] - self.defence[i])
if debuff:
self.add_buff(debuff)
harm_total = 0
if state not in ["miss", "dodge"]:
# harm at least 1
harm_total = max(1, int(sum(harm.values())))
self.current_life -= harm_total
return [skill_id, state, bool_ignore_defence, bool_ignore_resistance, harm_total, buff, debuff]
class Gamer(Fighter):
def __init__(self, name, mf, skill_list, animation, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = str(name)
self.mf = int(mf)
self.skill_list = skill_list
self.skill = self._generate_skill(**kwargs)
self.type_ = "gamer"
self.animation = animation
self.environment_effect()
def get_skills(self):
return [i for i, _ in self.skill_list]
def _generate_skill(self, **kwargs):
return [FightSkill(i, level + kwargs.get(f"talent_{i}", 0)) for i, level in self.skill_list]
def environment_effect(self):
self.original["attack"] = self.original["attack"] * configs.get_environment_ratio_by_type(
"character_attack_strengthen")
def get_next_skill(self):
for skill in self.skill:
skill.update_cold()
for skill in self.skill:
if skill.bool_cold():
skill.update_cold(skill.cold_round)
return skill
return FightSkill(100, 1)
class Monster(Fighter):
def __init__(self, monster_id, ratio=1, *args, **kwargs):
super().__init__(name=self._get_name(monster_id), *args, **self._generate_attribute(monster_id, ratio),
**kwargs)
self.monster_id = monster_id
self.type_, self.animation = configs.get_values("monster", monster_id, ["type", "icon"])
self.skill = cycle([FightSkill(i) for i in self.action_tree])
self.environment_effect()
def get_skills(self):
return self.action_tree
def reset_skill(self):
self.skill = cycle([FightSkill(i) for i in self.action_tree])
def rebuild(self, monster_id, ratio=1, *args, **kwargs):
self.__init__(monster_id, ratio, *args, **kwargs)
def environment_effect(self):
environment = configs.get_environment_ratio_by_type("monster_life_weaken")
self.original["life"] = int(self.original["life"] * environment)
self.current_life = int(self.current_life * environment)
self.original["attack"] = int(self.original["attack"] * configs.get_environment_ratio_by_type(
"monster_attack_strengthen"))
def _get_name(self, monster_id):
return configs.get_value("monster", monster_id, "name")
def _generate_attribute(self, monster_id, ratio):
monster_params = configs.get_params('monster', monster_id, configs.legal_params('attribute'))
if ratio != 1:
monster_params = {k: int(v * ratio) for k, v in monster_params.items()}
action_tree_id = configs.get_value('monster', monster_id, 'action_tree')
self.action_tree = configs.get_values('action_tree', action_tree_id, drop_na=True)
return monster_params
# ============================================= Fight ==========================================
#
class States(Enum):
prepare = auto()
init = auto()
progress = auto()
terminal = auto()
done = auto()
class Fight(object):
transitions = [{'trigger': 'prepare', 'source': States.prepare, 'dest': States.init, 'prepare': '_prepare'},
{'trigger': 'progress', 'source': [States.init, States.progress], 'dest': States.terminal,
'conditions': '_check_terminal'},
{'trigger': 'progress', 'source': States.progress, 'dest': States.terminal,
'conditions': '_check_time_out'},
{'trigger': 'progress', 'source': [States.init, States.progress], 'dest': States.progress,
'conditions': '_reverse_check_terminal', 'after': '_progress'},
{'trigger': 'statement', 'source': States.terminal, 'dest': States.done, 'prepare': '_statement'}]
def __init__(self, gamer_list, monster_list, gamer_fight=False, distance=1000, second_limit=3000,
monster_reset_skill=False):
self.gamer_list = gamer_list
self.monster_list = monster_list
if monster_reset_skill:
for monster in self.monster_list:
monster.reset_skill()
self.fighters = gamer_list + monster_list
self.gamer_fight = gamer_fight
self.distance = distance
self.second_limit = second_limit
self.current = 0
self.bool_succeeded = False
self.summary = []
self.total_damage_summary = {"gamer": 0,
"monster": 0}
self.machine = Machine(model=self, states=States, transitions=Fight.transitions, initial=States.prepare)
def _prepare(self):
self.timer = {x: 0 for x in self.fighters}
def _reverse_check_terminal(self):
return not self._check_terminal()
def _bool_succeeded(self):
for x in self.monster_list:
if x.bool_alive():
return False
return True
def _bool_failed(self):
for x in self.gamer_list:
if x.bool_alive():
return False
return True
def _check_terminal(self):
if self._bool_failed():
return True
elif self._bool_succeeded():
self.bool_succeeded = True
return True
return False
def _check_time_out(self):
return self.current > self.second_limit
def _generate_attack(self, fighter):
skill = self._get_skill(fighter)
target = self._get_target(fighter, skill)
return skill, target, fighter.generate_attack(skill)
def _generate_harm(self, fighters, attack_params):
summary = []
for fighter in fighters:
summary_ = fighter.generate_harm(*attack_params)
summary.append(summary_)
if fighter in self.gamer_list:
self.total_damage_summary["gamer"] += summary_[4]
else:
self.total_damage_summary["monster"] += summary_[4]
return summary
def _get_skill(self, fighter):
if fighter in self.gamer_list or self.gamer_fight:
return fighter.get_next_skill()
elif fighter in self.monster_list:
return next(fighter.skill)
def _get_target(self, fighter, skill):
if fighter in self.gamer_list:
target_list = [x for x in self.monster_list if x.bool_alive()]
else:
target_list = [x for x in self.gamer_list if x.bool_alive()]
target = skill.target if skill else "start"
# print(target, target_list)
if target == "all":
return target_list
elif target == "start":
return target_list[:1]
elif target == "terminal":
return target_list[-1:]
else:
return target_list[:1]
def _get_next_action(self):
self.timer = {x: self.timer[x] for x in self.fighters if x.bool_alive()}
next_cost = {k: round((self.distance - v) / k.speed, 2) for k, v in self.timer.items()}
next_ = min(next_cost, key=next_cost.get)
cost = next_cost[next_]
self.timer = {k: min(self.distance, v + k.speed * cost) if k != next_ else 0 for k, v in self.timer.items()}
return cost, next_
def _progress(self):
# print(self.action_index, self.action_tree)
cost, fighter = self._get_next_action()
self.current += cost
# print(fighter.name, fighter.speed)
# fighter.buff_progress()
skill, target, attack_params = self._generate_attack(fighter)
if fighter in self.gamer_list:
type_ = "attack"
gamer = self.gamer_list.index(fighter)
monster = [self.monster_list.index(x) for x in target]
else:
type_ = "harm"
gamer = [self.gamer_list.index(x) for x in target]
monster = self.monster_list.index(fighter)
self.summary.append({str(self.current): {"type": type_,
"fighter": gamer,
"monster": monster,
"params": self._generate_harm(target, attack_params)}})
def _statement(self):
self.flag = "Win" if self.bool_succeeded else "Lose"
def get_total_damage(self):
return self.total_damage_summary["monster"]
def get_total_harm(self):
return self.total_damage_summary["gamer"]
def get_summary(self):
self.prepare()
while True:
self.progress()
if self.state == States.terminal:
break
self.statement()
return self.flag, {"type": "fight",
"state": self.bool_succeeded,
"battle_terminal": False,
"award": None,
"gamer_fight": self.gamer_fight,
"gamers_summary": [
{"life": x.original["life"], "current_life": x.original["current_life"], "name": x.name,
"animation": x.animation, "type": x.type_, "skill": x.get_skills()}
for x in self.gamer_list],
"monsters_summary": [
{"life": x.original["life"], "current_life": x.original["current_life"], "name": x.name,
"animation": x.animation, "type": x.type_, "skill": x.get_skills()}
for x in self.monster_list],
"summary": self.summary}
if __name__ == "__main__":
fight = Fight([Gamer("", 0, [], 0, attack=100, life=100, speed=1000, buff=[1000])], [Monster(11)])
state, summary = fight.get_summary()
print(state)
print(summary)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment