I recently became aware of a Tic-Tac-Toe challenge on github where the programmer is supposed to create a Tic-Tac-Toe game that let’s the player win every time. You have to code it in Python, although the people behind this challenge prefer you do it with Django. I don’t know Django very well, so I decided to try to create the game using wxPython. Yes, wxPython is primarily for creating cross-platform desktop user interfaces, not games. But for this particular subject, it actually works pretty well.
The First Steps
The first step is to try to figure out which widgets to use. For some reason, I thought I should use wx.TextCtrls at the beginning. Then I realized that that would add a lot of extra work since I would have to add all kinds of validation to keep the user from entering more than one character and also limiting the number of characters. Ugh. So I switched to using Toggle buttons, specifically the GenToggleButton from wx.lib.buttons. If you want to see the complete history of the game, you can check it out on my github repo. Anyway, for now, we’ll spend a little time looking at one of the first semi-working iterations. You can see what this version looked like in the screenshot above.
Here’s the code:
import wx import wx.lib.buttons as buttons ######################################################################## class TTTPanel(wx.Panel): """ Tic-Tac-Toe Panel object """ #---------------------------------------------------------------------- def __init__(self, parent): """ Initialize the panel """ wx.Panel.__init__(self, parent) self.toggled = False self.layoutWidgets() #---------------------------------------------------------------------- def checkWin(self): """ Check if the player won """ for button1, button2, button3 in self.methodsToWin: if button1.GetLabel() == button2.GetLabel() and \ button2.GetLabel() == button3.GetLabel() and \ button1.GetLabel() != "": print "Player wins!" button1.SetBackgroundColour("Red") button2.SetBackgroundColour("Red") button3.SetBackgroundColour("Red") self.Layout() return True #---------------------------------------------------------------------- def layoutWidgets(self): """ Create and layout the widgets """ mainSizer = wx.BoxSizer(wx.VERTICAL) self.fgSizer = wx.FlexGridSizer(rows=3, cols=3, vgap=5, hgap=5) font = wx.Font(22, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) size = (100,100) self.button1 = buttons.GenToggleButton(self, size=size) self.button2 = buttons.GenToggleButton(self, size=size) self.button3 = buttons.GenToggleButton(self, size=size) self.button4 = buttons.GenToggleButton(self, size=size) self.button5 = buttons.GenToggleButton(self, size=size) self.button6 = buttons.GenToggleButton(self, size=size) self.button7 = buttons.GenToggleButton(self, size=size) self.button8 = buttons.GenToggleButton(self, size=size) self.button9 = buttons.GenToggleButton(self, size=size) self.widgets = [self.button1, self.button2, self.button3, self.button4, self.button5, self.button6, self.button7, self.button8, self.button9] for button in self.widgets: button.SetFont(font) button.Bind(wx.EVT_BUTTON, self.onToggle) self.fgSizer.AddMany(self.widgets) mainSizer.Add(self.fgSizer, 0, wx.ALL|wx.CENTER, 5) endTurnBtn = wx.Button(self, label="End Turn") endTurnBtn.Bind(wx.EVT_BUTTON, self.onEndTurn) mainSizer.Add(endTurnBtn, 0, wx.ALL|wx.CENTER, 5) self.methodsToWin = [(self.button1, self.button2, self.button3), (self.button4, self.button5, self.button6), (self.button7, self.button8, self.button9), # vertical ways to win (self.button1, self.button4, self.button7), (self.button2, self.button5, self.button8), (self.button3, self.button6, self.button9), # diagonal ways to win (self.button1, self.button5, self.button9), (self.button3, self.button5, self.button7)] self.SetSizer(mainSizer) #---------------------------------------------------------------------- def enableUnusedButtons(self): """ Re-enable unused buttons """ for button in self.widgets: if button.GetLabel() == "": button.Enable() self.Refresh() self.Layout() #---------------------------------------------------------------------- def onEndTurn(self, event): """ Let the computer play """ # rest toggled flag state self.toggled = False # disable all played buttons for btn in self.widgets: if btn.GetLabel(): btn.Disable() computerPlays = [] for button1, button2, button3 in self.methodsToWin: if button1.GetLabel() == button2.GetLabel() and button1.GetLabel() != "": continue elif button1.GetLabel() == button3.GetLabel() and button1.GetLabel() != "": continue if button1.GetLabel() == "": computerPlays.append(button1) break if button2.GetLabel() == "": computerPlays.append(button2) break if button3.GetLabel() == "": computerPlays.append(button3) break computerPlays[0].SetLabel("O") computerPlays[0].Disable() self.enableUnusedButtons() #---------------------------------------------------------------------- def onToggle(self, event): """ On button toggle, change the label of the button pressed and disable the other buttons unless the user changes their mind """ button = event.GetEventObject() button.SetLabel("X") button_id = button.GetId() self.checkWin() if not self.toggled: self.toggled = True for btn in self.widgets: if button_id != btn.GetId(): btn.Disable() else: self.toggled = False button.SetLabel("") self.enableUnusedButtons() ######################################################################## class TTTFrame(wx.Frame): """ Tic-Tac-Toe Frame object """ #---------------------------------------------------------------------- def __init__(self): """Constructor""" title = "Tic-Tac-Toe" size = (500, 500) wx.Frame.__init__(self, parent=None, title=title, size=size) panel = TTTPanel(self) self.Show() if __name__ == "__main__": app = wx.App(False) frame = TTTFrame() app.MainLoop()
I’m not going to go over everything in this code. I will comment on it though. If you run it, you’ll notice that the computer can still win. It tries not to win and it’s really easy to beat, but occasionally it will win if you let it. You can also just hit the “End Turn” button repeatedly until it wins because it’s too dumb to realize it is skipping your turn. There’s also no way to restart the game. What’s the fun of that? So I ended up doing a bunch of work to make the computer “smarter” and I added a few different ways for the player to restart the game.
The Final Version
This is my current final version. This code is a little more complex, but I think you’ll be able to figure it out:
import random import wx import wx.lib.buttons as buttons ######################################################################## class TTTPanel(wx.Panel): """ Tic-Tac-Toe Panel object """ #---------------------------------------------------------------------- def __init__(self, parent): """ Initialize the panel """ wx.Panel.__init__(self, parent) self.toggled = False self.playerWon = False self.layoutWidgets() #---------------------------------------------------------------------- def checkWin(self, computer=False): """ Check if the player won """ for button1, button2, button3 in self.methodsToWin: if button1.GetLabel() == button2.GetLabel() and \ button2.GetLabel() == button3.GetLabel() and \ button1.GetLabel() != "": print "Player wins!" self.playerWon = True button1.SetBackgroundColour("Yellow") button2.SetBackgroundColour("Yellow") button3.SetBackgroundColour("Yellow") self.Layout() if not computer: msg = "You Won! Would you like to play again?" dlg = wx.MessageDialog(None, msg, "Winner!", wx.YES_NO | wx.ICON_WARNING) result = dlg.ShowModal() if result == wx.ID_YES: wx.CallAfter(self.restart) dlg.Destroy() break else: return True #---------------------------------------------------------------------- def layoutWidgets(self): """ Create and layout the widgets """ mainSizer = wx.BoxSizer(wx.VERTICAL) self.fgSizer = wx.FlexGridSizer(rows=3, cols=3, vgap=5, hgap=5) btnSizer = wx.BoxSizer(wx.HORIZONTAL) font = wx.Font(22, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) size = (100,100) self.button1 = buttons.GenToggleButton(self, size=size, name="btn1") self.button2 = buttons.GenToggleButton(self, size=size, name="btn2") self.button3 = buttons.GenToggleButton(self, size=size, name="btn3") self.button4 = buttons.GenToggleButton(self, size=size, name="btn4") self.button5 = buttons.GenToggleButton(self, size=size, name="btn5") self.button6 = buttons.GenToggleButton(self, size=size, name="btn6") self.button7 = buttons.GenToggleButton(self, size=size, name="btn7") self.button8 = buttons.GenToggleButton(self, size=size, name="btn8") self.button9 = buttons.GenToggleButton(self, size=size, name="btn9") self.normalBtnColour = self.button1.GetBackgroundColour() self.widgets = [self.button1, self.button2, self.button3, self.button4, self.button5, self.button6, self.button7, self.button8, self.button9] # change all the main game buttons' font and bind them to an event for button in self.widgets: button.SetFont(font) button.Bind(wx.EVT_BUTTON, self.onToggle) # add the widgets to the sizers self.fgSizer.AddMany(self.widgets) mainSizer.Add(self.fgSizer, 0, wx.ALL|wx.CENTER, 5) self.endTurnBtn = wx.Button(self, label="End Turn") self.endTurnBtn.Bind(wx.EVT_BUTTON, self.onEndTurn) self.endTurnBtn.Disable() btnSizer.Add(self.endTurnBtn, 0, wx.ALL|wx.CENTER, 5) startOverBtn = wx.Button(self, label="Restart") startOverBtn.Bind(wx.EVT_BUTTON, self.onRestart) btnSizer.Add(startOverBtn, 0, wx.ALL|wx.CENTER, 5) mainSizer.Add(btnSizer, 0, wx.CENTER) self.methodsToWin = [(self.button1, self.button2, self.button3), (self.button4, self.button5, self.button6), (self.button7, self.button8, self.button9), # vertical ways to win (self.button1, self.button4, self.button7), (self.button2, self.button5, self.button8), (self.button3, self.button6, self.button9), # diagonal ways to win (self.button1, self.button5, self.button9), (self.button3, self.button5, self.button7)] self.SetSizer(mainSizer) #---------------------------------------------------------------------- def enableUnusedButtons(self): """ Re-enable unused buttons """ for button in self.widgets: if button.GetLabel() == "": button.Enable() self.Refresh() self.Layout() #---------------------------------------------------------------------- def onEndTurn(self, event): """ Let the computer play """ # rest toggled flag state self.toggled = False # disable all played buttons for btn in self.widgets: if btn.GetLabel(): btn.Disable() computerPlays = [] noPlays = [] for button1, button2, button3 in self.methodsToWin: if button1.GetLabel() == button2.GetLabel() and button3.GetLabel() == "": if button1.GetLabel() == "" and button2.GetLabel() == "" and button1.GetLabel() == "": pass else: #if button1.GetLabel() == "O": noPlays.append(button3) elif button1.GetLabel() == button3.GetLabel() and button2.GetLabel() == "": if button1.GetLabel() == "" and button2.GetLabel() == "" and button1.GetLabel() == "": pass else: noPlays.append(button2) elif button2.GetLabel() == button3.GetLabel() and button1.GetLabel() == "": if button1.GetLabel() == "" and button2.GetLabel() == "" and button1.GetLabel() == "": pass else: noPlays.append(button1) noPlays = list(set(noPlays)) if button1.GetLabel() == "" and button1 not in noPlays: if not self.checkWin(computer=True): computerPlays.append(button1) if button2.GetLabel() == "" and button2 not in noPlays: if not self.checkWin(computer=True): computerPlays.append(button2) if button3.GetLabel() == "" and button3 not in noPlays: if not self.checkWin(computer=True): computerPlays.append(button3) computerPlays = list(set(computerPlays)) print noPlays choices = len(computerPlays) while 1 and computerPlays: btn = random.choice(computerPlays) if btn not in noPlays: print btn.GetName() btn.SetLabel("O") btn.Disable() break else: print "Removed => " + btn.GetName() computerPlays.remove(btn) if choices < 1: self.giveUp() break choices -= 1 else: # Computer cannot play without winning self.giveUp() self.endTurnBtn.Disable() self.enableUnusedButtons() #---------------------------------------------------------------------- def giveUp(self): """ The computer cannot find a way to play that lets the user win, so it gives up. """ msg = "I give up, Dave. You're too good at this game!" dlg = wx.MessageDialog(None, msg, "Game Over!", wx.YES_NO | wx.ICON_WARNING) result = dlg.ShowModal() if result == wx.ID_YES: self.restart() else: wx.CallAfter(self.GetParent().Close) dlg.Destroy() #---------------------------------------------------------------------- def onRestart(self, event): """ Calls the restart method """ self.restart() #---------------------------------------------------------------------- def onToggle(self, event): """ On button toggle, change the label of the button pressed and disable the other buttons unless the user changes their mind """ button = event.GetEventObject() button.SetLabel("X") button_id = button.GetId() self.checkWin() if not self.toggled: self.toggled = True self.endTurnBtn.Enable() for btn in self.widgets: if button_id != btn.GetId(): btn.Disable() else: self.toggled = False self.endTurnBtn.Disable() button.SetLabel("") self.enableUnusedButtons() # check if it's a "cats game" - no one's won if not self.playerWon: labels = [True if btn.GetLabel() else False for btn in self.widgets] if False not in labels: msg = "Cats Game - No one won! Would you like to play again?" dlg = wx.MessageDialog(None, msg, "Game Over!", wx.YES_NO | wx.ICON_WARNING) result = dlg.ShowModal() if result == wx.ID_YES: self.restart() dlg.Destroy() #---------------------------------------------------------------------- def restart(self): """ Restart the game and reset everything """ for button in self.widgets: button.SetLabel("") button.SetValue(False) button.SetBackgroundColour(self.normalBtnColour) self.toggled = False self.playerWon = False self.endTurnBtn.Disable() self.enableUnusedButtons() ######################################################################## class TTTFrame(wx.Frame): """ Tic-Tac-Toe Frame object """ #---------------------------------------------------------------------- def __init__(self): """Constructor""" title = "Tic-Tac-Toe" size = (500, 500) wx.Frame.__init__(self, parent=None, title=title, size=size) panel = TTTPanel(self) self.Show() if __name__ == "__main__": app = wx.App(False) frame = TTTFrame() app.MainLoop()
I just noticed that even this version has a minor bug in that it's no longer high-lighting the 3 winning buttons like it did in the previous version. However, now it takes turns correctly and if the user is about to lose either because the computer can win or no one will win, the computer just gives up and tells the player that they won. Yeah, it's kind of a cop out, but it works! Here are a few other things I think would be fun to add:
- Music
- Sound effects
- More colorful interface
- The ability for the user to choose X or O
- Different difficulty levels
- Maybe add a mode where you can make the field of play bigger than a 3x3. Instead, make it a 6x6 or some other larger amount.
Anyway, I hope you'll find this application interesting and maybe even helpful in figuring out wxPython.
Pingback: Mike Driscoll: wxPython: Creating a Simple Tic-Tac-Toe Game | The Black Velvet Room