package aima.gui.applications.search.games; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JToolBar; import aima.core.environment.connectfour.ConnectFourAIPlayer; import aima.core.environment.connectfour.ConnectFourGame; import aima.core.environment.connectfour.ConnectFourState; import aima.core.search.adversarial.AdversarialSearch; import aima.core.search.adversarial.AlphaBetaSearch; import aima.core.search.adversarial.IterativeDeepeningAlphaBetaSearch; import aima.core.search.adversarial.MinimaxSearch; import aima.core.search.framework.Metrics; /** * Simple graphical Connect Four game application. It demonstrates the Minimax * algorithm with alpha-beta pruning, iterative deepening, and action ordering. * The implemented action ordering strategy tries to maximize the impact of the * chosen action for later game phases. * * @author Ruediger Lunde */ public class ConnectFourApp { /** Used for integration into the universal demo application. */ public JFrame constructApplicationFrame() { JFrame frame = new JFrame(); JPanel panel = new ConnectFourPanel(); frame.add(panel, BorderLayout.CENTER); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); return frame; } /** Application starter. */ public static void main(String[] args) { JFrame frame = new ConnectFourApp().constructApplicationFrame(); frame.setSize(450, 450); frame.setVisible(true); } /** Simple panel to control the game. */ private static class ConnectFourPanel extends JPanel implements ActionListener { private static final long serialVersionUID = 1L; JComboBox<String> strategyCombo; JComboBox<String> timeCombo; JButton clearButton; JButton proposeButton; JLabel statusBar; ConnectFourGame game; ConnectFourState currState; Metrics searchMetrics; /** Standard constructor. */ ConnectFourPanel() { game = new ConnectFourGame(); currState = game.getInitialState(); setLayout(new BorderLayout()); setBackground(Color.BLUE); JToolBar toolBar = new JToolBar(); toolBar.setFloatable(false); strategyCombo = new JComboBox<String>(new String[] { "Minimax (not recommended)", "Alpha-Beta (not recommended)", "Iterative Deepening Alpha-Beta", "Advanced Alpha-Beta", "Advanced Alpha-Beta (log)" }); strategyCombo.setSelectedIndex(3); toolBar.add(strategyCombo); timeCombo = new JComboBox<String>(new String[] { "5sec", "10sec", "15sec", "20sec" }); timeCombo.setSelectedIndex(0); toolBar.add(timeCombo); toolBar.add(Box.createHorizontalGlue()); clearButton = new JButton("Clear"); clearButton.addActionListener(this); toolBar.add(clearButton); proposeButton = new JButton("Propose Move"); proposeButton.addActionListener(this); toolBar.add(proposeButton); add(toolBar, BorderLayout.NORTH); int rows = currState.getRows(); int cols = currState.getCols(); JPanel boardPanel = new JPanel(); boardPanel.setLayout(new GridLayout(rows, cols, 5, 5)); boardPanel.setBorder(BorderFactory.createEtchedBorder()); boardPanel.setBackground(Color.BLUE); for (int i = 0; i < rows * cols; i++) { GridElement element = new GridElement(i / cols, i % cols); boardPanel.add(element); element.addActionListener(this); } add(boardPanel, BorderLayout.CENTER); statusBar = new JLabel(" "); statusBar.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); add(statusBar, BorderLayout.SOUTH); updateStatus(); } /** Handles all button events and updates the view. */ @Override public void actionPerformed(ActionEvent e) { searchMetrics = null; if (e == null || e.getSource() == clearButton) { currState = game.getInitialState(); } else if (!game.isTerminal(currState)) { if (e.getSource() == proposeButton) { proposeMove(); } else if (e.getSource() instanceof GridElement) { GridElement el = (GridElement) e.getSource(); currState = game.getResult(currState, el.col); // take next // turn } } repaint(); // paint all disks! updateStatus(); } /** Uses adversarial search for selecting the next action. */ private void proposeMove() { Integer action; int time = (timeCombo.getSelectedIndex() + 1) * 5; AdversarialSearch<ConnectFourState, Integer> search; switch (strategyCombo.getSelectedIndex()) { case 0: search = MinimaxSearch.createFor(game); break; case 1: search = AlphaBetaSearch.createFor(game); break; case 2: search = IterativeDeepeningAlphaBetaSearch.createFor(game, 0.0, 1.0, time); break; case 3: search = new ConnectFourAIPlayer(game, time); break; default: search = new ConnectFourAIPlayer(game, time); ((ConnectFourAIPlayer) search).setLogEnabled(true); } action = search.makeDecision(currState); searchMetrics = search.getMetrics(); currState = game.getResult(currState, action); } /** Updates the status bar. */ private void updateStatus() { String statusText; if (!game.isTerminal(currState)) { String toMove = (String) game.getPlayer(currState); statusText = "Next move: " + toMove; statusBar.setForeground(toMove.equals("red") ? Color.RED : Color.YELLOW); } else { String winner = null; for (int i = 0; i < 2; i++) if (game.getUtility(currState, game.getPlayers()[i]) == 1) winner = game.getPlayers()[i]; if (winner != null) statusText = "Color " + winner + " has won. Congratulations!"; else statusText = "No winner :-("; statusBar.setForeground(Color.WHITE); } if (searchMetrics != null) statusText += " " + searchMetrics; statusBar.setText(statusText); } /** Represents a space within the grid where discs can be placed. */ @SuppressWarnings("serial") private class GridElement extends JButton { int row; int col; GridElement(int row, int col) { this.row = row; this.col = col; setBackground(Color.BLUE); } public void paintComponent(Graphics g) { super.paintComponent(g); // should have look and feel of a // button... int playerNum = currState.getPlayerNum(row, col); if (playerNum != 0) { drawDisk(g, playerNum); // draw disk on top! } for (int pNum = 1; pNum <= 2; pNum++) if (currState.isWinPositionFor(row, col, pNum)) drawWinSituation(g, pNum); } /** Fills a simple oval. */ void drawDisk(Graphics g, int playerNum) { int size = Math.min(getWidth(), getHeight()); g.setColor(playerNum == 1 ? Color.RED : Color.YELLOW); g.fillOval((getWidth() - size) / 2, (getHeight() - size) / 2, size, size); } /** Draws a simple oval. */ void drawWinSituation(Graphics g, int playerNum) { int size = Math.min(getWidth(), getHeight()); g.setColor(playerNum == 1 ? Color.RED : Color.YELLOW); g.drawOval((getWidth() - size) / 2 + playerNum, (getHeight() - size) / 2 + playerNum, size - 2 * playerNum, size - 2 * playerNum); } } } }