add shuffle and resize options in tournaments

This commit is contained in:
Martin Michelsen
2023-09-15 23:23:03 -07:00
parent 1d45c18ce8
commit 4ddc4fce1d
4 changed files with 163 additions and 74 deletions
+133 -55
View File
@@ -297,8 +297,7 @@ Tournament::Tournament(
shared_ptr<const MapIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2,
bool has_com_teams)
uint8_t flags)
: log(string_printf("[Tournament/%s] ", name.c_str())),
map_index(map_index),
com_deck_index(com_deck_index),
@@ -306,8 +305,7 @@ Tournament::Tournament(
map(map),
rules(rules),
num_teams(num_teams),
is_2v2(is_2v2),
has_com_teams(has_com_teams),
flags(flags),
current_state(State::REGISTRATION),
menu_item_id(0xFFFFFFFF) {
if (this->num_teams < 4) {
@@ -339,8 +337,10 @@ void Tournament::init() {
this->name = this->source_json.get_string("name");
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);
this->flags = this->source_json.get_int("flags", 0x02);
if (this->source_json.get_bool("is_2v2", false)) {
this->flags |= Flag::IS_2V2;
}
is_registration_complete = this->source_json.get_bool("is_registration_complete");
for (const auto& team_json : this->source_json.get_list("teams")) {
@@ -368,40 +368,18 @@ void Tournament::init() {
// Create empty teams
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->shared_from_this(), this->teams.size(), (this->flags & Flag::IS_2V2) ? 2 : 1);
this->teams.emplace_back(t);
}
is_registration_complete = false;
}
// Create the match structure
while (this->zero_round_matches.size() < this->num_teams) {
this->zero_round_matches.emplace_back(make_shared<Match>(
this->shared_from_this(), this->teams[this->zero_round_matches.size()]));
}
// Compute the match state from the teams' states
if (is_registration_complete) {
this->current_state = State::IN_PROGRESS;
this->create_bracket_matches();
// 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(std::move(m));
}
current_round_matches = std::move(next_round_matches);
}
this->final_match = current_round_matches.at(0);
// Start with all first-round matches in the match queue
// Start with all zero-round matches in the match queue
unordered_set<shared_ptr<Match>> match_queue;
for (auto match : this->zero_round_matches) {
match_queue.emplace(match->following.lock());
@@ -457,16 +435,47 @@ void Tournament::init() {
}
} else {
// 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);
}
this->current_state = State::REGISTRATION;
}
}
void Tournament::create_bracket_matches() {
if (this->teams.size() < 4) {
throw logic_error("tournaments must have at least 4 teams");
}
if (this->teams.size() > 32) {
throw logic_error("tournaments must have at most 32 teams");
}
if (this->teams.size() & (this->teams.size() - 1)) {
throw logic_error("tournaments team count is not a power of 2");
}
// Create the zero-round matches and make them all pending
this->zero_round_matches.clear();
for (const auto& team : this->teams) {
auto m = make_shared<Match>(this->shared_from_this(), team);
this->zero_round_matches.emplace_back(m);
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(std::move(m));
}
current_round_matches = std::move(next_round_matches);
}
this->final_match = current_round_matches.at(0);
}
JSON Tournament::json() const {
auto teams_list = JSON::list();
for (auto team : this->teams) {
@@ -490,8 +499,7 @@ JSON Tournament::json() const {
{"name", this->name},
{"map_number", this->map->map.map_number.load()},
{"rules", this->rules.json()},
{"is_2v2", this->is_2v2},
{"has_com_teams", this->has_com_teams},
{"flags", this->flags},
{"is_registration_complete", (this->current_state != State::REGISTRATION)},
{"teams", std::move(teams_list)},
});
@@ -557,22 +565,67 @@ void Tournament::start() {
throw runtime_error("tournament has already started");
}
bool has_com_teams = (this->flags & Flag::HAS_COM_TEAMS);
// 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()) {
for (size_t z = 0; z < this->teams.size(); z++) {
if (this->teams[z]->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)) {
if (num_human_teams < (has_com_teams ? 1 : 2)) {
throw runtime_error("not enough registrants to start tournament");
}
if ((this->flags & Flag::SHUFFLE_ENTRIES) && (this->flags & Flag::RESIZE_ON_START)) {
// If both of these flags are set, pack the human teams into the lowest part
// of the teams list so we can resize the tournament to the smallest
// possible size. This is OK since we're going to shuffle them later anyway
size_t r_offset = 0, w_offset = 0;
for (; r_offset < this->teams.size(); r_offset++) {
if (this->teams[r_offset]->has_any_human_players()) {
if (r_offset != w_offset) {
this->teams[r_offset].swap(this->teams[w_offset]);
}
w_offset++;
}
}
}
if (this->flags & Flag::RESIZE_ON_START) {
// Resize the tournament by repeatedly deleting the second half of it, until
// the second half contains human players or the tournament size is 4
while (this->teams.size() > 4) {
size_t z;
for (z = this->teams.size() >> 1; z < this->teams.size(); z++) {
if (this->teams[z]->has_any_human_players()) {
break;
}
}
if (z == this->teams.size()) {
this->teams.resize(this->teams.size() >> 1);
} else {
break;
}
}
this->num_teams = this->teams.size();
}
if (this->flags & Flag::SHUFFLE_ENTRIES) {
// Shuffle all the tournament entries
for (size_t z = this->teams.size(); z > 0; z--) {
size_t index = random_object<uint32_t>() % z;
if (index != z - 1) {
this->teams[z - 1].swap(this->teams[index]);
}
}
}
this->current_state = State::IN_PROGRESS;
this->create_bracket_matches();
// Assign names to COM teams, and assign COM decks to all empty slots unless
// has_com_teams is false
@@ -580,7 +633,7 @@ void Tournament::start() {
auto m = this->zero_round_matches[z];
auto t = m->winner_team;
if (t->name.empty()) {
t->name = this->has_com_teams ? string_printf("COM:%zu", z) : "(no entrant)";
t->name = has_com_teams ? string_printf("COM:%zu", z) : "(no entrant)";
}
for (const auto& player : t->players) {
if (player.is_com()) {
@@ -592,7 +645,7 @@ void Tournament::start() {
}
// 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()) {
if (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) {
@@ -624,6 +677,20 @@ void Tournament::send_all_state_updates(shared_ptr<ServerState> s) const {
}
}
void Tournament::send_all_state_updates_on_deletion() const {
for (const auto& team : this->teams) {
for (const auto& player : team->players) {
auto c = player.client.lock();
if (c &&
(c->flags & Client::Flag::IS_EPISODE_3) &&
!(c->flags & Client::Flag::IS_EP3_TRIAL_EDITION) &&
(c->ep3_tournament_team.lock() == team)) {
send_ep3_confirm_tournament_entry(nullptr, c, nullptr);
}
}
}
}
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++) {
@@ -644,7 +711,10 @@ void Tournament::print_bracket(FILE* stream) const {
fprintf(stream, " Map: %08" PRIX32 " (%s)\n", this->map->map.map_number.load(), map_name.c_str());
string rules_str = this->rules.str();
fprintf(stream, " Rules: %s\n", rules_str.c_str());
fprintf(stream, " Structure: %s, %zu entries\n", this->is_2v2 ? "2v2" : "1v1", this->num_teams);
fprintf(stream, " Structure: %s, %zu entries\n", (this->flags & Flag::IS_2V2) ? "2v2" : "1v1", this->num_teams);
fprintf(stream, " COM teams: %s\n", (this->flags & Flag::HAS_COM_TEAMS) ? "allowed" : "forbidden");
fprintf(stream, " Shuffle entries: %s\n", (this->flags & Flag::SHUFFLE_ENTRIES) ? "yes" : "no");
fprintf(stream, " Resize on start: %s\n", (this->flags & Flag::RESIZE_ON_START) ? "yes" : "no");
switch (this->current_state) {
case State::REGISTRATION:
fprintf(stream, " State: REGISTRATION\n");
@@ -663,10 +733,18 @@ void Tournament::print_bracket(FILE* stream) const {
fprintf(stream, " Standings:\n");
print_match(this->final_match, 2);
}
fprintf(stream, " Pending matches:\n");
for (const auto& match : this->pending_matches) {
string match_str = match->str();
fprintf(stream, " %s\n", match_str.c_str());
if (this->current_state == State::REGISTRATION) {
fprintf(stream, " Teams:\n");
for (const auto& team : this->teams) {
string team_str = team->str();
fprintf(stream, " %s\n", team_str.c_str());
}
} else {
fprintf(stream, " Pending matches:\n");
for (const auto& match : this->pending_matches) {
string match_str = match->str();
fprintf(stream, " %s\n", match_str.c_str());
}
}
}
@@ -733,7 +811,7 @@ void TournamentIndex::save() const {
for (const auto& it : this->name_to_tournament) {
json.emplace(it.second->get_name(), it.second->json());
}
save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT));
save_file(this->state_filename, json.serialize(JSON::SerializeOption::FORMAT | JSON::SerializeOption::HEX_INTEGERS));
}
shared_ptr<Tournament> TournamentIndex::create_tournament(
@@ -741,14 +819,13 @@ shared_ptr<Tournament> TournamentIndex::create_tournament(
shared_ptr<const MapIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2,
bool has_com_teams) {
uint8_t flags) {
if (this->name_to_tournament.size() >= 0x20) {
throw runtime_error("there can be at most 32 tournaments at a time");
}
auto t = make_shared<Tournament>(
this->map_index, this->com_deck_index, name, map, rules, num_teams, is_2v2, has_com_teams);
this->map_index, this->com_deck_index, name, map, rules, num_teams, flags);
t->init();
this->name_to_tournament.emplace(t->get_name(), t);
@@ -780,6 +857,7 @@ bool TournamentIndex::delete_tournament(const string& name) {
it->second->set_menu_item_id(0xFFFFFFFF);
}
}
it->second->send_all_state_updates_on_deletion();
this->name_to_tournament.erase(it);
this->save();
return true;
+14 -11
View File
@@ -22,6 +22,12 @@ namespace Episode3 {
class Tournament : public std::enable_shared_from_this<Tournament> {
public:
enum Flag : uint8_t {
IS_2V2 = 0x01,
HAS_COM_TEAMS = 0x02,
SHUFFLE_ENTRIES = 0x04,
RESIZE_ON_START = 0x08,
};
enum class State {
REGISTRATION = 0,
IN_PROGRESS,
@@ -104,8 +110,7 @@ public:
std::shared_ptr<const MapIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2,
bool has_com_teams);
uint8_t flags);
Tournament(
std::shared_ptr<const MapIndex> map_index,
std::shared_ptr<const COMDeckIndex> com_deck_index,
@@ -124,11 +129,8 @@ public:
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 uint8_t get_flags() const {
return this->flags;
}
inline State get_state() const {
return this->current_state;
@@ -155,10 +157,13 @@ public:
void start();
void send_all_state_updates(std::shared_ptr<ServerState> s) const;
void send_all_state_updates_on_deletion() const;
void print_bracket(FILE* stream) const;
private:
void create_bracket_matches();
PrefixedLogger log;
std::shared_ptr<const MapIndex> map_index;
@@ -168,8 +173,7 @@ private:
std::shared_ptr<const MapIndex::MapEntry> map;
Rules rules;
size_t num_teams;
bool is_2v2;
bool has_com_teams;
uint8_t flags;
State current_state;
uint32_t menu_item_id;
@@ -223,8 +227,7 @@ public:
std::shared_ptr<const MapIndex::MapEntry> map,
const Rules& rules,
size_t num_teams,
bool is_2v2,
bool has_com_teams);
uint8_t flags);
bool delete_tournament(const std::string& name);
std::shared_ptr<Tournament::Team> team_for_serial_number(uint32_t serial_number) const;
+5 -3
View File
@@ -2258,6 +2258,8 @@ void send_ep3_confirm_tournament_entry(
shared_ptr<ServerState> s,
shared_ptr<Client> c,
shared_ptr<const Episode3::Tournament> tourn) {
// WARNING: s is permitted to be null if tourn is null
if (c->flags & Client::Flag::IS_EP3_TRIAL_EDITION) {
throw runtime_error("cannot send tournament entry command to Episode 3 Trial Edition client");
}
@@ -2326,7 +2328,7 @@ void send_ep3_tournament_entry_list(
shared_ptr<const Episode3::Tournament> tourn,
bool is_for_spectator_team_create) {
S_TournamentEntryList_GC_Ep3_E2 cmd;
cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1;
cmd.players_per_team = (tourn->get_flags() & Episode3::Tournament::Flag::IS_2V2) ? 2 : 1;
size_t z = 0;
for (const auto& team : tourn->all_teams()) {
if (z >= 0x20) {
@@ -2366,7 +2368,7 @@ void send_ep3_tournament_details(
cmd.bracket_entries[z].team_name = teams[z]->name;
}
cmd.num_bracket_entries = teams.size();
cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1;
cmd.players_per_team = (tourn->get_flags() & Episode3::Tournament::Flag::IS_2V2) ? 2 : 1;
send_command_t(c, 0xE3, 0x02, cmd);
}
@@ -2409,7 +2411,7 @@ void send_ep3_game_details(shared_ptr<Client> c, shared_ptr<Lobby> l) {
entry.team_name = teams[z]->name;
}
cmd.num_bracket_entries = teams.size();
cmd.players_per_team = tourn->get_is_2v2() ? 2 : 1;
cmd.players_per_team = (tourn->get_flags() & Episode3::Tournament::Flag::IS_2V2) ? 2 : 1;
if (primary_lobby) {
auto serial_number_to_client = primary_lobby->clients_by_serial_number();
+11 -5
View File
@@ -162,6 +162,9 @@ Server commands:\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\
shuffle: Shuffle entries when starting the tournament\n\
resize: If the tournament is less than half full when it starts, reduce\n\
the number of rounds to fit the existing entries\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,16 +463,19 @@ Proxy session commands:\n\
uint32_t num_teams = stoul(get_quoted_string(command_args), nullptr, 0);
Episode3::Rules rules;
rules.set_defaults();
bool is_2v2 = false;
bool has_com_teams = true;
uint8_t flags = Episode3::Tournament::Flag::HAS_COM_TEAMS;
if (!command_args.empty()) {
auto tokens = split(command_args, ' ');
for (auto& token : tokens) {
token = tolower(token);
if (token == "2v2") {
is_2v2 = true;
flags |= Episode3::Tournament::Flag::IS_2V2;
} else if (token == "no-coms") {
has_com_teams = false;
flags &= (~Episode3::Tournament::Flag::HAS_COM_TEAMS);
} else if (token == "shuffle") {
flags |= Episode3::Tournament::Flag::SHUFFLE_ENTRIES;
} else if (token == "resize") {
flags |= Episode3::Tournament::Flag::RESIZE_ON_START;
} else if (starts_with(token, "dice=")) {
auto subtokens = split(token.substr(5), '-');
if (subtokens.size() != 2) {
@@ -539,7 +545,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, has_com_teams);
name, map, rules, num_teams, flags);
fprintf(stderr, "created tournament \"%s\"\n", tourn->get_name().c_str());
} else if (command_name == "delete-tournament") {