public class Constants {

    /**
     * The width of the Scene2D stage. The bigger this value is, the greater
     * the space is going to be for child elements of this screen. However,
     * the smallest everything is going to be seen, specially in small screen.s
     */
    public static final int VIEWPORT_WIDTH = 480;

    /**
     * The height of the Scene2D stage. The bigger this value is, the greater
     * the space is going to be for child elements of this screen. However,
     * the smallest everything is going to be seen, specially in small screens.
     */
    public static final int VIEWPORT_HEIGHT = 768;

    /**
     * The padding for the table in every stage.
     */
    public static final int STAGE_PADDING = 20;

    public static final int SECONDS = 30;
} public class RectballGame extends StateBasedGame {

    /** The Android activity that is currently displaying this game instance. */
    private final AndroidLauncher context;

    /** The game state is used during a game in order to pause and serialize it. */
    private final GameState currentGame;

    /** The asset manager holds the resources required during the game. */
    public AssetManager manager;

    /** The sound player manages the sound effects during the game. */
    public SoundPlayer player;

    /** Skin used by Scene2D. */
    private RectballSkin uiSkin;

    /** Contains the bubbles textures, depending on the application settings. */
    private TextureAtlas ballAtlas;

    /** Whether the game is restoring state from an Android kill or not. */
    private boolean restoredState;

    /** Batch instance in use by the game. */
    private Batch batch;

    public RectballGame(AndroidLauncher context) {
        this.context = context;
        this.currentGame = new GameState();
        this.restoredState = false;
    }

    public RectballGame(AndroidLauncher context, GameState state) {
        this.context = context;
        this.currentGame = state;
        this.restoredState = true;
    }

    @Override
    public void create() {
        if (BuildConfig.FINE_DEBUG) {
            Gdx.app.setLogLevel(Application.LOG_DEBUG);
        }

        // Android enables dithering by default on some phones. Disable it for higher quality.
        Gdx.gl.glDisable(GL20.GL_DITHER);

        addScreen(new GameScreen(this));
        addScreen(new GameOverScreen(this));
        addScreen(new MainMenuScreen(this));
        addScreen(new SettingsScreen(this));
        addScreen(new LoadingScreen(this));
        addScreen(new LoadingBackScreen(this));
        addScreen(new StatisticsScreen(this));
        addScreen(new AboutScreen(this));
        addScreen(new TutorialScreen(this));

        batch = new SpriteBatch();
        manager = AssetManagerBuilder.INSTANCE.build();

        if (!restoredState) {
            getScreen(Screens.LOADING).load();
            setScreen(getScreen(Screens.LOADING));
        } else {
            getScreen(Screens.LOADING_BACK).load();
            setScreen(getScreen(Screens.LOADING_BACK));
        }
    }

    public void finishLoading() {
        // These resources cannot be initialized until the AssetManager finishes loading.
        player = new SoundPlayer(this);
        uiSkin = new RectballSkin(this);
        updateBallAtlas();

        for (AbstractScreen screen : getAllScreens()) {
            screen.load();
        }

        // Enter next screen.
        pushScreen(Screens.MAIN_MENU);
        if (restoredState) {
            // If playing, enter the game screen as well. Keep MAIN_MENU in the stack.
            pushScreen(Screens.GAME);
        }
    }

    public void updateBallAtlas() {
        boolean isColorblind = context.getSettings().getPreferences().getBoolean(SettingsManager.TAG_ENABLE_COLORBLIND, false);
        String ballsTexture = isColorblind ? "board/colorblind.png" : "board/normal.png";
        Texture balls = manager.get(ballsTexture);
        TextureRegion[][] regions = TextureRegion.split(balls, 256, 256);
        ballAtlas = new TextureAtlas();
        ballAtlas.addRegion("ball_red", regions[0][0]);
        ballAtlas.addRegion("ball_yellow", regions[0][1]);
        ballAtlas.addRegion("ball_blue", regions[1][0]);
        ballAtlas.addRegion("ball_green", regions[1][1]);
        ballAtlas.addRegion("ball_gray", regions[1][2]);
    }

    @Override
    public void dispose() {
        manager.dispose();
    }

    public RectballSkin getSkin() {
        return uiSkin;
    }

    public GameState getState() {
        return currentGame;
    }

    public boolean isRestoredState() {
        return restoredState;
    }

    public Batch getBatch() {
        return batch;
    }

    public AndroidLauncher getContext() {
        return context;
    }

    public TextureAtlas getBallAtlas() {
        return ballAtlas;
    }

    public void setRestoredState(boolean restoredState) {
        this.restoredState = restoredState;
    }

    @Override
    public int getEmptyStackScreen() {
        return Screens.MAIN_MENU;
    }
} public static enum SoundCode {
    GAME_OVER("sound/game_over.ogg"),
    SELECT("sound/select.ogg"),
    UNSELECT("sound/unselect.ogg"),
    SUCCESS("sound/success.ogg"),
    FAIL("sound/fail.ogg"),
    PERFECT("sound/perfect.ogg");

    String internalPath;
    SoundCode(String internalPath) {
        this.internalPath = internalPath;
    }
} public class Ball {

    private int x, y;

    private BallColor color = null;

    public Ball() {

    }

    public Ball(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public BallColor getColor() {
        return color;
    }

    public void setColor(BallColor color) {
        this.color = color;
    }
} public enum BallColor {
    BLUE,
    GREEN,
    RED,
    YELLOW
} public class Bounds {

    public final int minX;
    public final int minY;
    public final int maxX;
    public final int maxY;

    public Bounds() {
        // No arg constructor.
        this.minX = this.minY = this.maxX = this.maxY = 0;
    }

    public Bounds(int minX, int minY, int maxX, int maxY) {
        this.minX = minX;
        this.minY = minY;
        this.maxX = maxX;
        this.maxY = maxY;
    }

    public Bounds(Coordinate c1, Coordinate c2) {
        this.minX = Math.min(c1.x, c2.x);
        this.maxX = Math.max(c1.x, c1.x);
        this.minY = Math.min(c1.y, c2.y);
        this.maxY = Math.max(c1.y, c2.y);
    }

    public int rows() {
        return maxY - minY + 1;
    }

    public int cols() {
        return maxX - minX + 1;
    }

    public static Bounds fromBallList(List<Ball> balls) {
        int minX, minY, maxX, maxY;
        minX = minY = Integer.MAX_VALUE;
        maxX = maxY = Integer.MIN_VALUE;
        for (Ball ball : balls) {
            minX = Math.min(minX, ball.getX());
            minY = Math.min(minY, ball.getY());
            maxX = Math.max(maxX, ball.getX());
            maxY = Math.max(maxY, ball.getY());
        }
        return new Bounds(minX, minY, maxX, maxY);
    }

    public boolean inBounds(int x, int y) {
        return (x >= minX && x <= maxX) && (y >= minY && y <= maxY);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Bounds bounds = (Bounds) o;

        return minX == bounds.minX && minY == bounds.minY && maxX == bounds.maxX && maxY == bounds.maxY;

    }

    @Override
    public int hashCode() {
        int result = minX;
        result = 31 * result + minY;
        result = 31 * result + maxX;
        result = 31 * result + maxY;
        return result;
    }
} public class CombinationFinder {

    private static CombinationFinder instance = null;

    private Board board;

    private int width, height;

    private final List<Bounds> bounds;

    private CombinationFinder() {
        bounds = new ArrayList<>();
    }

    public static CombinationFinder create(Board board) {
        if (instance == null)
            instance = new CombinationFinder();
        instance.board = board;
        instance.width = instance.height = board.getSize();
        instance.calculateCombinations();
        return instance;
    }

    private void calculateCombinations() {
        if (!bounds.isEmpty())
            bounds.clear();
        for (int y = 0; y < height - 1; y++) {
            for (int x = 0; x < width - 1; x++) {
                BallColor refColor = board.getBall(x, y).getColor();

                // Iterate through all the balls in the same row
                for (int row = x + 1; row < width; row++) {
                    // Skip through this ball if it's not of the same color.
                    if (board.getBall(row, y).getColor() != refColor)
                        continue;
                    // Ok, it is. Let's see if we can find two balls in the
                    // same columns as our ref and the one we just found with
                    // the same color.
                    for (int col = y + 1; col < height; col++) {
                        if (board.getBall(x, col).getColor() == refColor
                            && board.getBall(row, col).getColor() == refColor)
                        {
                            int bx = Math.min(x, row);
                            int by = Math.min(y, col);
                            int BX = Math.max(x, row);
                            int BY = Math.max(y, col);
                            bounds.add(new Bounds(bx, by, BX, BY));
                        }
                    }
                }
            }
        }
    }

    public List<Bounds> getPossibleBounds() {
        return Collections.unmodifiableList(bounds);
    }

    public Bounds getCombination() {
        return bounds.isEmpty() ? null : bounds.get(0);
    }

    public Bounds getBestCombination() {
        if (bounds.isEmpty())
            return null;
        Bounds maxBounds = bounds.get(0);
        for (Bounds thisBounds : bounds)
            if (getWeightForCombination(thisBounds) > getWeightForCombination(maxBounds))
                maxBounds = thisBounds;
        return maxBounds;
    }

    public Bounds getWorstCombination() {
        if (bounds.isEmpty())
            return null;
        Bounds minBounds = bounds.get(0);
        for (Bounds thisBounds : bounds)
            if (getWeightForCombination(thisBounds) < getWeightForCombination(minBounds))
                minBounds = thisBounds;
        return minBounds;
    }

    public boolean areThereCombinations() {
        return !(bounds.isEmpty());
    }

    /**
     * Calculate the weight of the provided combination. Please note that the
     * weight is not the same as the score that the user receives from this
     * combination. Weight is a local comparing system used by the
     * CombinationFinder to decide what is the greatest combination can be
     * offered to the user.
     *
     * @since 0.4
     *
     * @param bounds  the bounds whose weight we want to know.
     * @return  the weight for this combination
     */
    private static int getWeightForCombination(Bounds bounds) {
        int cols = bounds.maxX - bounds.minX + 1;
        int rows = bounds.maxY - bounds.minY + 1;
        return cols * rows;
    }
} public class Coordinate {

    public final int x;
    public final int y;

    public Coordinate() {
        this.x = 0;
        this.y = 0;
    }

    public Coordinate(int x, int y) {
        this.x = x;
        this.y = y;
    }

} public class ScoreCalculator {

    private CombinationFinder finder;

    private Bounds bounds;

    private Board board;

    public ScoreCalculator(Board board, Bounds bounds) {
        this.bounds = bounds;
        this.board = board;
        this.finder = CombinationFinder.create(board);
    }

    public int calculate() {
        int rows = bounds.maxY - bounds.minY + 1;
        int cols = bounds.maxX - bounds.minX + 1;
        int score = rows * cols;

        // Is this the best combination of all the possible ones?
        if (finder.getBestCombination().equals(bounds)) {
            int possibleCombinations = finder.getPossibleBounds().size();
            switch (possibleCombinations) {
                case 1:
                    // For a single one, no bonus is given.
                    break;
                case 2:
                    score = Math.round((float) score * 1.1f);
                    break;
                case 3:
                    score = Math.round((float) score * 1.2f);
                    break;
                default:
                    score = Math.round((float) score * 1.3f);
                    break;
            }
        }

        // In fact, does this combination use the entire board?
        if (bounds.minX == 0 && bounds.minY == 0
                && bounds.maxX == board.getSize() - 1 && bounds.maxY == board.getSize() - 1) {
            // Yes, make that another bonus.
            score = Math.round((float) score * 1.5f);
        }

        return score;
    }

} public class BoardActor extends Table {

    private final BallActor[][] actors;

    private final Set<BallActor> selection = new HashSet<>();

    private final List<BallSelectionListener> subscribers = new ArrayList<>();

    private final Board board;

    private Skin skin;

    public BoardActor(TextureAtlas atlas, Skin skin, Board board) {
        this.board = board;
        this.skin = skin;
        this.actors = new BallActor[board.getSize()][board.getSize()];

        for (int y = board.getSize() - 1; y >= 0; y--) {
            for (int x = 0; x < board.getSize(); x++) {
                actors[x][y] = new BallActor(board.getBall(x, y), atlas);
                add(actors[x][y]).pad(4, 2, 4, 2);
            }
            row();
        }

        // Add an input handle to select items in the board.
        addListener(new DragBoardSelectionListener(this));
    }

    public void syncColors() {
        for (BallActor[] row : actors) {
            for (BallActor ball : row) {
                ball.syncColor();
            }
        }
    }

    public Label showPartialScore(int score, Bounds bounds, boolean special, boolean usedHelp) {
        // Get the graphical center of the region.
        BallActor bottomLeftBall = getBall(bounds.minX, bounds.minY);
        BallActor upperRightBall = getBall(bounds.maxX, bounds.maxY);
        float minX = bottomLeftBall.getX();
        float maxX = upperRightBall.getX() + upperRightBall.getWidth();
        float minY = bottomLeftBall.getY();
        float maxY = upperRightBall.getY() + upperRightBall.getHeight();
        float centerX = (minX + maxX) / 2;
        float centerY = (minY + maxY) / 2;

        // Build the label used to present the score.
        Label label = new Label("+" + score, skin, "monospace");
        label.setFontScale(7f);
        label.setSize(140, 70);
        label.setAlignment(Align.center);
        label.setPosition(centerX - label.getWidth() / 2, centerY - label.getHeight() / 2);
        label.addAction(Actions.sequence(
                Actions.parallel(
                        Actions.moveBy(0, 80, 0.5f),
                        Actions.sequence(
                                Actions.delay(0.25f),
                                Actions.fadeOut(0.25f))
                        ),
                Actions.removeActor()
        ));
        if (usedHelp) {
            label.setColor(Color.RED);
        } else if (special) {
            label.setColor(Color.CYAN);
        }

        return label;
    }

    public BallActor getBall(int x, int y) {
        return actors[x][y];
    }

    public void setColoured(boolean coloured) {
        for (BallActor[] actor : actors) {
            for (BallActor ballActor : actor) {
                ballActor.setColoured(coloured);
            }
        }
    }

    public void select(Bounds bounds) {
        selection.clear();
        selection.addAll(Arrays.asList(
                actors[bounds.minX][bounds.minY],
                actors[bounds.minX][bounds.maxY],
                actors[bounds.maxX][bounds.minY],
                actors[bounds.maxX][bounds.maxY]
        ));
        if (selection.size() == 4) {
            finishSelection();
        } else {
            for (BallSelectionListener subscriber : subscribers)
                subscriber.onSelectionFailed(new ArrayList<>(selection));
            selection.clear();
        }
    }

    private List<Ball> getModelBalls() {
        List<Ball> ballSelection = new ArrayList<>();
        for(BallActor ball : selection) {
            ballSelection.add(ball.getBall());
        }
        return ballSelection;
    }

    /**
     * This method is executed when the selection is complete. It asserts the selection is valid and notifies the game
     * so that it can trigger the proper animations depending on whether it has been successful or not.
     */
    private void finishSelection() {
        if (selection.size() != 4) throw new IllegalStateException("Select 4 balls first");

        /* Check whether the selection is valid and provide the valid message to our subscribers. */
        if (board.selection(getModelBalls())) {
            for (BallSelectionListener subscriber : subscribers) {
                subscriber.onSelectionSucceeded(new ArrayList<>(selection));
            }
        } else {
            for (BallSelectionListener subscriber : subscribers) {
                subscriber.onSelectionFailed(new ArrayList<>(selection));
            }
        }

        selection.clear();
    }

    /**
     * Clear the selection.
     */
    public void clearSelection() {
        List<BallActor> unselectedBalls = new ArrayList<>(selection);
        for (BallSelectionListener subscriber : subscribers)
            subscriber.onSelectionCleared(unselectedBalls);
        selection.clear();
    }

    /**
     * Attach a new subscriber to this board. The subscriber will receive
     * notifications about selection events happened in this board.
     *
     * @param subscriber subscriber that wants to be notified
     */
    public void addSubscriber(BallSelectionListener subscriber) {
        subscribers.add(subscriber);
    }

    public void clearSubscribers() {
        subscribers.clear();
    }

    /**
     * Resize some balls from this board.
     *
     * @param bounds bounding box for the balls that will be resized.
     * @param scale  the final scale for these balls.
     * @param time   the time that the animation will last for
     * @return the action that will animate these balls
     */
    private Action resizeBalls(final Bounds bounds, final float scale, final float time) {
        Action scalingAction = Actions.run(new Runnable() {
            @Override
            public void run() {
                for (int x = bounds.minX; x <= bounds.maxX; x++) {
                    for (int y = bounds.minY; y <= bounds.maxY; y++) {
                        BallActor scaledBall = actors[x][y];
                        scaledBall.addAction(Actions.scaleTo(scale, scale, time));
                    }
                }
            }
        });
        return Actions.sequence(scalingAction, Actions.delay(time));
    }

    public Action hideBoard() {
        Bounds boardBounds = new Bounds(0, 0, board.getSize() - 1, board.getSize() - 1);
        return resizeBalls(boardBounds, 0, 0.15f);
    }

    public Action hideRegion(Bounds bounds) {
        return resizeBalls(bounds, 0, 0.15f);
    }

    public Action showRegion(Bounds bounds) {
        return resizeBalls(bounds, 1, 0.15f);
    }

    public Action shake(float shakiness, int times, float speed) {
        return shake(new Bounds(0, 0, board.getSize() - 1, board.getSize() - 1), shakiness, times, speed);
    }

    public Action shake(final Bounds region, final float shakiness, final int times, final float speed) {
        Action shakingAction = Actions.run(new Runnable() {
            @Override
            public void run() {
                for (int y = region.minY; y <= region.maxY; y++)
                    for (int x = region.minX; x <= region.maxX; x++)
                        actors[x][y].addAction(Actions.repeat(times, Actions.sequence(
                                                                                             Actions.moveBy(shakiness / 2, 0, speed / 2),
                                                                                             Actions.moveBy(-shakiness, 0, speed),
                                                                                             Actions.moveBy(shakiness / 2, 0, speed / 2)
                        )));
            }
        });
        return Actions.sequence(shakingAction, Actions.delay(times * speed));
    }
} public interface ScoreListener {

    void onScoreGoNuts();

}
 public interface TimerCallback {

    /**
     * This is the method that receives the message that the time is over.
     */
    void onTimeOut();

} public interface BallSelectionListener {

    /**
     * This event is triggered when a ball has been selected on a board.
     *
     * @param ball ball that has been selected.
     */
    void onBallSelected(BallActor ball);

    /**
     * This event is triggered when a ball has been unselected from a board.
     *
     * @param ball ball that has been unselected.
     */
    void onBallUnselected(BallActor ball);

    /**
     * This event is triggered when a selection is made and the selection has
     * already be checked to be valid. All the selected balls form a rectangle
     * whose four corners are made of the same color.
     *
     * @param selection balls that form this selection.
     */
    void onSelectionSucceeded(List<BallActor> selection);

    /**
     * This event is triggered when a selection is made and the selection is
     * checked not to be valid. Any of the conditions required to be a valid
     * selection is not happening.
     *
     * @param selection balls that don't form this selection.
     */
    void onSelectionFailed(List<BallActor> selection);

    /**
     * This event is triggered when the board is forced to unselect every ball.
     * This happens, for instance, when the game is over, so that no selected
     * balls remain on the screen while the animations start.
     *
     * @param selection the list of balls that has been unselected.
     */
    void onSelectionCleared(List<BallActor> selection);
} public class ConfirmDialog extends CommonDialog {

    private ConfirmCallback callback = null;

    public ConfirmDialog(Skin skin, String text, String okText, String cancelText) {
        super(skin, text);
        button(okText, "ok").button(cancelText, "cancel");

    }

    public void setCallback(ConfirmCallback callback) {
        this.callback = callback;
    }

    @Override
    protected void result(Object object) {
        if (callback != null) {
            if (object.equals("ok")) {
                callback.ok();
            } else if (object.equals("cancel")) {
                callback.cancel();
            }
        }

        cancel();
        hide(null);
    }

    /**
     * This is the callback class that will subscribe to events happening in
     * this confirmation dialog class.
     */
    public interface ConfirmCallback {

        /**
         * This event is triggered when the OK button is pressed.
         */
        void ok();

        /**
         * This event is triggered when the CANCEL button is pressed.
         */
        void cancel();
    }
}
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Touchable;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.Dialog;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import es.danirod.rectball.Constants;
import es.danirod.rectball.RectballGame;
import es.danirod.rectball.SoundPlayer.SoundCode;
import es.danirod.rectball.android.R;
import es.danirod.rectball.android.settings.SettingsManager;
import es.danirod.rectball.model.Ball;
import es.danirod.rectball.model.BallColor;
import es.danirod.rectball.model.Bounds;
import es.danirod.rectball.model.CombinationFinder;
import es.danirod.rectball.model.Coordinate;
import es.danirod.rectball.model.ScoreCalculator;
import es.danirod.rectball.scene2d.game.BallActor;
import es.danirod.rectball.scene2d.game.BoardActor;
import es.danirod.rectball.scene2d.game.Hud;
import es.danirod.rectball.scene2d.game.ScoreActor.ScoreListener;
import es.danirod.rectball.scene2d.game.TimerActor.TimerCallback;
import es.danirod.rectball.scene2d.listeners.BallSelectionListener;
import es.danirod.rectball.scene2d.ui.ConfirmDialog;
                // Leave
                game.player.playSound(SoundCode.SELECT);
                showLeaveDialog();
            }
        });

        dialog.show(getStage());
        askingLeave = true;
    }

    @Override
    public int getID() {
        return Screens.GAME;
    }

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

        // Reset data
        if (!game.isRestoredState()) {
            game.getState().reset();
        } else {
            game.setRestoredState(false);
            if (game.getState().isTimeout()) {
                game.pushScreen(Screens.GAME_OVER);
            }
        }

        // The player is playing
        game.getState().setPlaying(true);
        hud.getScore().setValue(game.getState().getScore());
        hud.getTimer().setSeconds(game.getState().getRemainingTime());

        paused = running = askingLeave = false;

        if (!game.getState().isCountdownFinished()) {
            countdown(2, new Runnable() {
                @Override
                public void run() {
                    // Report that a game is starting.
                    Map<Integer, String> dimensions = new HashMap<>();
                    dimensions.put(1, game.getContext().getSettings().getPreferences().getBoolean(SettingsManager.TAG_ENABLE_SOUND, false) ? "Yes" : "No");
                    dimensions.put(2, game.getContext().getSettings().getPreferences().getBoolean(SettingsManager.TAG_ENABLE_COLORBLIND, false) ? "Yes" : "No");
                    dimensions.put(3, game.getContext().getSettings().getPreferences().getBoolean(SettingsManager.TAG_ENABLE_FULLSCREEN, false) ? "Yes" : "No");
                    game.getState().setCountdownFinished(true);

                    // Start the game unless the user is leaving or is paused.
                    if (!paused && !askingLeave) {
                        running = true;
                        board.setColoured(true);
                        hud.getTimer().setRunning(true);
                        board.setTouchable(Touchable.enabled);
                        game.player.playSound(SoundCode.SUCCESS);
                    }
                }
            });
        } else {
            pauseGame();
        }
    }

    /**
     * Create a countdown in the screen lasting for the amount of seconds given.
     * When the countdown reaches 0, the code provided in the runnable will
     * be executed as a callback.
     *
     * @param seconds how many seconds should the countdown be displayed.
     */
    private void countdown(final int seconds, final Runnable after) {
        // Since this is a recursive function, avoid the case where you pass
        // a number of seconds that might trigger an infinite loop.
        if (seconds <= 0) {
            return;
        }

        // Create the label that will contain this number
        String number = Integer.toString(seconds);
        final Label label = new Label(number, game.getSkin(), "monospace");
        label.setFontScale(5f);
        label.setSize(150, 150);
        label.setAlignment(Align.center);
        label.setPosition(
                (getStage().getWidth() - label.getWidth()) / 2,
                (getStage().getHeight() - label.getHeight()) / 2);

        // Add the label to the stage and play a sound to notify the user.
        getStage().addActor(label);
        game.player.playSound(SoundCode.SELECT);

        label.addAction(Actions.sequence(
                Actions.parallel(Actions.moveBy(0, 80, 1f)),

                // After the animation, decide. If the countdown hasn't finished
                // yet, run another countdown with 1 second less.
                Actions.run(new Runnable() {
                    @Override
                    public void run() {
                        label.remove();
                        if (seconds > 1) {
                            countdown(seconds - 1, after);
                        } else {
                            after.run();
                        }
                    }
                })
        ));
    }

    /**
     * Generate new colors. This method is executed by the callback action
     * when the player selects a valid rectangle. The purpose of this method
     * is to regenerate the board and apply any required checks to make sure
     * that the game doesn't enter in an infinite loop.
     *
     * @param bounds the bounds that have to be regenerated.
     */
    private void generate(Bounds bounds) {
        // Generate new balls;
        game.getState().getBoard().randomize(
                new Coordinate(bounds.minX, bounds.minY),