croupier: Improve Game Affordances for Pass and Play (split)

Pass Affordances added
- dragging animations restored
- now has a status bar to show who to pass to/take from
- has arrows indicating direction

Play affordances added to split view
- better animations in for new cards

Cards can no longer be dropped on the card collection they
originate from.

Change-Id: I02724eef628b9545f6f11d27d3fe9377ae34c8bd
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 41b072c..bb611ac 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -76,6 +76,8 @@
 
 class HeartsBoardState extends State<HeartsBoard> {
   Widget build(BuildContext context) {
+    double offscreenDelta = config.isMini ? 5.0 : 2.0;
+
     return new Container(
         height: config.height,
         width: config.width,
@@ -87,24 +89,24 @@
                   ? _buildMiniBoardLayout()
                   : _buildBoardLayout()),
           new Positioned(
-              top: config.height * 5.5,
+              top: config.height * (offscreenDelta + 0.5),
               left: (config.width - config.cardWidth) / 2,
-              child: _buildTrick(
+              child: _buildOffScreenCards(
                   config.isMini ? rotateByGamePlayerNumber(0) : 0)), // bottom
           new Positioned(
               top: (config.height - config.cardHeight) / 2,
-              left: config.width * -4.5,
-              child: _buildTrick(
+              left: config.width * (-offscreenDelta + 0.5),
+              child: _buildOffScreenCards(
                   config.isMini ? rotateByGamePlayerNumber(1) : 1)), // left
           new Positioned(
-              top: config.height * -4.5,
+              top: config.height * (-offscreenDelta + 0.5),
               left: (config.width - config.cardWidth) / 2,
-              child: _buildTrick(
+              child: _buildOffScreenCards(
                   config.isMini ? rotateByGamePlayerNumber(2) : 2)), // top
           new Positioned(
               top: (config.height - config.cardHeight) / 2,
-              left: config.width * 5.5,
-              child: _buildTrick(
+              left: config.width * (offscreenDelta + 0.5),
+              child: _buildOffScreenCards(
                   config.isMini ? rotateByGamePlayerNumber(3) : 3)) // right
         ]));
   }
@@ -161,7 +163,6 @@
         child: new CardCollectionComponent(
             showCard, true, CardCollectionOrientation.show1,
             useKeys: true,
-            animationType: component_card.CardAnimationType.NONE,
             acceptCallback: config.gameAcceptCallback,
             acceptType: isMe && isPlayerTurn ? DropType.card : DropType.none,
             widthCard: config.cardWidth - 6.0,
@@ -319,7 +320,7 @@
             useKeys: true));
   }
 
-  Widget _buildTrick(int playerNumber) {
+  Widget _buildOffScreenCards(int playerNumber) {
     HeartsGame game = config.game;
 
     List<logic_card.Card> cards =
@@ -327,9 +328,18 @@
     // If took trick, exclude the last 4 cards for the trick taking animation.
     if (config.trickTaking && playerNumber == game.lastTrickTaker) {
       cards = new List.from(cards.sublist(0, cards.length - 4));
+    } else {
+      cards = new List.from(cards);
     }
 
-    double sizeFactor = config.isMini ? 1.0 : 2.0;
+    double sizeFactor = 2.0;
+    if (config.isMini) {
+      sizeFactor = 1.0;
+      if (playerNumber != game.playerNumber) {
+        cards.addAll(
+            game.cardCollections[playerNumber + HeartsGame.OFFSET_HAND]);
+      }
+    }
 
     return new CardCollectionComponent(
         cards, true, CardCollectionOrientation.show1,
diff --git a/lib/components/card_collection.dart b/lib/components/card_collection.dart
index 1a2f93f..6ede30d 100644
--- a/lib/components/card_collection.dart
+++ b/lib/components/card_collection.dart
@@ -75,10 +75,12 @@
 class CardCollectionComponentState extends State<CardCollectionComponent> {
   String status = 'bar';
 
-  bool _handleWillAccept(dynamic data) {
-    print('will accept?');
-    print(data);
-    return true;
+  bool _handleWillAccept(component_card.Card data) {
+    return !config.cards.contains(data.card);
+  }
+
+  bool _handleWillAcceptMultiple(CardCollectionComponent data) {
+    return data != config; // don't accept your own self.
   }
 
   void _handleAccept(component_card.Card data) {
@@ -344,7 +346,7 @@
         });
       case DropType.card_collection:
         return new DragTarget<CardCollectionComponent>(
-            onWillAccept: _handleWillAccept,
+            onWillAccept: _handleWillAcceptMultiple,
             onAccept: _handleAcceptMultiple, builder:
                 (BuildContext context, List<CardCollectionComponent> data, _) {
           return new Container(
diff --git a/lib/components/game.dart b/lib/components/game.dart
index c726f92..7a858ad 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -10,6 +10,7 @@
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
+import 'package:vector_math/vector_math_64.dart' as vector_math;
 
 import '../logic/card.dart' as logic_card;
 import '../logic/croupier.dart' show Croupier;
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 60cef30..01f223b 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -141,15 +141,17 @@
                 .add(HeartsGame.OFFSET_HAND + playerNum);
             break;
           case HeartsPhase.Play:
-            for (int i = 0; i < 4; i++) {
-              if (_showSplitView || i == playerNum) {
+            if (_showSplitView) {
+              for (int i = 0; i < 4; i++) {
+                visibleCardCollectionIndexes.add(HeartsGame.OFFSET_HAND + i);
+                visibleCardCollectionIndexes.add(HeartsGame.OFFSET_TRICK + i);
                 visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PLAY + i);
               }
-              if (_showSplitView) {
-                visibleCardCollectionIndexes
-                    .add(HeartsGame.OFFSET_HAND + playerNum);
-                visibleCardCollectionIndexes.add(HeartsGame.OFFSET_TRICK + i);
-              }
+            } else {
+              visibleCardCollectionIndexes
+                  .add(HeartsGame.OFFSET_PLAY + playerNum);
+              visibleCardCollectionIndexes
+                  .add(HeartsGame.OFFSET_HAND + playerNum);
             }
 
             break;
@@ -252,7 +254,7 @@
         game.debugString = null;
       } catch (e) {
         print("You can't do that! ${e.toString()}");
-        config.game.debugString = e.toString();
+        config.game.debugString = "You must pass 3 cards";
       }
     });
   }
@@ -394,16 +396,44 @@
   String _getStatus() {
     HeartsGame game = config.game;
 
-    // Who's turn is it?
-    String name = _getName(game.whoseTurn) ?? "Player ${game.whoseTurn}";
-    String status =
-        game.whoseTurn == game.playerNumber ? "Your turn" : "${name}'s turn";
+    String status;
+    switch (game.phase) {
+      case HeartsPhase.Play:
+        // Who's turn is it?
+        String name = _getName(game.whoseTurn) ?? "Player ${game.whoseTurn}";
+        status = game.whoseTurn == game.playerNumber
+            ? "Your turn"
+            : "${name}'s turn";
 
-    // Override if someone is taking a trick.
-    if (this.trickTaking) {
-      String trickTaker =
-          _getName(game.lastTrickTaker) ?? "Player ${game.lastTrickTaker}";
-      status = "${trickTaker}'s trick";
+        // Override if someone is taking a trick.
+        if (this.trickTaking) {
+          String trickTaker =
+              _getName(game.lastTrickTaker) ?? "Player ${game.lastTrickTaker}";
+          status = game.lastTrickTaker == game.playerNumber
+              ? "Your trick"
+              : "${trickTaker}'s trick";
+        }
+        break;
+      case HeartsPhase.Pass:
+        if (game.hasPassed(game.playerNumber)) {
+          status = "Waiting for cards...";
+        } else {
+          String name =
+              _getName(game.passTarget) ?? "Player ${game.passTarget}";
+          status = "Pass to ${name}";
+        }
+        break;
+      case HeartsPhase.Take:
+        if (game.hasTaken(game.playerNumber)) {
+          status = "Waiting for other players...";
+        } else {
+          String name =
+              _getName(game.takeTarget) ?? "Player ${game.takeTarget}";
+          status = "Take from ${name}";
+        }
+        break;
+      default:
+        break;
     }
 
     // Override if there is a debug string.
@@ -415,18 +445,53 @@
   }
 
   Widget _buildStatusBar() {
+    HeartsGame game = config.game;
+
+    List<Widget> statusWidgets = new List<Widget>();
+    statusWidgets.add(new Text(_getStatus(), style: style.Text.largeStyle));
+
+    switch (game.phase) {
+      case HeartsPhase.Play:
+        statusWidgets
+            .add(new IconButton(icon: "action/swap_vert", onPressed: () {
+          setState(() {
+            _showSplitView = !_showSplitView;
+          });
+        }));
+        break;
+      case HeartsPhase.Pass:
+      case HeartsPhase.Take:
+        // TODO(alexfandrianto): Icons for arrow_upward and arrow_downward were
+        // just added to the material icon list. However, they are not available
+        // through Flutter yet.
+        double rotationAngle = 0.0; // right
+        switch (game.roundNumber % 4) {
+          case 1:
+            rotationAngle = math.PI; // left
+            break;
+          case 2:
+            rotationAngle = -math.PI / 2; // up
+            break;
+        }
+        if (game.phase == HeartsPhase.Take) {
+          rotationAngle = rotationAngle + math.PI; // opposite
+        }
+        statusWidgets.add(new Transform(
+            transform:
+                new vector_math.Matrix4.identity().rotateZ(rotationAngle),
+            alignment: new FractionalOffset(0.5, 0.5),
+            child: new Icon(icon: "navigation/arrow_forward")));
+        break;
+      default:
+        break;
+    }
+
     return new Container(
         padding: new EdgeDims.all(10.0),
         decoration:
             new BoxDecoration(backgroundColor: style.theme.primaryColor),
-        child: new Row([
-          new Text(_getStatus(), style: style.Text.largeStyle),
-          new IconButton(icon: "action/swap_vert", onPressed: () {
-            setState(() {
-              _showSplitView = !_showSplitView;
-            });
-          })
-        ], justifyContent: FlexJustifyContent.spaceBetween));
+        child: new Row(statusWidgets,
+            justifyContent: FlexJustifyContent.spaceBetween));
   }
 
   Widget _buildFullMiniBoard() {
@@ -465,7 +530,6 @@
                   true,
                   CardCollectionOrientation.show1,
                   useKeys: true,
-                  animationType: component_card.CardAnimationType.NONE,
                   acceptCallback: _makeGameMoveCallback,
                   acceptType:
                       p == game.whoseTurn ? DropType.card : DropType.none,
@@ -478,7 +542,7 @@
       cardCollections.add(new Container(
           decoration:
               new BoxDecoration(backgroundColor: style.theme.primaryColor),
-          child: new Column([_buildStatusBar(), playArea])));
+          child: new BlockBody([_buildStatusBar(), playArea])));
     }
 
     List<logic_card.Card> cards = game.cardCollections[p];
@@ -487,10 +551,8 @@
         dragChildren: true, // Can drag, but may not have anywhere to drop
         comparator: _compareCards,
         width: config.width,
-        useKeys: _showSplitView);
-    cardCollections.add(c); // flex
-
-    cardCollections.add(_makeDebugButtons());
+        useKeys: true);
+    cardCollections.add(new BlockBody([c, _makeDebugButtons()]));
 
     return new Column(cardCollections,
         justifyContent: FlexJustifyContent.spaceBetween);
@@ -600,32 +662,39 @@
 
     Color bgColor = completed ? Colors.teal[600] : Colors.teal[500];
 
+    Widget statusBar = _buildStatusBar();
+
     Widget topArea = new Container(
         decoration: new BoxDecoration(backgroundColor: bgColor),
         padding: new EdgeDims.all(10.0),
         width: config.width,
         child: new Flex(topCardWidgets,
             justifyContent: FlexJustifyContent.spaceBetween));
+    Widget combinedTopArea = new BlockBody([statusBar, topArea]);
 
     Widget handArea = new CardCollectionComponent(
         hand, true, CardCollectionOrientation.suit,
         dragChildren: draggable,
         comparator: _compareCards,
         width: config.width,
+        acceptCallback: cb,
+        acceptType: cb != null ? DropType.card : null,
         backgroundColor: Colors.grey[500],
         altColor: Colors.grey[700],
         useKeys: true);
 
-    return new Column(<Widget>[topArea, handArea, _makeDebugButtons()],
+    Widget combinedBottomArea = new BlockBody([handArea, _makeDebugButtons()]);
+
+    return new Column(<Widget>[combinedTopArea, combinedBottomArea],
         justifyContent: FlexJustifyContent.spaceBetween);
   }
 
   Widget _topCardWidget(List<logic_card.Card> cards, AcceptCb cb) {
     Widget ccc = new CardCollectionComponent(
         cards, true, CardCollectionOrientation.show1,
+        dragChildren: cb != null,
         acceptCallback: cb,
         acceptType: cb != null ? DropType.card : null,
-        animationType: component_card.CardAnimationType.NONE,
         backgroundColor: Colors.white,
         altColor: Colors.grey[200],
         useKeys: true);
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index a635060..8a25172 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -207,10 +207,14 @@
       cardCollections[PLAYER_C].length == 13 &&
       cardCollections[PLAYER_D].length == 13;
 
+  bool hasPassed(int player) =>
+      cardCollections[player + OFFSET_PASS].length == 3;
   bool get allPassed => cardCollections[PLAYER_A_PASS].length == 3 &&
       cardCollections[PLAYER_B_PASS].length == 3 &&
       cardCollections[PLAYER_C_PASS].length == 3 &&
       cardCollections[PLAYER_D_PASS].length == 3;
+  bool hasTaken(int player) =>
+      cardCollections[getTakeTarget(player) + OFFSET_PASS].length == 0;
   bool get allTaken => cardCollections[PLAYER_A_PASS].length == 0 &&
       cardCollections[PLAYER_B_PASS].length == 0 &&
       cardCollections[PLAYER_C_PASS].length == 0 &&
diff --git a/manifest.yaml b/manifest.yaml
index f9e8c92..6fbe3fb 100644
--- a/manifest.yaml
+++ b/manifest.yaml
@@ -6,6 +6,7 @@
   - name: av/play_arrow
   - name: action/swap_vert
   - name: navigation/arrow_back
+  - name: navigation/arrow_forward
   - name: navigation/menu
 assets:
   - images/default/classic/down/c10.png