• fullscreen
  • board.pde
  • edge.pde
  • enemy.pde
  • mazegenerator.pde
  • mypathfinder.pde
  • node.pde
  • orbiter.pde
  • player.pde
  • roller.pde
  • spinning_maze9_addfeatures.pde
  • 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()));
      }
    }
    

    code

    tweaks (0)

    about this sketch

    This sketch is running as Java applet, exported from Processing.

    license

    advertisement

    Greg Schafer

    Spin Maze

    Add to Faves Me Likey@! 9
    You must login/register to add this sketch to your favorites.

    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

    Pierre MARZIN
    30 Jul 2012
    Great work! Funny to play too!
    Victor M.
    2 Aug 2013
    So fun to play with!
    You need to login/register to comment.