I am trying to write code to determine a win in tic-tac-toe game. In this question, I am not focusing on the whole game, I am only interested in the question of how to determine that the game ended with the victory of one of the players. So I made a synthetic example to test different approaches.
My guess is that a winning line on the board can only appear after the current player has made his move. Therefore, it makes no sense to check all the lines on the board. I only consider those that include the last move made. I assume that the board can be of any size (but only square) and the winning line has the same size. Thus, it is one of the rows, or one of the columns, or one of the two diagonals.
After several attempts, I ended up with the code below:
import java.util.List;
import java.util.function.Predicate;
enum Cell {
X,
O,
B // blank cell
}
public class Board {
private final int size = 3;
private final Cell[][] board;
public Board(Cell[][] board) {
this.board = board;
}
private boolean isLineCompleted(Predicate<Integer> predicate) {
boolean result = true;
int i = 0;
while (result && i < size) {
result = predicate.test(i);
i++;
}
return result;
}
public boolean hasWinningLine(int row, int col) {
if (board[row][col] == Cell.B) return false;
Cell base = board[row][col];
List<Predicate<Integer>> checks = List.of(
i -> board[row][i] == base, //columns
i -> board[i][col] == base, //rows
i -> board[i][i] == base, //top-down diagonal
i -> board[size - i - 1][i] == base //down-top diagonal
);
boolean lineCompleted = false;
for (var check : checks) {
if (!lineCompleted) {
lineCompleted = isLineCompleted(check);
}
}
return lineCompleted;
}
}
And I wrote some tests that use this code something like this:
Cell[][] xWins = {
{Cell.X, Cell.O, Cell.X},
{Cell.O, Cell.X, Cell.O},
{Cell.X, Cell.B, Cell.B}};
board = new Board(xWins);
AssertTrue(board.hasWinningLine(0, 2));
I have several questions:
How easy is this code to read and understand? How can you improve it in this sense?
I try not to make unnecessary checks. The algorithm should return false as soon as it finds an unsuitable cell. Should I handle the loop like this
for (var check : checks) {
if (!lineCompleted) {
lineCompleted = isLineCompleted(check);
} else break; // That's better?
}
Is it possible to rewrite this loop using a stream API?
- When checking each line, this code can compare the cell to itself. How can i avoid this without complicating the code? Should I even think about it?
I am new to programming and would appreciate any advice that would help me code better.
EDITED: The predicate checking loop can be rewritten as
return checks.stream()
.map(this::isLineCompleted)
.filter(b -> b == true)
.findFirst()
.orElse(false);
What are the disadvantages of this approach?