222 lines
7.4 KiB
Java
222 lines
7.4 KiB
Java
|
|
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";
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|