From e26cfe0d9172405853ce326519b9d3b7abb1ab57 Mon Sep 17 00:00:00 2001 From: djmil Date: Mon, 18 Sep 2023 10:49:06 +0200 Subject: [PATCH] GameID a universal ID shared between GameProposal, GameBoard and a GameResult --- .../cordaclient/CordaClient.java | 28 +++++++++++ .../cordaclient/dao/GameBoard.java | 1 + .../cordaclient/dao/GameBoardCommand.java | 4 +- .../flow/arguments/GameBoardResGameBoard.java | 7 +++ .../cordaclient/CordaClientTest.java | 30 ++++++++++++ .../contracts/GameBoardCommand.java | 34 ++++++++++++-- .../contracts/GameBoardContract.java | 18 +++----- .../contracts/GameResultContract.java | 7 +-- .../cordacheckers/states/GameBoardState.java | 35 +++++++++++--- .../cordacheckers/states/GameResultState.java | 2 +- .../cordacheckers/gameboard/CommandFlow.java | 46 +++++++++---------- .../gameboard/CommandResponderFlow.java | 4 +- .../gameboard/GameBoardView.java | 20 ++++++-- .../gameproposal/CommitResponderFlow.java | 1 + 14 files changed, 178 insertions(+), 59 deletions(-) create mode 100644 backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameBoard.java diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java index 685f7b9..ec3324d 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java @@ -35,6 +35,7 @@ import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardCommandReq; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardResGameResult; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardListRes; +import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardResGameBoard; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandAcceptRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandReq; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandRes; @@ -227,6 +228,33 @@ public class CordaClient { return moveResult.successStatus(); } + public GameBoard gameBoardMove( + HoldingIdentity myHoldingIdentity, + UUID gameBoardUuid, + List move + ) { + final RequestBody requestBody = new RequestBody( + "gb.move-" +UUID.randomUUID(), + "djmil.cordacheckers.gameboard.CommandFlow", + new GameBoardCommandReq( + gameBoardUuid, + new GameBoardCommand(move)) + ); + + final GameBoardResGameBoard moveResult = cordaFlowExecute( + myHoldingIdentity, + requestBody, + GameBoardResGameBoard.class + ); + + if (moveResult.failureStatus() != null) { + System.out.println("GameBoard.CommandFlow failed: " + moveResult.failureStatus()); + throw new RuntimeException("GameBoard: CommandFlow execution has failed"); + } + + return moveResult.successStatus(); + } + private T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class flowResultType) { try { diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java index ec70639..d2c61f5 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoard.java @@ -10,6 +10,7 @@ public record GameBoard( String opponentName, Piece.Color opponentColor, Boolean opponentMove, + Integer moveNumber, Map board, GameBoardCommand previousCommand, String message, diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java index 0eaf30b..b13a393 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/GameBoardCommand.java @@ -6,9 +6,7 @@ public class GameBoardCommand { public static enum Type { MOVE, SURRENDER, - DRAW, - VICTORY, - ACCEPT; + VICTORY; } private final Type type; diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameBoard.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameBoard.java new file mode 100644 index 0000000..c9bd0ae --- /dev/null +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/arguments/GameBoardResGameBoard.java @@ -0,0 +1,7 @@ +package djmil.cordacheckers.cordaclient.dao.flow.arguments; + +import djmil.cordacheckers.cordaclient.dao.GameBoard; + +public record GameBoardResGameBoard(GameBoard successStatus, String failureStatus) { + +} diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java index f328a98..fac10a4 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java @@ -3,6 +3,7 @@ package djmil.cordacheckers.cordaclient; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -215,6 +216,35 @@ public class CordaClientTest { assertThat(gameResult.victoryColor()).isEqualByComparingTo(Piece.Color.BLACK); } + @Test + void testGameBoardMove() throws JsonMappingException, JsonProcessingException, InvalidNameException { + final var hiAlice = holdingIdentityResolver.getByUsername("alice"); + final var hiBob = holdingIdentityResolver.getByUsername("bob"); + final var bobColor = Piece.Color.WHITE; + + final UUID gpUuid = cordaClient.gameProposalCreate( + hiAlice, hiBob, + bobColor, "GameBoard MOVE test" + ); + + System.out.println("New GameProposal UUID "+ gpUuid); + + final GameBoard gbState = cordaClient.gameProposalAccept( + hiBob, gpUuid + ); + + System.out.println("New GameBoard UUID "+ gbState.id()); + + assertThatThrownBy(() -> { // Alice can not move, since it is Bob's turn + cordaClient.gameBoardMove( + hiAlice, gbState.id(), + Arrays.asList(1, 2)); + }); + + final GameBoard gameBoard = cordaClient.gameBoardMove( + hiBob, gbState.id(), Arrays.asList(1, 2)); + } + private T findByUuid(List statesList, UUID uuid) { for (T state : statesList) { if (state.id().compareTo(uuid) == 0) diff --git a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java index 254a2e0..ea61155 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardCommand.java @@ -21,9 +21,7 @@ public class GameBoardCommand implements Command { public static enum Type { MOVE, SURRENDER, - DRAW, - VICTORY, - ACCEPT; // aka accept opponents DRAW or VICTORY request + FINISH; } private final Type type; @@ -81,11 +79,41 @@ public class GameBoardCommand implements Command { requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameResultState.getBlackPlayerName()) == 0, IN_OUT_PARTICIPANTS); } + public static void validateMoveTrx(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, MOVE_INPUT_STATE); + final var inGameBoardState = getSingleInputState(trx, GameBoardState.class); + + requireThat(trx.getOutputContractStates().size() == 1, MOVE_OUTPUT_STATE); + final var outGameBoardState = getSingleOutputState(trx, GameBoardState.class); + + requireThat(inGameBoardState.getWhitePlayerName().compareTo(outGameBoardState.getWhitePlayerName()) == 0, IN_OUT_PARTICIPANTS); + requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameBoardState.getBlackPlayerName()) == 0, IN_OUT_PARTICIPANTS); + } + + public static void validateFinishTrx(UtxoLedgerTransaction trx) { + requireThat(trx.getInputContractStates().size() == 1, FINAL_MOVE_INPUT_STATE); + final var inGameBoardState = getSingleInputState(trx, GameBoardState.class); + + requireThat(trx.getOutputContractStates().size() == 1, FINAL_MOVE_OUTPUT_STATE); + final var outGameResultState = getSingleOutputState(trx, GameResultState.class); + + requireThat(inGameBoardState.getWhitePlayerName().compareTo(outGameResultState.getWhitePlayerName()) == 0, IN_OUT_PARTICIPANTS); + requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameResultState.getBlackPlayerName()) == 0, IN_OUT_PARTICIPANTS); + } + static final String BAD_ACTIONMOVE_CONSTRUCTOR = "Bad constructor for Action.MOVE"; static final String SURRENDER_INPUT_STATE = "SURRENDER command should have exactly one GameBoardState input state"; static final String SURRENDER_OUTPUT_STATE = "SURRENDER command should have exactly one GameResultState output state"; + static final String MOVE_INPUT_STATE = "MOVE command should have exactly one GameBoardState input state"; + static final String MOVE_OUTPUT_STATE = "MOVE command should have exactly one GameBoardState output state"; + static final String MOVE_OUT_BOARD = "MOVE command: move checkers rules violation"; + static final String MOVE_OUT_COLOR = "MOVE command: moveColor checkers rules violation"; + + static final String FINAL_MOVE_INPUT_STATE = "FINAL_MOVE command should have exactly one GameBoardState input state"; + static final String FINAL_MOVE_OUTPUT_STATE = "FINAL_MOVE command should have exactly one GameResultState output state"; + static final String IN_OUT_PARTICIPANTS = "InputState and OutputState participants do not match"; public static class CommandTypeException extends RuntimeException { 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 2e659ea..55a3b62 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameBoardContract.java @@ -1,17 +1,12 @@ package djmil.cordacheckers.contracts; import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputSar; -import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleReferenceSar; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import djmil.cordacheckers.states.GameProposalState; -import net.corda.v5.base.annotations.Suspendable; import net.corda.v5.base.exceptions.CordaRuntimeException; import net.corda.v5.ledger.utxo.Command; -import net.corda.v5.ledger.utxo.StateAndRef; import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction; public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { @@ -40,18 +35,17 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract { log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType()); switch (((GameBoardCommand)command).getType()) { case MOVE: - break; + GameBoardCommand.validateMoveTrx(trx); + break; case SURRENDER: GameBoardCommand.validateSurrenderTrx(trx); break; - case DRAW: - break; - case VICTORY: - break; - case ACCEPT: - break; + case FINISH: + GameBoardCommand.validateFinishTrx(trx); + break; + default: throw new GameBoardCommand.CommandTypeException(); } 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 74ec8b7..bbcd75b 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/contracts/GameResultContract.java @@ -24,10 +24,11 @@ public class GameResultContract implements net.corda.v5.ledger.utxo.Contract { switch (command.getType()) { case SURRENDER: GameBoardCommand.validateSurrenderTrx(trx); - break; + break; - // case ACCEPT: - // break; + case FINISH: + GameBoardCommand.validateFinishTrx(trx); + break; default: throw new CordaRuntimeException(UNKNOWN_COMMAND); 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 30555c3..f278de9 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameBoardState.java @@ -20,6 +20,7 @@ public class GameBoardState implements ContractState, Counterparty { private final MemberX500Name blackPlayerName; private final Piece.Color moveColor; + private final Integer moveNumber; private final Map board; private final String message; @@ -27,27 +28,45 @@ public class GameBoardState implements ContractState, Counterparty { private final List participants; public GameBoardState( - GameProposalState gameBoard + GameProposalState gameProposalState ) { - this.whitePlayerName = gameBoard.getWhitePlayerName(); - this.blackPlayerName = gameBoard.getBlackPlayerName(); + this.whitePlayerName = gameProposalState.getWhitePlayerName(); + this.blackPlayerName = gameProposalState.getBlackPlayerName(); // Initial GameBoard state this.moveColor = Piece.Color.WHITE; + this.moveNumber = 0; this.board = new LinkedHashMap(initialBoard); this.message = null; - this.id = UUID.randomUUID(); - this.participants = gameBoard.getParticipants(); + this.id = gameProposalState.getId(); + this.participants = gameProposalState.getParticipants(); + } + + public GameBoardState( + GameBoardState oldGameBoardState, Map newBoard, Piece.Color moveColor + ) { + this.whitePlayerName = oldGameBoardState.getWhitePlayerName(); + this.blackPlayerName = oldGameBoardState.getBlackPlayerName(); + + // Initial GameBoard state + this.moveColor = moveColor; + this.moveNumber = oldGameBoardState.getMoveNumber() +1; + this.board = newBoard; + this.message = null; + + this.id = oldGameBoardState.getId(); + this.participants = oldGameBoardState.getParticipants(); } @ConstructorForDeserialization public GameBoardState(MemberX500Name whitePlayerName, MemberX500Name blackPlayerName, - Color moveColor, Map board, String message, + Color moveColor, Integer moveNumber, Map board, String message, UUID id, List participants) { this.whitePlayerName = whitePlayerName; this.blackPlayerName = blackPlayerName; this.moveColor = moveColor; + this.moveNumber = moveNumber; this.board = board; this.message = message; this.id = id; @@ -66,6 +85,10 @@ public class GameBoardState implements ContractState, Counterparty { return moveColor; } + public Integer getMoveNumber() { + return moveNumber; + } + public Map getBoard() { return board; } 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 2435c0f..34b0e06 100644 --- a/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java +++ b/corda/contracts/src/main/java/djmil/cordacheckers/states/GameResultState.java @@ -36,7 +36,7 @@ public class GameResultState implements ContractState, Counterparty { this.blackPlayerName = stateGameBoard.getBlackPlayerName(); this.victoryColor = victoryColor; - this.id = UUID.randomUUID(); + this.id = stateGameBoard.getId(); this.participants = stateGameBoard.getParticipants(); } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java index 17def4e..d5f5340 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandFlow.java @@ -102,20 +102,29 @@ public class CommandFlow implements ClientStartableFlow { UtxoTransactionBuilder trxBuilder = utxoLedgerService.createTransactionBuilder() .setNotary(utxoSarGameBoard.getState().getNotaryName()) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) - .addInputState(utxoSarGameBoard.getRef()) .addCommand(command) + .addInputState(utxoSarGameBoard.getRef()) .addSignatories(stateGameBoard.getParticipants()); switch (command.getType()) { case SURRENDER: - case ACCEPT: - final Piece.Color winnerColor = winnerColor(command, stateGameBoard, myName); trxBuilder = trxBuilder - .addOutputState( new GameResultState(stateGameBoard, winnerColor) ); + .addOutputState( new GameResultState( + stateGameBoard, + stateGameBoard.getCounterpartyColor(myName)) // winner color + ); break; + + case MOVE: + trxBuilder = trxBuilder + .addOutputState( new GameBoardState(stateGameBoard, stateGameBoard.getBoard(), stateGameBoard.getMoveColor())); // TODO: advance color + break; + + case FINISH: + throw new CommandTypeException("GameBoard.CommandFlow can not process externaly issued FINISH commnd"); default: - throw new CommandTypeException("GameBoard.CommandFlow doTransaction()"); + throw new CommandTypeException("GameBoard.CommandFlow doTransaction(): Unknown command"); } return commit( @@ -123,22 +132,6 @@ public class CommandFlow implements ClientStartableFlow { opponentName); } - @Suspendable - Piece.Color winnerColor(GameBoardCommand command, GameBoardState stateGameBoard, MemberX500Name myName) { - - switch (command.getType()) { - case SURRENDER: - return stateGameBoard.getCounterpartyColor(myName); - - case ACCEPT: - //stateGameBoard.getMoveColor(); - throw new RuntimeException("Unimplemented"); - - default: - throw new CommandTypeException("GameBoard.CommandFlow winnerColor()"); - } - } - @Suspendable SecureHash commit(UtxoSignedTransaction candidateTrx, MemberX500Name counterpartyName) { log.info("About to commit " +candidateTrx.getId()); @@ -170,12 +163,17 @@ public class CommandFlow implements ClientStartableFlow { switch (command.getType()) { case SURRENDER: - case ACCEPT: return viewGameResult(utxoTrx); case MOVE: - case VICTORY: // request, shall be accepted by opponent - case DRAW: // request, shall be accepted by opponent + // 1. create initial GameBoardView + // 2. check for winning conditions + // - run subcommnd FINISH that will consume current gbUtxo and will produce GameResult state + // - update GameBoardView to have FINISH command + // 3. return GameBoardView + return viewGameBoard(utxoTrx); + + case FINISH: return viewGameBoard(utxoTrx); default: diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java index 69283f9..1857f30 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/CommandResponderFlow.java @@ -65,12 +65,10 @@ public class CommandResponderFlow implements ResponderFlow { GameBoardState getGameBoardState(UtxoLedgerTransaction utxoGameBoard, GameBoardCommand command) { switch (command.getType()) { case SURRENDER: - case ACCEPT: + case FINISH: return getSingleInputState(utxoGameBoard, GameBoardState.class); case MOVE: - case VICTORY: - case DRAW: return getSingleOutputState(utxoGameBoard, GameBoardState.class); default: diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java index 8d50d54..fa849c2 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameboard/GameBoardView.java @@ -17,16 +17,26 @@ public class GameBoardView { public final String opponentName; public final Piece.Color opponentColor; public final Boolean opponentMove; + public final Integer moveNumber; public final Map board; public final GameBoardCommand previousCommand; public final String message; public final UUID id; + /* + * GameStatus enum: + * - YOUR_TURN + * - WAIT_FOR_OPPONENT + * - VICTORY + * - YOU_LOOSE + */ + // Serialisation service requires a default constructor public GameBoardView() { this.opponentName = null; this.opponentColor = null; this.opponentMove = null; + this.moveNumber = null; this.board = null; this.previousCommand = null; this.message = null; @@ -36,6 +46,11 @@ public class GameBoardView { // A view from a perspective of a concrete player, on a ledger transaction that has // produced new GameBoardState public GameBoardView(UtxoLedgerTransaction utxoGameBoard, MemberX500Name myName) throws NotInvolved { + // TODO check command type: MOVE vs FINNISH | SURRENDER + // SingleOutPut vs SingleInput + this.previousCommand = UtxoLedgerTransactionUtil + .getOptionalCommand(utxoGameBoard, GameBoardCommand.class) + .orElseGet(() -> null); // there is no previous command for GameProposal.Accept case final GameBoardState stateGameBoard = UtxoLedgerTransactionUtil .getSingleOutputState(utxoGameBoard, GameBoardState.class); @@ -44,13 +59,10 @@ public class GameBoardView { this.opponentColor = stateGameBoard.getCounterpartyColor(myName); this.opponentMove = this.opponentColor == stateGameBoard.getMoveColor(); + this.moveNumber = stateGameBoard.getMoveNumber(); this.board = stateGameBoard.getBoard(); this.message = stateGameBoard.getMessage(); this.id = stateGameBoard.getId(); - - this.previousCommand = UtxoLedgerTransactionUtil - .getOptionalCommand(utxoGameBoard, GameBoardCommand.class) - .orElseGet(() -> null); // there is no previous command for GameProposal.Accept case } } diff --git a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java index 9dd668b..15b5efb 100644 --- a/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java +++ b/corda/workflows/src/main/java/djmil/cordacheckers/gameproposal/CommitResponderFlow.java @@ -34,6 +34,7 @@ public class CommitResponderFlow implements ResponderFlow { @Suspendable @Override public void call(FlowSession session) { + log.info("GameProposal: Commit responder flow"); try { UtxoTransactionValidator txValidator = ledgerTransaction -> { final GameProposalCommand command = ledgerTransaction.getCommands(GameProposalCommand.class).get(0);