4
\$\begingroup\$

(See also Noughts and Crosses GUI game in Java - Part 1/2: AI)

This part is dedicated to the View/Controller of the MVC pattern. So once again these are the building blocks of the Noughts and Crosses game:

Configuring the game

Playing

ConfigurationFrame.java:

package net.coderodde.game.crosses;

import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import static net.coderodde.game.crosses.Application.centerFrame;

/**
 * This class implements a configuration frame.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Oct 8, 2015)
 */
public class ConfigurationFrame extends JFrame {

    private final JTextField heightField;
    private final JTextField widthField;
    private final JTextField patternLengthField;
    private final JTextField depthField;

    private final JLabel heightLabel;
    private final JLabel widthLabel;
    private final JLabel patternLengthLabel;
    private final JLabel depthLabel;

    private final JButton startGameButton;

    private final GameFrame gameFrame;

    public ConfigurationFrame() {
        this(5, 5, 4, 5);
    }

    public ConfigurationFrame(int height, 
                              int width, 
                              int patternLength, 
                              int depth) {
        super("Configure your game");
        this.heightField        = new JTextField("" + height);
        this.widthField         = new JTextField("" + width);
        this.patternLengthField = new JTextField("" + patternLength);
        this.depthField         = new JTextField("" + depth);
        this.heightLabel        = new JLabel("Field height");
        this.widthLabel         = new JLabel("Field width");
        this.patternLengthLabel = new JLabel("Winning pattern length");
        this.depthLabel         = new JLabel("AI depth");
        this.startGameButton    = new JButton("Start");

        this.gameFrame = new GameFrame(this);

        Border labelBorder = new EmptyBorder(0, 10, 0, 10);

        heightLabel       .setBorder(labelBorder);
        widthLabel        .setBorder(labelBorder);
        patternLengthLabel.setBorder(labelBorder);
        depthLabel.        setBorder(labelBorder);

        Border panelBorder = BorderFactory.createLineBorder(Color.RED);

        JPanel heightPanel        = new JPanel();
        JPanel widthPanel         = new JPanel();
        JPanel patternLengthPanel = new JPanel();
        JPanel depthPanel         = new JPanel();

        heightPanel        .setBorder(panelBorder);
        widthPanel         .setBorder(panelBorder);
        patternLengthPanel .setBorder(panelBorder);
        depthPanel         .setBorder(panelBorder);

        heightPanel        .setLayout(new GridLayout(1, 2));
        widthPanel         .setLayout(new GridLayout(1, 2));
        patternLengthPanel .setLayout(new GridLayout(1, 2));
        depthPanel         .setLayout(new GridLayout(1, 2));

        heightPanel.add(heightLabel);
        heightPanel.add(heightField);

        widthPanel.add(widthLabel);
        widthPanel.add(widthField);

        patternLengthPanel.add(patternLengthLabel);
        patternLengthPanel.add(patternLengthField);

        depthPanel.add(depthLabel);
        depthPanel.add(depthField);

        getContentPane().setLayout(new GridLayout(5, 1, 20, 10));

        getContentPane().add(heightPanel);
        getContentPane().add(widthPanel);
        getContentPane().add(patternLengthPanel);
        getContentPane().add(depthPanel);
        getContentPane().add(startGameButton);

        StartButtonActionListener startButtonActionListener = 
                new StartButtonActionListener(heightField,
                                              widthField,
                                              patternLengthField,
                                              depthField);

        startGameButton.addActionListener(startButtonActionListener);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();
        centerFrame(this);
        setResizable(false);
        startGameButton.requestFocus();
        setVisible(true);
    }

    public void setHeight(int height) {
        this.heightField.setText("" + height);
    }

    public void setWidth(int width) {
        this.widthField.setText("" + width);
    }

    public void setPatternLength(int patternLength) {
        this.patternLengthField.setText("" + patternLength);
    }

    public void setDepth(int depth) {
        this.depthField.setText("" + depth);
    }

    private class StartButtonActionListener implements ActionListener {

        private final JTextField heightField;
        private final JTextField widthField;
        private final JTextField patternLengthField;
        private final JTextField depthField;

        private TicTacToeGrid resultGrid;

        StartButtonActionListener(JTextField heightField,
                                  JTextField widthField,
                                  JTextField patternLengthField,
                                  JTextField depthField) {
            this.heightField        = heightField;
            this.widthField         = widthField;
            this.patternLengthField = patternLengthField;
            this.depthField         = depthField;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            resultGrid = null;

            String stmp = heightField.getText().trim();

            if (stmp.isEmpty()) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this,
                        "Please input the field height.",
                        "Input error",
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            int height;

            try {
                height = Integer.parseInt(stmp);
            } catch (NumberFormatException ex) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this, 
                        "Bad height: " + stmp, 
                        "Input error", 
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            stmp = widthField.getText().trim();

            if (stmp.isEmpty()) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this,
                        "Please input the field width.",
                        "Input error",
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            int width;

            try {
                width = Integer.parseInt(stmp);
            } catch (NumberFormatException ex) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this, 
                        "Bad width: " + stmp, 
                        "Input error", 
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            stmp = patternLengthField.getText().trim();

            if (stmp.isEmpty()) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this,
                        "Please input the winning pattern length.",
                        "Input error",
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            int patternLength;

            try {
                patternLength = Integer.parseInt(stmp);
            } catch (NumberFormatException ex) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this, 
                        "Bad pattern length: " + stmp, 
                        "Input error", 
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            stmp = depthField.getText().trim();

            if (stmp.isEmpty()) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this,
                        "Please input the AI depth.",
                        "Input error",
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            int depth;

            try {
                depth = Integer.parseInt(stmp);
            } catch (NumberFormatException ex) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this, 
                        "Bad depth: " + stmp, 
                        "Input error", 
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            try {
              resultGrid = new TicTacToeGrid(height, width, patternLength); 
            } catch (Exception ex) {
                JOptionPane.showMessageDialog(
                        ConfigurationFrame.this,
                        "Bad configuration: " + ex.getMessage(),
                        "Input error",
                        JOptionPane.ERROR_MESSAGE);
                return;
            }

            ConfigurationFrame.this.setVisible(false);
            gameFrame.startGame(resultGrid, depth);
            gameFrame.setVisible(true);
        }
    }
}

GameFrame.java:

package net.coderodde.game.crosses;

import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import javax.swing.JProgressBar;
import static net.coderodde.game.crosses.Application.centerFrame;

/**
 * This class implements a frame showing the game grid.
 *
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Oct 8, 2015)
 */
public class GameFrame extends JFrame {

    private final JProgressBar progressBar;
    private final ConfigurationFrame configurationFrame;

    public GameFrame(ConfigurationFrame configurationFrame) {
        this.progressBar = new JProgressBar();
        this.configurationFrame = configurationFrame;
    }

    public void startGame(TicTacToeGrid grid, int depth) {
        getContentPane().removeAll();
        progressBar.setVisible(false);
        centerFrame(this);

        getContentPane().setLayout(new GridBagLayout());
        setDefaultCloseOperation(EXIT_ON_CLOSE);

        MoveGenerator moveGenerator = new MoveGenerator();
        HeuristicFunction heuristicFunction = new HeuristicFunction();

        TicTacToePanel panel = new TicTacToePanel(progressBar,
                                                    moveGenerator,
                                                    heuristicFunction,
                                                    depth,
                                                    configurationFrame,
                                                    this);
        panel.setCurrentGrid(grid);
        panel.unlock();
        panel.repaint();

        GridBagConstraints c = new GridBagConstraints();

        c.gridx = 0;
        c.gridy = 0;
        c.fill = GridBagConstraints.BOTH;
        c.weightx = 1.0;
        c.weighty = 1.0;

        getContentPane().add(panel, c);

        c = new GridBagConstraints();

        c.gridx = 0;
        c.gridy = 1;
        c.weightx = 1.0;
        c.weighty = 0.0;
        c.fill = GridBagConstraints.HORIZONTAL;

        getContentPane().add(progressBar, c);

        setMinimumSize(panel.getMinimumSize());

        centerFrame(this);
        pack();
    }
}

TicTacToePanel.java:

package net.coderodde.game.crosses;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;

/**
 * This class implements the panel for playing Tic Tac Toe.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Oct 5, 2015)
 */
public class TicTacToePanel extends JPanel {

    private static final int  DEFAULT_PADDING      = 10;
    private static final int  DEFAULT_BORDER_WIDTH = 3;
    private static final int  MINIMUM_CELL_LENGTH  = 60;
    private static final Font DEFAULT_FONT = new Font("Times New Roman", 
                                                      Font.BOLD, 
                                                      50);

    private final JProgressBar       progressBar;
    private final MoveGenerator      moveGenerator;
    private final HeuristicFunction  heuristicFunction;
    private final int                maximumDepth;
    private final ConfigurationFrame configurationFrame;
    private final GameFrame          gameFrame;

    private TicTacToeGrid currentGrid;
    private int           padding;
    private int           borderWidth;
    private Color         backgroundColor;
    private Color         borderColor;
    private Color         foregroundColor; 
    private Color         highlightOccupiedBorderColor;
    private Color         highlightOccupiedCellColor;
    private Color         highlightFreeBorderColor;
    private Color         highlightFreeCellColor;

    private int highlightCellX;
    private int highlightCellY;

    private int previousHighlightCellX;
    private int previousHighlightCellY;

    private int lastValidCellX = -1;
    private int lastValidCellY = -1;

    /**
     * Specifies whether the AI is currently computing its next move. If the
     * lock is on, the user's input will be ignored. The default value is 
     * {@code false}.
     */
    private volatile boolean lock;

    public TicTacToePanel(JProgressBar progressBar,
                          MoveGenerator moveGenerator,
                          HeuristicFunction heuristicFunction,
                          int maximumDepth,
                          ConfigurationFrame configurationFrame,
                          GameFrame gameFrame) {
        this.progressBar = progressBar;
        this.moveGenerator = moveGenerator;
        this.heuristicFunction = heuristicFunction;
        this.maximumDepth = maximumDepth;
        this.configurationFrame = configurationFrame;
        this.gameFrame = gameFrame;

        this.padding = DEFAULT_PADDING;
        this.borderWidth = DEFAULT_BORDER_WIDTH;
        this.backgroundColor = Color.WHITE;
        this.borderColor = Color.GRAY;
        this.foregroundColor = Color.BLACK;
        this.highlightFreeBorderColor = new Color(100, 200, 100);
        this.highlightFreeCellColor = Color.GREEN;
        this.highlightOccupiedBorderColor = Color.PINK;
        this.highlightOccupiedCellColor = Color.RED;

        setFont(DEFAULT_FONT);

        this.setMinimumSize(new Dimension(100, 100));
        CanvasMouseListener mouseListener = new CanvasMouseListener();
        this.addMouseListener(mouseListener);
        this.addMouseMotionListener(mouseListener);
        CanvasKeyListener keyListener = new CanvasKeyListener();
        this.addKeyListener(keyListener);
        this.setFocusable(true);
        this.requestFocus();
    }

    public void lock() {
        lock = true;
    }

    public void unlock() {
        lock = false;
    }

    public void setPadding(int padding) {
        this.padding = Math.max(1, padding);
    }

    public void setBorderWidth(int borderWidth) {
        this.borderWidth = Math.max(1, borderWidth);
    }

    public void setBackgroundColor(Color color) {
        this.backgroundColor = color;
    }

    public void setBorderColor(Color color) {
        this.borderColor = color;
    }

    public void setForegroundColor(Color color) {
        this.foregroundColor = color;
    }

    public void setHighlightOccupiedBorderColor(Color color) {
        this.highlightOccupiedBorderColor = color;
    }

    public void setHighlightOccupiedCellColor(Color color) {
        this.highlightOccupiedCellColor = color;
    }

    public void setHighlightFreeBorderColor(Color color) {
        this.highlightFreeBorderColor = color;
    }

    public void setHighlightFreeCellColor(Color color) {
        this.highlightFreeCellColor = color;
    }

    public void setCurrentGrid(TicTacToeGrid grid) {
        if (this.currentGrid == null) {
            this.highlightCellX = grid.getWidth()  / 2;
            this.highlightCellY = grid.getHeight() / 2;

            this.lastValidCellX = this.highlightCellX;
            this.lastValidCellY = this.highlightCellY;

            this.previousHighlightCellX = this.highlightCellX;
            this.previousHighlightCellY = this.highlightCellY;
        }

        this.currentGrid = grid;
        repaint();
    }

    public TicTacToeGrid getCurrentGrid() {
        return currentGrid;
    }

    @Override
    public Dimension getMinimumSize() {
        int horizontalCells = currentGrid.getWidth();
        int verticalCells   = currentGrid.getHeight();

        return new Dimension(2 * padding + horizontalCells * 
                                (borderWidth + MINIMUM_CELL_LENGTH) + 
                                 borderWidth,
                            (2 * padding + verticalCells * 
                                (borderWidth + MINIMUM_CELL_LENGTH) + 
                                 borderWidth));
    }

    @Override
    public Dimension getPreferredSize() {
        return getMinimumSize();
    }

    @Override
    public void update(Graphics g) {
        int availableWidth  = getWidth() - 2 * padding;
        int availableHeight = getHeight() - 2 * padding;

        int horizontalCells = currentGrid.getWidth();
        int verticalCells   = currentGrid.getHeight();

        int cellWidth  = (availableWidth - (horizontalCells + 1) * borderWidth) 
                         / horizontalCells;

        int cellHeight = (availableHeight - (verticalCells + 1) * borderWidth) 
                         / verticalCells;

        int cellLength = Math.min(cellWidth, cellHeight);

        g.setColor(backgroundColor);
        g.fillRect(0, 0, getWidth(), getHeight());

        int occupiedWidth = (horizontalCells * (cellLength + borderWidth)) + 
                             borderWidth;
        int occupiedHeight = (verticalCells * (cellLength + borderWidth)) +
                             borderWidth;

        int skipX = (getWidth()  - occupiedWidth) / 2;
        int skipY = (getHeight() - occupiedHeight) / 2;

        g.setColor(borderColor);

        // Draw horizontal borders.
        for (int y = 0; y <= verticalCells; ++y) {
            g.fillRect(skipX, 
                       skipY + y * (borderWidth + cellLength), 
                       horizontalCells * (borderWidth + cellLength) + borderWidth, 
                       borderWidth);
        }

        // Draw vertical borders.
        for (int x = 0; x <= horizontalCells; ++x) {
            g.fillRect(skipX + x * (borderWidth + cellLength),
                       skipY,
                       borderWidth,
                       verticalCells * (borderWidth + cellLength) + borderWidth);
        }

        if (highlightCellX >= 0
                && highlightCellX < horizontalCells
                && highlightCellY >= 0
                && highlightCellY < verticalCells) {
            boolean cellOccupied = currentGrid.read(highlightCellX,
                                                    highlightCellY) != null;
            if (cellOccupied) {
                g.setColor(this.highlightOccupiedBorderColor);
            } else {
                g.setColor(this.highlightFreeBorderColor);
            }

            // Draw the border.
            g.fillRect(skipX + highlightCellX * (borderWidth + cellLength),
                       skipY + highlightCellY * (borderWidth + cellLength), 
                       2 * borderWidth + cellLength,
                       2 * borderWidth + cellLength);

            if (cellOccupied) {
                g.setColor(this.highlightOccupiedCellColor);
            } else {
                g.setColor(this.highlightFreeCellColor);
            }

            // Draw the cell.
            g.fillRect(skipX + highlightCellX * (borderWidth + cellLength) + borderWidth,
                       skipY + highlightCellY * (borderWidth + cellLength) + borderWidth, 
                       cellLength ,
                       cellLength);

        }

        g.setColor(foregroundColor);
        g.setFont(getFont());

        if (currentGrid == null) {
            return;
        }

        int verticalSkip = 16;

        Font font = prepareFont(cellLength, verticalSkip, g);

        g.setFont(font);

        Graphics2D g2 = (Graphics2D) g;

        g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                            RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        FontMetrics fm = g.getFontMetrics(font);

        int textHeight = fm.getAscent();
        int textWidth = fm.stringWidth("X");

        int dx = (cellLength - textWidth)  / 2;
        int dy = (cellLength - textHeight) / 2;

        // Draw the marks.
        for (int y = 0; y < currentGrid.getHeight(); ++y) {
            for (int x = 0; x < currentGrid.getWidth(); ++x) { 
                Mark mark = currentGrid.read(x, y);

                if (mark == null) {
                    continue;
                }

                g.drawString(mark.equals(Mark.X) ? "X" : "O", 
                             skipX + dx + borderWidth * (1 + x) + x * cellLength,
                             skipY - dy - 8 + borderWidth * (1 + y) + (1 + y) * cellLength);
            }
        }
    }

    private Font prepareFont(int cellLength, int verticalSkip, Graphics g) {
        Font currentFont = getFont();

        for (int fontSize = 1; ; ++fontSize) {
            Font f = new Font(currentFont.getFontName(), Font.BOLD, fontSize);
            FontMetrics fm = g.getFontMetrics(f);

            int height = fm.getAscent();

            if (height >= cellLength - verticalSkip) {
                return new Font(currentFont.getFontName(), 
                                Font.BOLD, 
                                fontSize - 1);
            }
        }
    }

    @Override
    public void paint(Graphics g) {
        update(g);
    }

    private Point toCellCoordinates(int x, int y) {
        Point ret = new Point();

        int availableWidth  = getWidth() - 2 * padding;
        int availableHeight = getHeight() - 2 * padding;

        int horizontalCells = currentGrid.getWidth();
        int verticalCells   = currentGrid.getHeight();

        int cellWidth  = (availableWidth - (horizontalCells + 1) * borderWidth) 
                         / horizontalCells;

        int cellHeight = (availableHeight - (verticalCells + 1) * borderWidth) 
                         / verticalCells;

        int cellLength = Math.min(cellWidth, cellHeight);

        int occupiedWidth = (horizontalCells * (cellLength + borderWidth)) + 
                             borderWidth;
        int occupiedHeight = (verticalCells * (cellLength + borderWidth)) +
                             borderWidth;

        x -= (getWidth()  - occupiedWidth)  / 2;
        y -= (getHeight() - occupiedHeight) / 2;

        if (x % (cellLength + borderWidth) < borderWidth
                || y % (cellLength + borderWidth) < borderWidth) {
            ret.x = -1;
            return ret;
        }

        ret.x = x / (cellLength + borderWidth);
        ret.y = y / (cellLength + borderWidth);

        return ret;
    }

    private void tryClick(int x, int y) {
        Point p = toCellCoordinates(x, y);

        if (p.x >= 0 && p.x < currentGrid.getWidth() 
                && p.y >= 0 && p.y < currentGrid.getHeight()) {
            try {
                currentGrid.mark(p.x, p.y, Mark.X);
                repaint();

                AIThread ai = new AIThread(configurationFrame,
                                           gameFrame,
                                           currentGrid,
                                           this,
                                           progressBar,
                                           moveGenerator,
                                           heuristicFunction,
                                           maximumDepth);

                ai.start();
            } catch (Exception ex) {
                return;
            }
        }
    }

    private void tryHighlight(int x, int y) {
        Point p = toCellCoordinates(x, y);

        // Check that the mouse is on top of a border.
        if (p.x < 0) {
            if (previousHighlightCellX != p.x) {
                highlightCellX = p.x;
                previousHighlightCellX = p.x;
                repaint();
            }

            return;
        }

        if (p.x >= 0 && p.x < currentGrid.getWidth()) {
            this.lastValidCellX = p.x;
            this.lastValidCellY = p.y;
        }

        this.highlightCellX = p.x;
        this.highlightCellY = p.y;

        if (highlightCellX != previousHighlightCellX 
                || highlightCellY != previousHighlightCellY) {
            previousHighlightCellX = highlightCellX;
            previousHighlightCellY = highlightCellY;
            repaint();
        }
    }

    private class CanvasKeyListener implements KeyListener {

        @Override
        public void keyTyped(KeyEvent e) {
            process(e);
        }

        @Override
        public void keyPressed(KeyEvent e) {
            process(e);
        }

        @Override
        public void keyReleased(KeyEvent e) {

        }

        private void process(KeyEvent e) {
            int cellX = lastValidCellX;
            int cellY = lastValidCellY;

            switch (e.getKeyCode()) {
                case KeyEvent.VK_UP:
                case KeyEvent.VK_W:

                    if (cellY > 0) {
                        previousHighlightCellY = lastValidCellY;
                        highlightCellY = --lastValidCellY;
                        repaint();
                    }

                    break;

                case KeyEvent.VK_RIGHT:
                case KeyEvent.VK_D:

                    if (cellX < currentGrid.getWidth() - 1) {
                        previousHighlightCellX = lastValidCellX;
                        highlightCellX = ++lastValidCellX;
                        repaint();
                    }

                    break;

                case KeyEvent.VK_DOWN:
                case KeyEvent.VK_S:

                    if (cellY < currentGrid.getHeight() - 1) {
                        previousHighlightCellY = lastValidCellY;
                        highlightCellY = ++lastValidCellY;
                        repaint();
                    }

                    break;

                case KeyEvent.VK_LEFT: 
                case KeyEvent.VK_A:

                    if (cellX > 0) {
                        previousHighlightCellX = lastValidCellX;
                        highlightCellX = --lastValidCellX;
                        repaint();
                    }

                    break;

                case KeyEvent.VK_SPACE:
                    if (!lock) {
                        // Try mark.
                        try {
                            currentGrid.mark(lastValidCellX,
                                             lastValidCellY,
                                             Mark.X);
                            repaint();
                            AIThread ai = new AIThread(configurationFrame,
                                                       gameFrame,
                                                       TicTacToePanel.this.currentGrid,
                                                       TicTacToePanel.this,
                                                       progressBar,
                                                       moveGenerator,
                                                       heuristicFunction,
                                                       maximumDepth);

                            ai.start();
                        } catch (Exception ex) {

                        }

                        Mark winner = currentGrid.getWinner();

                        String message = null;

                        if (winner != null) {
                            message = winner.equals(Mark.X) ? 
                                    "You won!" : 
                                    "You lost.";

                        } else if (currentGrid.isFull()) {
                            message = "It's a tie.";
                        }

                        if (message != null) {
                            JOptionPane.showMessageDialog(
                                    gameFrame, 
                                    message,
                                    "Game over",
                                    JOptionPane.INFORMATION_MESSAGE);

                            gameFrame.setVisible(false);
                            configurationFrame.setHeight(currentGrid.getHeight());
                            configurationFrame.setWidth(currentGrid.getWidth());
                            configurationFrame.setPatternLength(currentGrid.getWinningLength());
                            configurationFrame.setDepth(maximumDepth);
                            configurationFrame.setVisible(true);
                        }
                    }

                    break;
            }
        }
    }

    private class CanvasMouseListener implements MouseListener,
                                                 MouseMotionListener {

        @Override
        public void mouseClicked(MouseEvent e) {
            if (!lock) {
                tryClick(e.getX(), e.getY());
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {

        }

        @Override
        public void mouseReleased(MouseEvent e) {

        }

        @Override
        public void mouseEntered(MouseEvent e) {
        }

        @Override
        public void mouseExited(MouseEvent e) {

        }

        @Override
        public void mouseDragged(MouseEvent e) {

        }

        @Override
        public void mouseMoved(MouseEvent e) {
            tryHighlight(e.getX(), e.getY());
        }
    }
}

The entire game project lives in https://github.com/coderodde/NoughtsAndCrosses in case you want to run it.

\$\endgroup\$
5
\$\begingroup\$
    public void actionPerformed(ActionEvent e) {
        resultGrid = null;

        String stmp = heightField.getText().trim();

        if (stmp.isEmpty()) {
            JOptionPane.showMessageDialog(
                    ConfigurationFrame.this,
                    "Please input the field height.",
                    "Input error",
                    JOptionPane.ERROR_MESSAGE);
            return;
        }

        int height;

        try {
            height = Integer.parseInt(stmp);
        } catch (NumberFormatException ex) {
            JOptionPane.showMessageDialog(
                    ConfigurationFrame.this, 
                    "Bad height: " + stmp, 
                    "Input error", 
                    JOptionPane.ERROR_MESSAGE);
            return;
        }

This is a dreadful mess, especially if you are trying to follow MVC practices. Heuristic: how are you expecting to write tests for the validation logic?

Your implementation should have a clear separation between the UI layer and your application. In this case

  • The UI is responsible for providing input from the user
  • The UI is not responsible for validating input from the user
  • The UI is responsible for reporting validation errors to the user

"One of these things, is not like the others".

interface GridFactory {
    TicTacToeGrid create(String height, String width, String patternLength) throws ValidationException;
}

class ValidationException extends Exception {
    //... 
    public String getUserFriendlyMessage();
}

public void actionPerformed(ActionEvent e) {
    try {
        resultGrid = factory.createGrid (heightField.getText()
            , widthField.getText()
            , patternLengthField.getText());

        // ...
    } catch (ValidationException ex) {
                    JOptionPane.showMessageDialog(
                    ConfigurationFrame.this,
                    ex.getUserFriendlyMessage(),
                    "Input error",
                    JOptionPane.ERROR_MESSAGE);
    } 

Much cleaner: now your tests can be written against the GridFactory and the ValidationException without needing to construct the JFrame with all of its component parts.

An even more extreme example

public void actionPerformed(ActionEvent e) {
    application.configuration().heightField().setText(heightField.getText());
    application.configuration().weidthField().setText(widthField.getText());
    application.configuration().patternLengthField().setText(patternLengthField.getText());
    application.configuration().depthField().setText(depthField.getText());
    application.configuration().startGame().click();
}

I haven't defined application - it's just a shim that stands between the UI layer and an in memory representation of the state of the UI.

Another way of thinking about it: the UI layer is very difficult to test, so you want the code that lives there to be so trivial that it can't possibly hide a bug. It's really nothing more than a dumb translation layer -- it copies data from the UI elements into a DTO that gets passed to the application, it copies to the controls properties that are found in the DTO's it gets from the application.

DTO's are easy to create in a test environment, in nearly infinite variety.

In this example, the ActionEvent (in the UI domain) needs to be translated into a StartGameCommand in the application domain.

// Notice that this version is even dumber than the previous; it doesn't know
// the difference between the grid dimensions and the ai search parameters
StartGameCommand command = new StartGameCommand(
command = new StartGameCommand((heightField.getText()
            , widthField.getText()
            , patternLengthField.getText()
            , depthField.getText());

Response response = application.submit(command);
if (response.isError) {
        JOptionPane.showMessageDialog(
                ConfigurationFrame.this,
                response.errorMessage,
                response.errorTitle,
                JOptionPane.ERROR_MESSAGE);
        return;        
}

// Super disciplined version: *everything* you need is in the DTO!
if (response.showGame) {
    gameFrame.update(response);
    ConfigurationFrame.this.setVisible(response.showConfigurationFrame);
    gameFrame.setVisible(response.showGameFrame);
}

You might event notice that these responses have the feel of a command to be run against the UI.

UICommand<ConfigurationFrame> response = application.submit(request);
response.apply(ConfigurationFrame.this)

... which would be very good; provided that you remember that the application shouldn't know anything about the ui framework; there's an translation layer in the middle that binds the two

class Translator {
    // ...
    public UICommand<ConfigurationFrame> submit(AppCommand command) {
        Response response = application.submit(command);
        if (response.isError) {
            return new ShowMessageDialog(response);
        }
        return new UpdateGameFrame(response);
    }
}

This is still very testable - you can check that the command has the right type, you can expose the properties of the command so that they can be tested. If the body of the apply method is too simple to possibly go wrong, then you know you are on the right track

class ShowMessageDialog implements UICommand<ConfigurationFrame> {
    public void apply(ConfigurationFrame parent) {
       JOptionPane.showMessageDialog(
            parent,
            // note, the following getters would all be exposed for testing
            getMessage(),
            getTitle(),
            getMessageType());
    }

    //...
}
\$\endgroup\$
5
\$\begingroup\$

Your lock isn't thread-safe. 2 rapid clicks can result in 2 AIThreads starting to run and wrecking all kinds of havok.

Instead only check and change lock on the event thread.

I also suggest you change AIThread into a SwingWorker and do the processing in its doInBackground() which returns the best move and then passing the the result back in the done() method.

//only call in the gui thread
private void startAI(){
    if(lock) return;

    lock = true;

    AIWorker worker = new AIWorker(configurationFrame,
                                   gameFrame,
                                   currentGrid,
                                   this,
                                   progressBar,
                                   moveGenerator,
                                   heuristicFunction,
                                   maximumDepth);

    worker.execute();
    //you can also submit it to a executor of your choice

}

public void processingFinished(TicTacToeGrid newGrid){
    lock = false;
    currentGrid = newGrid;

}

And AIWorker looks like:

public class AIWorker extends SwingWorker<TicTacToeGrid, Void> {

    //init fields


    public TicTacToeGrid doInBackground(){
        //dispatch processing & collect results

        return bestState;
    }

    protected void done(){
        canvas.processingFinished(get());
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ Would using an AtomicBoolean solve the problem? \$\endgroup\$
    – coderodde
    Oct 8 '15 at 10:04
  • \$\begingroup\$ not really; what needs to happen is that the lock is checked and if false immediately set to true. So that the second one that comes along sees true. \$\endgroup\$ Oct 8 '15 at 10:17
  • \$\begingroup\$ Redacted comment - I overlooked some of the threading considerations. \$\endgroup\$ Oct 8 '15 at 19:38

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.