CannonField
#!/usr/bin/env python3 # -*- coding: utf-8 -*- ''' PyQt4 conversion of Qt Tutorial 14 The cannon can now handle mouse events. The differences from 113_battle.pyw are: - re-organised code and added slot decorators - added methods: mousePressEvent(), mouseMoveEvent(), mouseReleaseEvent() _paintBarrier(), _barrierRect(), _barrelHit() - added attribute: barrelPressed, timerCount last modified: 2012-01-22 jg ref: http://doc.trolltech.com/3.3/tutorial1-14.html ''' from math import (cos, sin, atan) from random import (seed, randrange) from PyQt4.QtGui import (QWidget, QColor, QPainter, QSizePolicy, QRegion, QPixmap, QFont, QMatrix) from PyQt4.QtCore import (Qt, pyqtSignal, pyqtSlot, QRect, QTimer, QPoint, QTime, QSize) class CannonField(QWidget): cRect = None # class attribute, only one can exist bRect = QRect(33, -4, 15, 8) # class attribute, cannon barrel definition firstTarget = True # first time we're creating a target def __init__(self, parent=None): super(QWidget, self).__init__(parent) self.setObjectName('cannonField') self.ang = 45 self.f = 0 # force self.target = QPoint(0, 0) self.gameEnded = False self.barrelPressed = False # add timer to handle shooting self.autoShootTimer = QTimer(self) self.autoShootTimer.timeout.connect(self._moveShot) self.timerCount = 0 self.shoot_ang = 0 self.shoot_f = 0 # set background colour pal = self.palette() pal.setColor(self.backgroundRole(), QColor(250, 250, 200)) self.setPalette(pal) self.setAutoFillBackground(True) self.newTarget() # create a target # custom signals ----------------------------------------------------------------------------- angleChanged = pyqtSignal(int, name="angleChanged") forceChanged = pyqtSignal(int, name="forceChanged") missed = pyqtSignal(name="missed") hit = pyqtSignal(name="hit") canShoot = pyqtSignal(bool, name="canShoot") # custom slots ------------------------------------------------------------------------------- @pyqtSlot() def setGameOver(self): if self.gameEnded: return if self.isShooting(): self.autoShootTimer.stop() self.gameEnded = True self.repaint() @pyqtSlot() def restartGame(self): if self.isShooting(): self.autoShootTimer.stop() self.gameEnded = False self.target = QPoint(0, 0) # force repaint of old target self.repaint() self.canShoot.emit(True) @pyqtSlot() def newTarget(self): if CannonField.firstTarget: # seed random number generator CannonField.firstTarget = False midnight = QTime(0, 0, 0) seed(midnight.secsTo(QTime.currentTime())) r = QRegion(self._targetRect()) self.target = QPoint(200 + randrange(0, 190), 10 + randrange(0, 255)) self.repaint(r.unite(QRegion(self._targetRect()))) @pyqtSlot() def shoot(self): if self.isShooting(): return self.timerCount = 0 self.shoot_ang = self.ang self.shoot_f = self.f self.autoShootTimer.start(1) self.canShoot.emit(False) @pyqtSlot(int) def setAngle(self, degrees): if degrees < 5: degrees = 5 elif degrees > 70: degrees = 70 elif self.ang == degrees: return self.ang = degrees self.repaint(self._cannonRect()) self.angleChanged.emit(self.ang) @pyqtSlot(int) def setForce(self, newton): if newton < 0: newton = 0 elif self.f == newton: return self.f = newton self.forceChanged.emit(self.f) @pyqtSlot() def _moveShot(self): r = self._shotRect() self.timerCount += 1 shotR = self._shotRect() if shotR.intersects(self._targetRect()): self.autoShootTimer.stop() self.hit.emit() self.canShoot.emit(True) elif (shotR.x() > self.width() or shotR.y() > self.height() or shotR.intersects(self._barrierRect())): self.autoShootTimer.stop() self.missed.emit() self.canShoot.emit(True) else: r = r.unite(shotR) self.repaint(r) # public methods ------------------------------------------------------------------------------------ def gameOver(self): return self.gameEnded def isShooting(self): return self.autoShootTimer.isActive() # method overrides --------------------------------------------------------------------------- # override parent (QWidget) class methods to provide customised behaviours def sizePolicy(self): return QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def sizeHint(self): return QSize(400, 300) def paintEvent(self, event): updateR = event.rect() p = QPainter(self) if self.gameEnded: p.setPen(Qt.black) p.setFont(QFont("Courier", 48, QFont.Bold)) p.drawText(self.rect(), Qt.AlignCenter, "Game Over") if updateR.intersects(self._cannonRect()): self._paintCannon(p) if updateR.intersects(self._barrierRect()): self._paintBarrier(p) if self.autoShootTimer.isActive() and updateR.intersects(self._shotRect()): self._paintShot(p) if updateR.intersects(self._targetRect()): self._paintTarget(p) def mousePressEvent(self, evt): if evt.button() != Qt.LeftButton: return if self._barrelHit(evt.pos()): self.barrelPressed = True def mouseMoveEvent(self, evt): if not self.barrelPressed: return pnt = QPoint(evt.pos()) if pnt.x() <= 0: pnt.setX(1) if pnt.y() >= self.height(): pnt.setY(self.height() - 1) rad = atan((self.rect().bottom() - pnt.y()) / pnt.x()) self.setAngle(int(rad * 180 / 3.14159265)) def mouseReleaseEvent(self, evt): if evt.button() == Qt.LeftButton: self.barrelPressed = False # private methods ---------------------------------------------------------------------------- # technically, methods which should not be called by external classes def _cannonRect(self): if not CannonField.cRect: # create once r = QRect(0, 0, 50, 50) r.moveBottomLeft(self.rect().bottomLeft()) return r else: # already defined so return existing rectangle return CannonField.cRect def _shotRect(self): gravity = 4 time = self.timerCount / 4.0 velocity = self.shoot_f radians = self.shoot_ang * 3.14159265 / 180 velx = velocity * cos(radians) vely = velocity * sin(radians) x0 = (CannonField.bRect.right() + 5) * cos(radians) y0 = (CannonField.bRect.right() + 5) * sin(radians) x = x0 + velx * time y = y0 + vely * time - 0.5 * gravity * time * time r = QRect(0, 0, 6, 6) r.moveCenter(QPoint(int(x), self.height() - 1 - int(y))) return r def _targetRect(self): r = QRect(0, 0, 20, 10) r.moveCenter(QPoint(self.target.x(), self.height() - 1 - self.target.y())) return r def _barrierRect(self): return QRect(145, self.height() - 100, 15, 100) def _barrelHit(self, p): # p = mouse position mtx = QMatrix() mtx.translate(0, self.height() - 1) mtx.rotate(-self.ang) mtx = QMatrix(mtx.inverted()[0]) # returns a tuple, 1st element is # the inverted matrix print(self.bRect.contains(mtx.map(p))) return self.bRect.contains(mtx.map(p)) # private custom paint methods ------------------------------------------------------------------------------- def _paintTarget(self, p): p.setBrush(Qt.red) p.setPen(Qt.black) p.drawRect(self._targetRect()) def _paintShot(self, p): p.setBrush(Qt.black) p.setPen(Qt.NoPen) p.drawRect(self._shotRect()) def _paintBarrier(self, p): p.setBrush(Qt.yellow) p.setPen(Qt.black) p.drawRect(self._barrierRect()) def _paintCannon(self, p): cr = self._cannonRect() pix = QPixmap(cr.size()) pix.fill(self, cr.topLeft()) tmp = QPainter(pix) tmp.setBrush(Qt.blue) # brush colour for filling object tmp.setPen(Qt.NoPen) # no special edges # set QPainter's origin (0,0) coords to the bottom-left tmp.translate(0, pix.height() - 1) # draw a quarter circle in the bottom left corner tmp.drawPie(QRect(-35, -35, 70, 70), 0, 90 * 16) # rotate counter-clockwise 'ang' degrees around the origin # and draw the cannon's barrel tmp.rotate(-self.ang) tmp.drawRect(CannonField.bRect) tmp.end() # paint the pixmap on the screen p.drawPixmap(cr.topLeft(), pix) def main(): import sys from PyQt4.QtGui import (QApplication) app = QApplication(sys.argv) # required w = CannonField() w.setGeometry(100, 100, 500, 355) w.show() #w.setGameOver() sys.exit(app.exec_()) # start main event loop, exit when app closed if __name__ == '__main__': main()GameBoard (previously MyWidget)
#!/usr/bin/env python3 # -*- coding: utf-8 -*- ''' PyQt4 conversion of Qt Tutorial 14 This is the final example: a complete game. We add keyboard accelerators and introduce mouse events to CannonField. We put a frame around the CannonField and add a barrier (wall) to make the game more challenging. The differences from 113_battle.pyw are: - modified _init__() - added override for keyPressEvent() - blocked resizing, leaving minimizing as only option NOTES: ===== The original C++ code placed the cannonField in a styled QVBox. In PyQt4 there is no QVBox and QVBoxLayout does not have a setFrameStyle() method. To reproduce the same look had to: 1. Create a QFrame 2. Style the frame 3. Create a QVBoxLayout 4. Set the layouts margins to 0 4. Set the frame layout to the QVBoxLayout 5. Add the cannonField to the layout 6. Add the frame to the grid PyQt4 does not include QAccel (for creating keyboard accelerators) To reproduce the same behaviour i.e. trigger 'shoot()' if 'Enter' or 'Return' pressed, needed to override the keyPressEvent() BEHAVIOUR: ========= The cannon now shoots when you press Enter. You can also position the cannon's angle using the mouse. The barrier makes it a little more challenging to play the game. We also have a nice looking frame around the CannonField. last modified: 2012-01-22 jg ref: http://doc.trolltech.com/3.3/tutorial1-14.html ''' import sys from PyQt4.QtGui import (QApplication, QWidget, QPushButton, QFont, QLabel, QLCDNumber, QVBoxLayout, QGridLayout, QHBoxLayout, QFrame, QSizePolicy) from PyQt4.QtCore import (Qt) from t114_lcdrange import (LCDRange) from t114_cannon import (CannonField) class GameBoard(QWidget): def __init__(self, parent=None, name=''): super(GameBoard, self).__init__(parent) if name: self.setObjectName(name) quitBtn = QPushButton('&Quit', self) quitBtn.setFont(QFont("Times", 18, QFont.Bold)) quitBtn.clicked.connect(QApplication.instance().quit) angle = LCDRange(wlabel="ANGLE") angle.setRange(5, 70) # add LCDRange widget to handle force force = LCDRange(wlabel="FORCE") force.setRange(10, 50) self.cannonField = CannonField(self) self.cannonField.setContentsMargins(10, 10, 10, 10) angle.slider.valueChanged.connect(self.cannonField.setAngle) self.cannonField.angleChanged.connect(angle.setValue) # add event handling for 'force' force.slider.valueChanged.connect(self.cannonField.setForce) self.cannonField.forceChanged.connect(force.setValue) # handle target hits/misses self.cannonField.hit.connect(self.hit) self.cannonField.missed.connect(self.missed) # buttons shootBtn = QPushButton('&Shoot', self) shootBtn.setFont(QFont("Times", 18, QFont.Bold)) shootBtn.clicked.connect(self.fire) self.cannonField.canShoot.connect(self.setEnabled) restartBtn = QPushButton("&New Game", self) restartBtn.setFont(QFont("Times", 18, QFont.Bold)) restartBtn.clicked.connect(self.newGame) self.hits = QLCDNumber(2, self) self.shotsLeft = QLCDNumber(2, self) self.hitsL = QLabel("HITS", self) self.shotsLeftL = QLabel("SHOTS LEFT", self) # layout widgets grid = QGridLayout() grid.addWidget(quitBtn, 0, 0) # create a frame to hold the cannonfield frame = QFrame() frame.setFrameStyle(QFrame.WinPanel | QFrame.Sunken) cfvbox = QVBoxLayout() cfvbox.setMargin(0) cfvbox.addWidget(self.cannonField) frame.setLayout(cfvbox) grid.addWidget(frame, 1, 1) grid.setColumnStretch(1, 10) # add the angle and force widgets leftBox = QVBoxLayout() grid.addLayout(leftBox, 1, 0) leftBox.addWidget(angle) leftBox.addWidget(force) # add the 'shoot' button topBox = QHBoxLayout() grid.addLayout(topBox, 0, 1) topBox.addWidget(shootBtn) topBox.addWidget(self.hits) topBox.addWidget(self.hitsL) topBox.addWidget(self.shotsLeft) topBox.addWidget(self.shotsLeftL) topBox.addStretch(1) topBox.addWidget(restartBtn) self.setLayout(grid) angle.setValue(60) force.setValue(25) angle.setFocus() # give the LCDRange object keyboard focus self.setFixedSize(500, 355) # prevent resizing self.newGame() def keyPressEvent(self, evt): if evt.key() in (Qt.Key_Enter, Qt.Key_Return): self.fire() evt.accept() def fire(self): if self.cannonField.gameOver() or self.cannonField.isShooting(): return self.shotsLeft.display(self.shotsLeft.intValue() - 1) self.cannonField.shoot() def hit(self): self.hits.display(self.hits.intValue() + 1) if self.shotsLeft.intValue() == 0: self.cannonField.setGameOver() else: self.cannonField.newTarget() def missed(self): if self.shotsLeft.intValue() == 0: self.cannonField.setGameOver() def newGame(self): self.shotsLeft.display(15) self.hits.display(0) self.cannonField.restartGame() self.cannonField.newTarget() def main(): app = QApplication(sys.argv) # required w = GameBoard() w.setGeometry(100, 100, 500, 355) w.setWindowTitle("Battle Game") w.show() sys.exit(app.exec_()) # start main event loop, exit when app closed if __name__ == '__main__': main()