First version: [Go (board game) in Java](https://codereview.stackexchange.com/questions/93901/go-board-game-in-java) 

**What's new:**

* I now use (as advised) `HashMap<GoPoint, StoneColor>` where `GoPoint` is my "coordinate" class and `StoneColor` is an enum.
* Game allows only legal moves.
* Player can pass (skip turn).
* Player can choose the size of game board.
* Code is more readable (I hope).
* Better separation of view and model.
* Everything should work and you can actually play.

**What's missing:**

* Score counting. I'm not even sure wherever I'll implement it. Any advice on how to do it (that's not too complicated if possible) is welcome.
* A.I. This is not going to happen in near future. I definitely want to try building A.I. but I'll try it with something simpler.

Anything that could improve my code is welcome. Is it readable? Should I do something diferently?...

**GoMain**

```
package go;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * Builds UI and starts the game.
 *
 */
public class GoMain {

    public static final String TITLE = "Simple Go";
    public static final int OUTSIDE_BORDER_SIZE = 25;

    private StartDialog startDialog;

    public static void main(String[] args) {
        new GoMain().init();
    }

    private void init() {
        startDialog = new StartDialog(this);
        startDialog.pack();
        startDialog.setLocationByPlatform(true);
        startDialog.setVisible(true);
    }

    public void startGame(int size) {
        JFrame f = new JFrame();
        f.setTitle(TITLE);

        f.add(createMainContainer(size));

        f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        f.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                super.windowClosing(e);
                startDialog.setVisible(true);
            }
        });
        f.pack();
        f.setResizable(false);
        f.setLocationByPlatform(true);
        f.setVisible(true);
    }

    private JPanel createMainContainer(int size) {
        JPanel container = new JPanel();
        container.setBackground(Color.GRAY);
        container.setLayout(new BorderLayout());
        container.setBorder(BorderFactory.createEmptyBorder(
                OUTSIDE_BORDER_SIZE, OUTSIDE_BORDER_SIZE, OUTSIDE_BORDER_SIZE,
                OUTSIDE_BORDER_SIZE));

        GameBoard board = new GameBoard(size);
        container.add(board, BorderLayout.CENTER);
        container.add(createBottomContainer(board), BorderLayout.SOUTH);

        return container;
    }

    private JPanel createBottomContainer(GameBoard board) {
        JPanel bottomContainer = new JPanel();
        JButton passButton = new JButton(new AbstractAction("Pass") {

            @Override
            public void actionPerformed(ActionEvent e) {
                board.getGameState().pass();
            }
        });
        bottomContainer.add(passButton, BorderLayout.SOUTH);
        return bottomContainer;
    }

}

```

**GameBoard**

```
package go;

import go.GameState.StoneColor;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JPanel;

/**
 * Provides I/O.
 * 
 *
 */
public class GameBoard extends JPanel {

    public static final int TILE_SIZE = 40;
    public static final int BORDER_SIZE = TILE_SIZE;

    public final int size;
    private final GameState gameState;

    /**
     * 
     * @param size
     *            number of rows/columns
     */
    public GameBoard(int size) {
        this.size = size;
        gameState = new GameState(size);
        this.setBackground(Color.ORANGE);

        this.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseReleased(MouseEvent e) {
                // Converts to float for float division and then rounds to
                // provide nearest intersection.
                int row = Math.round((float) (e.getY() - BORDER_SIZE)
                        / TILE_SIZE);
                int col = Math.round((float) (e.getX() - BORDER_SIZE)
                        / TILE_SIZE);

                // DEBUG INFO
                // System.out.println(String.format("y: %d, x: %d", row, col));

                if (gameState.playAt(row, col)) {
                    repaint();
                }
            }
        });
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        g2.setColor(Color.BLACK);
        drawRows(g2);
        drawColumns(g2);
        drawStones(g2);
        highlightLastMove(g2);
    }

    private void highlightLastMove(Graphics2D g2) {
        GoPoint lastMove = gameState.getLastMove();
        if (lastMove != null) {
            g2.setColor(Color.RED);
            g2.drawOval(mapToBoard(lastMove.getCol()) - TILE_SIZE / 2,
                    mapToBoard(lastMove.getRow()) - TILE_SIZE / 2, TILE_SIZE,
                    TILE_SIZE);
        }
    }

    private void drawStones(Graphics2D g2) {
        for (GoPoint gp : gameState.getAllPoints()) {
            StoneColor stoneColor = gameState.getColor(gp);
            if (stoneColor != StoneColor.NONE) {
                if (stoneColor == StoneColor.BLACK) {
                    g2.setColor(Color.BLACK);
                } else {
                    g2.setColor(Color.WHITE);
                }
                g2.fillOval(mapToBoard(gp.getCol()) - TILE_SIZE / 2,
                        mapToBoard(gp.getRow()) - TILE_SIZE / 2, TILE_SIZE,
                        TILE_SIZE);
            }
        }
    }

    private void drawRows(Graphics2D g2) {
        for (int i = 0; i < size; i++) {
            g2.drawLine(mapToBoard(0), mapToBoard(i), mapToBoard(size - 1),
                    mapToBoard(i));
        }
    }

    private void drawColumns(Graphics2D g2) {
        for (int i = 0; i < size; i++) {
            g2.drawLine(mapToBoard(i), mapToBoard(0), mapToBoard(i),
                    mapToBoard(size - 1));
        }
    }

    /**
     * Returns x/y coordinate of column/row
     * 
     * @param i row/column
     * @return x/y coordinate of column/row
     */
    private int mapToBoard(int i) {
        return i * TILE_SIZE + BORDER_SIZE;
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension((size - 1) * TILE_SIZE + BORDER_SIZE * 2,
                (size - 1) * TILE_SIZE + BORDER_SIZE * 2);
    }

    public GameState getGameState() {
        return gameState;
    }

}

```

**GameState**

```
package go;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import javax.swing.JOptionPane;

/**
 * Provides game logic.
 * 
 *
 */
public class GameState {

    /**
     * Black/white stone
     * 
     *
     */
    public enum StoneColor {
        BLACK, WHITE, NONE
    }

    private final int size;

    private GoPoint lastMove;
    private boolean itsBlacksTurn;
    private HashMap<GoPoint, StoneColor> stones;
    // Previous position after black played. For "ko rule".
    private HashMap<GoPoint, StoneColor> previousBlackPosition;
    private HashMap<GoPoint, StoneColor> previousWhitePosition;
    private boolean passedPreviously;
    /**
     * True if any stone was removed this turn.
     */
    private boolean removedStone;

    public GameState(int size) {
        this.size = size;
        // Black always starts
        itsBlacksTurn = true;
        lastMove = null;
        previousBlackPosition = new HashMap<>();
        previousWhitePosition = new HashMap<>();
        populateBoard();
    }

    /**
     * Initializes the game map with empty GoPoint(s).
     */
    private void populateBoard() {
        stones = new HashMap<>();
        for (int row = 0; row < size; row++) {
            for (int col = 0; col < size; col++) {
                stones.put(new GoPoint(row, col), StoneColor.NONE);
            }
        }
    }

    /**
     * Passes and shows score if the game ended. TODO
     * 
     */
    public void pass() {
        if (passedPreviously) {
            // TODO: scoring
            JOptionPane.showMessageDialog(null, "Game over.");
        }
        savePosition();
        lastMove = null;
        passedPreviously = true;

    }

    /**
     * Processes input and handles game logic. Returns false if move is invalid.
     * 
     * @param row
     * @param col
     * @return false if move is invalid
     */
    public boolean playAt(int row, int col) {
        if (row >= size || col >= size || row < 0 || col < 0) {
            return false;
        }

        GoPoint newStone = getPointAt(row, col);

        if (isOccupied(newStone)) {
            return false;
        }
        removedStone = false;

        addStone(newStone);

        // Suicide is legal (i.e. you don't actually commit suicide) if you
        // remove enemy stones with it.
        if (!removedStone && isSuicide(newStone)) {
            return false;
        }

        // "ko rule": previous position can't be repeated
        if ((itsBlacksTurn && previousBlackPosition.equals(stones))
                || (!itsBlacksTurn && previousWhitePosition.equals(stones))) {
            System.out.println("true");
            stones = previousBlackPosition;
            return false;
        }

        savePosition();

        changePlayer();
        lastMove = newStone;

        return true;

    }

    /**
     * Saves position so we can check violations of "ko rule".
     */
    private void savePosition() {
        if (itsBlacksTurn) {
            previousBlackPosition = new HashMap<>(stones);
        } else {
            previousWhitePosition = new HashMap<>(stones);
        }
    }

    private boolean isOccupied(GoPoint gp) {
        return stones.get(gp) != StoneColor.NONE;
    }

    private GoPoint getPointAt(int row, int col) {
        return new GoPoint(row, col);
    }

    private void changePlayer() {
        itsBlacksTurn = !itsBlacksTurn;
    }

    public Iterable<GoPoint> getAllPoints() {
        return stones.keySet();
    }

    public StoneColor getColor(GoPoint gp) {
        return stones.get(gp);
    }

    /**
     * Returns location of last move or null.
     * 
     * @return
     */
    public GoPoint getLastMove() {
        return lastMove;
    }

    /**
     * Returns true (and removes the Stone) if the move is suicide. You need to
     * actually add the stone first.
     * 
     * @param gp
     * @return true if the move is suicide
     */
    private boolean isSuicide(GoPoint gp) {
        if (isDead(gp, new HashSet<GoPoint>())) {
            removeStone(gp);
            return true;
        }
        return false;
    }

    /**
     * Adds Stone and removes dead neighbors.
     * 
     * @param gp
     */
    private void addStone(GoPoint gp) {
        StoneColor stoneColor;
        if (itsBlacksTurn) {
            stoneColor = StoneColor.BLACK;
        } else {
            stoneColor = StoneColor.WHITE;
        }

        stones.put(gp, stoneColor);

        for (GoPoint neighbor : getNeighbors(gp)) {
            removeIfDead(neighbor);
        }

    }

    private void removeStone(GoPoint gp) {
        stones.put(gp, StoneColor.NONE);
    }

    private Set<GoPoint> getNeighbors(GoPoint gp) {
        Set<GoPoint> neighbors = new HashSet<>();

        if (gp.getRow() > 0) {
            neighbors.add(getPointAt(gp.getRow() - 1, gp.getCol()));
        }
        if (gp.getRow() < size - 1) {
            neighbors.add(getPointAt(gp.getRow() + 1, gp.getCol()));
        }
        if (gp.getCol() > 0) {
            neighbors.add(getPointAt(gp.getRow(), gp.getCol() - 1));
        }
        if (gp.getCol() < size - 1) {
            neighbors.add(getPointAt(gp.getRow(), gp.getCol() + 1));
        }
        return neighbors;
    }

    /**
     * Removes all stones with 0 liberties.
     * 
     * @param gp
     *            starting point
     * 
     */
    private void removeIfDead(GoPoint gp) {
        Set<GoPoint> searchedPoints = new HashSet<>();
        if (isDead(gp, searchedPoints)) {
            // Starting points needs to be added (otherwise it works only with
            // chains of 2+ stones).
            searchedPoints.add(gp);
            if (!searchedPoints.isEmpty()) {
                removedStone = true;
            }
            for (GoPoint toRemove : searchedPoints) {
                removeStone(toRemove);
            }
        }
    }

    /**
     * Checks wherever given stone is dead. Checks the whole chain (all
     * connected stones).
     * 
     * Starting stone needs to be added explicitly. If it finds false then
     * nothing is dead and we want to end quickly as possible. Otherwise it
     * needs to CONTINUE CHECKING. Some stones may seem dead because we're
     * trying to check every stone only once. See below.
     * 
     * <pre>
     * Image situation:
     *  (B, W - black, white)
     *  (W3 - last move)
     *  B|W1|B
     *  B|W2|B
     *  B|W3|B
     * </pre>
     * 
     * Let's say player places W3 so we checks it's neighbors. It starts with W2
     * so we check's W2's neighbors. We can't check north, east and west are
     * filled. But we also can't check south because we don't want to check the
     * stone twice to prevent looping. So W1 is dead. This doesn't matter if we
     * continue checking because W3 (and W2) still return false.
     * 
     * @param gp
     *            starting point
     * @param searchedPoints
     *            set containing already searched stones (of the same color as
     *            starting point)
     * @return false if given stone is alive, but not necessarily true if given
     *         stone is dead (see full description)
     */
    private boolean isDead(GoPoint gp, Set<GoPoint> searchedPoints) {
        for (GoPoint neighbor : getNeighbors(gp)) {
            if (getColor(neighbor) == StoneColor.NONE) {
                return false;
            }
            if (getColor(neighbor) == getColor(gp)
                    && !searchedPoints.contains(neighbor)) {
                /*
                 * We add only neighbors that are stones of the same color
                 * because we can afford to check other neighbors more that once
                 */
                searchedPoints.add(neighbor);
                if (!isDead(neighbor, searchedPoints)) {
                    return false;
                }
            }
        }
        return true;
    }
}

```

**GoPoint**

```
package go;

public final class GoPoint {

    private final int row;
    private final int col;

    public GoPoint(int row, int col) {
        this.row = row;
        this.col = col;
    }

    public int getRow() {
        return row;
    }

    public int getCol() {
        return col;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + col;
        result = prime * result + row;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        GoPoint other = (GoPoint) obj;
        if (col != other.col)
            return false;
        if (row != other.row)
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "GoPoint [row=" + row + ", col=" + col + "]";
    }

}

```

**StartDialog**

```
package go;

import java.awt.Font;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

/**
 * Dialog that lets user pick size and starts the game.
 *
 */
public class StartDialog extends JDialog {

    private static final String TITLE = "Choose size";
    private static final int BUTTON_TEXT_SIZE = 30;
    private static final Font BUTTON_FONT = new Font(null, Font.PLAIN,
            BUTTON_TEXT_SIZE);
    private static final int GAP_SIZE = 10;

    private static final int FIRST_OPTION = 9;
    private static final int SECOND_OPTION = 19;
    private static final String CUSTOM_OPTION = "Custom";

    public StartDialog(GoMain goMain) {
        super((Frame) null, TITLE);
        this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        this.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                super.windowClosing(e);
                System.exit(0);
            }
        });

        JPanel container = new JPanel(new GridLayout(1, 1, GAP_SIZE, GAP_SIZE));
        container.add(createOptionButton(FIRST_OPTION, goMain, this));
        container.add(createOptionButton(SECOND_OPTION, goMain, this));
        JButton customSizeBtn = new JButton(new AbstractAction(CUSTOM_OPTION) {

            @Override
            public void actionPerformed(ActionEvent e) {
                String sizeString = JOptionPane.showInputDialog("Custom size:");
                try {
                    int size = Integer.parseInt(sizeString);
                    if (size > 1 && size < 30) {
                        goMain.startGame(size);
                    } else {
                        throw new IllegalArgumentException();
                    }
                } catch (IllegalArgumentException ex) {
                    JOptionPane
                            .showMessageDialog(
                                    null,
                                    "Invalid input. Please enter a number between 1 and 30.");
                }
            }
        });
        customSizeBtn.setFont(BUTTON_FONT);
        container.add(customSizeBtn);
        add(container);
    }

    private JButton createOptionButton(int option, GoMain goMain,
            StartDialog parent) {
        JButton optionButton = new JButton(new AbstractAction(
                String.valueOf(option)) {

            @Override
            public void actionPerformed(ActionEvent e) {
                parent.setVisible(false);
                goMain.startGame(option);
            }
        });
        optionButton.setFont(BUTTON_FONT);
        return optionButton;
    }

}

```