showPlayCard(action, animateDuration, showDuration, noFlip) {
    const sdkCard = action.getCard();
    const currentSdkCard = this._playCardNode.getSdkCard();
    if (currentSdkCard == null || currentSdkCard !== sdkCard) {
      // stop showing previous
      this.stopShowingPlayCard(currentSdkCard);

      // set sdk card to trigger rebuild
      this._playCardNode.setSdkCard(sdkCard, action);
      const cardContentSize = this._playCardNode.getCardContentSize();
      const ownerId = action.getOwnerId();
      const isOwnedByPlayer2 = ownerId === SDK.GameSession.getInstance().getPlayer2Id();
      const isOwnedByMyPlayer = !SDK.GameSession.getInstance().getIsSpectateMode()
        && ownerId === SDK.GameSession.getInstance().getMyPlayerId()
        && !(action instanceof SDK.RevealHiddenCardAction);

      // calculate positions
      let sourceScreenPosition;
      let targetScreenPosition;

      // start position from general
      if (isOwnedByPlayer2) {
        sourceScreenPosition = this.getGeneralNodeForPlayer2().getPosition();
      } else {
        sourceScreenPosition = this.getGeneralNodeForPlayer1().getPosition();
      }
      sourceScreenPosition.y += cardContentSize.height * 0.5;

      if (isOwnedByMyPlayer) {
        // target position in fixed my play card location
        if (isOwnedByPlayer2) {
          targetScreenPosition = UtilsEngine.getPlayer2InspectOffBoardCardPosition();
          targetScreenPosition.y -= cardContentSize.height * 0.5;
        } else {
          targetScreenPosition = UtilsEngine.getPlayer1InspectOffBoardCardPosition();
          targetScreenPosition.y -= cardContentSize.height * 0.5;
        }
      } else {
        // target position is center of screen for opponent
        targetScreenPosition = UtilsEngine.getGSIWinCenterPosition();
      }

      // ensure card itself doesn't go outside screen
      targetScreenPosition.x = Math.max(Math.min(targetScreenPosition.x, UtilsEngine.getGSIWinRight() - cardContentSize.width * 0.5), UtilsEngine.getGSIWinLeft() + cardContentSize.width * 0.5);
      targetScreenPosition.y = Math.max(Math.min(targetScreenPosition.y, UtilsEngine.getGSIWinTop() - cardContentSize.height * 0.5), UtilsEngine.getGSIWinBottom() + cardContentSize.height * 0.5);

      // show play
      this._playCardNode.showPlay(null, action, EVENTS.update_cache_action, sourceScreenPosition, targetScreenPosition, animateDuration, showDuration, noFlip, this.getShouldShowCardInstructionals())
        .then(() => {
          const playerActor = SDK.GameSession.getInstance().getIsSpectateMode() ? this._altPlayer : this._player;
          if (!isOwnedByMyPlayer || !playerActor.getHasCardsWithFollowup()) {
            this.stopShowingPlayCard(sdkCard, CONFIG.ANIMATE_MEDIUM_DURATION);
          }
        });

      this.getEventBus().trigger(EVENTS.play_card_start, { type: EVENTS.play_card_start, card: sdkCard });
    }
  }
Starts showing a played card
@param {SDK.Action} action action that played card to show
@param {Number} animateDuration duration of animation in seconds
@param {Number} showDuration duration to show card in seconds
@param {Boolean} [noFlip=false] whether to skip flip animation