Midterm Project: Minion Puzzle

a snapshot from the puzzle

Inspiration:

My inspiration for this game was my puzzle assignments from Intro to CS that involved matrices calculations and boards (tik tac to, connect 4, etc.) and also this artwork in the Arts Center hallway:

The idea of the game is very simple. Just like the frame above, there is only one empty tile (randomly placed as a result of shuffling). Click on any empty tile’s adjacent cell to swap the two. Continue till all the pieces are in place!

Implementation:

Two (huge) classes Puzzle and Tile, seven methods, eight functions, 292 lines, and lots of debugging prints in between. 

The basic idea is how you think about any puzzle; we need to create a board, divide it into cells (that can be replaced or swapped), assign each tile a pattern in such a way that the whole board represents a complete photo, then shuffle the cells and try to solve the puzzle. Each puzzle has a different set of rules; in this puzzle, you have one empty cell and are allowed to swap it with an adjacent cell.

The tiles:

It starts with a single tile. The tile class creates an object Tile with the appropriate properties. Each tile object has a specific position on the board tile[row][column]:

// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
class Tile {
  
  constructor (r, c, img_index, numRows, numCols) {   
    this.r = r;
    this.c = c;
    this.numRows = numRows;
    this.numCols = numCols;
    this.img_index = img_index;
    this.img = loadImage ('resources/' + this.img_index+ '.png');
    // print("image index is: ", this.img_index);
  }
}
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Each tile object is also assigned an image index ranging from 0-15, and the way we are displaying it is by multiplying the tile’s row and column number by its size (defined at the beginning of the program):

// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
display_tile () { 
  if (this.img_index != (this.numRows*this.numCols) - 1) {
  // print("image index is: ", this.img_index)
  image(this.img, this.c*my_width, this.r*my_height, my_width, my_height);
  }
}
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

The board (matrix):

We are now ready to populate an array of arrays with our tile objects. The Puzzle class has a createBoard() method that loops through the range of numRows and numCols (passed as arguments in the Puzzle object constructor). A temporary array is created for each row in numRows, and then numCols arrays (numbers of columns) are pushed into the temp array. Then a new Tile object is created for each cell. Then we push this temporary list to the board (every tile is in the correct place at this point). Now before you exit the function, you shuffle all the tiles.

// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    createBoard () {
      this.board = [];
      
      for (var r=0; r<this.numRows; r++) {
        // for each row create a temp list
        var tempList = [];
        
        // add numCols tiles to the list
        for (var c=0; c<this.numCols; c++) {
          // r*c is the image index
          var new_tile = new Tile(r, c, r*this.numCols+c, this.numRows, this.numCols);
          tempList.push(new_tile);
        }
        
        // push the list to the board
        this.board.push(tempList);
      }
      this.shuffle_tiles();
    }
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Shuffling tiles:

Picking random values for any tile’s [r] and [c] values (ranging from 0 to 4). Identify its neighbors -> [[0,-1], [1,0], [0,1], [-1,0]]. Swap the tile with a neighbor from this list (also random). 

// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    shuffle_tiles () {
      
      // var current_r = this.numRows-1;
      // var current_c = this.numCols-1;
      var current_r = int(random(0,4));  // initially 0-3, left most cells or right most? 
      var current_c = int(random(0,4));      

      var neighbors = [[0,-1], [1,0], [0,1], [-1,0]];
      
      // increase s to have a more standard shuffle - was 20 initially
      for (var s=0; s<10; s++) {
        var empty_tile = this.board[current_r][current_c];
        var shuffling = random(neighbors);

        var destination_r = current_r + shuffling [0];
        var destination_c = current_c + shuffling [1];

        while (destination_r < 0 || destination_c < 0 || destination_r > this.numRows-1 || destination_c > this.numCols-1) {
          shuffling = random(neighbors);
          destination_r = current_r + shuffling [0];
          destination_c = current_c + shuffling [1];
        }

        var new_tile = this.board[destination_r][destination_c];
        
        // print("Before swap: " + str(this.board [current_r][current_c].img_index) + ", " + str(this.board [destination_r][destination_c].img_index));
        
        var temp = empty_tile.img_index;
        empty_tile.img_index = new_tile.img_index;
        new_tile.img_index = temp;

        // print("After swap: " + str(this.board [current_r][current_c].img_index) + ", " + str(this.board [destination_r][destination_c].img_index));

        current_r = destination_r;
        current_c = destination_c;

        for (var r=0; r<this.numRows; r++) {
          for (var c=0; c<this.numCols; c++) {
              var the_tile = this.board[r][c];
              the_tile.img = loadImage ('resources/'+str(the_tile.img_index)+'.png');
          }
        }
      }
    }
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Swapping tiles:

Method tile_empty_adj() returns two important values: the current empty tile (empty tile has a fixed index of [3][3], but it moves around as a result of shuffle), and also a list of empty tile’s neighbors.

We record mouse[x][y] when a click happens on the empty cell’s neighbors and swap their img_index so at the next iteration of draw() when the display() method is called, the tiles would be switched and now you have a new empty cell. Repeat process.

// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    swap(nonempty, empty) {
      // takes coordinates of two tiles, swap their index
      var empty_tile = this.board[empty[0]][empty[1]];
      // print("this is empty tile & img index: ", empty_tile, empty_tile.img_index);
      var new_tile = this.board [nonempty[0]][nonempty[1]];
      // print("this is new tile & img index: ", new_tile, new_tile.img_index);

      var temp = empty_tile.img_index;
      empty_tile.img_index = new_tile.img_index;
      new_tile.img_index = temp;


      var tmp=new_tile.img;
      new_tile.img = empty_tile.img;
      // loadImage ('resources/'+str(new_tile.img_index)+'.png');
      empty_tile.img = tmp;
      // loadImage ('resources/'+str(empty_tile.img_index)+'.png');
      
      // print("after swap: ", empty_tile, empty_tile.img_index);
      // print("after swap: ", new_tile, new_tile.img_index);

      for (var r=0; r<this.numRows; r++) {            
        for (var c=0; c<this.numCols; c++) {
          // print(r,c, this.board[r][c].img_index);
          if (this.board[r][c].img_index != r*4 + c) {
              return false;
          }
        }
      }

      this.win = true;
      print ("won game!");
      this.win_sound.play();
    }
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function mouseClicked () {
  
  var empty, adj, empty_adj; 
  empty_adj = my_puzzle.tile_empty_adj();

  empty = empty_adj[0];
  adj = empty_adj[1];
  // print("adjacent cells: ", adj);

  mouse = [int(mouseY/my_height), int(mouseX/my_width)];

  // print(mouse);
  for(var i=0; i<adj.length;i++) {
    if(mouse[0] == adj[i][0] && mouse[1] == adj[i][1]) {
      // print("we are here in the swap call\n");
      // switch the empty and non-empty slots
      my_puzzle.swap(mouse, empty);
      // my_puzzle.display();
    }
  }
}
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Win conditions:

You win if ALL the tiles in the board have the img_index initially assigned to them e.g. tile[0][0] -> img_index 0, tile[2][3] -> img_index 12 and so on. The empty cell belongs to the last tile (tile[4][4]) and that’s where it should end up for the win condition to be true.

// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
      for (var r=0; r<this.numRows; r++) {            
        for (var c=0; c<this.numCols; c++) {
          // print(r,c, this.board[r][c].img_index);
          if (this.board[r][c].img_index != r*4 + c) {
              return false;
          }
        }
      }

      this.win = true;
      print ("won game!");
      this.win_sound.play();
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Gameplay:

I tried to keep the instructions very minimal:

1) User is presented with an instructions screen that they can go back to at any point in the game by pressing i (i = instructions)

2) User can start the game by pressing p (p = puzzle)

3) User has the choice to play the minions’ music or mute it while they solve the puzzle (it has a low amplitude anyway). (m = mute, u = unmute)

4) In some rounds the shuffling is very complicated (due to randomness) and the user might not want to spend a lot of time on solving it (e.g. for playtesting purposes). The user has the option to reshuffle the board by pressing s (s = shuffle).

Embedded sketch:

Improvements:

Currently, I’m working on writing a function that slices any image into X equal parts (very useful for puzzle purposes). This can extend the current program from a minion puzzle to any puzzle of the user’s choosing. While this function is easy to implement, the next step that involves saving each slice (of image) back to the assets folder is a bit tricky and needs some research. But overall I think the createBoard()/swap()/shuffle()/display() methods are very extendible and are basic foundations for many board games.

[the end]

Leave a Reply