# Copyright 2007 Firas Kraiem . All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met : # # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL # I BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF # THE POSSIBILITY OF SUCH DAMAGE. # # # This is a Python implementation of the classic Mastermind game # made by Firas Kraiem for a project at ENIB. Please read the # README file for further information. This code is licensed under # a 2-clause BSD-style license (see above). # # This is the GUI version of the game. It needs PyGTK 2.0 or greater. # See the README for information about installing PyGTK. import pygtk pygtk.require('2.0') import gtk import masterMind.mmBase from functions import * class Base : gameStarted = False def __init__(self) : #first, we create the main window and a VBox in it self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) path = self.imagePath('newBackground.png') self.backgroundPixbuf = gtk.gdk.pixbuf_new_from_file(path) self.backgroundPixmap = self.backgroundPixbuf.render_pixmap_and_mask()[0] del self.backgroundPixbuf self.window.set_app_paintable(True) self.window.resize(470, 590) self.window.realize() self.window.window.set_back_pixmap(self.backgroundPixmap, False) del self.backgroundPixmap self.window.set_title('Mastermind') self.window.connect("delete_event", self.quitGame) self.window.set_resizable(True) self.box = gtk.VBox(False, 0) self.window.add(self.box) #Here come the menus : #First, the File menu, with the New Game submenu and the Save and Quit items self.menuItems = {} self.menus = {} self.menus['file'] = gtk.Menu() for i in ['New Game', 'Quit'] : self.menuItems[i] = gtk.MenuItem(i) self.menus['file'].append(self.menuItems[i]) self.menuItems[i].show() #The New Game submenu, with one item for each level self.menus['newGame'] = gtk.Menu() j = 1 for i in ['Beginner', 'Intermediate', 'Advanced', 'Expert'] : self.menuItems[i] = gtk.MenuItem(i) self.menuItems[i].connect_object('activate', self.startGameClicked, j) self.menus['newGame'].append(self.menuItems[i]) self.menuItems[i].show() j += 1 #The Help menu, with the Highscores and About items self.menus['help'] = gtk.Menu() for i in ['Highscores', 'About'] : self.menuItems[i] = gtk.MenuItem(i) self.menus['help'].append(self.menuItems[i]) self.menuItems[i].show() self.menuItems['About'].connect_object('activate', self.oneButtonPopup, Base, 'about') self.menuItems['Highscores'].connect_object('activate', self.oneButtonPopup, Base, 'highScores') self.menuItems['Quit'].connect_object('activate', self.quitGame, None) #And we pack the menus in the menu bar self.menuBar = gtk.MenuBar() self.box.pack_start(self.menuBar, False, False, 0) self.menuBar.show() for i in ['File', 'Help'] : self.menuItems[i] = gtk.MenuItem(i) self.menuItems[i].show() self.menuItems[i].set_submenu(self.menus[i.lower()]) self.menuBar.append(self.menuItems[i]) self.menuItems['New Game'].set_submenu(self.menus['newGame']) #then we make a HBox to contain the game board and buttons self.mainBox = gtk.HBox(False, 0) #and another dict to contain all the image objects self.images = {} #we'll make two boxes for the buttons - one for the "color" buttons and the #other for the OK and Cancel buttons - but first, we need a VBox to contain #them both, so here goes : self.buttonBoxes = gtk.VBox(False, 0) self.colorButtonBox = gtk.VButtonBox() self.colorButtonBox.set_layout(gtk.BUTTONBOX_START) self.colorButtonBox.set_border_width(10) self.colorButtonBox.set_spacing(20) #we create the images for the buttons and add them to our dict #We also create a dict for the buttons themselves. self.buttons = {} for i in ['r', 'g', 'o', 'b', 'y', 'w', 'e', 'q'] : self.images[i] = gtk.Image() self.images[i].set_from_file(self.imagePath('plot_%s.png' % i)) self.images[i].show() if i != 'q' : #The 'q' item is the image with a question mark, so we don't #pack it on a button. self.buttons[i] = gtk.Button() self.buttons[i].add(self.images[i]) self.buttons[i].connect('clicked', self.buttonClicked, i) self.buttons[i].show() self.colorButtonBox.add(self.buttons[i]) self.colorButtonBox.show() #fancy OK and Cancel buttons and a box to contain them self.spButtonBox = gtk.VButtonBox() self.spButtonBox.set_layout(gtk.BUTTONBOX_START) self.spButtonBox.set_border_width(10) self.spButtonBox.set_spacing(10) self.buttons['ok'] = gtk.Button(stock=gtk.STOCK_OK) self.buttons['cancel'] = gtk.Button(stock=gtk.STOCK_CANCEL) for i in ['ok', 'cancel'] : self.buttons[i].show() self.buttons[i].connect('clicked', self.buttonClicked, i) self.spButtonBox.add(self.buttons[i]) self.spButtonBox.show() #This is the "Fixed" container, that will contain the tokens #on the board. self.fixed = gtk.Fixed() self.fixed.show() #we pack and show the last elements self.buttonBoxes.show() self.buttonBoxes.pack_start(self.colorButtonBox, False, False, 0) self.buttonBoxes.pack_end(self.spButtonBox, False, False, 0) self.mainBox.pack_start(self.buttonBoxes, False, False, 0) self.mainBox.pack_start(self.fixed, False, False, 10) self.box.pack_start(self.mainBox, False, False, 0) self.mainBox.show() self.box.show() self.window.show() #finally, we print a nice message in the console print("\nGUI mode successfully started. So far, so good...\n") def main(self) : """ The main GTK loop... """ gtk.main() def closeGame(self, widget, data=None) : """ Closes the main game window. """ print("Good bye !") gtk.main_quit() def imagePath(self, s) : """ The location of the images will differ depending on whether the user installed the game with distutils or not. This fuction determines the correct path to the image file s (s is the filename, e.g. 'foo.png'). """ #begin precondition assert type(s) is str #end preconditions import os, sys if os.path.exists(os.path.join(sys.prefix, 'share', 'masterMind')) : path = os.path.join(sys.prefix, 'share', 'masterMind', 'graphics', s) else : path = os.path.join('..', 'data', 'graphics', s) return path def buttonClicked(self, widget, button) : """ Is called when a button is clicked. All the if's are here because the behaviour of this function varies depending on the state of the game. """ #begin preconditions assert type(button) is str assert button in ['r', 'g', 'o', 'b', 'y', 'w', 'e', 'ok', 'cancel'] #end preconditions if not self.gameStarted : #If the game has not yet been started, print an error message startNewGameFirst(self) else : #"hole" allowed only in Expert level and if we do not already have one self.holeAllowed = (self.level == 4 and 'e' not in self.curAttempt) #This is the "position" of the current attempt, starting from 0 #It will be used to calculate the coordinates of the new token. posOfCurAttempt = len(self.gameImages) - 1 #This is the position of the current "colour" in the current attempt. #It will also be used to calculate the coordinates of the token. curPosInAttempt = len(self.curAttempt) if posOfCurAttempt == self.maxAttempts : #If we've reached the maximum number of attempts... #Should never occur as the game should be stopped before #this is reached but you never know... noMoreAttemptAllowed(self) elif (curPosInAttempt >= 0 and curPosInAttempt <= 6) : #If we have the right to add or remove a colour. That's where #the fun begins, baby ! if (button in ['r', 'g', 'o', 'b', 'y', 'w']) or \ (button == 'e' and self.holeAllowed) : #If we clicked a "colour" button. if curPosInAttempt < 6 : #If we are _really_ allowed to add a new colour, then we do it :p self.addNewColour(self, \ posOfCurAttempt, curPosInAttempt, button) else : #If there are already six colours, we cannot add a new one sixColoursOnly(self) elif button == 'e' : # "and not self.holeAllowed" is implicit #If we clicked the "hole" button but are not allowed #to add one holeNotAllowed(self) elif button == 'ok' : #If we clicked the OK button if curPosInAttempt != 6 : #There must be six colours for the attempt to be valid notSixColours(self) else : self.testAttempt(self, data=None) else : #If we clicked the Cancel button. if curPosInAttempt == 0 : #We cannot delete a colour if there is none ! zeroColours(self) else : #If we can delete a colour, first we destroy the #image widget self.gameImages[-1][-1].destroy() #then we delete it from the array self.gameImages[-1].pop() #And from that other one, too :p self.curAttempt.pop() else : print("""This definitely shouldn't have occured. Self-destruct in 5 seconds. Have a nice day.""") def startGameClicked(self, level) : """ Is called when the player clicks the "New game" menu item. Tests if a game is already started and if so, popups a window asking the player to confirm. """ #begin preconditions assert type(level) is int assert level in [1, 2, 3, 4] #end preconditions #define the level attribute at class-level so we can use it in #other methods without bothering to pass it each time. self.level = level if self.gameStarted : #popup a menu self.twoButtonsPopup(Base, popup='newGame') else : self.startGame(self.level) def reallyStartNewGamePopupClose(self, widget, newGame) : """ Closes the "really start a ne game ?" popup and starts or does not start a new game, depending on the button clicked. """ #begin precontitions assert type(newGame) is bool #end preconditions self.twoButtonsPopup.destroy() if newGame : self.startGame(self.level) def startGame(self, level) : """ This is called when starting a new game. """ #begin preconditions assert type(level) is int assert level in [1, 2, 3, 4] #end preconditions #set the self.level attribute self.level = level #if there's already a game started, clear the board #before starting a new one if self.gameStarted : for i in self.gameImages : for j in i : j.destroy() #generates the new sequence to guess self.seq = masterMind.mmBase.newGame(level)[0] #sets the max number of attempts permitted depending on the game level if level == 3 : self.maxAttempts = 6 else : self.maxAttempts = 10 #put the tokens with a question mark self.seqImages = [] for i in range(6) : tmpImage = gtk.Image() tmpImage.set_from_file(self.imagePath('plot_q.png')) self.seqImages.append(tmpImage) x = 5 + i*48 self.fixed.put(self.seqImages[i], x, 505) self.seqImages[i].show() self.gameImages = [[]] self.curAttempt = [] self.scores = [[]] self.gameStarted = True def addNewColour(self, widget, row, col, colour) : """ Adds the new token on the board. """ #begin preconditions assert type(row) is int assert type(col) is int assert type(colour) is str #end preconditions #create the GTK image object using the correct image. tmpImage = gtk.Image() tmpImage.set_from_file(self.imagePath('plot_%s.png' % colour)) #add it to the matrix self.gameImages[row].append(tmpImage) #calculate the coordinates x = 5 + col*48 y = 505 - (row+1)*48 #put it at the right place and show it self.fixed.put(self.gameImages[row][col], x, y) self.gameImages[row][col].show() #finally add the colour to the array representing the current attempt #for comparison. self.curAttempt.append(colour) def testAttempt(self, widget, data=None) : """ Is called when the player presses the OK button and there are six colours in the current attempt. Tests the attempt and display the results. """ #begin preconditions assert len(self.curAttempt) == 6 #end preconditions #We must first replace the colour initials with the corresponding #numbers to allow comparison. for i in range(6) : if self.curAttempt[i] == 'r' : self.curAttempt[i] = 1 elif self.curAttempt[i] == 'g' : self.curAttempt[i] = 2 elif self.curAttempt[i] == 'o' : self.curAttempt[i] = 3 elif self.curAttempt[i] == 'b' : self.curAttempt[i] = 4 elif self.curAttempt[i] == 'y' : self.curAttempt[i] = 5 elif self.curAttempt[i] == 'w' : self.curAttempt[i] = 6 elif self.curAttempt[i] == 'e' : self.curAttempt[i] = 7 else : print('Seems you have done something I didn\'t think of...') self.results = masterMind.mmBase.testAttempt(self.curAttempt, self.seq) #In Beginner mode, we're supposed to tell the player what the coulours #he/she correctly guessed are, so we just replace the "question mark" token #with the correct one. And it goes like this : if self.level == 1 and self.results[0] >= 1 : #first, we need to know where the correct colours are, and what they are self.correctColours = masterMind.mmBase.getCorrectColours(self.curAttempt, self.seq) for i in self.correctColours : if self.seq[i] == 1 : tmp = 'r' elif self.seq[i] == 2 : tmp = 'g' elif self.seq[i] == 3 : tmp = 'o' elif self.seq[i] == 4 : tmp = 'b' elif self.seq[i] == 5 : tmp = 'y' elif self.seq[i] == 6 : tmp = 'w' elif self.seq[i] == 7 : tmp = 'e' #Not an integet between 1 and 7 ? God, what have you done ? else : print('What the hell is _wrong_ with you ?') #now that we know which token we need to change #and to which coulour, we just do it :p self.seqImages[i].set_from_file(self.imagePath('plot_%s.png' % tmp)) print("Your last attempt had %s correct colours and %s misplaced colours." % \ (self.results[0], self.results[1])) #Do it graphically... for i in range(2) : self.scores[-1].append(gtk.Label(str(self.results[i]))) #calculate the coordinates of the upper item x = 297 y = 507 - len(self.scores)*48 for i in range(2) : self.fixed.put(self.scores[-1][i], x + i*20, y + i*20) self.scores[-1][i].show() if self.results[0] == 6 : #If the player has correctly guessed... score = len(self.gameImages) * self.level youWin(self, score) #ends the game self.endGame(self, data=None) elif len(self.gameImages) == self.maxAttempts : #If this was the last allowed attempt... print("\nSorry, you lose.") #ends the game self.endGame(self) else : self.gameImages.append([]) self.curAttempt = [] self.scores.append([]) def endGame(self, widget, data=None) : """ Is called when the game ends. Clears the board and ask the player whether he/she wants to start a new game. """ self.gameStarted=False #destroy all the token images on the board for i in self.gameImages : for j in i : j.destroy() for i in self.seqImages : i.destroy() #and print a message in the console print("You can now start a new game or quit.") def quitGame(self, widget, data=None) : """ Is called when the player tries to exit the game. """ if self.gameStarted : #if there's a game started, popup a "really quit ?" window. self.twoButtonsPopup(Base, popup='quit') #and return True because we do not want the main window to be destroyed return True else : self.closeGame(Base) def oneButtonPopup(self, widget, popup) : """ Popups a window with only one button. This is used for the High Scores and About popups. """ #begin preconditions assert type(popup) is str assert popup in ['about', 'highScores'] #end preconditions self.oneButtonPopup = gtk.Window(gtk.WINDOW_TOPLEVEL) self.oneButtonPopup.set_transient_for(self.window) self.oneButtonPopup.set_modal(True) self.oneButtonPopup.set_property('skip-taskbar-hint', True) if popup == 'highScores' : self.oneButtonPopup.set_title('High scores') else : self.oneButtonPopup.set_title('About this game') self.oneButtonPopup.set_resizable(False) self.oneButtonPopup.connect("delete_event", self.oneButtonPopupClose) self.oneButtonPopupBox = gtk.VBox(False, 0) if popup == 'highScores' : hScoresArray = masterMind.mmBase.fetchHighScores() text = "\nHigh Scores :\n\n" for i in hScoresArray : text += "%s :\t\t%s (%s)\n" % (i[0], i[1], i[2]) elif popup == 'about' : text = "\nThis is a Python implementation of the classic Mastermind game \ made by Firas Kraiem for a project at ENIB. This code is released \ under a 2-clause BSD-style license. See the README file for more information.\n" self.oneButtonPopupText = gtk.Label(text) self.oneButtonPopupText.set_justify(gtk.JUSTIFY_CENTER) self.oneButtonPopupText.set_line_wrap(True) self.oneButtonPopupText.show() self.oneButtonPopupButton = gtk.Button(stock=gtk.STOCK_CLOSE) self.oneButtonPopupButton.connect('clicked', self.oneButtonPopupClose) self.oneButtonPopupButton.show() self.oneButtonPopupBox.pack_start(self.oneButtonPopupText, False, False, 0) self.oneButtonPopupBox.pack_start(self.oneButtonPopupButton, False, False, 0) self.oneButtonPopupBox.show() self.oneButtonPopup.add(self.oneButtonPopupBox) self.oneButtonPopup.show() def oneButtonPopupClose(self, widget, data=None) : """ Closes the "one-button" popup. """ self.oneButtonPopup.destroy() def twoButtonsPopup(self, widget, popup) : """ Popups a window with two buttons. This is used for the "really quit ?" and "really start new game ?" dialogs. """ #begin preconditions assert type(popup) is str assert popup in ['quit', 'newGame'] #end preconditions self.twoButtonsPopup = gtk.Window(gtk.WINDOW_TOPLEVEL) self.twoButtonsPopup.set_transient_for(self.window) self.twoButtonsPopup.set_modal(True) self.twoButtonsPopup.set_property('skip-taskbar-hint', True) if popup == 'quit' : title = 'Really quit ?' elif popup == 'newGame' : title = 'Really start a new game ?' self.twoButtonsPopup.set_title(title) self.twoButtonsPopup.set_resizable(False) self.twoButtonsPopupBox = gtk.VBox(False, 0) if popup == 'quit' : text = "\nThere is a game started. Are you sure you want to quit ?\n" elif popup == 'newGame' : text = "\nThere is a game started already.\nAre you sure you want to start a new one ?\n" else : text = "Error." self.twoButtonsPopupText = gtk.Label(text) self.twoButtonsPopupText.set_justify(gtk.JUSTIFY_CENTER) self.twoButtonsPopupText.set_line_wrap(True) self.twoButtonsPopupText.show() if popup == 'quit' : self.twoButtonsPopupYesButton = gtk.Button(stock=gtk.STOCK_QUIT) elif popup == 'newGame' : self.twoButtonsPopupYesButton = gtk.Button(stock=gtk.STOCK_YES) if popup == 'quit' : self.twoButtonsPopupYesButton.connect('clicked', self.closeGame) elif popup == 'newGame' : self.twoButtonsPopupYesButton.connect('clicked', self.reallyStartNewGamePopupClose, True) self.twoButtonsPopupYesButton.show() if popup == 'quit' : self.twoButtonsPopupNoButton = gtk.Button(stock=gtk.STOCK_CANCEL) elif popup == 'newGame' : self.twoButtonsPopupNoButton = gtk.Button(stock=gtk.STOCK_NO) if popup == 'quit' : self.twoButtonsPopupNoButton.connect('clicked', self.twoButtonsPopupClose) elif popup == 'newGame' : self.twoButtonsPopupYesButton.connect('clicked', self.reallyStartNewGamePopupClose, False) self.twoButtonsPopupNoButton.show() self.twoButtonsPopupButtonBox = gtk.HButtonBox() self.twoButtonsPopupButtonBox.set_layout(gtk.BUTTONBOX_SPREAD) self.twoButtonsPopupButtonBox.set_border_width(10) self.twoButtonsPopupButtonBox.set_spacing(20) self.twoButtonsPopupButtonBox.add(self.twoButtonsPopupYesButton) self.twoButtonsPopupButtonBox.add(self.twoButtonsPopupNoButton) self.twoButtonsPopupButtonBox.show() self.twoButtonsPopupBox.pack_start(self.twoButtonsPopupText, False, False, 0) self.twoButtonsPopupBox.pack_start(self.twoButtonsPopupButtonBox, False, False, 0) self.twoButtonsPopupBox.show() self.twoButtonsPopup.add(self.twoButtonsPopupBox) self.twoButtonsPopup.show() def twoButtonsPopupClose(self, widget, data=None) : """ Closes the popup... """ self.twoButtonsPopup.destroy() if __name__ == "__main__" : print("You are not supposed to execute this directly. Please run\nmastermind.py instead.")