[Date Prev][Date Next][Thread Prev][Thread Next] [Search] [Date Index] [Thread Index]

[MacPerl] Re: TicTacToe



Darryl Tang writes 17 June 1999:

>I'm working on a little programming exercise with my kids - tic tac toe -
>to learn more about MacPerl.  We've worked out the logic pretty well and
>are now moving on to developing a graphical interface of sorts.
>
>We want to put up a window with a tic tac toe grid and then let the human
>player click on the spot where he/she wants to move.  We've figured out how
>to draw the window and grid, by looking at the MacPerl Power and Ease Book
>and Inside Macintosh (very briefly).  We're stuck now because we can't
>figure out how to make the program respond to the human player's mouse
>click on the appropriate spot.

Panes.pm will be useful here. 'Panes' is a protocol which will do much of
the work for you in handling clicks specific to cells in the grid. 'Panes'
is an integral part of Windows.pm but the documentation on it is sparse. It
is also a protocol which is a good deal easier to use than it is to
explain, so it might be most helpful to put forward a more or less complete
script by way of an explanation.

To begin with, you would create a MacWindow like this:

    my $win = MacColorWindow->new($win_r, "TicTacToe", 1, 5, 1);
    WaitNextEvent while $win->window;
    END { $win->dispose if defined $win }

The END{} block allows the window to be closed by "StopScript": this window
has no 'goaway' box as it happens.

The next step is to create nine new panes (which in this case will just be
rectangles). The panes are really only hashes which will be registered as
'panes' in the window '$win'. It sounds complicated but is in fact quite
simple.

First a separate package (let's call it 'Grid') is created with a 'new'
constructor which would look something like this:

   package Grid;

    BEGIN {
        use Mac::QuickDraw;
        use Mac::Events;
        use Mac::Pane;
        use vars qw(@ISA);
        @ISA = qw(Mac::Pane);
    }

    sub new {
        my ($class, $r) = @_;

        my $me = bless {
           rect => $r,
           hit => ''
        }, $class;

        $win->add_pane($me);
        $me;
    }

The BEGIN block lists the modules the package will use: in this case we
need just QuickDraw, Events and Panes.

The constructor returns a hash with two elements, the rectangle 'rect'
which defines the cell in the grid, and a 'hit' into which an 'X' or a 'O'
will be put when the cell is clicked. The final stage is to 'register' the
pane in Windows by means of 'add_pane()'. All that does is to put the hash
belonging to the package 'Grid' in a list in Windows.pm.

Now we define a rectangle, say, $cell, to be the size of one of the boxes
in the grid, and shifted to its right position in the grid like so:

     $cell = Rect->new(0, 0, $side, $side);  # $side is the length of the side
     $pane_1 = Grid->new(OffsetRect($cell, 0, 0))
     $pane_2 = Grid->new(OffsetRect($cell, 0, $side));
     $pane_2 = Grid->new(OffsetRect($cell, 0, 2*$side));
     ...
and so on for all nine boxes or cells in the grid. It is a little more
elegant to do this in a loop, rather than write nine lines, and it is
convenient to refer to each pane as an element in an array '@c'. That is a
matter of taste and convenience, not fundamental in any way to the matter
in hand.

Now, to draw the grid, that is the black lines around the cells, all that
needs to be done is to write (in package Grid) a subroutine 'redraw()' like
this:

    sub redraw {
        my($me, $win) = @_;
        FrameRect($me->{rect}
    }

What happens is that on an update event Windows redraws each pane in the
window in turn, and finally redraws the main window itself. Hence, quite
automatically, each of the nine rectangles registered as 'panes' will be
'framed'. That gets that part of the job done.

Clicks are also handled by Windows in much the same way. Any click in 'our
window' is sent (or more specifically the 'Point' is sent) in turn to each
of the panes. Hence it is only necessary to find out which pane rectangle
the click occured in to know that was the cell the user clicked. In package
Grid, a subroutine like this:-

    sub click {
        my($me, $win, $pt) = @_;
        if(PtInRect($pt, $me->{rect})) {
            $j++;
            $me->{hit} = $j%2 ? 'X' : 'O';
        }
        $win->redraw;
    }

will do most of the work. If the click is in the rectangle represented by
the hash '$me', then an X or a O is written to $me->{hit}. (I have assumed
the players take turns and the program alternately writes 'X' and 'O'
according to whether the flag '$j' is odd or even.)

There is a little more to consider. If one of the players clicks in a cell
which has already been 'hit', then a '0' should be returned. This indicates
to Windows that the click is of no interest in the pane but maybe still
unfinished business, that is to say it may be needed in the main window. On
the other hand if nine hits have been recorded, that is the end of the game
and any further clicks must be totally ignored, so a 1 should be returned.
These two lines suffice:

        return 1 if $j > 9;
        return 0 if $me->{hit} ne '';

The 'click' subroutine thus gets an 'X' or a 'O' entered into the panes
hash, but now the question arises as to how to get it drawn in the window.
It is very easy: just add these lines to 'redraw()' in package 'Grid':

        MoveTo(X, Y);   # X, Y a suitable starting point in the cell
        DrawString($me->{hit})

Again the windows protocol will do all the work of drawing and updating all
nine cells appropriately. A few other lines, to select a font and a font
size, and maybe font colour complete the subroutine.

The last problem is to devise a routine to see who's won. That is most
easily done in the 'main' package with a hook to 'click'. A 'click' event
is received in the main window after visiting each of the panes, so that's
a good time to look at state of the nine 'hit' values. These can be
accessed in the _main_ package by $c[0]->{hit} etc. A routine examines the
state of lines of cells (for instance the diagonal 0, 4, 8) looking for
'XXX' or 'OOO'.

The complete script is shown below. It might be preferrable to transfer any
further discussion to the 'macperl-toolbox' mailing list. But I hope this
will be of some general interest anyway.

>We think that what we need is a subroutine that is hooked onto the window
>and called when a mouse event is detected.  The subroutine would put an "x"
>or "o" in the spot where the mouse was clicked.  We think it would look
>something like this:
>
>        $win->sethook('mouseUp' => \&handlemouseroutine);

Close, but not quite there: 'mouseUp' should be 'click'.

>Unfortunately, this didn't work.  Anyone have any ideas?  More generally,
>anyone know of a source of examples on using the Mac GUI interface in
>MacPerl?

If you want a really authoritative tutorial on Panes.pm get hold of
Matthias Neeracher's Calculator.pm which is still to be found I think on

    ftp://err.ethz.ch/pub/neeri/MacPerl/Beta/

HTH.

Alan Fry

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

#!perl -w

use strict;
use Mac::Windows;
use Mac::QuickDraw;
use Mac::Events;

my $side = 60;
my $cell = Rect->new(0, 0, $side, $side);
my $wind = Rect->new(0, 0, 3*$side, 3*$side);
my $j = 0;
my $k = 0;
my @c;

my $win_r = OffsetRect($wind, 20, 50);
my $win = MacColorWindow->new($win_r, "TicTacToe", 1, 5, 1);
$win->sethook('click', \&whos_won);

for my $m (0..2) {
    for my $n (0..2) {
        $c[$k++] = Grid->new(OffsetRect($cell, $m*$side, $n*$side))
    }
}

WaitNextEvent while $win->window;

END { $win->dispose if defined $win }

sub whos_won {
    examine(0, 1, 2);
    examine(3, 4, 5);
    examine(6, 7, 8);
    examine(0, 3, 6);
    examine(1, 4, 7);
    examine(2, 5, 8);
    examine(0, 4, 8);
    examine(2, 4, 6);
}

sub examine {
    my($a, $b, $c) = @_;
    my $str = '';
    for ($a, $b, $c) { $str .= $c[$_]->{hit} }
    if ($str eq 'XXX' or $str eq 'OOO') {
        for ($a, $b, $c) {InvertRect(InsetRect($c[$_]->{rect}, 4, 4))}
        $j = 10
    }
}

package Grid;

BEGIN {
    use Mac::QuickDraw;
    use Mac::Events;
    use Mac::Pane;
    use vars qw(@ISA);
    @ISA = qw(Mac::Pane);
}

sub new {
    my ($class, $r) = @_;

    my $me = bless {
       rect => $r,
        hit => ' '
    }, $class;

    $win->add_pane($me);
    $me;
}

sub redraw {
    my ($me, $win) = @_;
    my $r = InsetRect($me->{rect}, -1, -1);
    FrameRect($r);
    $win->window->txFont(1);
    $win->window->txSize(48);
    MoveTo($r->left+16, $r->bottom-12);
    ForeColor redColor;
    DrawString($me->{hit});
    ForeColor blackColor;
}

sub click {
    my($me, $win, $pt) = @_;
    return 1 if $j > 9;
    return 0 if $me->{hit} ne ' ';
    if(PtInRect($pt, $me->{rect})) {
        $j++;
        $me->{hit} = $j % 2 ? 'X' : 'O';
    }
    $win->redraw;
}

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=







===== Want to unsubscribe from this list?
===== Send mail with body "unsubscribe" to macperl-request@macperl.org