Skip to content

Commit

Permalink
Added the puzz chapter, edited it a bit.
Browse files Browse the repository at this point in the history
  • Loading branch information
kthakore committed Nov 7, 2010
1 parent 806c261 commit 26df2f5
Show file tree
Hide file tree
Showing 7 changed files with 348 additions and 5 deletions.
11 changes: 6 additions & 5 deletions Makefile
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ else
src/04-game.pod \ src/04-game.pod \
src/05-pong.pod \ src/05-pong.pod \
src/06-tetris.pod \ src/06-tetris.pod \
src/07-music_and_sound.pod \ src/07-puzz.pod \
src/08-CPAN.pod \ src/08-music_and_sound.pod \
src/09-profiling.pod \ src/09-CPAN.pod \
src/10-XS_effects.pod \ src/10-profiling.pod \
src/11-PDL_OpenGL.pod src/11-XS_effects.pod \
src/12-PDL_OpenGL.pod


endif endif


Expand Down
342 changes: 342 additions & 0 deletions src/07-puzz.pod
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,342 @@

=head0 Puzz! A puzzle game

=head1 Abstract

We are now ready to write another complete game. Instead of listing the code and then explaining it, I will go through the process of how I might write it.

Puzz is a simple rearrangment puzzle. A random image from the folder Puzz is in is chosen and broken into a 4x4 grid. The top left corner piece is then taken away, and every other piece is then moved to a random position, scrambling the image up. The goal is then to move pieces which are in the 4 squares adjacent to the empty square on to the empty square, and eventually restore the image.


=head1 The Window

So, first thing we do is create the window. I've decided I want each piece to be 100x100, so the window needs to be 400x400.

use strict;
use warnings;

use SDL;
use SDLx::App;

my $App = SDLx::App->new(w => 400, h => 400, t => 'Puzz');

Next thing we usually do is figure out what global vars we will be needing. As with $App, I like to name my globals with title case, so they are easily distinguishable from lexical vars. The globals we need are the grid (the positions of the pieces), the images we have to use, the current image, and a construct that will give us piece movement, along with an animation.

my @Grid;
my @Img;
my $CurrentImg;
my %Move;

For now, lets fill in @Grid with what it's going to look like:

@Grid = (
[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11],
[12, 13, 14, 15],
);

C<0> will be our blank piece, but we could have chosen it to be any other number. When the grid looks like this, it's solved, so eventually we will need a way to scramble it. It's good enough for now, though.

=head1 Loading the images

To load the images, we would normally use C<SDLx::Surface>, but we're going to do it the libsdl way with C<SDL::Image> because we need to do our own error handling.


use SDL::Image;
use SDL::GFX::Rotozoom 'SMOOTHING_ON';

while(<./*>) {
if(-f and my $i = SDL::Image::load($_)) {
$i = SDL::GFX::Rotozoom::surface_xy($i, 0, 400 / $i->w, 400 / $i->h, SMOOTHING_ON);
push @Img, $i;
}
}
$CurrentImg = $Img[rand @Img];

We just go through every file in the current directory, and try to load it as an image. C<SDL::Image::load> will return false if there was an error, so we want to discard it when that happens. If we used C<SDLx::Surface> to load the images, we would get a warning every time a file fails to load as an image, which we don't want. The C<my $i = SDL::Image::load($_)> is just an idiom for setting a var and checking it for truth at the same time.

We want the image to be 400x400, and C<SDL::GFX::Rotozoom> makes this possible. The two Rotozoom functions that are the most useful are C<surface> and C<surface_xy>. They work like this:

$zoomed_src = SDL::GFX::Rotozoom::surface($src, $angle, $zoom, $smoothing)
$zoomed_src = SDL::GFX::Rotozoom::surface_xy($src, $angle, $x_zoom, $y_zoom, $smoothing)

The zoom values are the multiplier for that component, or for both components at once as with C<$zoom>. C<$angle> is an angle of rotation in degrees. C<$smoothing> should be C<SMOOTHING_ON> or C<SMOOTHING_OFF> (which can be exported by C<SDL::GFX::Rotozoom>) or just 1 or 0.

Once the image is zoomed, it is added to the image array. The current image is then set to a random value of the array.

=head1 Handling Events

The next part I like to write is the events. We're going to make Escape quit, and left click will move the pieces around. We C<use SDL::Events> for the constants.

use SDL::Events;

sub on_event {
my ($e) = @_;
if($e->type == SDL_QUIT or $e->type == SDL_KEYDOWN and $e->key_sym == SDLK_ESCAPE) {
$App->stop;
}
elsif($e->type == SDL_MOUSEBUTTONDOWN and $e->button_button == SDL_BUTTON_LEFT) {
...
}
}

$App->add_event_handler(\&on_event);
# $App->add_move_handler(\&on_move);
# $App->add_show_handler(\&on_show);
$App->run;

=head1 Filling the Grid

Once we have something like this, it's a good time to put some C<warn> messages in to make sure the inputs are working correctly. Once they are, it's time to fill it in.

my $x = int($e->button_x / 100);
my $y = int($e->button_y / 100);
if(!%Move and $Grid[$y][$x]) {`
...
}

From the pixel coordinates of the click (0 to 399), we want to find out the grid coordinates (0 to 3), so we divide both components by 100 and round them down. Then, we only want to continue on to see if that piece can move if no other piece is moving (C<%Move> is false), and the piece clicked isn't the blank piece (0).

for([-1, 0], [0, -1], [1, 0], [0, 1]) {
my $nx = $x + $_->[0];
my $ny = $y + $_->[1];
if($nx >= 0 and $nx < 4 and $ny >= 0 and $ny < 4 and !$Grid[$ny][$nx]) {
...
}
}

=head1 Moving the Pieces

We check that the blank piece is in the 4 surrounding places by constructing 4 vectors. These will take us to those squares. The C<x> component is first and the second is C<y>. We iterate through them, setting C<$nx> and C<$ny> to the new position. Then if both C<$nx> and C<$ny> are within the grid (0 to 3), and that position in the grid is 0, we can move the piece to the blank square.

%Move = (
x => $x,
y => $y,
x_dir => $_->[0],
y_dir => $_->[1],
offset => 0,
);

To make a piece move, we construct the move hash with all the information it needs to move the piece. The C<x> and C<y> positions of the piece, the C<x> and C<y> directions it will be moving (the vector), and it's current pixel offset from it's position (for the moving animation), which starts at 0.

=head2 The Move Handler Callback

Next we will write the move handler. All it needs to do is move any moving piece along by updating the offset, and click it in to where it's being moved to when it has moved the whole way (offset is 100 or more).

sub on_move {
if(%Move) {
$Move{offset} += 30 * $_[0];
if($Move{offset} >= 100) {
$Grid[$Move{y} + $Move{y_dir}][$Move{x} + $Move{x_dir}] = $Grid[$Move{y}][$Move{x}];
$Grid[$Move{y}][$Move{x}] = 0;
undef %Move;
}
}
}

30 has been arbitrarily chosen as the speed of the move, as it felt the best after a little playing and tweaking. Always remember to multiply things like this by the step value in C<$_[0]> so that the animation moves in correct time with the updating.

Once the offset is 100 or more, the grid place that the piece is moving to is set to the value of the piece, and the piece is set to the blank value. The move is then finished, so C<%Move> is deleted.

=head1 Rendering the Game

Now that we have all the functionality we need it's finally time to see the game.

sub on_show {
$App->draw_rect;
for my $y (0..3) {
for my $x (0..3) {
...
}
}
$App->flip;
}

We start the show handler by drawing a black rect over the entire app. Entire surface and black are the defaults of C<draw_rect>, so letting it use the defaults is good. Next we iterate through a C<y> and C<x> of 0 to 3 so that we can go through each piece of the grid. At the end of the handler we update the app with a call to C<flip>.

next unless my $val = $Grid[$y][$x];
my $xval = $val % 4;
my $yval = int($val / 4);
my $move = %Move && $Move{x} == $x && $Move{y} == $y;
...

Inside the two loops we put this. First we set C<$val> to the grid value at the current position, and we skip to the next piece if it's the blank piece. We have the C<x> and C<y> coordinates of where that piece is on the board, but we need to figure out where it is on the image. If you refer back to the initialisation of the grid, the two operations to find the values should make sense. C<$move> is set with a bool of whether it is this piece that is moving, if there is a piece moving at all.

$App->blit_by(
$CurrentImg,
[$xval * 100, $yval * 100, 100, 100],
[$x * 100 + ($move ? $Move{offset} * $Move{x_dir} : 0),
$y * 100 + ($move ? $Move{offset} * $Move{y_dir} : 0)]
);

Now that we have all of this, we can blit the portion of the current image we need to the app. We use C<blit_by> because the image we're blitting isn't an SDLx::Surface (because we didn't load it as one), but the app is. Here's how C<blit_by> works as opposed to C<blit>:

$src->blit($dest, $src_rect, $dest_rect)
$dest->blit_by($src, $src_rect, $dest_rect)

The portion we need is from the C<$xval> and C<$yval>, and where it needs to go to is from C<$x> and C<$y>. All are multiplied by 100 because we're dealing with 0 to 300, not 0 to 3. If the piece is moving, the offset multiplied by the diretion is added to the position.

When the code is run with all 3 handlers, we have a fully working game. The pieces move around nicely when clicked. The only things it still needs are a shuffled grid and a way to check if the player has won. To imlement these two things, we will make two more functions.

use List::Util 'shuffle';

sub new_grid {
my @new = shuffle(0..15);
@Grid = map { [@new[ $_*4..$_*4+3 ]] } 0..3;
$CurrentImg = $Img[rand @Img];
}

We will replace the grid initialising we did with this sub. First it shffles the numbers 0 through 15 with C<List::Util::shuffle>. This array is then arranged into a 2D grid with a C<map> and put in to @Grid. Setting the current image is also put into this sub.

sub won {
my $correct = 0;
for(@Grid) {
for(@$_) {
return 0 if $correct != $_;
$correct++;
}
}
return 1;
}

This sub returns whether the grid is in the winning configuration, that is, all piece values are in order from 0 to 15.

Now we put a call to C<new_grid> to replace the grid initialisation we had before. We put C<won> into the event handler to make click call C<new_grid> if you have won. Finally, C<won> is put into the show handler to show the blank piece if you have won.

=head1 Complete Code

Here is the finished code:

=for programlisting

use strict;
use warnings;

use SDL;
use SDLx::App;
use SDL::Events;
use SDL::Image;
use SDL::GFX::Rotozoom 'SMOOTHING_ON';
use List::Util 'shuffle';

my $App = SDLx::App->new(w => 400, h => 400, t => 'Puzz');

my @Grid;
my @Img;
my $CurrentImg;
my %Move;

while(<./*>) {
if(-f and my $i = SDL::Image::load($_)) {
$i = SDL::GFX::Rotozoom::surface_xy($i, 0, 400 / $i->w, 400 / $i->h, SMOOTHING_ON);
push @Img, $i;
}
}

new_grid();

sub on_event {
my ($e) = @_;
if($e->type == SDL_QUIT or $e->type == SDL_KEYDOWN and $e->key_sym == SDLK_ESCAPE) {
$App->stop;
}
elsif($e->type == SDL_MOUSEBUTTONDOWN and $e->button_button == SDL_BUTTON_LEFT) {
my($x, $y) = map { int($_ / 100) } $e->button_x, $e->button_y;
if(won()) {
new_grid();
}
elsif(!%Move and $Grid[$y][$x]) {
for([-1, 0], [0, -1], [1, 0], [0, 1]) {
my($nx, $ny) = ($x + $_->[0], $y + $_->[1]);
if($nx >= 0 and $nx < 4 and $ny >= 0 and $ny < 4 and !$Grid[$ny][$nx]) {
%Move = (
x => $x,
y => $y,
x_dir => $_->[0],
y_dir => $_->[1],
offset => 0,
);
}
}
}
}
}

sub on_move {
if(%Move) {
$Move{offset} += 30 * $_[0];
if($Move{offset} >= 100) {
$Grid[$Move{y} + $Move{y_dir}][$Move{x} + $Move{x_dir}] = $Grid[$Move{y}][$Move{x}];
$Grid[$Move{y}][$Move{x}] = 0;
undef %Move;
}
}
}

sub on_show {
$App->draw_rect;
for my $y (0..3) {
for my $x (0..3) {
next if not my $val = $Grid[$y][$x] and !won();
my $xval = $val % 4;
my $yval = int($val / 4);
my $move = %Move && $Move{x} == $x && $Move{y} == $y;
$App->blit_by(
$CurrentImg,
[$xval * 100, $yval * 100, 100, 100],
[$x * 100 + ($move ? $Move{offset} * $Move{x_dir} : 0),
$y * 100 + ($move ? $Move{offset} * $Move{y_dir} : 0)]
);
}
}
$App->flip;
}

sub new_grid {
my @new = shuffle(0..15);
@Grid = map { [@new[ $_*4..$_*4+3 ]] } 0..3;
$CurrentImg = $Img[rand @Img];
}

sub won {
my $correct = 0;
for(@Grid) {
for(@$_) {
return 0 if $correct != $_;
$correct++;
}
}
return 1;
}

$App->add_event_handler(\&on_event);
$App->add_move_handler(\&on_move);
$App->add_show_handler(\&on_show);
$App->run;

=end programlisting

You now hopefully know more of the process that goes in to creating a simple game. The process of creating a complex game is similar, it just requires more careful planning. You should have also picked up a few other tricks, like with C<SDL::GFX::Rotozoom>, C<SDL::Image::load> and C<blit_by>.

=head1 Activities

=over

=item 1

Make the blank piece the bottom right piece instead of the top left piece.

=item 2

Make the grid dimensions variable by getting the value from C<$ARGV[0]>. The grid will then be 5x5 if C<$ARGV[0]> is 5 and so on.

=back


=head1 Author

This chapter's content graciously provided by Blaizer.

=for vim: spell

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 comments on commit 26df2f5

Please sign in to comment.