5
\$\begingroup\$

GitHub

The entire project relies here (ConnectFourFX.java) and is dependent on Connect4.java.

Code

com.github.coderodde.game.connect4.ConnectFourBoard.java:

package com.github.coderodde.game.connect4;

import com.github.coderodde.game.zerosum.PlayerType;
import com.github.coderodde.game.zerosum.GameState;
import java.awt.Point;
import java.util.ArrayList;
import java.util.List;

/**
 * This class implements a board that corresponds to a game state in the game
 * search tree.
 * 
 * @version 1.0.0 (Jun 5, 2024)
 * @since 1.0.0 (Jun 5, 2024)
 */
public class ConnectFourBoard implements GameState<ConnectFourBoard> {

    public static final int ROWS = 6;
    public static final int COLUMNS = 7;
    public static final int VICTORY_LENGTH = 4;
    
    final PlayerType[] boardData = new PlayerType[ROWS * COLUMNS];
    
    public ConnectFourBoard(final ConnectFourBoard other) {
        System.arraycopy(other.boardData, 0, boardData, 0, ROWS * COLUMNS);
    }
    
    public ConnectFourBoard() {
        
    }
    
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        
        for (int y = 0; y < ROWS; y++) {
            // Build the row:
            for (int x = 0; x < COLUMNS; x++) {
                sb.append("|");
                sb.append(getCellChar(get(x, y)));
            }
            
            sb.append("|\n");
        }
        
        sb.append("+-+-+-+-+-+-+-+\n");
        sb.append(" 1 2 3 4 5 6 7");
        
        return sb.toString();
    }
    
    @Override
    public List<ConnectFourBoard> expand(final PlayerType playerType) {
        final List<ConnectFourBoard> children = new ArrayList<>(COLUMNS);
        
        for (int x = 0; x < COLUMNS; x++) {
            if (notFullAtX(x)) {
                children.add(dropAtX(x, playerType));
            }
        }
        
        return children;
    }
    
    @Override
    public boolean isWinningFor(final PlayerType playerType) {
        return hasAscendingDiagonalStrike(playerType, VICTORY_LENGTH) ||
              hasDescendingDiagonalStrike(playerType, VICTORY_LENGTH) ||
                      hasHorizontalStrike(playerType, VICTORY_LENGTH) ||
                        hasVerticalStrike(playerType, VICTORY_LENGTH);
    }
    
    @Override
    public boolean isTie() {
        for (int x = 0; x < COLUMNS; x++) {
            if (get(x, 0) == null) {
                return false;
            }
        }
        
        return true;
    }
    
    public List<Point> getWinningPattern() {
        if (!isWinningFor(PlayerType.MINIMIZING_PLAYER) &&
            !isWinningFor(PlayerType.MAXIMIZING_PLAYER)) {
            return null;
        }
        
        // Check whether the minimizing/human player has a winning pattern:
        List<Point> winningPattern =
                tryLoadAscendingWinningPattern(PlayerType.MINIMIZING_PLAYER);
        
        if (winningPattern != null) {
            return winningPattern;
        }
        
        winningPattern = 
                tryLoadDescendingWinningPattern(PlayerType.MINIMIZING_PLAYER);
        
        if (winningPattern != null) {
            return winningPattern;
        }
        
        winningPattern = 
                tryLoadHorizontalWinningPattern(PlayerType.MINIMIZING_PLAYER);
        
        if (winningPattern != null) {
            return winningPattern;
        }
        
        winningPattern = 
                tryLoadVerticalWinningPattern(PlayerType.MINIMIZING_PLAYER);
        
        if (winningPattern != null) {
            return winningPattern;
        }
        
        // Check whether the maximizing/CPU player has a winning pattern:
        winningPattern =
                tryLoadAscendingWinningPattern(PlayerType.MAXIMIZING_PLAYER);
        
        if (winningPattern != null) {
            return winningPattern;
        }
        
        winningPattern = 
                tryLoadDescendingWinningPattern(PlayerType.MAXIMIZING_PLAYER);
        
        if (winningPattern != null) {
            return winningPattern;
        }
        
        winningPattern = 
                tryLoadHorizontalWinningPattern(PlayerType.MAXIMIZING_PLAYER);
        
        if (winningPattern != null) {
            return winningPattern;
        }
        
        winningPattern = 
                tryLoadVerticalWinningPattern(PlayerType.MAXIMIZING_PLAYER);
        
        if (winningPattern != null) {
            return winningPattern;
        }
        
        throw new IllegalStateException("Should not get here.");
    }
    
    public PlayerType get(final int x, final int y) {
        return boardData[y * COLUMNS + x];
    }
    
    public void set(final int x,
                    final int y,
                    final PlayerType playerType) {
        boardData[y * COLUMNS + x] = playerType;
    }
    
    public boolean makePly(final int x, final PlayerType playerType) {
        for (int y = ROWS - 1; y >= 0; y--) {
            if (get(x, y) == null) {
                set(x, y, playerType);
                return true;
            }
        }
        
        return false;
    }
    
    public void unmakePly(final int x) {
        for (int y = 0; y < ROWS; y++) {
            if (get(x, y) != null) {
                set(x, y, null);
                return;
            }
        }
    }
    
    boolean hasHorizontalStrike(final PlayerType playerType, final int length) {
        
        final int lastX = COLUMNS - length;
        
        for (int y = ROWS - 1; y >= 0; y--) {
            horizontalCheck:
            for (int x = 0; x <= lastX; x++) {
                for (int i = 0; i < length; i++) {
                    if (get(x + i, y) != playerType) {
                        continue horizontalCheck;
                    }
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    boolean hasVerticalStrike(final PlayerType playerType, final int length) {
        
        int lastY = ROWS - length;
        
        for (int x = 0; x < COLUMNS; x++) {
            verticalCheck:
            for (int y = 0; y <= lastY; y++) {
                for (int i = 0; i < length; i++) {
                    if (get(x, y + i) != playerType) {
                        continue verticalCheck;
                    }
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    boolean hasAscendingDiagonalStrike(final PlayerType playerType, 
                                       final int length) {
        
        final int lastX = COLUMNS - length;
        final int lastY = ROWS - length;
        
        for (int y = ROWS - 1; y > lastY; y--) {
            diagonalCheck:
            for (int x = 0; x <= lastX; x++) {
                for (int i = 0; i < length; i++) {
                    if (get(x + i, y - i) != playerType) {
                        continue diagonalCheck;
                    }
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    boolean hasDescendingDiagonalStrike(final PlayerType playerType, 
                                        final int length) {
        
        final int firstX = COLUMNS - length;
        final int lastY = ROWS - length;
        
        for (int y = ROWS - 1; y > lastY; y--) {
            diagonalCheck:
            for (int x = firstX; x < COLUMNS; x++) {
                for (int i = 0; i < length; i++) {
                    if (get(x - i, y - i) != playerType) {
                        continue diagonalCheck;
                    }
                }
                
                return true;
            }
        }
        
        return false;
    }
    
    private List<Point> tryLoadAscendingWinningPattern(
            final PlayerType playerType) {
        
        final int lastX = COLUMNS - VICTORY_LENGTH;
        final int lastY = ROWS - VICTORY_LENGTH;
        final List<Point> winningPattern = new ArrayList<>(VICTORY_LENGTH);
        
        for (int y = ROWS - 1; y > lastY; y--) {
            diagonalCheck:
            for (int x = 0; x <= lastX; x++) {
                for (int i = 0; i < VICTORY_LENGTH; i++) {
                    if (get(x + i, y - i) == playerType) {
                        winningPattern.add(new Point(x + i, y - i));
                        
                        if (winningPattern.size() == VICTORY_LENGTH) {
                            return winningPattern;
                        }
                    } else {
                        winningPattern.clear();
                        continue diagonalCheck;
                    }
                }
            }
        }
        
        return null;
    }
    
    private List<Point> tryLoadDescendingWinningPattern(
            final PlayerType playerType) {
        
        final int firstX = COLUMNS - VICTORY_LENGTH;
        final int lastY = ROWS - VICTORY_LENGTH;
        final List<Point> winningPattern = new ArrayList<>(VICTORY_LENGTH);
        
        for (int y = ROWS - 1; y > lastY; y--) {
            diagonalCheck:
            for (int x = firstX; x < COLUMNS; x++) {
                for (int i = 0; i < VICTORY_LENGTH; i++) {
                    if (get(x - i, y - i) == playerType) {
                        winningPattern.add(new Point(x - i, y - i));
                        
                        if (winningPattern.size() == VICTORY_LENGTH) {
                            return winningPattern;
                        }
                    } else {
                        winningPattern.clear();
                        continue diagonalCheck;
                    }
                }
            }
        }
        
        return null;
    }
    
    private List<Point> tryLoadHorizontalWinningPattern(
            final PlayerType playerType) {
        
        final int lastX = COLUMNS - VICTORY_LENGTH;
        final List<Point> winningPattern = new ArrayList<>(VICTORY_LENGTH);
        
        for (int y = ROWS - 1; y >= 0; y--) {
            horizontalCheck:
            for (int x = 0; x <= lastX; x++) {
                for (int i = 0; i < VICTORY_LENGTH; i++) {
                    if (get(x + i, y) == playerType) {
                        winningPattern.add(new Point(x + i, y));
                        
                        if (winningPattern.size() == VICTORY_LENGTH) {
                            return winningPattern;
                        }
                    } else {
                        winningPattern.clear();
                        continue horizontalCheck;
                    }
                }
            }
        }
        
        return null;
    }
    
    private List<Point> tryLoadVerticalWinningPattern(
            final PlayerType playerType) {
        
        final int lastY = ROWS - VICTORY_LENGTH;
        final List<Point> winningPattern = new ArrayList<>(VICTORY_LENGTH);
        
        for (int x = 0; x < COLUMNS; x++) {
            verticalCheck:
            for (int y = 0; y <= lastY; y++) {
                for (int i = 0; i < VICTORY_LENGTH; i++) {
                    if (get(x, y + i) == playerType) {
                        winningPattern.add(new Point(x, y + i));
                        
                        if (winningPattern.size() == VICTORY_LENGTH) {
                            return winningPattern;
                        }
                    } else {
                        winningPattern.clear();
                        continue verticalCheck;
                    }
                }
            }
        }
        
        return null;
    }
    
    private boolean notFullAtX(final int x) {
        return get(x, 0) == null;
    }
    
    private ConnectFourBoard dropAtX(final int x, final PlayerType playerType) {
        final ConnectFourBoard nextBoard = new ConnectFourBoard(this);
        
        for (int y = ROWS - 1; y >= 0; y--) {
            if (nextBoard.get(x, y) == null) {
                nextBoard.set(x, y, playerType);
                return nextBoard;
            }
        }
        
        throw new IllegalStateException("Should not get here.");
    }
    
    private static char getCellChar(final PlayerType playerType) {
        if (playerType == null) {
            return '.';
        }
        
        switch (playerType) {
            case MAXIMIZING_PLAYER:
                return 'O';
                
            case MINIMIZING_PLAYER:
                return 'X';
                
            default:
                throw new IllegalStateException("Should not get here.");
        }
    }
    
    private int nextXIndex(final int x, final int y) {
        return y * COLUMNS + x + 1;
    }
    
    private int nextYIndex(final int x, final int y) {
        return y * (COLUMNS + 1) + x;
    }
    
    private int nextAscendingDiagonalIndex(final int x, final int y) {
        return y * (COLUMNS - 1) + x - 1;
    }
    
    private int nextDescendingDiagonalIndex(final int x, final int y) {
        return y * (COLUMNS - 1) - x + 1;
    }
}

com.github.coderodde.game.zerosum.impl.ConnectFourAlphaBetaPruningSearchEngine.java:

package com.github.coderodde.game.zerosum.impl;

import com.github.coderodde.game.connect4.ConnectFourBoard;
import static com.github.coderodde.game.connect4.ConnectFourBoard.COLUMNS;
import com.github.coderodde.game.zerosum.PlayerType;
import com.github.coderodde.game.zerosum.HeuristicFunction;
import com.github.coderodde.game.zerosum.SearchEngine;

/**
 * This class implements the 
 * <a href="https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning">
 * Alpha-beta pruning</a> algorithm for making a move.
 * 
 * @version 1.0.0 (Jun 5, 2024)
 * @since 1.0.0 (Jun 5, 2024)
 */
public final class ConnectFourAlphaBetaPruningSearchEngine
        implements SearchEngine<ConnectFourBoard> {

    private ConnectFourBoard bestMoveState;
    private final HeuristicFunction<ConnectFourBoard> heuristicFunction;
    
    public ConnectFourAlphaBetaPruningSearchEngine(
            final HeuristicFunction<ConnectFourBoard> heuristicFunction) {
        this.heuristicFunction = heuristicFunction;
    }
    
    @Override
    public ConnectFourBoard search(final ConnectFourBoard root, 
                                   final int depth) {
        bestMoveState = null;
        
        alphaBetaRootImpl(root, 
                          depth,
                          Double.NEGATIVE_INFINITY, 
                          Double.POSITIVE_INFINITY);
        
        return bestMoveState;
    }
    
    private void alphaBetaRootImpl(final ConnectFourBoard root, 
                                   final int depth,
                                   double alpha,
                                   double beta) {
        
        // The first turn belongs to AI/the maximizing player:
        double tentativeValue = Double.NEGATIVE_INFINITY;
        
        for (int x = 0; x < COLUMNS; x++) {
            if (!root.makePly(x, PlayerType.MAXIMIZING_PLAYER)) {
                continue;
            }
            
            double value = alphaBetaImpl(root,
                                         depth - 1,
                                         Double.NEGATIVE_INFINITY,
                                         Double.POSITIVE_INFINITY,
                                         PlayerType.MINIMIZING_PLAYER);

            if (tentativeValue < value) {
                tentativeValue = value;
                bestMoveState = new ConnectFourBoard(root);
            }
            
            root.unmakePly(x);
            
            if (value > beta) {
                break;
            }
            
            alpha = Math.max(alpha, value);
        }
    }
    
    private double alphaBetaImpl(final ConnectFourBoard state,
                                 final int depth, 
                                 double alpha,
                                 double beta,
                                 final PlayerType playerType) {

        if (depth == 0 || state.isTerminal()) {
            return heuristicFunction.evaluate(state, depth);
        }
        
        if (playerType == PlayerType.MAXIMIZING_PLAYER) {
            double value = Double.NEGATIVE_INFINITY;
            
            for (int x = 0; x < COLUMNS; x++) {
                if (!state.makePly(x, PlayerType.MAXIMIZING_PLAYER)) {
                    continue;
                }
                
                value = Math.max(value, 
                                 alphaBetaImpl(state,
                                               depth - 1,
                                               alpha,
                                               beta,
                                               PlayerType.MINIMIZING_PLAYER));
                
                state.unmakePly(x);
                
                if (value > beta) {
                    break;
                }
                
                alpha = Math.max(alpha, value);
            }   
            
            return value;
        } else {
            double value = Double.POSITIVE_INFINITY;
            
            for (int x = 0; x < COLUMNS; x++) {
                if (!state.makePly(x, PlayerType.MINIMIZING_PLAYER)) {
                    continue;
                }
                
                value = Math.min(value,
                                 alphaBetaImpl(state,
                                               depth - 1,
                                               alpha,
                                               beta,
                                               PlayerType.MAXIMIZING_PLAYER));
                
                state.unmakePly(x);
                
                if (value < alpha) {
                    break;
                }
                
                beta = Math.min(beta, value);
            }
            
            return value;
        }          
    }   
}

com.github.coderodde.game.connect4fx.ConnectFourFX.java:

package com.github.coderodde.game.connect4fx;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

/**
 *
 * @author PotilasKone
 */
public class ConnectFourFX extends Application {
    
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("rodde's Connect 4");
        
        final StackPane root = new StackPane();
        final Canvas canvas = new ConnectFourFXCanvas();
        
        root.getChildren().add(canvas);
        stage.setScene(new Scene(root));
        
        stage.setResizable(false);
        stage.show();
    }
}

com.github.coderodde.game.connect4fx.ConnectFourFXCanvas.java:

package com.github.coderodde.game.connect4fx;

import com.github.coderodde.game.connect4.ConnectFourBoard;
import static com.github.coderodde.game.connect4.ConnectFourBoard.COLUMNS;
import static com.github.coderodde.game.connect4.ConnectFourBoard.ROWS;
import com.github.coderodde.game.connect4.ConnectFourHeuristicFunction;
import com.github.coderodde.game.zerosum.PlayerType;
import com.github.coderodde.game.zerosum.impl.ConnectFourAlphaBetaPruningSearchEngine;
import java.awt.Point;
import java.util.List;
import java.util.Optional;
import javafx.application.Platform;
import javafx.geometry.Rectangle2D;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.stage.Screen;

/**
 * This class implements the canvas for drawing the game board.
 * 
 * @version 1.0.0 (Jun 6, 2024)
 * @since 1.0.0 (Jun 6, 2024)
 */
public final class ConnectFourFXCanvas extends Canvas {
    
    private static final Color BACKGROUND_COLOR = Color.valueOf("#295c9e");
    private static final Color AIM_COLOR        = Color.valueOf("#167a0f");
    
    private static final Color HUMAN_PLAYER_CELL_COLOR = 
            Color.valueOf("#bfa730");
    
    private static final Color AI_PLAYER_CELL_COLOR = 
            Color.valueOf("#b33729");
    
    private static final Color WINNING_PATTERN_COLOR = Color.BLACK;
    
    private static final double CELL_LENGTH_SUBSTRACT = 10.0;
    private static final double RADIUS_SUBSTRACTION_DELTA = 10.0;
    private static final int CELL_Y_NOT_FOUND = -1;
    private static final int INITIAL_AIM_X = 3;
    private static final int SEARCH_DEPTH = 8;
    
    private final ConnectFourAlphaBetaPruningSearchEngine engine =
                    new ConnectFourAlphaBetaPruningSearchEngine(
                            new ConnectFourHeuristicFunction());
    
//            new ConnectFourAlphaBetaPruningSearchEngine(
//                    new ConnectFourHeuristicFunction());
    
    private int previousAimX = INITIAL_AIM_X;
    private ConnectFourBoard board = new ConnectFourBoard();
    private double cellLength;
    
    public ConnectFourFXCanvas() {
        setSize();
        paintBackground();
        
        this.addEventFilter(MouseEvent.MOUSE_MOVED, event -> {
            processMouseMoved(event);
        });
        
        this.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
            processMouseClicked(event);
        });
    }
    
    public void hit(final int x) {
        int y = getEmptyCellYForX(x);
        
        if (y == CELL_Y_NOT_FOUND) {
            // The column at X-index of x is full:
            return;
        }
        
        previousAimX = x;
        board.makePly(x, PlayerType.MINIMIZING_PLAYER);
        
        paintBackground();
        paintBoard();
        
        if (board.isTerminal()) {
            paintBackground();
            paintBoard();
            reportEndResult();
            return;
        }
        
        board = engine.search(board, SEARCH_DEPTH);
        
        if (board.isTerminal()) {
            paintBackground();
            paintBoard();
            reportEndResult();
            return;
        }
        
        paintBackground();
        paintBoard();
        
        y = getEmptyCellYForX(x);
        
        if (y != CELL_Y_NOT_FOUND) {
            paintCell(AIM_COLOR, x, y);
        }
    }
    
    private static Alert getEndResultReportAlert(final String contentText) {
        final Alert alert = new Alert(AlertType.CONFIRMATION);
        
        alert.getButtonTypes().clear();
        alert.getButtonTypes().addAll(ButtonType.YES, ButtonType.NO);
        alert.setTitle("Game over");
        alert.setHeaderText(contentText);
        alert.setContentText("Do you want to play again?");
        
        return alert;
    }
    
    private void processEndOfGameOptional(final Optional<ButtonType> optional) {
        if (optional.isPresent() && optional.get().equals(ButtonType.YES)) {
            board = new ConnectFourBoard();
        } else {
            Platform.exit();
            System.exit(0);
        }
    }
    
    private void reportEndResult() {
        if (board.isTie()) {
            
            final Optional<ButtonType> optional =
                    getEndResultReportAlert(
                            "It's a tie!")
                            .showAndWait();
            
            processEndOfGameOptional(optional);
            
        } else if (board.isWinningFor(PlayerType.MINIMIZING_PLAYER)) {
            
            colorWinningPattern(PlayerType.MINIMIZING_PLAYER,
                                board.getWinningPattern());
            
            final Optional<ButtonType> optional = 
                    getEndResultReportAlert(
                            "You won!")
                            .showAndWait();
            
            processEndOfGameOptional(optional);
            
        } else if (board.isWinningFor(PlayerType.MAXIMIZING_PLAYER)) {
            
            colorWinningPattern(PlayerType.MAXIMIZING_PLAYER,
                                board.getWinningPattern());
            
            final Optional<ButtonType> optional = 
                    getEndResultReportAlert(
                            "You lost!")
                            .showAndWait();
            
            processEndOfGameOptional(optional);
        }
    }
    
    private void colorWinningPattern(final PlayerType playerType,
                                     final List<Point> winningPattern) {
        
        for (final Point point : winningPattern) {
            paintCell(WINNING_PATTERN_COLOR, point.x, point.y);
            paintInnerCell(playerType, point.x, point.y);
        }
    }
    
    private void paintInnerCell(final PlayerType playerType,
                                final int x, 
                                final int y) {
        final double topLeftX = 
                cellLength * x + 2.0 * RADIUS_SUBSTRACTION_DELTA;
        
        final double topLeftY = 
                cellLength * y + 2.0 * RADIUS_SUBSTRACTION_DELTA;
        
        final double diameter = cellLength - 4.0 * RADIUS_SUBSTRACTION_DELTA;
        
        this.getGraphicsContext2D().setFill(getColor(playerType));
        this.getGraphicsContext2D().fillOval(topLeftX,
                                             topLeftY,
                                             diameter, 
                                             diameter);
    }
    
    private void processMouseClicked(final MouseEvent mouseEvent) {
        final double mouseX = mouseEvent.getSceneX();
        final int cellX = (int)(mouseX / cellLength);
        
        hit(cellX);
    }
    
    private void processMouseMoved(final MouseEvent mouseEvent) {
        final double mouseX = mouseEvent.getSceneX();
        final int cellX = (int)(mouseX / cellLength);
        final int emptyCellY = getEmptyCellYForX(cellX);
        
        if (cellX == previousAimX) {
            // Nothing changed.
            return;
        }
        
        previousAimX = cellX;
        
        if (emptyCellY == CELL_Y_NOT_FOUND) {
            return;
        }
        
        paintBackground();
        paintBoard();
        paintCellSelection(cellX, emptyCellY);
    }
    
    private void paintBoard() {
        for (int y = 0; y < ROWS; y++) {
            for (int x = 0; x < COLUMNS; x++) {
                final PlayerType playerType = board.get(x, y);
                final Color color = getColor(playerType);
                
                paintCell(color, x, y);
            }
        }
    }
    
    private static Color getColor(final PlayerType playerType) {
        if (playerType == null) {
            return Color.WHITE;
        } else switch (playerType) {
            case MAXIMIZING_PLAYER -> {
                return AI_PLAYER_CELL_COLOR;
            }
                
            case MINIMIZING_PLAYER -> {
                return HUMAN_PLAYER_CELL_COLOR;
            }
                
            default -> throw new IllegalStateException(
                        "Unknown PlayerType: " + playerType);
        }
    }
    
    private void paintCell(final Color color, 
                           final int x,
                           final int y) {
        
        final double topLeftX = cellLength * x + RADIUS_SUBSTRACTION_DELTA;
        final double topLeftY = cellLength * y + RADIUS_SUBSTRACTION_DELTA;
        final double innerWidth = cellLength - 2.0 * RADIUS_SUBSTRACTION_DELTA;
        
        this.getGraphicsContext2D().setFill(color);
        this.getGraphicsContext2D()
            .fillOval(topLeftX, 
                      topLeftY, 
                      innerWidth, 
                      innerWidth);
    }
    
    private void paintCellSelection(final int cellX, final int cellY) {
        final double topLeftX = cellLength * cellX + RADIUS_SUBSTRACTION_DELTA;
        final double topLeftY = cellLength * cellY + RADIUS_SUBSTRACTION_DELTA;
        final double innerWidth = cellLength - 2.0 * RADIUS_SUBSTRACTION_DELTA;
        
        this.getGraphicsContext2D().setFill(AIM_COLOR);
        this.getGraphicsContext2D()
                .fillOval(
                        topLeftX, 
                        topLeftY,
                        innerWidth,
                        innerWidth);
    }
    
    private int getEmptyCellYForX(final int cellX) {
        for (int y = ROWS - 1; y >= 0; y--) {
            if (board.get(cellX, y) == null) {
                return y;
            }
        }
        
        return CELL_Y_NOT_FOUND;
    }
    
    private void setSize() {
        final Rectangle2D primaryScreenBounds =
                Screen.getPrimary().getVisualBounds();
        
        final double verticalLength =  
                primaryScreenBounds.getHeight() / ConnectFourBoard.ROWS;
        
        final double horizontalLength = 
                primaryScreenBounds.getWidth() / ConnectFourBoard.COLUMNS;
        
        final double selectedLength = Math.min(verticalLength,
                                               horizontalLength) 
                                    - CELL_LENGTH_SUBSTRACT;
        
        this.cellLength = selectedLength;
        
        this.setWidth(ConnectFourBoard.COLUMNS * selectedLength);
        this.setHeight(ConnectFourBoard.ROWS * selectedLength);
    }
    
    private void paintBackground() {
        final double width = this.getWidth();
        final double height = this.getHeight();
        
        this.getGraphicsContext2D().setFill(BACKGROUND_COLOR);
        this.getGraphicsContext2D().fillRect(0.0, 0.0, width, height);
    }
}

Game play

It may look like this:

Playing Connect 4 against AI

Critique request

As always, I would like to hear any commentary on how to improve my work.

\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

The functions has*Strike() are basically the same function, but with different starting points and different strides through the boardData.

If we create a general hasStrike() that accepts these differences as parameters:

    // untested
    boolean hasStrike(final PlayerType playerType, final int length,
                      final int x0; final int x1;
                      final int y0; final int y1;
                      final int stride) {

        for (int y = y0;  y < y1;  ++y) {
            for (int x = x0;  x < x1;  ++x) {
                // have we got a line starting here?
                final int p = y * COLUMNS + x;
                int count = 0;
                for (int i = 0; i < length; i++) {
                    if (boardData[p] == playerType) {
                        ++count;
                    }
                    p += stride;
                }
                if (count == length) {
                    return true;
                }
            }
        }

        return false;
    }

Then the other functions become simple calls to it:

    boolean hasHorizontalStrike(final PlayerType playerType, final int length) {
        return hasStrike(playerType, length,
                         0, COLUMNS - length,
                         0, ROWS,
                         1);
    }

    boolean hasVerticalStrike(final PlayerType playerType, final int length) {
        return hasStrike(playerType, length,
                         0, COLUMNS,
                         0, ROWS - length,
                         COLUMNS);
    }

    boolean hasAscendingDiagonalStrike(final PlayerType playerType, final int length) {
        return hasStrike(playerType, length,
                         0, COLUMNS - length,
                         0, ROWS - length,
                         COLUMNS + 1);
    }

    boolean hasDescendingDiagonalStrike(final PlayerType playerType, final int length) {
        return hasStrike(playerType, length,
                         length, COLUMNS,
                         0, ROWS - length,
                         COLUMNS - 1);
    }

Or we could even compute the start and end points in the common function:

    // UNTESTED
    boolean hasStrike(final PlayerType playerType, final int length,
                      final strideX, final strideY) {

        final int x0 = strideX < 0 ? length : 0;
        final int x1 = COLUMNS - (strideX > 0 ? length : 0);
        final int y0 = 0;
        final int y1 = ROWS - (strideY > 0 ? length : 0);
        final int stride = strideX + COLUMNS * strideY;

        for (int y = y0;  y < y1;  ++y) {
            for (int x = x0;  x < x1;  ++x) {
                // have we got a line starting here?
                final int p = x + COLUMNS * y;
                int count = 0;
                for (int i = 0; i < length; i++) {
                    if (boardData[p] == playerType && ++count == length) {
                        return true;
                    }
                    p += stride;
                }
            }
        }

        return false;
    }

    boolean hasHorizontalStrike(final PlayerType playerType, final int length) {
        return hasStrike(playerType, length, 1, 0);
    }

    boolean hasVerticalStrike(final PlayerType playerType, final int length) {
        return hasStrike(playerType, length, 0, 1);
    }

    boolean hasAscendingDiagonalStrike(final PlayerType playerType, final int length) {
        return hasStrike(playerType, length, 1, 1);
    }

    boolean hasDescendingDiagonalStrike(final PlayerType playerType, final int length) {
        return hasStrike(playerType, length, 1, -1);
    }

In any case, ensure the functions are covered by unit tests before refactoring.

There are other function families that can be reduced in a similar manner.

Since the next*Index() functions are private and unused, they can be removed.

\$\endgroup\$
2
  • \$\begingroup\$ +1, but looks like overkill. \$\endgroup\$
    – coderodde
    Commented Jun 9 at 13:41
  • 2
    \$\begingroup\$ Maybe it's a matter of taste. I don't like having to maintain multiple versions of essentially the same logic. The great thing about advice, of course, is that you're not obliged to follow it! \$\endgroup\$ Commented Jun 9 at 13:43

Not the answer you're looking for? Browse other questions tagged or ask your own question.