Exit Full View

Itchy / src / main / java / uk / co / nickthecoder / tetra / Tetra.java

/*******************************************************************************
 * Copyright (c) 2013 Nick Robinson All rights reserved. This program and the accompanying materials are made available under the terms of
 * the GNU Public License v3.0 which accompanies this distribution, and is available at http://www.gnu.org/licenses/gpl.html
 ******************************************************************************/
package uk.co.nickthecoder.tetra;

/*
 * Thanks to Colin Fahey, for an excellent guide to all things tetrisy : http://www.colinfahey.com/tetris/
 */

import java.awt.Point;
import java.util.Random;

import uk.co.nickthecoder.itchy.AbstractDirector;
import uk.co.nickthecoder.itchy.Actor;
import uk.co.nickthecoder.itchy.Actor.AnimationEvent;
import uk.co.nickthecoder.itchy.Input;
import uk.co.nickthecoder.itchy.Itchy;
import uk.co.nickthecoder.itchy.Launcher;
import uk.co.nickthecoder.itchy.ZOrderStage;
import uk.co.nickthecoder.itchy.extras.Fragments;
import uk.co.nickthecoder.itchy.extras.Timer;
import uk.co.nickthecoder.itchy.role.ExplosionBuilder;
import uk.co.nickthecoder.itchy.role.PlainRole;
import uk.co.nickthecoder.jame.Sound;
import uk.co.nickthecoder.jame.event.KeyboardEvent;
import uk.co.nickthecoder.jame.event.Keys;

public class Tetra extends AbstractDirector
{
    /**
     * A static reference to the Tetris object, which makes it easy for external classes to interact with the game.
     */
    public static Tetra director;

    /**
     * The size of a tetris square
     */
    public static final int SCALE = 20;
    /**
     * The left edge of the playing area
     */
    public static final int LEFT = 55;
    /**
     * The bottom edge of the playing area
     */
    public static final int BOTTOM = 30;
    /**
     * The width of the playing area in squares
     */
    public static final int WIDTH = 10;
    /**
     * The height of the playing area in squares
     */
    public static final int HEIGHT = 20;

    /**
     * The scores for destroying N (0..4) lines at once.
     */
    public static final int[] SCORE_PER_LINE = new int[] { 0, 40, 100, 300, 1200 };

    private static final int[] cyan = new int[] { -1, 0, -2, 0, 1, 0, 0, 1, 0, -1, 0, -2, -1, 0,
        -2, 0, 1, 0, 0, 1, 0, -1, 0, -2 };
    private static final int[] yellow = new int[] { -1, 0, -1, -1, 0, -1, -1, 0, -1, -1, 0, -1, -1,
        0, -1, -1, 0, -1, -1, 0, -1, -1, 0, -1 };
    private static final int[] green = new int[] { 1, 0, 0, -1, -1, -1, 0, 1, 1, 0, 1, -1, 1, 0, 0,
        -1, -1, -1, 0, 1, 1, 0, 1, -1 };
    private static final int[] red = new int[] { -1, 0, 0, -1, 1, -1, 1, 0, 1, 1, 0, -1, -1, 0, 0,
        -1, 1, -1, 1, 0, 1, 1, 0, -1 };
    private static final int[] orange = new int[] { -1, 0, -1, -1, 1, 0, 0, -1, 1, -1, 0, 1, 1, 0,
        1, 1, -1, 0, 0, 1, -1, 1, 0, -1 };
    private static final int[] blue = new int[] { -1, 0, 1, 0, 1, -1, 0, -1, 0, 1, 1, 1, 1, 0, -1,
        0, -1, 1, 0, 1, 0, -1, -1, -1 };
    private static final int[] purple = new int[] { -1, 0, 1, 0, 0, -1, 0, 1, 0, -1, 1, 0, -1, 0,
        1, 0, 0, 1, 0, -1, 0, 1, -1, 0 };

    /**
     * The data for each of the tetris shapes. Each array holds a list of offsets from the tetris shapes central square. The x and y
     * coordinates are mushed together into a single array i.e. x1a,y1a, x1b,y1b, x1c,y1c, x2a,y2a, x2b,y2b x2c, y2c, etc. The central
     * square isn't included, so there are (x,y) pairs in groups of three (as a tetris shape has four squares), and there are 4 lots of
     * these, one for each possible rotation.
     */
    private static final int[][] data = new int[][] { cyan, yellow, green, red, orange, blue,
        purple };

    /**
     * The costume names for each of the squares
     */
    private static final String[] names = new String[] { "cyan", "yellow", "green", "red",
        "orange", "blue", "purple" };

    /**
     * An array of size WIDTH +2, HEIGHT + 2, representing the pieces fixed on the tetris playing area. It does not hold the piece currently
     * falling, only the pieces that have already fallen. Each entry in the grid is null if it is empty, or contains an Actor. The actor is
     * how the pieces are visible on the screen.
     */
    public Actor[][] grid;

    boolean playing = false;

    Timer escapeTimer;

    /**
     * A countdown timer, which regulates the speed of the game. The speed is changed in setLevel, which is increased by one for each ten
     * lines removed.
     */
    Timer timer;

    /**
     * The currently falling piece.
     */
    Piece piece;

    /**
     * The current level (1 to 10)
     */
    public int level = 1;

    /**
     * The current score.
     */
    public int score = 0;

    /**
     * Total number of lines removed
     */
    public int completedLines;

    private Input inputLeft;

    private Input inputRight;

    private Input inputRotate;

    private Input inputDrop;

    private Input inputExit;

    private Input inputPlay;

    private Input inputEditor;

    private Input inputDebug;

    @Override
    public void onActivate()
    {
        super.onActivate();
        this.level = getStartingLevel();

        this.inputLeft = Input.find("left");
        this.inputRight = Input.find("right");
        this.inputRotate = Input.find("rotate");
        this.inputDrop = Input.find("drop");
        this.inputExit = Input.find("exit");
        this.inputPlay = Input.find("play");
        this.inputEditor = Input.find("editor");
        this.inputDebug = Input.find("debug");
    }

    @Override
    public void onMessage( String message )
    {
        if (message.equals("quit")) {
            this.game.end();
        }
        if (message.equals("play")) {
            this.play();
        }
        if (message.equals("levelUp")) {
            this.chooseLevel(getStartingLevel() + 1);
        }
        if (message.equals("levelDown")) {
            this.chooseLevel(getStartingLevel() - 1);
        }
    }

    @Override
    public void tick()
    {
        super.tick();

        if (this.escapeTimer != null) {
            if (this.escapeTimer.isFinished()) {
                this.game.addKeyListener(this); // See removeKeyListener in onKeyDown
                this.level = getStartingLevel();
                getGame().startScene("menu");
                this.escapeTimer = null;
            }
            return;
        }

        if (this.playing) {
            if (this.timer.isFinished()) {
                this.timer.reset();
                if (this.piece == null) {
                    createNextPiece();
                } else {
                    if (this.piece.down()) {
                        this.piece.fix();
                        createNextPiece();
                    }
                }
            }
        }
    }

    private void createNextPiece()
    {
        int n = new Random().nextInt(names.length);
        this.piece = new Piece(n, 5, 20);
        if (this.piece.isOverlapping()) {
            gameOver();
            setHighScore(this.score);
        }
    }

    @Override
    public void onKeyDown( KeyboardEvent ke )
    {
        if (ke.isReleased()) {
            return;
        }

        if (this.inputEditor.matches(ke)) {
            this.game.startEditor();
        }

        if (this.inputDebug.matches(ke)) {
            debug();
        }

        if ((ke.symbol >= Keys.KEY_0) && (ke.symbol <= Keys.KEY_9)) {
            chooseLevel(ke.symbol - Keys.KEY_0);
        }

        if ((this.inputExit.matches(ke)) && (this.game.getSceneName().equals("main"))) {
            gameOver();
            this.game.resources.getSound("shatter").getSound().play();
            for (int x = 1; x <= WIDTH; x++) {
                for (int y = 1; y <= HEIGHT; y++) {
                    Actor actor = this.grid[x][y];
                    if (actor != null) {
                        kill(actor);
                        this.grid[x][y] = null;
                    }
                }
            }
            // Ignore all key presses will the escape time has elapsed.
            this.game.removeKeyListener(this);
            this.escapeTimer = Timer.createTimerSeconds(2);
        }

        if (!this.playing) {
            if (this.inputPlay.matches(ke)) {
                onMessage("play");
            }
        } else {
            if (this.piece != null) {
                if (this.inputDrop.matches(ke)) {
                    this.piece.drop();
                }
                if (this.inputRotate.matches(ke)) {
                    this.piece.rotate();
                }
                if (this.inputLeft.matches(ke)) {
                    this.piece.slide(-1);
                }
                if (this.inputRight.matches(ke)) {
                    this.piece.slide(1);
                }
            }
        }
    }

    private void chooseLevel( int level )
    {
        if (level == 0) {
            level = 10;
        }
        if ((level > 10) || (level < 1)) {
            return;
        }

        this.game.getPreferences().putInt("startingLevel", level);
        if (!this.playing || this.level < level) {
            setLevel(level);
        }
    }

    public int getStartingLevel()
    {
        return this.game.getPreferences().getInt("startingLevel", 1);
    }

    public void clearLines()
    {
        int destroyed = 0;
        for (int y = 1; y <= HEIGHT; y++) {
            if (isLineFull(y)) {
                destroyed += 1;
                completedLine();

                for (int x = 1; x <= WIDTH; x++) {
                    kill(this.grid[x][y]);
                    this.grid[x][y] = null;
                }
            } else {
                for (int x = 1; x <= WIDTH; x++) {
                    if (destroyed > 0) {
                        if (this.grid[x][y] != null) {
                            moveDown(this.grid[x][y], destroyed);
                        }
                        this.grid[x][y - destroyed] = this.grid[x][y];
                        this.grid[x][y] = null;
                    }
                }
            }
        }

        Sound sound = null;
        if (destroyed == 4) {
            sound = this.game.resources.getSound("explode").getSound();
        } else if (destroyed == 0) {
            sound = this.game.resources.getSound("pop").getSound();
        } else {
            sound = this.game.resources.getSound("shatter").getSound();
        }
        if (sound != null) {
            sound.play();
        }

        this.score += SCORE_PER_LINE[destroyed] * this.level;
    }

    private void completedLine()
    {
        this.completedLines += 1;
        int level = 0;
        if (this.completedLines <= 0) {
            level = 1;
        } else if ((this.completedLines >= 1) && (this.completedLines <= 90)) {
            level = 1 + ((this.completedLines) / 10);
        } else if (this.completedLines >= 90) {
            level = 10;
        }
        if (level > this.level) {
            setLevel(level);
        }
    }

    public void setLevel( int level )
    {
        this.level = level;
        double delay = (11 - this.level) * 60.0 / 1000.0;
        this.timer = Timer.createTimerSeconds(delay);
    }

    public boolean isLineFull( int y )
    {
        for (int x = 1; x <= WIDTH; x++) {
            if (this.grid[x][y] == null) {
                return false;
            }
        }
        return true;
    }

    private void kill( Actor actor )
    {
        new Fragments()
            .pieces(5)
            .createPoses(actor);

        new ExplosionBuilder(actor)
            .projectiles(5)
            .speed(2, 4)
            .fade(3)
            .pose("fragment")
            .create();

        actor.kill();
    }

    private void moveDown( Actor actor, int lines )
    {
        actor.event("moveDown" + lines, null, AnimationEvent.SEQUENCE);
    }

    private void gameOver()
    {
        if (this.piece != null) {
            for (Actor actor : this.piece.actors) {
                kill(actor);
            }

            this.game.resources.getSound("shatter").getSound().play();
        }
        this.playing = false;
        this.piece = null;
        this.timer = null;
    }

    private void play()
    {
        this.score = 0;
        this.completedLines = 0;
        setLevel(getStartingLevel());
        Actor dummy = new Actor(this.game.resources.getCostume(names[0]));
        dummy.setRole(new PlainRole());

        this.grid = new Actor[WIDTH + 2][HEIGHT + 2];
        for (int x = 0; x < WIDTH + 2; x++) {
            for (int y = 0; y < HEIGHT + 2; y++) {
                if ((x == 0) || (y == 0) || (x == WIDTH + 1)) {
                    this.grid[x][y] = dummy;
                } else {
                    this.grid[x][y] = null;
                }
            }
        }
        getGame().startScene("main");
        this.playing = true;
    }

    public int getScore()
    {
        return this.score;
    }

    public int getHighScore()
    {
        return this.game.getPreferences().getInt("highScore", 0);
    }

    public void setHighScore( int value )
    {
        if (value > getHighScore()) {
            this.game.getPreferences().putInt("highScore", value);
        }
    }

    public void debug()
    {
        System.err.println();
        for (int y = HEIGHT + 1; y >= 0; y--) {
            for (int x = 0; x < WIDTH + 2; x++) {
                System.out.print(this.grid[x][y] == null ? " " : "X");
            }
            System.err.println();
        }
        System.err.println();
    }

    public class Piece
    {
        static final int ROTATIONS = 4;
        static final int PIECES = 4;

        int centerX;
        int centerY;
        int rotation = 0;

        Point[][] places;

        Actor[] actors;

        public Piece( int n, int x, int y )
        {

            this.centerX = x;
            this.centerY = y;

            this.places = new Point[ROTATIONS][PIECES];
            for (int r = 0; r < 4; r++) {
                this.places[r][0] = new Point(0, 0);
                for (int s = 0; s < 3; s++) {
                    int i = r * 6 + s * 2;
                    this.places[r][s + 1] = new Point(data[n][i], data[n][i + 1]);
                }
            }
            this.actors = new Actor[PIECES];
            for (int i = 0; i < PIECES; i++) {
                this.actors[i] = new Actor(Tetra.this.game.resources.getCostume(names[n]));
                this.actors[i].setRole(new PlainRole());
                ((ZOrderStage) Itchy.getGame().getLayout().findStage("main")).addTop(this.actors[i]);
            }
            update();

        }

        public void rotate()
        {
            this.rotation = (this.rotation + 1) % 4;
            if (isOverlapping()) {
                this.rotation = (this.rotation + 3) % 4;
            } else {
                this.update();
            }
        }

        public void slide( int dx )
        {
            this.centerX += dx;
            if (isOverlapping()) {
                this.centerX -= dx;
            }
            update();
        }

        public void drop()
        {
            int dropped = 0;
            while (!down()) {
                dropped += 1;
            }
            Tetra.this.score += dropped;
            update();
        }

        public boolean down()
        {
            this.centerY -= 1;
            if (isOverlapping()) {
                this.centerY += 1;
                update();
                return true;
            } else {
                update();
                return false;
            }
        }

        public void fix()
        {
            for (int i = 0; i < PIECES; i++) {
                Point point = this.places[this.rotation][i];
                int x = point.x + this.centerX;
                int y = point.y + this.centerY;
                Tetra.this.grid[x][y] = this.actors[i];
            }
            clearLines();
        }

        public boolean isOverlapping()
        {
            for (int i = 0; i < PIECES; i++) {
                Point point = this.places[this.rotation][i];
                int x = point.x + this.centerX;
                int y = point.y + this.centerY;
                if (Tetra.this.grid[x][y] != null) {
                    return true;
                }
            }
            return false;
        }

        public void update()
        {
            for (int i = 0; i < PIECES; i++) {
                Actor actor = this.actors[i];
                Point point = this.places[this.rotation][i];
                actor.moveTo(LEFT + (this.centerX + point.x) * SCALE, BOTTOM +
                    (this.centerY + point.y) * SCALE);
            }
        }

    }

    public static void main( String argv[] ) throws Exception
    {
        Launcher.main(new String[] { "tetra" });
    }

}