Bejeweled clone // (was: Reference array [solved])

Started by
4 comments, last by c4c0d3m0n 16 years, 9 months ago
struct field {
    bool u; // 'Used' boolean
    int j; // Type of jewel
};



// Game Functions
//

int anyMatches( field *check[][] ) // Returns the amount of matches found
{
    int returnvalue = 0;
    
    for( int i = 0; i < 6; i++ ) {
        for( int j = 0; j < 6; j++ ) {
            if( ( check[j].j == check[j+1].j ) && ( check[j].j == check[j+2].j ) ) {
                check[j].u = check[j+1].u = check[j+2].u = false;
                returnvalue++;
            }
            if( ( check[j].j == check[i+1][j].j ) && ( check[j].j == check[i+2][j].j ) ) {
                check[j].u = check[i+1][j].u = check[i+2][j].u = false;
                returnvalue++;
            }
        }
    }
}



I'm programming a simple clone of Bejeweled. I want to pass a double array reference of field's and then check it through with a huge amount of if()'s .In my main game loop, I already have a field[][], so what I want to do is pass it on to this function, and then continue with the modified field (without nasty return values). I believe that#s the purpose of references, but I don't know how to use them correctly. Can someone tell me what the correct syntax for the argument in anyMatches() is? Thanks [Edited by - c4c0d3m0n on June 24, 2007 12:44:15 PM]
Advertisement
You should remove the * because arrays are always passed by pointer. For example, when you write:

int anyMatches( field check[][] ){    check[2][5] = ...;}// Later in another fuctionfield f[10][10];anyMatches(f);


Then check is actually a pointer to the array passed to the function so you can modify it from inside the function.
Short answer: C++'s built-in arrays don't really work right; use boost::array instead.

Long answer: When you pass the array, it "decays" to a pointer to the beginning element. You'll want to read all of this. Things are actually more complicated in the case of a 2-dimensional array: when passed, it decays to a *single* level of indirection (field*, not field**), because the 2-dimensional array is actually a single chunk of memory (the compiler knows at compile time what the "row length" is, so it can "collapse" each pair of indices into a single index into the chunk).

If you don't mind hard-coding the array size that needs to be passed (and in your case, you seem to be fine with that - given that the array bounds are also hard-coded), you can just do that; but honestly, things will be much, much easier for you if you just wrap up the playing field in a larger struct (which you can then pass by reference normally) - and at that point, you might as well make use of boost::array anyway.

But while I'm looking at your code:

1) Passing a pointer IS NOT passing by reference. C++ has real references; use them.

2) Don't write single-letter variable names and then explain them in a comment. Use a variable name that actually says what is being stored. It really won't kill you to do a little more typing each time, and it saves you from constantly referring back to comments (in who knows whatever other file) when you try to read a line like "if (a.u && b.u) { a.j = (a.j + b.j) % NUM_J; }".

3) Similarly, *think* about your names when they are full. A 'field' normally means a whole playing field, not a single element of it. Those are normally called something like 'cells'. A 'check' is what the function performs, not a proper name for the thing it's performed upon.

4) You might want to actually return your return value.

5) Even if the field size is "hard-coded", it still deserves a constant.

struct cell {    bool used;    int type;};const int fieldsize = 8;typedef boost::array<boost::array<cell, fieldsize>, fieldsize> field;// Mark any matched jewels as unused. Return the number of matched sets.int findMatches(field& f) {    int result = 0;        for (int i = 0; i < fieldsize - 2; ++i) {        for (int j = 0; j < fieldsize - 2; ++j) {            if ((f[j].type == f[j+1].type) &&                 (f[j].type == f[j+2].type)) {                f[j].used = f[j+1].used = f[j+2].used = false;                result++;            }            if ((f[j].type == f[i+1][j].type) &&                 (f[j].type == f[i+2][j].type)) {                f[j].used = f[i+1][j].used = f[i+2][j].used = false;                result++;            }        }    }    return result;}


Although we can actually be a little clearer by using nicely named references, and being more clever about the loop bounds:

// Mark any matched jewels as unused. Return the number of matched sets.int findMatches(field& f) {    int result = 0;        for (int i = 1; i < fieldsize - 1; ++i) {        for (int j = 1; j < fieldsize - 1; ++j) {            cell& current = f[j];            cell& above = f[i-1][j];            cell& below = f[i+1][j];            cell& left = f[j-1];            cell& right = f[j+1];            if ((current.type == above.type) &&                 (current.type == below.type)) {                current.used = above.used = below.used = false;                result++;            }            if ((current.type == left.type) &&                 (current.type == right.type)) {                current.used = left.used = right.used = false;                result++;            }        }    }    return result;}

Quote:Original post by Zahlman
Short answer: C++'s built-in arrays don't really work right; use boost::array instead.

Long answer: When you pass the array, it "decays" to a pointer to the beginning element. You'll want to read all of this. Things are actually more complicated in the case of a 2-dimensional array: when passed, it decays to a *single* level of indirection (field*, not field**), because the 2-dimensional array is actually a single chunk of memory (the compiler knows at compile time what the "row length" is, so it can "collapse" each pair of indices into a single index into the chunk).

Hmm... I read some of that :) Looks very high level, not sure wether that's for me yet.. What's the difference between boost::array, a normal C++ array or a Vector?

Quote:If you don't mind hard-coding the array size that needs to be passed (and in your case, you seem to be fine with that - given that the array bounds are also hard-coded), you can just do that; but honestly, things will be much, much easier for you if you just wrap up the playing field in a larger struct (which you can then pass by reference normally) - and at that point, you might as well make use of boost::array anyway.

I rewrote my code so that the fieldsize is now defined as const int FIELD_SIZE = 8;

Quote:But while I'm looking at your code:

1) Passing a pointer IS NOT passing by reference. C++ has real references; use them.

What is the difference? How do I do the latter? I have no experience in references nor pointers, I was just experimenting.

Quote:2) Don't write single-letter variable names and then explain them in a comment. Use a variable name that actually says what is being stored. It really won't kill you to do a little more typing each time, and it saves you from constantly referring back to comments (in who knows whatever other file) when you try to read a line like "if (a.u && b.u) { a.j = (a.j + b.j) % NUM_J; }".

3) Similarly, *think* about your names when they are full. A 'field' normally means a whole playing field, not a single element of it. Those are normally called something like 'cells'. A 'check' is what the function performs, not a proper name for the thing it's performed upon.

4) You might want to actually return your return value.

Fixed, fixed and fixed :) Couldn't find a good name, I guess cell is good. I forgot to return the returnvalue, probably because I was still looking at the other compiler errors.

Quote:5) Even if the field size is "hard-coded", it still deserves a constant.

*** Source Snippet Removed ***

Although we can actually be a little clearer by using nicely named references, and being more clever about the loop bounds:

*** Source Snippet Removed ***

I gave it a constant as stated. I'll write some references though, it really does look cleaner :) I guess it's better to check 1..7 instead of 0..6 regarding the namegiving.

--------------------
My source codes:
// Bejeweled [main.cpp]// Rob Spoel//#include <cstdlib>#include "windows.h"#include "SDL/SDL.h" // Simple Directmedia Layer#include "SDL/SDL_image.h"#include "SDL/SDL_ttf.h"#include "SDL/SDL_mixer.h"#include <string>#include <sstream>#include <fstream>const int SCREEN_WIDTH = 480;const int SCREEN_HEIGHT = 480;const int SCREEN_BPP = 32;const int SQUARE_SIZE = 48; // The field exists of 8*8 SQUARE_SIZEd squares (px)const int FIELD_SIZE = 8;const int FRAMES_PER_SEC = 30;SDL_Surface *screen = NULL; // Screen surfaceSDL_Surface *bluej = NULL, *greenj = NULL, *orangej = NULL, *purplej = NULL, *redj = NULL, *silverj = NULL, *yellowj = NULL;SDL_Event event;#include "beclassed.h" // Classes#include "befunctioned.h" // Functionsint main( int argc, char* argv[] ){    if( !init() ) {        return -1;    }    if ( !loadFiles() ) {        return -1;    }    srand( time( 0 ) ); // Seed randomizer    bool quit = false; // Used to quit main loop    //Timer fps; // Used to regulate framerate    cell field[FIELD_SIZE][FIELD_SIZE]; // Playfield [y][x]    for( int i = 0; i < 8; i++ ) {        for( int j = 0; j < 8; j++ ) {            field[j].used = true;            field[j].jewel = rand() %7; // Random jewel (0..6)        }    }    while( !quit ) {        //fps.start(); // Start timer        SDL_FillRect( screen, &screen->clip_rect, SDL_MapRGB( screen->format, 0x63, 0x42, 0x18 ) ); // Fill the screen brown        for( int i = 0; i < 8; i++ ) {            for( int j = 0; j < 8; j++ ) {                SDL_Rect offset;                offset.x = ( SCREEN_WIDTH/2 - (FIELD_SIZE/2)*SQUARE_SIZE ) + j*SQUARE_SIZE; // Center                offset.y = ( SCREEN_HEIGHT/2 - (FIELD_SIZE/2)*SQUARE_SIZE ) + i*SQUARE_SIZE; // Center                offset.w = offset.h = SQUARE_SIZE;                if( (i+j)%2 == 1 ) { // Have each square get a different colour                    SDL_FillRect( screen, &offset, SDL_MapRGB( screen->format, 0xE7, 0xCE, 0xA5 ) ); // Dark                }                else {                    SDL_FillRect( screen, &offset, SDL_MapRGB( screen->format, 0x9C, 0x84, 0x5A ) ); // Light                }                switch( field[j].jewel * field[j].used ) {                    case 0 :                        if( field[j].used ) {                            applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, bluej, screen );                        }                        break;                    case 1 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, greenj, screen );                        break;                    case 2 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, orangej, screen );                        break;                    case 3 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, purplej, screen );                        break;                    case 4 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, redj, screen );                        break;                    case 5 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, silverj, screen );                        break;                    case 6 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, yellowj, screen );                        break;                }            }        }        while( iMatches( field ) != 0 ) {}        while( SDL_PollEvent( &event ) ) { // While there are events to handle            if( event.type == SDL_QUIT ) { // If user Xed out of the window, switch quit flag				quit = true;			}        }        SDL_Flip( screen ); // Update screen        /*while( fps.get_ticks() < 1000/FRAMES_PER_SEC ) { // Resource-saver            Sleep( 1 );        }*/        Sleep( 1 );    } // End of main loop    return 0;} // End of main()
// Bejeweled [beclassed.h]// Classes for Bejeweled//enum jewel { BLUE, GREEN, ORANGE, PURPLE, RED, SILVER, YELLOW };struct cell {    bool used; // 'Used' boolean    int jewel; // Type of jewel};typedef boost::array<boost::array<cell, FIELD_SIZE>, FIELD_SIZE> field;
// Bejeweled [befunctioned.h]// Functions for Bejeweled//bool init(){	if( SDL_Init( SDL_INIT_EVERYTHING ) == -1 ) { // Initialize all SDL subsystems		return false;	}	    //SDL_WM_SetIcon( SDL_LoadBMP( "data\\icon.bmp" ), NULL );    screen = SDL_SetVideoMode( SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP, SDL_SWSURFACE ); // Set up the screen    if( screen == NULL ) {        return false;    }    if( TTF_Init() == -1 ) { // Initialize SDL_ttf        return false;    }    if( Mix_OpenAudio( 22050, MIX_DEFAULT_FORMAT, 2, 4096 ) == -1 ) { // Initialize SDL_mixer        return false;    }    SDL_WM_SetCaption( "Bejeweled v0.0", NULL ); // Set the window caption    return true;}SDL_Surface *loadImage( std::string filename ) // Function to load+optimize images{	SDL_Surface* loaded_image = NULL; // The image that's loaded	SDL_Surface* optimized_image = NULL; // The optimized image that will be used	loaded_image = IMG_Load( filename.c_str() ); // Load the image using SDL_image	if( loaded_image != NULL ) // If the image loaded	{		optimized_image = SDL_DisplayFormat( loaded_image ); // Create an optimized image		SDL_FreeSurface( loaded_image ); // Free the old image		if( optimized_image != NULL ) // If the image was optimized just fine		{			Uint32 colorkey = SDL_MapRGB( optimized_image->format, 0, 0xFF, 0xFF ); // Map the color key #00FFFF			SDL_SetColorKey( optimized_image, SDL_RLEACCEL | SDL_SRCCOLORKEY, colorkey ); // Set all pixels of color R 0, G 0xFF, B 0xFF to be transparent		}	}	return optimized_image;}void applySurface( int x, int y, SDL_Surface* source, SDL_Surface* destination ){	SDL_Rect offset; // Make a temporary rectangle to hold the offsets	offset.x = x; // Give the offsets to the rectangle	offset.y = y;	SDL_BlitSurface( source, NULL, destination, &offset ); // Blit the surface}void applySurface( int x, int y, int w, int h, SDL_Surface* source, SDL_Surface* destination ){	SDL_Rect offset; // Make a temporary rectangle to hold the offsets	offset.x = x; // Give the offsets to the rectangle	offset.y = y;	offset.w = w;	offset.h = h;	SDL_BlitSurface( source, NULL, destination, &offset ); // Blit the surface}bool loadFiles(){    bluej = loadImage( "data\\blue.bmp" );    greenj = loadImage( "data\\green.bmp" );    orangej = loadImage( "data\\orange.bmp" );    purplej = loadImage( "data\\purple.bmp" );    redj = loadImage( "data\\red.bmp" );    silverj = loadImage( "data\\silver.bmp" );    yellowj = loadImage( "data\\yellow.bmp" );}// Game Functions//int iMatches( cell& f ) // Returns the amount of matches found and marks matched jewels as !used{    int result = 0;        for( int i = 1; i < FIELD_SIZE-1; i++ ) {        for( int j = 1; j < FIELD_SIZE-1; j++ ) {            cell& current = f[j];            cell& above = f[i-1][j];            cell& below = f[i+1][j];            cell& left = f[j-1];            cell& right = f[j+1];                        if( ( current.jewel == above.jewel ) &&                ( current.jewel == below.jewel ) ) {                current.used = above.used = below.used = false;                result++;            }            if( ( current.jewel == left.jewel ) &&                ( current.jewel == right.jewel ) ) {                current.used = left.used = right.used = false;                result++;            }        }    }        return result;}
Don't mind the unused includes, I have them there since I am planning on using them in a later stage. The fps capping is commented out, since I was unable to use Timer.

When I compile, I get an error that boost hasn't been declared. All the following errors have to do with the iMatches() function.
Quote:Original post by c4c0d3m0n
What's the difference between boost::array, a normal C++ array or a Vector?


Normal C++ arrays are, well, the horrible beasts you have been reading about.

boost::array is a class that provides the same basic interface as an array (i.e. you can get and set elements by using [index]es), but the convenience of working with an actual object (because you actually have one). That is, you can treat the type name (boost::array<elementtype, count>) the same way you treat 'int' or 'double', so specifying a reference to one is much more straightforward (it looks the same as specifying a reference to an int or double), and you never have to worry about weird things like pointer decay.

It's actually very easy to get the same effect by writing it yourself: basically, all they do is make a class which has an array of the appropriate type and size as a data member (this is where templating comes in - so that the one template can be instantiated for any kind of array you may need), and give it overloads for 'operator[]' which delegate to the array.

std::vector, although it's in the standard library rather than the third-party Boost libraries, provides a rather more complex behaviour. It's designed to behave like a *resizable* array. It does this by keeping a dynamic array-allocation on the heap somewhere, and remembering the size of its allocation (the 'capacity'), as well as the amount of that allocation that is actually in use (the 'size'). You can explicitly resize to a larger amount (causing the remaining slots to be filled in with default-constructed values) or implicitly by appending things to the end (with '.push_back()'), and if the size is about to exceed the capacity, the vector will automatically allocate a bigger chunk, copy all the elements over, put in the requested new elements, and then clean up the old chunk. It also cleans up its current memory chunk in the destructor, and does similar things in the copy constructor and assignment operator. The net effect is that you can use the vector as if the array elements were all just on the stack (rather than being in a dynamic allocation); the vector does all the "memory management" for you.

As you can imagine, implementing this one properly is considerably more involved. There's also no reason to, since the standard library already provides it.

Quote:
Quote:Passing a pointer IS NOT passing by reference. C++ has real references; use them.

What is the difference? How do I do the latter? I have no experience in references nor pointers, I was just experimenting.


References.

Out of curiousity, though, if you *don't* know any of this, then where did you find the term "reference" at all?

Quote:When I compile, I get an error that boost hasn't been declared.


Well, of course. The Boost libraries are third-party; they don't come with your compiler - you need to download some headers, and in some cases (not for boost::array though), do some installation work (i.e. compile some implementation files).

(Although, as mentioned before, it's really not that hard to do yourself:

template <typename T, size_t size>class array {  T storage[size];  public:  const T& operator[](size_t pos) const { return storage[pos]; }  T& operator[](size_t pos) { return storage[pos]; }};


If any of that doesn't make sense, see here.

And IIRC, boost::array *does* do a few other things besides that, but they're probably not things you'll desperately need.)

Quote:All the following errors have to do with the iMatches() function.


Don't pass a single cell to it; pass a field.



Anyway, just wanted to comment on this monstrousity:

                switch( field[j].jewel * field[j].used ) {                    case 0 :                        if( field[j].used ) {                            applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, bluej, screen );                        }                        break;                    case 1 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, greenj, screen );                        break;                    case 2 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, orangej, screen );                        break;                    case 3 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, purplej, screen );                        break;                    case 4 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, redj, screen );                        break;                    case 5 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, silverj, screen );                        break;                    case 6 :                        applySurface( offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, yellowj, screen );                        break;                }


We're using tricky arithmetic (multiplying by a boolean? o_O) that actually doesn't help us out (because we *still* have to handle a special case if the result is 0), and duplicating code (the only thing that changes is which jewel to use).

The natural way of letting a number specify which jewel image to use would be to put the jewel images into an array, and let the number index into the array:

// And again, a reference can help us...cell& c = field[j];if (c.used) {  applySurface(offset.x, offset.y, SQUARE_SIZE, SQUARE_SIZE, jewels[c.jewel], screen);}
I came accross the term reference while creating my first ever classes, Card and Deck. I had some trouble building the deck and its functions, but passing a Card reference from the Deck array was the solution to everything. I thought I understood the principle, but I am proven wrong :)

I decided to use the hand built template, since I'm not in the mood for installing more stuff to my computer, it's getting a total fresh system soon anyway.

Everything seems to work great now, I'll continue developping my game :)

This topic is closed to new replies.

Advertisement