''' Created on Jun 13, 2016, Finished 6-15-16 (so, about two working days) Copyright Brett Paufler Simple Tk implementation of Proximity Fuse (a Proximity clone) Note: This might be faster if the reveal value (number of id_nums_of_neighbors) was figured at the start of the game Also, if had to do over, would only have one constant for size of field and dynamically determine width, height, and field_size from that I coded this under the influence of Extreme Programming In truth, it's probably not Extreme enough, in that the functions are not small enough. Also, it because less Extreme as I went along Refactoring to the Extreme Ideal being a wonderful goal but as the end nears, why bother? Finally, prior to posting I added more comments Maybe I can't help myself Maybe they were needed all along Plays like Mine Sweeper Avoid the @ Signs, Clear the Board New Game: starts a new game based on current selectors Lvl: level, how many @ or mines, 1, 2, or 5 times the size of field Size: 10, 15, 20, board is square so 10 is 10x10 board ''' from Tkinter import Frame, Tk, Button, Label, Grid, IntVar, Radiobutton from Tkconstants import SUNKEN, ACTIVE, RAISED, DISABLED, N, S, E, W from random import shuffle #Could be a namedtuple, but I am 'off' those at the moment, classes are easier class GameSquare(): '''One for each game square.''' def __init__(self, num, button): self.num = num self.button = button self.unplayed = True self.is_mined = False self.freeze = False class Proximity(Frame): '''The game application.''' def __init__(self, master): Frame.__init__(self, master) self.windows_background_color = self.cget('bg') self.grid(row=0, column=0, sticky=N+S+E+W) #required for dynamic resizing self.game_squares = [] self.game_over = False self.num_mines = 10 self.width = 10 self.height = 10 self.field_size = self.width * self.height self.create_widgets() self.reset_game() def debug_squares(self): '''This was nice during development as it pushed a number to screen. I just refactored this to use the Square object, So it well may be broken. It certainly isn't called anywhere. Strictly for debugging.''' for square in self.game_squares: square.button['text'] = str(square.num) def get_square_by_num(self, num): square = [square for square in self.game_squares if square.num == num] square = square.pop() return square def create_gamefield_buttons(self): '''Main play area buttons. Personally, I really like blank lines in my code. It adds clarity to me.''' for num in range(1, 1 + self.field_size): row = (2 * self.width + num - 1) / self.width col = (num - 1) % self.width button = Button(self, text='', height=1, width=3) button.grid(row=row, column=col, sticky=N+S+E+W) game_square = GameSquare(num, button) button['command'] = self.press_game_square(game_square) button['text'] = str(num) button.bind('', self.press_game_square_right(game_square)) self.game_squares.append(game_square) #The values for these two are 'left over' from the previous for loop for r in range(row + 1): Grid.rowconfigure(self, r, weight=1) for c in range(col + 1): Grid.columnconfigure(self, c, weight=1) def creat_new_game_button(self): '''Just like it sounds, the reset button creation.''' #Forget why I needed this check, probably for field size changes if getattr(self, 'NEW', None): self.NEW.destroy() span = self.width * 3 / 10 col = self.width - span self.NEW = Button(self, text=' New Game ') self.NEW.grid(row=0, column=col, columnspan=span, sticky=N+S+E+W) self.NEW['command'] = self.reset_game def create_static_widgets(self): #Labels for following self.diff_text = Label(self, text='Lvl: ') self.diff_text.grid(row=0, column=0, columnspan=1, sticky=N+S+E+W) self.size_text = Label(self, text='Size:') self.size_text.grid(row=1, column=0, columnspan=1, sticky=N+S+E+W) #Field Size Radio Buttons self.radio_field_size = IntVar() self.size_1 = Radiobutton(self, text='10', variable=self.radio_field_size, value=10) self.size_1.grid(row=1, column=1, columnspan=2, sticky=N+S+E+W) self.size_2 = Radiobutton(self, text='15', variable=self.radio_field_size, value=15) self.size_2.grid(row=1, column=3, columnspan=2, sticky=N+S+E+W) self.size_3 = Radiobutton(self, text='20', variable=self.radio_field_size, value=20) self.size_3.grid(row=1, column=5, columnspan=2, sticky=N+S+E+W) self.size_1.select() #Level Radio Buttons self.radio_num_mines = IntVar() self.mines_1 = Radiobutton(self, text='1x', variable=self.radio_num_mines, value=1) self.mines_1.grid(row=0, column=1, columnspan=2, sticky=N+S+E+W) self.mines_2 = Radiobutton(self, text='2x', variable=self.radio_num_mines, value=2) self.mines_2.grid(row=0, column=3, columnspan=2, sticky=N+S+E+W) self.mines_3 = Radiobutton(self, text='5x', variable=self.radio_num_mines, value=5) self.mines_3.grid(row=0, column=5, columnspan=2, sticky=N+S+E+W) self.mines_1.select() def create_widgets(self): '''Master widget creation. I'm not going to refactor this, but if writing today, I'd follow this function with the functions this one calls. It reads so much faster that way. This sort of 'Control' method is at the essense of Extreme Programming''' self.create_static_widgets() self.create_gamefield_buttons() self.creat_new_game_button() def place_mines(self): indexes = range(self.field_size) shuffle(indexes) indexes = indexes[:self.num_mines] for i in indexes: square = self.game_squares[i] square.is_mined = True def reset_game_squares(self): for square in self.game_squares: square.unplayed = True square.freeze = False square.is_mined = False square.button.config(relief=RAISED) square.button.config(state=ACTIVE) square.button['text'] = '' square.button['bg'] = self.windows_background_color def update_field_size(self): self.width = self.radio_field_size.get() self.height = self.radio_field_size.get() self.field_size = self.width * self.height def update_num_mines(self): multiplier = self.radio_num_mines.get() self.num_mines = self.width * multiplier def redraw_field(self): '''Redraws the field on size change. As before, I'd list the functions called in this after rather than before. It reads so much faster.''' for square in self.game_squares: square.button.grid_forget() del square del self.game_squares[:] self.game_squares = [] self.create_gamefield_buttons() self.creat_new_game_button() def reset_game(self): old_field_size = self.field_size self.update_field_size() if old_field_size != self.field_size: self.redraw_field() self.update_num_mines() self.reset_game_squares() self.game_over = False self.place_mines() def id_nums_of_neighbors(self, square): '''Finds a square's neighbors. Currently, I'm reading a Roulette walk thru, and they create a 'bin' class to hold this information. Certainly, identifying 'possible moves' is important to this sort of game, even if those 'possible moves' mostly dissapear into automatic reveals in this game. Yeah, so, my code comments, not only about code. Oddly, I write a lot of my webpage into image captions. So, as time goes on, I will, perhaps, do more and more of that with code. Who knows?''' center = square.num row_expansion = [-1, 0, +1] col_expansion = [-self.width, 0, +self.width] if center % self.width == 1: row_expansion.remove(-1) elif center % self.width == 0: row_expansion.remove(+1) if center <= self.width: col_expansion.remove(-self.width) elif center >= self.field_size - self.width: col_expansion.remove(+self.width) middle_row = [center + num for num in row_expansion] grid = [r + c for c in col_expansion for r in middle_row] grid.remove(center) return grid #I'll double space here, due to the spaces in method above def neighbors_button_not_sunken(self, square): '''Is it unplayed and a neighor? If so, return it.''' unplayed = [neighbor for neighbor in self.id_nums_of_neighbors(square) if self.get_square_by_num(neighbor).button['state'] != SUNKEN] return unplayed def number_of_neighbors_mined(self, square): '''Yeah, see, changing the name to proximity, a late change. Can't think of a better word for 'mined' at the moment. Also, can't see how the name doesn't say it all.''' factors_edges_only = self.id_nums_of_neighbors(square) neighbors_mined = [1 for neighbor in factors_edges_only if self.get_square_by_num(neighbor).is_mined == True] num_neighbors_mined = sum(neighbors_mined) return num_neighbors_mined def press_game_square(self, square): '''Wrapper required because button creation uses a loop, and need to save the current variable in the wrapper.''' def wrapper(): if square.unplayed and not self.game_over: square.unplayed = False square.button.config(relief=SUNKEN) square.button.config(state=DISABLED) self.play_square(square) #the next method return wrapper #Note, double space above, method with blank lines ahead def play_square(self, square): '''Game square is pressed and calls this. Do we need to know anything else? Turns out the wrapper wasn't required. I should have used bind instead of command on the button.''' if square.is_mined: square.button['text'] = "@" square.button['bg'] = 'red' else: num = self.number_of_neighbors_mined(square) if num: square.button['text'] = str(num) else: for neighbor in self.neighbors_button_not_sunken(square): next_square = self.get_square_by_num(neighbor) button_press = self.press_game_square(next_square) button_press() if self.is_game_over(): self.game_over_mechanics() ########################################################################### # Is the Game Over Check # I refactored this, it's just easier than reading 'backwards' ########################################################################### def is_game_over(self): game_over = self.has_mine_been_revealed() or self.only_mines_remain() return game_over def has_mine_been_revealed(self): mine_hits = [square for square in self.game_squares if square.button['bg'] == 'red'] hit = any(mine_hits) return hit def only_mines_remain(self): only_mines_remain = (self.num_mines == self.field_size - self.num_explored_spaces()) return only_mines_remain def num_explored_spaces(self): squares_revealed = [square for square in self.game_squares if square.button['relief'] ==SUNKEN] num = len(squares_revealed) return num ########################################################################### # Game Over Mechanics ########################################################################### def game_over_mechanics(self): self.freeze_all_buttons() self.reveal_all_mines() self.game_over = True def freeze_all_buttons(self): for square in self.game_squares: square.button['state'] = DISABLED def reveal_all_mines(self): for square in self.game_squares: if square.is_mined == True: square.button['foreground'] = 'red' square.button['text'] = '@' ########################################################################### # Seriously, with the above all in order # I felt no need to comment it # seemed, self evident to me # # Of course, these bars are some pretty heavy commenting in themselves # risk_it_logic is completely commented in this fashion # so I won't bother to polish this up ########################################################################### def press_game_square_right(self, square): '''Disables and marks square yellow, second push reverses.''' def wrapper(unknown_passed_arg): if square.unplayed and not self.game_over: if square.freeze: square.freeze = False square.button['text'] = '' square.button['bg'] = self.windows_background_color square.button.config(state=ACTIVE) else: square.freeze = True square.button['text'] = 'X' square.button['bg'] = 'yellow' square.button.config(state=DISABLED) return wrapper #Launch Proximity def create_main_window(): main_window = Tk() main_window.wm_title('Proximity') main_window.geometry('+500+200') main_window.iconbitmap(default='p_net.ico') #Code required for auto resizing, needed at every layer Grid.columnconfigure(main_window, 0, weight=1) Grid.rowconfigure(main_window, 0, weight=1) return main_window def launch_mine_sweeper(): '''Essentially main, runs the game.''' main_window = create_main_window() play_mine_sweeper = Proximity(master=main_window) play_mine_sweeper.mainloop() if __name__ == '__main__': '''Somehow, I feel the need to include some sort of Easter Egg here. Well, more like witty commentary, not hidden code. But nothing comes to mind. In the restructuring above, putting a function definition after it's called (for those called once) or in it's own section (misc, helper function) for others I wanted to put this if __name__ == '__main__': at the top of the module, but that doesn't work. So, classes are parsed 'whole-istically', but individual expressions or statements are not. Or in other words, I can pull the GameSquare class definition below the Proximity class even though GameSquare is used in Proximity. Anyhow, enough. This is every bit as fun to me to play as the real. ''' launch_mine_sweeper()