mancala/apps/backend-quarkus/src/main/java/app/playmancala/engine/Board.java

222 lines
7.4 KiB
Java
Raw Normal View History

package app.playmancala.engine;
import java.util.ArrayList;
import java.util.List;
import java.util.function.IntConsumer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
// Port of packages/mancala.js/src/core/Board.ts. Pits are indexed counterclockwise: player 1's pits, player 1's bank,
// player 2's pits, player 2's bank.
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonPropertyOrder({ "pits", "playerPitCount", "initialStoneCountInPits" })
public class Board {
private static final int BANK_COUNT = 2;
private final List<Pit> pits;
private final int playerPitCount;
private final int initialStoneCountInPits;
@JsonIgnore
IntConsumer onGameMoveStart = index -> { };
@JsonIgnore
IntConsumer onGameMove = index -> { };
@JsonIgnore
IntConsumer onGameMoveEnd = index -> { };
public Board(int playerPitCount, int initialStoneCountInPits) {
this(playerPitCount, initialStoneCountInPits, null);
}
public Board(int playerPitCount, int initialStoneCountInPits, List<Pit> pits) {
this.playerPitCount = playerPitCount;
this.initialStoneCountInPits = initialStoneCountInPits;
this.pits = pits != null ? pits : createPits(initialStoneCountInPits);
}
private List<Pit> createPits(int initialStoneCountInPits) {
List<Pit> pitList = new ArrayList<>(playerPitCount * 2 + BANK_COUNT);
for (int index = 0; index < totalPitCount(); index++) {
PitType pitType = getPitTypeByIndex(index);
pitList.add(pitType.isBank() ? new Bank(index, 0) : new Pit(index, initialStoneCountInPits));
}
return pitList;
}
public List<Pit> getPits() {
return pits;
}
public int getPlayerPitCount() {
return playerPitCount;
}
public int getInitialStoneCountInPits() {
return initialStoneCountInPits;
}
public int player1PitStartIndex() {
return 0;
}
public int player1PitStopIndex() {
return player1PitStartIndex() + playerPitCount - 1;
}
public int player1BankIndex() {
return player1PitStopIndex() + 1;
}
public int player2PitStartIndex() {
return player1BankIndex() + 1;
}
public int player2PitStopIndex() {
return player2PitStartIndex() + playerPitCount - 1;
}
public int player2BankIndex() {
return player2PitStopIndex() + 1;
}
public int totalPitCount() {
return player2BankIndex() + 1;
}
public boolean checkIndex(int index) {
return index >= player1PitStartIndex() && index <= player2BankIndex();
}
void checkIndexAndMaybeThrowError(int index) {
if (!checkIndex(index)) {
throw new IllegalStateException("IndexOutOfRange => index : " + index + " [start, stop] = ["
+ player1PitStartIndex() + ", " + player2BankIndex() + "]");
}
}
public PitType getPitTypeByIndex(int index) {
checkIndexAndMaybeThrowError(index);
if (index >= player1PitStartIndex() && index <= player1PitStopIndex()) {
return PitType.PLAYER1_PIT;
} else if (index == player1BankIndex()) {
return PitType.PLAYER1_BANK;
} else if (index <= player2PitStopIndex()) {
return PitType.PLAYER2_PIT;
} else {
return PitType.PLAYER2_BANK;
}
}
public boolean checkPitTypeIsNormalPitByIndex(int index) {
return getPitTypeByIndex(index).isNormalPit();
}
public boolean checkPitTypeIsBankByIndex(int index) {
return getPitTypeByIndex(index).isBank();
}
public void move(int index) {
checkIndexAndMaybeThrowError(index);
PitType pitType = getPitTypeByIndex(index);
if (pitType.isBank()) {
throw new IllegalStateException("InCorrectPit => index : " + index + " pitType = " + pitTypeName(pitType));
}
Pit pit = pits.get(index);
if (pit.getStoneCount() <= 0) {
throw new IllegalStateException("StoneNotFound => index : " + index + " stoneCount = " + pit.getStoneCount());
}
List<Integer> path = getSowingPath(index);
pit.setStoneCount(0);
onGameMoveStart.accept(path.getFirst());
path.forEach(this::onGameMoveStep);
onGameMoveEnd.accept(path.getLast());
}
public List<Integer> getSowingPath(int index) {
return getSowingPath(index, pits.get(index).getStoneCount());
}
// Indexes the stones of the pit are dropped into, in order. A single stone goes to the next pit; otherwise the
// first stone goes back into the played pit. The player's own bank gets a stone, the opponent's bank is skipped.
// The last index is where the last stone lands. Empty for a bank or an empty pit.
public List<Integer> getSowingPath(int index, int stoneCount) {
checkIndexAndMaybeThrowError(index);
if (stoneCount <= 0 || checkPitTypeIsBankByIndex(index)) return List.of();
int opponentBankIndex = getPitTypeByIndex(index) == PitType.PLAYER1_PIT ? player2BankIndex() : player1BankIndex();
List<Integer> path = new ArrayList<>(stoneCount);
int current = stoneCount == 1 ? getNextPitIndexCircularly(index) : index;
while (path.size() < stoneCount) {
if (current != opponentBankIndex) path.add(current);
current = getNextPitIndexCircularly(current);
}
return path;
}
public void onGameMoveStep(int index) {
pits.get(getPitIndexCircularly(index)).increaseStone();
onGameMove.accept(index);
}
public int getNextPitIndexCircularly(int currentPitIndex) {
checkIndexAndMaybeThrowError(currentPitIndex);
return getPitIndexCircularly(currentPitIndex + 1);
}
public Pit getPitCircularly(int currentPitIndex) {
return pits.get(getPitIndexCircularly(currentPitIndex));
}
public int getPitIndexCircularly(int currentPitIndex) {
return currentPitIndex % totalPitCount();
}
public int[] getStoneArray() {
return pits.stream().mapToInt(Pit::getStoneCount).toArray();
}
public List<Pit> player1Pits() {
return pits.subList(player1PitStartIndex(), player1BankIndex());
}
public List<Pit> player2Pits() {
return pits.subList(player2PitStartIndex(), player2BankIndex());
}
public Pit player1Bank() {
return pits.get(player1BankIndex());
}
public Pit player2Bank() {
return pits.get(player2BankIndex());
}
public int getOppositePitIndex(int index) {
if (index > player1BankIndex()) {
return player2PitStopIndex() - index;
} else {
return player1BankIndex() + playerPitCount - index;
}
}
public void clearPlayer1Pits() {
player1Pits().forEach(pit -> pit.setStoneCount(0));
}
public void clearPlayer2Pits() {
player2Pits().forEach(pit -> pit.setStoneCount(0));
}
// The TypeScript name of a pit type, for error messages.
static String pitTypeName(PitType pitType) {
return switch (pitType) {
case PLAYER1_PIT -> "player1Pit";
case PLAYER1_BANK -> "player1Bank";
case PLAYER2_PIT -> "player2Pit";
case PLAYER2_BANK -> "player2Bank";
};
}
}