Merge "Removed Position struct Put vecs directly into card and staticimg"
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 4e98cbb..5614354 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -80,7 +80,8 @@
           cards, false, ori,
           width: i % 2 == 0 ? cccSize : null,
           height: i % 2 != 0 ? cccSize : null,
-          rotation: -math.PI / 2 * i);
+          rotation: -math.PI / 2 * i,
+          useKeys: true);
       Widget w;
       switch (i) {
         case 2:
@@ -138,7 +139,8 @@
           widthCard: this.cardWidth,
           height: this.cardHeight,
           heightCard: this.cardHeight,
-          rotation: -math.PI / 2 * i);
+          rotation: -math.PI / 2 * i,
+          useKeys: true);
       Widget w;
 
       double left02 = (this.width - this.cardWidth) / 2;
diff --git a/lib/components/card.dart b/lib/components/card.dart
index 03f6db7..de932aa 100644
--- a/lib/components/card.dart
+++ b/lib/components/card.dart
@@ -4,44 +4,248 @@
 
 import '../logic/card.dart' as logic_card;
 
+import 'dart:async';
+
+import 'package:flutter/animation.dart';
 import 'package:flutter/material.dart' as widgets;
+import 'package:flutter/rendering.dart';
 import 'package:vector_math/vector_math_64.dart' as vector_math;
 
-class Card extends widgets.StatelessComponent {
-  final logic_card.Card card;
-  final bool faceUp;
-  final double _width;
-  final double _height;
-  final double rotation;
+enum CardAnimationType {
+  NONE, OLD_TO_NEW, IN_TOP
+}
 
-  double get width => _width ?? 40.0;
-  double get height => _height ?? 40.0;
+enum CardUIType {
+  CARD, ZCARD
+}
 
-  Card(this.card, this.faceUp,
-      {double width, double height, this.rotation: 0.0})
-      : _width = width,
-        _height = height;
+class GlobalCardKey extends widgets.GlobalKey {
+  logic_card.Card card;
+  CardUIType type;
 
-  widgets.Widget build(widgets.BuildContext context) {
-    // TODO(alexfandrianto): This isn't a nice way of doing Rotation.
-    // The reason is that you must know the width and height of the image.
-    // Feature Request: https://github.com/flutter/engine/issues/1452
-    return new widgets.Listener(
-        child: new widgets.Container(
-            width: width,
-            height: height,
-            child: new widgets.Transform(
-                child: _imageFromCard(card, faceUp),
-                transform: new vector_math.Matrix4.identity()
-                    .translate(this.width / 2, this.height / 2)
-                    .rotateZ(this.rotation)
-                    .translate(-this.width / 2, -this.height / 2))));
+  GlobalCardKey(this.card, this.type): super.constructor();
+
+  bool operator ==(Object other) {
+    if (other is! GlobalCardKey) {
+      return false;
+    }
+    GlobalCardKey k = other;
+    return k.card == card && k.type == type;
   }
 
-  static widgets.Widget _imageFromCard(logic_card.Card c, bool faceUp) {
-    // TODO(alexfandrianto): Instead of 'default', what if we were told which theme to use?
-    String imageName =
-        "images/default/${c.deck}/${faceUp ? 'up' : 'down'}/${c.identifier}.png";
-    return new widgets.NetworkImage(src: imageName);
+  int get hashCode {
+    return 17 * card.hashCode + 33 * type.hashCode;
+  }
+}
+
+class ZCard extends widgets.StatefulComponent {
+  final logic_card.Card card;
+  final bool faceUp;
+  final double width;
+  final double height;
+  final double rotation;
+  final bool animateEntrance;
+  final double z;
+
+  final Point startingPosition;
+  final Point endingPosition;
+
+  ZCard(Card dataComponent, this.startingPosition, this.endingPosition) :
+    super(key: new GlobalCardKey(dataComponent.card, CardUIType.ZCARD)),
+    card = dataComponent.card,
+    faceUp = dataComponent.faceUp,
+    width = dataComponent.width ?? 40.0,
+    height = dataComponent.height ?? 40.0,
+    rotation = dataComponent.rotation,
+    animateEntrance = dataComponent.animateEntrance,
+    z = dataComponent.z;
+
+  _ZCardState createState() => new _ZCardState();
+}
+
+class Card extends widgets.StatefulComponent {
+  final logic_card.Card card;
+  final bool faceUp;
+  final double width;
+  final double height;
+  final double rotation;
+  final bool useKey;
+  final bool visible;
+  final bool animateEntrance;
+  final double z;
+
+  Card(logic_card.Card card, this.faceUp,
+      {double width, double height, this.rotation: 0.0, bool useKey: false, this.visible: true, this.animateEntrance: true, this.z})
+      : card = card,
+        width = width ?? 40.0,
+        height = height ?? 40.0,
+        useKey = useKey,
+        super(key: useKey ? new GlobalCardKey(card, CardUIType.CARD) : null);
+
+  // Use this helper to help create a Card clone.
+  // Used by the drag and drop layer.
+  Card clone({bool visible}) {
+    return new Card(this.card, this.faceUp,
+      width: width, height: height, rotation: rotation,
+      useKey: false, visible: visible ?? this.visible, animateEntrance: false,
+      z: z);
+  }
+
+  CardState createState() => new CardState();
+}
+
+class CardState extends widgets.State<Card> {
+  // TODO(alexfandrianto): This bug is why some cards appear slightly off.
+  // https://github.com/flutter/engine/issues/1939
+  Point getGlobalPosition() {
+    RenderBox box = context.findRenderObject();
+    return box.localToGlobal(Point.origin);
+  }
+
+  widgets.Widget build(widgets.BuildContext context) {
+    widgets.Widget image = null;
+    if (config.visible) {
+      image = new widgets.Transform(
+                child: _imageFromCard(config.card, config.faceUp),
+                transform: new vector_math.Matrix4.identity()
+                    .rotateZ(config.rotation),
+                alignment: new FractionalOffset(0.5, 0.5));
+    }
+
+    return new widgets.Container(
+            width: config.width,
+            height: config.height,
+            child: image);
+  }
+}
+
+widgets.Widget _imageFromCard(logic_card.Card c, bool faceUp) {
+  // TODO(alexfandrianto): Instead of 'default', what if we were told which theme to use?
+  String imageName =
+      "images/default/${c.deck}/${faceUp ? 'up' : 'down'}/${c.identifier}.png";
+  return new widgets.NetworkImage(src: imageName);
+}
+
+class _ZCardState extends widgets.State<ZCard> {
+  ValuePerformance<Point> _performance;
+  List<Point> _pointQueue; // at least 1 longer than the current animation index.
+  int _animationIndex;
+  bool _cardUpdateScheduled = false;
+
+  @override
+  void initState() {
+    super.initState();
+    _initialize();
+    scheduleUpdatePosition();
+  }
+
+  void _initialize() {
+    _pointQueue = new List<Point>();
+    _animationIndex = 0;
+    if (config.startingPosition != null) {
+      _pointQueue.add(config.startingPosition);
+    }
+    _pointQueue.add(config.endingPosition);
+    _performance = new ValuePerformance<Point>(
+      variable: new AnimatedValue<Point>(Point.origin, curve: Curves.ease),
+      duration: const Duration(milliseconds: 250)
+    );
+    _performance.addStatusListener((PerformanceStatus status) {
+      if (status == PerformanceStatus.completed) {
+        _animationIndex++;
+        _tryAnimate();
+      }
+    });
+  }
+
+  void scheduleUpdatePosition() {
+    if (!_cardUpdateScheduled) {
+      _cardUpdateScheduled = true;
+      scheduleMicrotask(_updatePosition);
+    }
+  }
+
+  // These microtasks are being scheduled on every build change.
+  // Theoretically, this is too often, but to be safe, it is also good to do it.
+  @override
+  void didUpdateConfig(ZCard oldConfig) {
+    if (config.key != oldConfig.key) {
+      _initialize();
+    } else {
+      // Do we need to animate to a new location? If so, add it to the queue.
+      if (config.endingPosition != _pointQueue.last) {
+        setState(() {
+          _pointQueue.add(config.endingPosition);
+          _tryAnimate();
+        });
+      }
+    }
+    scheduleUpdatePosition();
+  }
+
+  // A callback that sets up the animation from point a to point b.
+  void _updatePosition() {
+    _cardUpdateScheduled = false; // allow the next attempt to schedule _updatePosition to succeed.
+    if (!config.animateEntrance || _pointQueue.length == 1) {
+      RenderBox box = context.findRenderObject();
+      Point endingLocation = box.globalToLocal(config.endingPosition);
+      _performance.variable
+        ..begin = endingLocation
+        ..value = endingLocation
+        ..end = endingLocation;
+      _performance.progress = 0.0;
+      return;
+    }
+
+    _tryAnimate();
+  }
+
+  bool _needsAnimation() {
+    return _animationIndex < _pointQueue.length - 1;
+  }
+
+  void _tryAnimate() {
+    // Let animations finish... (Is this a good idea?)
+    if (!_performance.isAnimating && _needsAnimation()) {
+      RenderBox box = context.findRenderObject();
+      Point globalStart = _pointQueue[_animationIndex];
+      Point globalEnd = _pointQueue[_animationIndex + 1];
+      Point startingLocation = box.globalToLocal(globalStart);
+      Point endingLocation = box.globalToLocal(globalEnd);
+      _performance.variable
+        ..begin = startingLocation
+        ..value = startingLocation
+        ..end = endingLocation;
+      _performance.progress = 0.0;
+      _performance.play();
+    }
+  }
+
+  widgets.Widget build(widgets.BuildContext context) {
+    widgets.Widget image = new widgets.Transform(
+      child: _imageFromCard(config.card, config.faceUp),
+      transform: new vector_math.Matrix4.identity()
+          .rotateZ(config.rotation),
+      alignment: new FractionalOffset(0.5, 0.5));
+
+    // Set up the drag listener.
+    widgets.Widget listeningCard = new widgets.Listener(
+        child: new widgets.Container(
+            width: config.width,
+            height: config.height,
+            child: image));
+
+    // Set up the slide transition.
+    // During animation, we must ignore all events.
+    widgets.Widget retWidget = new widgets.IgnorePointer(
+      ignoring: _performance.isAnimating,
+      child: new widgets.SlideTransition(
+        performance: _performance.view,
+        position: _performance.variable,
+        child: listeningCard
+      )
+    );
+
+    return retWidget;
   }
 }
diff --git a/lib/components/card_collection.dart b/lib/components/card_collection.dart
index 80c76a5..363a269 100644
--- a/lib/components/card_collection.dart
+++ b/lib/components/card_collection.dart
@@ -7,7 +7,6 @@
 
 import 'dart:math' as math;
 import 'package:flutter/material.dart';
-import 'package:flutter/material.dart' as material;
 
 enum CardCollectionOrientation { vert, horz, fan, show1, suit }
 enum DropType {
@@ -17,6 +16,7 @@
   // I can see that both would be nice, but I'm not sure how to do that yet.
 }
 
+typedef double PosComputer(int index);
 typedef void AcceptCb(dynamic data, List<logic_card.Card> cards);
 
 const double DEFAULT_WIDTH = 200.0;
@@ -43,10 +43,11 @@
   final Color _backgroundColor;
   final Color _altColor;
   final double rotation; // This angle is in radians.
+  final bool useKeys; // If set, every Card created in this collection will be keyed.
 
   DropType get acceptType => _acceptType ?? DropType.none;
-  Color get backgroundColor => _backgroundColor ?? material.Colors.grey[500];
-  Color get altColor => _altColor ?? material.Colors.grey[500];
+  Color get backgroundColor => _backgroundColor ?? Colors.grey[500];
+  Color get altColor => _altColor ?? Colors.grey[500];
 
   CardCollectionComponent(
       this.cards, this.faceUp, this.orientation,
@@ -60,7 +61,8 @@
       this.heightCard: DEFAULT_CARD_HEIGHT,
       Color backgroundColor,
       Color altColor,
-      this.rotation: 0.0})
+      this.rotation: 0.0,
+      this.useKeys: false})
       : _acceptType = acceptType,
         _backgroundColor = backgroundColor,
         _altColor = altColor;
@@ -135,30 +137,49 @@
     }
   }
 
-  double get _produceColumnWidth => config.widthCard + CARD_MARGIN * 2;
-  Widget _produceColumn(List<Widget> cardWidgets) {
-    // Let's do a stack of positioned cards!
-    List<Widget> kids = new List<Widget>();
+  List<Widget> _makeDraggableAndPositioned(List<component_card.Card> cardWidgets, PosComputer topComputer, PosComputer leftComputer) {
+    List<Widget> ret = new List<Widget>();
+    for (int i = 0; i < cardWidgets.length; i++) {
+      Point p = new Point(leftComputer(i), topComputer(i));
 
+      component_card.Card w = cardWidgets[i];
+      Widget widgetToAdd = w;
+      if (config.dragChildren) {
+        widgetToAdd = new Draggable(
+          child: w,
+          data: w,
+          feedback: new Opacity(child: w.clone(visible: true), opacity: 0.5));
+      }
+      widgetToAdd = new Positioned(
+        left: p.x,
+        top: p.y,
+        child: widgetToAdd
+      );
+
+      ret.add(widgetToAdd);
+    }
+    return ret;
+  }
+
+  double get _produceColumnWidth => config.widthCard + CARD_MARGIN * 2;
+  Widget _produceColumn(List<component_card.Card> cardWidgets) {
     double h = config.height ?? config.heightCard * 5;
     double spacing = math.min(config.heightCard + CARD_MARGIN * 2,
         (h - config.heightCard - 2 * CARD_MARGIN) / (cardWidgets.length - 1));
 
-    for (int i = 0; i < cardWidgets.length; i++) {
-      kids.add(new Positioned(
-          top: CARD_MARGIN + spacing * i,
-          left: CARD_MARGIN,
-          child: cardWidgets[i]));
-    }
+    PosComputer topComputer = (int i) => CARD_MARGIN + spacing * i;
+    PosComputer leftComputer = (int i) => CARD_MARGIN;
+
+    List<Widget> draggableKids = _makeDraggableAndPositioned(cardWidgets, topComputer, leftComputer);
     return new Container(
         decoration: new BoxDecoration(backgroundColor: config.backgroundColor),
         height: config.height,
         width: _produceColumnWidth,
-        child: new Stack(kids));
+        child: new Stack(draggableKids));
   }
 
   double get _produceRowHeight => config.heightCard + CARD_MARGIN * 2;
-  Widget _produceRow(List<Widget> cardWidgets, {emptyBackgroundImage: ""}) {
+  Widget _produceRow(List<component_card.Card> cardWidgets, {emptyBackgroundImage: ""}) {
     if (cardWidgets.length == 0) {
       // Just return a centered background image.
       return new Container(
@@ -176,43 +197,36 @@
                           : new NetworkImage(src: emptyBackgroundImage)))));
     }
 
-    // Let's do a stack of positioned cards!
-    List<Widget> kids = new List<Widget>();
-
     double w = config.width ?? config.widthCard * 5;
     double spacing = math.min(config.widthCard + CARD_MARGIN * 2,
         (w - config.widthCard - 2 * CARD_MARGIN) / (cardWidgets.length - 1));
 
-    for (int i = 0; i < cardWidgets.length; i++) {
-      kids.add(new Positioned(
-          top: CARD_MARGIN,
-          left: CARD_MARGIN + spacing * i,
-          child: cardWidgets[i]));
-    }
+    PosComputer topComputer = (int i) => CARD_MARGIN;
+    PosComputer leftComputer = (int i) => CARD_MARGIN + spacing * i;
+
+    List<Widget> draggableKids = _makeDraggableAndPositioned(cardWidgets, topComputer, leftComputer);
     return new Container(
         decoration: new BoxDecoration(backgroundColor: config.backgroundColor),
         height: _produceRowHeight,
         width: config.width,
-        child: new Stack(kids));
+        child: new Stack(draggableKids));
   }
 
-  Widget _produceSingle(List<Widget> cardWidgets) {
-    List<Widget> kids = new List<Widget>();
+  Widget _produceSingle(List<component_card.Card> cardWidgets) {
+    PosComputer topComputer = (int i) => CARD_MARGIN;
+    PosComputer leftComputer = (int i) => CARD_MARGIN;
 
-    for (int i = 0; i < cardWidgets.length; i++) {
-      kids.add(new Positioned(
-          top: CARD_MARGIN, left: CARD_MARGIN, child: cardWidgets[i]));
-    }
+    List<Widget> draggableKids = _makeDraggableAndPositioned(cardWidgets, topComputer, leftComputer);
     return new Container(
         decoration: new BoxDecoration(backgroundColor: config.backgroundColor),
         height: _produceRowHeight,
         width: _produceColumnWidth,
-        child: new Stack(kids));
+        child: new Stack(draggableKids));
   }
 
   double get _whiteLineHeight => WHITE_LINE_HEIGHT;
 
-  Widget wrapCards(List<Widget> cardWidgets) {
+  Widget wrapCards(List<component_card.Card> cardWidgets) {
     switch (config.orientation) {
       case CardCollectionOrientation.vert:
         return _produceColumn(cardWidgets);
@@ -224,10 +238,10 @@
       case CardCollectionOrientation.show1:
         return _produceSingle(cardWidgets);
       case CardCollectionOrientation.suit:
-        List<Widget> cs = new List<Widget>();
-        List<Widget> ds = new List<Widget>();
-        List<Widget> hs = new List<Widget>();
-        List<Widget> ss = new List<Widget>();
+        List<component_card.Card> cs = new List<component_card.Card>();
+        List<component_card.Card> ds = new List<component_card.Card>();
+        List<component_card.Card> hs = new List<component_card.Card>();
+        List<component_card.Card> ss = new List<component_card.Card>();
 
         List<logic_card.Card> theCards =
             config.comparator != null ? this._sortedCards : config.cards;
@@ -253,7 +267,7 @@
         }
         return new Container(
             decoration:
-                new BoxDecoration(backgroundColor: material.Colors.white),
+                new BoxDecoration(backgroundColor: Colors.white),
             child: new Stack(<Widget>[
               new Positioned(
                   top: 0.0,
@@ -283,7 +297,7 @@
   }
 
   Widget _buildCollection() {
-    List<Widget> cardComponents = new List<Widget>();
+    List<component_card.Card> cardComponents = new List<component_card.Card>();
     List<logic_card.Card> cs =
         config.comparator != null ? this._sortedCards : config.cards;
 
@@ -291,16 +305,12 @@
       component_card.Card c = new component_card.Card(cs[i], config.faceUp,
           width: config.widthCard,
           height: config.heightCard,
-          rotation: config.rotation);
+          rotation: config.rotation,
+          visible: !config.useKeys, // TODO(alexfandrianto): Is there a case where you want an invisible card and a key?
+          useKey: config.useKeys,
+          z: 0.0 + i);
 
-      if (config.dragChildren) {
-        cardComponents.add(new Draggable(
-            child: c,
-            data: c,
-            feedback: new Opacity(child: c, opacity: 0.5)));
-      } else {
-        cardComponents.add(c);
-      }
+      cardComponents.add(c);
     }
 
     // Let's draw a stack of cards with DragTargets.
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index bd1bc62..26e5e99 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -149,7 +149,7 @@
                 config.croupier.game, () {
               config.croupier.game.quit();
               makeSetStateCallback(logic_croupier.CroupierState.Welcome)();
-            }, width: screenSize.width, height: screenSize.height));
+            }, width: screenSize.width, height: screenSize.height - ui.window.padding.top));
       default:
         assert(false);
         return null;
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 3bb7105..fb376d8 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -9,11 +9,13 @@
 import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase, HeartsType;
 import '../logic/solitaire/solitaire.dart' show SolitaireGame, SolitairePhase;
 import 'board.dart' show HeartsBoard;
+import 'card.dart' as component_card;
 import 'card_collection.dart'
     show CardCollectionComponent, DropType, CardCollectionOrientation, AcceptCb;
 
+import 'package:flutter/animation.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/material.dart' as material;
+import 'package:flutter/rendering.dart';
 
 part 'hearts/hearts.part.dart';
 part 'proto/proto.part.dart';
@@ -32,12 +34,19 @@
 }
 
 abstract class GameComponentState<T extends GameComponent> extends State<T> {
+  Map<logic_card.Card, CardAnimationData> cardLevelMap;
+
   void initState() {
     super.initState();
 
+    cardLevelMap = new Map<logic_card.Card, CardAnimationData>();
     config.game.updateCallback = update;
   }
 
+  void _reset() {
+    cardLevelMap.clear();
+  }
+
   // This callback is used to force the UI to draw when state changes occur
   // outside of the UIs control (e.g., synced data).
   void update() {
@@ -58,6 +67,84 @@
 
   @override
   Widget build(BuildContext context); // still UNIMPLEMENTED
+
+
+  void _cardLevelMapProcessAllVisible(List<int> visibleCardCollections) {
+    Game game = config.game;
+
+    for (int i = 0; i < visibleCardCollections.length; i++) {
+      int index = visibleCardCollections[i];
+      for (int j = 0; j < game.cardCollections[index].length; j++) {
+        _cardLevelMapProcess(game.cardCollections[index][j]);
+      }
+    }
+  }
+
+  void _cardLevelMapProcess(logic_card.Card logicCard) {
+    component_card.GlobalCardKey key = new component_card.GlobalCardKey(logicCard, component_card.CardUIType.CARD);
+    component_card.CardState cardState = key.currentState;
+    if (cardState == null) {
+      return; // There's nothing we can really do about this card since it hasn't drawn yet.
+    }
+    Point p = cardState.getGlobalPosition();
+    double z = cardState.config.z;
+    component_card.Card c = key.currentWidget;
+
+    assert(c == cardState.config);
+
+    CardAnimationData cad = cardLevelMap[logicCard];
+    if (cad == null || cad.newPoint != p || cad.z != z) {
+      setState(() {
+        cardLevelMap[logicCard] = new CardAnimationData(c, cad?.newPoint, p, z);
+      });
+    }
+  }
+
+  // Helper to build the card animation layer.
+  // Note: This isn't a component because of its dependence on Widgets.
+  Widget buildCardAnimationLayer(List<int> visibleCardCollections) {
+    // It's possible that some cards need to be moved after this build.
+    // If so, we can catch it in the next frame.
+    scheduler.requestPostFrameCallback((Duration d) {
+      _cardLevelMapProcessAllVisible(visibleCardCollections);
+    });
+
+    List<Widget> positionedCards = new List<Widget>();
+
+    // Sort the cards by z-index.
+    List<logic_card.Card> orderedKeys = cardLevelMap.keys.toList()..sort((logic_card.Card a, logic_card.Card b) {
+      double diff = cardLevelMap[a].z - cardLevelMap[b].z;
+      return diff.sign.toInt();
+    });
+
+    orderedKeys.forEach((logic_card.Card c) {
+      // Don't show a card if it isn't part of a visible collection.
+      if (!visibleCardCollections.contains(config.game.findCard(c))) {
+        cardLevelMap.remove(c); // It is an old card, which we can clean up.
+        return;
+      }
+
+      CardAnimationData data = cardLevelMap[c];
+      RenderBox box = context.findRenderObject();
+      Point p = data.newPoint;
+      Point trueP = box.globalToLocal(p);
+
+      positionedCards.add(new Positioned(
+        key: new GlobalObjectKey(c.toString()), //needed, or else the Positioned wrapper may be traded out and animations fail.
+        top: trueP.y, // must pass x and y or else it expands to the maximum Stack size.
+        left: trueP.x, // must pass x and y or else it expands to the maximum Stack size.
+        child: new component_card.ZCard(data.comp_card, data.oldPoint, data.newPoint)));
+    });
+
+    return new IgnorePointer(
+      ignoring: true,
+      child: new Container(
+        width: config.width,
+        height: config.height,
+        child: new Stack(positionedCards)
+      )
+    );
+  }
 }
 
 GameComponent createGameComponent(
@@ -79,3 +166,15 @@
       return null;
   }
 }
+
+/// CardAnimationData contains the relevant information for a ZCard to be built.
+/// It uses the comp_card's properties, the oldPoint, newPoint, and z-index to
+/// determine how it needs to animate.
+class CardAnimationData {
+  component_card.Card comp_card;
+  Point oldPoint;
+  Point newPoint;
+  double z;
+
+  CardAnimationData(this.comp_card, this.oldPoint, this.newPoint, this.z);
+}
\ No newline at end of file
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 2785ff0..966b3b3 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -17,12 +17,79 @@
   List<logic_card.Card> passingCards2 = new List<logic_card.Card>();
   List<logic_card.Card> passingCards3 = new List<logic_card.Card>();
 
+  HeartsType _lastViewType;
+
+  @override
+  void initState() {
+    super.initState();
+    _reset();
+  }
+
+  @override
+  void _reset() {
+    super._reset();
+    HeartsGame game = config.game as HeartsGame;
+    _lastViewType = game.viewType;
+  }
+
   @override
   Widget build(BuildContext context) {
-    return new Container(
+    HeartsGame game = config.game as HeartsGame;
+
+    // check if we need to swap out our 's map.
+    if (_lastViewType != game.viewType) {
+      _reset();
+    }
+
+    // Hearts Widget
+    Widget heartsWidget = new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.grey[300]),
+            new BoxDecoration(backgroundColor: Colors.grey[300]),
         child: buildHearts());
+
+    List<Widget> children = new List<Widget>();
+    children.add(new Container(
+          decoration:
+              new BoxDecoration(backgroundColor: Colors.grey[300]),
+          width: config.width,
+          height: config.height,
+          child: heartsWidget));
+    if (game.phase == HeartsPhase.Deal || game.phase != HeartsPhase.Score) {
+      List<int> visibleCardCollections = new List<int>();
+      int playerNum = game.playerNumber;
+      if (game.viewType == HeartsType.Player) {
+        switch(game.phase) {
+          case HeartsPhase.Pass:
+            visibleCardCollections.add(HeartsGame.OFFSET_PASS + playerNum);
+            visibleCardCollections.add(HeartsGame.OFFSET_HAND + playerNum);
+            break;
+          case HeartsPhase.Take:
+            visibleCardCollections.add(HeartsGame.OFFSET_PASS + game.takeTarget);
+            visibleCardCollections.add(HeartsGame.OFFSET_HAND + playerNum);
+            break;
+          case HeartsPhase.Play:
+            visibleCardCollections.add(HeartsGame.OFFSET_HAND + playerNum);
+            visibleCardCollections.add(HeartsGame.OFFSET_PLAY + playerNum);
+            break;
+          default:
+            break;
+        }
+      } else {
+        // A board will need to see these things.
+        for (int i = 0; i < 4; i++) {
+          visibleCardCollections.add(HeartsGame.OFFSET_PLAY + i);
+          visibleCardCollections.add(HeartsGame.OFFSET_PASS + i);
+          visibleCardCollections.add(HeartsGame.OFFSET_HAND + i);
+        }
+      }
+      children.add(this.buildCardAnimationLayer(visibleCardCollections));
+    }
+
+    return new Container(
+      width: config.width,
+      height: config.height,
+      child: new Stack(children)
+    );
   }
 
   void _switchViewCallback() {
@@ -160,8 +227,8 @@
   @override
   Widget _makeButton(String text, NoArgCb callback, {bool inactive: false}) {
     var borderColor =
-        inactive ? material.Colors.grey[500] : material.Colors.white;
-    var backgroundColor = inactive ? material.Colors.grey[500] : null;
+        inactive ? Colors.grey[500] : Colors.white;
+    var backgroundColor = inactive ? Colors.grey[500] : null;
     return new FlatButton(
         child: new Container(
             decoration: new BoxDecoration(
@@ -238,6 +305,8 @@
 
     List<Widget> cardCollections = new List<Widget>();
 
+    // Note that this shouldn't normally be shown.
+    // Since this is a duplicate card collection, it will not have keyed cards.
     List<Widget> plays = new List<Widget>();
     for (int i = 0; i < 4; i++) {
       plays.add(new CardCollectionComponent(
@@ -248,7 +317,7 @@
     }
     cardCollections.add(new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.teal[600]),
+            new BoxDecoration(backgroundColor: Colors.teal[600]),
         width: config.width,
         child:
             new Flex(plays, justifyContent: FlexJustifyContent.spaceAround)));
@@ -257,22 +326,23 @@
 
     Widget playArea = new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.teal[500]),
+            new BoxDecoration(backgroundColor: Colors.teal[500]),
         width: config.width,
         child: new Center(
             child: new CardCollectionComponent(
                 game.cardCollections[p + HeartsGame.OFFSET_PLAY],
                 true,
                 CardCollectionOrientation.show1,
+                useKeys: true,
                 acceptCallback: _makeGameMoveCallback,
                 acceptType: p == game.whoseTurn ? DropType.card : DropType.none,
                 width: config.width,
                 backgroundColor: p == game.whoseTurn
-                    ? material.Colors.white
-                    : material.Colors.grey[500],
+                    ? Colors.white
+                    : Colors.grey[500],
                 altColor: p == game.whoseTurn
-                    ? material.Colors.grey[200]
-                    : material.Colors.grey[600])));
+                    ? Colors.grey[200]
+                    : Colors.grey[600])));
     cardCollections.add(playArea);
 
     List<logic_card.Card> cards = game.cardCollections[p];
@@ -280,7 +350,8 @@
         cards, game.playerNumber == p, CardCollectionOrientation.suit,
         dragChildren: game.whoseTurn == p,
         comparator: _compareCards,
-        width: config.width);
+        width: config.width,
+        useKeys: true);
     cardCollections.add(c); // flex
 
     cardCollections.add(new Text("Player ${game.whoseTurn}'s turn"));
@@ -305,7 +376,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex([
           new Text('Player ${game.playerNumber}'),
           // TODO(alexfandrianto): we want to show round by round, deltas too, don't we?
@@ -321,7 +392,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex([
           new Text('Player ${game.playerNumber}'),
           _makeButton('Deal', game.dealCards),
@@ -351,7 +422,7 @@
     topCardWidgets.add(_makeButton(name, buttoncb, inactive: completed));
 
     Color bgColor =
-        completed ? material.Colors.teal[600] : material.Colors.teal[500];
+        completed ? Colors.teal[600] : Colors.teal[500];
 
     Widget topArea = new Container(
         decoration: new BoxDecoration(backgroundColor: bgColor),
@@ -367,8 +438,9 @@
         acceptType: draggable ? DropType.card : null,
         comparator: _compareCards,
         width: config.width,
-        backgroundColor: material.Colors.grey[500],
-        altColor: material.Colors.grey[700]);
+        backgroundColor: Colors.grey[500],
+        altColor: Colors.grey[700],
+        useKeys: true);
 
     return new Column(<Widget>[
       topArea,
@@ -384,8 +456,9 @@
         acceptCallback: cb,
         dragChildren: cb != null,
         acceptType: cb != null ? DropType.card : null,
-        backgroundColor: material.Colors.white,
-        altColor: material.Colors.grey[200]);
+        backgroundColor: Colors.white,
+        altColor: Colors.grey[200],
+        useKeys: true);
 
     if (cb == null) {
       ccc = new Container(child: ccc);
diff --git a/lib/components/proto/proto.part.dart b/lib/components/proto/proto.part.dart
index d714e89..7711ba0 100644
--- a/lib/components/proto/proto.part.dart
+++ b/lib/components/proto/proto.part.dart
@@ -32,7 +32,7 @@
 
     cardCollections.add(new Container(
         decoration: new BoxDecoration(
-            backgroundColor: material.Colors.green[500], borderRadius: 5.0),
+            backgroundColor: Colors.green[500], borderRadius: 5.0),
         child: new CardCollectionComponent(
             config.game.cardCollections[4], true, CardCollectionOrientation.show1,
             dragChildren: true,
@@ -44,7 +44,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex(cardCollections, direction: FlexDirection.vertical));
   }
 
diff --git a/lib/components/solitaire/solitaire.part.dart b/lib/components/solitaire/solitaire.part.dart
index 15ebe8f..a1fde84 100644
--- a/lib/components/solitaire/solitaire.part.dart
+++ b/lib/components/solitaire/solitaire.part.dart
@@ -15,12 +15,37 @@
 
 class SolitaireGameComponentState
     extends GameComponentState<SolitaireGameComponent> {
+
   @override
   Widget build(BuildContext context) {
+    SolitaireGame game = config.game as SolitaireGame;
+
+    print("Building Solitaire!");
+
+    // Build Solitaire and have it fill up the card level map.
+    // Unfortunately, this is required so that we can know which card components
+    // to collect.
+    Widget solitaireWidget = buildSolitaire();
+
+    List<Widget> children = new List<Widget>();
+    children.add(new Container(
+          decoration:
+              new BoxDecoration(backgroundColor: Colors.grey[300]),
+          width: config.width,
+          height: config.height,
+          child: solitaireWidget));
+    if (game.phase == SolitairePhase.Play) {
+      // All cards are visible.
+      List<int> visibleCardCollections = game.cardCollections.asMap().keys.toList();
+
+      children.add(this.buildCardAnimationLayer(visibleCardCollections));
+    }
+
     return new Container(
-        decoration:
-            new BoxDecoration(backgroundColor: material.Colors.grey[300]),
-        child: buildSolitaire());
+      width: config.width,
+      height: config.height,
+      child: new Stack(children)
+    );
   }
 
   void _cheatCallback() {
@@ -50,8 +75,8 @@
   @override
   Widget _makeButton(String text, NoArgCb callback, {bool inactive: false}) {
     var borderColor =
-        inactive ? material.Colors.grey[500] : material.Colors.white;
-    var backgroundColor = inactive ? material.Colors.grey[500] : null;
+        inactive ? Colors.grey[500] : Colors.white;
+    var backgroundColor = inactive ? Colors.grey[500] : null;
     return new FlatButton(
         child: new Container(
             decoration: new BoxDecoration(
@@ -104,44 +129,19 @@
     double cardSize = config.width / 8.0;
 
     List<Widget> row1 = new List<Widget>();
-    row1.add(new Row([
-      new CardCollectionComponent(
-          game.cardCollections[SolitaireGame.OFFSET_ACES + 0],
+    List<CardCollectionComponent> aces = [0, 1, 2, 3].map((int i) {
+      return new CardCollectionComponent(
+          game.cardCollections[SolitaireGame.OFFSET_ACES + i],
           true,
           CardCollectionOrientation.show1,
           widthCard: cardSize,
           heightCard: cardSize,
           acceptCallback: _moveCallback,
           dragChildren: true,
-          acceptType: DropType.card),
-      new CardCollectionComponent(
-          game.cardCollections[SolitaireGame.OFFSET_ACES + 1],
-          true,
-          CardCollectionOrientation.show1,
-          widthCard: cardSize,
-          heightCard: cardSize,
-          acceptCallback: _moveCallback,
-          dragChildren: true,
-          acceptType: DropType.card),
-      new CardCollectionComponent(
-          game.cardCollections[SolitaireGame.OFFSET_ACES + 2],
-          true,
-          CardCollectionOrientation.show1,
-          widthCard: cardSize,
-          heightCard: cardSize,
-          acceptCallback: _moveCallback,
-          dragChildren: true,
-          acceptType: DropType.card),
-      new CardCollectionComponent(
-          game.cardCollections[SolitaireGame.OFFSET_ACES + 3],
-          true,
-          CardCollectionOrientation.show1,
-          widthCard: cardSize,
-          heightCard: cardSize,
-          acceptCallback: _moveCallback,
-          dragChildren: true,
-          acceptType: DropType.card),
-    ]));
+          acceptType: DropType.card,
+          useKeys: true);
+    }).toList();
+    row1.add(new Row(aces));
 
     row1.add(new Row([
       new CardCollectionComponent(
@@ -150,14 +150,16 @@
           CardCollectionOrientation.show1,
           widthCard: cardSize,
           heightCard: cardSize,
-          dragChildren: true),
+          dragChildren: true,
+          useKeys: true),
       new InkWell(
           child: new CardCollectionComponent(
               game.cardCollections[SolitaireGame.OFFSET_DRAW],
               false,
               CardCollectionOrientation.show1,
               widthCard: cardSize,
-              heightCard: cardSize),
+              heightCard: cardSize,
+              useKeys: true),
           onTap: game.canDrawCard ? game.drawCardUI : null),
     ]));
 
@@ -169,7 +171,8 @@
               false,
               CardCollectionOrientation.show1,
               widthCard: cardSize,
-              heightCard: cardSize),
+              heightCard: cardSize,
+              useKeys: true),
           onTap: game.cardCollections[SolitaireGame.OFFSET_UP + i].length == 0
               ? _makeFlipCallback(i)
               : null));
@@ -185,7 +188,8 @@
           height: config.height * 0.6,
           acceptCallback: _moveCallback,
           dragChildren: true,
-          acceptType: DropType.card));
+          acceptType: DropType.card,
+          useKeys: true));
     }
 
     return new Column([
@@ -201,7 +205,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex([
           new Text('Player ${game.playerNumber}'),
           _makeButton("Return to Lobby", _quitGameCallback),
@@ -214,7 +218,7 @@
 
     return new Container(
         decoration:
-            new BoxDecoration(backgroundColor: material.Colors.pink[500]),
+            new BoxDecoration(backgroundColor: Colors.pink[500]),
         child: new Flex([
           new Text('Player ${game.playerNumber}'),
           _makeButton('Deal', game.dealCardsUI),
diff --git a/lib/src/syncbase/croupier_client.dart b/lib/src/syncbase/croupier_client.dart
index 3a9b8af..1bc1ece 100644
--- a/lib/src/syncbase/croupier_client.dart
+++ b/lib/src/syncbase/croupier_client.dart
@@ -52,10 +52,12 @@
     if (!(await app.exists())) {
       await app.create(util.openPerms);
     }
+    util.log('CroupierClient.got app');
     var db = app.noSqlDatabase(util.dbName);
     if (!(await db.exists())) {
       await db.create(util.openPerms);
     }
+    util.log('CroupierClient.got db');
     return db;
   }
 
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index 367ee76..c7ef239 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -198,7 +198,14 @@
     util.log(
         "SettingsScanHandler Found ${s.instanceUuid} ${s.instanceName} ${s.addrs}");
 
-    _cc.joinSyncgroup(s.addrs[0]);
+    // TODO(alexfandrianto): Filter based on instanceName?
+    if (s.addrs.length > 0) {
+      _cc.joinSyncgroup(s.addrs[0]);
+    } else {
+      // An unexpected service was found. Who is advertising it?
+      // https://github.com/vanadium/issues/issues/846
+      util.log("Unexpected service found: ${s.toString()}");
+    }
   }
 
   void lost(List<int> instanceId) {