Quarkus 3.40 on Java 25, built with the Maven wrapper. The engine is a line-by-line port of packages/mancala.js; a parity test replays games recorded with the TypeScript engine, including illegal moves, and compares every board, turn, error and the game JSON.
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";
|
|
};
|
|
}
|
|
}
|