Sign in to follow this  
azure kitsune

Tetris implementation, need help

Recommended Posts

azure kitsune    122
Hi everyone, I'm pretty new to game programming (and programming in general). A few days ago, I decided to sit down and write a Tetris program (in C++). Right now, it works, but there are certain parts of the code that I am not satisfied with. Here's what I have. - A Block class. It represents a certain square on the tetris grid. It has an x-coordinate and y-coordinate. - A Tetromino class. An array of 4 blocks. - A TetrisGrid class. Contains a 2D array of the grid (1 if a space is filled and 0 if not), and also the current Tetromino falling in the grid. For the different types of Tetrominos, I created subclasses (IBlock, LBlock, etc.) so that they correctly set the coordinates of the 4 blocks. I realized that this was kind of useless because the piece in TetrisGrid was declared to be Tetromino, so the methods in the subclass wouldn't work (for example, I wanted to override the rotate() method for the O block). What should I do here? Should I just condense all the different subclasses into the Tetromino class and use a lot of 'if' statements? Also, my current way of detecting collision is this: Move the tetromino where it wants, check to see if it is in a valid position, if it is then move back. I think this is rather inefficient but I can't come up with anything better. Can anyone help? Thanks! :)

Share this post


Link to post
Share on other sites
Jouei    102
Hi well i will try and answer these questions for you i will first start off by saying the collision detection is actualy probaly the best because alot of other methods would envolve heaver math and be less efficent.

As for the subclass i am not completly sure what it is you are going for but if you want to overide a function with slicing (polymophism) then you have to make the function yo uwant virtual that way if the subclass you slice into will overide it and if there is none the origanal will remain the same. If you have not already and i sort of assumed you have check out polymorphism of C++ class and advanced inhertiance.

Hope this helps if not try and specifiy what it si you are exactly trying to do.
It may seem like the question is oveus but we have not seen the code.


Regards Jouei.

Behold the allmighty power of the if statment may you treble in its presence...

Share this post


Link to post
Share on other sites
azure kitsune    122
Thanks for your reply Jouei. Okay, I'll keep my current collision detection algorithm.

Here's the other part I was talking about


class Block {
private:
int x;
int y;
public:
int get_x() {
return x;
}
int get_y() {
return y;
}
void set_x(int value) {
x = value;
}
void set_y(int value) {
y = value;
}
void set(int value_x, int value_y) {
x = value_x; y = value_y;
}
void translate(int delta_x, int delta_y) {
x += delta_x; y += delta_y;
}
void descend() {
y++;
}
void move_left() {
x--;
}
void move_right() {
x++;
}

};

class Tetromino {
protected:
Block block[4];
public:
void set_center(int, int);

void descend() {
for (int i = 0; i < 4; i++) {
block[i].translate(0,1);
}
}

void ascend() {
for (int i = 0; i < 4; i++) {
block[i].translate(0,-1);
}
}

void move_side(int dir) {
if (dir == -1) {
move_left();
} else if (dir == 1) {
move_right();
}
}

void move_left() {
for (int i = 0; i < 4; i++) {
block[i].translate(-1,0);
}
}
void move_right() {
for (int i = 0; i < 4; i++) {
block[i].translate(1,0);
}
}

int get_block_x(int i) {
return block[i].get_x();
}

int get_block_y(int i) {
return block[i].get_y();
}

void rotate_cw() {
int axis_x = block[0].get_x();
int axis_y = block[0].get_y();

for (int i = 1; i <= 3; i++) {
int old_x = block[i].get_x();
int old_y = block[i].get_y();
block[i].set(old_y - axis_y + axis_x, axis_x - old_x + axis_y);
}
}

void rotate_ccw() {
int axis_x = block[0].get_x();
int axis_y = block[0].get_y();

for (int i = 1; i <= 3; i++) {
int old_x = block[i].get_x();
int old_y = block[i].get_y();
block[i].set(axis_y - old_y + axis_x, old_x - axis_x + axis_y);
}
}
};

class OPiece : public Tetromino {
public:
OPiece(){}
OPiece(int x, int y) {
set_center(x,y);
}
void set_center(int x, int y) {
block[0].set(x,y);
block[1].set(x+1,y);
block[2].set(x,y+1);
block[3].set(x+1,y+1);
}
};

// there's more in the middle, but I took them out.
class ZPiece : public Tetromino {
public:
ZPiece(){}
ZPiece(int x, int y) {
set_center(x,y);
}
void set_center(int x, int y) {
block[0].set(x,y);
block[1].set(x-1,y);
block[2].set(x+1,y+1);
block[3].set(x,y+1);
}
};

class TetrisGrid {
public:
TetrisGrid() {
for (int i = 0; i < WIDTH; i++) {
for (int j = 0; j < HEIGHT; j++) {
grid[i][j] = 0;
}
}
}

// some functions

void new_piece() {
switch(rand()%7+1){
case 1:
currentPiece = new IPiece(WIDTH/2,0);
break;
case 2:
currentPiece = new LPiece(WIDTH/2,0);
break;
case 3:
currentPiece = new OPiece(WIDTH/2,0);
break;
case 4:
currentPiece = new SPiece(WIDTH/2,0);
break;
case 5:
currentPiece = new TPiece(WIDTH/2,0);
break;
case 6:
currentPiece = new JPiece(WIDTH/2,0);
break;
case 7:
currentPiece = new ZPiece(WIDTH/2,0);
break;
}
}

// some more functions

static const int HEIGHT = 20;
static const int WIDTH = 10;

private:
int grid[WIDTH][HEIGHT];
Tetromino *currentPiece;

};



My problem is when I call currentPiece->someMethod(), the program goes to the Tetromino class to find the method, not the subclass.

Jouei, I am not familiar with the concept you are talking about. Would it apply to my program now that I put up the code?

Share this post


Link to post
Share on other sites
VanillaSnake21    175
Quote:
Original post by azure kitsune


My problem is when I call currentPiece->someMethod(), the program goes to the Tetromino class to find the method, not the subclass.

Jouei, I am not familiar with the concept you are talking about. Would it apply to my program now that I put up the code?


In the Tetromino class declare the function you want to call in the subclass as virtual. Something like this



class Tetromino
{
protected:

virtual int your_function() { //code here }
};


then in you derived class (subclass) overwrite the function like usual


class AnySubclass : public Tetromino
{

int your_function() { //code herre }
};


Now the correct function should be called. Hope this was of any help [smile], if you want to look more into virtual functions check out this link Clicky

Share this post


Link to post
Share on other sites
Lothia    102
I would definitely suggest not putting a huge set of if statements in a single class but simply keep your current route, of having each piece shape subclass a main piece class that can have a certain rotate method. There is a certain way you can rotate each piece through the same algorithm by assigning the middle of a piece to instead being a corner be the actual middle of the piece, that means you will have negative values. Doing this results in no real changes being required for the rotate method.
For your testing if you can move it the best route IMO is to clone the piece and then TEST to see if it can be moved, if not then don't do the actual movement, however if there is no problem then do that to the actual piece. This gets rid of the actual moving it and then moving it back which if you use a GUI might look sketchy.

Share this post


Link to post
Share on other sites
DevFred    840
Quote:
Original post by azure kitsune
For the different types of Tetrominos, I created subclasses (IBlock, LBlock, etc.)

Overkill. Different shapes don't really behave differently, they just have different data. You wouldn't subclass a string class to make a "hello" and a "world" string, would you?

Share this post


Link to post
Share on other sites
Alastair Gould    100
Quote:
Original post by DevFred
Quote:
Original post by azure kitsune
For the different types of Tetrominos, I created subclasses (IBlock, LBlock, etc.)

Overkill. Different shapes don't really behave differently, they just have different data. You wouldn't subclass a string class to make a "hello" and a "world" string, would you?


Maybe composition/aggrogation. Have a block class which can contain different shape classes.

it keeps the behavier the same, but the shape is different.

Conforms to the OCP principle, and code reuse.

Share this post


Link to post
Share on other sites
azure kitsune    122
@Jouei && @VanillaSnake21: Thank you guys for your help. I didn't know about virtual functions before. (I'm taking AP Computer Science right now and it's ruining all of my C++!)

Quote:
Original post by Lothia
For your testing if you can move it the best route IMO is to clone the piece and then TEST to see if it can be moved, if not then don't do the actual movement, however if there is no problem then do that to the actual piece. This gets rid of the actual moving it and then moving it back which if you use a GUI might look sketchy.


Thanks for the advice. I tried that and here is what I have in my TetrisGrid class:


void rotate_piece(int dir) {
Tetromino *copy;
copy = new Tetromino(currentPiece);
copy->rotate(dir);
if (is_valid_position(copy)) {
delete currentPiece;
currentPiece = copy;
}
}



Is this right?

Quote:
Original post by DevFred
Overkill. Different shapes don't really behave differently, they just have different data. You wouldn't subclass a string class to make a "hello" and a "world" string, would you?


I thought about it and I see your point. The only reason it worked out for me is because there are only 7 different tetrominoes. (If Tetris used hexominoes, I would have 60 subclasses!)

Quote:
Original post by Alastair Gould
Maybe composition/aggrogation. Have a block class which can contain different shape classes.

it keeps the behavier the same, but the shape is different.

Conforms to the OCP principle, and code reuse.


I'm sorry. I don't really understand. Do you mean that I should create a Shape class for the 7 different shapes?

Share this post


Link to post
Share on other sites
Drigovas    509
Quote:
Original post by Alastair Gould
Maybe composition/aggrogation. Have a block class which can contain different shape classes.

it keeps the behavier the same, but the shape is different.

Conforms to the OCP principle, and code reuse.
I am of course not DevFred, so I can't with certainty speak on this other person's behalf, but I am reasonably certain the intention was to shift from subclassing to something more data driven. By making a series of shape classes, you are ending up with the exact same problem that you started with. Instead of having a bunch of shape classes, you would just pass a description of the shape in to the block's constructor [or by some other means]. Just as a primitive example, say the block classes constructor takes a 4x4 matrix of colors, where a null color marks a part that doesn't have a tetris piece. You have a known pivot within the matrix, so you aren't hard-coding a center. You can also easily make a bunch of different tetris pieces without all the headache and maintainability issues of subclassing all over the place. These blocks also become something you can arbitrarily load from a file.

It doesn't really stick out as bad for something this small scale, but this sort of hard-wired class-based distinction between things that are differentiated only by the data they hold results in a huge problem later, as projects get more complicated.

Share this post


Link to post
Share on other sites
DevFred    840
Quote:
Original post by Drigovas
I am of course not DevFred, so I can't with certainty speak on this other person's behalf, but I am reasonably certain the intention was to shift from subclassing to something more data driven.

Spot on!

Share this post


Link to post
Share on other sites
Lothia    102
I apologize I cannot help you with the clone as I program in Java not C++, however when I cloned I had to not only close the piece class but also the points (your squares) with in it, and my piviot point.
I agree to a certain degree with the others about not having a subclass for each piece as when it becomes more complicated it can cause problems. If you can find that way to rotate around a central point then you should be fine (think of a 3 x 3 matrix, the pivot point would always be at 2,2.)
Your current code should just have the sub classes that call the main constructor and set some pivot point, nothing else should be different, at this point you can kill the subclasses and just call the main constructor, put that in a method that will be called run but not your main method because generally you want to control what a person can call when running a program, and calling each piece should be unknown to others.

Share this post


Link to post
Share on other sites
azure kitsune    122
Quote:
Original post by Lothia
I apologize I cannot help you with the clone as I program in Java not C++.


It's okay. I figured out the cloning part now. Thanks for your help. :)

Okay, so I made a few modifications to my code.

New class:


class TetrominoShape {
public:
static const int I_BLOCK[2][4];
static const int L_BLOCK[2][4];
static const int O_BLOCK[2][4];
static const int S_BLOCK[2][4];
static const int T_BLOCK[2][4];
static const int J_BLOCK[2][4];
static const int Z_BLOCK[2][4];
};

const int TetrominoShape::O_BLOCK[2][4] =
{ {0,2,1,0},
{0,1,1,0} };

// more here, but removed

const int TetrominoShape::Z_BLOCK[2][4] =
{ {1,2,0,0},
{0,1,1,0} };



New constructor:


class Tetromino {
protected:
Block block[4];
Block pivot;

public:
Tetromino(const int shape[2][4]) {
int i = 0;
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 2; y++) {
if (shape[y][x] != 0) {
block[i].set(x,y);
i++;
}
if (shape[y][x] == 2) {
pivot.set(x,y);
}
}
}
}
};



Am I on the right track?

Share this post


Link to post
Share on other sites
azure kitsune    122
I apologize for double posting but I really would like to know if my changes are in the right direction. This kind of approach is new to me and I don't want to have to unlearn it all because I was doing it incorrectly from the start. Like you guys have suggested, I removed all the subclasses and switched to matrices instead. I realized that this brings the rotation problem back. Now the program cannot distinguish between the shapes, so the O block (the 2x2 square one) rotates again.

Can anyone help?

Share this post


Link to post
Share on other sites
Oluseyi    2114
A tetramino is a collection of four blocks, each with an adjacency to at least one other block. There are seven tetramino shapes: I, J, L, O, S, T and Z. You do not need distinct classes to represent each of the shapes, since they all behave the same. The tetraminoes can be rotated, and the blocks must be independent entities so that individual lines can be cleared while left over pieces remain.

All of the tetraminoes can be represented on a 4x4 grid. They have a variable number of rotations: I, S and Z have two orientations, O has just one, and the remainder have four. You could set up a regular grid indicating which cells in the 4x4 grid would be filled for each tetramino and store this as a template for the instantiation of new tetraminoes on the playing field.

The lifetime of a tetramino is from just before it is entered at the very top of the Tetris grid until its lowest block can no longer drop. At that point, the blocks in the tetramino become part of the grid, or are cleared as lines (technically, they become part of the grid and then the grid is evaluated for any lines that can be cleared).

Based on the above, we only need one class for tetraminoes - Tetramino - which includes a 4x4 grid for block positions, a color or image for each block, an index into the rotation of the shape and a position on the Tetris grid. Collision detection is fairly simple because you test a single object, the current tetramino, against the Tetris grid, which is perfectly regular. Specifically, you test against the left and right barriers to limit tetraminoes from being moved off screen and against the row in the Tetris grid directly below the tetramino.


Try putting the above into code.

Share this post


Link to post
Share on other sites
Zahlman    1682
Quote:
Original post by azure kitsune
Am I on the right track?


You're trying to represent the Tetromino as a set of 4 Blocks, each of which has a position. This seems logical, but it turns out not to be particularly convenient. In particular, it ignores the fact that the Blocks in question need to be near each other, and ends up requiring some rather messy code to specify a "pivot" for rotation.

The usual way to do this is to represent the Tetromino as a grid of 16 (4 by 4) boolean values (on or off), where exactly 4 of the values are set (representing spaces occupied by a Block), and a position where the top-left of that grid is within the world. This exploits the fact that every Tetromino can always fit into a 4x4 grid. The TetrominoShapes can then directly encode the initial grid states, and rotation is done by simply permuting the grid. Alternatively, you can make a full collection of TetrominoShapes (including rotated versions), link them together, and have each Tetromino simply hold a pointer to its current TetrominoShape, rather than copying the data. To rotate, you simply change the pointer to point to the rotated version; this can be done by having the TetrominoShape include links to left and right rotations. If all the shapes are kept in an array somewhere, you can use array indices instead of pointers (since the array contents will never change.

The linked approach could look like:


class TetrominoShape {
bool mask[4][4];
TetrominoShape* left, right;
};

enum { LINE_A, LINE_B, /* etc. */, SHAPE_COUNT } ShapeName;

TetrominoShape* getShape(ShapeName id) {
static TetrominoShape all_shapes[] = {
{ /* LINE_A */
{
{0, 0, 0, 0},
{1, 1, 1, 1},
{0, 0, 0, 0},
{0, 0, 0, 0}
}, LINE_B, LINE_B /* Rotating a line either way has the same result. */
},
{ /* LINE_B */
{
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0}
}, LINE_A, LINE_A
},
/* etc. */
}
return all_shapes + id; /* this is the same as &(all_shapes[id]) */
}

class Tetromino {
int x, y;
TetrominoShape* shape;
Tetromino() : shape(getShape(ShapeName(rand() % SHAPE_COUNT))) {}
Tetromino(ShapeName id) : shape(getShape(id)) {}
void rotate_left() { shape = shape-&gt;left; }
void rotate_right() { shape = shape-&gt;right; }
bool collides(const World& w) {
// check if shape-&gt;mask[0][0] collides with world.data[y + 0][x + 0],
// if shape-&gt;mask[0][1] collides with world.data[y + 0][x + 1], etc.
// in a loop. Collision occurs when the corresponding space is "full" in
// both the piece's mask and the world's data.
}
};



(EDIT: I always keep opening with a code tag and closing with a source tag. Optimism about how little code will be required maybe? :) )

I'll let you think about the other approach (copying a 'mask' into the Tetromino data and rotating that array to rotate the block). :)

Share this post


Link to post
Share on other sites
azure kitsune    122
Okay, I have tried what you guys have suggested. I think I ran into a few problems though.

How do I read the data correctly? What I'm doing right now involves a lot of pointer casting, and the read_data() method in Tetromino makes a copy of the data into its own array. Can I just point to the data so I don't have to make a copy of it each time?

Here's what I have now. (I got rid of the block class like Zalhman said.)

data class:


class TetrominoData {
private:
static const char shape_data[7][4][4][4];
public:
static char** get_tetromino_data(int shape, int rotation) {
return (char**) TetrominoData::shape_data[shape][rotation];
}
};

const char TetrominoData::shape_data[7][4][4][4] =
{
{ // I-block
{
{0, 0, 0, 0},
{1, 1, 1, 1},
{0, 0, 0, 0},
{0, 0, 0, 0}
},
{
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0}
},
{
{0, 0, 0, 0},
{0, 0, 0, 0},
{1, 1, 1, 1},
{0, 0, 0, 0},
},
{
{0, 0, 1, 0},
{0, 0, 1, 0},
{0, 0, 1, 0},
{0, 0, 1, 0},
}
},

/*** the other 6 blocks are here ***/

};





tetromino class:


class Tetromino {
private:
int shape;
int rotation;

//corresponds to position of upper left corner
int x;
int y;

char data[4][4];

public:
Tetromino(int i_shape, int i_rotation) {
shape = i_shape;
rotation = i_rotation;

x = 0;
y = 0;

read_data();
}

void read_data() {
char* temp = (char*) TetrominoData::get_tetromino_data(shape, rotation);

for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; y++) {
data[x][y] = *(temp+4*y+x);
}
}
}

void set_location(int i_x, int i_y) {
x = i_x;
y = i_y;
};

void rotate(int dir) {
if (dir == 1) { //clockwise
rotation = (rotation + 1) % 4;
} else if (dir == -1) { //counter clockwise
rotation = (rotation + 3) % 4;
}
read_data();
}
};




And Zahlman, sorry about my lack of c++ knowledge, but I don't understand your code. Can you explain what these following lines do?

Tetromino() : shape(getShape(ShapeName(rand() % SHAPE_COUNT))) {}
Tetromino(ShapeName id) : shape(getShape(id)) {}

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this