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