a9_1.py

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("<Configure>", self._redrawBoard)
		self.canvas.bind("<Button-1>", 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 tkMessageBox 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.iteritems():
			piece = self.canvas.create_oval(self._bbox(ij),
							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
		piece = self.canvas.create_oval(self._bbox(ij),
							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()