# 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("<Configure>", 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("<Button-1>", 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()