From 44ee9e658c347d74f89634ccf25c37171291d4b9 Mon Sep 17 00:00:00 2001 From: ksmith Date: Mon, 5 Jan 2026 18:34:59 -0700 Subject: [PATCH] Initial commit - MSMD Player with bug fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the Monkey See Monkey Do (MSMD) educational game application. Bug fixes applied: - Fixed AttributeError when showReferenceCreator is disabled in config - Fixed keyboard input with modifiers (Shift+key) not being recognized by making scancode table lookup case-insensitive 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .claude/settings.local.json | 12 + .gitignore | 26 ++ MSMD_multiLevel.py | 778 ++++++++++++++++++++++++++++++++++++ Settings.py | 171 ++++++++ config.ini | 6 + 5 files changed, 993 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 .gitignore create mode 100644 MSMD_multiLevel.py create mode 100644 Settings.py create mode 100644 config.ini diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9a30977 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(python3:*)", + "Bash(source venv/bin/activate)", + "Bash(pip install:*)", + "Bash(python:*)", + "Bash(git init:*)", + "Bash(git add:*)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a818e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# Virtual Environment +venv/ +env/ +ENV/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db +._* + +# Application specific +*.pyc diff --git a/MSMD_multiLevel.py b/MSMD_multiLevel.py new file mode 100644 index 0000000..0e9fa3a --- /dev/null +++ b/MSMD_multiLevel.py @@ -0,0 +1,778 @@ +# -*- coding: utf-8 -*- +""" +Created on Sat Feb 24 17:36:10 2018 + +@author: JohnPaul + +@version: 1.2.4 - updated to be compatible with all screen sizes + +Version History: + 1.2.3 - added multiple base station capability + 1.2.2 - added reference file creation tool + 1.2.1 - added game mode that changes the amount of time the robot can move instead of the robots speed + 1.2.0 - added config file. created option to upgrade robot after every hotspot or every level. Added refresh port button. Added different robot upgrade modes (selectable only in config file) + 1.1.1 - fixed left and right alt key bugs + 1.1.0 - Added keyboard input + 1.0.0 - Initial release (only includes mouse clicks) + +""" + +import sys +import os +import time +from PyQt5.QtWidgets import (QApplication, QWidget, QLineEdit, QFileDialog, +QPushButton, QLabel, QHBoxLayout, QVBoxLayout, QMessageBox, QStackedLayout, +QGraphicsScene, QGraphicsView, QDesktopWidget, QGraphicsEllipseItem, +QGraphicsItem) +from PyQt5.QtGui import QIcon, QImage, QPixmap, QColor, QBrush, QPen +from PyQt5.QtCore import Qt, QRect, pyqtSignal, QThread +#this is the pyserial package (can be installed using pip) +import serial +import serial.tools.list_ports +import json +import configparser +from Settings import Settings +try: + import pyautogui +except : + pass +import pyaudio +import wave +import glob + +textToScanCodeTable = {} +def buildScanCodeTranslationTable (hotSpotDict): + for imageName,metadata in hotSpotDict.items(): + hType = metadata.get('type','') + if hType == "key": + scancode = metadata.get("scancode",0) + name = metadata.get("name",'') + textToScanCodeTable[name] = scancode + print('name: %s, scancode %s' % (name,scancode)) + +class GraphicsView(QGraphicsView): + itemClickedEvent = pyqtSignal(QGraphicsItem, Qt.KeyboardModifiers, Qt.MouseButton) + keyPressed = pyqtSignal(int, str, Qt.KeyboardModifiers) + + def __inti__(self, parent=None): + super(GraphicsView, self).__init__(parent) + + def mousePressEvent(self, event): + scenePosition = self.mapToScene(event.pos()).toPoint() + #print ('moserPressEvent pos %s scenePosition %s' % (event.pos(), scenePosition)) + itemClicked = self.itemAt(scenePosition) + keyModifiers = event.modifiers() + mouseButton = event.button() + self.itemClickedEvent.emit(itemClicked, keyModifiers, mouseButton) + + def keyPressEvent(self, event): + super(GraphicsView, self).keyPressEvent(event) + text = event.text() + code = event.key() + modifiers = event.modifiers() + try: + if code == 16777220: + textFromCode = 'enter' + elif code == 16777217: + textFromCode = 'tab' + else: + textFromCode = chr(code) + except : + textFromCode = text + + translatedScanCode = textToScanCodeTable.get(text.lower(),textToScanCodeTable.get(textFromCode.lower(),0)) + print('keyPressEvent text "%s" textFromCode %s scanCode %s key %s modifiers %s' % ( + text, + textFromCode, + translatedScanCode, + code, + self.convertModifier(modifiers))) + + self.keyPressed.emit(translatedScanCode, textFromCode, modifiers) + + def convertModifier(self, pressedModifiers): + modifierTextList = [] + if(pressedModifiers & Qt.ShiftModifier): + modifierTextList.append('shift') + if(pressedModifiers & Qt.AltModifier): + modifierTextList.append('alt') + if(pressedModifiers & Qt.ControlModifier): + modifierTextList.append('cmd') # on the mac this is the command key + if(pressedModifiers & Qt.MetaModifier): + modifierTextList.append('ctrl')# on the mac this is the control key + + return modifierTextList + + +class App(QWidget): + cleanupEvent = pyqtSignal() + + def __init__(self): + super().__init__() + self.versionNumber = '1.2.4' + self.title = 'Monkey See Monkey Do v'+self.versionNumber + self.left = 10 + self.top = 80 + self.width = 640 + self.height = 100 + self.folderName = '' + self.imageList = [] + self.numImages = 0 + self.currentImageNumber = 0 + self.currentTotalImageNumber = 0 + self.hotSpotFilename = 'hotspots.json' + self.hotSpotFile = None + self.hotSpotSize = 50 + self.currentHotSpot = None + self.startTime = None + self.endTime = None + self.screen = QDesktopWidget().availableGeometry() + print(self.screen) + print('width', self.screen.width(), 'height', self.screen.height()) + self.initUI() + + + def initUI(self): + + self.readConfig() + + self.portLabel = QLabel('Port(s): ', self) + self.portDisplay = QLineEdit(self) + self.portDisplay.setEnabled(False) + self.portRefreshButton = QPushButton(self) + self.portRefreshButton.setToolTip('Press to detect port of connected base station') + self.portRefreshButton.clicked.connect(self.refreshPorts) + self.portRefreshButton.setIcon(QIcon('refresh.png')) + self.portRefreshButton.setFixedWidth(24) + + self.settingsButton = QPushButton() + self.settingsButton.setToolTip('Open the Settings Dialog') + self.settingsButton.setIcon(QIcon('settings.png')) + self.settingsButton.setMaximumWidth(24) + self.settingsButton.clicked.connect(self.openSettings) + + self.connected = False + self.refreshPorts() + if self.showReferenceCreator: + self.referenceCreator = QPushButton('Create Reference', self) + self.referenceCreator.setToolTip('Create a reference file from the selected image set') + self.referenceCreator.clicked.connect(self.createReferenceFile) + self.referenceCreator.setEnabled(False) + + self.folderButton = QPushButton('Select Folder', self) + self.folderButton.setToolTip('Select the folder that contains the content you would like to play') + self.folderButton.clicked.connect(self.folderButtonClicked) + + self.folderLabel = QLabel('Selected Folder:', self) + + self.selectedFolder = QLineEdit(self) + self.selectedFolder.setEnabled(False) + + self.numLevelsLabel = QLabel('Number of Levels:', self) + self.numLevelsDisplay = QLineEdit(self) + self.numLevelsDisplay.setEnabled(False) + + self.numImagesLabel = QLabel('Number of Images:', self) + self.numImagesDisplay = QLineEdit(self) + self.numImagesDisplay.setEnabled(False) + + self.startLabel = QLabel('Press "Start" to begin game', self) + + self.startButton = QPushButton('Start', self) + self.startButton.setToolTip('Start Game') + self.startButton.clicked.connect(self.startButtonClicked) + self.startButton.setEnabled(False) + + self.hboxPort = QHBoxLayout() + self.hboxPort.addWidget(self.portLabel) + self.hboxPort.addWidget(self.portDisplay) + self.hboxPort.addWidget(self.portRefreshButton) + self.hboxPort.addWidget(self.settingsButton) + + self.hbox = QHBoxLayout() + self.hbox.addWidget(self.folderLabel) + self.hbox.addWidget(self.selectedFolder) + + self.hboxNumLevels = QHBoxLayout() + self.hboxNumLevels.addWidget(self.numLevelsLabel) + self.hboxNumLevels.addWidget(self.numLevelsDisplay) + + self.hboxNumImages = QHBoxLayout() + self.hboxNumImages.addWidget(self.numImagesLabel) + self.hboxNumImages.addWidget(self.numImagesDisplay) + + self.vbox = QVBoxLayout() + self.vbox.addLayout(self.hboxPort) + self.vbox.addWidget(self.folderButton) + self.vbox.addLayout(self.hbox) + self.vbox.addLayout(self.hboxNumLevels) + self.vbox.addLayout(self.hboxNumImages) + if self.showReferenceCreator: + self.vbox.addWidget(self.referenceCreator) + self.vbox.addWidget(self.startLabel) + self.vbox.addWidget(self.startButton) + self.vbox.addStretch(4) + + self.startPage = QWidget() + self.startPage.setLayout(self.vbox) + + self.scene = QGraphicsScene() + self.graphicsView = GraphicsView(self.scene) + self.graphicsView.itemClickedEvent.connect(self.hotSpotClickedHandler) + self.graphicsView.keyPressed.connect(self.keyPressedHandler) + + self.graphicsLayout = QVBoxLayout() + self.graphicsLayout.addWidget(self.graphicsView) + self.graphicsLayout.setContentsMargins(0,0,0,0) + + self.gamePage = QWidget() + self.gamePage.setLayout(self.graphicsLayout) + + self.stackedLayout = QStackedLayout() + self.stackedLayout.addWidget(self.startPage) + self.stackedLayout.addWidget(self.gamePage) + self.stackedLayout.setCurrentIndex(0) + + self.setLayout(self.stackedLayout) + self.setWindowTitle(self.title) + self.setGeometry(self.left, self.top, self.width, self.height) + self.setWindowIcon(QIcon('MSMD32.png')) + self.cleanupEvent.connect(self.cleanupStuff) + self.show() + self.bringToFront() + + + def readConfig(self): + self.config = configparser.ConfigParser() + fileCheck = self.config.read('config.ini') + if(fileCheck == []): + QMessageBox.critical(self, 'Config Error!', 'config.ini was not found', QMessageBox.Ok) + self.robotSettings = self.config['robot'] + self.upgradeTrigger = self.robotSettings['upgradeTrigger'] + self.upgradeMode = self.robotSettings['upgradeMode'] + self.minPowerToMove = self.robotSettings['minPowerToMove'] + self.maxPowerToMove = self.robotSettings['maxPowerToMove'] + self.showReferenceCreator = int(self.robotSettings.get('showReferenceCreator', '1')) + + def writeConfig(self): + self.robotSettings['upgradeTrigger'] = self.upgradeTrigger + self.robotSettings['upgradeMode'] = self.upgradeMode + self.robotSettings['minPowerToMove'] = self.minPowerToMove + self.robotSettings['maxPowerToMove'] = self.maxPowerToMove + with open('config.ini', 'w') as configFile: + self.config.write(configFile) + + def openSettings(self): + try: + SettingsIn = {'upgradeTrigger': 'hotspot', + 'upgradeMode': 'left', + 'minPowerToMove': '80'} + self.settingsWindow = Settings(self.robotSettings) + self.settingsWindow.Closing.connect(self.settingsClosed) + self.settingsWindow.show() + self.setDisabled(True) + except: + print ('ERROR - Setting.py Load Failed!') + + def settingsClosed(self, message): + if(message == 'Abort'): + print ('Settigns Aborted!') + elif(message == 'Closed'): + print ('Settings Closed!') + #Set New Settings + newSettings = self.settingsWindow.getSettings() + self.upgradeTrigger = newSettings['upgradeTrigger'] + self.upgradeMode = newSettings['upgradeMode'] + self.minPowerToMove = newSettings['minPowerToMove'] + self.maxPowerToMove = newSettings['maxPowerToMove'] + self.writeConfig() + else: + print ('ERROR - Unknown message returned from Settings.py Window!') + self.setDisabled(False) + + def bringToFront(self): + self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive) + self.activateWindow() + + def refreshPorts(self): + if(self.connected): + for baseStation in self.robot: + baseStation.close() + comPorts = self.findPorts() + if(comPorts): + self.robot = [] + self.portDisplayText = '' + for i, port in enumerate(comPorts): + self.robot.append(serial.Serial(port)) + self.robot[i].baudrate = 115200 + self.robot[i].timeout = 0.05 + self.portDisplayText += (port + ' ') + + self.portDisplay.setText(self.portDisplayText) + self.connected = True + else: + self.robot = [] + self.connected = False + + def folderButtonClicked(self): + self.folderName = QFileDialog.getExistingDirectory(self, "Select Folder Location for Recorded Content") + print(self.folderName) + if os.path.isdir(self.folderName): + self.numLevels = 0 + self.numTotalImages = 0 + self.listOfFilesInSelectedFolder = os.listdir(self.folderName) + self.folderList = [] + self.folderListNameOnly = [] + for name in self.listOfFilesInSelectedFolder: + fullFileName = os.path.join(self.folderName, name) + if os.path.isdir(fullFileName): + result = self.loadLevel(fullFileName) + if(result<0): + return + self.folderList.append(fullFileName) + self.folderListNameOnly.append(name) + self.numLevels += 1 + self.numTotalImages += result + + if(self.numLevels>0): + #multiLevel game selected + self.loadLevel(self.folderList[0]) + self.numLevelsDisplay.setText(str(self.numLevels)) + else: + result = self.loadLevel(self.folderName) + if(result<0): + return + self.numTotalImages = result + self.numLevelsDisplay.setText('1') + + self.currentLevel = 0 + self.numImagesDisplay.setText(str(self.numTotalImages)) + self.startButton.setEnabled(True) + if self.showReferenceCreator: + self.referenceCreator.setEnabled(True) + self.selectedFolder.setText(self.folderName) + else: + QMessageBox.warning(self, 'Folder Error!', 'The folder does not exist!\nPlease select a valid folder', QMessageBox.Ok) + + def loadLevel(self, levelToLoad): + try: + print('Trying to load '+levelToLoad) + print('%s' % levelToLoad+os.path.sep+self.hotSpotFilename) + self.hotSpotFile = open(levelToLoad+os.path.sep+self.hotSpotFilename, 'r') + self.hotSpotDict = json.load(self.hotSpotFile) + self.numHotSpotRecords = len(self.hotSpotDict) + #self.hotSpotCsv = csv.reader(self.hotSpotFile) + #next(self.hotSpotCsv) + #self.numHotSpotRecords = sum(1 for row in self.hotSpotCsv) + #self.hotSpotFile.seek(0) + #next(self.hotSpotCsv) #skip column labels on first line + self.hotSpotFile.close() + buildScanCodeTranslationTable(self.hotSpotDict) + except IOError: + QMessageBox.critical(self, 'Error: No hotspots.json', 'hotspots.json does not exist\nA Hot Spot file is required to play the game. Please select a complete and valid content folder', QMessageBox.Ok) + self.selectedFolder.setText('Error: No hotspots.json') + return -1 + self.imageList = [] + try: + for imageFile in sorted((imfile for imfile in os.listdir(levelToLoad) if imfile.endswith('.png'))): + + self.imageList.append(QImage(levelToLoad+os.path.sep+imageFile)) + + except IOError: + QMessageBox.critical(self, 'Error: images reading', 'Images could not be read\nPlease select a complete and valid content folder', QMessageBox.Ok) + return -1 + self.numImages = len(self.imageList)-1 + if(self.numImages != self.numHotSpotRecords): + QMessageBox.critical(self, 'Error: number of images in level "'+str(levelToLoad)+'" do not match the number of hot spot records', QMessageBox.Ok) + return -1 + return self.numImages + + def startButtonClicked(self): + print('start') + self.stackedLayout.setCurrentIndex(1) + self.paintImageIndex(0) + self.showMaximized() + + if(self.upgradeTrigger == 'level'): + self.setPower((int(self.minPowerToMove)*100)//255) + + self.startTime = time.time() + + def paintImageIndex(self, imageNumber): + if(self.upgradeTrigger == 'hotspot'): + powerLevel = (self.currentTotalImageNumber/(self.numTotalImages-1))*100 + print('power:', powerLevel, ' currentTotalImageNum:', self.currentTotalImageNumber, ' numTotalImages:', self.numTotalImages) + self.setPower(powerLevel) + + self.scene.clear() + print('current image number:', imageNumber) + self.nextHotSpotInput = self.hotSpotDict[str(self.currentImageNumber).zfill(6)] + print('nextHotSpotInput', self.nextHotSpotInput) + self.currentPixmap = QPixmap.fromImage(self.imageList[imageNumber]).copy(QRect(0,0,1920,1020)).scaled(self.screen.width(), self.screen.height(), aspectRatioMode=Qt.IgnoreAspectRatio) + + self.scene.addPixmap(self.currentPixmap) + + self.currentInputModifiers = self.simplifyModifierList(self.nextHotSpotInput['modifiers']) + + if(self.nextHotSpotInput['type'] == 'mouse'): + commandString = '' + if self.currentInputModifiers != []: + commandString += 'Press ' + for mod in self.currentInputModifiers: + commandString += mod + ' + ' + commandString += 'Click ' + self.currentMouseButton = self.nextHotSpotInput['button'] + if(self.currentMouseButton == 'right'): + pen = QPen(QColor(0,0,255,128)) + commandString += 'right mouse button' + elif(self.currentMouseButton == 'left'): + pen = QPen(QColor(255,0,0,128)) + commandString += 'left mouse button' + elif(self.currentMouseButton == 'middle'): + pen = QPen(QColor(0,255,0,128)) + commandString += 'scroll wheel (middle mouse button)' + else: + pen = QPen(QColor(0,0,0,128)) + xScale = self.screen.width()/1920 + yScale = self.screen.height()/1020 + if(xScale>yScale): + minScale = yScale + else: + minScale = xScale + scaledHotSpotSize = self.hotSpotSize*minScale + xPosition = self.nextHotSpotInput['position'][0]*xScale + yPosition = self.nextHotSpotInput['position'][1]*yScale + print('next hotspot pos x %s y %s' %(xPosition,yPosition)) + brush = QBrush(QColor(180, 180, 180, 100)) + self.currentHotSpot = QGraphicsEllipseItem() + self.currentHotSpot.setRect(xPosition-scaledHotSpotSize/2, yPosition-scaledHotSpotSize/2, scaledHotSpotSize, scaledHotSpotSize) + self.currentHotSpot.setBrush(brush) + self.currentHotSpot.setPen(pen) + self.scene.addItem(self.currentHotSpot) + self.currentInputKey = -1 + elif(self.nextHotSpotInput['type'] == 'key'): + #print('key') + self.currentInputKey = self.nextHotSpotInput['scancode'] + commandString = 'Press ' + for mod in self.currentInputModifiers: + commandString += mod + commandString += ' + ' + commandString += self.nextHotSpotInput['name'] + self.currentHotSpot = 'not a hotspot' + else: + QMessageBox.critical(self, 'Error: hotSpotInput type is incorrect. got: "'+self.nextHotSpotInput['type']+'" expected: "key" or "mouse"', QMessageBox.Ok) + + self.setWindowTitle(self.title + ' ' + commandString) + + def playSound(self): + + class SoundThread(QThread): + signal = pyqtSignal('PyQt_PyObject') + def __init__ (self, soundFilename): + super().__init__() + + self.soundFilename = soundFilename + + def run (self): + + #define stream chunk + chunk = 1024 + + #open a wav format music + f = wave.open(self.soundFilename,"rb") + #instantiate PyAudio + p = pyaudio.PyAudio() + #open stream + stream = p.open(format = p.get_format_from_width(f.getsampwidth()), + channels = f.getnchannels(), + rate = f.getframerate(), + output = True) + #read data + data = f.readframes(chunk) + + #play stream + while data: + stream.write(data) + data = f.readframes(chunk) + + #stop stream + stream.stop_stream() + stream.close() + + #close PyAudio + p.terminate() + self.signal.emit(soundFilename) + + soundFilename = '%s%ssound%s.wav'% (self.folderName, os.path.sep, self.currentImageNumber) + if not os.path.isfile(soundFilename): + return + try: + self.soundThread = SoundThread(soundFilename) + self.soundThread.signal.connect(self.soundFinished) + self.soundThread.start() + except: + print('something when wrong with the sound thread') + + def soundFinished (self, soundFile): + print('finished playing %s' % soundFile) + + def hotSpotClickedHandler(self, itemClicked, modifiers, mouseButton): + + print('itemClicked %s, self.currentHotSpot %s, mouseButton %s' % (itemClicked, self.currentHotSpot, mouseButton)) + + if itemClicked is self.currentHotSpot: + if self.checkModifierMatch(modifiers): + if self.checkButtonMatch(mouseButton): + #print('clicked on hot spot!') + + self.playSound() + + self.currentImageNumber += 1 + self.currentTotalImageNumber += 1 + if self.currentImageNumber >= self.numImages: + self.levelCompleted() + else: + self.paintImageIndex(self.currentImageNumber) + else: + #print('wrong mouse button clicked') + a = 0 + else: + #print("modifiers don't match") + a = 0 + else: + #print('wrong spot clicked') + a = 0 + + def checkButtonMatch(self, pressedMouseButton): + if pressedMouseButton == Qt.LeftButton: + pressedMouseButtonString = 'left' + if pressedMouseButton == Qt.RightButton: + pressedMouseButtonString = 'right' + if pressedMouseButton == Qt.MiddleButton: + pressedMouseButtonString = 'middle' + + return self.currentMouseButton == pressedMouseButtonString + + + def keyPressedHandler(self, nativeScanCode, keyText, modifiers): + print('scanCode %s, currentInputKey %s' % (nativeScanCode, self.currentInputKey)) + if (nativeScanCode == self.currentInputKey) and self.checkModifierMatch(modifiers): + #print('pressed correct key (or key combination)') + + self.playSound() + + self.currentImageNumber += 1 + self.currentTotalImageNumber += 1 + if self.currentImageNumber >= self.numImages: + self.levelCompleted() + else: + self.paintImageIndex(self.currentImageNumber) + else: + #print('wrong key or key combination pressed') + a = 0 + + def checkModifierMatch(self, pressedModifiers): + modifierTextList = [] + if(pressedModifiers & Qt.ShiftModifier): + modifierTextList.append('shift') + if(pressedModifiers & Qt.AltModifier): + modifierTextList.append('alt') + if(pressedModifiers & Qt.ControlModifier): + #modifierTextList.append('ctrl') + modifierTextList.append('cmd') # on the mac this is the command key + if(pressedModifiers & Qt.MetaModifier): + modifierTextList.append('ctrl')# on the mac this is the control key + #modifierTextList.append('win') + return set(modifierTextList) == set(self.currentInputModifiers) + + def simplifyModifierList(self, modifierList): + tempSet = set() + for item in modifierList: + if item == 'left shift': + tempSet.add('shift') + elif item == 'right shift': + tempSet.add('shift') + elif item == 'left ctrl': + tempSet.add('ctrl') + elif item == 'right ctrl': + tempSet.add('ctrl') + elif item == 'left alt': + tempSet.add('alt') + elif item == 'right alt': + tempSet.add('alt') + else: + tempSet.add(item) + return list(tempSet) + + def levelCompleted(self): + print('completed level: ', self.currentLevel+1) + if(self.upgradeTrigger == 'level'): + powerLevel = (self.currentLevel/(self.numLevels-1))*100 + self.setPower(powerLevel) + + self.currentLevel += 1 + if(self.currentLevel>=self.numLevels): + self.gameCompleted() + else: + self.currentImageNumber = 0 + self.loadLevel(self.folderList[self.currentLevel]) + self.paintImageIndex(0) + + def gameCompleted(self): + self.endTime = time.time() + self.scene.clear() + self.currentHotSpot = None + self.currentImageNumber = 0 + self.currentTotalImageNumber = 0 + self.currentPixmap = None + self.currentPixmap = QPixmap.fromImage(self.imageList[self.numImages]).copy(QRect(0,0,1920,1020)).scaled(self.screen.width(), self.screen.height(), aspectRatioMode=Qt.IgnoreAspectRatio) + self.scene.addPixmap(self.currentPixmap) + buttonReply = QMessageBox.information(self, 'You Win!', 'Congradulations, You Won!\nYou completed the game in ' + "%.2f" % (self.endTime-self.startTime) + ' seconds', QMessageBox.Ok | QMessageBox.Close) + if buttonReply == QMessageBox.Ok: + self.stackedLayout.setCurrentIndex(0) + self.showNormal() + if(self.numLevels>0): + self.loadLevel(self.folderList[0]) + else: + self.loadLevel(self.folderName) + self.currentLevel = 0 + + def findPorts(self): + ports = glob.glob('/dev/tty.SLAB_USB*') + + comPortsList = ports + #microcontrollerPort = None + #for port in ports: + # if 'Silicon Labs' in str(port[1]): + # comPortsList.append(port[0]) + return comPortsList + + def setPower(self, powerLevel): + if(powerLevel>100): + raise ValueError('powerLevel cannot be set above 100') + if(powerLevel<0): + raise ValueError('powerLevel cannot be set below 0') + + minPower = int(self.minPowerToMove) + mode = self.upgradeMode + leftPower = minPower + rightPower = minPower + maxPower = int(self.maxPowerToMove) + if(mode == "left"): + if(powerLevel<=50): + leftPower = self.interpolate( powerLevel, 0, 100, minPower, maxPower) + rightPower = self.interpolate(powerLevel, 0, 50, minPower, maxPower) + else: + leftPower = self.interpolate(powerLevel, 0, 100, minPower, maxPower) + rightPower = self.interpolate(powerLevel,50, 100, minPower, maxPower) + elif(mode == "right"): + if(powerLevel<=50): + rightPower = self.interpolate(powerLevel, 0,100, minPower, maxPower) + leftPower = self.interpolate(powerLevel, 0, 50, minPower, maxPower) + else: + rightPower = self.interpolate(powerLevel, 0,100, minPower, maxPower) + leftPower = self.interpolate(powerLevel, 50,100, minPower, maxPower) + elif(mode == "both"): + leftPower = self.interpolate(powerLevel, 0,100, minPower, maxPower) + rightPower = leftPower + elif(mode == "distance"): + #add fuel to the robot "tank" + a=None + else: + raise ValueError('upgradeMode in config.ini does not match any accepted value') + + iLP = int(leftPower) + iRP = int(rightPower) + + #desiredPowerLevel -= 45 + if self.robot: + print('\nconnected to BaseStation, attempting to set power to', powerLevel, ' L:', leftPower, 'R:', rightPower,'\n') + for baseStation in self.robot: + baseStation.write(bytes([0,0,iLP,iRP])+b'\n') + baseStation.write(bytes([0,0,iLP,iRP])+b'\n') + else: + print('BaseStation not connected, cannot change power level') + + def interpolate(self, inputValue, inputMin, inputMax, outputMin, outputMax): + ratio = (inputValue - inputMin)/(inputMax - inputMin) + outputValue = (outputMax - outputMin) * ratio + outputMin + return outputValue + + def closeEvent(self, event): + print('emitting cleanup event') + try: + self.settingsWindow.Abort() + except: + print('ERROR - Could not properly close settings window!') + self.cleanupEvent.emit() + + def cleanupStuff(self): + if self.robot: + for baseStation in self.robot: + baseStation.close() + print('closing') + + def createReferenceFile(self): + referenceFolder = QFileDialog.getExistingDirectory(self, "Select Folder Location for Reference") + self.startTime = time.time() + if os.path.isdir(referenceFolder): + + if(self.numLevels>0): + #multiLevel game selected + print('multiLevelGame Reference started') + for i in range(0,self.numLevels): + #create folder to hold level in reference file + levelFolderName = referenceFolder+os.path.sep+self.folderListNameOnly[i] + os.mkdir(levelFolderName) + + self.loadLevel(self.folderList[i]) + + #start msmd level + self.stackedLayout.setCurrentIndex(1) + self.showMaximized() + time.sleep(0.2) + + for j in range(0, self.numImages): + print('next image: '+str(j)) + self.paintImageIndex(j) + QApplication.processEvents() + + time.sleep(0.05) + imageName = str(self.currentImageNumber).zfill(6) + pyautogui.screenshot(levelFolderName+os.path.sep+imageName+'.png') + + self.currentImageNumber += 1 + self.currentTotalImageNumber += 1 + + self.currentImageNumber = 0 + else: + print('singleLevelGame Reference Started') + self.loadLevel(self.folderName) + self.stackedLayout.setCurrentIndex(1) + self.showMaximized() + + for j in range(0, self.numImages): + print('next image: '+str(j)) + self.paintImageIndex(j) + QApplication.processEvents() + + time.sleep(0.05) + imageName = str(self.currentImageNumber).zfill(6) + pyautogui.screenshot(levelFolderName+os.path.sep+imageName+'.png') + + self.currentImageNumber += 1 + self.currentTotalImageNumber += 1 + + self.currentImageNumber = 0 + + self.gameCompleted() + + + + + +if __name__ == '__main__': + app = 0 + app = QApplication(sys.argv) + ex = App() + sys.exit(app.exec_()) diff --git a/Settings.py b/Settings.py new file mode 100644 index 0000000..764fc9e --- /dev/null +++ b/Settings.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Apr 20 18:11:20 2018 + +@author: Nick +""" + +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QIcon +from PyQt5.QtWidgets import (QHBoxLayout, QVBoxLayout, QGridLayout, QComboBox, QLabel, QSpinBox, QGroupBox, QPushButton, QWidget, QFrame, QSpacerItem, QSizePolicy) + +class QHLine(QFrame): + def __init__(self): + super(QHLine, self).__init__() + self.setFrameShape(QFrame.HLine) + self.setFrameShadow(QFrame.Sunken) + +class Settings(QWidget): + + Closing = pyqtSignal(str) + + def __init__(self, SettingsIn): + super(QWidget, self).__init__() + + self.__UserAbort = True + + #-----Widget Settings----- + self.setWindowIcon(QIcon('MSMD32.png')) + self.setWindowTitle('MSMD Settings') + + #Remove Maximize and Minimize buttons + self.setWindowFlags(self.windowFlags() & ~Qt.WindowMinMaxButtonsHint) + + #-----Widget Lists----- + + MasterUpgradeTriggers = ['Folder', 'Hotspot'] + MasterUpgradeMode = ['Both', 'Left', 'Right', 'Distance'] + + TriggerIdx = [x.lower() for x in MasterUpgradeTriggers].index(SettingsIn['upgradeTrigger']) + ModeIdx = [x.lower() for x in MasterUpgradeMode].index(SettingsIn['upgradeMode']) + + #-----Widgets----- + + UpgradeFrame = QGroupBox() + UpgradeFrame.setTitle('Upgrades') + + SpeedFrame = QGroupBox() + SpeedFrame.setTitle('Speed') + + self.UpgradeTrigger = QComboBox() + self.UpgradeTrigger.setToolTip('Set robot upgrade interval to hotspots or levels') + self.UpgradeTrigger.setFixedWidth(80) + for Trigger in MasterUpgradeTriggers: + self.UpgradeTrigger.addItem(Trigger) + self.UpgradeTrigger.setCurrentIndex(TriggerIdx) + + self.UpgradeMode = QComboBox() + self.UpgradeMode.setToolTip('Set robot upgrade mode') + self.UpgradeMode.setFixedWidth(80) + for Mode in MasterUpgradeMode: + self.UpgradeMode.addItem(Mode) + self.UpgradeMode.setCurrentIndex(ModeIdx) + + self.MinPower = QSpinBox() + self.MinPower.setToolTip('Set minimum power for robot to start moving') + self.MinPower.setMaximum(255) + self.MinPower.setMinimum(0) + self.MinPower.setSingleStep(5) + self.MinPower.setFixedWidth(60) + self.MinPower.setValue(int(SettingsIn['minPowerToMove'])) + + self.MaxPower = QSpinBox() + self.MaxPower.setToolTip('Set maximum power for robot to start moving') + self.MaxPower.setMaximum(255) + self.MaxPower.setMinimum(0) + self.MaxPower.setSingleStep(5) + self.MaxPower.setFixedWidth(60) + self.MaxPower.setValue(int(SettingsIn['maxPowerToMove'])) + + SetButton = QPushButton() + SetButton.setToolTip('Use the current settings') + SetButton.setText('Set') + SetButton.setFixedWidth(80) + SetButton.clicked.connect(self.__Close) + + CancelButton = QPushButton() + CancelButton.setToolTip('Cancel') + CancelButton.setText('Cancel') + CancelButton.setFixedWidth(80) + CancelButton.clicked.connect(self.Abort) + + #-----Layouts----- + + hlayout1 = QHBoxLayout() + hlayout2 = QHBoxLayout() + glayout1 = QGridLayout() + glayout2 = QGridLayout() + vlayout3 = QVBoxLayout() + + glayout1.addWidget(QLabel('Upgrade Trigger'), 1, 1) + glayout1.addWidget(self.UpgradeTrigger, 1, 3) + glayout1.addWidget(QLabel('Upgrade Mode'), 3, 1) + glayout1.addWidget(self.UpgradeMode, 3, 3) + + UpgradeFrame.setLayout(glayout1) + + space = QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding) + + glayout2.addWidget(QLabel('Min Power to Move'), 1, 1) + glayout2.addWidget(self.MinPower, 1, 3) + glayout2.addWidget(QLabel('Max Power to Move'), 3, 1) + glayout2.addWidget(self.MaxPower, 3, 3) + + SpeedFrame.setLayout(glayout2) + + hlayout1.addWidget(UpgradeFrame) + hlayout1.addWidget(SpeedFrame) + + hlayout2.addStretch(1) + hlayout2.addWidget(SetButton) + hlayout2.addWidget(CancelButton) + + vlayout3.addLayout(hlayout1) + vlayout3.addStretch(1) + vlayout3.addWidget(QHLine()) + vlayout3.addLayout(hlayout2) + + self.setLayout(vlayout3) + + def getSettings(self): + out = {'upgradeTrigger': str(self.UpgradeTrigger.currentText()).lower(), + 'upgradeMode': str(self.UpgradeMode.currentText()).lower(), + 'minPowerToMove': str(self.MinPower.value()), + 'maxPowerToMove': str(self.MaxPower.value())} + return(out) + +#============================================================================== +# Input Parameters: none +# Output Returns: none +# +# Description: This function closes the window and emits 'Abort' when the 'x' +# is pressed. +#============================================================================== + def closeEvent(self, event): + if(self.__UserAbort): + self.Closing.emit('Abort') + event.accept() + +#============================================================================== +# Input Parameters: none +# Output Returns: none +# +# Description: This function closes the window and sets the user abort to false +#============================================================================== + def Abort(self): + #Close the application + self.__UserAbort = False + self.Closing.emit('Abort') + self.close() + +#============================================================================== +# Input Parameters: none +# Output Returns: none +# +# Description: This function closes the window and sets the user abort to false +#============================================================================== + def __Close(self): + #Close the application + self.__UserAbort = False + self.Closing.emit('Closed') + self.close() \ No newline at end of file diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..343ebbe --- /dev/null +++ b/config.ini @@ -0,0 +1,6 @@ +[robot] +upgradetrigger = hotspot +upgrademode = both +minpowertomove = 55 +maxpowertomove = 95 +showReferenceCreator = 0