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 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.
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:
Anyway, I hope you'll find this application interesting and maybe even helpful in figuring out wxPython.
Copyright © 2025 Mouse Vs Python | Powered by Pythonlibrary