a universal ID shared between GameProposal, GameBoard and a GameResult
This commit is contained in:
djmil 2023-09-18 10:49:06 +02:00
parent a34ea39dfb
commit e26cfe0d91
14 changed files with 178 additions and 59 deletions

View File

@ -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.GameBoardCommandReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardResGameResult; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardResGameResult;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameBoardListRes; 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.GameProposalCommandAcceptRes;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandReq; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandReq;
import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandRes; import djmil.cordacheckers.cordaclient.dao.flow.arguments.GameProposalCommandRes;
@ -227,6 +228,33 @@ public class CordaClient {
return moveResult.successStatus(); return moveResult.successStatus();
} }
public GameBoard gameBoardMove(
HoldingIdentity myHoldingIdentity,
UUID gameBoardUuid,
List<Integer> 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> T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class<T> flowResultType) { private <T> T cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody, Class<T> flowResultType) {
try { try {

View File

@ -10,6 +10,7 @@ public record GameBoard(
String opponentName, String opponentName,
Piece.Color opponentColor, Piece.Color opponentColor,
Boolean opponentMove, Boolean opponentMove,
Integer moveNumber,
Map<Integer, Piece> board, Map<Integer, Piece> board,
GameBoardCommand previousCommand, GameBoardCommand previousCommand,
String message, String message,

View File

@ -6,9 +6,7 @@ public class GameBoardCommand {
public static enum Type { public static enum Type {
MOVE, MOVE,
SURRENDER, SURRENDER,
DRAW, VICTORY;
VICTORY,
ACCEPT;
} }
private final Type type; private final Type type;

View File

@ -0,0 +1,7 @@
package djmil.cordacheckers.cordaclient.dao.flow.arguments;
import djmil.cordacheckers.cordaclient.dao.GameBoard;
public record GameBoardResGameBoard(GameBoard successStatus, String failureStatus) {
}

View File

@ -3,6 +3,7 @@ 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.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -215,6 +216,35 @@ public class CordaClientTest {
assertThat(gameResult.victoryColor()).isEqualByComparingTo(Piece.Color.BLACK); 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 extends CordaState> T findByUuid(List<T> statesList, UUID uuid) { private <T extends CordaState> T findByUuid(List<T> statesList, UUID uuid) {
for (T state : statesList) { for (T state : statesList) {
if (state.id().compareTo(uuid) == 0) if (state.id().compareTo(uuid) == 0)

View File

@ -21,9 +21,7 @@ public class GameBoardCommand implements Command {
public static enum Type { public static enum Type {
MOVE, MOVE,
SURRENDER, SURRENDER,
DRAW, FINISH;
VICTORY,
ACCEPT; // aka accept opponents DRAW or VICTORY request
} }
private final Type type; private final Type type;
@ -81,11 +79,41 @@ public class GameBoardCommand implements Command {
requireThat(inGameBoardState.getBlackPlayerName().compareTo(outGameResultState.getBlackPlayerName()) == 0, IN_OUT_PARTICIPANTS); 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 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_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 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"; static final String IN_OUT_PARTICIPANTS = "InputState and OutputState participants do not match";
public static class CommandTypeException extends RuntimeException { public static class CommandTypeException extends RuntimeException {

View File

@ -1,17 +1,12 @@
package djmil.cordacheckers.contracts; package djmil.cordacheckers.contracts;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand; 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.Logger;
import org.slf4j.LoggerFactory; 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.base.exceptions.CordaRuntimeException;
import net.corda.v5.ledger.utxo.Command; import net.corda.v5.ledger.utxo.Command;
import net.corda.v5.ledger.utxo.StateAndRef;
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 {
@ -40,18 +35,17 @@ public class GameBoardContract implements net.corda.v5.ledger.utxo.Contract {
log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType()); log.info("GameBoardContract.verify() as GameBoardCommand "+((GameBoardCommand)command).getType());
switch (((GameBoardCommand)command).getType()) { switch (((GameBoardCommand)command).getType()) {
case MOVE: case MOVE:
break; GameBoardCommand.validateMoveTrx(trx);
break;
case SURRENDER: case SURRENDER:
GameBoardCommand.validateSurrenderTrx(trx); GameBoardCommand.validateSurrenderTrx(trx);
break; break;
case DRAW: case FINISH:
break; GameBoardCommand.validateFinishTrx(trx);
case VICTORY: break;
break;
case ACCEPT:
break;
default: default:
throw new GameBoardCommand.CommandTypeException(); throw new GameBoardCommand.CommandTypeException();
} }

View File

@ -24,10 +24,11 @@ public class GameResultContract implements net.corda.v5.ledger.utxo.Contract {
switch (command.getType()) { switch (command.getType()) {
case SURRENDER: case SURRENDER:
GameBoardCommand.validateSurrenderTrx(trx); GameBoardCommand.validateSurrenderTrx(trx);
break; break;
// case ACCEPT: case FINISH:
// break; GameBoardCommand.validateFinishTrx(trx);
break;
default: default:
throw new CordaRuntimeException(UNKNOWN_COMMAND); throw new CordaRuntimeException(UNKNOWN_COMMAND);

View File

@ -20,6 +20,7 @@ public class GameBoardState implements ContractState, Counterparty {
private final MemberX500Name blackPlayerName; private final MemberX500Name blackPlayerName;
private final Piece.Color moveColor; private final Piece.Color moveColor;
private final Integer moveNumber;
private final Map<Integer, Piece> board; private final Map<Integer, Piece> board;
private final String message; private final String message;
@ -27,27 +28,45 @@ public class GameBoardState implements ContractState, Counterparty {
private final List<PublicKey> participants; private final List<PublicKey> participants;
public GameBoardState( public GameBoardState(
GameProposalState gameBoard GameProposalState gameProposalState
) { ) {
this.whitePlayerName = gameBoard.getWhitePlayerName(); this.whitePlayerName = gameProposalState.getWhitePlayerName();
this.blackPlayerName = gameBoard.getBlackPlayerName(); this.blackPlayerName = gameProposalState.getBlackPlayerName();
// Initial GameBoard state // Initial GameBoard state
this.moveColor = Piece.Color.WHITE; this.moveColor = Piece.Color.WHITE;
this.moveNumber = 0;
this.board = new LinkedHashMap<Integer, Piece>(initialBoard); this.board = new LinkedHashMap<Integer, Piece>(initialBoard);
this.message = null; this.message = null;
this.id = UUID.randomUUID(); this.id = gameProposalState.getId();
this.participants = gameBoard.getParticipants(); this.participants = gameProposalState.getParticipants();
}
public GameBoardState(
GameBoardState oldGameBoardState, Map<Integer, Piece> 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 @ConstructorForDeserialization
public GameBoardState(MemberX500Name whitePlayerName, MemberX500Name blackPlayerName, public GameBoardState(MemberX500Name whitePlayerName, MemberX500Name blackPlayerName,
Color moveColor, Map<Integer, Piece> board, String message, Color moveColor, Integer moveNumber, Map<Integer, Piece> board, String message,
UUID id, List<PublicKey> participants) { UUID id, List<PublicKey> participants) {
this.whitePlayerName = whitePlayerName; this.whitePlayerName = whitePlayerName;
this.blackPlayerName = blackPlayerName; this.blackPlayerName = blackPlayerName;
this.moveColor = moveColor; this.moveColor = moveColor;
this.moveNumber = moveNumber;
this.board = board; this.board = board;
this.message = message; this.message = message;
this.id = id; this.id = id;
@ -66,6 +85,10 @@ public class GameBoardState implements ContractState, Counterparty {
return moveColor; return moveColor;
} }
public Integer getMoveNumber() {
return moveNumber;
}
public Map<Integer, Piece> getBoard() { public Map<Integer, Piece> getBoard() {
return board; return board;
} }

View File

@ -36,7 +36,7 @@ public class GameResultState implements ContractState, Counterparty {
this.blackPlayerName = stateGameBoard.getBlackPlayerName(); this.blackPlayerName = stateGameBoard.getBlackPlayerName();
this.victoryColor = victoryColor; this.victoryColor = victoryColor;
this.id = UUID.randomUUID(); this.id = stateGameBoard.getId();
this.participants = stateGameBoard.getParticipants(); this.participants = stateGameBoard.getParticipants();
} }

View File

@ -102,20 +102,29 @@ public class CommandFlow implements ClientStartableFlow {
UtxoTransactionBuilder trxBuilder = utxoLedgerService.createTransactionBuilder() UtxoTransactionBuilder trxBuilder = utxoLedgerService.createTransactionBuilder()
.setNotary(utxoSarGameBoard.getState().getNotaryName()) .setNotary(utxoSarGameBoard.getState().getNotaryName())
.setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis())) .setTimeWindowBetween(Instant.now(), Instant.now().plusMillis(Duration.ofDays(1).toMillis()))
.addInputState(utxoSarGameBoard.getRef())
.addCommand(command) .addCommand(command)
.addInputState(utxoSarGameBoard.getRef())
.addSignatories(stateGameBoard.getParticipants()); .addSignatories(stateGameBoard.getParticipants());
switch (command.getType()) { switch (command.getType()) {
case SURRENDER: case SURRENDER:
case ACCEPT:
final Piece.Color winnerColor = winnerColor(command, stateGameBoard, myName);
trxBuilder = trxBuilder trxBuilder = trxBuilder
.addOutputState( new GameResultState(stateGameBoard, winnerColor) ); .addOutputState( new GameResultState(
stateGameBoard,
stateGameBoard.getCounterpartyColor(myName)) // winner color
);
break; 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: default:
throw new CommandTypeException("GameBoard.CommandFlow doTransaction()"); throw new CommandTypeException("GameBoard.CommandFlow doTransaction(): Unknown command");
} }
return commit( return commit(
@ -123,22 +132,6 @@ public class CommandFlow implements ClientStartableFlow {
opponentName); 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 @Suspendable
SecureHash commit(UtxoSignedTransaction candidateTrx, MemberX500Name counterpartyName) { SecureHash commit(UtxoSignedTransaction candidateTrx, MemberX500Name counterpartyName) {
log.info("About to commit " +candidateTrx.getId()); log.info("About to commit " +candidateTrx.getId());
@ -170,12 +163,17 @@ public class CommandFlow implements ClientStartableFlow {
switch (command.getType()) { switch (command.getType()) {
case SURRENDER: case SURRENDER:
case ACCEPT:
return viewGameResult(utxoTrx); return viewGameResult(utxoTrx);
case MOVE: case MOVE:
case VICTORY: // request, shall be accepted by opponent // 1. create initial GameBoardView
case DRAW: // request, shall be accepted by opponent // 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); return viewGameBoard(utxoTrx);
default: default:

View File

@ -65,12 +65,10 @@ public class CommandResponderFlow implements ResponderFlow {
GameBoardState getGameBoardState(UtxoLedgerTransaction utxoGameBoard, GameBoardCommand command) { GameBoardState getGameBoardState(UtxoLedgerTransaction utxoGameBoard, GameBoardCommand command) {
switch (command.getType()) { switch (command.getType()) {
case SURRENDER: case SURRENDER:
case ACCEPT: case FINISH:
return getSingleInputState(utxoGameBoard, GameBoardState.class); return getSingleInputState(utxoGameBoard, GameBoardState.class);
case MOVE: case MOVE:
case VICTORY:
case DRAW:
return getSingleOutputState(utxoGameBoard, GameBoardState.class); return getSingleOutputState(utxoGameBoard, GameBoardState.class);
default: default:

View File

@ -17,16 +17,26 @@ public class GameBoardView {
public final String opponentName; public final String opponentName;
public final Piece.Color opponentColor; public final Piece.Color opponentColor;
public final Boolean opponentMove; public final Boolean opponentMove;
public final Integer moveNumber;
public final Map<Integer, Piece> board; public final Map<Integer, Piece> board;
public final GameBoardCommand previousCommand; public final GameBoardCommand previousCommand;
public final String message; public final String message;
public final UUID id; public final UUID id;
/*
* GameStatus enum:
* - YOUR_TURN
* - WAIT_FOR_OPPONENT
* - VICTORY
* - YOU_LOOSE
*/
// Serialisation service requires a default constructor // Serialisation service requires a default constructor
public GameBoardView() { public GameBoardView() {
this.opponentName = null; this.opponentName = null;
this.opponentColor = null; this.opponentColor = null;
this.opponentMove = null; this.opponentMove = null;
this.moveNumber = null;
this.board = null; this.board = null;
this.previousCommand = null; this.previousCommand = null;
this.message = 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 // A view from a perspective of a concrete player, on a ledger transaction that has
// produced new GameBoardState // produced new GameBoardState
public GameBoardView(UtxoLedgerTransaction utxoGameBoard, MemberX500Name myName) throws NotInvolved { 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 final GameBoardState stateGameBoard = UtxoLedgerTransactionUtil
.getSingleOutputState(utxoGameBoard, GameBoardState.class); .getSingleOutputState(utxoGameBoard, GameBoardState.class);
@ -44,13 +59,10 @@ public class GameBoardView {
this.opponentColor = stateGameBoard.getCounterpartyColor(myName); this.opponentColor = stateGameBoard.getCounterpartyColor(myName);
this.opponentMove = this.opponentColor == stateGameBoard.getMoveColor(); this.opponentMove = this.opponentColor == stateGameBoard.getMoveColor();
this.moveNumber = stateGameBoard.getMoveNumber();
this.board = stateGameBoard.getBoard(); this.board = stateGameBoard.getBoard();
this.message = stateGameBoard.getMessage(); this.message = stateGameBoard.getMessage();
this.id = stateGameBoard.getId(); this.id = stateGameBoard.getId();
this.previousCommand = UtxoLedgerTransactionUtil
.getOptionalCommand(utxoGameBoard, GameBoardCommand.class)
.orElseGet(() -> null); // there is no previous command for GameProposal.Accept case
} }
} }

View File

@ -34,6 +34,7 @@ public class CommitResponderFlow implements ResponderFlow {
@Suspendable @Suspendable
@Override @Override
public void call(FlowSession session) { public void call(FlowSession session) {
log.info("GameProposal: Commit responder flow");
try { try {
UtxoTransactionValidator txValidator = ledgerTransaction -> { UtxoTransactionValidator txValidator = ledgerTransaction -> {
final GameProposalCommand command = ledgerTransaction.getCommands(GameProposalCommand.class).get(0); final GameProposalCommand command = ledgerTransaction.getCommands(GameProposalCommand.class).get(0);