И снова здравствуйте, друзья!
Не так давно я поделился с вами своим опытом применения нейронной сети для для решения задачи выбора действия ботом. Чтобы подробнее узнать о сути задачи, пожалуйста, ознакомьтесь с первой частью статьи.
А я перейду к рассказу о следующем этапе работы!
function buildActionBranch (situation) {
var actionList = createActionList(situation);
/* функция createActionList находит все действия,
которые можно совершить в данной ситуации
и делает для них симуляцию с подсчётом очков.
Каждый элемент массива actionList будет
содержать информацию о той ситуации к которой
приведёт его применение (newSituation). */
for(var i = 0; i < actionList.length; i++){
if(actionList[i].type !== "endTurn") {
/* Если действие может привести к новой ситуации,
тогда нужно строить дерево дальше */
actionList[i].branch = buildActionBranch(actionList[i].newSituation);
/* Значение selfScore - это очки непосредственно
от применения данного действия,
а поле score содержит не только своё значение,
но и суммы очков всех лучших действий
от дочерних уровней дерева */
actionList[i].score = actionList[i].selfScore + actionList[i].branch[0].score;
/* actionList[i].branch[0] - это действие с максимальным
показателем score на дочернем уровне */
}
else {
actionList[i].score = actionList[i].selfScore;
}
}
/* сортируем действия по значению score с убыванием */
actionList.sort(function (a, b) {
if (a.score <= b.score) {
return 1;
}
else if (a.score > b.score) {
return -1;
}
});
/* массив actionList станет значением поля branch
для родительского действия, которое привело к этой ситуации */
return actionList;
}
function situationCost (activeChar, myTeam, enemyTeam, wallPositions){
var score = 0;
var effectScores = 0;
//Подсчёт очков для активного персонажа
score += activeChar.curHealth / activeChar.maxHealth * 110;
score += activeChar.curMana / activeChar.maxMana * 55;
var positionWeights = arenaService.calculatePositionWeight(activeChar.position, activeChar, myTeam.characters, enemyTeam.characters, arenaService.getOptimalRange(activeChar), wallPositions);
score += positionWeights[0] * 250 + Math.random();
score += positionWeights[1] * 125 + Math.random();
for(var j = 0; j < activeChar.buffs.length; j++){
if(activeChar.buffs[j].score) {
effectScores = activeChar.buffs[j].score(activeChar, myTeam.characters, enemyTeam.characters, wallPositions);
score += this.calculateEffectScore(effectScores, activeChar.buffs[j].name);
}
}
for(j = 0; j < activeChar.debuffs.length; j++){
if(activeChar.debuffs[j].score) {
effectScores = activeChar.debuffs[j].score(activeChar, myTeam.characters, enemyTeam.characters, wallPositions);
score -= this.calculateEffectScore(effectScores, activeChar.debuffs[j].name);
}
}
//myTeam - союзники активного персонажа
for(var i = 0; i < myTeam.characters.length; i++){
if(myTeam.characters[i].id !== activeChar.id) {
var ally = myTeam.characters[i];
score += ally.curHealth / ally.maxHealth * 100;
score += ally.curMana / ally.maxMana * 50;
for(j = 0; j < ally.buffs.length; j++){
if(ally.buffs[j].score) {
effectScores = ally.buffs[j].score(ally, myTeam.characters, enemyTeam.characters, wallPositions);
score += this.calculateEffectScore(effectScores, ally.buffs[j].name);
}
}
for(j = 0; j < ally.debuffs.length; j++){
if(ally.debuffs[j].score) {
effectScores = ally.debuffs[j].score(ally, myTeam.characters, enemyTeam.characters, wallPositions);
score -= this.calculateEffectScore(effectScores, ally.debuffs[j].name);
}
}
}
}
//enemyTeam - противники
for(i = 0; i < enemyTeam.characters.length; i++){
var enemy = enemyTeam.characters[i];
score -= Math.exp(enemy.curHealth / enemy.maxHealth * 3) * 15 - 200;
score -= enemy.curMana / enemy.maxMana * 50;
for(j = 0; j < enemy.buffs.length; j++){
if(enemy.buffs[j].score) {
effectScores = enemy.buffs[j].score(enemy, enemyTeam.characters, myTeam.characters, wallPositions);
score -= this.calculateEffectScore(effectScores, enemy.buffs[j].name);
}
}
for(j = 0; j < enemy.debuffs.length; j++){
if(enemy.debuffs[j].score) {
effectScores = enemy.debuffs[j].score(enemy, enemyTeam.characters, myTeam.characters, wallPositions);
score += this.calculateEffectScore(effectScores, enemy.debuffs[j].name);
}
}
}
return score;
}
score += activeChar.curHealth / activeChar.maxHealth * 110;
score -= Math.exp(enemy.curHealth / enemy.maxHealth * 3) * 15 - 200;
score: function(owner, myTeam, enemyTeam, walls) {
var buffer = {};
/* buffer - персонаж, который применил данный эффект */
for (var i = 0; i < myTeam.length; i++) {
if (myTeam[i].id === this.casterId) buffer = myTeam[i];
}
/* Вычисляем количество восстанавливаемого здоровья */
var heal = (this.variant * 80) * (1 + buffer.spellPower);
/* Корректируем размер исцеления с учётом
характеристик того, кто его применил */
heal = arenaService.calculateExpectedHeal(heal, buffer);
/* определяем ценность позиции обладателя эффекта */
var positionWeights = arenaService.calculatePositionWeight(owner.position, owner, myTeam, enemyTeam, arenaService.getOptimalRange(owner), walls);
return {
effectScore: heal / 10,
leftScore: this.left * 5,
offensivePositionScore: 0,
defensivePositionScore: - positionWeights[1] * 25,
healthScore: - owner.curHealth / owner.maxHealth * 25,
manaScore: owner.curMana / owner.maxMana * 15
};
}
cast : function (caster, target, myTeam, enemyTeam, walls) {
/* Сперва потратим ресурсы на применение способности */
caster.spendEnergy(this.energyCost());
caster.spendMana(this.manaCost());
/* Установим, что теперь способность использована
и нужно время на её восстановление */
this.cd = this.cooldown();
/* Проверка попадания по противнику */
if(caster.checkHit()){
/* Урон от способности определяется случайным образом
в интервале от минимального до максимального */
var physDamage = randomService.randomInt(caster.minDamage *
(1 + this.variant * 0.35), caster.maxDamage * (1 + this.variant * 0.35));
/* Проверка критического попадания по противнику */
var critical = caster.checkCrit();
if(critical){
physDamage = caster.applyCrit(physDamage);
}
/* Снижаем урон от способности с учётом защиты цели */
physDamage = target.applyResistance(physDamage, false);
/* Записываем в специальное свойство название звука,
который потом проиграется на клиенте */
caster.soundBuffer.push(this.name);
/* Цель получает рассчитанное количество повреждений physDamage */
target.takeDamage(physDamage, caster, {name: this.name, icon: this.icon(), role: this.role()}, true, true, critical, myTeam, enemyTeam);
}
else {
/* Некоторые эффекты могут спадать после промаха,
поэтому выполним эту функцию */
caster.afterMiss(target.charName, {name: this.name, icon: this.icon(), role: this.role()}, myTeam, enemyTeam);
}
/* Некоторые эффекты могут спадать после применения способностей,
поэтому выполним эту функцию */
caster.afterCast(this.name, myTeam, enemyTeam);
}
/* Сперва потратим ресурсы на применение способности */
caster.spendEnergy(this.energyCost());
caster.spendMana(this.manaCost());
/* Установим, что теперь способность использована
и нужно время на её восстановление */
this.cd = this.cooldown();
/* Теперь наносимый урон - это среднее арифметическое
между минимальным и максимальным значениями */
var physDamage = (caster.minDamage * (1 + this.variant * 0.35) + caster.maxDamage * (1 + this.variant * 0.35)) / 2;
/* Расчёт математического ожидания для шанса
попадания и шанса нанести критический урон */
physDamage = caster.hitChance * ((1 - caster.critChance) * physDamage + caster.critChance * (1.5 + caster.critChance) * physDamage);
physDamage = target.applyResistance(physDamage, false);
/* Следующие две функции также были "облегчены" для симуляции */
target.takeDamageSimulation(physDamage, caster, true, true, myTeam, enemyTeam);
caster.afterCastSimulation(this.name);
/* Данная функция - это "точка входа" в построение дерева решений */
function buildActionBranchAsync(myTeam, enemyTeam, activeCharId, wallPositions, cb){
var self = this;
/* Реальный список действий, доступных персонажу */
var actionList = self.createActionList(myTeam, enemyTeam, activeCharId, wallPositions);
async.eachOf(actionList, function(actionInList, index, cb){
/* Обёртка для освобождения потока до следующей итерации */
process.nextTick(function() {
if(actionInList.type != "endTurn" ) {
/* Ветви строятся уже рекурсивно и синхронно */
actionInList.branch = self.buildActionBranchSync(actionInList.myTeamState, actionInList.enemyTeamState, actionInList.activeCharId, wallPositions);
if(actionInList.branch && actionInList.branch[0]) {
actionInList.score = actionInList.selfScore + actionInList.branch[0].score;
}
else {
actionInList.score = actionInList.selfScore;
}
}
else {
actionInList.score = actionInList.selfScore;
}
cb(null, null);
});
}, function(err, temp){
if(err){
return console.error(err);
}
actionList.sort(function (a, b) {
if (a.score <= b.score) {
return 1;
}
else if (a.score > b.score) {
return -1;
}
});
cb(actionList);
})
}
function lightWeightTeamBeforeSimulation(team){
delete team.teamName;
delete team.lead;
for(var i = 0; i < team.characters.length; i++){
var char = team.characters[i];
delete char.battleTextBuffer;
delete char.logBuffer;
delete char.soundBuffer;
delete char.battleColor;
delete char.charName;
delete char.gender;
delete char.isBot;
delete char.portrait;
delete char.race;
delete char.role;
delete char.state;
delete char.calcParamsByPoint;
delete char.calcItem;
delete char.updateMods;
delete char.removeRandomBuff;
delete char.removeRandomDebuff;
delete char.removeAllDebuffs;
delete char.removeRandomDOT;
delete char.stealRandomBuff;
delete char.afterDealingDamage;
delete char.afterDamageTaken;
delete char.afterMiss;
delete char.removeImmobilization;
delete char.afterCast;
delete char.getSize;
for(var j = 0; j < char.abilities.length; j++){
var ability = char.abilities[j];
delete ability.cast;
delete ability.icon;
delete ability.role;
}
for(j = 0; j < char.buffs.length; j++){
var effect = char.buffs[j];
delete effect.icon;
delete effect.role;
delete effect.apply;
}
for(j = 0; j < char.debuffs.length; j++){
var effect = char.debuffs[j];
delete effect.icon;
delete effect.role;
delete effect.apply;
}
}
return team;
}
function buildActionBranchSync: function(myTeam, enemyTeam, activeCharId, wallPositions){
var actionList = this.createActionList(myTeam, enemyTeam, activeCharId, wallPositions);
for(var z = 0; z < actionList.length; z++){
/* ... */
actionList[z].branch = this.buildActionBranchSync(actionList[z].myTeamState, actionList[z].enemyTeamState, actionList[z].activeCharId, wallPositions);
actionList[z].score = actionList[z].selfScore + actionList[z].branch[0].score;
delete actionList[z].branch; /* Ветка больше не нужна */
/* ... */
}
/* sort actionList */
return actionList;
}
usageLogic: function(target) {
/* Текущее здоровье цели ниже 60% */
return target.curHealth < target.maxHealth * 0.6;
}
/* ... */
var bestMovePoints = [];
/* находим массив всех точек, доступных для перемещения */
var movePoints = arenaService.findMovePoints(myTeam, enemyTeam, activeChar, false, wallPositions);
/* Для каждой точки посчитаем вес и составим новый массив */
for(var i = 0; i < movePoints.length; i++){
var weights = arenaService.calculatePositionWeight(movePoints[i], activeChar, myTeam.characters, enemyTeam.characters, arenaService.getOptimalRange(activeChar), wallPositions);
/* вес позиции с точки зрения нападения (weights[0]) считается
выгоднее, чем вес позиции с точки зрения защиты (weights[1]) */
bestMovePoints.push({
point: movePoints[i],
weightScore: weights[0] * 6 + weights[1] * 4
})
}
/* Сортируем массив по убыванию весов */
bestMovePoints.sort(function (a, b) {
if (a.weightScore <= b.weightScore) {
return 1;
}
else if (a.weightScore > b.weightScore) {
return -1;
}
});
/* И используем только 3 наилучших */
bestMovePoints = bestMovePoints.slice(0, 3);
for(j = 0; j < bestMovePoints.length; j++){
/* Симуляция */
}
score += activeChar.curHealth / activeChar.maxHealth * 110;
К сожалению, не доступен сервер mySQL