# This solution uses the MVC (model-view-controller) paradigm. # The model is the game board and rules, implemented in class # Gomoku. The view is the canvas displayed in the user interface. # The controller is the event handler for mouse clicks in the # canvas that place new stones. def main(): """Play gomoku.""" # Initialize local variables size = 19 side1 = "white" side2 = "black" # Process command line arguments import getopt, sys opts, args = getopt.getopt(sys.argv[1:], "s:1:2:") if args: print >> sys.stderr, ("Usage: gomuku " "[-s size] " "[-1 player1_color] " "[-2 player2_color] ") raise SystemExit(1) for opt, val in opts: if opt == "-s": try: size = int(val) except ValueError: print >> sys.stderr, "size must be integer" raise SystemExit(1) elif opt == "-1": validate_color(val) side1 = val elif opt == "-2": validate_color(val) side2 = val # Create MVC (model-view-controller) instances # For our purpose, view and controller are both part # of the user interface ui = GomokuUI(Gomoku(size), side1, side2) # Run until user quits while True: ui.run() if not ui.ask("Start another game?"): break def validate_color(c): """Raise SystemExit exception if 'c' is not a valid color name.""" if c in [ "red", "green", "blue", "white", "black" ]: return import sys print >> sys.stderr, "%s is not a valid color" % c raise SystemExit(1) class Gomoku: """Gomoku game board.""" # "model" def __init__(self, size): "Create initial board." self.size = size # public attribute self.reset() def reset(self): "Reset the board." # "stone" is a public attribute # dictionary whose keys are (i, j) and values are player id self.stones = {} def play(self, i, j, player): "Add stone for player at (i, j) and see if game ends." self.stones[(i, j)] = player return (self._check_win(i, j, player, 1, 0) or self._check_win(i, j, player, 1, 1) or self._check_win(i, j, player, 0, 1) or self._check_win(i, j, player, -1, 1)) def _check_win(self, i, j, player, di, dj): "Check for winning run in given direction di, dj." # "player" 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 belong to the same player. # 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: p = self.stones[(ci, cj)] except KeyError: # Cell is unoccupied (or non-existent) break else: if p != player: # Cell is not ours break else: # All five belong to the same player! return True return False class GomokuUI: """Gomoku game user interface (view and controller).""" def __init__(self, game, c1, c2): self.game = game self._fill = { 1:c1, 2:c2 } # Set up widgets for display ("view") import Tkinter self.app = Tkinter.Tk() self.app.protocol("WM_DELETE_WINDOW", self._terminate) self.canvas = Tkinter.Canvas(self.app, width=400, height=400, bg="gray") self.canvas.grid(row=0, column=0, sticky="nsew") self.msg = Tkinter.Label(self.app, text="Message line") self.msg.grid(row=1, column=0, sticky="ew") # Make canvas (row=0, column=0) resizable self.app.rowconfigure(0, weight=1) self.app.columnconfigure(0, weight=1) self.canvas.bind("", self._redraw) # cellSize is the size of each cell in pixels and is # updated by _redraw when we know the size of the canvas self._cell_size = 0 # Set up event handlers for view ("controller") self.canvas.bind("", self._user_click) def run(self): "Play game until someone wins." self.message("Starting new game") self._reset() while True: if self._play(1): break if self._play(2): break def message(self, msg): "Display message in message line." self.msg.config(text=msg) def ask(self, question): "Ask yes/no question and return True/False." from tkMessageBox import askyesno return askyesno("Gomoku", question) def _reset(self): "Reset instance for new game." self.game.reset() self.canvas.delete("stone") self._finished = False def _play(self, player): "Let one player make a move." self.message("Player %d's turn" % player) # Check if there is any available space used = len(self.game.stones) cell_count = self.game.size * self.game.size if used >= cell_count: self._finished = True self.message("It's a draw!") return True # Set current player and go into loop waiting # user to select cell self._last_played = None self.app.mainloop() if self._finished: # User must have tried to close window return True # Get selected location (guaranteed unoccupied) # Display stone, then add to game i, j = self._last_played self._stone(i, j, player) if self.game.play(i, j, player): self.message("Player %d wins" % player) return True return False def _redraw(self, event): "Redraw board according to window size." size = min(event.width, event.height) cell_size = int(size / self.game.size) if cell_size % 2 == 0: self._cell_size = cell_size - 1 else: self._cell_size = cell_size offset = int(self._cell_size / 2) + 1 # Redraw the board low = offset high = (self.game.size - 1) * self._cell_size + offset self.canvas.delete("board") for i in range(self.game.size): where = (i * self._cell_size) + offset self.canvas.create_line(where, low, where, high, tags="board") self.canvas.create_line(low, where, high, where, tags="board") # Reposition the played stones self.canvas.delete("stone") for ij, player in self.game.stones.items(): i, j = ij self._stone(i, j, player) def _stone(self, i, j, player): "Return bounding box for location (i, j)." llx = i * self._cell_size urx = llx + self._cell_size lly = j * self._cell_size ury = lly + self._cell_size return self.canvas.create_oval((llx, lly, urx, ury), tags="stone", fill=self._fill[player], outline="") def _user_click(self, event): "Handle user mouse click in canvas." if self._finished or self._cell_size == 0: # not playing, just return return # Find click location indices i = int(event.x / self._cell_size) j = int(event.y / self._cell_size) ij = (i, j) if ij in self.game.stones: # cell is already occupied self.message("Cell (%d, %d) occupied" % (i, j)) return self._last_played = ij # End player turn self.app.quit() def _terminate(self): "Check if user wants to end game." if self.ask("Really quit?"): self.app.quit() self._finished = True self.message("Game terminated") if __name__ == "__main__": main()