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()