110 lines
6.1 KiB
C++
110 lines
6.1 KiB
C++
#include "db.hpp"
|
|
#include "game_config.hpp"
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
using J = nlohmann::json;
|
|
|
|
// Exercise real transactions from a new account. Only the server clock is advanced
|
|
// past the required playback time; no wallet, ownership or clear flags are seeded.
|
|
int main(int argc, char** argv) {
|
|
auto tempRoot = std::filesystem::weakly_canonical(std::filesystem::temp_directory_path());
|
|
auto folder = tempRoot /
|
|
("aigame-first-day-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
|
std::filesystem::create_directories(folder);
|
|
J report = {{"status", "running"}, {"seededResources", false}, {"injectedClock", true},
|
|
{"battles", J::array()}, {"operations", J::array()}};
|
|
try {
|
|
{
|
|
db::Database database((folder / "test.db").string());
|
|
if (!database.open_ok()) throw std::runtime_error("isolated database");
|
|
auto pid = database.create_user("first-day-new-player", "test-hash", "test-salt");
|
|
if (!pid) throw std::runtime_error("new account");
|
|
database.store_token("first-day-test-token", *pid, 1, 4102444800LL);
|
|
int64_t now = 1900040000; int serial = 0;
|
|
auto read = [&]() {
|
|
auto r = database.game_request("first-day-test-token", "", now);
|
|
if (r.status != 200) throw std::runtime_error("read snapshot");
|
|
return J::parse(r.body);
|
|
};
|
|
J current = read();
|
|
report["initial"] = current["state"];
|
|
auto send = [&](const std::string& action, J extra = J::object()) {
|
|
extra["requestId"] = "first-day-command-" + std::to_string(++serial);
|
|
extra["action"] = action; extra["revision"] = current["state"]["revision"];
|
|
auto r = database.game_request("first-day-test-token", extra.dump(), now);
|
|
auto reply = J::parse(r.body);
|
|
if (r.status != 200 || !reply.value("ok", false))
|
|
throw std::runtime_error(action + ": " + reply.value("error", "HTTP error"));
|
|
current = reply;
|
|
report["operations"].push_back({{"action", action}, {"coins", current["state"]["coins"]},
|
|
{"candy", current["state"]["candy"]}, {"balls", current["state"]["balls"]},
|
|
{"level", current["state"]["level"]}, {"shots", current["state"]["shots"]}});
|
|
return reply;
|
|
};
|
|
auto shot = [&]() {
|
|
if (current["state"]["shots"].get<int>() >= 60) throw std::runtime_error("shot budget exhausted");
|
|
auto begin = send("shot.begin", {{"aim", 0}});
|
|
now = std::max(now, begin["shot"]["readyAt"].get<int64_t>());
|
|
send("shot.finish", {{"activityId", begin["shot"]["id"]}});
|
|
};
|
|
auto train = [&](int target) {
|
|
while (current["state"]["level"].get<int>() < target) {
|
|
while (current["state"]["candy"].get<int>() < current["state"]["level"].get<int>() * gameplay::config()["economy"]["trainCost"].get<int>()) shot();
|
|
send("train");
|
|
}
|
|
};
|
|
shot(); send("capture"); train(2);
|
|
for (int stage = 1; stage <= 5; ++stage) {
|
|
train(std::max(2, stage)); send("stage.select", {{"stageId", stage}}); send("autoDeploy");
|
|
auto before = current["state"];
|
|
auto begin = send("battle.begin", {{"combatProtocol", 1}});
|
|
if (begin["battle"]["snapshot"]["rulesVersion"] != 3) throw std::runtime_error("not authoritative runtime");
|
|
now = std::max(now, begin["battle"]["readyAt"].get<int64_t>());
|
|
auto result = send("battle.finish", {{"activityId", begin["battle"]["id"]}});
|
|
report["battles"].push_back({{"stage", stage}, {"level", before["level"]},
|
|
{"formation", before["formation"]}, {"shots", before["shots"]},
|
|
{"result", result["result"]}});
|
|
if (!result["result"]["won"].get<bool>()) throw std::runtime_error("starter roster lost stage " + std::to_string(stage));
|
|
const auto& reward = gameplay::stage(stage)["reward"];
|
|
for (const auto* resource : {"coins", "candy", "balls"})
|
|
if (current["state"][resource].get<int>() != before[resource].get<int>() + reward[resource].get<int>())
|
|
throw std::runtime_error("clear reward mismatch");
|
|
if (stage == 1) {
|
|
send("goal.claim"); send("mail.claimAll", {{"mailIds", {"welcome_supply_v1"}}});
|
|
send("signin.claim", {{"item", 0}}); send("shop.buy", {{"item", 0}, {"day", current["state"]["dailyDay"]}});
|
|
send("chest.open", {{"chest", 0}});
|
|
}
|
|
}
|
|
auto beforeRetry = current["state"];
|
|
auto retry = send("battle.begin", {{"combatProtocol", 1}});
|
|
now = std::max(now, retry["battle"]["readyAt"].get<int64_t>());
|
|
auto replayed = send("battle.finish", {{"activityId", retry["battle"]["id"]}});
|
|
if (!replayed["result"]["won"].get<bool>() || replayed["result"]["firstClear"].get<bool>())
|
|
throw std::runtime_error("repeat clear flags");
|
|
for (const auto* field : {"coins", "candy", "balls", "tickets", "chests"})
|
|
if (current["state"][field] != beforeRetry[field]) throw std::runtime_error("repeat clear paid again");
|
|
report["repeatClearNoReward"] = true;
|
|
report["final"] = current["state"]; report["status"] = "pass";
|
|
}
|
|
} catch (const std::exception& error) {
|
|
report["status"] = "failed"; report["error"] = error.what();
|
|
}
|
|
if (argc > 1) {
|
|
auto destination = std::filesystem::path(argv[1]); std::error_code error;
|
|
if (!destination.parent_path().empty()) std::filesystem::create_directories(destination.parent_path(), error);
|
|
std::ofstream out(destination, std::ios::binary); out << report.dump(2);
|
|
if (error || !out.good()) { report["status"] = "failed"; report["error"] = "cannot write acceptance report"; }
|
|
}
|
|
if (std::filesystem::weakly_canonical(folder).parent_path() != tempRoot ||
|
|
folder.filename().string().rfind("aigame-first-day-", 0) != 0)
|
|
throw std::runtime_error("refusing cleanup outside isolated temp directory");
|
|
std::filesystem::remove_all(folder);
|
|
if (report["status"] != "pass") { std::cerr << report.value("error", "failed") << '\n'; return 1; }
|
|
std::cout << "PASS fresh account through first five stages; normal rewards and T1 tutorial roster\n";
|
|
return 0;
|
|
}
|