}
// Load the matches themselves.
- ret = sqlite3_prepare_v2(db, "SELECT match, description FROM match ORDER BY match", -1, &stmt, 0);
+ ret = sqlite3_prepare_v2(db, "SELECT match, description, gender_rule_a, gender_pull_rule FROM match ORDER BY match", -1, &stmt, 0);
if (ret != SQLITE_OK) {
fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
abort();
QJsonObject m;
m.insert("match_id", match);
m.insert("description", (const char *)sqlite3_column_text(stmt, 1));
+ m.insert("gender_rule_a", (bool)sqlite3_column_int64(stmt, 2));
+ m.insert("gender_pull_rule", (bool)sqlite3_column_int64(stmt, 3));
if (events_per_match.count(match)) {
m.insert("events", std::move(*events_per_match[match]));
delete events_per_match[match];
)", nullptr, nullptr, nullptr); // Ignore errors.
sqlite3_exec(db, R"(
- CREATE TABLE IF NOT EXISTS match (match INTEGER PRIMARY KEY, description VARCHAR, video_filename VARCHAR);
+ CREATE TABLE IF NOT EXISTS match (match INTEGER PRIMARY KEY, description VARCHAR, video_filename VARCHAR, gender_rule_a BOOLEAN DEFAULT false, gender_pull_rule BOOLEAN DEFAULT false);
)", nullptr, nullptr, nullptr); // Ignore errors.
sqlite3_exec(db, R"(
}
// Insert the new row into the database.
- ret = sqlite3_prepare_v2(db, "INSERT INTO match (description) VALUES (?)", -1, &stmt, 0);
+ ret = sqlite3_prepare_v2(db, "INSERT INTO match (description, gender_rule_a, gender_pull_rule) VALUES (?, COALESCE((SELECT gender_rule_a FROM match ORDER BY match DESC LIMIT 1),false), COALESCE((SELECT gender_pull_rule FROM match ORDER BY match DESC LIMIT 1),false))", -1, &stmt, 0);
if (ret != SQLITE_OK) {
fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
abort();
return filename;
}
+bool get_match_property(sqlite3 *db, int match_id, const string &prop_name)
+{
+ sqlite3_stmt *stmt;
+
+ int ret = sqlite3_prepare_v2(db, ("SELECT " + prop_name + " FROM match WHERE match=?").c_str(), -1, &stmt, 0);
+ if (ret != SQLITE_OK) {
+ fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
+ abort();
+ }
+
+ sqlite3_bind_int64(stmt, 1, match_id);
+
+ ret = sqlite3_step(stmt);
+ if (ret != SQLITE_ROW) {
+ fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
+ abort();
+ }
+
+ if (sqlite3_column_type(stmt, 0) != SQLITE_INTEGER) {
+ return "";
+ }
+ bool value = sqlite3_column_int(stmt, 0);
+
+ ret = sqlite3_finalize(stmt);
+ if (ret != SQLITE_OK) {
+ fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
+ abort();
+ }
+ return value;
+}
+
void save_video_filename(sqlite3 *db, int match_id, const string &filename)
{
sqlite3_stmt *stmt;
ret = sqlite3_step(stmt);
if (ret == SQLITE_ROW) {
- fprintf(stderr, "INSERT step: %s\n", sqlite3_errmsg(db));
+ fprintf(stderr, "UPDATE step: %s\n", sqlite3_errmsg(db));
+ abort();
+ }
+
+ ret = sqlite3_finalize(stmt);
+ if (ret != SQLITE_OK) {
+ fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
+ abort();
+ }
+}
+
+void save_match_property(sqlite3 *db, int match_id, const string &prop_name, bool value)
+{
+ sqlite3_stmt *stmt;
+
+ int ret = sqlite3_prepare_v2(db, ("UPDATE match SET " + prop_name + "=? WHERE match=?").c_str(), -1, &stmt, 0);
+ if (ret != SQLITE_OK) {
+ fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
+ abort();
+ }
+
+ sqlite3_bind_int64(stmt, 1, value);
+ sqlite3_bind_int64(stmt, 2, match_id);
+
+ ret = sqlite3_step(stmt);
+ if (ret == SQLITE_ROW) {
+ fprintf(stderr, "UPDATE step: %s\n", sqlite3_errmsg(db));
abort();
}
// Menus.
connect(ui->action_exit, &QAction::triggered, [this] { close(); });
connect(ui->action_export_json, &QAction::triggered, [db] { export_to_json(db, "ultimate.json"); });
+
+ ui->action_gender_rule_a->setChecked(get_match_property(db, match_id, "gender_rule_a"));
+ ui->action_gender_pull_rule->setChecked(get_match_property(db, match_id, "gender_pull_rule"));
+ connect(ui->action_gender_rule_a, &QAction::toggled, [this, db, match_id] {
+ save_match_property(db, match_id, "gender_rule_a", ui->action_gender_rule_a->isChecked());
+ });
+ connect(ui->action_gender_pull_rule, &QAction::toggled, [this, db, match_id] {
+ save_match_property(db, match_id, "gender_pull_rule", ui->action_gender_pull_rule->isChecked());
+ });
}
void MainWindow::position_changed(uint64_t pos)
<addaction name="action_export_json"/>
<addaction name="action_exit"/>
</widget>
+ <widget class="QMenu" name="menuMatch">
+ <property name="title">
+ <string>Match</string>
+ </property>
+ <addaction name="action_gender_rule_a"/>
+ <addaction name="action_gender_pull_rule"/>
+ </widget>
<addaction name="menu_File"/>
+ <addaction name="menuMatch"/>
</widget>
<action name="action_export_json">
<property name="text">
<string>E&xit</string>
</property>
</action>
+ <action name="action_gender_rule_a">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Gender rule &A</string>
+ </property>
+ </action>
+ <action name="action_gender_pull_rule">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>Gender &pull rule</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
let our_score = 0;
let their_score = 0;
+ let between_points = true;
let handler = null;
let handler_got_by_interception = false; // Only relevant if handler !== null.
let prev_handler = null;
let point_num = 0;
let game_started = null;
let last_goal = null;
+ let current_predominant_gender = null;
+ let current_num_players_on_field = null;
// The last used formations of the given kind, if any; they may be reused
// when the point starts, if nothing else is set.
keep = keep_event(players, formations_used_this_point, last_pull_was_ours, filters); // Recompute after in/out.
+ if (match['gender_rule_a']) {
+ if (type === 'restart' && !between_points) {
+ let predominant_gender = find_predominant_gender(players);
+ let num_players_on_field = find_num_players_on_field(players);
+ if (predominant_gender !== current_predominant_gender && current_predominant_gender !== null) {
+ console.log(match['description'] + ' ' + format_time(t) + ': Stoppage changed predominant gender from ' + current_predominant_gender + ' to ' + predominant_gender);
+ }
+ if (num_players_on_field !== current_num_players_on_field && current_num_players_on_field !== null) {
+ console.log(match['description'] + ' ' + format_time(t) + ': Stoppage changed number of players on field from ' + current_num_players_on_field + ' to ' + num_players_on_field);
+ }
+ current_predominant_gender = predominant_gender;
+ current_num_players_on_field = num_players_on_field;
+ } else if (type === 'pull' || type === 'their_pull') {
+ let predominant_gender = find_predominant_gender(players);
+ let num_players_on_field = find_num_players_on_field(players);
+ let changed = (predominant_gender !== current_predominant_gender);
+ if (point_num !== 0) {
+ let should_change = (point_num % 4 == 1 || point_num % 4 == 3); // ABBA changes on 1 and 3.
+ if (changed && !should_change) {
+ console.log(match['description'] + ' ' + format_time(t) + ': Gender ratio should have stayed the same, changed to predominance of ' + predominant_gender);
+ } else if (!changed && should_change) {
+ console.log(match['description'] + ' ' + format_time(t) + ': Gender ratio should have changed, remained predominantly ' + predominant_gender);
+ }
+ if (num_players_on_field !== current_num_players_on_field && current_num_players_on_field !== null) {
+ console.log(match['description'] + ' ' + format_time(t) + ': Number of players on field changed from ' + current_num_players_on_field + ' to ' + num_players_on_field);
+ }
+ }
+ current_predominant_gender = predominant_gender;
+ current_num_players_on_field = num_players_on_field;
+ }
+ }
+ if (match['gender_pull_rule']) {
+ if (type === 'pull') {
+ if (current_predominant_gender !== null &&
+ p.gender !== current_predominant_gender) {
+ console.log(match['description'] + ' ' + format_time(t) + ': ' + p.name + ' pulled, should have been ' + current_predominant_gender);
+ }
+ }
+ }
+
// Liveness management
if (type === 'pull' || type === 'their_pull' || type === 'restart') {
live_since = t;
+ between_points = false;
} else if (type === 'catch' && last_pull_was_ours === null) {
// Someone forgot to add the pull, so we'll need to wing it.
console.log(match['description'] + ' ' + format_time(t) + ': Missing pull on ' + our_score + '\u2013' + their_score + '; pretending to have one.');
live_since = t;
last_pull_was_ours = !offense;
+ between_points = false;
} else if (type === 'goal' || type === 'their_goal' || type === 'stoppage') {
for (const [q,p] of Object.entries(players)) {
if (p.on_field_since === null) {
// Score management
if (type === 'goal') {
++our_score;
+ between_points = true;
} else if (type === 'their_goal') {
++their_score;
+ between_points = true;
}
// Point count management
return true;
}
-function find_gender_ratio_code(players) {
+// Returns a map of e.g. F => 4, M => 3.
+function find_gender_ratio(players) {
let map = {};
for (const [q,p] of Object.entries(players)) {
if (p.on_field_since === null) {
++map[gender];
}
}
+ return map;
+}
+
+function find_gender_ratio_code(players) {
+ let map = find_gender_ratio(players);
let all_genders = Array.from(Object.keys(map)).sort(
(a,b) => {
if (map[a] !== map[b]) {
return code;
}
+// null if none (e.g., if playing 3–3).
+function find_predominant_gender(players) {
+ let max = 0;
+ let predominant_gender = null;
+ for (const [gender, num] of Object.entries(find_gender_ratio(players))) {
+ if (num > max) {
+ max = num;
+ predominant_gender = gender;
+ } else if (num == max) {
+ predominant_gender = null; // At least two have the same.
+ }
+ }
+ return predominant_gender;
+}
+
+function find_num_players_on_field(players) {
+ let num = 0;
+ for (const [q,p] of Object.entries(players)) {
+ if (p.on_field_since !== null) {
+ ++num;
+ }
+ }
+ return num;
+}
+
function filter_passes(players, formations_used_this_point, last_pull_was_ours, filter) {
if (filter.type === 'player_any') {
for (const p of Array.from(filter.elements)) {