add prohibited move checks

This commit is contained in:
djmil 2023-09-29 11:29:46 +02:00
parent 58da85a5fd
commit 7d26dca752
8 changed files with 293 additions and 83 deletions

View File

@ -95,7 +95,21 @@ public class GameBoardTests {
assertThat(m0.board().get(18)).isNull();
assertThat(m1.board().get(22)).isNull();
assertThat(m1.board().get(18)).isEqualTo(WHITE_MAN);
assertThat(m1.moveNumber() == 1);
assertThat(m1.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT);
assertThatThrownBy(() -> {
cordaClient.gameBoardMove(hiBlack, game.uuid(), move(12, 13),
"Prohibitted move shall be rejected");
});
final var m2 = cordaClient.gameBoardMove(hiBlack, game.uuid(), move(11, 15), null);
assertThat(m1.board().get(11)).isEqualTo(BLACK_MAN);
assertThat(m1.board().get(15)).isNull();
assertThat(m2.board().get(11)).isNull();
assertThat(m2.board().get(15)).isEqualTo(BLACK_MAN);
assertThat(m2.moveNumber() == 2);
assertThat(m2.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT);
}
ArrayList<Integer> move(int from, int to) {

View File

@ -0,0 +1,194 @@
package djmil.cordacheckers.checkers;
import static java.lang.Math.abs;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class Move {
final public Integer from;
final public Integer to;
final public Integer jumpOver;
public Move(int from, int to) {
this.from = from;
this.to = to;
Set<Integer> fromSteps = getSteps(from);
if (fromSteps.contains(to))
this.jumpOver = null;
else
this.jumpOver = intersection(fromSteps, getSteps(to));
}
public static Stone.Color apply(List<Integer> moves, Map<Integer, Stone> board, Stone.Color moveColor) {
Move move = new Move(moves.get(0), moves.get(1));
return move.apply(board, moveColor);
}
private Stone.Color apply(Map<Integer, Stone> board, Stone.Color expectedMoveColor) {
Stone movingStone = board.remove(from);
if (movingStone == null)
throw new Exception("An empty starting tile");
if (movingStone.getColor() != expectedMoveColor)
throw new Exception("Only " +expectedMoveColor.name() +" color is expected to move");
final Set<Move> allMoves = movingStone.getMoves(from);
if (!allMoves.contains(this))
throw new Exception("Prohibited move");
// TODO: check for mandatory captures
movingStone = movingStone.promoteIfPossible(to);
if (board.put(to, movingStone) != null)
throw new Exception("An occupied finishing tile");
if (isJump()) {
final Stone jumpOver = board.remove(this.jumpOver);
if (jumpOver == null || jumpOver.getColor() != expectedMoveColor.opposite())
throw new Exception("Must jump over an opponent's stone");
}
return expectedMoveColor.opposite();
}
public boolean isJump() {
return this.jumpOver != null;
}
static Integer intersection(Set<Integer> a, Set<Integer> b) {
Set<Integer> intersection = new HashSet<Integer>(a);
intersection.retainAll(b);
if (intersection.size() != 1)
// A legit move is characterized by single intersection point
throw new Exception("Prohibited move");
return intersection.iterator().next();
}
static Set<Integer> getSteps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> abs(idx - cur) <= 5)
.collect(Collectors.toSet());
}
static Set<Integer> getJumps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> abs(idx - cur) > 5)
.collect(Collectors.toSet());
}
static Set<Integer> getBlackSteps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> cur > idx && cur - idx <= 5)
.collect(Collectors.toSet());
}
static Set<Integer> getWhiteSteps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> cur < idx && idx - cur <= 5)
.collect(Collectors.toSet());
}
static Set<Integer> getBlackJumps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> cur > idx && cur - idx > 5)
.collect(Collectors.toSet());
}
static Set<Integer> getWhiteJumps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> cur < idx && idx - cur > 5)
.collect(Collectors.toSet());
}
final static Map<Integer, List<Integer>> adjacentCell = Map.ofEntries(
Map.entry(1, Arrays.asList(5, 6, 10)),
Map.entry(2, Arrays.asList(6, 7, 9, 11)),
Map.entry(3, Arrays.asList(7, 8, 10, 12)),
Map.entry(4, Arrays.asList(8, 11)),
Map.entry(5, Arrays.asList(1, 9, 14, 9, 11)),
Map.entry(6, Arrays.asList(1, 2, 9, 10, 13, 15)),
Map.entry(7, Arrays.asList(2, 3, 10, 11, 14, 16)),
Map.entry(8, Arrays.asList(3, 4, 11, 12, 15)),
Map.entry(9, Arrays.asList(2, 5, 6, 13, 14, 18)),
Map.entry(10, Arrays.asList(1, 3, 6, 7, 14, 15, 17, 19)),
Map.entry(11, Arrays.asList(2, 4, 7, 8, 15, 16, 18, 20)),
Map.entry(12, Arrays.asList(3, 8, 16, 19)),
Map.entry(13, Arrays.asList(6, 9, 17, 22)),
Map.entry(14, Arrays.asList(5, 7, 9, 10, 17, 18, 21, 23)),
Map.entry(15, Arrays.asList(6, 8, 10, 11, 18, 19, 22, 24)),
Map.entry(16, Arrays.asList(7, 11, 12, 19, 20, 23)),
Map.entry(17, Arrays.asList(10, 13, 14, 21, 22, 26)),
Map.entry(18, Arrays.asList(9, 11, 14, 15, 22, 23, 25, 27)),
Map.entry(19, Arrays.asList(10, 12, 15, 16, 23, 24)),
Map.entry(20, Arrays.asList(11, 16, 24, 27)),
Map.entry(21, Arrays.asList(14, 17, 25, 30)),
Map.entry(22, Arrays.asList(13, 15, 17, 18, 25, 26, 29, 31)),
Map.entry(23, Arrays.asList(14, 16, 18, 19, 26, 27, 30, 32)),
Map.entry(24, Arrays.asList(15, 19, 20, 27, 28, 31)),
Map.entry(25, Arrays.asList(18, 21, 22, 29, 30)),
Map.entry(26, Arrays.asList(17, 19, 22, 23, 30, 31)),
Map.entry(27, Arrays.asList(18, 20, 23, 24, 31, 32)),
Map.entry(28, Arrays.asList(19, 24, 32)),
Map.entry(29, Arrays.asList(22, 25)),
Map.entry(30, Arrays.asList(21, 23, 25, 26)),
Map.entry(31, Arrays.asList(22, 24, 26, 27)),
Map.entry(32, Arrays.asList(23, 27, 28))
);
public static class Exception extends RuntimeException {
public Exception(String message) {
super(message);
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((from == null) ? 0 : from.hashCode());
result = prime * result + ((to == null) ? 0 : to.hashCode());
result = prime * result + ((jumpOver == null) ? 0 : jumpOver.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Move other = (Move) obj;
if (from == null) {
if (other.from != null)
return false;
} else if (!from.equals(other.from))
return false;
if (to == null) {
if (other.to != null)
return false;
} else if (!to.equals(other.to))
return false;
if (jumpOver == null) {
if (other.jumpOver != null)
return false;
} else if (!jumpOver.equals(other.jumpOver))
return false;
return true;
}
@Override
public String toString() {
return "[from=" + from + ", to=" + to + ", jumpOver=" + jumpOver + "]";
}
}

View File

@ -1,4 +1,7 @@
package djmil.cordacheckers.states;
package djmil.cordacheckers.checkers;
import java.util.HashSet;
import java.util.Set;
import net.corda.v5.base.annotations.ConstructorForDeserialization;
import net.corda.v5.base.annotations.CordaSerializable;
@ -31,11 +34,6 @@ public class Stone {
private final Color color;
private final Type type;
public final static Stone WHITE_MAN = new Stone(Stone.Color.WHITE, Stone.Type.MAN);
public final static Stone WHITE_KING = new Stone(Stone.Color.WHITE, Stone.Type.KING);
public final static Stone BLACK_MAN = new Stone(Stone.Color.BLACK, Stone.Type.MAN);
public final static Stone BLACK_KING = new Stone(Stone.Color.BLACK, Stone.Type.KING);
@ConstructorForDeserialization
public Stone(Color color, Type type) {
this.color = color;
@ -66,4 +64,49 @@ public class Stone {
return true;
}
public Set<Move> getJumps(Integer from) {
Set<Integer> jumps;
if (type == Type.KING)
jumps = Move.getJumps(from);
else if (color == Color.BLACK)
jumps = Move.getBlackJumps(from);
else
jumps = Move.getWhiteJumps(from);
Set<Move> res = new HashSet<>();
for (Integer jump : jumps)
res.add(new Move(from, jump));
return res;
}
public Set<Move> getMoves(Integer from) {
Set<Integer> steps;
if (type == Type.KING)
steps = Move.getSteps(from);
else if (color == Color.BLACK)
steps = Move.getBlackSteps(from);
else
steps = Move.getWhiteSteps(from);
Set<Move> moves = getJumps(from); // <<--- Steps + Jumps
for (Integer step : steps) {
moves.add(new Move(from, step));
}
return moves;
}
public Stone promoteIfPossible(Integer to) {
if ((color == Color.WHITE && to >= 1 && to <= 4) ||
(color == Color.BLACK && to >= 29 && to <= 32))
return new Stone(color, Type.KING);
else
return this;
}
}

View File

@ -3,35 +3,16 @@ package djmil.cordacheckers.contracts;
import static djmil.cordacheckers.contracts.GameCommand.requireThat;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.states.Stone;
import djmil.cordacheckers.states.Stone.Color;
import djmil.cordacheckers.checkers.Stone;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
public static class MoveResult {
final public Map<Integer, Stone> board;
final public Stone.Color moveColor;
public MoveResult(Map<Integer, Stone> board, Color moveColor) {
this.board = board;
this.moveColor = moveColor;
}
public static class Exception extends RuntimeException {
public Exception(String message) {
super(message);
}
}
}
private final static Logger log = LoggerFactory.getLogger(GameBoardContract.class);
@Override
@ -59,55 +40,33 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
}
}
public static MoveResult applyMove(List<Integer> move, Map<Integer, Stone> board, Stone.Color moveColor) {
final int mFrom = move.get(0);
final int mTo = move.get(1);
final Stone piece = board.get(mFrom);
if (piece == null)
throw new MoveResult.Exception("An empty starting tile");
if (piece.getColor() != moveColor)
throw new MoveResult.Exception("Can not move opponent's piece");
if (board.get(mTo) != null)
throw new MoveResult.Exception("An occupied finishing tile");
final Map<Integer, Stone> newBoard = new LinkedHashMap<Integer, Stone>(board);
newBoard.remove(mFrom);
newBoard.put(mTo, piece);
return new GameBoardContract.MoveResult(newBoard, moveColor.opposite());
}
public final static Map<Integer, Stone> initialBoard = Map.ofEntries(
// Inspired by Checkers notation rules: https://www.bobnewell.net/nucleus/checkers.php
Map.entry( 1, Stone.BLACK_MAN),
Map.entry( 2, Stone.BLACK_MAN),
Map.entry( 3, Stone.BLACK_MAN),
Map.entry( 4, Stone.BLACK_MAN),
Map.entry( 5, Stone.BLACK_MAN),
Map.entry( 6, Stone.BLACK_MAN),
Map.entry( 7, Stone.BLACK_MAN),
Map.entry( 8, Stone.BLACK_MAN),
Map.entry( 9, Stone.BLACK_MAN),
Map.entry(10, Stone.BLACK_MAN),
Map.entry(11, Stone.BLACK_MAN),
Map.entry(12, Stone.BLACK_MAN),
Map.entry(21, Stone.WHITE_MAN),
Map.entry(22, Stone.WHITE_MAN),
Map.entry(23, Stone.WHITE_MAN),
Map.entry(24, Stone.WHITE_MAN),
Map.entry(25, Stone.WHITE_MAN),
Map.entry(26, Stone.WHITE_MAN),
Map.entry(27, Stone.WHITE_MAN),
Map.entry(28, Stone.WHITE_MAN),
Map.entry(29, Stone.WHITE_MAN),
Map.entry(30, Stone.WHITE_MAN),
Map.entry(31, Stone.WHITE_MAN),
Map.entry(32, Stone.WHITE_MAN)
);
Map.entry( 1, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry( 2, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry( 3, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry( 4, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry( 5, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry( 6, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry( 7, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry( 8, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry( 9, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry(10, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry(11, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry(12, new Stone(Stone.Color.BLACK, Stone.Type.MAN)),
Map.entry(21, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(22, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(23, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(24, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(25, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(26, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(27, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(28, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(29, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(30, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(31, new Stone(Stone.Color.WHITE, Stone.Type.MAN)),
Map.entry(32, new Stone(Stone.Color.WHITE, Stone.Type.MAN))
);
}

View File

@ -6,9 +6,9 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import djmil.cordacheckers.checkers.Move;
import djmil.cordacheckers.checkers.Stone;
import djmil.cordacheckers.contracts.GameBoardContract;
import djmil.cordacheckers.contracts.GameBoardContract.MoveResult;
import djmil.cordacheckers.states.Stone.Color;
import net.corda.v5.base.annotations.ConstructorForDeserialization;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.ledger.utxo.BelongsToContract;
@ -32,9 +32,8 @@ public class GameBoardState extends GameState {
super(currentGameBoardState.whitePlayer, currentGameBoardState.blackPlayer, currentGameBoardState.gameUuid,
message, currentGameBoardState.participants);
final MoveResult moveResult = GameBoardContract.applyMove(move, currentGameBoardState.getBoard(), currentGameBoardState.getMoveColor());
this.moveColor = moveResult.moveColor;
this.board = moveResult.board;
this.board = new LinkedHashMap<Integer, Stone>(currentGameBoardState.getBoard());
this.moveColor = Move.apply(move, this.board, currentGameBoardState.getMoveColor());
this.moveNumber = (currentGameBoardState.moveColor == this.moveColor)
? currentGameBoardState.getMoveNumber() // current player has not finished his move jet
@ -43,7 +42,7 @@ public class GameBoardState extends GameState {
@ConstructorForDeserialization
public GameBoardState(MemberX500Name whitePlayer, MemberX500Name blackPlayer,
Color moveColor, Integer moveNumber, Map<Integer, Stone> board, String message,
Stone.Color moveColor, Integer moveNumber, Map<Integer, Stone> board, String message,
UUID gameUuid, List<PublicKey> participants) {
super(whitePlayer, blackPlayer, gameUuid, message, participants);

View File

@ -5,6 +5,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import djmil.cordacheckers.checkers.Stone;
import net.corda.v5.base.annotations.CordaSerializable;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.ledger.utxo.ContractState;

View File

@ -10,13 +10,13 @@ import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.checkers.Stone;
import djmil.cordacheckers.contracts.GameCommand;
import djmil.cordacheckers.gamestate.CommitSubFlow;
import djmil.cordacheckers.gamestate.FlowResponce;
import djmil.cordacheckers.gamestate.View;
import djmil.cordacheckers.gamestate.ViewBuilder;
import djmil.cordacheckers.states.GameProposalState;
import djmil.cordacheckers.states.Stone;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject;

View File

@ -4,10 +4,10 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import djmil.cordacheckers.checkers.Stone;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState;
import djmil.cordacheckers.states.GameResultState;
import djmil.cordacheckers.states.Stone;
import net.corda.v5.base.types.MemberX500Name;
// GameBoard from the player's point of view