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.

asked Jun 9, 2024 at 4:45
\$\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.

answered Jun 9, 2024 at 6:11
\$\endgroup\$
1
  • 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, 2024 at 13:43

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.