/*
* Copyright (C) 2012 Sebastian Straub <sebastian-straub@gmx.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.nx42.wotcrawler.ext;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.nx42.wotcrawler.db.BaseProperties.Development;
import de.nx42.wotcrawler.db.module.Engine;
import de.nx42.wotcrawler.db.module.Gun;
import de.nx42.wotcrawler.db.module.Radio;
import de.nx42.wotcrawler.db.module.Suspension;
import de.nx42.wotcrawler.db.module.Turret;
import de.nx42.wotcrawler.db.tank.Equipment;
import de.nx42.wotcrawler.db.tank.Tank;
import de.nx42.wotcrawler.db.tank.Tank.TankType;
/**
* The TankRating is an approach to show the strengths and weaknesses of every
* tank by comparing it's stats to any other tank of the same class.
*
* The existing tank stats are combined to create ratings in different categories,
* which results in an overall tank rating, which is, of course, ignores some
* important facts and is totally subjective anyway (though hopefully not too
* arbitrary).
*
* For more details, see http://www.nx42.de/projects/wot/rating.html
*
* @author Sebastian Straub <sebastian-straub@gmx.net>
*/
public class TankRating {
private static final Logger log = LoggerFactory.getLogger(TankRating.class);
// base object: Tank
protected Tank t;
// derived tank details
protected Equipment eq;
protected Engine e;
protected Gun g;
protected Radio r;
protected Suspension s;
protected Turret tu;
// abstraction layer 1: basic ratings
protected double hitpoints;
protected double weight;
protected double firechance;
protected double traverseTurret;
protected double traverseSuspension;
protected double gunAccuracy;
protected double gunAimTime;
protected double gunAmmo;
protected double speed;
protected double enginePower;
protected double powerWeightRatio;
protected double radioRange;
protected double viewRange;
// more advanced ratings, but focused on single basic stats
protected double hullArmor;
protected double turretArmor;
protected double gunArc;
protected double gunElevation;
protected double damage;
protected double penetration;
// abstraction layer 2: cumulation of basic ratings in categories
protected double ratingDefense;
protected double ratingAttack;
protected double ratingMobility;
protected double ratingRecon;
protected double ratingCostBenefit;
// abstraction layer 3: final rating
protected double ratingOverall;
/**
* Prepares a new TankRating for the specified tank with the given
* development.
*
* @param t the tank to create a rating for
* @param dev the development of this tank
*/
public TankRating(Tank t, Development dev) {
this.t = t;
this.e = ModuleMap.getModuleByDev(Engine.class, t, dev);
this.g = ModuleMap.getModuleByDev(Gun.class, t, dev);
this.r = ModuleMap.getModuleByDev(Radio.class, t, dev);
this.s = ModuleMap.getModuleByDev(Suspension.class, t, dev);
this.tu = ModuleMap.getModuleByDev(Turret.class, t, dev);
switch(dev) {
case Stock:
this.eq = t.equipmentStock;
break;
case Top:
this.eq = t.equipmentTop;
break;
default:
log.warn("Unsupported enum value: " + dev);
}
}
/**
* Calculates all available ratings.
*/
public void calculateRatings() {
calculateBaseRatings();
calculateAdvancedRatings();
}
/**
* Calculates the basic ratings (comparison of module parts and basic
* properties)
*/
protected void calculateBaseRatings() {
// base
this.hitpoints = percentage(Field.TE_Hitpoints, eq.hitpoints);
this.weight = percentage(Field.TE_Weight , eq.weight);
this.firechance = percentageInverse(Field.ME_Firechance , e.firechance);
this.traverseTurret = percentage(Field.MT_Traverse , tu.traverse);
this.traverseSuspension = percentage(Field.MS_Traverse , s.traverse);
this.speed = percentage(Field.T_TopSpeed , t.speed);
this.enginePower = percentage(Field.ME_Power , e.power);
this.powerWeightRatio = percentage(Field.DP_HPperTon, t, eq.development);
this.radioRange = percentage(Field.MR_Range , r.range);
this.viewRange = percentage(Field.TE_ViewRange , eq.viewRange);
// depending on development
switch (eq.development) {
case Stock:
this.gunAccuracy = percentageInverse(Field.MG_Accuracy_Max, g.accuracyMax);
this.gunAimTime = percentageInverse(Field.MG_AimTime_Max, g.aimTimeMax);
break;
case Top:
this.gunAccuracy = percentageInverse(Field.MG_Accuracy_Min, g.accuracyMin);
this.gunAimTime = percentageInverse(Field.MG_AimTime_Min, g.aimTimeMin);
break;
}
// advanced
this.gunElevation = percentage(Field.DP_Elevation, t, eq.development);
this.gunArc = percentage(Field.DP_GunArc, t, eq.development);
this.gunAmmo = percentage(Field.DP_Ammo_Normalized, t, eq.development);
// prepare for calculation
double hullF = percentage(Field.T_Hull_Front , t.hullFront);
double hullS = percentage(Field.T_Hull_Side , t.hullRear);
double hullR = percentage(Field.T_Hull_Rear , t.hullSide);
double turretF = percentage(Field.MT_Armor_Front , tu.armorFront);
double turretS = percentage(Field.MT_Armor_Side , tu.armorSide);
double turretR = percentage(Field.MT_Armor_Rear , tu.armorRear);
double dmgAP = percentage(Field.DP_DmgPS_AP, t, eq.development);
double dmgAPCR = percentage(Field.DP_DmgPS_APCR, t, eq.development);
double dmgHE = percentage(Field.DP_DmgPS_HE, t, eq.development);
double dmgHEAT = percentage(Field.DP_DmgPS_HEAT, t, eq.development);
double penAP = percentage(Field.MG_Penetration_AP , g.penAP);
double penAPCR = percentage(Field.MG_Penetration_APCR , g.penAPCR);
double penHE = percentage(Field.MG_Penetration_HE , g.penHE);
double penHEAT = percentage(Field.MG_Penetration_HEAT , g.penHEAT);
this.hullArmor = carlculateArmorRating(hullF, hullS, hullR);
this.turretArmor = carlculateArmorRating(turretF, turretS, turretR);
this.damage = carlculateDamageRating(new double[]{ dmgAP, dmgAPCR, dmgHE, dmgHEAT });
this.penetration = carlculateDamageRating(new double[]{ penAP, penAPCR, penHE, penHEAT });
}
/**
* Calculates the advanced ratings, depending on the tank type
* (weighed cumulation of ratings in different categories - nonexclusive!)
*/
protected void calculateAdvancedRatings() {
/*
* values that might not be set (on purpose):
* -> turret: hull, traverse
* only for TD, SPG
*/
switch(t.type) {
case LightTank:
ratingLightTank();
break;
case MediumTank:
ratingMediumTank();
break;
case HeavyTank:
ratingHeavyTank();
break;
case TankDestroyer:
ratingTD();
break;
case SelfPropelledGun:
ratingSPG();
break;
default:
log.warn("Unknown Tank type: "+t.type);
}
}
/**
* Calculates the ratings for light tanks
*/
private void ratingLightTank() {
this.ratingDefense =
0.3 * hitpoints +
0.2 * hullArmor +
0.2 * turretArmor +
0.1 * weight + // resist ram
0.03 * firechance +
0.05 * gunElevation +
0.06 * traverseSuspension +
0.06 * traverseTurret;
this.ratingAttack =
0.3 * penetration +
0.4 * damage +
0.08 * gunAmmo +
0.08 * gunAccuracy +
0.04 * gunAimTime +
0.04 * gunElevation +
0.06 * weight; // ram
this.ratingMobility =
0.45 * speed +
0.30 * powerWeightRatio +
0.15 * traverseSuspension +
0.10 * traverseTurret;
this.ratingRecon =
0.35 * radioRange +
0.65 * viewRange;
this.ratingCostBenefit = -1; // ignore for now...
this.ratingOverall =
0.2 * ratingDefense +
0.4 * ratingAttack +
0.3 * ratingMobility +
0.1 * ratingRecon;
}
/**
* Calculates the ratings for medium tanks
*/
private void ratingMediumTank() {
this.ratingDefense =
0.25 * hitpoints +
0.3 * hullArmor +
0.25 * turretArmor +
0.05 * weight + // resist ram
0.02 * firechance +
0.03 * gunArc +
0.03 * gunElevation +
0.03 * traverseSuspension +
0.04 * traverseTurret;
this.ratingAttack =
0.4 * penetration +
0.3 * damage +
0.05 * gunAmmo +
0.1 * gunAccuracy +
0.05 * gunAimTime +
0.03 * gunArc +
0.03 * gunElevation +
0.04 * weight; // ram
this.ratingMobility =
0.4 * speed +
0.3 * powerWeightRatio +
0.15 * traverseSuspension +
0.15 * traverseTurret;
this.ratingRecon =
0.35 * radioRange +
0.65 * viewRange;
this.ratingCostBenefit = -1; // ignore for now...
this.ratingOverall =
0.34 * ratingDefense +
0.34 * ratingAttack +
0.2 * ratingMobility +
0.12 * ratingRecon;
}
/**
* Calculates the ratings for heavy tanks
*/
private void ratingHeavyTank() {
this.ratingDefense =
0.25 * hitpoints +
0.3 * hullArmor +
0.25 * turretArmor +
0.05 * weight + // resist ram
0.02 * firechance +
0.03 * gunArc +
0.03 * gunElevation +
0.03 * traverseSuspension +
0.04 * traverseTurret;
this.ratingAttack =
0.4 * penetration +
0.3 * damage +
0.05 * gunAmmo +
0.08 * gunAccuracy +
0.07 * gunAimTime +
0.03 * gunArc +
0.03 * gunElevation +
0.04 * weight; // ram
this.ratingMobility =
0.24 * speed +
0.16 * powerWeightRatio +
0.2 * traverseSuspension +
0.4 * traverseTurret;
this.ratingRecon =
0.5 * radioRange +
0.5 * viewRange;
this.ratingCostBenefit = -1; // ignore for now...
this.ratingOverall =
0.38 * ratingDefense +
0.37 * ratingAttack +
0.15 * ratingMobility +
0.1 * ratingRecon;
}
/**
* Calculates the ratings for tank destroyers
*/
private void ratingTD() {
boolean noturret = (turretArmor == -1);
if(noturret) {
this.ratingDefense =
0.3 * hitpoints +
0.5 * hullArmor +
0.04 * weight + // resist ram
0.02 * firechance +
0.07 * gunArc +
0.04 * gunElevation +
0.03 * traverseSuspension;
this.ratingMobility =
0.35 * speed +
0.15 * powerWeightRatio +
0.5 * traverseSuspension;
} else {
this.ratingDefense =
0.3 * hitpoints +
0.3 * hullArmor +
0.2 * turretArmor +
0.03 * weight + // resist ram
0.01 * firechance +
0.06 * gunArc +
0.04 * gunElevation +
0.02 * traverseSuspension +
0.04 * traverseTurret;
this.ratingMobility =
0.35 * speed +
0.15 * powerWeightRatio +
0.15 * traverseSuspension +
0.35 * traverseTurret;
}
this.ratingAttack =
0.30 * penetration +
0.36 * damage +
0.05 * gunAmmo +
0.1 * gunAccuracy +
0.07 * gunAimTime +
0.05 * gunArc +
0.05 * gunElevation +
0.02 * weight; // ram
this.ratingRecon =
0.35 * radioRange +
0.65 * viewRange;
this.ratingCostBenefit = -1; // ignore for now...
this.ratingOverall =
0.25 * ratingDefense +
0.5 * ratingAttack +
0.15 * ratingMobility +
0.1 * ratingRecon +
0.0 * ratingCostBenefit;
}
/**
* Calculates the ratings for SPGs
*/
private void ratingSPG() {
boolean noturret = (turretArmor == -1);
if(noturret) {
this.ratingDefense =
0.25 * hitpoints +
0.35 * hullArmor +
0.1 * weight + // resist ram
0.02 * firechance +
0.1 * gunArc +
0.03 * gunElevation +
0.15 * traverseSuspension;
this.ratingMobility =
0.2 * speed +
0.1 * powerWeightRatio +
0.7 * traverseSuspension;
} else {
this.ratingDefense =
0.25 * hitpoints +
0.20 * hullArmor +
0.15 * turretArmor +
0.08 * weight + // resist ram
0.02 * firechance +
0.07 * gunArc +
0.03 * gunElevation +
0.05 * traverseSuspension +
0.15 * traverseTurret;
this.ratingMobility =
0.15 * speed +
0.08 * powerWeightRatio +
0.2 * traverseSuspension +
0.57 * traverseTurret;
}
this.ratingAttack =
0.2 * penetration +
0.5 * damage +
0.03 * gunAmmo +
0.08 * gunAccuracy +
0.08 * gunAimTime +
0.08 * gunArc +
0.03 * gunElevation;
this.ratingRecon =
0.8 * radioRange +
0.2 * viewRange;
this.ratingCostBenefit = -1; // ignore for now...
this.ratingOverall =
0.1 * ratingDefense +
0.7 * ratingAttack +
0.1 * ratingMobility +
0.1 * ratingRecon;
}
/**
* Calculates the armor rating from the single values for front, side and
* rear armor.
* @param front the front armor rating
* @param side the side armor rating
* @param rear the rear armor rating
* @return the overall armor rating
*/
private double carlculateArmorRating(double front, double side, double rear) {
return front * 0.5 + side * 0.3 + rear * 0.2;
}
/**
* Calculates the damage rating from the single ratings of the four damage
* values (ap, apcr, he, heat), which need to be specified in this order in
* the array parameter. Nonexisting damage types need to be specified with -1.0.
* The average of all existing ratings will be the overall damage rating
* (nonexisting damage types are ignored)
* @param damage the four damage ratings (ap, apcr, he, heat), in this, and
* ONLY THIS order. nonexisting damage ratings need to be set to -1
* @return the overall damage rating for this gun
*/
private double carlculateDamageRating(double[] damage) {
List<Double> valid = new LinkedList<Double>();
for (double dmg : damage) {
if(dmg >= 0 && dmg <= 1) {
valid.add(dmg);
} else if(dmg != -1.0) {
// -1 is the magic number for nonexistent damage type. All others are errors...
log.warn("Tank {} has an illegal damage rating of {}. "
+ "This should not be possible!", t.name, dmg);
}
}
double sum = 0.0;
for (Double dmg : valid) {
sum += dmg;
}
return valid.size() > 0 ? (sum / valid.size()) : -1;
}
/**
* Creates the rating for a single field with the specified value.
* The rating is the fraction of the actual value from the best value
* that has been determined before.
* @param f the field to look up the best value from
* @param value the actual value of this field (for the current tank)
* @return the rating of this field for the specified value
*/
private double percentage(Field f, double value) {
if (value > 0.0) {
if (value > f.best) {
log.warn("value not within reasonable bounds: {}/{}", value, f.best);
} else {
return value / f.best;
}
} else if (value < 0.0) {
log.warn(String.format("Field %s for Tank %s is associated with illegal value %s.", f.toString(), t.name, value));
}
// ignore 0.0 values, these are expected e.g. for incompatible damage types
// return error value -1
return -1;
}
/**
* Creates the rating for a single field with the value from the specified
* tank.
* The rating is the fraction of the actual value from the best value
* that has been determined before.
* @param f the field to look up the best value from
* @param t the tank to look up the actual value from
* @param dev the development of this tank (in case the actual value depends
* on it...)
* @return the rating of this field for the specified tank
*/
private double percentage(Field f, Tank t, Development dev) {
return percentage(f, f.calc(t, dev));
}
/**
* Creates the rating for a single field where the principle "lower is
* better" applies.
* As opposed to the percentage-Method, the rating is determined by the
* inverse fraction of actual and best value: 1.0 / (actual / best)
* @param f the field to look up the best (=lowest) value from
* @param value the actual value of this field (for the current tank)
* @return the rating of this field for the specified value
*/
private double percentageInverse(Field f, double value) {
if(value > 0.0) {
if(value < f.best) {
log.warn("value (lower is better) not within reasonable bounds: {}/{}", value, f.best);
} else {
return 1.0 / (value / f.best);
}
} else if (value < 0.0) {
log.warn(String.format("Field %s for Tank %s is associated with illegal value %s.", f.toString(), t.name, value));
}
// ignore 0.0 values, these are expected e.g. for incompatible damage types
// return error value -1
return -1;
}
/**
* Creates the rating for a single field where the principle "lower is
* better" applies.
* As opposed to the percentage-Method, the rating is determined by the
* inverse fraction of actual and best value: 1.0 / (actual / best)
* @param f the field to look up the best (=lowest) value from
* @param t the tank to look up the actual value from
* @param dev the development of this tank (in case the actual value depends
* on it...)
* @return the rating of this field for the specified tank
*/
private double percentageInverse(Field f, Tank t, Development dev) {
return percentageInverse(f, f.calc(t, dev));
}
// ---------- static stuff ----------
/**
* Creates a HTML snipped that contains the specified value inside a
* <code><td></code> tag and the specified comment inside a
* <code><abbr></code> tag which is wrapped around the value.
* @param value the field value
* @param comment the comment to add to this value
* @return a html snipped containing the value and the comment inside a
* table cell
*/
public static String fieldToHTML(String value, String comment) {
return String.format("<td><abbr title=\"%s\">%s</abbr></td>", comment, value);
}
/**
* Creates a HTML snipped that contains the specified value inside a
* <code><td></code> tag.
* @param value the field value
* @return a html snipped containing the value inside a table cell
*/
public static String fieldToHTML(String value) {
return String.format("<td>%s</td>", value);
}
/**
* Get the ratings of a specific tank type
* @param ratings all available ratings
* @param type the type of rating to filter
* @return only ratings of the specified tank type
*/
public static List<TankRating> getRatings(List<TankRating> ratings, TankType type) {
List<TankRating> r = new ArrayList<TankRating>(ratings.size() / 5);
for (TankRating tr : ratings) {
if(tr.t.type == type) {
r.add(tr);
}
}
return r;
}
}