Compare commits

...

6 Commits

Author SHA1 Message Date
7d26dca752 add prohibited move checks 2023-09-29 11:29:46 +02:00
58da85a5fd final 2023-09-27 17:28:54 +02:00
162e5c97dc rename 2023-09-27 17:27:03 +02:00
a9ffd4b0b9 Checkers move rules improved 2023-09-27 17:25:30 +02:00
9cbeaceca9 Checkers move rules 2023-09-27 17:02:31 +02:00
9144683de3 rename Piece to Stone 2023-09-27 11:35:49 +02:00
18 changed files with 616 additions and 191 deletions

View File

@ -18,7 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Stone;
import djmil.cordacheckers.cordaclient.dao.Rank;
import djmil.cordacheckers.cordaclient.dao.VirtualNode;
import djmil.cordacheckers.cordaclient.dao.VirtualNodeList;
@ -95,7 +95,7 @@ public class CordaClient {
.getResponce(requestBody);
}
public GameState gameProposalCreate(HoldingIdentity issuer, HoldingIdentity acquier, Piece.Color acquierColor,
public GameState gameProposalCreate(HoldingIdentity issuer, HoldingIdentity acquier, Stone.Color acquierColor,
String message
) {
final RequestBody requestBody = new RequestBody(

View File

@ -7,9 +7,9 @@ import java.util.UUID;
public record GameState(
Status status,
String opponentName,
Piece.Color opponentColor,
Stone.Color opponentColor,
Map<Integer, Piece> board,
Map<Integer, Stone> board,
Integer moveNumber,
List<Integer> previousMove,

View File

@ -1,6 +1,6 @@
package djmil.cordacheckers.cordaclient.dao;
public class Piece {
public class Stone {
public enum Type {
MAN,
@ -19,12 +19,12 @@ public class Piece {
Color color;
Type type;
public Piece() {
public Stone() {
this.color = null;
this.type = null;
}
public Piece(Color color, Type type) {
public Stone(Color color, Type type) {
this.color = color;
this.type = type;
}
@ -50,7 +50,7 @@ public class Piece {
return false;
if (getClass() != obj.getClass())
return false;
Piece other = (Piece) obj;
Stone other = (Stone) obj;
if (color != other.color)
return false;
if (type != other.type)

View File

@ -1,10 +1,10 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Stone;
public record ReqGameProposalCreate(
String opponentName,
Piece.Color opponentColor,
Stone.Color opponentColor,
String message
) {

View File

@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.JsonMappingException;
import djmil.cordacheckers.cordaclient.CordaClient;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.HoldingIdentity;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Stone;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameProposalCreate;
import djmil.cordacheckers.user.HoldingIdentityResolver;
import djmil.cordacheckers.user.User;
@ -59,7 +59,7 @@ public class GameProposalController {
final HoldingIdentity gpSender = sender.getHoldingIdentity();
// TODO: throw execption with custom type
final HoldingIdentity gpReceiver = holdingIdentityResolver.getByUsername(gpRequest.opponentName());
final Piece.Color gpReceiverColor = gpRequest.opponentColor();
final Stone.Color gpReceiverColor = gpRequest.opponentColor();
// TODO handle expectionns here
GameState gameStateView = cordaClient.gameProposalCreate(

View File

@ -0,0 +1,215 @@
package djmil.cordacheckers;
import static java.lang.Math.abs;
import static org.assertj.core.api.Assertions.assertThat;
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;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.annotation.Testable;
@Testable
public class CheckersMoveTest {
public 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 Jump {
final public int jump;
final public int step;
Jump(int jump, int step) {
this.jump = jump;
this.step = step;
}
static Set<Jump> intersect(Set<Integer> jumps, Set<Integer> steps) {
Set<Jump> res = new HashSet<Jump>();
for (Integer jump :jumps) {
var jumpSteps = getKingSteps(jump);
jumpSteps.retainAll(steps);
if (jumpSteps.size() == 1)
res.add(new Jump(jump, jumpSteps.iterator().next()));
}
return res;
}
@Override
public String toString() {
return "[jump=" + jump + ", step=" + step + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + jump;
result = prime * result + step;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Jump other = (Jump) obj;
if (jump != other.jump)
return false;
if (step != other.step)
return false;
return true;
}
}
public static Set<Integer> getBlackSteps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> cur > idx && cur - idx <= 5)
.collect(Collectors.toSet());
}
public static Set<Integer> getWhiteSteps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> cur < idx && idx - cur <= 5)
.collect(Collectors.toSet());
}
public static Set<Integer> getKingSteps(Integer idx) {
return adjacentCell.get(idx).stream()
.filter(cur -> abs(idx - cur) <= 5)
.collect(Collectors.toSet());
}
public static Set<Jump> getBlackJumps(Integer idx) {
final var allSteps = getBlackSteps(idx);
final var allJumps = adjacentCell.get(idx).stream()
.filter(cur -> cur > idx && cur - idx > 5)
.collect(Collectors.toSet());
return Jump.intersect(allJumps, allSteps);
}
public static Set<Jump> getWhiteJumps(Integer idx) {
final var allSteps = getWhiteSteps(idx);
final var allJumps = adjacentCell.get(idx).stream()
.filter(cur -> idx > cur && idx - cur > 5)
.collect(Collectors.toSet());
return Jump.intersect(allJumps, allSteps);
}
public static Set<Jump> getKingJumps(Integer idx) {
final var allSteps = getKingSteps(idx);
final var allJumps = adjacentCell.get(idx).stream()
.filter(cur -> abs(idx - cur) > 5)
.collect(Collectors.toSet());
return Jump.intersect(allJumps, allSteps);
}
@Test
void blackStepTest() {
assertThat(getBlackSteps(1)).containsAll(Arrays.asList(5, 6));
assertThat(getBlackSteps(4)).containsAll(Arrays.asList(8));
assertThat(getBlackSteps(18)).containsAll(Arrays.asList(22, 23));
assertThat(getBlackSteps(21)).containsAll(Arrays.asList(25));
}
@Test
void whiteStepTest() {
assertThat(getWhiteSteps(29)).containsAll(Arrays.asList(25));
assertThat(getWhiteSteps(28)).containsAll(Arrays.asList(24));
assertThat(getWhiteSteps(14)).containsAll(Arrays.asList(9, 10));
assertThat(getWhiteSteps(8)).containsAll(Arrays.asList(3, 4));
}
@Test
void kingStepTest() {
assertThat(getKingSteps(29)).containsAll(Arrays.asList(25));
assertThat(getKingSteps(31)).containsAll(Arrays.asList(26, 27));
assertThat(getKingSteps(20)).containsAll(Arrays.asList(16, 24));
assertThat(getKingSteps(13)).containsAll(Arrays.asList(9, 17));
assertThat(getKingSteps(2)).containsAll(Arrays.asList(6, 7));
assertThat(getKingSteps(15)).containsAll(Arrays.asList(10, 11, 18, 19));
}
@Test
void blackJumpTest() {
assertThat(getBlackJumps(4)).containsAll(
Arrays.asList(new Jump(11, 8)));
assertThat(getBlackJumps(16)).containsAll(
Arrays.asList(new Jump(23, 19)));
assertThat(getBlackJumps(15)).containsAll(
Arrays.asList(new Jump(22, 18), new Jump(24, 19)));
assertThat(getBlackJumps(28)).isEmpty();
}
@Test
void whiteJumpTest() {
assertThat(getWhiteJumps(30)).containsAll(
Arrays.asList(new Jump(21, 25), new Jump(23, 26)));
assertThat(getWhiteJumps(17)).containsAll(
Arrays.asList(new Jump(10, 14)));
assertThat(getWhiteJumps(7)).isEmpty();
assertThat(getWhiteJumps(9)).containsAll(
Arrays.asList(new Jump(2, 6)));
}
@Test
void kingJumpTest() {
assertThat(getKingJumps(11)).containsAll(
Arrays.asList(new Jump(2, 7), new Jump(4, 8), new Jump(18, 15),new Jump(20, 16)));
assertThat(getKingJumps(17)).containsAll(
Arrays.asList(new Jump(10, 14), new Jump(26, 22)));
assertThat(getKingJumps(32)).containsAll(
Arrays.asList(new Jump(23, 27)));
}
}

View File

@ -12,7 +12,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.GameState.Status;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Stone;
import djmil.cordacheckers.user.HoldingIdentityResolver;
@SpringBootTest
@ -26,10 +26,10 @@ public class GameBoardTests {
final String whitePlayerName = "alice";
final String blackPlayerName = "bob";
final static Piece WHITE_MAN = new Piece(Piece.Color.WHITE, Piece.Type.MAN);
final static Piece WHITE_KING = new Piece(Piece.Color.WHITE, Piece.Type.KING);
final static Piece BLACK_MAN = new Piece(Piece.Color.BLACK, Piece.Type.MAN);
final static Piece BLACK_KING = new Piece(Piece.Color.BLACK, Piece.Type.KING);
final static Stone WHITE_MAN = new Stone(Stone.Color.WHITE, Stone.Type.MAN);
final static Stone WHITE_KING = new Stone(Stone.Color.WHITE, Stone.Type.KING);
final static Stone BLACK_MAN = new Stone(Stone.Color.BLACK, Stone.Type.MAN);
final static Stone BLACK_KING = new Stone(Stone.Color.BLACK, Stone.Type.KING);
@Test
void testSurrender() {
@ -38,14 +38,14 @@ public class GameBoardTests {
final String message = "GameBoard SURRENDER test";
final GameState game = cordaClient.gameProposalCreate(
hiWhite, hiBlack, Piece.Color.BLACK, message);
hiWhite, hiBlack, Stone.Color.BLACK, message);
System.out.println("Game UUID " +game.uuid());
final GameState acceptedGameBlackView = cordaClient.gameProposalAccept(
hiBlack, game.uuid());
assertThat(acceptedGameBlackView.opponentColor()).isEqualByComparingTo(Piece.Color.WHITE);
assertThat(acceptedGameBlackView.opponentColor()).isEqualByComparingTo(Stone.Color.WHITE);
assertThat(acceptedGameBlackView.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT);
assertThatThrownBy(() -> { // Black can not surrender, since it is opponent's turn
@ -57,14 +57,14 @@ public class GameBoardTests {
hiWhite, game.uuid());
assertThat(surrendererGameView.opponentName()).isEqualToIgnoringCase(blackPlayerName);
assertThat(surrendererGameView.opponentColor()).isEqualByComparingTo(Piece.Color.BLACK);
assertThat(surrendererGameView.opponentColor()).isEqualByComparingTo(Stone.Color.BLACK);
assertThat(surrendererGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_LOOSE);
final GameState winnerGameView = cordaClient.gameStateGet(
hiBlack, game.uuid());
assertThat(winnerGameView.opponentName()).isEqualToIgnoringCase(whitePlayerName);
assertThat(winnerGameView.opponentColor()).isEqualByComparingTo(Piece.Color.WHITE);
assertThat(winnerGameView.opponentColor()).isEqualByComparingTo(Stone.Color.WHITE);
assertThat(winnerGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_WON);
}
@ -75,7 +75,7 @@ public class GameBoardTests {
final String message = "GameBoard MOVE test";
final GameState game = cordaClient.gameProposalCreate(
hiWhite, hiBlack, Piece.Color.BLACK, message);
hiWhite, hiBlack, Stone.Color.BLACK, message);
System.out.println("Game UUID " +game.uuid());
final var m0 = cordaClient.gameProposalAccept(hiBlack, game.uuid());
@ -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

@ -10,7 +10,7 @@ import org.springframework.boot.test.context.SpringBootTest;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.GameState.Status;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Stone;
import djmil.cordacheckers.user.HoldingIdentityResolver;
@SpringBootTest
@ -23,7 +23,7 @@ public class GameProposalTests {
final String issuer = "alice";
final String acquier = "bob";
final Piece.Color acquierColor = Piece.Color.WHITE;
final Stone.Color acquierColor = Stone.Color.WHITE;
@Test
void testCreate() {

View File

@ -8,7 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Stone;
import djmil.cordacheckers.user.HoldingIdentityResolver;
@SpringBootTest
@ -21,7 +21,7 @@ public class GameStateTests {
final String issuer = "alice";
final String acquier = "bob";
final Piece.Color acquierColor = Piece.Color.WHITE;
final Stone.Color acquierColor = Stone.Color.WHITE;
@Test
void testList() {

View File

@ -9,7 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import djmil.cordacheckers.cordaclient.dao.GameState;
import djmil.cordacheckers.cordaclient.dao.Piece;
import djmil.cordacheckers.cordaclient.dao.Stone;
import djmil.cordacheckers.cordaclient.dao.Rank;
import djmil.cordacheckers.user.HoldingIdentityResolver;
@ -32,7 +32,7 @@ public class RankingTests {
final var hiLooser = holdingIdentityResolver.getByUsername("Bob");
final GameState game = cordaClient.gameProposalCreate(
hiWinner, hiLooser, Piece.Color.WHITE, "GameBoard GLOBAL_RANKING test");
hiWinner, hiLooser, Stone.Color.WHITE, "GameBoard GLOBAL_RANKING test");
cordaClient.gameProposalAccept(hiLooser, game.uuid());
cordaClient.gameBoardSurrender(hiLooser, game.uuid());

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

@ -0,0 +1,112 @@
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;
@CordaSerializable
public class Stone {
@CordaSerializable
public enum Type {
MAN,
KING;
}
@CordaSerializable
public enum Color {
WHITE,
BLACK;
public Color opposite() {
switch (this) {
case WHITE:
return BLACK;
case BLACK:
return WHITE;
}
throw new RuntimeException("Unknown Color");
}
}
private final Color color;
private final Type type;
@ConstructorForDeserialization
public Stone(Color color, Type type) {
this.color = color;
this.type = type;
}
public Color getColor() {
return color;
}
public Type getType() {
return type;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Stone other = (Stone) obj;
if (color != other.color)
return false;
if (type != other.type)
return false;
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.Piece;
import djmil.cordacheckers.states.Piece.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, Piece> board;
final public Piece.Color moveColor;
public MoveResult(Map<Integer, Piece> 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,60 +40,33 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
}
}
public static MoveResult applyMove(List<Integer> move, Map<Integer, Piece> board, Piece.Color moveColor) {
final int mFrom = move.get(0);
final int mTo = move.get(1);
final Piece 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, Piece> newBoard = new LinkedHashMap<Integer, Piece>(board);
newBoard.remove(mFrom);
newBoard.put(mTo, piece);
return new GameBoardContract.MoveResult(newBoard, moveColor.opposite());
}
final static Piece WHITE_MAN = new Piece(Piece.Color.WHITE, Piece.Type.MAN);
final static Piece WHITE_KING = new Piece(Piece.Color.WHITE, Piece.Type.KING);
final static Piece BLACK_MAN = new Piece(Piece.Color.BLACK, Piece.Type.MAN);
final static Piece BLACK_KING = new Piece(Piece.Color.BLACK, Piece.Type.KING);
public final static Map<Integer, Piece> initialBoard = Map.ofEntries(
public final static Map<Integer, Stone> initialBoard = Map.ofEntries(
// Inspired by Checkers notation rules: https://www.bobnewell.net/nucleus/checkers.php
Map.entry( 1, BLACK_MAN),
Map.entry( 2, BLACK_MAN),
Map.entry( 3, BLACK_MAN),
Map.entry( 4, BLACK_MAN),
Map.entry( 5, BLACK_MAN),
Map.entry( 6, BLACK_MAN),
Map.entry( 7, BLACK_MAN),
Map.entry( 8, BLACK_MAN),
Map.entry( 9, BLACK_MAN),
Map.entry(10, BLACK_MAN),
Map.entry(11, BLACK_MAN),
Map.entry(12, BLACK_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, WHITE_MAN),
Map.entry(22, WHITE_MAN),
Map.entry(23, WHITE_MAN),
Map.entry(24, WHITE_MAN),
Map.entry(25, WHITE_MAN),
Map.entry(26, WHITE_MAN),
Map.entry(27, WHITE_MAN),
Map.entry(28, WHITE_MAN),
Map.entry(29, WHITE_MAN),
Map.entry(30, WHITE_MAN),
Map.entry(31, WHITE_MAN),
Map.entry(32, WHITE_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,25 +6,25 @@ 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.Piece.Color;
import net.corda.v5.base.annotations.ConstructorForDeserialization;
import net.corda.v5.base.types.MemberX500Name;
import net.corda.v5.ledger.utxo.BelongsToContract;
@BelongsToContract(GameBoardContract.class)
public class GameBoardState extends GameState {
private final Piece.Color moveColor;
private final Stone.Color moveColor;
private final Integer moveNumber;
private final Map<Integer, Piece> board;
private final Map<Integer, Stone> board;
public GameBoardState(GameProposalState gameProposalState) {
super(gameProposalState.whitePlayer, gameProposalState.blackPlayer,
gameProposalState.gameUuid, gameProposalState.message, gameProposalState.participants);
this.board = new LinkedHashMap<Integer, Piece>(GameBoardContract.initialBoard);
this.moveColor = Piece.Color.WHITE;
this.board = new LinkedHashMap<Integer, Stone>(GameBoardContract.initialBoard);
this.moveColor = Stone.Color.WHITE;
this.moveNumber = 0;
}
@ -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, Piece> 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);
@ -52,7 +51,7 @@ public class GameBoardState extends GameState {
this.board = board;
}
public Piece.Color getMoveColor() {
public Stone.Color getMoveColor() {
return moveColor;
}
@ -61,10 +60,10 @@ public class GameBoardState extends GameState {
}
public MemberX500Name getActivePlayerName() {
return moveColor == Piece.Color.WHITE ? whitePlayer : blackPlayer;
return moveColor == Stone.Color.WHITE ? whitePlayer : blackPlayer;
}
public Map<Integer, Piece> getBoard() {
public Map<Integer, Stone> getBoard() {
return board;
}

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;
@ -67,11 +68,11 @@ public abstract class GameState implements ContractState {
throw new NotInvolved(playerName, this.getClass(), gameUuid);
}
public Piece.Color getOpponentColor(MemberX500Name playerName) throws NotInvolved {
public Stone.Color getOpponentColor(MemberX500Name playerName) throws NotInvolved {
if (playerName.compareTo(whitePlayer) == 0)
return Piece.Color.BLACK;
return Stone.Color.BLACK;
if (playerName.compareTo(blackPlayer) == 0)
return Piece.Color.WHITE;
return Stone.Color.WHITE;
throw new NotInvolved(playerName, this.getClass(), gameUuid);
}

View File

@ -1,64 +0,0 @@
package djmil.cordacheckers.states;
import net.corda.v5.base.annotations.ConstructorForDeserialization;
import net.corda.v5.base.annotations.CordaSerializable;
@CordaSerializable
public class Piece {
@CordaSerializable
public enum Type {
MAN,
KING;
}
@CordaSerializable
public enum Color {
WHITE,
BLACK;
public Color opposite() {
switch (this) {
case WHITE:
return BLACK;
case BLACK:
return WHITE;
}
throw new RuntimeException("Unknown Color");
}
}
private final Color color;
private final Type type;
@ConstructorForDeserialization
public Piece(Color color, Type type) {
this.color = color;
this.type = type;
}
public Color getColor() {
return color;
}
public Type getType() {
return type;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Piece other = (Piece) obj;
if (color != other.color)
return false;
if (type != other.type)
return false;
return true;
}
}

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.Piece;
import net.corda.v5.application.flows.ClientRequestBody;
import net.corda.v5.application.flows.ClientStartableFlow;
import net.corda.v5.application.flows.CordaInject;
@ -88,10 +88,10 @@ public class CreateFlow implements ClientStartableFlow{
private GameProposalState buildGameProposalStateFrom(ClientRequestBody requestBody) {
final CreateFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, CreateFlowArgs.class);
final Piece.Color opponentColor = Piece.Color.valueOf(args.opponentColor);
final Stone.Color opponentColor = Stone.Color.valueOf(args.opponentColor);
if (opponentColor == null) {
throw new RuntimeException("Allowed values for opponentColor are: "
+ Piece.Color.WHITE.name() +", " + Piece.Color.BLACK.name()
+ Stone.Color.WHITE.name() +", " + Stone.Color.BLACK.name()
+ ". Actual value was: " + args.opponentColor);
}
@ -101,8 +101,8 @@ public class CreateFlow implements ClientStartableFlow{
"MemberLookup can't find opponentName specified in flow arguments: " + args.opponentName
);
final MemberInfo whitePlayerInfo = opponentColor == Piece.Color.WHITE ? opponentInfo : myInfo;
final MemberInfo blackPlayerInfo = opponentColor == Piece.Color.BLACK ? opponentInfo : myInfo;
final MemberInfo whitePlayerInfo = opponentColor == Stone.Color.WHITE ? opponentInfo : myInfo;
final MemberInfo blackPlayerInfo = opponentColor == Stone.Color.BLACK ? opponentInfo : myInfo;
return new GameProposalState(
whitePlayerInfo.getName(),

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.Piece;
import net.corda.v5.base.types.MemberX500Name;
// GameBoard from the player's point of view
@ -27,9 +27,9 @@ public class View {
public final Status status;
public final String opponentName;
public final Piece.Color opponentColor;
public final Stone.Color opponentColor;
public final Map<Integer, Piece> board;
public final Map<Integer, Stone> board;
public final Integer moveNumber;
public final List<Integer> previousMove;