package io.github.lonamiwebs.klooni.game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import io.github.lonamiwebs.klooni.Klooni;
import io.github.lonamiwebs.klooni.serializer.BinSerializable;
// Represents the on screen board, with all the put cells
// and functions to determine when it is game over given a PieceHolder
public class Board implements BinSerializable {
//region Members
public final int cellCount;
public float cellSize;
private Cell[][] cells;
final Vector2 pos;
private final Sound stripClearSound;
// Used to animate cleared cells vanishing
private final Vector2 lastPutPiecePos;
//endregion
//region Constructor
public Board(final GameLayout layout, int cellCount) {
this.cellCount = cellCount;
stripClearSound = Gdx.audio.newSound(Gdx.files.internal("sound/strip_clear.mp3"));
lastPutPiecePos = new Vector2();
pos = new Vector2();
// Cell size depends on the layout to be updated first
layout.update(this);
cells = new Cell[this.cellCount][this.cellCount];
for (int i = 0; i < this.cellCount; ++i) {
for (int j = 0; j < this.cellCount; ++j) {
cells[i][j] = new Cell(
pos.x + j * cellSize, pos.y + i * cellSize, cellSize);
}
}
}
//endregion
//region Private methods
// True if the given cell coordinates are inside the bounds of the board
private boolean inBounds(int x, int y) {
return x >= 0 && x < cellCount && y >= 0 && y < cellCount;
}
// True if the given piece at the given coordinates is not outside the bounds of the board
private boolean inBounds(Piece piece, int x, int y) {
return inBounds(x, y) && inBounds(x + piece.cellCols - 1, y + piece.cellRows - 1);
}
// This only tests for the piece on the given coordinates, not the whole board
private boolean canPutPiece(Piece piece, int x, int y) {
if (!inBounds(piece, x, y))
return false;
for (int i = 0; i < piece.cellRows; ++i)
for (int j = 0; j < piece.cellCols; ++j)
if (!cells[y+i][x+j].isEmpty() && piece.filled(i, j))
return false;
return true;
}
// Returns true iff the piece was put on the board
private boolean putPiece(Piece piece, int x, int y) {
if (!canPutPiece(piece, x, y))
return false;
lastPutPiecePos.set(piece.calculateGravityCenter());
for (int i = 0; i < piece.cellRows; ++i)
for (int j = 0; j < piece.cellCols; ++j)
if (piece.filled(i, j))
cells[y+i][x+j].set(piece.colorIndex);
return true;
}
//endregion
//region Public methods
public void draw(SpriteBatch batch) {
for (int i = 0; i < cellCount; ++i)
for (int j = 0; j < cellCount; ++j)
cells[i][j].draw(batch);
}
public boolean canPutPiece(Piece piece) {
for (int i = 0; i < cellCount; ++i)
for (int j = 0; j < cellCount; ++j)
if (canPutPiece(piece, j, i))
return true;
return false;
}
boolean putScreenPiece(Piece piece) {
// Convert the on screen coordinates of the piece to the local-board-space coordinates
// This is done by subtracting the piece coordinates from the board coordinates
Vector2 local = piece.pos.cpy().sub(pos);
int x = MathUtils.round(local.x / piece.cellSize);
int y = MathUtils.round(local.y / piece.cellSize);
return putPiece(piece, x, y);
}
Vector2 snapToGrid(final Piece piece, final Vector2 position) {
// Snaps the given position (e.g. mouse) to the grid,
// assuming piece wants to be put at the specified position.
// If the piece was not on the grid, the original position is returned
//
// Logic to determine the x and y is a copy-paste from putScreenPiece
final Vector2 local = position.cpy().sub(pos);
int x = MathUtils.round(local.x / piece.cellSize);
int y = MathUtils.round(local.y / piece.cellSize);
if (canPutPiece(piece, x, y))
return new Vector2(pos.x + x * piece.cellSize, pos.y + y * piece.cellSize);
else
return position;
}
// This will clear both complete rows and columns, all at once.
// The reason why we can't check first rows and then columns
// (or vice versa) is because the following case (* filled, _ empty):
//
// 4x4 boardHeight piece
// _ _ * * * *
// _ * * * *
// * * _ _
// * * _ _
//
// If the piece is put on the top left corner, all the cells will be cleared.
// If we first cleared the columns, then the rows wouldn't have been cleared.
public int clearComplete() {
int clearCount = 0;
boolean[] clearedRows = new boolean[cellCount];
boolean[] clearedCols = new boolean[cellCount];
// Analyze rows and columns that will be cleared
for (int i = 0; i < cellCount; ++i) {
clearedRows[i] = true;
for (int j = 0; j < cellCount; ++j) {
if (cells[i][j].isEmpty()) {
clearedRows[i] = false;
break;
}
}
if (clearedRows[i])
clearCount++;
}
for (int j = 0; j < cellCount; ++j) {
clearedCols[j] = true;
for (int i = 0; i < cellCount; ++i) {
if (cells[i][j].isEmpty()) {
clearedCols[j] = false;
break;
}
}
if (clearedCols[j])
clearCount++;
}
if (clearCount > 0) {
float pan = 0;
// Do clear those rows and columns
for (int i = 0; i < cellCount; ++i)
if (clearedRows[i])
for (int j = 0; j < cellCount; ++j)
cells[i][j].vanish(lastPutPiecePos);
for (int j = 0; j < cellCount; ++j) {
if (clearedCols[j]) {
pan += 2f * (j - cellCount / 2) / (float)cellCount;
for (int i = 0; i < cellCount; ++i) {
cells[i][j].vanish(lastPutPiecePos);
}
}
}
if (Klooni.soundsEnabled()) {
pan = MathUtils.clamp(pan, -1, 1);
stripClearSound.play(
MathUtils.random(0.7f, 1f), MathUtils.random(0.8f, 1.2f), pan);
}
}
return clearCount;
}
//endregion
//region Serialization
@Override
public void write(DataOutputStream out) throws IOException {
// Cell count, cells in row-major order
out.writeInt(cellCount);
for (int i = 0; i < cellCount; ++i)
for (int j = 0; j < cellCount; ++j)
cells[i][j].write(out);
}
@Override
public void read(DataInputStream in) throws IOException {
// If the saved cell count does not match the current cell count,
// then an IOException is thrown since the data saved was invalid
final int savedCellCount = in.readInt();
if (savedCellCount != cellCount)
throw new IOException("Invalid cellCount saved.");
for (int i = 0; i < cellCount; ++i)
for (int j = 0; j < cellCount; ++j)
cells[i][j].read(in);
}
//endregion
}