pyFlood - logikai játék készítése színekkel

A napokban a Google Plus-on elérhető FloodIt! játék inspirálása nyomán elkészítettem saját verziómat pyFlood néven.
Nézzük át a kódját, hogy is működik...

A játékot Qt felülettel készítettem el, amihez a Pyside nevű, a Nokiások által fejlesztett és LGPL licensz alatt közreadott nyílt forrású eszközkészletet használtam fel.
Első lépésként megterveztem a felületet a Qtdesigner nevű felülettervezővel. Noha a felület teljesen egyszerű, mivel túl sok tapasztalatom nincs sem Qt felületek kialakításában, sem a Qtdesigner használatában, elbabráltam vele egy darabig.


Az ablak alapja egy Horizontal Layout, abban van egy Vertical Layout, ami a színes gombokat, a feliratokat és az indítógombokat tartalmazza, mellette meg egy Horizontal Layout, ami a játéktáblát, azaz egy QGraphicsView objektumot tartalmaz.
A színes gombokhoz nem akartam saját widgetet készíteni, ezért azokat QPushButton objektumokkal oldottam meg, amiket egy 3x2-es GridLayout-ba pakoltam. Az ui tartalmaz még egy állapotsort, meg egy menüsort (alapból), de a menüt nem használtam, így nem jelenik meg.

A következő lépés az ui-ból python modult faragni. Ezt a pyside-uic segédprogrammal tudjuk megtenni:


pyside-iuc pyflood.ui ui_pyflood.py


Ez egy python osztályt generál nekünk nagyon okosan, amelyet egyszerűen be tudunk importálni a programunkba.

A játék működése a programozás szempontjából a következő:
  1. tábla feltöltése és kirajzolása
  2. várakozás a start gomb kattintására, addig a többi gomb (a színes négyzetek) le vannak tiltva
  3. bármely színes gomb lenyomása esetén a megnyomott színnek megfelelően a tábla frissítése
  4. a 3. lépés ismétlése amíg a lépések száma el nem éri a maximumot, vagy a tábla egyszínű nem lesz
  5. üzenet kiírása a nyerésről vagy vesztésről, start gomb frissítése 'Új játék'-ra és kezdődik előlről

 A játék közben is lehetőség van az 'Új játék' megnyomására, ilyenkor a tábla resetelődik, és indul előlröl a dolog, illetve él még egy 'Segítség' link, amire kattintva egy kisablakba leírunk pár sort a játék működéséről.

A következő importokra van szükségünk:

import sys, locale
import random
from PySide.QtCore import *
from PySide.QtGui import *
from ui_pyflood import *
import os
basedir = os.path.dirname(os.path.realpath(__file__))

VERSION='1.1'

Ezenkívül már itt elmentjük egy változóba a futó program helyét, kelleni fog a nyelvi fájlok betöltéséhez, valamint egy verzió változót hozunk létre az esetleges változatok követéséhez.
A program törzse végtelenül egyszerű:

if __name__=="__main__":
    app=QApplication(sys.argv)
    translator = QTranslator()
    translator.load(basedir + '/'+ locale.getdefaultlocale()[0] + ".qm")
    app.installTranslator(translator)
    g=Game()
    g.show()
    app.exec_()

Létrehozunk egy QApplication példányt (ez minden Qt programhoz kell), példányosítunk egy QTranslatort, betöltjük a locale szerinti nyelvi fájlt (ha van, de ezt a translator lekezeli), majd betoljuk az app-ba a translator példányunkat, így az majd szorgalmasan lefordít "mindent".
Példányosítjuk Game osztályunkat, ami egy QMainWindow tulajdonképpen, csak megspékeltük a feldolgozó metódusainkkal, megjelenítjük a főablakot, majd indítjuk az app feldolgozó hurkát.

Mielőtt rátérnénk a Game osztály "kibeszélésére" előbb meg kell ismerkednünk a Board osztállyal, ugyanis ez jeleníti meg a játéktáblánkat, és a Game is őt használja.
Íme:

class Board(QGraphicsScene):
    def __init__(self,parent):
        super(Board,self).__init__()
        self.colors=[]
        self.squares=[]
        for i in range(14):
            self.colors.append([0]*14)
            self.squares.append([0]*14)
        self.colorcodes=['#FFAAFF','#0055FF','#F4F400','#DE0000','#00D8D8','#007E00']
        self.recolor()
        for i in range(14):
            for j in range(14):
                pen=QPen(QColor(self.colorcodes[self.colors[i][j]]),0)
                brush=QBrush(QColor(self.colorcodes[self.colors[i][j]]))
                self.squares[i][j]=self.addRect(i*24,j*24,24,24,pen,brush)


    def recolor(self):
        for i in range(14):
            for j in range(14):
                self.colors[i][j]=random.randint(0,5)

    def repaint(self):
        for i in range(14):
            for j in range(14):
                self.squares[i][j].setBrush(QBrush(QColor(self.colorcodes[self.colors[i][j]])))
                self.squares[i][j].setPen(QPen(QColor(self.colorcodes[self.colors[i][j]])))

Ez az osztály tulajdonképpen egy QtGraphicsScene néhány belső változóval és saját metódussal, vagy is egy grafikai jelenet tárolására képes osztály.
A konstruktor létrehozza a négyzetek színeit tároló kétdimenziós listát (self.colors), és egy másikat maguknak a grafikai elemeknek, vagyis a négyzeteknek (self.squares). Ezután feltölti a színlistát random színekkel, és megalkotja a négyzet elemeket.

Két metódust kapott a Board osztályunk, recolor és repaint, az előbbi arra szolgál hogy véletlenszerűen feltöltse a színek listáját, az utóbbi pedig, hogy az aktuális színekkel (self.colors tartalma) újraszínezze a négyzeteket.

Jöhet a Game osztály!

class Game(QMainWindow,Ui_MainWindow):
    def __init__(self, parent=None):
        super(Game, self).__init__(parent)
        self.setupUi(self)
        self.board=Board(self.horizontalLayout)
        self.floodBoard.setScene(self.board)
        self.gameInProgress=False
        self.fillInProgress=False
        self.steps=0
        self.board_dirty=False
        self.setWindowTitle("pyFlood" + ' (v ' + VERSION + ')')
        self.pinkButton.clicked.connect(self.pinkclicked)
        self.blueButton.clicked.connect(self.blueclicked)
        self.yellowButton.clicked.connect(self.yellowclicked)
        self.redButton.clicked.connect(self.redclicked)
        self.cyanButton.clicked.connect(self.cyanclicked)
        self.greenButton.clicked.connect(self.greenclicked)
        self.statusbar.showMessage(QtGui.QApplication.translate("MainWindow",'Click \'Start\' to start the game.', None, QtGui.QApplication.UnicodeUTF8))
        self.startButton.clicked.connect(self.restartgame)
        self.ruleLabel.linkActivated.connect(self.showrules)

    def pinkclicked(self):
        self.colorclicked(0)

    def blueclicked(self):
        self.colorclicked(1)

    def yellowclicked(self):
        self.colorclicked(2)

    def redclicked(self):
        self.colorclicked(3)

    def cyanclicked(self):
        self.colorclicked(4)

    def greenclicked(self):
        self.colorclicked(5)

    def colorclicked(self,color):
        if self.gameInProgress and not self.fillInProgress:
            self.fillInProgress=True
            currentcolor=self.board.colors[0][0]
            fillbuffer=[(0,0)]
            self.steps=self.steps+1
            self.stepLabel.setText("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
"</style></head><body style=\" font-family:\'Droid Sans\'; font-size:10pt; font-weight:400; font-style:normal;\">\n"
"<p align=\"center\" style=\" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\"><span style=\" font-size:12pt; font-weight:600;\">%s</span></p></body></html>" % (QtGui.QApplication.translate("MainWindow",'Step: %d/25', None, QtGui.QApplication.UnicodeUTF8) % self.steps))
            while len(fillbuffer)>0:
                cur=fillbuffer.pop()
                if self.board.colors[cur[0]][cur[1]]==currentcolor:
                    self.board.colors[cur[0]][cur[1]]=color
                for i,j in [(-1,0),(0,-1),(1,0),(0,1)]:
                    x,y=cur[0]+i,cur[1]+j
                    if x>-1 and x<14 and y>-1 and y<14:
                        if self.board.colors[x][y]==currentcolor and (x,y) not in fillbuffer:
                            fillbuffer.append((x,y))
            self.board.repaint()
            self.fillInProgress=False
            self.checkifwin()

    def checkifwin(self):
        currentcolor=self.board.colors[0][0]
        wincolors=[]
        for i in range(14):
            wincolors.append([currentcolor]*14)
        if self.board.colors == wincolors:
            self.statusbar.showMessage(QtGui.QApplication.translate("MainWindow","You win!", None, QtGui.QApplication.UnicodeUTF8))
            self.win=True
            self.gameInProgress=False
            ret=QMessageBox.information(self, "pyFlood",
                                QtGui.QApplication.translate("MainWindow","Congratulations!\nYou have won the game in %d steps!", None, QtGui.QApplication.UnicodeUTF8) % self.steps,
                                QMessageBox.Ok)
            if ret==QMessageBox.Ok:
                self.statusbar.showMessage(QtGui.QApplication.translate("MainWindow",'Click \'New game\' to start another game.', None, QtGui.QApplication.UnicodeUTF8))
        elif self.steps >= 25:
            self.statusbar.showMessage(QtGui.QApplication.translate("MainWindow","You have lost the game!", None, QtGui.QApplication.UnicodeUTF8))
            self.win=False
            self.gameInProgress=False
            ret=QMessageBox.information(self, "pyFlood",
                                QtGui.QApplication.translate("MainWindow","You have lost the game!\nCould have a better luck next time...", None, QtGui.QApplication.UnicodeUTF8),
                                QMessageBox.Ok)
            if ret==QMessageBox.Ok:
                self.statusbar.showMessage(QtGui.QApplication.translate("MainWindow",'Click \'New game\' to start another game.', None, QtGui.QApplication.UnicodeUTF8))

    def showrules(self):
        ret=QMessageBox.information(self, "pyFlood",
                                ('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> \
<html><head><meta name="qrichtext" content="1" /><style type="text/css"> \
p, li { white-space: pre-wrap; } \
</style></head><body style=" font-family:\'Droid Sans\'; font-size:10pt; font-weight:400; font-style:normal;"> \
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">%s</p> \
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> \
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">%s</p> \
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> \
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">%s</p> \
<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"></p> \
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">%s</p> \
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">%s</p></body></html>') % (QtGui.QApplication.translate("MainWindow",'Click the \'Start\' buton to start a game (It will change to \'New game\').', None, QtGui.QApplication.UnicodeUTF8),QtGui.QApplication.translate("MainWindow",'On the upper left you can see six colored squares, and on the right you can see the gameboard.', None, QtGui.QApplication.UnicodeUTF8),QtGui.QApplication.translate("MainWindow","Your task is to make the whole board colored in one color. To achieve this, you should use the six colored square-buttons. The coloring starts from the upper left corner of the board. When you click on a color, all the squares started from the upper left with the same color will turn to the new color you clicked.", None, QtGui.QApplication.UnicodeUTF8),QtGui.QApplication.translate("MainWindow","It is harder to explain than do it :D", None, QtGui.QApplication.UnicodeUTF8),QtGui.QApplication.translate("MainWindow","Try it out!", None, QtGui.QApplication.UnicodeUTF8)),
                                QMessageBox.Ok)

    def restartgame(self):
        if self.gameInProgress:
            ret = QMessageBox.information(self, self.tr("pyFlood"),
                                QtGui.QApplication.translate("MainWindow","This will terminate the current running game.\nDo you really want to do that?", None, QtGui.QApplication.UnicodeUTF8),
                                QMessageBox.Ok | QMessageBox.Cancel)
            print ret
            if ret==QMessageBox.Ok:
                self.steps=0
                self.board.recolor()
                self.board.repaint()
                self.statusbar.showMessage(QtGui.QApplication.translate("MainWindow","New game started.", None, QtGui.QApplication.UnicodeUTF8))
        else:
            self.gameInProgress=True
            self.steps=0
            if self.board_dirty:
                self.board.recolor()
                self.board.repaint()
            else:
                self.board_dirty=True
                self.startButton.setText(QtGui.QApplication.translate("MainWindow", "New game", None, QtGui.QApplication.UnicodeUTF8))
            self.statusbar.showMessage(QtGui.QApplication.translate("MainWindow","Game started.", None, QtGui.QApplication.UnicodeUTF8))

Az osztály egy QtMainWindow ami ráadásként a Qtdesigner és a pyside-uic által generált Ui_MainWindow osztálynak is gyermeke, vagyis tartalmazza az összes widgetet, amit a designerrel megalkottunk.

Nagyon nem ragoznám a működését, nem olyan bonyolult osztály. A konstruktor létrehozza a táblát a fenti Board osztályból, beállít pár játékváltozót, és metódusokat rendel a színgombokhoz, amik tulajdonképpen ugyanazt a metódust hívják meg - colorclicked - , csak a színüknek megfelelő színkóddal.
A colorclicked metódusba kellett egy "lock" mechanizmust építeni, ami megakadályozza, hogy a túl sűrű kattintások ne keverjék meg a színkezelést. Ezt a fillInProgress objektumváltozóval csináljuk. Ha elindul egy kitöltés, akkor ezt igazra állítjuk, így az újabb kattintás esetén, ha ez a változó igaz, akkor az előző kitöltés még nem fejeződött be, így nem kell csinálni semmit. Minden kitöltés után meghívódik a chekifwin metódus, ami azt ellenőrzi, hogy a játékot megnyerte-e a játékos, vagy esetleg elvesztette-e. Ha valamelyik igen, akkor megteszi a kiírásokat, és alaphelyzetbe állítja a játékot.

Röviden ennyi, remélem követhető volt. Lehet kérdezni!
Innen le tudod tölteni a forrást egyben: katt

2 megjegyzés:

  1. A következő lépés az ui-ból python modult faragni. Ezt a pyside-uic segédprogrammal tudjuk megtenni:


    pyside-iuc pyflood.ui ui_pyflood.py

    -------------------
    leírnád részletesen hogy ezt a lépést hogy kell emgcsinálni?
    A C:\Python32\Scripts\pyside-uic.exe nekem csak így átfut és eltűnik, hol/hogy tudom megadni neki hogy meik fájlt mibe rakja át?
    köszi!

    VálaszTörlés
  2. Én linux alatt pythonozok, lényegesen egyszerűbb, de gondolom win alatt ezt egy parancssorból a legegyszerűbb végrehajtani.
    Start menü -> Futtatás -> cmd -> Enter
    itt a cd paranccsal elnavigálsz a mappába ahol a fájlok vannak, és ott adod ki a parancsot. Szerintem így sikerrel jársz.

    VálaszTörlés