3

I'm trying to create a GUI for a virtual board for the game Go. There should be an nxn grid of tiles where a player can place a stone, either black or white. Clicking on a tile will make it change from tan(the default) to black, click again to white, and click a third time to go back to tan. Player one can click once on a spot to place a stone there, and player two can click twice (you need to remove stones later, so three clicks resets it). I created a tile object and then used a nested for loop to instantiate 9 by 9 of them. Unfortunately, running the code only seems to produce 1 functional tile, not 81. This code should work on any python machine (I'm using Python 3.4), so you can try to run it and see for yourself. Can anyone point out the reason the loop is only running once?

from tkinter import *
window = Tk()
n = 9

"""
A tile is a point on a game board where black or white pieces can be placed. If there are no pieces, it remains tan.
The basic feature is the "core" field which is a tkinter button. when the color is changed, the button is configured to represent this.
"""
class tile(object):
    core = Button(window, height = 2, width = 3, bg = "#F4C364")

    def __init__(self):
        pass

    """the cycle function makes the tile object actually change color, going between three options: black, white, or tan."""
    def cycle(self):

        color = self.core.cget("bg")

        if(color == "#F4C364"): #tan, the inital value.
            self.core.config(bg = "#111111")#white.
        elif (color == "#111111"):
            self.core.config(bg = "#DDDDDD")#black.
        else:
            self.core.config(bg = "#F4C364")#back to tan.

board = [] #create overall array
for x in range(n):
    board.append([])#add subarrays inside it
    for y in range(n):
        board[x].append(tile())#add a tile n times in each of the n subarrays
        T = board[x][y] #for clarity, T means tile
        T.core.config(command = lambda: T.cycle()) #I do this now because cycle hadn't been defined yet when I created the "core" field
        T.core.grid(row = x, column = y) #put them into tkinter.

window.mainloop()
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
gobblemeat
  • 33
  • 4
  • Your command function won't work properly: it will only cycle the final button. You can easily fix that: `command = lambda t=T: t.cycle()`. But there's an even better way; I'll post an answer shortly. – PM 2Ring Sep 27 '15 at 11:29

3 Answers3

1

As mhawke points out in his answer you need to make the core an instance variable, so that each Tile gets its own core.

And as I mention in my comment above, you also need to fix the Button's command callback function. The code you use in your question will call the .cycle() method of the current value of T, which happens to be the last tile created. So no matter where you click only the last tile changes color. One way to fix that is to pass the current tile as a default argument of the lambda function when you create it. But because you are using OOP to create your Tile there's a better way, which you can see below.

I've made a few modifications to your code.

Although many Tkinter examples use from tkinter import * it's not a good practice. When you do from some_module import * it brings all of the names from some_module into the current module (your script), which means you could accidentally override those names with your own names. Even worse, if you do import * with multiple modules each new module's names can clash with the previous module's names, and you have no way of knowing that's happened until you start getting mysterious bugs. Using import tkinter as tk means you need to do a little more typing, but it makes the resulting program less bug-prone and easier to read.

I've modified the __init__ method so that it is called with the window and the (x, y) location of the tile (it's customary to use x for the horizontal coordinate and y for the vertical coordinate). Each Tile object now keeps track of its current state, where 0=empty, 1=black, 2=white. This makes it easier to update the colors. And because we've passed in the window and (x, y) we can use that info to add the tile to the grid. The tile also remembers the location (in self.location), which may come in handy.

I've modified the cycle method so that it updates both the background color and the activebackground of the tile. So when the mouse hovers over the tile it changes to a color that's (roughly) halfway between its current color and the color it will turn if you click it. IMO, this is nicer than the tile always turning pale grey when the mouse hovers over it.

I've also optimized the code that creates all the tiles and stores them in the board list of lists.

import tkinter as tk

colors = (
    #background, #activebackground
    ("#F4C364", "#826232"), #tan
    ("#111111", "#777777"), #black
    ("#DDDDDD", "#E8C8A8"),  #white
)

class Tile(object):
    """ A tile is a point on a game board where black or white pieces can be placed. 
        If there are no pieces, it remains tan.
        The basic feature is the "core" field which is a tkinter button. 
        when the color is changed, the button is configured to represent this.
    """

    def __init__(self, win, x, y):
        #States: 0=empty, 1=black, 2=white
        self.state = 0
        bg, abg = colors[self.state]

        self.core = tk.Button(win, height=2, width=3,
            bg=bg, activebackground=abg,
            command=self.cycle)
        self.core.grid(row=y, column=x)

        #self.location = x, y

    def cycle(self):
        """ the cycle function makes the tile object actually change color,
            going between three options: black, white, or tan.
        """
        #cycle to the next state. 0 -> 1 -> 2 -> 0
        self.state = (self.state + 1) % 3
        bg, abg = colors[self.state]
        self.core.config(bg=bg, activebackground=abg)

        #print(self.location)


window = tk.Tk()
n = 9

board = [] 
for y in range(n):
    row = [Tile(window, x, y) for x in range(n)]
    board.append(row)

window.mainloop()
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
0

The problem is that core is a class variable which is created once and shared by all instances of class tile. It should be an instance variable for each tile instance.

Move core = Button(window, height = 2, width = 3, bg = "#F4C364") into tile.__init__() like this:

class Tile(object):
    def __init__(self):
        self.core = Button(window, height = 2, width = 3, bg = "#F4C364")
mhawke
  • 84,695
  • 9
  • 117
  • 138
0

The root of the problem is that core is shared by all instances of the class by virtue of how you've defined it. You need to move creation of the button into the initializer.

I also suggest moving the configuration of the command into the button itself. The caller shouldn't need (nor care) how the button works internally. Personally I'd have the tile inherit from Button, but if you favor composition over inheritance I'll stick with that.

Example:

class tile(object):

    def __init__(self):
        self.core = Button(window, height = 2, width = 3, bg = "#F4C364"
                           command=self.cycle)
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685