X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=update_sheets.js;h=e58a086a7ff763843f8ea1f46b05fb6e597611eb;hb=46bc9b650cfe85153eec72e5dae92c904696daee;hp=b03c48dbe4ef224b0ad1907069fac35a0f53afd7;hpb=64c11ffc84930a031dffd586d403d7a1faf7d9cb;p=ultimatescore diff --git a/update_sheets.js b/update_sheets.js index b03c48d..e58a086 100644 --- a/update_sheets.js +++ b/update_sheets.js @@ -68,16 +68,20 @@ function possibly_update_oauth_key(cb) { function publish_group_rank(response, group_name) { let updates = []; - let cols = ultimateconfig['score_sheet_cols'][group_name]; + let config = ultimateconfig['group_cells'][group_name]; + let cols = config['score_sheet_cols']; let teams = parse_teams_from_spreadsheet(response); let games = parse_games_from_spreadsheet(response, group_name, false); apply_games_to_teams(games, teams); + teams = filter_teams(teams, response); // Write the points total to the unsorted columns. - for (let i = 0; i < teams.length; ++i) { - let row = ultimateconfig['point_total_start_row'] + i; - updates.push({ "range": cols[2] + row, "values": [ [ teams[i].pts ] ] }); + if (config['point_total_start_row'] !== null) { + for (let i = 0; i < teams.length; ++i) { + let row = config['point_total_start_row'] + i; + updates.push({ "range": cols[2] + row, "values": [ [ teams[i].pts ] ] }); + } } let tiebreakers = []; @@ -85,8 +89,8 @@ function publish_group_rank(response, group_name) // Write the ranking table, from scratch. for (let i = 0; i < teams.length; ++i) { - let row = ultimateconfig['ranking_list_start_row'] + i; - updates.push({ "range": cols[0] + row, "values": [ [ teams[i].rank ] ] }); + let row = config['ranking_list_start_row'] + i; + updates.push({ "range": cols[0] + row, "values": [ [ teams[i].rank + config['rank_offset'] - 1] ] }); updates.push({ "range": cols[1] + row, "values": [ [ teams[i].mediumname ] ] }); updates.push({ "range": cols[2] + row, "values": [ [ teams[i].pts ] ] }); } @@ -95,7 +99,7 @@ function publish_group_rank(response, group_name) if (tiebreakers.length != 0) { tb_str = tiebreakers.join("\n"); } - updates.push({ "range": cols[0] + ultimateconfig['ranking_list_explain_row'], "values": [ [ tb_str ] ]}); + updates.push({ "range": config['ranking_list_explain_cell'], "values": [ [ tb_str ] ]}); let json = { "valueInputOption": "USER_ENTERED", @@ -125,13 +129,14 @@ function montecarlo(responses) { } for (let simulation_idx = 0; simulation_idx < 100; ++simulation_idx) { // 100 seems to be enough. - let thirds = []; + let thirds = [], ignoreds = []; + let groups = []; for (let group_idx = 0; group_idx < responses.length; ++group_idx) { // Fill in random results. We deliberately use a uniform [-13,+13] // model here, since we are interested in the extremal results. // Of course, not all real games go to 13, but the risk of that // influencing the tiebreakers is very slim. - let games_copy = []; + let games_copy = [], games_with_synth = []; for (const game of games[group_idx]) { games_copy.push(Object.assign({}, game)); } @@ -169,7 +174,15 @@ function montecarlo(responses) { teams_copy[idx2].gd += score2; teams_copy[idx1].gd -= score2; teams_copy[idx2].gd -= score1; + + games_with_synth.push({ + "name1": games_copy[i].name1, + "name2": games_copy[i].name2, + "score1": score1, + "score2": score2 + }); } else { + games_with_synth.push(games_copy[i]); continue; } } @@ -207,12 +220,31 @@ function montecarlo(responses) { } } } + if (ultimateconfig['kick_fifth_from_third'] && teams_copy.length >= 5) { + if (teams_copy[4].rank != 5) { + // A real tie for fifth; the rules are unclear, so just give up. + busted_thirds = true; + } else { + ignoreds.push(teams_copy[4]); + } + } } + + groups.push({ + "games": games_with_synth, + "teams": teams_copy + }); } // Also rank thirds. if (!busted_thirds) { let tiebreakers = []; + if (ultimateconfig['kick_fifth_from_third']) { + // Recompute scores (but not ranks!) without the ignored games. (thirds point to these objects.) + for (let group_idx = 0; group_idx < responses.length; ++group_idx) { + apply_games_to_teams(groups[group_idx].games, groups[group_idx].teams, ignoreds); + } + } let ranked = rank_thirds([], thirds, 1, tiebreakers); if (simulation_idx == 0) { third_groups = ranked; @@ -252,6 +284,13 @@ function names_for_team(team, expansions) { return [ longteam, longteam, team ]; } +function expand_mediumname_if_single_team(team, expansions) { + if (expansions.hasOwnProperty(team)) { + return expansions[team][1]; + } + return team; +} + function do_replacements(str, replacements) { for (const r of replacements) { str = str.replace(r[0], r[1]); @@ -270,24 +309,38 @@ function fill_playoff(replacements, teams) { let games = ultimateconfig['playoff_games']; get_results('Results', function(response) { - let updates = []; + let updates = [], meta_updates = []; let game_num = 0; for (const game of games) { let team1 = do_replacements(game[0], replacements); let team2 = do_replacements(game[1], replacements); + let team1_mediumname = expand_mediumname_if_single_team(team1, team_expansions); + let team2_mediumname = expand_mediumname_if_single_team(team2, team_expansions); let row = ultimateconfig['playoff_games_start_row'] + game[3]; let cols = ultimateconfig['playoff_games_cols'][game[2]]; let cell_team1 = "Results!" + String.fromCharCode(cols[0] + 65) + row; let cell_score1 = "Results!" + String.fromCharCode(cols[1] + 65) + row; let cell_score2 = "Results!" + String.fromCharCode(cols[2] + 65) + row; let cell_team2 = "Results!" + String.fromCharCode(cols[3] + 65) + row; - updates.push({ "range": cell_team1, "values": [ [ team1 ] ] }); - updates.push({ "range": cell_team2, "values": [ [ team2 ] ] }); + updates.push({ "range": cell_team1, "values": [ [ team1_mediumname ] ] }); + updates.push({ "range": cell_team2, "values": [ [ team2_mediumname ] ] }); let score1 = response['values'][row - 1][cols[1]]; let score2 = response['values'][row - 1][cols[2]]; let game_name = game[4]; let game_name2 = game_name.replace("Semi", "semi"); + let game_day = game[7]; + if (game_day === undefined) { + game_day = 7; // Sunday. + } + + let range = { + "sheetId": ultimateconfig['score_sheet_index'], + "startColumnIndex": cols[1], + "endColumnIndex": cols[2] + 1, + "startRowIndex": row - 1, + "endRowIndex": row + }; if (parseInt(score1) >= 0 && parseInt(score2) >= 0 && score1 != score2) { if (parseInt(score1) > parseInt(score2)) { @@ -301,16 +354,22 @@ function fill_playoff(replacements, teams) { replacements.unshift(["W " + game_name2, team2]); replacements.unshift(["L " + game_name2, team1]); } + meta_updates.push({ "unmergeCells": { "range": range }}); } else if (game[5]) { + // No score yet, so write the name of the game (e.g. “L-semi 1”) + // where the score would normally be, to mark what this game is called. + // This is useful with the limited space on the tablet. score1 = score2 = ""; updates.push({ "range": cell_score1, "values": [ [ game[4] ] ] }); + meta_updates.push({ "mergeCells": { "range": range, "mergeType": "MERGE_ALL" }}); } if (game[2] == 0) { // Stream field. // Game. + let ss_row = ultimateconfig['playoff_games_start_row_detail_sheet'] + game_num; updates.push({ - "range": "Playoffs!A" + (game_num + 32) + ":J" + (game_num + 32), - "values": [ [ team1, team2, score1, score2, "", "", "", 7, response['values'][row - 1][1].replace(".",":"), game[6] ] ] + "range": "Playoffs!A" + ss_row + ":J" + ss_row, + "values": [ [ team1, team2, score1, score2, "", "", "", game_day, response['values'][row - 1][1].replace(".",":"), game[6] ] ] }); // Team codes. @@ -330,8 +389,13 @@ function fill_playoff(replacements, teams) { "valueInputOption": "USER_ENTERED", "data": updates }; + let meta_json = { + "requests": meta_updates + }; possibly_update_oauth_key(function() { - post_json('https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + '/values:batchUpdate?key=' + ultimateconfig['api_key'], json, function(response) {}, current_oauth_access_token); + post_json('https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + '/values:batchUpdate?key=' + ultimateconfig['api_key'], json, function(response) { + }, current_oauth_access_token); + post_json('https://sheets.googleapis.com/v4/spreadsheets/' + ultimateconfig['score_sheet_id'] + ':batchUpdate?key=' + ultimateconfig['api_key'], meta_json, function(response) {}, current_oauth_access_token); }); }); } @@ -349,17 +413,13 @@ function get_results(sheet_name, cb) function publish_group_ranks() { get_group('Group A', function(response_a) { get_group('Group B', function(response_b) { - get_group('Group C', function(response_c) { publish_group_rank(response_a, 'Group A'); publish_group_rank(response_b, 'Group B'); - publish_group_rank(response_c, 'Group C'); - let replacements = montecarlo([response_a, response_b, response_c]); + let replacements = montecarlo([response_a, response_b]); let team_a = parse_teams_from_spreadsheet(response_a); let team_b = parse_teams_from_spreadsheet(response_b); - let team_c = parse_teams_from_spreadsheet(response_c); - fill_playoff(replacements, [team_a, team_b, team_c]); - }); + fill_playoff(replacements, [team_a, team_b]); }); }); } @@ -368,6 +428,7 @@ function get_ranked(response, group_name) { let teams = parse_teams_from_spreadsheet(response); let games = parse_games_from_spreadsheet(response, group_name, false); apply_games_to_teams(games, teams); + teams = filter_teams(teams, response); let tiebreakers = []; teams = rank(games, teams, 1, tiebreakers); return teams; @@ -375,6 +436,10 @@ function get_ranked(response, group_name) { // Pick out everything that is at rank N _or_ avoids rank N by lack of tiebreakers only. function pick_out_rank(teams, rank, candidates) { + if (teams.length < rank) { + return; + } + let lowest_rank = teams[rank - 1].rank; let count = 0; @@ -396,7 +461,19 @@ function pick_out_rank(teams, rank, candidates) { } } +function addsign(x) +{ + if (x < 0) { + return "−" + (-x); + } else if (x == 0) { + return "=0"; + } else { + return "+" + x; + } +} + function publish_best_thirds() { + if (!ultimateconfig['best_thirds']) return; get_group('Group A', function(response_a) { get_group('Group B', function(response_b) { get_group('Group C', function(response_c) { @@ -409,11 +486,81 @@ function publish_best_thirds() { pick_out_rank(B, 3, candidates); pick_out_rank(C, 3, candidates); + let ignoreds = []; + let ignored_games = [], ignored_games_expl = []; + if (ultimateconfig['kick_fifth_from_third']) { + let ignoreds_A = [], ignoreds_B = [], ignoreds_C = []; + pick_out_rank(A, 5, ignoreds_A); + pick_out_rank(B, 5, ignoreds_B); + pick_out_rank(C, 5, ignoreds_C); + + if (ignoreds_A.length >= 2) { + ignoreds_A = [ ignoreds_A[ignoreds_A.length - 1] ]; + } + if (ignoreds_B.length >= 2) { + ignoreds_B = [ ignoreds_B[ignoreds_B.length - 1] ]; + } + if (ignoreds_C.length >= 2) { + ignoreds_C = [ ignoreds_C[ignoreds_C.length - 1] ]; + } + ignoreds = ignoreds_A.concat(ignoreds_B).concat(ignoreds_C); + + // Protect the “candidates” array, so that apply_games_to_teams() further down + // doesn't modify it (we want to compare old and new). + A = jsonclone(A); + B = jsonclone(B); + C = jsonclone(C); + + // Recompute scores (but not ranks!) without the ignored games. + let games_a = parse_games_from_spreadsheet(response_a, 'Group A', false); + apply_games_to_teams(games_a, A, ignoreds, ignored_games); + let games_b = parse_games_from_spreadsheet(response_b, 'Group B', false); + apply_games_to_teams(games_b, B, ignoreds, ignored_games); + let games_c = parse_games_from_spreadsheet(response_c, 'Group C', false); + apply_games_to_teams(games_c, C, ignoreds, ignored_games); + + // Filter out ignored games involving the candidate thirds. + let candidates_to_idx = make_teams_to_idx(candidates); + for (const game of ignored_games) { + if (candidates_to_idx[game[0]] !== undefined || + candidates_to_idx[game[1]] !== undefined) { + if (game[2]) { + ignored_games_expl.push("Ignoring (arbitrarily) " + game[0] + "–" + game[1]); + } else { + ignored_games_expl.push("Ignoring " + game[0] + "–" + game[1]); + } + } + } + + let new_teams = A.concat(B).concat(C); + let new_teams_to_idx = make_teams_to_idx(new_teams); + + // Move back the scores (points, gd, goals). + for (let cand of candidates) { + let new_version = new_teams[new_teams_to_idx[cand.shortname]]; + if (cand.pts != new_version.pts || + cand.gd != new_version.gd || + cand.goals != new_version.goals) { + cand.pts = new_version.pts; + cand.gd = new_version.gd; + cand.goals = new_version.goals; + ignored_games_expl.push(cand.shortname + " at " + cand.pts + " pts, " + addsign(new_version.gd) + " GD"); + } + } + } + let tiebreakers = []; let text = ""; if (candidates.length >= 2) { let ranked = rank_thirds([], candidates, 1, tiebreakers); - text = "Best thirds: " + ranked[0].mediumname + ", " + ranked[1].mediumname + "\n" + tiebreakers.join("\n"); + let best_thirds = ranked.filter(function(team) { return team.rank <= 2; }); + if (best_thirds.length == 2) { + text = "Best thirds: " + best_thirds.map(function(team) { return team.mediumname }).join(', ') + "\n"; + if (ignored_games_expl.length > 0) { + text += ignored_games_expl.join("; ") + "\n"; + } + text += tiebreakers.join("\n"); + } } let updates = []; updates.push({ "range": ultimateconfig['explain_third_cell'], "values": [ [ text ] ] });