From 3f7cd86babde664f9395477bb02de95d4c42d8f8 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Wed, 26 Jul 2023 17:34:43 +0200 Subject: [PATCH] Implement optional gender rule A and pull rules. 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 | 4 ++- main.cpp | 4 +-- mainwindow.cpp | 68 +++++++++++++++++++++++++++++++++++++++++- mainwindow.ui | 24 +++++++++++++++ ultimate.js | 80 +++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 175 insertions(+), 5 deletions(-) diff --git a/json.cpp b/json.cpp index 0bd2452..2e3398d 100644 --- 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]; diff --git a/main.cpp b/main.cpp index bda8558..e233c08 100644 --- 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(); diff --git a/mainwindow.cpp b/mainwindow.cpp index feaf3ab..3b29d72 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -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) diff --git a/mainwindow.ui b/mainwindow.ui index 1a9ede5..3806b60 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -523,7 +523,15 @@ + + + Match + + + + + @@ -535,6 +543,22 @@ E&xit + + + true + + + Gender rule &A + + + + + true + + + Gender &pull rule + + diff --git a/ultimate.js b/ultimate.js index e9b0d80..292534f 100644 --- a/ultimate.js +++ b/ultimate.js @@ -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)) { -- 2.39.2