a9_1a.py

# 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()