Hey guys!
I have a project for school that involves changing the code I posted under this. One problem that I cannot figure out is how I would convert the mouse listener to a key listener. I believe the checkEmpty, moveTile, and exchangeTile methods will have to be coded differently in the LOGIC portion of the code, along with the listener in the GUI section of the code.

Any help would be greatly appreciated! Thanks!
Gary

MAIN PROGRAM

 import javax.swing.JFrame;

    ///////////////////////////////////////////// class SlidePuzzle
    class SlidePuzzle {
        //============================================= method main
        public static void main(String[] args) {
            JFrame window = new JFrame("Slide Puzzle");
            window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            window.setContentPane(new SlidePuzzleGUI());
            window.pack();  // finalize layout
            window.show();  // make window visible
            window.setResizable(false);
        }//end main
    }//endclass SlidePuzzle

GUI

  import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.event.*;

    /////////////////////////////////////////////////// class SlidePuzzleGUI
    // This class contains all the parts of the GUI interface
    class SlidePuzzleGUI extends JPanel {
        //=============================================== instance variables
        private GraphicsPanel    _puzzleGraphics;
        private SlidePuzzleModel _puzzleModel = new SlidePuzzleModel();
        //end instance variables


        //====================================================== constructor
        public SlidePuzzleGUI() {
            //--- Create a button.  Add a listener to it.
            JButton newGameButton = new JButton("New Game");
            newGameButton.addActionListener(new NewGameAction());

            //--- Create control panel
            JPanel controlPanel = new JPanel();
            controlPanel.setLayout(new FlowLayout());
            controlPanel.add(newGameButton);

            //--- Create graphics panel
            _puzzleGraphics = new GraphicsPanel();

            //--- Set the layout and add the components
            this.setLayout(new BorderLayout());
            this.add(controlPanel, BorderLayout.NORTH);
            this.add(_puzzleGraphics, BorderLayout.CENTER);
        }//end constructor


        //////////////////////////////////////////////// class GraphicsPanel
        // This is defined inside the outer class so that
        // it can use the outer class instance variables.
        class GraphicsPanel extends JPanel implements MouseListener {
            private static final int ROWS = 3;
            private static final int COLS = 3;

            private static final int CELL_SIZE = 80; // Pixels
            private Font _biggerFont;


            //================================================== constructor
            public GraphicsPanel() {
                _biggerFont = new Font("SansSerif", Font.BOLD, CELL_SIZE/2);
                this.setPreferredSize(
                       new Dimension(CELL_SIZE * COLS, CELL_SIZE*ROWS));
                this.setBackground(Color.black);
                this.addMouseListener(this);  // Listen own mouse events.
            }//end constructor


            //=======================================x method paintComponent
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                for (int r=0; r<ROWS; r++) {
                    for (int c=0; c<COLS; c++) {
                        int x = c * CELL_SIZE;
                        int y = r * CELL_SIZE;
                        String text = _puzzleModel.getFace(r, c);
                        if (text != null) {
                            g.setColor(Color.gray);
                            g.fillRect(x+2, y+2, CELL_SIZE-4, CELL_SIZE-4);
                            g.setColor(Color.black);
                            g.setFont(_biggerFont);
                            g.drawString(text, x+20, y+(3*CELL_SIZE)/4);
                        }
                    }
                }
            }//end paintComponent


            //======================================== listener mousePressed
            public void mousePressed(MouseEvent e) {
                //--- map x,y coordinates into a row and col.
                int col = e.getX()/CELL_SIZE;
                int row = e.getY()/CELL_SIZE;

                if (!_puzzleModel.moveTile(row, col)) {
                    // moveTile moves tile if legal, else returns false.
                    Toolkit.getDefaultToolkit().beep();
                }

                this.repaint();  // Show any updates to model.
            }//end mousePressed


            //========================================== ignore these events
            public void mouseClicked (MouseEvent e) {}
            public void mouseReleased(MouseEvent e) {}
            public void mouseEntered (MouseEvent e) {}
            public void mouseExited  (MouseEvent e) {}
        }//end class GraphicsPanel

        ////////////////////////////////////////// inner class NewGameAction
        public class NewGameAction implements ActionListener {
            public void actionPerformed(ActionEvent e) {
                _puzzleModel.reset();
                _puzzleGraphics.repaint();
            }
        }//end inner class NewGameAction

    }//end class SlidePuzzleGUI

LOGIC

class SlidePuzzleModel {
    private static final int ROWS = 3;
    private static final int COLS = 3;

    private Tile[][] _contents;  // All tiles.
    private Tile     _emptyTile; // The empty space.


    //================================================= constructor
    public SlidePuzzleModel() {
        _contents = new Tile[ROWS][COLS];
        reset();               // Initialize and shuffle tiles.
    }//end constructor


    //===================================================== getFace
    // Return the string to display at given row, col.
    String getFace(int row, int col) {
        return _contents[row][col].getFace();
    }//end getFace


    //======================================================= reset
    // Initialize and shuffle the tiles.
    public void reset() {
        for (int r=0; r<ROWS; r++) {
            for (int c=0; c<COLS; c++) {
                _contents[r][c] = new Tile(r, c, "" + (r*COLS+c+1));
            }
        }
        //--- Set last tile face to null to mark empty space
        _emptyTile = _contents[ROWS-1][COLS-1];
        _emptyTile.setFace(null);

        //-- Shuffle - Exchange each tile with random tile.
        for (int r=0; r<ROWS; r++) {
            for (int c=0; c<COLS; c++) {
                exchangeTiles(r, c, (int)(Math.random()*ROWS)
                                  , (int)(Math.random()*COLS));
            }
        }
    }//end reset


    //==================================================== moveTile
    // Move a tile to empty position beside it, if possible.
    // Return true if it was moved, false if not legal.
    public boolean moveTile(int r, int c) {
        //--- It's a legal move if the empty cell is next to it.
        return checkEmpty(r, c, -1, 0) || checkEmpty(r, c, 1, 0)
            || checkEmpty(r, c, 0, -1) || checkEmpty(r, c, 0, 1);
    }//end moveTile


    //================================================== checkEmpty
    // Check to see if there is an empty position beside tile.
    // Return true and exchange if possible, else return false.
    private boolean checkEmpty(int r, int c, int rdelta, int cdelta) {
        int rNeighbor = r + rdelta;
        int cNeighbor = c + cdelta;
        //--- Check to see if this neighbor is on board and is empty.
        if (isLegalRowCol(rNeighbor, cNeighbor) 
                  && _contents[rNeighbor][cNeighbor] == _emptyTile) {
            exchangeTiles(r, c, rNeighbor, cNeighbor);
            return true;
        }
        return false;
    }//end checkEmpty


    //=============================================== isLegalRowCol
    // Check for legal row, col
    public boolean isLegalRowCol(int r, int c) {
        return r>=0 && r<ROWS && c>=0 && c<COLS;
    }//end isLegalRowCol


    //=============================================== exchangeTiles
    // Exchange two tiles.
    private void exchangeTiles(int r1, int c1, int r2, int c2) {
        Tile temp = _contents[r1][c1];
        _contents[r1][c1] = _contents[r2][c2];
        _contents[r2][c2] = temp;
    }//end exchangeTiles


    //=================================================== isGameOver
    public boolean isGameOver() {
        for (int r=0; r<ROWS; r++) {
            for (int c=0; c<ROWS; c++) {
                Tile trc = _contents[r][c];
                return trc.isInFinalPosition(r, c);
            }
        }

        //--- Falling thru loop means nothing out of place.
        return true;
    }//end isGameOver
}//end class SlidePuzzleModel



////////////////////////////////////////////////////////// class Tile
// Represents the individual "tiles" that slide in puzzle.
class Tile {
    //============================================ instance variables
    private int _row;     // row of final position
    private int _col;     // col of final position
    private String _face;  // string to display 
    //end instance variables


    //==================================================== constructor
    public Tile(int row, int col, String face) {
        _row = row;
        _col = col;
        _face = face;
    }//end constructor


    //======================================================== setFace
    public void setFace(String newFace) {
        _face = newFace;
    }//end getFace


    //======================================================== getFace
    public String getFace() {
        return _face;
    }//end getFace


    //=============================================== isInFinalPosition
    public boolean isInFinalPosition(int r, int c) {
        return r==_row && c==_col;
    }//end isInFinalPosition
}//end class Tile

Recommended Answers

All 18 Replies

You should not have to change the logic classes - that's why we separate logic from user interface in the first place. Obviously you will need new listeners, but they should call the same methods that the mouse listeners call

I tried that, but the way it figures out which tile to move depends on the location of the mouse cursor, so it does not work when I try to just change it to a key listener. This is why I thought the method would have to be rewritten. How can I make the key listener always focus the empty tile? That is the main problem I am having.

Looking forward to your input.

Not sure exactly what you mean?
Does the logic require the concept of a "current tile" corresponding to where the mouse is when the button is pressed (lines 80,81)? If so you will need to add that to the logic, and update it when the user presses the arrow keys. ie Arrow keys move the "current tile". Clicking the mouse sets the current tile. check entry, moves etc are all use the current tile.
It's not so much a case of changing the logic, it more like moving he last bit of the logic from the mouse handler into the model so the keyboard can use it too.

For example, if I change it to keylistener and leave the methods, the code will be as follows:

public void keyPressed(KeyEvent e) {
    if(e.getKeyCode() == KeyEvent.VK_LEFT){
                int col = e.getX()/CELL_SIZE;
                int row = e.getY()/CELL_SIZE;
                if (!_puzzleModel.moveTile(row, col)) {
                    // moveTile moves tile if legal, else returns false.
                    Toolkit.getDefaultToolkit().beep();
                }
                this.repaint();  // Show any updates to model.
    }
}

However in lines 3 & 4 the previous method calls for the method getX and getY which cannot be identified for KeyEvent.
This is where I'm stuck.

Sorry I don't think I explained it very well.
With the current code, you click on a tile, and it will check if that tile has any legal moves and if so it will move the clicked tile to the empty space. Whatever tile is clicked is the selected tile that is checked and moved.

What I want to accomplish:
Instead of clicking on a tile and moving it to the empty space, I want to use the arrow keys to move whichever tile can move into the empty space in the direction of the corresponding key that was pressed.
I believe the easiest way to do this would be to have the direction keys move the EMPTY tile. Since the current methods work by checking whatever tile is clicked, I don't think I can do it with these methods. I need the EMPTY tile to always be the tile selected by the listener.

I hope I am making more sense :)
Thanks for your input thus far!

It can't be hard to add a small
public void moveEmptyCell(int direction)
method to your logic, and just call that from the key handler?

ps On second thoughts, by the time you've coded the switch in that method, it will be shorter and clearer just to have four methods:
public void moveEmptySquareUp()
public void moveEmptySquareLeft()
etc

very helpful so far, thank you.

The tiles are set up in an array. How would I make those newly added methods focus on the empty tile? And swap with the tile in the corresponding direction?

You know the empty tile, its _emptyTile, and you have a exchangeTiles method. Just get the row & col for the empty tile, exchange it with row, (col-1) for moveEmptySquareLeft etc

I tried this with no luck, I believe this is what you meant?

public void moveEmptySquareLeft(){      
        exchangeTiles(ROWS-1,COLS-1,-1,0);

}

Sorry, I'm very new to coding, but I appreciate all the help you're giving me!

Not quite...
eg - moveEmptySquareLeft:
Need to: swap the empty cell with the cell immediatley to its left.
First: Get the row and col for the empty square in (eg) emptySquareRow and emptySquareCol
Then: exchangeTiles(emptySquareRow, emptySquareCol, emptySquareRow , emptySquareCol -1);
(but first check that emptySquareCol -1 doesn't take you off the grid)

ps: going out now, so no more DaniWeb from me until tomorrow :)

I can't figure out how to find the row and col of the emptyTile to set it to the emptySquareRow and emptySquareCol variables.

And okay have a good time, thanks again for everything so far.

The brute-force method is to have nested loops for row and col, loop thru the whole array until you find the empty cell then exit the loops - that will leave the row/col values that you need.
WHat his really illustrates is why you should design and implement your model before doing too much work on the view. By starting with the GUI your model design is compromised towards one particular user interface need. Personally I would have instance variables in the Tile class for the current position of each tile and have the move methods in Tile to update those variables. As the last line of a move method I would just update the contents array, but maybe it's too late to do that now.

Hey again, this is what I've tried to find the location. I have no errors, but it still isn't working.

    public void moveEmptySquareLeft(){      
        _emptyTile = _contents[0][0];

        for(int i = 0; i < _contents.length; i++){
            for(int j = 0; j < _contents.length; j++){
                if(_contents[i][j] == _emptyTile){
                    emptyRow = i;
                    emptyCol = j;
                }
            }
        }
        exchangeTiles(emptyRow,emptyCol,emptyRow-1,emptyCol);


    }

What is line 2 for - it seems just to overwrite the empty tile?

True, I guess that line of code needs to be taken out. Regardless, I'm still doing something wrong, I still have no movement with the tiles :/

Time for some simple debugging, eg print the values of emptyRow,emptyCol just before you call excahngeTiles so you can see what your code is actually doing.

There is somethint wrong with my keyListener

        public void keyPressed(KeyEvent e){
            if(e.getKeyCode() == KeyEvent.VK_LEFT){
                System.out.print("test");
                _puzzleModel.moveEmptySquareLeft();
            }
            this.repaint();

This does not result in any prints when the left arrow key is pressed

Good debugging! Seriously. You identified where the problem really is, which is a good start.

Key Listeners can be really frustrating because they only respond when the keyboard focus is on the component that you added the listener to. So most of the time the keyboard focus is on some other component and nothing happens. I don't know what components you have in your window, so I can't guess where the keyboard focus might be, but if you know/can guess, then try adding the key listener to that component (or components - adding the listener to everying in sight often works).

Sun realised this problem, and implemented key bindings to avoid it. Key bindings link keystrokes to Actions, even when the focus is somewhere else in the window. Here's the tutorial:
http://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html
It's a bit confusing at first because there are a few steps to make it work, but stick with it and follow the steps one at a time. Once you've "got" it you'll be a keyboard ninja master and never have this problem again.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.