Example #1
0
    def renderPlot(self):
        # generate a random plot path!
        plotPath = tempfile.mkstemp(suffix='.png', dir=self.getTempPath())[1]

        self.plotThread = PlotRenderer(self.getScores(), plotPath, False, 100,
                                       self)
        self.plotThread.finishedPlot.connect(self.displayPlot)
        self.plotThread.start()
        self.log('Renering plot')
Example #2
0
 def renderPlot(self):
     # generate a random plot path!
     plotPath = tempfile.mkstemp(suffix = '.png', dir = self.getTempPath())[1]
     
     self.plotThread = PlotRenderer(self.getScores(), plotPath, False, 100, self)
     self.plotThread.finishedPlot.connect(self.displayPlot)
     self.plotThread.start()
     self.log('Renering plot')
Example #3
0
class Gui(QWidget):
    gameStarted = pyqtSignal()

    def __init__(self, parent=None):
        super(Gui, self).__init__(parent)
        self.width = None
        self.height = None
        self.pixmap = None  # used for score plot at the end of the game

    def startGame(self):
        raise NotImplementedError(
            'startGame is virtual and must be overridden')

    """
    The basic layout is the same - a grid layout with a QuestionDisplay / ButtonGrid in a QStackedLayout
    on the left and a PlayerTable on the right.
    The label on top displays informative messages and is hidden when not used.
    - buttonText is the text shown in the QuestionDisplay's button
    """

    def setupGui(self, buttonText, width, height):
        layout = QGridLayout()
        self.setLayout(layout)

        w = QLabel()
        w.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
        w.setFont(QFont(QFont.defaultFamily(w.font()), 28))
        layout.addWidget(w, 0, 1)
        layout.addLayout(QStackedLayout(), 1, 0)
        layout.addWidget(self.setupTable(), 1, 1)

        # width and height of the QuestionDisplay are specified via the 'size' structure in the 'rules.json' file
        self.width = width
        self.height = height

        w = ButtonGrid(self.getRound(), width=self.width, height=self.height)
        self.getStack().addWidget(w)  # the ButtonGrid will always have index 0

        w = QuestionDisplay('html',
                            self.getTemplate(),
                            self.getTempPath(),
                            buttonText,
                            width=width,
                            height=height)
        self.getStack().addWidget(
            w)  # the QuestionDisplay will always have index 1

        self.setupSignals()

    def setupTable(self):
        raise NotImplementedError(
            'setupTable is virtual and must be overridden')

    def setupSignals(self):
        raise NotImplementedError(
            'setupSignals is virtual and must be overridden')

    def deleteTempFiles(self):
        path = self.getTempPath()
        for name in os.listdir(path):
            os.remove(path + '/' + name)
        os.removedirs(path)

    # messages from different components are prefixed with their source, in this case the gui thread; redirect to create log
    def log(self, message):
        print 'Gui: ' + message

    """
    display* functions modify the visible widget of the QStackedLayoutswitching between the
    ButtonGrid and QuestionDisplay:
    displayQuestion, displayAnswer
    displayGrid : note that, in case of an update (for going to the next round), a different
    function is called to replace the buttons, but in the end displayGrid switches the QStackedLayout's
    current widget
    """

    # displays the current question's statement
    def displayQuestion(self, i):
        self.getGrid().layout().itemAt(i).widget().setEnabled(False)
        self.log('displaying question ' + str(i))

        question = self.getQuestion()
        template = self.getTemplateFromQuestion(question)

        # the template contains an answer placeholder as well, which needs to be erased
        answer = question['answer']
        question['answer'] = ''

        statement = Template(template).substitute(question)

        question['answer'] = answer

        self.getDisplay().updateGui(statement, self.getTempPath())
        self.getStack().setCurrentIndex(1)

    def displayAnswer(self):
        question = self.getQuestion()
        template = self.getTemplateFromQuestion(question)
        answer = Template(template).substitute(question)
        self.getDisplay().updateGui(answer, self.getTempPath())

    def displayGrid(self):
        self.log('displaying grid')
        self.getStack().setCurrentIndex(0)

    def updateGrid(self):
        self.log('updating grid')
        oldGrid = self.getStack().takeAt(
            0).widget()  # the old grid is removed from the QStackedLayout
        del oldGrid
        self.getStack().insertWidget(
            0, ButtonGrid(self.getRound(),
                          width=self.width,
                          height=self.height))

    """
    Displays the plot drawn by the PlotRenderer. The pixmap getPixmap() returns
    is always scaled down to the appropriate width and height.
    """

    def displayEndGame(self):
        self.getLabel().hide()
        w = QLabel()
        w.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
        w.setFont(QFont(QFont.defaultFamily(w.font()), 32))
        w.setText('Rendering plot...')
        self.getStack().addWidget(w)
        self.getStack().setCurrentIndex(2)
        """
        TODO: Color calculation
        hue = 0
        hueInc = 360 / self.getPlayers()
        """
        self.renderPlot()

    def renderPlot(self):
        # generate a random plot path!
        plotPath = tempfile.mkstemp(suffix='.png', dir=self.getTempPath())[1]

        self.plotThread = PlotRenderer(self.getScores(), plotPath, False, 100,
                                       self)
        self.plotThread.finishedPlot.connect(self.displayPlot)
        self.plotThread.start()
        self.log('Renering plot')

    def displayPlot(self, path):
        self.getTable().showColors(self.getColors())
        self.pixmap = QPixmap(path)
        self.getPlot().setPixmap(
            self.pixmap.scaled(self.width, self.height,
                               Qt.KeepAspectRatioByExpanding,
                               Qt.SmoothTransformation))
        self.resize(self.minimumSize())
        self.adjustSize()
        self.plotThread.exit()

    def setLabelText(self, message):
        self.getLabel().setText(message)

    """
    Getter functions for the different UI elements.
    Accessing them manualy would have been awkward and error-prone because of the layout indices.
    """

    def getLabel(self):
        return self.layout().itemAtPosition(0, 1).widget()

    def getStack(self):
        return self.layout().itemAtPosition(1, 0).layout()

    def getTable(self):
        return self.layout().itemAtPosition(1, 1).widget()

    def getGrid(self):
        return self.getStack().itemAt(0).widget()

    def getDisplay(self):
        return self.getStack().itemAt(1).widget()

    def getDisplayButton(self):
        return self.getDisplay().layout().itemAt(1).widget()

    def getGridButton(self, i):
        return self.getGrid().layout().itemAt(i).widget()

    def getPlot(self):
        return self.getStack().itemAt(2).widget()

    """
    Each player is associated a color by dividing the hue range by the number of
    players. When there are lots of players, there is going to be a 'rainbow'
    effect and some colors might be very similar and quite hard to distinguish.
    Perhaps try changing saturation and value as well...?
    """

    def getColors(self):
        return dict([(player[0], player[1][1])
                     for player in self.getScores().items()])

    """
    These functions get game related data, which is typically obtained differently for the client and server guis.
    This is why these functions are virtual and their specific implementation is in the derived classes.
    """

    def getRound(self):
        raise NotImplementedError('getRound is virtual and must be overridden')

    def getQuestion(self):
        raise NotImplementedError(
            'getQuestion is virtual and must be overridden')

    def getTempPath(self):
        raise NotImplementedError(
            'getTmpPath is virtual and must be overridden')

    def getTemplate(self):
        raise NotImplementedError(
            'getTemplate is virtual and must be overridden')

    def getScores(self):
        raise NotImplementedError(
            'getScores is virtual and must be overridden')

    """
    Helper function, for the case where a question defines a custom html template which needs to be read.
    Is in practice always called, because finding custom templates is not the job of the display{Question,Answer} methods.
    """

    def getTemplateFromQuestion(self, question):
        if 'template' not in question or question == None:
            return self.getTemplate()
        templateFile = open(self.getTempPath() + '/' + question['template'])
        return ''.join([line for line in templateFile.readlines()])
Example #4
0
class AdminGui(Gui):
    """
    Signals are used to communicate with the GameServer, which is in a
    different thread. Any instance where a GameServer method is called
    directly is a _bug_. Please report it.
    """    
    answerShown = pyqtSignal()
    answerChecked = pyqtSignal(str, bool)

    mutePlayers = pyqtSignal(list)
    unmutePlayers = pyqtSignal(list)
    
    def __init__(self, parent=None):    
        super(AdminGui, self).__init__(parent)
        self.game = GameServer(self, 'jeopardy')

        self.loadRules()

        """
        Setup the player table, and wait for players to login before
        the game may be started.
        """
        self.playerAdmin = PlayerAdminDialog(self)        
        self.playerAdmin.startGame.connect(self.startGame)
        self.playerAdmin.show() # does not block thread, __init__ continues
        
        # starts the GameServer thread
        self.game.start()
        
        # prepare to tell the time elapsed since the game started
        self.time = QTime(0, 0, 0)
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.displayTime)

    """
    loadRules is where the user selects a game file (*.jeop) and where it is
    validated by the RuleLoader module. After (if) this function finishes, the
    GameServer object, self.game will contain the rules read from the file.
    The RuleLoader may be used on its own to test the validity of .jeop files.
    """
    def loadRules(self): 
        fileName = ''
        while fileName == '':  
            fileName = QFileDialog.getOpenFileName(self,
                                                   'Select a Jeopardy game file',
                                                   'example.jeop',
                                                   'Jeopardy game files (*.jeop)')
            if fileName == '':
                sys.exit()
            self.log('Validating ' + fileName)
            
            # validateFile is the actual function from RuleLoader that is called
            if validateFile(self.game, fileName) == False:
                ans = QMessageBox.warning(self, '',
                                          'The selected game file is invalid.\nPlease choose a different one.',
                                          QMessageBox.Ok | QMessageBox.Cancel)
                if ans == QMessageBox.Cancel:
                    sys.exit()
                fileName = ''
            else:
                self.log('Loaded ' + fileName)

    """
    The actual game ui is set up and the AdminGui instructs the GameServer to
    startGames for all players
    """
    def startGame(self):
        self.playerAdmin.close()
        self.game.loginEnabled = False
        
        self.game.nextRound()
        self.setupGui('Show Answer', self.game.width, self.game.height)
        self.getDisplayButton().clicked.connect(self.game.showAnswer)
        self.timer.start(1000)

        self.show()
        self.gameStarted.emit()

    """
    Virtual methods required in Gui implemented here. In this case, all the data
    is in the GameServer. No mutexes are used because GameServer cannot ever modify
    these fields at the same time AdminGui reads them.
    """
    def getRound(self):
        return self.game.round

    def getQuestion(self):
        return self.game.question

    def getTempPath(self):
        return self.game.tempPath

    def getTemplate(self):
        return self.game.template

    def getScores(self):
        return self.game.scores

    """
    Custom table used in the AdminGui.
    Players can be muted and their Status is displayed. Use it to determine who
    can select a question, who can answer and who is disconnected.
    """
    def setupTable(self):
        table = PlayerTable(['Nickname', 'IP', 'Status', 'Score'], 'mute')
        for player in self.game.players.items():
            table.addPlayer((player[0], player[1][1], player[1][2], player[1][3]))
        return table

    """
    Communication between AdminGui and GameServer is setup here. The actual logic
    behind most operations pertaining players is in the GameServer.
    - player Mute/Unmute, from table buttons
    - answerChecked, for score modification
    - showing the answer to everyone from QuestionDisplay button;
      note: this signal is not always connected as that button is also used
      for progressing to the next question in addition to showing the answer
    - question selection from ButtonGrid
    """
    def setupSignals(self):
        self.gameStarted.connect(self.game.startGame)
        self.getTable().playersMuted.connect(self.game.mutePlayers)
        self.getTable().playersUnmuted.connect(self.game.unmutePlayers)
        self.answerChecked.connect(self.game.checkAnswer)
        self.getDisplay().buttonClicked.connect(self.game.showAnswer)
        self.getGrid().buttonClicked.connect(self.game.selectQuestion)
        

    """
    The admin user decides whether a question is correct or not regardless of the
    actual answer's formulation.
    """
    def playerBuzzed(self, name):
         ans = QMessageBox.information(self, '', 'Player ' + name + ' is answering.\nIs the answer correct?', QMessageBox.Yes | QMessageBox.No)
         if ans == QMessageBox.Yes:
             add = True
         else:
             add = False
         self.answerChecked.emit(name, add)

    # refreshes the timer every second
    def displayTime(self):
        self.time = self.time.addSecs(1)
        self.getLabel().setText(self.time.toString())

    """
    Same functions as in Gui class, but also handling the double-use button
    """
    def displayQuestion(self, i):
        Gui.displayQuestion(self, i)
        Gui.displayAnswer(self)
        self.displayShowAnswerButton()
        
    def displayAnswer(self):
        Gui.displayAnswer(self)
        self.displayNextQuestionButton()

    def displayNextQuestionButton(self):
        self.getDisplayButton().clicked.disconnect()
        self.getDisplayButton().setText('Next Question')
        self.getDisplayButton().clicked.connect(self.game.nextQuestion)

    def displayShowAnswerButton(self):
        self.getDisplayButton().clicked.disconnect()
        self.getDisplayButton().setText('Show Answer')
        self.getDisplayButton().clicked.connect(self.game.showAnswer)
        
    """
    Since the ButtonGrid is used for question selection, when a new one is created,
    because of a change of round, the buttonClicked signal must be reconnected.
    """
    def updateGrid(self):
        self.getGrid().buttonClicked.disconnect()
        Gui.updateGrid(self)
        self.getGrid().buttonClicked.connect(self.game.selectQuestion)

    """
    The AdminGui can also save the scores in addition to displaying them.
    """
    def displayEndGame(self):
        self.getTable().hideButtons()
        Gui.displayEndGame(self)

    def displayPlot(self, path):
        Gui.displayPlot(self, path)
        
        w = QPushButton('Save scores as PNG')
        w.clicked.connect(self.saveScoresPng)
        self.layout().addWidget(w, 2, 0)
        
        w = QPushButton('Save scores as text file')
        w.clicked.connect(self.saveScoresText)
        self.layout().addWidget(w, 2, 1)

    def saveScoresText(self):
        fileName = QFileDialog.getSaveFileName(self, 'Save Scores', 'scores.txt', 'Text files (*.txt)')
        fp = open(fileName, 'w')
        for player in self.game.players.items():
            fp.write(player[0] + '\t' + str(player[1][3]) + '\n')
        fp.close()

    def saveScoresPng(self):
        fileName = QFileDialog.getSaveFileName(self, 'Save Scores', 'scores.png', 'Images (*.png)')

        self.plotThread = PlotRenderer(self.getScores(), str(fileName), True, 300, self)
        self.plotThread.start()        
Example #5
0
    def saveScoresPng(self):
        fileName = QFileDialog.getSaveFileName(self, 'Save Scores', 'scores.png', 'Images (*.png)')

        self.plotThread = PlotRenderer(self.getScores(), str(fileName), True, 300, self)
        self.plotThread.start()        
Example #6
0
class Gui(QWidget):
    gameStarted = pyqtSignal()

    def __init__(self, parent = None):
        super(Gui, self).__init__(parent)
        self.width = None
        self.height = None
        self.pixmap = None # used for score plot at the end of the game
        
    def startGame(self):
        raise NotImplementedError('startGame is virtual and must be overridden')

    """
    The basic layout is the same - a grid layout with a QuestionDisplay / ButtonGrid in a QStackedLayout
    on the left and a PlayerTable on the right.
    The label on top displays informative messages and is hidden when not used.
    - buttonText is the text shown in the QuestionDisplay's button
    """
    def setupGui(self, buttonText, width, height):
        layout = QGridLayout()
        self.setLayout(layout)

        w = QLabel()
        w.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
        w.setFont(QFont(QFont.defaultFamily(w.font()), 28))
        layout.addWidget(w, 0, 1)
        layout.addLayout(QStackedLayout(), 1, 0)
        layout.addWidget(self.setupTable(), 1, 1)

        # width and height of the QuestionDisplay are specified via the 'size' structure in the 'rules.json' file
        self.width = width
        self.height = height
        
        w = ButtonGrid(self.getRound(), width = self.width, height = self.height)
        self.getStack().addWidget(w) # the ButtonGrid will always have index 0

        w = QuestionDisplay('html', self.getTemplate(), self.getTempPath(), buttonText, width = width, height = height)
        self.getStack().addWidget(w) # the QuestionDisplay will always have index 1

        self.setupSignals()

    def setupTable(self):
        raise NotImplementedError('setupTable is virtual and must be overridden')

    def setupSignals(self):
        raise NotImplementedError('setupSignals is virtual and must be overridden')
        
    def deleteTempFiles(self):
        path = self.getTempPath()
        for name in os.listdir(path):
            os.remove(path + '/' + name)
        os.removedirs(path)

    # messages from different components are prefixed with their source, in this case the gui thread; redirect to create log
    def log(self, message):
        print 'Gui: ' + message

    """
    display* functions modify the visible widget of the QStackedLayoutswitching between the
    ButtonGrid and QuestionDisplay:
    displayQuestion, displayAnswer
    displayGrid : note that, in case of an update (for going to the next round), a different
    function is called to replace the buttons, but in the end displayGrid switches the QStackedLayout's
    current widget
    """
    # displays the current question's statement
    def displayQuestion(self, i):
        self.getGrid().layout().itemAt(i).widget().setEnabled(False)
        self.log('displaying question ' + str(i))
        
        question = self.getQuestion()
        template = self.getTemplateFromQuestion(question)

        # the template contains an answer placeholder as well, which needs to be erased
        answer = question['answer']
        question['answer'] = ''

        statement = Template(template).substitute(question)

        question['answer'] = answer

        self.getDisplay().updateGui(statement, self.getTempPath())
        self.getStack().setCurrentIndex(1)

    def displayAnswer(self):
        question = self.getQuestion()
        template = self.getTemplateFromQuestion(question)
        answer = Template(template).substitute(question)
        self.getDisplay().updateGui(answer, self.getTempPath())

    def displayGrid(self):
        self.log('displaying grid')
        self.getStack().setCurrentIndex(0)

    def updateGrid(self):
        self.log('updating grid')
        oldGrid = self.getStack().takeAt(0).widget() # the old grid is removed from the QStackedLayout
        del oldGrid
        self.getStack().insertWidget(0, ButtonGrid(self.getRound(), width = self.width, height = self.height))

    """
    Displays the plot drawn by the PlotRenderer. The pixmap getPixmap() returns
    is always scaled down to the appropriate width and height.
    """
    def displayEndGame(self):
        self.getLabel().hide()
        w = QLabel()
        w.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
        w.setFont(QFont(QFont.defaultFamily(w.font()), 32))
        w.setText('Rendering plot...')
        self.getStack().addWidget(w)
        self.getStack().setCurrentIndex(2)

        """
        TODO: Color calculation
        hue = 0
        hueInc = 360 / self.getPlayers()
        """
        self.renderPlot()

    def renderPlot(self):
        # generate a random plot path!
        plotPath = tempfile.mkstemp(suffix = '.png', dir = self.getTempPath())[1]
        
        self.plotThread = PlotRenderer(self.getScores(), plotPath, False, 100, self)
        self.plotThread.finishedPlot.connect(self.displayPlot)
        self.plotThread.start()
        self.log('Renering plot')

    def displayPlot(self, path):
        self.getTable().showColors(self.getColors())
        self.pixmap = QPixmap(path)
        self.getPlot().setPixmap(self.pixmap.scaled(self.width, self.height,
                                                    Qt.KeepAspectRatioByExpanding,
                                                    Qt.SmoothTransformation))
        self.resize(self.minimumSize())
        self.adjustSize()
        self.plotThread.exit()

    def setLabelText(self, message):
        self.getLabel().setText(message)

    """
    Getter functions for the different UI elements.
    Accessing them manualy would have been awkward and error-prone because of the layout indices.
    """
    def getLabel(self):
        return self.layout().itemAtPosition(0, 1).widget()

    def getStack(self):
        return self.layout().itemAtPosition(1, 0).layout()

    def getTable(self):
        return self.layout().itemAtPosition(1, 1).widget()

    def getGrid(self):
        return self.getStack().itemAt(0).widget()

    def getDisplay(self):
        return self.getStack().itemAt(1).widget()

    def getDisplayButton(self):
        return self.getDisplay().layout().itemAt(1).widget()

    def getGridButton(self, i):
        return self.getGrid().layout().itemAt(i).widget()

    def getPlot(self):
        return self.getStack().itemAt(2).widget()

    """
    Each player is associated a color by dividing the hue range by the number of
    players. When there are lots of players, there is going to be a 'rainbow'
    effect and some colors might be very similar and quite hard to distinguish.
    Perhaps try changing saturation and value as well...?
    """
    def getColors(self):
        return dict([ (player[0], player[1][1])
                      for player in
                      self.getScores().items() ])
    
    """
    These functions get game related data, which is typically obtained differently for the client and server guis.
    This is why these functions are virtual and their specific implementation is in the derived classes.
    """
    def getRound(self):
        raise NotImplementedError('getRound is virtual and must be overridden')

    def getQuestion(self):
        raise NotImplementedError('getQuestion is virtual and must be overridden')

    def getTempPath(self):
        raise NotImplementedError('getTmpPath is virtual and must be overridden')
    
    def getTemplate(self):
        raise NotImplementedError('getTemplate is virtual and must be overridden')

    def getScores(self):
        raise NotImplementedError('getScores is virtual and must be overridden')

    """
    Helper function, for the case where a question defines a custom html template which needs to be read.
    Is in practice always called, because finding custom templates is not the job of the display{Question,Answer} methods.
    """
    def getTemplateFromQuestion(self, question):
        if 'template' not in question or question == None: 
            return self.getTemplate()
        templateFile = open(self.getTempPath() + '/' + question['template'])
        return ''.join([ line for line in templateFile.readlines() ])