// qotw17_puzzle // Kevin Pfeiffer //#html work-in-progress //#html Use the 'x' key to flip and the 'z' or 'c' keys to rotate a tile. // Based on the Perl 'Expert' Quiz-of-the-Week #17 // http://perl.plover.com/qotw/e/017 // Code based on an approach developed by Mark Jason Dominus // TODO // auto-solve puzzle method! // work on color pallette // change rotate() to use BB centerpoint instead of vertex // add selection marquee & "moveAllSelect" for repositioning placed pieces! // improve snap method (use angle of sides!) // prevent overlapping tiles // suitable shuffle-tiles method // instructions? (or on web page)? // displayProperties method (angle, lengths, area) // animated_moving routine /* An "S" in the "Sides" column represents a side with length sqrt(3). The areas in the "Area" column are measured in multiples of tile A, which actually has an area of sqrt(3)/4 square units. Piece "E" is rather irregular. It is formed by taking a copy of tile C and gluing a copy of tile A to its short side. Tiles C and D are identical. It is permissible to flip the tiles over. And, of course, to rotate them or slide them around on the table. But it is not permitted to overlap them. */ int sc; int unit; Puzzle puzzle; final float S = sqrt(3); // sqrt of 3 * unit void setup() { size(700,500); sc = width/5; // scale tiles to window unit = sc * 1; // unit size of tiles puzzle = new Puzzle(unit); smooth(); noStroke(); } void loop() { background(45, 55, 50); puzzle.run(); } void mousePressed() { puzzle.mousePressed(); } void mouseReleased() { puzzle.mouseReleased(); } void keyPressed() { puzzle.keyPressed(); } class Puzzle { // fields Stack stack = new Stack(); // puzzle tiles int unit; // scalar for sizes Tile selected; // currently selected tile boolean tileSelected; // true if tile selected final color selColor = color(40, 40, 40, 200); // selection stroke final static int snapTolerance = 10; // for placing adjoining tiles final static int X = 0; // easier vector array handling final static int Y = 1; Puzzle(int unitSize) { unit = unitSize; /* A Equilateral triangle 60 60 60 1 1 1 1 B Isosceles triangle 30 30 120 1 1 S 1 C Right triangle 30 60 90 1 S 2 2 D Right triangle 30 60 90 1 S 2 2 E Quadrilateral 60 150 30 120 1 S 2 1 3 F Trapezoid 120 60 60 120 1 1 2 1 3 G Rectangle 90 90 90 90 S 1 S 1 4 */ //NOTE: All drawn counterclockwise (CCW)! int[][] vtx = new int[7][]; // vertices float[][] sides = new float[7][]; // lengths int[] colors = new int[7]; vtx[0] = new int[] { 0, 240, 120 }; // A -- equilateral triangle sides[0] = new float[] { 1, 1, 1 }; colors[0] = color(200, 100, 20); vtx[1] = new int[] { 0, 210, 150 }; // B -- isoceles triangle sides[1] = new float[] { S, 1, 1 }; colors[1] = color(100, 100, 20); vtx[2] = new int[] { 0, 240, 90 }; // C -- right triangle sides[2] = new float[] { 1, 2, S }; colors[2] = color(80, 130, 80); vtx[3] = new int[] { 0, 270, 120 }; // D -- right triangle sides[3] = new float[] { 1, S, 2 }; colors[3] = color(80, 80, 170); vtx[4] = new int[] { 120, 90, 300, 240 }; // E -- quadrilateral sides[4] = new float[] { 1, S, 2, 1 }; colors[4] = color(150, 150, 40); vtx[5] = new int[] { 0, 240, 180, 120 }; // F -- trapezoid sides[5] = new float[] { 2, 1, 1, 1 }; colors[5] = color(130, 110, 40); vtx[6] = new int[] { 0, 270, 180, 90 }; // G -- rectangle sides[6] = new float[] { S, 1, S, 1 }; colors[6] = color(100, 80, 140); for(int i = 0; i < vtx.length; i++) { stack.push(new Tile(unit, 0, vtx[i], sides[i], int( random(width/2) + 50 ), int( random(height/2) + 100 ), colors[i])); } } // end puzzle constructor void run() { if(mousePressed && tileSelected) { // move selected tile selected.updatePos(mouseX, mouseY); } for(int i = 0; i < stack.size(); i++) { // display tiles ((Tile) stack.get(i)).display(); } } void mousePressed() { if(tileSelected) selected.isOver = false; // clear old tile tileSelected = false; for(int i = stack.size()-1; i >= 0; i--) { // from top of stack down (we want upper-most tile) boolean newSelect = false; Tile tile = (Tile) stack.get(i); newSelect = tile.isInside(mouseX, mouseY); if(newSelect == true) { // tile found under curser; move to top of stack and break out of loop selected = tile; tileSelected = true; stack.removeElementAt(i); // remove from current index in stack stack.push(tile); // push to top of stack selected.isOver = true; // flag tile that it is selected selected.calcOffset(); // offset mouse to tile break; } } } void mouseReleased() { if(tileSelected) { for(int i = stack.size()-2; i >= 0; i--) { // skip 1st tile (selected) in stack Tile tile = (Tile) stack.get(i); for(int j = 0; j < 2; j++) { for(int k = 2; k < 4; k++) { if( tile.isInsideBB(selected.bb[j], selected.bb[k]) ) { snapTo(tile); // try to snap two tiles together } } } } } } // end mouseReleased void keyPressed() { if(key == 'x' && tileSelected) { selected.flip(); } // DEBUG - turn on box for tile if(key == 'b' && tileSelected) { if(selected.showBB == false) { selected.showBB = true; } else { selected.showBB = true; } } // To rotate selected tile else if(key == LEFT || key == RIGHT || key == 'z' || key == 'c') { int sign = 1; if(key == LEFT || key == 'z') sign = -1; selected.rotate(sign); } // DEBUG dump vertices else if(key == 'p') { for(int i = 0; i < selected.ang.length; i++) { println(i + ": " + selected.ang[i] + " " + selected.sides[i]); } } } // end keyPressed() // *** Sides matching will have the same length and differ in angle by PI boolean sidesMatch() { // TODO // compare two sides return false; } boolean snapTo(Tile tile) { // look for two pairs of close vertices int match = 0; int a = 0; int b = 0; for(int i = 0; i < tile.vtx.length; i++) { for(int j = 0; j < selected.vtx.length; j++) { if(abs(tile.vtx[i][X] - selected.vtx[j][X]) < snapTolerance && abs(tile.vtx[i][Y] - selected.vtx[j][Y]) < snapTolerance) { a = i; b = j; match++; //println(match); } } } if(match > 0) { selected.moveTo(b, tile.vtx[a][X], tile.vtx[a][Y]); return true; } else { return false; } } } // end of Puzzle class class Tile { // fields int unit; // size in pixels of 1 unit; float[] ang; // angles for each vertex float[] sides; // length of each side int[][] vtx; // actual x,y vtx of current position int[] bb = new int[4]; // bounding box boolean isOver; // is mousePressed over tile? // int area; // area in terms of tile A (info only) int xPos, yPos; // current position of first vertex (lowerleft) int offsetX, offsetY; // from mousePressed float rot; // rotation - radians boolean flipped; // tile is flipped over boolean showBB; // debug - display bounding box color c; // fill color final static float rotInc = PI/6; // 30 degree increment final static int X = 0; // for vector array handling final static int Y = 1; Tile(int _unit, float _rot, int[] _angles, float[] _sides, float _xPos, float _yPos, color clr) { unit = _unit; rot = _rot; // NOTE: For finding solution, probably should work with degrees (int)!! - another array? // convert angles to radians ang = new float[_angles.length]; for(int i = 0; i < _angles.length; i++) { ang[i] = radians(_angles[i]); } sides = _sides; xPos = (int) _xPos; yPos = (int) _yPos; c = clr; vtx = new int[sides.length][2]; calcVtx(); } // end constructor void calcBoundingBox() { bb[0] = vtx[0][X]; bb[1] = vtx[0][X]; bb[2] = vtx[0][Y]; bb[3] = vtx[0][Y]; // get dimensions for(int i = 0; i < vtx.length; i++) { bb[0] = min(bb[0], vtx[i][X]); // min x -- right bb[1] = max(bb[1], vtx[i][X]); // + puzzle.snapTolerance; // max x -- left bb[2] = min(bb[2], vtx[i][Y]); // min y -- top bb[3] = max(bb[3], vtx[i][Y]); // + 8; // max y -- bottom } // loosen bb[0] -= puzzle.snapTolerance; bb[1] += puzzle.snapTolerance; bb[2] -= puzzle.snapTolerance; bb[3] += puzzle.snapTolerance; } // calculcate offset between mousePressed and xPos/yPos of tile void calcOffset() { offsetX = mouseX - xPos; offsetY = mouseY - yPos; } boolean isInsideBB(int _x, int _y) { // check bounding box if(_x > bb[0] && _x < bb[1] && _y > bb[2] && _y < bb[3]) { return true; } else { return false; } } // is coord. inside polygon? boolean isInside(int _x, int _y) { if( isInsideBB(_x, _y) ) { float b0, b1, b2, b3; int n = vtx.length; for(int i = 0; i < n; i++) { int result = (_y - vtx[i][Y]) * (vtx[(i+1) % n][X] - vtx[i][X]) - (_x - vtx[i][X]) * (vtx[(i+1) % n][Y] - vtx[i][Y]); if(result > 0) { return false; // outside } } return true; // must be inside or on line } else { return false; // outside bounding box } } // move to new position using coords for vertex given void moveTo(int index, int _x, int _y) { int moveX = _x - vtx[index][X]; int moveY = _y - vtx[index][Y]; xPos += moveX; yPos += moveY; calcOffset(); // offset to mouse position has changed! calcVtx(); } // new position; recalculate vtx void updatePos(int _x, int _y) { if(isOver) { xPos = _x - offsetX; yPos = _y - offsetY; } else { xPos = _x; yPos = _y; } calcVtx(); } // calculate vertices for current position void calcVtx() { float x = 0; float y = 0; for(int i = 0; i < ang.length; i++) { x += unit * sides[i] * cos(ang[i]); vtx[i][X] = int(x + xPos + 0.5); // add 0.5 for rounding y += unit * sides[i] * sin(ang[i]); vtx[i][Y] = int(y + yPos + 0.5); } calcBoundingBox(); } void rotate(int direction) { // direction: -1 is CCW; 1 is CW for(int i = 0; i < ang.length; i++) { ang[i] += rotInc * direction; if(ang[i] <= 0) ang[i] = TWO_PI + ang[i]; else if(ang[i] > TWO_PI) ang[i] = ang[i] - TWO_PI; } calcVtx(); } void flip() { // To flip a tile over, reverse the order of the numbers and subtract // each one from TWO_PI. (Also reverse order of sides!) if(flipped) flipped = false; else { flipped = true; }; // flip ang array (Fred Swartz) and subtract from PI; flip sides array int left = 0; // index of leftmost element int right = ang.length-1; // index of rightmost element while (left < right) { // exchange the left and right elements float tempAng = ang[left]; float tempSide = sides[left]; ang[left] = TWO_PI - ang[right]; ang[right] = TWO_PI - tempAng; sides[left] = sides[right]; sides[right] = tempSide; // move the bounds toward the center left++; right--; } // need to also do subtraction for center index!!! if(ang.length % 2 != 0) ang[left] = TWO_PI - ang[left]; // recenter xPos = bb[0] + bb[1] - xPos; calcVtx(); } void display() { if(puzzle.tileSelected && puzzle.selected == this) { if(showBB) drawBoundingBox(); // DEBUG stroke(puzzle.selColor); } fill(c); beginShape(POLYGON); { for(int i = 0; i < vtx.length; i++) { vertex( vtx[i][X], vtx[i][Y] ); } } endShape(); noStroke(); } void drawBoundingBox() { stroke(100, 100, 100); beginShape(LINE_LOOP); { vertex( bb[0], bb[3] ); vertex( bb[1], bb[3] ); vertex( bb[1], bb[2] ); vertex( bb[0], bb[2] ); vertex( bb[0], bb[3] ); } endShape(); noStroke(); } } // end Tile class