Simple Graphics in Python – Part 3
Last time we left off discussing Color in John Zelle’s graphics.py library. If you didn’t catch part 1 and 2 of this series I recommend you read those parts first and then return here. You can find part 1 here: Simple Graphics in Python.
We’re almost done going over the library from a user standpoint. However, in the future I may discuss how the library works if there is enough interest. This time, we’ll be discussing window updating and animations. We’ll develop a few sample apps and have some fun. My intention here isn’t to develop full fledged apps but, rather give you a starting point for your own apps using the graphics.py library. So let’s get started.
The graphics.py library usually handles window updates for you anytime an object that has been drawn to the window changes. Under some circumstances it may be necessary to force a window update. For example, when using the library from some interactive shells. The window may be forced to update using the update() method on the GraphWin object. This will redraw all the items in the window.
The window auto update feature is great for simple graphics. However, as your scenes become more complex you may want to take charge and start updating the window when it bests fits your program’s schedule. This may become necessary when you are drawing many, many items to the window. You can improve efficiency by updating the window only after all items have been drawn. If you want to turn off the auto update window feature you can do so when you create the window by passing
autoflush=False as in:
win = GraphWin("Window Title", 400, 400, autoflush=False)
This will disable the auto update feature and you’ll be responsible for calling win.update() when you desire to redraw all the objects in the window. Here’s an example:
""" Prog: ex-11_02.py Auth: R. Morgan Desc: Demonstrate how to use John Zelle's graphics.py library turning off the window's auto update feature and calling win.update() yourself. Lic: This code is placed in the public domain. """ from graphics import * from random import randint def main(): win = GraphWin("Rectangles", 640, 480, autoflush=False) points =  for i in range(1): for p in range(0, randint(4, 20)): points.append(Point(randint(0,639), randint(0,479))) r = randint(0,255) g = randint(0,255) b = randint(0,255) poly = Polygon(points) poly.setOutline(color_rgb(r, g, b)) poly.draw(win) # First mouse click adds a polygon points = win.getMouse() points[-1] = points poly2 = Polygon(points) poly2.setOutline('white') poly2.draw(win) # Second mouse click should show the new polygon win.getMouse() update() # Thrid mouse click should close the window. win.getMouse() win.close() main()
When I read the docs and implemented this program I expected that updates would only occur when the update() method was called. When this didn’t work as expected I re-read the docs and when I still couldn’t understand what was happening, I emailed John. He was kind enough to respond and set me straight about my misunderstandings. After reading his response and once again, re-reading the docs, I realized I had read, but ignored one statement in the docs. This was the cause of misunderstanding. Here’s the line I skimmed over and missed the details:
Now changes to the objects in win will only be shown when the graphics system has some idle time or when the changes are forced by a call to update().
When I read this I walked away with the impression that updates only occur when update() was called if you passed autoflush=False. However, the statement clearly says that auto-updates still occur when the system has idle time. So update is only useful if you have a blocking operation that keeps the auto-update from running.
John pointed out that in the code above, the getMouse() method calls are blocking methods but that he coded them to call update() and force drawing on the window. So my calls to getMouse don’t actually work as commented in the code above. In fact, they force an update to the window.
So with that insight let’s see if we can write a sample that actually demonstrates the use of update. Create the example below:
""" Prog: ex-11_03.py Auth: R. Morgan Desc: Demonstrate how to use John Zelle's graphics.py library turning off the window's auto update feature and calling win.update() yourself. Lic: This code is placed in the public domain. """ from graphics import * from random import randint def main(): win1 = GraphWin("Rectangles", 640, 480, autoflush=False) #win2 = GraphWin("Rectangles", 640, 480) points =  for i in range(1): # Build up initial polygon for p in range(0, randint(4, 20)): points.append(Point(randint(0,639), randint(0,479))) r = randint(80,255) g = randint(0,255) b = randint(50,255) poly = Polygon(points) poly.setOutline(color_rgb(r, g, b)) poly.draw(win1) # Loop to keep system busy # Since we are using a loop # to delay the system you may need to increa max_iters = 999999999 delay_frac = 33333333 td = 0 for j in range(0,max_iters): if j % delay_frac == 0: poly.undraw() poly = None r = randint(30,255) g = randint(30,255) b = randint(30,255) points.append(Point(randint(0,639), randint(0,479))) poly = Polygon(points) poly.setOutline(color_rgb(r, g, b)) poly.draw(win1) print("Updated #", td) td += 1 update() # We manually call update here, then delay again. # mouse click should show the new polygon # after busy loop completes win.getMouse() win.close() main()
OK, with our new understanding of the autoflush=False option we’ll run the app above. You may need to adjust the value of max_iters and delay_frac as max_iters controls the total run-time of the delay loop and delay_frac controls the delay between updates during the loops.
Our program begins by creating a polygon and displaying it on the screen. Next, we enter a loop and stay in the loop for a very long time. This loop blocks the auto-update feature from updating the display. During the execution of the delay loop we check if we have made delay_frac (delay fraction) iterations since our last update. If so, the modulus expression will return 0 and the if statement will evaluate to true and we enter the if clause. Next, we erase and destroy the original polygon, and generate a new polygon using the points of last polygon with one new point added for good measure. Adding a point allows us to see the shape changed on update. The important thing to understand here is that the object isn’t being drawn to the window until we reach the update line at the bottom of the if clause.
However, if we were to forego the delay loop the auto-update feature would take over and draw the polygon when the system became idle or a method that itself (like the getMouse()) calls update() is called.
One last thing to know about the update() method is that it can take an integer parameter for the desired frame-rate. If you pass a desired frame-rate to update() it as in:
It will update the window at this rate.
While it’s possible to use the graphics library for a GUI (Graphical User Interface), most NooBs will want to do something a bit more entertaining with it. I’m not going to tech game development here but I thought I would toss out a few example apps that I’ll intentionally leave unfinished so you, the reader, can have fun adding features and completing the demo apps.
There are a few ways to accomplish animation on a computer. The most often used is motion animation where an object is moved into it’s new location, then drawn, then erased and moved again. This cycle is known as the animation loop, or if you’re a gamer, the game loop.
Our first animation is a simple Pong-like game (remember these apps will be unfinished and incomplete) that simply draws a circle and a rectangle on the screen then moves them around the screen.
We will use an OOP (Object Oriented Programming) approach for the Handball app.
#!/usr/bin/env python3 """ Prog: ex-12_01.py Auth: R. Morgan Desc: Demonstrate how to use John Zelle's graphics.py library to do simple animation of a pendulum. Lic: This code is placed in the public domain. """ from graphics import * from math import * width = 640 height = 480 class Paddle(): def __init__(self, x, y, win): self.x = x self.y = y self.w = 10 self.h = 50 self.win = win self.rect = Rectangle(Point(self.x, self.y), Point(self.x+self.w, self.y+self.h)) self.rect.setFill('white') def getX(self): return self.rect.p1.getX() def getY(self): return self.rect.p1.getY() def getW(self): return self.w def getH(self): return self.h def move(self, xspeed, yspeed): self.rect.move(xspeed, yspeed) def draw(self): self.rect.draw(self.win) def undraw(self): self.rect.undraw() class Ball(): def __init__(self, x, y, win): self.x = x self.y = y self.xspeed = 3 self.yspeed = 1 self.r = 10 self.win = win self.cir = Circle(Point(self.x, self.y), self.r) self.cir.setFill('white') def set_speed(self, xspeed, yspeed): self.xspeed = xspeed self.yspeed = yspeed def move(self): self.cir.move(self.xspeed, self.yspeed) p1 = self.cir.getP1() self.x = p1.getX() self.y = p1.getY() def draw(self): self.cir.draw(self.win) def undraw(self): self.cir.undraw() def check_collision(self, pad): print("Xspeed: ", self.xspeed, "Yspeed: ", self.yspeed) xbound = self.within_x_bounds(pad) ybound = self.within_y_bounds(pad) if xbound and ybound: if xbound: self.xspeed = -self.xspeed if ybound: self.yspeed = -self.yspeed self.move() return True return False def within_x_bounds(self, pad): if self.xspeed < 0: if (self.x < pad.getX() + pad.getW()) and (self.x > pad.getX()): return True else: return False else: if (self.x + self.r >= pad.getX()) and (self.x <= pad.getX() + pad.getW()): return True else: return False def within_y_bounds(self, pad): if self.y+self.r >= pad.getY() and self.y <= pad.getY()+pad.getH(): return True else: return False def check_edges(self, width, height): p1 = self.cir.getP1() if p1.getX() < 0 or p1.getX()+self.r > width: self.xspeed = -self.xspeed; if p1.getY() < 0 or p1.getY()+self.r > height: self.yspeed = -self.yspeed; def main(): win = GraphWin("Handball", width, height) win.setBackground('black') xspeed = 0.1 yspeed = 0.1 # initial placement of paddle pad = Paddle(10, (height/2)-25, win) # Draw paddles. pad.draw() ball = Ball(width/2, height/2, win) ball.set_speed(xspeed, yspeed) ball.draw() while 1: # get imput if any k = win.checkKey() if k == 'a': if pad.getY() < 0: pad.move(0, 0) else: pad.move(0, -20) print('Pad1: Move Up', pad.getX(), pad.getY()) if k == 'z': if pad.getY() > height - 50: pad.move(0, 0) else: pad.move(0, 20) print('Pad1: Move down', pad.getX(), pad.getY()) ball.check_edges(width, height) ball.move() if ball.check_collision(pad): print("Ball hit paddle") win.getMouse() win.close() main()
This may not be the most efficient implementation however, it is only meant to provide you with some inspiration for creating your own apps by demonstrating what can be accomplished using the graphics.py library.
If you scan the code you quickly see that we have a Paddle calls, a Ball class, and a main function. Our Paddle object encapsulates properties (data) and methods (actions) our paddle can take. Our paddle needs to keep track of it’s position (x,y) and size (w,h). When we create a Paddle object from the class (a class is a blueprint for the object we want to create) we pass in these values along with the window we want the paddle to draw itself on. We save the window for future use as we will always draw the paddle to the same window. So saving it here simplifies our code and we no-longer have the need to pass the window each time we call draw() on the paddle.
When a paddle is instantiated (an object is created from the class), Python calls the __init__() method. In this method you place all the code that you need to run to set things up for use. So we create the rectangle that will represent our paddle on the screen. We also set the fill color on the paddle to white. Our paddle is now ready for use.
Often you’ll need to have access to the state of an object. Later, we’ll need to be able to determine if the ball hits our paddle so, we need access to the location and size of our paddle. Do enable this we provide accessor methods getX(), getY(), getW(), getH(). These return the paddles x, y, width, and height respectively.
Our paddle also needs to move up and down so we can hit the ball as it bounces across the court. So, well need a move() method. There may be times we want to move at different speeds. So will pass in the xspeed and yspeed for our paddle. You may be wondering why we need the xspeed. Truly we don’t. We could just hard code the xspeed in our class code. But that would restrict us to moving only in the Y plane. Yes, it’s true that the paddle in Pong moves only in the Y play (up and down). But think how much fun it would be to animate the paddle to shake when the ball hits it. Here, we would need access to the yspeed to accomplish this. Including it also opens the class up for reuse. For example, suppose you want to use the paddle in a falling object game. If we didn’t include the yspeed here, you wouldn’t be able to.
In almost all motion animations each object will need to complete the three tasks of the animation loop, Move, Draw, Erase, Repeat… The graphics library actually takes care of this for use in the move() method of the various shape objects. So we really don’t need to worry about it. But you do need to know it’s happening under the hood.
we’ll add a draw() method to our paddle. Here we only need to call draw on the rectangle that represents our paddle on on the display. We may also need an erase method at some point. So, we’ll include it here and again however, we’ll call it undraw() to stay consistent with the library methods. All we need to do in the undraw() method is to call it’s namesake on the rectangle that represents our paddle.
The Ball class is a bit more complicated. Mostly because we encapsulated the logic of what to do when the ball comes in contact with another object. For example, if the ball hits the edge of the screen or the paddle. The balls move method is a bit different than the paddle’s move method. This is because it is expected that once the ball is moving it will keep moving. Also, we don’t want to have to changed the balls direction ourselves. We want it to include this action when it hits an object so it bounces off on it’s own. So in the Ball class we provide an xspeed and yspeed and set default values for them. Our ball is represented by a circle on the screen. So we have to create a circle and save it. We laos set the fill color in the __init__() method.
We may need to change the ball’s speed so we include a set_speed() method. We will also need to know when the ball has hit the edge of the court. This is handled in the check_edges() method. Here you’ll need to pass in the courts with and height. It is assumed that the upper right corner of the court is (0,0) and all calculations make this assumption.
The check_collision() method is passed the paddle object to test for collision with the ball. The ball object includes two helper methods, within_x_bounds() and within_y_bounds() to check if the ball is within the bounds of the paddle object.
To make this a complete game you need to add scoring and allow the ball to reset and be re-served if it passes the paddle. You can also use this as the basic frame work for Pong by adding another paddle and additional input handling for another player. Just a hint if you try this, google keyboard input methods for python before you attempt this. As, the current approach wont report multiple key presses at once. Their are solutions but I’ll leave that as an exercise for the reader.
Games and GUIs aren’t the only things that graphics can help with. Graphics are often used to convey information about some chemical or mathematical process. Let’s take a simple case, that of calculating pi. It is well known that PI can be estimated to surprising accuracy by randomly throwing darts at art board. OK, so it’s a bit more complex than that but, only a little. First, what we really need is a circle inside a square. The circle’s diameter must fit snugly inside the square. More precisely the diameter of the circle must equal the length of one side of the square.
The logic is simple: If the circle’s diameter is equal to the square’s length, than the area of the circle should be equal to: (area of the square / area of circle)*4. To learn more about this you can checkout this link: https://www.youtube.com/watch?v=M34TO71SKGk
We can draw circles, squares, and points (to represent darts) using the graphics.py library. So all we need to do is draw a circle inside a square and throw darts at it, then calculate the ratio of darts that landed in the circle to the total number of darts thrown. We will simply plot random points for our darts and keep track of how many we throw and where they landed. Let’s see how we might do this in python:
#!/usr/bin/env python3 """ Prog: ex-13.py Auth: R. Morgan Desc: Demonstrate how to use John Zelle's graphics.py library to visualize the process of estimating pi by randomly throwing darts. Lic: This code is placed in the public domain. """ from graphics import * from random import * from math import * width = 400 height = 400 center = width/2 r = width/2 # Find the deststance between two points def dest(x1,y1, x2,y2): return sqrt((x1 - x2)**2 + (y1 - y2)**2) def main(): win = GraphWin("Pi Estimation", width, height) # Draw a square sq = Rectangle(Point(0,0), Point(width-1,height-1)) sq.setOutline("blue") sq.draw(win) # Draw a circle fitting the square c = Circle(Point(center,center), r) c.setOutline("white") c.draw(win) darts_thrown = 0 darts_in_circle = 0 best = 0 estimate = 0 best_estimate = 0 for i in range(1,100000): x = randint(0, 400) y = randint(0, 400) p = Point(x,y) darts_thrown += 1 # Is are point in the circle? if(dest(center,center, x, y) < r): darts_in_circle += 1 p.setFill(color_rgb(220,200, 120)) p.draw(win) else: p.setFill(color_rgb(127, 200, 127)) p.draw(win) if i % 3000 == 0: estimate = (darts_in_circle/darts_thrown)*4 if abs(pi - estimate) < abs(pi - best_estimate): best_estimate = estimate print("Iteration: ", i, " Estimated PI: ", best_estimate) print("Done!") win.getMouse() win.close() main()
If you run this code you should get a printed output of something like this:
Iteration: 3000 Estimated PI: 3.0893333333333333
Iteration: 6000 Estimated PI: 3.0893333333333333
Iteration: 9000 Estimated PI: 3.089777777777778
Iteration: 12000 Estimated PI: 3.0936666666666666
Iteration: 15000 Estimated PI: 3.1018666666666665
Iteration: 18000 Estimated PI: 3.110222222222222
Iteration: 21000 Estimated PI: 3.1125714285714285
Iteration: 24000 Estimated PI: 3.1161666666666665
Iteration: 27000 Estimated PI: 3.1161666666666665
Iteration: 30000 Estimated PI: 3.1161666666666665
Iteration: 33000 Estimated PI: 3.1161666666666665
Iteration: 36000 Estimated PI: 3.1172222222222223
Iteration: 39000 Estimated PI: 3.12174358974359
Iteration: 42000 Estimated PI: 3.12174358974359
Iteration: 45000 Estimated PI: 3.1226666666666665
Iteration: 48000 Estimated PI: 3.12475
Iteration: 51000 Estimated PI: 3.124941176470588
Iteration: 54000 Estimated PI: 3.124941176470588
Iteration: 57000 Estimated PI: 3.124941176470588
Iteration: 60000 Estimated PI: 3.124941176470588
Iteration: 63000 Estimated PI: 3.124941176470588
Iteration: 66000 Estimated PI: 3.1267878787878787
Iteration: 69000 Estimated PI: 3.1267878787878787
Iteration: 72000 Estimated PI: 3.1267878787878787
Iteration: 75000 Estimated PI: 3.1267878787878787
Iteration: 78000 Estimated PI: 3.1267878787878787
Iteration: 81000 Estimated PI: 3.1267878787878787
Iteration: 84000 Estimated PI: 3.1267878787878787
Iteration: 87000 Estimated PI: 3.1267878787878787
Iteration: 90000 Estimated PI: 3.1267878787878787
Iteration: 93000 Estimated PI: 3.1267878787878787
Iteration: 96000 Estimated PI: 3.1267878787878787
Iteration: 99000 Estimated PI: 3.1267878787878787
I ran this program several times and the best I did was 3.1419. Which is pretty good given the fact that our random number generator is actually a pseudo random number generator. I also believe that the math library in python may be rounding our calculations. Using a more precise math library would improve the estimate. However, this app is only meant to demonstrate the process. So, I’ll leave implementing a more precise version up to the reader.
Running the application longer with more dart throws will improve the estimation of PI.
The graphics.py library allows you to have multiple windows. This can be handy for both GUIs and data visualization. You could for example display the plot of darts in a PI estimation program in one window while plotting the error on a graph in another window.
You might wonder why you would ever need more than one window. Well, how often do you use a drop down menu? The drop down menu is actually a small window with a list of items that is placed over the main window. Dialog boxes, popups, etc… are all windows. So being able to create additional windows comes in very handy for GUI applications. However, other types of applications can make use of multiple window. Take our PI estimating application above. We could use an additional window to plot the standard deviation of our a current estimate. Using multiple windows you can show many plots at the same time. This would allow the user to correlate the information in the various plots.
I’m going to show you a simple demo that is once again, an incomplete game. This game is a two player version of Battleship. It has several issues left for you to resolve. however, it does demonstrate the use of two windows being used in a single application. The code here is a bit longer than our other applications and I would say this code is in a pre-alpha state. It is only meant to ignite you imagination and give you a base from which to work to complete the game.
I’m sure I don’t have to explain how Battleship is played. However, if you need and explanation, google “battleship game” and you’ll find a wikipedia article on it. Let’s see some code:
#!/usr/bin/env python3 """ Prog: ex-14.py Auth: R. Morgan Desc: Demonstrate how to use John Zelle's graphics.py library and the use of multiple windows in a single app. Lic: This code is placed in the public domain. """ from graphics import * from random import * from math import * width = 400 height = 400 class Board(): def __init__(self, title, width, height): self.xsize = 10 self.ysize = 10 self.w = width self.h = height self.grid =  self.win = GraphWin(title, width, height) self.vessels =  # returns pixels per division def xdiv(self): xdiv = self.w / self.xsize return xdiv # returns pixels per division def ydiv(self): ydiv = self.h / self.ysize return ydiv def rowcol_to_xy(self, r, c): y = int(r * self.ydiv()) x = int(c * self.xdiv()) return (x, y) def rowcol_to_point(self, r, c): p = self.rowcol_to_xy(r, c) return Point(p, p) def xy_to_rowcol(self, x, y): r = int(y / self.ydiv()) c = int(x / self.xdiv()) return (r, c) # return the coordinates in pixels for # the center of the cell at (row, col) def center_xy(self, r, c): # calc (x,y) position of upper left # corner of cell at (r,c) xy1 = self.rowcol_to_xy(r, c) # Calculate lower right corner xy2 = self.rowcol_to_xy(r+1, c+1) # find the middel of the cell dx = (xy2 - xy1) - self.xdiv() / 2 dy = (xy2 - xy1) - self.ydiv() / 2 cx = dx + xy1 cy = dy + xy1 return (cx, cy) def dist(self, x1, y1, x2, y2): return sqrt(((x2-x1)**2) + ((y2 - y1)**2)) # draws the grid of cells on the board def draw(self): # Expects (0,0) to be located in the upper left xdiv = self.w / self.xsize ydiv = self.h / self.ysize for i in range(0, self.w, int(xdiv)): l = Line(Point(i, 0), Point(i, self.h)) l.draw(self.win) for j in range(0, self.h, int(ydiv)): l = Line(Point(0, j), Point(self.w, j)) l.draw(self.win) # place a vessel on the board def place(self, vessel): plX = vessel.row * self.ydiv() plY = vessel.col * self.xdiv() if vessel.rect == None: rowcol = self.rowcol_to_xy(vessel.row, vessel.col) x1 = rowcol y1 = rowcol if vessel.horz: x2 = x1 + (vessel.length * self.xdiv()) y2 = y1 + self.ydiv() else: y2 = y1 + (vessel.length * self.ydiv()) x2 = x1 + self.xdiv() vessel.rect = Rectangle(Point(x1, y1), Point(x2, y2)) vessel.rect.setOutline(color_rgb(127,220,127)) vessel.rect.draw(self.win) # tests to see if the vessels on this board # have been hit by the shot taken, and call # draw_hit() to mark the shot with a red X # in the cell where it landed. def hit(self, loc): col = int(loc.getX() / (self.w / self.xsize)) row = int(loc.getY() / (self.h / self.ysize)) self.draw_hit(row, col) # draws the actual red X, called by hit() def draw_hit(self, row, col): xy1 = self.rowcol_to_xy(row, col) x1 = xy1 y1 = xy1 xy2 = self.rowcol_to_xy(row+1, col+1) x2 = xy2 y2 = xy2 p1 = Point(x1,y1) p2 = Point(x2, y2) p3 = Point(x1,y2) p4 = Point(x2, y1) l1 = Line(p1, p2) l2 = Line(p3, p4) l1.setOutline('red') l2.setOutline('red') l1.draw(self.win) l2.draw(self.win) # Use to mark the shooter's board # for shots taken. So the player may # know where they have already shot def mark(self, r, c): c = self.center_xy(r, c) print("Center of mark, x: " + str(c) + ", y: " + str(c)) pc = Point(c, c) cir = Circle(pc, int(self.xdiv()/2)) cir.setOutline(color_rgb(50, 50, 200)) cir.draw(self.win) # Simple vessel class class Vessel(): def __init__(self, name, row, col, length, place_horz): self.row = row self.col = col self.length = length self.horz = place_horz self.name = name self.hit_count = 0 self.rect = None # created in board.place() if self.name == 'Carrier': self.makeCarrier() print("Row: " + str(self.row)) print("Col: " + str(self.col)) elif self.name == 'Battleship': self.makeBattleship() print("Row: " + str(self.row)) print("Col: " + str(self.col)) elif self.name == 'Cruiser': self.makeCruiser() print("Row: " + str(self.row)) print("Col: " + str(self.col)) elif self.name == 'Submarine': self.makeSubmarine() print("Row: " + str(self.row)) print("Col: " + str(self.col)) elif self.name == 'Destroyer': self.makeDestroyer() print("Row: " + str(self.row)) print("Col: " + str(self.col)) else: print('Illegal Vessel Type: "'+name+'" not defined') return None def makeCarrier(self): if self.name != 'Carrier': return elif self.horz: self.col = randint(0, 4) self.row = randint(0, 9) else: self.col = randint(0, 9) self.row = randint(0, 4) def makeBattleship(self): if self.name != 'Battleship': return elif self.horz: self.col = randint(0,5) self.row = randint(0, 9) else: self.col = randint(0, 9) self.row = randint(0, 5) def makeCruiser(self): if self.name != 'Cruiser': return elif self.horz: self.col = randint(0,6) self.row = randint(0, 9) else: self.col = randint(0, 9) self.row = randint(0, 6) def makeSubmarine(self): if self.name != 'Submarine': return elif self.horz: self.col = randint(0,6) self.row = randint(0, 9) else: self.col = randint(0, 9) self.row = randint(0, 6) def makeDestroyer(self): if self.name != 'Destroyer': return elif self.horz: self.col = randint(0,7) self.row = randint(0, 9) else: self.col = randint(0, 9) self.row = randint(0, 7) def getName(self): return self.name def move(self, x, y): self.rect.move(x, y) def draw(self): self.rect.draw() # Not Yet Implemented # Given a row, col value for # a shot, return true if the # vessel was hit by shot def hit(self, r, c): return False # Simple player class class Player(): def __init__(self, name, width, height): self.name = name self.board = Board(name, width, height) # Create fleet self.Carrier = Vessel('Carrier', randint(0,4), randint(0, 9), 5, True) self.Battleship = Vessel('Battleship', randint(0,5), randint(0,5), 4, False) self.Cruiser = Vessel('Cruiser', randint(0,4), randint(0,4), 3, True) self.Submarine = Vessel('Submarine', randint(1,4), randint(1,4), 3, True) self.Destroyer = Vessel('Destroyer', randint(1,4), randint(1,4), 2, True) def getName(self): return self.name def getMouse(self): return self.board.win.getMouse() # called when player should take turn def turn(self, board): loc = self.getMouse() board.hit(loc) rc = self.board.xy_to_rowcol(loc.getX(), loc.getY()) self.board.mark(rc, rc) # Not Yet Implemented # Should test if the player's # entire fleet has been sunk, # if so, game over! def fleetSunk(self): return False pass def close(self): self.board.win.close() # Initialize fleet def draw(self): self.board.draw() self.board.place(self.Carrier) self.board.place(self.Battleship) self.board.place(self.Cruiser) self.board.place(self.Submarine) self.board.place(self.Destroyer) def main(): # Open game boards player1 = Player("Player 1", 400, 400) player2 = Player("Player 2", 400, 400) player1.draw() player2.draw() while ~player1.fleetSunk() and ~player2.fleetSunk(): player1.turn(player2.board) player2.turn(player1.board) player1.getMouse() player1.close() player2.close() main()
Looking over this code we can see it is really rather simple. In main() we create two players and call draw() on them. Next, we enter a while loop. This loop will loop forever as I left the fleetSunk() method unimplemented. It is hard coded to return false. I’ve left implementing this method up to the reader.
Within the loop we call turn on each player passing in the opponent’s game board. Each game board is responsible for calculating it’s own size, and completing all drawing operations on it’s grid.
The player.turn() method takes the opponent’s game board as a parameter and and after getting the mouse click location, passes that location to the opponent’s board.hit() method. Next, we convert the pixel (x,y) values to (row, column) values and pass those to our own board’s mark() method to draw a blue circle to indicate where we’ve taken shots. You could leave this set out or toggle it to make the game more challenging.
The board’s hit method is not implemented in this code and is also left as an exercise for the reader. However, it should take the row, column values passed in and determine if any of the vessels on it’s board have been hit. If so, it should mark that vessel as damaged and increment the vessel.hit_count. This should be done by calling the vessel’s hit() method. The vessel is sunk if the hit_count matches the vessel’s length.
The player’s fleetSunk() method should simply test if all the player’s vessels have been sunk and return true if they have.
I’m leaving the completion of this up to the readers. You’ll most likely want to add some type of scoring. You might even change the X draw for hit’s and the circle drawn as a marker, to an image of an explosion and a slash in the water respectively. You might also make the shooter’s board indicate whether the shot was a hit or a miss. You should have all the tools you need to implement these features. If you take a little time and analyze each class and each of it’s methods, you should have little trouble.
There is one issue that this code has I didn’t have time to correct. That is that the vessels are drawn at random locations and therefor often overlap each other. This isn’t good, as one shot can damage two vessels. This might be allowed if this were Angry Birds (two bird, one shot…). However, it’s Battleship! SO you’ll need to implement some method for ensuring that all vessels are placed in such a manner that they wont overlap. You can find one such solution here: https://stackoverflow.com/questions/3265986. This isn’t the only solution but it’s one that isn’t too hard to implement. Do note that one issue with this method is that everything is placed around a focal point that will never be occupied by a vessel. Effectively ensuring that the center call of the board will always be empty. This could be dealt with by shrinking the field for the purpose of the placement calculation and then randomly shifting it up or down one row.
Good luck! If you have questions of comments I’d enjoy hearing from you.