#!perl -w # T([ia]c|oe) v1.1 # by Kevin Reid # # Tic-tac-toe against another human or the computer. # # Revision history # # 1.1 # * Player selection window now has keyboard control. # 1.0 # * First release. use Mac::Events; use Mac::Windows; use Mac::QuickDraw; use Mac::Dialogs; sub ltwh ($$$$) { my ($left, $top, $width, $height) = @_; new Rect ($left, $top, $left+$width, $top+$height); } @Lines = ( [[0, 0], [0, 1], [0, 2]], [[1, 0], [1, 1], [1, 2]], [[2, 0], [2, 1], [2, 2]], [[0, 0], [1, 0], [2, 0]], [[0, 1], [1, 1], [2, 1]], [[0, 2], [1, 2], [2, 2]], [[0, 0], [1, 1], [2, 2]], [[0, 2], [1, 1], [2, 0]], ); @SectLines = ( [3, 2, 3], [2, 4, 2], [3, 2, 3], ); $Title = 'Tic-Tac-Toe'; # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # sub userect { my ($item) = @_; SetDialogItemProc($Q_Dlg->window, $item, sub { FrameRect $Q_Dlg->item_box($_[1]); RGBForeColor new RGBColor(($_[1] == $focus ? 0 : 65535) x 3); FrameRect InsetRect $Q_Dlg->item_box($_[1]), 2, 2; RGBForeColor new RGBColor((0) x 3); }); $Q_Dlg->item_hit($item => sub {set_focus($_[1])}); } sub set_focus { my ($targ) = @_; my $of = $focus; $focus = $targ; DrawDialog $Q_Dlg->window if $of != $targ; } $Q_Dlg = new MacDialog ( ltwh(100, 90, 250, 170), 'Setup', 1, movableDBoxProc, 1, [kStaticTextDialogItem, ltwh(10, 10, 380, 16), 'Please select the player types:'], [kButtonDialogItem, ltwh(160, 140, 80, 20), 'Begin'], [kButtonDialogItem, ltwh( 60, 140, 80, 20), 'Quit'], [kRadioButtonDialogItem, ltwh(($w = 15)+8, 62, 80, 16), 'Human'], [kRadioButtonDialogItem, ltwh($w+8, 82, 80, 16), 'Computer'], [kUserDialogItem, ltwh($w, 50, 100, 56)], [kStaticTextDialogItem, ltwh($w+8, 42, 70, 16), '³X² Player'], [kRadioButtonDialogItem, ltwh(($w = 135)+8, 62, 80, 16), 'Human'], [kRadioButtonDialogItem, ltwh($w+8, 82, 80, 16), 'Computer'], [kUserDialogItem, ltwh($w, 50, 100, 56)], [kStaticTextDialogItem, ltwh($w+8, 42, 70, 16), '³O² Player'], ); $Q_OK = 0; $focus = 6; $Q_Dlg->item_hit(2 => sub {$Q_OK = 1}); $Q_Dlg->item_hit(3 => sub {$Q_OK = -1}); SetDialogDefaultItem($Q_Dlg->window, 2); SetDialogCancelItem($Q_Dlg->window, 3); $Q_Dlg->item_hit(4 => sub {set_focus(6); $IsComp{'x'} = 0; $Q_Dlg->item_value($_[1], 1); $Q_Dlg->item_value($_[1]+1, 0)}); $Q_Dlg->item_hit(5 => sub {set_focus(6); $IsComp{'x'} = 1; $Q_Dlg->item_value($_[1], 1); $Q_Dlg->item_value($_[1]-1, 0)}); $Q_Dlg->item_value(4, 1); $IsComp{'x'} = 0; userect 6; $Q_Dlg->item_hit(7 => sub {set_focus(6)}); $Q_Dlg->item_hit(8 => sub {set_focus(10); $IsComp{o} = 0; $Q_Dlg->item_value($_[1], 1); $Q_Dlg->item_value($_[1]+1, 0)}); $Q_Dlg->item_hit(9 => sub {set_focus(10); $IsComp{o} = 1; $Q_Dlg->item_value($_[1], 1); $Q_Dlg->item_value($_[1]-1, 0)}); $Q_Dlg->item_value(8, 1); $IsComp{'o'} = 0; userect 10; $Q_Dlg->item_hit(11 => sub {set_focus(10)}); $Q_Dlg->sethook(key => sub { (my $key = uc chr $_[1]) =~ tr/\cC/\cM/; my $btn = { "\cM" => 2, "\c[" => 3, "B" => 2, "Q" => 3, "\t" => $focus == 6 ? 10 : 6, "\x1c" => 6, "\x1d" => 10, "\x1e" => $focus == 6 ? 4 : 8, "\x1f" => $focus == 6 ? 5 : 9, "H" => $focus == 6 ? 4 : 8, "C" => $focus == 6 ? 5 : 9, }->{$key}; if ($btn) { $_[0]->hit($btn); } }); WaitNextEvent until $Q_OK; dispose $Q_Dlg; exit if $Q_OK == -1; $AllComp = not grep !$_, values %IsComp; # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # sub Init { @Board = ( [' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' '], ); $Player = 'o'; $PWin = undef; $Clicks = 0; SetPort $win->window; InvalRect $win->window->portRect; SetWTitle $win->window, "$Title - @{[uc $Player]}'s turn"; 1; } sub automove { # This algorithm is NOT perfect. I thought about improving # it, but why not let the other player win occasionally? # The rand() is so that it will randomly select from # equal moves. my ($me) = @_; my @pmoves; my $him = $me eq 'o' ? 'x' : 'o'; $Board[1][1] eq ' ' and push @pmoves, [15 + rand, 1, 1]; foreach (@Lines) { my $line = $Board[$$_[0][0]][$$_[0][1]] . $Board[$$_[1][0]][$$_[1][1]] . $Board[$$_[2][0]][$$_[2][1]]; # Finish my lines $line eq " $me$me" and push @pmoves, [10 + $SectLines[$$_[0][0]][$$_[0][1]] + rand, $$_[0][0], $$_[0][1]]; $line eq "$me $me" and push @pmoves, [10 + $SectLines[$$_[1][0]][$$_[1][1]] + rand, $$_[1][0], $$_[1][1]]; $line eq "$me$me " and push @pmoves, [10 + $SectLines[$$_[2][0]][$$_[2][1]] + rand, $$_[2][0], $$_[2][1]]; # Block opposing lines $line eq " $him$him" and push @pmoves, [5 + $SectLines[$$_[0][0]][$$_[0][1]] + rand, $$_[0][0], $$_[0][1]]; $line eq "$him $him" and push @pmoves, [5 + $SectLines[$$_[1][0]][$$_[1][1]] + rand, $$_[1][0], $$_[1][1]]; $line eq "$him$him " and push @pmoves, [5 + $SectLines[$$_[2][0]][$$_[2][1]] + rand, $$_[2][0], $$_[2][1]]; # If nothing else, find empty space $line =~ /^ ..$/ and push @pmoves, [0 + $SectLines[$$_[0][0]][$$_[0][1]] + rand, $$_[0][0], $$_[0][1]]; $line =~ /^. .$/ and push @pmoves, [0 + $SectLines[$$_[1][0]][$$_[1][1]] + rand, $$_[1][0], $$_[1][1]]; $line =~ /^.. $/ and push @pmoves, [0 + $SectLines[$$_[2][0]][$$_[2][1]] + rand, $$_[2][0], $$_[2][1]]; } die unless @pmoves; @pmoves = sort {$b->[0] <=> $a->[0]} @pmoves; return @{$pmoves[0]}[1..2]; } # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # $win = new MacWindow ( new Rect (100, 100, 300, 300), '', 1, documentProc, 1, ); Init(); $win->sethook(layout => sub { my $wRect = $win->window->portRect; $CWidth = int($wRect->right / 3); $CHeight = int($wRect->bottom / 3); SizeWindow $win->window, $CWidth * 3, $CHeight * 3; SetPort $win->window; InvalRect $wRect; }); $win->layout; $win->sethook(redraw => sub { PenSize(2, 2); RGBForeColor new RGBColor(0,0,0); MoveTo($CWidth - 1, 0); LineTo($CWidth - 1, $CHeight*3); MoveTo($CWidth*2 - 1, 0); LineTo($CWidth*2 - 1, $CHeight*3); MoveTo(0, $CHeight - 1); LineTo($CWidth*3, $CHeight - 1); MoveTo(0, $CHeight*2 - 1); LineTo($CWidth*3, $CHeight*2 - 1); PenSize(5, 5); for ($h = 0; $h < 3; $h++) { for ($v = 0; $v < 3; $v++) { my $p = $Board[$h][$v]; RGBForeColor($p !~ /[XO]/ ? new RGBColor(0,0,0) : new RGBColor(0,65535,0)); $p = lc $p; if ($p eq 'o') { FrameOval(new Rect( $h * $CWidth + $CWidth * .1, $v * $CHeight + $CHeight * .1, ($h+1) * $CWidth - $CWidth * .1, ($v+1) * $CHeight - $CHeight * .1, )); } elsif ($p eq 'x') { MoveTo( $h * $CWidth + $CWidth * .1, $v * $CHeight + $CHeight * .1, ); LineTo( ($h+1) * $CWidth - $CWidth * .1 - 5, ($v+1) * $CHeight - $CHeight * .1 - 5, ); MoveTo( ($h+1) * $CWidth - $CWidth * .1 - 5, $v * $CHeight + $CHeight * .1, ); LineTo( $h * $CWidth + $CWidth * .1, ($v+1) * $CHeight - $CHeight * .1 - 5, ); } } } }); $win->sethook(click => sub { my $pt = $_[1]; Init() and return if $PWin; my $gridh = int($pt->h/$CWidth); my $gridv = int($pt->v/$CHeight); return if $Board[$gridh][$gridv] ne ' '; my $r = new Rect( $gridh * $CWidth + ($CWidth * .1), $gridv * $CHeight + ($CHeight * .1), ($gridh+1) * $CWidth - ($CWidth * .1), ($gridv+1) * $CHeight - ($CHeight * .1) ); SetPort($win->window); my ($in, $oin) = (1, 1); InvertRect($r); while (StillDown()) { $in = PtInRect(GetMouse, $r); if ($in != $oin) { InvertRect($r); } $oin = $in; } return unless $in; move($gridh, $gridv); }); sub move { my ($gridh, $gridv) = @_; return if $Board[$gridh][$gridv] ne ' '; $Clicks++; $Board[$gridh][$gridv] = $Player; $Player = ($Player eq 'o') ? 'x' : 'o'; if ($Clicks >= 9) { SetWTitle $win->window, "$Title - Draw"; $PWin = -1; $Wins{'Draws'}++; Init() if $AllComp; } else { SetWTitle $win->window, "$Title - @{[uc $Player]}'s turn"; } SetPort $win->window; InvalRect new Rect( $gridh * $CWidth + ($CWidth * .1), $gridv * $CHeight + ($CHeight * .1), ($gridh+1) * $CWidth - ($CWidth * .1), ($gridv+1) * $CHeight - ($CHeight * .1) ); # Win check routine: foreach (@Lines) { next unless (my $p = $Board[$$_[0][0]][$$_[0][1]]) ne ' '; next unless $p eq $Board[$$_[1][0]][$$_[1][1]]; next unless $p eq $Board[$$_[2][0]][$$_[2][1]]; $PWin = $p; $Board[$$_[0][0]][$$_[0][1]] = uc $Board[$$_[0][0]][$$_[0][1]]; $Board[$$_[1][0]][$$_[1][1]] = uc $Board[$$_[1][0]][$$_[1][1]]; $Board[$$_[2][0]][$$_[2][1]] = uc $Board[$$_[2][0]][$$_[2][1]]; InvalRect $win->window->portRect; SetWTitle $win->window, "$Title - Won"; $Wins{uc $PWin}++; Init() if $AllComp; } } while ($win->window) { if (!$PWin and $IsComp{$Player}) { move(automove($Player)); } WaitNextEvent; } END { $win->dispose if $win; if (keys %Wins) { for (qw(X O Draws)) { print "$_: @{[$Wins{$_} || 0]}\n"; } } } __END__