Refactoring to use XYPair + moving loader to a separate file.

This commit is contained in:
2021-10-17 17:11:58 +01:00
parent 0c0938d651
commit 56679bd1be
14 changed files with 642 additions and 473 deletions

View File

@@ -4,6 +4,9 @@ plugins {
apply plugin: "androidx.navigation.safeargs"
//This is needed for SVG->PNG
//apply plugin: 'com.trello.victor'
//
// Convert SVG images from art directory into PNG files.
//
@@ -19,7 +22,7 @@ apply plugin: "androidx.navigation.safeargs"
//}
//preBuild.dependsOn generateDrawablesFromArt
String gitVersion() {
static String gitVersion() {
def versionP = 'git describe --tags --long --dirty=-x --always --abbrev=8'
.execute()
versionP.waitFor()
@@ -28,15 +31,15 @@ String gitVersion() {
return versionP.text.trim()
}
String gitHash() {
def githashP = 'git rev-parse HEAD'.execute()
githashP.waitFor()
if (githashP.exitValue())
static String gitHash() {
def hashP = 'git rev-parse HEAD'.execute()
hashP.waitFor()
if (hashP.exitValue())
throw new GradleException("Couldn't extract git_hash. Git exited unexpectedly.")
return githashP.text.trim()
return hashP.text.trim()
}
Integer gitVersionCode() {
static Integer gitVersionCode() {
def gitP = 'git rev-list v2..HEAD --count'.execute()
gitP.waitFor()
if (gitP.exitValue())
@@ -44,19 +47,26 @@ Integer gitVersionCode() {
return gitP.text.trim().toInteger()
}
String buildDate() {
def builddate = new Date()
return builddate.toString()
static String buildDate() {
def date = new Date()
return date.toString()
}
android {
signingConfigs {
release {
storeFile file('/Users/vahagnk/devel/banvor/keys/signing_key')
storeFile file('keys/signing_key')
keyAlias 'key0'
}
}
compileSdk 31
// sourceSets {
// main {
// svg.srcDir 'src/main/art'
// }
// }
defaultConfig {
applicationId "org.vostan.banvor"
minSdk 21
@@ -106,6 +116,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
// This is for unit tests.
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View File

@@ -61,23 +61,23 @@ public class PuzzleBoardFragment extends Fragment
public void onTouch() {
}
});
binding.btnTest.setOnClickListener((View.OnClickListener) view4 -> {
binding.btnTest.setOnClickListener((View.OnClickListener) v -> {
createNextLevelDialog((d, w) -> {
initAndShowCurrentPuzzle();
}).show();
});
binding.btnPrev.setOnClickListener((View.OnClickListener) view1 -> {
binding.btnPrev.setOnClickListener((View.OnClickListener) v -> {
gameState.setCurrentLevel(gameState.getCurrentLevel()-1);
PuzzleBoardFragment.this.initAndShowCurrentPuzzle();
});
binding.btnNext.setOnClickListener((View.OnClickListener) view2 -> {
binding.btnNext.setOnClickListener((View.OnClickListener) v -> {
gameState.setCurrentLevel(gameState.getCurrentLevel()+1);
PuzzleBoardFragment.this.initAndShowCurrentPuzzle();
});
binding.btnReset.setOnClickListener((View.OnClickListener) view3 -> {
binding.btnReset.setOnClickListener((View.OnClickListener) v -> {
PuzzleBoardFragment.this.initAndShowCurrentPuzzle();
});
binding.btnUndo.setOnClickListener((View.OnClickListener) view4 -> {
binding.btnUndo.setOnClickListener((View.OnClickListener) v -> {
createNextLevelDialog((d, w) -> {
gameState.advanceCurrentLevel();
initAndShowCurrentPuzzle();

View File

@@ -5,6 +5,9 @@
package org.vostan.banvor.board;
import androidx.core.view.ViewCompat;
import org.vostan.banvor.model.XYPair;
import java.util.LinkedList;
/**
@@ -28,21 +31,17 @@ public class Animator implements Runnable
protected static abstract class XYAction extends Action
{
int x;
int y;
protected XYPair _xy;
public XYAction( int _x, int _y )
{
x = _x;
y = _y;
public XYAction( XYPair xy ){
_xy = xy;
}
}
public static class Move extends XYAction
{
public Move( int x, int y )
{
super(x,y);
public Move( XYPair xy ){
super(xy);
}
public void intermediate( Animator ap, int step )
@@ -50,16 +49,15 @@ public class Animator implements Runnable
public void last( Animator ap )
{
ap.view.getPuzzle().walk(x,y);
ap.view.getPuzzle().walk(_xy);
ap.view.invalidate();
}
}
public static class Push extends XYAction
{
public Push( int x, int y )
{
super(x,y);
public Push( XYPair xy ){
super(xy);
}
public void intermediate( Animator ap, int step )
@@ -68,16 +66,15 @@ public class Animator implements Runnable
public void last( Animator ap )
{
ap.view.getPuzzle().push(x,y);
ap.view.getPuzzle().push(_xy);
ap.view.invalidate();
}
}
public static class Select extends XYAction
{
public Select(int x, int y )
{
super(x,y);
public Select( XYPair xy ){
super(xy);
}
public void intermediate( Animator ap, int step )
@@ -86,7 +83,7 @@ public class Animator implements Runnable
public void last( Animator ap )
{
ap.view.getPuzzle().select(x,y);
ap.view.getPuzzle().select(_xy);
ap.view.invalidate();
}
}
@@ -108,9 +105,8 @@ public class Animator implements Runnable
public static class NoMove extends XYAction
{
public NoMove( int x, int y )
{
super(x,y);
public NoMove( XYPair xy ){
super(xy);
}
public void intermediate( Animator ap, int step )

View File

@@ -13,6 +13,7 @@ import android.view.ScaleGestureDetector;
import org.vostan.banvor.game.PuzzleLogic;
import org.vostan.banvor.model.Puzzle;
import org.vostan.banvor.model.XYPair;
//import android.support.v4.view.GestureDetectorCompat;
@@ -117,7 +118,7 @@ public class PuzzleControl extends PuzzleView
// If the tap is not on a valid tile then there is not point to
// continue.
//
if ( !puzzle.isValid(singleTapTile.x, singleTapTile.y) )
if ( !puzzle.isValid(new XYPair(singleTapTile.x, singleTapTile.y) ))
return true;
//
// Create sequence of steps and then animate it.

View File

@@ -18,6 +18,7 @@ import static java.lang.Math.*;
import org.vostan.banvor.model.Puzzle;
import static org.vostan.banvor.model.Puzzle.*;
import org.vostan.banvor.R;
import org.vostan.banvor.model.XYPair;
/**
*/
@@ -381,7 +382,7 @@ public class PuzzleView extends View
//tile_hlpr.roundOut(tile_hlpr);
//tile_hlpr.right -=1;
//tile_hlpr.bottom -=1;
int sym = puzzle.getSym(x,y);
int sym = puzzle.getSym(new XYPair(x,y));
if ( !Puzzle.hasWorker(sym) )
{
canvas.drawBitmap(tile[sym],
@@ -403,30 +404,30 @@ public class PuzzleView extends View
Bitmap normal_worker = ((sym&SELECTED)!=0) ?
tile[WORKER|SELECTED] : tile[WORKER];
int direction = sym & WORKER_MASK;
if ( direction == WORKER_NORTH )
{
canvas.translate(tile_dest.centerX(), tile_dest.centerY());
canvas.rotate(180+rotated);
canvas.translate(-tile_dest.centerX(), -tile_dest.centerY());
}
else if ( direction == WORKER_SOUTH )
if ( direction == WORKER_FACE_DOWN)
{
canvas.translate(tile_dest.centerX(), tile_dest.centerY());
canvas.rotate(0+rotated);
canvas.translate(-tile_dest.centerX(), -tile_dest.centerY());
}
else if ( direction == WORKER_WEST )
else if ( direction == WORKER_FACE_UP)
{
canvas.translate(tile_dest.centerX(), tile_dest.centerY());
canvas.rotate(90+rotated);
canvas.rotate(180+rotated);
canvas.translate(-tile_dest.centerX(), -tile_dest.centerY());
}
else if ( direction == WORKER_EAST )
else if ( direction == WORKER_FACE_LEFT)
{
canvas.translate(tile_dest.centerX(), tile_dest.centerY());
canvas.rotate(-90+rotated);
canvas.translate(-tile_dest.centerX(), -tile_dest.centerY());
}
else if ( direction == WORKER_FACE_RIGHT)
{
canvas.translate(tile_dest.centerX(), tile_dest.centerY());
canvas.rotate(90+rotated);
canvas.translate(-tile_dest.centerX(), -tile_dest.centerY());
}
canvas.drawBitmap(normal_worker,
tile_src,tile_dest,
mPaint);

View File

@@ -6,9 +6,12 @@ package org.vostan.banvor.game;
import static java.lang.Math.*;
import java.util.Arrays;
import java.util.ListIterator;
import java.util.Vector;
import org.vostan.banvor.board.Animator;
import org.vostan.banvor.model.Puzzle;
import org.vostan.banvor.model.XYPair;
/**
*
@@ -18,7 +21,7 @@ public class PuzzleLogic
//protected PlayActivity activity;
protected Puzzle puzzle = null;
protected int [] moves;
protected int [] setOfCells;
protected Vector<XYPair> setOfCells = new Vector<XYPair>();
public PuzzleLogic()
{}
@@ -27,21 +30,25 @@ public class PuzzleLogic
{
puzzle = p;
moves = new int[puzzle.getColumnCount()*puzzle.getRowCount()];
setOfCells = new int[2*puzzle.getColumnCount()*puzzle.getRowCount()];
setOfCells.ensureCapacity(puzzle.getColumnCount()*puzzle.getRowCount());
}
public boolean createSteps(Animator animator, final int x, final int y )
{
return createSteps(animator, new XYPair(x,y));
}
public boolean createSteps(Animator animator, XYPair xy )
{
//
// Check that the x,y are valid.
//
if ( !puzzle.isValid(x, y) )
if ( !puzzle.isValid(xy) )
return false;
//
// Now check what tile was tapped.
// If the tapped is a floor then ...
//
int tile = puzzle.getSym( x, y);
int tile = puzzle.getSym(xy);
if ( Puzzle.isEmpty(tile) )
{
//
@@ -53,18 +60,18 @@ public class PuzzleLogic
// If yes then we are done.
//
if ( puzzle.isSelected()
&& tryMoveBox(animator, x, y) )
&& tryMoveBox(animator, xy) )
return true;
//
// Either nothing was selected or the box cannot be moved to
// tapped location. Try move worker alone.
//
if ( tryMoveWorker( animator, x, y ) )
if ( tryMoveWorker( animator, xy ) )
return true;
//
// Show that action is not allowed.
//
undoable( animator, x, y );
undoable(animator, xy);
}
//
// The tapped is the worker. Try move the box. If not possible
@@ -85,7 +92,7 @@ public class PuzzleLogic
// Check if worker selected a box and can push it to the location?
// If yes then we are done.
//
if ( tryMoveBox(animator, x, y) )
if ( tryMoveBox(animator, xy) )
return true;
//
// If the box is not movable then unselect it.
@@ -104,7 +111,7 @@ public class PuzzleLogic
//
// If the box is selected then unselect it.
//
if ( puzzle.isSelected(x,y) )
if ( puzzle.isSelected(xy) )
{
unselect( animator );
return true;
@@ -113,12 +120,12 @@ public class PuzzleLogic
// Try move the worker next to the box and select it if the
// box is not selected yet.
//
if ( trySelectBox( animator, x, y ) )
if ( trySelectBox( animator, xy ) )
return true;
//
// Show that action is not allowed if reached till here.
//
undoable( animator, x, y );
undoable( animator, xy );
}
return true;
}
@@ -127,12 +134,12 @@ public class PuzzleLogic
//
// Routes to accessible cells from where worker stands.
//
protected boolean tryMoveWorker( Animator animator, int x, int y )
protected boolean tryMoveWorker( Animator animator, XYPair xy )
{
//
// If the filed is not accessable then move failed.
//
if ( !isAccessible(x,y) )
if ( !isAccessible(xy) )
return false;
//
// First unselect box.
@@ -142,16 +149,18 @@ public class PuzzleLogic
//
// Get directions and queue moves accordingly.
//
int dirs[] = getDirections(x, y);
for ( int i = 0; i < dirs.length; i+=2 )
move( animator, dirs[i],dirs[i+1] );
Vector<XYPair> dirs = getDirections(xy);
ListIterator<XYPair> it = dirs.listIterator(dirs.size());
while ( it.hasPrevious() ) {
move(animator, it.previous());
}
//
// Done.
//
return true;
}
protected boolean tryMoveBox( Animator animator, int x, int y )
protected boolean tryMoveBox( Animator animator, XYPair xy )
{
//
// If no box is selected then we cannot move no box.
@@ -161,51 +170,44 @@ public class PuzzleLogic
//
// Check that asked move is orthogonal to the slected box.
//
int bx = puzzle.getSelectedX();
int by = puzzle.getSelectedY();
int dx = x-bx;
int dy = y-by;
if ( dx != 0 && dy != 0 )
XYPair box_xy = puzzle.getSelected();
XYPair dxy = xy.sub(box_xy);
if ( dxy.x() != 0 && dxy.y() != 0 )
return false;
//
// There is no point to continue also in case if the asked cell
// is the box.
//
if ( dx == 0 && dy == 0 )
if ( dxy.isEqual(XYPair.ZERO) )
return false;
//
// Now find the desired place for the worker to start push this
// box.
//
int wx = bx;
int wy = by;
if ( x < bx )
++wx;
else if ( x > bx )
--wx;
else if ( y < by )
++wy;
XYPair w = new XYPair(box_xy);
if ( xy.x() < box_xy.x() )
w = w.right();
else if ( xy.x() > box_xy.x() )
w = w.left();
else if ( xy.y() < box_xy.y() )
w = w.up();
else
--wy;
w = w.down();
//
// Check if the desired place for the worker is accessable? If not
// then there is no point to continue.
//
if ( !isAccessible(wx, wy) )
if ( !isAccessible(w) )
return false;
//
// Now check that all cell till x,y are empty and that we can
// push box till there.
//
int sx = bx-wx;
int sy = by-wy;
int ix = bx;
int iy = by;
while ( x != ix || y != iy )
XYPair step_xy = box_xy.sub(w);
for ( XYPair i = box_xy; !xy.isEqual(i); )
{
ix+=sx;
iy+=sy;
int v = puzzle.getSym(ix,iy);
i = i.add(step_xy);
int v = puzzle.getSym(i);
if ( !puzzle.isEmpty(v) && !puzzle.hasWorker(v) )
return false;
}
@@ -214,21 +216,16 @@ public class PuzzleLogic
// what to do. First move worker to desired position if he is not
// there already.
//
if ( wx != puzzle.getWorkerX() || wy != puzzle.getWorkerY() )
if ( !w.isEqual(puzzle.getWorker()) )
{
tryMoveWorker( animator, wx, wy );
select( animator, bx, by );
tryMoveWorker( animator, w );
select( animator, box_xy );
}
//
// Now create the steps to push the box.
//
ix = bx;
iy = by;
while ( x != ix || y != iy )
{
push( animator, ix, iy );
ix+=sx;
iy+=sy;
for ( XYPair i = box_xy; !i.isEqual(xy); i = i.add(step_xy) ) {
push( animator, i );
}
//
// Done
@@ -236,12 +233,12 @@ public class PuzzleLogic
return true;
}
protected boolean trySelectBox( Animator animator, int x, int y )
protected boolean trySelectBox( Animator animator, XYPair xy )
{
int north = puzzle.getSym(x,y-1);
int south = puzzle.getSym(x,y+1);
int west = puzzle.getSym(x-1,y);
int east = puzzle.getSym(x+1,y);
int north = puzzle.getSym(xy.up());
int south = puzzle.getSym(xy.down());
int west = puzzle.getSym(xy.left());
int east = puzzle.getSym(xy.right());
//
// First check if there is a worker in a nighbour cell. If
// yes then simplly select the box. If the box is already selected
@@ -253,11 +250,11 @@ public class PuzzleLogic
|| Puzzle.hasWorker( north )
|| Puzzle.hasWorker( south ) )
{
if ( !puzzle.isSelected(x,y) )
if ( !puzzle.isSelected(xy) )
{
if ( puzzle.isSelected() )
unselect( animator );
select( animator, x, y );
select( animator, xy );
}
}
//
@@ -266,48 +263,43 @@ public class PuzzleLogic
//
else
{
int pref_x = -1;
int pref_y = -1;
XYPair pref = new XYPair(-1, -1);
int shortest = Integer.MAX_VALUE;
if ( Puzzle.isEmpty( north )
&& shortest > stepsAway( x, y-1) )
&& shortest > stepsAway(xy.down()) )
{
shortest = stepsAway( x, y-1);
pref_x = x;
pref_y = y-1;
shortest = stepsAway(xy.down());
pref.set(xy.down());
}
if ( Puzzle.isEmpty( south )
&& shortest > stepsAway( x, y+1) )
&& shortest > stepsAway(xy.up()) )
{
shortest = stepsAway( x, y+1);
pref_x = x;
pref_y = y+1;
shortest = stepsAway(xy.up());
pref.set(xy.up());
}
if ( Puzzle.isEmpty( west )
&& shortest > stepsAway( x-1, y) )
&& shortest > stepsAway(xy.left()) )
{
shortest = stepsAway( x-1, y);
pref_x = x-1;
pref_y = y;
shortest = stepsAway(xy.left());
pref.set(xy.left());
}
if ( Puzzle.isEmpty( east )
&& shortest > stepsAway( x+1, y) )
&& shortest > stepsAway( xy.right()) )
{
shortest = stepsAway( x+1, y);
pref_x = x+1;
pref_y = y;
shortest = stepsAway( xy.right());
pref.set(xy.right());
}
//
// Move the worker to the direction. If we cannot move worker
// next to the box then we cannot select it.
//
if ( !puzzle.isValid(pref_x, pref_y)
|| !tryMoveWorker(animator, pref_x, pref_y) )
if ( !puzzle.isValid(pref)
|| !tryMoveWorker(animator, pref) )
return false;
//
// Select the box.
//
select(animator,x,y);
select(animator,xy);
}
//
// Done
@@ -315,19 +307,25 @@ public class PuzzleLogic
return true;
}
protected void move( Animator animator, int x, int y )
//////////////////////////////////////////////////////////////////////////
//
// These should move to a callback.
//
protected void move( Animator animator, XYPair xy )
{
animator.queue( new Animator.Move(x,y) );
animator.queue( new Animator.Move(xy) );
}
protected void push( Animator animator, int x, int y )
protected void push( Animator animator, XYPair xy )
{
animator.queue( new Animator.Push(x,y) );
animator.queue( new Animator.Push(xy) );
}
protected void select( Animator animator, int x, int y )
protected void select( Animator animator, XYPair xy )
{
animator.queue( new Animator.Select(x,y) );
animator.queue( new Animator.Select(xy) );
}
protected void unselect( Animator animator )
@@ -340,9 +338,9 @@ public class PuzzleLogic
//animator.queue( new Animator.NoMove(x,y) );
}
protected void undoable( Animator animator, int x, int y )
protected void undoable( Animator animator, XYPair xy )
{
animator.queue( new Animator.NoMove(x,y) );
animator.queue( new Animator.NoMove(xy) );
}
//////////////////////////////////////////////////////////////////////////
@@ -350,29 +348,28 @@ public class PuzzleLogic
// Routes to accessible cells from where worker stands.
//
protected final int getMoves( int x, int y )
protected final int getMoves( XYPair xy )
{
return moves[puzzle.getIndex(x,y)];
return moves[puzzle.getIndex(xy)];
}
protected void setMoves( int x, int y, int v )
protected void setMoves( XYPair xy, int v )
{
moves[puzzle.getIndex(x,y)]=v;
moves[puzzle.getIndex(xy)]=v;
}
private boolean setMovesIfGreater( int x, int y, int l )
private boolean setMovesIfGreater( XYPair xy, int l )
{
//
// If out of borders then nothing to do.
//
if ( y < 0 || y >= puzzle.getRowCount()
|| x < 0 || x >= puzzle.getColumnCount() )
if ( !puzzle.isValid(xy) )
return false;
//
// Check if the cell is a floor or goal. If yes then set the l
// if current value is greater than l.
//
if ( getMoves(x,y) > l && puzzle.isEmpty(puzzle.getSym(x,y)))
if ( getMoves(xy) > l && puzzle.isEmpty(puzzle.getSym(xy)))
{
setMoves(x,y,l);
setMoves(xy,l);
return true;
}
else
@@ -389,105 +386,84 @@ public class PuzzleLogic
// For the beginning there is no cell in the list.
//
int front = 0;
int last = 0;
setOfCells.removeAllElements();
//
// Set the seed.
//
setMoves(puzzle.getWorkerX(),puzzle.getWorkerY(),0);
setOfCells[last++] = puzzle.getWorkerX();
setOfCells[last++] = puzzle.getWorkerY();
setMoves(puzzle.getWorker(),0);
setOfCells.add(puzzle.getWorker());
//
// Now on each loop pop one cell from the list and calculate the
// distance of cell around that cell.
//
while ( front < last )
while ( front < setOfCells.size() )
{
//
// Pop the cell.
//
int x = setOfCells[front++];
int y = setOfCells[front++];
XYPair xy = setOfCells.elementAt(front++);
//
// Increase the length of cells all around given cell and push
// them into the list.
//
int l = getMoves(x,y)+1;
if ( setMovesIfGreater(x-1,y,l) )
{
setOfCells[last++] = x-1;
setOfCells[last++] = y;
int l = getMoves(xy)+1;
if ( setMovesIfGreater(xy.left(),l) ) {
setOfCells.add(xy.left());
}
if ( setMovesIfGreater(x+1,y,l) )
{
setOfCells[last++] = x+1;
setOfCells[last++] = y;
if ( setMovesIfGreater(xy.right(),l) ) {
setOfCells.add(xy.right());
}
if ( setMovesIfGreater(x,y-1,l) )
{
setOfCells[last++] = x;
setOfCells[last++] = y-1;
if ( setMovesIfGreater(xy.down(),l) ) {
setOfCells.add(xy.down());
}
if ( setMovesIfGreater(x,y+1,l) )
{
setOfCells[last++] = x;
setOfCells[last++] = y+1;
if ( setMovesIfGreater(xy.up(),l) ) {
setOfCells.add(xy.up());
}
}
}
public final int stepsAway( int x, int y )
public final int stepsAway( XYPair xy )
{
return getMoves(x,y);
return getMoves(xy);
}
public final boolean isAccessible( int x, int y )
public final boolean isAccessible( XYPair xy )
{
return puzzle.isValid(x, y) && stepsAway(x, y) != Integer.MAX_VALUE;
return puzzle.isValid(xy) && stepsAway(xy) != Integer.MAX_VALUE;
}
public int [] getDirections( int x, int y )
public Vector<XYPair> getDirections( XYPair xy )
{
int away = stepsAway(x,y);
int away = stepsAway(xy);
if (away == Integer.MAX_VALUE)
return null;
//
// Ok looks there is a routh to given cell. Now create an array
// and fill in the step to get to the cell.
//
int [] steps = new int[2*away];
int i = steps.length;
steps[--i] = y;
steps[--i] = x;
while ( i > 0 )
Vector<XYPair> steps = new Vector<XYPair>(away);
steps.add(xy);
while ( steps.size() < away )
{
x = steps[i];
y = steps[i+1];
int j = stepsAway(x,y);
if ( stepsAway(x-1,y) < j )
{
steps[--i] = y;
steps[--i] = x-1;
xy = steps.lastElement();
int j = stepsAway(xy);
if ( stepsAway(xy.left()) < j ) {
steps.add(xy.left());
}
else if ( stepsAway(x+1,y) < j )
{
steps[--i] = y;
steps[--i] = x+1;
else if ( stepsAway(xy.right()) < j ) {
steps.add(xy.right());
}
else if ( stepsAway(x,y-1) < j )
{
steps[--i] = y-1;
steps[--i] = x;
else if ( stepsAway(xy.down()) < j ) {
steps.add(xy.down());
}
else if ( stepsAway(x,y+1) < j )
{
steps[--i] = y+1;
steps[--i] = x;
else if ( stepsAway(xy.up()) < j ) {
steps.add(xy.up());
}
}
return steps;
}
public int [] getPushDirections( int x, int y )
public XYPair [] getPushDirections( XYPair xy )
{
//
// The selected box can be moved only orthogonally.
@@ -498,43 +474,35 @@ public class PuzzleLogic
// push direction is directed to x,y and is not opposit. In other words
// selected->x,y and worker->selected are codirectional.
//
int selx = puzzle.getSelectedX();
int sely = puzzle.getSelectedY();
int xx = x-selx;
int yy = y-sely;
int sx = selx-puzzle.getWorkerX();
int sy = sely-puzzle.getWorkerY();
double scaliar = (double)(xx)*(double)(sx)
+(double)(yy)*(double)(sy);
int len = abs(xx)+abs(yy);
XYPair sel = puzzle.getSelected();
XYPair dist = XYPair.sub(xy, puzzle.getSelected());
XYPair dir = XYPair.sub(puzzle.getSelected(), puzzle.getWorker());
double scaliar = (double)(dist.x())*(double)(dir.x())
+(double)(dist.y())*(double)(dir.y());
int len = dist.l1_norm();
if ( scaliar != (double)len )
return null;
//
// Now check that all cell till x,y are free.
//
int ix = selx;
int iy = sely;
while ( x != ix || y != iy )
XYPair ixy = new XYPair(puzzle.getSelected());
while ( !xy.isEqual(ixy) )
{
ix+=sx;
iy+=sy;
if ( !puzzle.isEmpty(puzzle.getSym(ix,iy)) )
ixy = ixy.add(dir);
if ( !puzzle.isEmpty(puzzle.getSym(ixy)) )
return null;
}
//
// Looks we could move the box till x,y. Create the steps array
// and fill it with push steps.
//
int steps[] = new int [2*len];
XYPair steps[] = new XYPair[len];
int i = 0;
ix = selx;
iy = sely;
while ( x != ix || y != iy )
ixy.set(puzzle.getSelected());
while ( !xy.isEqual(ixy) )
{
steps[i++] = ix;
steps[i++] = iy;
ix+=sx;
iy+=sy;
steps[i++] = ixy;
ixy = ixy.add(dir);
}
return steps;
}

View File

@@ -1,6 +0,0 @@
package org.vostan.banvor.model;
public class Coord {
public int x;
public int y;
}

View File

@@ -1,14 +1,9 @@
package org.vostan.banvor.model;
//import java.lang.Exception;
import java.io.*;
import static java.lang.Math.*;
import android.util.Log;
import java.util.LinkedList;
import static org.vostan.banvor.App.TAG;
public class Puzzle
{
public final static int FLOOR = 0x0;
@@ -18,28 +13,45 @@ public class Puzzle
public final static int BOX = 0x4;
public final static int BINGO = BOX | GOAL;
public final static int WORKER = 0x8;
public final static int WORKER_NORTH = 0x00 | WORKER;
public final static int WORKER_SOUTH = 0x10 | WORKER;
public final static int WORKER_WEST = 0x20 | WORKER;
public final static int WORKER_EAST = 0x30 | WORKER;
public final static int WORKER_MASK = WORKER_EAST | WORKER_WEST | WORKER_SOUTH | WORKER_NORTH;
public final static int WORKER_FACE_DOWN = 0x00 | WORKER;
public final static int WORKER_FACE_UP = 0x10 | WORKER;
public final static int WORKER_FACE_LEFT = 0x20 | WORKER;
public final static int WORKER_FACE_RIGHT = 0x30 | WORKER;
public final static int WORKER_MASK = WORKER_FACE_RIGHT | WORKER_FACE_LEFT | WORKER_FACE_UP | WORKER_FACE_DOWN;
public final static int SELECTED = 0x40;
public final static int MAX_COUNT = 0x80-1;
protected int columns;
protected int rows;
protected int box_count;
private final static XYPair unselected = new XYPair(-1,-1);
private final static XYPair zero = new XYPair(0,0);
protected int [] board;
protected int worker_x;
protected int worker_y;
protected int selected_x;
protected int selected_y;
protected int bingos;
public Puzzle( InputStream is )
private int [] board;
private XYPair board_size = new XYPair(0,0);
private int bingos = 0;
private int box_count = 0;
private XYPair worker = new XYPair(0,0);
private XYPair selected = new XYPair(-1,-1);
public Puzzle(){
}
public Puzzle(int cols, int rows, int [] b)
{
load( is );
board_size.set(new XYPair(cols, rows));
box_count = 0;
bingos = 0;
// board = new int[board_size.x()*board_size.y()];
board = b;
for( int i = 0; i < board.length; ++i ){
if ((board[i] & BOX) == BOX){
++box_count;
}
if ((board[i] & BINGO) == BINGO) {
++bingos;
}
if ((board[i] & WORKER) == WORKER){
worker = getXY(i);
}
}
}
//////////////////////////////////////////////////////////////////////////
@@ -49,37 +61,40 @@ public class Puzzle
public final int getColumnCount()
{
return columns;
return board_size.x();
}
public final int getRowCount()
{
return rows;
return board_size.y();
}
public static int getSymCount()
{
return MAX_COUNT;
}
public final int getIndex( int x, int y )
public final int getIndex( XYPair c )
{
return y*columns+x;
return c.y()*board_size.x()+c.x();
}
public final XYPair getXY( int idx )
{
return new XYPair(getX(idx), getY(idx));
}
public final int getX( int idx )
{
return idx % columns;
return idx % board_size.x();
}
public final int getY( int idx )
{
return idx / columns;
return idx / board_size.x();
}
public final int getSym( int x, int y )
{
return board[getIndex(x,y)];
public final int getSym( XYPair c ) {
return board[getIndex(c)];
}
public void setSym( int x, int y, int v )
public void setSym( XYPair xy, int v )
{
board[getIndex(x,y)]=v;
board[getIndex(xy)]=v;
}
public static boolean isEmpty( int v )
@@ -95,47 +110,33 @@ public class Puzzle
return (v & WORKER) != 0;
}
public final int getWorkerX()
public final XYPair getWorker()
{
return worker_x;
}
public final int getWorkerY()
{
return worker_y;
return worker;
}
public final boolean isSelected()
{
return selected_x != -1;
return !selected.isEqual(unselected);
}
public final boolean isSelected( int x, int y )
public final boolean isSelected( XYPair s )
{
return selected_x == x && selected_y == y;
return selected.isEqual(s);
}
public final int getSelectedX()
public final XYPair getSelected()
{
return selected_x;
return selected;
}
public final int getSelectedY()
public final boolean isValid( XYPair xy )
{
return selected_y;
return xy.isInside(zero,board_size);
}
public final boolean isValid( int x, int y )
{
return ( 0 <= x && 0 <= y
&& x < getColumnCount()
&& y < getRowCount() );
}
public final boolean isOneStep( int x, int y )
{
return isValid(x,y)
&& (abs(worker_x-x )+abs(worker_y-y) == 1);
public final boolean isOneStep( XYPair xy ) {
return isValid(xy) && XYPair.sub(worker, xy).l1_norm() == 1;
}
public final boolean isDone()
@@ -143,73 +144,42 @@ public class Puzzle
return bingos == box_count;
}
//////////////////////////////////////////////////////////////////////////
//
// Loading.
//
private void load( InputStream is )
{try{
columns = is.read();
rows = is.read();
box_count = 0;
bingos = 0;
selected_x = -1;
selected_y = -1;
board = new int[columns*rows];
byte [] b = new byte [columns*rows];
is.read( b, 0, columns*rows);
for( int i = 0; i < b.length; ++i )
{
switch( b[i] )
{
case 0:
board[i] = FLOOR;
break;
case 1:
board[i] = WALL;
break;
case 2:
board[i] = BOX;
++box_count;
break;
case 3:
board[i] = GOAL;
break;
case 4:
board[i] = BINGO;
++box_count;
++bingos;
break;
case 5:
board[i] = WORKER;
worker_x = getX(i);
worker_y = getY(i);
break;
default:
}
}
}
catch ( Exception e )
{
Log.d( TAG, "load()", e);
}}
//////////////////////////////////////////////////////////////////////////
//
// Move worker.
//
public boolean walk( int x, int y)
private int worker_direction(XYPair xy) {
//
// Find direction to move.
//
int w;
if (worker.left().isEqual(xy)) {
w = WORKER_FACE_LEFT;
}
else if (worker.right().isEqual(xy)) {
w = WORKER_FACE_RIGHT;
}
else if (worker.up().isEqual(xy)) {
w = WORKER_FACE_UP;
}
else {
w = WORKER_FACE_DOWN;
}
return w;
}
public boolean walk(XYPair xy)
{
//
// Check that this is one step away.
//
if ( !isOneStep(x,y) )
if ( !isOneStep(xy) )
return false;
//
// Check that this is empty space.
//
int v = getSym(x,y);
int v = getSym(xy);
if ( !isEmpty(v) )
return false;
//
@@ -218,38 +188,25 @@ public class Puzzle
if ( isSelected() )
unselect();
//
// Find direction to move.
//
int worker;
if ( worker_x < x )
worker = WORKER_WEST;
else if ( worker_x > x )
worker = WORKER_EAST;
else if ( worker_y > y )
worker = WORKER_SOUTH;
else
worker = WORKER_NORTH;
//
// Move worker marker from current position to asked position.
//
setSym(worker_x,worker_y,getSym(worker_x,worker_y)&~WORKER_MASK);
worker_x =x;
worker_y =y;
setSym(worker_x,worker_y,getSym(worker_x,worker_y)|worker);
setSym(worker,getSym(worker)&~WORKER_MASK);
worker.set(xy);
setSym(worker,getSym(worker)|worker_direction(xy));
return true;
}
public boolean select( int x, int y)
public boolean select(XYPair xy)
{
//
// Check that this is one step away.
//
if ( !isOneStep(x,y) )
if ( !isOneStep(xy) )
return false;
//
// Check that this is empty space.
//
int v = getSym(x,y);
int v = getSym(xy);
if ( !hasBox(v) )
return false;
//
@@ -258,24 +215,11 @@ public class Puzzle
if ( isSelected() )
unselect();
//
// Find direction to move.
//
int worker;
if ( worker_x < x )
worker = WORKER_WEST;
else if ( worker_x > x )
worker = WORKER_EAST;
else if ( worker_y > y )
worker = WORKER_SOUTH;
else
worker = WORKER_NORTH;
//
// Move worker marker from current position to asked position.
//
selected_x =x;
selected_y =y;
setSym(worker_x,worker_y,getSym(worker_x,worker_y)&~WORKER_MASK|worker|SELECTED);
setSym(selected_x,selected_y,getSym(selected_x,selected_y)|SELECTED);
selected.set(xy);
setSym(worker,getSym(worker)&~WORKER_MASK|SELECTED|worker_direction(xy));
setSym(selected,getSym(selected)|SELECTED);
return true;
}
@@ -289,14 +233,13 @@ public class Puzzle
//
// Move worker marker from current position to asked position.
//
setSym(worker_x,worker_y,getSym(worker_x,worker_y)&~SELECTED);
setSym(selected_x,selected_y,getSym(selected_x,selected_y)&~SELECTED);
selected_x =-1;
selected_y =-1;
setSym(worker,getSym(worker)&~SELECTED);
setSym(selected,getSym(selected)&~SELECTED);
selected.set(unselected);
return true;
}
public boolean push(int x, int y)
public boolean push(XYPair xy)
{
//
// If not selected then do nothing.
@@ -306,30 +249,29 @@ public class Puzzle
//
// Check that we go to the selected direction.
//
if ( selected_x != x && selected_y != y )
if ( !selected.isEqual(xy) )
return false;
//
// Check that this is a box that we move.
//
int v = getSym(x,y);
int v = getSym(xy);
if ( !hasBox(v) )
return false;
//
// Check that the next space to the box is empty.
//w - 2*(w-x) = 2x -w
int next_x = 2*x - worker_x;
int next_y = 2*y - worker_y;
int next_v = getSym(next_x,next_y);
XYPair next = xy.mul(2).sub(worker);
int next_v = getSym(next);
if ( !isEmpty(next_v) )
return false;
//
// Ok, looks we can move the box. Do it actually.
//
unselect();
setSym(x,y,getSym(x,y)&~BOX);
setSym(next_x,next_y,getSym(next_x,next_y)|BOX);
walk(x,y);
select(next_x,next_y);
setSym(xy,getSym(xy)&~BOX);
setSym(next,getSym(next)|BOX);
walk(xy);
select(next);
//
// Keep track of box count in place.
//
@@ -348,29 +290,23 @@ public class Puzzle
protected class State
{
public int [] board;
public int worker_x;
public int worker_y;
public int selected_x;
public int selected_y;
public int bingos;
public XYPair worker = new XYPair(0,0);
public XYPair selected = new XYPair(0, 0);
public int bingos = 0;
public State(Puzzle puzzle)
{
board = puzzle.board.clone();
worker_x = puzzle.worker_x;
worker_y = puzzle.worker_y;
selected_x = puzzle.selected_x;
selected_y = puzzle.selected_y;
worker.set(puzzle.worker);
selected.set(puzzle.selected);
bingos = puzzle.bingos;
}
public void restore(Puzzle puzzle)
{
puzzle.board = board;
puzzle.worker_x = worker_x;
puzzle.worker_y = worker_y;
puzzle.selected_x = selected_x;
puzzle.selected_y = selected_y;
puzzle.worker.set(worker);
puzzle.selected.set(selected);
puzzle.bingos = bingos;
}
}

View File

@@ -0,0 +1,111 @@
package org.vostan.banvor.model;
import static org.vostan.banvor.App.TAG;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
public class PuzzleBinLoader {
/*
* Load puzzle from the puzzle.bin.
*/
public static Puzzle loadNthPuzzle( InputStream is, int i )
{try{
//
// Read amount of puzzles.
//
int count = read_i32(is);
if ( i >= count )
return null;
//
// Read the offset.
//
is.skip( i*4 );
int offset = read_i32(is);
//
// Jump to the offset and read the puzzle.
//
is.skip( offset - 4 - (i+1)*4);
Puzzle p = loadPuzzle(is);
//
// Finally return the puzzle and we are done.
//
return p;
}
catch ( java.lang.Exception e )
{
//Log.d(TAG, "Exception: " + e.getMessage() );
e.printStackTrace();
return null;
}}
//
// Retrive amount of puzzles.
//
public static int getCount(InputStream is)
{try{
int count = read_i32(is);
return count;
}
catch ( java.lang.Exception e )
{
//Log.d(TAG, "Exception: " + e.getMessage() );
e.printStackTrace();
return 0;
}}
private static Puzzle loadPuzzle( InputStream is )
{ try{
int cols = is.read();
int rows = is.read();
int [] board = new int[cols*rows];
byte [] b = new byte [cols*rows];
is.read( b, 0, cols*rows);
for( int i = 0; i < b.length; ++i )
{
switch( b[i] )
{
case 0:
board[i] = Puzzle.FLOOR;
break;
case 1:
board[i] = Puzzle.WALL;
break;
case 2:
board[i] = Puzzle.BOX;
break;
case 3:
board[i] = Puzzle.GOAL;
break;
case 4:
board[i] = Puzzle.BINGO;
break;
case 5:
board[i] = Puzzle.WORKER;
break;
default:
}
}
return new Puzzle(cols, rows, board);
}
catch ( Exception e )
{
Log.d( TAG, "load()", e);
return null;
}}
//
// Helper function to read little endian 32bit integer.
//
private static int read_i32( InputStream is ) throws IOException
{
int i = is.read();
i |= is.read() << 8;
i |= is.read() << 16;
i |= is.read() << 24;
return i;
}
}

View File

@@ -15,63 +15,20 @@ public class PuzzleContainer implements IPuzzleSource
* Load puzzle from the puzzle.bin.
*/
public Puzzle getPuzzle( int i )
{try{
InputStream is = theApp().getResources().openRawResource(R.raw.puzzles);
//
// Read amount of puzzles.
//
count = read_i32(is);
if ( i >= count )
return null;
//
// Read the offset.
//
is.skip( i*4 );
int offset = read_i32(is);
//
// Jump to the offset and read the puzzle.
//
is.skip( offset - 4 - (i+1)*4);
Puzzle p = new Puzzle( is );
//
// Finally return the puzzle and we are done.
//
return p;
}
catch ( java.lang.Exception e )
{
//Log.d(TAG, "Exception: " + e.getMessage() );
e.printStackTrace();
return null;
}}
InputStream is = theApp().getResources().openRawResource(R.raw.puzzles);
return PuzzleBinLoader.loadNthPuzzle(is, i);
}
//
// Retrive amount of puzzles.
// Retrieve amount of puzzles.
//
public int getCount()
{try{
{
if ( count == 0 )
{
InputStream is = theApp().getResources().openRawResource(R.raw.puzzles);
count = read_i32(is);
count = PuzzleBinLoader.getCount(is);
}
return count;
}
catch ( java.lang.Exception e )
{
//Log.d(TAG, "Exception: " + e.getMessage() );
e.printStackTrace();
return 0;
}}
//
// Helper function to read little endian 32bit integer.
//
protected int read_i32( InputStream is ) throws IOException
{
int i = is.read();
i |= is.read() << 8;
i |= is.read() << 16;
i |= is.read() << 24;
return i;
}
}

View File

@@ -0,0 +1,89 @@
package org.vostan.banvor.model;
import static java.lang.Math.abs;
public class XYPair {
public static final XYPair UP = new XYPair(0, 1);
public static final XYPair DOWN = new XYPair(0, -1);
public static final XYPair LEFT = new XYPair(-1, 0);
public static final XYPair RIGHT = new XYPair(1, 0);
public static final XYPair ZERO = new XYPair(0, 0);
private int _x = 0;
private int _y = 0;
public XYPair(int x, int y){
_x = x;
_y = y;
}
public XYPair(XYPair op){
_x = op.x();
_y = op.y();
}
public int x(){
return _x;
}
public int y(){
return _y;
}
public void set(XYPair xy){
_x = xy.x();
_y = xy.y();
}
public static XYPair add(XYPair op1, XYPair op2){
return new XYPair(op1._x+op2._x, op1._y+op2._y);
}
public static XYPair sub(XYPair op1, XYPair op2){
return new XYPair(op1._x-op2._x, op1._y-op2._y);
}
public static XYPair mul(XYPair op1, int scaliar){
return new XYPair(op1._x*scaliar, op1._y*scaliar);
}
public XYPair mul(int scaliar){
return mul(this, scaliar);
}
public XYPair sub(XYPair op){
return sub(this, op);
}
public XYPair add(XYPair op){
return add(this, op);
}
public int l1_norm(){
return abs(_x)+abs(_y);
}
public boolean isEqual(XYPair op){
return _x == op._x && _y == op._y;
}
public boolean isInside(XYPair op1, XYPair op2){
return op1.x() <= this.x() && this.x() < op2.x()
&& op1.y() <= this.y() && this.y() < op2.y();
}
public XYPair left(){
return this.add(LEFT);
}
public XYPair right(){
return this.add(RIGHT);
}
public XYPair up(){
return this.add(UP);
}
public XYPair down(){
return this.add(DOWN);
}
}

View File

@@ -1,17 +0,0 @@
package org.vostan.banvor;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}

View File

@@ -0,0 +1,114 @@
package org.vostan.banvor;
import org.junit.Test;
import org.vostan.banvor.model.XYPair;
import static org.junit.Assert.*;
public class UnitTestXYPair {
public XYPair bottom_left = new XYPair(0, 0);
public XYPair upper_right = new XYPair(10, 10);
public XYPair middle = new XYPair(5, 5);
@Test
public void isInside_bottom_left_is_inside() {
assertTrue(bottom_left.isInside(bottom_left, upper_right));
assertTrue(new XYPair(bottom_left.x(), middle.y()).isInside(bottom_left, upper_right));
assertTrue(new XYPair(middle.x(), bottom_left.y()).isInside(bottom_left, upper_right));
}
@Test
public void isInside_upper_right_is_not_inside() {
assertFalse(upper_right.isInside(bottom_left, upper_right));
assertFalse(new XYPair(upper_right.x(), middle.y()).isInside(bottom_left, upper_right));
assertFalse(new XYPair(middle.x(), upper_right.y()).isInside(bottom_left, upper_right));
}
@Test
public void isInside_middle_is_inside() {
assertTrue(middle.isInside(bottom_left, upper_right));
}
@Test
public void isInside_outside_is_not_inside() {
assertFalse(bottom_left.left().isInside(bottom_left, upper_right));
assertFalse(new XYPair(middle.x(), bottom_left.y()-1).isInside(bottom_left, upper_right));
assertFalse(new XYPair(upper_right.x()+1, middle.y()).isInside(bottom_left, upper_right));
assertFalse(new XYPair(middle.x(), upper_right.y()+1).isInside(bottom_left, upper_right));
}
@Test
public void l1_norne_test() {
assertEquals(6, new XYPair(2,4).l1_norm());
assertEquals(6, new XYPair(-2,4).l1_norm());
assertEquals(6, new XYPair(2,-4).l1_norm());
assertEquals(6, new XYPair(-2,-4).l1_norm());
assertEquals(0, new XYPair(0,0).l1_norm());
}
@Test
public void add_test() {
XYPair c = XYPair.add(new XYPair(2,4), new XYPair(3, 5));
assertEquals(5, c.x());
assertEquals(9, c.y());
c = XYPair.add(new XYPair(-2,4), new XYPair(3, -5));
assertEquals(1, c.x());
assertEquals(-1, c.y());
}
@Test
public void sub_test() {
XYPair c = XYPair.sub(new XYPair(2,9), new XYPair(3, 5));
assertEquals(-1, c.x());
assertEquals(4, c.y());
c = XYPair.sub(new XYPair(-2,-4), new XYPair(3, -5));
assertEquals(-5, c.x());
assertEquals(1, c.y());
}
@Test
public void mul_test() {
XYPair c = XYPair.mul(new XYPair(2,9), 0);
assertEquals(0, c.x());
assertEquals(0, c.y());
c = XYPair.mul(new XYPair(-2,-4), 3);
assertEquals(-6, c.x());
assertEquals(-12, c.y());
c = XYPair.mul(new XYPair(-2,-4), -2);
assertEquals(4, c.x());
assertEquals(8, c.y());
}
@Test
public void left_test() {
XYPair c = new XYPair(5,5).left();
assertEquals(4, c.x());
assertEquals(5, c.y());
}
@Test
public void right_test() {
XYPair c = new XYPair(5,5).right();
assertEquals(6, c.x());
assertEquals(5, c.y());
}
@Test
public void up_test() {
XYPair c = new XYPair(5,5).up();
assertEquals(5, c.x());
assertEquals(6, c.y());
}
@Test
public void down_test() {
XYPair c = new XYPair(5,5).down();
assertEquals(5, c.x());
assertEquals(4, c.y());
}
}

View File

@@ -3,6 +3,10 @@ buildscript {
repositories {
google()
mavenCentral()
//This is needed for SVG->PNG
// maven {
// url "https://plugins.gradle.org/m2/"
// }
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.2"
@@ -10,6 +14,9 @@ buildscript {
def nav_version = "2.3.5"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
//This is needed for SVG->PNG
// classpath 'com.trello:victor:1.1.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}