]> git.sesse.net Git - pkanalytics/commitdiff
Implement optional gender rule A and pull rules.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 26 Jul 2023 15:34:43 +0000 (17:34 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Wed, 26 Jul 2023 15:34:43 +0000 (17:34 +0200)
Only used for warnings in the viewer's console for now, but may be
used for sanity checking when subbing in the future.

json.cpp
main.cpp
mainwindow.cpp
mainwindow.ui
ultimate.js

index 0bd24522e839bdcbfa64fa33b295ed5d3aa88fd6..2e3398d9057d8f431aa3251ad3bb2eea42f45b38 100644 (file)
--- a/json.cpp
+++ b/json.cpp
@@ -121,7 +121,7 @@ QJsonArray export_matches_to_json(sqlite3 *db)
        }
 
        // 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();
@@ -133,6 +133,8 @@ QJsonArray export_matches_to_json(sqlite3 *db)
                        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];
index bda85588e02cd167f9e7d73f7ecba257c00e5f35..e233c08c30f7a305c00f752455a6e7e7be457cfd 100644 (file)
--- a/main.cpp
+++ b/main.cpp
@@ -33,7 +33,7 @@ sqlite3 *open_db(const char *filename)
        )", 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"(
@@ -118,7 +118,7 @@ int get_match_id(sqlite3 *db, QWidget *parent, int requested_match)
        }
 
        // 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();
index feaf3abc525ed4b0c88fe53e849004a7513b72c4..3b29d729b1f42f8cc2df9161ac2c2524efd0c2c1 100644 (file)
@@ -67,6 +67,37 @@ string get_video_filename(sqlite3 *db, int match_id)
        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;
@@ -82,7 +113,33 @@ void save_video_filename(sqlite3 *db, int match_id, const string &filename)
 
        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();
        }
 
@@ -276,6 +333,15 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players,
        // 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)
index 1a9ede58200af306676f19f66b3e811f154dd54d..3806b608d438c57fac0839556312ddc51e0ea829 100644 (file)
     <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&amp;xit</string>
    </property>
   </action>
+  <action name="action_gender_rule_a">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Gender rule &amp;A</string>
+   </property>
+  </action>
+  <action name="action_gender_pull_rule">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Gender &amp;pull rule</string>
+   </property>
+  </action>
  </widget>
  <customwidgets>
   <customwidget>
index e9b0d801db0490bd13df4f3c484c553aa44f2e27..292534f23dd1f25293c4dad28133d1e0cd4f08bb 100644 (file)
@@ -241,6 +241,7 @@ function calc_stats(json, filters) {
 
                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;
@@ -252,6 +253,8 @@ function calc_stats(json, filters) {
                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.
@@ -309,14 +312,56 @@ function calc_stats(json, filters) {
 
                        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) {
@@ -381,8 +426,10 @@ function calc_stats(json, filters) {
                        // Score management
                        if (type === 'goal') {
                                ++our_score;
+                               between_points = true;
                        } else if (type === 'their_goal') {
                                ++their_score;
+                               between_points = true;
                        }
 
                        // Point count management
@@ -1780,7 +1827,8 @@ function keep_match(match_id, filters) {
        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) {
@@ -1796,6 +1844,11 @@ q                } else {
                        ++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]) {
@@ -1820,6 +1873,31 @@ q                } else {
        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)) {