15.3 ImplementationWe will focus only on those routines that either are central to the game or illustrate Tk in action. The main program simply consists of the two calls to init() and MainLoop() . init creates the screen, sets up the key bindings, and configures a timer to call tick . Let us jump into the meat of the action by starting with this procedure. tick moves the block down and then reloads the timer, specifying itself as a callback: sub tick { return if ($state == $PAUSED); if (!@block_cells) { if (!create_random_block()) { game_over(); # Heap is full:could not place block return; # at next tick interval } $w_top->after($interval, \&tick); return; } move_down(); # move the block down $w_top->after($interval, \&tick); # reload timer for nex } fall() is called when the space bar is hit; it keeps moving the block down until it hits any tile in the heap or hits bottom. move_down returns a false when either of these happens. sub fall { # Called when spacebar hit return if (!@block_cells); # Return if not initialized 1 while (move_down()); # Move down until it hits heap or bottom. }
move_down()
simply adds
$MAX_COLS
to each of the block tile's cell positions to effectively move it one row down. It then checks whether any of these new positions touch the bottom of the grid or intersect with any preexisting heap tile's cell position. If so, it calls the sub move_down { my $cell; my $first_cell_last_row = ($MAX_ROWS-1)*$MAX_COLS; # if already at the bottom of the heap, or if a move down # intersects with the heap, then merge both. foreach $cell (@block_cells) { if (($cell >= $first_cell_last_row) || ($heap[$cell+$MAX_COLS])) { merge_block_and_heap(); return 0; } } foreach $cell (@block_cells) { $cell += $MAX_COLS; } $w_heap->move('block', 0, $TILE_HEIGHT); return 1; }
$w_canvas->addtag('delete', 'withtag' => $heap[$i]);
The straightforward way to delete a row would be to remove the corresponding entries in the heap and to delete the corresponding tiles on the canvas. But that method doesn't give the user an idea of which rows are being consolidated; besides, it's too boring. So
merge_block_and_heap
fills all the tiles tagged my $last_cell = $MAX_COLS * $MAX_ROWS; sub merge_block_and_heap { my $cell; # merge block foreach $cell (@block_cells) { $heap[$cell] = shift @tile_ids; } $w_heap->dtag('block'); # Forget about the block - it is now merged # check for full rows, and get rid of them # All rows above them need to be moved down, both in @heap and # the canvas, $w_heap my $last_cell = $MAX_ROWS * $MAX_COLS; my $filled_cell_count; my $rows_to_be_deleted = 0; my $i; for ($cell = 0; $cell < $last_cell; ) { $filled_cell_count = 0; my $first_cell_in_row = $cell; for ($i = 0; $i < $MAX_COLS; $i++) { $filled_cell_count++ if ($heap[$cell++]); } if ($filled_cell_count == $MAX_COLS) { # this row is full for ($i = $first_cell_in_row; $i < $cell; $i++) { $w_heap->addtag('delete', 'withtag' => $heap[$i]); } splice(@heap, $first_cell_in_row, $MAX_COLS); unshift (@heap, (undef) x $MAX_COLS); $rows_to_be_deleted = 1; } } @block_cells = (); @tile_ids = (); if ($rows_to_be_deleted) { $w_heap->itemconfigure('delete', '-fill'=> 'white'); $w_top->after (300, sub { $w_heap->delete('delete'); my ($i); my $last = $MAX_COLS * $MAX_ROWS; for ($i = 0; $i < $last; $i++) { next if !$heap[$i]; # get where they are my $col = $i % $MAX_COLS; my $row = int($i / $MAX_COLS); $w_heap->coords( $heap[$i], $col * $TILE_WIDTH, #x0 $row * $TILE_HEIGHT, #y0 ($col+1) * $TILE_WIDTH, #x1 ($row+1) * $TILE_HEIGHT); #y1 } }); } } Let us now look at two of the other routines to manipulate the block: move_left and rotate . We'll skip move_right because it is similar to move_left .
sub move_left { my $cell; foreach $cell (@block_cells) { # Check if cell is at the left edge already # If not, check whether the cell to its left is already # occupied if ((($cell % $MAX_COLS) == 0) || ($heap[$cell-1])){ return; } } foreach $cell (@block_cells) { $cell--; # This affects the contents of @block_cells } $w_heap->move('block', - $TILE_WIDTH, 0); } rotate is a trifle more complex. It computes a pivot row and column from the block's tile positions and calculates new tile positions by a simple transformation explained in the following code. It also ensures that the newly computed positions are not illegal in any way (moving out of the grid or intersecting with the heap). It then calls the canvas's coords method to move each of the tiles individually to their new places. sub rotate { # rotates the block counter_clockwise return if (!@block_cells); my $cell; # Calculate the pivot position around which to turn # The pivot is at (average x, average y) of all block_cells my $row_total = 0; my $col_total = 0; my ($row, $col); my @cols = map {$_ % $MAX_COLS} @block_cells; my @rows = map {int($_ / $MAX_COLS)} @block_cells; foreach (0 .. $#cols) { $row_total += $rows[$_]; $col_total += $cols[$_]; } my $pivot_row = int ($row_total / @cols + 0.5); # pivot row my $pivot_col = int ($col_total / @cols + 0.5); # pivot col # To position each cell counter_clockwise, we need to do a small # transformation. A row offset from the pivot becomes an equivalent # column offset, and a column offset becomes a negative row offset. my @new_cells = (); my @new_rows = (); my @new_cols = (); my ($new_row, $new_col); while (@rows) { $row = shift @rows; $col = shift @cols; # Calculate new $row and $col $new_col = $pivot_col + ($row - $pivot_row); $new_row = $pivot_row - ($col - $pivot_col); $cell = $new_row * $MAX_COLS + $new_col; # Check if the new row and col are invalid (is outside or # something is already occupying that cell) # If valid, then no-one should be occupying it. if (($new_row < 0) || ($new_row > $MAX_ROWS) || ($new_col < 0) || ($new_col > $MAX_COLS) || $heap[$cell]) { return 0; } push (@new_rows, $new_row); push (@new_cols, $new_col); push (@new_cells, $cell); } # Move the UI tiles to the appropriate coordinates my $i= @new_rows-1; while ($i >= 0) { $new_row = $new_rows[$i]; $new_col = $new_cols[$i]; $w_heap->coords($tile_ids[$i], $new_col * $TILE_WIDTH, #x0 $new_row * $TILE_HEIGHT, #y0 ($new_col+1) * $TILE_WIDTH, #x1 ($new_row+1) * $TILE_HEIGHT); $i--; } @block_cells = @new_cells; 1; # Success } When this mutant version of Tetris starts, it draws a small red triangular "gun" (cell number 70 in Figure 15.1 ). shoot is called when the "a" or "s" key is pressed. The "a" key shoots an arrow from the gun and blows off the leftmost tile of the block in the gun's row if the block happens to be passing by. The "s" key takes a shot at the rightmost tile. This is quite cheesy, really, but useful if you want to see how an animation sequence can be staged by using the canvas. The first part of the procedure simply determines which block tile is to be removed, if any. It then creates an arrow (a line with an arrowhead) from the gun to the selected tile, changes its stippling, and after a 200-ms interval, deletes both the tile and the arrow. This has the visual effect of blowing up a tile. sub shoot { my ($dir) = @_; my $first_cell_shoot_row = $shoot_row*$MAX_COLS; my $last_cell_shoot_row = $first_cell_shoot_row + $MAX_COLS; my $cell; my (@indices) = sort { $dir eq 'left' ? $block_cells[$a] <=> $block_cells[$b] : $block_cells[$b] <=> $block_cells[$a] } (0 .. $#block_cells); my $found = -1; my $i; foreach $i (@indices) { $cell = $block_cells[$i]; if (($cell >= $first_cell_shoot_row) && ($cell < $last_cell_shoot_row)) { $found = $i; last; } } if ($found != -1) { my $shot_tile = $tile_ids[$found]; ($cell) = splice (@block_cells, $found, 1); splice (@tile_ids, $found, 1); my $y = ($shoot_row + 0.5)*$TILE_HEIGHT; my $arrow = $w_heap->create( 'line', 0, $y, (($cell % $MAX_COLS) + 0.5) * $TILE_WIDTH, $y, '-fill' => 'white', '-arrow' => 'last', '-arrowshape' => [7,7,3] ); $w_heap->itemconfigure($shot_tile, '-stipple' => 'gray25'); $w_top->after (200,sub { $w_heap->delete($shot_tile); $w_heap->delete($arrow); }); } }
Let us now see the two routines responsible for setting up the screen:
create_screen
and
bind_key
. Both these functions are called by
init()
. Note the way the
pack
method is used in
create_screen
and how the space character is translated to an event-binding in sub create_screen { $w_top = MainWindow->new('Tetris - Perl/Tk'); $w_heap = $w_top->Canvas('-width' => $MAX_COLS * $TILE_WIDTH, '-height' => $MAX_ROWS * $TILE_HEIGHT, '-border' => 1, '-relief' => 'ridge'); $w_start = $w_top->Button('-text' => 'Start', '-command' => \&start_pause, ); my $w_quit = $w_top->Button('-text' => 'Quit', '-command' => sub {exit(0)} ); $w_heap->pack(); $w_start->pack('-side'=> 'left', '-fill' => 'y', '-expand' => 'y'); $w_quit->pack('-side'=> 'right', '-fill' => 'y', '-expand' => 'y'); } sub bind_key { my ($keychar, $callback) = @_; if ($keychar eq ' ') { $keychar = "KeyPress-space"; } $w_top->bind("<${keychar}>", $callback); } |
|