#include "testHarness.h" #include "vf/Scoreboard.h" #include using namespace vf; namespace { ScoreObjective manualObjective(const std::string& name) { ScoreObjective objective; objective.name = name; objective.displayName = name; return objective; } ScoreObjective statObjective(const std::string& name, StatKind kind, int subject = -1) { ScoreObjective objective = manualObjective(name); objective.criterion = ScoreObjective::Criterion::Statistic; objective.statKind = kind; objective.statSubject = subject; return objective; } ScoreTeam makeTeam(const std::string& name, ScoreTeam::Color color) { ScoreTeam team; team.name = name; team.displayName = name; team.color = color; return team; } } // namespace VF_TEST(statSheetsTallyEveryKindWithAndWithoutASubject) { StatSheet sheet; CHECK_EQ(sheet.revision(), 0); CHECK_EQ(sheet.total(StatKind::Deaths), 0); sheet.add(StatKind::Deaths, 1); sheet.add(StatKind::PlayTicks, 12000); sheet.add(StatKind::DistanceWalked, 450); sheet.add(StatKind::DistanceSprinted, 1200); sheet.addBlockBroken(Block::Stone, 40); sheet.addBlockBroken(Block::Dirt, 7); sheet.addItemUsed(ItemId::IronPickaxe, 3); sheet.addMobKilled(Species::Zombie, 2); sheet.addMobKilled(Species::Creeper); CHECK_EQ(sheet.total(StatKind::Deaths), 1); CHECK_EQ(sheet.total(StatKind::PlayTicks), 12000); CHECK_EQ(sheet.blocksBroken(Block::Stone), 40); CHECK_EQ(sheet.blocksBroken(Block::Sand), 0); CHECK_EQ(sheet.total(StatKind::BlocksBroken), 47); CHECK_EQ(sheet.itemsUsed(ItemId::IronPickaxe), 3); CHECK_EQ(sheet.mobsKilled(Species::Zombie), 2); CHECK_EQ(sheet.mobsKilled(Species::Creeper), 1); CHECK_EQ(sheet.total(StatKind::MobsKilled), 3); CHECK_EQ(sheet.distanceTravelled(), 1650); CHECK_EQ(sheet.distinctSubjects(StatKind::BlocksBroken), 2); CHECK_EQ(sheet.distinctSubjects(StatKind::Deaths), 1); long long before = sheet.revision(); sheet.add(StatKind::Deaths, 0); CHECK_EQ(sheet.revision(), before); sheet.add(StatKind::Deaths, 1); CHECK_EQ(sheet.revision(), before + 1); CHECK_EQ(sheet.total(StatKind::Deaths), 2); std::vector counters = sheet.counters(); CHECK_EQ(counters.size(), static_cast(9)); for (std::size_t i = 1u; i < counters.size(); ++i) { bool ordered = counters[i - 1u].kind < counters[i].kind || (counters[i - 1u].kind == counters[i].kind && counters[i - 1u].subject < counters[i].subject); CHECK(ordered); } sheet.clear(); CHECK_EQ(sheet.counters().size(), static_cast(0)); CHECK_EQ(sheet.total(StatKind::BlocksBroken), 0); CHECK(sheet.revision() > before); for (int i = 0; i < static_cast(StatKind::Count); ++i) { StatKind kind = static_cast(i); StatKind reparsed = StatKind::Count; CHECK(parseStatKindName(statKindName(kind), reparsed)); CHECK(reparsed == kind); } StatKind unknown = StatKind::Count; CHECK(!parseStatKindName("blocksPlaced", unknown)); CHECK(statKindHasSubject(StatKind::BlocksBroken)); CHECK(!statKindHasSubject(StatKind::Deaths)); CHECK(statKindIsDistance(StatKind::DistanceFallen)); CHECK(!statKindIsDistance(StatKind::PlayTicks)); } VF_TEST(anAutomaticObjectiveFollowsTheStatSheetWithNoUpdateStep) { Scoreboard board; CHECK(board.addObjective(statObjective("mined", StatKind::BlocksBroken))); CHECK(board.addObjective(statObjective("stone", StatKind::BlocksBroken, static_cast(Block::Stone)))); CHECK(board.addObjective(statObjective("deaths", StatKind::Deaths))); CHECK_EQ(board.score("mined", "foksiu"), 0); CHECK(!board.tracked("mined", "foksiu")); board.stats("foksiu").addBlockBroken(Block::Stone, 12); CHECK(board.tracked("mined", "foksiu")); CHECK_EQ(board.score("mined", "foksiu"), 12); CHECK_EQ(board.score("stone", "foksiu"), 12); board.stats("foksiu").addBlockBroken(Block::Dirt, 5); CHECK_EQ(board.score("mined", "foksiu"), 17); CHECK_EQ(board.score("stone", "foksiu"), 12); board.stats("foksiu").add(StatKind::Deaths, 1); CHECK_EQ(board.score("deaths", "foksiu"), 1); CHECK_EQ(board.score("mined", "foksiu"), 17); // An automatic objective refuses to be written by hand. CHECK(!board.setScore("mined", "foksiu", 999)); CHECK(!board.addScore("mined", "foksiu", 1)); CHECK_EQ(board.score("mined", "foksiu"), 17); CHECK(!board.addObjective(statObjective("mined", StatKind::Deaths))); CHECK_EQ(board.objectiveNames().size(), static_cast(3)); CHECK(board.objective("mined")->automatic()); CHECK(board.objective("nothing") == nullptr); CHECK_EQ(board.score("nothing", "foksiu"), 0); } VF_TEST(manualObjectivesOnlyMoveWhenSomethingSetsThem) { Scoreboard board; CHECK(board.addObjective(manualObjective("bounty"))); CHECK(!board.addObjective(manualObjective(""))); CHECK(!board.tracked("bounty", "foksiu")); CHECK(board.setScore("bounty", "foksiu", 10)); CHECK(board.tracked("bounty", "foksiu")); CHECK_EQ(board.score("bounty", "foksiu"), 10); CHECK(board.addScore("bounty", "foksiu", -4)); CHECK_EQ(board.score("bounty", "foksiu"), 6); CHECK(board.addScore("bounty", "newcomer", 3)); CHECK_EQ(board.score("bounty", "newcomer"), 3); CHECK(!board.setScore("absent", "foksiu", 1)); CHECK(board.resetScore("bounty", "foksiu")); CHECK(!board.resetScore("bounty", "foksiu")); CHECK(!board.tracked("bounty", "foksiu")); CHECK_EQ(board.score("bounty", "foksiu"), 0); board.stats("newcomer").add(StatKind::Deaths, 2); board.resetPlayer("newcomer"); CHECK(!board.tracked("bounty", "newcomer")); CHECK(board.statsFor("newcomer") == nullptr); board.setScore("bounty", "a", 2147483647); CHECK(board.addScore("bounty", "a", 10)); CHECK_EQ(board.score("bounty", "a"), 2147483647); } VF_TEST(entriesComeBackSortedAndFormattedForTheirObjective) { Scoreboard board; ScoreObjective bounty = manualObjective("bounty"); board.addObjective(bounty); board.setScore("bounty", "cara", 5); board.setScore("bounty", "bea", 9); board.setScore("bounty", "abel", 5); std::vector ranked = board.entries("bounty"); CHECK_EQ(ranked.size(), static_cast(3)); CHECK_EQ(ranked[0].player, std::string("bea")); CHECK_EQ(ranked[1].player, std::string("abel")); CHECK_EQ(ranked[2].player, std::string("cara")); CHECK_EQ(ranked[0].formatted, std::string("9")); ScoreObjective ascending = manualObjective("race"); ascending.sort = ScoreObjective::Sort::Ascending; ascending.format = ScoreObjective::Format::Duration; board.addObjective(ascending); board.setScore("race", "bea", 20 * 65); board.setScore("race", "abel", 20 * 3661); board.setScore("race", "cara", 20 * 9); std::vector fastest = board.entries("race"); CHECK_EQ(fastest[0].player, std::string("cara")); CHECK_EQ(fastest[0].formatted, std::string("9s")); CHECK_EQ(fastest[1].formatted, std::string("1m 05s")); CHECK_EQ(fastest[2].formatted, std::string("1h 01m 01s")); ScoreObjective walked = statObjective("walked", StatKind::DistanceWalked); walked.format = ScoreObjective::Format::Distance; board.addObjective(walked); board.stats("abel").add(StatKind::DistanceWalked, 123456); board.stats("bea").add(StatKind::DistanceWalked, 7); std::vector distances = board.entries("walked"); CHECK_EQ(distances.size(), static_cast(2)); CHECK_EQ(distances[0].player, std::string("abel")); CHECK_EQ(distances[0].formatted, std::string("1234.56 m")); CHECK_EQ(distances[1].formatted, std::string("0.07 m")); CHECK(board.entries("missing").empty()); CHECK_EQ(walked.formatScore(-250), std::string("-2.50 m")); CHECK_EQ(ascending.formatScore(0), std::string("0s")); } VF_TEST(displaySlotsShowOneObjectiveEachAndCanBeCleared) { Scoreboard board; board.addObjective(manualObjective("bounty")); board.addObjective(statObjective("deaths", StatKind::Deaths)); board.setScore("bounty", "abel", 3); board.setScore("bounty", "bea", 8); board.setScore("bounty", "cara", 1); CHECK(board.displayedObjective(DisplaySlot::Sidebar).empty()); board.setDisplay(DisplaySlot::Sidebar, "bounty"); board.setDisplay(DisplaySlot::PlayerList, "deaths"); board.setDisplay(DisplaySlot::BelowName, "nothing at all"); CHECK_EQ(board.displayedObjective(DisplaySlot::Sidebar), std::string("bounty")); CHECK_EQ(board.displayedObjective(DisplaySlot::PlayerList), std::string("deaths")); CHECK(board.displayedObjective(DisplaySlot::BelowName).empty()); std::vector sidebar = board.displayLines(DisplaySlot::Sidebar, 2); CHECK_EQ(sidebar.size(), static_cast(2)); CHECK_EQ(sidebar[0].player, std::string("bea")); CHECK_EQ(sidebar[1].player, std::string("abel")); CHECK_EQ(board.displayLines(DisplaySlot::Sidebar, 15).size(), static_cast(3)); CHECK(board.displayLines(DisplaySlot::BelowName, 15).empty()); board.clearDisplay(DisplaySlot::Sidebar); CHECK(board.displayedObjective(DisplaySlot::Sidebar).empty()); CHECK(board.displayLines(DisplaySlot::Sidebar, 15).empty()); board.setDisplay(DisplaySlot::Sidebar, "bounty"); CHECK(board.removeObjective("bounty")); CHECK(!board.removeObjective("bounty")); CHECK(board.displayedObjective(DisplaySlot::Sidebar).empty()); CHECK_EQ(board.score("bounty", "bea"), 0); CHECK_EQ(std::string(displaySlotName(DisplaySlot::Sidebar)), std::string("sidebar")); CHECK_EQ(std::string(displaySlotName(DisplaySlot::BelowName)), std::string("belowName")); } VF_TEST(teamsDecideFriendlyFireNameVisibilityAndDisplayName) { Scoreboard board; ScoreTeam red = makeTeam("red", ScoreTeam::Color::Red); red.friendlyFire = false; red.prefix = "[R] "; red.suffix = " *"; red.members.push_back("abel"); red.members.push_back("bea"); CHECK(board.addTeam(red)); CHECK(!board.addTeam(red)); CHECK(board.addTeam(makeTeam("blue", ScoreTeam::Color::Blue))); CHECK(board.joinTeam("blue", "cara")); CHECK(!board.joinTeam("green", "cara")); CHECK_EQ(board.teamNames().size(), static_cast(2)); CHECK(board.teamOf("abel") != nullptr); CHECK_EQ(board.teamOf("abel")->name, std::string("red")); CHECK_EQ(board.team("red")->members.size(), static_cast(2)); CHECK(board.teamOf("nobody") == nullptr); CHECK(!board.friendlyFireAllowed("abel", "bea")); CHECK(board.friendlyFireAllowed("abel", "cara")); CHECK(board.friendlyFireAllowed("cara", "abel")); CHECK(board.friendlyFireAllowed("nobody", "abel")); CHECK_EQ(board.displayNameOf("abel"), std::string("[R] abel *")); CHECK_EQ(board.displayNameOf("cara"), std::string("cara")); // Moving a player leaves the old roster behind. CHECK(board.joinTeam("blue", "bea")); CHECK_EQ(board.team("red")->members.size(), static_cast(1)); CHECK_EQ(board.teamOf("bea")->name, std::string("blue")); CHECK(board.friendlyFireAllowed("abel", "bea")); CHECK(board.leaveTeam("bea")); CHECK(!board.leaveTeam("bea")); CHECK(board.teamOf("bea") == nullptr); CHECK_EQ(board.team("blue")->members.size(), static_cast(1)); CHECK(board.removeTeam("blue")); CHECK(!board.removeTeam("blue")); CHECK(board.teamOf("cara") == nullptr); CHECK_EQ(std::string(scoreTeamColorName(ScoreTeam::Color::Red)), std::string("red")); CHECK_EQ(std::string(scoreTeamColorName(ScoreTeam::Color::Aqua)), std::string("aqua")); } VF_TEST(nameVisibilityFollowsTheTeamRuleForTheWatcher) { Scoreboard board; ScoreTeam hidden = makeTeam("hidden", ScoreTeam::Color::Gray); hidden.nameVisibility = ScoreTeam::Visibility::Never; hidden.members.push_back("ghost"); board.addTeam(hidden); ScoreTeam clan = makeTeam("clan", ScoreTeam::Color::Green); clan.nameVisibility = ScoreTeam::Visibility::HideForOtherTeams; clan.members.push_back("abel"); clan.members.push_back("bea"); board.addTeam(clan); ScoreTeam masked = makeTeam("masked", ScoreTeam::Color::Purple); masked.nameVisibility = ScoreTeam::Visibility::HideForOwnTeam; masked.members.push_back("cara"); masked.members.push_back("dana"); board.addTeam(masked); CHECK(!board.nameVisibleTo("abel", "ghost")); CHECK(!board.nameVisibleTo("ghost", "ghost")); CHECK(board.nameVisibleTo("abel", "bea")); CHECK(!board.nameVisibleTo("cara", "bea")); CHECK(!board.nameVisibleTo("dana", "cara")); CHECK(board.nameVisibleTo("abel", "cara")); CHECK(board.nameVisibleTo("abel", "loner")); CHECK(board.nameVisibleTo("loner", "loner")); } VF_TEST(everyPlayerTouchedByTheBoardIsListedOnceInOrder) { Scoreboard board; board.addObjective(manualObjective("bounty")); board.addObjective(statObjective("mined", StatKind::BlocksBroken)); board.addTeam(makeTeam("red", ScoreTeam::Color::Red)); board.setScore("bounty", "cara", 1); board.stats("abel").addBlockBroken(Block::Stone); board.joinTeam("red", "bea"); board.stats("cara").addBlockBroken(Block::Dirt, 4); std::vector everyone = board.players(); CHECK_EQ(everyone.size(), static_cast(3)); CHECK_EQ(everyone[0], std::string("abel")); CHECK_EQ(everyone[1], std::string("bea")); CHECK_EQ(everyone[2], std::string("cara")); // The automatic column only lists players who actually have a sheet. std::vector mined = board.entries("mined"); CHECK_EQ(mined.size(), static_cast(2)); CHECK_EQ(mined[0].player, std::string("cara")); CHECK_EQ(mined[0].score, 4); CHECK_EQ(mined[1].player, std::string("abel")); } VF_TEST(aStatisticObjectiveOnOneSubjectIgnoresTheOthers) { Scoreboard board; board.addObjective(statObjective("zombieHunter", StatKind::MobsKilled, static_cast(Species::Zombie))); board.addObjective(statObjective("anyKill", StatKind::MobsKilled)); board.addObjective(statObjective("pickaxeUse", StatKind::ItemsUsed, static_cast(ItemId::IronPickaxe))); StatSheet& sheet = board.stats("foksiu"); sheet.addMobKilled(Species::Zombie, 3); sheet.addMobKilled(Species::Sheep, 9); sheet.addItemUsed(ItemId::IronPickaxe, 2); sheet.addItemUsed(ItemId::WoodAxe, 40); CHECK_EQ(board.score("zombieHunter", "foksiu"), 3); CHECK_EQ(board.score("anyKill", "foksiu"), 12); CHECK_EQ(board.score("pickaxeUse", "foksiu"), 2); sheet.addMobKilled(Species::Zombie); CHECK_EQ(board.score("zombieHunter", "foksiu"), 4); CHECK_EQ(board.score("anyKill", "foksiu"), 13); // A stat far past the score range is clamped, not wrapped. sheet.add(StatKind::PlayTicks, 4000000000ll); board.addObjective(statObjective("played", StatKind::PlayTicks)); CHECK_EQ(board.score("played", "foksiu"), 2147483647); }