I have this GitHub repository. It implements the command-line version of the Connect Four game. The AI bot is implemented via Alpha-beta pruning.
Code
com.github.coderodde.game.zerosum.impl.AlphaBetaPruningSearchEngine.java:
package com.github.coderodde.game.zerosum.impl;
import com.github.coderodde.game.zerosum.PlayerType;
import com.github.coderodde.game.zerosum.GameState;
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.
*
* @param <S> the game state type.
*
* @version 1.0.0 (Jun 5, 2024)
* @since 1.0.0 (Jun 5, 2024)
*/
public final class AlphaBetaPruningSearchEngine<S extends GameState<S>>
implements SearchEngine<S> {
private S bestMoveState;
private final HeuristicFunction<S> heuristicFunction;
public AlphaBetaPruningSearchEngine(
final HeuristicFunction<S> heuristicFunction,
final double minimizingPlayerVictoryScore,
final double maximizingPlayerVictoryScore) {
this.heuristicFunction = heuristicFunction;
}
@Override
public S search(final S root, final int depth) {
bestMoveState = null;
alphaBetaRootImpl(root,
depth,
Double.NEGATIVE_INFINITY,
Double.POSITIVE_INFINITY);
return bestMoveState;
}
private void alphaBetaRootImpl(final S root,
final int depth,
double alpha,
double beta) {
bestMoveState = null;
// The first turn belongs to AI/the maximizing player:
double tentativeValue = Double.NEGATIVE_INFINITY;
for (final S child : root.expand(PlayerType.MAXIMIZING_PLAYER)) {
double value = alphaBetaImpl(child,
depth - 1,
Double.NEGATIVE_INFINITY,
Double.POSITIVE_INFINITY,
PlayerType.MINIMIZING_PLAYER);
if (tentativeValue < value) {
tentativeValue = value;
bestMoveState = child;
}
if (value > beta) {
break;
}
alpha = Math.max(alpha, value);
}
}
private double alphaBetaImpl(final S 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 (final S child : state.expand(playerType)) {
value = Math.max(
value,
alphaBetaImpl(
child,
depth - 1,
alpha,
beta,
PlayerType.MINIMIZING_PLAYER));
if (value > beta) {
break;
}
alpha = Math.max(alpha, value);
}
return value;
} else {
double value = Double.POSITIVE_INFINITY;
for (final S child : state.expand(playerType)) {
value = Math.min(
value,
alphaBetaImpl(
child,
depth - 1,
alpha,
beta,
PlayerType.MAXIMIZING_PLAYER));
if (value < alpha) {
break;
}
beta = Math.min(beta, value);
}
return value;
}
}
}
com.github.coderodde.game.connect4.ConnectFour.java:
package com.github.coderodde.game.connect4;
import com.github.coderodde.game.zerosum.HeuristicFunction;
import com.github.coderodde.game.zerosum.PlayerType;
import com.github.coderodde.game.zerosum.impl.AlphaBetaPruningSearchEngine;
import java.util.Scanner;
import com.github.coderodde.game.zerosum.SearchEngine;
/**
* This class implements the REPL for playring Connect Four against an AI bot.
*
* @version 1.0.0 (Jun 5, 2024)
* @since 1.0.0 (Jun 5, 2024)
*/
public class ConnectFour {
private static final double MINIMUM_PLAYER_VICTORY_SCORE = +1E6;
private static final double MAXIMUM_PLAYER_VICTORY_SCORE = -1E6;
private static final int DEFAULT_DEPTH = 8;
private static final int MINIMUM_DEPTH = 1;
public static void main(String[] args) {
final int depth = parseDepth(args);
System.out.printf(">>> Using search depth: %d.\n", depth);
final Scanner scanner = new Scanner(System.in);
final HeuristicFunction<ConnectFourBoard> heuristicFunction =
new ConnectFourHeuristicFunction();
final SearchEngine<ConnectFourBoard> bot =
new AlphaBetaPruningSearchEngine<>(
heuristicFunction,
MINIMUM_PLAYER_VICTORY_SCORE,
MAXIMUM_PLAYER_VICTORY_SCORE);
ConnectFourBoard currentBoard = new ConnectFourBoard();
while (true) {
System.out.println(currentBoard);
final String command = scanner.next().trim();
if (command.equals("quit") || command.equals("q")) {
return;
}
int column;
try {
column = Integer.parseInt(command);
} catch (final NumberFormatException ex) {
System.out.printf(">>> Command \"%s\" not recognized.\n",
command);
continue;
}
if (0 < column && column <= ConnectFourBoard.COLUMNS) {
column--; // 1-based indexing to 0-based.
currentBoard =
currentBoard.makePly(
column,
PlayerType.MINIMIZING_PLAYER);
long startTime = System.currentTimeMillis();
final ConnectFourBoard nextConnectFourBoard =
bot.search(currentBoard, depth);
long endTime = System.currentTimeMillis();
System.out.printf(">>> AI took %d milliseconds.\n",
endTime - startTime);
if (nextConnectFourBoard != null) {
currentBoard = nextConnectFourBoard;
}
if (currentBoard.isWinningFor(PlayerType.MINIMIZING_PLAYER)) {
System.out.println(">>> You won!");
System.out.println(currentBoard);
return;
}
if (currentBoard.isWinningFor(PlayerType.MAXIMIZING_PLAYER)) {
System.out.println(">>> AI won!");
System.out.println(currentBoard);
return;
}
if (currentBoard.isTie()) {
System.out.println(">>> It's a tie!");
System.out.println(currentBoard);
return;
}
System.out.println(">>> Board after AI's move:");
}
}
}
/**
* Attempts to read the search depth from the {@code args}. If not present,
* returns the default depth.
*
* @param args the array of command line arguments.
*
* @return the search depth to use.
*/
private static int parseDepth(final String[] args) {
if (args.length == 0) {
return DEFAULT_DEPTH;
}
int depth;
try {
depth = Integer.parseInt(args[0]);
} catch (final NumberFormatException ex) {
return DEFAULT_DEPTH;
}
return Math.max(depth, MINIMUM_DEPTH);
}
}
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.util.ArrayList;
import java.util.Arrays;
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> {
static final int ROWS = 6;
static final int COLUMNS = 7;
static final int VICTORY_LENGTH = 4;
final PlayerType[][] boardData = new PlayerType[ROWS][COLUMNS];
public ConnectFourBoard(final PlayerType[][] boardData) {
for (int y = 0; y < ROWS; y++) {
this.boardData[y] = Arrays.copyOf(boardData[y], 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(boardData[y][x]));
}
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) {
if (isTerminalHorizontal(playerType)) {
return true;
}
if (isTerminalVertical(playerType)) {
return true;
}
if (isTerminalAscendingDiagonal(playerType)) {
return true;
}
if (isTerminalDescendingDiagonal(playerType)) {
return true;
}
return false;
}
@Override
public boolean isTie() {
for (int x = 0; x < COLUMNS; x++) {
if (boardData[0][x] == null) {
return false;
}
}
return true;
}
public ConnectFourBoard makePly(final int x, final PlayerType playerType) {
return dropAtX(x, playerType);
}
private boolean isTerminalHorizontal(final PlayerType playerType) {
int lastX = COLUMNS - VICTORY_LENGTH;
for (int y = ROWS - 1; y >= 0; y--) {
horizontalCheck:
for (int x = 0; x <= lastX; x++) {
for (int i = x; i < x + VICTORY_LENGTH; i++) {
if (boardData[y][i] != playerType) {
continue horizontalCheck;
}
}
return true;
}
}
return false;
}
private boolean isTerminalVertical(final PlayerType playerType) {
int lastY = ROWS - VICTORY_LENGTH;
for (int x = 0; x < COLUMNS; x++) {
verticalCheck:
for (int y = 0; y <= lastY; y++) {
for (int i = y; i < y + VICTORY_LENGTH; i++) {
if (boardData[i][x] != playerType) {
continue verticalCheck;
}
}
return true;
}
}
return false;
}
private boolean isTerminalAscendingDiagonal(final PlayerType playerType) {
int lastX = COLUMNS - VICTORY_LENGTH;
int lastY = ROWS - VICTORY_LENGTH + 1;
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 (boardData[y - i][x + i] != playerType) {
continue diagonalCheck;
}
}
return true;
}
}
return false;
}
private boolean isTerminalDescendingDiagonal(final PlayerType playerType) {
int lastX = VICTORY_LENGTH - 1;
int lastY = ROWS - VICTORY_LENGTH + 1;
for (int y = ROWS - 1; y >= lastY; y--) {
diagonalCheck:
for (int x = lastX; x < COLUMNS; x++) {
for (int i = 0; i < VICTORY_LENGTH; i++) {
if (boardData[y - i][x - i] != playerType) {
continue diagonalCheck;
}
}
return true;
}
}
return false;
}
private boolean notFullAtX(final int x) {
return boardData[0][x] == null;
}
private ConnectFourBoard dropAtX(final int x, final PlayerType playerType) {
final ConnectFourBoard nextBoard = new ConnectFourBoard();
for (int y = 0; y < ROWS; y++) {
nextBoard.boardData[y] = Arrays.copyOf(this.boardData[y], COLUMNS);
}
for (int y = ROWS - 1; y >= 0; y--) {
if (nextBoard.boardData[y][x] == null) {
nextBoard.boardData[y][x] = 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.");
}
}
}
com.github.coderodde.game.connect4.ConnectFourHeuristicFunction.java:
package com.github.coderodde.game.connect4;
import static com.github.coderodde.game.connect4.ConnectFourBoard.COLUMNS;
import static com.github.coderodde.game.connect4.ConnectFourBoard.ROWS;
import com.github.coderodde.game.zerosum.HeuristicFunction;
import com.github.coderodde.game.zerosum.PlayerType;
/**
* This class implements a heuristic function for the Connect Four game.
*
* @version 1.0.0 (Jun 5, 2024)
* @since 1.0.0 (Jun 5, 2024)
*/
public final class ConnectFourHeuristicFunction
implements HeuristicFunction<ConnectFourBoard> {
private static final double TWO_BLOCKS_SCORE = 1.0;
private static final double THREE_BLOCKS_SCORE = 10.0;
private static final double MINIMIZING_PLAYER_VICTORY_SCORE = -10E6;
private static final double MAXIMIZING_PLAYER_VICTORY_SCORE = +10E6;
@Override
public double evaluate(final ConnectFourBoard state, final int depth) {
if (state.isWinningFor(PlayerType.MINIMIZING_PLAYER)) {
return MINIMIZING_PLAYER_VICTORY_SCORE + depth;
}
if (state.isWinningFor(PlayerType.MAXIMIZING_PLAYER)) {
return MAXIMIZING_PLAYER_VICTORY_SCORE - depth;
}
return evaluate2(state) + evaluate3(state);
}
private static double evaluate2(final ConnectFourBoard state) {
return evaluate2Horizontal(state) +
evaluate2Vertical(state) +
evaluate2Ascending(state) +
evaluate2Descending(state);
}
private static double evaluate3(final ConnectFourBoard state) {
return evaluate3Horizontal(state) +
evaluate3Vertical(state) +
evaluate3Ascending(state) +
evaluate3Descending(state);
}
private static double evaluate2Horizontal(final ConnectFourBoard state) {
double sum = 0.0;
for (int y = 0; y < ROWS; y++) {
for (int x = 0; x < COLUMNS - 1; x++) {
if (state.boardData[y][x] == PlayerType.MAXIMIZING_PLAYER &&
state.boardData[y][x + 1] == PlayerType.MAXIMIZING_PLAYER) {
sum += TWO_BLOCKS_SCORE;
} else if (state.boardData[y][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y][x + 1]
== PlayerType.MINIMIZING_PLAYER) {
sum -= TWO_BLOCKS_SCORE;
}
}
}
return sum;
}
private static double evaluate2Vertical(final ConnectFourBoard state) {
double sum = 0.0;
for (int y = 0; y < ROWS - 1; y++) {
for (int x = 0; x < COLUMNS; x++) {
if (state.boardData[y][x] == PlayerType.MAXIMIZING_PLAYER &&
state.boardData[y + 1][x] == PlayerType.MAXIMIZING_PLAYER) {
sum += TWO_BLOCKS_SCORE;
} else if (state.boardData[y][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y + 1][x]
== PlayerType.MINIMIZING_PLAYER) {
sum -= TWO_BLOCKS_SCORE;
}
}
}
return sum;
}
private static double evaluate2Ascending(final ConnectFourBoard state) {
double sum = 0.0;
for (int y = ROWS - 1; y > 0; y--) {
for (int x = 0; x < COLUMNS - 1; x++) {
if (state.boardData[y][x]
== PlayerType.MAXIMIZING_PLAYER
&&
state.boardData[y - 1][x + 1]
== PlayerType.MAXIMIZING_PLAYER) {
sum += TWO_BLOCKS_SCORE;
} else if (state.boardData[y][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y - 1][x + 1]
== PlayerType.MINIMIZING_PLAYER) {
sum -= TWO_BLOCKS_SCORE;
}
}
}
return sum;
}
private static double evaluate2Descending(final ConnectFourBoard state) {
double sum = 0.0;
for (int y = ROWS - 1; y > 0; y--) {
for (int x = 1; x < COLUMNS; x++) {
if (state.boardData[y][x]
== PlayerType.MAXIMIZING_PLAYER
&&
state.boardData[y - 1][x - 1]
== PlayerType.MAXIMIZING_PLAYER) {
sum += TWO_BLOCKS_SCORE;
} else if (state.boardData[y][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y - 1][x - 1]
== PlayerType.MINIMIZING_PLAYER) {
sum -= TWO_BLOCKS_SCORE;
}
}
}
return sum;
}
private static double evaluate3Horizontal(final ConnectFourBoard state) {
double sum = 0.0;
for (int y = 0; y < ROWS; y++) {
for (int x = 0; x < COLUMNS - 2; x++) {
if (state.boardData[y][x] == PlayerType.MAXIMIZING_PLAYER &&
state.boardData[y][x + 1] == PlayerType.MAXIMIZING_PLAYER &&
state.boardData[y][x + 2] == PlayerType.MAXIMIZING_PLAYER) {
sum += THREE_BLOCKS_SCORE;
} else if (state.boardData[y][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y][x + 1]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y][x + 2]
== PlayerType.MINIMIZING_PLAYER) {
sum -= THREE_BLOCKS_SCORE;
}
}
}
return sum;
}
private static double evaluate3Vertical(final ConnectFourBoard state) {
double sum = 0.0;
for (int y = 0; y < ROWS - 2; y++) {
for (int x = 0; x < COLUMNS; x++) {
if (state.boardData[y][x] == PlayerType.MAXIMIZING_PLAYER &&
state.boardData[y + 1][x] == PlayerType.MAXIMIZING_PLAYER &&
state.boardData[y + 2][x] == PlayerType.MAXIMIZING_PLAYER) {
sum += THREE_BLOCKS_SCORE;
} else if (state.boardData[y][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y + 1][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y + 2][x]
== PlayerType.MINIMIZING_PLAYER) {
sum -= THREE_BLOCKS_SCORE;
}
}
}
return sum;
}
private static double evaluate3Ascending(final ConnectFourBoard state) {
double sum = 0.0;
for (int y = ROWS - 1; y > 1; y--) {
for (int x = 0; x < COLUMNS - 2; x++) {
if (state.boardData[y][x]
== PlayerType.MAXIMIZING_PLAYER
&&
state.boardData[y - 1][x + 1]
== PlayerType.MAXIMIZING_PLAYER
&&
state.boardData[y - 2][x + 2]
== PlayerType.MAXIMIZING_PLAYER) {
sum += THREE_BLOCKS_SCORE;
} else if (state.boardData[y][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y - 1][x + 1]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y - 2][x + 2]
== PlayerType.MINIMIZING_PLAYER) {
sum -= THREE_BLOCKS_SCORE;
}
}
}
return sum;
}
private static double evaluate3Descending(final ConnectFourBoard state) {
double sum = 0.0;
for (int y = ROWS - 1; y > 1; y--) {
for (int x = 2; x < COLUMNS; x++) {
if (state.boardData[y][x]
== PlayerType.MAXIMIZING_PLAYER
&&
state.boardData[y - 1][x - 1]
== PlayerType.MAXIMIZING_PLAYER
&&
state.boardData[y - 2][x - 2]
== PlayerType.MAXIMIZING_PLAYER) {
sum += THREE_BLOCKS_SCORE;
} else if (state.boardData[y][x]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y - 1][x - 1]
== PlayerType.MINIMIZING_PLAYER
&&
state.boardData[y - 2][x - 2]
== PlayerType.MINIMIZING_PLAYER) {
sum -= THREE_BLOCKS_SCORE;
}
}
}
return sum;
}
}
My game experience
My AI seems to be rather competitive:
>>> Using search depth: 8.
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
4
>>> AI took 1008 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|O|.|X|.|.|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
4
>>> AI took 787 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|O|.|X|.|.|.|
|.|O|.|X|.|.|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
4
>>> AI took 593 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|O|.|.|.|
|.|.|.|X|.|.|.|
|.|O|.|X|.|.|.|
|.|O|.|X|.|.|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
6
>>> AI took 672 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|.|.|O|.|.|.|
|.|O|.|X|.|.|.|
|.|O|.|X|.|.|.|
|.|O|.|X|.|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
2
>>> AI took 963 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|X|.|O|.|.|.|
|.|O|.|X|.|.|.|
|.|O|.|X|.|.|.|
|.|O|.|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
5
>>> AI took 1185 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|X|.|O|.|.|.|
|.|O|.|X|O|.|.|
|.|O|.|X|X|.|.|
|.|O|.|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
6
>>> AI took 1363 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|X|.|O|.|.|.|
|.|O|.|X|O|O|.|
|.|O|.|X|X|X|.|
|.|O|.|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
5
>>> AI took 745 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|X|.|O|X|.|.|
|.|O|.|X|O|O|.|
|.|O|.|X|X|X|.|
|O|O|.|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
3
>>> AI took 417 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|X|.|O|X|.|.|
|.|O|.|X|O|O|.|
|.|O|O|X|X|X|.|
|O|O|X|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
3
>>> AI took 460 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|.|.|.|.|.|
|.|X|O|O|X|.|.|
|.|O|X|X|O|O|.|
|.|O|O|X|X|X|.|
|O|O|X|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
4
>>> AI took 523 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|.|O|X|.|.|.|
|.|X|O|O|X|.|.|
|.|O|X|X|O|O|.|
|.|O|O|X|X|X|.|
|O|O|X|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
1
>>> AI took 188 milliseconds.
>>> Board after AI's move:
|.|.|.|.|.|.|.|
|.|O|O|X|.|.|.|
|.|X|O|O|X|.|.|
|.|O|X|X|O|O|.|
|X|O|O|X|X|X|.|
|O|O|X|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
3
>>> AI took 26 milliseconds.
>>> AI won!
|.|O|X|.|.|.|.|
|.|O|O|X|.|.|.|
|.|X|O|O|X|.|.|
|.|O|X|X|O|O|.|
|X|O|O|X|X|X|.|
|O|O|X|X|O|X|.|
+-+-+-+-+-+-+-+
1 2 3 4 5 6 7
Critique request
Please, tell me anything that comes to mind. Especially, I would like to hear whether my Alpha-beta pruning is correctly implemented. Also, perhaps you could comment on the heuristic function.
makePly
a typo formakePlay
? \$\endgroup\$|X|X|.|X|.|.|.|
is just as much of a threat as|X|X|X|.|.|.|.|
. Specifically, I iterate through all sequences of 4 spots, and assign them a score of 0 if they contain pieces from both players (since that means neither of them can score there), or a positive/negative score for my/opponent's pieces, with magnitude exponentially relative to the number of pieces. \$\endgroup\$