diff --git a/images/preview.PNG b/images/preview.PNG index ed80b6d..b1e2151 100644 Binary files a/images/preview.PNG and b/images/preview.PNG differ diff --git a/src/main.py b/src/main.py index 6678a1b..b74b231 100644 --- a/src/main.py +++ b/src/main.py @@ -6,7 +6,7 @@ customtkinter.set_default_color_theme("blue") # Themes: blue (default), dark-blue, green app = customtkinter.CTk() # create CTk window like you do with the Tk window -app.title("Race Engine V0.0.3") +app.title("Race Engine V0.0.4") controller = race_engine_controller.RaceEngineController(app) app.after(0, lambda:app.state("zoomed")) diff --git a/src/race_engine_controller/race_engine_controller.py b/src/race_engine_controller/race_engine_controller.py index 7375966..81a30af 100644 --- a/src/race_engine_controller/race_engine_controller.py +++ b/src/race_engine_controller/race_engine_controller.py @@ -26,6 +26,12 @@ def simulate_race(self): self.model.advance() self.view.timing_screen.update_view(self.model.get_data_for_view()) + + # GET PIT STRATEGY FROM VIEW + driver1_data = self.view.timing_screen.strategy_editor_driver1.get_data() + driver2_data = self.view.timing_screen.strategy_editor_driver2.get_data() + self.model.update_player_drivers_strategy(driver1_data, driver2_data) + self.app.after(3000, self.simulate_race) def pause_resume(self): diff --git a/src/race_engine_model/race_engine_model.py b/src/race_engine_model/race_engine_model.py index 5e6c520..42817ed 100644 --- a/src/race_engine_model/race_engine_model.py +++ b/src/race_engine_model/race_engine_model.py @@ -34,6 +34,9 @@ def __init__(self): self.retirements = [] + self.player_driver1 = self.get_particpant_model_by_name("Nico Rosberg") + self.player_driver2 = self.get_particpant_model_by_name("Michael Schumacher") + def log_event(self, event): logging.info(f"Lap {self.current_lap}: {event}") @@ -324,4 +327,9 @@ def get_data_for_view(self): for p in self.participants: data["laptimes"][p.name] = copy.deepcopy(p.laptimes) - return data \ No newline at end of file + return data + + def update_player_drivers_strategy(self, driver1_data, driver2_data): + self.player_driver1.update_player_pitstop_laps(driver1_data) + self.player_driver2.update_player_pitstop_laps(driver2_data) + \ No newline at end of file diff --git a/src/race_engine_model/race_engine_particpant_model.py b/src/race_engine_model/race_engine_particpant_model.py index 0ec5e49..94894f1 100644 --- a/src/race_engine_model/race_engine_particpant_model.py +++ b/src/race_engine_model/race_engine_particpant_model.py @@ -74,7 +74,7 @@ def calculate_laptime(self, gap_ahead): self.laptime = self.base_laptime + random.randint(0, 700) + self.car_model.fuel_effect + self.car_model.tyre_wear + dirty_air_effect - if self.current_lap == self.pit1_lap: + if self.current_lap in [self.pit1_lap, self.pit2_lap, self.pit3_lap]: self.status = "pitting in" if "pitting in" in self.status: @@ -107,6 +107,8 @@ def recalculate_laptime_when_passed(self, revised_laptime): def calculate_pitstop_laps(self): self.pit1_lap = random.randint(19, 32) + self.pit2_lap = None + self.pit3_lap = None def calculate_if_retires(self): self.retires = False @@ -121,3 +123,15 @@ def update_fastest_lap(self): self.fastest_laptime = self.laptime elif min(self.laptimes) == self.laptime: self.fastest_laptime = self.laptime + + def update_player_pitstop_laps(self, data): + ''' + data is a dict optained from the view + { + "pit1_lap": 24, + "pit2_lap": ... etc + } + ''' + self.pit1_lap = data["pit1_lap"] + self.pit2_lap = data["pit2_lap"] + self.pit3_lap = data["pit3_lap"] diff --git a/src/race_engine_view/custom_widgets/strategy_editor.py b/src/race_engine_view/custom_widgets/strategy_editor.py new file mode 100644 index 0000000..ebf9ba7 --- /dev/null +++ b/src/race_engine_view/custom_widgets/strategy_editor.py @@ -0,0 +1,206 @@ +from tkinter import ttk +from tkinter import * + +import customtkinter + + + +class StrategyEditor: + def __init__(self, parent, view, driver_name, start_col, start_row): + self.parent = parent + self.view = view + self.start_col = start_col + self.number_laps = 56 + + self.one_third_distance = int(self.number_laps / 3) + self.half_distance = int(self.number_laps / 2) + self.two_third_distance = int(2 * (self.number_laps / 3)) + self.one_quarter_distance = int(self.number_laps / 4) + self.three_quarters_distance = int(3 * (self.number_laps / 4)) + + customtkinter.CTkLabel(self.parent, text=driver_name, anchor=W).grid(row=start_row, column=start_col, columnspan=4, padx=self.view.padx, pady=self.view.pady, sticky="EW") + + self.setup_variables() + + self.progress_bars = [] + self.lap_labels = [] + self.minus_buttons = [] + self.plus_buttons = [] + + row = start_row + 1 + for idx in [0, 1, 2]: + self.setup_edit_widgets(idx, row) + row += 2 + + self.set_default_pit_laps(1) + + def setup_variables(self): + self.pit1_lap = None + self.pit2_lap = None + self.pit3_lap = None + + self.pit1_var = customtkinter.StringVar(value="on") + self.pit2_var = customtkinter.StringVar(value="off") + self.pit3_var = customtkinter.StringVar(value="off") + + self.pit_number_vars = [self.pit1_var, self.pit2_var, self.pit3_var] + + def setup_edit_widgets(self, idx, row): + btn_width = 40 + + reduced_pady = 1 + l = customtkinter.CTkLabel(self.parent, text="Some Lap") + l.grid(row=row, column=self.start_col + 2, sticky="EW", padx=self.view.padx, pady=(self.view.pady, reduced_pady)) + self.lap_labels.append(l) + + m = customtkinter.CTkButton(self.parent, text= "-", width=btn_width, command=lambda idx=idx:self.minus_lap_event(idx)) + m.grid(row=row+1, column=self.start_col + 1, sticky="EW", padx=self.view.padx, pady=(reduced_pady, self.view.pady)) + self.minus_buttons.append(m) + + c = customtkinter.CTkCheckBox(self.parent, text=f"{idx + 1} Stop", variable=self.pit_number_vars[idx], onvalue="on", offvalue="off", width=10, + command=lambda idx=idx: self.pit_strategy_combo_event(idx)) + c.grid(row=row, column=self.start_col, rowspan=2, sticky="SE", padx=self.view.padx, pady=(reduced_pady, self.view.pady)) + + p = customtkinter.CTkProgressBar(self.parent, orientation="horizontal") + p.grid(row=row+1, column=self.start_col + 2, sticky="EW", padx=self.view.padx, pady=(reduced_pady, self.view.pady)) + self.progress_bars.append(p) + + p = customtkinter.CTkButton(self.parent, text= "+", width=btn_width, command=lambda idx=idx:self.plus_lap_event(idx)) + p.grid(row=row+1, column=self.start_col + 3, sticky="EW", padx=self.view.padx, pady=(reduced_pady, self.view.pady)) + self.plus_buttons.append(p) + + def pit_strategy_combo_event(self, idx_clicked): + for idx, var in enumerate(self.pit_number_vars): + if idx == idx_clicked: + var.set("on") + else: + var.set("off") + + self.set_default_pit_laps(idx_clicked + 1) + + def set_default_pit_laps(self, number_of_stops): + self.update_lap_label(1, None) + self.update_lap_label(2, None) + + # disable buttons + self.minus_buttons[1].configure(state="disabled") + self.minus_buttons[2].configure(state="disabled") + + self.plus_buttons[1].configure(state="disabled") + self.plus_buttons[2].configure(state="disabled") + + # SET PROGRESS BARS + self.progress_bars[1].set(0) + self.progress_bars[2].set(0) + + if number_of_stops == 1: + self.progress_bars[0].set(0.5) + self.pit1_lap = self.half_distance + self.update_lap_label(0, self.pit1_lap) + + elif number_of_stops == 2: + self.progress_bars[0].set(0.33) + self.pit1_lap = self.one_third_distance + self.update_lap_label(0, self.pit1_lap) + + self.progress_bars[1].set(0.66) + self.pit2_lap = self.two_third_distance + self.update_lap_label(1, self.pit2_lap) + self.minus_buttons[1].configure(state="normal") + self.plus_buttons[1].configure(state="normal") + + elif number_of_stops == 3: + self.progress_bars[0].set(0.25) + self.pit1_lap = self.one_quarter_distance + self.update_lap_label(0, self.pit1_lap) + + self.progress_bars[1].set(0.50) + self.pit2_lap = self.half_distance + self.update_lap_label(1, self.pit2_lap) + self.minus_buttons[1].configure(state="normal") + self.plus_buttons[1].configure(state="normal") + + self.progress_bars[2].set(0.75) + self.pit3_lap = self.three_quarters_distance + self.update_lap_label(2, self.pit3_lap) + self.minus_buttons[2].configure(state="normal") + self.plus_buttons[2].configure(state="normal") + + def update_lap_label(self, label_idx, pit_lap): + if pit_lap == None: + self.lap_labels[label_idx].configure(text="N/A") + else: + percentage = int((pit_lap/self.number_laps)*100) + self.lap_labels[label_idx].configure(text=f"Lap {pit_lap} / {self.number_laps} ({percentage}%)") + self.progress_bars[label_idx].set(percentage/100) + + def minus_lap_event(self, idx): + process = True + + # AVOID LAP 1 or BELOW + if idx == 0 and self.pit1_lap < 3: + process = False + + # AVOID STOP 2 BEFORE STOP 1 + if idx == 1 and self.pit2_lap - self.pit1_lap == 1: + process = False + + # AVOID STOP 3 BEFORE STOP 2 + if idx == 2 and self.pit3_lap - self.pit2_lap == 1: + process = False + + if process is True: + if idx == 0: + self.pit1_lap -= 1 + self.update_lap_label(idx, self.pit1_lap) + + elif idx == 1: + self.pit2_lap -= 1 + self.update_lap_label(idx, self.pit2_lap) + + elif idx == 2: + self.pit3_lap -= 1 + self.update_lap_label(idx, self.pit3_lap) + + def plus_lap_event(self, idx): + process = True + + # AVOID LAST 1 or ABOVE + if idx == 0 and self.pit1_lap == self.number_laps - 1: + process = False + + if idx == 1 and self.pit2_lap == self.number_laps - 1: + process = False + + if idx == 2 and self.pit3_lap == self.number_laps - 1: + process = False + + # AVOID STOP 1 AFTTER STOP 2 + if idx == 0 and self.pit2_lap is not None and self.pit2_lap - self.pit1_lap == 1: + process = False + + # AVOID STOP 2 AFTER STOP 2 + if idx == 1 and self.pit3_lap is not None and self.pit3_lap - self.pit2_lap == 1: + process = False + + if process is True: + if idx == 0: + self.pit1_lap += 1 + self.update_lap_label(idx, self.pit1_lap) + + elif idx == 1: + self.pit2_lap += 1 + self.update_lap_label(idx, self.pit2_lap) + + elif idx == 2: + self.pit3_lap += 1 + self.update_lap_label(idx, self.pit3_lap) + + def get_data(self): + data = { + "pit1_lap": self.pit1_lap, + "pit2_lap": self.pit2_lap, + "pit3_lap": self.pit3_lap, + } + + return data \ No newline at end of file diff --git a/src/race_engine_view/race_engine_icons.py b/src/race_engine_view/race_engine_icons.py index 1cb4063..3aa5fbe 100644 --- a/src/race_engine_view/race_engine_icons.py +++ b/src/race_engine_view/race_engine_icons.py @@ -20,6 +20,9 @@ def setup_icons(view): data= "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABDklEQVR4nO3dwQ3CQBAEwQsPyD8CCGSR3/6BJbetqgBOo+0Abi0AAICYmXnNzGei1n5v1XbD5xFBsjEuFmTzPiJI2rr4XkFOJkiMIDGCxAgSI0iMIDGCxAgSI0iMIDGCxAgSI0iMIDGCxAgSI0iMIDGCxAgSI0iMIHcLAgC/mrh18b2CnEyQGEFiBIkRJEaQGEFiBIkRJEaQGEFiBIkRJEaQGEFiBIkRJEaQGEFiBIkRJEaQGEHuFgQAfjVx6+J7BTmZIDGCxAgSI0iMIDGCxAgSI0iMIDGCxAgSI0iMIDGCxAgSI0iMIDGCxAgSI0iMIDGC3DCID+5jH9w/t4cmau33Vm03fPwdBAAAYB3rCwuHoscz8qtzAAAAAElFTkSuQmCC" view.timing_icon2 = customtkinter.CTkImage(light_image=decode_base64_image(size, data), size=size) + data = "iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAYAAAA4qEECAAAACXBIWXMAAAsTAAALEwEAmpwYAAACdklEQVR4nO2cTU7DMBBGZ1W4DD9nKwuWyZJjFHoDFlXPUzhGqg8ZORKUluZnPJlJvrcG9fnJmCSNLUIIIYQQQgghSwFADeBFgoII/lmyxbdsVP8TSd+yUf0vSPqUjep/RdKXbFT/jpJuY4fw7ynpLnYI/4GSLWtT2fP+TxhOHSFyogFwbyJ73v8xO8BtbIXILa9FRf8fwxY61N4jJz6KSHYbxyf0qD1HTjSqgv3GMnbZKBcbwLOyHNTk+o9Fm0pbUDW2TITryCViy0S4j6wdWyYiRGTlf443JrK/vW8UvGtr6bEzew/g1tB3BeA9xEyOGhuRI/8YxHrktemu5DKSl4v0GUNpPDyf+SY9u0i31emOb2D0XYnYIyKnMRwAbADcSRTQ7U9XdRmZ4jNdAMOBLzayZYDFR7YIwcgGQRjZIAyWviZbBAIjl48NRi4/s8HIw0CPcIxsc8u86/gz5o9hQ4HxT9qWe3VhGJuRDWIzskFsRh4DGLo84NLhMnILl5Cu8PLOAPCGxSTyirfgjiLLiN9ZNOBjUt+RWzizrwB+lVUe8MvZmJFbuIxk+AKNAeArYfOKPOtlJG/73ebNkkfEf233mMfyBuBBPJA3sPNFdOebhvaBtlaEOq8j+mahUOd1tHD7m0FkmM2Kv/4alJ3ZmqccyERo+ReLrX2UhEyE5hjUYxc4rwOqgv3GArexeTBK3FNoDqpycznqp0DsTRHBbmNIt9UauD8prJly2296duH+OLYZHTC4dh955BESlTghlH9P2UqcEcq/o2wlTgnlf0W2EueE8r8g60tyLv4nsj4l5+KfL/1sL4EUie5PCCGEEEIIIdKXL/FeSve/F5iQAAAAAElFTkSuQmCC" + view.pit_icon2 = customtkinter.CTkImage(light_image=decode_base64_image(size, data), size=size) + def decode_base64_image(size, data): diff --git a/src/race_engine_view/timing_screen.py b/src/race_engine_view/timing_screen.py index 88ed05f..a05c985 100644 --- a/src/race_engine_view/timing_screen.py +++ b/src/race_engine_view/timing_screen.py @@ -10,7 +10,7 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk from matplotlib.figure import Figure -from race_engine_view.custom_widgets import timing_screen_table +from race_engine_view.custom_widgets import timing_screen_table, strategy_editor class TimingScreen(customtkinter.CTkFrame): def __init__(self, master, view): @@ -44,6 +44,11 @@ def config_grid(self): self.player_frame.grid_columnconfigure(0, weight=1) self.player_frame.grid_columnconfigure(1, weight=1) + # note, frames are used as seperators on the strategy frame, these are expanded by the commands below + self.strategy_frame.grid_columnconfigure(6, weight=1) + self.strategy_frame.grid_columnconfigure(12, weight=1) + self.strategy_frame.grid_rowconfigure(10, weight=1) + def setup_frames(self): self.top_frame = customtkinter.CTkFrame(self) self.top_frame.grid(row=0, column=0, columnspan=2, sticky="EW", pady=self.view.pady) @@ -54,6 +59,9 @@ def setup_frames(self): self.button_frame = customtkinter.CTkFrame(self) self.button_frame.grid(row=2, column=0, sticky="NSEW", pady=self.view.pady, padx=(0, self.view.padx)) + self.strategy_frame = customtkinter.CTkFrame(self) + self.strategy_frame.grid(row=2, column=1, sticky="NSEW", pady=self.view.pady, padx=(self.view.padx, 0)) + self.laptimes_frame = customtkinter.CTkFrame(self) self.laptimes_frame.grid(row=2, column=1, sticky="NSEW", pady=self.view.pady, padx=(self.view.padx, 0)) @@ -88,6 +96,7 @@ def setup_labels(self): self.commentary_label = customtkinter.CTkLabel(self.commentary_frame, text="") self.commentary_label.grid(row=1, column=1, padx=self.view.padx, pady=self.view.pady, sticky="EW") + # driver labels customtkinter.CTkLabel(self.driver1_frame, text="Rosberg").grid(row=1, column=1, sticky="EW") customtkinter.CTkLabel(self.driver2_frame, text="Schumacher").grid(row=1, column=1, sticky="EW") @@ -106,6 +115,9 @@ def setup_buttons(self): self.lap_times_button = customtkinter.CTkButton(self.button_frame, text="Lap Times", command=lambda window="laptimes": self.show_window(window), image=self.view.stopwatch_icon2, anchor="w") self.lap_times_button.grid(row=1, column=0, sticky="EW", padx=self.view.padx, pady=self.view.pady) + self.strategy_button = customtkinter.CTkButton(self.button_frame, text="Strategy", command=lambda window="strategy": self.show_window(window), image=self.view.pit_icon2, anchor="w") + self.strategy_button.grid(row=2, column=0, sticky="EW", padx=self.view.padx, pady=self.view.pady) + self.start_btn = customtkinter.CTkButton(self.button_frame, text="Start Race", command=self.start_race, image=self.view.play_icon2, anchor="w") self.start_btn.grid(row=10, column=0, padx=self.view.padx, pady=self.view.pady, sticky="SEW") @@ -127,6 +139,20 @@ def setup_widgets(self): self.driver2_laptime_combo.grid(row=1, column=3, sticky="EW", padx=(5, 5), pady=self.view.pady) self.driver2_laptime_combo.set("Michael Schumacher") + # STRATEGY CHECKBOXES + self.driver1_pit1_var = customtkinter.StringVar(value="on") + self.driver1_pit2_var = customtkinter.StringVar(value="off") + self.driver1_pit3_var = customtkinter.StringVar(value="off") + self.driver1_pit_vars = [self.driver1_pit1_var, self.driver1_pit2_var, self.driver1_pit3_var] + + # STRATEGY EDITORS + self.strategy_editor_driver1 = strategy_editor.StrategyEditor(self.strategy_frame, self.view, "Rosberg", 1, 1) + + # hack to create a seperator + customtkinter.CTkFrame(self.strategy_frame, width=10).grid(column=6, row=1, rowspan=20, padx=(100, 10), sticky="NSEW") + self.strategy_editor_driver2 = strategy_editor.StrategyEditor(self.strategy_frame, self.view, "Schumacher", 7, 1) + customtkinter.CTkFrame(self.strategy_frame, width=10).grid(column=12, row=1, rowspan=20, padx=50, sticky="NSEW") + def setup_plots(self): # LAPTIMES self.laptimes_figure = Figure(figsize=(5,5), dpi=100) @@ -163,6 +189,8 @@ def show_window(self, window): self.timing_frame.tkraise() elif window == "laptimes": self.laptimes_frame.tkraise() + elif window == "strategy": + self.strategy_frame.tkraise() def update_laptimes_plot(self, event=None): self.laptimes_axis.cla() @@ -186,4 +214,6 @@ def update_laptimes_plot(self, event=None): self.laptimes_axis.set_yticks(default_y_ticks) self.laptimes_axis.set_yticklabels([self.view.milliseconds_to_minutes_seconds(t) for t in default_y_ticks]) - self.laptimes_canvas.draw() \ No newline at end of file + self.laptimes_canvas.draw() + +