GameStatistics.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. /*
  2. * GameStatistics.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "GameStatistics.h"
  12. #include "../CPlayerState.h"
  13. #include "../constants/StringConstants.h"
  14. #include "../VCMIDirs.h"
  15. #include "CGameState.h"
  16. #include "TerrainHandler.h"
  17. #include "StartInfo.h"
  18. #include "HighScore.h"
  19. #include "../mapObjects/CGHeroInstance.h"
  20. #include "../mapObjects/CGTownInstance.h"
  21. #include "../mapObjects/CGObjectInstance.h"
  22. #include "../mapObjects/MiscObjects.h"
  23. #include "../mapping/CMap.h"
  24. #include "../entities/building/CBuilding.h"
  25. #include "../serializer/JsonDeserializer.h"
  26. #include "../serializer/JsonUpdater.h"
  27. #include "../entities/ResourceTypeHandler.h"
  28. VCMI_LIB_NAMESPACE_BEGIN
  29. void StatisticDataSet::add(StatisticDataSetEntry entry)
  30. {
  31. data.push_back(entry);
  32. }
  33. StatisticDataSet::PlayerAccumulatedValueStorage & StatisticDataSet::getPlayerAccumulator(PlayerColor player)
  34. {
  35. if(!player.isValidPlayer())
  36. throw std::runtime_error("Invalid player color " + std::to_string(player.getNum()) + " for statistics accumulator");
  37. return accumulatedValues[player];
  38. }
  39. const StatisticDataSet::PlayerAccumulatedValueStorage & StatisticDataSet::getPlayerAccumulator(PlayerColor player) const
  40. {
  41. if(!player.isValidPlayer())
  42. throw std::runtime_error("Invalid player color " + std::to_string(player.getNum()) + " for statistics accumulator");
  43. auto it = accumulatedValues.find(player);
  44. if(it == accumulatedValues.end())
  45. throw std::runtime_error("No statistics accumulator found for player " + std::to_string(player.getNum()));
  46. return it->second;
  47. }
  48. void StatisticDataSet::filterByTeam(const TeamState * team)
  49. {
  50. for(auto it = accumulatedValues.begin(); it != accumulatedValues.end();) {
  51. if (std::find(team->players.begin(), team->players.end(), it->first) == team->players.end())
  52. it = accumulatedValues.erase(it);
  53. else
  54. ++it;
  55. }
  56. data.erase(std::remove_if(data.begin(), data.end(), [&team](const StatisticDataSetEntry& entry) {
  57. return std::find(team->players.begin(), team->players.end(), entry.player) == team->players.end();
  58. }), data.end());
  59. }
  60. StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, const CGameState * gs, const StatisticDataSet & accumulatedData)
  61. {
  62. StatisticDataSetEntry data;
  63. HighScoreParameter param = HighScore::prepareHighScores(gs, ps->color, false);
  64. HighScoreCalculation scenarioHighScores;
  65. scenarioHighScores.parameters.push_back(param);
  66. scenarioHighScores.isCampaign = false;
  67. data.map = gs->getMap().name.toString();
  68. data.timestamp = std::time(nullptr);
  69. data.day = gs->getDate(Date::DAY);
  70. data.player = ps->color;
  71. data.playerName = gs->getStartInfo()->playerInfos.at(ps->color).name;
  72. data.team = ps->team;
  73. data.isHuman = ps->isHuman();
  74. data.status = ps->status;
  75. data.resources = ps->resources;
  76. data.numberHeroes = ps->getHeroes().size();
  77. data.numberTowns = gs->howManyTowns(ps->color);
  78. data.numberArtifacts = Statistic::getNumberOfArts(ps);
  79. data.numberDwellings = Statistic::getNumberOfDwellings(ps);
  80. data.armyStrength = Statistic::getArmyStrength(ps, true);
  81. data.totalExperience = Statistic::getTotalExperience(ps);
  82. data.income = Statistic::getIncome(gs, ps);
  83. data.mapExploredRatio = Statistic::getMapExploredRatio(gs, ps->color);
  84. data.obeliskVisitedRatio = Statistic::getObeliskVisitedRatio(gs, ps->team);
  85. data.townBuiltRatio = Statistic::getTownBuiltRatio(ps);
  86. data.hasGrail = param.hasGrail;
  87. data.numMines = Statistic::getNumMines(gs, ps);
  88. data.score = scenarioHighScores.calculate().total;
  89. data.maxHeroLevel = Statistic::findBestHero(gs, ps->color) ? Statistic::findBestHero(gs, ps->color)->level : 0;
  90. data.numBattlesNeutral = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numBattlesNeutral : 0;
  91. data.numBattlesPlayer = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numBattlesPlayer : 0;
  92. data.numWinBattlesNeutral = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numWinBattlesNeutral : 0;
  93. data.numWinBattlesPlayer = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numWinBattlesPlayer : 0;
  94. data.numHeroSurrendered = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numHeroSurrendered : 0;
  95. data.numHeroEscaped = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numHeroEscaped : 0;
  96. data.spentResourcesForArmy = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources();
  97. data.spentResourcesForBuildings = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources();
  98. data.tradeVolume = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).tradeVolume : TResources();
  99. data.eventCapturedTown = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).lastCapturedTownDay == gs->getDate(Date::DAY) : false;
  100. data.eventDefeatedStrongestHero = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).lastDefeatedStrongestHeroDay == gs->getDate(Date::DAY) : false;
  101. data.movementPointsUsed = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).movementPointsUsed : 0;
  102. return data;
  103. }
  104. void StatisticDataSetEntry::serializeJson(JsonSerializeFormat & handler)
  105. {
  106. handler.serializeString("map", map);
  107. handler.serializeInt("timestamp", timestamp);
  108. handler.serializeInt("day", day);
  109. handler.serializeId("player", player, PlayerColor::CANNOT_DETERMINE);
  110. handler.serializeString("playerName", playerName);
  111. handler.serializeInt("team", team);
  112. handler.serializeBool("isHuman", isHuman);
  113. handler.serializeEnum("status", status, {"ingame", "loser", "winner"});
  114. resources.serializeJson(handler, "resources");
  115. handler.serializeInt("numberHeroes", numberHeroes);
  116. handler.serializeInt("numberTowns", numberTowns);
  117. handler.serializeInt("numberArtifacts", numberArtifacts);
  118. handler.serializeInt("numberDwellings", numberDwellings);
  119. handler.serializeInt("armyStrength", armyStrength);
  120. handler.serializeInt("totalExperience", totalExperience);
  121. handler.serializeInt("income", income);
  122. handler.serializeFloat("mapExploredRatio", mapExploredRatio);
  123. handler.serializeFloat("obeliskVisitedRatio", obeliskVisitedRatio);
  124. handler.serializeFloat("townBuiltRatio", townBuiltRatio);
  125. handler.serializeBool("hasGrail", hasGrail);
  126. {
  127. auto zonesData = handler.enterStruct("numMines");
  128. for(auto & idx : LIBRARY->resourceTypeHandler->getAllObjects())
  129. handler.serializeInt(idx.toResource()->getJsonKey(), numMines[idx], 0);
  130. }
  131. handler.serializeInt("score", score);
  132. handler.serializeInt("maxHeroLevel", maxHeroLevel);
  133. handler.serializeInt("numBattlesNeutral", numBattlesNeutral);
  134. handler.serializeInt("numBattlesPlayer", numBattlesPlayer);
  135. handler.serializeInt("numWinBattlesNeutral", numWinBattlesNeutral);
  136. handler.serializeInt("numWinBattlesPlayer", numWinBattlesPlayer);
  137. handler.serializeInt("numHeroSurrendered", numHeroSurrendered);
  138. handler.serializeInt("numHeroEscaped", numHeroEscaped);
  139. spentResourcesForArmy.serializeJson(handler, "spentResourcesForArmy");
  140. spentResourcesForBuildings.serializeJson(handler, "spentResourcesForBuildings");
  141. tradeVolume.serializeJson(handler, "tradeVolume");
  142. handler.serializeBool("eventCapturedTown", eventCapturedTown);
  143. handler.serializeBool("eventDefeatedStrongestHero", eventDefeatedStrongestHero);
  144. handler.serializeInt("movementPointsUsed", movementPointsUsed);
  145. }
  146. void StatisticDataSet::PlayerAccumulatedValueStorage::serializeJson(JsonSerializeFormat & handler)
  147. {
  148. handler.serializeInt("numBattlesNeutral", numBattlesNeutral);
  149. handler.serializeInt("numBattlesPlayer", numBattlesPlayer);
  150. handler.serializeInt("numWinBattlesNeutral", numWinBattlesNeutral);
  151. handler.serializeInt("numWinBattlesPlayer", numWinBattlesPlayer);
  152. handler.serializeInt("numHeroSurrendered", numHeroSurrendered);
  153. handler.serializeInt("numHeroEscaped", numHeroEscaped);
  154. spentResourcesForArmy.serializeJson(handler, "spentResourcesForArmy");
  155. spentResourcesForBuildings.serializeJson(handler, "spentResourcesForBuildings");
  156. tradeVolume.serializeJson(handler, "tradeVolume");
  157. handler.serializeInt("movementPointsUsed", movementPointsUsed);
  158. handler.serializeInt("lastCapturedTownDay", lastCapturedTownDay);
  159. handler.serializeInt("lastDefeatedStrongestHeroDay", lastDefeatedStrongestHeroDay);
  160. }
  161. void StatisticDataSet::serializeJson(JsonSerializeFormat & handler)
  162. {
  163. {
  164. auto eventsHandler = handler.enterArray("data");
  165. eventsHandler.syncSize(data, JsonNode::JsonType::DATA_VECTOR);
  166. eventsHandler.serializeStruct(data);
  167. }
  168. {
  169. auto eventsHandler = handler.enterStruct("accumulatedValues");
  170. for(auto & val : accumulatedValues)
  171. eventsHandler->serializeStruct(GameConstants::PLAYER_COLOR_NAMES[val.first], val.second);
  172. }
  173. }
  174. std::string StatisticDataSet::toCsv(std::string sep) const
  175. {
  176. std::stringstream ss;
  177. auto resources = LIBRARY->resourceTypeHandler->getAllObjects();
  178. ss << "Map" << sep;
  179. ss << "Timestamp" << sep;
  180. ss << "Day" << sep;
  181. ss << "Player" << sep;
  182. ss << "PlayerName" << sep;
  183. ss << "Team" << sep;
  184. ss << "IsHuman" << sep;
  185. ss << "Status" << sep;
  186. ss << "NumberHeroes" << sep;
  187. ss << "NumberTowns" << sep;
  188. ss << "NumberArtifacts" << sep;
  189. ss << "NumberDwellings" << sep;
  190. ss << "ArmyStrength" << sep;
  191. ss << "TotalExperience" << sep;
  192. ss << "Income" << sep;
  193. ss << "MapExploredRatio" << sep;
  194. ss << "ObeliskVisitedRatio" << sep;
  195. ss << "TownBuiltRatio" << sep;
  196. ss << "HasGrail" << sep;
  197. ss << "Score" << sep;
  198. ss << "MaxHeroLevel" << sep;
  199. ss << "NumBattlesNeutral" << sep;
  200. ss << "NumBattlesPlayer" << sep;
  201. ss << "NumWinBattlesNeutral" << sep;
  202. ss << "NumWinBattlesPlayer" << sep;
  203. ss << "NumHeroSurrendered" << sep;
  204. ss << "NumHeroEscaped" << sep;
  205. ss << "EventCapturedTown" << sep;
  206. ss << "EventDefeatedStrongestHero" << sep;
  207. ss << "MovementPointsUsed";
  208. for(auto & resource : resources)
  209. ss << sep << resource.toResource()->getJsonKey();
  210. for(auto & resource : resources)
  211. ss << sep << resource.toResource()->getJsonKey() + "Mines";
  212. for(auto & resource : resources)
  213. ss << sep << resource.toResource()->getJsonKey() + "SpentResourcesForArmy";
  214. for(auto & resource : resources)
  215. ss << sep << resource.toResource()->getJsonKey() + "SpentResourcesForBuildings";
  216. for(auto & resource : resources)
  217. ss << sep << resource.toResource()->getJsonKey() + "TradeVolume";
  218. ss << "\r\n";
  219. for(auto & entry : data)
  220. {
  221. ss << entry.map << sep;
  222. ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << sep;
  223. ss << entry.day << sep;
  224. ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << sep;
  225. ss << entry.playerName << sep;
  226. ss << entry.team.getNum() << sep;
  227. ss << entry.isHuman << sep;
  228. ss << static_cast<int>(entry.status) << sep;
  229. ss << entry.numberHeroes << sep;
  230. ss << entry.numberTowns << sep;
  231. ss << entry.numberArtifacts << sep;
  232. ss << entry.numberDwellings << sep;
  233. ss << entry.armyStrength << sep;
  234. ss << entry.totalExperience << sep;
  235. ss << entry.income << sep;
  236. ss << entry.mapExploredRatio << sep;
  237. ss << entry.obeliskVisitedRatio << sep;
  238. ss << entry.townBuiltRatio << sep;
  239. ss << entry.hasGrail << sep;
  240. ss << entry.score << sep;
  241. ss << entry.maxHeroLevel << sep;
  242. ss << entry.numBattlesNeutral << sep;
  243. ss << entry.numBattlesPlayer << sep;
  244. ss << entry.numWinBattlesNeutral << sep;
  245. ss << entry.numWinBattlesPlayer << sep;
  246. ss << entry.numHeroSurrendered << sep;
  247. ss << entry.numHeroEscaped << sep;
  248. ss << entry.eventCapturedTown << sep;
  249. ss << entry.eventDefeatedStrongestHero << sep;
  250. ss << entry.movementPointsUsed;
  251. for(auto & resource : resources)
  252. ss << sep << entry.resources[resource];
  253. for(auto & resource : resources)
  254. ss << sep << entry.numMines.at(resource);
  255. for(auto & resource : resources)
  256. ss << sep << entry.spentResourcesForArmy[resource];
  257. for(auto & resource : resources)
  258. ss << sep << entry.spentResourcesForBuildings[resource];
  259. for(auto & resource : resources)
  260. ss << sep << entry.tradeVolume[resource];
  261. ss << "\r\n";
  262. }
  263. return ss.str();
  264. }
  265. std::string StatisticDataSet::writeCsv() const
  266. {
  267. const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic";
  268. boost::filesystem::create_directories(outPath);
  269. const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv");
  270. std::ofstream file(filePath.c_str());
  271. std::string csv = toCsv(";");
  272. file << csv;
  273. return filePath.string();
  274. }
  275. //calculates total number of artifacts that belong to given player
  276. int Statistic::getNumberOfArts(const PlayerState * ps)
  277. {
  278. int ret = 0;
  279. for(auto h : ps->getHeroes())
  280. {
  281. ret += h->artifactsInBackpack.size() + h->artifactsWorn.size();
  282. }
  283. return ret;
  284. }
  285. int Statistic::getNumberOfDwellings(const PlayerState * ps)
  286. {
  287. int ret = 0;
  288. for(const auto * obj : ps->getOwnedObjects())
  289. if (!obj->asOwnable()->providedCreatures().empty())
  290. ret += 1;
  291. return ret;
  292. }
  293. // get total strength of player army
  294. si64 Statistic::getArmyStrength(const PlayerState * ps, bool withTownGarrison)
  295. {
  296. si64 str = 0;
  297. for(auto h : ps->getHeroes())
  298. {
  299. if(!h->isGarrisoned() || withTownGarrison) //original h3 behavior
  300. str += h->getArmyStrength();
  301. }
  302. return str;
  303. }
  304. // get total experience of all heroes
  305. si64 Statistic::getTotalExperience(const PlayerState * ps)
  306. {
  307. si64 tmp = 0;
  308. for(auto h : ps->getHeroes())
  309. tmp += h->exp;
  310. return tmp;
  311. }
  312. // get total gold income
  313. int Statistic::getIncome(const CGameState * gs, const PlayerState * ps)
  314. {
  315. int totalIncome = 0;
  316. //Heroes can produce gold as well - skill, specialty or arts
  317. for(const auto & object : ps->getOwnedObjects())
  318. totalIncome += object->asOwnable()->dailyIncome()[EGameResID::GOLD];
  319. return totalIncome;
  320. }
  321. float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player)
  322. {
  323. float visible = 0.0;
  324. float numTiles = 0.0;
  325. for(int layer = 0; layer < gs->getMap().levels(); layer++)
  326. for(int y = 0; y < gs->getMap().height; ++y)
  327. for(int x = 0; x < gs->getMap().width; ++x)
  328. {
  329. TerrainTile tile = gs->getMap().getTile(int3(x, y, layer));
  330. if(tile.blocked() && !tile.visitable())
  331. continue;
  332. if(gs->isVisibleFor(int3(x, y, layer), player))
  333. visible++;
  334. numTiles++;
  335. }
  336. return visible / numTiles;
  337. }
  338. const CGHeroInstance * Statistic::findBestHero(const CGameState * gs, const PlayerColor & color)
  339. {
  340. const auto &h = gs->players.at(color).getHeroes();
  341. if(h.empty())
  342. return nullptr;
  343. //best hero will be that with highest exp
  344. int best = 0;
  345. for(int b=1; b<h.size(); ++b)
  346. {
  347. if(h[b]->exp > h[best]->exp)
  348. {
  349. best = b;
  350. }
  351. }
  352. return h[best];
  353. }
  354. std::vector<std::vector<PlayerColor>> Statistic::getRank(std::vector<std::pair<PlayerColor, si64>> stats)
  355. {
  356. std::sort(stats.begin(), stats.end(), [](const std::pair<PlayerColor, si64> & a, const std::pair<PlayerColor, si64> & b) { return a.second > b.second; });
  357. //put first element
  358. std::vector< std::vector<PlayerColor> > ret;
  359. ret.push_back( { stats[0].first } );
  360. //the rest of elements
  361. for(int g=1; g<stats.size(); ++g)
  362. {
  363. if(stats[g].second == stats[g-1].second)
  364. {
  365. (ret.end()-1)->push_back( stats[g].first );
  366. }
  367. else
  368. {
  369. //create next occupied rank
  370. ret.push_back( { stats[g].first });
  371. }
  372. }
  373. return ret;
  374. }
  375. int Statistic::getObeliskVisited(const CGameState * gs, const TeamID & t)
  376. {
  377. if(gs->getMap().obelisksVisited.count(t))
  378. return gs->getMap().obelisksVisited.at(t);
  379. else
  380. return 0;
  381. }
  382. float Statistic::getObeliskVisitedRatio(const CGameState * gs, const TeamID & t)
  383. {
  384. if(!gs->getMap().obeliskCount)
  385. return 0;
  386. return static_cast<float>(getObeliskVisited(gs, t)) / gs->getMap().obeliskCount;
  387. }
  388. std::map<EGameResID, int> Statistic::getNumMines(const CGameState * gs, const PlayerState * ps)
  389. {
  390. std::map<EGameResID, int> tmp;
  391. for(auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
  392. tmp[res] = 0;
  393. for(const auto * object : ps->getOwnedObjects())
  394. {
  395. //Mines
  396. if(object->ID == Obj::MINE || object->ID == Obj::ABANDONED_MINE)
  397. {
  398. const auto * mine = dynamic_cast<const CGMine *>(object);
  399. assert(mine);
  400. tmp[mine->producedResource]++;
  401. }
  402. }
  403. return tmp;
  404. }
  405. float Statistic::getTownBuiltRatio(const PlayerState * ps)
  406. {
  407. float built = 0.0;
  408. float total = 0.0;
  409. for(const auto & t : ps->getTowns())
  410. {
  411. built += t->getBuildings().size();
  412. for(const auto & b : t->getTown()->buildings)
  413. if(!t->forbiddenBuildings.count(b.first))
  414. total += 1;
  415. }
  416. if(total < 1)
  417. return 0;
  418. return built / total;
  419. }
  420. VCMI_LIB_NAMESPACE_END