implement episode 3 tournaments

This commit is contained in:
Martin Michelsen
2022-12-06 00:16:13 -08:00
parent 8c2ea48b80
commit 9a1ba56982
24 changed files with 1721 additions and 191 deletions
+44 -2
View File
@@ -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
+19
View File
@@ -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;
};
+2 -2
View File
@@ -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
View File
@@ -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();
+6 -2
View File
@@ -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);
+449
View File
@@ -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
+164
View File
@@ -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