I recently started learning Java and decided to create a project to practice. I developed a console-based tic-tac-toe game. I also wrote tests for some of the classes, not because they were necessary, but because I wanted to practice writing them.
I would love to hear your thoughts on the project. What can be improved, simplified, or made more efficient?
Here’s the link to the GitHub repository: https://github.com/qwert312/Tic-Tac-Toe-Console-game.git
Code (does not include test classes):
//Class Board3x3
import java.util.Arrays;
import java.util.stream.IntStream;
public class Board3x3 {
private String[][] board = new String[3][3];
public Board3x3() {
resetBoard();
}
public String[][] copyOfBoard() {
return Arrays.stream(board).map(String[]::clone).toArray(String[][]::new);
}
public void resetBoard() {
board = Arrays.stream(board)
.map(row -> row = new String[]{" ", " ", " "})
.toArray(String[][]::new);
}
public void setSymbol(String symbol, int row, int col) throws IllegalArgumentException {
if ((row >= board.length || row < 0 || col >= board.length || col < 0)
|| !board[row][col].equals(" "))
throw new IllegalArgumentException();
board[row][col] = symbol;
}
public boolean isFull() {
return Arrays.stream(board).allMatch(row -> Arrays.stream(row).noneMatch(" "::equals));
}
public String checkWinnerSymbol() {
String checkRowsResult = checkRows();
if (!checkRowsResult.equals(" "))
return checkRowsResult;
String checkColumnsResult = checkColumns();
if (!checkColumnsResult.equals(" "))
return checkColumnsResult;
String checkDiagonalResult = checkDiagonals();
if (!checkDiagonalResult.equals(" "))
return checkDiagonalResult;
return " ";
}
private String checkRows() {
return Arrays.stream(board)
.map(row -> checkLine(Arrays.stream(row).toArray(String[]::new)))
.filter(winner -> !winner.equals(" ")).findFirst().orElse(" ");
}
private String checkColumns() {
return IntStream
.range(0, board.length)
.mapToObj(column ->
checkLine(Arrays.stream(board)
.map(row -> row[column])
.toArray(String[]::new)))
.filter(winner -> !winner.equals(" ")).findFirst().orElse(" ");
}
private String checkDiagonals() {
return IntStream
.range(0, 2)
.mapToObj(diagonal ->
checkLine(IntStream.range(0, board.length).mapToObj((ind) -> {
if (diagonal == 0)
return board[ind][ind];
else
return board[ind][board.length - 1 - ind];
})
.toArray(String[]::new)))
.filter(winner -> !winner.equals(" ")).findFirst().orElse(" ");
}
private String checkLine(String[] line) {
long xCount = Arrays.stream(line).filter("X"::equals).count();
long oCount = Arrays.stream(line).filter("O"::equals).count();
if (xCount == 3) return "X";
if (oCount == 3) return "O";
return " ";
}
public String toString() {
StringBuilder stringBoard = new StringBuilder();
for (int row = 0; row < board.length; row++) {
stringBoard.append(String.join("|", board[row]));
if (row != board.length - 1)
stringBoard.append("\n-+-+-\n");
}
return stringBoard.toString();
}
}
//Class Player
public class Player {
private String symbol;
public Player(String symbol) {
this.symbol = symbol;
}
public String getSymbol() {
return symbol;
}
public void makeMove(Board3x3 board, int row, int col) throws IllegalArgumentException {
board.setSymbol(symbol, row, col);
}
public String toString() {
return "Player (" + symbol + ")";
}
}
//Class AI
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;
public class AI {
private String symbol;
public AI(String symbol) {
this.symbol = symbol;
}
public String getSymbol() {
return symbol;
}
public List<Integer> makeMove(Board3x3 board) {
List<Integer> randomCoordinates = getRandomCoordinates(board);
board.setSymbol(symbol, randomCoordinates.get(0), randomCoordinates.get(1));
return randomCoordinates;
}
private List<Integer> getRandomCoordinates(Board3x3 board) {
Random rand = new Random();
List<List<Integer>> possibleCoordinates = new ArrayList<>();
String[][] boardCopy = board.copyOfBoard();
IntStream.range(0, boardCopy.length)
.boxed()
.flatMap(rowInd ->
IntStream
.range(0, boardCopy[rowInd].length)
.filter(colInd -> boardCopy[rowInd][colInd].equals(" "))
.mapToObj(colInd -> List.of(rowInd, colInd)))
.forEach(possibleCoordinates::add);
return possibleCoordinates.get(rand.nextInt(0, possibleCoordinates.size()));
}
public String toString() {
return "AI (" + symbol + ")";
}
}
//Class GameManager
import java.util.List;
import java.util.Scanner;
public class GameManager {
private final Scanner scan;
private Player player;
private AI ai;
private final Board3x3 board = new Board3x3();
public GameManager(Scanner scanner) {
this.scan = scanner;
}
public void start() {
System.out.println("TicTacToe console game");
typePlayerSymbol();
System.out.println();
while (true) {
System.out.println("Start! Type coordinates on your turn in this format: row,column\n");
playGame();
System.out.print("Next match! (type n to stop) ");
String answer = scan.nextLine();
if (answer.equals("n")) break;
System.out.println();
board.resetBoard();
}
}
private void typePlayerSymbol() {
while (true) {
System.out.print("Choose player symbol (X/O): ");
String selectedSymbol = scan.nextLine().trim().toUpperCase();
try {
if (selectedSymbol.equals("X")) {
player = new Player(selectedSymbol);
ai = new AI("O");
break;
} else if (selectedSymbol.equals("O")) {
player = new Player(selectedSymbol);
ai = new AI("X");
break;
}
throw new IllegalArgumentException();
} catch (IllegalArgumentException e) {
System.out.println("Illegal symbol! Try again");
}
}
}
private void playGame() {
while (true) {
for (int i = 0; i < 2; i++) {
if ((player.getSymbol().equals("X") && i == 0)
|| (player.getSymbol().equals("O") && i == 1))
playerTurn();
else
aiTurn();
System.out.println(board.toString() + "\n");
String winnerSymbol = board.checkWinnerSymbol();
if (!winnerSymbol.equals(" ")) {
String winner = player.getSymbol().equals(winnerSymbol)
? player.toString() : ai.toString();
System.out.println("Winner winner chicken dinner. " + winner + " won!");
return;
}
if (board.isFull()) {
System.out.println("Board is full. Draw!");
return;
}
}
}
}
private void playerTurn() {
while (true) {
System.out.println(player + " turn");
System.out.print("Coordinates: ");
try {
String[] coordinates = scan.nextLine().split(",");
int row = Integer.parseInt(coordinates[0]) - 1;
int column = Integer.parseInt(coordinates[1]) - 1;
player.makeMove(board, row, column);
break;
} catch (Exception e) {
System.out.println("Illegal coordinates! Try again");
}
}
}
private void aiTurn() {
System.out.println(ai + " turn");
System.out.print("Coordinates: ");
List<Integer> coordinates = ai.makeMove(board);
System.out.println((coordinates.get(0) + 1) + "," + (coordinates.get(1) + 1));
}
}
//Class Program
import java.util.Scanner;
public class Program {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
GameManager gameManager = new GameManager(scan);
gameManager.start();
}
}
GameManager.start
should rather be calledrun
,runLoop
or smth like that -start
conveys an undesired meaning of "there's astop
to be called later", similarly toopen
that mandates correspondingclose
. \$\endgroup\$String
? It seems these strings are always 1 character long, so why notchar[3][3]
? \$\endgroup\$