Files
voxelforge/tests/testScoreboard.cpp
T
2026-09-12 23:01:51 +02:00

374 lines
15 KiB
C++

#include "testHarness.h"
#include "vf/Scoreboard.h"
#include <string>
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<StatCounter> counters = sheet.counters();
CHECK_EQ(counters.size(), static_cast<std::size_t>(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<std::size_t>(0));
CHECK_EQ(sheet.total(StatKind::BlocksBroken), 0);
CHECK(sheet.revision() > before);
for (int i = 0; i < static_cast<int>(StatKind::Count); ++i) {
StatKind kind = static_cast<StatKind>(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<int>(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<std::size_t>(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<ScoreEntry> ranked = board.entries("bounty");
CHECK_EQ(ranked.size(), static_cast<std::size_t>(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<ScoreEntry> 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<ScoreEntry> distances = board.entries("walked");
CHECK_EQ(distances.size(), static_cast<std::size_t>(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<ScoreEntry> sidebar = board.displayLines(DisplaySlot::Sidebar, 2);
CHECK_EQ(sidebar.size(), static_cast<std::size_t>(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<std::size_t>(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<std::size_t>(2));
CHECK(board.teamOf("abel") != nullptr);
CHECK_EQ(board.teamOf("abel")->name, std::string("red"));
CHECK_EQ(board.team("red")->members.size(), static_cast<std::size_t>(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<std::size_t>(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<std::size_t>(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<std::string> everyone = board.players();
CHECK_EQ(everyone.size(), static_cast<std::size_t>(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<ScoreEntry> mined = board.entries("mined");
CHECK_EQ(mined.size(), static_cast<std::size_t>(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<int>(Species::Zombie)));
board.addObjective(statObjective("anyKill", StatKind::MobsKilled));
board.addObjective(statObjective("pickaxeUse", StatKind::ItemsUsed,
static_cast<int>(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);
}