I have been struggling with this problem now for over a week, and would really appreciate some help. I am developing my first Java game using a gui, and I currently have about 20 classes involved. The game is a simple grid-based representation of Star Trek, with JLabel icons that move around the galaxy grid. The problem is that usually after about 7 to 10 moves, one of two things will happen: one, the grid of sectors in my current quadrant will disappear, leaving only a single sector in the top left corner; or two, the Enterprise icon will disappear.
I have no experience dealing with threads, but after some reading I thought this was probably a result of the Event Dispatch Thread not being properly synchronized with the program logic. I read up on the proper way to update a GUI, and surrounded all my statements that had any effect on the GUI (I think) with invokeLater and invokeAndWait blocks.
However, this did not solve the problem. So, today I rewrote everything into the smallest compilable unit I could (it isn't that small, but I can't figure out how to make it smaller) while still keeping my basic game structure to see if that would change anything. It didn't. The GUI still gets corrupted after 7 to 10 moves.
I am at my wits' end here. I would be truly grateful for some help.
Here is my code. It compiles and runs as is.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SpringLayout;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
public class GUI extends JFrame
{
int screenwidth;
int screenheight;
public static void main(String[] args)
{
GUI gui = new GUI();
run(gui);
}
public static void run(final GUI gui)
{
Quadrant[][] galaxy = new Quadrant[8][8];
//populate galaxy with quadrants
for(int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
galaxy[i][j] = new Quadrant(i, j);
}
}
//Quadrant to put in the view when game starts
Quadrant startingQuadrant = galaxy[0][0];
final QuadrantView quadrantView = startingQuadrant.getQuadrantView();
Enterprise enterprise;
Sector startingSector;
//add SectorViews to the QuadrantView
for (int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
startingQuadrant.getQuadrantView().addSectorView(startingQuadrant.getSectorArray()[i][j].getSectorView(), i, j);
}
}
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
//initialize gui with the starting quadrant quadrantView
gui.intiGUI(quadrantView);
}
});
//start on sector (0, 0)
startingSector = startingQuadrant.getSectorArray()[0][0];
enterprise = new Enterprise(startingQuadrant, startingSector);
startingSector.setContainsEnterprise(true);
Scanner input = new Scanner(System.in);
Sector destinationSector;
int qRow; //destination quadrant row
int qCol; //destination quadrant column
int sRow; //destination sector row
int sCol; //destination sector column
while(true)
{
System.out.println("Enter quadrant row: ");
qRow = input.nextInt();
System.out.println("Enter quadrant column: ");
qCol = input.nextInt();
System.out.println("Enter sector row: ");
sRow = input.nextInt();
System.out.println("Enter sector column: ");
sCol = input.nextInt();
destinationSector = galaxy[qRow][qCol].getSectorArray()[sRow][sCol];
enterprise.move(destinationSector, galaxy[qRow][qCol], gui);
}
}
public GUI()
{
super("Star Trek");
//create an anonymous listener to close window and end game
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
dispose();
System.exit(0);
}
});
// get user's screen width and height
screenwidth = (int)Toolkit.getDefaultToolkit().getScreenSize().getWidth();
screenheight = (int)Toolkit.getDefaultToolkit().getScreenSize().getHeight();
//set layout
getContentPane().setLayout(new BorderLayout());
resizeGUI();
setVisible(true);
validate();
}
private void resizeGUI()
{
// set window size
if (screenwidth >= 1280)
setSize(1024, 768);
else if (screenwidth >= 1024)
setSize(800, 600);
else if (screenwidth >= 800)
setSize(640, 480);
// maximize window
setExtendedState(this.getExtendedState() | this.MAXIMIZED_BOTH);
}
//initialize this gui with the starting QuadrantView
public void intiGUI(QuadrantView quadrantView)
{
getContentPane().add(quadrantView, BorderLayout.CENTER);
validate();
}
//reset the gui to hold the new QuadrantView
public void resetGUI(QuadrantView newQuadrantView)
{
getContentPane().add(newQuadrantView, BorderLayout.CENTER);
validate();
}
static class Quadrant
{
private int row;
private int col;
private QuadrantView quadrantView;
private Sector[][] sectorArray;
public Quadrant(int r, int c)
{
// quadrant row
row = r;
// quadrant columns
col = c;
// the view object associated with this quadrant
setQuadrantView(new QuadrantView(8, 8));
// an array to hold the sectors in this quadrant (req. 3.1.0)
sectorArray = new Sector[8][8];
// create the 64 sectors in this quadrant and add them to the array (req. 3.1.0)
for (int i = 0; i < sectorArray.length; i ++)
{
for (int j = 0; j < sectorArray[i].length; j++)
{
sectorArray[i][j] = new Sector(i, j, this);
}
}
}
public int getRow()
{
return row;
}
public int getCol()
{
return col;
}
public void setRow(int r)
{
row = r;
}
public void setCol(int c)
{
col = c;
}
public Sector[][] getSectorArray()
{
return sectorArray;
}
public QuadrantView getQuadrantView()
{
return quadrantView;
}
public void setQuadrantView(QuadrantView quadrantView)
{
this.quadrantView = quadrantView;
}
}
static class Sector
{
//sector row
private int row;
//sector column
private int col;
//the quadrant this sector is in
private Quadrant quadrant;
//the view associated with this sector
private SectorView sectorView;
//boolean values to determine what this sector holds (Req. 4.1.0)
private boolean containsEnterprise;
//if the sector holds the Enterprise, store a reference to it
private Enterprise enterprise;
public Sector(int r, int c, Quadrant q)
{
row = r;
col = c;
quadrant = q;
setSectorView(new SectorView());
containsEnterprise = false;
//print the sector's coordinates on the gui
sectorView.setID(row + ", " + col);
}
public int getRow()
{
return row;
}
public int getCol()
{
return col;
}
public void setRow(int r)
{
row = r;
}
public void setCol(int c)
{
col = c;
}
public Quadrant getQuadrant()
{
return quadrant;
}
public boolean containsEnterprise()
{
return containsEnterprise;
}
public void setContainsEnterprise(boolean containsEnterprise)
{
this.containsEnterprise = containsEnterprise;
if (containsEnterprise)
{
sectorView.showEnterpriseIcon();
}
else
{
sectorView.hideEnterpriseIcon();
}
}
public Enterprise getEnterprise()
{
return enterprise;
}
public void addEnterprise(Enterprise enterprise)
{
this.enterprise = enterprise;
}
public void removeEnterprise()
{
enterprise = null;
}
public SectorView getSectorView()
{
return sectorView;
}
public void setSectorView(SectorView sectorView)
{
this.sectorView = sectorView;
}
public String toString()
{
return Integer.toString(row)+ "." + Integer.toString(col);
}
}
//end Sector class
static class SectorView extends JPanel
{
// default font for text
private final Font TREK_FONT = new Font("Verdana", Font.BOLD, 10);
// color for text
private final Color LABEL_COLOR = Color.BLACK;
// component layout
private SpringLayout layout;
// displays sector ID
private JLabel IDLabel;
//enterprise display
private JLabel enterpriseIcon;
/*
* create a new SectorView
*/
public SectorView()
{
super();
//create and set layout for child components
layout = new SpringLayout();
this.setLayout(layout);
//initialize child components
initComponents();
//position and display child components
layoutComponents();
//set background color
setBackground(Color.DARK_GRAY);
//set border
setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
//set size of sectors
setPreferredSize(new Dimension(QuadrantView.SECTOR_SIZE, QuadrantView.SECTOR_SIZE));
}
/*
* initialize components
*/
private void initComponents()
{
// displays ID of this view
IDLabel = new JLabel("");
IDLabel.setFont(TREK_FONT);
IDLabel.setForeground(Color.WHITE);
// create an enterprise icon and make it invisible
enterpriseIcon = new JLabel("E");
enterpriseIcon.setForeground(Color.WHITE);
enterpriseIcon.setVisible(false);
}
/*
* lay out components and add them to this view
*/
private void layoutComponents()
{
// position components:
// ID label
layout.putConstraint(SpringLayout.WEST, IDLabel, 1, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, IDLabel, 1, SpringLayout.NORTH, this);
// enterprise icon
layout.putConstraint(SpringLayout.WEST, enterpriseIcon, 5, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, enterpriseIcon, 30, SpringLayout.NORTH, this);
// add to view
this.add(IDLabel);
this.add(enterpriseIcon);
}
public void showEnterpriseIcon()
{
enterpriseIcon.setVisible(true);
}
public void hideEnterpriseIcon()
{
enterpriseIcon.setVisible(false);
}
//the sector's (row, col) coordinates within the quadrant
public void setID(String id)
{
IDLabel.setText(id);
}
}
//end SectorView class
static class QuadrantView extends JPanel
{
//size of sectors
public final static int SECTOR_SIZE = 100;
private final Color BACKGROUND_COLOR = Color.DARK_GRAY;
private SpringLayout layout;
/*
* create a new QuadrantView with the specified width
* and height
*
* #param quadrantHeight height of quad. in sectors
* #param quadrantWidth width of quad. in secors
*/
public QuadrantView(int quadrantHeight, int quadrantWidth)
{
//call JPanel constructor
super();
//create and set the layout
layout = new SpringLayout();
setLayout(layout);
//set the size of the QuadrantView we are creating using the inherited JComponent method
setPreferredSize(new Dimension(quadrantWidth * SECTOR_SIZE, quadrantHeight * SECTOR_SIZE));
//set background color using the inherited JComponent method
setBackground(BACKGROUND_COLOR);
}
/*
* add the specified Sector to this view
*
* each sector is represented by a (row, column) pair
* #param sectorView SectorView to be added to the QuadrantView
* #param row row coordinate
* #param col column coordinate
*/
public void addSectorView(SectorView sectorView, int row, int col)
{
//position the sector
layout.putConstraint(SpringLayout.WEST, sectorView, col * SECTOR_SIZE, SpringLayout.WEST, this);
layout.putConstraint(SpringLayout.NORTH, sectorView, row * SECTOR_SIZE, SpringLayout.NORTH, this);
//add sectorView to the layout using inherited method of Container class
this.add(sectorView);
}
}
static class Enterprise
{
protected Sector sectorLocation;
protected Quadrant quadrantLocation;
public Enterprise(Quadrant quadrant, Sector sector)
{
sectorLocation = sector;
quadrantLocation = quadrant;
sector.addEnterprise(this);
sector.setContainsEnterprise(true);
}
// Requirement 9.4.0
public boolean move(Sector destinationSector, final Quadrant destinationQuadrant, final GUI gui)
{
//if the destination quadrant is not our current quadrant, we need to update the gui (is updating this way causing a problem?)
if (!destinationQuadrant.equals(this.quadrantLocation))
{
//Put the new SectorViews in the new quadrant.
for (int i = 0; i < 8; i++)
{
for(int j = 0; j < 8; j++)
{
destinationQuadrant.getQuadrantView().addSectorView(destinationQuadrant.getSectorArray()[i][j].getSectorView(), i, j);
}
}
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
//initialize gui with the starting quadrant quadrantView
//replace the old quadrant view with the new one
gui.resetGUI(destinationQuadrant.getQuadrantView());
}
});
}
//remove the reference to this starship from the current sector
sectorLocation.removeEnterprise();
//sector no longer contains the Enterprise
sectorLocation.setContainsEnterprise(false);
//move to destination quadrant
quadrantLocation = destinationQuadrant;
//move to destination sector
sectorLocation = destinationSector;
//add a reference to this starship to the new sector
sectorLocation.addEnterprise(this);
//new sector now contains Enterprise
sectorLocation.setContainsEnterprise(true);
return true;
}
}//end Enterprise class
}
Limit the amount of data exchanged between threads. The only data which needs to be exchanged is the input from keyboard. Especially avoid sharing fields between threads - this leads to race conditions. Your main loop should look like this:
while(true)
{
final int qRow = input.nextInt();
final int qCol = input.nextInt();
final int sRow = input.nextInt();
final int sCol = input.nextInt();
SwingUtilities.invokeAndWait(new Runnable() {
#Override
public void run() {
move(qRow,qCol,sRow,sCol);
}
});
}
Remove all other invokeAndWait and invokeLater. Do not use invokeLater at all. It makes your program unpredictable.
Try to declare variables right before they are initialized and mark them as final. Mutable state leads to bugs.
I was not able to figure out why the table shrinks to 1x1. Try using GridLayout instead of SpringLayout. It seems better suited for this scenario.
Related
I am trying to add a thing like this in my music player application in swing.
I tried to add a rectangle to BorderLayout.SOUTH, but it never appeared on screen. Here is what I did:
public class MyDrawPanel extends JPanel {
#Override
public void paintComponent(Graphics g) {
g.setColor(Color.GREEN);
g.fillRect(200,200,200,200);
}
public static void main(String[] args) {
JFrame frame = new JFrame();
MyDrawPanel a = new MyDrawPanel();
frame.getContentPane().add(BorderLayout.SOUTH,a);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000,1000);
frame.setVisible(true);
}
}
I just did not try 200,200,200,200, but I tried a lot of values, even with the help of a for loop, but it never appeared on screen. If I used CENTER instead of SOUTH it appeared. I read the documentation to check how fillRect works, but it simply said it added x+width and y+height. The point (0,0) is the top left corner. I checked that by adding a rectangle to CENTER layout. How cam I do it?
I did not share the output, because it was just a blank screen.
The values you give to fillRect are wrong. The first two are the top left corner's coordinates, relative to the component you're painting in; in your case the MyDrawPanel. With the code you posted, this drawing area is outside of the container the panel is placed in. You want to do
g.fillRect(0,0,200,200);
A note: You usually want to call frame.pack() after you've finished adding all components, so it can layout itself. In your case, this results in a tiny window because the system doesn't know how large it should be. You probably want to add a method
public Dimension getPreferredSize() {
System.out.println("getting pref size");
return new Dimension(200, 200);
}
to ensure it's always large enough to draw the full rectangle.
Also, you should call frame.getContentPane().setLayout(new BorderLayout()) before. You can print it out without setting it to see it is not the default. EDIT: As VGR points out, the documentation says that it is in fact a BorderLayout. I cannot confirm that is the case - it is in fact a RootLayout. That seems to behave like a BorderLayout though.
I thought this might make a quick little project. Here's the level meter I came up with.
The important parts are the DrawingPanel and the LevelMeterModel. The DrawingPanel takes the information from the LevelMeterModel and paints the bars on a JPanel.
The LevelMeterModel is an int array of levels, a minimum level, and a maximum level. The maximum level could be calculated, but I assumed music has a certain volume and frequency range.
The JFrame holds the DrawingPanel. A Swing Timer varies the levels somewhat randomly. The random numbers are in a small range so the bar heights don't change abruptly.
Here's the complete runnable code.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class LevelMeterGUI implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new LevelMeterGUI());
}
private final DrawingPanel drawingPanel;
private final LevelMeterModel model;
public LevelMeterGUI() {
this.model = new LevelMeterModel();
this.drawingPanel = new DrawingPanel(model);
}
#Override
public void run() {
JFrame frame = new JFrame("Level Meter GUI");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
System.out.println("Frame size: " + frame.getSize());
Timer timer = new Timer(250, new ActionListener() {
#Override
public void actionPerformed(ActionEvent event) {
model.setRandomLevels();
drawingPanel.repaint();
}
});
timer.start();
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
private final int drawingWidth, drawingHeight, margin, rows;
private final Dimension barDimension;
private final LevelMeterModel model;
public DrawingPanel(LevelMeterModel model) {
this.model = model;
this.margin = 10;
this.rows = 20;
this.barDimension = new Dimension(50, 10);
int columns = model.getLevels().length;
drawingWidth = columns * barDimension.width + (columns + 1) * margin;
drawingHeight = rows * barDimension.height + (rows + 1) * margin;
this.setBackground(Color.WHITE);
this.setPreferredSize(new Dimension(drawingWidth, drawingHeight));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int maximum = model.getMaximumLevel();
double increment = (double) maximum / rows;
int peak = rows * 75 / 100;
int x = margin;
for (int level : model.getLevels()) {
int steps = (int) Math.round((double) level / increment);
int y = drawingHeight - margin - barDimension.height;
for (int index = 0; index < steps; index++) {
if (index < peak) {
g.setColor(Color.GREEN);
} else {
g.setColor(Color.RED);
}
g.fillRect(x, y, barDimension.width, barDimension.height);
y = y - margin - barDimension.height;
}
x += margin + barDimension.width;
}
}
}
public class LevelMeterModel {
private final int minimumLevel, maximumLevel;
private int[] levels;
private final Random random;
public LevelMeterModel() {
this.minimumLevel = 100;
this.maximumLevel = 999;
this.levels = new int[8];
this.random = new Random();
setRandomLevels();
}
public void setRandomLevels() {
for (int index = 0; index < levels.length; index++) {
levels[index] = getRandomLevel(levels[index]);
}
}
private int getRandomLevel(int level) {
if (level == 0) {
return random.nextInt(maximumLevel - minimumLevel) + minimumLevel;
} else {
int minimum = Math.max(level * 90 / 100, minimumLevel);
int maximum = Math.min(level * 110 / 100, maximumLevel);
return random.nextInt(maximum - minimum) + minimum;
}
}
public int[] getLevels() {
return levels;
}
public int getMinimumLevel() {
return minimumLevel;
}
public int getMaximumLevel() {
return maximumLevel;
}
}
}
I have written a short game. In the existing implementation I have a GridBagLayout with buttons located as chess board. Each button occupies the whole grid. Game works fine. My next task is to change the board to be consist of hexagonal buttons, not rectangles like currently. I completely don't know how to do this. Buttons should look like these on the picture:
This isn't the prettiest way, but It will at least give you an Idea:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class HexagonPattern extends JPanel {
private static final long serialVersionUID = 1L;
private static final int ROWS = 7;
private static final int COLUMNS = 7;
private HexagonButton[][] hexButton = new HexagonButton[ROWS][COLUMNS];
public HexagonPattern() {
setLayout(null);
initGUI();
}
public void initGUI() {
int offsetX = -10;
int offsetY = 0;
for(int row = 0; row < ROWS; row++) {
for(int col = 0; col < COLUMNS; col++){
hexButton[row][col] = new HexagonButton(row, col);
hexButton[row][col].addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
HexagonButton clickedButton = (HexagonButton) e.getSource();
System.out.println("Button clicked: [" + clickedButton.getRow() + "][" + clickedButton.getCol() + "]");
}
});
add(hexButton[row][col]);
hexButton[row][col].setBounds(offsetY, offsetX, 105, 95);
offsetX += 87;
}
if(row%2 == 0) {
offsetX = -52;
} else {
offsetX = -10;
}
offsetY += 76;
}
}
public static void main(String[] args) {
HexagonPattern hexPattern = new HexagonPattern();
JFrame frame = new JFrame();
frame.setTitle("Hexagon Pattern");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(new Point(700, 300));
frame.add(hexPattern);
frame.setSize(550, 525);
frame.setResizable(false);
frame.setVisible(true);
}
//Following class draws the Buttons
class HexagonButton extends JButton {
private static final long serialVersionUID = 1L;
private static final int SIDES = 6;
private static final int SIDE_LENGTH = 50;
public static final int LENGTH = 95;
public static final int WIDTH = 105;
private int row = 0;
private int col = 0;
public HexagonButton(int row, int col) {
setContentAreaFilled(false);
setFocusPainted(true);
setBorderPainted(false);
setPreferredSize(new Dimension(WIDTH, LENGTH));
this.row = row;
this.col = col;
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Polygon hex = new Polygon();
for (int i = 0; i < SIDES; i++) {
hex.addPoint((int) (50 + SIDE_LENGTH * Math.cos(i * 2 * Math.PI / SIDES)), //calculation for side
(int) (50 + SIDE_LENGTH * Math.sin(i * 2 * Math.PI / SIDES))); //calculation for side
}
g.drawPolygon(hex);
}
public int getRow() {
return row;
}
public int getCol() {
return col;
}
}
}
Test it out!
This program consists of 2 classes:
HexagonButton, which uses Graphics to draw a hexagon into a JButton. It also returns the row and column values when getRow or getCol are called.
HexagonPattern, which is the main class. It makes the pattern by laying them out with setBounds(x, y, width, height). It uses an ActionListener to print the coordinates of the Hexagon clicked, by calling getRow and getCol.
Like I said, this isn't the greatest program. If you want to make the hexagons smaller, then you'll have to change many variables.
So I'm trying to figure out a tutorial on Tic Tac Toe. But I am wondering where i initialize the Computer player.
So I have an abstract AIPlayer Class
public abstract class AIPlayer {
protected int ROWS = 3; // number of rows
protected int COLS = 3; // number of columns
protected Cell[][] cells; // the board's ROWS-by-COLS array of Cells
protected Seed mySeed; // computer's seed
protected Seed oppSeed; // opponent's seed
/** Constructor with reference to game board */
public AIPlayer(Board board) {
cells = board.cells;
}
/** Set/change the seed used by computer and opponent */
public void setSeed(Seed seed) {
this.mySeed = seed;
oppSeed = (mySeed == Seed.CROSS) ? Seed.NOUGHT : Seed.CROSS;
}
/** Abstract method to get next move. Return int[2] of {row, col} */
abstract int[] move(); // to be implemented by subclasses
}
This is my Algorithm to find out the best move for the Computer
import AIPlayer;
import Board;
import Seed;
import java.util.*;
/** AIPlayer using Minimax algorithm */
public class AIPlayerMinimax extends AIPlayer {
/** Constructor with the given game board */
public AIPlayerMinimax(Board board) {
super(board);
}
int[] move() {
int[] result = minimax(2, mySeed); // depth, max turn
return new int[] {result[1], result[2]}; // row, col
}
private int[] minimax(int depth, Seed player) {
//minmax code
}
private List<int[]> generateMoves() {
//generate moves
}
private int evaluate() {
//Evaluating
}
private int evaluateLine(int row1, int col1, int row2, int col2, int row3, int col3) {
// ..
}
private int[] winningPatterns = {
//
};
private boolean hasWon(Seed thePlayer) {
//
}
}
And this is my GameMain which has the graphics and all the good stuff
public class GameMain extends JPanel {
// Named-constants for the game board
public static final int ROWS = 3; // ROWS by COLS cells
public static final int COLS = 3;
public static final String TITLE = "Tic Tac Toe";
// Name-constants for the various dimensions used for graphics drawing
public static final int CELL_SIZE = 100; // cell width and height (square)
public static final int CANVAS_WIDTH = CELL_SIZE * COLS; // the drawing canvas
public static final int CANVAS_HEIGHT = CELL_SIZE * ROWS;
public static final int GRID_WIDTH = 8; // Grid-line's width
public static final int GRID_WIDHT_HALF = GRID_WIDTH / 2; // Grid-line's half-width
// Symbols (cross/nought) are displayed inside a cell, with padding from border
public static final int CELL_PADDING = CELL_SIZE / 6;
public static final int SYMBOL_SIZE = CELL_SIZE - CELL_PADDING * 2;
public static final int SYMBOL_STROKE_WIDTH = 8; // pen's stroke width
private Board board; // the game board
private GameState currentState; // the current state of the game
private Seed currentPlayer; // the current player
private JLabel statusBar; // for displaying status message
/** Constructor to setup the UI and game components */
public GameMain() {
// This JPanel fires MouseEvent
this.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) { // mouse-clicked handler
int mouseX = e.getX();
int mouseY = e.getY();
// Get the row and column clicked
int rowSelected = mouseY / CELL_SIZE;
int colSelected = mouseX / CELL_SIZE;
if (currentState == GameState.PLAYING) {
if (rowSelected >= 0 && rowSelected < ROWS
&& colSelected >= 0 && colSelected < COLS
&& board.cells[rowSelected][colSelected].content == Seed.EMPTY) {
board.cells[rowSelected][colSelected].content = currentPlayer; // move
updateGame(currentPlayer, rowSelected, colSelected); // update currentState
// Switch player
currentPlayer = (currentPlayer == Seed.CROSS) ? Seed.NOUGHT : Seed.CROSS;
}
} else { // game over
initGame(); // restart the game
}
// Refresh the drawing canvas
repaint(); // Call-back paintComponent().
}
});
// Setup the status bar (JLabel) to display status message
statusBar = new JLabel(" ");
statusBar.setFont(new Font(Font.DIALOG_INPUT, Font.BOLD, 14));
statusBar.setBorder(BorderFactory.createEmptyBorder(2, 5, 4, 5));
statusBar.setOpaque(true);
statusBar.setBackground(Color.LIGHT_GRAY);
setLayout(new BorderLayout());
add(statusBar, BorderLayout.PAGE_END); // same as SOUTH
setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT + 30));
// account for statusBar in height
board = new Board(); // allocate the game-board
initGame(); // Initialize the game variables
}
/** Initialize the game-board contents and the current-state */
public void initGame() {
for (int row = 0; row < ROWS; ++row) {
for (int col = 0; col < COLS; ++col) {
board.cells[row][col].content = Seed.EMPTY; // all cells empty
}
}
currentState = GameState.PLAYING; // ready to play
currentPlayer = Seed.CROSS; // cross plays first
}
/** Update the currentState after the player with "theSeed" has placed on (row, col) */
public void updateGame(Seed theSeed, int row, int col) {
if (board.hasWon(theSeed, row, col)) { // check for win
currentState = (theSeed == Seed.CROSS) ? GameState.CROSS_WON : GameState.NOUGHT_WON;
} else if (board.isDraw()) { // check for draw
currentState = GameState.DRAW;
}
// Otherwise, no change to current state (PLAYING).
}
/** Custom painting codes on this JPanel */
#Override
public void paintComponent(Graphics g) { // invoke via repaint()
super.paintComponent(g); // fill background
setBackground(Color.WHITE); // set its background color
board.paint(g); // ask the game board to paint itself
// Print status-bar message
if (currentState == GameState.PLAYING) {
statusBar.setForeground(Color.BLACK);
if (currentPlayer == Seed.CROSS) {
statusBar.setText("X's Turn");
} else {
statusBar.setText("O's Turn");
}
} else if (currentState == GameState.DRAW) {
statusBar.setForeground(Color.RED);
statusBar.setText("It's a Draw! Click to play again.");
} else if (currentState == GameState.CROSS_WON) {
statusBar.setForeground(Color.RED);
statusBar.setText("'X' Won! Click to play again.");
} else if (currentState == GameState.NOUGHT_WON) {
statusBar.setForeground(Color.RED);
statusBar.setText("'O' Won! Click to play again.");
}
}
/** The entry "main" method */
public static void main(String[] args) {
// Run GUI construction codes in Event-Dispatching thread for thread safety
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFrame frame = new JFrame(TITLE);
// Set the content-pane of the JFrame to an instance of main JPanel
frame.setContentPane(new GameMain());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null); // center the application window
frame.setVisible(true); // show it
}
});
}
}
So my question is, how do I initialize the AIPlayer to compete with me? At the moment this works as a multiplayer game, Human vs Human.
Well, you probably want to add a property go GameMain:
private AIPlayer aiPlayer;
Instantiate your AIPlayer in initGame():
aiPlayer = new AIPlayer(board)
aiPlayer.setSeed(Seed.NOUGHT);
Then it depends on when you want the AI Player to make its move. You'd need to call something like this:
public void AIMove() {
int[] generatedMove = aiPlayer.move();
board.cells[generatedMove[0]][generatedMove[1]].content = currentPlayer;
updateGame(currentPlayer, generatedMove[0], generatexMove[1]);
repaint();
}
That should work, but isn't the best code-design. As you can see there is a lot of duplication, no seperation of concerns and other design flaws. But that would be a better question for CodeReview.SE.
I am making a GUI component to represent something like a Chess board in a window. Normally it will be a grid of 8x8 squares, although some variants require a 10x8 board etc. The first step is to make a panel that contains a grid of 8x8 components.
The class Board extends JPanel and uses a GridLayout to model a grid of 8x8 components. In an effort to get something done these are simply of class Square which extends JButton. The trouble is that they're not squares!
The Board has been added to a freshly instantiated JFrame, packed and rendered on the screen. Of course, right now the board takes up the entire frame as it is resized by the user. The grid scales with the board and this distorts the squares into rectangles.
This is not entirely undesired behaviour. I would like the board to scale with the frame. However, I would like to ensure that the squares remain square at all times. The board could be rectangular (10x8) but should maintain a fixed proportion.
How do I get square squares?
You can choose to use a LayoutManager that honors the preferred size of the cells instead. GridLayout will provide a equal amount of the available space to each cell, which doesn't appear to be quite what you want.
For example, something like GridBagLayout
public class TestChessBoard {
public static void main(String[] args) {
new TestChessBoard();
}
public TestChessBoard() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new ChessBoardPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ChessBoardPane extends JPanel {
public ChessBoardPane() {
int index = 0;
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
Color color = index % 2 == 0 ? Color.BLACK : Color.WHITE;
gbc.gridx = col;
gbc.gridy = row;
add(new Cell(color), gbc);
index++;
}
index++;
}
}
}
public class Cell extends JButton {
public Cell(Color background) {
setContentAreaFilled(false);
setBorderPainted(false);
setBackground(background);
setOpaque(true);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(25, 25);
}
}
}
Updated with proportional example
Now, if you want to do a proportional layout (so that each cell of the grid remains proportional to the other regardless of the available space), things begin to get ... fun ...
public class TestChessBoard {
public static void main(String[] args) {
new TestChessBoard();
}
public TestChessBoard() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestChessBoard.ChessBoardPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ChessBoardPane extends JPanel {
public ChessBoardPane() {
int index = 0;
setLayout(new ChessBoardLayoutManager());
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
Color color = index % 2 == 0 ? Color.BLACK : Color.WHITE;
add(new TestChessBoard.Cell(color), new Point(col, row));
index++;
}
index++;
}
}
}
public class Cell extends JButton {
public Cell(Color background) {
setContentAreaFilled(false);
setBorderPainted(false);
setBackground(background);
setOpaque(true);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(25, 25);
}
}
public class ChessBoardLayoutManager implements LayoutManager2 {
private Map<Point, Component> mapComps;
public ChessBoardLayoutManager() {
mapComps = new HashMap<>(25);
}
#Override
public void addLayoutComponent(Component comp, Object constraints) {
if (constraints instanceof Point) {
mapComps.put((Point) constraints, comp);
} else {
throw new IllegalArgumentException("ChessBoard constraints must be a Point");
}
}
#Override
public Dimension maximumLayoutSize(Container target) {
return preferredLayoutSize(target);
}
#Override
public float getLayoutAlignmentX(Container target) {
return 0.5f;
}
#Override
public float getLayoutAlignmentY(Container target) {
return 0.5f;
}
#Override
public void invalidateLayout(Container target) {
}
#Override
public void addLayoutComponent(String name, Component comp) {
}
#Override
public void removeLayoutComponent(Component comp) {
Point[] keys = mapComps.keySet().toArray(new Point[mapComps.size()]);
for (Point p : keys) {
if (mapComps.get(p).equals(comp)) {
mapComps.remove(p);
break;
}
}
}
#Override
public Dimension preferredLayoutSize(Container parent) {
return new CellGrid(mapComps).getPreferredSize();
}
#Override
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}
#Override
public void layoutContainer(Container parent) {
int width = parent.getWidth();
int height = parent.getHeight();
int gridSize = Math.min(width, height);
CellGrid grid = new CellGrid(mapComps);
int rowCount = grid.getRowCount();
int columnCount = grid.getColumnCount();
int cellSize = gridSize / Math.max(rowCount, columnCount);
int xOffset = (width - (cellSize * columnCount)) / 2;
int yOffset = (height - (cellSize * rowCount)) / 2;
Map<Integer, List<CellGrid.Cell>> cellRows = grid.getCellRows();
for (Integer row : cellRows.keySet()) {
List<CellGrid.Cell> rows = cellRows.get(row);
for (CellGrid.Cell cell : rows) {
Point p = cell.getPoint();
Component comp = cell.getComponent();
int x = xOffset + (p.x * cellSize);
int y = yOffset + (p.y * cellSize);
comp.setLocation(x, y);
comp.setSize(cellSize, cellSize);
}
}
}
public class CellGrid {
private Dimension prefSize;
private int cellWidth;
private int cellHeight;
private Map<Integer, List<Cell>> mapRows;
private Map<Integer, List<Cell>> mapCols;
public CellGrid(Map<Point, Component> mapComps) {
mapRows = new HashMap<>(25);
mapCols = new HashMap<>(25);
for (Point p : mapComps.keySet()) {
int row = p.y;
int col = p.x;
List<Cell> rows = mapRows.get(row);
List<Cell> cols = mapCols.get(col);
if (rows == null) {
rows = new ArrayList<>(25);
mapRows.put(row, rows);
}
if (cols == null) {
cols = new ArrayList<>(25);
mapCols.put(col, cols);
}
Cell cell = new Cell(p, mapComps.get(p));
rows.add(cell);
cols.add(cell);
}
int rowCount = mapRows.size();
int colCount = mapCols.size();
cellWidth = 0;
cellHeight = 0;
for (List<Cell> comps : mapRows.values()) {
for (Cell cell : comps) {
Component comp = cell.getComponent();
cellWidth = Math.max(cellWidth, comp.getPreferredSize().width);
cellHeight = Math.max(cellHeight, comp.getPreferredSize().height);
}
}
int cellSize = Math.max(cellHeight, cellWidth);
prefSize = new Dimension(cellSize * colCount, cellSize * rowCount);
System.out.println(prefSize);
}
public int getRowCount() {
return getCellRows().size();
}
public int getColumnCount() {
return getCellColumns().size();
}
public Map<Integer, List<Cell>> getCellColumns() {
return mapCols;
}
public Map<Integer, List<Cell>> getCellRows() {
return mapRows;
}
public Dimension getPreferredSize() {
return prefSize;
}
public int getCellHeight() {
return cellHeight;
}
public int getCellWidth() {
return cellWidth;
}
public class Cell {
private Point point;
private Component component;
public Cell(Point p, Component comp) {
this.point = p;
this.component = comp;
}
public Point getPoint() {
return point;
}
public Component getComponent() {
return component;
}
}
}
}
}
This got a bit long, so here is the quick answer: You can't maintain a square board with square squares given your board dimensions (8x8, 10x8) and fully fill the screen if the user can resize it. You should limit the size of the board so that it maintains the aspect ratio even if that means you have some blank space in your frame. OK, read on for the long-winded explanation...
There are two ways you can make this work. Either you can limit the possible sizes of the JFrame, or you can limit the size of your Board so it doesn't always fill the frame. Limiting the size of the board is the more common method, so let's start with that.
Option 1: Limiting the Board
If you are working with a fixed set of board dimensions (8x8, 10x8, and a couple others maybe), and assuming each square has some minimum size (1 pixel squares on a chess board don't sound too practical), there are only so many frame dimensions that the board can fully fill. If your frame is 80pixels by 80pixels, your 8x8 board fits perfectly. But as soon as the user resizes to something like 85x80 you're stuck because you can't fully fill that while maintaining squares with the board dimensions you gave.
In this case you want to leave 5 pixels empty, whether it's 5 above or below, or 2.5 above and below, or whatever, doesn't matter. This should sound familiar - it's an aspect ratio problem and basically why you can get black bars on the edges of your TV depending on TV vs. movie dimensions.
Option 2: Limiting the Frame
If you want the board to always fully fill the frame, probably not what you want, then you have to adjust the size of the frame after a user resizes it. Say you are using a 10x8 board, and the user sets the frame to 107x75. That's not too bad, and with a little math you can figure out 100x80 is your closest aspect ratio that works, and fix the window. It will probably a bit frustrating for the user if the window keeps jumping around on them though, especially if they tried to make it something way off like 50x200.
Last thoughts / Example
Limiting the board is most likely the correct solution. Everything from games to desktop apps follows that principle. Take the ribbon in MS Office products for example. As you make the window larger, the ribbon will expand (maintaining its proportions) until it hits it max size, and then you just get more space for your document. When you make the window smaller the ribbon gets smaller (again maintaining its proportions) until it hits a minimum size and then you start losing parts of it (remember, don't want 1x1 squares on your board).
On the other hand you can prevent the user from resizing the window at all. I'm pretty sure this is how MineSweeper works (don't have it on this computer to double check), and may be a better/easier solution for what you need.
Right now I am trying to make it so that the connect 4 grid on the gui is always a 7x8 no matter what window size. I have been trying to set the button array with a setMaximumSize and it's not working.
Here is the code that sets the JButton array
void ResetGame()
{
JLabel label = new JLabel("Click a column to drop piece");
for(int r=0;r<gameBoard.length;r++)
{
java.util.Arrays.fill(gameBoard[r],0,gameBoard[r].length,'0');
//loop through board columns
for(int c=0;c<gameBoard[r].length;c++)
{
gameButtons[r][c]= new JButton(empty);
panel.add(gameButtons[r][c]);
gameButtons[r][c].setPreferredSize(new Dimension(70,70));
//Allows buttons to be arranged as grid.
GridLayout grid = new GridLayout(0,8);
//Sets into grid.
gameButtons[r][c].setLayout(grid);
gameButtons[r][c].setMaximumSize(new Dimension(0,10));
}
panel.add(label);
}
// loop through array setting char array back to ' ' and buttons array back to empty pic
// reset currentPlayer and numMoves variables
}
Just in case I'll also include the window creation method here.
public void CreateWindow()
{
//Sets window title and create window object.
JFrame aWindow = new JFrame("Connect Four");
//Set window position and size
aWindow.setBounds(200,100,600,800);
//What close button does.
aWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Make window visible.
aWindow.setVisible(true);
//Sets content area to work with stuff.
aWindow.setContentPane(panel);
//Gets content pane.
Container content = aWindow.getContentPane();
}
Not sure of what you are trying to achieve with setMaximumSize. Without explicit and precise requirements, we can hardly help you.
So, I would suggest that you take a look at the following snippet (which is an SSCCE) and try to find out what you are doing wrong:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class Connect4 {
public class GameButton extends JPanel {
private final int row;
private final int column;
private Color color;
public GameButton(final int row, final int column) {
super();
this.row = row;
this.column = column;
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
System.out.println("Game button " + row + " " + column + " has been pressed");
}
});
}
public void setColor(Color color) {
this.color = color;
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int size = Math.min(getWidth(), getHeight());
int offset = (int) ((double) size / 10);
size = size - 2 * offset;
if (color == null) {
g.setColor(Color.BLACK);
g.drawOval(offset, offset, size, size);
} else {
g.setColor(color);
g.fillOval(offset, offset, size, size);
}
}
}
protected void initUI() {
JPanel gridPanel = new JPanel(new GridLayout(7, 8));
for (int i = 0; i < 7; i++) {
for (int j = 0; j < 8; j++) {
GameButton gameButton = new GameButton(i, j);
gridPanel.add(gameButton);
}
}
JFrame frame = new JFrame(Connect4.class.getSimpleName());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(gridPanel);
frame.setSize(600, 600);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new Connect4().initUI();
}
});
}
}
setMaximumSize() puts a bound on how large something can be. Depending on what you are using for a layout manager you want either setPreferredSize() or setSize().