Tetris implementation, need help

Started by
15 comments, last by azure kitsune 15 years, 3 months ago
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.
Advertisement
Double post lag, please delete.
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.set(x,y);					i++;				}				if (shape[y][x] == 2) {					pivot.set(x,y);				}			}		}	}};


Am I on the right track?
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?
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.
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). :)
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)) {}

This topic is closed to new replies.

Advertisement