class ChooseDifficultyUI: def __init__(self, game_ui: 'GameUI') -> None: self.view = ChooseDifficultyView( lambda event: self.update_slider_range(), lambda event: self.draw_field()) self.canvas = self.view.canvas self.game_ui = game_ui # initialise these to arbitrary values, as these are # overwritten before being used anyway self.apothem: float = 0 self.hshift: float = 0 self.view.window.protocol("WM_DELETE_WINDOW", self.on_window_close) # set default slider values to values from the previous game # this makes it easier for the user to make small adjustments # without having to remember previous game values self.view.game_size_slider.set(self.game_ui.hex_grid.size) self.last_size = self.game_ui.hex_grid.size self.view.set_mine_count_slider_upper_bound( HexGrid.highest_possible_mine_count_for_size( self.game_ui.hex_grid.size)) self.view.mine_count_slider.set(self.game_ui.hex_grid.mine_count) Button(self.view.window, text='Select this difficulty', command=self.select_difficulty_clicked).grid(row=2, column=0, columnspan=2) self.draw_field() # put self.window in the foreground and # make self.game_ui.window (the root window) inaccessible self.view.window.transient(self.game_ui.window) self.view.window.grab_set() self.game_ui.window.wait_window(self.view.window) @staticmethod def border() -> int: return 20 def update_slider_range(self) -> None: new_size = self.view.game_size_slider.get() last_mine_count = self.view.mine_count_slider.get() if new_size == self.last_size: # unchanged, no need to make adjustments return # start by setting up some variables for later last_max_mine_count = \ HexGrid.highest_possible_mine_count_for_size( self.last_size) new_max_mine_count = \ HexGrid.highest_possible_mine_count_for_size(new_size) # update mine count slider upper bound self.view.set_mine_count_slider_upper_bound(new_max_mine_count) # Calculate new suggested mine count using proportion # of mines to total mine count (here max_mine_count, which # is basically equal to total tile count). # Slider.set() is used to update current slider value. new_suggested_mine_count = round( last_mine_count / last_max_mine_count * new_max_mine_count) self.view.mine_count_slider.set(new_suggested_mine_count) # set self.last_size so it can be used next time when # the slider is updated and this event handler is called self.last_size = self.view.game_size_slider.get() # the field preview also needs to be # redrawn after all these adjustments self.draw_field() def draw_field(self) -> None: """ Redraw the preview field. Includes generating a new HexGrid. """ size = self.view.game_size_slider.get() mine_count = self.view.mine_count_slider.get() # create a new, random HexGrid on every redraw # this only causes slight lag with huge field sizes, # so it is not a major issue self.hex_grid = HexGrid(size, mine_count) # As demonstrated in hexgrid.py (at the top), # (size - 1, size - 1) always refers to the centre tile. # Using the center tile as a guaranteed empty # tile looks nice and symmetrical. self.hex_grid.try_generate_mines(size - 1, size - 1) # hidden tiles (just like in an actual game) would # make the preview worthless, so all tiles (including # mines) need to be revealed for pos in self.hex_grid.all_valid_coords(): self.hex_grid[pos].reveal() # finally draw the field, just like in the # actual game (see game_ui.py) HexGridUIUtilities.draw_field(self, self.border()) def select_difficulty_clicked(self) -> None: size = self.view.game_size_slider.get() mine_count = self.view.mine_count_slider.get() self.game_ui.hex_grid.size = size self.game_ui.hex_grid.mine_count = mine_count self.on_window_close() def on_window_close(self) -> None: self.game_ui.hex_grid.restart_game() self.game_ui.window.focus_set() self.game_ui.draw_field() self.view.close_window()
class ChooseDifficultyUI: def __init__(self, game_ui): self.game_ui = game_ui # programs can only have one window, but can # create multiple "Toplevel"s (effectively new windows) self.window = Toplevel() self.window.title('HexSweeper - Choose Difficulty') self.window.geometry('400x473') # size maximises space in the window self.window.bind('<Configure>', lambda event: self.draw_field()) self.window.protocol("WM_DELETE_WINDOW", self.on_window_close) # these statements allow the hexgrid (in col 1, row 3) # to stretch as the window is resized # stretch the second column horizontally: Grid.columnconfigure(self.window, 1, weight=1) # stretch the fourth row vertically: Grid.rowconfigure(self.window, 3, weight=1) Label(self.window, text = 'Board Size:') \ .grid(row = 0, column = 0, sticky = W) # TkInter "Scale" objects are sliders self.game_size_slider = Scale( self.window, # from_ because from is a Python keyword from_=2, to=15, orient=HORIZONTAL, command=lambda event: self.update_slider_range(event) ) # default slider resolution/accuracy is 1 self.game_size_slider.grid(row=0, column=1, sticky=E + W) Label(self.window, text = 'Number of mines:') \ .grid(row = 1, column = 0, sticky = W) self.mine_count_slider = Scale(self.window, from_=1, to=315, orient=HORIZONTAL, command=lambda event: self.draw_field()) self.mine_count_slider.grid(row=1, column=1, sticky=E + W) # set default slider values to values from the previous game # this makes it easier for the user to make small adjustments # without having to remember previous game values self.game_size_slider.set(self.game_ui.hex_grid.size) self.last_size = self.game_ui.hex_grid.size self.mine_count_slider.config( to=HexGrid.highest_possible_mine_count_for_size( self.game_ui.hex_grid.size)) self.mine_count_slider.set(self.game_ui.hex_grid.mine_count) Button(self.window, text='Select difficulty', command=self.select_difficulty_clicked).grid(row=2, column=0, columnspan=2) self.canvas = Canvas(self.window, bg='white') self.canvas.grid( row=3, column=0, # span columns 0 and 1 columnspan=2, # resize with the window sticky=E + N + W + S) self.border = 5 self.draw_field() # put self.window in the foreground and # make self.game_ui.window (the root window) inaccessible self.window.transient(self.game_ui.window) self.window.grab_set() self.game_ui.window.wait_window(self.window) def update_slider_range(self, event): """ When the board size slider is adjusted, the mine count slider is readjusted to maintain the same ratio of empty tiles to tiles with a mine. As this the ratio involved the total tile count instead of the board size, it looks like the slider value shifts along slightly (try it out to see this), but that is only due to the quadratic relationship between board size and tile count (specifically, tile count = 3s^2 - 3s + 1). The maximum value for the mine count slider is also adjusted based on HexGrid.highest_possible_mine_count_for_size(). """ new_size = self.game_size_slider.get() last_mine_count = self.mine_count_slider.get() if new_size == self.last_size: # unchanged, no need to make adjustments return # start by setting up some variables for later last_max_mine_count = \ HexGrid.highest_possible_mine_count_for_size(self.last_size) new_max_mine_count = \ HexGrid.highest_possible_mine_count_for_size(new_size) # update mine count slider upper bound self.mine_count_slider.config(to=new_max_mine_count) # Calculate new suggested mine count using proportion # of mines to total mine count (here max_mine_count, which # is basically equal to total tile count). # Slider.set() is used to update current slider value. new_suggested_mine_count = round( last_mine_count / last_max_mine_count * new_max_mine_count) self.mine_count_slider.set(new_suggested_mine_count) # set self.last_size so it can be used next time when # the slider is updated and this event handler is called self.last_size = self.game_size_slider.get() # the field preview also needs to be # redrawn after all these adjustments self.draw_field() def draw_field(self): size = self.game_size_slider.get() mine_count = self.mine_count_slider.get() # create a new, random HexGrid on every redraw # this only causes slight lag with huge field sizes, # so it is not a major issue self.hex_grid = HexGrid(size, mine_count) # As demonstrated in hexgrid.py (at the top), # (size - 1, size - 1) always refers to the centre tile. # Using the center tile as a guaranteed empty # tile looks nice and symmetrical. self.hex_grid.try_generate_mines(size - 1, size - 1) # hidden tiles (just like in an actual game) would # make the preview worthless, so all tiles (including # mines) need to be revealed for pos in self.hex_grid.all_valid_coords(): self.hex_grid[pos].reveal() # finally draw the field, just like in the actual game (see game_ui.py) HexGridUIUtilities.draw_field(self) def select_difficulty_clicked(self): """ Called when the user clicks on the "Select difficulty" button. Selected board size and mine count are saved in the game_ui.hex_grid object and the window on_close event handler is called, which takes care of closing the window, restoring focus to the actual game and redrawing the game. """ size = self.game_size_slider.get() mine_count = self.mine_count_slider.get() self.game_ui.hex_grid.size = size self.game_ui.hex_grid.mine_count = mine_count self.on_window_close() def on_window_close(self): """ Called when the user either closes the window using the red 'X' or when the user clicks on the "Select difficulty" button. Because we don't know how the user got to this method, we can't save the board size/mine count options (if required, they will have already been saved). So we just restart and redraw the main game and close the window. """ self.game_ui.hex_grid.restart_game() self.game_ui.window.focus_set() self.game_ui.draw_field() self.window.destroy()