Hi all,

The continuing quest to master Tkinter has led to this implementation of the dots and boxes game. The rules are well-known: each player gets to create a new line between two dots (single-click near the line you wish to create). If a player creates a box, he gets a point *and* gets to go again. (Two boxes gets two points, but only one extra turn.) The highest score out of 81 boxes wins!

Please feel free to mercilessly criticize the UI or the code; I'm looking to improve it and submit to the Code Snippets thread.

Thanks,
Jeff

P.S. Anyone know how to make canvas text be semi-transparent?

from Tkinter import *
import tkFont

TOL = 8
CELLSIZE = 40
OFFSET = 10
CIRCLERAD = 2
DOTOFFSET = OFFSET + CIRCLERAD
GAME_H = 400
GAME_W = 400


class Player(object):

    def __init__(self, name, color="black"):
        self.score = 0
        self.str = StringVar()
        self.name = name
        self.color = color

    def update(self):
        self.str.set(self.name + ": %d" % self.score)



class MyFrame(Frame):

    def __init__(self, master):
        Frame.__init__(self, master)
        self.GO_font = tkFont.Font(self, \
                                   name="GOFont", \
                                   family = "Times", \
                                   weight="bold", \
                                   size=36)
        self.canvas = Canvas(self, height = GAME_H, width = GAME_W)
        self.canvas.bind("<Button-1>", lambda e:self.click(e))
        self.canvas.grid(row=0,column=0)

        self.dots = [[self.canvas.create_oval(CELLSIZE*i+OFFSET, \
                                              CELLSIZE*j+OFFSET, \
                                              CELLSIZE*i+OFFSET+2*CIRCLERAD, \
                                              CELLSIZE*j+OFFSET+2*CIRCLERAD, \
                                              fill="black") \
                      for j in range(10)] for i in range(10)]
        self.lines = []

        self.infoframe = Frame(self)
        self.players = [Player("Player 1","blue"), Player("Player 2","red")]
        self.infoframe.players = [Label(self.infoframe, textvariable = i.str) for i in self.players]
        for i in self.infoframe.players:
            i.grid()
        
        self.turn = self.players[0]
        self.update_players()
        self.infoframe.grid(row = 0, column = 1, sticky = N)

        self.grid()

    def update_players(self):
        for i in self.players:
            i.update()

    def click(self, event):
        x,y = event.x, event.y
        orient = self.isclose(x,y)

        if orient:
            if self.line_exists(x,y, orient):
                return
            l = self.create_line(x,y, orient)
            score = self.new_box_made(l)
            if score:
                self.turn.score += score
                self.turn.update()
                self.check_game_over()
            else:
                index = self.players.index(self.turn)
                self.turn = self.players[1-index]
            self.lines.append(l)

    def create_line(self, x, y, orient):
        startx = CELLSIZE * ((x-OFFSET)//CELLSIZE) + DOTOFFSET
        starty = CELLSIZE * ((y-OFFSET)//CELLSIZE) + DOTOFFSET
        tmpx = (x-OFFSET)//CELLSIZE
        tmpy = (y-OFFSET)//CELLSIZE
        
        if orient == HORIZONTAL:
            endx = startx + CELLSIZE
            endy = starty
        else:
            endx = startx
            endy = starty + CELLSIZE
        #print "line drawn: %d,%d to %d,%d" % (startx,starty,endx,endy)
        return self.canvas.create_line(startx,starty,endx,endy)
        

    def new_box_made(self, line):
        score = 0
        x0,y0,x1,y1 = self.canvas.coords(line)
        if x0 == x1: # vertical line
            midx = x0
            midy = (y0+y1)/2
            pre = (x0 - CELLSIZE/2, midy)
            post = (x0 + CELLSIZE/2, midy)
        elif y0 == y1: # horizontal line
            midx = (x0 + x1)/2
            midy = y0
            pre = (midx, y0 - CELLSIZE/2)
            post = (midx, y0 + CELLSIZE/2)
        
        if len(self.find_lines(pre)) == 3:  # not 4, because newly created line is 
            self.fill_in(pre)               # is not returned (?!)
            score += 1
        if len(self.find_lines(post)) == 3:
            self.fill_in(post)
            score += 1
        return score

    def find_lines(self, coords):
        x, y = coords
        if x < 0 or x > GAME_W:
            return []
        if y < 0 or y > GAME_W:
            return []
        #print "Cell center: %d,%d" % (x,y)
        lines = [x for x in self.canvas.find_enclosed(x-CELLSIZE,\
                                                      y-CELLSIZE,\
                                                      x+CELLSIZE,\
                                                      y+CELLSIZE)\
                 if x in self.lines]
        #print lines
        return lines

    def fill_in(self, coords):
        x,y = coords
        self.canvas.create_text(x,y,text=self.turn.name, fill=self.turn.color)
        
    def isclose(self, x, y):
        x -= OFFSET
        y -= OFFSET
        dx = x - (x//CELLSIZE)*CELLSIZE
        dy = y - (y//CELLSIZE)*CELLSIZE
        
        if abs(dx) < TOL:
            if abs(dy) < TOL:
                return None  # mouse in corner of box; ignore
            else:
                return VERTICAL
        elif abs(dy) < TOL:
            return HORIZONTAL
        else:
            return None

    def line_exists(self, x,y, orient):
        id_ = self.canvas.find_closest(x,y,halo=TOL)[0]
        if id_ in self.lines:
            return True
        else:
            return False

    def check_game_over(self):
        total = sum([x.score for x in self.players])
        if total == 81:
            self.canvas.create_text(GAME_W/2, GAME_H/2, \
                                    text="GAME OVER", font="GOFont", \
                                    fill="#888")
        
        
mainw = Tk()
mainw.f = MyFrame(mainw)
mainw.mainloop()

At first glance and play --> nice game, but I would like to know which player's turn it is. You could put that into the title, or color the line for each player (red or blue).

At first glance and play --> nice game, but I would like to know which player's turn it is. You could put that into the title, or color the line for each player (red or blue).

I liked the title idea. Done.

How can I copy this code into Python? If I just copy-paste, all the indents go away and the whole thing doesn´t work.

Double click on the code to select all of it, then right click selection and pick copy from the dropdown menu.

This question has already been answered. Start a new discussion instead.