Go is a board game (it looks like 5-in-a-row and plays like chess) and I tried to program it in Java.
Rules:
Two players take turn placing stones on a board. The goal is to capture teritory. Stones can be captured (removed from game) if they have no unoccupied adjacent tiles horizontaly/verticaly. Adjacent stones (horizontaly/verticaly) of one color are combined into chains which share unoccupied tiles.
If you're interested here's more.
My code:
I implemented core rules and basic input/output. My goal is to make the game scalable so I can add functionality without breaking everything.
My concerns:
- (main concern) I don't think it's expandable (enough). I had to redo (nearly) everything from scratch because it was complete mess. It works but I think that if I add more game mechanism everything will break. What can I do to avoid this?
- Is
Main
an acceptable name for a class? Or should it beGame
/App
or something completely different? - What about my comments?
Main
package go;
import java.awt.BorderLayout;
import java.awt.Color;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
/**
* Builds UI and starts the game.
*
*/
public class Main {
public static final String TITLE = "";
public static final int BORDER_SIZE = 25;
public static void main(String[] args) {
new Main().init();
}
private void init() {
JFrame f = new JFrame();
f.setTitle(TITLE);
JPanel container = new JPanel();
container.setBackground(Color.GRAY);
container.setLayout(new BorderLayout());
f.add(container);
container.setBorder(BorderFactory.createEmptyBorder(BORDER_SIZE, BORDER_SIZE, BORDER_SIZE, BORDER_SIZE));
GameBoard board = new GameBoard();
container.add(board);
f.pack();
f.setResizable(false);
f.setLocationByPlatform(true);
f.setVisible(true);
}}
GameBoard
package go;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
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 {
private static final long serialVersionUID = -494530433694385328L;
/**
* Number of rows/columns.
*/
public static final int SIZE = 9;
/**
* Number of tiles in row/column. (Size - 1)
*/
public static final int N_OF_TILES = SIZE - 1;
public static final int TILE_SIZE = 40;
public static final int BORDER_SIZE = TILE_SIZE;
/**
* Black/white player/stone
*
*
*/
public enum State {
BLACK, WHITE
}
private State current_player;
private Grid grid;
private Point lastMove;
public GameBoard() {
this.setBackground(Color.ORANGE);
grid = new Grid(SIZE);
// Black always starts
current_player = State.BLACK;
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));
// Check wherever it's valid
if (row >= SIZE || col >= SIZE || row < 0 || col < 0) {
return;
}
if (grid.isOccupied(row, col)) {
return;
}
grid.addStone(row, col, current_player);
lastMove = new Point(col, row);
// Switch current player
if (current_player == State.BLACK) {
current_player = State.WHITE;
} else {
current_player = State.BLACK;
}
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);
// Draw rows.
for (int i = 0; i < SIZE; i++) {
g2.drawLine(BORDER_SIZE, i * TILE_SIZE + BORDER_SIZE, TILE_SIZE
* N_OF_TILES + BORDER_SIZE, i * TILE_SIZE + BORDER_SIZE);
}
// Draw columns.
for (int i = 0; i < SIZE; i++) {
g2.drawLine(i * TILE_SIZE + BORDER_SIZE, BORDER_SIZE, i * TILE_SIZE
+ BORDER_SIZE, TILE_SIZE * N_OF_TILES + BORDER_SIZE);
}
// Iterate over intersections
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
State state = grid.getState(row, col);
if (state != null) {
if (state == State.BLACK) {
g2.setColor(Color.BLACK);
} else {
g2.setColor(Color.WHITE);
}
g2.fillOval(col * TILE_SIZE + BORDER_SIZE - TILE_SIZE / 2,
row * TILE_SIZE + BORDER_SIZE - TILE_SIZE / 2,
TILE_SIZE, TILE_SIZE);
}
}
}
// Highlight last move
if (lastMove != null) {
g2.setColor(Color.RED);
g2.drawOval(lastMove.x * TILE_SIZE + BORDER_SIZE - TILE_SIZE / 2,
lastMove.y * TILE_SIZE + BORDER_SIZE - TILE_SIZE / 2,
TILE_SIZE, TILE_SIZE);
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(N_OF_TILES * TILE_SIZE + BORDER_SIZE * 2,
N_OF_TILES * TILE_SIZE + BORDER_SIZE * 2);
}
}
Grid
package go;
import go.GameBoard.State;
/**
* Provides game logic.
*
*
*/
public class Grid {
private final int SIZE;
/**
* [row][column]
*/
private Stone[][] stones;
public Grid(int size) {
SIZE = size;
stones = new Stone[SIZE][SIZE];
}
/**
* Adds Stone to Grid.
*
* @param row
* @param col
* @param black
*/
public void addStone(int row, int col, State state) {
Stone newStone = new Stone(row, col, state);
stones[row][col] = newStone;
// Check neighbors
Stone[] neighbors = new Stone[4];
// Don't check outside the board
if (row > 0) {
neighbors[0] = stones[row - 1][col];
}
if (row < SIZE - 1) {
neighbors[1] = stones[row + 1][col];
}
if (col > 1) {
neighbors[2] = stones[row][col - 1];
}
if (col < SIZE - 1) {
neighbors[3] = stones[row][col + 1];
}
// Prepare Chain for this new Stone
Chain finalChain = new Chain(newStone.state);
for (Stone neighbor : neighbors) {
// Do nothing if no adjacent Stone
if (neighbor == null) {
continue;
}
newStone.liberties--;
neighbor.liberties--;
// If it's different color than newStone check him
if (neighbor.state != newStone.state) {
checkStone(neighbor);
continue;
}
if (neighbor.chain != null) {
finalChain.join(neighbor.chain);
}
}
finalChain.addStone(newStone);
}
/**
* Check liberties of Stone
*
* @param stone
*/
public void checkStone(Stone stone) {
// Every Stone is part of a Chain so we check total liberties
if (stone.chain.getLiberties() == 0) {
for (Stone s : stone.chain.stones) {
s.chain = null;
stones[s.row][s.col] = null;
}
}
}
/**
* Returns true if given position is occupied by any stone
*
* @param row
* @param col
* @return true if given position is occupied
*/
public boolean isOccupied(int row, int col) {
return stones[row][col] != null;
}
/**
* Returns State (black/white) of given position or null if it's unoccupied.
* Needs valid row and column.
*
* @param row
* @param col
* @return
*/
public State getState(int row, int col) {
Stone stone = stones[row][col];
if (stone == null) {
return null;
} else {
// System.out.println("getState != null");
return stone.state;
}
}
}
Chain
package go;
import go.GameBoard.State;
import java.util.ArrayList;
/**
* A collection of adjacent Stone(s).
*
*/
public class Chain {
public ArrayList<Stone> stones;
public State state;
public Chain(State state) {
stones = new ArrayList<>();
}
public int getLiberties() {
int total = 0;
for (Stone stone : stones) {
total += stone.liberties;
}
return total;
}
public void addStone(Stone stone) {
stone.chain = this;
stones.add(stone);
}
public void join(Chain chain) {
for (Stone stone : chain.stones) {
addStone(stone);
}
}
}
Stone
package go;
import go.GameBoard.State;
/**
* Basic game element.
*
*/
public class Stone {
public Chain chain;
public State state;
public int liberties;
// Row and col are need to remove (set to null) this Stone from Grid
public int row;
public int col;
public Stone(int row, int col, State state) {
chain = null;
this.state = state;
liberties = 4;
this.row = row;
this.col = col;
}
}