I have to do a Battleship game. The game should have some kind of AI. The AI CAN place ships with a hard coded pattern, but i want to take things a step further and make the ship placement random.
I have implemented a "trial and error" method, where ships get randomly placed on the field with a random rotation, until the algorithm runs out of either ships to be placed or fields to place the ships in. In the second case the recursive implementation allows to try out other ship/field/rotation combos, until the first condition is reached. So to speak: try all possible ship/rotation/field combo's (picked randomly) until you find one valid one, where all ships are placed.
As you can imagine, this is a terrible effort, when it comes to runtime.
The size of the "board" can be 5x5 up to 30x30 with exactly 30% of the fields beeing occupied by a ship.
Now, Runtime isn't my concern for sizes up to 14x14, but then runtime increases so badly, that i have to think of a way to reduce runtime.
Any suggestions? (I would like to get general thinking advice/idea's, not code)
In case, my explenation wasn't enough: Here's the class that tries to place the ships on the field:
public class RandomShipFactory {
private int size;
private Game game;
private ArrayList<Ship> toBeCheckedShips = new ArrayList<Ship>();
private ArrayList<Position> toBeCheckedPositions = new ArrayList<Position>();
public RandomShipFactory(int size, Game game) {
this.size = size;
this.game = game;
this.toBeCheckedShips.addAll(game.ownBoard.getShips());
for(int i = 0; i < this.size; i++) {
for(int j = 0; j < this.size; j++) {
this.toBeCheckedPositions.add(new Position(i,j));
}
}
}
public void makeRandomShipPlacements() {
this.makeRandomShipPlacements(this.toBeCheckedPositions, this.toBeCheckedShips);
game.ownBoard.printBoard();
}
private boolean makeRandomShipPlacements(ArrayList<Position> currentPositions, ArrayList<Ship> currentShips) {
if(currentPositions.isEmpty()) {
return false;
}
ArrayList<Position> checkedPositionsInThisRun = new ArrayList<Position>();
Random r = new Random();
boolean success = false;
while(!success && !currentShips.isEmpty() && !currentPositions.isEmpty()) {
int randomPositionIndex = r.nextInt(currentPositions.size());
Position randomPosition = currentPositions.remove(randomPositionIndex);
checkedPositionsInThisRun.add(randomPosition);
ArrayList<Ship> currentShipQueue = new ArrayList<Ship>();
currentShipQueue.addAll(currentShips);
while(!success && !currentShipQueue.isEmpty()) {
Ship currentShip = currentShipQueue.remove(0);
boolean shouldRotate = r.nextBoolean();
if(shouldRotate) {
currentShip.rotate();
}
boolean canPlaceShip = this.game.placeShipOnOwnBoard(currentShip, randomPosition.x, randomPosition.y);
if(canPlaceShip) {
currentShips.remove(currentShip);
ArrayList<Position> boxPositions = removeBoxSquarePositions(currentPositions, currentShip, randomPosition);
if(currentShips.isEmpty()) {
success = true;
}else {
boolean recursiveSuccess = this.makeRandomShipPlacements(currentPositions, currentShips);
if(!recursiveSuccess) {
this.game.removeShipFromOwnBoard(currentShip);
currentPositions.addAll(boxPositions);
currentShips.add(currentShip);
}else {
success = true;
}
}
}else {
currentShip.rotate();
canPlaceShip = this.game.placeShipOnOwnBoard(currentShip, randomPosition.x, randomPosition.y);
if(canPlaceShip) {
currentShips.remove(currentShip);
ArrayList<Position> boxPositions = removeBoxSquarePositions(currentPositions, currentShip, randomPosition);
if(currentShips.isEmpty()) {
success = true;
}else {
boolean recursiveSuccess = this.makeRandomShipPlacements(currentPositions, currentShips);
if(!recursiveSuccess) {
this.game.removeShipFromOwnBoard(currentShip);
currentPositions.addAll(boxPositions);
currentShips.add(currentShip);
}else {
success = true;
}
}
}
}
}
}
currentPositions.addAll(checkedPositionsInThisRun);
return success;
}
private ArrayList<Position> removeBoxSquarePositions(ArrayList<Position> positionList, Ship ship, Position pos) {
ArrayList<Position> boxPositions = new ArrayList<Position>();
for(int i = 0; i < ship.length + 2; i++) {
for(int j = 0; j < 3; j++) {
int nextX, nextY;
switch(ship.getRotation()) {
case HORIZONTAL:
nextX = pos.x - 1 + i;
nextY = pos.y - 1 + j;
break;
case VERTICAL:
nextX = pos.x - 1 + j;
nextY = pos.y - 1 + i;
break;
default:
nextX = pos.x - 1 + j;
nextY = pos.y - 1 + i;
break;
}
if(nextX >= 0 && nextX < this.size && nextY >= 0 && nextY < this.size) {
Position currentPosition = Position.findPosInList(positionList, nextX, nextY);
if(currentPosition != null) {
positionList.remove(currentPosition);
boxPositions.add(currentPosition);
}
}
}
}
return boxPositions;
}
}
Multiple things to consider here, where improvement can take place:
This point is INVALID, because the OP has actually accounted for that. But this is still a basic thing to keep in mind: NEVER try to hit positions with random addresses. As soon as the field has a few ships, this will decrease speed drastically. In addition, 'random' functions can be really really slow, depending on their implementation
a) Your code is also slow because you search items in linear lists, so in average your additional effort will be (list size) / 2. Use HashMaps or TreeMaps or HeapMaps, or their Set version.
b) Your code uses remove(anyIndex). In ArrayLists, this will start to copy (on average) half of the array data to another position on every call.
And it does not play a role, whether you 'pop' from the very top (index 0) or not.
Worst of all, you use this remove() in loops, exploding your runtime.
Use an unstable removal (order will not be maintained), or LinkedLists, or some Map.
a) instead of targeting random positions, build a map of ALL available spaces (on a 30x30 board, this will add 900 entries to the hashmap).
The advantage of this approach is that build-up is slow, but all other operations are linear or minimized.
have a parameter for the size of a ship
create the HashMap (should be its own method, because we might need this at different locations in the code):
iterate over all positions, add the free ones (where you could validly place a ship of required length and rotation) to a hashmap, use coordinates as key
alternatively, if you expect coverage to be less than 85%, add ALL positions, then iterate over already placed ships and remove their positions from the HashMap
placement collisions for future placements would be possible at this point, but get removed once we hit (y)
place the ships:
x) selecting a random 'index' from a hashmap is a little challenge here, but you'll figure that out
y) once you start placing ships, remove all affected positions from the hashmap (include the ship's length and orientation in your calculations)
resume at (x)
b) extension of that algorithm - improvement for differing sizes
if you run this algorithm for ships with differing sizes, calculate the available positions for the longest ships first, then you can also re-use the created hashmap for shorter ones. if the hashmap is empty but there are still ships remaining: when you run the 'create hashmap of valid positions' algorithm again, adjust/reduce the searched ship length to the currently required one. (This then will not be 'truly random' anymore, but random enough that no player will ever realize it. But in cryptography for example, this here would break the algorithm)
this will greatly increase speed while coverage is below ~80%
A little hint on switches: use fall-through for combined code blocks:
Your code is
switch(ship.getRotation()) {
case HORIZONTAL:
nextX = pos.x - 1 + i;
nextY = pos.y - 1 + j;
break;
case VERTICAL:
nextX = pos.x - 1 + j;
nextY = pos.y - 1 + i;
break;
default:
nextX = pos.x - 1 + j;
nextY = pos.y - 1 + i;
break;
}
it can easily be reduced to
switch(ship.getRotation()) {
case HORIZONTAL:
nextX = pos.x - 1 + i;
nextY = pos.y - 1 + j;
break;
case VERTICAL: // fall-through to default, as VERTICAL seems to be the default^^
default:
nextX = pos.x - 1 + j;
nextY = pos.y - 1 + i;
break;
}
Related
To preface, I'll elaborate a bit more about what the current project is. I'm working on a game that puts the player in a room-based dungeon. Rooms are entered/exited through doors.
Here is the generator() method I have for my floor creator.
The turn(int, int, int) method takes in a direction (randomly created number 0 - 3), and two starting locations, and x and y coordinate, and returns an array that contains the previous numbers, but depending on the direction, one was modified by increment/decrements of one. For example, if the direction was 0, that would mean up, and the yCoordinate would be subtracted by 1. In the array, returned, the first value is he X, the second is the Y.
int roomTag = 1; //tags the room as created
int currentX = startLocation; //the column that navigates the map
int currentY = startLocation;
map[currentY][currentX] = roomTag;
int rooms = 0;
while(rooms < maxRooms) {
int direction = randomRange(0, 4);
currentX = turn(direction, currentX, currentY)[0];
currentY = turn(direction, currentX, currentY)[1];
if(currentX > 0 && currentY > 0 && currentX < maxSize && currentY < maxSize) {
if(map[currentY][currentX] != roomTag) {
map[currentY][currentX] = roomTag;
roomList[currentY][currentX] = new NormalRoom(this, currentX, currentY, rooms);
rooms++;
}
} else {
currentX = startLocation;
currentY = startLocation;
}
}
roomList[startLocation][startLocation] = new StartRoom(this, startLocation, startLocation, 0);
As of now, the procedural generation works great. It generates the dungeon differently each time, and all rooms are connected horizontally or vertically to at least one other room.
Rooms simply hold the information about the doors, location, and enemies, if applicable. Rooms have a method for searching for which doors to create. Here is the method that is called upon instantiation of the room. It is called first to get the amount of doors that are to be created, then sets the boolean created to true, then runs the method again to create/add the doors to the array.
int doorIndex = doorCount - 1;
int[][] tempMap = floor.getMap();
if(yLocation > 0) {
for(int i = 0; i < tempMap.length; i++) {
for(int j = 0; j < tempMap.length; j++) {
if(i == tempMap.length/2 && j == tempMap.length/2) System.out.print("() ");
else if(tempMap[j][i] == 1) System.out.print("[] ");
else System.out.print(" ");
}
System.out.println(" ");
}
if(tempMap[yLocation - 1][xLocation] == 1) {
System.out.println("UP DOOR IN: (" + yLocation + ", " + xLocation + ")");
if(created) {
door[doorIndex] = new Door(0);
doorIndex--;
} else {
doorCount++;
}
}
}
if(yLocation < floor.getMap().length - 1) {
if(tempMap[yLocation + 1][xLocation] == 1) {
System.out.println("DOWN DOOR IN: (" + yLocation + ", " + xLocation + ")");
if(created) {
door[doorIndex] = new Door(2);
doorIndex--;
} else {
doorCount++;
}
}
}
if(xLocation < floor.getMap().length - 1) {
if(tempMap[yLocation][xLocation + 1] == 1) {
System.out.println("RIGHT DOOR IN: (" + yLocation + ", " + xLocation + ")");
if(created) {
door[doorIndex] = new Door(1);
doorIndex--;
} else {
doorCount++;
}
}
}
if(xLocation > 0) {
if(tempMap[yLocation][xLocation - 1] == 1) {
System.out.println("LEFT DOOR IN: (" + yLocation + ", " + xLocation + ")");
if(created) {
door[doorIndex] = new Door(3);
} else {
doorCount++;
}
}
}
if(created) {
for(int i = 0; i < door.length; i++) {
door[i].instantiate();
}
}
The problem seems to be that it never creates left doors, and rarely creates up doors. I cannot seem to find out why it is doing this. I have been stuck on this for awhile now and cannot seem to find what is causing it, as I cannot find any consistencies in the issue.
I leaned on the side of posting less code, but if more is needed, let me know.
Thanks in advance.
Edit: I may have found the solution. Given the fact the method is called during instantiation of the rooms, it cannot find the other rooms as they don't exist yet. The rooms can only create doors for rooms that were created before it. If they were created after, the map would't have them listed as existing yet.
I will try to amend the problem with this in mind.
EDIT 2: This was the problem. Creating the rooms after creating the map fixed it. I simply made it so the room creator was separate from the map creator, then I created the rooms based on the map.
The problem was that I was instantiating the rooms as I was creating them, and part of the rooms' instantiation was finding the amount of and the location of the doors. The problem with this is that I was asking the algorithm to find rooms that weren't created yet.
To fix this, I used the map I created earlier, that was a 2DArray, with the rooms marked with 1s, and everything else marked with a 0. After the map was fully created, I iterated through the map, and put a Room in the coordinates marked with a 1 in a separate 2DArray that contained Room objects.
The doors now accurately lead to new rooms as they are supposed to.
I am trying to save the method outOfBounds which is called inside the lengthOfColor method more than once to a local variable, so that less processing power is used. I provided the lengthOfColor method in which I want to store the variable, and I also provided the outOfBounds method. As you can see the outOfBounds method is a boolean and I am not sure how to store it with integer parameters.
private Integer[] lengthOfColor(int col, boolean color, int pattern, int row) {
int x = 0;
int y = 0;
if (pattern == 1) {
// vertical pattern
y = 1;
} else if (pattern == 2) {
// horizontal pattern
x = 1;
} else if (pattern == 3) {
// diagonal slope left pattern
x = 1;
y = 1;
} else {
// diagonal slope right pattern
x = 1;
y = -1;
}
// length = how many neighbor slots are of same color
// possible equals number of slots, that you can play off of.
// whichSide = left or right if horizontal and top or bottom if vertical.
int length = 0;
int possible = 0;
Integer[] whichSide = new Integer[]{1, -1};
for (int side : whichSide) {
int i = 1;
boolean complete = false;
//while complete is false continue the loop
while (!complete) {
//mainX == horizontal pattern distance
//mainY == vertical pattern distance
int mainX = x * i * side;
int mainY = y * i * side;
//if still inbounds and if the same slot is filled and it matches the color, increment length
if (!outOfBounds(col, mainX, mainY, row) && getIsFilled(col, mainX, mainY, row) &&
checkColor(col, mainX, mainY, row) == color)
{
length++;
}
//if still inbounds and if the same slot is empty, increment possible number of spots and change complete to true
else if (!outOfBounds(col, mainX, mainY, row) && !getIsFilled(col, mainX, mainY, row) &&
getLowestEmptyIndex(myGame.getColumn(col + mainX)) == getLowestEmptyIndex(myGame.getColumn(col)) + mainY - row)
{
possible++;
complete = true;
}
//finish the statement to avoid a infinite loop if neither conditions are met.
else
{
complete = true;
}
// If not complete, then check one slot further.
i = i + 1;
}
}
return new Integer[] {length, possible};
}
private boolean outOfBounds(int col, int x , int y, int row)
{
int currentX = col;
int currentY = getLowestEmptyIndex(myGame.getColumn(col)) - row;
return currentX + x >= myGame.getColumnCount() || currentY + y >= myGame.getRowCount() || currentX + x < 0 || currentY + y < 0;
}
I see that mainX and mainY change values so there isn't any real optimization that can be done outside of the for and while loop besides creating a boolean value that holds the result of outOfBounds before the if check is called which would reduce the number of operations you need to do. To be honest, the optimization is so insignificant that it wouldn't really matter but would be good coding practice I suppose (JIT might optimize for you as well depending on your code). More importantly the method reduces the extra lines of code you need to type and does not necessarily mean that there is less computing.
So something like this before any outOfBounds call but inside the while loop,
boolean outOfBounds = outOfBounds(col, mainX, mainY, row);
and change your current if(!outOfBounds(col, mainX, mainY, row) && ....) into if (!outOfBounds && ...)
Also the #1 rule to optimization is to not optimize until you are done with your project and notice a significant performance dip. In which case you would start with the biggest bottleneck until the optimal performance is gained. Of course this does not mean coding in an incorrect way which would of course create unnecessary performance losses. In those cases it would also be wise to consider whether or not you are looking at the problem the right way rather than micro-optimizing.
Here's a snippet of what I would do to micro-optimize the code shown.
private Integer[] lengthOfColor(int col, boolean color, int pattern, int row) { // consider changing Integer[] into
// int[] if you don't need a boxed integer. It will increase performance
int x = 0;
int y = 0;
// length = how many neighbor slots are of same color
// possible equals number of slots, that you can play off of.
// whichSide = left or right if horizontal and top or bottom if vertical.
int length = 0;
int possible = 0;
switch (pattern) { // switch may be a tad faster but insignificant. More importantly it provides clarity.
case 1:
y = 1;
break;
case 2:
x = 1;
break;
case 3:
x = 1;
y = 1;
break;
default:
x = 1;
y = -1;
break;
}
//int[] whichSide = new int[]{1, -1}; // changed to int[] because you don't need a boxed primitive from what is
// shown
// nevermind, this line isn't needed and you will be able to avoid an instantiation.
for (int i = 1; i != -3; i-=2) {
int count = 1;
int mainX; // bring this to a higher scope. (honestly this is micro optimization but a habit of mine if this is
// can be considered in scope)
int mainY;
boolean outOfBounds = false;
//boolean complete = false; // removed as its unnecessary to break out of the while loop.
//while complete is false continue the loop
while (true) {
//mainX == horizontal pattern distance
//mainY == vertical pattern distance
mainX = x * count * i;
mainY = y * count * i;
outOfBounds = outOfBounds(col, mainX, mainY, row);
//if still inbounds and if the same slot is filled and it matches the color, increment length
if (!outOfBounds && getIsFilled(col, mainX, mainY, row) &&
checkColor(col, mainX, mainY, row) == color) {
length++;
}
//if still inbounds and if the same slot is empty, increment possible number of spots and change complete to
// true
else if (!outOfBounds && !getIsFilled(col, mainX, mainY, row) &&
getLowestEmptyIndex(myGame.getColumn(col + mainX)) == getLowestEmptyIndex(myGame.getColumn(col)) + mainY -
row) {
possible++;
break;
}
//finish the statement to avoid a infinite loop if neither conditions are met.
else {
break;
}
// If not complete, then check one slot further.
count++;
}
}
return new Integer[]{length, possible}; // once again consider whether or not you need a boxed integer
}
private boolean outOfBounds(int col, int x, int y, int row) {
//int currentX = col; this is an unnecessary line
int currentY = getLowestEmptyIndex(myGame.getColumn(col)) - row;
return col + x >= myGame.getColumnCount() || currentY + y >= myGame.getRowCount() || col + x < 0 ||
currentY + y < 0;
}
I got a project in my Java class which I'm having trouble with.
The project is basically marking coordinates on the screen, making a (complex) polynomial out of them, then solving the polynomial with Newton's method using random guesses and drawing the path of the guesses on the screen.
I don't have a problem with any of the drawing, marking, etc.
But for some reason, my Newton's method algorithm randomly misses roots. Sometimes it hits none of them, sometimes it misses one or two. I've been changing stuff up for hours now but I couldn't really come up with a solution.
When a root is missed, usually the value I get in the array is either converging to infinity or negative infinity (very high numbers)
Any help would be really appreciated.
> // Polynomial evaluation method.
public Complex evalPoly(Complex complexArray[], Complex guess) {
Complex result = new Complex(0, 0);
for (int i = 0; i < complexArray.length; i++) {
result = result.gaussMult(guess).addComplex(complexArray[complexArray.length - i - 1]);
}
return result;
}
> // Polynomial differentation method.
public Complex[] diff(Complex[] comp) {
Complex[] result = new Complex[comp.length - 1];
for (int j = 0; j < result.length; j++) {
result[j] = new Complex(0, 0);
}
for (int i = 0; i < result.length - 1; i++) {
result[i].real = comp[i + 1].real * (i + 1);
result[i].imaginary = comp[i + 1].imaginary * (i + 1);
}
return result;
}
> // Method which eliminates some of the things that I don't want to go into the array
public boolean rootCheck2(Complex[] comps, Complex comp) {
double accLim = 0.01;
if (comp.real == Double.NaN)
return false;
if (comp.real == Double.NEGATIVE_INFINITY || comp.real == Double.POSITIVE_INFINITY)
return false;
if (comp.imaginary == Double.NaN)
return false;
if (comp.imaginary == Double.NEGATIVE_INFINITY || comp.imaginary == Double.POSITIVE_INFINITY)
return false;
for (int i = 0; i < comps.length; i++) {
if (Math.abs(comp.real - comps[i].real) < accLim && Math.abs(comp.imaginary - comps[i].imaginary) < accLim)
return false;
}
return true;
}
> // Method which finds (or attempts) to find all of the roots
public Complex[] addUnique2(Complex[] poly, Bitmap bitmapx, Paint paint, Canvas canvasx) {
Complex[] rootsC = new Complex[poly.length - 1];
int iterCount = 0;
int iteLim = 20000;
for (int i = 0; i < rootsC.length; i++) {
rootsC[i] = new Complex(0, 0);
}
while (iterCount < iteLim && MainActivity.a < rootsC.length) {
double guess = -492 + 984 * rand.nextDouble();
double guess2 = -718 + 1436 * rand.nextDouble();
if (rootCheck2(rootsC, findRoot2(poly, new Complex(guess, guess2), bitmapx, paint, canvasx))) {
rootsC[MainActivity.a] = findRoot2(poly, new Complex(guess, guess2), bitmapx, paint, canvasx);
MainActivity.a = MainActivity.a + 1;
}
iterCount = iterCount + 1;
}
return rootsC;
}
> // Method which finds a single root of the complex polynomial.
public Complex findRoot2(Complex[] comp, Complex guess, Bitmap bitmapx, Paint paint, Canvas canvasx) {
int iterCount = 0;
double accLim = 0.001;
int itLim = 20000;
Complex[] diffedComplex = diff(comp);
while (Math.abs(evalPoly(comp, guess).real) >= accLim && Math.abs(evalPoly(comp, guess).imaginary) >= accLim) {
if (iterCount >= itLim) {
return new Complex(Double.NaN, Double.NaN);
}
if (evalPoly(diffedComplex, guess).real == 0 || evalPoly(diffedComplex, guess).imaginary == 0) {
return new Complex(Double.NaN, Double.NaN);
}
iterCount = iterCount + 1;
guess.real = guess.subtractComplex(evalPoly(comp, guess).divideComplex(evalPoly(diffedComplex, guess))).real;
guess.imaginary = guess.subtractComplex(evalPoly(comp, guess).divideComplex(evalPoly(diffedComplex, guess))).imaginary;
drawCircles((float) guess.real, (float) guess.imaginary, paint, canvasx, bitmapx);
}
return guess;
}
> // Drawing method
void drawCircles(float x, float y, Paint paint, Canvas canvasx, Bitmap bitmapx) {
canvasx.drawCircle(x + 492, shiftBackY(y), 5, paint);
coordPlane.setAdjustViewBounds(false);
coordPlane.setImageBitmap(bitmapx);
}
}
Error 1
The lines
guess.real = guess.subtractComplex(evalPoly(comp, guess).divideComplex(evalPoly(diffedComplex, guess))).real;
guess.imaginary = guess.subtractComplex(evalPoly(comp, guess).divideComplex(evalPoly(diffedComplex, guess))).imaginary;
first introduce a needless complication and second introduce an error that makes it deviate from Newton's method. The guess used in the second line is different from the guess used in the first line since the real part has changed.
Why do you not use, like in the evaluation procedure, the complex assignment in
guess = guess.subtractComplex(evalPoly(comp, guess).divideComplex(evalPoly(diffedComplex, guess)));
Error 2 (Update)
In the computation of the differentiated polynomial, you are missing the highest degree term in
for (int i = 0; i < result.length - 1; i++) {
result[i].real = comp[i + 1].real * (i + 1);
result[i].imaginary = comp[i + 1].imaginary * (i + 1);
It should be either i < result.length or i < comp.length - 1. Using the wrong derivative will of course lead to unpredictable results in the iteration.
On root bounds and initial values
To each polynomial you can assign an outer root bound such as
R = 1+max(abs(c[0:N-1]))/abs(c[N])
Using 3*N points, random or equidistant, on or close to this circle should increase the probability to reach each of the roots.
But the usual way to find all of the roots is to use polynomial deflation, that is, splitting off the linear factors corresponding to the root approximations already found. Then a couple of additional Newton steps using the full polynomial restores maximal accuracy.
Newton fractals
Each root has a basin or domain of attraction with fractal boundaries between the domains. In rebuilding a similar situation to the one used in
I computed a Newton fractal showing that the attraction to two of the roots and ignorance of the other two is a feature of the mathematics behind it, not an error in implementing the Newton method.
Different shades of the same color belong to the domain of the same root where brightness corresponds to the number of steps used to reach the white areas around the roots.
I'm trying to write a basic AI for a game where the character collects points on a grid. The points are randomly generated on the grid and my AI just has to move onto that coordinate on the grid.
The AI can't see the full grid however, it can only see a 5x5 grid with it at the center as it moves around each "turn". (So it's at coordinate (2, 2) each turn.)
My algorithm is basic, but it basically scans the 5x5 grid and tries to find the closest point, then it moves the character toward that direction. Then the next turn that point will be even closer (as we just moved even closer to it) so it will choose to keep approaching it until it finds gets to the desired coordinate.
Here it is in Java:
Field[][] fields = gameboard.getFields();
int centerX = 2;
int centerY = 2;
// Large numbers that points will always be closer than
int nearestPointX = 1000;
int nearestPointY = 1000;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
Field field = fields[i][j];
if (field.getType() == 0 && field.hasPoint()) {
int distanceX = Math.abs(i - centerX);
int distanceY = Math.abs(j - centerY);
int totalDistance = distanceX + distanceY;
int totalBestDistance = nearestPointX + nearestPointY;
if (totalDistance < totalBestDistance) {
nearestPointX = i;
nearestPointY = j;
}
}
}
}
Field northField = fields[centerX][centerY-1];
Field southField = fields[centerX][centerY+1];
Field eastField = fields[centerX+1][centerY];
Field westField = fields[centerX-1][centerY];
String move = previousMove;
// If there's a point nearby
if (nearestPointX != 1000) {
if (nearestPointX > centerX) {
move = "E";
} else if (nearestPointX < centerX) {
move = "W";
} else if (nearestPointY > centerY) {
move = "S";
} else if (nearestPointY < centerY) {
move = "N";
}
}
But under certain conditions it will approach an area with multiple points, seemingly get confused and just pulsate back and forth over two blocks. It also seems sporadic with choosing the exact best block sometimes, which I assume is related.
I've read it over a million times and tried to debug it, but I just can't figure out what is causing the bug. Can anyone pinpoint what I'm doing wrong in finding the closest point?
Look at the comparison you're doing. totalDistance is equal to the number of steps from your current location to the point being considered, but totalBestDistance is the in-frame x,y coordinates of the previous "best point". So (for example) if you had points at (2, 4) and (3, 0), your code would:
Evaluate (2, 4) - distanceX is 0, distanceY is 2, totalDistance is 2. This updates
nearestPointX to 2 and nearestPointY to 4.
Evaluate (3, 0) - distanceX is 1, distanceY is 2. totalDistance is 3. totalBestDistance then becomes nearestPointX + nearestPointY, which is 6. So (3, 0) becomes your new "best point", even though your previous one was closer (actual distance was 2).
You problem is in totalBestDistance it should be updated in if and updated with totalDistance value. And declared outside of loops.
int totalBestDistance = MAX_INT;
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
Field field = fields[i][j];
if (field.getType() == 0 && field.hasPoint()) {
int distanceX = Math.abs(i - centerX);
int distanceY = Math.abs(j - centerY);
int totalDistance = distanceX + distanceY;
if (totalDistance < totalBestDistance) {
totalBestDistance = totalDistance;
nearestPointX = i;
nearestPointY = j;
}
}
}
}
I'm trying to make the game Tetris in java.
I've gotten it to the point where:
a new block is generated when it hits the floor or its y+1 is not null (meaning there's another block under it)
public void collisionCheck(int x, int y) {
if (activetile.getY() == this.height-2 || getTileAt(x, y+1) != null) {
activetile = new Tile(this, 0, 0);
}
}
A row clears when the bottom row is full of non-null values, or the Tetris pieces (for y = 4 (the floor), loop through x till x = 4 and check if all non-null)
public void checkBottomFull(int x, int y) {
while (getTileAt(x,y) != null) {
say("(" + x + ", " + y +")");
if (x == 3) {
say("row is full");
//replace full row with tiles from above
for (int i = 0; i < 4; i++) {
for (int j = 5; j > 0; j--) {
grid[j][i] = getTileAt(i,j-1);
grid[j-1][i] = null;
}
}
break;
}
x++;
}
}
Right now, I'm using keys to move the block:
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if(keyCode == KeyEvent.VK_DOWN) {
activetile.setLocation(activetile.getX(), activetile.getY()+1);
System.out.println("coordinates: " + activetile.getX() + ", " + activetile.getY());
collisionCheck(activetile.getX(),activetile.getY());
checkBottomFull(0,4);
repaint();
}
}
There's two issues I'm having:
1) In the picture you'll notice I've dropped the block all the way to the floor... and the row cleared. After it's cleared, it will generate a block to the top left (x=0, y=1) which I have no control over.
2) On the floor there seems to be a red line... which I'm assuming is a row of blocks hidden by the JFrame... I'm not sure why that's there.
FYI: If you're wondering why grid[j][i] has the rows and columns flipped (aka, why it's not grid[i][j]) is because I instantiated it as grid = new Tile[height][width];
Any thoughts?
Thanks!
It is hard to say what is wrong without actually debugging your app.
But maybe try this one:
public void checkBottomFull(int x, int y) {
while (getTileAt(x,y) != null) {
say("(" + x + ", " + y +")");
if (x == 3) {
say("row is full");
//replace full row with tiles from above
for (int i = 0; i < 4; i++) {
for (int j = 4; j >= 0; j--) {
grid[j][i] = getTileAt(i,j-1);
grid[j-1][i] = null;
}
}
break;
}
x++;
}
}
You have 5 rows (indexed from 0 to 4) and 4 columns (indexed from 0 to 3).
What values of height and width do you pass to:
grid = new Tile[height][width];
Because from what I see you should do something like that:
grid = new Tile[5][4];
Bah,
Turns out in the key event, I needed to check if the bottom was full before checking if there is a collision.
I guess what was happening is, when I was checking collisionCheck(activetile.getX(),activetile.getY()); before checkBottomFull(0,4);, when the bottom was full, it would clear the row and set the current row equal to the row above it: grid[j][i] = getTileAt(i,j-1);, the problem was that collisionCheck was generating a new piece and the that newly generated piece was getting cleared and replaced by checkBottomFull.
Putting the collision check after the checkBottomFull ensures that the newly generated piece won't be replaced if bottom is full.