DEBUG = False def main(): # Process command line arguments import getopt, sys opts, args = getopt.getopt(sys.argv[1:], "ds:1:2:") if args: print >> sys.stderr, ("Usage: gomoku " "[-d] " "[-s size] " "[-1 player1_color] " "[-2 player1_color]") raise SystemExit(1) # Initialize variables global DEBUG size = 19 # Default board is 19x19 side1 = "white" # with one side being white side2 = "black" # and the other black for opt, val in opts: if opt == "-d": # Turn on debugging DEBUG = True elif opt == "-s": # Check board size try: size = int(val) except ValueError: print >> sys.stderr, "size must be integer" raise SystemExit(1) elif opt == "-1": # Check color for side 1 # There should be error checks to make sure # that the color is valid side1 = val elif opt == "-2": # Check color for side 2 # There should be error checks to make sure # that the color is valid side2 = val # Create board board = Gomoku(size, side1, side2) # Run until they do not want to play anymore while True: board.run() # Ask whether to start new game. If not, exit. if not board.ask("Start another game?"): break class Gomoku(object): def __init__(self, size, side1, side2): # Save away board parameters self.size = size self.side1 = side1 self.side2 = side2 # Create toplevel import tkinter self.app = tkinter.Tk() self.app.protocol("WM_DELETE_WINDOW", self.terminate) # Add place to show debugging output if DEBUG: self.dbg = tkinter.Label(self.app, text="Debug message appears here") self.dbg.pack(fill="x", expand=False, side="bottom") # Add place to show normal messages self.msg = tkinter.Label(self.app, text="Message appears here") self.msg.pack(fill="x", expand=False, side="bottom") # Board is drawn using a canvas self.canvas = tkinter.Canvas(self.app, width=400, height=400, bg="gray") self.canvas.pack(fill="both", expand=True) self.canvas.bind("", self._redrawBoard) self.canvas.bind("", self._userClick) self.reset() # cellSize is the size of each cell in pixels and is # updated by _redrawBoard when we know the size of the canvas self.cellSize = 0 # lastPlayed is used to track whether user clicked on a cell. self.lastPlayed = None def run(self): self.message("Starting new game") self.reset() while True: self.play(self.side1) if self.finished: break self.play(self.side2) if self.finished: break def play(self, side): self.message("%s to play" % side) # If there is no open space left, it's a draw used = len(self.pieces) cellCount = self.size * self.size if used == cellCount: self.finished = True self.message("It's a draw") return # Wait for player to make a move self.playing = side self.lastPlayed = None self.app.mainloop() if self.lastPlayed: # Check for winning move (checking all four # possible directions) i, j = self.lastPlayed if (self._checkWin(side, i, j, 1, 0) or self._checkWin(side, i, j, 1, 1) or self._checkWin(side, i, j, 0, 1) or self._checkWin(side, i, j, -1, 1)): self.finished = True self.message("%s wins" % side) self.app.update_idletasks() self.debug("%s played" % side) def message(self, msg): # Display message in UI self.msg.config(text=msg) def debug(self, msg): # Display debug message in UI if not DEBUG: return self.dbg.config(text=msg) def ask(self, question): # Ask player the question and return True or False. # Use standard tkinter dialog for this. from tkinter.messagebox import askyesno return askyesno("Gomoku", question) def terminate(self): # Mark game as finished if self.ask("Really quit?"): self.app.quit() self.finished = True self.message("Game terminated") def reset(self): # Reset variables for next game self.finished = False self.playing = None self.pieces = dict() self.canvas.delete("piece") def _redrawBoard(self, event): # Redraw board according to new window size. # Compute individual cell size. We want an odd cell # size so that pieces appear centered. size = min(event.width, event.height) cellSize = int(size / self.size) if cellSize % 2 == 0: self.cellSize = cellSize - 1 else: self.cellSize = cellSize self.offset = int(self.cellSize / 2) + 1 # Redraw the board low = self.offset high = (self.size - 1) * self.cellSize + self.offset self.canvas.delete("board") for i in range(self.size): where = (i * self.cellSize) + self.offset self.canvas.create_line(where, low, where, high, tags="board") self.canvas.create_line(low, where, high, where, tags="board") # Reposition the played pieces self.canvas.delete("piece") for ij, (fill, piece) in self.pieces.items(): i, j = ij piece = self.canvas.create_oval(self._bbox(i, j), tags="piece", fill=fill, outline="") self.pieces[ij] = (fill, piece) def _userClick(self, event): if self.finished or self.cellSize == 0: # Not playing, just return return # Respond to user clicking on canvas # Map click to cell ij = (int(event.x / self.cellSize), int(event.y / self.cellSize)) self.debug("Clicked (%d,%d)" % ij) # Make sure cell is not already occupied if ij in self.pieces: self.debug("Already filled (%d,%d)" % ij) return # Create new piece to fill the cell i, j = ij piece = self.canvas.create_oval(self._bbox(i, j), tags="piece", fill=self.playing, outline="") self.pieces[ij] = (self.playing, piece) # Update lastPlayed so that we know user played a piece # rather than quit the game. self.lastPlayed = ij # Exit mainloop (see "play" above) self.app.quit() def _bbox(self, i, j): # Return the bounding box for cell (i, j) llx = i * self.cellSize urx = llx + self.cellSize lly = j * self.cellSize ury = lly + self.cellSize return llx, lly, urx, ury def _checkWin(self, fill, i, j, di, dj): # Player "fill" just clicked cell (i, j). # We want to see if that was a winning move, # ie, if this cell is part of five cells in # a row with the same fill. # (di, dj) defines the one-cell offset (so we # may be checking on diagonals or along x or # y axes). We loop over the five positions # that (i, j) may occupy in a 5-cell run, and # see if all those cells have the same fill. # So the outer loop is over the position that # (i, j) occupies in the run, and the inner # loop is over the 5 cells in the run. for d in range(5): si = i - d * di sj = j - d * dj for offset in range(5): ci = si + di * offset cj = sj + dj * offset try: f, p = self.pieces[(ci, cj)] except KeyError: # Cell is unoccupied (or non-existent) break else: if f != fill: # Cell is not ours break else: # All five have the same fill! return True return False if __name__ == "__main__": # We use a main function rather than putting the script # code here because that helps avoid creating a bunch # of global variables. main()