telemetry.gui.gui
Docstring for telemetry.gui.gui
This module is responsible for running the Telemetry App GUI.
The GUI is implemented using tkinter.
This module is responsible for the logic of refreshing plot data.
The GUI logic lives in the TelemetryAppGUI class.
1""" 2Docstring for telemetry.gui.gui 3 4This module is responsible for running the Telemetry App GUI. 5 6The GUI is implemented using tkinter. 7 8This module is responsible for the logic of refreshing plot data. 9 10The GUI logic lives in the TelemetryAppGUI class. 11""" 12 13import tkinter as tk 14from tkinter import ttk, messagebox 15import seaborn as sns 16from pathlib import Path 17 18from telemetry.core.logic import EventLogicEngine 19from telemetry.gui.plotting import PlotTab 20from telemetry.auth.auth import google_login, Role 21 22 23ROOT_DIRECTORY = Path.cwd().parent 24POLLING_INTERVAL_MS = 3000 25 26 27class GUI_SETTINGS: 28 """Stores settings for tkinter GUI appearance.""" 29 WINDOW_TITLE = "Telemetry App" # Title of window. 30 WINDOW_GEOMETRY = "800x600" # Size at startup. 31 WINDOW_MINIMUM_WIDTH = 600 # Minimum width of window. 32 WINDOW_MINIMUM_HEIGHT = 450 # Minimum height of window. 33 FONT_FAMILY = "Arial" 34 FONT_SIZE = 12 35 BACKGROUND_COLOR = "#edd68f" 36 37 38class TelemetryAppGUI(tk.Tk): 39 def __init__(self) -> None: 40 super().__init__() 41 self.title(GUI_SETTINGS.WINDOW_TITLE) 42 self.geometry(GUI_SETTINGS.WINDOW_GEOMETRY) 43 self.configure(background=GUI_SETTINGS.BACKGROUND_COLOR) 44 style = ttk.Style(self) 45 style.theme_use("clam") 46 self.file_name = ROOT_DIRECTORY / "event_logs" / "telemetry_events.json" 47 self.logic_engine = EventLogicEngine() 48 self.authenticated = False 49 self.current_user_name = None 50 51 style.configure( 52 ".", 53 font=(GUI_SETTINGS.FONT_FAMILY, GUI_SETTINGS.FONT_SIZE), 54 padding=6, 55 ) 56 style.configure( 57 "TFrame", 58 background=GUI_SETTINGS.BACKGROUND_COLOR 59 ) 60 style.configure( 61 "TLabel", 62 background=GUI_SETTINGS.BACKGROUND_COLOR 63 ) 64 65 self.minsize( 66 width=GUI_SETTINGS.WINDOW_MINIMUM_WIDTH, 67 height=GUI_SETTINGS.WINDOW_MINIMUM_HEIGHT 68 ) 69 70 self.notebook = ttk.Notebook(self) 71 self.notebook.grid(row=0, column=0, sticky="nsew") 72 self.grid_rowconfigure(0, weight=1) 73 self.grid_columnconfigure(0, weight=1) 74 75 self.tab_home = ttk.Frame(self.notebook) 76 self.tab_funnel = ttk.Frame(self.notebook) 77 self.tab_spike = ttk.Frame(self.notebook) 78 self.tab_curves = ttk.Frame(self.notebook) 79 self.tab_fairness = ttk.Frame(self.notebook) 80 self.tab_suggestions = ttk.Frame(self.notebook) 81 82 self.notebook.add(self.tab_home, text="Home") 83 84 self.make_welcome_screen() 85 86 sns.set_theme(style="dark", context="notebook") 87 88 89 def make_welcome_screen(self): 90 self.welcome_label = ttk.Label( 91 self.tab_home, 92 text=self.get_personalised_welcome_message(), 93 justify="center" 94 ) 95 self.welcome_label.pack(pady=(30, 15)) 96 97 self.sign_in_button = ttk.Button( 98 self.tab_home, 99 text="Sign in with Google", 100 command=self.handle_sign_in 101 ) 102 self.sign_in_button.pack(pady=(10, 20)) 103 104 105 def get_personalised_welcome_message(self) -> str: 106 return "Welcome to the Telemetry App" if self.current_user_name is None else "Welcome to the Telemetry App, " + self.current_user_name 107 108 109 def handle_sign_in(self): 110 _, self.current_user_name, role = google_login() # TODO: DEAL WITH ROLE RETURN 111 self.authenticated = True 112 AUTHORISED_ROLES = [Role.DESIGNER, Role.DEVELOPER] 113 if role in AUTHORISED_ROLES: 114 self.sign_in_button.pack_forget() 115 self.welcome_label.config(text=self.get_personalised_welcome_message()) 116 self.on_authenticated() 117 else: 118 # self.make_welcome_screen() 119 messagebox.showerror( 120 "Authorisation Error", 121 "Only Designers and Developers may access telemetry data" 122 ) 123 124 125 def on_authenticated(self): 126 self.switch_btn_text = tk.StringVar() 127 self.switch_btn_text.set("Change to simulation data") 128 129 switch_simulation_button = ttk.Button( 130 self.tab_home, 131 textvariable=self.switch_btn_text, 132 command=self.toggle_file 133 ) 134 switch_simulation_button.pack(pady=(10,20)) 135 136 reset_telemetry_button = ttk.Button( 137 self.tab_home, 138 text="Reset Telemetry Data", 139 command=self.reset_telemetry 140 ) 141 reset_telemetry_button.pack(pady=(10,20)) 142 143 self.notebook.add(self.tab_funnel, text="Funnel view") 144 self.notebook.add(self.tab_spike, text="Difficulty spike") 145 self.notebook.add(self.tab_curves, text="Health") 146 self.notebook.add(self.tab_fairness, text="Coins") 147 self.notebook.add(self.tab_suggestions, text="Suggestions") 148 149 self.tab_spike.rowconfigure(0, weight=1) 150 self.tab_spike.columnconfigure(0, weight=1) 151 152 self.funnel_plot = PlotTab( 153 parent=self.tab_funnel, 154 title="Funnel view", 155 xlabel="Stage", 156 ylabel="Players remaining after round", 157 ) 158 self.spike_plot = PlotTab( 159 parent=self.tab_spike, 160 title="Difficulty spikes", 161 xlabel="Stage", 162 ylabel="Number of failures", 163 ) 164 self.spike_suggestion = ttk.Label( 165 self.tab_suggestions, 166 text="Suggestion: " + self.generate_spike_suggestion() 167 ) 168 self.spike_suggestion.pack(pady=(30, 15)) 169 self.curves_plot = PlotTab( 170 parent=self.tab_curves, 171 title="HP remaining by stage)", 172 xlabel="Stage", 173 ylabel="Average HP Remaining", 174 ) 175 self.fairness_plot = PlotTab( 176 parent=self.tab_fairness, 177 title="Coins gained per stage", 178 xlabel="Stage", 179 ylabel="Coins gained", 180 ) 181 182 self.refresh_all() 183 self.do_auto_refresh() 184 185 186 def do_auto_refresh(self, interval_ms=POLLING_INTERVAL_MS): 187 """Use polling to refresh data for plots.""" 188 self.refresh_all() 189 self.after(interval_ms, self.do_auto_refresh, interval_ms) 190 191 192 def toggle_file(self): 193 if self.switch_btn_text.get() == "Change to simulation data": 194 self.switch_btn_text.set("Change to telemetry data") 195 self.file_name = ROOT_DIRECTORY / "event_logs" / "simulation_events.json" 196 self.refresh_all() 197 else: 198 self.switch_btn_text.set("Change to simulation data") 199 self.file_name = ROOT_DIRECTORY / "event_logs" / "telemetry_events.json" 200 self.refresh_all() 201 202 203 def reset_telemetry(self): 204 confirmed = messagebox.askyesno( 205 title = "Switch Data Source", 206 message = "Are you sure you want to reset telemetry data? " 207 + "All existing telemetry data will be lost") 208 if confirmed: 209 with open(ROOT_DIRECTORY / "event_logs" / "telemetry_events.json", 'w') as f: 210 f.write('') 211 212 213 def refresh_all(self): 214 self.refresh_funnel_graph() 215 self.refresh_coins_gained_plots() 216 self.refresh_difficulty_spike_failure_plot() 217 self.refresh_health_plots() 218 219 220 def refresh_funnel_graph(self): 221 """ 222 Refreshes the plot of players remaining per stage (referred to 223 as funnel view). 224 """ 225 self.logic_engine.categorise_events(self.file_name) 226 funnel_data: dict[int, int] = self.logic_engine.funnel_view() 227 self.funnel_plot.plot_line( 228 funnel_data.keys(), 229 funnel_data.values(), 230 label="Players Remaining" 231 ) 232 233 234 def refresh_difficulty_spike_failure_plot(self): 235 """ 236 Refreshes the plots for difficulty spike in terms of number 237 of failures per stage. 238 """ 239 self.logic_engine.categorise_events(self.file_name) 240 spike_data: dict[int, int] = self.logic_engine.fail_difficulty_spikes() 241 self.spike_plot.plot_line( 242 spike_data.keys(), 243 spike_data.values(), 244 label="Difficulty spikes (by failure rate)" 245 ) 246 self.spike_suggestion.config(text="Suggestion: " + self.generate_spike_suggestion()) 247 248 249 def get_average_dict_of_stage_dicts( 250 self, 251 per_stage_data: list[dict[int, int]] 252 ) -> dict[int, float]: 253 """ 254 Helper function to get the average of a list of dictionaries 255 which map stage numbers to values like HP remaining or coins 256 gained. 257 Example: Given a list of dictionaries of health per stage data, 258 this function would return a dictionary which maps the stage 259 number to the average of health at that stage. 260 261 :param per_stage_data: List of dictionaries which represent 262 "x per stage" data. 263 :type per_stage_data: list[dict[int, int]] 264 :return: Average of the given data per stage. 265 :rtype: dict[int, float] 266 """ 267 stages = range(1, 11) 268 averages: dict[int, float] = {} 269 for stage in stages: 270 vals = [dictionary.get(stage, 0) 271 for dictionary in per_stage_data] 272 averages[stage] = (sum(vals) / len(vals)) if vals else 0.0 273 return averages 274 275 276 def refresh_health_plots(self): 277 """ 278 Refreshes the plots of HP remaining per stage per difficulty. 279 """ 280 self.logic_engine.categorise_events(self.file_name) 281 health_by_difficulty = self.logic_engine.compare_health_per_stage_per_difficulty() 282 283 series = [] 284 for difficulty, list_of_dicts in health_by_difficulty.items(): 285 averages = self.get_average_dict_of_stage_dicts(list_of_dicts) 286 x = averages.keys() 287 y = averages.values() 288 series.append((x, y, str(difficulty.value))) 289 290 self.curves_plot.plot_multi_line(series) 291 292 293 def refresh_coins_gained_plots(self): 294 """ 295 Refreshed the plots for average coins gained per stage per 296 difficulty. 297 """ 298 self.logic_engine.categorise_events(self.file_name) 299 coins_by_difficulty = self.logic_engine.compare_coins_per_stage_per_difficulty() 300 301 series = [] 302 for difficulty, list_of_dicts in coins_by_difficulty.items(): 303 averages = self.get_average_dict_of_stage_dicts(list_of_dicts) 304 x = averages.keys() 305 y = averages.values() 306 series.append((x, y, str(difficulty.value))) 307 308 self.fairness_plot.plot_multi_line(series) 309 310 311 def generate_spike_suggestion(self): 312 """ 313 Generates a difficulty change suggestion 314 for difficulty spikes. 315 """ 316 stages = "" 317 spikes = self.logic_engine.fail_difficulty_spikes() 318 mean = sum(spikes.values())/len(spikes) 319 for stage in spikes: 320 if spikes[stage] > mean: 321 stages += str(stage) + ", " 322 if stages != "": 323 return "High failure rate in " + stages + " consider increasing lives by 2." 324 return "No suggestions available"
28class GUI_SETTINGS: 29 """Stores settings for tkinter GUI appearance.""" 30 WINDOW_TITLE = "Telemetry App" # Title of window. 31 WINDOW_GEOMETRY = "800x600" # Size at startup. 32 WINDOW_MINIMUM_WIDTH = 600 # Minimum width of window. 33 WINDOW_MINIMUM_HEIGHT = 450 # Minimum height of window. 34 FONT_FAMILY = "Arial" 35 FONT_SIZE = 12 36 BACKGROUND_COLOR = "#edd68f"
Stores settings for tkinter GUI appearance.
39class TelemetryAppGUI(tk.Tk): 40 def __init__(self) -> None: 41 super().__init__() 42 self.title(GUI_SETTINGS.WINDOW_TITLE) 43 self.geometry(GUI_SETTINGS.WINDOW_GEOMETRY) 44 self.configure(background=GUI_SETTINGS.BACKGROUND_COLOR) 45 style = ttk.Style(self) 46 style.theme_use("clam") 47 self.file_name = ROOT_DIRECTORY / "event_logs" / "telemetry_events.json" 48 self.logic_engine = EventLogicEngine() 49 self.authenticated = False 50 self.current_user_name = None 51 52 style.configure( 53 ".", 54 font=(GUI_SETTINGS.FONT_FAMILY, GUI_SETTINGS.FONT_SIZE), 55 padding=6, 56 ) 57 style.configure( 58 "TFrame", 59 background=GUI_SETTINGS.BACKGROUND_COLOR 60 ) 61 style.configure( 62 "TLabel", 63 background=GUI_SETTINGS.BACKGROUND_COLOR 64 ) 65 66 self.minsize( 67 width=GUI_SETTINGS.WINDOW_MINIMUM_WIDTH, 68 height=GUI_SETTINGS.WINDOW_MINIMUM_HEIGHT 69 ) 70 71 self.notebook = ttk.Notebook(self) 72 self.notebook.grid(row=0, column=0, sticky="nsew") 73 self.grid_rowconfigure(0, weight=1) 74 self.grid_columnconfigure(0, weight=1) 75 76 self.tab_home = ttk.Frame(self.notebook) 77 self.tab_funnel = ttk.Frame(self.notebook) 78 self.tab_spike = ttk.Frame(self.notebook) 79 self.tab_curves = ttk.Frame(self.notebook) 80 self.tab_fairness = ttk.Frame(self.notebook) 81 self.tab_suggestions = ttk.Frame(self.notebook) 82 83 self.notebook.add(self.tab_home, text="Home") 84 85 self.make_welcome_screen() 86 87 sns.set_theme(style="dark", context="notebook") 88 89 90 def make_welcome_screen(self): 91 self.welcome_label = ttk.Label( 92 self.tab_home, 93 text=self.get_personalised_welcome_message(), 94 justify="center" 95 ) 96 self.welcome_label.pack(pady=(30, 15)) 97 98 self.sign_in_button = ttk.Button( 99 self.tab_home, 100 text="Sign in with Google", 101 command=self.handle_sign_in 102 ) 103 self.sign_in_button.pack(pady=(10, 20)) 104 105 106 def get_personalised_welcome_message(self) -> str: 107 return "Welcome to the Telemetry App" if self.current_user_name is None else "Welcome to the Telemetry App, " + self.current_user_name 108 109 110 def handle_sign_in(self): 111 _, self.current_user_name, role = google_login() # TODO: DEAL WITH ROLE RETURN 112 self.authenticated = True 113 AUTHORISED_ROLES = [Role.DESIGNER, Role.DEVELOPER] 114 if role in AUTHORISED_ROLES: 115 self.sign_in_button.pack_forget() 116 self.welcome_label.config(text=self.get_personalised_welcome_message()) 117 self.on_authenticated() 118 else: 119 # self.make_welcome_screen() 120 messagebox.showerror( 121 "Authorisation Error", 122 "Only Designers and Developers may access telemetry data" 123 ) 124 125 126 def on_authenticated(self): 127 self.switch_btn_text = tk.StringVar() 128 self.switch_btn_text.set("Change to simulation data") 129 130 switch_simulation_button = ttk.Button( 131 self.tab_home, 132 textvariable=self.switch_btn_text, 133 command=self.toggle_file 134 ) 135 switch_simulation_button.pack(pady=(10,20)) 136 137 reset_telemetry_button = ttk.Button( 138 self.tab_home, 139 text="Reset Telemetry Data", 140 command=self.reset_telemetry 141 ) 142 reset_telemetry_button.pack(pady=(10,20)) 143 144 self.notebook.add(self.tab_funnel, text="Funnel view") 145 self.notebook.add(self.tab_spike, text="Difficulty spike") 146 self.notebook.add(self.tab_curves, text="Health") 147 self.notebook.add(self.tab_fairness, text="Coins") 148 self.notebook.add(self.tab_suggestions, text="Suggestions") 149 150 self.tab_spike.rowconfigure(0, weight=1) 151 self.tab_spike.columnconfigure(0, weight=1) 152 153 self.funnel_plot = PlotTab( 154 parent=self.tab_funnel, 155 title="Funnel view", 156 xlabel="Stage", 157 ylabel="Players remaining after round", 158 ) 159 self.spike_plot = PlotTab( 160 parent=self.tab_spike, 161 title="Difficulty spikes", 162 xlabel="Stage", 163 ylabel="Number of failures", 164 ) 165 self.spike_suggestion = ttk.Label( 166 self.tab_suggestions, 167 text="Suggestion: " + self.generate_spike_suggestion() 168 ) 169 self.spike_suggestion.pack(pady=(30, 15)) 170 self.curves_plot = PlotTab( 171 parent=self.tab_curves, 172 title="HP remaining by stage)", 173 xlabel="Stage", 174 ylabel="Average HP Remaining", 175 ) 176 self.fairness_plot = PlotTab( 177 parent=self.tab_fairness, 178 title="Coins gained per stage", 179 xlabel="Stage", 180 ylabel="Coins gained", 181 ) 182 183 self.refresh_all() 184 self.do_auto_refresh() 185 186 187 def do_auto_refresh(self, interval_ms=POLLING_INTERVAL_MS): 188 """Use polling to refresh data for plots.""" 189 self.refresh_all() 190 self.after(interval_ms, self.do_auto_refresh, interval_ms) 191 192 193 def toggle_file(self): 194 if self.switch_btn_text.get() == "Change to simulation data": 195 self.switch_btn_text.set("Change to telemetry data") 196 self.file_name = ROOT_DIRECTORY / "event_logs" / "simulation_events.json" 197 self.refresh_all() 198 else: 199 self.switch_btn_text.set("Change to simulation data") 200 self.file_name = ROOT_DIRECTORY / "event_logs" / "telemetry_events.json" 201 self.refresh_all() 202 203 204 def reset_telemetry(self): 205 confirmed = messagebox.askyesno( 206 title = "Switch Data Source", 207 message = "Are you sure you want to reset telemetry data? " 208 + "All existing telemetry data will be lost") 209 if confirmed: 210 with open(ROOT_DIRECTORY / "event_logs" / "telemetry_events.json", 'w') as f: 211 f.write('') 212 213 214 def refresh_all(self): 215 self.refresh_funnel_graph() 216 self.refresh_coins_gained_plots() 217 self.refresh_difficulty_spike_failure_plot() 218 self.refresh_health_plots() 219 220 221 def refresh_funnel_graph(self): 222 """ 223 Refreshes the plot of players remaining per stage (referred to 224 as funnel view). 225 """ 226 self.logic_engine.categorise_events(self.file_name) 227 funnel_data: dict[int, int] = self.logic_engine.funnel_view() 228 self.funnel_plot.plot_line( 229 funnel_data.keys(), 230 funnel_data.values(), 231 label="Players Remaining" 232 ) 233 234 235 def refresh_difficulty_spike_failure_plot(self): 236 """ 237 Refreshes the plots for difficulty spike in terms of number 238 of failures per stage. 239 """ 240 self.logic_engine.categorise_events(self.file_name) 241 spike_data: dict[int, int] = self.logic_engine.fail_difficulty_spikes() 242 self.spike_plot.plot_line( 243 spike_data.keys(), 244 spike_data.values(), 245 label="Difficulty spikes (by failure rate)" 246 ) 247 self.spike_suggestion.config(text="Suggestion: " + self.generate_spike_suggestion()) 248 249 250 def get_average_dict_of_stage_dicts( 251 self, 252 per_stage_data: list[dict[int, int]] 253 ) -> dict[int, float]: 254 """ 255 Helper function to get the average of a list of dictionaries 256 which map stage numbers to values like HP remaining or coins 257 gained. 258 Example: Given a list of dictionaries of health per stage data, 259 this function would return a dictionary which maps the stage 260 number to the average of health at that stage. 261 262 :param per_stage_data: List of dictionaries which represent 263 "x per stage" data. 264 :type per_stage_data: list[dict[int, int]] 265 :return: Average of the given data per stage. 266 :rtype: dict[int, float] 267 """ 268 stages = range(1, 11) 269 averages: dict[int, float] = {} 270 for stage in stages: 271 vals = [dictionary.get(stage, 0) 272 for dictionary in per_stage_data] 273 averages[stage] = (sum(vals) / len(vals)) if vals else 0.0 274 return averages 275 276 277 def refresh_health_plots(self): 278 """ 279 Refreshes the plots of HP remaining per stage per difficulty. 280 """ 281 self.logic_engine.categorise_events(self.file_name) 282 health_by_difficulty = self.logic_engine.compare_health_per_stage_per_difficulty() 283 284 series = [] 285 for difficulty, list_of_dicts in health_by_difficulty.items(): 286 averages = self.get_average_dict_of_stage_dicts(list_of_dicts) 287 x = averages.keys() 288 y = averages.values() 289 series.append((x, y, str(difficulty.value))) 290 291 self.curves_plot.plot_multi_line(series) 292 293 294 def refresh_coins_gained_plots(self): 295 """ 296 Refreshed the plots for average coins gained per stage per 297 difficulty. 298 """ 299 self.logic_engine.categorise_events(self.file_name) 300 coins_by_difficulty = self.logic_engine.compare_coins_per_stage_per_difficulty() 301 302 series = [] 303 for difficulty, list_of_dicts in coins_by_difficulty.items(): 304 averages = self.get_average_dict_of_stage_dicts(list_of_dicts) 305 x = averages.keys() 306 y = averages.values() 307 series.append((x, y, str(difficulty.value))) 308 309 self.fairness_plot.plot_multi_line(series) 310 311 312 def generate_spike_suggestion(self): 313 """ 314 Generates a difficulty change suggestion 315 for difficulty spikes. 316 """ 317 stages = "" 318 spikes = self.logic_engine.fail_difficulty_spikes() 319 mean = sum(spikes.values())/len(spikes) 320 for stage in spikes: 321 if spikes[stage] > mean: 322 stages += str(stage) + ", " 323 if stages != "": 324 return "High failure rate in " + stages + " consider increasing lives by 2." 325 return "No suggestions available"
Toplevel widget of Tk which represents mostly the main window of an application. It has an associated Tcl interpreter.
40 def __init__(self) -> None: 41 super().__init__() 42 self.title(GUI_SETTINGS.WINDOW_TITLE) 43 self.geometry(GUI_SETTINGS.WINDOW_GEOMETRY) 44 self.configure(background=GUI_SETTINGS.BACKGROUND_COLOR) 45 style = ttk.Style(self) 46 style.theme_use("clam") 47 self.file_name = ROOT_DIRECTORY / "event_logs" / "telemetry_events.json" 48 self.logic_engine = EventLogicEngine() 49 self.authenticated = False 50 self.current_user_name = None 51 52 style.configure( 53 ".", 54 font=(GUI_SETTINGS.FONT_FAMILY, GUI_SETTINGS.FONT_SIZE), 55 padding=6, 56 ) 57 style.configure( 58 "TFrame", 59 background=GUI_SETTINGS.BACKGROUND_COLOR 60 ) 61 style.configure( 62 "TLabel", 63 background=GUI_SETTINGS.BACKGROUND_COLOR 64 ) 65 66 self.minsize( 67 width=GUI_SETTINGS.WINDOW_MINIMUM_WIDTH, 68 height=GUI_SETTINGS.WINDOW_MINIMUM_HEIGHT 69 ) 70 71 self.notebook = ttk.Notebook(self) 72 self.notebook.grid(row=0, column=0, sticky="nsew") 73 self.grid_rowconfigure(0, weight=1) 74 self.grid_columnconfigure(0, weight=1) 75 76 self.tab_home = ttk.Frame(self.notebook) 77 self.tab_funnel = ttk.Frame(self.notebook) 78 self.tab_spike = ttk.Frame(self.notebook) 79 self.tab_curves = ttk.Frame(self.notebook) 80 self.tab_fairness = ttk.Frame(self.notebook) 81 self.tab_suggestions = ttk.Frame(self.notebook) 82 83 self.notebook.add(self.tab_home, text="Home") 84 85 self.make_welcome_screen() 86 87 sns.set_theme(style="dark", context="notebook")
Return a new top level widget on screen SCREENNAME. A new Tcl interpreter will be created. BASENAME will be used for the identification of the profile file (see readprofile). It is constructed from sys.argv[0] without extensions if None is given. CLASSNAME is the name of the widget class.
90 def make_welcome_screen(self): 91 self.welcome_label = ttk.Label( 92 self.tab_home, 93 text=self.get_personalised_welcome_message(), 94 justify="center" 95 ) 96 self.welcome_label.pack(pady=(30, 15)) 97 98 self.sign_in_button = ttk.Button( 99 self.tab_home, 100 text="Sign in with Google", 101 command=self.handle_sign_in 102 ) 103 self.sign_in_button.pack(pady=(10, 20))
110 def handle_sign_in(self): 111 _, self.current_user_name, role = google_login() # TODO: DEAL WITH ROLE RETURN 112 self.authenticated = True 113 AUTHORISED_ROLES = [Role.DESIGNER, Role.DEVELOPER] 114 if role in AUTHORISED_ROLES: 115 self.sign_in_button.pack_forget() 116 self.welcome_label.config(text=self.get_personalised_welcome_message()) 117 self.on_authenticated() 118 else: 119 # self.make_welcome_screen() 120 messagebox.showerror( 121 "Authorisation Error", 122 "Only Designers and Developers may access telemetry data" 123 )
126 def on_authenticated(self): 127 self.switch_btn_text = tk.StringVar() 128 self.switch_btn_text.set("Change to simulation data") 129 130 switch_simulation_button = ttk.Button( 131 self.tab_home, 132 textvariable=self.switch_btn_text, 133 command=self.toggle_file 134 ) 135 switch_simulation_button.pack(pady=(10,20)) 136 137 reset_telemetry_button = ttk.Button( 138 self.tab_home, 139 text="Reset Telemetry Data", 140 command=self.reset_telemetry 141 ) 142 reset_telemetry_button.pack(pady=(10,20)) 143 144 self.notebook.add(self.tab_funnel, text="Funnel view") 145 self.notebook.add(self.tab_spike, text="Difficulty spike") 146 self.notebook.add(self.tab_curves, text="Health") 147 self.notebook.add(self.tab_fairness, text="Coins") 148 self.notebook.add(self.tab_suggestions, text="Suggestions") 149 150 self.tab_spike.rowconfigure(0, weight=1) 151 self.tab_spike.columnconfigure(0, weight=1) 152 153 self.funnel_plot = PlotTab( 154 parent=self.tab_funnel, 155 title="Funnel view", 156 xlabel="Stage", 157 ylabel="Players remaining after round", 158 ) 159 self.spike_plot = PlotTab( 160 parent=self.tab_spike, 161 title="Difficulty spikes", 162 xlabel="Stage", 163 ylabel="Number of failures", 164 ) 165 self.spike_suggestion = ttk.Label( 166 self.tab_suggestions, 167 text="Suggestion: " + self.generate_spike_suggestion() 168 ) 169 self.spike_suggestion.pack(pady=(30, 15)) 170 self.curves_plot = PlotTab( 171 parent=self.tab_curves, 172 title="HP remaining by stage)", 173 xlabel="Stage", 174 ylabel="Average HP Remaining", 175 ) 176 self.fairness_plot = PlotTab( 177 parent=self.tab_fairness, 178 title="Coins gained per stage", 179 xlabel="Stage", 180 ylabel="Coins gained", 181 ) 182 183 self.refresh_all() 184 self.do_auto_refresh()
187 def do_auto_refresh(self, interval_ms=POLLING_INTERVAL_MS): 188 """Use polling to refresh data for plots.""" 189 self.refresh_all() 190 self.after(interval_ms, self.do_auto_refresh, interval_ms)
Use polling to refresh data for plots.
193 def toggle_file(self): 194 if self.switch_btn_text.get() == "Change to simulation data": 195 self.switch_btn_text.set("Change to telemetry data") 196 self.file_name = ROOT_DIRECTORY / "event_logs" / "simulation_events.json" 197 self.refresh_all() 198 else: 199 self.switch_btn_text.set("Change to simulation data") 200 self.file_name = ROOT_DIRECTORY / "event_logs" / "telemetry_events.json" 201 self.refresh_all()
204 def reset_telemetry(self): 205 confirmed = messagebox.askyesno( 206 title = "Switch Data Source", 207 message = "Are you sure you want to reset telemetry data? " 208 + "All existing telemetry data will be lost") 209 if confirmed: 210 with open(ROOT_DIRECTORY / "event_logs" / "telemetry_events.json", 'w') as f: 211 f.write('')
221 def refresh_funnel_graph(self): 222 """ 223 Refreshes the plot of players remaining per stage (referred to 224 as funnel view). 225 """ 226 self.logic_engine.categorise_events(self.file_name) 227 funnel_data: dict[int, int] = self.logic_engine.funnel_view() 228 self.funnel_plot.plot_line( 229 funnel_data.keys(), 230 funnel_data.values(), 231 label="Players Remaining" 232 )
Refreshes the plot of players remaining per stage (referred to as funnel view).
235 def refresh_difficulty_spike_failure_plot(self): 236 """ 237 Refreshes the plots for difficulty spike in terms of number 238 of failures per stage. 239 """ 240 self.logic_engine.categorise_events(self.file_name) 241 spike_data: dict[int, int] = self.logic_engine.fail_difficulty_spikes() 242 self.spike_plot.plot_line( 243 spike_data.keys(), 244 spike_data.values(), 245 label="Difficulty spikes (by failure rate)" 246 ) 247 self.spike_suggestion.config(text="Suggestion: " + self.generate_spike_suggestion())
Refreshes the plots for difficulty spike in terms of number of failures per stage.
250 def get_average_dict_of_stage_dicts( 251 self, 252 per_stage_data: list[dict[int, int]] 253 ) -> dict[int, float]: 254 """ 255 Helper function to get the average of a list of dictionaries 256 which map stage numbers to values like HP remaining or coins 257 gained. 258 Example: Given a list of dictionaries of health per stage data, 259 this function would return a dictionary which maps the stage 260 number to the average of health at that stage. 261 262 :param per_stage_data: List of dictionaries which represent 263 "x per stage" data. 264 :type per_stage_data: list[dict[int, int]] 265 :return: Average of the given data per stage. 266 :rtype: dict[int, float] 267 """ 268 stages = range(1, 11) 269 averages: dict[int, float] = {} 270 for stage in stages: 271 vals = [dictionary.get(stage, 0) 272 for dictionary in per_stage_data] 273 averages[stage] = (sum(vals) / len(vals)) if vals else 0.0 274 return averages
Helper function to get the average of a list of dictionaries which map stage numbers to values like HP remaining or coins gained. Example: Given a list of dictionaries of health per stage data, this function would return a dictionary which maps the stage number to the average of health at that stage.
Parameters
- per_stage_data: List of dictionaries which represent "x per stage" data.
Returns
Average of the given data per stage.
277 def refresh_health_plots(self): 278 """ 279 Refreshes the plots of HP remaining per stage per difficulty. 280 """ 281 self.logic_engine.categorise_events(self.file_name) 282 health_by_difficulty = self.logic_engine.compare_health_per_stage_per_difficulty() 283 284 series = [] 285 for difficulty, list_of_dicts in health_by_difficulty.items(): 286 averages = self.get_average_dict_of_stage_dicts(list_of_dicts) 287 x = averages.keys() 288 y = averages.values() 289 series.append((x, y, str(difficulty.value))) 290 291 self.curves_plot.plot_multi_line(series)
Refreshes the plots of HP remaining per stage per difficulty.
294 def refresh_coins_gained_plots(self): 295 """ 296 Refreshed the plots for average coins gained per stage per 297 difficulty. 298 """ 299 self.logic_engine.categorise_events(self.file_name) 300 coins_by_difficulty = self.logic_engine.compare_coins_per_stage_per_difficulty() 301 302 series = [] 303 for difficulty, list_of_dicts in coins_by_difficulty.items(): 304 averages = self.get_average_dict_of_stage_dicts(list_of_dicts) 305 x = averages.keys() 306 y = averages.values() 307 series.append((x, y, str(difficulty.value))) 308 309 self.fairness_plot.plot_multi_line(series)
Refreshed the plots for average coins gained per stage per difficulty.
312 def generate_spike_suggestion(self): 313 """ 314 Generates a difficulty change suggestion 315 for difficulty spikes. 316 """ 317 stages = "" 318 spikes = self.logic_engine.fail_difficulty_spikes() 319 mean = sum(spikes.values())/len(spikes) 320 for stage in spikes: 321 if spikes[stage] > mean: 322 stages += str(stage) + ", " 323 if stages != "": 324 return "High failure rate in " + stages + " consider increasing lives by 2." 325 return "No suggestions available"
Generates a difficulty change suggestion for difficulty spikes.