From 5168e387100289d90b3f126604a7deb10fbdc5a6 Mon Sep 17 00:00:00 2001 From: djmil Date: Wed, 4 Oct 2023 10:51:36 +0200 Subject: [PATCH] DRAW: request, accept, reject - tests for draw - ranking updates, to suuport draw results - replaca GameCommand.getCounterparty() with GameState.getOpponent(myName) makes code much readable and maintainable - uuid check in a contract - better exception handling in corda flows --- .../cordaclient/CordaClient.java | 34 +++- .../cordaclient/dao/GameState.java | 8 +- .../cordacheckers/cordaclient/dao/Rank.java | 3 +- .../{RspRankList.java => RspRankMap.java} | 2 +- .../cordaclient/GameBoardTests.java | 106 +++++++++++- .../cordaclient/GameProposalTests.java | 5 +- .../cordaclient/GameStateTests.java | 2 +- .../cordaclient/RankingTests.java | 37 ++++- .../contracts/GameBoardContract.java | 24 ++- .../cordacheckers/contracts/GameCommand.java | 156 +++++++++++------- .../cordacheckers/contracts/GameInfo.java | 26 ++- .../contracts/GameResultContract.java | 12 +- .../cordacheckers/states/GameBoardState.java | 27 ++- .../cordacheckers/states/GameResultState.java | 5 +- .../gameboard/DrawAcceptFlow.java | 61 +++++++ .../gameboard/DrawRejectFlow.java | 84 ++++++++++ .../gameboard/DrawRequestFlow.java | 88 ++++++++++ .../cordacheckers/gameboard/MoveFlow.java | 15 +- .../gameboard/SurrenderFlow.java | 2 +- .../cordacheckers/gameboard/VictoryFlow.java | 2 +- .../gameproposal/AcceptFlow.java | 4 +- .../gameproposal/CancelFlow.java | 2 +- .../gameproposal/CreateFlow.java | 2 +- .../gameproposal/RejectFlow.java | 2 +- .../gameresult/GameResultCommiter.java | 22 ++- .../djmil/cordacheckers/gameresult/Rank.java | 16 +- .../cordacheckers/gameresult/RankingFlow.java | 18 +- .../gameresult/RankingFlowResponce.java | 2 +- .../gamestate/ListFlowResponce.java | 2 +- .../djmil/cordacheckers/gamestate/View.java | 6 +- .../cordacheckers/gamestate/ViewBuilder.java | 16 +- 31 files changed, 668 insertions(+), 123 deletions(-) rename backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/{RspRankList.java => RspRankMap.java} (89%) create mode 100644 corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawAcceptFlow.java create mode 100644 corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawRejectFlow.java create mode 100644 corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawRequestFlow.java diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java index 1d7c5ee..f481387 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java @@ -32,7 +32,7 @@ import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameBoardMove; import djmil.cordacheckers.cordaclient.dao.flow.arguments.ReqGameProposalCreate; import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspGameState; import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspGameStateList; -import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspRankList; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.RspRankMap; @Service public class CordaClient { @@ -73,7 +73,7 @@ public class CordaClient { "djmil.cordacheckers.gameresult.RankingFlow", new Req()); - return cordaFlowExecute(holdingIdentity, requestBody, RspRankList.class) + return cordaFlowExecute(holdingIdentity, requestBody, RspRankMap.class) .getResponce(requestBody); } @@ -174,6 +174,36 @@ public class CordaClient { .getResponce(requestBody); } + public GameState gameDrawRequest(HoldingIdentity holdingIdentity, UUID gameUuid) { + final RequestBody requestBody = new RequestBody( + "gd.request-" +UUID.randomUUID(), + "djmil.cordacheckers.gameboard.DrawRequestFlow", + gameUuid); + + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); + } + + public GameState gameDrawAccept(HoldingIdentity holdingIdentity, UUID gameUuid) { + final RequestBody requestBody = new RequestBody( + "gd.accept-" +UUID.randomUUID(), + "djmil.cordacheckers.gameboard.DrawAcceptFlow", + gameUuid); + + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); + } + + public GameState gameDrawReject(HoldingIdentity holdingIdentity, UUID gameUuid) { + final RequestBody requestBody = new RequestBody( + "gd.reject-" +UUID.randomUUID(), + "djmil.cordacheckers.gameboard.DrawRejectFlow", + gameUuid); + + return cordaFlowExecute(holdingIdentity, requestBody, RspGameState.class) + .getResponce(requestBody); + } + private > T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class flowReponceType) { try { final String requestBodyJson = this.jsonMapper diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameState.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameState.java index 48a344c..4793917 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameState.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameState.java @@ -21,10 +21,16 @@ public record GameState( GAME_PROPOSAL_WAIT_FOR_YOU, GAME_PROPOSAL_REJECTED, GAME_PROPOSAL_CANCELED, + GAME_BOARD_WAIT_FOR_OPPONENT, GAME_BOARD_WAIT_FOR_YOU, + + DRAW_REQUEST_WAIT_FOR_OPPONENT, + DRAW_REQUEST_WAIT_FOR_YOU, + GAME_RESULT_YOU_WON, - GAME_RESULT_YOU_LOOSE; + GAME_RESULT_YOU_LOOSE, + GAME_RESULT_DRAW; } public final static Map defaultGameBoard = Map.ofEntries( diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/Rank.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/Rank.java index 8005ffd..18ff3f9 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/Rank.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/Rank.java @@ -2,7 +2,8 @@ package djmil.cordacheckers.cordaclient.dao; public record Rank( Integer gamesPlayed, - Integer gamesWon + Integer gamesWon, + Integer gamesDraw ) { } diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspRankList.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspRankMap.java similarity index 89% rename from backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspRankList.java rename to backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspRankMap.java index 3adb703..712ca51 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspRankList.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/RspRankMap.java @@ -4,7 +4,7 @@ import java.util.Map; import djmil.cordacheckers.cordaclient.dao.Rank; -public record RspRankList( +public record RspRankMap( Map successStatus, String failureStatus) implements Rsp> { diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameBoardTests.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameBoardTests.java index c907f9b..0e0238d 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameBoardTests.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameBoardTests.java @@ -193,7 +193,7 @@ public class GameBoardTests { } @Test - void testVictoryTest1() { + void testVictory_NoStones() { final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName); final var hiBlack = holdingIdentityResolver.getByUsername(blackPlayerName); @@ -217,7 +217,7 @@ public class GameBoardTests { } @Test - void testVictoryTest2() { + void testVictory_StonesBlocked() { final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName); final var hiBlack = holdingIdentityResolver.getByUsername(blackPlayerName); @@ -240,6 +240,108 @@ public class GameBoardTests { assertThat(m1BlackView.moveNumber() == 1); } + @Test + void testDrawRequest() { + final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName); + final var hiBlack = holdingIdentityResolver.getByUsername(blackPlayerName); + + final GameState game = cordaClient.gameProposalCreate(hiWhite, hiBlack, Stone.Color.BLACK, + "Test draw request"); + + assertThat(cordaClient.gameProposalAccept(hiBlack, game.uuid()) + .status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT); + + assertThatThrownBy(() -> { // Black can not request draw, since it is not his turn + cordaClient.gameDrawRequest(hiBlack, game.uuid()); + }); + + final GameState drawReqIssuerView = cordaClient.gameDrawRequest(hiWhite, game.uuid()); + assertThat(drawReqIssuerView.status()).isEqualByComparingTo(Status.DRAW_REQUEST_WAIT_FOR_OPPONENT); + + final GameState drawReqAcquierView = cordaClient.gameStateGet(hiBlack, game.uuid()); + assertThat(drawReqAcquierView.status()).isEqualByComparingTo(Status.DRAW_REQUEST_WAIT_FOR_YOU); + + assertThat(drawReqAcquierView.board()).containsAllEntriesOf(drawReqIssuerView.board()); + + assertThatThrownBy(() -> { + cordaClient.gameBoardMove(hiBlack, game.uuid(), move(10, 15), + "Black can not move since it is not his turn and draw request is pending"); + }); + + assertThatThrownBy(() -> { + cordaClient.gameBoardMove(hiWhite, game.uuid(), move(22, 17), + "White can not move since draw request is pending"); + }); + } + + @Test + void testDrawAccept() { + final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName); + final var hiBlack = holdingIdentityResolver.getByUsername(blackPlayerName); + + final GameState game = cordaClient.gameProposalCreate(hiBlack, hiWhite, Stone.Color.WHITE, + "Draw ACCEPT test"); + + assertThat(cordaClient.gameProposalAccept(hiWhite, game.uuid()) + .status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_YOU); + + assertThatThrownBy(() -> { // Black can not request draw, since it is not his turn + cordaClient.gameDrawRequest(hiBlack, game.uuid()); + }); + + final GameState drawReqIssuerView = cordaClient.gameDrawRequest(hiWhite, game.uuid()); + assertThat(drawReqIssuerView.status()).isEqualByComparingTo(Status.DRAW_REQUEST_WAIT_FOR_OPPONENT); + + assertThatThrownBy(() -> { // White shall not be able to accept own Draw Request + cordaClient.gameDrawAccept(hiWhite, game.uuid()); + }); + + final GameState drawReqAcquierView = cordaClient.gameStateGet(hiBlack, game.uuid()); + assertThat(drawReqAcquierView.status()).isEqualByComparingTo(Status.DRAW_REQUEST_WAIT_FOR_YOU); + + assertThatThrownBy(() -> { // Black shall not be able to send counter Draw Request + cordaClient.gameDrawRequest(hiBlack, game.uuid()); + }); + + final GameState drawAcceptBlackView = cordaClient.gameDrawAccept(hiBlack, game.uuid()); + assertThat(drawAcceptBlackView.status()).isEqualByComparingTo(Status.GAME_RESULT_DRAW); + + final GameState drawAcceptWhiteView = cordaClient.gameStateGet(hiWhite, game.uuid()); + assertThat(drawAcceptWhiteView.status()).isEqualByComparingTo(Status.GAME_RESULT_DRAW); + } + + @Test + void testDrawReject() { + final var hiWhite = holdingIdentityResolver.getByUsername(whitePlayerName); + final var hiBlack = holdingIdentityResolver.getByUsername(blackPlayerName); + + final GameState game = cordaClient.gameProposalCreate(hiWhite, hiBlack, Stone.Color.BLACK, + "Draw REJECT test"); + + assertThat(cordaClient.gameProposalAccept(hiBlack, game.uuid()) + .status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT); + + final GameState drawReqIssuerView = cordaClient.gameDrawRequest(hiWhite, game.uuid()); + assertThat(drawReqIssuerView.status()).isEqualByComparingTo(Status.DRAW_REQUEST_WAIT_FOR_OPPONENT); + + assertThatThrownBy(() -> { // White shall not be able to reject own Draw Request + cordaClient.gameDrawReject(hiWhite, game.uuid()); + }); + + final GameState drawReqAcquierView = cordaClient.gameStateGet(hiBlack, game.uuid()); + assertThat(drawReqAcquierView.status()).isEqualByComparingTo(Status.DRAW_REQUEST_WAIT_FOR_YOU); + + assertThatThrownBy(() -> { // Black shall not be able to send counter Draw Request + cordaClient.gameDrawRequest(hiBlack, game.uuid()); + }); + + final GameState drawRejectBlackView = cordaClient.gameDrawReject(hiBlack, game.uuid()); + assertThat(drawRejectBlackView.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT); + + final GameState drawRejectWhiteView = cordaClient.gameStateGet(hiWhite, game.uuid()); + assertThat(drawRejectWhiteView.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_YOU); + } + ArrayList move(int from, int to) { return new ArrayList(Arrays.asList(from, to)); } diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameProposalTests.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameProposalTests.java index c2b5fc4..c1c5143 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameProposalTests.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameProposalTests.java @@ -61,7 +61,7 @@ public class GameProposalTests { assertThat(acquierGameView.opponentName()).isEqualToIgnoringCase(issuer); assertThat(acquierGameView.opponentColor()).isEqualByComparingTo(acquierColor.opposite()); assertThat(acquierGameView.board()).containsAllEntriesOf(GameState.defaultGameBoard); - assertThat(acquierGameView.previousMove()).isEmpty(); + assertThat(acquierGameView.previousMove()).isNull(); assertThat(acquierGameView.moveNumber() == 0); assertThat(acquierGameView.message()).isEqualToIgnoringCase(message); assertThat(acquierGameView.uuid()).isEqualByComparingTo(issuerGameView.uuid()); @@ -134,8 +134,7 @@ public class GameProposalTests { hiIssuer, hiAcquier, acquierColor, "GameProposal ACCEPT test"); assertThatThrownBy(() -> { // Issuer can not accept - cordaClient.gameProposalAccept( - hiIssuer, game.uuid()); + cordaClient.gameProposalAccept(hiIssuer, game.uuid()); }); final GameState acceptedGameAcquierView = cordaClient.gameProposalAccept( diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameStateTests.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameStateTests.java index 258ff60..b8b230b 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/GameStateTests.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/GameStateTests.java @@ -36,7 +36,7 @@ public class GameStateTests { final UUID gameUuid = p1GameView.uuid(); /* - * Both players shall be able to find newly created GameProposal aka GameList + * Both players shall be able to find newly created GameProposal within GameList */ final List p1GameList = cordaClient.gameStateList(hiPlayer1); final List p2GameList = cordaClient.gameStateList(hiPlayer2); diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/RankingTests.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/RankingTests.java index b376a7e..ae34f5c 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/RankingTests.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/RankingTests.java @@ -13,8 +13,8 @@ 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.Stone; import djmil.cordacheckers.cordaclient.dao.Rank; +import djmil.cordacheckers.cordaclient.dao.Stone; import djmil.cordacheckers.user.HoldingIdentityResolver; @SpringBootTest @@ -25,8 +25,8 @@ public class RankingTests { @Autowired HoldingIdentityResolver holdingIdentityResolver; - final String player1 = "kumar"; - final String player2 = "bobik"; + final String player1 = "Kumar"; + final String player2 = "Bobik"; 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); @@ -60,9 +60,11 @@ public class RankingTests { assertThat(liderboard1.get(winnerName).gamesWon() +1 == liderboard2.get(winnerName).gamesWon() ); assertThat(liderboard1.get(winnerName).gamesPlayed() +1 == liderboard2.get(winnerName).gamesPlayed()); + assertThat(liderboard1.get(winnerName).gamesDraw() == liderboard2.get(winnerName).gamesDraw() ); assertThat(liderboard1.get(losserName).gamesWon() == liderboard2.get(losserName).gamesWon() ); assertThat(liderboard1.get(losserName).gamesPlayed() +1 == liderboard2.get(losserName).gamesPlayed()); + assertThat(liderboard1.get(losserName).gamesDraw() == liderboard2.get(losserName).gamesDraw() ); } @Test @@ -87,9 +89,38 @@ public class RankingTests { assertThat(liderboard1.get(winnerName).gamesWon() +1 == liderboard2.get(winnerName).gamesWon() ); assertThat(liderboard1.get(winnerName).gamesPlayed() +1 == liderboard2.get(winnerName).gamesPlayed()); + assertThat(liderboard1.get(winnerName).gamesDraw() == liderboard2.get(winnerName).gamesDraw() ); assertThat(liderboard1.get(losserName).gamesWon() == liderboard2.get(losserName).gamesWon() ); assertThat(liderboard1.get(losserName).gamesPlayed() +1 == liderboard2.get(losserName).gamesPlayed()); + assertThat(liderboard1.get(losserName).gamesDraw() == liderboard2.get(losserName).gamesDraw() ); + } + + @Test + void testDraw() throws InvalidNameException { + final var hiCustodian = holdingIdentityResolver.getCustodian(); + assertThat(hiCustodian).isNotNull(); + + final var hiPlayer1 = holdingIdentityResolver.getByUsername(player1); + final var hiPlayer2 = holdingIdentityResolver.getByUsername(player2); + + final GameState game = cordaClient.gameProposalCreate(hiPlayer1, hiPlayer2, Stone.Color.WHITE, + "GameBoard GLOBAL_RANKING draw test"); + + cordaClient.gameProposalAccept(hiPlayer2, game.uuid()); + cordaClient.gameDrawRequest(hiPlayer2, game.uuid()); + + final Map liderboard1 = cordaClient.fetchRanking(hiCustodian); + cordaClient.gameDrawAccept(hiPlayer1, game.uuid()); + final Map liderboard2 = cordaClient.fetchRanking(hiCustodian); + + assertThat(liderboard1.get(player1).gamesWon() == liderboard2.get(player1).gamesWon() ); + assertThat(liderboard1.get(player1).gamesPlayed() +1 == liderboard2.get(player1).gamesPlayed()); + assertThat(liderboard1.get(player1).gamesDraw() +1 == liderboard2.get(player1).gamesDraw() ); + + assertThat(liderboard1.get(player2).gamesWon() == liderboard2.get(player2).gamesWon() ); + assertThat(liderboard1.get(player2).gamesPlayed() +1 == liderboard2.get(player2).gamesPlayed()); + assertThat(liderboard1.get(player2).gamesDraw() +1 == liderboard2.get(player2).gamesDraw() ); } @Test diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java index 34392d2..604a2d7 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java @@ -24,16 +24,28 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { command.validateGameProposalAccept(trx); break; - case GAME_BOARD_MOVE: - command.validateGameBoardMove(trx, command.getMove()); + case MOVE: + command.validateMove(trx, command.getMove()); break; - case GAME_BOARD_SURRENDER: - command.validateGameBoardSurrender(trx); + case SURRENDER: + command.validateSurrender(trx); break; - case GAME_BOARD_VICTORY: - command.validateGameBoardVictory(trx); + case CLAIM_VICTORY: + command.validateClaimVictory(trx); + break; + + case DRAW_REQUEST: + command.validateDrawRequest(trx); + break; + + case DRAW_ACCEPT: + command.validateDrawAcquire(trx); + break; + + case DRAW_REJECT: + command.validateDrawDecline(trx); break; default: diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameCommand.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameCommand.java index dc66a6a..081a588 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameCommand.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameCommand.java @@ -9,12 +9,9 @@ import djmil.cordacheckers.checkers.Move; import djmil.cordacheckers.states.GameBoardState; import djmil.cordacheckers.states.GameProposalState; import djmil.cordacheckers.states.GameResultState; -import djmil.cordacheckers.states.GameState; import net.corda.v5.base.annotations.ConstructorForDeserialization; import net.corda.v5.base.annotations.CordaSerializable; -import net.corda.v5.base.types.MemberX500Name; import net.corda.v5.ledger.utxo.Command; -import net.corda.v5.ledger.utxo.StateAndRef; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; public class GameCommand implements Command { @@ -25,9 +22,12 @@ public class GameCommand implements Command { GAME_PROPOSAL_ACCEPT, GAME_PROPOSAL_REJECT, - GAME_BOARD_MOVE, - GAME_BOARD_SURRENDER, - GAME_BOARD_VICTORY; + MOVE, + DRAW_REQUEST, + DRAW_ACCEPT, + DRAW_REJECT, + SURRENDER, + CLAIM_VICTORY; } private final Action action; @@ -46,7 +46,7 @@ public class GameCommand implements Command { } public GameCommand(Action action) { - if (action == Action.GAME_BOARD_MOVE) + if (action == Action.MOVE) throw new ActionException(); this.action = action; @@ -54,7 +54,7 @@ public class GameCommand implements Command { } public GameCommand(List move) { - this.action = Action.GAME_BOARD_MOVE; + this.action = Action.MOVE; this.move = move; } @@ -72,46 +72,6 @@ public class GameCommand implements Command { return move; } - public MemberX500Name getCounterparty(StateAndRef gameStateSar) { - return getCounterparty(gameStateSar.getState().getContractState()); - } - - public MemberX500Name getCounterparty(GameState gameState) { - switch (this.action) { - case GAME_PROPOSAL_CREATE: - case GAME_PROPOSAL_CANCEL: - if (gameState instanceof GameProposalState) - return ((GameProposalState)gameState).getAcquierName(); - break; - - case GAME_PROPOSAL_ACCEPT: - case GAME_PROPOSAL_REJECT: - if (gameState instanceof GameProposalState) - return ((GameProposalState)gameState).getIssuerName(); - break; - - case GAME_BOARD_MOVE: - if (gameState instanceof GameBoardState) - return ((GameBoardState)gameState).getIdelPlayerName(); - break; - - case GAME_BOARD_SURRENDER: - if (gameState instanceof GameResultState) - return ((GameResultState)gameState).getWinnerName(); - break; - - case GAME_BOARD_VICTORY: - if (gameState instanceof GameResultState) - return ((GameResultState)gameState).getLooserName(); - break; - - default: - throw new ActionException(); - } - - throw new RuntimeException(action +": unexpected GameState type " +gameState.getClass()); - } - public void validateGameProposalCreate(UtxoLedgerTransaction trx) { requireThat(trx.getInputContractStates().isEmpty(), CREATE_INPUT_STATE); requireThat(trx.getOutputContractStates().size() == 1, CREATE_OUTPUT_STATE); @@ -119,7 +79,7 @@ public class GameCommand implements Command { final GameProposalState gameProposal = getSingleOutputState(trx, GameProposalState.class); /* - * Major command logick check + * Major command logic check */ requireThat(gameProposal.getIssuerName().compareTo(gameProposal.getBlackPlayer()) == 0 || gameProposal.getIssuerName().compareTo(gameProposal.getWhitePlayer()) == 0, @@ -136,9 +96,10 @@ public class GameCommand implements Command { requireThat(outGameBoard.getWhitePlayer().compareTo(inGameProposal.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS); requireThat(outGameBoard.getBlackPlayer().compareTo(inGameProposal.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS); requireThat(outGameBoard.getParticipants().containsAll(inGameProposal.getParticipants()), IN_OUT_PARTICIPANTS); + requireThat(outGameBoard.getGameUuid().compareTo(inGameProposal.getGameUuid()) == 0, UUID_MUST_NOT_CHANGE); /* - * Major command logick check + * Major command logic check */ requireThat(outGameBoard.getBoard().size() > 0, "GameBoard initial state was not found"); } @@ -157,7 +118,7 @@ public class GameCommand implements Command { getSingleInputState(trx, GameProposalState.class); } - public void validateGameBoardMove(UtxoLedgerTransaction trx, List move) { + public void validateMove(UtxoLedgerTransaction trx, List move) { requireThat(trx.getInputContractStates().size() == 1, MOVE_INPUT_STATE); requireThat(trx.getOutputContractStates().size() == 1, MOVE_OUTPUT_STATE); @@ -167,15 +128,18 @@ public class GameCommand implements Command { requireThat(outGameBoard.getWhitePlayer().compareTo(inGameBoard.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS); requireThat(outGameBoard.getBlackPlayer().compareTo(inGameBoard.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS); requireThat(outGameBoard.getParticipants().containsAll(inGameBoard.getParticipants()), IN_OUT_PARTICIPANTS); + requireThat(outGameBoard.getGameUuid().compareTo(inGameBoard.getGameUuid()) == 0, UUID_MUST_NOT_CHANGE); /* - * Major command logick check + * Major command logic check */ final var newGameBoard = new GameBoardState(inGameBoard, move, outGameBoard.getMessage()); requireThat(outGameBoard.equals(newGameBoard), "Unexpected output state"); + requireThat(inGameBoard.isDrawRequested() == false, "Draw was requested"); + requireThat(outGameBoard.isDrawRequested() == false, "Draw can not be requested during move"); } - public void validateGameBoardSurrender(UtxoLedgerTransaction trx) { + public void validateSurrender(UtxoLedgerTransaction trx) { requireThat(trx.getInputContractStates().size() == 1, SURRENDER_INPUT_STATE); requireThat(trx.getOutputContractStates().size() == 1, SURRENDER_OUTPUT_STATE); @@ -185,18 +149,19 @@ public class GameCommand implements Command { requireThat(outGameResult.getWhitePlayer().compareTo(inGameBoard.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS); requireThat(outGameResult.getBlackPlayer().compareTo(inGameBoard.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS); requireThat(outGameResult.getParticipants().containsAll(inGameBoard.getParticipants()), IN_OUT_PARTICIPANTS); + requireThat(outGameResult.getGameUuid().compareTo(inGameBoard.getGameUuid()) == 0, UUID_MUST_NOT_CHANGE); /* - * Major command logick check + * Major command logic check */ requireThat(outGameResult.getLooserName().compareTo(outGameResult.getBlackPlayer()) == 0 || outGameResult.getLooserName().compareTo(outGameResult.getWhitePlayer()) == 0, "Surenderer must be either Black or White player"); - requireThat(outGameResult.getTotalMoves() == inGameBoard.getMoveNumber(), GAME_RESULT_MOVES); + requireThat(outGameResult.getTotalMoves() == inGameBoard.getMoveNumber(), BAD_TOTAL_MOVES); } - public void validateGameBoardVictory(UtxoLedgerTransaction trx) { + public void validateClaimVictory(UtxoLedgerTransaction trx) { requireThat(trx.getInputContractStates().size() == 1, VICTORY_INPUT_STATE); requireThat(trx.getOutputContractStates().size() == 1, VICTORY_OUTPUT_STATE); @@ -206,15 +171,75 @@ public class GameCommand implements Command { requireThat(outGameResult.getWhitePlayer().compareTo(inGameBoard.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS); requireThat(outGameResult.getBlackPlayer().compareTo(inGameBoard.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS); requireThat(outGameResult.getParticipants().containsAll(inGameBoard.getParticipants()), IN_OUT_PARTICIPANTS); + requireThat(outGameResult.getGameUuid().compareTo(inGameBoard.getGameUuid()) == 0, UUID_MUST_NOT_CHANGE); /* - * Major command logick check + * Major command logic check */ final var possibleMoves = Move.getPossibleMoves(inGameBoard.getBoard(), inGameBoard.getActiveColor()); requireThat(possibleMoves.isEmpty(), "Victory condition violation"); requireThat(outGameResult.getWinnerName().compareTo(inGameBoard.getIdelPlayerName()) == 0, "Bad winner name"); - requireThat(outGameResult.getTotalMoves() == inGameBoard.getMoveNumber(), GAME_RESULT_MOVES); + requireThat(outGameResult.getTotalMoves() == inGameBoard.getMoveNumber(), BAD_TOTAL_MOVES); + } + + public void validateDrawRequest(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, Action.DRAW_REQUEST + BAD_IN_STATE_SIZE); + requireThat(trx.getOutputContractStates().size() == 1, Action.DRAW_REQUEST + BAD_OUT_STATE_SIZE); + + final var inGameBoard = getSingleInputState (trx, GameBoardState.class); + final var outGameBoard = getSingleOutputState(trx, GameBoardState.class); + + requireThat(outGameBoard.getWhitePlayer().compareTo(inGameBoard.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS); + requireThat(outGameBoard.getBlackPlayer().compareTo(inGameBoard.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS); + requireThat(outGameBoard.getParticipants().containsAll(inGameBoard.getParticipants()), IN_OUT_PARTICIPANTS); + requireThat(outGameBoard.getGameUuid().compareTo(inGameBoard.getGameUuid()) == 0, UUID_MUST_NOT_CHANGE); + + /* + * Major command logic check + */ + requireThat(isGameBoardEqual(inGameBoard, outGameBoard) == true, GAME_BOARD_STATE_MUST_NOT_CHANGE); + requireThat(inGameBoard.getDrawRequest() == null, "Draw can be requested only once per move"); + requireThat(outGameBoard.isDrawRequested() == true, DRAW_REQ_EXPECTED); + } + + public void validateDrawDecline(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, Action.DRAW_REJECT + BAD_IN_STATE_SIZE); + requireThat(trx.getOutputContractStates().size() == 1, Action.DRAW_REJECT + BAD_OUT_STATE_SIZE); + + final var inGameBoard = getSingleInputState (trx, GameBoardState.class); + final var outGameBoard = getSingleOutputState(trx, GameBoardState.class); + + requireThat(outGameBoard.getWhitePlayer().compareTo(inGameBoard.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS); + requireThat(outGameBoard.getBlackPlayer().compareTo(inGameBoard.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS); + requireThat(outGameBoard.getParticipants().containsAll(inGameBoard.getParticipants()), IN_OUT_PARTICIPANTS); + requireThat(outGameBoard.getGameUuid().compareTo(inGameBoard.getGameUuid()) == 0, UUID_MUST_NOT_CHANGE); + + /* + * Major command logic check + */ + requireThat(isGameBoardEqual(inGameBoard, outGameBoard) == true, GAME_BOARD_STATE_MUST_NOT_CHANGE); + requireThat(inGameBoard.isDrawRequested() == true, DRAW_REQ_EXPECTED); + requireThat(outGameBoard.isDrawRequested() == false, "Draw decline expected"); + } + + public void validateDrawAcquire(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, Action.DRAW_ACCEPT + BAD_IN_STATE_SIZE); + requireThat(trx.getOutputContractStates().size() == 1, Action.DRAW_ACCEPT + BAD_OUT_STATE_SIZE); + + final var inGameBoard = getSingleInputState (trx, GameBoardState.class); + final var outGameResult = getSingleOutputState(trx, GameResultState.class); + + requireThat(outGameResult.getWhitePlayer().compareTo(inGameBoard.getWhitePlayer()) == 0, IN_OUT_PARTICIPANTS); + requireThat(outGameResult.getBlackPlayer().compareTo(inGameBoard.getBlackPlayer()) == 0, IN_OUT_PARTICIPANTS); + requireThat(outGameResult.getParticipants().containsAll(inGameBoard.getParticipants()), IN_OUT_PARTICIPANTS); + requireThat(outGameResult.getGameUuid().compareTo(inGameBoard.getGameUuid()) == 0, UUID_MUST_NOT_CHANGE); + + /* + * Major command logic check + */ + requireThat(inGameBoard.isDrawRequested() == true, DRAW_REQ_EXPECTED); + requireThat(outGameResult.getWinnerName() == null, "Draw can not have winner"); } public static void requireThat(boolean asserted, String errorMessage) { @@ -223,6 +248,12 @@ public class GameCommand implements Command { } } + private boolean isGameBoardEqual(GameBoardState a, GameBoardState b) { + return a.getBoard().equals(b.getBoard()) && + a.getActiveColor() == b.getActiveColor() && + a.getMoveNumber() == b.getMoveNumber(); + } + static final String REQUIRE_SINGLE_COMMAND = "Require a single command"; static final String IN_OUT_PARTICIPANTS = "Output participants should include all input participants"; @@ -249,5 +280,12 @@ public class GameCommand implements Command { static final String VICTORY_INPUT_STATE = "VICTORY command should have exactly one GameBoardState input state"; static final String VICTORY_OUTPUT_STATE = "VICTORY command should have exactly one GameResultState output state"; - static final String GAME_RESULT_MOVES = "Wrong number of total moves"; + static final String BAD_TOTAL_MOVES = "Wrong number of total moves"; + + static final String BAD_IN_STATE_SIZE = " - wrong input state size"; + static final String BAD_OUT_STATE_SIZE = " - wrong output state size"; + + static final String DRAW_REQ_EXPECTED = "Draw request expected"; + static final String GAME_BOARD_STATE_MUST_NOT_CHANGE = "GameBoard state must not be changed"; + static final String UUID_MUST_NOT_CHANGE = "UUID must not be changed"; } diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameInfo.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameInfo.java index 506132a..c9b88ea 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameInfo.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameInfo.java @@ -47,19 +47,37 @@ public class GameInfo { this.actor = ((GameBoardState)this.state).getActivePlayerName(); return; - case GAME_BOARD_MOVE: + case MOVE: this.state = getSingleOutputState(utxoTrx, GameBoardState.class); this.issuer = getSingleInputState(utxoTrx, GameBoardState.class).getActivePlayerName(); this.actor = ((GameBoardState)this.state).getActivePlayerName(); return; - - case GAME_BOARD_VICTORY: + + case DRAW_REQUEST: + this.state = getSingleOutputState(utxoTrx, GameBoardState.class); + this.issuer = ((GameBoardState)this.state).getActivePlayerName(); + this.actor = ((GameBoardState)this.state).getIdelPlayerName(); + return; + + case DRAW_REJECT: + this.state = getSingleOutputState(utxoTrx, GameBoardState.class); + this.issuer = ((GameBoardState)this.state).getIdelPlayerName(); + this.actor = ((GameBoardState)this.state).getActivePlayerName(); + return; + + case DRAW_ACCEPT: + this.state = getSingleOutputState(utxoTrx, GameResultState.class); + this.issuer = getSingleInputState(utxoTrx, GameBoardState.class).getIdelPlayerName(); + this.actor = null; + return; + + case CLAIM_VICTORY: this.state = getSingleOutputState(utxoTrx, GameResultState.class); this.issuer = ((GameResultState)this.state).getWinnerName(); this.actor = null; return; - case GAME_BOARD_SURRENDER: + case SURRENDER: this.state = getSingleOutputState(utxoTrx, GameResultState.class); this.issuer = ((GameResultState)this.state).getLooserName(); this.actor = null; diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java index 2b27a75..d328633 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java @@ -20,12 +20,16 @@ public class GameResultContract implements net.corda.v5.ledger.utxo.Contract { final GameCommand command = getSingleCommand(trx, GameCommand.class); switch (command.getAction()) { - case GAME_BOARD_SURRENDER: - command.validateGameBoardSurrender(trx); + case SURRENDER: + command.validateSurrender(trx); break; - case GAME_BOARD_VICTORY: - command.validateGameBoardVictory(trx); + case CLAIM_VICTORY: + command.validateClaimVictory(trx); + break; + + case DRAW_ACCEPT: + command.validateDrawAcquire(trx); break; default: diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java index 478a9b0..a683a09 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java @@ -7,6 +7,8 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import org.jetbrains.annotations.Nullable; + import djmil.cordacheckers.checkers.Move; import djmil.cordacheckers.checkers.Stone; import djmil.cordacheckers.contracts.GameBoardContract; @@ -18,6 +20,7 @@ import net.corda.v5.ledger.utxo.BelongsToContract; public class GameBoardState extends GameState { private final Stone.Color activeColor; private final Integer moveNumber; + private final Boolean drawRequest; private final Map board; public GameBoardState(GameProposalState gameProposalState) { @@ -27,6 +30,7 @@ public class GameBoardState extends GameState { this.board = gameProposalState.getBoard(); this.activeColor = Stone.Color.WHITE; this.moveNumber = 0; + this.drawRequest = null; } public GameBoardState(GameBoardState currentGameBoardState, List move, String message) { @@ -39,10 +43,21 @@ public class GameBoardState extends GameState { this.moveNumber = (currentGameBoardState.activeColor == this.activeColor) ? currentGameBoardState.getMoveNumber() // current player has not finished his move jet : currentGameBoardState.getMoveNumber() +1; + this.drawRequest = null; + } + + public GameBoardState(GameBoardState currentGameBoardState, Boolean drawRequest) { + super(currentGameBoardState.whitePlayer, currentGameBoardState.blackPlayer, currentGameBoardState.gameUuid, + null, currentGameBoardState.participants); + + this.board = currentGameBoardState.getBoard(); + this.activeColor = currentGameBoardState.activeColor; + this.moveNumber = currentGameBoardState.moveNumber; + this.drawRequest = drawRequest; } @ConstructorForDeserialization - public GameBoardState(MemberX500Name whitePlayer, MemberX500Name blackPlayer, + public GameBoardState(MemberX500Name whitePlayer, MemberX500Name blackPlayer, Boolean drawRequest, Stone.Color activeColor, Integer moveNumber, Map board, String message, UUID gameUuid, List participants) { super(whitePlayer, blackPlayer, gameUuid, message, participants); @@ -50,6 +65,7 @@ public class GameBoardState extends GameState { this.activeColor = activeColor; this.moveNumber = moveNumber; this.board = board; + this.drawRequest = drawRequest; } public Stone.Color getActiveColor() { @@ -72,6 +88,15 @@ public class GameBoardState extends GameState { return Collections.unmodifiableMap(board); } + @Nullable + public Boolean getDrawRequest() { + return drawRequest; + } + + public Boolean isDrawRequested() { + return drawRequest != null && drawRequest == true; + } + @Override public boolean equals(Object obj) { if (this == obj) diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java index b196896..64785ee 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java @@ -4,6 +4,8 @@ import java.security.PublicKey; import java.util.List; import java.util.UUID; +import org.jetbrains.annotations.Nullable; + import djmil.cordacheckers.contracts.GameResultContract; import net.corda.v5.base.annotations.ConstructorForDeserialization; import net.corda.v5.base.types.MemberX500Name; @@ -12,7 +14,7 @@ import net.corda.v5.ledger.utxo.BelongsToContract; @BelongsToContract(GameResultContract.class) public class GameResultState extends GameState { - private final MemberX500Name winnerName; + private final MemberX500Name winnerName; // NULL if it is a draw private final Integer totalMoves; @ConstructorForDeserialization @@ -29,6 +31,7 @@ public class GameResultState extends GameState { this.totalMoves = gameBoardState.getMoveNumber(); } + @Nullable public MemberX500Name getWinnerName() { return winnerName; } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawAcceptFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawAcceptFlow.java new file mode 100644 index 0000000..8d4446f --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawAcceptFlow.java @@ -0,0 +1,61 @@ +package djmil.cordacheckers.gameboard; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gameresult.GameResultCommiter; +import djmil.cordacheckers.gamestate.FlowResponce; +import djmil.cordacheckers.gamestate.View; +import djmil.cordacheckers.gamestate.ViewBuilder; +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.UtxoLedgerService; + +public class DrawAcceptFlow implements ClientStartableFlow{ + + private final static Logger log = LoggerFactory.getLogger(DrawAcceptFlow.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 GameCommand command = new GameCommand(GameCommand.Action.DRAW_ACCEPT); + final UUID gameUuid = UUID.fromString(requestBody.getRequestBody()); + + utxoTrxId = this.flowEngine + .subFlow(new GameResultCommiter(gameUuid, command)); + + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(utxoTrxId)); + + return new FlowResponce(gameStateView, utxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn(requestBody + " [ERROR] " +e.toString()); + e.printStackTrace(); + return new FlowResponce(e, utxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawRejectFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawRejectFlow.java new file mode 100644 index 0000000..e77323b --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawRejectFlow.java @@ -0,0 +1,84 @@ +package djmil.cordacheckers.gameboard; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gamestate.CommitTrx; +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 DrawRejectFlow implements ClientStartableFlow{ + + private final static Logger log = LoggerFactory.getLogger(DrawRejectFlow.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 GameCommand command = new GameCommand(GameCommand.Action.DRAW_REJECT); + final UUID gameUuid = UUID.fromString(requestBody.getRequestBody()); + + final StateAndRef currentGameSar = this.flowEngine + .subFlow(new GetFlow(gameUuid)); + final GameBoardState currenGameBoardState = (GameBoardState)currentGameSar.getState().getContractState(); + + final GameBoardState newGameBoard = new GameBoardState( + currenGameBoardState, + false); // <<-- Decline draw request + + final UtxoSignedTransaction drawDeclineTrx = utxoLedgerService.createTransactionBuilder() + .addCommand(command) + .addInputState(currentGameSar.getRef()) + .addOutputState(newGameBoard) + .addSignatories(newGameBoard.getParticipants()) + .setNotary(currentGameSar.getState().getNotaryName()) + .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .toSignedTransaction(); + + utxoTrxId = this.flowEngine + .subFlow(new CommitTrx(drawDeclineTrx, currenGameBoardState.getActivePlayerName())); + + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(utxoTrxId)); + + return new FlowResponce(gameStateView, utxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn(requestBody + " [ERROR] " +e.toString()); + return new FlowResponce(e, utxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawRequestFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawRequestFlow.java new file mode 100644 index 0000000..f7866a8 --- /dev/null +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/DrawRequestFlow.java @@ -0,0 +1,88 @@ +package djmil.cordacheckers.gameboard; + +import java.time.Duration; +import java.time.Instant; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import djmil.cordacheckers.contracts.GameCommand; +import djmil.cordacheckers.gamestate.CommitTrx; +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.application.membership.MemberLookup; +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 DrawRequestFlow implements ClientStartableFlow{ + + private final static Logger log = LoggerFactory.getLogger(DrawRequestFlow.class); + + @CordaInject + public JsonMarshallingService jsonMarshallingService; + + @CordaInject + public MemberLookup memberLookup; + + @CordaInject + public UtxoLedgerService utxoLedgerService; + + @CordaInject + public FlowEngine flowEngine; + + @Suspendable + @Override + public String call(ClientRequestBody requestBody) { + SecureHash utxoTrxId = null; + + try { + final GameCommand command = new GameCommand(GameCommand.Action.DRAW_REQUEST); + final UUID gameUuid = UUID.fromString(requestBody.getRequestBody()); + + final StateAndRef currentGameSar = this.flowEngine + .subFlow(new GetFlow(gameUuid)); + final GameBoardState currenGameBoardState = (GameBoardState)currentGameSar.getState().getContractState(); + + final GameBoardState newGameBoard = new GameBoardState( + currenGameBoardState, + true); // <<-- draw request + + final UtxoSignedTransaction drawReqTrx = utxoLedgerService.createTransactionBuilder() + .addCommand(command) + .addInputState(currentGameSar.getRef()) + .addOutputState(newGameBoard) + .addSignatories(newGameBoard.getParticipants()) + .setNotary(currentGameSar.getState().getNotaryName()) + .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) + .toSignedTransaction(); + + utxoTrxId = this.flowEngine + .subFlow(new CommitTrx(drawReqTrx, currenGameBoardState.getIdelPlayerName())); + + final View gameStateView = this.flowEngine + .subFlow(new ViewBuilder(utxoTrxId)); + + return new FlowResponce(gameStateView, utxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + catch (Exception e) { + log.warn(requestBody + " [ERROR] " +e.toString()); + return new FlowResponce(e, utxoTrxId) + .toJsonEncodedString(jsonMarshallingService); + } + } + +} diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlow.java index 6a18ae3..835415d 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/MoveFlow.java @@ -43,7 +43,7 @@ public class MoveFlow implements ClientStartableFlow{ public FlowEngine flowEngine; @CordaInject - public MemberLookup memberLookup; + public MemberLookup memberLookup; @Suspendable @Override @@ -55,32 +55,33 @@ public class MoveFlow implements ClientStartableFlow{ final GameCommand command = new GameCommand(args.move); - final StateAndRef currenrGameSar = this.flowEngine + final StateAndRef currentGameSar = this.flowEngine .subFlow(new GetFlow(args.gameUuid)); + final GameBoardState currenGameBoardState = (GameBoardState)currentGameSar.getState().getContractState(); final GameBoardState newGameBoard = new GameBoardState( - (GameBoardState)currenrGameSar.getState().getContractState(), + currenGameBoardState, args.move, args.message); final UtxoSignedTransaction moveTrx = utxoLedgerService.createTransactionBuilder() .addCommand(command) - .addInputState(currenrGameSar.getRef()) + .addInputState(currentGameSar.getRef()) .addOutputState(newGameBoard) .addSignatories(newGameBoard.getParticipants()) - .setNotary(currenrGameSar.getState().getNotaryName()) + .setNotary(currentGameSar.getState().getNotaryName()) .setTimeWindowUntil(Instant.now().plusMillis(Duration.ofDays(1).toMillis())) .toSignedTransaction(); utxoTrxId = this.flowEngine - .subFlow(new CommitTrx(moveTrx, command.getCounterparty(currenrGameSar))); + .subFlow(new CommitTrx(moveTrx, currenGameBoardState.getIdelPlayerName())); if (amIwon(newGameBoard)) { log.info("Opponent has no possible moves. Claim victory!"); utxoTrxId = this.flowEngine .subFlow(new GameResultCommiter( newGameBoard.getGameUuid(), - new GameCommand(GameCommand.Action.GAME_BOARD_VICTORY))); + new GameCommand(GameCommand.Action.CLAIM_VICTORY))); } final View gameStateView = this.flowEngine diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java index 2412003..0730a87 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/SurrenderFlow.java @@ -38,7 +38,7 @@ public class SurrenderFlow implements ClientStartableFlow{ SecureHash utxoTrxId = null; try { - final GameCommand command = new GameCommand(GameCommand.Action.GAME_BOARD_SURRENDER); + final GameCommand command = new GameCommand(GameCommand.Action.SURRENDER); final UUID gameUuid = UUID.fromString(requestBody.getRequestBody()); utxoTrxId = this.flowEngine diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/VictoryFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/VictoryFlow.java index 0100591..b3e31ec 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/VictoryFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/VictoryFlow.java @@ -39,7 +39,7 @@ public class VictoryFlow implements ClientStartableFlow { try { final UUID gameUuid = UUID.fromString(requestBody.getRequestBody()); - final GameCommand command = new GameCommand(GameCommand.Action.GAME_BOARD_VICTORY); + final GameCommand command = new GameCommand(GameCommand.Action.CLAIM_VICTORY); utxoTrxId = this.flowEngine .subFlow(new GameResultCommiter(gameUuid, command)); diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/AcceptFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/AcceptFlow.java index 63cc45c..6914f54 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/AcceptFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/AcceptFlow.java @@ -56,7 +56,7 @@ public class AcceptFlow implements ClientStartableFlow{ final GameBoardState gameBoard = new GameBoardState(gameProposal); // <<-- accepted - final UtxoSignedTransaction acceptTrx = utxoLedgerService.createTransactionBuilder() + final UtxoSignedTransaction gameProposalAcceptTrx = utxoLedgerService.createTransactionBuilder() .addCommand(command) .addInputState(gameProposalSar.getRef()) .addOutputState(gameBoard) @@ -66,7 +66,7 @@ public class AcceptFlow implements ClientStartableFlow{ .toSignedTransaction(); utxoTrxId = this.flowEngine - .subFlow(new CommitTrx(acceptTrx, command.getCounterparty(gameProposal))); + .subFlow(new CommitTrx(gameProposalAcceptTrx, gameProposal.getIssuerName())); final View gameView = this.flowEngine .subFlow(new ViewBuilder(utxoTrxId)); diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CancelFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CancelFlow.java index ff7fdc2..07b2164 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CancelFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CancelFlow.java @@ -62,7 +62,7 @@ public class CancelFlow implements ClientStartableFlow{ .toSignedTransaction(); utxoTrxId = this.flowEngine - .subFlow(new CommitTrx(gameProposalCancelTrx, command.getCounterparty(gameProposal))); + .subFlow(new CommitTrx(gameProposalCancelTrx, gameProposal.getAcquierName())); final View gameStateView = this.flowEngine .subFlow(new ViewBuilder(utxoTrxId)); diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java index 568fddf..807f66b 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CreateFlow.java @@ -71,7 +71,7 @@ public class CreateFlow implements ClientStartableFlow{ .toSignedTransaction(); utxoTrxId = this.flowEngine - .subFlow(new CommitTrx(gameProposalCreateTrx, command.getCounterparty(gameProposal))); + .subFlow(new CommitTrx(gameProposalCreateTrx, gameProposal.getAcquierName())); final View gameView = this.flowEngine .subFlow(new ViewBuilder(utxoTrxId)); diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/RejectFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/RejectFlow.java index 0d8fabe..2ef6c38 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/RejectFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/RejectFlow.java @@ -70,7 +70,7 @@ public class RejectFlow implements ClientStartableFlow{ .toSignedTransaction(); utxoTrxId = this.flowEngine - .subFlow(new CommitTrx(gameProposalRejectTrx, command.getCounterparty(gameProposal))); + .subFlow(new CommitTrx(gameProposalRejectTrx, gameProposal.getIssuerName())); final View gameStateView = this.flowEngine .subFlow(new ViewBuilder(utxoTrxId)); diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultCommiter.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultCommiter.java index 3480ca5..2c964ce 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultCommiter.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/GameResultCommiter.java @@ -65,7 +65,7 @@ public class GameResultCommiter implements SubFlow { return this.flowEngine.subFlow( new CommitTrx(gameResultTrx, - command.getCounterparty(gameResult), + getCounterparty(gameResult), custodianInfo.getName()) ); } @@ -84,17 +84,27 @@ public class GameResultCommiter implements SubFlow { final MemberX500Name myName = memberLookup.myInfo().getName(); switch(this.command.getAction()) { - case GAME_BOARD_VICTORY: + case CLAIM_VICTORY: return new GameResultState(myName, // i'm a winner gameBoard, custodianPublicKey); - - case GAME_BOARD_SURRENDER: - return new GameResultState(gameBoard.getOpponentName(myName), // me surrender to + + case SURRENDER: + return new GameResultState(gameBoard.getOpponentName(myName), // me surrender to opponent gameBoard, custodianPublicKey); - + + case DRAW_ACCEPT: + return new GameResultState(null, // there is no winner, it's a draw + gameBoard, custodianPublicKey); + default: throw new IllegalStateException("GameResult: bad reason"); } } + @Suspendable + MemberX500Name getCounterparty(GameState gameState) { + final MemberX500Name myName = this.memberLookup.myInfo().getName(); + return gameState.getOpponentName(myName); + } + } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/Rank.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/Rank.java index 1f1017f..0e19c95 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/Rank.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/Rank.java @@ -3,16 +3,19 @@ package djmil.cordacheckers.gameresult; public class Rank { private Integer gamesPlayed; private Integer gamesWon; + private Integer gamesDraw; // Serialisation service requires a default constructor public Rank() { gamesPlayed = 0; gamesWon = 0; + gamesDraw = 0; } - public Rank(Integer gamesPlayed, Integer gamesWon) { + public Rank(Integer gamesPlayed, Integer gamesWon, Integer gamesDraw) { this.gamesPlayed = gamesPlayed; this.gamesWon = gamesWon; + this.gamesDraw = gamesDraw; } Rank gamePlayed() { @@ -25,6 +28,13 @@ public class Rank { return this; } + Rank gameDraw(boolean isDraw) { + if (isDraw) + gamesDraw++; + + return this; + } + public Integer getGamesPlayed() { return gamesPlayed; } @@ -33,4 +43,8 @@ public class Rank { return gamesWon; } + public Integer getGamesDraw() { + return gamesDraw; + } + } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/RankingFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/RankingFlow.java index 6cee272..e320af5 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/RankingFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/RankingFlow.java @@ -39,17 +39,21 @@ public class RankingFlow implements ClientStartableFlow { Map leaderboard = newLeaderboard(); for (GameResultState gs : gameStateResutList) { - final var winner = gs.getWinnerName().getCommonName(); - if (winner != null) - leaderboard.put(winner, leaderboard.get(winner).gameWon() ); - + + boolean isDraw = true; + if (gs.getWinnerName() != null) { + final var winnerCommonName = gs.getWinnerName().getCommonName(); + leaderboard.put(winnerCommonName, leaderboard.get(winnerCommonName).gameWon() ); + isDraw = false; + } + final var blackPlayer = gs.getBlackPlayer().getCommonName(); - leaderboard.put(blackPlayer, leaderboard.get(blackPlayer).gamePlayed() ); + leaderboard.put(blackPlayer, leaderboard.get(blackPlayer).gamePlayed().gameDraw(isDraw) ); final var whitePlayer = gs.getWhitePlayer().getCommonName(); - leaderboard.put(whitePlayer, leaderboard.get(whitePlayer).gamePlayed() ); + leaderboard.put(whitePlayer, leaderboard.get(whitePlayer).gamePlayed().gameDraw(isDraw) ); } - + return new RankingFlowResponce(leaderboard) .toJsonEncodedString(jsonMarshallingService); } catch (Exception e) { diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/RankingFlowResponce.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/RankingFlowResponce.java index e67635e..815657c 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/RankingFlowResponce.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameresult/RankingFlowResponce.java @@ -15,7 +15,7 @@ public class RankingFlowResponce { public RankingFlowResponce(Exception exception) { this.successStatus = null; - this.failureStatus = exception.getMessage(); + this.failureStatus = exception.toString(); } public String toJsonEncodedString(JsonMarshallingService jsonMarshallingService) { diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlowResponce.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlowResponce.java index d12f046..42d2c07 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlowResponce.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ListFlowResponce.java @@ -15,7 +15,7 @@ public class ListFlowResponce { public ListFlowResponce(Exception exception) { this.successStatus = null; - this.failureStatus = exception.getMessage(); + this.failureStatus = exception.toString(); } public String toJsonEncodedString(JsonMarshallingService jsonMarshallingService) { diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/View.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/View.java index 07340de..9b1a965 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/View.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/View.java @@ -21,8 +21,12 @@ public class View { GAME_BOARD_WAIT_FOR_OPPONENT, GAME_BOARD_WAIT_FOR_YOU, + DRAW_REQUEST_WAIT_FOR_OPPONENT, + DRAW_REQUEST_WAIT_FOR_YOU, + GAME_RESULT_YOU_WON, - GAME_RESULT_YOU_LOOSE; + GAME_RESULT_YOU_LOOSE, + GAME_RESULT_DRAW; } public final Status status; diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ViewBuilder.java b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ViewBuilder.java index dc18050..d64b7ac 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ViewBuilder.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gamestate/ViewBuilder.java @@ -76,19 +76,29 @@ public class ViewBuilder implements SubFlow { return View.Status.GAME_PROPOSAL_CANCELED; case GAME_PROPOSAL_ACCEPT: - case GAME_BOARD_MOVE: + case DRAW_REJECT: + case MOVE: if (game.isMyAction(myName)) return View.Status.GAME_BOARD_WAIT_FOR_YOU; else return View.Status.GAME_BOARD_WAIT_FOR_OPPONENT; - case GAME_BOARD_SURRENDER: + case DRAW_REQUEST: + if (game.isMyAction(myName)) + return View.Status.DRAW_REQUEST_WAIT_FOR_YOU; + else + return View.Status.DRAW_REQUEST_WAIT_FOR_OPPONENT; + + case DRAW_ACCEPT: + return View.Status.GAME_RESULT_DRAW; + + case SURRENDER: if (game.issuer.compareTo(myName) == 0) return View.Status.GAME_RESULT_YOU_LOOSE; else return View.Status.GAME_RESULT_YOU_WON; - case GAME_BOARD_VICTORY: + case CLAIM_VICTORY: if (game.issuer.compareTo(myName) == 0) return View.Status.GAME_RESULT_YOU_WON; else