a simple source of truth about UtxoGameState transaction for
- ViewBuilder
- CommitSubFlowResponder
This commit is contained in:
djmil 2023-09-30 20:09:25 +02:00
parent 729384fb62
commit eb1e7fd93c
10 changed files with 152 additions and 194 deletions

View File

@ -9,7 +9,7 @@ public interface Rsp <T> {
public default T getResponce(RequestBody requestBody) {
if (failureStatus() != null) {
final String msg = requestBody.flowClassName() +" requestId " +requestBody.clientRequestId() +" has failed: " +failureStatus();
System.err.println(msg +". Reson " +failureStatus());
System.err.println(msg);
throw new RuntimeException(msg);
}

View File

@ -48,23 +48,24 @@ public class GameBoardTests {
assertThat(acceptedGameBlackView.opponentColor()).isEqualByComparingTo(Stone.Color.WHITE);
assertThat(acceptedGameBlackView.status()).isEqualByComparingTo(Status.GAME_BOARD_WAIT_FOR_OPPONENT);
assertThatThrownBy(() -> { // Black can not surrender, since it is opponent's turn
cordaClient.gameBoardSurrender(
hiBlack, game.uuid());
});
// Black shall be able to surrender, even if it is not his turn
final GameState surrendererGameView = cordaClient.gameBoardSurrender(
hiWhite, game.uuid());
assertThat(surrendererGameView.opponentName()).isEqualToIgnoringCase(blackPlayerName);
assertThat(surrendererGameView.opponentColor()).isEqualByComparingTo(Stone.Color.BLACK);
assertThat(surrendererGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_LOOSE);
final GameState winnerGameView = cordaClient.gameStateGet(
hiBlack, game.uuid());
assertThat(winnerGameView.opponentName()).isEqualToIgnoringCase(whitePlayerName);
assertThat(winnerGameView.opponentColor()).isEqualByComparingTo(Stone.Color.WHITE);
assertThat(surrendererGameView.opponentName()).isEqualToIgnoringCase(whitePlayerName);
assertThat(surrendererGameView.opponentColor()).isEqualByComparingTo(Stone.Color.WHITE);
assertThat(surrendererGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_LOOSE);
assertThatThrownBy(() -> { // White can not surrender, since Black already did
cordaClient.gameBoardSurrender(
hiWhite, game.uuid());
});
final GameState winnerGameView = cordaClient.gameStateGet(
hiWhite, game.uuid());
assertThat(winnerGameView.opponentName()).isEqualToIgnoringCase(blackPlayerName);
assertThat(winnerGameView.opponentColor()).isEqualByComparingTo(Stone.Color.BLACK);
assertThat(winnerGameView.status()).isEqualByComparingTo(Status.GAME_RESULT_YOU_WON);
}

View File

@ -27,8 +27,7 @@ public class GameCommand implements Command {
GAME_BOARD_MOVE,
GAME_BOARD_SURRENDER,
GAME_RESULT_CREATE;
GAME_BOARD_VICTORY;
}
private final Action action;
@ -72,61 +71,42 @@ public class GameCommand implements Command {
public List<Integer> getMove() {
return move;
}
/*
* Session initiator/respondent
*/
public MemberX500Name getCounterparty(StateAndRef<GameState> gameStateSar) {
final GameState gameState = gameStateSar.getState().getContractState();
return gameState.getOpponentName(getInitiator(gameState));
return getCounterparty(gameStateSar.getState().getContractState());
}
public MemberX500Name getCounterparty(GameState gameState) {
return gameState.getOpponentName(getInitiator(gameState));
}
public MemberX500Name getInitiator(StateAndRef<GameState> gameStateSar) {
return getInitiator(gameStateSar.getState().getContractState());
}
public MemberX500Name getInitiator(GameState gameState) {
switch (action) {
switch (this.action) {
case GAME_PROPOSAL_CREATE:
case GAME_PROPOSAL_CANCEL:
if (gameState instanceof GameProposalState)
return ((GameProposalState)gameState).getIssuerName();
break;
case GAME_PROPOSAL_REJECT:
if (gameState instanceof GameProposalState)
return ((GameProposalState)gameState).getAcquierName();
break;
case GAME_PROPOSAL_ACCEPT:
if (gameState instanceof GameProposalState) // <<-- Session validation perspective
return ((GameProposalState)gameState).getAcquierName();
if (gameState instanceof GameBoardState) // <<-- GameViewBuilder perspective
return ((GameBoardState)gameState).getActivePlayerName();
break;
case GAME_BOARD_SURRENDER:
if (gameState instanceof GameBoardState) // <<-- Session validation perspective
return ((GameBoardState)gameState).getActivePlayerName();
if (gameState instanceof GameResultState) // <<-- GameViewBuilder perspective
return ((GameResultState)gameState).getLooserName();
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).getActivePlayerName();
if (gameState instanceof GameBoardState) {
final var activePlayer = ((GameBoardState)gameState).getActivePlayerName();
return gameState.getOpponentName(activePlayer);
}
break;
case GAME_RESULT_CREATE:
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();
}
@ -192,10 +172,10 @@ public class GameCommand implements Command {
requireThat(inActors.containsAll(outActors) && outActors.containsAll(inActors), IN_OUT_PARTICIPANTS);
final var activePlayerName = getInitiator(inGameBoardState);
final var expectedWinnerName = inGameBoardState.getOpponentName(activePlayerName);
requireThat(outGameResultState.getWinnerName().compareTo(expectedWinnerName) == 0,
"Expected winner "+expectedWinnerName.getCommonName() +", proposed winner " +outGameResultState.getWinnerName().getCommonName());
final GameInfo gameInfo = new GameInfo(trx);
final var winnerName = inGameBoardState.getOpponentName(gameInfo.issuer);
requireThat(outGameResultState.getWinnerName().compareTo(winnerName) == 0,
"Expected winner "+winnerName.getCommonName() +", proposed winner " +outGameResultState.getWinnerName().getCommonName());
}
public void validateGameBoardMove(UtxoLedgerTransaction trx, List<Integer> move) {

View File

@ -0,0 +1,79 @@
package djmil.cordacheckers.contracts;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState;
import djmil.cordacheckers.contracts.GameCommand.ActionException;
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.types.MemberX500Name;
import net.corda.v5.ledger.utxo.transaction.UtxoLedgerTransaction;
public class GameInfo {
public final GameCommand command;
public final MemberX500Name issuer; // a person who has created/updated state (important for CommitSubFlowResponder)
public final MemberX500Name actor; // a person who is expected to act on a state (inmportnat for ViewBuilder)
public final GameState state; // lates, most actual state
public GameInfo(UtxoLedgerTransaction utxoTrx) {
this.command = getSingleCommand(utxoTrx, GameCommand.class);
switch (this.command.getAction()) {
case GAME_PROPOSAL_CREATE:
this.state = getSingleOutputState(utxoTrx, GameProposalState.class);
this.issuer = ((GameProposalState)this.state).getIssuerName();
this.actor = ((GameProposalState)this.state).getAcquierName();
return;
case GAME_PROPOSAL_CANCEL:
this.state = getSingleInputState(utxoTrx, GameProposalState.class);
this.issuer = ((GameProposalState)this.state).getIssuerName();
this.actor = null;
return;
case GAME_PROPOSAL_REJECT:
this.state = getSingleInputState(utxoTrx, GameProposalState.class);
this.issuer = ((GameProposalState)this.state).getAcquierName();
this.actor = null;
return;
case GAME_PROPOSAL_ACCEPT:
this.state = getSingleOutputState(utxoTrx, GameBoardState.class);
this.issuer = getSingleInputState(utxoTrx, GameProposalState.class).getAcquierName();
this.actor = ((GameBoardState)this.state).getActivePlayerName();
return;
case GAME_BOARD_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:
this.state = getSingleOutputState(utxoTrx, GameResultState.class);
this.issuer = ((GameResultState)this.state).getWinnerName();
this.actor = null;
return;
case GAME_BOARD_SURRENDER:
this.state = getSingleOutputState(utxoTrx, GameResultState.class);
this.issuer = ((GameResultState)this.state).getLooserName();
this.actor = null;
return;
}
throw new ActionException();
}
public boolean isMyAction(MemberX500Name myName) {
if (this.actor == null)
return false;
return this.actor.compareTo(myName) == 0;
}
}

View File

@ -24,7 +24,7 @@ public class GameResultContract implements net.corda.v5.ledger.utxo.Contract {
command.validateGameBoardSurrender(trx);
break;
case GAME_RESULT_CREATE:
case GAME_BOARD_VICTORY:
command.validateGameResultCreate(trx);
break;

View File

@ -91,5 +91,4 @@ public class GameBoardState extends GameState {
return true;
}
}

View File

@ -67,7 +67,7 @@ public class SurrenderFlow implements ClientStartableFlow{
utxoTrxId = this.flowEngine
.subFlow(new CommitSubFlow(gameBoardSurrenderTrx,
command.getCounterparty(gameBoardSar),
command.getCounterparty(out.gameResult),
out.custodyName));
final View gameStateView = this.flowEngine

View File

@ -1,17 +1,9 @@
package djmil.cordacheckers.gamestate;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import djmil.cordacheckers.contracts.GameCommand;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState;
import djmil.cordacheckers.states.GameResultState;
import djmil.cordacheckers.states.GameState;
import djmil.cordacheckers.contracts.GameInfo;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.InitiatedBy;
import net.corda.v5.application.flows.ResponderFlow;
@ -40,10 +32,7 @@ public class CommitSubFlowResponder implements ResponderFlow {
public void call(FlowSession session) {
UtxoTransactionValidator txValidator = trxToValidate -> {
try {
final GameCommand gameCommand = getSingleCommand(trxToValidate, GameCommand.class);
final GameState gameState = getActualGameStateFromTransaction(trxToValidate, gameCommand);
checkParticipants(session, gameCommand, gameState);
checkParticipants(session, trxToValidate);
/*
* Other checks / actions ?
@ -66,52 +55,20 @@ public class CommitSubFlowResponder implements ResponderFlow {
}
}
/**
*
* @param gameStateTransaction an utxo ledger transaction, that involves GameState
* @param command
* @return the most recent (from perspective of session participants validation) GameState for a given transaction
*
* @see djmil.cordacheckers.gamestate.ViewBuilder#getLatestGameStateFromTransaction(UtxoLedgerTransaction, GameCommand)
*/
@Suspendable
GameState getActualGameStateFromTransaction(UtxoLedgerTransaction gameStateTransaction, GameCommand command) {
switch (command.getAction()) {
case GAME_PROPOSAL_CREATE:
return getSingleOutputState(gameStateTransaction, GameProposalState.class);
case GAME_PROPOSAL_ACCEPT:
case GAME_PROPOSAL_REJECT:
case GAME_PROPOSAL_CANCEL:
return getSingleInputState(gameStateTransaction, GameProposalState.class);
case GAME_BOARD_SURRENDER:
return getSingleInputState(gameStateTransaction, GameBoardState.class);
case GAME_BOARD_MOVE:
return getSingleInputState(gameStateTransaction, GameBoardState.class);
case GAME_RESULT_CREATE:
return getSingleOutputState(gameStateTransaction, GameResultState.class);
}
throw new GameCommand.ActionException();
}
@Suspendable
void checkParticipants(FlowSession session, GameCommand gameCommand, GameState gameState) throws ParticipantException {
void checkParticipants(FlowSession session, UtxoLedgerTransaction gameStateUtxo) throws ParticipantException {
final GameInfo info = new GameInfo(gameStateUtxo);
final var conterpartyName = session.getCounterparty();
final var actorName = gameCommand.getInitiator(gameState);
if (actorName.compareTo(conterpartyName) != 0)
throw new ParticipantException("Actor", conterpartyName, actorName);
if (info.issuer.compareTo(conterpartyName) != 0)
throw new ParticipantException("Issuer", conterpartyName, info.issuer);
final var myName = memberLookup.myInfo().getName();
if (myName.getOrganizationUnit().equals("Custodian"))
return; // Custodian shall not validate state's counterparty
final var opponentName = gameState.getOpponentName(myName); // throws NotInvolved
final var opponentName = info.state.getOpponentName(myName); // throws NotInvolved
if (conterpartyName.compareTo(opponentName) != 0)
throw new ParticipantException("Counterparty", conterpartyName, opponentName);
throw new ParticipantException("Opponent", conterpartyName, opponentName);
}
public static class ParticipantException extends Exception {

View File

@ -48,7 +48,7 @@ public class View {
this.uuid = null;
}
public View(View.Status status, GameProposalState gameProposal, MemberX500Name myName) {
public View(View.Status status, MemberX500Name myName, GameProposalState gameProposal) {
this.status = status;
this.opponentName = gameProposal.getOpponentName(myName).getCommonName();
this.opponentColor = gameProposal.getOpponentColor(myName);
@ -59,18 +59,7 @@ public class View {
this.uuid = gameProposal.getGameUuid();
}
public View(View.Status status, GameBoardState gameBoard, MemberX500Name myName) {
this.status = status;
this.opponentName = gameBoard.getOpponentName(myName).getCommonName();
this.opponentColor = gameBoard.getOpponentColor(myName);
this.board = gameBoard.getBoard();
this.moveNumber = gameBoard.getMoveNumber();
this.previousMove = null;
this.message = gameBoard.getMessage();
this.uuid = gameBoard.getGameUuid();
}
public View(View.Status status, GameBoardState gameBoard, List<Integer> previousMove, MemberX500Name myName) {
public View(View.Status status, MemberX500Name myName, GameBoardState gameBoard, List<Integer> previousMove) {
this.status = status;
this.opponentName = gameBoard.getOpponentName(myName).getCommonName();
this.opponentColor = gameBoard.getOpponentColor(myName);
@ -81,7 +70,7 @@ public class View {
this.uuid = gameBoard.getGameUuid();
}
public View(View.Status status, GameResultState gameResult, MemberX500Name myName) {
public View(View.Status status, MemberX500Name myName, GameResultState gameResult) {
this.status = status;
this.opponentName = gameResult.getOpponentName(myName).getCommonName();
this.opponentColor = gameResult.getOpponentColor(myName);

View File

@ -1,14 +1,11 @@
package djmil.cordacheckers.gamestate;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleCommand;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleOutputState;
import static djmil.cordacheckers.contracts.UtxoLedgerTransactionUtil.getSingleInputState;
import djmil.cordacheckers.contracts.GameCommand;
import djmil.cordacheckers.contracts.GameInfo;
import djmil.cordacheckers.states.GameBoardState;
import djmil.cordacheckers.states.GameProposalState;
import djmil.cordacheckers.states.GameResultState;
import djmil.cordacheckers.states.GameState;
import net.corda.v5.application.flows.CordaInject;
import net.corda.v5.application.flows.SubFlow;
import net.corda.v5.application.membership.MemberLookup;
@ -45,76 +42,32 @@ public class ViewBuilder implements SubFlow<View> {
@Suspendable
View buildGameStateView(UtxoLedgerTransaction gameStateUtxo) {
MemberX500Name myName = memberLookup.myInfo().getName();
final GameInfo game = new GameInfo(gameStateUtxo);
final MemberX500Name myName = memberLookup.myInfo().getName();
final View.Status viewStatus = action2status(game, myName);
final GameCommand command = getSingleCommand(gameStateUtxo, GameCommand.class);
final GameState state = getLatestGameStateFromTransaction(gameStateUtxo, command);
final View.Status viewStatus = action2status(command, state, myName);
if (game.state instanceof GameProposalState)
return new View(viewStatus, myName, (GameProposalState) game.state);
switch (command.getAction()) {
case GAME_PROPOSAL_CREATE:
case GAME_PROPOSAL_CANCEL:
case GAME_PROPOSAL_REJECT:
if (state instanceof GameProposalState)
return new View(viewStatus, (GameProposalState)state, myName);
break;
if (game.state instanceof GameBoardState)
return new View(viewStatus, myName, (GameBoardState) game.state, game.command.getMove());
case GAME_PROPOSAL_ACCEPT:
case GAME_BOARD_MOVE:
if (state instanceof GameBoardState)
return new View(viewStatus, (GameBoardState)state, command.getMove(), myName);
break;
case GAME_BOARD_SURRENDER:
case GAME_RESULT_CREATE:
if (state instanceof GameResultState)
return new View(viewStatus, (GameResultState)state, myName);
break;
}
if (game.state instanceof GameResultState)
return new View(viewStatus, myName, (GameResultState) game.state);
throw new GameCommand.ActionException();
}
/**
*
* @param gameStateTransaction an utxo ledger transaction, that involves GameState
* @param command
* @return the most recent (from perspective of building a GameView) GameState for a given transaction
*
* @see djmil.cordacheckers.gamestate.CommitSubFlowResponder#getActualGameStateFromTransaction(UtxoLedgerTransaction, GameCommand)
*/
@Suspendable
GameState getLatestGameStateFromTransaction(UtxoLedgerTransaction gameStateTransaction, GameCommand command) {
switch (command.getAction()) {
case GAME_PROPOSAL_CREATE:
return getSingleOutputState(gameStateTransaction, GameProposalState.class);
case GAME_PROPOSAL_REJECT:
case GAME_PROPOSAL_CANCEL:
return getSingleInputState(gameStateTransaction, GameProposalState.class);
case GAME_PROPOSAL_ACCEPT:
case GAME_BOARD_MOVE:
return getSingleOutputState(gameStateTransaction, GameBoardState.class);
case GAME_BOARD_SURRENDER:
case GAME_RESULT_CREATE:
return getSingleOutputState(gameStateTransaction, GameResultState.class);
}
throw new GameCommand.ActionException();
throw new RuntimeException("Unexpected GameState");
}
@Suspendable
View.Status action2status(GameCommand command, GameState state, MemberX500Name myName) {
final boolean myAction = command.getInitiator(state).compareTo(myName) == 0;
View.Status action2status(GameInfo game, MemberX500Name myName) {
switch (command.getAction()) {
switch (game.command.getAction()) {
case GAME_PROPOSAL_CREATE:
if (myAction)
return View.Status.GAME_PROPOSAL_WAIT_FOR_OPPONENT;
else
if (game.isMyAction(myName))
return View.Status.GAME_PROPOSAL_WAIT_FOR_YOU;
else
return View.Status.GAME_PROPOSAL_WAIT_FOR_OPPONENT;
case GAME_PROPOSAL_REJECT:
return View.Status.GAME_PROPOSAL_REJECTED;
@ -124,19 +77,19 @@ public class ViewBuilder implements SubFlow<View> {
case GAME_PROPOSAL_ACCEPT:
case GAME_BOARD_MOVE:
if (myAction)
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:
if (myAction)
if (game.issuer.compareTo(myName) == 0)
return View.Status.GAME_RESULT_YOU_LOOSE;
else
return View.Status.GAME_RESULT_YOU_WON;
case GAME_RESULT_CREATE:
if (myAction)
case GAME_BOARD_VICTORY:
if (game.issuer.compareTo(myName) == 0)
return View.Status.GAME_RESULT_YOU_WON;
else
return View.Status.GAME_RESULT_YOU_LOOSE;