#!/usr/bin/perl ############## # to do ############## # # blanks are not being unhidden properly # bomb-counting is not quite right, thus YOU WIN is not working # need 'options' to change board size on-the-fly # ??? use tiny gifs of famous people as bombs # ############## use Tk; ########################################################## # globals ########################################################## $numcols = 8 unless $numcols = shift; $numrows = 8 unless $numrows = shift; # dimensions of rectangles (cells) on minefield $cellwidth = .5; $cellheight = .5; # width of rectangle (cell) borders on minefield $hidden_width = "1.5p"; $unhidden_width = ".7p"; # color of hidden /unhidden rectangle borders $hiddenOutlineColor = "white"; $unHiddenOutlineColor = "darkgrey"; my $totalcells = $numcols * $numrows; my $numBombs = int($totalcells * .17); # bombs should be ~20% of total cells # Used to cycle through the labels for the cells. # blank becomes F (flag) which becomes question mark, which becomes blank my %changeto = ( ' '=>'F', 'F'=>'?', '?'=>' ', ); @colors = qw(yellow blue green red navy brown pink skyblue orange); ########################################################## # main program starts here ########################################################## srand(); $M = new MainWindow(-title=>"Perl Mines"); # set up the basic game window with a row of buttons and a minefield # the top frame holds the control buttons $control = $M->Frame(); $control->pack(); # add a "new game" button $new = $control->Button(-text=>"New Game",-command=>\&newGame); $new->pack(-side=>'left'); # add an "exit" button $exit = $control->Button(-text=>"Quit",-command=>[$M,"destroy"]); $exit->pack(-side=>'left'); # add a "cheat" button $Cheat = $control->Button(-text=>"Cheat",-command=>\&showAll); $Cheat->pack(-side=>'left'); buildMineField(); $display = $M->Frame(); $display->pack(); $bombsleftlab = $display->Label(-text=>"Bombs left: "); $bombsleftlab->pack(-side=>'left'); $bombsleftnum = $display->Label(-text=>" "); $bombsleftnum->pack(-side=>'left'); # set up the game for the first time newGame(); # # start the event loop MainLoop(); ########################################################## # subroutines start here ########################################################## # build the initial minefield sub buildMineField { my ($h,$w,$row,$col,$xt,$yt,$text); $h = ($numrows * $cellheight) + (2 * $cellheight); $w = ($numcols * .5) + (2 * $cellwidth); $minefield = $M->Canvas(-height=>"${h}c",-width=>"${w}c"); # set borders to -1 so they don't appear to be blanks # this makes neighbor checking easier later foreach $col (0..($numcols+1)) { $cells[$col][0] = -1; } foreach $row (0..($numrow+1)) { $cells[0][$row] = -1; } foreach $col (1..$numcols) { foreach $row (1..$numrows) { $x = ($col * $cellwidth); $y = ($row * $cellheight); $xt = ($col * $cellwidth)+.25; $yt = ($row * $cellheight)+.25; $x2 = ($col * $cellwidth)+.5; $y2 = ($row * $cellheight)+.5; # make a tag for the cell (rectangle) $tag = "r$col-$row"; $minefield->createRectangle("${x}c","${y}c","${x2}c","${y2}c", -outline=>$hiddenOutlineColor,-tag=>$tag,-width=>"1p"); # make a tag for the text in the cell $tag = "c$col-$row"; $tt = $minefield->createText("${xt}c","${yt}c",-text=>" ", -fill=>'blue',-tag=>$tag); $minefield->bind($tag,'<1>',[\&doLeft,$tag]); $minefield->bind($tag,'<3>',[\&doRight,$tag]); # $minefield->bind($tag,'<1>',[\&onclick,$tag]); } } # and add it to the game window $minefield->pack(); } sub onclick { print "in onclick!\n"; } sub doRight { my ($c,$tag) = @_; my ($curr,$new); $curr = $c->itemcget($tag,'-text'); $new = $changeto{$curr}; $c->itemconfigure($tag,-text=>$new,-fill=>'black'); if ($new eq 'F') { $numBombsLeft--; $tag =~ s/^c/r/; $c->itemconfigure($tag,-outline=>$unHiddenOutlineColor); } if ($new eq '?') { $numBombsLeft++; } if ($numBombsLeft > 0) { $bombsleftnum->configure(-text=>$numBombsLeft); } else { $bombsleftnum->configure(-foreground=>'red',-text=>"You win!"); } } sub doLeft { my ($c,$tag) = @_; my ($col,$row,$count); ($col,$row) = ($tag =~ /c(\d+)-(\d+)/); $count = $cells[$col][$row]; if ($count == 9) { # boom! showBombs(); $bombsleftnum->configure(-foreground=>'red',-text=>"Boom!"); } elsif ($count > 0) { $c->itemconfigure($tag,-text=>$cells[$col][$row], -fill=>$colors[$cells[$col][$row]]); } elsif ($count == 0) { # if blank cell unhide surrounding cells recursively unHideBlanks($col,$row,1); } $c->itemconfigure("r$col-$row",-outline=>$unHiddenOutlineColor); } # next 2 subs are only **kind of** working... sub unHideBlanks { my($col,$row,$flag) = @_; if ($flag) { # start new search @checked = 0; } return if ($col < 1 or $col > $numcols); return if ($row < 1 or $row > $numrows); return if $checked[$col][$row]; # without this, function would # recurse forever... $checked[$col][$row] = 1; # set flag to prevent infinite recursion $minefield->itemconfigure("r$col-$row",-outline=>$unHiddenOutlineColor); # check neighbors checkCells(($col-1),($row-1)); # nw checkCells(($col), ($row-1)); # n checkCells(($col+1),($row-1)); # ne checkCells(($col-1),($row)); # w checkCells(($col+1),($row)); # e checkCells(($col-1),($row+1)); # sw checkCells(($col), ($row+1)); # s checkCells(($col+1),($row+1)); # se } sub checkCells { my($col,$row) = @_; # don't go beyond borders return if ($col < 1 or $col > $numcols); return if ($row < 1 or $row > $numrows); if ($cells[$col][$row] == 0) { unHideBlanks($col,$row,0); } else { $minefield->itemconfigure("c$col-$row",-text=>$cells[$col][$row], -fill=>$colors[$cells[$col][$row]]); } $minefield->itemconfigure("r$col-$row",-outline=>$unHiddenOutlineColor); } sub newGame { my ($col,$row); # redistribute the mines placeBombs(); @checked = (); $numBombsLeft = $numBombs; # calculate numbers for non-mine cells placeNumbers(); $bombsleftnum->configure(-text=>$numBombs); # clear the board foreach $col (1..$numcols) { foreach $row (1..$numrows) { $minefield->itemconfigure("c$col-$row",-text=>" "); $minefield->itemconfigure("r$col-$row", -outline=>$hiddenOutlineColor); } } } # used for debugging -- display bombs and numbers together... sub showAll { foreach $col (1..$numcols) { foreach $row (1..$numrows) { $xt = ($col * $cellwidth)+($cellwidth/2); $yt = ($row * $cellheight)+($cellheight/2); if ($cells[$col][$row] == 9) { $fill = "black"; $text = "B"; } elsif ($cells[$col][$row] == 0) { $fill = "black"; $text = " "; } else { $text = $cells[$col][$row]; $fill = $colors[$cells[$col][$row]]; } $minefield->itemconfigure("c$col-$row",-fill=>$fill, -text=>$text); } } } # used when player uncovers a bomb... sub showBombs { foreach $col (1..$numcols) { foreach $row (1..$numrows) { $xt = ($col * $cellwidth)+($cellwidth/2); $yt = ($row * $cellheight)+($cellheight/2); if ($cells[$col][$row] == 9) { $minefield->itemconfigure("c$col-$row",-fill=>"black",-text=>"B"); } } } } sub placeBombs { my ($n) = $numBombs; my ($col,$row); my ($i,$j); @cells = (); while ($n) { $col = int(rand($numcols))+1; $row = int(rand($numrows))+1; next if $cells[$col][$row] == 9; # only get here if this col and row do NOT contain a bomb $cells[$col][$row] = 9; # mark this cell with a bomb $n--; } } sub placeNumbers { my $bombs; foreach $col (1..$numcols) { foreach $row (1..$numrows) { $bombs = 0; next if $cells[$col][$row] == 9; # skip bombs # check 8 possibilities for neighbors to a cell... $bombs++ if $cells[$col-1][$row-1] == 9; # nw $bombs++ if $cells[$col] [$row-1] == 9; # n $bombs++ if $cells[$col+1][$row-1] == 9; # ne $bombs++ if $cells[$col-1][$row] == 9; # w $bombs++ if $cells[$col+1][$row] == 9; # e $bombs++ if $cells[$col-1][$row+1] == 9; # sw $bombs++ if $cells[$col] [$row+1] == 9; # s $bombs++ if $cells[$col+1][$row+1] == 9; # se $cells[$col][$row] = $bombs; } } }