I believe there is no need for much introduction to this game.

Any Microsoft Window’s users would be very familiar with this famous game.

I just wanted to try out how it would be, and what I would have learnt if I were to make this out from python + pyqt4.

And indeed, I’ve really gained a lot in the process of writing this game.

[ad code=1]

[ad code=1]

I won’t go thru everything here, but will only pick a few of the more interesting thing to share it over here.

[ad code=1]

[ad code=1]

Laying out the …. Layout

    self.button = []
    for row in range(16):
      self.button.append( [] )
      for col in range(16):
        self.button[row].append( MyButton() )
        self.button[row][col].setFixedSize(20,20)
        self.button[row][col].row = row
        self.button[row][col].col = col
        self.button[row][col].parent = self
        self.button[row][col].revealed = False
        layout.addWidget( self.button[row][col], row, col)
        QtCore.QObject.connect(self.button[row][col], QtCore.SIGNAL("clicked()"), self.revealButtonWrapper )
  • I’m here fixing the land mines to a 16 x 16 layout.
  • Each box is a customized QPushButton. We need a customized QPushButton(MyButton in line 05) because we would like the buttons to react to a right-mouse-click, which the original button doesn’t give the flexibility of doing it. More to this later.

[ad code=1]

[ad code=1]

Customizing Our Own QPushButton

class MyButton(QtGui.QPushButton):
  def __init__(self, parent=None):
    super(MyButton, self).__init__(parent)
  def contextMenuEvent(self, event):
    if self.text() == "?":
      self.setText("")
      self.setPalette(QtGui.QPalette(QtGui.QColor(222,222,222)))
      self.parent.marked -= 1
    else:
      self.setText("?")
      self.setPalette(QtGui.QPalette(QtGui.QColor(222,255,222)))
      self.parent.marked += 1
    self.parent.bombcount.setText("%d/40" %self.parent.marked)
  • [line 01] We create a new class call MyButton, inheriting the original QtGui.QPushButton class,
  • [line 02-03] The standard stuff, instantiating the original QPushButton Object into out current class.
  • [line 04] Now this is the interesting part. contextMenuEvent is a built-in event handler which is called whenever a right-mouse-click event happens.
    • The reason why we can’t use this contextMenuEvent in the main class is because, we want to capture the right-mouse-click event which happens inside each of the buttons instead of the parent widget
    • if we were to incorperate this event handler in the main class, we wouldn’t be able to track which exact button is being right clicked.
  • [line 05-12] This if-else loop checks that, if the right-click event happens on an empty box, then it will display a “?” on the box, else, it will remove the existing “?” text from the box.

[ad code=1]

[ad code=1]

Expanding/Hiding the score frame without affecting the main layout

    self.scoreframe.setVisible(False)
    mainLayout = QtGui.QGridLayout()
    mainLayout.addLayout(layouttop,0,0)
    mainLayout.addLayout(layout,1,0)
    mainLayout.addWidget(self.scoreframe,0,1,2,1)
    self.setLayout(mainLayout)
    self.layout().setSizeConstraint(QtGui.QLayout.SetFixedSize)
    QtCore.QObject.connect(newgameButton, QtCore.SIGNAL("clicked()"), self.newGame)
    QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.updateTime)
    QtCore.QObject.connect(topscore, QtCore.SIGNAL("clicked(bool)"), self.scoreframe.setVisible )
  • This is the 2nd thing that I’ve learnt from the process of writing this game.
  • [line 10] We are creating a button call “topscore”. Every time it is being clicked, we will expand the scoreboard so it is visible, clicked again, the scoreboard will contract and be hidden.
  • [line 01] First, lets set the scoreframe to invisible.
  • [line 07] Here, we set the main widget’s layout’s size to ‘SetFixedSize’ modal. This will make the widget to expand/contact as needed ….automagically, so that whenever we set the scoreboard to visible, the widget will expand to accomodate for it, and when the scoreboard is set to invisible. the main widget will in turn contract its size according.

[ad code=1]

[ad code=1]

These are the main 2 things that I’ve picked up this time.

The rest of the parts of the program are just pure logical things that is easily achievable, and to name a few (without spoiling the fun so that you can figure it yourself), here are those few useful methods that completes the program:-

def grepSurroundingBombCount(self, row, col):
def grepSurroundingLocation(self, row, col):

[ad code=1]

[ad code=1]

As for those that doesn’t like so much challenges, then here’s the entire code.

Just click to reveal it, and you are free to copy and use it anyway you like.

Enjoy :)

#!/usr/bin/python
"""
Mine Sweeper Game
"""

import optparse
import sys
import os
import re
import subprocess
import random
from PyQt4 import QtCore, QtGui

#-----------------------------------------------------
#-----------------------------------------------------
#-----------------------------------------------------

class MyButton(QtGui.QPushButton):
  def __init__(self, parent=None):
    super(MyButton, self).__init__(parent)
  def contextMenuEvent(self, event):
    if self.text() == "?":
      self.setText("")
      self.setPalette(QtGui.QPalette(QtGui.QColor(222,222,222)))
      self.parent.marked -= 1
    else:
      self.setText("?")
      self.setPalette(QtGui.QPalette(QtGui.QColor(222,255,222)))
      self.parent.marked += 1
    self.parent.bombcount.setText("%d/40" %self.parent.marked)

#-----------------------------------------------------
#-----------------------------------------------------
#-----------------------------------------------------
class MainWindow(QtGui.QWidget):
  """ Main Wrapper For GUI """
  def __init__(self, parent=None):
    super(MainWindow, self).__init__(parent)
    setting = QtCore.QSettings()
    tmp = setting.value("position", QtCore.QPoint(0,0))
    self.move(tmp)
    layout = QtGui.QGridLayout()
    layout.setMargin(1)
    layout.setSpacing(1)
    self.button = []
    for rr in range(16):
      self.button.append( [] )
      for cc in range(16):
        self.button[rr].append( MyButton() )
        self.button[rr][cc].setFixedSize(20,20)
        self.button[rr][cc].row = r
        self.button[rr][cc].col = c
        self.button[rr][cc].parent = self
        self.button[rr][cc].revealed = False
        layout.addWidget( self.button[rr][cc], rr, cc)
        QtCore.QObject.connect(self.button[rr][cc], QtCore.SIGNAL("clicked()"), self.revealButtonWrapper )
    layouttop = QtGui.QHBoxLayout()
    layouttop.addWidget(QtGui.QLabel("BombCount"))
    self.bombcount = QtGui.QLabel("0/40")
    layouttop.addWidget(self.bombcount)
    self.timer = QtCore.QTimer()
    self.timelabel = QtGui.QLabel("0.0 secs")
    self.timelabel.setStyleSheet("background-color: rgb(0, 0, 0); color: rgb(0,255,0)")
    self.newGame()
    layouttop.addWidget(self.timelabel)
    newgameButton = QtGui.QPushButton("&NewGame")
    newgameButton.setEnabled(True)
    layouttop.addWidget(newgameButton)
    topscore = QtGui.QPushButton("Top&Score>>")
    topscore.setCheckable(True)
    layouttop.addWidget(topscore)
    self.scoreframe = QtGui.QFrame()
    self.scoreframe.setFrameStyle(QtGui.QFrame.StyledPanel|QtGui.QFrame.Sunken)
    scorelayout = QtGui.QVBoxLayout()
    scorelayout.addWidget(QtGui.QLabel("<b>Created By</b> <i>-Lionel Tan-</i> <br><br><br>" +
                                        "Email:<a href=#>yltan@altera.com</a><br>" +
                                        "Ext  :<a href=#>#6315</a> <br><br>" +
                                        "<a href=#>http://lionel.textmalaysia.com</a><br>") )
    self.scoreframe.setLayout(scorelayout)
    self.scoreframe.setVisible(False)
    mainLayout = QtGui.QGridLayout()
    mainLayout.addLayout(layouttop,0,0)
    mainLayout.addLayout(layout,1,0)
    mainLayout.addWidget(self.scoreframe,0,1,2,1)
    self.setLayout(mainLayout)
    self.layout().setSizeConstraint(QtGui.QLayout.SetFixedSize)
    QtCore.QObject.connect(newgameButton, QtCore.SIGNAL("clicked()"), self.newGame)
    QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.updateTime)
    QtCore.QObject.connect(topscore, QtCore.SIGNAL("clicked(bool)"), self.scoreframe.setVisible )

#-----------------------------------------------------
#-----------------------------------------------------
#-----------------------------------------------------
  def newGame(self):
    self.populateBombs()
    self.marked = 0
    self.gamestarted = 0
    self.timer.stop()
    self.time = QtCore.QTime(0,0,0)
    self.timelabel.setText("0.0 secs")
    self.bombcount.setText("0/40")

    for rr in range(16):
      for cc in range(16):
        self.button[rr][cc].revealed  = False
        self.button[rr][cc].setText("")
        self.button[rr][cc].setFlat(False)
        self.button[rr][cc].setEnabled(True)
        self.button[rr][cc].setPalette(QtGui.QPalette(QtGui.QColor(222,222,222)))
#-----------------------------------------------------
  def disableAll(self):
    for rr in range(16):
      for cc in range(16):
        self.button[rr][cc].setEnabled(False)
        self.button[rr][cc].setFlat(True)
#-----------------------------------------------------
  def updateTime(self):
    tmp = round(self.time.elapsed() / 1000, 1)
    self.timelabel.setText( str(tmp) + " secs" )
#-----------------------------------------------------
  def closeEvent(self, event):
    setting = QtCore.QSettings()
    setting.setValue("position", self.pos())
#-----------------------------------------------------
  def revealButtonWrapper(self):
    if self.gamestarted == 0:
      self.time.start()
      self.timer.start(100)
      self.gamestarted = 1
    button = self.sender()
    self.revealButton( button )
#-----------------------------------------------------
  def revealButton(self, button):
    if button.bomb == True:
      txt = "X"
      color = QtGui.QColor(222,0,0)
      self.timer.stop()
      self.disableAll()
    else:
      txt = str( self.grepSurroundingBombCount(button.row, button.col) )
      color = QtGui.QColor(0,0,222)
    button.revealed = True
    button.setText(txt)
    button.setFlat(True)
    button.setEnabled(False)
    button.setPalette(QtGui.QPalette(color))
    if txt == "0":
      for rr,cc in self.grepSurroundingLocation(button.row, button.col):
        if self.button[rr][cc].revealed == False:
          self.revealButton( self.button[rr][cc] )
#-----------------------------------------------------
  def grepSurroundingBombCount(self, row, col):
    count = 0
    tmp = self.grepSurroundingLocation(row,col)
    for rr,cc in self.grepSurroundingLocation(row,col):
      count = count + 1 if self.button[rr][cc].bomb == True else count
    return(count)
#-----------------------------------------------------
  def grepSurroundingLocation(self, row, col):
    """
    Grep all the surrounding cells, and
    return back the location in a list of [row,col]
    format. e.g:-
    [ [row,col], [r,c], [r,c] ....]
    """
    ret = []
    ttop = row-1
    bot = row+1
    left = col-1
    right = col+1
    ### Getting top 3 cells if available
    if top > -1:
      ret.append( [ttop, col] )      # Top
    if bot < 16:
      ret.append( [bot, col] )      # Bot
    if left > -1:
      ret.append( [row, left] )     # Left
    if right < 16:
      ret.append( [row, right] )    # Right
    if top > -1 and left > -1:
      ret.append( [ttop, left] )   # UpperLeft
    if top > -1 and right < 16:
      ret.append( [ttop, right] )  # UpperRight
    if bot < 16 and left > -1:
      ret.append( [bot, left] )   # LowerLeft
    if bot < 16 and right < 16:
      ret.append( [bot, right] )  # LowerRight
    return(ret)
#-----------------------------------------------------
  def populateBombs(self):
    bomblocation = random.sample(range(256), 40)
    x = 0
    for row in range(16):
      for col in range(16):
        self.button[row][col].bomb = True if x in bomblocation else False
        x = x + 1
#-----------------------------------------------------
#-----------------------------------------------------
#-----------------------------------------------------

### Main script
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    myapp = MainWindow()
    myapp.show()
    sys.exit(app.exec_())
#__________________________________________________________________________
#__________________________________________________________________________
    self.button = []
    for r in range(16):
      self.button.append( [] )
      for c in range(16):
        self.button[r].append( MyButton() )
        self.button[r]
[/c]

 .setFixedSize(20,20)  self.button[r]1

 .col = c  self.button[r]1

 .revealed = False  layout.addWidget( self.button[r]1

, QtCore.SIGNAL("clicked()"), self.revealButtonWrapper )

Recommended Readings:-

  26 Responses to “Minesweeper In Python GUI”

  1. I subscribed to your blog as a python noob and only flicked past your posts but this one caught my eye, great work. I’ll be sure I read all your posts from now on.

  2. too many Top in your code (ligne 181 etc.) ;-) but, nice idea to implement a classic game…

  3. Oh, yeah, this is a perfect fit for the Planet Perl Ironman feed.

  4. Not sure which version of QT, I got this error.
    Me using Python 2.6.5
    pyqt_version 4.7.2

    <<<
    Traceback (most recent call last):
    File "minesweeper-in-python-gui.py", line 204, in
    myapp = MainWindow()
    File “minesweeper-in-python-gui.py”, line 41, in __init__
    self.move(tmp)
    TypeError: arguments did not match any overloaded call:
    QWidget.move(QPoint): argument 1 has unexpected type ‘QVariant’
    QWidget.move(int, int): argument 1 has unexpected type ‘QVariant’
    >>>

  5. Very nice article. I absolutely love Python + PyQt combination. BTW if anyone is interested in how this is done in C++ + Qt take a look at source code here: http://websvn.kde.org/trunk/KDE/kdegames/kmines/

  6. Which version of PyQt are you using? If it’s a relatively modern one, then you really should use the new-style connections, they’re much nicer. Instead of

    QtCore.QObject.connect(self.button[r][c], QtCore.SIGNAL("clicked()"), self.revealButtonWrapper )
    

    you can write:

    self.button[r][c].clicked.connect(self.revealButtonWrapper)
    

  7. Michael Ang:

    Not sure which version of QT, I got this error.
    Me using Python 2.6.5
    pyqt_version 4.7.2
    <<<
    Traceback (most recent call last):
    File “minesweeper-in-python-gui.py”, line 204, in
    myapp = MainWindow()
    File “minesweeper-in-python-gui.py”, line 41, in __init__
    self.move(tmp)
    TypeError: arguments did not match any overloaded call:
    QWidget.move(QPoint): argument 1 has unexpected type ‘QVariant’
    QWidget.move(int, int): argument 1 has unexpected type ‘QVariant’
    >>>

    Yiaks ~~ >.<
    can u pls comment out [line 041] "self.move(tmp)" and give it a try again?

    • removing line 041 works,
      I received the same error, and commenting that line did the trick.
      I just found Qt and I’m falling in love rapidly :) )
      I will study your code,
      Thank you very much


  8. AndrejT:

    Very nice article. I absolutely love Python + PyQt combination. BTW if anyone is interested in how this is done in C++ + Qt take a look at source code here: http://websvn.kde.org/trunk/KDE/kdegames/kmines/

    Thanks :)


  9. PAG:

    Which version of PyQt are you using? If it’s a relatively modern one, then you really should use the new-style connections, they’re much nicer. Instead of
    QtCore.QObject.connect(self.button[r]
    [/c], QtCore.SIGNAL(“clicked()”), self.revealButtonWrapper )

    you can write:

    self.button[r][/c]
    .clicked.connect(self.revealButtonWrapper)

    Yupe i’m aware of that, but I just seems to be much more comfortable sticking to the old way … hahaha :D
    Thanks a lot of the sharing anyway :)

  10. [...] Minesweeper In Python GUI [...]

  11. [...] and PyQT The best game ever written for Microsoft Windows can now finally be duplicated using python and the PyQT widget framework. var a2a_config = a2a_config || {}; a2a_config.linkname="Minesweeper in Python [...]

  12. [...] Minesweeper In Python GUI [...]

  13. Владельцу сайта Lionel's Blog…

    Здравствуйте! Предлагаем вам поучаствовать в партнерской программе с оплатой за клики. Цена: 700р за 1000 кликов. Формат рекламы: Тизеры. Наш сайт: http://bit.ly/d4Pd5E...

  14. [...] a view of my recently minesweeper game board (link), added with the above mentioned 2 [...]

  15. [...] Minesweeper In Python GUI [...]

  16. [...] Minesweeper In Python GUI [...]

  17. [...] Minesweeper In Python GUI [...]

  18. [...] Minesweeper In Python GUI [...]

  19. [...] Minesweeper In Python GUI [...]

 Leave a Reply

(required)

(required)


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

   

Visitors Since Nov 2009

© 2012 Lionel's Blog Suffusion theme by Sayontan Sinha