1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-10-08 23:22:25 +02:00
Files
vcmi/lib/gameState/GameStatistics.cpp

444 lines
16 KiB
C++
Raw Normal View History

2024-07-27 02:11:26 +02:00
/*
* GameStatistics.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "GameStatistics.h"
2024-08-01 21:30:53 +02:00
#include "../CPlayerState.h"
#include "../constants/StringConstants.h"
2024-08-12 20:14:36 +02:00
#include "../VCMIDirs.h"
2024-08-01 21:30:53 +02:00
#include "CGameState.h"
2024-08-01 23:21:41 +02:00
#include "TerrainHandler.h"
2024-08-01 23:56:06 +02:00
#include "StartInfo.h"
2024-08-02 19:37:46 +02:00
#include "HighScore.h"
2024-08-01 22:36:32 +02:00
#include "../mapObjects/CGHeroInstance.h"
#include "../mapObjects/CGTownInstance.h"
#include "../mapObjects/CGObjectInstance.h"
#include "../mapObjects/MiscObjects.h"
#include "../mapping/CMap.h"
2024-08-03 19:53:05 +02:00
#include "../entities/building/CBuilding.h"
2025-07-05 13:51:27 +02:00
#include "../serializer/JsonDeserializer.h"
#include "../serializer/JsonUpdater.h"
2025-09-14 15:29:14 +02:00
#include "../entities/ResourceTypeHandler.h"
2024-08-03 19:53:05 +02:00
2024-07-27 02:11:26 +02:00
VCMI_LIB_NAMESPACE_BEGIN
void StatisticDataSet::add(StatisticDataSetEntry entry)
{
data.push_back(entry);
}
StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, const CGameState * gs, const StatisticDataSet & accumulatedData)
2024-08-01 21:30:53 +02:00
{
StatisticDataSetEntry data;
2024-08-02 19:37:46 +02:00
HighScoreParameter param = HighScore::prepareHighScores(gs, ps->color, false);
HighScoreCalculation scenarioHighScores;
scenarioHighScores.parameters.push_back(param);
scenarioHighScores.isCampaign = false;
data.map = gs->getMap().name.toString();
data.timestamp = std::time(nullptr);
2024-08-01 21:30:53 +02:00
data.day = gs->getDate(Date::DAY);
data.player = ps->color;
2024-08-14 02:49:54 +02:00
data.playerName = gs->getStartInfo()->playerInfos.at(ps->color).name;
2024-08-01 21:30:53 +02:00
data.team = ps->team;
2024-08-01 23:21:41 +02:00
data.isHuman = ps->isHuman();
data.status = ps->status;
2024-08-01 21:30:53 +02:00
data.resources = ps->resources;
data.numberHeroes = ps->getHeroes().size();
2024-08-02 19:37:46 +02:00
data.numberTowns = gs->howManyTowns(ps->color);
2024-08-01 22:36:32 +02:00
data.numberArtifacts = Statistic::getNumberOfArts(ps);
data.numberDwellings = Statistic::getNumberOfDwellings(ps);
2024-08-02 01:18:39 +02:00
data.armyStrength = Statistic::getArmyStrength(ps, true);
2024-08-03 18:48:45 +02:00
data.totalExperience = Statistic::getTotalExperience(ps);
2024-08-01 23:56:06 +02:00
data.income = Statistic::getIncome(gs, ps);
2024-08-03 17:55:43 +02:00
data.mapExploredRatio = Statistic::getMapExploredRatio(gs, ps->color);
data.obeliskVisitedRatio = Statistic::getObeliskVisitedRatio(gs, ps->team);
2024-08-03 19:53:05 +02:00
data.townBuiltRatio = Statistic::getTownBuiltRatio(ps);
data.hasGrail = param.hasGrail;
2024-08-02 01:18:39 +02:00
data.numMines = Statistic::getNumMines(gs, ps);
2024-08-02 19:37:46 +02:00
data.score = scenarioHighScores.calculate().total;
2024-08-02 20:40:24 +02:00
data.maxHeroLevel = Statistic::findBestHero(gs, ps->color) ? Statistic::findBestHero(gs, ps->color)->level : 0;
data.numBattlesNeutral = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numBattlesNeutral : 0;
data.numBattlesPlayer = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numBattlesPlayer : 0;
data.numWinBattlesNeutral = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numWinBattlesNeutral : 0;
data.numWinBattlesPlayer = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numWinBattlesPlayer : 0;
data.numHeroSurrendered = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numHeroSurrendered : 0;
data.numHeroEscaped = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).numHeroEscaped : 0;
data.spentResourcesForArmy = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources();
data.spentResourcesForBuildings = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources();
data.tradeVolume = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).tradeVolume : TResources();
data.eventCapturedTown = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).lastCapturedTownDay == gs->getDate(Date::DAY) : false;
data.eventDefeatedStrongestHero = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).lastDefeatedStrongestHeroDay == gs->getDate(Date::DAY) : false;
data.movementPointsUsed = accumulatedData.accumulatedValues.count(ps->color) ? accumulatedData.accumulatedValues.at(ps->color).movementPointsUsed : 0;
2024-08-01 21:30:53 +02:00
return data;
}
2025-07-05 13:51:27 +02:00
void StatisticDataSetEntry::serializeJson(JsonSerializeFormat & handler)
{
handler.serializeString("map", map);
handler.serializeInt("timestamp", timestamp);
handler.serializeInt("day", day);
handler.serializeId("player", player, PlayerColor::CANNOT_DETERMINE);
handler.serializeString("playerName", playerName);
2025-07-05 15:02:16 +02:00
handler.serializeInt("team", team);
2025-07-05 13:51:27 +02:00
handler.serializeBool("isHuman", isHuman);
2025-07-05 15:02:16 +02:00
handler.serializeEnum("status", status, {"ingame", "loser", "winner"});
2025-07-05 13:51:27 +02:00
resources.serializeJson(handler, "resources");
handler.serializeInt("numberHeroes", numberHeroes);
handler.serializeInt("numberTowns", numberTowns);
handler.serializeInt("numberArtifacts", numberArtifacts);
handler.serializeInt("numberDwellings", numberDwellings);
handler.serializeInt("armyStrength", armyStrength);
handler.serializeInt("totalExperience", totalExperience);
handler.serializeInt("income", income);
handler.serializeFloat("mapExploredRatio", mapExploredRatio);
handler.serializeFloat("obeliskVisitedRatio", obeliskVisitedRatio);
handler.serializeFloat("townBuiltRatio", townBuiltRatio);
handler.serializeBool("hasGrail", hasGrail);
{
auto zonesData = handler.enterStruct("numMines");
for(auto & idx : LIBRARY->resourceTypeHandler->getAllObjects())
if(idx != GameResID::MITHRIL)
2025-09-15 00:08:18 +02:00
handler.serializeInt(idx.toResource()->getJsonKey(), numMines[idx], 0);
2025-07-05 13:51:27 +02:00
}
handler.serializeInt("score", score);
handler.serializeInt("maxHeroLevel", maxHeroLevel);
handler.serializeInt("numBattlesNeutral", numBattlesNeutral);
handler.serializeInt("numBattlesPlayer", numBattlesPlayer);
handler.serializeInt("numWinBattlesNeutral", numWinBattlesNeutral);
handler.serializeInt("numWinBattlesPlayer", numWinBattlesPlayer);
handler.serializeInt("numHeroSurrendered", numHeroSurrendered);
handler.serializeInt("numHeroEscaped", numHeroEscaped);
spentResourcesForArmy.serializeJson(handler, "spentResourcesForArmy");
spentResourcesForBuildings.serializeJson(handler, "spentResourcesForBuildings");
tradeVolume.serializeJson(handler, "tradeVolume");
handler.serializeBool("eventCapturedTown", eventCapturedTown);
handler.serializeBool("eventDefeatedStrongestHero", eventDefeatedStrongestHero);
handler.serializeInt("movementPointsUsed", movementPointsUsed);
}
void StatisticDataSet::PlayerAccumulatedValueStorage::serializeJson(JsonSerializeFormat & handler)
{
handler.serializeInt("numBattlesNeutral", numBattlesNeutral);
handler.serializeInt("numBattlesPlayer", numBattlesPlayer);
handler.serializeInt("numWinBattlesNeutral", numWinBattlesNeutral);
handler.serializeInt("numWinBattlesPlayer", numWinBattlesPlayer);
handler.serializeInt("numHeroSurrendered", numHeroSurrendered);
handler.serializeInt("numHeroEscaped", numHeroEscaped);
spentResourcesForArmy.serializeJson(handler, "spentResourcesForArmy");
spentResourcesForBuildings.serializeJson(handler, "spentResourcesForBuildings");
tradeVolume.serializeJson(handler, "tradeVolume");
handler.serializeInt("movementPointsUsed", movementPointsUsed);
handler.serializeInt("lastCapturedTownDay", lastCapturedTownDay);
handler.serializeInt("lastDefeatedStrongestHeroDay", lastDefeatedStrongestHeroDay);
}
void StatisticDataSet::serializeJson(JsonSerializeFormat & handler)
{
{
auto eventsHandler = handler.enterArray("data");
eventsHandler.syncSize(data, JsonNode::JsonType::DATA_VECTOR);
eventsHandler.serializeStruct(data);
}
{
auto eventsHandler = handler.enterStruct("accumulatedValues");
for(auto & val : accumulatedValues)
eventsHandler->serializeStruct(GameConstants::PLAYER_COLOR_NAMES[val.first], val.second);
}
}
std::string StatisticDataSet::toCsv(std::string sep) const
2024-07-27 02:11:26 +02:00
{
std::stringstream ss;
2024-08-01 21:30:53 +02:00
auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
2024-08-14 21:01:37 +02:00
ss << "Map" << sep;
ss << "Timestamp" << sep;
ss << "Day" << sep;
ss << "Player" << sep;
ss << "PlayerName" << sep;
ss << "Team" << sep;
ss << "IsHuman" << sep;
ss << "Status" << sep;
ss << "NumberHeroes" << sep;
ss << "NumberTowns" << sep;
ss << "NumberArtifacts" << sep;
ss << "NumberDwellings" << sep;
ss << "ArmyStrength" << sep;
ss << "TotalExperience" << sep;
ss << "Income" << sep;
ss << "MapExploredRatio" << sep;
ss << "ObeliskVisitedRatio" << sep;
ss << "TownBuiltRatio" << sep;
ss << "HasGrail" << sep;
ss << "Score" << sep;
ss << "MaxHeroLevel" << sep;
ss << "NumBattlesNeutral" << sep;
ss << "NumBattlesPlayer" << sep;
ss << "NumWinBattlesNeutral" << sep;
ss << "NumWinBattlesPlayer" << sep;
ss << "NumHeroSurrendered" << sep;
ss << "NumHeroEscaped" << sep;
ss << "EventCapturedTown" << sep;
ss << "EventDefeatedStrongestHero" << sep;
ss << "MovementPointsUsed";
2024-08-01 21:30:53 +02:00
for(auto & resource : resources)
2025-09-15 00:08:18 +02:00
ss << sep << resource.toResource()->getJsonKey();
2024-08-02 01:18:39 +02:00
for(auto & resource : resources)
2025-09-15 00:08:18 +02:00
ss << sep << resource.toResource()->getJsonKey() + "Mines";
2024-08-03 19:53:05 +02:00
for(auto & resource : resources)
2025-09-15 00:08:18 +02:00
ss << sep << resource.toResource()->getJsonKey() + "SpentResourcesForArmy";
2024-08-03 19:53:05 +02:00
for(auto & resource : resources)
2025-09-15 00:08:18 +02:00
ss << sep << resource.toResource()->getJsonKey() + "SpentResourcesForBuildings";
2024-08-03 19:53:05 +02:00
for(auto & resource : resources)
2025-09-15 00:08:18 +02:00
ss << sep << resource.toResource()->getJsonKey() + "TradeVolume";
2024-08-01 21:30:53 +02:00
ss << "\r\n";
2024-07-27 02:11:26 +02:00
for(auto & entry : data)
{
2024-08-14 21:01:37 +02:00
ss << entry.map << sep;
ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << sep;
ss << entry.day << sep;
ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << sep;
ss << entry.playerName << sep;
ss << entry.team.getNum() << sep;
ss << entry.isHuman << sep;
ss << static_cast<int>(entry.status) << sep;
ss << entry.numberHeroes << sep;
ss << entry.numberTowns << sep;
ss << entry.numberArtifacts << sep;
ss << entry.numberDwellings << sep;
ss << entry.armyStrength << sep;
ss << entry.totalExperience << sep;
ss << entry.income << sep;
ss << entry.mapExploredRatio << sep;
ss << entry.obeliskVisitedRatio << sep;
ss << entry.townBuiltRatio << sep;
ss << entry.hasGrail << sep;
ss << entry.score << sep;
ss << entry.maxHeroLevel << sep;
ss << entry.numBattlesNeutral << sep;
ss << entry.numBattlesPlayer << sep;
ss << entry.numWinBattlesNeutral << sep;
ss << entry.numWinBattlesPlayer << sep;
ss << entry.numHeroSurrendered << sep;
ss << entry.numHeroEscaped << sep;
ss << entry.eventCapturedTown << sep;
ss << entry.eventDefeatedStrongestHero << sep;
ss << entry.movementPointsUsed;
2024-08-01 21:30:53 +02:00
for(auto & resource : resources)
2024-08-14 21:01:37 +02:00
ss << sep << entry.resources[resource];
2024-08-02 01:18:39 +02:00
for(auto & resource : resources)
ss << sep << entry.numMines.at(resource);
2024-08-03 19:53:05 +02:00
for(auto & resource : resources)
2024-08-14 21:01:37 +02:00
ss << sep << entry.spentResourcesForArmy[resource];
2024-08-03 19:53:05 +02:00
for(auto & resource : resources)
2024-08-14 21:01:37 +02:00
ss << sep << entry.spentResourcesForBuildings[resource];
2024-08-03 19:53:05 +02:00
for(auto & resource : resources)
2024-08-14 21:01:37 +02:00
ss << sep << entry.tradeVolume[resource];
2024-08-01 21:30:53 +02:00
ss << "\r\n";
2024-07-27 02:11:26 +02:00
}
return ss.str();
}
std::string StatisticDataSet::writeCsv() const
2024-08-12 20:14:36 +02:00
{
const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic";
boost::filesystem::create_directories(outPath);
const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv");
std::ofstream file(filePath.c_str());
2024-08-14 21:01:37 +02:00
std::string csv = toCsv(";");
2024-08-12 20:14:36 +02:00
file << csv;
return filePath.string();
}
2024-08-01 22:36:32 +02:00
//calculates total number of artifacts that belong to given player
int Statistic::getNumberOfArts(const PlayerState * ps)
{
int ret = 0;
for(auto h : ps->getHeroes())
2024-08-01 22:36:32 +02:00
{
ret += h->artifactsInBackpack.size() + h->artifactsWorn.size();
2024-08-01 22:36:32 +02:00
}
return ret;
}
int Statistic::getNumberOfDwellings(const PlayerState * ps)
{
int ret = 0;
for(const auto * obj : ps->getOwnedObjects())
if (!obj->asOwnable()->providedCreatures().empty())
ret += 1;
return ret;
}
2024-08-01 22:36:32 +02:00
// get total strength of player army
2024-08-02 01:18:39 +02:00
si64 Statistic::getArmyStrength(const PlayerState * ps, bool withTownGarrison)
2024-08-01 22:36:32 +02:00
{
si64 str = 0;
for(auto h : ps->getHeroes())
2024-08-01 22:36:32 +02:00
{
if(!h->isGarrisoned() || withTownGarrison) //original h3 behavior
2024-08-01 22:36:32 +02:00
str += h->getArmyStrength();
}
return str;
}
2024-08-03 18:48:45 +02:00
// get total experience of all heroes
si64 Statistic::getTotalExperience(const PlayerState * ps)
{
si64 tmp = 0;
for(auto h : ps->getHeroes())
2024-08-03 18:48:45 +02:00
tmp += h->exp;
return tmp;
}
2024-08-01 22:36:32 +02:00
// get total gold income
2024-08-01 23:56:06 +02:00
int Statistic::getIncome(const CGameState * gs, const PlayerState * ps)
2024-08-01 22:36:32 +02:00
{
int totalIncome = 0;
//Heroes can produce gold as well - skill, specialty or arts
2025-03-13 19:42:18 +00:00
for(const auto & object : ps->getOwnedObjects())
totalIncome += object->asOwnable()->dailyIncome()[EGameResID::GOLD];
2024-08-01 22:36:32 +02:00
return totalIncome;
}
2024-08-03 17:55:43 +02:00
float Statistic::getMapExploredRatio(const CGameState * gs, PlayerColor player)
2024-08-01 23:21:41 +02:00
{
2024-08-03 17:55:43 +02:00
float visible = 0.0;
float numTiles = 0.0;
2024-08-01 23:21:41 +02:00
2025-08-01 00:37:32 +02:00
for(int layer = 0; layer < gs->getMap().levels(); layer++)
for(int y = 0; y < gs->getMap().height; ++y)
for(int x = 0; x < gs->getMap().width; ++x)
2024-08-01 23:21:41 +02:00
{
TerrainTile tile = gs->getMap().getTile(int3(x, y, layer));
2024-08-01 23:21:41 +02:00
2024-07-15 07:41:53 +00:00
if(tile.blocked() && !tile.visitable())
2024-08-01 23:21:41 +02:00
continue;
if(gs->isVisibleFor(int3(x, y, layer), player))
2024-08-01 23:21:41 +02:00
visible++;
numTiles++;
}
2024-08-01 23:56:06 +02:00
return visible / numTiles;
2024-08-01 23:21:41 +02:00
}
2024-08-02 19:38:33 +02:00
const CGHeroInstance * Statistic::findBestHero(const CGameState * gs, const PlayerColor & color)
2024-08-02 00:04:41 +02:00
{
const auto &h = gs->players.at(color).getHeroes();
2024-08-02 00:04:41 +02:00
if(h.empty())
return nullptr;
//best hero will be that with highest exp
int best = 0;
for(int b=1; b<h.size(); ++b)
{
if(h[b]->exp > h[best]->exp)
{
best = b;
}
}
return h[best];
}
2024-08-03 17:55:43 +02:00
std::vector<std::vector<PlayerColor>> Statistic::getRank(std::vector<std::pair<PlayerColor, si64>> stats)
2024-08-02 00:04:41 +02:00
{
2024-08-03 17:55:43 +02:00
std::sort(stats.begin(), stats.end(), [](const std::pair<PlayerColor, si64> & a, const std::pair<PlayerColor, si64> & b) { return a.second > b.second; });
2024-08-02 00:04:41 +02:00
//put first element
std::vector< std::vector<PlayerColor> > ret;
ret.push_back( { stats[0].first } );
2024-08-02 00:04:41 +02:00
//the rest of elements
for(int g=1; g<stats.size(); ++g)
{
if(stats[g].second == stats[g-1].second)
{
(ret.end()-1)->push_back( stats[g].first );
}
else
{
//create next occupied rank
ret.push_back( { stats[g].first });
2024-08-02 00:04:41 +02:00
}
}
return ret;
}
2024-08-02 01:18:39 +02:00
int Statistic::getObeliskVisited(const CGameState * gs, const TeamID & t)
{
if(gs->getMap().obelisksVisited.count(t))
return gs->getMap().obelisksVisited.at(t);
2024-08-02 01:18:39 +02:00
else
return 0;
}
2024-08-03 17:55:43 +02:00
float Statistic::getObeliskVisitedRatio(const CGameState * gs, const TeamID & t)
2024-08-02 01:18:39 +02:00
{
if(!gs->getMap().obeliskCount)
2024-08-03 17:55:43 +02:00
return 0;
return static_cast<float>(getObeliskVisited(gs, t)) / gs->getMap().obeliskCount;
2024-08-02 01:18:39 +02:00
}
std::map<EGameResID, int> Statistic::getNumMines(const CGameState * gs, const PlayerState * ps)
{
std::map<EGameResID, int> tmp;
2025-09-14 15:29:14 +02:00
for(auto & res : LIBRARY->resourceTypeHandler->getAllObjects())
2024-08-02 01:18:39 +02:00
tmp[res] = 0;
2025-03-13 19:42:18 +00:00
for(const auto * object : ps->getOwnedObjects())
{
//Mines
if(object->ID == Obj::MINE || object->ID == Obj::ABANDONED_MINE)
2025-03-13 19:42:18 +00:00
{
const auto * mine = dynamic_cast<const CGMine *>(object);
assert(mine);
tmp[mine->producedResource]++;
}
}
2024-08-02 01:18:39 +02:00
return tmp;
}
2024-08-03 19:53:05 +02:00
float Statistic::getTownBuiltRatio(const PlayerState * ps)
{
float built = 0.0;
float total = 0.0;
for(const auto & t : ps->getTowns())
2024-08-03 19:53:05 +02:00
{
built += t->getBuildings().size();
for(const auto & b : t->getTown()->buildings)
2024-08-03 19:53:05 +02:00
if(!t->forbiddenBuildings.count(b.first))
total += 1;
}
if(total < 1)
return 0;
return built / total;
}
2024-07-27 02:11:26 +02:00
VCMI_LIB_NAMESPACE_END