From 9cef4a14f8ecce15803998769772a20f1c187326 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 15 Sep 2023 19:49:12 -0700 Subject: [PATCH] add a tournament option to disable COM entries --- src/Episode3/Tournament.cc | 114 +++++++++++++++++++------------------ src/Episode3/Tournament.hh | 44 ++++++++++---- src/ServerShell.cc | 6 +- 3 files changed, 96 insertions(+), 68 deletions(-) diff --git a/src/Episode3/Tournament.cc b/src/Episode3/Tournament.cc index 07763d84..beeed4c5 100644 --- a/src/Episode3/Tournament.cc +++ b/src/Episode3/Tournament.cc @@ -200,24 +200,32 @@ string Tournament::Match::str() const { return string_printf("[Match round=%zu winner=%s]", this->round_num, winner_str.c_str()); } -bool Tournament::Match::resolve_if_no_human_players() { +bool Tournament::Match::resolve_if_skippable() { if (this->winner_team) { return true; } - // 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->preceding_a->winner_team && - this->preceding_b->winner_team && - !this->preceding_a->winner_team->has_any_human_players() && - !this->preceding_b->winner_team->has_any_human_players()) { - this->set_winner_team((random_object() & 1) - ? this->preceding_b->winner_team - : this->preceding_a->winner_team); - return true; - } else { + + auto winner_a = this->preceding_a->winner_team; + auto winner_b = this->preceding_b->winner_team; + + // If at least one match before this is not resolved, don't resolve this one + if (!winner_a || !winner_b) { return false; } + // If one of the preceding winner teams is empty, make the other the winner + if (winner_a->players.empty() != winner_b->players.empty()) { + this->set_winner_team(winner_a->players.empty() ? winner_b : winner_a); + return true; + } + // If neither preceding winner team has any humans on it, skip this match + // entirely and just make one team advance arbitrarily (note that this also + // handles the case where both preceding winner teams are empty) + if (!winner_a->has_any_human_players() && !winner_b->has_any_human_players()) { + this->set_winner_team((random_object() & 1) ? winner_b : winner_a); + return true; + } + + return false; } void Tournament::Match::on_winner_team_set() { @@ -231,7 +239,7 @@ void Tournament::Match::on_winner_team_set() { // Resolve the following match if possible (this skips CPU-only matches). If // the following match can't be resolved, mark it pending. auto following = this->following.lock(); - if (following && !following->resolve_if_no_human_players()) { + if (following && !following->resolve_if_skippable()) { tournament->pending_matches.emplace(following); } @@ -287,7 +295,8 @@ Tournament::Tournament( shared_ptr map, const Rules& rules, size_t num_teams, - bool is_2v2) + bool is_2v2, + bool has_com_teams) : log(string_printf("[Tournament/%02hhX] ", number)), map_index(map_index), com_deck_index(com_deck_index), @@ -297,6 +306,7 @@ Tournament::Tournament( rules(rules), num_teams(num_teams), is_2v2(is_2v2), + has_com_teams(has_com_teams), current_state(State::REGISTRATION) { if (this->num_teams < 4) { throw invalid_argument("team count must be 4 or more"); @@ -330,6 +340,7 @@ void Tournament::init() { this->map = this->map_index->definition_for_number(this->source_json.get_int("map_number")); this->rules = Rules(this->source_json.at("rules")); this->is_2v2 = this->source_json.get_bool("is_2v2"); + this->has_com_teams = this->source_json.get_bool("has_com_teams", true); is_registration_complete = this->source_json.get_bool("is_registration_complete"); for (const auto& team_json : this->source_json.get_list("teams")) { @@ -480,43 +491,12 @@ JSON Tournament::json() const { {"map_number", this->map->map.map_number.load()}, {"rules", this->rules.json()}, {"is_2v2", this->is_2v2}, + {"has_com_teams", this->has_com_teams}, {"is_registration_complete", (this->current_state != State::REGISTRATION)}, {"teams", std::move(teams_list)}, }); } -uint8_t Tournament::get_number() const { - return this->number; -} - -const string& Tournament::get_name() const { - return this->name; -} - -shared_ptr 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>& Tournament::all_teams() const { - return this->teams; -} - -shared_ptr Tournament::get_team(size_t index) const { - return this->teams.at(index); -} - shared_ptr Tournament::get_winner_team() const { if (this->current_state != State::COMPLETE) { return nullptr; @@ -574,14 +554,30 @@ void Tournament::start() { throw runtime_error("tournament has already started"); } + // If there aren't enough entrants (1 if has_com_teams is false, else 2), + // don't allow the tournament to start (because it would enter the COMPLETE + // state immediately) + size_t num_human_teams = 0; + for (size_t z = 0; z < this->zero_round_matches.size(); z++) { + if (this->zero_round_matches[z]->winner_team->has_any_human_players()) { + num_human_teams++; + } + } + fprintf(stderr, "num_human_teams: %zu\n", num_human_teams); + fprintf(stderr, "has_com_teams: %s\n", this->has_com_teams ? "true" : "false"); + if (num_human_teams < (this->has_com_teams ? 1 : 2)) { + throw runtime_error("not enough registrants to start tournament"); + } + this->current_state = State::IN_PROGRESS; - // Assign names to COM teams, and assign COM decks to all empty slots + // Assign names to COM teams, and assign COM decks to all empty slots unless + // has_com_teams is false 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); + t->name = this->has_com_teams ? string_printf("COM:%zu", z) : "(no entrant)"; } for (const auto& player : t->players) { if (player.is_com()) { @@ -591,14 +587,18 @@ void Tournament::start() { if (this->com_deck_index->num_decks() < t->max_players - t->players.size()) { throw runtime_error("not enough COM decks to complete team"); } - // TODO: Don't allow duplicate COM decks, nor duplicate COM SCs on the same - // team - while (t->players.size() < t->max_players) { - t->players.emplace_back(this->com_deck_index->random_deck()); + // If we allow all-COM teams, or this is a 2v2 tournament and the team has + // only one human on it, add a COM + if (this->has_com_teams || !t->players.empty()) { + // TODO: Don't allow duplicate COM decks, nor duplicate COM SCs on the + // same team + while (t->players.size() < t->max_players) { + t->players.emplace_back(this->com_deck_index->random_deck()); + } } } - // Resolve all possible CPU-only matches + // Resolve all possible skippable matches for (auto m : this->zero_round_matches) { m->on_winner_team_set(); } @@ -666,6 +666,7 @@ TournamentIndex::TournamentIndex( } catch (const cannot_open_file&) { json = JSON::list(); } + if (json.size() > 0x20) { throw runtime_error("tournament JSON list length is incorrect"); } @@ -708,7 +709,8 @@ shared_ptr TournamentIndex::create_tournament( shared_ptr map, const Rules& rules, size_t num_teams, - bool is_2v2) { + bool is_2v2, + bool has_com_teams) { // Find an unused tournament number uint8_t number; for (number = 0; number < 0x20; number++) { @@ -721,7 +723,7 @@ shared_ptr TournamentIndex::create_tournament( } auto t = make_shared( - this->map_index, this->com_deck_index, number, name, map, rules, num_teams, is_2v2); + this->map_index, this->com_deck_index, number, name, map, rules, num_teams, is_2v2, has_com_teams); t->init(); this->tournaments[number] = t; return t; diff --git a/src/Episode3/Tournament.hh b/src/Episode3/Tournament.hh index be19b79a..21d114c1 100644 --- a/src/Episode3/Tournament.hh +++ b/src/Episode3/Tournament.hh @@ -84,7 +84,7 @@ public: std::shared_ptr winner_team); std::string str() const; - bool resolve_if_no_human_players(); + bool resolve_if_skippable(); void on_winner_team_set(); void set_winner_team(std::shared_ptr team); void set_winner_team_without_triggers(std::shared_ptr team); @@ -99,7 +99,8 @@ public: std::shared_ptr map, const Rules& rules, size_t num_teams, - bool is_2v2); + bool is_2v2, + bool has_com_teams); Tournament( std::shared_ptr map_index, std::shared_ptr com_deck_index, @@ -110,15 +111,34 @@ public: JSON json() const; - uint8_t get_number() const; - const std::string& get_name() const; - std::shared_ptr get_map() const; - const Rules& get_rules() const; - bool get_is_2v2() const; - State get_state() const; + inline uint8_t get_number() const { + return this->number; + } + inline const std::string& get_name() const { + return this->name; + } + inline std::shared_ptr get_map() const { + return this->map; + } + inline const Rules& get_rules() const { + return this->rules; + } + inline bool get_is_2v2() const { + return this->is_2v2; + } + inline bool get_has_com_teams() const { + return this->has_com_teams; + } + inline State get_state() const { + return this->current_state; + } + inline const std::vector>& all_teams() const { + return this->teams; + } + std::shared_ptr get_team(size_t index) const { + return this->teams.at(index); + } - const std::vector>& all_teams() const; - std::shared_ptr get_team(size_t index) const; std::shared_ptr get_winner_team() const; std::shared_ptr next_match_for_team(std::shared_ptr team) const; std::shared_ptr get_final_match() const; @@ -141,6 +161,7 @@ private: Rules rules; size_t num_teams; bool is_2v2; + bool has_com_teams; State current_state; std::set all_player_serial_numbers; @@ -177,7 +198,8 @@ public: std::shared_ptr map, const Rules& rules, size_t num_teams, - bool is_2v2); + bool is_2v2, + bool has_com_teams); void delete_tournament(uint8_t number); std::shared_ptr get_tournament(uint8_t number) const; std::shared_ptr get_tournament(const std::string& name) const; diff --git a/src/ServerShell.cc b/src/ServerShell.cc index fb52aa87..030cb8f9 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -161,6 +161,7 @@ Server commands:\n\ and map names, unless the names contain no spaces.\n\ OPTIONS may include:\n\ 2v2: Set team size to 2 players (default is 1 without this option)\n\ + no-coms: Don\'t add any COM teams to the tournament bracket\n\ dice=MIN-MAX: Set minimum and maximum dice rolls\n\ overall-time-limit=N: Set battle time limit (in multiples of 5 minutes)\n\ phase-time-limit=N: Set phase time limit (in seconds)\n\ @@ -460,12 +461,15 @@ Proxy session commands:\n\ Episode3::Rules rules; rules.set_defaults(); bool is_2v2 = false; + bool has_com_teams = true; if (!command_args.empty()) { auto tokens = split(command_args, ' '); for (auto& token : tokens) { token = tolower(token); if (token == "2v2") { is_2v2 = true; + } else if (token == "no-coms") { + has_com_teams = false; } else if (starts_with(token, "dice=")) { auto subtokens = split(token.substr(5), '-'); if (subtokens.size() != 2) { @@ -535,7 +539,7 @@ Proxy session commands:\n\ fprintf(stderr, "warning: some rules were invalid and reset to defaults\n"); } auto tourn = this->state->ep3_tournament_index->create_tournament( - name, map, rules, num_teams, is_2v2); + name, map, rules, num_teams, is_2v2, has_com_teams); this->state->ep3_tournament_index->save(); fprintf(stderr, "created tournament %02hhX\n", tourn->get_number());