Modeling block shape

The approach used to modeling shape varies depending on numerous variables, such as the kind of shape that must be measured and in what spacial dimension the shape is to be modeled. Modeling three-dimensional shapes—all things being equal—is more difficult than modeling two-dimensional shapes. Lucky for us, tetrominoes are two-dimensional in nature. Before we start modeling our shapes programmatically, it is important we know the exact shapes we are attempting to model. There are seven fundamental tetromino pieces that exist in Tetris. These pieces are the O, I, T, L, J, S and Z tetrominos. The following image shows the fundamental tetromino shapes that exist in Tetris:

All preceding shapes take up space within the confines of their edges. The area of space covered by a shape can be seen as an outline or a frame. This is similar to how a picture is held within a frame. We need to model this frame that will contain individual shapes. As the shapes being held within the frame are two-dimensional in nature, we will utilize a two-dimensional byte array to hold frame-specific information. A byte is a digital unit of information that generally consists of eight bits. A bit is a binary digit. It is the smallest unit of data in a computer and has a value of either 1 or 0.

The idea is to model the frame of a shape with a two-dimensional array by representing areas covered by the frame with a byte value of 1 and those not covered by it with a value of 0. Take the following frame, for example:

Instead of visualizing it as a whole shape, we can visualize it as a two-dimensional array of bytes possessing two rows and three columns:

A byte value of 1 is assigned to cells in the array that make up the frame's shape. On the other hand, a byte value of 0 is assigned to cells that are not part of the frame's shape. Modeling this with a class is fairly easy. Firstly, we will need a function that generates the required byte array structure we will use for storing frame bytes. Create a new package within your source package and give it the name helpers. Within this package, create a HelperFunctions.kt file. This file will contain all helper functions used in the course of the development of this app. Open HelperFunctions.kt and type the following code into the file:

package com.mydomain.tetris.helpers

fun array2dOfByte(sizeOuter: Int, sizeInner: Int): Array<ByteArray>
= Array(sizeOuter) { ByteArray(sizeInner) }

The preceding code defines a array2dOfByte() function, which takes two arguments. The first argument is the desired row number of the array to be generated and the second is the desired column number of the generated byte array. The array2dOFByte() method generates and returns a new array with the specified properties. Now that we have our byte array generating helper function set up, let's go ahead and create the Frame class. Create a new package within your source package and give it the name models. All object models will be packaged within this created package. Within the models package, create a Frame class in the Frame.kt file and type the following code into the file:

package com.mydomain.tetris.models

import com.mydomain.tetris.helpers.array2dOfByte

class Frame(private val width: Int) {
val data: ArrayList<ByteArray> = ArrayList()

fun addRow(byteStr: String): Frame {
val row = ByteArray(byteStr.length)

for (index in byteStr.indices) {
row[index] = "${byteStr[index]}".toByte()
}
data.add(row)
return this
}

fun as2dByteArray(): Array<ByteArray> {
val bytes = array2dOfByte(data.size, width)
return data.toArray(bytes)
}
}

The Frame class has two properties: width and data. Width is an integer property that holds the desired width of the frame to be generated (the number of columns in the frame's byte array). The data property holds an array list of elements in the ByteArray value space. We declare two distinct functions, addRow() and get(). addRow() takes a string, converts each individual character of the string into a byte representation, and adds the byte representation into a byte array, after which it adds the byte array to the data list. get() converts the data array list into a byte array and returns the array.

Having modeled a suitable frame to hold our block, we can go ahead and model the distinct shapes of possible tetrominoes in the game. In order to do this, we will make use of an enum class.  Create a Shape.kt file in the models package before proceeding. We will start by modeling the following simple tetromino shape:

Applying the concept of envisioning frames as a two-dimensional array of bytes, we can envision the frame of the preceding shape as a two-dimensional array of bytes with four rows and a single column with each cell filled with the byte value of 1. With this in mind, let's model the shape. In Shape.kt, create a Shape enum class, as shown in the following code:

enum class Shape(val frameCount: Int, val startPosition: Int) {
Tetromino(2, 2) {
override fun getFrame(frameNumber: Int): Frame {
return when (frameNumber) {
0 -> Frame(4).addRow("1111")
1 -> Frame(1)
.addRow("1")
.addRow("1")
.addRow("1")
.addRow("1")
else -> throw IllegalArgumentException("$frameNumber is an invalid
frame number.")
}
}
};
abstract fun getFrame(frameNumber: Int): Frame
}

An enum class is declared by placing the enum keyword before the class keyword. The primary constructor of the preceding Shape enum class takes two arguments. The first argument is frameCount, which is an integer variable that specifies the number of possible frames a shape can be in. The second argument is startPosition, which specifies the intended start position of the shape along the X axis within the gameplay field. Further down the enum class file, a getFrame() function is declared. There's a notable difference between this function and the functions we have declared until now. getFrame() has been declared with the abstract keyword. An abstract function possesses no implementation (thus no body) and is used to abstract a behavior that must be implemented by an extending class. Let's scrutinize the following lines of code within the enum class:

Tetromino(2, 2) {
override fun getFrame(frameNumber: Int): Frame {
return when (frameNumber) {
0 -> Frame(4).addRow("1111")
1 -> Frame(1)
.addRow("1")
.addRow("1")
.addRow("1")
.addRow("1")
else -> throw IllegalArgumentException("$frameNumber is an invalid
frame number."
)
}
}
};

In the preceding code block, an instance of the enum that provides an implementation of the declared abstract function is being created. The instance's identifier is Tetromino. We passed the integer value 2 as the argument for both the frameCount and startPosition properties of the Tetromino's constructor. In addition, Tetromino provides an implementation for the getFrame() function in its corresponding block by overriding the getFrame() function declared in Shape. Functions are overriden with the override keyword. The implementation of getFrame() in Tetromino takes a frameNumber integer. This frame number determines the frame of Tetromino that will be returned. You may be asking at this point why Tetromino possesses more than one frame. This is simply a result of the possibility of rotation of a tetromino. The single-column tetromino we previously looked at can be rotated either leftwards or rightwards to take the form shown in the following diagram:

When frameNumber passed to getFrame() is 0, getFrame() returns a Frame object that models the frame for the Tetromino in its horizontal state, as shown earlier. When frameNumber is 1, it returns a frame object modeling the shape in its vertical state. 

In the case that frameNumber is neither 0 nor 1, an IllegalArgumentException is thrown by the function.

It is important to note that along with being an object, Tetromino is a constant. Generally, enum classes are used to create constants. An enum class is a perfect choice for modeling our tetromino shapes because we have a fixed set of shapes that we need to implement.

Having understood how the Shape enum class works, we can model the rest of the possible tetromino shapes as shown in the following code block:

enum class Shape(val frameCount: Int, val startPosition: Int) {

Let's create a tetromino shape with one frame and a start position of 1. The tetromino modeled here is the square or 'O' shaped tetromino.

Tetromino1(1, 1) {
override fun getFrame(frameNumber: Int): Frame {
return Frame(2)
.addRow("11")
.addRow("11")
}
},

Let's create a tetromino shape with two frames and a start position of 1. The tetromino modeled here is the 'Z' shaped tetromino.

  Tetromino2(2, 1) {
override fun getFrame(frameNumber: Int): Frame {
return when (frameNumber) {
0 -> Frame(3)
.addRow("110")
.addRow("011")
1 -> Frame(2)
.addRow("01")
.addRow("11")
.addRow("10")
else -> throw IllegalArgumentException("$frameNumber is an invalid
frame number."
)
}
}
},

Let's create a tetromino shape with two frames and a start position of 1. The tetromino modeled here is the 'S' shaped tetromino.

    Tetromino3(2, 1) {
override fun getFrame(frameNumber: Int): Frame {
return when (frameNumber) {
0 -> Frame(3)
.addRow("011")
.addRow("110")
1 -> Frame(2)
.addRow("10")
.addRow("11")
.addRow("01")
else -> throw IllegalArgumentException("$frameNumber is
an invalid frame number."
)
}
}
},

Let's create a tetromino shape with two frames and a start position of 2. The tetromino modeled here is the 'I' shaped tetromino.

    Tetromino4(2, 2) {
override fun getFrame(frameNumber: Int): Frame {
return when (frameNumber) {
0 -> Frame(4).addRow("1111")
1 -> Frame(1)
.addRow("1")
.addRow("1")
.addRow("1")
.addRow("1")
else -> throw IllegalArgumentException("$frameNumber is an
invalid frame number."
)
}
}
},

Let's create a tetromino shape with four frames and a start position of 1. The tetromino modeled here is the 'T' shaped tetromino.

    Tetromino5(4, 1) {
override fun getFrame(frameNumber: Int): Frame {
return when (frameNumber) {
0 -> Frame(3)
.addRow("010")
.addRow("111")
1 -> Frame(2)
.addRow("10")
.addRow("11")
.addRow("10")
2 -> Frame(3)
.addRow("111")
.addRow("010")
3 -> Frame(2)
.addRow("01")
.addRow("11")
.addRow("01")
else -> throw IllegalArgumentException("$frameNumber is an
invalid frame number."
)
}
}
},

Let's create a tetromino shape with four frames and a start position of 1. The tetromino modeled here is the 'J' shaped tetromino.

    Tetromino6(4, 1) {
override fun getFrame(frameNumber: Int): Frame {
return when (frameNumber) {
0 -> Frame(3)
.addRow("100")
.addRow("111")
1 -> Frame(2)
.addRow("11")
.addRow("10")
.addRow("10")
2 -> Frame(3)
.addRow("111")
.addRow("001")
3 -> Frame(2)
.addRow("01")
.addRow("01")
.addRow("11")
else -> throw IllegalArgumentException("$frameNumber is
an invalid frame number."
)
}
}
},

Let's create a tetromino shape with four frames and a start position of 1. The tetromino modeled here is the 'L' shaped tetromino.

    Tetromino7(4, 1) {
override fun getFrame(frameNumber: Int): Frame {
return when (frameNumber) {
0 -> Frame(3)
.addRow("001")
.addRow("111")
1 -> Frame(2)
.addRow("10")
.addRow("10")
.addRow("11")
2 -> Frame(3)
.addRow("111")
.addRow("100")
3 -> Frame(2)
.addRow("11")
.addRow("01")
.addRow("01")
else -> throw IllegalArgumentException("$frameNumber is
an invalid frame number."
)
}
}
};

abstract fun getFrame(frameNumber: Int): Frame
}

Having modeled both the block frame and shape, the next thing we must model programmatically is the block itself. We will use this as an opportunity to demonstrate Kotlin's seamless interoperability with Java by implementing the model with Java. Create a new Java class in the models directory (models | New | Java Class) with the name Block. We will start the modeling process by adding instance variables that represent the characteristics of a block. Consider the following code:

package com.mydomain.tetris.models;
import android.graphics.Color;
import android.graphics.Point;

public class Block {
private int shapeIndex;
private int frameNumber;
private BlockColor color;
private Point position;

public enum BlockColor {
PINK(Color.rgb(255, 105, 180), (byte) 2),
GREEN(Color.rgb(0, 128, 0), (byte) 3),
ORANGE(Color.rgb(255, 140, 0), (byte) 4),
YELLOW(Color.rgb(255, 255, 0), (byte) 5),
CYAN(Color.rgb(0, 255, 255), (byte) 6);

BlockColor(int rgbValue, byte value) {
this.rgbValue = rgbValue;
this.byteValue = value;
}

private final int rgbValue;
private final byte byteValue;
}
}

In the preceding code block, we add four instance variables: shapeIndex, frameNumber, color, and position. shapeIndex will hold the index of the shape of the block, frameNumber will keep track of the number of frames the block's shape has, color will hold the color characteristic of the block, and position will be used to keep track of the block's current spatial position in the gaming field.

An enum template, BlockColor, is added within the Block class. This enum creates a constant set of BlockColor instances, with each possessing rgbValue and byteValue properties. rgbValue is an integer that uniquely identifies an RGB color specified with the Color.rgb() method. Color is a class provided by the Android application framework and rgb() is a class method defined within the Color class. The five Colour.rgb() calls specify the colors pink, green, orange, yellow, and cyan, respectively.

In Block, we made use of the private and public keywords. These were not added for eye candy; they each have a use. These two keywords, along with the protected keyword, are called access modifiers.

Access modifiers are keywords used to specify access restrictions on classes, methods, functions, variables, and structures. Java has three access modifiers: private, public, and protected. In Kotlin, access modifiers are called visibility modifiers. The available visibility modifiers in Kotlin are public, protected, private, and internal.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.72.74