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 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 pits) { this.playerPitCount = playerPitCount; this.initialStoneCountInPits = initialStoneCountInPits; this.pits = pits != null ? pits : createPits(initialStoneCountInPits); } private List createPits(int initialStoneCountInPits) { List 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 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 path = getSowingPath(index); pit.setStoneCount(0); onGameMoveStart.accept(path.getFirst()); path.forEach(this::onGameMoveStep); onGameMoveEnd.accept(path.getLast()); } public List 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 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 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 player1Pits() { return pits.subList(player1PitStartIndex(), player1BankIndex()); } public List 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"; }; } }