From d1e0abca8b9948f63ea5a8f63ccdf5197a2ff6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Fri, 10 Nov 2023 17:02:14 +0100 Subject: [PATCH 01/21] add code to analyze endgames --- bdr.py | 2 -- get_sheet.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/bdr.py b/bdr.py index 1e8e9bc..0cafdad 100644 --- a/bdr.py +++ b/bdr.py @@ -25,8 +25,6 @@ def analyze_game(instance: HanabLiveInstance, actions: List[Action]) -> Tuple[Li play = instance.deck[action.target] if (not game.is_playable(play)) and game.is_critical(play): termination = 'Bomb crit' - print('Bombed crit {}'.format(play)) - print(game.deck[game.progress:], game.stacks) game.make_action(action) if termination == '': if game.strikes == 3: diff --git a/get_sheet.py b/get_sheet.py index 4674427..14978d1 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -4,10 +4,13 @@ import json import csv import pandas -from bdr import describe_game +from hanabi.database import global_db_connection_manager from hanabi.live.site_api import get -from hanabi.database import global_db_connection_manager +from bdr import describe_game +from endgames import analyze_game_cached + +# Init db connection global_db_connection_manager.read_config() global_db_connection_manager.connect() @@ -45,7 +48,8 @@ player_cols = set() for _, p in player_mapping.items(): player_cols.add(p) -class Entry(): + +class Entry: def __init__(self, game_id, seed, score, players, bdr=0): self.game_id = game_id self.seed = seed @@ -60,6 +64,7 @@ def get_player_games(player: str): if r.status_code == 200: return json.loads(r.text) + def collect_player_games(): global_games = {} for player in player_mapping.keys(): @@ -89,6 +94,7 @@ def collect_player_games(): return global_games + def analyze_games(games): retval = {} for game_id in games.keys(): @@ -97,6 +103,15 @@ def analyze_games(games): retval[game_id] = bdrs, termination return retval +def analyze_endgames(games): + retval = {} + for game_id in games.keys(): + print('Analysing endgames of game {}'.format(game_id)) + result = analyze_game_cached(game_id) + retval[game_id] = result + print(result) + return retval + def sort_players_by_num_games(games_dict): nums = {} @@ -108,9 +123,11 @@ def sort_players_by_num_games(games_dict): nums[col] = num + 1 return sorted(player_cols, key = lambda col: -nums[col]) + if __name__ == "__main__": games = collect_player_games() analysis = analyze_games(games) + endgames = analyze_endgames(games) streaks = {} fieldnames = ['Game ID', 'Seed', 'Player #', 'Result', 'BDR'] fieldnames += sort_players_by_num_games(games) @@ -145,5 +162,19 @@ if __name__ == "__main__": num_others = row.get('Other', 0) row['Other'] = num_others + 1 writer.writerow(row) + + fieldnames = ['Game ID'] + [str(i) for i in range(1, 16)] + with open('endgames.csv', 'w', newline='') as f: + f.writelines([','.join(fieldnames), "\n"]) + + with open('endgames.csv', 'a', newline='') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + for game_id, endgame in sorted(endgames.items()): + endgame['Game ID'] = "{}".format(game_id, game_id) + writer.writerow(endgame) + a = pandas.read_csv("games.csv") a.to_html("games.html", escape=False) + + b = pandas.read_csv('endgames.csv') + b.to_html("endgames.html", escape=False) From 5ce58b375d29c1b255e143c28e6c99ecac8a5394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sat, 11 Nov 2023 01:38:37 +0100 Subject: [PATCH 02/21] add endgames.py --- endgames.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 endgames.py diff --git a/endgames.py b/endgames.py new file mode 100644 index 0000000..614611f --- /dev/null +++ b/endgames.py @@ -0,0 +1,63 @@ +import json +import re +import subprocess +from typing import Dict + +from pathlib import Path +DATA_FILE = Path('endgame-data.json') + +if not DATA_FILE.exists(): + DATA_FILE.write_text('{}') + + +with open(DATA_FILE, 'r') as f: + DATA: Dict = json.loads(f.read()) + + +def analyze_game(game_id: int): + probabilities = {} + for deck_size in range(1, 16): + try: + result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(deck_size), '-i', '0'], stdout=subprocess.PIPE, timeout=30) + except subprocess.TimeoutExpired: + return probabilities + output = result.stdout.decode('utf-8') + m = re.search('Probability with optimal play: .*/.* ~ ([0-9.]+)', output) + if not m: + raise ValueError("Invalid program output: {}".format(output)) + probabilities[str(deck_size)] = m.group(1) + return probabilities + +def full_analyze_game(game_id: int): + probabilities = {} + try: + result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(deck_size), '-i', '0', '--all-clues', '-r'], stdout=subprocess.PIPE, timeout=180) + except subproces.TimeoutExpired: + return probabilities + output = result.stdout.decode('utf-8') + for m in re.finditer('Probability with (\d+) cards left in deck and (\d) clues (+|-\d): .*/.* ~ ([0-9.]+)', output): + probabilities[m.group(1)][m.group(3)] = m.group(4) + return probabilities + +def full_analyze_game_cached(game_id: int): + cached = DATA.get('all', {}).get(str(game_id), None) + if cached is not None: + return cached + result = full_analyze_game(game_id) + DATA['all'][game_id] = result + save_cache() + return result + +def analyze_game_cached(game_id: int): + cached = DATA['normal'].get(str(game_id), None) + if cached is not None: + return cached + result = analyze_game(game_id) + DATA['normal'][game_id] = result + save_cache() + return result + + +def save_cache(): + with open(DATA_FILE, 'w') as f: + f.writelines(json.dumps(DATA, indent=2)) From d485488d1ea03a580c2a24f5db311594c898706f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sat, 11 Nov 2023 03:47:38 +0100 Subject: [PATCH 03/21] fix bug: count bdrs if bombed --- bdr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bdr.py b/bdr.py index 0cafdad..8d57e0b 100644 --- a/bdr.py +++ b/bdr.py @@ -9,7 +9,7 @@ def analyze_game(instance: HanabLiveInstance, actions: List[Action]) -> Tuple[Li termination = '' game = HanabLiveGameState(instance) for action in actions: - if action.type == ActionType.Discard: + if action.type == ActionType.Discard or (action.type == ActionType.Play and not game.is_playable(instance.deck[action.target])): discard = instance.deck[action.target] if not game.is_trash(discard): if game.is_critical(discard): From c98e9f3c7ecdd0d2b943a8e36bfa9467bb2e1acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sat, 11 Nov 2023 04:20:41 +0100 Subject: [PATCH 04/21] add endgame parsing --- endgames.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/endgames.py b/endgames.py index 614611f..54b790b 100644 --- a/endgames.py +++ b/endgames.py @@ -23,27 +23,33 @@ def analyze_game(game_id: int): return probabilities output = result.stdout.decode('utf-8') m = re.search('Probability with optimal play: .*/.* ~ ([0-9.]+)', output) - if not m: - raise ValueError("Invalid program output: {}".format(output)) - probabilities[str(deck_size)] = m.group(1) + if m: + probabilities[str(deck_size)] = m.group(1) return probabilities + def full_analyze_game(game_id: int): probabilities = {} try: - result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(deck_size), '-i', '0', '--all-clues', '-r'], stdout=subprocess.PIPE, timeout=180) - except subproces.TimeoutExpired: + result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(10), '-i', '0', '--all-clues', '-r', '--quiet'], stdout=subprocess.PIPE, timeout=180) + except subprocess.TimeoutExpired: return probabilities output = result.stdout.decode('utf-8') - for m in re.finditer('Probability with (\d+) cards left in deck and (\d) clues (+|-\d): .*/.* ~ ([0-9.]+)', output): - probabilities[m.group(1)][m.group(3)] = m.group(4) + print(output) + for m in re.finditer('Probability with ([0-9]+) cards left in deck and [0-8] clues \((.[0-8])\).*: .*/.* ~ ([0-9.]*)', output): + if m.group(1) not in probabilities.keys(): + probabilities[m.group(1)] = {} + probabilities[m.group(1)][m.group(2)] = m.group(3) return probabilities + def full_analyze_game_cached(game_id: int): cached = DATA.get('all', {}).get(str(game_id), None) if cached is not None: return cached result = full_analyze_game(game_id) + if 'all' not in DATA.keys(): + DATA['all'] = {} DATA['all'][game_id] = result save_cache() return result From 8bd1f3bc2501a7a032032853d1d93a1bb243d1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sat, 11 Nov 2023 04:20:59 +0100 Subject: [PATCH 05/21] analyze with different clue counts --- get_sheet.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/get_sheet.py b/get_sheet.py index 14978d1..cb129a4 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -8,7 +8,7 @@ from hanabi.database import global_db_connection_manager from hanabi.live.site_api import get from bdr import describe_game -from endgames import analyze_game_cached +from endgames import analyze_game_cached, full_analyze_game_cached # Init db connection global_db_connection_manager.read_config() @@ -112,6 +112,15 @@ def analyze_endgames(games): print(result) return retval +def full_analyze_endgames(games): + retval = {} + for game_id in games.keys(): + print('Analysing all endgames of game {}'.format(game_id)) + result = full_analyze_game_cached(game_id) + retval[game_id] = result + print(result) + return retval + def sort_players_by_num_games(games_dict): nums = {} @@ -124,6 +133,20 @@ def sort_players_by_num_games(games_dict): return sorted(player_cols, key = lambda col: -nums[col]) +def lookup_val(endgame_dict, clue_modifier): + if clue_modifier > 0: + for lookup in range(clue_modifier, 0, -1): + val = endgame_dict.get('+' + str(lookup), None) + if val is not None: + return val + if clue_modifier < 0: + for lookup in range(clue_modifier, 0): + val = endgame_dict.get(str(lookup)) + if val is not None: + return val + return endgame_dict.get('+0', None) + + if __name__ == "__main__": games = collect_player_games() analysis = analyze_games(games) @@ -173,6 +196,27 @@ if __name__ == "__main__": endgame['Game ID'] = "{}".format(game_id, game_id) writer.writerow(endgame) + all_endgames = full_analyze_endgames(games) + fieldnames = ['Game ID'] + [str(i) for i in range(1, 11)] + for clue_modifier in range(-2, 3): + filename = 'endgames{}.csv'.format(clue_modifier) + with open(filename, 'w') as f: + f.writelines([','.join(fieldnames), "\n"]) + with open(filename, 'a') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + for game_id, endgame in sorted(all_endgames.items()): + row = {'Game ID': game_id} + for deck_size in range(1, 11): + val = lookup_val(endgame.get(str(deck_size), {}), clue_modifier) + if val is not None: + row[str(deck_size)] = val + writer.writerow(row) + + print('processed file {}'.format(filename)) + + x = pandas.read_csv(filename) + x.to_html('endgames_{}.html'.format(clue_modifier), escape=False) + a = pandas.read_csv("games.csv") a.to_html("games.html", escape=False) From f0263ab25007866b20772230d814a83e1e4c3202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sat, 11 Nov 2023 14:02:44 +0100 Subject: [PATCH 06/21] adjust endgame analysis to new cli interface --- endgames.py | 25 +++++++++++++++++-------- get_sheet.py | 2 ++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/endgames.py b/endgames.py index 54b790b..19438fb 100644 --- a/endgames.py +++ b/endgames.py @@ -16,15 +16,24 @@ with open(DATA_FILE, 'r') as f: def analyze_game(game_id: int): probabilities = {} - for deck_size in range(1, 16): - try: - result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(deck_size), '-i', '0'], stdout=subprocess.PIPE, timeout=30) - except subprocess.TimeoutExpired: + raw_output = None + try: + result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', '16', '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=30) + raw_output = result.stdout + except subprocess.TimeoutExpired as time_err: + raw_output = time_err.stdout + output = raw_output.decode('utf-8') + + # Check if the game was just over before reaching the specified draw pile size + if re.match("This given draw pile size \(.*\) cannot be obtained with the specified replay.", output): + for i in range(0,16): + probabilities[str(i)] = 0 return probabilities - output = result.stdout.decode('utf-8') - m = re.search('Probability with optimal play: .*/.* ~ ([0-9.]+)', output) - if m: - probabilities[str(deck_size)] = m.group(1) + + # Now, parse all results that we obtained (unclear how many depending on whether we ran into the timeout) + for m in re.finditer('Probability with ([0-9]+) cards left in the deck: .*/.* ~ ([0-9.]+)', output): + probabilities[str(m.group(1))] = m.group(2) + return probabilities diff --git a/get_sheet.py b/get_sheet.py index cb129a4..ca95634 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -210,6 +210,8 @@ if __name__ == "__main__": val = lookup_val(endgame.get(str(deck_size), {}), clue_modifier) if val is not None: row[str(deck_size)] = val + else: + print("No results found for game {} and deck size {}: {}".format(game_id, deck_size, endgame.get(str(deck_size)))) writer.writerow(row) print('processed file {}'.format(filename)) From eb0699e775c9ed58c6fd0272b0a9da880dbdde27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sat, 11 Nov 2023 23:09:13 +0100 Subject: [PATCH 07/21] start adjustment to new cli interface: better parsing and default values --- endgames.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/endgames.py b/endgames.py index 19438fb..f667517 100644 --- a/endgames.py +++ b/endgames.py @@ -15,23 +15,26 @@ with open(DATA_FILE, 'r') as f: def analyze_game(game_id: int): + max_draw_pile_size = 15 probabilities = {} raw_output = None try: - result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', '16', '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=30) + result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(max_draw_pile_size), '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=10) raw_output = result.stdout except subprocess.TimeoutExpired as time_err: raw_output = time_err.stdout output = raw_output.decode('utf-8') + print(output) + # Check if the game was just over before reaching the specified draw pile size - if re.match("This given draw pile size \(.*\) cannot be obtained with the specified replay.", output): - for i in range(0,16): - probabilities[str(i)] = 0 - return probabilities + if re.search(r'The given draw pile size \({}\) cannot be obtained with the specified replay.'.format(max_draw_pile_size), output): + print('detected empty output') + return {str(size): 0 for size in range(1, max_draw_pile_size + 1)} # Now, parse all results that we obtained (unclear how many depending on whether we ran into the timeout) - for m in re.finditer('Probability with ([0-9]+) cards left in the deck: .*/.* ~ ([0-9.]+)', output): + x = 'Probability with 13 cards left in deck: 5855/6237 ~ 93.875%' + for m in re.finditer('Probability with ([0-9]+) cards left in deck: .*/.* ~ ([0-9.]+)', output): probabilities[str(m.group(1))] = m.group(2) return probabilities @@ -64,10 +67,12 @@ def full_analyze_game_cached(game_id: int): return result def analyze_game_cached(game_id: int): - cached = DATA['normal'].get(str(game_id), None) + cached = DATA.get('normal', {}).get(str(game_id), None) if cached is not None: return cached result = analyze_game(game_id) + if 'normal' not in DATA.keys(): + DATA['normal'] = {} DATA['normal'][game_id] = result save_cache() return result From ad7fbcc364005f605f75fbb1149a0df71b0bc4b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sun, 12 Nov 2023 00:01:22 +0100 Subject: [PATCH 08/21] improved parsing of endgame data --- endgames.py | 41 ++++++++++++++++++++++++++++------------- get_sheet.py | 11 ++++++++--- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/endgames.py b/endgames.py index f667517..25748f5 100644 --- a/endgames.py +++ b/endgames.py @@ -16,8 +16,6 @@ with open(DATA_FILE, 'r') as f: def analyze_game(game_id: int): max_draw_pile_size = 15 - probabilities = {} - raw_output = None try: result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(max_draw_pile_size), '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=10) raw_output = result.stdout @@ -27,13 +25,17 @@ def analyze_game(game_id: int): print(output) - # Check if the game was just over before reaching the specified draw pile size - if re.search(r'The given draw pile size \({}\) cannot be obtained with the specified replay.'.format(max_draw_pile_size), output): - print('detected empty output') - return {str(size): 0 for size in range(1, max_draw_pile_size + 1)} - # Now, parse all results that we obtained (unclear how many depending on whether we ran into the timeout) - x = 'Probability with 13 cards left in deck: 5855/6237 ~ 93.875%' + probabilities = {} + + # Check if the game was just over before reaching the specified draw pile size + for m in re.finditer(r'The given draw pile size \(([0-9]+)\) cannot be obtained with the specified replay.', output): + if m.group(1) == str(max_draw_pile_size): + print('detected empty output') + return {str(size): 0 for size in range(1, max_draw_pile_size + 1)} + else: + probabilities[str(m.group(1))] = 0 + for m in re.finditer('Probability with ([0-9]+) cards left in deck: .*/.* ~ ([0-9.]+)', output): probabilities[str(m.group(1))] = m.group(2) @@ -41,13 +43,26 @@ def analyze_game(game_id: int): def full_analyze_game(game_id: int): - probabilities = {} + max_draw_pile_size = 10 try: - result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(10), '-i', '0', '--all-clues', '-r', '--quiet'], stdout=subprocess.PIPE, timeout=180) - except subprocess.TimeoutExpired: - return probabilities - output = result.stdout.decode('utf-8') + result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(max_draw_pile_size), '-i', '0', '--all-clues', '-r', '--quiet'], stdout=subprocess.PIPE, timeout=180) + raw_output = result.stdout + except subprocess.TimeoutExpired as time_err: + raw_output = time_err.stdout + output = raw_output.decode('utf-8') + print(output) + + probabilities = {} + zero_dict = { + (('+' if clue_modifier >= 0 else '') + str(clue_modifier)): 0 for clue_modifier in range(-8, 9) + } + for m in re.finditer(r'The given draw pile size \(([0-9]+)\) cannot be obtained with the specified replay.', output): + if m.group(1) == str(max_draw_pile_size): + return {str(size): zero_dict for size in range(1, max_draw_pile_size + 1)} + else: + probabilities[str(m.group(1))] = zero_dict + for m in re.finditer('Probability with ([0-9]+) cards left in deck and [0-8] clues \((.[0-8])\).*: .*/.* ~ ([0-9.]*)', output): if m.group(1) not in probabilities.keys(): probabilities[m.group(1)] = {} diff --git a/get_sheet.py b/get_sheet.py index ca95634..6c51136 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -14,7 +14,7 @@ from endgames import analyze_game_cached, full_analyze_game_cached global_db_connection_manager.read_config() global_db_connection_manager.connect() -session = requests_cache.CachedSession('.hanab-live.cache',expire_after=300) +session = requests_cache.CachedSession('.hanab-live.cache',expire_after=30000) player_mapping = { 'RamaNoVarjan': 'Ramanujan', @@ -133,7 +133,8 @@ def sort_players_by_num_games(games_dict): return sorted(player_cols, key = lambda col: -nums[col]) -def lookup_val(endgame_dict, clue_modifier): +def lookup_val(endgame_dict, clue_modifier) -> str: + print('looking up val {} in {}'.format(clue_modifier, endgame_dict)) if clue_modifier > 0: for lookup in range(clue_modifier, 0, -1): val = endgame_dict.get('+' + str(lookup), None) @@ -144,7 +145,8 @@ def lookup_val(endgame_dict, clue_modifier): val = endgame_dict.get(str(lookup)) if val is not None: return val - return endgame_dict.get('+0', None) + retval = endgame_dict.get('+0', None) + return retval if __name__ == "__main__": @@ -197,6 +199,7 @@ if __name__ == "__main__": writer.writerow(endgame) all_endgames = full_analyze_endgames(games) + print(all_endgames.keys()) fieldnames = ['Game ID'] + [str(i) for i in range(1, 11)] for clue_modifier in range(-2, 3): filename = 'endgames{}.csv'.format(clue_modifier) @@ -205,9 +208,11 @@ if __name__ == "__main__": with open(filename, 'a') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) for game_id, endgame in sorted(all_endgames.items()): + print(endgame) row = {'Game ID': game_id} for deck_size in range(1, 11): val = lookup_val(endgame.get(str(deck_size), {}), clue_modifier) + print('looked up val {}'.format(val)) if val is not None: row[str(deck_size)] = val else: From 323c09d3c55ba112914d9bd3401afd7a6a339894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sun, 12 Nov 2023 00:19:13 +0100 Subject: [PATCH 09/21] set 15min timeout --- endgames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endgames.py b/endgames.py index 25748f5..d1cf82e 100644 --- a/endgames.py +++ b/endgames.py @@ -17,7 +17,7 @@ with open(DATA_FILE, 'r') as f: def analyze_game(game_id: int): max_draw_pile_size = 15 try: - result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(max_draw_pile_size), '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=10) + result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(max_draw_pile_size), '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=60*15) raw_output = result.stdout except subprocess.TimeoutExpired as time_err: raw_output = time_err.stdout From 49142ba4f5506d28bebe6e8206570e38c7c38a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Sun, 12 Nov 2023 00:20:46 +0100 Subject: [PATCH 10/21] clean up prints --- endgames.py | 5 ----- get_sheet.py | 5 +---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/endgames.py b/endgames.py index d1cf82e..b03fd1b 100644 --- a/endgames.py +++ b/endgames.py @@ -23,15 +23,12 @@ def analyze_game(game_id: int): raw_output = time_err.stdout output = raw_output.decode('utf-8') - print(output) - # Now, parse all results that we obtained (unclear how many depending on whether we ran into the timeout) probabilities = {} # Check if the game was just over before reaching the specified draw pile size for m in re.finditer(r'The given draw pile size \(([0-9]+)\) cannot be obtained with the specified replay.', output): if m.group(1) == str(max_draw_pile_size): - print('detected empty output') return {str(size): 0 for size in range(1, max_draw_pile_size + 1)} else: probabilities[str(m.group(1))] = 0 @@ -51,8 +48,6 @@ def full_analyze_game(game_id: int): raw_output = time_err.stdout output = raw_output.decode('utf-8') - print(output) - probabilities = {} zero_dict = { (('+' if clue_modifier >= 0 else '') + str(clue_modifier)): 0 for clue_modifier in range(-8, 9) diff --git a/get_sheet.py b/get_sheet.py index 6c51136..ed5f236 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -134,7 +134,6 @@ def sort_players_by_num_games(games_dict): def lookup_val(endgame_dict, clue_modifier) -> str: - print('looking up val {} in {}'.format(clue_modifier, endgame_dict)) if clue_modifier > 0: for lookup in range(clue_modifier, 0, -1): val = endgame_dict.get('+' + str(lookup), None) @@ -199,7 +198,6 @@ if __name__ == "__main__": writer.writerow(endgame) all_endgames = full_analyze_endgames(games) - print(all_endgames.keys()) fieldnames = ['Game ID'] + [str(i) for i in range(1, 11)] for clue_modifier in range(-2, 3): filename = 'endgames{}.csv'.format(clue_modifier) @@ -212,11 +210,10 @@ if __name__ == "__main__": row = {'Game ID': game_id} for deck_size in range(1, 11): val = lookup_val(endgame.get(str(deck_size), {}), clue_modifier) - print('looked up val {}'.format(val)) if val is not None: row[str(deck_size)] = val else: - print("No results found for game {} and deck size {}: {}".format(game_id, deck_size, endgame.get(str(deck_size)))) + print("WARN: No results found for game {} and deck size {}: {}".format(game_id, deck_size, endgame.get(str(deck_size)))) writer.writerow(row) print('processed file {}'.format(filename)) From c829cd1afd309b4b71d16fae10d45cd8a539d741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 12:27:09 +0100 Subject: [PATCH 11/21] Adjust regex parsing, remove prints --- endgames.py | 25 ++++++++++++++----------- get_sheet.py | 4 +--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/endgames.py b/endgames.py index b03fd1b..8d33a95 100644 --- a/endgames.py +++ b/endgames.py @@ -26,12 +26,11 @@ def analyze_game(game_id: int): # Now, parse all results that we obtained (unclear how many depending on whether we ran into the timeout) probabilities = {} - # Check if the game was just over before reaching the specified draw pile size - for m in re.finditer(r'The given draw pile size \(([0-9]+)\) cannot be obtained with the specified replay.', output): - if m.group(1) == str(max_draw_pile_size): - return {str(size): 0 for size in range(1, max_draw_pile_size + 1)} - else: - probabilities[str(m.group(1))] = 0 + m = re.search(r"Specified draw pile size of ([0-9]+) cannot be reached with specified replay\.\nReplay ends at turn [0-9]+ with score of ([0-9]+)\.", output, re.M) + if m: + won = '100' if m.group(2) == '25' else 0 + for draw_pile_size in range(1, int(m.group(1)) + 1): + probabilities[str(draw_pile_size)] = won for m in re.finditer('Probability with ([0-9]+) cards left in deck: .*/.* ~ ([0-9.]+)', output): probabilities[str(m.group(1))] = m.group(2) @@ -52,11 +51,15 @@ def full_analyze_game(game_id: int): zero_dict = { (('+' if clue_modifier >= 0 else '') + str(clue_modifier)): 0 for clue_modifier in range(-8, 9) } - for m in re.finditer(r'The given draw pile size \(([0-9]+)\) cannot be obtained with the specified replay.', output): - if m.group(1) == str(max_draw_pile_size): - return {str(size): zero_dict for size in range(1, max_draw_pile_size + 1)} - else: - probabilities[str(m.group(1))] = zero_dict + hundred_dict = { + (('+' if clue_modifier >= 0 else '') + str(clue_modifier)): 100 for clue_modifier in range(-8, 9) + } + + m = re.search(r"Specified draw pile size of ([0-9]+) cannot be reached with specified replay\.\nReplay ends at turn [0-9]+ with score of ([0-9]+)\.", output, re.M) + if m: + won = hundred_dict if m.group(2) == '25' else zero_dict + for draw_pile_size in range(1, int(m.group(1)) + 1): + probabilities[str(draw_pile_size)] = won for m in re.finditer('Probability with ([0-9]+) cards left in deck and [0-8] clues \((.[0-8])\).*: .*/.* ~ ([0-9.]*)', output): if m.group(1) not in probabilities.keys(): diff --git a/get_sheet.py b/get_sheet.py index ed5f236..03f8b9e 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -109,7 +109,6 @@ def analyze_endgames(games): print('Analysing endgames of game {}'.format(game_id)) result = analyze_game_cached(game_id) retval[game_id] = result - print(result) return retval def full_analyze_endgames(games): @@ -118,7 +117,6 @@ def full_analyze_endgames(games): print('Analysing all endgames of game {}'.format(game_id)) result = full_analyze_game_cached(game_id) retval[game_id] = result - print(result) return retval @@ -206,7 +204,7 @@ if __name__ == "__main__": with open(filename, 'a') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) for game_id, endgame in sorted(all_endgames.items()): - print(endgame) + # print(endgame) row = {'Game ID': game_id} for deck_size in range(1, 11): val = lookup_val(endgame.get(str(deck_size), {}), clue_modifier) From 06156d15e7b7415dd5e78c04c22dd60b1dfc4e79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 12:31:08 +0100 Subject: [PATCH 12/21] add output directory --- get_sheet.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/get_sheet.py b/get_sheet.py index 03f8b9e..013fd22 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -155,10 +155,10 @@ if __name__ == "__main__": fieldnames += sort_players_by_num_games(games) fieldnames += ['Other'] - with open('games.csv', 'w', newline='') as f: + with open('out/games.csv', 'w', newline='') as f: f.writelines([','.join(fieldnames), "\n"]) - with open('games.csv', 'a', newline='') as f: + with open('out/games.csv', 'a', newline='') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) for game_id, entry in sorted(games.items()): bdrs, termination = analysis[game_id] @@ -186,10 +186,10 @@ if __name__ == "__main__": writer.writerow(row) fieldnames = ['Game ID'] + [str(i) for i in range(1, 16)] - with open('endgames.csv', 'w', newline='') as f: + with open('out/endgames.csv', 'w', newline='') as f: f.writelines([','.join(fieldnames), "\n"]) - with open('endgames.csv', 'a', newline='') as f: + with open('out/endgames.csv', 'a', newline='') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) for game_id, endgame in sorted(endgames.items()): endgame['Game ID'] = "{}".format(game_id, game_id) @@ -199,9 +199,9 @@ if __name__ == "__main__": fieldnames = ['Game ID'] + [str(i) for i in range(1, 11)] for clue_modifier in range(-2, 3): filename = 'endgames{}.csv'.format(clue_modifier) - with open(filename, 'w') as f: + with open('out/' + filename, 'w') as f: f.writelines([','.join(fieldnames), "\n"]) - with open(filename, 'a') as f: + with open('out/' + filename, 'a') as f: writer = csv.DictWriter(f, fieldnames=fieldnames) for game_id, endgame in sorted(all_endgames.items()): # print(endgame) @@ -216,11 +216,11 @@ if __name__ == "__main__": print('processed file {}'.format(filename)) - x = pandas.read_csv(filename) - x.to_html('endgames_{}.html'.format(clue_modifier), escape=False) + x = pandas.read_csv('out/' + filename) + x.to_html('out/endgames_{}.html'.format(clue_modifier), escape=False) - a = pandas.read_csv("games.csv") - a.to_html("games.html", escape=False) + a = pandas.read_csv("out/games.csv") + a.to_html("out/games.html", escape=False) - b = pandas.read_csv('endgames.csv') - b.to_html("endgames.html", escape=False) + b = pandas.read_csv('out/endgames.csv') + b.to_html("out/endgames.html", escape=False) From 9028c7401a1e64e5be34829bdee4c97782841103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 13:05:20 +0100 Subject: [PATCH 13/21] Store replay files on local disc --- games.py | 23 +++++++++++++++++++++++ get_sheet.py | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 games.py diff --git a/games.py b/games.py new file mode 100644 index 0000000..1411d6a --- /dev/null +++ b/games.py @@ -0,0 +1,23 @@ +import json + +from typing import Dict +from pathlib import Path + +from hanabi.live.site_api import get + +GAMES_PATH = Path('games') + +if not GAMES_PATH.exists(): + GAMES_PATH.mkdir(parents=True) + + +def get_game_json(game_id: int) -> Dict: + filename = GAMES_PATH / str(game_id) + if filename.exists(): + with open(filename, 'r') as f: + return json.load(f) + + game = get("export/" + str(game_id)) + with open(filename, 'w') as f: + f.write(json.dumps(game, indent=2)) + return game diff --git a/get_sheet.py b/get_sheet.py index 013fd22..493b396 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -9,6 +9,7 @@ from hanabi.live.site_api import get from bdr import describe_game from endgames import analyze_game_cached, full_analyze_game_cached +from games import get_game_json # Init db connection global_db_connection_manager.read_config() @@ -98,8 +99,7 @@ def collect_player_games(): def analyze_games(games): retval = {} for game_id in games.keys(): - game = get("export/" + str(game_id)) - bdrs, termination = describe_game(game) + bdrs, termination = describe_game(get_game_json(game_id)) retval[game_id] = bdrs, termination return retval From 6e960e442566beb041afced4e0178d4cd5457ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 13:26:40 +0100 Subject: [PATCH 14/21] add option to analyse cheat games. read games from disk --- endgames.py | 32 ++++++++++++++++++-------------- get_sheet.py | 1 + 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/endgames.py b/endgames.py index 8d33a95..e8aca18 100644 --- a/endgames.py +++ b/endgames.py @@ -3,6 +3,8 @@ import re import subprocess from typing import Dict +from games import GAMES_PATH + from pathlib import Path DATA_FILE = Path('endgame-data.json') @@ -14,10 +16,10 @@ with open(DATA_FILE, 'r') as f: DATA: Dict = json.loads(f.read()) -def analyze_game(game_id: int): +def analyze_game(filename: str): max_draw_pile_size = 15 try: - result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(max_draw_pile_size), '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=60*15) + result = subprocess.run(['./endgame-analyzer', '-f', filename, '-d', str(max_draw_pile_size), '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=60*15) raw_output = result.stdout except subprocess.TimeoutExpired as time_err: raw_output = time_err.stdout @@ -38,10 +40,10 @@ def analyze_game(game_id: int): return probabilities -def full_analyze_game(game_id: int): +def full_analyze_game(filename: str): max_draw_pile_size = 10 try: - result = subprocess.run(['./endgame-analyzer', '-g', str(game_id), '-d', str(max_draw_pile_size), '-i', '0', '--all-clues', '-r', '--quiet'], stdout=subprocess.PIPE, timeout=180) + result = subprocess.run(['./endgame-analyzer', '-f', filename, '-d', str(max_draw_pile_size), '-i', '0', '--all-clues', '-r', '--quiet'], stdout=subprocess.PIPE, timeout=180) raw_output = result.stdout except subprocess.TimeoutExpired as time_err: raw_output = time_err.stdout @@ -68,25 +70,27 @@ def full_analyze_game(game_id: int): return probabilities -def full_analyze_game_cached(game_id: int): +def full_analyze_game_cached(game_id: int, cheat: bool = False): cached = DATA.get('all', {}).get(str(game_id), None) if cached is not None: return cached - result = full_analyze_game(game_id) - if 'all' not in DATA.keys(): - DATA['all'] = {} - DATA['all'][game_id] = result + result = full_analyze_game(str(GAMES_PATH / str(game_id)) + '-cheat' if cheat else '') + key = 'all' if not cheat else 'all-cheat' + if key not in DATA.keys(): + DATA[key] = {} + DATA[key][game_id] = result save_cache() return result -def analyze_game_cached(game_id: int): +def analyze_game_cached(game_id: int, cheat: bool = False): cached = DATA.get('normal', {}).get(str(game_id), None) if cached is not None: return cached - result = analyze_game(game_id) - if 'normal' not in DATA.keys(): - DATA['normal'] = {} - DATA['normal'][game_id] = result + result = analyze_game(str(GAMES_PATH / str(game_id)) + '-cheat' if cheat else '') + key = 'normal' if not cheat else 'normal-cheat' + if key not in DATA.keys(): + DATA[key] = {} + DATA[key][game_id] = result save_cache() return result diff --git a/get_sheet.py b/get_sheet.py index 493b396..e8ae92f 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -10,6 +10,7 @@ from hanabi.live.site_api import get from bdr import describe_game from endgames import analyze_game_cached, full_analyze_game_cached from games import get_game_json +from bots import run_cheating_strategy # Init db connection global_db_connection_manager.read_config() From a59119c0c434ac7919d5130dccafa887910fdccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 14:04:52 +0100 Subject: [PATCH 15/21] Also analyse endgames of cheating strategy --- endgames.py | 14 +++-- games.py | 8 ++- get_sheet.py | 151 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 113 insertions(+), 60 deletions(-) diff --git a/endgames.py b/endgames.py index e8aca18..f7da8d0 100644 --- a/endgames.py +++ b/endgames.py @@ -4,6 +4,7 @@ import subprocess from typing import Dict from games import GAMES_PATH +from bots import ensure_cheat_game_exists from pathlib import Path DATA_FILE = Path('endgame-data.json') @@ -71,23 +72,28 @@ def full_analyze_game(filename: str): def full_analyze_game_cached(game_id: int, cheat: bool = False): - cached = DATA.get('all', {}).get(str(game_id), None) + key = 'all' if not cheat else 'all-cheat' + if cheat: + ensure_cheat_game_exists(game_id) + cached = DATA.get(key, {}).get(str(game_id), None) if cached is not None: return cached result = full_analyze_game(str(GAMES_PATH / str(game_id)) + '-cheat' if cheat else '') - key = 'all' if not cheat else 'all-cheat' if key not in DATA.keys(): DATA[key] = {} DATA[key][game_id] = result save_cache() return result + def analyze_game_cached(game_id: int, cheat: bool = False): - cached = DATA.get('normal', {}).get(str(game_id), None) + key = 'normal' if not cheat else 'normal-cheat' + if cheat: + ensure_cheat_game_exists(game_id) + cached = DATA.get(key, {}).get(str(game_id), None) if cached is not None: return cached result = analyze_game(str(GAMES_PATH / str(game_id)) + '-cheat' if cheat else '') - key = 'normal' if not cheat else 'normal-cheat' if key not in DATA.keys(): DATA[key] = {} DATA[key][game_id] = result diff --git a/games.py b/games.py index 1411d6a..3b33107 100644 --- a/games.py +++ b/games.py @@ -11,12 +11,16 @@ if not GAMES_PATH.exists(): GAMES_PATH.mkdir(parents=True) -def get_game_json(game_id: int) -> Dict: - filename = GAMES_PATH / str(game_id) +def get_game_json(game_id: int, cheat: bool = False) -> Dict: + filename = GAMES_PATH / (str(game_id) + ('-cheat' if cheat else '')) if filename.exists(): with open(filename, 'r') as f: return json.load(f) + if cheat: + print('Failed to load replay of cheating game with id {}'.format(game_id)) + return {} + game = get("export/" + str(game_id)) with open(filename, 'w') as f: f.write(json.dumps(game, indent=2)) diff --git a/get_sheet.py b/get_sheet.py index e8ae92f..faa60d1 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -3,14 +3,16 @@ import requests_cache import json import csv import pandas +from typing import List +from pathlib import Path from hanabi.database import global_db_connection_manager -from hanabi.live.site_api import get +from hanabi.live.hanab_live import parse_json_game, HanabLiveGameState +from hanabi.live.compress import link from bdr import describe_game from endgames import analyze_game_cached, full_analyze_game_cached from games import get_game_json -from bots import run_cheating_strategy # Init db connection global_db_connection_manager.read_config() @@ -18,6 +20,10 @@ global_db_connection_manager.connect() session = requests_cache.CachedSession('.hanab-live.cache',expire_after=30000) +OUT_PATH = Path('out') +if not OUT_PATH.exists(): + OUT_PATH.mkdir(parents=True) + player_mapping = { 'RamaNoVarjan': 'Ramanujan', 'purplejoe2': 'PurpleJoe', @@ -104,19 +110,21 @@ def analyze_games(games): retval[game_id] = bdrs, termination return retval -def analyze_endgames(games): + +def analyze_endgames(games, cheat): retval = {} - for game_id in games.keys(): - print('Analysing endgames of game {}'.format(game_id)) - result = analyze_game_cached(game_id) + for game_id in games: + print('Analysing endgames {} of game {}'.format('with cheating' if cheat else '', game_id)) + result = analyze_game_cached(game_id, cheat) retval[game_id] = result return retval -def full_analyze_endgames(games): + +def full_analyze_endgames(games, cheat): retval = {} - for game_id in games.keys(): - print('Analysing all endgames of game {}'.format(game_id)) - result = full_analyze_game_cached(game_id) + for game_id in games: + print('Analysing all endgames {} of game {}'.format('with cheating' if cheat else '', game_id)) + result = full_analyze_game_cached(game_id, cheat) retval[game_id] = result return retval @@ -147,10 +155,72 @@ def lookup_val(endgame_dict, clue_modifier) -> str: return retval -if __name__ == "__main__": +def make_endgame_tables(ids: List[int], cheat: bool): + endgames = analyze_endgames(ids, cheat) + + postfix = '-cheat' if cheat else '' + main_fname = OUT_PATH / ('endgames' + postfix + '.csv') + + fieldnames = ['Game ID'] + [str(i) for i in range(1, 16)] + with open(main_fname, 'w', newline='') as f: + f.writelines([','.join(fieldnames), "\n"]) + + with open(main_fname, 'a', newline='') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + for game_id, endgame in sorted(endgames.items()): + endgame['Game ID'] = "{}".format(game_id, game_id) + writer.writerow(endgame) + + special_fname = str(OUT_PATH / ('endgames' + postfix + '_modifier_{}.csv')) + + x = pandas.read_csv(main_fname) + x.to_html(main_fname.with_suffix('.html'), escape=False) + + all_endgames = full_analyze_endgames(ids, cheat) + fieldnames = ['Game ID'] + [str(i) for i in range(1, 11)] + for clue_modifier in range(-2, 3): + filename = special_fname.format(clue_modifier) + with open(filename, 'w') as f: + f.writelines([','.join(fieldnames), "\n"]) + with open(filename, 'a') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + for game_id, endgame in sorted(all_endgames.items()): + # print(endgame) + row = {'Game ID': game_id} + for deck_size in range(1, 11): + val = lookup_val(endgame.get(str(deck_size), {}), clue_modifier) + if val is not None: + row[str(deck_size)] = val + else: + print("WARN: No results found for game {} and deck size {}: {}".format(game_id, deck_size, endgame.get(str(deck_size)))) + writer.writerow(row) + + print('processed file {}'.format(filename)) + + x = pandas.read_csv(filename) + x.to_html(Path(filename).with_suffix('.html'), escape=False) + + +def create_cheating_replay_links(ids: List[int]): + outfile = Path('out/cheating_links.csv') + with open(outfile, 'w') as f: + writer = csv.writer(f) + writer.writerow(["Game ID", "Cheating Replay Link"]) + for game_id in ids: + replay = get_game_json(game_id, True) + instance, actions = parse_json_game(replay) + game = HanabLiveGameState(instance) + for action in actions: + game.make_action(action) + writer.writerow([game_id, link(game)]) + + x = pandas.read_csv(outfile) + x.to_html(outfile.with_suffix('.html'), render_links=True) + + +def main(): games = collect_player_games() analysis = analyze_games(games) - endgames = analyze_endgames(games) streaks = {} fieldnames = ['Game ID', 'Seed', 'Player #', 'Result', 'BDR'] fieldnames += sort_players_by_num_games(games) @@ -164,12 +234,12 @@ if __name__ == "__main__": for game_id, entry in sorted(games.items()): bdrs, termination = analysis[game_id] row = { - 'Game ID': "{}".format(entry.game_id, entry.game_id), - 'Seed': "{}".format(entry.seed, entry.seed), - 'Player #': entry.num_players, - 'Result': 'Win' if entry.won else termination, - 'BDR': len(bdrs), - } + 'Game ID': "{}".format(entry.game_id, entry.game_id), + 'Seed': "{}".format(entry.seed, entry.seed), + 'Player #': entry.num_players, + 'Result': 'Win' if entry.won else termination, + 'BDR': len(bdrs), + } for player in entry.players: col = player_mapping.get(player, None) if col is not None: @@ -186,42 +256,15 @@ if __name__ == "__main__": row['Other'] = num_others + 1 writer.writerow(row) - fieldnames = ['Game ID'] + [str(i) for i in range(1, 16)] - with open('out/endgames.csv', 'w', newline='') as f: - f.writelines([','.join(fieldnames), "\n"]) - - with open('out/endgames.csv', 'a', newline='') as f: - writer = csv.DictWriter(f, fieldnames=fieldnames) - for game_id, endgame in sorted(endgames.items()): - endgame['Game ID'] = "{}".format(game_id, game_id) - writer.writerow(endgame) - - all_endgames = full_analyze_endgames(games) - fieldnames = ['Game ID'] + [str(i) for i in range(1, 11)] - for clue_modifier in range(-2, 3): - filename = 'endgames{}.csv'.format(clue_modifier) - with open('out/' + filename, 'w') as f: - f.writelines([','.join(fieldnames), "\n"]) - with open('out/' + filename, 'a') as f: - writer = csv.DictWriter(f, fieldnames=fieldnames) - for game_id, endgame in sorted(all_endgames.items()): - # print(endgame) - row = {'Game ID': game_id} - for deck_size in range(1, 11): - val = lookup_val(endgame.get(str(deck_size), {}), clue_modifier) - if val is not None: - row[str(deck_size)] = val - else: - print("WARN: No results found for game {} and deck size {}: {}".format(game_id, deck_size, endgame.get(str(deck_size)))) - writer.writerow(row) - - print('processed file {}'.format(filename)) - - x = pandas.read_csv('out/' + filename) - x.to_html('out/endgames_{}.html'.format(clue_modifier), escape=False) - a = pandas.read_csv("out/games.csv") a.to_html("out/games.html", escape=False) - b = pandas.read_csv('out/endgames.csv') - b.to_html("out/endgames.html", escape=False) + game_ids = [int(key) for key in games.keys()] + make_endgame_tables(game_ids, False) + create_cheating_replay_links(game_ids) + make_endgame_tables(game_ids, True) + + + +if __name__ == "__main__": + main() From 1484d0d90f287bb7bafa4c70223fd10013b209c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 16:45:43 +0100 Subject: [PATCH 16/21] include information strategy bot --- endgames.py | 28 +++++++++--------- games.py | 10 +++---- get_sheet.py | 82 ++++++++++++++++++++++++++++++---------------------- 3 files changed, 67 insertions(+), 53 deletions(-) diff --git a/endgames.py b/endgames.py index f7da8d0..1437fa0 100644 --- a/endgames.py +++ b/endgames.py @@ -1,10 +1,10 @@ import json import re import subprocess -from typing import Dict +from typing import Dict, Optional from games import GAMES_PATH -from bots import ensure_cheat_game_exists +from bots import ensure_bot_game_exists from pathlib import Path DATA_FILE = Path('endgame-data.json') @@ -20,7 +20,7 @@ with open(DATA_FILE, 'r') as f: def analyze_game(filename: str): max_draw_pile_size = 15 try: - result = subprocess.run(['./endgame-analyzer', '-f', filename, '-d', str(max_draw_pile_size), '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=60*15) + result = subprocess.run(['./endgame-analyzer', '-f', filename, '-d', str(max_draw_pile_size), '--interactive', '0', '--quiet', '-r'], stdout=subprocess.PIPE, timeout=30) raw_output = result.stdout except subprocess.TimeoutExpired as time_err: raw_output = time_err.stdout @@ -44,7 +44,7 @@ def analyze_game(filename: str): def full_analyze_game(filename: str): max_draw_pile_size = 10 try: - result = subprocess.run(['./endgame-analyzer', '-f', filename, '-d', str(max_draw_pile_size), '-i', '0', '--all-clues', '-r', '--quiet'], stdout=subprocess.PIPE, timeout=180) + result = subprocess.run(['./endgame-analyzer', '-f', filename, '-d', str(max_draw_pile_size), '-i', '0', '--all-clues', '-r', '--quiet'], stdout=subprocess.PIPE, timeout=30) raw_output = result.stdout except subprocess.TimeoutExpired as time_err: raw_output = time_err.stdout @@ -71,14 +71,14 @@ def full_analyze_game(filename: str): return probabilities -def full_analyze_game_cached(game_id: int, cheat: bool = False): - key = 'all' if not cheat else 'all-cheat' - if cheat: - ensure_cheat_game_exists(game_id) +def full_analyze_game_cached(game_id: int, strategy: Optional[str] = None): + key = 'all' if strategy is None else 'all-{}'.format(strategy) + if strategy is not None: + ensure_bot_game_exists(game_id, strategy) cached = DATA.get(key, {}).get(str(game_id), None) if cached is not None: return cached - result = full_analyze_game(str(GAMES_PATH / str(game_id)) + '-cheat' if cheat else '') + result = full_analyze_game(str(GAMES_PATH / str(game_id)) + '-{}'.format(strategy) if strategy is not None else '') if key not in DATA.keys(): DATA[key] = {} DATA[key][game_id] = result @@ -86,14 +86,14 @@ def full_analyze_game_cached(game_id: int, cheat: bool = False): return result -def analyze_game_cached(game_id: int, cheat: bool = False): - key = 'normal' if not cheat else 'normal-cheat' - if cheat: - ensure_cheat_game_exists(game_id) +def analyze_game_cached(game_id: int, strategy: Optional[str] = None): + key = 'normal' if strategy is None else 'normal-{}'.format(strategy) + if strategy is not None: + ensure_bot_game_exists(game_id, strategy) cached = DATA.get(key, {}).get(str(game_id), None) if cached is not None: return cached - result = analyze_game(str(GAMES_PATH / str(game_id)) + '-cheat' if cheat else '') + result = analyze_game(str(GAMES_PATH / str(game_id)) + '-{}'.format(strategy) if strategy is not None else '') if key not in DATA.keys(): DATA[key] = {} DATA[key][game_id] = result diff --git a/games.py b/games.py index 3b33107..41a5264 100644 --- a/games.py +++ b/games.py @@ -1,6 +1,6 @@ import json -from typing import Dict +from typing import Dict, Optional from pathlib import Path from hanabi.live.site_api import get @@ -11,14 +11,14 @@ if not GAMES_PATH.exists(): GAMES_PATH.mkdir(parents=True) -def get_game_json(game_id: int, cheat: bool = False) -> Dict: - filename = GAMES_PATH / (str(game_id) + ('-cheat' if cheat else '')) +def get_game_json(game_id: int, strategy: Optional[str] = None) -> Dict: + filename = GAMES_PATH / (str(game_id) + ('-{}'.format(strategy) if strategy is not None else '')) if filename.exists(): with open(filename, 'r') as f: return json.load(f) - if cheat: - print('Failed to load replay of cheating game with id {}'.format(game_id)) + if strategy is not None: + print('Failed to load replay of {} strategy version of game with id {}'.format(strategy, game_id)) return {} game = get("export/" + str(game_id)) diff --git a/get_sheet.py b/get_sheet.py index faa60d1..bd228ad 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -3,7 +3,7 @@ import requests_cache import json import csv import pandas -from typing import List +from typing import List, Optional from pathlib import Path from hanabi.database import global_db_connection_manager @@ -111,20 +111,20 @@ def analyze_games(games): return retval -def analyze_endgames(games, cheat): +def analyze_endgames(games, strategy: Optional[str] = None): retval = {} for game_id in games: - print('Analysing endgames {} of game {}'.format('with cheating' if cheat else '', game_id)) - result = analyze_game_cached(game_id, cheat) + print('Analysing endgames {} of game {}'.format('with strategy {}'.format(strategy) if strategy is not None else '', game_id)) + result = analyze_game_cached(game_id, strategy) retval[game_id] = result return retval -def full_analyze_endgames(games, cheat): +def full_analyze_endgames(games, strategy: Optional[str] = None): retval = {} for game_id in games: - print('Analysing all endgames {} of game {}'.format('with cheating' if cheat else '', game_id)) - result = full_analyze_game_cached(game_id, cheat) + print('Analysing all endgames {} of game {}'.format('with strategy {}'.format(strategy) if strategy is not None else '', game_id)) + result = full_analyze_game_cached(game_id, strategy) retval[game_id] = result return retval @@ -155,10 +155,10 @@ def lookup_val(endgame_dict, clue_modifier) -> str: return retval -def make_endgame_tables(ids: List[int], cheat: bool): - endgames = analyze_endgames(ids, cheat) +def make_endgame_tables(ids: List[int], strategy: Optional[str] = None): + endgames = analyze_endgames(ids, strategy) - postfix = '-cheat' if cheat else '' + postfix = '-{}'.format(strategy) if strategy is not None else '' main_fname = OUT_PATH / ('endgames' + postfix + '.csv') fieldnames = ['Game ID'] + [str(i) for i in range(1, 16)] @@ -176,7 +176,7 @@ def make_endgame_tables(ids: List[int], cheat: bool): x = pandas.read_csv(main_fname) x.to_html(main_fname.with_suffix('.html'), escape=False) - all_endgames = full_analyze_endgames(ids, cheat) + all_endgames = full_analyze_endgames(ids, strategy) fieldnames = ['Game ID'] + [str(i) for i in range(1, 11)] for clue_modifier in range(-2, 3): filename = special_fname.format(clue_modifier) @@ -201,26 +201,9 @@ def make_endgame_tables(ids: List[int], cheat: bool): x.to_html(Path(filename).with_suffix('.html'), escape=False) -def create_cheating_replay_links(ids: List[int]): - outfile = Path('out/cheating_links.csv') - with open(outfile, 'w') as f: - writer = csv.writer(f) - writer.writerow(["Game ID", "Cheating Replay Link"]) - for game_id in ids: - replay = get_game_json(game_id, True) - instance, actions = parse_json_game(replay) - game = HanabLiveGameState(instance) - for action in actions: - game.make_action(action) - writer.writerow([game_id, link(game)]) - - x = pandas.read_csv(outfile) - x.to_html(outfile.with_suffix('.html'), render_links=True) - - -def main(): - games = collect_player_games() +def make_results_table(games): analysis = analyze_games(games) + streaks = {} fieldnames = ['Game ID', 'Seed', 'Player #', 'Result', 'BDR'] fieldnames += sort_players_by_num_games(games) @@ -259,11 +242,42 @@ def main(): a = pandas.read_csv("out/games.csv") a.to_html("out/games.html", escape=False) - game_ids = [int(key) for key in games.keys()] - make_endgame_tables(game_ids, False) - create_cheating_replay_links(game_ids) - make_endgame_tables(game_ids, True) +def create_replay_links(ids: List[int], strategy: str): + outfile = Path('out/{}_links.csv'.format(strategy)) + with open(outfile, 'w') as f: + writer = csv.writer(f) + writer.writerow(["Game ID", "{} Replay Link".format(strategy)]) + for game_id in ids: + replay = get_game_json(game_id, strategy) + instance, actions = parse_json_game(replay) + game = HanabLiveGameState(instance) + for action in actions: + game.make_action(action) + writer.writerow([game_id, link(game)]) + + x = pandas.read_csv(outfile) + x.to_html(outfile.with_suffix('.html'), render_links=True) + + +def main(): + games = collect_player_games() + game_ids = sorted(int(key) for key in games.keys()) + + # This is the main table, tracking streaks, loss reasons, BDRs + make_results_table(games) + + # Additional endgame analysis stats: + # For the real games + make_endgame_tables(game_ids, None) + # For the cheating strategy + make_endgame_tables(game_ids, 'cheat') + # For the information strategy + make_endgame_tables(game_ids, 'info') + + # Create JSON replay links to the bot games to watch + create_replay_links(game_ids, 'cheat') + create_replay_links(game_ids, 'info') if __name__ == "__main__": From beecacb89723f8cadee99b7019c51e05b6891467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 16:45:53 +0100 Subject: [PATCH 17/21] add missing file --- bots.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 bots.py diff --git a/bots.py b/bots.py new file mode 100644 index 0000000..430d8db --- /dev/null +++ b/bots.py @@ -0,0 +1,19 @@ +import subprocess + +from games import get_game_json, GAMES_PATH + + +def run_strategy(game_id: int, strategy: str): + if strategy not in ['cheat', 'info', 'random']: + print('Strategy has to be one of cheat, info or random') + return + get_game_json(game_id) + result = subprocess.run(['./rust_hanabi', '--file', str((GAMES_PATH / str(game_id))), '--json-output', '%-' + strategy, '--strategy', strategy]) + if result.returncode != 0: + print('Failed to run game {} with cheating stratgey'.format(game_id)) + + +def ensure_bot_game_exists(game_id: int, strategy: str): + filename = GAMES_PATH / '{}-{}'.format(game_id, strategy) + if not filename.exists(): + run_strategy(game_id, strategy) From 92f8e124bf9380fbc6c491b4a10654ba27c3c3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 17:01:07 +0100 Subject: [PATCH 18/21] add results of bot games to table --- get_sheet.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/get_sheet.py b/get_sheet.py index bd228ad..46c50af 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -247,14 +247,15 @@ def create_replay_links(ids: List[int], strategy: str): outfile = Path('out/{}_links.csv'.format(strategy)) with open(outfile, 'w') as f: writer = csv.writer(f) - writer.writerow(["Game ID", "{} Replay Link".format(strategy)]) + writer.writerow(["Game ID", "Result", "{} Replay Link".format(strategy)]) for game_id in ids: replay = get_game_json(game_id, strategy) instance, actions = parse_json_game(replay) game = HanabLiveGameState(instance) for action in actions: game.make_action(action) - writer.writerow([game_id, link(game)]) + bdrs, termination = describe_game(replay) + writer.writerow([game_id, 'Win' if game.is_won() else termination, link(game)]) x = pandas.read_csv(outfile) x.to_html(outfile.with_suffix('.html'), render_links=True) From b832a3aa5c305311cc9ffc1b10847c67dd6e7bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Mon, 20 Nov 2023 17:49:52 +0100 Subject: [PATCH 19/21] add team statistics --- bdr.py | 2 +- get_sheet.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/bdr.py b/bdr.py index 8d57e0b..b01cbef 100644 --- a/bdr.py +++ b/bdr.py @@ -13,7 +13,7 @@ def analyze_game(instance: HanabLiveInstance, actions: List[Action]) -> Tuple[Li discard = instance.deck[action.target] if not game.is_trash(discard): if game.is_critical(discard): - termination = 'Discard crit' + termination = 'Discard crit' if action.type == ActionType.Discard else 'Bomb crit' break if discard.rank != 1: if discard in game.deck[game.progress:]: diff --git a/get_sheet.py b/get_sheet.py index 46c50af..c410da8 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -3,7 +3,7 @@ import requests_cache import json import csv import pandas -from typing import List, Optional +from typing import List, Optional, Dict from pathlib import Path from hanabi.database import global_db_connection_manager @@ -73,7 +73,7 @@ def get_player_games(player: str): return json.loads(r.text) -def collect_player_games(): +def collect_player_games() -> Dict[int, Entry]: global_games = {} for player in player_mapping.keys(): print('Parsing games for player {}'.format(player)) @@ -201,9 +201,7 @@ def make_endgame_tables(ids: List[int], strategy: Optional[str] = None): x.to_html(Path(filename).with_suffix('.html'), escape=False) -def make_results_table(games): - analysis = analyze_games(games) - +def make_results_table(games, analysis): streaks = {} fieldnames = ['Game ID', 'Seed', 'Player #', 'Result', 'BDR'] fieldnames += sort_players_by_num_games(games) @@ -243,6 +241,51 @@ def make_results_table(games): a.to_html("out/games.html", escape=False) +def make_team_table(games, analysis): + stats = {3: {}, 4: {}, 5: {}} + for game_id, entry in games.items(): + normalized_players = sorted(map(lambda player: player_mapping.get(player, 'Other'), entry.players)) + if 'Other' in normalized_players: + continue + + num_p = len(normalized_players) + + key = ", ".join(normalized_players) + + bdr, termination = analysis[game_id] + + if key not in stats[num_p]: + stats[num_p][key] = { + 'Games': 0, + 'BDR': 0, + 'Win': 0, + 'Discard crit': 0, + 'Bomb crit': 0, + 'Strikeout': 0, + 'VTK': 0, + 'Lost Endgame': 0 + } + + stats[num_p][key]['Games'] += 1 + stats[num_p][key]['BDR'] += len(bdr) + if entry.won: + stats[num_p][key]['Win'] += 1 + else: + stats[num_p][key][termination] += 1 + + for num_p in range(3, 6): + filename = Path('out/teams_{}.csv'.format(num_p)) + with open(filename, 'w') as f: + writer = csv.DictWriter(f, fieldnames=['Team', 'Games', 'Win', 'BDR', 'Discard crit', 'Bomb crit', 'Strikeout', 'VTK', 'Lost Endgame']) + writer.writeheader() + for team, row in sorted(stats[num_p].items(), key=lambda item: -item[1]['Games']): + row['Team'] = team + writer.writerow(row) + + x = pandas.read_csv(filename) + x.to_html(filename.with_suffix('.html')) + + def create_replay_links(ids: List[int], strategy: str): outfile = Path('out/{}_links.csv'.format(strategy)) with open(outfile, 'w') as f: @@ -263,10 +306,14 @@ def create_replay_links(ids: List[int], strategy: str): def main(): games = collect_player_games() + analysis = analyze_games(games) game_ids = sorted(int(key) for key in games.keys()) # This is the main table, tracking streaks, loss reasons, BDRs - make_results_table(games) + make_results_table(games, analysis) + + # This tracks team statistics + make_team_table(games, analysis) # Additional endgame analysis stats: # For the real games From 9e4ecf59c72b4f095449bc83b62477c3deb677e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Tue, 21 Nov 2023 16:20:56 +0100 Subject: [PATCH 20/21] Fix input paths. Include losses in table. --- endgames.py | 4 ++-- get_sheet.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/endgames.py b/endgames.py index 1437fa0..db49f7d 100644 --- a/endgames.py +++ b/endgames.py @@ -78,7 +78,7 @@ def full_analyze_game_cached(game_id: int, strategy: Optional[str] = None): cached = DATA.get(key, {}).get(str(game_id), None) if cached is not None: return cached - result = full_analyze_game(str(GAMES_PATH / str(game_id)) + '-{}'.format(strategy) if strategy is not None else '') + result = full_analyze_game(str(GAMES_PATH / str(game_id)) + ('-{}'.format(strategy) if strategy is not None else '')) if key not in DATA.keys(): DATA[key] = {} DATA[key][game_id] = result @@ -93,7 +93,7 @@ def analyze_game_cached(game_id: int, strategy: Optional[str] = None): cached = DATA.get(key, {}).get(str(game_id), None) if cached is not None: return cached - result = analyze_game(str(GAMES_PATH / str(game_id)) + '-{}'.format(strategy) if strategy is not None else '') + result = analyze_game(str(GAMES_PATH / str(game_id)) + ('-{}'.format(strategy) if strategy is not None else '')) if key not in DATA.keys(): DATA[key] = {} DATA[key][game_id] = result diff --git a/get_sheet.py b/get_sheet.py index c410da8..ebd11c1 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -259,6 +259,7 @@ def make_team_table(games, analysis): 'Games': 0, 'BDR': 0, 'Win': 0, + 'Loss': 0, 'Discard crit': 0, 'Bomb crit': 0, 'Strikeout': 0, @@ -271,12 +272,13 @@ def make_team_table(games, analysis): if entry.won: stats[num_p][key]['Win'] += 1 else: + stats[num_p][key]['Loss'] += 1 stats[num_p][key][termination] += 1 for num_p in range(3, 6): filename = Path('out/teams_{}.csv'.format(num_p)) with open(filename, 'w') as f: - writer = csv.DictWriter(f, fieldnames=['Team', 'Games', 'Win', 'BDR', 'Discard crit', 'Bomb crit', 'Strikeout', 'VTK', 'Lost Endgame']) + writer = csv.DictWriter(f, fieldnames=['Team', 'Games', 'Win', 'Loss', 'BDR', 'Discard crit', 'Bomb crit', 'Strikeout', 'VTK', 'Lost Endgame']) writer.writeheader() for team, row in sorted(stats[num_p].items(), key=lambda item: -item[1]['Games']): row['Team'] = team From 5f51ab00f53fbdecc70bee80f5854371e0ca1926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Ke=C3=9Fler?= Date: Tue, 21 Nov 2023 16:21:17 +0100 Subject: [PATCH 21/21] add more alt accounts --- get_sheet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/get_sheet.py b/get_sheet.py index ebd11c1..3e4a78e 100644 --- a/get_sheet.py +++ b/get_sheet.py @@ -26,12 +26,15 @@ if not OUT_PATH.exists(): player_mapping = { 'RamaNoVarjan': 'Ramanujan', + 'RamaNoVarjan2': 'Ramanujan', 'purplejoe2': 'PurpleJoe', 'PurpleJoeVar': 'PurpleJoe', + 'PurpleJoeVar2': 'PurpleJoe', 'yagami_blank': 'Yagami', 'yagami_black': 'Yagami', 'MarkusKahlsen': 'Markus', 'NoVarkusKahlsen': 'Markus', + 'NoVarkusKarlsen': 'Markus', 'spring': 'spring', 'str8tsknacker': 'str8tsknacker', 'novarknacker': 'str8tsknacker',