Connor Hartzog and Phoebe He
Solution to the Sample Final
5/5-6/2015

Problem 1: Set Difference Method

  SetOfBlocks difference(SetOfBlocks other) {
    SetOfBlocks result = new SetOfBlocks();
    for (Block b : other) {
      if (! this.contains(b))
        result.add(b);
    }
    return result;
  }
  
Problem 2: Restrict Horizontal Movement

  void move(int dx, int dy) {
    if (! (this.blocks.maxX() + dx > 9)) {
      if (! (this.blocks.minX() + dx < 0)) {
        this.center.move(dx, dy); 
        this.blocks.move(dx, dy); 
      }
    }
  }

Problem 3: Ghost Block

For this problem, you need to add new/additional constructors to Block, Point, SetOfBlocks, and 
Tetromino. This so that you can create the ghost which is essentially a clone of the falling tetromino

  Block(Block block) {
    this(block.x, block.y, Color.LIGHT_GRAY);
  }

We mark the code above with // [1] in the code we post. 

  Point (Point p) {
    this(p.x, p.y);
  }

We mark the code above with // [2] in the code we post. 
  
  SetOfBlocks(SetOfBlocks blocks) {
    for (Block b : blocks)
      this.add(new Block(b));
  }

We mark the code above with // [3] in the code we post. 

  SetOfBlocks() {
  
  }

We mark the code above with // [4] in the code we post. 

This last one is necessary because we need it and it disappears when the cloning constructor is added. 

  Tetromino(Tetromino t) {
    this.center = new Point(t.center);
    this.blocks = new SetOfBlocks(t.blocks);
  }

We mark the code above with // [5] in the code we post. 

This is the reason we started all of this: we needed a deep clone of the falling tetromino.

Now cloning is possible. What else do we do? 
  
In Tetris you have to define the Tetromino ghost

   Tetromino t, 
             ghost; // [6] notice this initializes ghost with null 

Earlier we didn't have [6]. 

In the draw method you have to draw the ghost but only when there is one:

    if (ghost != null)
        ghost.draw(g);

We mark the code above with // [7] in the code we post. 

Notice the code occurs before drawing the falling tetromino so that the falling 
piece will overlap the ghost when the piece begins landing. Also notice some additional
debug code that was useful to us. 

In the update method you have to add:

    this.theGhostOf(this.t);

to the else statement so that you can get the ghost of the falling tetromino. This isn't
in fact necessary all the time, just the first time around, since falling down vertically 
does not have an effect on the shadow. But we can't do it otherwise and it's a good idea 
to recalculate the shadow after every movement (especially horizontal movement).

In the key event for pressing left, right, or r you must add:

    this.theGhostOf(this.t); // [oops!]

so that each time you press left, right, or r the ghost will be updated. 

We do this three times, and we mark this with // [8] each time. 

In the key event for pressing the space bar you must add:

    this.ghost = null;

so that the ghost piece will disappear and be replaced with the tetromino

We mark this as // [9]. 

You then finally need to add the definition of void method theGhostOf for calculating the ghost which 
is essentially a clone of the falling tetromino:

  void theGhostOf(Tetromino t) {   
    if (this.t == null) {
      this.ghost = null; 
      return; 
    }
    this.ghost = new Tetromino(t); // cloning it
    this.jumpDown(this.ghost); 
  }

We mark this with // [10] 

In the touchdown() method you must first call:

    this.ghost = null; // [11] 

so that the ghost piece will be removed (same reasoning as when hitting space bar). 

The jumpDown(), landedOnBlocks(), landedOnFloor(), and landed() methods must also be updated
These methods are all now called on (Tetromino t) and rather than acting on this, they act on t. 
This allows for the updating of only the falling tetromino, rather than the world

Code:

class Point { // represents position of a square (block) 
  int x, y;
  Point(Point p) {   // [2] 
    this(p.x, p.y);  // [2] 
  }                  // [2] 
  Point(int x, int y) {
    this.x = x;
    this.y = y;
  }
  void move(int dx, int dy) {
    this.x += dx; 
    this.y += dy; 
  }
  
  public String toString() {
    return "(" + this.x + ", " + this.y + ")";  
  }
}

// ------------------------------------------------------------------------------------------

import java.awt.*; 

class Block {
  final static int SIZE = 20; // pixels 
  int x, y;
  Color color; 
  Block(Block block) {                        // [1] 
    this(block.x, block.y, Color.LIGHT_GRAY); // [1]  
  }                                           // [1] 
  Block(int x, int y, Color c) {
    this.x = x;
    this.y = y;
    this.color = c; 
  }
  void draw(Graphics g) {
    int xp = this.x * SIZE, //  + SIZE/2, 
        yp = this.y * SIZE; //  + SIZE/2;
    g.setColor(this.color);     
    g.fillRect(xp, yp, SIZE, SIZE); 
    g.setColor(Color.BLACK); 
    g.drawRect(xp, yp, SIZE, SIZE); 
  }
  void move(int dx, int dy) {
    this.x += dx; 
    this.y += dy; 
  }  
  public String toString() {
    return "[" + this.x + ", " + this.y + "]"; 
  }
  void rotateCW(Point c) { // clockwise
    int newX, newY;
    newX = c.x + c.y - this.y;
    newY = c.y + this.x - c.x; 
    this.x = newX; 
    this.y = newY; 
    /********
     Why is this wrong: 
       this.x = c.x + c.y - this.y; 
       this.y = c.y + this.x - c.x; 
     How can you be sure? 
     ********/
  }
  public static void main(String[] args) { // this is testing the rotation 
    Point center = new Point(8, 3); 
    Block block = new Block(8, 2, Color.RED); 
    System.out.println( block ); 
    block.rotateCW(center); 
    System.out.println(block); // should produce (9, 3) 
  }
  boolean equals(Block other) {
    return this.x == other.x && this.y == other.y;  
  }
  void setColor(Color c) {
    this.color = c;  
  }
}

// ------------------------------------------------------------------------------------------

import java.awt.*; 
import java.util.*; 

class SetOfBlocks extends ArrayList<Block> {
  SetOfBlocks() { // [4] 
                  // [4]
  }               // [4]
  SetOfBlocks(SetOfBlocks blocks) { // [3] 
    for (Block b : blocks)          // [3] 
      this.add(new Block(b));       // [3] 
  }                                 // [3] 
  void draw(Graphics g) {
    for (Block b : this) 
      b.draw(g); 
  }
  void move(int dx, int dy) {
    for (Block b : this) 
      b.move(dx, dy); 
  }
  void rotateCW(Point center) { // clockwise 
    for (Block b : this)
      b.rotateCW(center); 
  }
  boolean contains(Block block) {
    if (this.size() != 0) 
      for (Block b : this)
        if (b.equals(block))
          return true;
    return false; 
  }
  boolean subset(SetOfBlocks blocks) {
    if (this.size() != 0)
      for (Block b : this)
        if (! blocks.contains(b))
          return false; 
    return true; 
  }
  boolean equals(SetOfBlocks blocks) {
    return this.subset(blocks) && blocks.subset(this);  
  }
  SetOfBlocks intersect(SetOfBlocks other) {
    SetOfBlocks result = new SetOfBlocks(); 
    for (Block b : this)
      if (other.contains(b))
        result.add(b); 
    return result; 
  }
  SetOfBlocks union(SetOfBlocks other) {
    SetOfBlocks result = new SetOfBlocks(); 
    for (Block b : this)
      if (! result.contains(b) )
        result.add(b); 
    for (Block b : other)
      if (! result.contains(b) )
        result.add(b); 
    return result; 
  }
  int count() {
    return this.size(); 
  }
  int maxY() {
    int result = 0; 
    for (Block b : this)
      if (b.y > result) 
        result = b.y; 
    return result; 
  }
  int minX() {
    int result = Tetris.COLUMNS; 
    for (Block b : this)
      if (b.x < result)
        result = b.x; 
    return result; 
  }
  int maxX() {
    int result = 0; 
    for (Block b : this)
      if (b.x > result)
        result = b.x; 
    return result; 
  }
  void changeColor(Color color) {
    for (Block b : this) 
      b.setColor(color); 
  }
  boolean overflow() {
    for (Block b : this)
      if (b.y <= 0)
        return true;
    return false;
  }
  SetOfBlocks row(int row) {
    SetOfBlocks result = new SetOfBlocks(); 
    for (Block b : this)
      if (b.y == Tetris.ROWS - row)
        result.add(b); 
    return result; 
  }
  boolean fullRow(int row) {
    int a = this.row(row).count(); 
    int b = Tetris.COLUMNS; 
    // System.out.println( a + " vs " + b ); 
    return a == b; 
  }
  void eliminateRow(int i) { // i == 0 indicates bottom row 
    SetOfBlocks row = new SetOfBlocks(); 
    for (Block b : this) 
      if (b.y == (Tetris.ROWS - i))
        row.add(b); 
    for (Block b : row)
      this.remove(b); 
    for (Block b : this) 
      if (b.y < (Tetris.ROWS - i))
        b.move(0, 1); 
  }
  void eliminateFullRows() {
    for (int i = 0; i < Tetris.ROWS;    ) {
      if (this.fullRow(i)) {
        // System.out.println("Eliminating: " + i); 
        this.eliminateRow(i); 
      } else i++; 
    }
  }
}

// ------------------------------------------------------------------------------------------

import java.awt.*; 

class Tetromino {
  // please note the centers of rotation CW
  static Tetromino sQuare()    { 
    return new Tetromino(new Point(0, -1), 
                         makeBlocks(new int[] {0, -1, 0, -2, 1, -1, 1, -2}, 
                         Color.GREEN   )); 
  } 
  static Tetromino liNe()      { 
    return new Tetromino(new Point(1, -1), 
                         makeBlocks(new int[] {0, -1, 1, -1, 2, -1, 3, -1}, 
                         Color.BLUE    )); 
  } 
  static Tetromino l()         { 
    return new Tetromino(new Point(1, -1), 
                         makeBlocks(new int[] {0, -1, 1, -1, 2, -1, 2, -2}, 
                         Color.MAGENTA )); 
  } 
  static Tetromino MirroredL() { 
    return new Tetromino(new Point(1, -1), 
                         makeBlocks(new int[] {0, -1, 1, -1, 2, -1, 0, -2}, 
                         Color.CYAN    )); 
  } 
  static Tetromino t()         { 
    return new Tetromino(new Point(1, -1), 
                         makeBlocks(new int[] {0, -1, 1, -1, 2, -1, 1, -2}, 
                         Color.ORANGE  )); 
  } 
  static Tetromino s()         { 
    return new Tetromino(new Point(1, -1), 
                         makeBlocks(new int[] {0, -1, 1, -1, 1, -2, 2, -2}, 
                         Color.RED     )); 
  } 
  static Tetromino z()         { 
    return new Tetromino(new Point(1, -2), 
                         makeBlocks(new int[] {0, -2, 1, -2, 1, -1, 2, -1}, 
                         Color.PINK    )); 
  } 
  
  static SetOfBlocks makeBlocks(int[] c, Color color) {
    SetOfBlocks a = new SetOfBlocks(); 
    a.add(new Block( c[0],  c[1], color)); 
    a.add(new Block( c[2],  c[3], color)); 
    a.add(new Block( c[4],  c[5], color)); 
    a.add(new Block( c[6],  c[7], color)); 
    return a;
  }
  Point center;
  SetOfBlocks blocks;
  
  Tetromino(Tetromino t) {                     // [5] 
    this.center = new Point(t.center);         // [5] 
    this.blocks = new SetOfBlocks(t.blocks);   // [5] 
  }                                            // [5] 
  
  Tetromino(Point center, SetOfBlocks blocks) {
    this.center = center; 
    this.blocks = blocks; 
  }
  void draw(Graphics g) {
    this.blocks.draw(g); 
  }
  void move(int dx, int dy) {
    this.center.move(dx, dy); 
    this.blocks.move(dx, dy); 
  }
  public String toString() {
    return this.center + " " + this.blocks ;  
  }
  void rotateCW() {
    // System.out.println("I will rotate when I want to."); 
    this.blocks.rotateCW(this.center);  
  }
  boolean overlapsBlocks(SetOfBlocks blocks) {
    return this.blocks.intersect(blocks).count() > 0;  
  }
  void changeColor(Color color) {
    this.blocks.changeColor(color);  
  }
  static Tetromino pickRandom() {
    int value = (int)(Math.random() * 7);  
    if (value == 0) return Tetromino.sQuare();
    else if (value == 1) return Tetromino.liNe(); 
    else if (value == 2) return Tetromino.l(); 
    else if (value == 3) return Tetromino.MirroredL(); 
    else if (value == 4) return Tetromino.t(); 
    else if (value == 5) return Tetromino.s(); 
    else return Tetromino.z(); 
  }
}

// ------------------------------------------------------------------------------------------

import java.awt.*; 
import java.awt.event.*; 
import javax.swing.*; 

class Tetris implements World {
  static final int ROWS = 20; 
  static final int COLUMNS = 10; 
  Tetromino t, ghost; 
  SetOfBlocks blocks; 
  Tetris(Tetromino t, SetOfBlocks s) {
    this.t = t;
    this.blocks = s; 
  }
  public void draw(Graphics g) { // world->image
    // System.out.println("World being drawn."); 
    if (ghost != null)  // [7] 
      ghost.draw(g);    // [7] 
    t.draw(g); 
    System.out.println( "Ghost: " + this.ghost );
    System.out.println( "Tetromino: " + this.t );
    blocks.draw(g); 
    g.drawRect(0, 0, Tetris.COLUMNS * Block.SIZE, Tetris.ROWS * Block.SIZE); 
  } 
  public void update() { 
    // System.out.println("World getting older."); 
    if (this.landed(this.t))
      this.touchdown(); 
    else {
      this.t.move(0, 1); 
      this.theGhostOf(this.t); // [oops!]
    }
  }
  public boolean hasEnded() { return false; } 
  public void keyPressed(KeyEvent e) { // world-key-move
    if (this.landed(this.t)) 
      this.touchdown(); 
    else if (e.getKeyCode() == KeyEvent.VK_LEFT ) { 
      this.t.move(-1,  0); 
      this.theGhostOf(this.t);  // [8]   
    }
    else if (e.getKeyCode() == KeyEvent.VK_RIGHT) { 
      this.t.move( 1,  0); 
      this.theGhostOf(this.t);  // [8] 
    } 
    else if (e.getKeyChar() == ' ') { 
      this.jumpDown(this.t); 
      this.ghost = null; // [9] 
    } 
    // else if (e.getKeyChar() == '2') { this.blocks.eliminateRow(2); } // basic test 
    // else if (e.getKeyChar() == '2') { System.out.println( "Row 2 has " + this.blocks.row(2).count() + " blocks currently." ); } // another basic test 
    // else if (e.getKeyChar() == '2') { System.out.println( "Row 2 full? Answer: " + blocks.fullRow(2) ); } // yet another basic test 
    // else if (e.getKeyChar() == 'e') { this.blocks.eliminateFullRows(); } // another basic test 
    else if (e.getKeyChar() == 'r') {    // Rotate CW
      this.t.rotateCW();  
      this.theGhostOf(this.t);  // [8]
    } else this.t.move( 0, 0 );     
  } 
  public static void main(String[] args) {
    BigBang game = new BigBang(new Tetris(Tetromino.sQuare(), new SetOfBlocks())); 
    JFrame frame = new JFrame("Tetris"); 
    frame.getContentPane().add( game ); 
    frame.addKeyListener( game ); 
    frame.setVisible(true); 
    frame.setSize(Tetris.COLUMNS * Block.SIZE + 20, Tetris.ROWS * Block.SIZE + 40); 
    frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); 
    game.start(); 
  }
  void touchdown() {
    this.ghost = null; // [11] 
    this.blocks = this.blocks.union(this.t.blocks);
    this.blocks.eliminateFullRows(); 
    this.t = Tetromino.pickRandom(); 
  }
  
  void theGhostOf(Tetromino t) {   // [10] 
    if (this.t == null) {          // [10] 
      this.ghost = null;           // [10] 
      return;                      // [10] 
    }                              // [10] 
    this.ghost = new Tetromino(t); // cloning it
    this.jumpDown(this.ghost);     // [10] 
  }                                // [10] 
  
  void jumpDown(Tetromino t) {  // [12] 
    if (! this.landed(t)) { 
      t.move(0, 1); 
      this.jumpDown(t); 
    }      
  }
  boolean landedOnBlocks(Tetromino t) { // [12] 
    t.move(0, 1); 
    if (t.overlapsBlocks(this.blocks)) { 
      t.move(0, -1); 
      return true; 
    } else {
      t.move(0, -1); 
      return false; 
    }
  }
  boolean landedOnFloor(Tetromino t) { // [12] 
    return t.blocks.maxY() == Tetris.ROWS - 1; 
  }
  boolean landed(Tetromino t) { // [12] 
    return this.landedOnFloor(t) || this.landedOnBlocks(t);  
  }
}

import java.awt.*; 
import java.awt.event.*; 

// ------------------------------------------------------------------------------------------

interface World {
  public void draw(Graphics g);
  public void update(); 
  public boolean hasEnded();
  public void keyPressed(KeyEvent e); 
}

import javax.swing.*; 
import java.awt.event.*; 
import java.awt.*; 

// ------------------------------------------------------------------------------------------

class BigBang extends JComponent implements KeyListener, ActionListener {
  Timer timer; 
  World world; 
  BigBang(int delay, World world) {
    timer = new Timer(delay, this); 
    this.world = world;
  } 
  public void start() {
    timer.start();  
  }
  BigBang(World world) {
    this(1000, world);  
  }
  public void paintComponent(Graphics g) {
    world.draw(g);  
  }
  public void actionPerformed(ActionEvent e) {
    world.update(); 
    if (world.hasEnded())
      timer.stop(); 
    this.repaint(); 
  }
  public void keyPressed(KeyEvent e) { 
    world.keyPressed(e); 
    this.repaint(); 
  } 
  public void keyTyped(KeyEvent e) { } 
  public void keyReleased(KeyEvent e) { } 
}

// ------------------------------------------------------------------------------------------