Let us look at the design of the user interface (specifically, our strategy for using the canvas widget effectively) and the data structures that record the state of the game and are not dependent on the user interface.
Tetris's screen layout, shown in Figure 15.1, is straightforward. You need two button widgets for "Start/Pause" and "Quit," a canvas widget for the graphics, and a main window to contain all this stuff.
The grid and the blocks are drawn on the canvas. Each block is composed of several square tiles moving as one unit. The heap is a similar collection of tiles. Each tile is a canvas item. An alternative for drawing the block is a filled polygon, but the tiled version is much simpler to implement. The block's tiles are tagged with the string "block," so it is easy to move them around as one unit, using the canvas's move method. We also remember each tile's canvas item ID so that they can be individually deleted or moved.
One concern with animation is flicker when the monitor's periodic refresh picks up changes to video memory as they are happening. To prevent flicker, you need to resort to double-buffering: first render the new animation frame into a pixmap and then copy that pixmap quickly into video memory. Fortunately, the canvas widget already does double-buffering, so we can simply move canvas items around without fear of flicker.
Both the block and the heap carry two pieces of information for each tile: its position on the grid (the cell) and the ID given by the canvas widget. The position can be expressed either as a row and a column or as a cell index expressed as (row * (columns in grid) + column). The formula assumes that both row and column are 0-based. That is, the cells are numbered from left to right and from top to bottom in increasing numbers starting from 0. Figure 15.1 shows the numbers assigned to the leftmost cells in each row. This approach is convenient, since it encodes both row and column into one number and easily fits into the data structures discussed next.
My first attempt was to model the block and the heap as objects. A tile has two attributes, tile_id (the ID given by the canvas widget) and position, and the block and heap have one attribute each, a sequence of tiles. But because there is only one block and one heap at any time, and because a tile belongs to either the block or the heap, I chose a simpler approach. The block is represented by the array @block_cells, each element of which contains the cell number occupied by the corresponding tile. Similarly, each element of the array @tile_ids contains the ID of the canvas widget item representing the block's tile in that position. The heap is organized in a different way. The array @heap contains as many elements as there are cells in the grid; an element of this array either is undef or contains the appropriate canvas item ID of a tile belonging to the heap. Of the different ways in which I tried organizing the block and heap information, I found that this scheme is the most convenient for the two complex operations: rotating the block and merging the block and heap.