''' Copyright Brett Paufler started 6-8-16, finished 6-12-16 (for the most) Tic-Tac-Toe Basic Implementation Easy Game = Random Hard Game = Perfect Logic To test, use 'Play 100' button in lower left. On hard, this should yield all draws For the most, this is written in 'Extreme Programming' style which basically means as many comments as possible have been omitted and replaced by meaningful variable names Of course, I'm recommenting it all prior to posting, but... ''' from Tkinter import Frame, Tk, Button, Label from Tkconstants import BOTH, YES from tkFont import Font from random import choice class GameSquare(): '''One for each square in the 3x3 tic-tac-toe grid, laid out thusly: 1 2 3 4 5 6 7 8 9 ''' def __init__(self, num, tk_button): self.num = num self.tk_button = tk_button class TicTacToe(Frame): '''Game Turn Mechanics and everything inside the Main Holding Window.''' difficulty = 'easy' current_player = 'X' game_over = False human_player = True #False and the computer plays itself, set by a button game_outcome_win_x = 0 #these are running counts game_outcome_win_o = 0 game_outcome_draw = 0 def __init__(self, master): Frame.__init__(self, master) self.pack(fill=BOTH, expand=YES) self.game_squares = [] self.create_all_widgets() def score_string_x(self): '''Formatting for running total score as appears in lower left.''' text = 'X: {: <4} Y: {: <4} D: {: <4}'.format( self.game_outcome_win_x, self.game_outcome_win_o, self.game_outcome_draw) return text def game_square_by_num(self, num): '''Returns the game square given it's number (1-9).''' game_squares = [game_square for game_square in self.game_squares if game_square.num == num] game_square = game_squares.pop() return game_square def toggle_current_turn_X_or_O(self): '''Switches player from X to O and vice versa.''' self.current_player = 'O' if self.current_player == 'X' else 'X' def end_of_turn_mechanics(self): if self.is_game_over(): self.game_over_mechanics() self.toggle_current_turn_X_or_O() def play_turn_player(self, tk_button): '''Needed a wrapper so auto created game_square list attached appropriate command to each tk_button. This is actually complete crap. I was using 'command', should have used 'bind'.''' def wrapper(): if self.game_over: return if tk_button['text'] == '': tk_button['text'] = self.current_player self.end_of_turn_mechanics() if not self.game_over: self.play_turn_computer() return wrapper def play_turn_computer(self): '''Simple, in order, doesn't really need a comment, I hope.''' logic = self.logic_handle(self.current_player) num_chosen = logic.play_turn(self.difficulty) button_chosen = self.game_square_by_num(num_chosen) button_chosen.tk_button['text'] = self.current_player self.end_of_turn_mechanics() def computer_plays_self(self, number_of_games): '''Another wrapper to provide optional multi-game functionality.''' def wrapper(): for _ in range(number_of_games): if self.game_over == True: self.reset_game() while not self.game_over: self.play_turn_computer() return wrapper def list_of_squares_empty(self): '''The game state was saved in the button's text field, so '' buttons haven't been played, 'X' is X, 'O' is O.''' empty_squares = [game_square for game_square in self.game_squares if game_square.tk_button['text'] == ''] return empty_squares def list_of_squares_X(self): squares_X = [game_square for game_square in self.game_squares if game_square.tk_button['text'] == 'X'] return squares_X def list_of_squares_O(self): squares_O = [button for button in self.game_squares if button.tk_button['text'] == 'O'] return squares_O def square_id_nums_empty(self): '''Returns list of ID nums for empty (playable) squares.''' game_square_nums = [game_square.num for game_square in self.list_of_squares_empty()] return game_square_nums def square_id_nums_containing_X(self): game_square_nums = [game_square.num for game_square in self.list_of_squares_X()] return game_square_nums def square_id_nums_containing_O(self): game_square_nums = [game_square.num for game_square in self.list_of_squares_O()] return game_square_nums def mark_winners_loosers(self, winning_nums): '''Called at end of game, so the win is more obvious.''' #whites out all squares for game_square in self.game_squares: game_square.tk_button['fg'] = 'white' #colors winners back in with red for num in winning_nums: game_square = self.game_square_by_num(num) game_square.tk_button['fg'] = 'red' def is_game_over(self): logic = self.logic_handle(player=self.current_player) game_over = (logic.is_winner() or logic.game_is_a_draw()) return game_over def update_scores(self): logic = self.logic_handle(player=self.current_player) if logic.is_winner(): if self.current_player == 'X': self.game_outcome_win_x += 1 else: self.game_outcome_win_o += 1 else: self.game_outcome_draw += 1 self.SCORE_X['text'] = self.score_string_x() def game_over_mechanics(self): self.game_over = True self.update_scores() logic = self.logic_handle(player=self.current_player) self.mark_winners_loosers(winning_nums=logic.maybe_winning_combo()) def reset_game(self): for game_square in self.game_squares: game_square.tk_button['text'] = '' game_square.tk_button['fg'] = 'black' self.current_player = 'X' self.game_over = False def on_press_button_easy(self): '''Not only did I not know how to expand the field correctly, in this instance, I used two buttons (this and next), when I should have been using a radio button.''' self.EASY['fg'] = 'blue' self.HARD['fg'] = 'black' self.difficulty = 'easy' def on_press_button_hard(self): self.EASY['fg'] = 'black' self.HARD['fg'] = 'blue' self.difficulty = 'hard' # Above plays the game # Below creats the board def create_all_widgets(self): '''Buttons and Labels are created one after the next. A long function, but very sequential. Nothing complicated here. I felt the need to qualify 'a long function' as I was trying out that Extreme Programming thing.''' #Top Row: new, easy, hard self.NEW = Button(self, text='New Game', width=15,) self.NEW.grid(row=0, column=0) self.NEW['command'] = self.reset_game self.EASY = Button(self, text='Easy', width=15, fg='blue') self.EASY.grid(row=0, column=1) self.HARD = Button(self, text='Hard', width=15,) self.HARD.grid(row=0, column=2) self.EASY['command'] = self.on_press_button_easy self.HARD['command'] = self.on_press_button_hard if self.difficulty == 'hard': self.on_press_button_hard() #Create the Game Grid 3x3 for num in range(1, 10): tk_button = Button(self, text='', height=1, width=3) tk_button['command'] = self.play_turn_player(tk_button) tk_button['font'] = Font(size=50) row, col = divmod(num + 2, 3) tk_button.grid(row=row, column=col) button = GameSquare(num, tk_button) self.game_squares.append(button) #Bottom row: score x, play self, score y self.SCORE_X = Label(self) self.SCORE_X['text'] = self.score_string_x() self.SCORE_X.grid(row=4, column=0) self.SELF_PLAY_ONE = Button(self, text='Self Play', width=15,) self.SELF_PLAY_ONE.grid(row=4, column=1) self.SELF_PLAY_ONE['command'] = self.computer_plays_self(number_of_games=1) self.SELF_PLAY_MANY = Button(self, text='Play 100', width=15,) self.SELF_PLAY_MANY.grid(row=4, column=2) self.SELF_PLAY_MANY['command'] = self.computer_plays_self(number_of_games=100) def logic_handle(self, player='X'): '''Creates a new Logic() instance for the current game state. Even if a new instance wasn't created at each iteration, values would still need to be updated.''' us = self.square_id_nums_containing_X() them = self.square_id_nums_containing_O() if player == 'O': us, them = them, us logic = Logic(squares_us=us, squares_them=them, squares_empty=self.square_id_nums_empty() ) return logic # The following two launch the Game # I would reverse the ordering of these two now # defining functions after they are first called # so program reads in order def create_main_window(): main_window = Tk() main_window.wm_title('Tic Tac Toe') main_window.geometry('+500+200') main_window.iconbitmap(default='p_net.ico') return main_window def launch_tic_tac_toe(): '''Essentially main, runs the game.''' main_window = create_main_window() play_tic_tac_toe = TicTacToe(master=main_window) play_tic_tac_toe.mainloop() class Logic(): '''Contains all Tic-Tac-Toe game-play logic. game cells are identified by number, thusly: 1 2 3 4 5 6 7 8 9 ''' #It would be possible to dynamically generate these combos, # but not the worth effort winning_combos = [(1, 2, 3), (4, 5, 6), (7, 8, 9), #across (1, 4, 7), (2, 5, 8), (3, 6, 9), #down (1, 5, 9), (3, 5, 7) #diagonally ] corners = [1, 3, 7, 9] center = [5] diagonal_corners = [(1, 9), (3, 7)] def __init__(self, squares_us, squares_them, squares_empty): #squares a player controls self.us = squares_us self.them = squares_them self.empty = squares_empty #Tic Tac Toe only has 9 playing squares no matter what... assert 9 == len(self.us + self.them + self.empty) #blocking means a player has one or more squares in a winning_combination #these essentially memonize the sets self.blocking_us = set(self.combos_blocked(self.us)) self.blocking_them = set(self.combos_blocked(self.them)) self.blocking_empty = set(self.combos_blocked(self.empty)) def players_opponent(self, player): '''Not the player who is playing, but the player's opponent. Now, who is that again?''' if player == self.us: opponent = self.them else: opponent = self.us return opponent def players_blocks(self, player): '''A convenience function for the most.''' if player == self.us: blocks = self.blocking_us else: blocks = self.blocking_them return blocks def opponents_blocks(self, player): '''If a 'block' still isn't clear, a winning combination is 'blocked' if a player controls one or more squares in that combination. By having control of at least one square, the opponent is prevented from winning. They are 'blocked'.''' player = self.players_opponent(player) blocks = self.players_blocks(player) return blocks def combo_weighted_list(self, player): '''Returns a combo once for each square player controls. So if player controls 1, 2 then (1, 2, 3) is returned twice. One copy of every combo 1 is in plus one copy of every combo 2 is in.''' player_weighted_combos = [combo for combo in self.winning_combos for sqr in player if sqr in combo] return player_weighted_combos def combos_doubly_represented(self, player): '''Two or more entries in the combo_weighted_list.''' doubles_only = self.combo_weighted_list(player) for first_copy in self.players_blocks(player): doubles_only.remove(first_copy) return doubles_only def combos_blocked(self, player): blocked = set(self.combo_weighted_list(player)) blocked = list(blocked) return blocked def maybe_winning_combo(self): '''Returns a three in a row combination, if any for current player. All maybes return None or a Value. I was really happy with the maybe's in this project. By the end of the sequence, I refactored to kill all None's in the code.''' maybe = self.blocking_us - self.blocking_them - self.blocking_empty maybe_winning_combo = maybe.pop() if maybe else () return maybe_winning_combo def is_winner(self): '''Does self.us have three in a row?''' is_win = bool(self.maybe_winning_combo()) return is_win def game_is_a_draw(self): '''No more empty squares, so poorly worded as this could be true even if a win.''' return not bool(self.empty) def play_turn(self, difficulty): if difficulty == 'easy': play = self.play_turn_random() else: play = self.play_turn_hard() return play def play_turn_random(self): play = choice(self.empty) return play def play_turn_hard(self): '''Not worried about the extra loops, if/then guards makes the logic needlessly complicated. All play_'s yield None or a game_square number, so maybe's essentially.''' #in order of preference play_strategy = [ self.play_first_turn(), self.play_winning_move(), self.play_block_winning_move(), self.play_fork(), self.play_block_fork(), self.play_center(), self.play_opposite_corner(), self.play_any_corner(), self.play_turn_random() ] plays = [play for play in play_strategy if play != None] play = plays.pop(0) #print play, play_strategy #yields a nice debugging string return play ''' This is likely where I decided writing the functions after they were first used made the most sense. All the 'play' maybes follow in order. It's nice and tidy if you ask me. ''' def play_first_turn(self): '''First turn can be center or corner.''' if len(self.empty) == 9: play = choice(self.center + self.corners) else: play = None return play def winning_move(self, player): '''If two in a row, returns the space that would make it three.''' maybe_winner = self.combos_doubly_represented(player) maybe_winner = [maybe for maybe in maybe_winner if maybe not in self.opponents_blocks(player)] if maybe_winner: winning_combo = maybe_winner.pop() play = [sqr for sqr in winning_combo if sqr not in player] play = play.pop() else: play = None return play def play_winning_move(self): '''If two in a row and remainder is empty, play to empty wins.''' play = self.winning_move(player=self.us) return play def play_block_winning_move(self): '''Prevents next turn win by other.''' play = self.winning_move(player=self.them) return play def forks(self, player): '''A fork is a play that results in two two-way sequences; thus, impossible to block.''' possible_combos = self.players_blocks(player) - self.opponents_blocks(player) possible_combos = list(possible_combos) possible_forking_plays = [] while possible_combos: possible_play = possible_combos.pop() forking_play = [play for play in possible_play if play not in player and any(map(lambda combos: play in combos, possible_combos))] _ = [possible_forking_plays.append(play) for play in forking_play] return possible_forking_plays def play_fork(self): '''Play a fork for next turn win.''' possible_fork = self.forks(self.us) if possible_fork: play = possible_fork.pop() else: play = None return play def play_block_fork(self): '''Block the fork the opponent could make on their next turn. The logic herein is weak, as in it's not beautiful. It works, but is not elegant.''' opponents_forking_plays = self.forks(self.them) if len(opponents_forking_plays) == 0: play = None elif len(opponents_forking_plays) == 1: play = opponents_forking_plays.pop() elif len(opponents_forking_plays) == 2: possible = [play for play in self.empty if play not in opponents_forking_plays] play = possible.pop() elif len(opponents_forking_plays) == 4: possible = [play for play in self.corners if play in self.empty] play = possible.pop() else: assert True == False return play def play_center(self): if 5 in self.empty: play = 5 #center else: play = None return play def play_opposite_corner(self): '''If opponent occupies a corner and opposite corner is empty, play it.''' possible_corner = [corner for corner in self.diagonal_corners if any(map(lambda sqr: sqr in corner, self.them)) and any(map(lambda sqr: sqr in corner, self.empty))] if possible_corner: corners = possible_corner.pop() corner = [c for c in corners if c in self.empty] play = corner.pop() else: play = None return play def play_any_corner(self): available_corners = [corner for corner in self.corners if corner in self.empty] if available_corners: play = choice(available_corners) else: play = None return play if __name__ == '__main__': '''To test, try the 'Play 100' button in lower left. On hard, this should yield all draws. So, if D is not incremented by 100, something is wrong.''' launch_tic_tac_toe()