package aima.gui.fx.applications.search.games; import aima.core.environment.tictactoe.TicTacToeGame; import aima.core.environment.tictactoe.TicTacToeState; 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; import aima.core.util.datastructure.XYLocation; import aima.gui.fx.framework.IntegrableApplication; import javafx.beans.binding.Bindings; import javafx.event.ActionEvent; import javafx.geometry.Pos; import javafx.scene.control.*; import javafx.scene.layout.*; import javafx.scene.text.Font; /** * Simple graphical Tic-tac-toe game application. It demonstrates the Minimax * algorithm for move selection as well as alpha-beta pruning. * * @author Ruediger Lunde */ public class TicTacToeApp extends IntegrableApplication { public static void main(String[] args) { launch(args); } private Button clearBtn; private ComboBox<String> strategyCombo; private Button proposeBtn; private Label statusLabel; private Button[] squareBtns = new Button[9]; TicTacToeGame game; TicTacToeState currState; Metrics searchMetrics; @Override public String getTitle() { return "Tic-tac-toe App"; } /** Simple pane to control the game. */ @Override public Pane createRootPane() { BorderPane root = new BorderPane(); ToolBar toolBar = new ToolBar(); clearBtn = new Button("Clear"); clearBtn.setOnAction(ev -> initialize()); strategyCombo = new ComboBox<String>(); strategyCombo.getItems().addAll("Minimax", "Alpha-Beta", "Iterative Deepening Alpha-Beta", "Iterative Deepening Alpha-Beta (log)"); strategyCombo.getSelectionModel().select(0); proposeBtn = new Button("Propose Move"); proposeBtn.setOnAction(ev -> proposeMove()); toolBar.getItems().addAll(clearBtn, new Separator(), strategyCombo, proposeBtn); root.setTop(toolBar); StackPane stateViewPane = new StackPane(); GridPane gridPane = new GridPane(); gridPane.maxWidthProperty().bind(Bindings.min(stateViewPane.widthProperty(), stateViewPane.heightProperty()) .subtract(20)); gridPane.maxHeightProperty().bind(Bindings.min(stateViewPane.widthProperty(), stateViewPane.heightProperty()) .subtract(20)); RowConstraints c1 = new RowConstraints(); c1.setPercentHeight(100.0 / 3); ColumnConstraints c2 = new ColumnConstraints(); c2.setPercentWidth(100.0 / 3); gridPane.setHgap(10); gridPane.setVgap(10); for (int i = 0; i < 3; i++) { gridPane.getRowConstraints().add(c1); gridPane.getColumnConstraints().add(c2); } Font font = Font.font(40); for (int i = 0; i < 9; i++) { Button btn = new Button(); btn.setOnAction(this::handleSquareButtonEvent); btn.setFont(font); btn.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); squareBtns[i] = btn; gridPane.add(btn, i % 3, i / 3); } stateViewPane.getChildren().add(gridPane); root.setCenter(stateViewPane); statusLabel = new Label(); statusLabel.setMaxWidth(Double.MAX_VALUE); statusLabel.setMaxWidth(Double.MAX_VALUE); statusLabel.setAlignment(Pos.CENTER); statusLabel.setFont(Font.font(16)); root.setBottom(statusLabel); return root; } @Override public void initialize() { game = new TicTacToeGame(); currState = game.getInitialState(); searchMetrics = null; update(); } @Override public void cleanup() { // nothing to do here... } /** Updates square texts according to game state and also the status bar. */ private void update() { for (int i = 0; i < 9; i++) { String val = currState.getValue(i % 3, i / 3); if (val == TicTacToeState.EMPTY) val = ""; squareBtns[i].setText(val); } proposeBtn.setDisable(game.isTerminal(currState)); // update status... String statusText; if (game.isTerminal(currState)) if (game.getUtility(currState, TicTacToeState.X) == 1) statusText = "X has won :-)"; else if (game.getUtility(currState, TicTacToeState.O) == 1) statusText = "O has won :-)"; else statusText = "No winner..."; else statusText = "Next move: " + game.getPlayer(currState); if (searchMetrics != null) statusText += " " + searchMetrics; statusLabel.setText(statusText); } /** Uses adversarial search for selecting the next action. */ private void proposeMove() { AdversarialSearch<TicTacToeState, XYLocation> search; XYLocation action; switch (strategyCombo.getSelectionModel().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, 1000); break; default: search = IterativeDeepeningAlphaBetaSearch.createFor(game, 0.0, 1.0, 1000); ((IterativeDeepeningAlphaBetaSearch<?, ?, ?>) search) .setLogEnabled(true); } action = search.makeDecision(currState); searchMetrics = search.getMetrics(); currState = game.getResult(currState, action); update(); } /** Handles user moves. */ private void handleSquareButtonEvent(ActionEvent ae) { for (int i = 0; i < 9; i++) if (ae.getSource() == squareBtns[i]) currState = game.getResult(currState, new XYLocation(i % 3, i / 3)); searchMetrics = null; update(); } }