GameBoard: naive MOVE implementation
This commit is contained in:
parent
8971462c74
commit
1f2ff242e4
@ -85,21 +85,18 @@ public class CordaClient {
|
|||||||
.getResponce(requestBody);
|
.getResponce(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameState gameStateGet(HoldingIdentity holdingIdentity, UUID gameStateUuid) {
|
public GameState gameStateGet(HoldingIdentity holdingIdentity, UUID gameUuid) {
|
||||||
final RequestBody requestBody = new RequestBody(
|
final RequestBody requestBody = new RequestBody(
|
||||||
"gs.get-" + UUID.randomUUID(),
|
"gs.get-" + UUID.randomUUID(),
|
||||||
"djmil.cordacheckers.gamestate.GetFlow",
|
"djmil.cordacheckers.gamestate.GetFlow",
|
||||||
gameStateUuid);
|
gameUuid);
|
||||||
|
|
||||||
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
||||||
.getResponce(requestBody);
|
.getResponce(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameState gameProposalCreate(
|
public GameState gameProposalCreate(HoldingIdentity issuer, HoldingIdentity acquier, Piece.Color acquierColor,
|
||||||
HoldingIdentity issuer,
|
String message
|
||||||
HoldingIdentity acquier,
|
|
||||||
Piece.Color acquierColor,
|
|
||||||
String message
|
|
||||||
) {
|
) {
|
||||||
final RequestBody requestBody = new RequestBody(
|
final RequestBody requestBody = new RequestBody(
|
||||||
"gp.create-" + UUID.randomUUID(),
|
"gp.create-" + UUID.randomUUID(),
|
||||||
@ -113,53 +110,53 @@ public class CordaClient {
|
|||||||
.getResponce(requestBody);
|
.getResponce(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameState gameProposalReject(HoldingIdentity holdingIdentity, UUID gameProposalUuid) {
|
public GameState gameProposalReject(HoldingIdentity holdingIdentity, UUID gameUuid) {
|
||||||
final RequestBody requestBody = new RequestBody(
|
final RequestBody requestBody = new RequestBody(
|
||||||
"gp.reject-" +UUID.randomUUID(),
|
"gp.reject-" +UUID.randomUUID(),
|
||||||
"djmil.cordacheckers.gameproposal.RejectFlow",
|
"djmil.cordacheckers.gameproposal.RejectFlow",
|
||||||
gameProposalUuid);
|
gameUuid);
|
||||||
|
|
||||||
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
||||||
.getResponce(requestBody);
|
.getResponce(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameState gameProposalCancel(HoldingIdentity holdingIdentity, UUID gameProposalUuid) {
|
public GameState gameProposalCancel(HoldingIdentity holdingIdentity, UUID gameUuid) {
|
||||||
final RequestBody requestBody = new RequestBody(
|
final RequestBody requestBody = new RequestBody(
|
||||||
"gp.reject-" +UUID.randomUUID(),
|
"gp.reject-" +UUID.randomUUID(),
|
||||||
"djmil.cordacheckers.gameproposal.CancelFlow",
|
"djmil.cordacheckers.gameproposal.CancelFlow",
|
||||||
gameProposalUuid);
|
gameUuid);
|
||||||
|
|
||||||
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
||||||
.getResponce(requestBody);
|
.getResponce(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameState gameProposalAccept(HoldingIdentity holdingIdentity, UUID gameProposalUuid) {
|
public GameState gameProposalAccept(HoldingIdentity holdingIdentity, UUID gameUuid) {
|
||||||
final RequestBody requestBody = new RequestBody(
|
final RequestBody requestBody = new RequestBody(
|
||||||
"gp.accept-" +UUID.randomUUID(),
|
"gp.accept-" +UUID.randomUUID(),
|
||||||
"djmil.cordacheckers.gameproposal.AcceptFlow",
|
"djmil.cordacheckers.gameproposal.AcceptFlow",
|
||||||
gameProposalUuid);
|
gameUuid);
|
||||||
|
|
||||||
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
||||||
.getResponce(requestBody);
|
.getResponce(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameState gameBoardSurrender(HoldingIdentity holdingIdentity, UUID gameBoardUuid) {
|
public GameState gameBoardSurrender(HoldingIdentity holdingIdentity, UUID gameUuid) {
|
||||||
final RequestBody requestBody = new RequestBody(
|
final RequestBody requestBody = new RequestBody(
|
||||||
"gb.surrender-" +UUID.randomUUID(),
|
"gb.surrender-" +UUID.randomUUID(),
|
||||||
"djmil.cordacheckers.gameboard.SurrenderFlow",
|
"djmil.cordacheckers.gameboard.SurrenderFlow",
|
||||||
gameBoardUuid);
|
gameUuid);
|
||||||
|
|
||||||
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
||||||
.getResponce(requestBody);
|
.getResponce(requestBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameState gameBoardMove(HoldingIdentity holdingIdentity, UUID gameBoardUuid, List<Integer> move) {
|
public GameState gameBoardMove(HoldingIdentity holdingIdentity, UUID gameUuid, List<Integer> move,
|
||||||
|
String message) {
|
||||||
final RequestBody requestBody = new RequestBody(
|
final RequestBody requestBody = new RequestBody(
|
||||||
"gb.move-" +UUID.randomUUID(),
|
"gb.move-" +UUID.randomUUID(),
|
||||||
"djmil.cordacheckers.gameboard.CommandFlow",
|
"djmil.cordacheckers.gameboard.MoveFlow",
|
||||||
new ReqGameBoardMove(
|
new ReqGameBoardMove(
|
||||||
gameBoardUuid,
|
move, gameUuid, message));
|
||||||
move));
|
|
||||||
|
|
||||||
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class)
|
||||||
.getResponce(requestBody);
|
.getResponce(requestBody);
|
||||||
|
@ -42,4 +42,20 @@ public class Piece {
|
|||||||
return color +"." +type;
|
return color +"." +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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,9 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public record ReqGameBoardMove(
|
public record ReqGameBoardMove(
|
||||||
UUID gameBoardUuid,
|
List<Integer> move,
|
||||||
List<Integer> move
|
UUID gameUuid,
|
||||||
|
String message
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@ package djmil.cordacheckers.cordaclient;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
@ -23,6 +26,11 @@ public class GameBoardTests {
|
|||||||
final String whitePlayerName = "alice";
|
final String whitePlayerName = "alice";
|
||||||
final String blackPlayerName = "bob";
|
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);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSurrender() {
|
void testSurrender() {
|
||||||
final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName);
|
final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName);
|
||||||
@ -60,4 +68,37 @@ public class GameBoardTests {
|
|||||||
assertThat(winnerGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_WON);
|
assertThat(winnerGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_WON);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMove() {
|
||||||
|
final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName);
|
||||||
|
final var hiBlack = holdingIdentityResolver.getByUsername(blackPlayerName);
|
||||||
|
final String message = "GameBoard MOVE test";
|
||||||
|
|
||||||
|
final GameState game = cordaClient.gameProposalCreate(
|
||||||
|
hiWhite, hiBlack, Piece.Color.BLACK, message);
|
||||||
|
System.out.println("Game UUID " +game.uuid());
|
||||||
|
|
||||||
|
final var m0 = cordaClient.gameProposalAccept(hiBlack, game.uuid());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> {
|
||||||
|
cordaClient.gameBoardMove(hiBlack, game.uuid(), move(12, 16),
|
||||||
|
"Black can not move, since it is opponent's turn");
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> {
|
||||||
|
cordaClient.gameBoardMove(hiWhite, game.uuid(), move(17, 14),
|
||||||
|
"Trying to move an empty tile");
|
||||||
|
});
|
||||||
|
|
||||||
|
final var m1 = cordaClient.gameBoardMove(hiWhite, game.uuid(), move(22, 18), null);
|
||||||
|
assertThat(m0.board().get(22)).isEqualTo(WHITE_MAN);
|
||||||
|
assertThat(m0.board().get(18)).isNull();
|
||||||
|
assertThat(m1.board().get(22)).isNull();
|
||||||
|
assertThat(m1.board().get(18)).isEqualTo(WHITE_MAN);
|
||||||
|
assertThat(m1.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<Integer> move(int from, int to) {
|
||||||
|
return new ArrayList<Integer>(Arrays.asList(from, to));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
import djmil.cordacheckers.cordaclient.dao.GameState;
|
import djmil.cordacheckers.cordaclient.dao.GameState;
|
||||||
import djmil.cordacheckers.cordaclient.dao.GameState.Status;
|
|
||||||
import djmil.cordacheckers.cordaclient.dao.Piece;
|
import djmil.cordacheckers.cordaclient.dao.Piece;
|
||||||
import djmil.cordacheckers.cordaclient.dao.Rank;
|
import djmil.cordacheckers.cordaclient.dao.Rank;
|
||||||
import djmil.cordacheckers.user.HoldingIdentityResolver;
|
import djmil.cordacheckers.user.HoldingIdentityResolver;
|
||||||
@ -25,6 +24,8 @@ public class RankingTests {
|
|||||||
@Test
|
@Test
|
||||||
void testGlobalRanking() {
|
void testGlobalRanking() {
|
||||||
final var hiCustodian = holdingIdentityResolver.getCustodian();
|
final var hiCustodian = holdingIdentityResolver.getCustodian();
|
||||||
|
assertThat(hiCustodian).isNotNull();
|
||||||
|
|
||||||
final List<Rank> liderboard1 = cordaClient.fetchRanking(hiCustodian);
|
final List<Rank> liderboard1 = cordaClient.fetchRanking(hiCustodian);
|
||||||
|
|
||||||
final var hiWinner = holdingIdentityResolver.getByUsername("Charlie");
|
final var hiWinner = holdingIdentityResolver.getByUsername("Charlie");
|
||||||
|
@ -3,12 +3,34 @@ package djmil.cordacheckers.contracts;
|
|||||||
import static djmil.cordacheckers.contracts.GameCommand.requireThat;
|
import static djmil.cordacheckers.contracts.GameCommand.requireThat;
|
||||||
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import djmil.cordacheckers.states.Piece;
|
||||||
|
import djmil.cordacheckers.states.Piece.Color;
|
||||||
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
|
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
|
||||||
|
|
||||||
public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
|
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);
|
private final static Logger log = LoggerFactory.getLogger(GameBoardContract.class);
|
||||||
|
|
||||||
@ -19,13 +41,13 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
|
|||||||
requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND);
|
requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND);
|
||||||
final GameCommand command = getSingleCommand(trx, GameCommand.class);
|
final GameCommand command = getSingleCommand(trx, GameCommand.class);
|
||||||
|
|
||||||
switch (command.action) {
|
switch (command.getAction()) {
|
||||||
case GAME_PROPOSAL_ACCEPT:
|
case GAME_PROPOSAL_ACCEPT:
|
||||||
command.validateGameProposalAccept(trx);
|
command.validateGameProposalAccept(trx);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GAME_BOARD_MOVE:
|
case GAME_BOARD_MOVE:
|
||||||
command.validateGameBoardMove(trx);
|
command.validateGameBoardMove(trx, command.getMove());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GAME_BOARD_SURRENDER:
|
case GAME_BOARD_SURRENDER:
|
||||||
@ -37,4 +59,60 @@ 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(
|
||||||
|
// 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(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)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,8 @@ public class GameCommand implements Command {
|
|||||||
GAME_RESULT_CREATE;
|
GAME_RESULT_CREATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final Action action;
|
private final Action action;
|
||||||
public final List<Integer> move; // [0] from, [1] to
|
private final List<Integer> move;
|
||||||
|
|
||||||
public static class ActionException extends RuntimeException {
|
public static class ActionException extends RuntimeException {
|
||||||
public ActionException() {
|
public ActionException() {
|
||||||
@ -156,6 +156,7 @@ public class GameCommand implements Command {
|
|||||||
final GameBoardState outGameBoard = getSingleOutputState(trx, GameBoardState.class);
|
final GameBoardState outGameBoard = getSingleOutputState(trx, GameBoardState.class);
|
||||||
|
|
||||||
requireThat(inGameProposal.getParticipants().containsAll(outGameBoard.getParticipants()), IN_OUT_PARTICIPANTS);
|
requireThat(inGameProposal.getParticipants().containsAll(outGameBoard.getParticipants()), IN_OUT_PARTICIPANTS);
|
||||||
|
requireThat(outGameBoard.getBoard().equals(GameBoardContract.initialBoard), "Bad GameBoard initial state");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateGameProposalReject(UtxoLedgerTransaction trx) {
|
public void validateGameProposalReject(UtxoLedgerTransaction trx) {
|
||||||
@ -197,7 +198,7 @@ public class GameCommand implements Command {
|
|||||||
"Expected winner "+expectedWinnerName.getCommonName() +", proposed winner " +outGameResultState.getWinnerName().getCommonName());
|
"Expected winner "+expectedWinnerName.getCommonName() +", proposed winner " +outGameResultState.getWinnerName().getCommonName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateGameBoardMove(UtxoLedgerTransaction trx) {
|
public void validateGameBoardMove(UtxoLedgerTransaction trx, List<Integer> move) {
|
||||||
requireThat(trx.getInputContractStates().size() == 1, MOVE_INPUT_STATE);
|
requireThat(trx.getInputContractStates().size() == 1, MOVE_INPUT_STATE);
|
||||||
final var inGameBoardState = getSingleInputState(trx, GameBoardState.class);
|
final var inGameBoardState = getSingleInputState(trx, GameBoardState.class);
|
||||||
|
|
||||||
@ -206,6 +207,9 @@ public class GameCommand implements Command {
|
|||||||
|
|
||||||
requireThat(inGameBoardState.getWhitePlayer().compareTo(outGameBoardState.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS);
|
requireThat(inGameBoardState.getWhitePlayer().compareTo(outGameBoardState.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS);
|
||||||
requireThat(inGameBoardState.getBlackPlayer().compareTo(outGameBoardState.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS);
|
requireThat(inGameBoardState.getBlackPlayer().compareTo(outGameBoardState.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS);
|
||||||
|
|
||||||
|
final var newGameBoard = new GameBoardState(inGameBoardState, move, outGameBoardState.getMessage());
|
||||||
|
requireThat(outGameBoardState.equals(newGameBoard), "Unexpected output state");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateGameResultCreate(UtxoLedgerTransaction trx) {
|
public void validateGameResultCreate(UtxoLedgerTransaction trx) {
|
||||||
|
@ -19,7 +19,7 @@ public class GameProposalContract implements net.corda.v5.ledger.utxo.Contract {
|
|||||||
requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND);
|
requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND);
|
||||||
final GameCommand command = getSingleCommand(trx, GameCommand.class);
|
final GameCommand command = getSingleCommand(trx, GameCommand.class);
|
||||||
|
|
||||||
switch (command.action) {
|
switch (command.getAction()) {
|
||||||
case GAME_PROPOSAL_CREATE:
|
case GAME_PROPOSAL_CREATE:
|
||||||
command.validateGameProposalCreate(trx);
|
command.validateGameProposalCreate(trx);
|
||||||
break;
|
break;
|
||||||
|
@ -19,7 +19,7 @@ public class GameResultContract implements net.corda.v5.ledger.utxo.Contract {
|
|||||||
requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND);
|
requireThat(trx.getCommands().size() == 1, GameCommand.REQUIRE_SINGLE_COMMAND);
|
||||||
final GameCommand command = getSingleCommand(trx, GameCommand.class);
|
final GameCommand command = getSingleCommand(trx, GameCommand.class);
|
||||||
|
|
||||||
switch (command.action) {
|
switch (command.getAction()) {
|
||||||
case GAME_BOARD_SURRENDER:
|
case GAME_BOARD_SURRENDER:
|
||||||
command.validateGameBoardSurrender(trx);
|
command.validateGameBoardSurrender(trx);
|
||||||
break;
|
break;
|
||||||
|
@ -7,6 +7,7 @@ import java.util.Map;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import djmil.cordacheckers.contracts.GameBoardContract;
|
import djmil.cordacheckers.contracts.GameBoardContract;
|
||||||
|
import djmil.cordacheckers.contracts.GameBoardContract.MoveResult;
|
||||||
import djmil.cordacheckers.states.Piece.Color;
|
import djmil.cordacheckers.states.Piece.Color;
|
||||||
import net.corda.v5.base.annotations.ConstructorForDeserialization;
|
import net.corda.v5.base.annotations.ConstructorForDeserialization;
|
||||||
import net.corda.v5.base.types.MemberX500Name;
|
import net.corda.v5.base.types.MemberX500Name;
|
||||||
@ -22,19 +23,22 @@ public class GameBoardState extends GameState {
|
|||||||
super(gameProposalState.whitePlayer, gameProposalState.blackPlayer,
|
super(gameProposalState.whitePlayer, gameProposalState.blackPlayer,
|
||||||
gameProposalState.gameUuid, gameProposalState.message, gameProposalState.participants);
|
gameProposalState.gameUuid, gameProposalState.message, gameProposalState.participants);
|
||||||
|
|
||||||
// Initial GameBoard state
|
this.board = new LinkedHashMap<Integer, Piece>(GameBoardContract.initialBoard);
|
||||||
this.moveColor = Piece.Color.WHITE;
|
this.moveColor = Piece.Color.WHITE;
|
||||||
this.moveNumber = 0;
|
this.moveNumber = 0;
|
||||||
this.board = new LinkedHashMap<Integer, Piece>(initialBoard);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameBoardState(GameBoardState oldGameBoardState, Map<Integer, Piece> newBoard, Piece.Color moveColor) {
|
public GameBoardState(GameBoardState currentGameBoardState, List<Integer> move, String message) {
|
||||||
super(oldGameBoardState.whitePlayer, oldGameBoardState.blackPlayer,
|
super(currentGameBoardState.whitePlayer, currentGameBoardState.blackPlayer, currentGameBoardState.gameUuid,
|
||||||
oldGameBoardState.gameUuid, oldGameBoardState.message, oldGameBoardState.participants);
|
message, currentGameBoardState.participants);
|
||||||
|
|
||||||
this.moveColor = moveColor;
|
final MoveResult moveResult = GameBoardContract.applyMove(move, currentGameBoardState.getBoard(), currentGameBoardState.getMoveColor());
|
||||||
this.moveNumber = oldGameBoardState.getMoveNumber() +1;
|
this.moveColor = moveResult.moveColor;
|
||||||
this.board = newBoard;
|
this.board = moveResult.board;
|
||||||
|
|
||||||
|
this.moveNumber = (currentGameBoardState.moveColor == this.moveColor)
|
||||||
|
? currentGameBoardState.getMoveNumber() // current player has not finished his move jet
|
||||||
|
: currentGameBoardState.getMoveNumber() +1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConstructorForDeserialization
|
@ConstructorForDeserialization
|
||||||
@ -64,33 +68,29 @@ public class GameBoardState extends GameState {
|
|||||||
return board;
|
return board;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move to contract
|
@Override
|
||||||
public final static Map<Integer, Piece> initialBoard = Map.ofEntries(
|
public boolean equals(Object obj) {
|
||||||
// Inspired by Checkers notation rules: https://www.bobnewell.net/nucleus/checkers.php
|
if (this == obj)
|
||||||
Map.entry( 1, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
return true;
|
||||||
Map.entry( 2, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
if (obj == null)
|
||||||
Map.entry( 3, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
return false;
|
||||||
Map.entry( 4, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
if (getClass() != obj.getClass())
|
||||||
Map.entry( 5, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
return false;
|
||||||
Map.entry( 6, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
GameBoardState other = (GameBoardState) obj;
|
||||||
Map.entry( 7, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
if (moveColor != other.moveColor)
|
||||||
Map.entry( 8, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
return false;
|
||||||
Map.entry( 9, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
if (moveNumber == null) {
|
||||||
Map.entry(10, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
if (other.moveNumber != null)
|
||||||
Map.entry(11, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
return false;
|
||||||
Map.entry(12, new Piece(Piece.Color.BLACK, Piece.Type.MAN)),
|
} else if (!moveNumber.equals(other.moveNumber))
|
||||||
|
return false;
|
||||||
|
if (board == null) {
|
||||||
|
if (other.board != null)
|
||||||
|
return false;
|
||||||
|
} else if (!board.equals(other.board))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Map.entry(21, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(22, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(23, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(24, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(25, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(26, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(27, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(28, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(29, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(30, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(31, new Piece(Piece.Color.WHITE, Piece.Type.MAN)),
|
|
||||||
Map.entry(32, new Piece(Piece.Color.WHITE, Piece.Type.MAN))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@ -17,21 +17,14 @@ public class Piece {
|
|||||||
WHITE,
|
WHITE,
|
||||||
BLACK;
|
BLACK;
|
||||||
|
|
||||||
public static Color oppositOf(Color color) {
|
public Color opposite() {
|
||||||
switch (color) {
|
switch (this) {
|
||||||
case WHITE:
|
case WHITE:
|
||||||
return BLACK;
|
return BLACK;
|
||||||
case BLACK:
|
case BLACK:
|
||||||
return WHITE;
|
return WHITE;
|
||||||
default:
|
|
||||||
throw new UnknownException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UnknownException extends RuntimeException {
|
|
||||||
public UnknownException() {
|
|
||||||
super("Unknown Color value");
|
|
||||||
}
|
}
|
||||||
|
throw new RuntimeException("Unknown Color");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,4 +45,20 @@ public class Piece {
|
|||||||
return type;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
package djmil.cordacheckers.gameboard;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import djmil.cordacheckers.contracts.GameCommand;
|
||||||
|
import djmil.cordacheckers.gamestate.CommitSubFlow;
|
||||||
|
import djmil.cordacheckers.gamestate.FlowResponce;
|
||||||
|
import djmil.cordacheckers.gamestate.GetFlow;
|
||||||
|
import djmil.cordacheckers.gamestate.View;
|
||||||
|
import djmil.cordacheckers.gamestate.ViewBuilder;
|
||||||
|
import djmil.cordacheckers.states.GameBoardState;
|
||||||
|
import djmil.cordacheckers.states.GameState;
|
||||||
|
import net.corda.v5.application.flows.ClientRequestBody;
|
||||||
|
import net.corda.v5.application.flows.ClientStartableFlow;
|
||||||
|
import net.corda.v5.application.flows.CordaInject;
|
||||||
|
import net.corda.v5.application.flows.FlowEngine;
|
||||||
|
import net.corda.v5.application.marshalling.JsonMarshallingService;
|
||||||
|
import net.corda.v5.base.annotations.Suspendable;
|
||||||
|
import net.corda.v5.crypto.SecureHash;
|
||||||
|
import net.corda.v5.ledger.utxo.StateAndRef;
|
||||||
|
import net.corda.v5.ledger.utxo.UtxoLedgerService;
|
||||||
|
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
|
||||||
|
|
||||||
|
public class MoveFlow implements ClientStartableFlow{
|
||||||
|
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(MoveFlow.class);
|
||||||
|
|
||||||
|
@CordaInject
|
||||||
|
public JsonMarshallingService jsonMarshallingService;
|
||||||
|
|
||||||
|
@CordaInject
|
||||||
|
public UtxoLedgerService utxoLedgerService;
|
||||||
|
|
||||||
|
@CordaInject
|
||||||
|
public FlowEngine flowEngine;
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
@Override
|
||||||
|
public String call(ClientRequestBody requestBody) {
|
||||||
|
SecureHash utxoTrxId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final MoveFlowArgs args = requestBody.getRequestBodyAs(jsonMarshallingService, MoveFlowArgs.class);
|
||||||
|
|
||||||
|
final GameCommand command = new GameCommand(args.move);
|
||||||
|
|
||||||
|
final StateAndRef<GameState> currntGameBoardSar = this.flowEngine
|
||||||
|
.subFlow(new GetFlow(args.gameUuid));
|
||||||
|
|
||||||
|
final GameBoardState newGameBoard = new GameBoardState(
|
||||||
|
(GameBoardState)currntGameBoardSar.getState().getContractState(),
|
||||||
|
args.move,
|
||||||
|
args.message);
|
||||||
|
|
||||||
|
final UtxoSignedTransaction gameBoardMoveTrx = utxoLedgerService.createTransactionBuilder()
|
||||||
|
.addCommand(command)
|
||||||
|
.addInputState(currntGameBoardSar.getRef())
|
||||||
|
.addOutputState(newGameBoard)
|
||||||
|
.addSignatories(newGameBoard.getParticipants())
|
||||||
|
.setNotary(currntGameBoardSar.getState().getNotaryName())
|
||||||
|
.setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
|
||||||
|
.toSignedTransaction();
|
||||||
|
|
||||||
|
utxoTrxId = this.flowEngine
|
||||||
|
.subFlow(new CommitSubFlow(gameBoardMoveTrx, command.getCounterparty(currntGameBoardSar)));
|
||||||
|
|
||||||
|
final View gameStateView = this.flowEngine
|
||||||
|
.subFlow(new ViewBuilder(utxoTrxId));
|
||||||
|
|
||||||
|
return new FlowResponce(gameStateView, utxoTrxId)
|
||||||
|
.toJsonEncodedString(jsonMarshallingService);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
log.warn(requestBody + " " +e.getClass() +": " +e.getMessage());
|
||||||
|
e.printStackTrace(System.out);
|
||||||
|
return new FlowResponce(e, utxoTrxId)
|
||||||
|
.toJsonEncodedString(jsonMarshallingService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package djmil.cordacheckers.gameboard;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class MoveFlowArgs {
|
||||||
|
public final List<Integer> move;
|
||||||
|
public final UUID gameUuid;
|
||||||
|
public final String message;
|
||||||
|
|
||||||
|
// Serialisation service requires a default constructor
|
||||||
|
public MoveFlowArgs() {
|
||||||
|
this.move = null;
|
||||||
|
this.gameUuid = null;
|
||||||
|
this.message = null;
|
||||||
|
}
|
||||||
|
}
|
@ -21,10 +21,8 @@ import net.corda.v5.application.flows.ClientStartableFlow;
|
|||||||
import net.corda.v5.application.flows.CordaInject;
|
import net.corda.v5.application.flows.CordaInject;
|
||||||
import net.corda.v5.application.flows.FlowEngine;
|
import net.corda.v5.application.flows.FlowEngine;
|
||||||
import net.corda.v5.application.marshalling.JsonMarshallingService;
|
import net.corda.v5.application.marshalling.JsonMarshallingService;
|
||||||
import net.corda.v5.application.membership.MemberLookup;
|
|
||||||
import net.corda.v5.base.annotations.Suspendable;
|
import net.corda.v5.base.annotations.Suspendable;
|
||||||
import net.corda.v5.crypto.SecureHash;
|
import net.corda.v5.crypto.SecureHash;
|
||||||
import net.corda.v5.ledger.common.NotaryLookup;
|
|
||||||
import net.corda.v5.ledger.utxo.StateAndRef;
|
import net.corda.v5.ledger.utxo.StateAndRef;
|
||||||
import net.corda.v5.ledger.utxo.UtxoLedgerService;
|
import net.corda.v5.ledger.utxo.UtxoLedgerService;
|
||||||
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
|
import net.corda.v5.ledger.utxo.transaction.UtxoSignedTransaction;
|
||||||
@ -36,12 +34,6 @@ public class SurrenderFlow implements ClientStartableFlow{
|
|||||||
@CordaInject
|
@CordaInject
|
||||||
public JsonMarshallingService jsonMarshallingService;
|
public JsonMarshallingService jsonMarshallingService;
|
||||||
|
|
||||||
@CordaInject
|
|
||||||
public MemberLookup memberLookup;
|
|
||||||
|
|
||||||
@CordaInject
|
|
||||||
public NotaryLookup notaryLookup;
|
|
||||||
|
|
||||||
@CordaInject
|
@CordaInject
|
||||||
public UtxoLedgerService utxoLedgerService;
|
public UtxoLedgerService utxoLedgerService;
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ public class CommitSubFlowResponder implements ResponderFlow {
|
|||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
GameState getActualGameStateFromTransaction(UtxoLedgerTransaction gameStateTransaction, GameCommand command) {
|
GameState getActualGameStateFromTransaction(UtxoLedgerTransaction gameStateTransaction, GameCommand command) {
|
||||||
switch (command.action) {
|
switch (command.getAction()) {
|
||||||
case GAME_PROPOSAL_CREATE:
|
case GAME_PROPOSAL_CREATE:
|
||||||
return getSingleOutputState(gameStateTransaction, GameProposalState.class);
|
return getSingleOutputState(gameStateTransaction, GameProposalState.class);
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ public class CommitSubFlowResponder implements ResponderFlow {
|
|||||||
return getSingleInputState(gameStateTransaction, GameBoardState.class);
|
return getSingleInputState(gameStateTransaction, GameBoardState.class);
|
||||||
|
|
||||||
case GAME_BOARD_MOVE:
|
case GAME_BOARD_MOVE:
|
||||||
return getSingleOutputState(gameStateTransaction, GameBoardState.class);
|
return getSingleInputState(gameStateTransaction, GameBoardState.class);
|
||||||
|
|
||||||
case GAME_RESULT_CREATE:
|
case GAME_RESULT_CREATE:
|
||||||
return getSingleOutputState(gameStateTransaction, GameResultState.class);
|
return getSingleOutputState(gameStateTransaction, GameResultState.class);
|
||||||
|
@ -17,7 +17,7 @@ public class FlowResponce {
|
|||||||
public FlowResponce(Exception exception, SecureHash transactionId) {
|
public FlowResponce(Exception exception, SecureHash transactionId) {
|
||||||
this.successStatus = null;
|
this.successStatus = null;
|
||||||
this.transactionId = transactionId;
|
this.transactionId = transactionId;
|
||||||
this.failureStatus = exception.getMessage();
|
this.failureStatus = exception.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toJsonEncodedString(JsonMarshallingService jsonMarshallingService) {
|
public String toJsonEncodedString(JsonMarshallingService jsonMarshallingService) {
|
||||||
|
@ -51,7 +51,7 @@ public class ViewBuilder implements SubFlow<View> {
|
|||||||
final GameState state = getLatestGameStateFromTransaction(gameStateUtxo, command);
|
final GameState state = getLatestGameStateFromTransaction(gameStateUtxo, command);
|
||||||
final View.Status viewStatus = action2status(command, state, myName);
|
final View.Status viewStatus = action2status(command, state, myName);
|
||||||
|
|
||||||
switch (command.action) {
|
switch (command.getAction()) {
|
||||||
case GAME_PROPOSAL_CREATE:
|
case GAME_PROPOSAL_CREATE:
|
||||||
case GAME_PROPOSAL_CANCEL:
|
case GAME_PROPOSAL_CANCEL:
|
||||||
case GAME_PROPOSAL_REJECT:
|
case GAME_PROPOSAL_REJECT:
|
||||||
@ -62,7 +62,7 @@ public class ViewBuilder implements SubFlow<View> {
|
|||||||
case GAME_PROPOSAL_ACCEPT:
|
case GAME_PROPOSAL_ACCEPT:
|
||||||
case GAME_BOARD_MOVE:
|
case GAME_BOARD_MOVE:
|
||||||
if (state instanceof GameBoardState)
|
if (state instanceof GameBoardState)
|
||||||
return new View(viewStatus, (GameBoardState)state, command.move, myName);
|
return new View(viewStatus, (GameBoardState)state, command.getMove(), myName);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GAME_BOARD_SURRENDER:
|
case GAME_BOARD_SURRENDER:
|
||||||
@ -85,7 +85,7 @@ public class ViewBuilder implements SubFlow<View> {
|
|||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
GameState getLatestGameStateFromTransaction(UtxoLedgerTransaction gameStateTransaction, GameCommand command) {
|
GameState getLatestGameStateFromTransaction(UtxoLedgerTransaction gameStateTransaction, GameCommand command) {
|
||||||
switch (command.action) {
|
switch (command.getAction()) {
|
||||||
case GAME_PROPOSAL_CREATE:
|
case GAME_PROPOSAL_CREATE:
|
||||||
return getSingleOutputState(gameStateTransaction, GameProposalState.class);
|
return getSingleOutputState(gameStateTransaction, GameProposalState.class);
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ public class ViewBuilder implements SubFlow<View> {
|
|||||||
View.Status action2status(GameCommand command, GameState state, MemberX500Name myName) {
|
View.Status action2status(GameCommand command, GameState state, MemberX500Name myName) {
|
||||||
final boolean myAction = command.getInitiator(state).compareTo(myName) == 0;
|
final boolean myAction = command.getInitiator(state).compareTo(myName) == 0;
|
||||||
|
|
||||||
switch (command.action) {
|
switch (command.getAction()) {
|
||||||
case GAME_PROPOSAL_CREATE:
|
case GAME_PROPOSAL_CREATE:
|
||||||
if (myAction)
|
if (myAction)
|
||||||
return View.Status.GAME_PROPOSAL_WAIT_FOR_OPPONENT;
|
return View.Status.GAME_PROPOSAL_WAIT_FOR_OPPONENT;
|
||||||
|
Loading…
Reference in New Issue
Block a user