fullscreen class Board
{
/* terrain (walls) */
private MazeNode nodes[][];
public Board(MazeNode nodes[][])
{
this.nodes = nodes;
}
public void rotateTerrain(float rotAmt)
{
mazeAng -= rotAmt;
mazeAng = mazeAng % (2*PI);
// linear transformation with matrix T = [[cos, -sin],[sin, cos]]
// Matrix Theory was a helpful class, yay!
PVector transX = new PVector(cos(rotAmt), -sin(rotAmt));
PVector transY = new PVector(sin(rotAmt), cos(rotAmt));
for (FBody fb : rotateUs)
{
fb.adjustRotation(rotAmt);
PVector v = new PVector(fb.getX() - width/2, fb.getY() - height/2);
fb.setPosition(v.dot(transX) + width/2, v.dot(transY) + height/2);
}
}
public void initTerrain()
{
int row,col;
// fill in north and west walls for every node
for (row = 0; row < ROWS; ++row)
{
for (col = 0; col < COLS; ++col)
{
if (nodes[row][col].walls[0] == 1) // north wall exists
{
FBox fb = new FBox(GAP, 2);
fb.setPosition(col * GAP + offsetX, row * GAP - (GAP/2) + offsetY);
fb.setStatic(true);
//fb.setGrabbable(false); // <-- THIS COULD PROVIDE INTERESTING OPPORTUNITIES
rotateUs.add(fb);
world.add(fb);
}
if (nodes[row][col].walls[3] == 1) // west wall exists
{
FBox fb = new FBox(2, GAP);
fb.setPosition(col * GAP - (GAP/2) + offsetX, row * GAP + offsetY);
fb.setStatic(true);
//fb.setGrabbable(false); // <-- THIS COULD PROVIDE INTERESTING OPPORTUNITIES
rotateUs.add(fb);
world.add(fb);
}
}
}
// fill in the east walls for the last column
col = COLS - 1;
for (row = 0; row < ROWS; ++row)
{
if (nodes[row][col].walls[1] == 1) // east wall exists
{
FBox fb = new FBox(2, GAP);
fb.setPosition(col * GAP + (GAP/2) + offsetX, row * GAP + offsetY);
fb.setStatic(true);
//fb.setGrabbable(false); // <-- THIS COULD PROVIDE INTERESTING OPPORTUNITIES
rotateUs.add(fb);
world.add(fb);
}
}
// fill in the south walls for the last row
row = ROWS - 1;
for (col = 0; col < COLS; ++col)
{
if (nodes[row][col].walls[2] == 1) // south wall exists
{
FBox fb = new FBox(GAP, 2);
fb.setPosition(col * GAP + offsetX, row * GAP + (GAP/2) + offsetY);
fb.setStatic(true);
//fb.setGrabbable(false); // <-- THIS COULD PROVIDE INTERESTING OPPORTUNITIES
rotateUs.add(fb);
world.add(fb);
}
}
}
}
/*
* Used by the Kruskal's maze generation algorithm
*
*/
class MazeEdge
{
public int row;
public int col;
public int dir; // will be 0 (north) or 3 (west)
public MazeEdge(int row, int col, int dir)
{
this.row = row;
this.col = col;
this.dir = dir;
}
}
class Enemy extends FCircle
{
public Enemy(float circleSize)
{
super(circleSize);
}
public Enemy(float circleSize, float r, float g, float b, float restitution, float friction)
{
super(circleSize);
this.setFill(r,g,b);
this.setRestitution(restitution);
this.setFriction(friction);
this.setPosition(int(random(0,COLS)) * GAP + offsetX, int(random(0,ROWS)) * GAP + offsetY);
this.setGrabbable(false); // <-- THIS COULD PROVIDE INTERESTING OPPORTUNITIES
}
}
/*
* Credit for algorithm implementation:
* http://weblog.jamisbuck.org/2011/2/7/maze-generation-algorithm-recap
*
* Other maze info:
* http://www.astrolog.org/labyrnth/algrithm.htm
*/
class MazeGenerator
{
/* accept nodes[][] in constructor
then a method will be called and that
maze generation algorithm will be performed
on nodes[][] */
public MyPathfinder dijk;
private static final int NUM_MAZES_AVAILABLE = 3;
private MazeNode nodes[][];
private int endRow;
private int endCol;
public MazeGenerator(MazeNode nodes[][])
{
this.nodes = nodes;
dijk = new MyPathfinder();
}
public void genMaze(int mazeType)
{
if (mazeType == 0)
mazeType = int(random(0, NUM_MAZES_AVAILABLE) + 1);
println("mazetype=" + mazeType);
switch (mazeType)
{
case 1:
genMazeRecursiveBacktrack(int(ROWS/2), int(COLS/2)); break;
case 2:
genMazeBinaryTree(); break;
case 3:
genMazeKruskal(); break;
default:
genMazeRecursiveBacktrack(int(ROWS/2), int(COLS/2)); break;
}
makeRandomEndSpot();
}
public void genMazeRecursiveBacktrack(int row, int col)
{
nodes[row][col].visited = true;
int directions[] = genRandDirections();
for (int i = 0; i < directions.length; ++i)
{
int dir = directions[i];
int newRow = row + DY[dir];
int newCol = col + DX[dir];
if (newCol >= 0 && newCol < COLS && newRow >= 0 && newRow < ROWS && !nodes[newRow][newCol].visited)
{
nodes[row][col].walls[dir] = 0;
nodes[newRow][newCol].walls[opp(dir)] = 0;
((Node)dijk.nodes.get(row * COLS + col)).setDistBoth((Node)dijk.nodes.get(newRow * COLS + newCol), 1);
genMazeRecursiveBacktrack(newRow, newCol);
}
}
}
public void genMazeBinaryTree()
{
Vector<Integer> directions = new Vector<Integer>();
for (int row = 0; row < ROWS; ++row)
{
for (int col = 0; col < COLS; ++col)
{
directions.clear();
// algorithm goes north or west, by default
if (row > 0)
directions.add(0);
if (col > 0)
directions.add(3);
if (directions.size() > 0)
{
int dir = directions.get(floor(random(directions.size())));
int newRow = row + DY[dir];
int newCol = col + DX[dir];
nodes[row][col].walls[dir] = 0;
nodes[newRow][newCol].walls[opp(dir)] = 0;
((Node)dijk.nodes.get(row * COLS + col)).setDistBoth((Node)dijk.nodes.get(newRow * COLS + newCol), 1);
}
}
}
}
public void genMazeKruskal()
{
ArrayList<MazeEdge> edges = new ArrayList<MazeEdge>();
int[][] sets = new int[ROWS][COLS];
for (int row = 0; row < ROWS; ++row)
{
for (int col = 0; col < COLS; ++col)
{
sets[row][col] = row * COLS + col; // every node is part of a (unique) set
if (row > 0)
edges.add(new MazeEdge(row, col, 0)); // add edge from this node directed north
if (col > 0)
edges.add(new MazeEdge(row, col, 3)); // add edge from this node directed west
}
}
Collections.shuffle(edges); // randomize edge-picking order
println("number of edges = " + edges.size());
while (!edges.isEmpty())
{
MazeEdge e = edges.remove(0);
int row = e.row;
int col = e.col;
int dir = e.dir;
int newRow = row + DY[dir];
int newCol = col + DX[dir];
if (sets[row][col] != sets[newRow][newCol])
{
nodes[row][col].walls[dir] = 0;
nodes[newRow][newCol].walls[opp(dir)] = 0;
((Node)dijk.nodes.get(row * COLS + col)).setDistBoth((Node)dijk.nodes.get(newRow * COLS + newCol), 1);
// update all set numbers
int newSetNum = sets[row][col];
int oldSetNum = sets[newRow][newCol];
for (int i = 0; i < ROWS; ++i)
{
for (int j = 0; j < COLS; ++j)
{
if (sets[i][j] == oldSetNum)
{
sets[i][j] = newSetNum;
}
}
}
}
}
}
public void makeRandomEndSpot() // belongs to board or maze generator?
{
int numSides = 2*(ROWS + COLS);
int spot = int(random(0,numSides));
//println("spot=" + spot);
int row = 0, col = 0;
if (spot < COLS) // top of maze
{
row = 0;
col = spot;
nodes[row][col].walls[0] = 0;
}
else if (spot >= numSides - COLS) // bottom of maze
{
row = ROWS - 1;
col = spot - (numSides - COLS);
nodes[row][col].walls[2] = 0;
}
else if (spot >= COLS) // side of maze
{
row = int((spot - COLS) / 2);
if ((spot - COLS) % 2 == 0) // left side
{
col = 0;
nodes[row][col].walls[3] = 0;
}
else // (spot - COL) % 2 == 1 // right side
{
col = COLS - 1;
nodes[row][col].walls[1] = 0;
}
}
endRow = row;
endCol = col;
}
public ArrayList<Node> getShortestPath()
{
return dijk.aStar((Node)dijk.nodes.get(int(ROWS/2) * COLS + int(COLS/2)), (Node)dijk.nodes.get(endRow * COLS + endCol));
}
public float getShortestDist()
{
// add dijkstra's here to make sure of shortest path
float shortestDist = 0;
path = dijk.aStar((Node)dijk.nodes.get(int(ROWS/2) * COLS + int(COLS/2)), (Node)dijk.nodes.get(endRow * COLS + endCol));
for (Node n : path)
{
if (n.parent != null)
{
shortestDist += ((Connector)n.links.get(n.indexOf(n.parent))).d;
}
}
return shortestDist * GAP;
}
// used by maze gen algorithms
private int opp(int dir)
{
return (dir + 2) % 4;
}
// used by recursive backtrack
private int[] genRandDirections()
{
int dirs[] = {0,1,2,3};
for (int i = dirs.length - 1; i > 0; --i)
{
int rand = floor(random(0,i+1));
int temp = dirs[i];
dirs[i] = dirs[rand];
dirs[rand] = temp;
}
return dirs;
}
}
class MyPathfinder extends Pathfinder
{
public MyPathfinder()
{
super();
this.corners = false;
this.setCuboidNodes(COLS, ROWS, 1.0);
for (int i = 0; i < this.nodes.size(); ++i) // set all distances to a large number, initially
{
Node n = (Node)this.nodes.get(i);
for (int j = 0; j < n.links.size(); ++j)
((Connector)n.links.get(j)).d = 1000;
}
}
}
class MazeNode
{
// [0] = north wall
// [1] = east wall
// [2] = south wall
// [3] = west wall
public int walls[] = {1,1,1,1};
public boolean visited;
public MazeNode()
{
visited = false;
}
public String toString()
{
return "[" + walls[0] + " " + walls[1] + " " + walls[2] + " " + walls[3] + "]";
}
}
class Orbiter extends Enemy
{
private MazeNode[][] maze;
public int nodeRow;
public int nodeCol;
public int targetNodeRow;
public int targetNodeCol;
public int direction;
public boolean traveling;
public Orbiter(float circleSize, float r, float g, float b, float restitution, float friction, MazeNode[][] m)
{
super(circleSize, r, g, b, restitution, friction);
maze = m;
traveling = false;
direction = int(random(0,4));
this.nodeRow = int(random(0,ROWS));
this.nodeCol = int(random(0,COLS));
this.setPosition(this.nodeCol * GAP + offsetX, this.nodeRow * GAP + offsetY);
this.setRotation(direction * PI/2);
this.setStatic(true);
}
public Orbiter(float circleSize, MazeNode[][] m)
{
super(circleSize);
maze = m;
traveling = false;
direction = int(random(0,4));
this.nodeRow = int(random(0,ROWS));
this.nodeCol = int(random(0,COLS));
this.setPosition(this.nodeCol * GAP + offsetX, this.nodeRow * GAP + offsetX);
this.setRotation(direction * PI/2);
this.setStatic(true);
}
public void determineTargetNode()
{
// check for walls
if (maze[nodeRow][nodeCol].walls[(direction + 1) % 4] == 0) // if wall to the right doesn't exist
direction = (direction + 1) % 4; // turn right
else // wall to the right does exist
{
if (maze[nodeRow][nodeCol].walls[direction] == 1) // if wall in front exists
{
if (maze[nodeRow][nodeCol].walls[(direction + 3) % 4] == 1) // if wall to the left exists also
direction = (direction + 2) % 4; // turn 180 degrees
else
direction = (direction + 3) % 4; // turn left
}
// else direction remains the same
}
targetNodeRow = nodeRow + DY[direction];
targetNodeCol = nodeCol + DX[direction];
// if target node is outside maze then it's trying to leave the maze, so turn around (NEED BETTER)
if (targetNodeRow < 0 || targetNodeRow >= ROWS || targetNodeCol < 0 || targetNodeCol >= COLS)
{
direction = (direction + 2) % 4; // direct outwards
determineTargetNode();
return;
}
traveling = true;
}
}
class Player extends FCircle
{
public Player(float circleSize)
{
super(circleSize);
//this.setBullet(true);
this.setGrabbable(false);
}
public void setPhysics(int setting)
{
switch (setting)
{
//case 1: // this is the default
case 2:
this.setFill(255,0,255);
this.setRestitution(1);
this.setFriction(1);
break;
case 3:
this.setFill(160,160,255);
this.setRestitution(0);
this.setFriction(0);
break;
case 4:
this.setFill(0,160,0);
this.setRestitution(0.5);
this.setFriction(0.5);
break;
default:
this.setFill(255);
this.setRestitution(0);
this.setFriction(1);
}
}
}
class Roller extends Enemy
{
public Roller(float circleSize, float r, float g, float b, float restitution, float friction)
{
super(circleSize, r, g, b, restitution, friction);
}
}
import controlP5.*;
import ai.pathfinder.*;
import fisica.*;
/**
* Rotating Maze with Ball
*
* Greg Schafer
*/
/*
DONE:
-give the user control over world rotation (damp movement? how much?)
-make sure the maze is "good" (actually maze-like and possible to solve...use A*?)
-if shortest path doesn't exist or is too short, regen the level
-instead of current A* implementation, use Dijkstra's to gen paths to any exterior node (check node.g for all of them and take the smallest as the shortestDist)
-use an actual maze generation algorithm instead of hoping randomness connects
-http://weblog.jamisbuck.org/2010/12/27/maze-generation-recursive-backtracking
-separate maze size (spacing and # of cells) from applet width and height
-victory detection (when ball falls off screen-bottom or intersects with sensor at final position)
-escape maze or somewhere in maze?
-scoring (time, lives, ?)
-use dijkstra and compare distance traveled by the ball with calculated shortest distance
-also have a timer (but what to compare it to since the mazes are random?)
-add a debug mode (push "~") to show shortest path, AI pathing, and other interesting lines/drawing
-make different ball types available (diff size, restitution, friction, ?)
-mouse being in wrong spot (coming off GUI or starting a new maze) is pretty annoying...give user countdown to get mouse to correct spot?)
-gray out the screen and pause game--have a highlighted circle over where the maze angle is...when cursor enters the circle, play resumes
-do this for exiting applet, losing focus, entering UI bar, hitting ctrl?
TODO:
-shortest distance is flawed because of cutting corners (make dijkstra's corner-connected?)
-make different maze types available (different properties like solution %, multiple paths)
-http://weblog.jamisbuck.org/2010/12/29/maze-generation-eller-s-algorithm
-http://www.ccs.neu.edu/home/snuffy/maze/
-PUT ALL MAZE GENERATION ALGORITHMS IN A DIFFERENT CLASS (PASS nodes[][] to constructor)
-ensure that the player has a chance (doesn't spawn in or directly above a ball, has a side entrance before nearest enemy, etc.)
-monsters/traps in the maze? (* = done)
-*red ball that rolls around (based on how the user rotates maze)...touching = death
-*blue ball that follows right-hand rule through maze (unaffected by physics/rotation)...touching = death
-conveyor squares, physical elevators, bounce pads, frictionless walls, bouncy walls, timed buttons, removable walls (ie get blue key to remove blue walls), etc
-for chasing enemies, need multiple paths or some other avoidance mechanic?
-multiple paths = booby trap squares or monsters that orbit an inner set of walls
-level editor (separate application)
-textual level format that can be copy-pasted between apps
-start with full maze or something generated with one of the algorithms...then user left/right click-drags through grid spots to make/remove walls
-palette of entities/objects that can be placed on the grid
*/
public FWorld world;
public ControlP5 controlP5;
public RadioButton radioBtn;
public Board board;
public MazeGenerator mazegen;
public MazeNode[][] nodes;
public ArrayList<Node> path;
public ArrayList<FBody> rotateUs;
public ArrayList<Enemy> enemies;
public ArrayList<Orbiter> orbiters;
public Player player;
public float ballDist;
public float ballLastDist;
public float totalDist;
public float startTime;
public float totalTime;
public float mazeAng;
public float mouseAng;
public int offsetX = 0;
public int offsetY = 0;
public PFont font48;
public PFont font36;
public int atEndScreen = 0; // 0 = false, 1 = victory screen, 2 = defeat screen
public int mazeType = 0;
public boolean debugMode = false;
public boolean gamePaused = false;
public boolean cursorLeftUnpauseCircle = true;
// constants
public static final int UI_BAR_HEIGHT = 40;
public static final float ROT_SPEED = 0.05; // this can be turned up, because the ball is in rotateUs
public static final float BALL_DIAM = 5;
public static final int DX[] = {0,1,0,-1};
public static final int DY[] = {-1,0,1,0};
public static final String RANDOM = "Random";
public static final String RECURSIVE_BACKTRACK = "Recursive Backtrack";
public static final String BINARY_TREE = "Binary Tree";
public static final String KRUSKAL = "Kruskal";
public int NUM_ROLLERS = 2;
public int NUM_ORBITERS = 1;
// stupid hack so changing maze settings in the UI doesn't frak up code (eg - orbiters) that use those settings
public int ROWS = 12;
public int COLS = 12;
public int GAP = 40;
public int rows = ROWS;
public int cols = COLS;
public int gap = GAP;
void setup()
{
frameRate(60);
size(640,640);
//size(640,640,P2D); // causes walls to be invisible...?
smooth();
initGUI(); // CALLING THIS MULTIPLE TIMES (BY RESETTING) MAKES THE BANG CLICK LOTS?
hint(ENABLE_NATIVE_FONTS);
font48 = loadFont("ArialMT-48.vlw");
font36 = loadFont("ArialMT-36.vlw");
//textMode(SCREEN);
// physics stuff
Fisica.init(this);
world = new FWorld();
reset();
}
public void reset()
{
// stupid hack so changing maze settings in the UI doesn't frak up code (eg - orbiters) that use those settings
ROWS = rows;
COLS = cols;
GAP = gap;
// end stupid hack
// offset values describe blank space between applet edges and the edges of the maze
offsetX = (width - COLS*GAP)/2 + GAP/2;
offsetY = (height - ROWS*GAP)/2 + GAP/2;
mazeAng = 0.0;
mouseAng = 0.0;
atEndScreen = 0;
cursorLeftUnpauseCircle = true;
if (mag(mouseX - (width/2 + width/4 * cos(mazeAng)), mouseY - (height/2 - height/4 * sin(mazeAng))) > 50)
gamePaused = true;
else
gamePaused = false;
// declare and initialize field of maze nodes/tiles
nodes = new MazeNode[ROWS][COLS];
for (int x = 0; x < ROWS; ++x)
for (int y = 0; y < COLS; ++y)
nodes[x][y] = new MazeNode();
// generate the maze (the maze will be stored in nodes)
mazegen = new MazeGenerator(nodes);
mazegen.genMaze(mazeType);
path = mazegen.getShortestPath();
println("shortest distance: " + mazegen.getShortestDist());
world.clear();
rotateUs = new ArrayList<FBody>();
board = new Board(nodes);
board.initTerrain();
player = new Player(GAP/2);
player.setPosition(COLS/2 * GAP + offsetX, ROWS/2 * GAP + offsetY);
rotateUs.add(player);
world.add(player);
// add enemies
enemies = new ArrayList<Enemy>();
// add roller enemies
for (int i = 0; i < NUM_ROLLERS; ++i)
{
enemies.add(new Enemy(GAP/2 + random(-GAP/4, GAP/4),255,0,0,0,1));
enemies.get(i).setRotation(random(-PI, PI));
rotateUs.add(enemies.get(i));
world.add(enemies.get(i));
}
// add orbiter enemies
orbiters = new ArrayList<Orbiter>();
for (int i = 0; i < NUM_ORBITERS; ++i)
{
orbiters.add(new Orbiter(GAP/2,128,128,255,0,1, nodes));
enemies.add(orbiters.get(i));
rotateUs.add(orbiters.get(i));
world.add(orbiters.get(i));
}
radioBtn.activate(0);
// reset scoring mechanisms (distance and time)
ballLastDist = dist(player.getX(), player.getY(), rotateUs.get(0).getX(), rotateUs.get(0).getY());
ballDist = 0;
startTime = millis();
}
void draw() // break out into more functions?
{
if (!focused) // applet lost focus
gamePaused = true;
if (!gamePaused)
background(255);
else // if game paused, shade out background and draw unpausing circle
{
background(180);
stroke(0);
fill(255);
ellipse(width/2 + width/4 * cos(mazeAng), height/2 - height/4 * sin(mazeAng),100,100);
}
// debug drawing (draws dot grid and shortest path)
if (debugMode)
debugDraw();
stroke(0);
// measures distance traveled by player relative to the maze
if (frameCount % 10 == 0)
{
ballDist += abs(dist(player.getX(), player.getY(), rotateUs.get(0).getX(), rotateUs.get(0).getY()) - ballLastDist);
ballLastDist = dist(player.getX(), player.getY(), rotateUs.get(0).getX(), rotateUs.get(0).getY());
}
// end of measuring distance traveled by player
// physics hack: prevents bodies from warping through walls when they go to rest
if (player.isResting())
player.addForce(0,0.1);
for (FBody e : enemies)
if (e.isResting())
e.addForce(0,0.1);
// end of physics hack
world.draw();
if (!gamePaused)
{
// calculate angle and rotate maze
float diffAng = mazeAng - mouseAng;
// angle adjustments for crossing the 180-degree line
if (abs(mazeAng - mouseAng + 2*PI) < abs(diffAng))
diffAng = mazeAng - mouseAng + 2*PI;
if (abs(mazeAng - mouseAng - 2*PI) < abs(diffAng))
diffAng = mazeAng - mouseAng - 2*PI;
board.rotateTerrain(ROT_SPEED*diffAng);
// end of angle calculation and maze rotation
// draw difference between maze angle and mouse angle
float colorScale = abs(diffAng) * 255 / PI;
fill(colorScale, 0, 255 - colorScale, 60);
arc(width/2, height/2, width/2, height/2, -mazeAng, -mazeAng + diffAng);
arc(width/2, height/2, width/2, height/2, -mazeAng + diffAng, -mazeAng);
// end of draw difference between maze angle and mouse angle
// game loops
moveEnemies(); // move active enemies
checkVictory(); // checks victory condition (and restarts game)
// fisica functions for drawing and simulating world physics
world.step();
}
// end screen should be drawn atop game objects
if (atEndScreen != 0)
drawEndScreen();
// UI bar must be drawn on top, so it's at the bottom
noStroke();
fill(0,80);
rect(0,0, width,UI_BAR_HEIGHT);
// needed if using P2D
//controlP5.draw();
}
public void moveEnemies() // package in enemies class?
{
for (Orbiter f : orbiters)
{
if (!f.traveling) // if not traveling
f.determineTargetNode(); // pick target
else // go towards target
{
PVector transX = new PVector(cos(mazeAng), sin(mazeAng));
PVector transY = new PVector(-sin(mazeAng), cos(mazeAng));
PVector v = new PVector(f.targetNodeCol * GAP - width/2 + offsetX, f.targetNodeRow * GAP - height/2 + offsetY);
float px = v.dot(transX) + width/2;
float py = v.dot(transY) + height/2;
float angle = atan2(py - f.getY(), px - f.getX());
float moveSpeed = 1.0;
stroke(255,0,0);
line(f.getX(), f.getY(), px, py);
f.setRotation(angle);
f.adjustPosition(moveSpeed * cos(angle), moveSpeed * sin(angle));
if (dist(f.getX(), f.getY(), px, py) < 1) // if at target
{
f.nodeRow = f.targetNodeRow;
f.nodeCol = f.targetNodeCol;
f.traveling = false; // set not traveling
}
}
}
}
public void drawEndScreen() // package in GUI class?
{
if (atEndScreen == 1) // victory
{
int distance = int(totalDist);
fill(255,90);
rect(64,64, width - 128, height - 128);
fill(0);
textFont(font48);
text("Victory!", width/2 - textWidth("Victory!")/2, height/2 - 64);
textFont(font36);
text("Distance traveled: " + distance, width/2 - textWidth("Distance traveled: " + distance)/2, height/2);
text("Time taken: " + totalTime + " s.", width/2 - textWidth("Time taken: " + totalTime + " s.")/2, height/2 + 64);
fill(64);
text("Click to restart", width/2 - textWidth("Click to restart")/2, height/2 + 128);
}
else if (atEndScreen == 2) // defeat
{
fill(255,80);
rect(64,64, width - 128, height - 128);
fill(0);
textFont(font48);
text("Defeat!", width/2 - textWidth("Defeat!")/2, height/2 - 64);
fill(64);
textFont(font36);
text("Click to restart", width/2 - textWidth("Click to restart")/2, height/2);
}
}
public void checkVictory()
{
// have a physics sensor at the end spot to use as the victory condition?
if (player.getY() > height + 100 && atEndScreen == 0)
{
atEndScreen = 1;
totalDist = ballDist;
totalTime = (millis() - startTime) / 1000;
// ballDist is not directly comparable to shortestDist because ballDist records distance falling offscreen
println("VICTORY!");
println("Distance traveled: " + ballDist);
println("Time taken: " + totalTime);
}
}
void contactStarted(FContact contact)
{
if (contact.getBody1() == player || contact.getBody2() == player)
if (enemies.contains(contact.getBody1()) || enemies.contains(contact.getBody2()))
atEndScreen = 2;
}
void mousePressed() // exits finish screen
{
if (atEndScreen != 0)
reset();
}
void keyPressed()
{
switch (key)
{
case 'r': // reset
case 'R':
reset(); break;
case '`': // toggle debug mode
debugMode = (debugMode ? false : true); break;
// 1-4 = player physics settings
case '1':
radioBtn.activate("1"); break;
case '2':
radioBtn.activate("2"); break;
case '3':
radioBtn.activate("3"); break;
case '4':
radioBtn.activate("4"); break;
case CODED:
if (keyCode == CONTROL)
gamePaused = true;
if (mag(mouseX - (width/2 + width/4 * cos(mazeAng)), mouseY - (height/2 - height/4 * sin(mazeAng))) < 50)
cursorLeftUnpauseCircle = false;
break;
}
}
void mouseMoved()
{
// if mouse is over the UI bar, pause game
if (mouseY < UI_BAR_HEIGHT)
gamePaused = true;
if (!gamePaused) // if game isn't paused rotate maze as normal
mouseAng = atan2(-(mouseY - height/2), mouseX - width/2);
else // if game is paused, don't unpause until mouse moves back into highlighted circle
{
if (cursorLeftUnpauseCircle && mag(mouseX - (width/2 + width/4 * cos(mazeAng)), mouseY - (height/2 - height/4 * sin(mazeAng))) < 50)
gamePaused = false;
else if (!cursorLeftUnpauseCircle && mag(mouseX - (width/2 + width/4 * cos(mazeAng)), mouseY - (height/2 - height/4 * sin(mazeAng))) > 50)
cursorLeftUnpauseCircle = true;
}
}
public void debugDraw()
{
stroke(0,255,0);
line(rotateUs.get(0).getX(), rotateUs.get(0).getY(), player.getX(), player.getY()); // draws reference line
// draws the grid of dots
stroke(0);
PVector transX = new PVector(cos(mazeAng), sin(mazeAng));
PVector transY = new PVector(-sin(mazeAng), cos(mazeAng));
for (int i = 0; i < ROWS; ++i)
{
for (int j = 0; j < COLS; ++j)
{
PVector v = new PVector(j * GAP - width/2 + offsetX, i * GAP - height/2 + offsetY);
point(v.dot(transX) + width/2, v.dot(transY) + height/2);
}
}
// end of drawing grid of dots
// draws shortest path (doesn't rotate)
for (Node n : path) // draw shortest path
{
if (n.parent != null)
{
stroke(255,0,255);
strokeWeight(2);
PVector v1 = new PVector(n.x * GAP - width/2 + offsetX, n.y * GAP - height/2 + offsetY);
PVector v2 = new PVector(n.parent.x * GAP - width/2 + offsetX, n.parent.y * GAP - height/2 + offsetY);
line(v1.dot(transX) + width/2, v1.dot(transY) + height/2, v2.dot(transX) + width/2, v2.dot(transY) + height/2);
}
}
// end of drawing shortest path
}
public void initGUI()
{
// controls for # rows, # cols, tile/gap size
controlP5 = new ControlP5(this);
controlP5.addSlider("rows", 1, 40, rows, 4, 2, 60, 12);
controlP5.addSlider("cols", 1, 40, cols, 4, 14, 60, 12);
controlP5.addSlider("gap", 10, 50, gap, 4, 26, 60, 12);
controlP5.addBang("reset", 130, 20, 52, 16).captionLabel().style().moveMargin(-16, 0, 0, 14); // BANG = BAD IDEA? it repeatedly resets, the more you push it
// controls for type of maze to generate (dropdown)
DropdownList mazeList = controlP5.addDropdownList("maze type", 120, 13, 100, 120);
mazeList.addItem(RANDOM, 0);
mazeList.addItem(RECURSIVE_BACKTRACK, 1);
mazeList.addItem(BINARY_TREE, 2);
mazeList.addItem(KRUSKAL, 3);
// kruskal's, prim's, growing tree?
// controls for fc (size, restitution, friction, color?)
controlP5.addTextlabel("phys_label", "Physics", 258, 3);
radioBtn = controlP5.addRadioButton("radioButton",260,14);
radioBtn.setItemsPerRow(2);
radioBtn.addItem("1", 1).captionLabel().style().moveMargin(0,0,0,-23);
radioBtn.addItem("2", 2).captionLabel();
radioBtn.addItem("3", 3).captionLabel().style().moveMargin(0,0,0,-23);
radioBtn.addItem("4", 4).captionLabel();
//radioBtn.getItem(0).setState(true);
// controls for number and type of enemies (checkbox and slider)
controlP5.addSlider("NUM_ROLLERS", 0, 20, NUM_ROLLERS, 340, 2, 60, 16);
controlP5.addSlider("NUM_ORBITERS", 0, 10, NUM_ORBITERS, 340, 22, 60, 16);
}
void controlEvent(ControlEvent theEvent)
{
if (theEvent.isGroup())
{
//println(theEvent.group().captionLabel());
if (theEvent.group().captionLabel().getText().equals(RANDOM))
mazeType = 0;
else if (theEvent.group().captionLabel().getText().equals(RECURSIVE_BACKTRACK))
mazeType = 1;
else if (theEvent.group().captionLabel().getText().equals(BINARY_TREE))
mazeType = 2;
else if (theEvent.group().captionLabel().getText().equals(KRUSKAL))
mazeType = 3;
else if (theEvent.group().name().equals("radioButton"))
player.setPhysics(int(theEvent.group().value()));
}
}
Escape the maze by spinning it and avoiding enemies.
UI elements from http://www.sojamo.de/libraries/controlP5/
Physics from http://www.ricardmarxer.com/fisica/
Pathfinding (A*) from http://www.robotacid.com/PBeta/AILibrary/Pathfinder/index.html
Controls:
-Mouse to rotate
-CTRL to "pause"
-` (tilde) for debug info
-1/2/3/4 to change ball physics
-r to reset