/*
* 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.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import de.nx42.wotcrawler.db.TanksDB;
import de.nx42.wotcrawler.db.module.Engine;
import de.nx42.wotcrawler.db.module.Gun;
import de.nx42.wotcrawler.db.module.Module;
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;
/**
* Evaluates a TankDB, detects common errors.
*
* @author Sebastian Straub <sebastian-straub@gmx.net>
*/
public class Evaluator {
/** The database to evaluate */
protected TanksDB db;
/** The ModuleMap, mapping from each tank to a list of compatible modules */
protected ModuleMap mm;
/** all tanks in this set have no engine associated */
protected Set<Tank> noEngine = new HashSet<Tank>();
/** all tanks in this set have no gun associated */
protected Set<Tank> noGun = new HashSet<Tank>();
/** all tanks in this set have no radio associated */
protected Set<Tank> noRadio = new HashSet<Tank>();
/** all tanks in this set have no turret associated (except for td and spg) */
protected Set<Tank> noTurret = new HashSet<Tank>();
/** all tanks in this set have no suspension associated */
protected Set<Tank> noSuspension = new HashSet<Tank>();
/** List of other reports for each tank */
Map<Tank,List<String>> tankReports = new HashMap<Tank,List<String>>();
/** List of other reports for each module */
Map<Module,List<String>> moduleReports = new HashMap<Module,List<String>>();
/** has a report already been built? */
boolean built = false;
/**
* Initializes the Evaluator
* @param db the TankDB to evaluate
* @param map the ModuleMap to work with
*/
public Evaluator(TanksDB db, ModuleMap map) {
this.db = db;
this.mm = map;
}
/**
* Initializes the Evaluator and generates a ModuleMap on the fly
* @param db the TankDB to evaluate
*/
public Evaluator(TanksDB db) {
this.db = db;
this.mm = ModuleMap.build(db);
}
// ------------ public accessors ------------
/**
* generates a report and stores it in the internal object structure
*/
public void buildReport() {
checkAllFields();
this.built = true;
}
/**
* Writes a previously generated report in plain text (with some basic
* markdown) and returns it as String
* @return the current report, as String
*/
public String writeReport() {
// build the report, if it was not created yet!
if(!this.built) {
buildReport();
}
// write the report
StringBuilder sb = new StringBuilder();
sb.append("\n------------------\n");
sb.append("TankDB Evaluation Report started. All Errors above this line \nare probably not covered in this report.\n");
// Test 1
sb.append("\n### Test 1\n\nMissing Tank -> Module Relations: ");
int relationErrors = noEngine.size() + noGun.size() + noRadio.size() + noSuspension.size() + noTurret.size();
if(relationErrors == 0) {
sb.append("great, all tanks have at least one of each module type!\n");
} else {
sb.append(relationErrors);
sb.append(" faulty relations\n\n");
missingModules(sb, noEngine, "Engines");
missingModules(sb, noGun, "Guns");
missingModules(sb, noRadio, "Radios");
missingModules(sb, noSuspension, "Suspensions");
missingModules(sb, noTurret, "Turrets (excluding TDs and SPGs)");
}
// Test 2
sb.append("\n### Test 2\n\nInvalid Tank Attributes: ");
if(tankReports.isEmpty()) {
sb.append("great, the attributes of every tank seem to be valid!\n");
} else {
sb.append("Each of these tanks has some broken fields:\n\n");
for (Tank t : tankReports.keySet()) {
sb.append(String.format("* %s (%s)\n", t.name, t.id));
for (String report : tankReports.get(t)) {
sb.append(String.format(" - %s\n", report));
}
}
}
// Test 3
sb.append("\n### Test 3\n\nInvalid Module Attributes: ");
if(moduleReports.isEmpty()) {
sb.append("great, the attributes of every module seem to be valid!\n");
} else {
sb.append("Each of these modules has some broken fields:\n\n");
for (Module m : moduleReports.keySet()) {
sb.append(String.format("* %s\n", m.name));
for (String report : moduleReports.get(m)) {
sb.append(String.format(" - %s\n", report));
}
}
}
sb.append("\nReport finished\n------------------\n");
return sb.toString();
}
// ------------ output ------------
/**
* Part of the text report system. writes down missing modules for each tank
* @param sb the stringbuilder where the current report is stored in
* @param tanks this set contains all tanks that have a missing module
* @param module this is the module that the tanks in the set are missing
*/
protected void missingModules(StringBuilder sb, Set<Tank> tanks, String module) {
sb.append("* ");
sb.append(module);
if(tanks.isEmpty()) {
sb.append(": great, every tank has at least one of these!\n");
} else {
sb.append("\n");
for (Tank t : tanks) {
sb.append(String.format(" - %s (%s)\n", t.name, t.id));
}
}
}
// ------------ evaluation ------------
/**
* Runs all tests, checks all fields for existence and validity
*/
protected void checkAllFields() {
for (Tank t : db.tanks) {
// check tank fields
checkTankFields(t);
// check, if compatible modules exist
if (ModuleMap.engine.get(t).isEmpty()) {
noEngine.add(t);
}
if (ModuleMap.gun.get(t).isEmpty()) {
noGun.add(t);
}
if (ModuleMap.radio.get(t).isEmpty()) {
noRadio.add(t);
}
if (ModuleMap.suspension.get(t).isEmpty()) {
noSuspension.add(t);
}
if (ModuleMap.turret.get(t).isEmpty()) {
// these tank types usually have no turret, so no reference here...
if(t.type != TankType.TankDestroyer && t.type != TankType.SelfPropelledGun) {
noTurret.add(t);
}
}
}
for (Engine e : db.modules.engines) {
checkModEngineFields(e);
}
for (Gun g : db.modules.guns) {
checkModGunFields(g);
}
for (Radio r : db.modules.radios) {
checkModRadioFields(r);
}
for (Turret t : db.modules.turrets) {
checkModTurretFields(t);
}
for (Suspension s : db.modules.suspensions) {
checkModSuspFields(s);
}
}
/**
* Checks all fields of a single tank
* @param t the tank to check
*/
protected void checkTankFields(Tank t) {
if(t.battleTierMax < 1 || t.battleTierMax > 12) {
report(t, "battleTierMax: " + t.battleTierMax);
}
if(t.battleTierMin < 1 || t.battleTierMin > 12) {
report(t, "battleTierMin: " + t.battleTierMin);
}
if(t.tier > 1 && t.cost < 1)
report(t, "cost: " + t.cost);
if(t.crewMembers < 1 || t.crewMembers > 10) {
report(t, "crewMembers: " + t.crewMembers);
}
if(t.currency == null) {
report(t, "currency is null");
}
if(t.gunArcRight < 1 || t.gunArcRight > 360) {
report(t, "gunArcHigh: " + t.gunArcRight);
}
if(t.gunArcLeft < -360 || t.gunArcLeft > 0) {
report(t, "gunArcLow: " + t.gunArcLeft);
}
if(t.hullFront < 5) {
report(t, "hullFront: " + t.hullFront);
}
if(t.hullSide < 5) {
report(t, "hullSide: " + t.hullSide);
}
if(t.hullRear < 5) {
report(t, "hullRear: " + t.hullRear);
}
if(t.id.length() < 2) {
report(t, "id: " + t.id);
}
if(t.name.length() < 2) {
report(t, "name: " + t.name);
}
if(t.nation == null) {
report(t, "nation is null");
}
if(t.tier < 1 || t.tier > 12) {
report(t, "tier: " + t.tier);
}
if(t.speed < 5 || t.speed > 120) {
report(t, "topSpeed: " + t.speed);
}
if(t.type == null) {
report(t, "type is null");
}
checkTankEquipment(t, t.equipmentStock);
checkTankEquipment(t, t.equipmentTop);
}
/**
* Checks all equipment of a single tank
* @param t the tank to check
* @param eq the equipment to check
*/
protected void checkTankEquipment(Tank t, Equipment eq) {
if(eq.development == null) {
report(t, "development is null");
}
if(eq.gunElevationHigh < 1 || eq.gunElevationHigh > 90) {
report(t, "elevationHigh: " + eq.gunElevationHigh);
}
if(eq.gunElevationLow < -40 || eq.gunElevationLow > 50) {
report(t, "elevationLow: " + eq.gunElevationLow);
}
if(eq.hitpoints < 10 || eq.hitpoints > 10000) {
report(t, "hitpoints: " + eq.hitpoints);
}
if(eq.viewRange < 50 || eq.viewRange > 2000) {
report(t, "viewRange: " + eq.viewRange);
}
if(eq.weight < 1) {
report(t, "weight: " + eq.weight);
}
if(eq.weightLimit < 1) {
report(t, "weightLimit: " + eq.weightLimit);
}
}
/**
* Checks all fields of a single engine
* @param e the engine to check
*/
protected void checkModEngineFields(Engine e) {
String engine = String.format("Engine %s: ", e.name);
if(e.compatibility.isEmpty()) {
report(e, engine + "has no compatible tanks");
}
if(e.cost < 0) { // stock modules are for free!
report(e, engine + "cost: " + e.cost);
}
if(e.currency == null) {
report(e, engine + "currency is null");
}
if(e.name.length() < 2) {
report(e, engine + "name: " + e.name);
}
if(e.nation == null) {
report(e, engine + "nation is null");
}
if(e.tier < 1 || e.tier > 12) {
report(e, engine + "tier: " + e.tier);
}
if(e.weight < 1) {
report(e, engine + "weight: " + e.weight);
}
if(e.firechance < 0.01 || e.firechance > 100) {
report(e, engine + "firechance: " + e.firechance);
}
if(e.gas == null) {
report(e, engine + "gas is null");
}
if(e.power < 5 || e.power > 10000) {
report(e, engine + "power: " + e.power);
}
}
/**
* Checks all fields of a single gun
* @param g the gun to check
*/
protected void checkModGunFields(Gun g) {
String gun = String.format("Gun %s: ", g.name);
if(g.compatibility.isEmpty()) {
report(g, gun + "has no compatible tanks");
}
if(g.cost < 0) { // stock modules are for free!
report(g, gun + "cost: " + g.cost);
}
if(g.currency == null) {
report(g, gun + "currency is null");
}
if(g.name.length() < 2) {
report(g, gun + "name: " + g.name);
}
if(g.nation == null) {
report(g, gun + "nation is null");
}
if(g.tier < 1 || g.tier > 12) {
report(g, gun + "tier: " + g.tier);
}
if(g.weight < 1) {
report(g, gun + "weight: " + g.weight);
}
if(g.accuracyMax < 0.1) {
report(g, gun + "accuracyMax: " + g.accuracyMax);
}
if(g.accuracyMin < 0.1) {
report(g, gun + "accuracyMin: " + g.accuracyMin);
}
if(g.aimTimeMax < 0.5) {
report(g, gun + "aimTimeMax: " + g.aimTimeMax);
}
if(g.aimTimeMin < 0.5) {
report(g, gun + "aimTimeMin: " + g.aimTimeMin);
}
if(g.ammoCapacityMax < 1) {
report(g, gun + "ammoCapacityMax: " + g.ammoCapacityMax);
}
if(g.ammoCapacityMin < 1) {
report(g, gun + "ammoCapacityMin: " + g.ammoCapacityMin);
}
if(g.fireRateMax < 0.5) {
report(g, gun + "fireRateMax: " + g.fireRateMax);
}
if(g.fireRateMin < 0.5) {
report(g, gun + "fireRateMin: " + g.fireRateMin);
}
// skip damage and penetration (can be 0)
}
/**
* Checks all fields of a single radio
* @param g the radio to check
*/
protected void checkModRadioFields(Radio r) {
String radio = String.format("Radio %s: ", r.name);
if(r.compatibility.isEmpty()) {
report(r, radio + "has no compatible tanks");
}
if(r.cost < 0) { // stock modules are for free!
report(r, radio + "cost: " + r.cost);
}
if(r.currency == null) {
report(r, radio + "currency is null");
}
if(r.name.length() < 2) {
report(r, radio + "name: " + r.name);
}
if(r.nation == null) {
report(r, radio + "nation is null");
}
if(r.tier < 1 || r.tier > 12) {
report(r, radio + "tier: " + r.tier);
}
if(r.weight < 1) {
report(r, radio + "weight: " + r.weight);
}
if(r.range < 50 || r.range > 10000) {
report(r, radio + "range: " + r.range);
}
}
/**
* Checks all fields of a single suspension
* @param s the suspension to check
*/
protected void checkModSuspFields(Suspension s) {
String susp = String.format("Engine %s: ", s.name);
if(s.compatibility.isEmpty()) {
report(s, susp + "has no compatible tanks");
}
if(s.cost < 0) { // stock modules are for free!
report(s, susp + "cost: " + s.cost);
}
if(s.currency == null) {
report(s, susp + "currency is null");
}
if(s.name.length() < 2) {
report(s, susp + "name: " + s.name);
}
if(s.nation == null) {
report(s, susp + "nation is null");
}
if(s.tier < 1 || s.tier > 12) {
report(s, susp + "tier: " + s.tier);
}
if(s.weight < 1) {
report(s, susp + "weight: " + s.weight);
}
if(s.load < 1) {
report(s, susp + "load: " + s.load);
}
if(s.traverse < 1) {
report(s, susp + "traverse: " + s.traverse);
}
}
/**
* Checks all fields of a single turret
* @param t the turret to check
*/
protected void checkModTurretFields(Turret t) {
String turret = String.format("Engine %s: ", t.name);
if(t.compatibility.isEmpty()) {
report(t, turret + "has no compatible tanks");
}
if(t.cost < 0) { // stock modules are for free!
report(t, turret + "cost: " + t.cost);
}
if(t.currency == null) {
report(t, turret + "currency is null");
}
if(t.name.length() < 2) {
report(t, turret + "name: " + t.name);
}
if(t.nation == null) {
report(t, turret + "nation is null");
}
if(t.tier < 1 || t.tier > 12) {
report(t, turret + "tier: " + t.tier);
}
if(t.weight < 1) {
report(t, turret + "weight: " + t.weight);
}
if(t.armorFront < 1) {
report(t, turret + "armorFront: " + t.armorFront);
}
if(t.armorSide < 1) {
report(t, turret + "armorSide: " + t.armorSide);
}
if(t.armorRear < 1) {
report(t, turret + "armorRear: " + t.armorRear);
}
if(t.traverse < 1) {
report(t, turret + "traverse: " + t.traverse);
}
if(t.viewRange < 50 || t.viewRange > 10000) {
report(t, turret + "viewrange: " + t.viewRange);
}
}
// ------------ helpers ------------
/**
* Creates a report for a specific tank
* @param t the tank to report about
* @param report the contents of the report
*/
protected void report(Tank t, String report) {
if(tankReports.containsKey(t)) {
tankReports.get(t).add(report);
} else {
List<String> reports = new LinkedList<String>();
reports.add(report);
tankReports.put(t, reports);
}
}
/**
* Creates a report for a specific module
* @param m the module to report about
* @param report the contents of the report
*/
protected void report(Module m, String report) {
if(moduleReports.containsKey(m)) {
moduleReports.get(m).add(report);
} else {
List<String> reports = new LinkedList<String>();
reports.add(report);
moduleReports.put(m, reports);
}
}
// ------------ static stuff ------------
/**
* Creates and immediately prints a report for the given TankDB
* @param db the database to write a report about
*/
public static void printReportOf(TanksDB db) {
System.out.println(writeReportOf(db));
}
/**
* Creates a report for the given TankDB
* @param db the database to write a report about
*/
public static String writeReportOf(TanksDB db) {
ModuleMap mm = ModuleMap.build(db);
Evaluator eva = new Evaluator(db, mm);
eva.buildReport();
return eva.writeReport();
}
}