Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add infinite mode #55

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added android/assets/ui/x0.75/infinite_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x0.75/infinite_on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x1.0/infinite_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x1.0/infinite_on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x1.25/infinite_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x1.25/infinite_on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x1.5/infinite_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x1.5/infinite_on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x2.0/infinite_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x2.0/infinite_on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x4.0/infinite_off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added android/assets/ui/x4.0/infinite_on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ buildscript {
}
dependencies {
classpath 'de.richsource.gradle.plugins:gwt-gradle-plugin:0.6'
classpath 'com.android.tools.build:gradle:2.3.2'
classpath 'com.android.tools.build:gradle:2.3.3'
classpath 'com.mobidevelop.robovm:robovm-gradle-plugin:2.3.0'
}
}
Expand Down
10 changes: 10 additions & 0 deletions core/src/io/github/lonamiwebs/klooni/Klooni.java
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ public static boolean toggleSnapToGrid() {
return result;
}

public static boolean shouldInfiniteMode() {
return prefs.getBoolean("infiniteMode", false);
}

public static boolean toggleInfiniteMode() {
final boolean result = !shouldInfiniteMode();
prefs.putBoolean("infiniteMode", result).flush();
return result;
}

// Themes related
public static boolean isThemeBought(Theme theme) {
if (theme.getPrice() == 0)
Expand Down
3 changes: 2 additions & 1 deletion core/src/io/github/lonamiwebs/klooni/SkinLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class SkinLoader {
private final static String[] ids = {
"play", "play_saved", "star", "stopwatch", "palette", "home", "replay",
"share", "sound_on", "sound_off", "snap_on", "snap_off", "issues", "credits",
"web", "back", "ok", "cancel", "power_off", "effects"
"web", "back", "ok", "cancel", "power_off", "effects", "infinite_on",
"infinite_off"
};

private final static float bestMultiplier;
Expand Down
5 changes: 5 additions & 0 deletions core/src/io/github/lonamiwebs/klooni/game/Board.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ public boolean putPiece(Piece piece, int x, int y) {

//region Public methods

// Return true if the cell in given coordinates is empty
public boolean isEmpty(int x, int y) {
return cells[x][y].isEmpty();
}

public void draw(final Batch batch) {
batch.setTransformMatrix(batch.getTransformMatrix().translate(pos.x, pos.y, 0));

Expand Down
26 changes: 16 additions & 10 deletions core/src/io/github/lonamiwebs/klooni/game/PieceHolder.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class PieceHolder implements BinSerializable {
//region Members

final Rectangle area;
private final Piece[] pieces;
private Piece[] pieces;

private final Sound pieceDropSound;
private final Sound invalidPieceDropSound;
Expand Down Expand Up @@ -99,6 +99,15 @@ public PieceHolder(final GameLayout layout, final Board board,

//region Private methods

// If no piece is currently being held, the area will be 0
private int calculateHeldPieceArea() {
return heldPiece > -1 ? pieces[heldPiece].calculateArea() : 0;
}

private Vector2 calculateHeldPieceCenter() {
return heldPiece > -1 ? pieces[heldPiece].calculateGravityCenter() : null;
}

// Determines whether all the pieces have been put (and the "hand" is finished)
private boolean handFinished() {
for (int i = 0; i < count; ++i)
Expand All @@ -112,6 +121,11 @@ private boolean handFinished() {
private void takeMore() {
for (int i = 0; i < count; ++i)
pieces[i] = Piece.random();

// If infinite mode is turned on, make sure all pieces always fit
if (Klooni.shouldInfiniteMode())
pieces = State.validateBlock(pieces, board);

updatePiecesStartLocation();

if (Klooni.soundsEnabled()) {
Expand Down Expand Up @@ -187,15 +201,6 @@ public Array<Piece> getAvailablePieces() {
return result;
}

// If no piece is currently being held, the area will be 0
private int calculateHeldPieceArea() {
return heldPiece > -1 ? pieces[heldPiece].calculateArea() : 0;
}

private Vector2 calculateHeldPieceCenter() {
return heldPiece > -1 ? pieces[heldPiece].calculateGravityCenter() : null;
}

// Tries to drop the piece on the given board. As a result, it
// returns one of the following: NO_DROP, NORMAL_DROP, ON_BOARD_DROP
public DropResult dropPiece() {
Expand Down Expand Up @@ -224,6 +229,7 @@ public DropResult dropPiece() {
heldPiece = -1;
if (handFinished())
takeMore();

} else
result = new DropResult(false);

Expand Down
282 changes: 282 additions & 0 deletions core/src/io/github/lonamiwebs/klooni/game/State.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
package io.github.lonamiwebs.klooni.game;
Lonami marked this conversation as resolved.
Show resolved Hide resolved

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;

/*
State is an object designed to help validating randomly-generated blocks. Every time a block is
inserted into the board, the game's state changes. Different permutation of blocks and different
position creates new states. Thus to validate a set of generated blocks, we have to check all
possible states. Instead of checking them directly on the board, we create a special class for
validation.

This is used in infinite mode.
*/

public class State {
Lonami marked this conversation as resolved.
Show resolved Hide resolved

// region Members

private boolean[][] state;
private final int cellCount;
private int emptySpace = 0;
private static int[] unfitBlock = new int[6];
Lonami marked this conversation as resolved.
Show resolved Hide resolved
private final static int [][] CHECKER = {{0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0}};
private final static Vector2 ORIGIN = new Vector2(0, -1);
Lonami marked this conversation as resolved.
Show resolved Hide resolved

// endregion

//region Constructors

private State(Board board) {
this.cellCount = board.cellCount;
this.state = new boolean[cellCount][cellCount];
for (int i = 0; i < board.cellCount; ++i)
for(int j = 0; j < board.cellCount; ++j)
Lonami marked this conversation as resolved.
Show resolved Hide resolved
if (board.isEmpty(i, j))
emptySpace += 1;
else
this.state[i][j] = true;
clearComplete();
}

private State(State toClone) {
this.cellCount = toClone.state.length;
this.state = new boolean[cellCount][cellCount];
for (int i = 0; i < cellCount; ++i)
for(int j = 0; j < cellCount; ++j)
if (toClone.state[i][j])
this.state[i][j] = true;
else
emptySpace += 1;
}

// endregion

//region Check piece

private static String getUnfitBlock() {
StringBuilder sb = new StringBuilder();
for (int i : unfitBlock) {
sb.append(i);
sb.append(" ");
}
return sb.toString();
}

// 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);
}

// Given coordinates as the starting point, return true if a piece fits in said coordinates
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 (state[y + i][x + j] && piece.filled(i, j)) {
return false;
}

return true;
}

// Given Vector2 as last checked coordinates, return a new vector where the piece fits,
// else the same vector if there are no longer any place in board to fit the piece
private Vector2 canPutPiece(Piece piece, Vector2 origin) {
for (int j = (int) (origin.x + 1); j < cellCount; ++j)
if (canPutPiece(piece, j, (int) origin.y))
return new Vector2(j, origin.y);

for (int i = (int) (origin.y + 1); i < cellCount; ++i)
for (int j = 0; j < cellCount; ++j)
if (canPutPiece(piece, j, i))
return new Vector2(j, i);

return origin;
}

// Check every possible state from a set of blocks and an initial state.
// Immediately return true if found one fitting occurrence.
private static boolean checkPermute(Piece[] holder, State state) {
Vector2 temp1, temp2, temp3, pos1 = ORIGIN, pos2 = ORIGIN;
for (int i = 0; i < 6; i++) {
Lonami marked this conversation as resolved.
Show resolved Hide resolved
unfitBlock[i] = -1;
// TODO: Optimize the checking algorithm (use any pruning techniques?)
// TODO: Determine better criteria to change unfit block
while(true) {
State state1 = new State(state);
Lonami marked this conversation as resolved.
Show resolved Hide resolved
temp1 = state.canPutPiece(holder[CHECKER[i][0]], pos1);
if (temp1.epsilonEquals(pos1, MathUtils.FLOAT_ROUNDING_ERROR)) {
if (unfitBlock[i] < 0)
unfitBlock[i] = 0;
break;
}
state1.putPiece(holder[CHECKER[i][0]], temp1);
while (true) {
State state2 = new State(state1);
Lonami marked this conversation as resolved.
Show resolved Hide resolved
temp2 = state1.canPutPiece(holder[CHECKER[i][1]], pos2);
if (temp2.epsilonEquals(pos2, MathUtils.FLOAT_ROUNDING_ERROR)) {
if (unfitBlock[i] < 1)
unfitBlock[i] = 1;
break;
}
state2.putPiece(holder[CHECKER[i][1]], temp2);
temp3 = state2.canPutPiece(holder[CHECKER[i][2]], ORIGIN);
if (!temp3.epsilonEquals(ORIGIN, MathUtils.FLOAT_ROUNDING_ERROR)) {
state2.putPiece(holder[CHECKER[i][2]], temp3);
Gdx.app.log("Check permute", "Piece " +
holder[CHECKER[i][0]].colorIndex + " at " + temp1.toString());
Gdx.app.log("Check permute", "Piece " +
holder[CHECKER[i][1]].colorIndex + " at " + temp2.toString());
Gdx.app.log("Check permute", "Piece " +
holder[CHECKER[i][2]].colorIndex + " at" + temp3.toString());
return true;
}
else
unfitBlock[i] = 2;
pos2 = new Vector2(temp2);
}
pos1 = new Vector2(temp1);
}
}
Gdx.app.log("Check permute", getUnfitBlock());
return false;
}

// Change unfit block that cause most problem
private static Piece[] changeBlock(Piece[] holder, State state) {
int[] weight = new int[3];
int max = 0;
for (int i = 0; i < 6; i++) {
int temp = CHECKER[i][unfitBlock[i]];
weight[temp] += (unfitBlock[i] + 1) * 5;
}

for (int i : weight) {
if (i > max)
max = i;
}

Gdx.app.log("Change block", holder[0].colorIndex + ", " +
holder[1].colorIndex + ", " + holder[2].colorIndex);
Gdx.app.log("Change block", weight[0] + ", " +
weight[1] + ", " + weight[2]);
for (int i = 0; i < 3; i++) {
if (weight[i] == max) {
int previous = holder[i].colorIndex;
Gdx.app.log("Change block", "Changing block");
holder[i] = Piece.random();
if (checkPermute(holder, state)) {
Gdx.app.log("Change block", "Piece " + previous +
" to Piece " + holder[i].colorIndex);
return holder;
}
}
}
return changeBlock(holder, state);
}

public static Piece[] validateBlock(Piece[] holder, Board board) {
long invocationTime = System.nanoTime();
State initialState = new State(board);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, we're rebuilding the entire state every time we need to validate a piece instead of keeping it in synchrony with the board itself. Though it's true this is only ran when more pieces are needed and that's not common, so I believe it's okay.

Gdx.app.log("Validation", "Board contains " + initialState.emptySpace +
" empty spaces.");
if (checkPermute(holder, initialState)) {
Gdx.app.log("Validation", "Validation ends, takes " +
(System.nanoTime() - invocationTime) + " ns!");
return holder;
}
else {
Gdx.app.log("Validation", "Validation ends, takes " +
(System.nanoTime() - invocationTime) + " ns with changeBlock.");
return changeBlock(holder, initialState);
}
}

// endregion

// region Set piece

private void 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 (!state[i][j]) {
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 (!state[i][j]) {
clearedCols[j] = false;
break;
}
}
if (clearedCols[j])
clearCount++;
}
if (clearCount > 0) {
// Do clear those rows and columns
for (int i = 0; i < cellCount; ++i) {
if (clearedRows[i]) {
for (int j = 0; j < cellCount; ++j) {
state[i][j] = false;
}
}
}

for (int j = 0; j < cellCount; ++j) {
if (clearedCols[j]) {
for (int i = 0; i < cellCount; ++i) {
state[i][j] = false;
}
}
}
}
}

private void putPiece(Piece piece, Vector2 vec) {
for (int i = 0; i < piece.cellRows; ++i)
for (int j = 0; j < piece.cellCols; ++j)
if (piece.filled(i, j))
state[(int) (vec.y + i)][(int) (vec.x + j)] = true;
clearComplete();
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder("State:\n");
for (int i = cellCount - 1 ; i > -1; --i) {
for (int j = 0; j < cellCount; ++j) {
if (state[i][j]) {
sb.append(1);
} else {
sb.append(0);
}
}
sb.append("\n");
}
return sb.toString();
}

// endregion
}
Loading