I wrote a program to render a Julia Set. The single threaded code is pretty straightforward and is essentially like so:
private Image drawFractal() {
BufferedImage img = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB);
for (int x = 0; x < WIDTH; x++) {
for (int y = 0; y < HEIGHT; y++) {
double X = map(x,0,WIDTH,-2.0,2.0);
double Y = map(y,0,HEIGHT,-1.0,1.0);
int color = getPixelColor(X,Y);
img.setRGB(x,y,color);
}
}
return img;
}
private int getPixelColor(double x, double y) {
float hue;
float saturation = 1f;
float brightness;
ComplexNumber z = new ComplexNumber(x, y);
int i;
for (i = 0; i < maxiter; i++) {
z.square();
z.add(c);
if (z.mod() > blowup) {
break;
}
}
brightness = (i < maxiter) ? 1f : 0;
hue = (i%maxiter)/(float)maxiter;
int rgb = Color.getHSBColor(hue,saturation,brightness).getRGB();
return rgb;
}
As you can see it is highly inefficient. Thus I went for Parallelizing this code using the fork/join framework in Java and this is what I came up with:
private Image drawFractal() {
BufferedImage img = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB);
ForkCalculate fork = new ForkCalculate(img, 0, WIDTH, HEIGHT);
ForkJoinPool forkPool = new ForkJoinPool();
forkPool.invoke(fork);
return img;
}
//ForkCalculate.java
public class ForkCalculate extends RecursiveAction {
BufferedImage img;
int minWidth;
int maxWidth;
int height;
int threshold;
int numPixels;
ForkCalculate(BufferedImage b, int minW, int maxW, int h) {
img = b;
minWidth = minW;
maxWidth = maxW;
height = h;
threshold = 100000; //TODO : Experiment with this value.
numPixels = (maxWidth - minWidth) * height;
}
void computeDirectly() {
for (int x = minWidth; x < maxWidth; x++) {
for (int y = 0; y < height; y++) {
double X = map(x,0,Fractal.WIDTH,-2.0,2.0);
double Y = map(y,0,Fractal.HEIGHT,-1.0,1.0);
int color = getPixelColor(X,Y);
img.setRGB(x,y,color);
}
}
}
#Override
protected void compute() {
if(numPixels < threshold) {
computeDirectly();
return;
}
int split = (minWidth + maxWidth)/2;
invokeAll(new ForkCalculate(img, minWidth, split, height), new ForkCalculate(img, split, maxWidth, height));
}
private int getPixelColor(double x, double y) {
float hue;
float saturation = 1f;
float brightness;
ComplexNumber z = new ComplexNumber(x, y);
int i;
for (i = 0; i < Fractal.maxiter; i++) {
z.square();
z.add(Fractal.c);
if (z.mod() > Fractal.blowup) {
break;
}
}
brightness = (i < Fractal.maxiter) ? 1f : 0;
hue = (i%Fractal.maxiter)/(float)Fractal.maxiter;
int rgb = Color.getHSBColor(hue*5,saturation,brightness).getRGB();
return rgb;
}
private double map(double x, double in_min, double in_max, double out_min, double out_max) {
return (x-in_min)*(out_max-out_min)/(in_max-in_min) + out_min;
}
}
I tested with a range of values varying the maxiter, blowup and threshold.
I made the threshold such that the number of threads are around the same as the number of cores that I have (4).
I measured the runtimes in both cases and expected some optimization in parallelized code. However the code ran in the same time if not slower sometimes. This has me baffled. Is this happening because the problem size isn't big enough? I also tested with varying image sizes ranging from 640*400 to 1020*720.
Why is this happening? How can I run the code parallely so that it runs faster as it should?
Edit
If you want to checkout the code in its entirety head over to my Github
The master branch has the single threaded code.
The branch with the name Multicore has the Parallelized code.
Edit 2 Image of the fractal for reference.
Here is your code rewritten to use concurrency. I found that my Lenovo Yoga misreported the number of processors by double. Also Windows 10 seems to take up an enormous amount of processing, so the results on my laptop are dubious. If you have more cores or a decent OS, it should be much better.
package au.net.qlt.canvas.test;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
public class TestConcurrency extends JPanel {
private BufferedImage screen;
final Fractal fractal;
private TestConcurrency(final Fractal f, Size size) {
fractal = f;
screen = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
setBackground(Color.BLACK);
setPreferredSize(new Dimension(size.width,size.height));
}
public void test(boolean CONCURRENT) {
int count = CONCURRENT ? Runtime.getRuntime().availableProcessors()/2 : 1;
Scheduler scheduler = new Scheduler(fractal.size);
Thread[] threads = new Thread[count];
long startTime = System.currentTimeMillis();
for (int p = 0; p < count; p++) {
threads[p] = new Thread() {
public void run() {
scheduler.schedule(fractal,screen);
}
};
threads[p].start();
try {
threads[p].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
DEBUG("test threads: %d - elasped time: %dms", count, (System.currentTimeMillis()-startTime));
}
#Override public void paint(Graphics g) {
if(g==null) return;
g.drawImage(screen, 0,0, null);
}
public static void main(String[]args) {
JFrame frame = new JFrame("FRACTAL");
Size size = new Size(1024, 768);
Fractal fractal = new Fractal(size);
TestConcurrency test = new TestConcurrency(fractal, size);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setResizable(false);
frame.add(test);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
for(int i=1; i<=10; i++) {
DEBUG("--- Test %d ------------------", i);
test.test(false);
test.repaint();
test.test(true);
test.repaint();
}
}
public static void DEBUG(String str, Object ... args) { Also System.out.println(String.format(str, args)); }
}
class Fractal {
ComplexNumber C;
private int maxiter;
private int blowup;
private double real;
private double imaginary;
private static double xs = -2.0, xe = 2.0, ys = -1.0, ye = 1.0;
public Size size;
Fractal(Size sz){
size = sz;
real = -0.8;
imaginary = 0.156;
C = new ComplexNumber(real, imaginary);
maxiter = 400;
blowup = 4;
}
public int getPixelColor(Ref ref) {
float hue;
float saturation = 1f;
float brightness;
double X = map(ref.x,0,size.width,xs,xe);
double Y = map(ref.y,0,size.height,ys,ye);
ComplexNumber Z = new ComplexNumber(X, Y);
int i;
for (i = 0; i < maxiter; i++) {
Z.square();
Z.add(C);
if (Z.mod() > blowup) {
break;
}
}
brightness = (i < maxiter) ? 1f : 0;
hue = (i%maxiter)/(float)maxiter;
return Color.getHSBColor(hue*5,saturation,brightness).getRGB();
}
private double map(double n, double in_min, double in_max, double out_min, double out_max) {
return (n-in_min)*(out_max-out_min)/(in_max-in_min) + out_min;
}
}
class Size{
int width, height, length;
public Size(int w, int h) { width = w; height = h; length = h*w; }
}
class ComplexNumber {
private double real;
private double imaginary;
ComplexNumber(double a, double b) {
real = a;
imaginary = b;
}
void square() {
double new_real = Math.pow(real,2) - Math.pow(imaginary,2);
double new_imaginary = 2*real*imaginary;
this.real = new_real;
this.imaginary = new_imaginary;
}
double mod() {
return Math.sqrt(Math.pow(real,2) + Math.pow(imaginary,2));
}
void add(ComplexNumber c) {
this.real += c.real;
this.imaginary += c.imaginary;
}
}
class Scheduler {
private Size size;
private int x, y, index;
private final Object nextSync = 4;
public Scheduler(Size sz) { size = sz; }
/**
* Update the ref object with next available coords,
* return false if no more coords to be had (image is rendered)
*
* #param ref Ref object to be updated
* #return false if end of image reached
*/
public boolean next(Ref ref) {
synchronized (nextSync) {
// load passed in ref
ref.x = x;
ref.y = y;
ref.index = index;
if (++index > size.length) return false; // end of the image
// load local counters for next access
if (++x >= size.width) {
x = 0;
y++;
}
return true; // there are more pixels to be had
}
}
public void schedule(Fractal fractal, BufferedImage screen) {
for(Ref ref = new Ref(); next(ref);)
screen.setRGB(ref.x, ref.y, fractal.getPixelColor(ref));
}
}
class Ref {
public int x, y, index;
public Ref() {}
}
Currently i'm using LWJGL to make a 2D game. But I've run into an issue. I am trying to make an "infinite world" but an issue comes up because if I create a lot of objects the frames drop and I believe it has to do with the amount of textures being loaded to objects.
Currently I'm creating my world in a grid fashion like this
for(int x = -WORLDWIDTH; x < WORLDWIDTH; x+= BW)
{
for(int y = -WORLDHEIGHT; y < WORLDHEIGHT; y += BH)
{
Tile newTile = new Tile(x,y,BW,BH,TileType.Grass);
tiles.add(newTile);
}
}
Where BW and BH are the block width and height. I try to keep texture loading to only objects on screen using this if statement
if(x >= 0-BW && x + width < WIDTH+BW && y >= 0-BH && y+ height < HEIGHT+BH)
{
DrawQuadTexRotate(getTexture(), getX(), getY(), getWidth(),getHeight(),angle);
}
To load textures I do this
public static void DrawQuadTexRotate(Texture tex, float x, float y, float width, float height, float angle)
{
tex.bind();
glTranslatef(x + width / 2, y + height / 2, 0);
glRotatef(angle, 0 ,0 , 1);
glTranslatef(- width / 2, - height / 2, 0);
glBegin(GL_QUADS);
glTexCoord2f(0,0);
glVertex2f(0,0);
glTexCoord2f(1,0);
glVertex2f(width,0);
glTexCoord2f(1,1);
glVertex2f(width,height);
glTexCoord2f(0,1);
glVertex2f(0,height);
glEnd();
glLoadIdentity();
}
public static Texture LoadTexture(String path, String fileType)
{
Texture tex = null;
InputStream in = ResourceLoader.getResourceAsStream(path);
try {
tex = TextureLoader.getTexture(fileType, in);
} catch (IOException e) {
e.printStackTrace();
}
return tex;
}
The Tile class basically has methods to do certain things based on what type it is. It holds the width, height, x, y and texture which my DrawQuadTex method uses
Here is the tile class
public class Tile {
public float x, y, width, height, angle;
private Texture texture;
private TileType type;
public long timeStart = 0;
public int filled = 1;
public int power = 0;
public int transmitPower = 0;
int connected = 0;
public boolean moveRight,moveLeft,moveUp,moveDown,moveAngle;
public Tile(float x, float y, float width, float height,TileType type)
{
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.type = type;
this.texture = QuickLoad(type.textureName);
timeStart = Timer.getTime();
updateType();
updateFill();
}
public void draw()
{
if(x >= 0-BW && x + width < WIDTH+BW && y >= 0-BH && y+ height < HEIGHT+BH)
{
DrawQuadTexRotate(getTexture(), getX(), getY(), getWidth(),getHeight(),angle);
}
}
public void addFood()
{
if(this.type == TileType.Farm && filled == 1)
{
long timing = Timer.getTime();
if(timing >= (timeStart + 10000))
{
Values.food += (1+Artist.riverAmount);
timeStart = timing;
}
}
}
public void mineOre()
{
if(this.type == TileType.Mine && filled == 1)
{
long timing = Timer.getTime();
if(timing >= (timeStart + 20000))
{
for(int i = 0; i < tiles.size();i++)
{
if(tiles.get(i).getType() == TileType.Ore)
{
tiles.get(i).setType(TileType.LandTile);
Values.oreAmount += 1;
timeStart = timing;
break;
}
}
}
}
}
public void move()
{
if(moveRight == true)
{
x -= 10;
}
if(moveLeft == true)
{
x += 10;
}
if(moveUp == true)
{
y += 10;
}
if(moveDown == true)
{
y -= 10;
}
}
public void testPower()
{
for(int i = 0; i < tiles.size(); i++)
{
if(tiles.get(i).x >= x - BW && tiles.get(i).x <= x + BW && tiles.get(i).y >= y - BH && tiles.get(i).y <= y + BH && tiles.get(i).getType() == TileType.Wire && tiles.get(i).transmitPower == 1 && type == TileType.HomeBasic)
{
power = 1;
break;
}
else
{
power = 0;
}
if(tiles.get(i).x >= x - BW && tiles.get(i).x <= x + BW && tiles.get(i).y >= y - BH && tiles.get(i).y <= y + BH && (tiles.get(i).transmitPower == 1 || tiles.get(i).type == TileType.PowerPlant) && type == TileType.Wire)
{
transmitPower = 1;
break;
}
else if(tiles.get(i).type == TileType.Wire && tiles.get(i).transmitPower == 0)
{
transmitPower = 0;
}
else
{
transmitPower = 0;
}
}
}
public void updateFill()
{
if(type == TileType.HomeBasic || type == TileType.Farm || type == TileType.Mine)
{
filled = 0;
}
}
public void stats()
{
if(type == TileType.HomeBasic)
{
//wt.drawString((int)x, (int)(y-25), Integer.toString(filled), Color.white);
}
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public float getWidth() {
return width;
}
public void setWidth(float width) {
this.width = width;
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
}
public Texture getTexture() {
return texture;
}
public void setTexture(Texture texture) {
this.texture = texture;
}
public TileType getType() {
return type;
}
public void setType(TileType type) {
this.type = type;
updateType();
}
public void updateType()
{
this.texture = QuickLoad(type.textureName);
timeStart = Timer.getTime();
updateFill();
}
}
public static Texture QuickLoad(String name)
{
Texture tex = null;
tex = LoadTexture("res/" + name + ".png", "PNG");
return tex;
}
So how would I go about trying to fix this?
I am a self taught programmer and I am coding Screen Snake for fun. I am using not using integers to store the position of the snake or apples, I am using doubles. I am having an issue when the snake goes through the apple. When the collide, the code does not register that it collided. I am assuming that this is because their X and Y values might be like .1 off. I have been trying to fix this for 2 weeks but have not been able to. Sorry if my code is a bit messy. I don't know exactly what you guys need from the code so I posted all of it. Also I really appreciate the help! Thanks!!
Main class:
Random random = new Random();
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
double ScreenW = screenSize.getWidth();
double ScreenH = screenSize.getHeight();
int ScreenX = (int)Math.round(ScreenW);
int ScreenY = (int)Math.round(ScreenH);
JFrame frame = new JFrame();
double x = 1, y = 1;
int size = 5;
int ticks;
private int columnCount = 25;
private int rowCount = 15;
double a = (ScreenW / columnCount) - 1;
double b = (ScreenH / rowCount) - 1;
private Key key;
private List<Rectangle2D> cells;
private Point selectedCell;
boolean up = false;
boolean down = false;
boolean right = true;
boolean left = false;
boolean running = true;
private Thread thread;
private BodyP p;
private ArrayList<BodyP> snake;
private Apple apple;
private ArrayList<Apple> apples;
double width = screenSize.width;
double height = screenSize.height;
double cellWidth = width / columnCount;
double cellHeight = height / rowCount;
double xOffset = (width - (columnCount * cellWidth)) / 2;
double yOffset = (height - (rowCount * cellHeight)) / 2;
public Max_SnakeGame() throws IOException {
System.out.println(screenSize);
System.out.println(a + "," + b);
System.out.println(ScreenH + b);
System.out.println(ScreenW + a);
frame.getContentPane().add(new Screen());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setUndecorated(true);
frame.setBackground(new Color(0, 0, 0, 0));
frame.setLocationRelativeTo(null);
frame.setMaximumSize(screenSize);
frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
frame.setVisible(true);
Image img = Toolkit
.getDefaultToolkit()
.getImage(
"C:/Users/Max/My Documents/High School/Sophomore year/Graphic Disign/People art/The Mods Who Tell Pointless Stories.jpg");
frame.setIconImage(img);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
new Max_SnakeGame();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
public class Screen extends JPanel implements Runnable {
private static final long serialVersionUID = 1L;
public Screen() {
key = new Key();
addKeyListener(key);
setMaximumSize(screenSize);
setOpaque(false);
setBackground(new Color(0, 0, 0, 0));
setFocusable(true);
snake = new ArrayList<BodyP>();
apples = new ArrayList<>();
start();
}
public void start() {
running = true;
thread = new Thread(this);
thread.start();
}
public void run() {
while (running) {
MoveUpdate();
repaint();
}
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
repaint();
Graphics2D g2d = (Graphics2D) g.create();
cells = new ArrayList<>(columnCount * rowCount);
if (cells.isEmpty()) {
for (int row = 0; row < rowCount; row++) {
for (int col = 0; col < columnCount; col++) {
Rectangle2D cell = new Rectangle2D.Double(xOffset
+ (col * cellWidth), yOffset
+ (row * cellHeight), cellWidth, cellHeight);
cells.add(cell);
}
}
}
g2d.setColor(Color.GRAY);
for (Rectangle2D cell : cells) {
g2d.draw(cell);
}
for (int i = 0; i < snake.size(); i++) {
snake.get(i).draw(g);
}
for (int i = 0; i < apples.size(); i++) {
apples.get(i).draw(g);
}
}
}
private class Key implements KeyListener {
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_RIGHT && !left) {
up = false;
down = false;
right = true;
}
if (keyCode == KeyEvent.VK_LEFT && !right) {
up = false;
down = false;
left = true;
}
if (keyCode == KeyEvent.VK_UP && !down) {
left = false;
right = false;
up = true;
}
if (keyCode == KeyEvent.VK_DOWN && !up) {
left = false;
right = false;
down = true;
}
}
#Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
#Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
}
}
public void MoveUpdate() {
if (snake.size() == 0) {
p = new BodyP(x, y, a, b);
snake.add(p);
}
if (apples.size() == 0){
double x1 = random.nextInt(25);
double Ax = ((x1*a+x1+1)*10)/10;
double y1 = random.nextInt(15);
double Ay = ((y1*b+y1+1)*10)/10;
double Afx = Math.round(Ax);
double Afy = Math.round(Ay);
System.out.println("Ax:"+Afx);
System.out.println("Ay:"+Afy);
apple = new Apple(Ax, Ay, a, b);
apples.add(apple);
}
for(int i = 0; i < apples.size(); i++) {
if(Math.round(x)-1 == apples.get(i).getx() || Math.round(x) == apples.get(i).getx() && Math.round(y)== apples.get(i).gety() || Math.round(y)-1 == apples.get(i).gety()) {
size++;
apples.remove(i);
i--;
}
}
ticks++;
if (ticks > 2500000) {
if (up == true) {
if (y <= 2) {
y = ScreenH - b;
System.out.println("Y:" + y);
} else {
y -= b + 1;
System.out.println("Y:" + y);
}
}
// down loop
else if (down == true) {
if (y >= ScreenH - b) {
y = 1;
System.out.println("Y:" + y);
}
else {
y += b + 1;
System.out.println("Y:" + y);
}
}
// left loop
else if (left == true) {
if (x <= 1) {
x = ScreenW - a;
System.out.println("X:" + x);
}
else {
x -= a + 1;
System.out.println("X:" + x);
}
}
// right loop
else if (right == true) {
if (x >= ScreenW - a) {
x = 1;
System.out.println("X:" + x);
}
else {
x += a + 1;
System.out.println("X:" + x);
}
}
ticks = 0;
p = new BodyP(x, y, a, b);
snake.add(p);
// rect.setFrame(x, y, a, b);
if (snake.size() > size) {
snake.remove(0);
}
}
}
}
Snake class:
public class BodyP {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
double ScreenW = screenSize.getWidth();
double ScreenH = screen`enter code here`Size.getHeight();
double x = 1, y = 1;
private int columnCount = 25;
private int rowCount = 15;
double a = (ScreenW / columnCount) - 1;
double b = (ScreenH / rowCount) - 1;
public BodyP(double x, double y, double a, double b) {
this.x = x;
this.y = y;
this.a = a;
this.b = b;
}
public void MoveUpdate(){
}
public void draw(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
Rectangle2D rect = new Rectangle2D.Double(x, y, a, b);
g.setColor(Color.BLACK);
g2.fill(rect);
}
public double getx() {
return x;
}
public void setx(double x) {
this.x = x;
}
public double gety() {
return y;
}
public void sety(double y) {
this.y = y;
}
}
Apple class:
public class Apple {
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
double ScreenW = screenSize.getWidth();
double ScreenH = screenSize.getHeight();
double x = 1, y = 1;
private int columnCount = 25;
private int rowCount = 15;
double a = (ScreenW / columnCount) - 1;
double b = (ScreenH / rowCount) - 1;
public Apple(double x, double y, double a, double b) {
this.x = x;
this.y = y;
this.a = a;
this.b = b;
}
public void MoveUpdate(){
}
public void draw(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
Rectangle2D rect = new Rectangle2D.Double(x, y, a, b);
g.setColor(Color.RED);
g2.fill(rect);
}
public double getx() {
return x;
}
public void setx(double x) {
this.x = x;
}
public double gety() {
return y;
}
public void sety(double y) {
this.y = y;
}
}
If you think this is due rounding errors, use Euclidean distance and compare with the desired tolerance:
final double tolerance = 1.0; // or whatsoever
double dx = snake.x - apple.x;
double dy = snake.y - apple.y;
if ( dx*dx + dy*dy < tolearance * tolerance ) ...
I suggest to implement something like Point.distanceTo(Point) method to make this convenient.
So I have code that runs, but I need to write the test for one of the classes (Cells) and I'm not sure how to go about this.
Typically, I'd have a helper class at the start of CellsTest, which would encapsulate the constructor call, which relies on the specific class name of the class that implements the overall program. This helper class would return an instance of that program.
So, what I'd think is that I'd need to have something like:
private Maze createMaze() {
return new Maze();
}
But then, my test methods can't access the Cell methods, just the Maze methods...So I'm guessing that that is wrong, and I'm not sure how I should actually be doing this.
//Maze.java
package falstad;
import java.awt.*;
import java.util.ArrayList;
import java.util.Iterator;
/**
* Class handles the user interaction for the maze.
*/
//public class Maze extends Panel {
public class Maze {
final private ArrayList<Viewer> views = new ArrayList<Viewer>() ;
MazePanel panel ; // graphics to draw on, shared by all views
private int state;
private int percentdone = 0;
private boolean showMaze;
private boolean showSolution;
private boolean solving;
private boolean mapMode;
//static final int viewz = 50;
int viewx, viewy, angle;
int dx, dy;
int px, py ;
int walkStep;
int viewdx, viewdy;
boolean deepdebug = false;
boolean allVisible = false;
boolean newGame = false;
int mazew; // width
int mazeh; // height
Cells mazecells ;
Distance mazedists ;
Cells seencells ;
BSPNode rootnode ;
// Mazebuilder is used to calculate a new maze together with a solution
// The maze is computed in a separate thread. It is started in the local Build method.
MazeBuilder mazebuilder;
final int ESCAPE = 27;
int method = 0 ; // 0 : default method, Falstad's original code
int zscale = Constants.VIEW_HEIGHT/2;
private RangeSet rset;
//Constructor
public Maze() {
super() ;
panel = new MazePanel() ;
}
//selects a generation method
public Maze(int method)
{
super() ;
if (1 == method)
this.method = 1 ;
panel = new MazePanel() ;
}
public void init() {
state = Constants.STATE_TITLE;
rset = new RangeSet();
panel.initBufferImage() ;
addView(new MazeView(this)) ;
notifyViewerRedraw() ;
}
// Method obtains a new Mazebuilder and computes new maze
private void build(int skill) {
state = Constants.STATE_GENERATING;
percentdone = 0;
notifyViewerRedraw() ;
// select generation method
switch(method){
case 1 : mazebuilder = new MazeBuilderPrim();
break ;
case 0:
default : mazebuilder = new MazeBuilder();
break ;
}
mazew = Constants.SKILL_X[skill];
mazeh = Constants.SKILL_Y[skill];
mazebuilder.build(this, mazew, mazeh, Constants.SKILL_ROOMS[skill], Constants.SKILL_PARTCT[skill]);
}
//Call back method for MazeBuilder to communicate newly generated maze
public void newMaze(BSPNode root, Cells c, Distance dists, int startx, int starty) {
if (Cells.deepdebugWall)
{
c.saveLogFile(Cells.deepedebugWallFileName);
}
showMaze = showSolution = solving = false;
mazecells = c ;
mazedists = dists;
seencells = new Cells(mazew+1,mazeh+1) ;
rootnode = root ;
setCurrentDirection(1, 0) ;
setCurrentPosition(startx,starty) ;
walkStep = 0;
viewdx = dx<<16;
viewdy = dy<<16;
angle = 0;
mapMode = false;
state = Constants.STATE_PLAY;
cleanViews() ;
addView(new FirstPersonDrawer(Constants.VIEW_WIDTH,Constants.VIEW_HEIGHT,
Constants.MAP_UNIT,Constants.STEP_SIZE, mazecells, seencells, 10, mazedists.getDists(), mazew, mazeh, root, this)) ;
addView(new MapDrawer(Constants.VIEW_WIDTH,Constants.VIEW_HEIGHT,Constants.MAP_UNIT,Constants.STEP_SIZE, mazecells, seencells, 10, mazedists.getDists(), mazew, mazeh, this)) ;
notifyViewerRedraw() ;
}
public void addView(Viewer view) {
views.add(view) ;
}
public void removeView(Viewer view) {
views.remove(view) ;
}
private void cleanViews() {
Iterator<Viewer> it = views.iterator() ;
while (it.hasNext())
{
Viewer v = it.next() ;
if ((v instanceof FirstPersonDrawer)||(v instanceof MapDrawer))
{
//System.out.println("Removing " + v);
it.remove() ;
}
}
}
private void notifyViewerRedraw() {
Iterator<Viewer> it = views.iterator() ;
while (it.hasNext())
{
Viewer v = it.next() ;
v.redraw(panel.getBufferGraphics(), state, px, py, viewdx, viewdy, walkStep, Constants.VIEW_OFFSET, rset, angle) ;
}
panel.update() ;
}
private void notifyViewerIncrementMapScale() {
Iterator<Viewer> it = views.iterator() ;
while (it.hasNext())
{
Viewer v = it.next() ;
v.incrementMapScale() ;
}
panel.update() ;
}
private void notifyViewerDecrementMapScale() {
Iterator<Viewer> it = views.iterator() ;
while (it.hasNext())
{
Viewer v = it.next() ;
v.decrementMapScale() ;
}
panel.update() ;
}
boolean isInMapMode() {
return mapMode ;
}
boolean isInShowMazeMode() {
return showMaze ;
}
boolean isInShowSolutionMode() {
return showSolution ;
}
public String getPercentDone(){
return String.valueOf(percentdone) ;
}
public Panel getPanel() {
return panel ;
}
private void setCurrentPosition(int x, int y)
{
px = x ;
py = y ;
}
private void setCurrentDirection(int x, int y)
{
dx = x ;
dy = y ;
}
void buildInterrupted() {
state = Constants.STATE_TITLE;
notifyViewerRedraw() ;
mazebuilder = null;
}
final double radify(int x) {
return x*Math.PI/180;
}
public boolean increasePercentage(int pc) {
if (percentdone < pc && pc < 100) {
percentdone = pc;
if (state == Constants.STATE_GENERATING)
{
notifyViewerRedraw() ;
}
else
dbg("Warning: Receiving update request for increasePercentage while not in generating state, skip redraw.") ;
return true ;
}
return false ;
}
private void dbg(String str) {
//System.out.println(str);
}
private void logPosition() {
if (!deepdebug)
return;
dbg("x="+viewx/Constants.MAP_UNIT+" ("+
viewx+") y="+viewy/Constants.MAP_UNIT+" ("+viewy+") ang="+
angle+" dx="+dx+" dy="+dy+" "+viewdx+" "+viewdy);
}
private boolean checkMove(int dir) {
int a = angle/90;
if (dir == -1)
a = (a+2) & 3;
return mazecells.hasMaskedBitsFalse(px, py, Constants.MASKS[a]) ;
}
private void rotateStep() {
angle = (angle+1800) % 360;
viewdx = (int) (Math.cos(radify(angle))*(1<<16));
viewdy = (int) (Math.sin(radify(angle))*(1<<16));
moveStep();
}
private void moveStep() {
notifyViewerRedraw() ;
try {
Thread.currentThread().sleep(25);
} catch (Exception e) { }
}
private void rotateFinish() {
setCurrentDirection((int) Math.cos(radify(angle)), (int) Math.sin(radify(angle))) ;
logPosition();
}
private void walkFinish(int dir) {
setCurrentPosition(px + dir*dx, py + dir*dy) ;
if (isEndPosition(px,py)) {
state = Constants.STATE_FINISH;
notifyViewerRedraw() ;
}
walkStep = 0;
logPosition();
}
private boolean isEndPosition(int x, int y) {
return x < 0 || y < 0 || x >= mazew || y >= mazeh;
}
synchronized private void walk(int dir) {
if (!checkMove(dir))
return;
for (int step = 0; step != 4; step++) {
walkStep += dir;
moveStep();
}
walkFinish(dir);
}
synchronized private void rotate(int dir) {
final int originalAngle = angle;
final int steps = 4;
for (int i = 0; i != steps; i++) {
angle = originalAngle + dir*(90*(i+1))/steps;
rotateStep();
}
rotateFinish();
}
//CLASS CONTROLLING PLAYER MOVEMENTS, REMOVED FOR CHARACTER COUNT
}
}
//Cells.java
package falstad;
import java.io.BufferedWriter;
import java.io.FileWriter;
//This class encapsulates all access to a grid of cells
public class Cells {
private int width;
private int height ;
private int[][] cells;
public Cells(int w, int h) {
width = w ;
height = h ;
cells = new int[w][h];
}
public Cells(int[][] target){
this(target.length, target[0].length);
for (int i=0; i<width; i++)
for (int j=0; j<height; j++)
this.cells[i][j]=target[i][j];
}
public int getCells( int x, int y )
{
return cells[x][y] ;
}
static public int[] getMasks() {
return Constants.MASKS ;
}
public boolean canGo(int x, int y, int dx, int dy) {
if (hasMaskedBitsTrue(x, y, (getBit(dx, dy) << Constants.CW_BOUND_SHIFT)))
return false;
return isFirstVisit(x+dx, y+dy);
}
private int getBit(int dx, int dy) {
int bit = 0;
switch (dx + dy * 2) {
case 1: bit = Constants.CW_RIGHT; break;
case -1: bit = Constants.CW_LEFT; break;
case 2: bit = Constants.CW_BOT; break;
case -2: bit = Constants.CW_TOP; break;
default: dbg("getBit problem "+dx+" "+dy); break;
}
return bit;
}
//CLASSES FOR BITWISE ADJUSTMENTS, REMOVED FOR CHARACTER COUNT
public void initialize() {
int x, y;
for (x = 0; x != width; x++) {
for (y = 0; y != height; y++) {
setBitToOne(x, y, (Constants.CW_VISITED | Constants.CW_ALL));
}
setBitToOne(x, 0, Constants.CW_TOP_BOUND);
setBitToOne(x, height-1, Constants.CW_BOT_BOUND);
}
for (y = 0; y != height; y++) {
setBitToOne(0, y, Constants.CW_LEFT_BOUND);
setBitToOne(width-1, y, Constants.CW_RIGHT_BOUND);
}
}
public boolean areaOverlapsWithRoom(int rx, int ry, int rxl, int ryl) {
int x, y;
for (x = rx-1; x <= rxl+1; x++)
{
for (y = ry-1; y <= ryl+1; y++)
{
if (isInRoom(x, y))
return true ;
}
}
return false ;
}
private void deleteBound(int x, int y, int dx, int dy) {
setBoundToZero(x, y, dx, dy);
setBoundToZero(x+dx, y+dy, -dx, -dy) ;
}
public void addBoundWall(int x, int y, int dx, int dy) {
setBoundAndWallToOne(x, y, dx, dy);
setBoundAndWallToOne(x+dx, y+dy, -dx, -dy);
}
public void deleteWall(int x, int y, int dx, int dy) {
setWallToZero(x, y, dx, dy);
setWallToZero(x+dx, y+dy, -dx, -dy);
if (deepdebugWall)
logWall( x, y, dx, dy);
public void markAreaAsRoom(int rw, int rh, int rx, int ry, int rxl, int ryl) {
int x;
int y;
for (x = rx; x <= rxl; x++)
for (y = ry; y <= ryl; y++) {
setAllToZero(x, y);
setInRoomToOne(x, y);
}
for (x = rx; x <= rxl; x++) {
addBoundWall(x, ry, 0, -1);
addBoundWall(x, ryl, 0, 1);
}
for (y = ry; y <= ryl; y++) {
addBoundWall(rx, y, -1, 0);
addBoundWall(rxl, y, 1, 0);
}
int wallct = (rw+rh)*2;
SingleRandom random = SingleRandom.getRandom() ;
for (int ct = 0; ct != 5; ct++) {
int door = random.nextIntWithinInterval(0, wallct-1);
int dx, dy;
if (door < rw*2) {
y = (door < rw) ? 0 : rh-1;
dy = (door < rw) ? -1 : 1;
x = door % rw;
dx = 0;
} else {
door -= rw*2;
x = (door < rh) ? 0 : rw-1;
dx = (door < rh) ? -1 : 1;
y = door % rh;
dy = 0;
}
deleteBound(x+rx, y+ry, dx, dy);
}
}
public boolean hasMaskedBitsTrue(int x, int y, int bitmask) {
return (cells[x][y] & bitmask) != 0;
}
public boolean isInRoom(int x, int y) {
return hasMaskedBitsTrue(x, y, Constants.CW_IN_ROOM);
}
private boolean isFirstVisit(int x, int y) {
return hasMaskedBitsTrue(x, y, Constants.CW_VISITED);
}
public boolean hasWallOnRight(int x, int y) {
return hasMaskedBitsTrue(x, y, Constants.CW_RIGHT);
}
public boolean hasWallOnLeft(int x, int y) {
return hasMaskedBitsTrue(x, y, Constants.CW_LEFT);
}
public boolean hasWallOnTop(int x, int y) {
return hasMaskedBitsTrue(x, y, Constants.CW_TOP);
}
public boolean hasWallOnBottom(int x, int y) {
return hasMaskedBitsTrue(x, y, Constants.CW_BOT);
}
public boolean hasNoWallOnBottom(int x, int y) {
return !hasMaskedBitsTrue(x, y, Constants.CW_BOT);
}
public boolean hasNoWallOnTop(int x, int y) {
return !hasMaskedBitsTrue(x, y, Constants.CW_TOP);
}
public boolean hasNoWallOnLeft(int x, int y) {
return !hasMaskedBitsTrue(x, y, Constants.CW_LEFT);
}
public boolean hasNoWallOnRight(int x, int y) {
return !hasMaskedBitsTrue(x, y, Constants.CW_RIGHT);
}
public boolean hasMaskedBitsFalse(int x, int y, int bitmask) {
return (cells[x][y] & bitmask) == 0;
}
public boolean hasMaskedBitsGTZero(int x, int y, int bitmask) {
return (cells[x][y] & bitmask) > 0;
}
private void dbg(String str) {
System.out.println("Cells: "+str);
}
public String toString() {
String s = "" ;
for (int i = 0 ; i < width ; i++)
{
for (int j = 0 ; j < height ; j++)
s += " i:" + i + " j:" + j + "=" + cells[i][j] ;
s += "\n" ;
}
return s ;
}
static boolean deepdebugWall = false;
static final String deepedebugWallFileName = "logDeletedWalls.txt" ;
StringBuffer traceWall = (deepdebugWall) ? new StringBuffer("x y dx dy\n") : null ;
private void logWall(int x, int y, int dx, int dy) {
if (null != traceWall)
{
traceWall.append(x + " " + y + " " + dx + " " + dy + "\n");
}
}
public void saveLogFile( String filename )
{
try {
BufferedWriter out = new BufferedWriter(new FileWriter(filename));
out.write(traceWall.toString());
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//MazeBuilder.java
package falstad;
public class MazeBuilder implements Runnable {
protected int width, height ;
Maze maze;
private int rooms;
int expectedPartiters;
protected int startx, starty ;
protected Cells cells;
protected Distance dists ;
protected SingleRandom random ;
Thread buildThread;
public MazeBuilder(){
random = SingleRandom.getRandom();
}
public MazeBuilder(boolean deterministic){
if (true == deterministic)
{
System.out.println("Project 2: functionality to make maze generation deterministic not implemented yet! Fix this!");
}
random = SingleRandom.getRandom();
}
static int getSign(int num) {
return (num < 0) ? -1 : (num > 0) ? 1 : 0;
}
protected void generate() {
generatePathways();
final int[] remote = dists.computeDistances(cells) ;
final int[] pos = dists.getStartPosition();
startx = pos[0] ;
starty = pos[1] ;
setExitPosition(remote[0], remote[1]);
}
protected void generatePathways() {
int[][] origdirs = new int[width][height] ;
int x = random.nextIntWithinInterval(0, width-1) ;
int y = 0;
final int firstx = x ;
final int firsty = y ;
int dir = 0;
int origdir = dir;
cells.setVisitedFlagToZero(x, y);
while (true) {
int dx = Constants.DIRS_X[dir];
int dy = Constants.DIRS_Y[dir];
if (!cells.canGo(x, y, dx, dy)) {
dir = (dir+1) & 3;
if (origdir == dir) {
if (x == firstx && y == firsty)
break;
int odr = origdirs[x][y];
dx = Constants.DIRS_X[odr];
dy = Constants.DIRS_Y[odr];
x -= dx;
y -= dy;
origdir = dir = random.nextIntWithinInterval(0, 3);
}
} else {
cells.deleteWall(x, y, dx, dy);
x += dx;
y += dy;
cells.setVisitedFlagToZero(x, y);
origdirs[x][y] = dir;
origdir = dir = random.nextIntWithinInterval(0, 3);
}
}
}
protected void setExitPosition(int remotex, int remotey) {
int bit = 0;
if (remotex == 0)
bit = Constants.CW_LEFT;
else if (remotex == width-1)
bit = Constants.CW_RIGHT;
else if (remotey == 0)
bit = Constants.CW_TOP;
else if (remotey == height-1)
bit = Constants.CW_BOT;
else
dbg("Generate 1");
cells.setBitToZero(remotex, remotey, bit);
//System.out.println("exit position set to zero: " + remotex + " " + remotey + " " + bit + ":" + cells.hasMaskedBitsFalse(remotex, remotey, bit)
// + ", Corner case: " + ((0 == remotex && 0 == remotey) || (0 == remotex && height-1 == remotey) || (width-1 == remotex && 0 == remotey) || (width-1 == remotex && height-1 == remotey)));
}
static final int MIN_ROOM_DIMENSION = 3 ;
static final int MAX_ROOM_DIMENSION = 8 ;
private boolean placeRoom() {
final int rw = random.nextIntWithinInterval(MIN_ROOM_DIMENSION, MAX_ROOM_DIMENSION);
if (rw >= width-4)
return false;
final int rh = random.nextIntWithinInterval(MIN_ROOM_DIMENSION, MAX_ROOM_DIMENSION);
if (rh >= height-4)
return false;
final int rx = random.nextIntWithinInterval(1, width-rw-1);
final int ry = random.nextIntWithinInterval(1, height-rh-1);
final int rxl = rx+rw-1;
final int ryl = ry+rh-1;
if (cells.areaOverlapsWithRoom(rx, ry, rxl, ryl))
return false ;
cells.markAreaAsRoom(rw, rh, rx, ry, rxl, ryl);
return true;
}
static void dbg(String str) {
System.out.println("MazeBuilder: "+str);
}
public void build(Maze mz, int w, int h, int roomct, int pc) {
init(mz, w, h, roomct, pc);
buildThread = new Thread(this);
buildThread.start();
}
private void init(Maze mz, int w, int h, int roomct, int pc) {
maze = mz;
width = w;
height = h;
rooms = roomct;
expectedPartiters = pc;
cells = new Cells(w,h) ;
dists = new Distance(w,h) ;
//colchange = random.nextIntWithinInterval(0, 255);
}
static final long SLEEP_INTERVAL = 100 ;
try {
cells.initialize();
// rooms into maze
generateRooms();
Thread.sleep(SLEEP_INTERVAL) ;
// pathways into the maze,
generate();
Thread.sleep(SLEEP_INTERVAL) ;
final int colchange = random.nextIntWithinInterval(0, 255);
final BSPBuilder b = new BSPBuilder(maze, dists, cells, width, height, colchange, expectedPartiters) ;
BSPNode root = b.generateBSPNodes();
Thread.sleep(SLEEP_INTERVAL) ;
// dbg("partiters = "+partiters);
maze.newMaze(root, cells, dists, startx, starty);
}
catch (InterruptedException ex) {
// dbg("Catching signal to stop") ;
}
}
static final int MAX_TRIES = 250 ;
private int generateRooms() {
int tries = 0 ;
int result = 0 ;
while (tries < MAX_TRIES && result <= rooms) {
if (placeRoom())
result++ ;
else
tries++ ;
}
return result ;
}
public void interrupt() {
buildThread.interrupt() ;
}
}
Obviously, I'm not asking for someone to write a test for me - not that any of you would, as this is fairly obviously a homework assignment - but I'd like some help getting started on this. So if anyone could clue me into the surely obvious trick that I'm missing, that would be wonderful!
A unit test for Cells class can look like the below:
While you unit test the Cells class you might not need the Maze or MazeBuilder, you only need an instance of Cells class and call its methods in order to check that they work correctly isolated from the rest of the application.
Also take a look at this: Unit testing in Java - what is it? as it provides a good reference.
import org.junit.Assert;
import org.junit.Test;
public class CellsTest {
#Test
void testHasWallOnRight() {
//setup
int[][] target = new int[][] { { 0, 0, 0, 0 },
{ 0, 0, 0, 0 },
{ 0, 0, 0, 0 },
{ 0, 0, 0, 0 } };
Cells a = new Cells(target);
//act
boolean result = a.hasWallOnRight(1,1);
//assert
Assert.assertFalse(result);
}
}