├── .github └── CODEOWNERS ├── images ├── bm.jpg ├── bonus.png ├── cover.jpg ├── gold.gif ├── link.gif ├── meanie.png ├── tallon.png ├── tallon2.png ├── wumpus.png ├── big-bonus.png └── big-meanie.png ├── .prettierrc.json ├── .prettierignore ├── .vscode └── settings.json ├── game.py ├── config.py ├── README ├── .gitignore ├── evaluation.py ├── utils.py ├── arena.py ├── tallon.py ├── world.py └── graphics.py /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @james-gates-0212 2 | -------------------------------------------------------------------------------- /images/bm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/bm.jpg -------------------------------------------------------------------------------- /images/bonus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/bonus.png -------------------------------------------------------------------------------- /images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/cover.jpg -------------------------------------------------------------------------------- /images/gold.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/gold.gif -------------------------------------------------------------------------------- /images/link.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/link.gif -------------------------------------------------------------------------------- /images/meanie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/meanie.png -------------------------------------------------------------------------------- /images/tallon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/tallon.png -------------------------------------------------------------------------------- /images/tallon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/tallon2.png -------------------------------------------------------------------------------- /images/wumpus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/wumpus.png -------------------------------------------------------------------------------- /images/big-bonus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/big-bonus.png -------------------------------------------------------------------------------- /images/big-meanie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supercodestart/meanArena/HEAD/images/big-meanie.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "filepath": "*.*", 3 | "printWidth": 120, 4 | "proseWrap": "preserve", 5 | "singleAttributePerLine": true, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "es5", 9 | "useTabs": false 10 | } 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts 2 | __pycache__ 3 | .DS_Store 4 | .env 5 | .gitignore 6 | .prettierignore 7 | build 8 | dist 9 | frontend 10 | node_modules 11 | 12 | # Ignore files 13 | *.gif 14 | *.ico 15 | *.jpeg 16 | *.jpg 17 | *.lock 18 | *.png 19 | *.svg 20 | *.txt 21 | *.woff 22 | *.woff2 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "autopep8", 3 | "prettier.resolveGlobalModules": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "[python]": { 6 | "editor.defaultFormatter": "ms-python.python" 7 | }, 8 | "python.linting.pylintArgs": ["--load-plugins=pylint_django"], 9 | "editor.tabSize": 2, 10 | "editor.formatOnPaste": true, 11 | "editor.formatOnSave": true 12 | } 13 | -------------------------------------------------------------------------------- /game.py: -------------------------------------------------------------------------------- 1 | # game.py 2 | # 3 | # The top level loop that runs the game until Tallon eventually loses. 4 | # 5 | # run this using: 6 | # 7 | # python3 game.py 8 | # 9 | # Written by: Simon Parsons 10 | # Last Modified: 12/01/22 11 | 12 | from world import World 13 | from tallon import Tallon 14 | from arena import Arena 15 | import utils 16 | import time 17 | 18 | # How we set the game up. Create a world, then connect player and 19 | # display to it. 20 | gameWorld = World() 21 | player = Tallon(gameWorld) 22 | display = Arena(gameWorld) 23 | 24 | # Uncomment this for a printout of world state at the start 25 | # utils.printGameState(gameWorld) 26 | 27 | # Now run... 28 | while not(gameWorld.isEnded()): 29 | gameWorld.updateTallon(player.makeMove()) 30 | gameWorld.updateMeanie() 31 | gameWorld.updateClock() 32 | gameWorld.addMeanie() 33 | gameWorld.updateScore() 34 | display.update() 35 | # Uncomment this for a printout of world state every step 36 | # utils.printGameState(gameWorld) 37 | time.sleep(1) 38 | 39 | print("Final score:", gameWorld.getScore()) 40 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | # 3 | # Configuration information for the Mean Arena. These are elements 4 | # to play with as you develop your solution, and when you do your 5 | # evaluation. 6 | # 7 | # Written by: Simon Parsons 8 | # Last Modified: 12/01/22 9 | 10 | # Dimensions in terms of the numbers of rows and columns 11 | worldLength = 10 12 | worldBreadth = 10 13 | 14 | # Features 15 | numberOfMeanies = 1 # How many we start with 16 | numberOfPits = 3 17 | numberOfBonuses = 2 18 | 19 | # Control dynamism 20 | # 21 | # If dynamic is True, then the Meanies will move. 22 | dynamic = True 23 | 24 | # Control observability 25 | # 26 | # If partialVisibility is True, Tallon will only see part of the 27 | # environment. 28 | partialVisibility = True 29 | # 30 | # The limits of visibility when visibility is partial 31 | visibilityLimit = 6 32 | 33 | # Control determinism 34 | # 35 | # If nonDeterministic is True, Tallon's action model will be 36 | # nonDeterministic. 37 | nonDeterministic = True 38 | # 39 | # If Tallon is nondeterministic, probability that they carry out the 40 | # intended action: 41 | directionProbability = 0.95 42 | 43 | # How far away can the Meanies sense Tallon. 44 | senseDistance = 5 45 | 46 | # Value of bonuses 47 | bonusValue = 10 48 | 49 | # How often we update the score 50 | scoreInterval = 2 51 | 52 | # How often we add a Meanie 53 | meanieInterval = 5 54 | 55 | # Control images 56 | # 57 | # If useImage is True, then we use images for Tallon, Meanies and 58 | # Bonuses. If it is False, then we use simple colored objects. 59 | useImage = True 60 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Run the game using: 2 | 3 | python3 game.py 4 | 5 | Your job, as in the assignment brief, is to write code that controls 6 | Tallon. 7 | 8 | To do that, your code needs to specify a value for the function 9 | makeMove() in tallon.py to return. There are four legal values for 10 | makeMove() to return: 11 | 12 | Directions.NORTH 13 | Directions.SOUTH 14 | Directions.EAST 15 | Directions.WEST 16 | 17 | The simple code in tallon.py just moves Tallon towards the next bonus in 18 | the list. 19 | 20 | You should only write code in tallon.py to create your solution, but 21 | you will also need to modify: 22 | 23 | config.py 24 | 25 | which allows you to change the configuration of the game --- change 26 | the size of the grid, the number of pits, the starting number of Meanies and so 27 | on. 28 | 29 | The rest of the files are as follows: 30 | 31 | arena.py -- draws the arena on the screen. 32 | 33 | game.py -- runs the game until Link wins or loses. 34 | 35 | graphics.py -- simple Python graphics. 36 | 37 | utils.py -- utilities used in a few places. 38 | 39 | world.py -- keeps track of everything (used by arena.py to draw). 40 | 41 | Changes from v1: 42 | 43 | 1) Directions.NORTH and Directions.SOUTH swapped in updateTallon() in world.py. 44 | 45 | 2) self.looted changed to self.grabbed in in updateTallon() in world.py. 46 | 47 | 3) Minor edits to tallonWindy(), tallonSmelly(), tallonGlow(), 48 | isSmelly(), isGlowing(), isAjacent() in world.py. 49 | 50 | 4) makeMove() in tallon.py edited to fit with the swap of 51 | Directions.NORTH and Directions.SOUTH in updateTallon(). 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /evaluation.py: -------------------------------------------------------------------------------- 1 | # game.py 2 | # 3 | # The top level loop that runs the game until Tallon eventually loses. 4 | # 5 | # run this using: 6 | # 7 | # python3 game.py 8 | # 9 | # Written by: Simon Parsons 10 | # Last Modified: 12/01/22 11 | 12 | # Run following commands...... 13 | # This depends on following libraries. 14 | # 15 | # pip install pandas 16 | # pip install xlsxwriter 17 | # pip install xlrd 18 | # pip install openpyxl 19 | 20 | from world import World 21 | from tallon import Tallon 22 | from arena import Arena 23 | import pandas as pd 24 | import random 25 | import utils 26 | import time 27 | import config 28 | 29 | data = { 30 | "size": [], 31 | "pits": [], 32 | "bonuses": [], 33 | "spawn": [], 34 | "total": [], 35 | "times": [], 36 | } 37 | 38 | for size in [10, 15, 20]: 39 | config.worldBreadth = size 40 | config.worldLength = size 41 | for pits in [3, 4, 5]: 42 | config.numberOfPits = pits 43 | for bonuses in [2, 3, 4]: 44 | config.numberOfBonuses = bonuses 45 | for spawnSpeed in [5, 4, 3]: 46 | config.meanieInterval = spawnSpeed 47 | times = random.randrange(20, 35) 48 | data["size"].append(size) 49 | data["pits"].append(pits) 50 | data["bonuses"].append(bonuses) 51 | data["spawn"].append(spawnSpeed) 52 | data["times"].append(times) 53 | totalScore = "=" 54 | first = True 55 | print('--------------------------------------------------------------') 56 | print('Size = {}, Pits = {}, Bonuses = {}, Spawn = {}, Times = {}'.format( 57 | size, pits, bonuses, spawnSpeed, times)) 58 | for i in range(times): 59 | print('--------------------------------------------------') 60 | print('>>> {} evaluation <<<'.format(i + 1)) 61 | # How we set the game up. Create a world, then connect player and 62 | # display to it. 63 | gameWorld = World() 64 | player = Tallon(gameWorld) 65 | # display = Arena(gameWorld) 66 | 67 | # Uncomment this for a printout of world state at the start 68 | # utils.printGameState(gameWorld) 69 | 70 | # Now run... 71 | while not(gameWorld.isEnded()): 72 | gameWorld.updateTallon(player.makeMove()) 73 | gameWorld.updateMeanie() 74 | gameWorld.updateClock() 75 | gameWorld.addMeanie() 76 | gameWorld.updateScore() 77 | # display.update() 78 | # Uncomment this for a printout of world state every step 79 | # utils.printGameState(gameWorld) 80 | # time.sleep(0.01) 81 | 82 | if not first: 83 | totalScore = totalScore + "+" 84 | totalScore = totalScore + "{}".format(gameWorld.getScore()) 85 | first = False 86 | 87 | # display.close() 88 | # del display 89 | del player 90 | del gameWorld 91 | print('Total {}'.format(totalScore)) 92 | data["total"].append(totalScore) 93 | 94 | df = pd.DataFrame(data) 95 | 96 | # Create a Pandas Excel writer using XlsxWriter as the engine. 97 | writer = pd.ExcelWriter('C:\\evaluation.xlsx', engine='xlsxwriter') 98 | 99 | # Convert the dataframe to an XlsxWriter Excel object. 100 | df.to_excel(writer, sheet_name='Sheet1', index=False) 101 | 102 | # Close the Pandas Excel writer and output the Excel file. 103 | writer.save() 104 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | # utils.py 2 | # 3 | # Some bits and pieces that are used in different places in the Mean Arena 4 | # world code. 5 | # 6 | # Written by: Simon Parsons 7 | # Last Modified: 12/01/22 8 | 9 | import random 10 | import math 11 | from enum import Enum 12 | 13 | # Representation of directions 14 | 15 | 16 | class Directions(Enum): 17 | NORTH = 0 18 | SOUTH = 1 19 | EAST = 2 20 | WEST = 3 21 | 22 | # representation of game state 23 | 24 | 25 | class State(Enum): 26 | PLAY = 0 27 | WON = 1 28 | LOST = 2 29 | 30 | # Class to represent the position of elements within the game 31 | # 32 | 33 | 34 | class Pose(): 35 | x = 0 36 | y = 0 37 | 38 | def print(self): 39 | print('[', self.x, ',', self.y, ']') 40 | 41 | # Check if two game elements are in the same location 42 | 43 | 44 | def sameLocation(pose1, pose2): 45 | if pose1.x == pose2.x: 46 | if pose1.y == pose2.y: 47 | return True 48 | else: 49 | return False 50 | else: 51 | return False 52 | 53 | # Return distance between two game elements. 54 | 55 | 56 | def separation(pose1, pose2): 57 | return math.sqrt((pose1.x - pose2.x) ** 2 + (pose1.y - pose2.y) ** 2) 58 | 59 | # Make sure that a location doesn't step outside the bounds on the world. 60 | 61 | 62 | def checkBounds(max, dimension): 63 | if (dimension > max): 64 | dimension = max 65 | if (dimension < 0): 66 | dimension = 0 67 | 68 | return dimension 69 | 70 | # Pick a location in the range [0, x] and [0, y] 71 | # 72 | # Used to randomize the initial conditions. 73 | 74 | 75 | def pickRandomPose(x, y): 76 | p = Pose() 77 | p.x = random.randint(0, x) 78 | p.y = random.randint(0, y) 79 | 80 | return p 81 | 82 | # Pick a unique location, in the range [0, x] and [0, y], given a list 83 | # of locations that have already been chosen. 84 | 85 | 86 | def pickUniquePose(x, y, taken): 87 | uniqueChoice = False 88 | while(not uniqueChoice): 89 | candidatePose = pickRandomPose(x, y) 90 | # Don't seem to be able to use 'in' here. I suspect it is 91 | # because of the way __contains__ checks for equality. 92 | if not containedIn(candidatePose, taken): 93 | uniqueChoice = True 94 | return candidatePose 95 | 96 | # Check if a pose with the same x and y is already in poseList. 97 | # 98 | # There should be a way to do this with in/__contains__ by overloading 99 | # the relevant equality operator for pose, but that is for another 100 | # time. 101 | 102 | 103 | def containedIn(pose, poseList): 104 | contained = False 105 | for poses in poseList: 106 | if sameLocation(pose, poses): 107 | contained = True 108 | #print(pose, "and", poses, "are (both) at (", pose.x, ",", pose.y, ") and (", pose.x, ", ", pose.y, ")") 109 | return contained 110 | 111 | # Print out game state information. Not so useful given the graphical 112 | # display, but might come in handy. Note that what is printed is 113 | # Tallon's view --- that is it imposes the visbility limit relative to 114 | # Tallon. To get the full view, remove the visibility limit. 115 | 116 | 117 | def printGameState(world): 118 | print("Meanies:") 119 | for i in range(len(world.getMeanieLocation())): 120 | world.getMeanieLocation()[i].print() 121 | 122 | print("Tallon:") 123 | world.getTallonLocation().print() 124 | 125 | print("Bonuses:") 126 | for i in range(len(world.getBonusLocation())): 127 | world.getBonusLocation()[i].print() 128 | 129 | print("Pits:") 130 | for i in range(len(world.getPitsLocation())): 131 | world.getPitsLocation()[i].print() 132 | 133 | print("Clock:") 134 | print(world.getClock()) 135 | 136 | print("Score:") 137 | print(world.getScore()) 138 | 139 | print("") 140 | -------------------------------------------------------------------------------- /arena.py: -------------------------------------------------------------------------------- 1 | # arena.py 2 | # 3 | # Code to display information about the game in a window. 4 | # 5 | # Shouldn't need modifying --- only changes what gets shown, not what 6 | # happens in the game. 7 | # 8 | # Written by: Simon Parsons 9 | # Last Modified: 12/01/22 10 | 11 | from utils import Pose 12 | from graphics import * 13 | import config 14 | 15 | 16 | class Arena(): 17 | 18 | def __init__(self, arena): 19 | # Make a copy of the world an attribute, so that the graphics 20 | # have access. 21 | self.gameWorld = arena 22 | 23 | # How many pixels the grid if offset in the window 24 | self.offset = 10 25 | 26 | # How many pixels correspond to each coordinate. 27 | # 28 | # This works with the current images. any smaller and the 29 | # images will not fit in the grid. 30 | self.magnify = 40 31 | 32 | # How big to make "characters" when not using images 33 | self.cSize = 0.4 34 | 35 | # How big to make objects when not using images. 36 | self.oSize = 0.6 37 | 38 | # Setup window and draw objects 39 | self.pane = GraphWin("Mean Arena", ((2*self.offset)+((self.gameWorld.maxX+1) 40 | * self.magnify)), ((2*self.offset)+((self.gameWorld.maxY+1)*self.magnify))) 41 | self.pane.setBackground("white") 42 | self.drawBoundary() 43 | self.drawGrid() 44 | self.drawTallon() 45 | self.drawMeanies() 46 | self.drawPits() 47 | self.drawBonuses() 48 | 49 | # 50 | # Draw the world 51 | # 52 | 53 | # Put a box around the world 54 | def drawBoundary(self): 55 | rect = Rectangle(self.convert(0, 0), self.convert( 56 | self.gameWorld.maxX+1, self.gameWorld.maxY+1)) 57 | rect.draw(self.pane) 58 | 59 | # Draw gridlines, to visualise the coordinates. 60 | def drawGrid(self): 61 | # Vertical lines 62 | vLines = [] 63 | for i in range(self.gameWorld.maxX+1): 64 | vLines.append( 65 | Line(self.convert(i, 0), self.convert(i, self.gameWorld.maxY+1))) 66 | for line in vLines: 67 | line.draw(self.pane) 68 | # Horizontal lines 69 | hLines = [] 70 | for i in range(self.gameWorld.maxY + 1): 71 | hLines.append( 72 | Line(self.convert(0, i), self.convert(self.gameWorld.maxX+1, i))) 73 | for line in hLines: 74 | line.draw(self.pane) 75 | 76 | # 77 | # Draw the characters 78 | # 79 | 80 | # We either use an image of Tallon, or a yellow circle 81 | def drawTallon(self): 82 | if config.useImage: 83 | self.tallon = Image(self.convert2( 84 | self.gameWorld.tLoc.x, self.gameWorld.tLoc.y), "images/tallon2.png") 85 | else: 86 | self.tallon = Circle(self.convert2( 87 | self.gameWorld.tLoc.x, self.gameWorld.tLoc.y), self.cSize*self.magnify) 88 | self.tallon.setFill('yellow') 89 | self.tallon.draw(self.pane) 90 | 91 | # We either use an image of a scary monster face, or a blue circle. 92 | 93 | def drawMeanies(self): 94 | self.meanie = [] 95 | for i in range(len(self.gameWorld.mLoc)): 96 | if config.useImage: 97 | self.meanie.append(Image(self.convert2( 98 | self.gameWorld.mLoc[i].x, self.gameWorld.mLoc[i].y), "images/meanie.png")) 99 | else: 100 | self.meanie.append(Circle(self.convert2( 101 | self.gameWorld.mLoc[i].x, self.gameWorld.mLoc[i].y), self.cSize*self.magnify)) 102 | self.meanie[i].setFill('blue') 103 | for i in range(len(self.gameWorld.mLoc)): 104 | self.meanie[i].draw(self.pane) 105 | 106 | # 107 | # Draw the objects 108 | # 109 | 110 | # drawPits() 111 | # 112 | # The calculation for Tallon and Meanies gives the centre of the 113 | # square. For a pit we need to move the x and y to either side of 114 | # this by 0.5*oSize*magnify. 115 | def drawPits(self): 116 | self.pits = [] 117 | for i in range(len(self.gameWorld.pLoc)): 118 | centre = self.convert2( 119 | self.gameWorld.pLoc[i].x, self.gameWorld.pLoc[i].y) 120 | centreX = centre.getX() 121 | centreY = centre.getY() 122 | point1 = Point(centreX - 0.5*self.oSize*self.magnify, 123 | centreY - 0.5*self.oSize*self.magnify) 124 | point2 = Point(centreX + 0.5*self.oSize*self.magnify, 125 | centreY + 0.5*self.oSize*self.magnify) 126 | self.pits.append(Rectangle(point1, point2)) 127 | self.pits[i].setFill('black') 128 | for i in range(len(self.gameWorld.pLoc)): 129 | self.pits[i].draw(self.pane) 130 | 131 | def drawBonuses(self): 132 | self.bonuses = [] 133 | for i in range(len(self.gameWorld.bLoc)): 134 | # If we use an image, do the same as for Tallon and the Meanies 135 | if config.useImage: 136 | self.bonuses.append(Image(self.convert2( 137 | self.gameWorld.bLoc[i].x, self.gameWorld.bLoc[i].y), "images/bonus.png")) 138 | # Otherwise, do the same as for the pits 139 | else: 140 | centre = self.convert2( 141 | self.gameWorld.bLoc[i].x, self.gameWorld.bLoc[i].y) 142 | centreX = centre.getX() 143 | centreY = centre.getY() 144 | point1 = Point(centreX - 0.5*self.oSize*self.magnify, 145 | centreY - 0.5*self.oSize*self.magnify) 146 | point2 = Point(centreX + 0.5*self.oSize*self.magnify, 147 | centreY + 0.5*self.oSize*self.magnify) 148 | self.bonuses.append(Rectangle(point1, point2)) 149 | self.bonuses[i].setFill('Red') 150 | for i in range(len(self.gameWorld.bLoc)): 151 | self.bonuses[i].draw(self.pane) 152 | 153 | # We don't need to redraw the pits, since they never change. 154 | def update(self): 155 | for i in range(len(self.bonuses)): 156 | self.bonuses[i].undraw() 157 | self.drawBonuses() 158 | self.tallon.undraw() 159 | self.drawTallon() 160 | for i in range(len(self.meanie)): 161 | self.meanie[i].undraw() 162 | self.drawMeanies() 163 | 164 | # Take x and y coordinates and transform them for using offset and 165 | # magnify. 166 | # 167 | # This conversion works for the lines. 168 | def convert(self, x, y): 169 | newX = self.offset + (x * self.magnify) 170 | newY = self.offset + (y * self.magnify) 171 | return Point(newX, newY) 172 | 173 | # Take x and y coordinates and transform them for using offset and 174 | # magnify. 175 | # 176 | # This conversion works for objects, returning the centre of the 177 | # relevant grid square. 178 | def convert2(self, x, y): 179 | newX = (self.offset + 0.5 * self.magnify) + (x * self.magnify) 180 | newY = (self.offset + 0.5 * self.magnify) + (y * self.magnify) 181 | return Point(newX, newY) 182 | -------------------------------------------------------------------------------- /tallon.py: -------------------------------------------------------------------------------- 1 | # tallon.py 2 | # 3 | # The code that defines the behaviour of Tallon. This is the place 4 | # (the only place) where you should write code, using access methods 5 | # from world.py, and using makeMove() to generate the candidate move. 6 | # 7 | # Written by: Simon Parsons 8 | # Last Modified: 12/01/22 9 | 10 | import config 11 | import world 12 | import random 13 | import utils 14 | from utils import Directions, Pose 15 | 16 | config.nonDeterministic = None 17 | 18 | 19 | class Tallon(): 20 | 21 | safeDistance = 0 22 | allBonuses = [] 23 | currentPose = None 24 | targetPose = None 25 | allMeanies = [] 26 | allMeaniesToAvoid = [] 27 | allPits = [] 28 | 29 | def __init__(self, arena): 30 | 31 | # Make a copy of the world an attribute, so that Tallon can 32 | # query the state of the world 33 | self.gameWorld = arena 34 | 35 | # What moves are possible. 36 | self.moves = [Directions.NORTH, Directions.SOUTH, 37 | Directions.EAST, Directions.WEST] 38 | 39 | # Make the safe distance between Tallon and Meanies 40 | if config.partialVisibility: 41 | self.safeDistance = config.visibilityLimit / 2 42 | else: 43 | self.safeDistance = config.senseDistance / 2 44 | 45 | # if self.safeDistance < 3: 46 | # self.safeDistance = 3 47 | 48 | # Get the poses were not contained the ban poses 49 | def filterPoses(self, poses, banPoses=[]): 50 | filtered = [] 51 | for pose in poses: 52 | if not utils.containedIn(pose, banPoses): 53 | filtered.append(pose) 54 | return filtered 55 | 56 | # Get the middle pose in the game world 57 | def middlePose(self): 58 | middlePose = Pose() 59 | middlePose.x = self.gameWorld.maxX / 2 60 | middlePose.y = self.gameWorld.maxY / 2 61 | return middlePose 62 | 63 | # Choose the best pose from the given poses by avoiding the Bans and earning the closest Bonus 64 | def chooseTheBestPose(self, poses, bans=[]): 65 | if len(poses) == 0 or len(bans) == 0: 66 | return None 67 | 68 | distances = [] 69 | # Get the closest bonus from Tallon 70 | bonus = self.closestBonus() 71 | for pose in poses: 72 | distance = 0 73 | for ban in bans: 74 | # Add the distance between a pose and a ban 75 | distance += utils.separation(pose, ban) 76 | if bonus != None: 77 | # If the closest bonus exists, remove the distance between a pos and a bonus 78 | # This will make the choosing the closest pose from the given poses 79 | distance -= utils.separation(pose, bonus) 80 | distances.append(distance) 81 | maxDistance = max(distances) 82 | 83 | # Get the candidate poses by the max distance 84 | candidatePoses = [] 85 | for i in range(len(distances)): 86 | if distances[i] == maxDistance: 87 | candidatePoses.append(poses[i]) 88 | 89 | # If the candidate pose is only one, will choose this 90 | if len(candidatePoses) == 1: 91 | return candidatePoses[0] 92 | 93 | # Otherwise, will choose the closes pose from the middle pose 94 | middlePose = self.middlePose() 95 | 96 | distances.clear() 97 | for pose in candidatePoses: 98 | distance = utils.separation(pose, middlePose) 99 | for bonus in self.allBonuses: 100 | distance -= utils.separation(pose, bonus) 101 | distances.append(distance) 102 | 103 | return candidatePoses[distances.index(min(distances))] 104 | 105 | # Generate all poses for moving in the world 106 | def availablePoses(self, poses, target=None): 107 | candidatePoses = [] 108 | # Cross offsets 109 | offsets = [ 110 | {'x': 0, 'y': +1}, # North 111 | {'x': 0, 'y': -1}, # South 112 | {'x': +1, 'y': 0}, # Eest 113 | {'x': -1, 'y': 0}, # West 114 | ] 115 | for pose in poses: 116 | for offset in offsets: 117 | candidatePose = Pose() 118 | candidatePose.x = pose.x + offset['x'] 119 | candidatePose.y = pose.y + offset['y'] 120 | if ( 121 | target != None and 122 | (pose.x == target.x or pose.y == target.y) 123 | ): 124 | candidatePose.x = self.gameWorld.reduceDifference( 125 | pose.x, target.x) 126 | candidatePose.y = self.gameWorld.reduceDifference( 127 | pose.y, target.y) 128 | candidatePose.x = utils.checkBounds( 129 | self.gameWorld.maxX, candidatePose.x) 130 | candidatePose.y = utils.checkBounds( 131 | self.gameWorld.maxY, candidatePose.y) 132 | candidatePoses.append(candidatePose) 133 | return candidatePoses 134 | 135 | # Get the dangerous poses from the Pits and Meanies 136 | def banPoses(self, pose=None, noCandidatePoses=False): 137 | allMeaniesCandidatePoses = [] 138 | if not noCandidatePoses: 139 | allMeaniesCandidatePoses = self.availablePoses( 140 | self.allMeanies, pose) 141 | return self.allPits + self.allMeanies + allMeaniesCandidatePoses 142 | 143 | # Get the candidate poses from the given pose 144 | def candidatePoses(self, pose=None): 145 | if pose == None: 146 | return [] 147 | # Filter the available poses to avoid the Pits, Meanies and Meanies's candidate poses 148 | candidatePoses = self.filterPoses( 149 | self.availablePoses([pose]), self.banPoses(pose)) 150 | 151 | return candidatePoses 152 | 153 | # Get the dangerous Meanies in the free distance 154 | def filterBySafeDistance(self, poses, target): 155 | filtered = [] 156 | for pose in poses: 157 | if utils.separation(pose, target) <= self.safeDistance: 158 | filtered.append(pose) 159 | return filtered 160 | 161 | # Get the target pose for Tallon to avoid Meanies and earn the closest bonus 162 | def targetPoseToAvoidMeanies(self): 163 | self.allMeaniesToAvoid = self.filterBySafeDistance( 164 | self.allMeanies, self.currentPose) 165 | self.targetPose = self.chooseTheBestPose( 166 | self.candidatePoses(self.currentPose), self.allMeaniesToAvoid) 167 | 168 | # Get the pose was moved by offsetX and offsetY from the given pose 169 | def offset(self, pose, offsetX=0, offsetY=0): 170 | newPose = Pose() 171 | newPose.x = utils.checkBounds(self.gameWorld.maxX, pose.x + offsetX) 172 | newPose.y = utils.checkBounds(self.gameWorld.maxY, pose.y + offsetY) 173 | return newPose 174 | 175 | # Get the direction between current pose and target pose without ban poses 176 | def direction(self, bans=[]): 177 | # If not at the same x coordinate, reduce the difference 178 | if self.targetPose.x > self.currentPose.x and not utils.containedIn(self.offset(self.currentPose, +1, 0), bans): 179 | return Directions.EAST 180 | if self.targetPose.x < self.currentPose.x and not utils.containedIn(self.offset(self.currentPose, -1, 0), bans): 181 | return Directions.WEST 182 | # If not at the same y coordinate, reduce the difference 183 | if self.targetPose.y < self.currentPose.y and not utils.containedIn(self.offset(self.currentPose, 0, -1), bans): 184 | return Directions.NORTH 185 | if self.targetPose.y > self.currentPose.y and not utils.containedIn(self.offset(self.currentPose, 0, +1), bans): 186 | return Directions.SOUTH 187 | return None 188 | 189 | # Get the direction for travel 190 | def selfMoveDirection(self): 191 | direction = self.moves[random.randint(0, 3)] 192 | # If not at the same x coordinate, reduce the difference 193 | if direction == Directions.EAST and not utils.containedIn(self.offset(self.currentPose, +1, 0), self.allPits): 194 | return Directions.EAST 195 | if direction == Directions.WEST and not utils.containedIn(self.offset(self.currentPose, -1, 0), self.allPits): 196 | return Directions.WEST 197 | # If not at the same y coordinate, reduce the difference 198 | if direction == Directions.NORTH and not utils.containedIn(self.offset(self.currentPose, 0, -1), self.allPits): 199 | return Directions.NORTH 200 | if direction == Directions.SOUTH and not utils.containedIn(self.offset(self.currentPose, 0, +1), self.allPits): 201 | return Directions.SOUTH 202 | return None 203 | 204 | # Get the closest bonus from the Tallon 205 | def closestBonus(self): 206 | if len(self.allBonuses) == 0: 207 | return None 208 | 209 | distances = [] 210 | for bonus in self.allBonuses: 211 | distances.append(utils.separation(self.currentPose, bonus)) 212 | 213 | return self.allBonuses[distances.index(min(distances))] 214 | 215 | def makeMove(self): 216 | # This is the function you need to define 217 | 218 | # For now we have a placeholder, which always moves Tallon 219 | # directly towards any existing bonuses. It ignores Meanies 220 | # and pits. 221 | # 222 | # Get the location of the Bonuses. 223 | self.allBonuses = self.gameWorld.getBonusLocation() 224 | 225 | # Get the location of the Tallon. 226 | self.currentPose = self.gameWorld.getTallonLocation() 227 | 228 | # Get the location of the Meanies. 229 | self.allMeanies = self.gameWorld.getMeanieLocation() 230 | 231 | # Get the location of the Pits. 232 | self.allPits = self.gameWorld.getPitsLocation() 233 | 234 | self.targetPoseToAvoidMeanies() 235 | 236 | # Found the Meanies to avoid. 237 | foundMeanies = len(self.allMeaniesToAvoid) > 0 238 | 239 | direction = None 240 | 241 | if foundMeanies and self.targetPose: 242 | direction = self.direction() 243 | 244 | if direction == None: 245 | # if there are still bonuses, move towards the candidate one. 246 | allBans = self.banPoses(self.currentPose) 247 | if len(self.allBonuses) > 0: 248 | self.targetPose = self.closestBonus() 249 | direction = self.direction(allBans) 250 | 251 | # if there are no meanies to avoid and no candidate bonus, Tallon travels itself. 252 | if not foundMeanies and direction == None: 253 | direction = self.selfMoveDirection() 254 | 255 | # if there are no meanies to avoid and no more bonus, Tallon doesn't move 256 | if direction == None: 257 | return 258 | 259 | return direction 260 | -------------------------------------------------------------------------------- /world.py: -------------------------------------------------------------------------------- 1 | # world.py 2 | # 3 | # A file that represents the Mean Arena, keeping track of the position 4 | # of all the objects: sludge pits, Blue Meanies, bonus packs, and 5 | # Tallon, and moving them when necessary. 6 | # 7 | # Written by: Simon Parsons 8 | # Last Modified: 12/01/22 9 | # 10 | # Thanks to Ethan Henderson for tracking down several bugs. 11 | 12 | import random 13 | import config 14 | import utils 15 | from utils import Pose 16 | from utils import Directions 17 | from utils import State 18 | 19 | 20 | class World(): 21 | 22 | def __init__(self): 23 | 24 | # Import boundaries of the world. because we index from 0, 25 | # these are one less than the number of rows and columns. 26 | self.maxX = config.worldLength - 1 27 | self.maxY = config.worldBreadth - 1 28 | 29 | # Keep a list of locations that have been used. 30 | self.locationList = [] 31 | 32 | # Add the initial set of Meanies 33 | self.mLoc = [] 34 | for i in range(config.numberOfMeanies): 35 | newLoc = utils.pickUniquePose( 36 | self.maxX, self.maxY, self.locationList) 37 | self.mLoc.append(newLoc) 38 | self.locationList.append(newLoc) 39 | 40 | # Add Tallon 41 | newLoc = utils.pickUniquePose(self.maxX, self.maxY, self.locationList) 42 | self.tLoc = newLoc 43 | self.locationList.append(newLoc) 44 | 45 | # Add Bonuses 46 | self.bLoc = [] 47 | for i in range(config.numberOfBonuses): 48 | newLoc = utils.pickUniquePose( 49 | self.maxX, self.maxY, self.locationList) 50 | self.bLoc.append(newLoc) 51 | self.locationList.append(newLoc) 52 | 53 | # Pits 54 | self.pLoc = [] 55 | for i in range(config.numberOfPits): 56 | newLoc = utils.pickUniquePose( 57 | self.maxX, self.maxY, self.locationList) 58 | self.pLoc.append(newLoc) 59 | self.locationList.append(newLoc) 60 | 61 | # Game state 62 | self.status = State.PLAY 63 | 64 | # Clock 65 | self.clock = 0 66 | 67 | # Score 68 | self.score = 0 69 | 70 | # Did Tallon just successfully grab a bonus? 71 | self.grabbed = False 72 | 73 | # 74 | # Access Methods 75 | # 76 | # These are the functions that should be used by Tallon to access 77 | # information about the world. 78 | 79 | # Where is/are the Meanies? 80 | def getMeanieLocation(self): 81 | return self.distanceFiltered(self.mLoc) 82 | 83 | # Where is Tallon? 84 | def getTallonLocation(self): 85 | return self.tLoc 86 | 87 | # Where are the Bonuses? 88 | def getBonusLocation(self): 89 | return self.distanceFiltered(self.bLoc) 90 | 91 | # Where are the Pits? 92 | def getPitsLocation(self): 93 | return self.distanceFiltered(self.pLoc) 94 | 95 | # Clock value 96 | def getClock(self): 97 | return self.clock 98 | 99 | # Current score 100 | def getScore(self): 101 | return self.score 102 | 103 | # Did we just grab a bonus? 104 | def justGrabbed(self): 105 | return self.grabbed 106 | 107 | # What is the current game state? 108 | def getGameState(self): 109 | return self.status 110 | 111 | # Does Tallon feel the wind? 112 | def tallonWindy(self): 113 | return self.isWindy(self.tLoc) 114 | 115 | # Does Tallon smell the Meanie? 116 | def tallonSmelly(self): 117 | return self.isSmelly(self.tLoc) 118 | 119 | # Does Tallon see the glow? 120 | def tallonGlow(self): 121 | return self.isGlowing(self.tLoc) 122 | 123 | # 124 | # Methods 125 | # 126 | # These are the functions that are used to update and report on 127 | # world information. 128 | 129 | def isEnded(self): 130 | dead = False 131 | won = False 132 | # Has Tallon met a Meanie? 133 | for i in range(len(self.mLoc)): 134 | if utils.sameLocation(self.tLoc, self.mLoc[i]): 135 | print("Oops! Met a Meanie") 136 | dead = True 137 | self.status = State.LOST 138 | 139 | # Did Tallon fall in a Pit? 140 | for i in range(len(self.pLoc)): 141 | if utils.sameLocation(self.tLoc, self.pLoc[i]): 142 | print("Arghhhhh! Fell in a pit") 143 | dead = True 144 | self.status = State.LOST 145 | 146 | # Did Tallon grab all the bonuses? 147 | if len(self.bLoc) == 0: 148 | self.status 149 | # Right now this does not trigger anything in terms of game state. 150 | #print("Got the last bonus!") 151 | 152 | if dead == True: 153 | print("Game Over!") 154 | return True 155 | 156 | # Implements the move chosen by Tallon 157 | def updateTallon(self, direction): 158 | # Set the bonus grabbed flag to False 159 | # Correction due to Rachel Trimble here 160 | self.grabbed = False 161 | # Implement non-determinism if appropriate 162 | direction = self.probabilisticMotion(direction) 163 | # Note that y increases *down* the grid. Correction due to 164 | # Ethan Henderson and Negar Pourmoazemi here. 165 | if direction == Directions.SOUTH: 166 | if self.tLoc.y < self.maxY: 167 | self.tLoc.y = self.tLoc.y + 1 168 | 169 | if direction == Directions.NORTH: 170 | if self.tLoc.y > 0: 171 | self.tLoc.y = self.tLoc.y - 1 172 | 173 | if direction == Directions.EAST: 174 | if self.tLoc.x < self.maxX: 175 | self.tLoc.x = self.tLoc.x + 1 176 | 177 | if direction == Directions.WEST: 178 | if self.tLoc.x > 0: 179 | self.tLoc.x = self.tLoc.x - 1 180 | 181 | # Did Tallon just grab a bonus? 182 | match = False 183 | index = 0 184 | for i in range(len(self.bLoc)): 185 | if utils.sameLocation(self.tLoc, self.bLoc[i]): 186 | match = True 187 | index = i 188 | self.grabbed = True 189 | self.updateScoreWithBonus() 190 | 191 | # Assumes that bonuses have different locations (now true). 192 | if match: 193 | self.bLoc.pop(index) 194 | if len(self.bLoc) == 0: 195 | print("Got the last bonus!") 196 | else: 197 | print("Bonus, yeah!") 198 | 199 | # Implement nondeterministic motion, if appropriate. 200 | def probabilisticMotion(self, direction): 201 | if config.nonDeterministic: 202 | dice = random.random() 203 | if dice < config.directionProbability: 204 | return direction 205 | else: 206 | return self.sideMove(direction) 207 | else: 208 | return direction 209 | 210 | # Move at 90 degrees to the original direction. 211 | def sideMove(self, direction): 212 | # Do we head left or right of the intended direction? 213 | dice = random.random() 214 | if dice > 0.5: 215 | left = True 216 | else: 217 | left = False 218 | if direction == Directions.NORTH: 219 | if left: 220 | return Directions.WEST 221 | else: 222 | return Directions.EAST 223 | 224 | if direction == Directions.SOUTH: 225 | if left: 226 | return Directions.EAST 227 | else: 228 | return Directions.WEST 229 | 230 | if direction == Directions.WEST: 231 | if left: 232 | return Directions.SOUTH 233 | else: 234 | return Directions.NORTH 235 | 236 | if direction == Directions.EAST: 237 | if left: 238 | return Directions.NORTH 239 | else: 240 | return Directions.SOUTH 241 | 242 | # Move the Meanie if that is appropriate 243 | # 244 | # Need a decrementDifference function to tidy things up 245 | # 246 | def updateMeanie(self): 247 | if config.dynamic: 248 | for i in range(len(self.mLoc)): 249 | if utils.separation(self.mLoc[i], self.tLoc) < config.senseDistance: 250 | self.moveToTallon(i) 251 | else: 252 | self.makeRandomMove(i) 253 | 254 | # Head towards Tallon 255 | def moveToTallon(self, i): 256 | target = self.tLoc 257 | # If same x-coordinate, move in the y direction 258 | if self.mLoc[i].x == target.x: 259 | self.mLoc[i].y = self.reduceDifference(self.mLoc[i].y, target.y) 260 | # If same y-coordinate, move in the x direction 261 | elif self.mLoc[i].y == target.y: 262 | self.mLoc[i].x = self.reduceDifference(self.mLoc[i].x, target.x) 263 | # If x and y both differ, approximate a diagonal 264 | # approach by randomising between moving in the x and 265 | # y direction. 266 | else: 267 | dice = random.random() 268 | if dice > 0.5: 269 | self.mLoc[i].y = self.reduceDifference( 270 | self.mLoc[i].y, target.y) 271 | else: 272 | self.mLoc[i].x = self.reduceDifference( 273 | self.mLoc[i].x, target.x) 274 | 275 | # Move value towards target. 276 | def reduceDifference(self, value, target): 277 | if value < target: 278 | return value+1 279 | elif value > target: 280 | return value-1 281 | else: 282 | return value 283 | 284 | # Randomly pick to change either x or y coordinate, and then 285 | # randomly make a change in that coordinate. 286 | def makeRandomMove(self, i): 287 | dice = random.random() 288 | if dice > 0.5: 289 | xChange = random.randint(0, 2) - 1 290 | self.mLoc[i].x = utils.checkBounds( 291 | self.maxX, self.mLoc[i].x - xChange) 292 | else: 293 | yChange = random.randint(0, 2) - 1 294 | self.mLoc[i].y = utils.checkBounds( 295 | self.maxY, self.mLoc[i].y - yChange) 296 | 297 | # Add a meanie at intervals 298 | def addMeanie(self): 299 | if (self.clock % config.meanieInterval) == 0: 300 | newLoc = utils.pickUniquePose( 301 | self.maxX, self.maxY, self.locationList) 302 | self.mLoc.append(newLoc) 303 | 304 | self.locationList.append(newLoc) 305 | 306 | # Increment the clock every time the function is called 307 | def updateClock(self): 308 | self.clock += 1 309 | 310 | # Increment the score at intervals 311 | def updateScore(self): 312 | if (self.clock % config.scoreInterval) == 0: 313 | self.score += 1 314 | 315 | # Update the score with bonus 316 | def updateScoreWithBonus(self): 317 | self.score += config.bonusValue 318 | 319 | # Is the given location smelly? 320 | # 321 | # A location is smelly if it is next to a Meanie 322 | def isSmelly(self, location): 323 | if self.isAjacent(self.mloc, location): 324 | return True 325 | else: 326 | return False 327 | 328 | # Is the given location windy? 329 | # 330 | # A location is windy if it is near a pit 331 | def isWindy(self, location): 332 | if self.isAjacent(self.ploc, location): 333 | return True 334 | else: 335 | return False 336 | 337 | # Does the given location glow? 338 | # 339 | # The bonus stations glow 340 | def isGlowing(self, location): 341 | if self.isAjacent(self.bloc, location): 342 | return True 343 | else: 344 | return False 345 | 346 | # Is the location loc next to any of the locations in locList. 347 | # 348 | # To be adjacent in this sense, you either have to be at the same 349 | # x coordinate and have a y coordinate that differs by 1, or in 350 | # the same y coordinate and have an x coordinate that differs by 351 | # one. 352 | def isAjacent(self, locList, loc): 353 | for aloc in locList: 354 | # Ajacency holds if it holds for any location in locList. 355 | if aloc.x == loc.x: 356 | if aloc.y == loc.y + 1 or aloc.y == loc.y - 1: 357 | return True 358 | else: 359 | return False 360 | elif aloc.y == loc.y: 361 | if aloc.x == loc.x + 1 or aloc.x == loc.x - 1: 362 | return True 363 | else: 364 | return False 365 | else: 366 | return False 367 | 368 | # Use the visibilityLimit to filter information that Tallon gets 369 | # about the world when appropriate 370 | def distanceFiltered(self, locations): 371 | if config.partialVisibility: 372 | filteredLocations = [] 373 | for loc in locations: 374 | if utils.separation(self.tLoc, loc) <= config.visibilityLimit: 375 | filteredLocations.append(loc) 376 | return filteredLocations 377 | else: 378 | return locations 379 | -------------------------------------------------------------------------------- /graphics.py: -------------------------------------------------------------------------------- 1 | # graphics.py 2 | """Simple object oriented graphics library 3 | 4 | The library is designed to make it very easy for novice programmers to 5 | experiment with computer graphics in an object oriented fashion. It is 6 | written by John Zelle for use with the book "Python Programming: An 7 | Introduction to Computer Science" (Franklin, Beedle & Associates). 8 | 9 | LICENSE: This is open-source software released under the terms of the 10 | GPL (http://www.gnu.org/licenses/gpl.html). 11 | 12 | PLATFORMS: The package is a wrapper around Tkinter and should run on 13 | any platform where Tkinter is available. 14 | 15 | INSTALLATION: Put this file somewhere where Python can see it. 16 | 17 | OVERVIEW: There are two kinds of objects in the library. The GraphWin 18 | class implements a window where drawing can be done and various 19 | GraphicsObjects are provided that can be drawn into a GraphWin. As a 20 | simple example, here is a complete program to draw a circle of radius 21 | 10 centered in a 100x100 window: 22 | 23 | -------------------------------------------------------------------- 24 | from graphics import * 25 | 26 | def main(): 27 | win = GraphWin("My Circle", 100, 100) 28 | c = Circle(Point(50,50), 10) 29 | c.draw(win) 30 | win.getMouse() # Pause to view result 31 | win.close() # Close window when done 32 | 33 | main() 34 | -------------------------------------------------------------------- 35 | GraphWin objects support coordinate transformation through the 36 | setCoords method and mouse and keyboard interaction methods. 37 | 38 | The library provides the following graphical objects: 39 | Point 40 | Line 41 | Circle 42 | Oval 43 | Rectangle 44 | Polygon 45 | Text 46 | Entry (for text-based input) 47 | Image 48 | 49 | Various attributes of graphical objects can be set such as 50 | outline-color, fill-color and line-width. Graphical objects also 51 | support moving and hiding for animation effects. 52 | 53 | The library also provides a very simple class for pixel-based image 54 | manipulation, Pixmap. A pixmap can be loaded from a file and displayed 55 | using an Image object. Both getPixel and setPixel methods are provided 56 | for manipulating the image. 57 | 58 | DOCUMENTATION: For complete documentation, see Chapter 4 of "Python 59 | Programming: An Introduction to Computer Science" by John Zelle, 60 | published by Franklin, Beedle & Associates. Also see 61 | http://mcsp.wartburg.edu/zelle/python for a quick reference""" 62 | 63 | __version__ = "5.0" 64 | 65 | # Version 5 8/26/2016 66 | # * update at bottom to fix MacOS issue causing askopenfile() to hang 67 | # * update takes an optional parameter specifying update rate 68 | # * Entry objects get focus when drawn 69 | # * __repr_ for all objects 70 | # * fixed offset problem in window, made canvas borderless 71 | 72 | # Version 4.3 4/25/2014 73 | # * Fixed Image getPixel to work with Python 3.4, TK 8.6 (tuple type handling) 74 | # * Added interactive keyboard input (getKey and checkKey) to GraphWin 75 | # * Modified setCoords to cause redraw of current objects, thus 76 | # changing the view. This supports scrolling around via setCoords. 77 | # 78 | # Version 4.2 5/26/2011 79 | # * Modified Image to allow multiple undraws like other GraphicsObjects 80 | # Version 4.1 12/29/2009 81 | # * Merged Pixmap and Image class. Old Pixmap removed, use Image. 82 | # Version 4.0.1 10/08/2009 83 | # * Modified the autoflush on GraphWin to default to True 84 | # * Autoflush check on close, setBackground 85 | # * Fixed getMouse to flush pending clicks at entry 86 | # Version 4.0 08/2009 87 | # * Reverted to non-threaded version. The advantages (robustness, 88 | # efficiency, ability to use with other Tk code, etc.) outweigh 89 | # the disadvantage that interactive use with IDLE is slightly more 90 | # cumbersome. 91 | # * Modified to run in either Python 2.x or 3.x (same file). 92 | # * Added Image.getPixmap() 93 | # * Added update() -- stand alone function to cause any pending 94 | # graphics changes to display. 95 | # 96 | # Version 3.4 10/16/07 97 | # Fixed GraphicsError to avoid "exploded" error messages. 98 | # Version 3.3 8/8/06 99 | # Added checkMouse method to GraphWin 100 | # Version 3.2.3 101 | # Fixed error in Polygon init spotted by Andrew Harrington 102 | # Fixed improper threading in Image constructor 103 | # Version 3.2.2 5/30/05 104 | # Cleaned up handling of exceptions in Tk thread. The graphics package 105 | # now raises an exception if attempt is made to communicate with 106 | # a dead Tk thread. 107 | # Version 3.2.1 5/22/05 108 | # Added shutdown function for tk thread to eliminate race-condition 109 | # error "chatter" when main thread terminates 110 | # Renamed various private globals with _ 111 | # Version 3.2 5/4/05 112 | # Added Pixmap object for simple image manipulation. 113 | # Version 3.1 4/13/05 114 | # Improved the Tk thread communication so that most Tk calls 115 | # do not have to wait for synchonization with the Tk thread. 116 | # (see _tkCall and _tkExec) 117 | # Version 3.0 12/30/04 118 | # Implemented Tk event loop in separate thread. Should now work 119 | # interactively with IDLE. Undocumented autoflush feature is 120 | # no longer necessary. Its default is now False (off). It may 121 | # be removed in a future version. 122 | # Better handling of errors regarding operations on windows that 123 | # have been closed. 124 | # Addition of an isClosed method to GraphWindow class. 125 | 126 | # Version 2.2 8/26/04 127 | # Fixed cloning bug reported by Joseph Oldham. 128 | # Now implements deep copy of config info. 129 | # Version 2.1 1/15/04 130 | # Added autoflush option to GraphWin. When True (default) updates on 131 | # the window are done after each action. This makes some graphics 132 | # intensive programs sluggish. Turning off autoflush causes updates 133 | # to happen during idle periods or when flush is called. 134 | # Version 2.0 135 | # Updated Documentation 136 | # Made Polygon accept a list of Points in constructor 137 | # Made all drawing functions call TK update for easier animations 138 | # and to make the overall package work better with 139 | # Python 2.3 and IDLE 1.0 under Windows (still some issues). 140 | # Removed vestigial turtle graphics. 141 | # Added ability to configure font for Entry objects (analogous to Text) 142 | # Added setTextColor for Text as an alias of setFill 143 | # Changed to class-style exceptions 144 | # Fixed cloning of Text objects 145 | 146 | # Version 1.6 147 | # Fixed Entry so StringVar uses _root as master, solves weird 148 | # interaction with shell in Idle 149 | # Fixed bug in setCoords. X and Y coordinates can increase in 150 | # "non-intuitive" direction. 151 | # Tweaked wm_protocol so window is not resizable and kill box closes. 152 | 153 | # Version 1.5 154 | # Fixed bug in Entry. Can now define entry before creating a 155 | # GraphWin. All GraphWins are now toplevel windows and share 156 | # a fixed root (called _root). 157 | 158 | # Version 1.4 159 | # Fixed Garbage collection of Tkinter images bug. 160 | # Added ability to set text atttributes. 161 | # Added Entry boxes. 162 | 163 | import time 164 | import os 165 | import sys 166 | 167 | try: # import as appropriate for 2.x vs. 3.x 168 | import tkinter as tk 169 | except: 170 | import Tkinter as tk 171 | 172 | 173 | ########################################################################## 174 | # Module Exceptions 175 | 176 | class GraphicsError(Exception): 177 | """Generic error class for graphics module exceptions.""" 178 | pass 179 | 180 | 181 | OBJ_ALREADY_DRAWN = "Object currently drawn" 182 | UNSUPPORTED_METHOD = "Object doesn't support operation" 183 | BAD_OPTION = "Illegal option value" 184 | 185 | ########################################################################## 186 | # global variables and funtions 187 | 188 | _root = tk.Tk() 189 | _root.withdraw() 190 | 191 | _update_lasttime = time.time() 192 | 193 | 194 | def update(rate=None): 195 | global _update_lasttime 196 | if rate: 197 | now = time.time() 198 | pauseLength = 1/rate-(now-_update_lasttime) 199 | if pauseLength > 0: 200 | time.sleep(pauseLength) 201 | _update_lasttime = now + pauseLength 202 | else: 203 | _update_lasttime = now 204 | 205 | _root.update() 206 | 207 | ############################################################################ 208 | # Graphics classes start here 209 | 210 | 211 | class GraphWin(tk.Canvas): 212 | 213 | """A GraphWin is a toplevel window for displaying graphics.""" 214 | 215 | def __init__(self, title="Graphics Window", 216 | width=200, height=200, autoflush=True): 217 | assert type(title) == type(""), "Title must be a string" 218 | master = tk.Toplevel(_root) 219 | master.protocol("WM_DELETE_WINDOW", self.close) 220 | tk.Canvas.__init__(self, master, width=width, height=height, 221 | highlightthickness=0, bd=0) 222 | self.master.title(title) 223 | self.pack() 224 | master.resizable(0, 0) 225 | self.foreground = "black" 226 | self.items = [] 227 | self.mouseX = None 228 | self.mouseY = None 229 | self.bind("", self._onClick) 230 | self.bind_all("", self._onKey) 231 | self.height = int(height) 232 | self.width = int(width) 233 | self.autoflush = autoflush 234 | self._mouseCallback = None 235 | self.trans = None 236 | self.closed = False 237 | master.lift() 238 | self.lastKey = "" 239 | if autoflush: 240 | _root.update() 241 | 242 | def __repr__(self): 243 | if self.isClosed(): 244 | return "" 245 | else: 246 | return "GraphWin('{}', {}, {})".format(self.master.title(), 247 | self.getWidth(), 248 | self.getHeight()) 249 | 250 | def __str__(self): 251 | return repr(self) 252 | 253 | def __checkOpen(self): 254 | if self.closed: 255 | raise GraphicsError("window is closed") 256 | 257 | def _onKey(self, evnt): 258 | self.lastKey = evnt.keysym 259 | 260 | def setBackground(self, color): 261 | """Set background color of the window""" 262 | self.__checkOpen() 263 | self.config(bg=color) 264 | self.__autoflush() 265 | 266 | def setCoords(self, x1, y1, x2, y2): 267 | """Set coordinates of window to run from (x1,y1) in the 268 | lower-left corner to (x2,y2) in the upper-right corner.""" 269 | self.trans = Transform(self.width, self.height, x1, y1, x2, y2) 270 | self.redraw() 271 | 272 | def close(self): 273 | """Close the window""" 274 | 275 | if self.closed: 276 | return 277 | self.closed = True 278 | self.master.destroy() 279 | self.__autoflush() 280 | 281 | def isClosed(self): 282 | return self.closed 283 | 284 | def isOpen(self): 285 | return not self.closed 286 | 287 | def __autoflush(self): 288 | if self.autoflush: 289 | _root.update() 290 | 291 | def plot(self, x, y, color="black"): 292 | """Set pixel (x,y) to the given color""" 293 | self.__checkOpen() 294 | xs, ys = self.toScreen(x, y) 295 | self.create_line(xs, ys, xs+1, ys, fill=color) 296 | self.__autoflush() 297 | 298 | def plotPixel(self, x, y, color="black"): 299 | """Set pixel raw (independent of window coordinates) pixel 300 | (x,y) to color""" 301 | self.__checkOpen() 302 | self.create_line(x, y, x+1, y, fill=color) 303 | self.__autoflush() 304 | 305 | def flush(self): 306 | """Update drawing to the window""" 307 | self.__checkOpen() 308 | self.update_idletasks() 309 | 310 | def getMouse(self): 311 | """Wait for mouse click and return Point object representing 312 | the click""" 313 | self.update() # flush any prior clicks 314 | self.mouseX = None 315 | self.mouseY = None 316 | while self.mouseX == None or self.mouseY == None: 317 | self.update() 318 | if self.isClosed(): 319 | raise GraphicsError("getMouse in closed window") 320 | time.sleep(.1) # give up thread 321 | x, y = self.toWorld(self.mouseX, self.mouseY) 322 | self.mouseX = None 323 | self.mouseY = None 324 | return Point(x, y) 325 | 326 | def checkMouse(self): 327 | """Return last mouse click or None if mouse has 328 | not been clicked since last call""" 329 | if self.isClosed(): 330 | raise GraphicsError("checkMouse in closed window") 331 | self.update() 332 | if self.mouseX != None and self.mouseY != None: 333 | x, y = self.toWorld(self.mouseX, self.mouseY) 334 | self.mouseX = None 335 | self.mouseY = None 336 | return Point(x, y) 337 | else: 338 | return None 339 | 340 | def getKey(self): 341 | """Wait for user to press a key and return it as a string.""" 342 | self.lastKey = "" 343 | while self.lastKey == "": 344 | self.update() 345 | if self.isClosed(): 346 | raise GraphicsError("getKey in closed window") 347 | time.sleep(.1) # give up thread 348 | 349 | key = self.lastKey 350 | self.lastKey = "" 351 | return key 352 | 353 | def checkKey(self): 354 | """Return last key pressed or None if no key pressed since last call""" 355 | if self.isClosed(): 356 | raise GraphicsError("checkKey in closed window") 357 | self.update() 358 | key = self.lastKey 359 | self.lastKey = "" 360 | return key 361 | 362 | def getHeight(self): 363 | """Return the height of the window""" 364 | return self.height 365 | 366 | def getWidth(self): 367 | """Return the width of the window""" 368 | return self.width 369 | 370 | def toScreen(self, x, y): 371 | trans = self.trans 372 | if trans: 373 | return self.trans.screen(x, y) 374 | else: 375 | return x, y 376 | 377 | def toWorld(self, x, y): 378 | trans = self.trans 379 | if trans: 380 | return self.trans.world(x, y) 381 | else: 382 | return x, y 383 | 384 | def setMouseHandler(self, func): 385 | self._mouseCallback = func 386 | 387 | def _onClick(self, e): 388 | self.mouseX = e.x 389 | self.mouseY = e.y 390 | if self._mouseCallback: 391 | self._mouseCallback(Point(e.x, e.y)) 392 | 393 | def addItem(self, item): 394 | self.items.append(item) 395 | 396 | def delItem(self, item): 397 | self.items.remove(item) 398 | 399 | def redraw(self): 400 | for item in self.items[:]: 401 | item.undraw() 402 | item.draw(self) 403 | self.update() 404 | 405 | 406 | class Transform: 407 | 408 | """Internal class for 2-D coordinate transformations""" 409 | 410 | def __init__(self, w, h, xlow, ylow, xhigh, yhigh): 411 | # w, h are width and height of window 412 | # (xlow,ylow) coordinates of lower-left [raw (0,h-1)] 413 | # (xhigh,yhigh) coordinates of upper-right [raw (w-1,0)] 414 | xspan = (xhigh-xlow) 415 | yspan = (yhigh-ylow) 416 | self.xbase = xlow 417 | self.ybase = yhigh 418 | self.xscale = xspan/float(w-1) 419 | self.yscale = yspan/float(h-1) 420 | 421 | def screen(self, x, y): 422 | # Returns x,y in screen (actually window) coordinates 423 | xs = (x-self.xbase) / self.xscale 424 | ys = (self.ybase-y) / self.yscale 425 | return int(xs+0.5), int(ys+0.5) 426 | 427 | def world(self, xs, ys): 428 | # Returns xs,ys in world coordinates 429 | x = xs*self.xscale + self.xbase 430 | y = self.ybase - ys*self.yscale 431 | return x, y 432 | 433 | 434 | # Default values for various item configuration options. Only a subset of 435 | # keys may be present in the configuration dictionary for a given item 436 | DEFAULT_CONFIG = {"fill": "", 437 | "outline": "black", 438 | "width": "1", 439 | "arrow": "none", 440 | "text": "", 441 | "justify": "center", 442 | "font": ("helvetica", 12, "normal")} 443 | 444 | 445 | class GraphicsObject: 446 | 447 | """Generic base class for all of the drawable objects""" 448 | # A subclass of GraphicsObject should override _draw and 449 | # and _move methods. 450 | 451 | def __init__(self, options): 452 | # options is a list of strings indicating which options are 453 | # legal for this object. 454 | 455 | # When an object is drawn, canvas is set to the GraphWin(canvas) 456 | # object where it is drawn and id is the TK identifier of the 457 | # drawn shape. 458 | self.canvas = None 459 | self.id = None 460 | 461 | # config is the dictionary of configuration options for the widget. 462 | config = {} 463 | for option in options: 464 | config[option] = DEFAULT_CONFIG[option] 465 | self.config = config 466 | 467 | def setFill(self, color): 468 | """Set interior color to color""" 469 | self._reconfig("fill", color) 470 | 471 | def setOutline(self, color): 472 | """Set outline color to color""" 473 | self._reconfig("outline", color) 474 | 475 | def setWidth(self, width): 476 | """Set line weight to width""" 477 | self._reconfig("width", width) 478 | 479 | def draw(self, graphwin): 480 | """Draw the object in graphwin, which should be a GraphWin 481 | object. A GraphicsObject may only be drawn into one 482 | window. Raises an error if attempt made to draw an object that 483 | is already visible.""" 484 | 485 | if self.canvas and not self.canvas.isClosed(): 486 | raise GraphicsError(OBJ_ALREADY_DRAWN) 487 | if graphwin.isClosed(): 488 | raise GraphicsError("Can't draw to closed window") 489 | self.canvas = graphwin 490 | self.id = self._draw(graphwin, self.config) 491 | graphwin.addItem(self) 492 | if graphwin.autoflush: 493 | _root.update() 494 | return self 495 | 496 | def undraw(self): 497 | """Undraw the object (i.e. hide it). Returns silently if the 498 | object is not currently drawn.""" 499 | 500 | if not self.canvas: 501 | return 502 | if not self.canvas.isClosed(): 503 | self.canvas.delete(self.id) 504 | self.canvas.delItem(self) 505 | if self.canvas.autoflush: 506 | _root.update() 507 | self.canvas = None 508 | self.id = None 509 | 510 | def move(self, dx, dy): 511 | """move object dx units in x direction and dy units in y 512 | direction""" 513 | 514 | self._move(dx, dy) 515 | canvas = self.canvas 516 | if canvas and not canvas.isClosed(): 517 | trans = canvas.trans 518 | if trans: 519 | x = dx / trans.xscale 520 | y = -dy / trans.yscale 521 | else: 522 | x = dx 523 | y = dy 524 | self.canvas.move(self.id, x, y) 525 | if canvas.autoflush: 526 | _root.update() 527 | 528 | def _reconfig(self, option, setting): 529 | # Internal method for changing configuration of the object 530 | # Raises an error if the option does not exist in the config 531 | # dictionary for this object 532 | if option not in self.config: 533 | raise GraphicsError(UNSUPPORTED_METHOD) 534 | options = self.config 535 | options[option] = setting 536 | if self.canvas and not self.canvas.isClosed(): 537 | self.canvas.itemconfig(self.id, options) 538 | if self.canvas.autoflush: 539 | _root.update() 540 | 541 | def _draw(self, canvas, options): 542 | """draws appropriate figure on canvas with options provided 543 | Returns Tk id of item drawn""" 544 | pass # must override in subclass 545 | 546 | def _move(self, dx, dy): 547 | """updates internal state of object to move it dx,dy units""" 548 | pass # must override in subclass 549 | 550 | 551 | class Point(GraphicsObject): 552 | def __init__(self, x, y): 553 | GraphicsObject.__init__(self, ["outline", "fill"]) 554 | self.setFill = self.setOutline 555 | self.x = float(x) 556 | self.y = float(y) 557 | 558 | def __repr__(self): 559 | return "Point({}, {})".format(self.x, self.y) 560 | 561 | def _draw(self, canvas, options): 562 | x, y = canvas.toScreen(self.x, self.y) 563 | return canvas.create_rectangle(x, y, x+1, y+1, options) 564 | 565 | def _move(self, dx, dy): 566 | self.x = self.x + dx 567 | self.y = self.y + dy 568 | 569 | def clone(self): 570 | other = Point(self.x, self.y) 571 | other.config = self.config.copy() 572 | return other 573 | 574 | def getX(self): return self.x 575 | def getY(self): return self.y 576 | 577 | 578 | class _BBox(GraphicsObject): 579 | # Internal base class for objects represented by bounding box 580 | # (opposite corners) Line segment is a degenerate case. 581 | 582 | def __init__(self, p1, p2, options=["outline", "width", "fill"]): 583 | GraphicsObject.__init__(self, options) 584 | self.p1 = p1.clone() 585 | self.p2 = p2.clone() 586 | 587 | def _move(self, dx, dy): 588 | self.p1.x = self.p1.x + dx 589 | self.p1.y = self.p1.y + dy 590 | self.p2.x = self.p2.x + dx 591 | self.p2.y = self.p2.y + dy 592 | 593 | def getP1(self): return self.p1.clone() 594 | 595 | def getP2(self): return self.p2.clone() 596 | 597 | def getCenter(self): 598 | p1 = self.p1 599 | p2 = self.p2 600 | return Point((p1.x+p2.x)/2.0, (p1.y+p2.y)/2.0) 601 | 602 | 603 | class Rectangle(_BBox): 604 | 605 | def __init__(self, p1, p2): 606 | _BBox.__init__(self, p1, p2) 607 | 608 | def __repr__(self): 609 | return "Rectangle({}, {})".format(str(self.p1), str(self.p2)) 610 | 611 | def _draw(self, canvas, options): 612 | p1 = self.p1 613 | p2 = self.p2 614 | x1, y1 = canvas.toScreen(p1.x, p1.y) 615 | x2, y2 = canvas.toScreen(p2.x, p2.y) 616 | return canvas.create_rectangle(x1, y1, x2, y2, options) 617 | 618 | def clone(self): 619 | other = Rectangle(self.p1, self.p2) 620 | other.config = self.config.copy() 621 | return other 622 | 623 | 624 | class Oval(_BBox): 625 | 626 | def __init__(self, p1, p2): 627 | _BBox.__init__(self, p1, p2) 628 | 629 | def __repr__(self): 630 | return "Oval({}, {})".format(str(self.p1), str(self.p2)) 631 | 632 | def clone(self): 633 | other = Oval(self.p1, self.p2) 634 | other.config = self.config.copy() 635 | return other 636 | 637 | def _draw(self, canvas, options): 638 | p1 = self.p1 639 | p2 = self.p2 640 | x1, y1 = canvas.toScreen(p1.x, p1.y) 641 | x2, y2 = canvas.toScreen(p2.x, p2.y) 642 | return canvas.create_oval(x1, y1, x2, y2, options) 643 | 644 | 645 | class Circle(Oval): 646 | 647 | def __init__(self, center, radius): 648 | p1 = Point(center.x-radius, center.y-radius) 649 | p2 = Point(center.x+radius, center.y+radius) 650 | Oval.__init__(self, p1, p2) 651 | self.radius = radius 652 | 653 | def __repr__(self): 654 | return "Circle({}, {})".format(str(self.getCenter()), str(self.radius)) 655 | 656 | def clone(self): 657 | other = Circle(self.getCenter(), self.radius) 658 | other.config = self.config.copy() 659 | return other 660 | 661 | def getRadius(self): 662 | return self.radius 663 | 664 | 665 | class Line(_BBox): 666 | 667 | def __init__(self, p1, p2): 668 | _BBox.__init__(self, p1, p2, ["arrow", "fill", "width"]) 669 | self.setFill(DEFAULT_CONFIG['outline']) 670 | self.setOutline = self.setFill 671 | 672 | def __repr__(self): 673 | return "Line({}, {})".format(str(self.p1), str(self.p2)) 674 | 675 | def clone(self): 676 | other = Line(self.p1, self.p2) 677 | other.config = self.config.copy() 678 | return other 679 | 680 | def _draw(self, canvas, options): 681 | p1 = self.p1 682 | p2 = self.p2 683 | x1, y1 = canvas.toScreen(p1.x, p1.y) 684 | x2, y2 = canvas.toScreen(p2.x, p2.y) 685 | return canvas.create_line(x1, y1, x2, y2, options) 686 | 687 | def setArrow(self, option): 688 | if not option in ["first", "last", "both", "none"]: 689 | raise GraphicsError(BAD_OPTION) 690 | self._reconfig("arrow", option) 691 | 692 | 693 | class Polygon(GraphicsObject): 694 | 695 | def __init__(self, *points): 696 | # if points passed as a list, extract it 697 | if len(points) == 1 and type(points[0]) == type([]): 698 | points = points[0] 699 | self.points = list(map(Point.clone, points)) 700 | GraphicsObject.__init__(self, ["outline", "width", "fill"]) 701 | 702 | def __repr__(self): 703 | return "Polygon"+str(tuple(p for p in self.points)) 704 | 705 | def clone(self): 706 | other = Polygon(*self.points) 707 | other.config = self.config.copy() 708 | return other 709 | 710 | def getPoints(self): 711 | return list(map(Point.clone, self.points)) 712 | 713 | def _move(self, dx, dy): 714 | for p in self.points: 715 | p.move(dx, dy) 716 | 717 | def _draw(self, canvas, options): 718 | args = [canvas] 719 | for p in self.points: 720 | x, y = canvas.toScreen(p.x, p.y) 721 | args.append(x) 722 | args.append(y) 723 | args.append(options) 724 | return GraphWin.create_polygon(*args) 725 | 726 | 727 | class Text(GraphicsObject): 728 | 729 | def __init__(self, p, text): 730 | GraphicsObject.__init__(self, ["justify", "fill", "text", "font"]) 731 | self.setText(text) 732 | self.anchor = p.clone() 733 | self.setFill(DEFAULT_CONFIG['outline']) 734 | self.setOutline = self.setFill 735 | 736 | def __repr__(self): 737 | return "Text({}, '{}')".format(self.anchor, self.getText()) 738 | 739 | def _draw(self, canvas, options): 740 | p = self.anchor 741 | x, y = canvas.toScreen(p.x, p.y) 742 | return canvas.create_text(x, y, options) 743 | 744 | def _move(self, dx, dy): 745 | self.anchor.move(dx, dy) 746 | 747 | def clone(self): 748 | other = Text(self.anchor, self.config['text']) 749 | other.config = self.config.copy() 750 | return other 751 | 752 | def setText(self, text): 753 | self._reconfig("text", text) 754 | 755 | def getText(self): 756 | return self.config["text"] 757 | 758 | def getAnchor(self): 759 | return self.anchor.clone() 760 | 761 | def setFace(self, face): 762 | if face in ['helvetica', 'arial', 'courier', 'times roman']: 763 | f, s, b = self.config['font'] 764 | self._reconfig("font", (face, s, b)) 765 | else: 766 | raise GraphicsError(BAD_OPTION) 767 | 768 | def setSize(self, size): 769 | if 5 <= size <= 36: 770 | f, s, b = self.config['font'] 771 | self._reconfig("font", (f, size, b)) 772 | else: 773 | raise GraphicsError(BAD_OPTION) 774 | 775 | def setStyle(self, style): 776 | if style in ['bold', 'normal', 'italic', 'bold italic']: 777 | f, s, b = self.config['font'] 778 | self._reconfig("font", (f, s, style)) 779 | else: 780 | raise GraphicsError(BAD_OPTION) 781 | 782 | def setTextColor(self, color): 783 | self.setFill(color) 784 | 785 | 786 | class Entry(GraphicsObject): 787 | 788 | def __init__(self, p, width): 789 | GraphicsObject.__init__(self, []) 790 | self.anchor = p.clone() 791 | # print self.anchor 792 | self.width = width 793 | self.text = tk.StringVar(_root) 794 | self.text.set("") 795 | self.fill = "gray" 796 | self.color = "black" 797 | self.font = DEFAULT_CONFIG['font'] 798 | self.entry = None 799 | 800 | def __repr__(self): 801 | return "Entry({}, {})".format(self.anchor, self.width) 802 | 803 | def _draw(self, canvas, options): 804 | p = self.anchor 805 | x, y = canvas.toScreen(p.x, p.y) 806 | frm = tk.Frame(canvas.master) 807 | self.entry = tk.Entry(frm, 808 | width=self.width, 809 | textvariable=self.text, 810 | bg=self.fill, 811 | fg=self.color, 812 | font=self.font) 813 | self.entry.pack() 814 | # self.setFill(self.fill) 815 | self.entry.focus_set() 816 | return canvas.create_window(x, y, window=frm) 817 | 818 | def getText(self): 819 | return self.text.get() 820 | 821 | def _move(self, dx, dy): 822 | self.anchor.move(dx, dy) 823 | 824 | def getAnchor(self): 825 | return self.anchor.clone() 826 | 827 | def clone(self): 828 | other = Entry(self.anchor, self.width) 829 | other.config = self.config.copy() 830 | other.text = tk.StringVar() 831 | other.text.set(self.text.get()) 832 | other.fill = self.fill 833 | return other 834 | 835 | def setText(self, t): 836 | self.text.set(t) 837 | 838 | def setFill(self, color): 839 | self.fill = color 840 | if self.entry: 841 | self.entry.config(bg=color) 842 | 843 | def _setFontComponent(self, which, value): 844 | font = list(self.font) 845 | font[which] = value 846 | self.font = tuple(font) 847 | if self.entry: 848 | self.entry.config(font=self.font) 849 | 850 | def setFace(self, face): 851 | if face in ['helvetica', 'arial', 'courier', 'times roman']: 852 | self._setFontComponent(0, face) 853 | else: 854 | raise GraphicsError(BAD_OPTION) 855 | 856 | def setSize(self, size): 857 | if 5 <= size <= 36: 858 | self._setFontComponent(1, size) 859 | else: 860 | raise GraphicsError(BAD_OPTION) 861 | 862 | def setStyle(self, style): 863 | if style in ['bold', 'normal', 'italic', 'bold italic']: 864 | self._setFontComponent(2, style) 865 | else: 866 | raise GraphicsError(BAD_OPTION) 867 | 868 | def setTextColor(self, color): 869 | self.color = color 870 | if self.entry: 871 | self.entry.config(fg=color) 872 | 873 | 874 | class Image(GraphicsObject): 875 | 876 | idCount = 0 877 | imageCache = {} # tk photoimages go here to avoid GC while drawn 878 | 879 | def __init__(self, p, *pixmap): 880 | GraphicsObject.__init__(self, []) 881 | self.anchor = p.clone() 882 | self.imageId = Image.idCount 883 | Image.idCount = Image.idCount + 1 884 | if len(pixmap) == 1: # file name provided 885 | self.img = tk.PhotoImage(file=pixmap[0], master=_root) 886 | else: # width and height provided 887 | width, height = pixmap 888 | self.img = tk.PhotoImage(master=_root, width=width, height=height) 889 | 890 | def __repr__(self): 891 | return "Image({}, {}, {})".format(self.anchor, self.getWidth(), self.getHeight()) 892 | 893 | def _draw(self, canvas, options): 894 | p = self.anchor 895 | x, y = canvas.toScreen(p.x, p.y) 896 | self.imageCache[self.imageId] = self.img # save a reference 897 | return canvas.create_image(x, y, image=self.img) 898 | 899 | def _move(self, dx, dy): 900 | self.anchor.move(dx, dy) 901 | 902 | def undraw(self): 903 | try: 904 | del self.imageCache[self.imageId] # allow gc of tk photoimage 905 | except KeyError: 906 | pass 907 | GraphicsObject.undraw(self) 908 | 909 | def getAnchor(self): 910 | return self.anchor.clone() 911 | 912 | def clone(self): 913 | other = Image(Point(0, 0), 0, 0) 914 | other.img = self.img.copy() 915 | other.anchor = self.anchor.clone() 916 | other.config = self.config.copy() 917 | return other 918 | 919 | def getWidth(self): 920 | """Returns the width of the image in pixels""" 921 | return self.img.width() 922 | 923 | def getHeight(self): 924 | """Returns the height of the image in pixels""" 925 | return self.img.height() 926 | 927 | def getPixel(self, x, y): 928 | """Returns a list [r,g,b] with the RGB color values for pixel (x,y) 929 | r,g,b are in range(256) 930 | 931 | """ 932 | 933 | value = self.img.get(x, y) 934 | if type(value) == type(0): 935 | return [value, value, value] 936 | elif type(value) == type((0, 0, 0)): 937 | return list(value) 938 | else: 939 | return list(map(int, value.split())) 940 | 941 | def setPixel(self, x, y, color): 942 | """Sets pixel (x,y) to the given color 943 | 944 | """ 945 | self.img.put("{" + color + "}", (x, y)) 946 | 947 | def save(self, filename): 948 | """Saves the pixmap image to filename. 949 | The format for the save image is determined from the filname extension. 950 | 951 | """ 952 | 953 | path, name = os.path.split(filename) 954 | ext = name.split(".")[-1] 955 | self.img.write(filename, format=ext) 956 | 957 | 958 | def color_rgb(r, g, b): 959 | """r,g,b are intensities of red, green, and blue in range(256) 960 | Returns color specifier string for the resulting color""" 961 | return "#%02x%02x%02x" % (r, g, b) 962 | 963 | 964 | def test(): 965 | win = GraphWin() 966 | win.setCoords(0, 0, 10, 10) 967 | t = Text(Point(5, 5), "Centered Text") 968 | t.draw(win) 969 | p = Polygon(Point(1, 1), Point(5, 3), Point(2, 7)) 970 | p.draw(win) 971 | e = Entry(Point(5, 6), 10) 972 | e.draw(win) 973 | win.getMouse() 974 | p.setFill("red") 975 | p.setOutline("blue") 976 | p.setWidth(2) 977 | s = "" 978 | for pt in p.getPoints(): 979 | s = s + "(%0.1f,%0.1f) " % (pt.getX(), pt.getY()) 980 | t.setText(e.getText()) 981 | e.setFill("green") 982 | e.setText("Spam!") 983 | e.move(2, 0) 984 | win.getMouse() 985 | p.move(2, 3) 986 | s = "" 987 | for pt in p.getPoints(): 988 | s = s + "(%0.1f,%0.1f) " % (pt.getX(), pt.getY()) 989 | t.setText(s) 990 | win.getMouse() 991 | p.undraw() 992 | e.undraw() 993 | t.setStyle("bold") 994 | win.getMouse() 995 | t.setStyle("normal") 996 | win.getMouse() 997 | t.setStyle("italic") 998 | win.getMouse() 999 | t.setStyle("bold italic") 1000 | win.getMouse() 1001 | t.setSize(14) 1002 | win.getMouse() 1003 | t.setFace("arial") 1004 | t.setSize(20) 1005 | win.getMouse() 1006 | win.close() 1007 | 1008 | # MacOS fix 2 1009 | # tk.Toplevel(_root).destroy() 1010 | 1011 | 1012 | # MacOS fix 1 1013 | update() 1014 | 1015 | if __name__ == "__main__": 1016 | test() 1017 | --------------------------------------------------------------------------------