Skip to main content
Code Review

Return to Revisions

6 of 6
replaced http://codereview.stackexchange.com/ with https://codereview.stackexchange.com/

Example of PyQt5 simple turn-based game code

I've made my first turn-based game in PyQt5. I suppose that its idea can also be used by other novice GUI programmers.

There is a 5 by 5 squared unpainted field and 4 players. Each player starts at corner square and has his own colour. Player can move to adjacent square and fill it with his colour if it isn’t occupied by other player or isn’t filled in player's colour.

If player has nowhere to move he is randomly teleported to non-filled square. Game ends when all squares are filled. Player who has most squares filled with his colour wins.

Also I would like to hear any suggestions about code improvement. The problem parts are probably self.turn() and self.turn_loop() in MyApp class since they have a bit complicated "if/elif/else" logic.

Main module (PainterField.py):

#!/usr/bin/env python
'''
Game name: PainterFiled
Author: Igor Vasylchenko
As this module and its sub-modules use PyQt5 they are distributed and
may be used under the terms of the GNU General Public License version 3.0.
See http://www.gnu.org/copyleft/gpl.html
'''
import random
import sys
import traceback
from PyQt5.QtCore import (QRectF, QTimer, Qt)
from PyQt5.QtGui import (QBrush, QColor, QImage)
from PyQt5.QtWidgets import (QApplication, QGraphicsItem, QGraphicsScene,
 QGraphicsView, QMainWindow, QPushButton)
# Custom classes
from FieldClasses import (PlayerQGraphics, SquareQGrapics)
from FieldFunctions import (create_obstacles, create_squares,
 create_players, print_main, print_rules)
# Gui generated by Qt5
from gui import Ui_Field as Ui_MainWindow
'''Exceptions hadling block. Needed to track errors during
operation.'''
sys._excepthook = sys.excepthook
def exception_hook(exctype, value, traceback):
 sys._excepthook(exctype, value, traceback)
 sys.exit(1)
sys.excepthook = exception_hook
class MyApp(QMainWindow, Ui_MainWindow): 
 
 def __init__(self):
 QMainWindow.__init__(self)
 Ui_MainWindow.__init__(self)
 self.setupUi(self)
 # Important property that deletes all widgets (including QTimer)
 # on close.
 self.setAttribute(Qt.WA_DeleteOnClose)
 # Variables
 self.key = None 
 self.players = create_players() 
 self.scene = QGraphicsScene()
 self.squares = create_squares(9, 9)
 '''Timer is used to repeat self.turn_loop and hadle turn sequence.
 It is stopped by default and starts after self.start() is called.
 Stops when current game ends or apllication closes (see note above).'''
 self.timer = QTimer(self)
 
 self.draw_field(self.squares, self.players) 
 self.QGraph_field.setScene(self.scene)
 # Connecting signals to slots
 self.QBut_main.clicked.connect(lambda: self.print_text(print_main()))
 self.QBut_reset.clicked.connect(self.reset)
 self.QBut_rules.clicked.connect(lambda: self.print_text(print_rules()))
 self.QBut_start.clicked.connect(self.start)
 self.timer.timeout.connect(self.turn_loop) 
 
 def draw_field(self, squares, players): 
 for xy in squares.keys():
 self.scene.addItem(squares[xy])
 for ID in players.keys():
 player = players[ID]
 self.scene.addItem(player)
 self.squares[player.xy].fill(player.colour)
 def isEnd(self): 
 for square in self.squares.values():
 if square.colour == 'cyan':
 end = 0
 break
 else:
 end = 1
 return end
 def keyPressEvent(self, event):
 key = event.key()
 
 # Player movement
 if key in (Qt.Key_Left, Qt.Key_Right,
 Qt.Key_Up, Qt.Key_Down):
 self.key = key
 
 # Start new game
 elif key in (Qt.Key_Enter, Qt.Key_Return):
 self.start()
 elif key == Qt.Key_Escape:
 self.close()
 
 # Reset current field and draw new one.
 # Starting the game is still needed.
 elif key == Qt.Key_R:
 self.reset()
 def print_text(self, source): 
 self.QText_status.setHtml(source)
 def reset(self):
 self.timer.stop()
 self.key = None
 self.players = create_players() 
 self.squares = create_squares(9, 9)
 self.scene.clear()
 self.draw_field(self.squares, self.players)
 self.print_text(print_main())
 
 def results(self):
 results = ()
 text = ''
 for ID in self.players:
 player = self.players[ID]
 colour = player.colour
 score = 0 
 for square in self.squares.values():
 if colour == square.colour:
 score += 1
 results += (score, )
 score = '<p>Player {0} ({1}): {2}</p>'.format(ID, player.colour,
 score)
 text += score
 max_score = max(results)
 text += 'Player {} won with score of {}!'.format(
 results.index(max_score),
 max_score
 )
 text = text.replace('Player 0', 'You')
 # Tie between player and computer is still considered a win)))
 return text
 def start(self): 
 self.key = None
 if not self.isEnd():
 self.timer.start(50)
 # Initially was divided in two functions turn_pl and turn_ai,
 # but they shared a lot of code. Still a bit messy though.
 def turn(self, ID):
 '''If there is no room to move player is teleported on
 unpainted square.'''
 player = self.players[ID]
 obstacles = create_obstacles(self.players)
 free_directions = player.findDirections(self.squares, obstacles)
 # Setting parameters to teleport
 if not free_directions: 
 for xy in self.squares.keys():
 square = self.squares[xy]
 if square.colour == 'cyan': 
 player.xy = xy 
 player.update()
 square.fill(player.colour)
 break
 
 # Returns 1 so next ai player can make move
 return 1
 
 # For human player pressed key is used for player movement
 elif ID == 0:
 key = self.key
 # Ai moves in random direction
 else:
 direction = random.sample(free_directions, 1)
 key = direction[0]
 # Moving player in designated direction
 obstacles = create_obstacles(self.players)
 xy = player.goto(key, obstacles, self.squares)
 if xy is not None:
 self.squares[xy].fill(player.colour)
 return xy
 def turn_loop(self): 
 end = self.isEnd() 
 if not end:
 
 # Player turn after arrow key is pressed
 if self.key is not None: 
 xy = self.turn(0) 
 self.key = None
 
 # Waiting on player turn
 else:
 xy = None
 
 # Computer ('ai') turn
 if xy is not None:
 for ID in range(1, len(self.players)):
 self.turn(ID)
 
 # Ending game and printing results
 else:
 self.timer.stop()
 self.print_text(self.results()) 
if __name__ == "__main__": 
 app = QApplication(sys.argv)
 window = MyApp() 
 window.show() 
 sys.exit(app.exec_())

Custom classes for players and squares (FieldClasses.py):

from PyQt5.QtCore import (QRectF, Qt)
from PyQt5.QtGui import (QBrush, QColor, QImage)
from PyQt5.QtWidgets import QGraphicsItem
class PlayerQGraphics(QGraphicsItem):
 def __init__(self, xy=(-1,-1),
 colour='green', icon='graphics/player.png'):
 QGraphicsItem.__init__(self)
 self.colour = colour
 self.icon = icon
 self.xy = xy 
 def boundingRect(self): #Is set to field dimensions
 return QRectF(0,0,270,270) 
 def findDirections(self, squares, obstacles, exceptColours=None): 
 '''Returns list of free directions to move.
 Direction is not free if colour of target square is similar
 to player’s or target square is already occupied.'''
 
 if exceptColours is None:
 exceptColours = (self.colour, )
 obstacles = tuple(obstacles)
 free_directions = []
 for n in range(1, 5): 
 xy = self.prepareGoto(n, exceptColours, obstacles, squares)
 if xy is not None:
 free_directions.append(n) 
 return free_directions
 def goto(self, direction, obstacles, squares, exceptColours=None): 
 '''Checks direction by self.prepareGoto(...) and moves
 player if direction is free.'''
 
 if exceptColours is None: 
 exceptColours = (self.colour, )
 xy = self.prepareGoto(direction, exceptColours, obstacles, squares) 
 if xy is not None:
 self.xy = xy
 self.update()
 return xy 
 def paint(self, painter, option, widget):
 x, y = self.xy
 target = QRectF(x*30, y*30, 28, 28)
 source = QRectF(0, 0, 28, 28)
 painter.drawImage(target, QImage(self.icon), source)
 def prepareGoto(self, direction, exceptColours, obstacles, squares):
 '''Checks if selected direction is free and returns actual
 coordinates to move is so. Otherwise returns None'''
 
 x, y = self.xy 
 if direction in (Qt.Key_Up, 'u', 1): 
 y = y - 1 
 elif direction in (Qt.Key_Down, 'd', 2): 
 y = y + 1 
 elif direction in (Qt.Key_Left, 'l', 3):
 x = x - 1 
 elif direction in (Qt.Key_Right, 'r', 4):
 x = x + 1
 xy = (x, y) 
 try:
 for n in obstacles: 
 if xy == n:
 xy = None 
 break 
 if (xy is not None
 and squares[xy].colour in exceptColours):
 xy = None 
 except KeyError:
 xy = None 
 return xy
class SquareQGrapics(QGraphicsItem):
 def __init__(self, xy=(-1,-1), colour='cyan'):
 QGraphicsItem.__init__(self)
 self.colour = colour
 self.xy = xy
 def boundingRect(self):
 x, y = self.xy
 return QRectF(x*30, y*30, 28, 28)
 def fill(self, new_colour='red'):
 '''Fills square with selected colour by updating self.colour.'''
 
 self.colour = new_colour
 self.update()
 def paint(self, painter, option, widget):
 x, y = self.xy
 colour = QBrush(QColor(self.colour))
 painter.setBrush(colour)
 painter.drawRect(x*30, y*30, 28, 28)

Custom functions (FieldFunctions.py):

#Custom classes
from FieldClasses import (PlayerQGraphics, SquareQGrapics)
#Obstacles are players' coordinates. And it is easier to return them as
#generator since they need update already when called.
def create_obstacles(players): 
 for player in players.values(): 
 yield player.xy
def create_squares(cols, rows):
 squares = {}
 for x in range(cols):
 for y in range(rows):
 squares[(x,y)] = SquareQGrapics((x,y))
 return squares
def create_players():
 players = {}
 ai = 'graphics/ai.png'
 players[0] = PlayerQGraphics(xy=(0,0)) # Human controlled player
 players[1] = PlayerQGraphics(xy=(8,0), colour='red', icon=ai)
 players[2] = PlayerQGraphics(xy=(0,8), colour='blue', icon=ai)
 players[3] = PlayerQGraphics(xy=(8,8), colour='black', icon=ai, )
 return players
def print_main():
 text = ('''<p>Welcome to PainterField!</p>
 
 <p>To start game press &quot;Start&quot; or &quot;Enter&quot;
 key. Use arrow keys to move player icon from the top left
 corner.</p>
 <p>Click &quot;Main menu&quot; to read this message.</p>
 
 <p>Click &quot;Rules&quot; to read them.</p>
 
 <p>Press &quot;Reset&quot; or &quot;R&quot; key to reset game
 field (arrow keys will be frozen).</p>''')
 return text
def print_rules():
 text = ('''<p>Each player goes to one of four adjacent squares
 and fills it with his colour. Squares of player’s
 colour are not allowed to enter.</p>
 <p>If player has nowhere to move he is randomly teleported
 to an empty square.</p>
 
 <p>Game ends when all squares are painted. Player,
 who painted the most squares, wins.</p>''')
 return text

Player icons (rename to ai.png and player.png and place in '/graphics'): place in 'graphics/ai.png' place in 'graphics/ai.png'

GUI code generated from .ui file (gui.py):

# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'PainterField.ui'
#
# Created by: PyQt5 UI code generator 5.7
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Field(object):
 def setupUi(self, Field):
 Field.setObjectName("Field")
 Field.resize(270, 570)
 Field.setMinimumSize(QtCore.QSize(270, 570))
 Field.setMaximumSize(QtCore.QSize(270, 570))
 self.QGraph_field = QtWidgets.QGraphicsView(Field)
 self.QGraph_field.setGeometry(QtCore.QRect(0, 300, 270, 270))
 sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
 sizePolicy.setHorizontalStretch(0)
 sizePolicy.setVerticalStretch(0)
 sizePolicy.setHeightForWidth(self.QGraph_field.sizePolicy().hasHeightForWidth())
 self.QGraph_field.setSizePolicy(sizePolicy)
 self.QGraph_field.setMinimumSize(QtCore.QSize(270, 270))
 self.QGraph_field.setMaximumSize(QtCore.QSize(270, 270))
 self.QGraph_field.setFocusPolicy(QtCore.Qt.NoFocus)
 self.QGraph_field.setFrameShape(QtWidgets.QFrame.NoFrame)
 self.QGraph_field.setLineWidth(0)
 self.QGraph_field.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
 self.QGraph_field.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
 self.QGraph_field.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
 self.QGraph_field.setSceneRect(QtCore.QRectF(0.0, 0.0, 270.0, 270.0))
 self.QGraph_field.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
 self.QGraph_field.setObjectName("QGraph_field")
 self.verticalLayoutWidget = QtWidgets.QWidget(Field)
 self.verticalLayoutWidget.setGeometry(QtCore.QRect(10, 10, 251, 271))
 self.verticalLayoutWidget.setObjectName("verticalLayoutWidget")
 self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalLayoutWidget)
 self.verticalLayout.setContentsMargins(0, 0, 0, 0)
 self.verticalLayout.setObjectName("verticalLayout")
 self.QText_status = QtWidgets.QTextBrowser(self.verticalLayoutWidget)
 font = QtGui.QFont()
 font.setPointSize(10)
 self.QText_status.setFont(font)
 self.QText_status.setFocusPolicy(QtCore.Qt.NoFocus)
 self.QText_status.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
 self.QText_status.setReadOnly(True)
 self.QText_status.setObjectName("QText_status")
 self.verticalLayout.addWidget(self.QText_status)
 self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
 self.horizontalLayout_4.setObjectName("horizontalLayout_4")
 self.QBut_start = QtWidgets.QPushButton(self.verticalLayoutWidget)
 font = QtGui.QFont()
 font.setPointSize(14)
 self.QBut_start.setFont(font)
 self.QBut_start.setFocusPolicy(QtCore.Qt.NoFocus)
 self.QBut_start.setObjectName("QBut_start")
 self.horizontalLayout_4.addWidget(self.QBut_start)
 self.QBut_rules = QtWidgets.QPushButton(self.verticalLayoutWidget)
 font = QtGui.QFont()
 font.setPointSize(14)
 self.QBut_rules.setFont(font)
 self.QBut_rules.setFocusPolicy(QtCore.Qt.NoFocus)
 self.QBut_rules.setObjectName("QBut_rules")
 self.horizontalLayout_4.addWidget(self.QBut_rules)
 self.verticalLayout.addLayout(self.horizontalLayout_4)
 self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
 self.horizontalLayout_3.setObjectName("horizontalLayout_3")
 self.QBut_reset = QtWidgets.QPushButton(self.verticalLayoutWidget)
 font = QtGui.QFont()
 font.setPointSize(14)
 self.QBut_reset.setFont(font)
 self.QBut_reset.setFocusPolicy(QtCore.Qt.NoFocus)
 self.QBut_reset.setObjectName("QBut_reset")
 self.horizontalLayout_3.addWidget(self.QBut_reset)
 self.QBut_main = QtWidgets.QPushButton(self.verticalLayoutWidget)
 font = QtGui.QFont()
 font.setPointSize(14)
 self.QBut_main.setFont(font)
 self.QBut_main.setFocusPolicy(QtCore.Qt.NoFocus)
 self.QBut_main.setObjectName("QBut_main")
 self.horizontalLayout_3.addWidget(self.QBut_main)
 self.verticalLayout.addLayout(self.horizontalLayout_3)
 self.retranslateUi(Field)
 QtCore.QMetaObject.connectSlotsByName(Field)
 def retranslateUi(self, Field):
 _translate = QtCore.QCoreApplication.translate
 Field.setWindowTitle(_translate("Field", "PainterField"))
 self.QText_status.setHtml(_translate("Field", "<!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:\'MS Shell Dlg 2\'; font-size:10pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Welcome to PainterField! </p>\n"
"<p style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">To start game press &quot;Start&quot; or &quot;Enter&quot; key. Use arrow keys to move player icon at top left corner. </p>\n"
"<p style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Click &quot;Main menu&quot; to read this message. </p>\n"
"<p style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Click &quot;Rules&quot; to read them. </p>\n"
"<p style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Press &quot;Reset&quot; or &quot;R&quot; key to reset game field (arrow keys will be frozen).</p></body></html>"))
 self.QBut_start.setText(_translate("Field", "Start"))
 self.QBut_rules.setText(_translate("Field", "Rules"))
 self.QBut_reset.setText(_translate("Field", "Reset"))
 self.QBut_main.setText(_translate("Field", "Main menu"))

Also see my answer below for further improvements of coding style/readability.

Follow-up question on this post: Example of PyQt5 Snake game

Vasyalisk
  • 436
  • 4
  • 10
lang-py

AltStyle によって変換されたページ (->オリジナル) /