1
\$\begingroup\$

I'm a Java beginner and I've just started learning about GUI. However, I'm not sure about GUI coding conventions and whether I'm getting the object orientation part of it right. I feel like I'm passing objects around different classes too much.

I would like some critique on my code and better ways to do the checkWinningMove() method.

main class:

public class TicTacToe {
public static void main(String[] args){
    Player player1 = new Player("X");
    Player player2 = new Player("O");
    Grid grid = new Grid(player1, player2);     
}
}

Grid class:

import java.awt.GridLayout;

import javax.swing.*;

public class Grid {
Player player1;
Player player2;

int rows = 3;
int columns = 3;
JFrame frame;
JPanel panel = new JPanel();
JButton[][] buttonGrid = new XOButton[rows][columns];
String[] stringGrid = new String[rows * columns];

public Grid(Player p1, Player p2) {
    this.player1 = p1;
    this.player2 = p2;

    try {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    } catch (Exception e) {
        e.printStackTrace();
    }
    createGrid();
}

public void createGrid() {
    frame = new JFrame("Tic Tac Toe");
    frame.setSize(300, 300);
    frame.setVisible(true);
    frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
    frame.setLocationRelativeTo(null);
    frame.setResizable(false);

    panel.setLayout(new GridLayout(3, 3));

    initialiseButtons();
    initialiseStringGrid();

    frame.add(panel);

}

public void initialiseButtons() {
    for (int i = 0; i < buttonGrid.length; i++) {
        for (int j = 0; j < buttonGrid[i].length; j++) {
            buttonGrid[i][j] = new XOButton(this);
            panel.add(buttonGrid[i][j]);
        }
    }
}

public void initialiseStringGrid() {
    for (int i = 0; i < stringGrid.length; i++) {
        stringGrid[i] = "-1";
    }
}

public void updateStringGrid(int position, String token) {
    stringGrid[position] = token;
}

public void checkIfWinningMove(int position, String token) {
    switch (position) {
    case 0:

        // check horizontal
        if (stringGrid[1].equals(token) && stringGrid[2].equals(token)) {
            winningMove(token);
        }
        // check diagonal
        else if (stringGrid[4].equals(token) && stringGrid[8].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[3].equals(token) && stringGrid[3].equals(token)) {
            winningMove(token);
        }

        break;
    case 1:

        // check horizontal
        if (stringGrid[0].equals(token) && stringGrid[2].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[4].equals(token) && stringGrid[7].equals(token)) {
            winningMove(token);
        }

        break;
    case 2:

        // check horizontal
        if (stringGrid[1].equals(token) && stringGrid[0].equals(token)) {
            winningMove(token);
        }
        // check diagonal
        else if (stringGrid[4].equals(token) && stringGrid[6].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[5].equals(token) && stringGrid[8].equals(token)) {
            winningMove(token);
        }

        break;
    case 3:
        // check horizontal
        if (stringGrid[4].equals(token) && stringGrid[5].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[0].equals(token) && stringGrid[6].equals(token)) {
            winningMove(token);
        }

        break;
    case 4:
        // check horizontal
        if (stringGrid[3].equals(token) && stringGrid[5].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[1].equals(token) && stringGrid[7].equals(token)) {
            winningMove(token);
        }
        // check diagonal 1
        if (stringGrid[0].equals(token) && stringGrid[8].equals(token)) {
            winningMove(token);
        }
        // check diagonal 2
        else if (stringGrid[2].equals(token) && stringGrid[6].equals(token)) {
            winningMove(token);
        }
        break;
    case 5:
        // check horizontal
        if (stringGrid[4].equals(token) && stringGrid[3].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[2].equals(token) && stringGrid[8].equals(token)) {
            winningMove(token);
        }   

        break;
    case 6:

        // check horizontal
        if (stringGrid[7].equals(token) && stringGrid[8].equals(token)) {
            winningMove(token);
        }
        // check diagonal
        else if (stringGrid[4].equals(token) && stringGrid[2].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[3].equals(token) && stringGrid[0].equals(token)) {
            winningMove(token);
        }

        break;
    case 7:
        // check horizontal
        if (stringGrid[6].equals(token) && stringGrid[8].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[4].equals(token) && stringGrid[1].equals(token)) {
            winningMove(token);
        }
        break;
    case 8:

        // check horizontal
        if (stringGrid[7].equals(token) && stringGrid[6].equals(token)) {
            winningMove(token);
        }
        // check diagonal
        else if (stringGrid[4].equals(token) && stringGrid[0].equals(token)) {
            winningMove(token);
        }
        // check vertical
        else if (stringGrid[5].equals(token) && stringGrid[2].equals(token)) {
            winningMove(token);
        }

        break;

    }
}

public void winningMove(String token) {
    if(token.equals(player1.getToken()))
        JOptionPane.showMessageDialog(frame, "Player 1 wins " + "(" + player1.getToken() + ")");
    else
        JOptionPane.showMessageDialog(frame, "Player 2 wins " + "(" + player2.getToken() + ")");
    System.exit(0);
}

}

Player class:

public class Player {
private String token;

public Player(String token){
    this.token = token;
}

public String getToken(){
    return token;
}

}

Button class:

import java.awt.Font;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JButton;

public class XOButton extends JButton { 
Grid g;
int status = 0;
static int clickCount=0;
static int id = 0;
int buttonId;

public XOButton(Grid grid){
    g = grid;
    buttonId += id;
    id++;
    setFont(new Font("Arial", Font.PLAIN, 60));
    this.buildButton();
}

public void buildButton(){
    this.addMouseListener(new MouseAdapter() {
        @Override
        public void mouseClicked(MouseEvent e) {

            if(clickCount == 0){
                if(isEnabled()){
                    setText(g.player1.getToken());
                    g.updateStringGrid(buttonId, g.player1.getToken());
                    g.checkIfWinningMove(buttonId, g.player1.getToken());
                }
                setEnabled(false);

            }else{
                if(isEnabled()){
                    setText(g.player2.getToken());
                    g.updateStringGrid(buttonId, g.player2.getToken());
                    g.checkIfWinningMove(buttonId, g.player2.getToken());
                }
                setEnabled(false);
            }

            clickCount++;
            clickCount %= 2;
        }
    });
}

}

Some screenshots of the GUI:

enter image description here

enter image description here

enter image description here

\$\endgroup\$
0
3
\$\begingroup\$

I hope you're enjoying learning about Java and programming in general. First, it's my belief that GUI programming is probably not the easiest way to learn how to program and I would suggest that you might want to consider learning more of the basics before digging too deeply into the GUI aspects.

As far as your code is concerned I don't think you're "passing objects around different classes too much" at all. In fact I think that you've done pretty well with regard to the general object oriented design of the classes.

When you said you would like to "know about better ways to do the checkWinningMove() method" my assumption is that you want to do it more generically rather than hardcoding all the possible outcomes. With that in mind I have modified your Grid and XOButton classes below. Most of my critiques are included in comments within the code.

You will notice that some of my comments refer to a link about access modifiers, you can find that right here. It would be a good idea to become familiar with these modifiers and use them appropriately to restrict access to the internal workings of your classes. Only make public what needs to be seen by code that is using your class.

BEGIN EDIT #1:

I realized after posting that you were using MouseListener instead of ActionListener in your button class. Please read this about why you should use ActionListener. I updated the code below to use an ActionListener.

One other thing that comes to mind - you could optimize the logic that performs the winning move check if you give your buttons additional fields to represent their row and column in the grid. If you do this then you could, for example, check only the row of the button that was clicked when looking for a horizontal win and check only the column of the button when searching for a vertical win. Since optimization was not part of your question and you said you're new to Java I did NOT implement this logic in the code below. If you want to try it out here's a code snippet to get you started:

Grid:

public void initialiseButtons() {
        //renamed i and j to row and col for readability
        for (int row = 0; row < buttonGrid.length; row++) {
            for (int col = 0; col < buttonGrid[row].length; col++) {

                //Changed this row to add row and col parameters in button constructor
                buttonGrid[row][col] = new XOButton(this, row, col);
                panel.add(buttonGrid[row][col]);
            }
        }
    }

XOButton:

public XOButton(Grid grid, int row, int col) {
    ...
    this.row = row;
    this.col = col;
    ...
}

public int getRow(){...}
public int getCol(){...}

END EDIT #1

BEGIN EDIT #2

I wrote my original post and the edit late and night so I didn't have much chance to polish the code. As I thought about it more this morning I decided it might be best to illustrate one of the principles of Object Oriented Design - code reuse. Although perhaps in this instance it falls under modular design more than Object Oriented since the code is reused within the same class, however, I think it's relevant to your question and will help you to see how a design is polished. As you look at my original post, pay attention to the following methods: isHorizontalWin() isVerticalWin() isDiagonalWin() and notice how the first two are almost identical and that the two parts of the third method are almost identical as well. This is a good indicator that it would be helpful to create a something more generic that can be reused. This is beneficial because when maintaining the code you can change only one place in order to fix bugs or add features that affect several methods. It also reduces the number of lines of code making it easier to read (if done well). Since you're new to programming let me explain my thinking - I thought that it would be easiest to implement a generic solution for the horizontal win check so I tackled that first. It then occurred to me that every win scenario involves a set of buttons all having the same "token" as their text. Therefore I wrote this method:

/**
     * Determines whether all of the buttons in the specified array have the 
     * same text. Note that empty string is not considered valid text.
     * @param lineToProcess an array of buttons representing a line in the grid
     * @return true if all buttons in the array have the same non-empty text, false otherwise.
     */
    private boolean isWin(JButton[] lineToProcess) {
        String rowToken = null;
        boolean foundWin = true;
        for(JButton button: lineToProcess) {
            if (rowToken == null) {
                rowToken = button.getText();
            } else if ("".equals(button.getText())) {
                foundWin = false;
                break;
            } else if (!rowToken.equals(button.getText())) {
                foundWin = false;
                break;
            }
        }
        return foundWin;
    }

Implementing the horizontal win method using this new method above proved very easy:

     /**
     * Determines if there is a winner by checking each row for consecutive
     * matching tokens.
     * @return true if there is a winner horizontally, false otherwise.
     */
    private boolean isHorizontalWin() {
        for(int row = 0; row < buttonGrid.length; row++){
            if(isWin(buttonGrid[row]))
                return true;
        }
        return false;
    }

The vertical win method was slightly more complex because I had to build an array containing all the buttons in a column of the grid:

/**
 * Determines whether there is a winner by checking column for consecutive
 * matching tokens.
 * @return true if there is a vertical winner, false otherwise.
 */
private boolean isVerticalWin() {
    JButton[] column = null;
    for(int col = 0; col < columns; col++){
        column = new JButton[rows];
        for(int row = 0; row < rows; row++){
            column[row] = buttonGrid[row][col];
        }
        if(isWin(column))
            return true;
    }
    return false;
}

Finally I implemented the diagonal win method by creating an array containing references to all of the buttons on a diagonal then repeating that for the second diagonal:

 /**
 * Determines if there is a winner by checking each diagonal for consecutive
 * matching tokens.
 * @return true if a diagonal winner exists, false otherwise.
 */
private boolean isDiagonalWin() {

    int row = 0, col = 0;

    //Create a one-dimensional array to represent the diagonal. Use the lesser
    // of the rows or columns to set its size. If the grid is rectangular then
    // a diagonal will always be the size of the lesser of its two dimensions.
    int size = columns < rows ? columns : rows;
    JButton[] diagonal = new JButton[size];

    //Since we know the grid is a square we really could just check one of
    // these - either row or col, but I left both in here anyway.
    while (row < rows && col < columns) {
        diagonal[col] = buttonGrid[row][col];

        row++;
        col++;
    }
    if (isWin(diagonal)) {
        return true;
    }


    row = rows - 1;
    col = 0;
    diagonal = new JButton[size];
    while (row >=0 && col < columns) {
        diagonal[col] = buttonGrid[row][col];
        row--;
        col++;
    }
    return isWin(diagonal);

}

END EDIT #2

Here is the Grid class:

import java.awt.GridLayout;

import javax.swing.*;

public class Grid {
    //The fields of this class do not have any access level modifiers,
    // it would probably be wise to look into this, see the link provided.

    Player player1;
    Player player2;
    //Added this field to track the player whose turn it is currently.
    Player currentPlayer;

    int rows = 3;
    int columns = 3;
    JFrame frame;
    JPanel panel = new JPanel();
    JButton[][] buttonGrid = new XOButton[rows][columns];
//    Removed this field as it is not needed any longer.
//    String[] stringGrid = new String[rows * columns];

    public Grid(Player p1, Player p2) {
        this.player1 = p1;
        this.player2 = p2;
        currentPlayer = player1; //added a variable to track the current player

        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //It is generally not good design to have overridable methods in a 
        // constructor call, consider making this method final or private.
        createGrid();
    }

    /**
     * @return the Player whose turn it is currently.
     */
    public Player getCurrentPlayer() {
        return currentPlayer;
    }

    /**
     * Yields control to the next player
     */
    public void nextPlayer() {
        if (currentPlayer.equals(player1)) {
            System.out.println("It's player2's (" + player2.getToken() + ") turn now.");
            currentPlayer = player2;
        } else {
            System.out.println("It's player1's (" + player1.getToken() + ") turn now.");
            currentPlayer = player1;
        }
    }

    public void createGrid() {
        frame = new JFrame("Tic Tac Toe");
        frame.setSize(300, 300);
        frame.setVisible(true);
        frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setResizable(false);

        //Changed this line to use the rows and columns variables
        panel.setLayout(new GridLayout(rows, columns));

        initialiseButtons();
//        removed - no longer needed
//        initialiseStringGrid();

        frame.add(panel);

    }

    public void initialiseButtons() {
        //renamed i and j to row and col for readability
        for (int row = 0; row < buttonGrid.length; row++) {
            for (int col = 0; col < buttonGrid[row].length; col++) {
                buttonGrid[row][col] = new XOButton(this);
                panel.add(buttonGrid[row][col]);
            }
        }
    }

//    removed - no longer needed
//    public void initialiseStringGrid() {
//        for (int i = 0; i < stringGrid.length; i++) {
//            stringGrid[i] = "-1";
//        }
//    }
//    removed - no longer needed
//    public void updateStringGrid(int position, String token) {
//        stringGrid[position] = token;
//    }

    public void checkIfWinningMove() {
        if (isHorizontalWin() || isVerticalWin() || isDiagonalWin()) {
            winningMove(currentPlayer.getToken());
        }
    }

    /**
     * Determines if there is a winner by checking each row for consecutive
     * matching tokens.
     * @return true if there is a winner horizontally, false otherwise.
     */
    private boolean isHorizontalWin() {
        for (int row = 0; row < buttonGrid.length; row++) {
            String rowToken = null;
            boolean foundWin = true;
            for (int col = 0; col < buttonGrid[row].length; col++) {
                if (rowToken == null) {
                    rowToken = buttonGrid[row][col].getText();
                } else if ("".equals(buttonGrid[row][col].getText())) {
                    foundWin = false;
                    break;
                } else if (!rowToken.equals(buttonGrid[row][col].getText())) {
                    foundWin = false;
                    break;
                }
            }
            if (foundWin) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determines whether there is a winner by checking column for consecutive
     * matching tokens.
     * @return true if there is a vertical winner, false otherwise.
     */
    private boolean isVerticalWin() {
        for (int col = 0; col < columns; col++) {
            String colToken = null;
            boolean foundWin = true;
            for (int row = 0; row < rows; row++) {
                if (colToken == null) {
                    colToken = buttonGrid[row][col].getText();
                } else if ("".equals(buttonGrid[row][col].getText())) {
                    foundWin = false;
                    break;
                } else if (!colToken.equals(buttonGrid[row][col].getText())) {
                    foundWin = false;
                    break;
                }
            }
            if (foundWin) {
                return true;
            }
        }
        return false;
    }

    /**
     * Determines if there is a winner by checking each diagonal for consecutive
     * matching tokens.
     * @return true if a diagonal winner exists, false otherwise.
     */
    private boolean isDiagonalWin() {
        int row = 0, col = 0;

        String diagToken = null;
        boolean foundWin = true;
        //Since we know the grid is a square we really could just check one of
        // these - either row or col, but I left both in here anyway.
        while (row < rows && col < columns) {
            if (diagToken == null) {
                diagToken = buttonGrid[row][col].getText();
            } else if ("".equals(buttonGrid[row][col].getText())) {
                foundWin = false;
                break;
            } else if (!diagToken.equals(buttonGrid[row][col].getText())) {
                foundWin = false;
                break;
            }
            row++;
            col++;
        }
        if (foundWin) {
            return true;
        }

        diagToken = null;
        row = rows - 1;
        col = 0;
        foundWin = true;
        while (row >=0 && col < columns) {
            if (diagToken == null) {
                diagToken = buttonGrid[row][col].getText();
            } else if ("".equals(buttonGrid[row][col].getText())) {
                foundWin = false;
                break;
            } else if (!diagToken.equals(buttonGrid[row][col].getText())) {
                foundWin = false;
                break;
            }
            row--;
            col++;
        }
        return foundWin;
    }

//    removed - no longer needed
//    public void checkIfWinningMove(int position, String token) {
//        switch (position) {
//            case 0:
//
//                // check horizontal
//                if (stringGrid[1].equals(token) && stringGrid[2].equals(token)) {
//                    winningMove(token);
//                } // check diagonal
//                else if (stringGrid[4].equals(token) && stringGrid[8].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[3].equals(token) && stringGrid[3].equals(token)) {
//                    winningMove(token);
//                }
//
//                break;
//            case 1:
//
//                // check horizontal
//                if (stringGrid[0].equals(token) && stringGrid[2].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[4].equals(token) && stringGrid[7].equals(token)) {
//                    winningMove(token);
//                }
//
//                break;
//            case 2:
//
//                // check horizontal
//                if (stringGrid[1].equals(token) && stringGrid[0].equals(token)) {
//                    winningMove(token);
//                } // check diagonal
//                else if (stringGrid[4].equals(token) && stringGrid[6].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[5].equals(token) && stringGrid[8].equals(token)) {
//                    winningMove(token);
//                }
//
//                break;
//            case 3:
//                // check horizontal
//                if (stringGrid[4].equals(token) && stringGrid[5].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[0].equals(token) && stringGrid[6].equals(token)) {
//                    winningMove(token);
//                }
//
//                break;
//            case 4:
//                // check horizontal
//                if (stringGrid[3].equals(token) && stringGrid[5].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[1].equals(token) && stringGrid[7].equals(token)) {
//                    winningMove(token);
//                }
//                // check diagonal 1
//                if (stringGrid[0].equals(token) && stringGrid[8].equals(token)) {
//                    winningMove(token);
//                } // check diagonal 2
//                else if (stringGrid[2].equals(token) && stringGrid[6].equals(token)) {
//                    winningMove(token);
//                }
//                break;
//            case 5:
//                // check horizontal
//                if (stringGrid[4].equals(token) && stringGrid[3].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[2].equals(token) && stringGrid[8].equals(token)) {
//                    winningMove(token);
//                }
//
//                break;
//            case 6:
//
//                // check horizontal
//                if (stringGrid[7].equals(token) && stringGrid[8].equals(token)) {
//                    winningMove(token);
//                } // check diagonal
//                else if (stringGrid[4].equals(token) && stringGrid[2].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[3].equals(token) && stringGrid[0].equals(token)) {
//                    winningMove(token);
//                }
//
//                break;
//            case 7:
//                // check horizontal
//                if (stringGrid[6].equals(token) && stringGrid[8].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[4].equals(token) && stringGrid[1].equals(token)) {
//                    winningMove(token);
//                }
//                break;
//            case 8:
//
//                // check horizontal
//                if (stringGrid[7].equals(token) && stringGrid[6].equals(token)) {
//                    winningMove(token);
//                } // check diagonal
//                else if (stringGrid[4].equals(token) && stringGrid[0].equals(token)) {
//                    winningMove(token);
//                } // check vertical
//                else if (stringGrid[5].equals(token) && stringGrid[2].equals(token)) {
//                    winningMove(token);
//                }
//
//                break;
//
//        }
//    }

    public void winningMove(String token) {
        if (token.equals(player1.getToken())) {
            JOptionPane.showMessageDialog(frame, "Player 1 wins " + "(" + player1.getToken() + ")");
        } else {
            JOptionPane.showMessageDialog(frame, "Player 2 wins " + "(" + player2.getToken() + ")");
        }
        System.exit(0);
    }

}

Here is the button class:

import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;

public class XOButton extends JButton {

    //None of the fields have access level modifiers, see provided link
    Grid g;
    int status = 0;
    static int clickCount = 0;
    static int id = 0;
    int buttonId;

    public XOButton(Grid grid) {
        g = grid;
        buttonId += id;
        id++;
        setFont(new Font("Arial", Font.PLAIN, 60));
        setText("");
        //It is generally not considered good practice to call an
        // overridable method in a constructor. Consider making this 
        // method final or private.
        this.buildButton();
    }

    public void buildButton() {
        this.addActionListener(new ActionListener(){

            @Override
            public void actionPerformed(ActionEvent e) {
                XOButton button = (XOButton)e.getSource();
                if ("".equals(button.getText())) {
                    button.setText(g.getCurrentPlayer().getToken());
                    g.checkIfWinningMove();
                    g.nextPlayer();
                } else {
                    System.out.println("invalid move, space already occupied by: " + getText());
                }
            }
        });
//            removed - use action listener instead, and created methods in Grid class to handle whose turn it is.
//        this.addMouseListener(new MouseAdapter() {
//            @Override
//            public void mouseClicked(MouseEvent e) {
//            if(clickCount == 0){
//                if(isEnabled()){
//                    setText(g.player1.getToken());
//                    g.updateStringGrid(buttonId, g.player1.getToken());
//                    g.checkIfWinningMove(buttonId, g.player1.getToken());
//                }
//                setEnabled(false);
//
//            }else{
//                if(isEnabled()){
//                    setText(g.player2.getToken());
//                    g.updateStringGrid(buttonId, g.player2.getToken());
//                    g.checkIfWinningMove(buttonId, g.player2.getToken());
//                }
//                setEnabled(false);
//            }
//
//            clickCount++;
//            clickCount %= 2;
//            }
//        });
    }

}
\$\endgroup\$
2
  • \$\begingroup\$ Thank you so much! This is exactly what I was looking for. The only question I have is that you say it's not good practice to call an overridable method in a constructor. I'm not sure exactly why this is. Is it because a potential user of my class could change the method from outside the class thus messing up the constructor and the building of the object? Thanks again. \$\endgroup\$ – dazzaondmic Jun 18 '16 at 20:46
  • \$\begingroup\$ Yes, that is exactly the reason. Here's a link to a good answer to this question: stackoverflow.com/questions/3404301/… \$\endgroup\$ – D.B. Jun 18 '16 at 22:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.