/* --- CSU213 Spring 2006 Lecture Notes --------- Copyright 2006 Viera K. Proulx Lecture 14: Looking Down. Goals: - Designing subclasses, restricting the visibility. Introduction: When designing super classes and unions we were looking for commonalities between several variants and designed a super class to abstract over them. We now look at situations when it becomes desirable to spawn off a subclass that has most of the behavior of its super class, but is distinguished in some way that makes it behave distinctly differently from its super class. ------------------------------------------------------------------------- Our problem concerns the design of a simplified 'Tetris' game. At any moment there is a block falling from the top. The player can move the block left or right using the arrow keys. When the block hits the bottom, or one of already 'resting' blocks, it stops there and becomes a 'resting' block for the remainder of the game. At that time a new block starts falling down. The game ends if one of the block ends up resting all the way on the top. If you wish to keep the score, count the number of resting blocks at that time. The key classes of data we need are the following: class BlockWorld -- to control the entire game class Canvas -- to display the progress of the game graphically class Block -- to represent the blocks in the game classes IBlock, MTBlock, ConsBlock -- to represent the list of resting blocks We will focus on the classes that represent blocks. We know that each block needs to record its location on the Canvas - let us measure how far right and down the block is from the top left corner. Of course, we also need to be able to draw the block on the given Canvas. The first design suggests that we define a class Block: +------------------------+ | Block | +------------------------+ | int down | | int right | +------------------------+ | boolean draw(Canvas c) | +------------------------+ However, we also need to model the movement of the falling block, while the resting blocks remain in one place for the rest of the game. It suggest that we design a separate subclass for the falling blocks. There we include methods that allow us to erase the block (so we can repaint it at the new location), drop the block a fixed distance down (on each tick of the clock) and steer the block left or right in response to the key events. Finally, we need to be able to tell when the dropping block came to its resting place - and produce a new resting block when that happens. The subclass we design is the following: +---------------------------------------+ | DropBlock | +---------------------------------------+ | int deltaY | +---------------------------------------+ | boolean erase(Canvas c) | | DropBlock drop() | | boolean landed(IBlocks r) | | DropBlock steer(String ke, IBlocks r) | +---------------------------------------+ The code fore these two class definitions is as follows: // to represent a block in a Tetris-like game class Block{ int down ; int right; int size = 10; Color color = new Black(); Block(int down, int right){ this.down = down; this.right = right; } // draw this block on the given Canvas boolean draw(Canvas c){ return c.drawRect(new Posn(this.right, this.down), this.size, this.color); } // to represent a dropping block in a Tetris-like game class DropBlock extends Block{ int deltaY = 3; DropBlock(int down, int right){ super(down, right); } // erase this block on the given Canvas boolean erase(Canvas c){ return c.drawRect(new Posn(this.right, this.down), this.size, new White()); // should be the background color } // drop this block at each time tick DropBlock drop(){ return this; } // did this block land on the ground or on the given resting blocks? boolean landed(IBlocks r){ return false; } // produce a block moved in response to the left-right keys, // avoiding the resting blocks DropBlock steer(String ke, IBlocks r){ return this; } } At this point we can construct a new DropBlock anywhere on the Canvas, even thought in the game a new block always drops from the top. On the other hand, both of the methods 'drop' and 'steer' need to produce a new DropBlock, moved from its previous location. To prevent the misuse of the constructor, we 'overload' the constructor --- i.e. define two versions of the constructor - one for constructing the initial dropping block, the second one for constructing the block that has moved from the previous position. We distinguish between the two alternatives by using a different set of arguments. When the block drops from the top, the user has no control over the location where it will be --- and so does not supply any arguments to the constructor. The second constructor is used by the methods 'drop' and 'steer' and consumes two arguments representing its location. This is not enough. Now that we have two variants of the constructor, we want to prevent the outside classes from ever using the second variant - we want it to be restricted for our 'private' use --- within the class definition for the class DropBlock. To do so, we use the privacy modifier 'private' for the second constructor and leave the first one as 'public': // to represent a dropping block in a Tetris-like game class DropBlock extends Block{ int deltaY = 3; public DropBlock(){ this.down = 0; // should be a random number between 0 and the canvas width this.right = new Random().nextInt(200); } private DropBlock(int down, int right){ super(down, right); } ... } ------------------------------------------------------------------ We have another concern. The list of resting blocks consists of instances of the class Block. However, there is nothing that prevents the programmer to include an instance of a DropBlock in the list of resting blocks --- it is an instance of a legitimate subclass. That suggests that we need a common abstract class with two subclasses --- one that represents the resting blocks and one that represents the dropping block. Before we draw the diagram, and complete the class definitions, we address two additional concerns. Further analysis shows that the two methods 'draw' and 'erase' look very similar - the only difference is in the color they use to paint. We can abstract over this property as well and design a new method 'paint' that consumes an additional argument --- the color for the drawing: // draw this block on the given Canvas in the given color boolean paint(Canvas cv, Color co){ return cv.drawRect(new Posn(this.right, this.down), this.size, co); } // draw this block on the given Canvas boolean draw(Canvas c){ return this.paint(c, this.COLOR); } // erase this block on the given Canvas boolean erase(Canvas c){ return this.paint(c, this.BACKG); // using the background color } However, the outside world should never use out 'paint' method directly. It should only ask to draw or erase a block. Again, we use the privacy modifiers to hide the 'paint' method as private and make the other two methods public. Our second concern is that the user of this class should not be concerned with the color used to draw the blocks or its size, but the subclasses need to be able to determine the block's location. A third type of privacy modifier 'protected' allows the subclasses the access to the 'protected' fields and methods, but prevents other unrelated classes from seeing their values. Our final design that addresses all of the above concerns is below. If we try to construct an instance of Block, we fail. Entering > Block b = new Block(20, 30); in the Interactions window results in the error: class Block is abstract. Abstract classes may not be instantiated. in: Block We do not include examples - make them and complete the code for the game. */ import draw.*; // to represent a block in a Tetris-like game abstract class Block{ protected int down ; protected int right; private int size = 10; private Color COLOR = new Black(); private Color BACKG = new White(); Block(int down, int right){ this.down = down; this.right = right; } // draw this block on the given Canvas in the given color private boolean paint(Canvas cv, Color co){ return cv.drawRect(new Posn(this.right, this.down), this.size, this.size, co); } // draw this block on the given Canvas public boolean draw(Canvas c){ return this.paint(c, this.COLOR); } // erase this block on the given Canvas public boolean erase(Canvas c){ return this.paint(c, this.BACKG); // using the background color } } // to represent a resting block class RestingBlock extends Block{ RestingBlock(int down, int right){ super(down, right); } } // to represent a dropping block in a Tetris-like game class DropBlock extends Block{ int deltaY = 3; DropBlock(int down, int right){ super(down, right); } // drop this block at each time tick DropBlock drop(){ return this; } // did this block land on the ground or on the given resting blocks? boolean landed(IBlocks r){ return false; } // produce a block moved in response to the left-right keys, // avoiding the resting blocks DropBlock steer(String ke, IBlocks r){ return this; } } // a stub of the class definition given, so that the code compiles class IBlocks{ IBlocks(){} }