implement episode 3 tournaments
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include <array>
|
||||
#include <deque>
|
||||
#include <phosg/Filesystem.hh>
|
||||
#include <phosg/Random.hh>
|
||||
#include <phosg/Time.hh>
|
||||
|
||||
#include "../Loggers.hh"
|
||||
@@ -791,6 +792,15 @@ Rules::Rules() {
|
||||
this->clear();
|
||||
}
|
||||
|
||||
void Rules::set_defaults() {
|
||||
this->clear();
|
||||
this->overall_time_limit = 24; // 2 hours
|
||||
this->phase_time_limit = 30;
|
||||
this->min_dice = 1;
|
||||
this->max_dice = 6;
|
||||
this->char_hp = 15;
|
||||
}
|
||||
|
||||
void Rules::clear() {
|
||||
this->overall_time_limit = 0;
|
||||
this->phase_time_limit = 0;
|
||||
@@ -1083,8 +1093,6 @@ string MapDefinition::str(const DataIndex* data_index) const {
|
||||
return join(lines, "\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool Rules::check_invalid_fields() const {
|
||||
Rules t = *this;
|
||||
return t.check_and_reset_invalid_fields();
|
||||
@@ -1333,6 +1341,7 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags)
|
||||
if (!this->maps.emplace(entry->map.map_number, entry).second) {
|
||||
throw runtime_error("duplicate map number");
|
||||
}
|
||||
this->maps_by_name.emplace(entry->map.name, entry);
|
||||
string name = entry->map.name;
|
||||
static_game_data_log.info("Indexed Episode 3 map %s (%08" PRIX32 "; %s)",
|
||||
filename.c_str(), entry->map.map_number.load(), name.c_str());
|
||||
@@ -1343,6 +1352,22 @@ DataIndex::DataIndex(const string& directory, uint32_t behavior_flags)
|
||||
filename.c_str(), e.what());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
auto json = JSONObject::parse(load_file(directory + "/com-decks.json"));
|
||||
for (const auto& def_json : json->as_list()) {
|
||||
auto& def = this->com_decks.emplace_back(new COMDeckDefinition());
|
||||
def->index = this->com_decks.size() - 1;
|
||||
def->player_name = def_json->at(0)->as_string();
|
||||
def->deck_name = def_json->at(1)->as_string();
|
||||
auto card_ids_json = def_json->at(2)->as_list();
|
||||
for (size_t z = 0; z < 0x1F; z++) {
|
||||
def->card_ids[z] = card_ids_json.at(z)->as_int();
|
||||
}
|
||||
}
|
||||
} catch (const exception& e) {
|
||||
static_game_data_log.warning("Failed to load Episode 3 COM decks: %s", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
DataIndex::MapEntry::MapEntry(const MapDefinition& map) : map(map) { }
|
||||
@@ -1452,6 +1477,11 @@ shared_ptr<const DataIndex::MapEntry> DataIndex::definition_for_map_number(uint3
|
||||
return this->maps.at(id);
|
||||
}
|
||||
|
||||
shared_ptr<const DataIndex::MapEntry> DataIndex::definition_for_map_name(
|
||||
const string& name) const {
|
||||
return this->maps_by_name.at(name);
|
||||
}
|
||||
|
||||
set<uint32_t> DataIndex::all_map_ids() const {
|
||||
set<uint32_t> ret;
|
||||
for (const auto& it : this->maps) {
|
||||
@@ -1460,6 +1490,18 @@ set<uint32_t> DataIndex::all_map_ids() const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
size_t DataIndex::num_com_decks() const {
|
||||
return this->com_decks.size();
|
||||
}
|
||||
|
||||
shared_ptr<const COMDeckDefinition> DataIndex::com_deck(size_t which) const {
|
||||
return this->com_decks.at(which);
|
||||
}
|
||||
|
||||
shared_ptr<const COMDeckDefinition> DataIndex::random_com_deck() const {
|
||||
return this->com_decks[random_object<size_t>() % this->com_decks.size()];
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
|
||||
@@ -642,6 +642,7 @@ struct Rules {
|
||||
bool operator==(const Rules& other) const;
|
||||
bool operator!=(const Rules& other) const;
|
||||
void clear();
|
||||
void set_defaults();
|
||||
|
||||
bool check_invalid_fields() const;
|
||||
bool check_and_reset_invalid_fields();
|
||||
@@ -791,6 +792,15 @@ struct MapDefinition { // .mnmd format; also the format of (decompressed) quests
|
||||
|
||||
|
||||
|
||||
struct COMDeckDefinition {
|
||||
size_t index;
|
||||
std::string player_name;
|
||||
std::string deck_name;
|
||||
parray<le_uint16_t, 0x1F> card_ids;
|
||||
};
|
||||
|
||||
|
||||
|
||||
class DataIndex {
|
||||
public:
|
||||
DataIndex(const std::string& directory, uint32_t behavior_flags);
|
||||
@@ -822,8 +832,14 @@ public:
|
||||
|
||||
const std::string& get_compressed_map_list() const;
|
||||
std::shared_ptr<const MapEntry> definition_for_map_number(uint32_t id) const;
|
||||
std::shared_ptr<const MapEntry> definition_for_map_name(
|
||||
const std::string& name) const;
|
||||
std::set<uint32_t> all_map_ids() const;
|
||||
|
||||
size_t num_com_decks() const;
|
||||
std::shared_ptr<const COMDeckDefinition> com_deck(size_t which) const;
|
||||
std::shared_ptr<const COMDeckDefinition> random_com_deck() const;
|
||||
|
||||
const uint32_t behavior_flags;
|
||||
|
||||
private:
|
||||
@@ -837,6 +853,9 @@ private:
|
||||
// compressed map list at load time.
|
||||
mutable std::string compressed_map_list;
|
||||
std::map<uint32_t, std::shared_ptr<MapEntry>> maps;
|
||||
std::unordered_map<std::string, std::shared_ptr<MapEntry>> maps_by_name;
|
||||
|
||||
std::vector<std::shared_ptr<COMDeckDefinition>> com_decks;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ PlayerState::PlayerState(uint8_t client_id, shared_ptr<Server> server)
|
||||
|
||||
void PlayerState::init() {
|
||||
if (this->server()->player_states[this->client_id].get() != this) {
|
||||
// TODO: The original code handles this, but we don't. Figure out if this is
|
||||
// actually needed and implement it if so.
|
||||
// Note: The original code handles this, but we don't. This appears not to
|
||||
// ever happen, so we didn't bother implementing it.
|
||||
throw logic_error("replacing a player state object is not permitted");
|
||||
}
|
||||
|
||||
|
||||
+60
-25
@@ -34,10 +34,12 @@ void ServerBase::PresenceEntry::clear() {
|
||||
ServerBase::ServerBase(
|
||||
shared_ptr<Lobby> lobby,
|
||||
shared_ptr<const DataIndex> data_index,
|
||||
uint32_t random_seed)
|
||||
uint32_t random_seed,
|
||||
bool is_tournament)
|
||||
: lobby(lobby),
|
||||
data_index(data_index),
|
||||
random_seed(random_seed) { }
|
||||
random_seed(random_seed),
|
||||
is_tournament(is_tournament) { }
|
||||
|
||||
void ServerBase::init() {
|
||||
this->reset();
|
||||
@@ -96,7 +98,7 @@ Server::Server(shared_ptr<ServerBase> base)
|
||||
team_num_ally_fcs_destroyed(0),
|
||||
team_num_cards_destroyed(0),
|
||||
hard_reset_flag(false),
|
||||
tournament_flag(0),
|
||||
tournament_flag(base->is_tournament ? 1 : 0),
|
||||
num_trap_tiles_of_type(0),
|
||||
chosen_trap_tile_index_of_type(0),
|
||||
has_done_pb(0),
|
||||
@@ -142,6 +144,39 @@ shared_ptr<const ServerBase> Server::base() const {
|
||||
return s;
|
||||
}
|
||||
|
||||
int8_t Server::get_winner_team_id() const {
|
||||
parray<size_t, 2> team_player_counts(0);
|
||||
parray<size_t, 2> team_win_flag_counts(0);
|
||||
for (size_t client_id = 0; client_id < 4; client_id++) {
|
||||
auto ps = this->player_states[client_id];
|
||||
if (!ps) {
|
||||
continue;
|
||||
}
|
||||
uint8_t team_id = ps->get_team_id();
|
||||
team_player_counts[team_id]++;
|
||||
if (ps->assist_flags & 4) {
|
||||
team_win_flag_counts[team_id]++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!team_player_counts[0] || !team_player_counts[1]) {
|
||||
throw logic_error("at least one team has no players");
|
||||
}
|
||||
if (team_win_flag_counts[0] && team_win_flag_counts[1]) {
|
||||
throw logic_error("both teams have winning players");
|
||||
}
|
||||
for (int8_t z = 0; z < 2; z++) {
|
||||
if (!team_win_flag_counts[z]) {
|
||||
continue;
|
||||
}
|
||||
if (team_win_flag_counts[z] != team_player_counts[z]) {
|
||||
throw logic_error("only some players on team 0 have won");
|
||||
}
|
||||
return z;
|
||||
}
|
||||
return -1; // No team has won (yet)
|
||||
}
|
||||
|
||||
void Server::send(const void* data, size_t size) const {
|
||||
auto l = this->base()->lobby.lock();
|
||||
if (!l) {
|
||||
@@ -418,7 +453,7 @@ bool Server::check_for_battle_end() {
|
||||
}
|
||||
} else { // Both teams defeated?? I guess this is technically possible
|
||||
ret = true;
|
||||
this->unknown_8023D4E0(0x4000);
|
||||
this->compute_losing_team_id_and_add_winner_flags(0x4000);
|
||||
}
|
||||
|
||||
} else { // Not DEFEAT_TEAM
|
||||
@@ -449,7 +484,7 @@ bool Server::check_for_battle_end() {
|
||||
}
|
||||
} else {
|
||||
ret = true;
|
||||
this->unknown_8023D4E0(0x4000);
|
||||
this->compute_losing_team_id_and_add_winner_flags(0x4000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,7 +684,7 @@ void Server::draw_phase_after() {
|
||||
}
|
||||
}
|
||||
if (unknown_v1) {
|
||||
this->unknown_8023D4E0(0);
|
||||
this->compute_losing_team_id_and_add_winner_flags(0);
|
||||
}
|
||||
this->round_num--;
|
||||
this->set_battle_ended();
|
||||
@@ -2136,7 +2171,7 @@ void Server::handle_6xB3x49_card_counts(const string& data) {
|
||||
decrypt_trivial_gci_data(dest_counts.data(), dest_counts.bytes(), in_cmd.basis);
|
||||
}
|
||||
|
||||
void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
void Server::compute_losing_team_id_and_add_winner_flags(uint32_t flags) {
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
auto ps = this->player_states[z];
|
||||
if (ps) {
|
||||
@@ -2146,8 +2181,8 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
|
||||
uint32_t flags_to_add = flags | 0x804;
|
||||
|
||||
// First, check which team has fewer surviving SCs
|
||||
int8_t team_id = -1;
|
||||
// First, check which team has more dead SCs
|
||||
int8_t losing_team_id = -1;
|
||||
uint32_t team_counts[2] = {0, 0};
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
auto ps = this->player_states[z];
|
||||
@@ -2160,13 +2195,13 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
}
|
||||
}
|
||||
if (team_counts[1] < team_counts[0]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 0;
|
||||
} else if (team_counts[0] < team_counts[1]) {
|
||||
team_id = 1;
|
||||
losing_team_id = 1;
|
||||
}
|
||||
|
||||
// If the SC counts match, break ties by remaining SC HP
|
||||
if (team_id == -1) {
|
||||
if (losing_team_id == -1) {
|
||||
team_counts[0] = 0;
|
||||
team_counts[1] = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
@@ -2180,14 +2215,14 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
}
|
||||
}
|
||||
if (team_counts[0] < team_counts[1]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 0;
|
||||
} else if (team_counts[1] < team_counts[0]) {
|
||||
team_id = 1;
|
||||
losing_team_id = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If still tied, break ties by number of opponent cards destroyed
|
||||
if (team_id == -1) {
|
||||
if (losing_team_id == -1) {
|
||||
team_counts[0] = 0;
|
||||
team_counts[1] = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
@@ -2198,14 +2233,14 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
team_counts[ps->get_team_id()] += ps->stats.num_opponent_cards_destroyed;
|
||||
}
|
||||
if (team_counts[0] < team_counts[1]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 0;
|
||||
} else if (team_counts[1] < team_counts[0]) {
|
||||
team_id = 1;
|
||||
losing_team_id = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If still tied, break ties by amount of damage given
|
||||
if (team_id == -1) {
|
||||
if (losing_team_id == -1) {
|
||||
team_counts[0] = 0;
|
||||
team_counts[1] = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
@@ -2216,15 +2251,15 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
team_counts[ps->get_team_id()] += ps->stats.damage_given;
|
||||
}
|
||||
if (team_counts[0] < team_counts[1]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 0;
|
||||
} else if (team_counts[1] < team_counts[0]) {
|
||||
team_id = 1;
|
||||
losing_team_id = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If STILL tied, roll dice and arbitrarily make one team the winner
|
||||
if (team_id == -1) {
|
||||
while (team_id == -1) {
|
||||
if (losing_team_id == -1) {
|
||||
while (losing_team_id == -1) {
|
||||
team_counts[1] = 0;
|
||||
team_counts[0] = 0;
|
||||
for (size_t z = 0; z < 4; z++) {
|
||||
@@ -2237,9 +2272,9 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
team_counts[0] *= this->team_client_count[1];
|
||||
team_counts[1] *= this->team_client_count[0];
|
||||
if (team_counts[0] < team_counts[1]) {
|
||||
team_id = 0;
|
||||
losing_team_id = 0;
|
||||
} else if (team_counts[1] < team_counts[0]) {
|
||||
team_id = 1;
|
||||
losing_team_id = 1;
|
||||
}
|
||||
}
|
||||
flags_to_add = flags | 0x1004;
|
||||
@@ -2250,7 +2285,7 @@ void Server::unknown_8023D4E0(uint32_t flags) {
|
||||
if (!ps) {
|
||||
continue;
|
||||
}
|
||||
if (team_id != ps->get_team_id()) {
|
||||
if (losing_team_id != ps->get_team_id()) {
|
||||
ps->assist_flags |= flags_to_add;
|
||||
}
|
||||
ps->update_hand_and_equip_state_and_send_6xB4x02_if_needed();
|
||||
|
||||
@@ -58,7 +58,8 @@ public:
|
||||
ServerBase(
|
||||
std::shared_ptr<Lobby> lobby,
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
uint32_t random_seed);
|
||||
uint32_t random_seed,
|
||||
bool is_tournament);
|
||||
void init();
|
||||
void reset();
|
||||
void recreate_server();
|
||||
@@ -74,6 +75,7 @@ public:
|
||||
std::weak_ptr<Lobby> lobby;
|
||||
std::shared_ptr<const DataIndex> data_index;
|
||||
uint32_t random_seed;
|
||||
bool is_tournament;
|
||||
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules1;
|
||||
std::shared_ptr<MapAndRulesState> map_and_rules2;
|
||||
@@ -94,6 +96,8 @@ public:
|
||||
std::shared_ptr<ServerBase> base();
|
||||
std::shared_ptr<const ServerBase> base() const;
|
||||
|
||||
int8_t get_winner_team_id() const;
|
||||
|
||||
template <typename T>
|
||||
void send(const T& cmd) const {
|
||||
if (cmd.header.size != sizeof(cmd) / 4) {
|
||||
@@ -200,7 +204,7 @@ public:
|
||||
void handle_6xB3x41_map_request(const std::string& data);
|
||||
void handle_6xB3x48_end_turn(const std::string& data);
|
||||
void handle_6xB3x49_card_counts(const std::string& data);
|
||||
void unknown_8023D4E0(uint32_t flags);
|
||||
void compute_losing_team_id_and_add_winner_flags(uint32_t flags);
|
||||
uint32_t get_team_exp(uint8_t team_id) const;
|
||||
uint32_t send_6xB4x06_if_card_ref_invalid(
|
||||
uint16_t card_ref, int16_t negative_value);
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
#include "Tournament.hh"
|
||||
|
||||
#include <phosg/Random.hh>
|
||||
|
||||
#include "../CommandFormats.hh"
|
||||
#include "../SendCommands.hh"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
|
||||
|
||||
Tournament::Team::Team(
|
||||
shared_ptr<Tournament> tournament, size_t index, size_t max_players)
|
||||
: tournament(tournament),
|
||||
index(index),
|
||||
max_players(max_players),
|
||||
name(""),
|
||||
password(""),
|
||||
num_rounds_cleared(0),
|
||||
is_active(true) { }
|
||||
|
||||
string Tournament::Team::str() const {
|
||||
string ret = string_printf("[Team/%zu %s %zu/%zuP name=%s pass=%s rounds=%zu",
|
||||
this->index, this->is_active ? "active" : "inactive",
|
||||
this->player_serial_numbers.size(), this->max_players, this->name.c_str(),
|
||||
this->password.c_str(), this->num_rounds_cleared);
|
||||
for (uint32_t serial_number : this->player_serial_numbers) {
|
||||
ret += string_printf(" %08" PRIX32, serial_number);
|
||||
}
|
||||
return ret + "]";
|
||||
}
|
||||
|
||||
void Tournament::Team::register_player(
|
||||
uint32_t serial_number,
|
||||
const string& team_name,
|
||||
const string& password) {
|
||||
if (this->player_serial_numbers.size() >= this->max_players) {
|
||||
throw runtime_error("team is full");
|
||||
}
|
||||
|
||||
if (!this->name.empty() && (password != this->password)) {
|
||||
throw runtime_error("incorrect password");
|
||||
}
|
||||
|
||||
auto tournament = this->tournament.lock();
|
||||
if (!tournament) {
|
||||
throw runtime_error("tournament has been deleted");
|
||||
}
|
||||
if (!tournament->all_player_serial_numbers.emplace(serial_number).second) {
|
||||
throw runtime_error("player already registered in same tournament");
|
||||
}
|
||||
|
||||
if (!this->player_serial_numbers.emplace(serial_number).second) {
|
||||
throw logic_error("player already registered in team but not in tournament");
|
||||
}
|
||||
|
||||
if (this->name.empty()) {
|
||||
this->name = team_name;
|
||||
this->password = password;
|
||||
}
|
||||
}
|
||||
|
||||
bool Tournament::Team::unregister_player(uint32_t serial_number) {
|
||||
if (this->player_serial_numbers.erase(serial_number)) {
|
||||
if (this->player_serial_numbers.empty()) {
|
||||
this->name.clear();
|
||||
this->password.clear();
|
||||
}
|
||||
|
||||
auto tournament = this->tournament.lock();
|
||||
if (!tournament) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the tournament has already started, make the team forfeit their game.
|
||||
// If any player withdraws from a team after the registration phase, the
|
||||
// entire team essentially forfeits their entry.
|
||||
if (tournament->get_state() != Tournament::State::REGISTRATION) {
|
||||
// Look through the pending matches to see if this team is involved in any
|
||||
// of them
|
||||
for (auto match : tournament->pending_matches) {
|
||||
if (!match->preceding_a || !match->preceding_b) {
|
||||
throw logic_error("zero-round match is pending after tournament registration phase");
|
||||
}
|
||||
if (match->preceding_a->winner_team.get() == this) {
|
||||
match->set_winner_team(match->preceding_b->winner_team);
|
||||
break;
|
||||
} else if (match->preceding_b->winner_team.get() == this) {
|
||||
match->set_winner_team(match->preceding_a->winner_team);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If the tournament has not started yet, just remove the player from the
|
||||
// team
|
||||
} else {
|
||||
if (!tournament->all_player_serial_numbers.erase(serial_number)) {
|
||||
throw logic_error("player removed from team but not from tournament");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Tournament::Match::Match(
|
||||
shared_ptr<Tournament> tournament,
|
||||
shared_ptr<Match> preceding_a,
|
||||
shared_ptr<Match> preceding_b)
|
||||
: tournament(tournament),
|
||||
preceding_a(preceding_a),
|
||||
preceding_b(preceding_b),
|
||||
winner_team(nullptr),
|
||||
round_num(0) {
|
||||
if (this->preceding_a->round_num != this->preceding_b->round_num) {
|
||||
throw logic_error("preceding matches have different round numbers");
|
||||
}
|
||||
this->round_num = this->preceding_a->round_num;
|
||||
}
|
||||
|
||||
Tournament::Match::Match(
|
||||
shared_ptr<Tournament> tournament,
|
||||
shared_ptr<Team> winner_team)
|
||||
: tournament(tournament),
|
||||
preceding_a(nullptr),
|
||||
preceding_b(nullptr),
|
||||
winner_team(winner_team),
|
||||
round_num(0) { }
|
||||
|
||||
string Tournament::Match::str() const {
|
||||
string winner_str = this->winner_team ? this->winner_team->str() : "(none)";
|
||||
return "[Match winner=" + winner_str + "]";
|
||||
}
|
||||
|
||||
bool Tournament::Match::resolve_if_no_players() {
|
||||
// If both matches before this one are resolved and neither winner team has
|
||||
// any humans on it, skip this match entirely and just make one team advance
|
||||
// arbitrarily
|
||||
if (!this->winner_team &&
|
||||
this->preceding_a->winner_team &&
|
||||
this->preceding_b->winner_team &&
|
||||
this->preceding_a->winner_team->player_serial_numbers.empty() &&
|
||||
this->preceding_b->winner_team->player_serial_numbers.empty()) {
|
||||
this->set_winner_team((random_object<uint8_t>() & 1)
|
||||
? this->preceding_b->winner_team : this->preceding_a->winner_team);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Tournament::Match::resolve_following_matches() {
|
||||
auto tournament = this->tournament.lock();
|
||||
if (!tournament) {
|
||||
return;
|
||||
}
|
||||
|
||||
tournament->pending_matches.erase(this->shared_from_this());
|
||||
|
||||
// Resolve all matches up the chain until we can't anymore (this
|
||||
// automatically skips CPU-only matches)
|
||||
auto following = this->following.lock();
|
||||
while (following && following->resolve_if_no_players()) {
|
||||
tournament->pending_matches.erase(following);
|
||||
following = following->following.lock();
|
||||
}
|
||||
|
||||
// If there's a following match that wasn't resolved, mark it pending
|
||||
if (following) {
|
||||
tournament->pending_matches.emplace(following);
|
||||
}
|
||||
|
||||
// If there are no pending matches, then the tournament is complete
|
||||
if (tournament->pending_matches.empty()) {
|
||||
tournament->current_state = Tournament::State::COMPLETE;
|
||||
}
|
||||
}
|
||||
|
||||
void Tournament::Match::set_winner_team(shared_ptr<Team> team) {
|
||||
if (!this->preceding_a || !this->preceding_b) {
|
||||
throw logic_error("set_winner_team called on zero-round match");
|
||||
}
|
||||
if ((team != this->preceding_a->winner_team) &&
|
||||
(team != this->preceding_b->winner_team)) {
|
||||
throw logic_error("winner team did not participate in match");
|
||||
}
|
||||
|
||||
this->winner_team = team;
|
||||
|
||||
this->winner_team->num_rounds_cleared++;
|
||||
if (this->winner_team == this->preceding_a->winner_team) {
|
||||
this->preceding_b->winner_team->is_active = false;
|
||||
} else {
|
||||
this->preceding_a->winner_team->is_active = false;
|
||||
}
|
||||
|
||||
this->resolve_following_matches();
|
||||
}
|
||||
|
||||
shared_ptr<Tournament::Team> Tournament::Match::opponent_team_for_team(
|
||||
shared_ptr<Team> team) const {
|
||||
if (!this->preceding_a || !this->preceding_b) {
|
||||
throw logic_error("zero-round matches do not have opponents");
|
||||
}
|
||||
if (team == this->preceding_a->winner_team) {
|
||||
return this->preceding_b->winner_team;
|
||||
} else if (team == this->preceding_b->winner_team) {
|
||||
return this->preceding_a->winner_team;
|
||||
} else {
|
||||
throw logic_error("team is not registered for this match");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Tournament::Tournament(
|
||||
shared_ptr<const DataIndex> data_index,
|
||||
uint8_t number,
|
||||
const string& name,
|
||||
shared_ptr<const DataIndex::MapEntry> map,
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
bool is_2v2)
|
||||
: log(string_printf("[Tournament/%02hhX] ", number)),
|
||||
data_index(data_index),
|
||||
number(number),
|
||||
name(name),
|
||||
map(map),
|
||||
rules(rules),
|
||||
num_teams(num_teams),
|
||||
is_2v2(is_2v2),
|
||||
current_state(State::REGISTRATION) {
|
||||
if (this->num_teams < 4) {
|
||||
throw invalid_argument("team count must be 4 or more");
|
||||
}
|
||||
if (this->num_teams > 32) {
|
||||
throw invalid_argument("team count must be 32 or fewer");
|
||||
}
|
||||
if (this->num_teams & (this->num_teams - 1)) {
|
||||
throw invalid_argument("team count must be a power of 2");
|
||||
}
|
||||
}
|
||||
|
||||
void Tournament::init() {
|
||||
// Create all the teams and initial matches
|
||||
while (this->teams.size() < this->num_teams) {
|
||||
auto t = make_shared<Team>(
|
||||
this->shared_from_this(), this->teams.size(), this->is_2v2 ? 2 : 1);
|
||||
this->teams.emplace_back(t);
|
||||
this->zero_round_matches.emplace_back(make_shared<Match>(
|
||||
this->shared_from_this(), t));
|
||||
}
|
||||
|
||||
// Make all the zero round matches pending (this is needed so that start()
|
||||
// will auto-resolve all-CPU matches in the first round)
|
||||
for (auto m : this->zero_round_matches) {
|
||||
this->pending_matches.emplace(m);
|
||||
}
|
||||
|
||||
// Create the bracket matches
|
||||
vector<shared_ptr<Match>> current_round_matches = this->zero_round_matches;
|
||||
while (current_round_matches.size() > 1) {
|
||||
vector<shared_ptr<Match>> next_round_matches;
|
||||
for (size_t z = 0; z < current_round_matches.size(); z += 2) {
|
||||
auto m = make_shared<Match>(
|
||||
this->shared_from_this(),
|
||||
current_round_matches[z],
|
||||
current_round_matches[z + 1]);
|
||||
current_round_matches[z]->following = m;
|
||||
current_round_matches[z + 1]->following = m;
|
||||
next_round_matches.emplace_back(move(m));
|
||||
}
|
||||
current_round_matches = move(next_round_matches);
|
||||
}
|
||||
this->final_match = current_round_matches.at(0);
|
||||
}
|
||||
|
||||
std::shared_ptr<const DataIndex> Tournament::get_data_index() const {
|
||||
return this->data_index;
|
||||
}
|
||||
|
||||
uint8_t Tournament::get_number() const {
|
||||
return this->number;
|
||||
}
|
||||
|
||||
const string& Tournament::get_name() const {
|
||||
return this->name;
|
||||
}
|
||||
|
||||
shared_ptr<const DataIndex::MapEntry> Tournament::get_map() const {
|
||||
return this->map;
|
||||
}
|
||||
|
||||
const Rules& Tournament::get_rules() const {
|
||||
return this->rules;
|
||||
}
|
||||
|
||||
bool Tournament::get_is_2v2() const {
|
||||
return this->is_2v2;
|
||||
}
|
||||
|
||||
Tournament::State Tournament::get_state() const {
|
||||
return this->current_state;
|
||||
}
|
||||
|
||||
const vector<shared_ptr<Tournament::Team>>& Tournament::all_teams() const {
|
||||
return this->teams;
|
||||
}
|
||||
|
||||
shared_ptr<Tournament::Team> Tournament::get_team(size_t index) const {
|
||||
return this->teams.at(index);
|
||||
}
|
||||
|
||||
shared_ptr<Tournament::Team> Tournament::get_winner_team() const {
|
||||
if (this->current_state != State::COMPLETE) {
|
||||
return nullptr;
|
||||
}
|
||||
if (!this->final_match->winner_team) {
|
||||
throw logic_error("tournament is complete but winner is not set");
|
||||
}
|
||||
return this->final_match->winner_team;
|
||||
}
|
||||
|
||||
shared_ptr<Tournament::Match> Tournament::next_match_for_team(
|
||||
shared_ptr<Team> team) const {
|
||||
if (this->current_state == Tournament::State::REGISTRATION) {
|
||||
return nullptr;
|
||||
}
|
||||
for (auto match : this->pending_matches) {
|
||||
if (!match->preceding_a || !match->preceding_b) {
|
||||
throw logic_error("zero-round match is pending after tournament registration phase");
|
||||
}
|
||||
if ((team == match->preceding_a->winner_team) ||
|
||||
(team == match->preceding_b->winner_team)) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Tournament::start() {
|
||||
if (this->current_state != State::REGISTRATION) {
|
||||
throw runtime_error("tournament has already started");
|
||||
}
|
||||
|
||||
this->current_state = State::IN_PROGRESS;
|
||||
|
||||
// Assign names to COM teams, and assign COM decks to all empty slots
|
||||
for (size_t z = 0; z < this->zero_round_matches.size(); z++) {
|
||||
auto m = this->zero_round_matches[z];
|
||||
auto t = m->winner_team;
|
||||
if (t->name.empty()) {
|
||||
t->name = string_printf("COM:%zu", z);
|
||||
}
|
||||
if (this->data_index->num_com_decks() < t->max_players - t->player_serial_numbers.size()) {
|
||||
throw runtime_error("not enough COM decks to complete team");
|
||||
}
|
||||
while (t->player_serial_numbers.size() + t->com_decks.size() < t->max_players) {
|
||||
t->com_decks.emplace(this->data_index->random_com_deck());
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve all possible CPU-only matches
|
||||
for (auto m : this->zero_round_matches) {
|
||||
m->resolve_following_matches();
|
||||
}
|
||||
}
|
||||
|
||||
void Tournament::print_bracket(FILE* stream) const {
|
||||
function<void(shared_ptr<Match>, size_t)> print_match = [&](shared_ptr<Match> m, size_t indent_level) -> void {
|
||||
for (size_t z = 0; z < indent_level; z++) {
|
||||
fputc(' ', stream);
|
||||
fputc(' ', stream);
|
||||
}
|
||||
string match_str = m->str();
|
||||
fprintf(stream, "%s\n", match_str.c_str());
|
||||
if (m->preceding_a) {
|
||||
print_match(m->preceding_a, indent_level + 1);
|
||||
}
|
||||
if (m->preceding_b) {
|
||||
print_match(m->preceding_b, indent_level + 1);
|
||||
}
|
||||
};
|
||||
print_match(this->final_match, 0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
vector<shared_ptr<Tournament>> TournamentIndex::all_tournaments() const {
|
||||
vector<shared_ptr<Tournament>> ret;
|
||||
for (size_t z = 0; z < this->tournaments.size(); z++) {
|
||||
if (this->tournaments[z]) {
|
||||
ret.emplace_back(this->tournaments[z]);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
shared_ptr<Tournament> TournamentIndex::create_tournament(
|
||||
shared_ptr<const DataIndex> data_index,
|
||||
const string& name,
|
||||
shared_ptr<const DataIndex::MapEntry> map,
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
bool is_2v2) {
|
||||
// Find an unused tournament number
|
||||
uint8_t number;
|
||||
for (number = 0; number < this->tournaments.size(); number++) {
|
||||
if (!this->tournaments[number]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (number >= this->tournaments.size()) {
|
||||
throw runtime_error("all tournament slots are full");
|
||||
}
|
||||
|
||||
auto t = make_shared<Tournament>(data_index, number, name, map, rules, num_teams, is_2v2);
|
||||
t->init();
|
||||
this->tournaments[number] = t;
|
||||
return t;
|
||||
}
|
||||
|
||||
void TournamentIndex::delete_tournament(uint8_t number) {
|
||||
this->tournaments[number].reset();
|
||||
}
|
||||
|
||||
shared_ptr<Tournament> TournamentIndex::get_tournament(uint8_t number) const {
|
||||
return this->tournaments[number];
|
||||
}
|
||||
|
||||
shared_ptr<Tournament> TournamentIndex::get_tournament(const string& name) const {
|
||||
for (size_t z = 0; z < this->tournaments.size(); z++) {
|
||||
if (this->tournaments[z] && (this->tournaments[z]->get_name() == name)) {
|
||||
return this->tournaments[z];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
@@ -0,0 +1,164 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <event2/event.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <string>
|
||||
#include <phosg/Strings.hh>
|
||||
|
||||
#include "../Player.hh"
|
||||
|
||||
struct Lobby;
|
||||
|
||||
namespace Episode3 {
|
||||
|
||||
// The comment in Server.hh does not apply to this file (and Tournament.cc).
|
||||
|
||||
|
||||
|
||||
// TODO: We should build a way to save tournament state to a file, so it can
|
||||
// persist across server restarts.
|
||||
|
||||
class Tournament : public std::enable_shared_from_this<Tournament> {
|
||||
public:
|
||||
enum class State {
|
||||
REGISTRATION = 0,
|
||||
IN_PROGRESS,
|
||||
COMPLETE,
|
||||
};
|
||||
|
||||
struct Team : public std::enable_shared_from_this<Team> {
|
||||
std::weak_ptr<Tournament> tournament;
|
||||
size_t index;
|
||||
size_t max_players;
|
||||
std::set<uint32_t> player_serial_numbers;
|
||||
std::set<std::shared_ptr<const COMDeckDefinition>> com_decks;
|
||||
std::string name;
|
||||
std::string password;
|
||||
size_t num_rounds_cleared;
|
||||
bool is_active;
|
||||
|
||||
Team(
|
||||
std::shared_ptr<Tournament> tournament,
|
||||
size_t index,
|
||||
size_t max_players);
|
||||
std::string str() const;
|
||||
|
||||
void register_player(
|
||||
uint32_t serial_number,
|
||||
const std::string& team_name,
|
||||
const std::string& password);
|
||||
bool unregister_player(uint32_t serial_number);
|
||||
};
|
||||
|
||||
struct Match : public std::enable_shared_from_this<Match> {
|
||||
enum class WinnerTeam {
|
||||
A = 0,
|
||||
B = 1,
|
||||
};
|
||||
std::weak_ptr<Tournament> tournament;
|
||||
std::shared_ptr<Match> preceding_a;
|
||||
std::shared_ptr<Match> preceding_b;
|
||||
std::weak_ptr<Match> following;
|
||||
std::shared_ptr<Team> winner_team;
|
||||
size_t round_num;
|
||||
|
||||
Match(
|
||||
std::shared_ptr<Tournament> tournament,
|
||||
std::shared_ptr<Match> preceding_a,
|
||||
std::shared_ptr<Match> preceding_b);
|
||||
Match(
|
||||
std::shared_ptr<Tournament> tournament,
|
||||
std::shared_ptr<Team> winner_team);
|
||||
std::string str() const;
|
||||
|
||||
bool resolve_if_no_players();
|
||||
void resolve_following_matches();
|
||||
void set_winner_team(std::shared_ptr<Team> team);
|
||||
std::shared_ptr<Team> opponent_team_for_team(std::shared_ptr<Team> team) const;
|
||||
};
|
||||
|
||||
Tournament(
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
uint8_t number,
|
||||
const std::string& name,
|
||||
std::shared_ptr<const DataIndex::MapEntry> map,
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
bool is_2v2);
|
||||
~Tournament() = default;
|
||||
void init();
|
||||
|
||||
std::shared_ptr<const DataIndex> get_data_index() const;
|
||||
uint8_t get_number() const;
|
||||
const std::string& get_name() const;
|
||||
std::shared_ptr<const DataIndex::MapEntry> get_map() const;
|
||||
const Rules& get_rules() const;
|
||||
bool get_is_2v2() const;
|
||||
State get_state() const;
|
||||
|
||||
const std::vector<std::shared_ptr<Team>>& all_teams() const;
|
||||
std::shared_ptr<Team> get_team(size_t index) const;
|
||||
std::shared_ptr<Team> get_winner_team() const;
|
||||
std::shared_ptr<Match> next_match_for_team(std::shared_ptr<Team> team) const;
|
||||
void start();
|
||||
|
||||
void print_bracket(FILE* stream) const;
|
||||
void print_bracket_stderr() const;
|
||||
|
||||
private:
|
||||
PrefixedLogger log;
|
||||
|
||||
std::shared_ptr<const DataIndex> data_index;
|
||||
uint8_t number;
|
||||
std::string name;
|
||||
std::shared_ptr<const DataIndex::MapEntry> map;
|
||||
Rules rules;
|
||||
size_t num_teams;
|
||||
bool is_2v2;
|
||||
State current_state;
|
||||
|
||||
std::set<uint32_t> all_player_serial_numbers;
|
||||
std::unordered_set<std::shared_ptr<Match>> pending_matches;
|
||||
|
||||
// This vector contains all teams in the original starting order of the
|
||||
// tournament (that is, all teams in the first round). The order within this
|
||||
// vector determines which team will play against which other team in the
|
||||
// first round: [0] will play against [1], [2] will play against [3], etc.
|
||||
std::vector<std::shared_ptr<Team>> teams;
|
||||
// The tournament begins with a "zero round", in which each team automatically
|
||||
// "wins" a match, putting them into the first round. This is just to make the
|
||||
// data model easier to manage, so we don't have to have a type of match with
|
||||
// no preceding round.
|
||||
std::vector<std::shared_ptr<Match>> zero_round_matches;
|
||||
std::shared_ptr<Match> final_match;
|
||||
};
|
||||
|
||||
class TournamentIndex {
|
||||
public:
|
||||
TournamentIndex() = default;
|
||||
~TournamentIndex() = default;
|
||||
|
||||
std::vector<std::shared_ptr<Tournament>> all_tournaments() const;
|
||||
|
||||
std::shared_ptr<Tournament> create_tournament(
|
||||
std::shared_ptr<const DataIndex> data_index,
|
||||
const std::string& name,
|
||||
std::shared_ptr<const DataIndex::MapEntry> map,
|
||||
const Rules& rules,
|
||||
size_t num_teams,
|
||||
bool is_2v2);
|
||||
void delete_tournament(uint8_t number);
|
||||
std::shared_ptr<Tournament> get_tournament(uint8_t number) const;
|
||||
std::shared_ptr<Tournament> get_tournament(const std::string& name) const;
|
||||
|
||||
private:
|
||||
parray<std::shared_ptr<Tournament>, 0x20> tournaments;
|
||||
};
|
||||
|
||||
|
||||
|
||||
} // namespace Episode3
|
||||
Reference in New Issue
Block a user