croupier: Improve Score View

Now shows the deltaScore and looks much more Material than before.
Both portrait and landscape mode fit pretty well on a thin phone.

Change-Id: If897873f5fef446450d20a830e4f9f8b36b79d0d
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index 5ccdf6e..132a09d 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -149,7 +149,7 @@
       case logic_croupier.CroupierState.PlayGame:
         return new Container(
             padding: new EdgeDims.only(top: ui.window.padding.top),
-            child: component_game.createGameComponent(config.croupier.game, () {
+            child: component_game.createGameComponent(config.croupier, () {
               config.croupier.game.quit();
               makeSetStateCallback(logic_croupier.CroupierState.Welcome)();
             },
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 66d13da..258a09a 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -4,7 +4,11 @@
 
 library game_component;
 
+import 'dart:math' as math;
+
 import '../logic/card.dart' as logic_card;
+import '../logic/croupier.dart' show Croupier;
+import '../logic/croupier_settings.dart' show CroupierSettings;
 import '../logic/game/game.dart' show Game, GameType;
 import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase, HeartsType;
 import '../logic/solitaire/solitaire.dart' show SolitaireGame, SolitairePhase;
@@ -12,6 +16,7 @@
 import 'card.dart' as component_card;
 import 'card_collection.dart'
     show CardCollectionComponent, DropType, CardCollectionOrientation, AcceptCb;
+import 'croupier_profile.dart' show CroupierProfileComponent;
 import '../styles/common.dart' as style;
 
 import 'package:flutter/animation.dart';
@@ -25,12 +30,13 @@
 typedef void NoArgCb();
 
 abstract class GameComponent extends StatefulComponent {
-  final Game game;
+  final Croupier croupier;
+  Game get game => croupier.game;
   final NoArgCb gameEndCallback;
   final double width;
   final double height;
 
-  GameComponent(this.game, this.gameEndCallback, {this.width, this.height});
+  GameComponent(this.croupier, this.gameEndCallback, {this.width, this.height});
 }
 
 abstract class GameComponentState<T extends GameComponent> extends State<T> {
@@ -69,11 +75,11 @@
   @override
   Widget build(BuildContext context); // still UNIMPLEMENTED
 
-  void _cardLevelMapProcessAllVisible(List<int> visibleCardCollections) {
+  void _cardLevelMapProcessAllVisible(List<int> visibleCardCollectionIndexes) {
     Game game = config.game;
 
-    for (int i = 0; i < visibleCardCollections.length; i++) {
-      int index = visibleCardCollections[i];
+    for (int i = 0; i < visibleCardCollectionIndexes.length; i++) {
+      int index = visibleCardCollectionIndexes[i];
       for (int j = 0; j < game.cardCollections[index].length; j++) {
         _cardLevelMapProcess(game.cardCollections[index][j]);
       }
@@ -129,11 +135,11 @@
 
   // Helper to build the card animation layer.
   // Note: This isn't a component because of its dependence on Widgets.
-  Widget buildCardAnimationLayer(List<int> visibleCardCollections) {
+  Widget buildCardAnimationLayer(List<int> visibleCardCollectionIndexes) {
     // 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);
+      _cardLevelMapProcessAllVisible(visibleCardCollectionIndexes);
     });
 
     List<Widget> positionedCards = new List<Widget>();
@@ -158,7 +164,7 @@
 
     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))) {
+      if (!visibleCardCollectionIndexes.contains(config.game.findCard(c))) {
         cardLevelMap.remove(c); // It is an old card, which we can clean up.
         return;
       }
@@ -188,17 +194,17 @@
   }
 }
 
-GameComponent createGameComponent(Game game, NoArgCb gameEndCallback,
+GameComponent createGameComponent(Croupier croupier, NoArgCb gameEndCallback,
     {double width, double height}) {
-  switch (game.gameType) {
+  switch (croupier.game.gameType) {
     case GameType.Proto:
-      return new ProtoGameComponent(game, gameEndCallback,
+      return new ProtoGameComponent(croupier, gameEndCallback,
           width: width, height: height);
     case GameType.Hearts:
-      return new HeartsGameComponent(game, gameEndCallback,
+      return new HeartsGameComponent(croupier, gameEndCallback,
           width: width, height: height);
     case GameType.Solitaire:
-      return new SolitaireGameComponent(game, gameEndCallback,
+      return new SolitaireGameComponent(croupier, gameEndCallback,
           width: width, height: height);
     default:
       // We're probably not ready to serve the other games yet.
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index a310ee5..0c5aa4f 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -5,8 +5,9 @@
 part of game_component;
 
 class HeartsGameComponent extends GameComponent {
-  HeartsGameComponent(Game game, NoArgCb cb, {double width, double height})
-      : super(game, cb, width: width, height: height);
+  HeartsGameComponent(Croupier croupier, NoArgCb cb,
+      {double width, double height})
+      : super(croupier, cb, width: width, height: height);
 
   HeartsGameComponentState createState() => new HeartsGameComponentState();
 }
@@ -51,25 +52,30 @@
         width: config.width,
         height: config.height,
         child: heartsWidget));
+    List<int> visibleCardCollectionIndexes = new List<int>();
     if (game.phase != HeartsPhase.StartGame &&
         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);
+            visibleCardCollectionIndexes
+                .add(HeartsGame.OFFSET_PASS + playerNum);
+            visibleCardCollectionIndexes
+                .add(HeartsGame.OFFSET_HAND + playerNum);
             break;
           case HeartsPhase.Take:
-            visibleCardCollections
+            visibleCardCollectionIndexes
                 .add(HeartsGame.OFFSET_PASS + game.takeTarget);
-            visibleCardCollections.add(HeartsGame.OFFSET_HAND + playerNum);
+            visibleCardCollectionIndexes
+                .add(HeartsGame.OFFSET_HAND + playerNum);
             break;
           case HeartsPhase.Play:
-            visibleCardCollections.add(HeartsGame.OFFSET_HAND + playerNum);
-            visibleCardCollections.add(HeartsGame.OFFSET_PLAY + playerNum);
+            visibleCardCollectionIndexes
+                .add(HeartsGame.OFFSET_HAND + playerNum);
+            visibleCardCollectionIndexes
+                .add(HeartsGame.OFFSET_PLAY + playerNum);
             break;
           default:
             break;
@@ -77,13 +83,13 @@
       } 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);
+          visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PLAY + i);
+          visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PASS + i);
+          visibleCardCollectionIndexes.add(HeartsGame.OFFSET_HAND + i);
         }
       }
-      children.add(this.buildCardAnimationLayer(visibleCardCollections));
     }
+    children.add(this.buildCardAnimationLayer(visibleCardCollectionIndexes));
 
     return new Container(
         width: config.width, height: config.height, child: new Stack(children));
@@ -363,6 +369,16 @@
         justifyContent: FlexJustifyContent.spaceBetween);
   }
 
+  Widget _getProfileComponent(int playerNumber) {
+    int userID = config.croupier.userIDFromPlayerNumber(playerNumber);
+
+    CroupierSettings cs; // If cs is null, a placeholder is used instead.
+    if (userID != null) {
+      cs = config.croupier.settings_everyone[userID];
+    }
+    return new CroupierProfileComponent(cs);
+  }
+
   Widget showScore() {
     HeartsGame game = config.game as HeartsGame;
 
@@ -375,16 +391,60 @@
       w = _makeButton('Ready?', game.setReadyUI);
     }
 
-    return new Container(
-        decoration: new BoxDecoration(backgroundColor: Colors.pink[500]),
+    bool isTall = MediaQuery.of(context).orientation == Orientation.portrait;
+    FlexDirection crossDirection =
+        isTall ? FlexDirection.horizontal : FlexDirection.vertical;
+    FlexDirection mainDirection =
+        isTall ? FlexDirection.vertical : FlexDirection.horizontal;
+    TextStyle bigStyle = isTall ? style.Text.hugeStyle : style.Text.largeStyle;
+    TextStyle bigRedStyle =
+        isTall ? style.Text.hugeRedStyle : style.Text.largeRedStyle;
+
+    List<Widget> scores = new List<Widget>();
+    scores.add(new Flexible(
         child: new Flex([
-          new Text('Player ${game.playerNumber}'),
-          // TODO(alexfandrianto): we want to show round by round, deltas too, don't we?
-          new Text('${game.scores}'),
-          w,
-          _makeButton("Return to Lobby", _quitGameCallback),
-          _makeDebugButtons()
-        ], direction: FlexDirection.vertical));
+          new Flexible(
+              child: new Center(child: new Text("Score:", style: bigStyle)),
+              flex: 1),
+          new Flexible(
+              child: new Center(child: new Text("Round", style: bigStyle)),
+              flex: 1),
+          new Flexible(
+              child: new Center(child: new Text("Total", style: bigStyle)),
+              flex: 1)
+        ], direction: crossDirection),
+        flex: 1));
+    for (int i = 0; i < 4; i++) {
+      bool isMaxForRound =
+          game.deltaScores.reduce(math.max) == game.deltaScores[i];
+      bool isMaxOverall = game.scores.reduce(math.max) == game.scores[i];
+
+      TextStyle deltaStyle = isMaxForRound ? bigRedStyle : bigStyle;
+      TextStyle scoreStyle = isMaxOverall ? bigRedStyle : bigStyle;
+
+      scores.add(new Flexible(
+          child: new Flex([
+            new Flexible(child: _getProfileComponent(i), flex: 1),
+            new Flexible(
+                child: new Center(
+                    child:
+                        new Text("${game.deltaScores[i]}", style: deltaStyle)),
+                flex: 1),
+            new Flexible(
+                child: new Center(
+                    child: new Text("${game.scores[i]}", style: scoreStyle)),
+                flex: 1)
+          ], direction: crossDirection),
+          flex: 2));
+    }
+    return new Column([
+      new Flexible(child: new Flex(scores, direction: mainDirection), flex: 5),
+      new Flexible(
+          child: new Row([w, _makeButton("Return to Lobby", _quitGameCallback)],
+              justifyContent: FlexJustifyContent.spaceAround),
+          flex: 1),
+      new Flexible(child: new Row([_makeDebugButtons()]), flex: 1)
+    ]);
   }
 
   Widget showDeal() {
diff --git a/lib/components/proto/proto.part.dart b/lib/components/proto/proto.part.dart
index 4a11900..a09f6f8 100644
--- a/lib/components/proto/proto.part.dart
+++ b/lib/components/proto/proto.part.dart
@@ -5,8 +5,9 @@
 part of game_component;
 
 class ProtoGameComponent extends GameComponent {
-  ProtoGameComponent(Game game, NoArgCb cb, {double width, double height})
-      : super(game, cb, width: width, height: height);
+  ProtoGameComponent(Croupier croupier, NoArgCb cb,
+      {double width, double height})
+      : super(croupier, cb, width: width, height: height);
 
   ProtoGameComponentState createState() => new ProtoGameComponentState();
 }
diff --git a/lib/components/solitaire/solitaire.part.dart b/lib/components/solitaire/solitaire.part.dart
index fdcec8f..933922f 100644
--- a/lib/components/solitaire/solitaire.part.dart
+++ b/lib/components/solitaire/solitaire.part.dart
@@ -5,8 +5,9 @@
 part of game_component;
 
 class SolitaireGameComponent extends GameComponent {
-  SolitaireGameComponent(Game game, NoArgCb cb, {double width, double height})
-      : super(game, cb, width: width, height: height);
+  SolitaireGameComponent(Croupier croupier, NoArgCb cb,
+      {double width, double height})
+      : super(croupier, cb, width: width, height: height);
 
   SolitaireGameComponentState createState() =>
       new SolitaireGameComponentState();
@@ -31,10 +32,10 @@
         child: solitaireWidget));
     if (game.phase == SolitairePhase.Play) {
       // All cards are visible.
-      List<int> visibleCardCollections =
+      List<int> visibleCardCollectionIndexes =
           game.cardCollections.asMap().keys.toList();
 
-      children.add(this.buildCardAnimationLayer(visibleCardCollections));
+      children.add(this.buildCardAnimationLayer(visibleCardCollectionIndexes));
     }
 
     return new Container(
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index ee119ac..8d85e76 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -70,6 +70,12 @@
     }
   }
 
+  int userIDFromPlayerNumber(int playerNumber) {
+    return players_found.keys.firstWhere(
+        (int user) => players_found[user] == playerNumber,
+        orElse: () => null);
+  }
+
   void _updatePlayerFoundCb(String playerID, String playerNum) {
     int id = int.parse(playerID);
     if (playerNum == null) {
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index b2bb768..fde65f7 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -63,6 +63,7 @@
 
   // Used by the score screen to track scores and see which players are ready to continue to the next round.
   List<int> scores = [0, 0, 0, 0];
+  List<int> deltaScores = [0, 0, 0, 0];
   List<bool> ready;
 
   HeartsGame(int playerNumber, {int gameID, bool isCreator})
@@ -443,11 +444,14 @@
   }
 
   void updateScore() {
+    // Clear out delta scores.
+    deltaScores = [0, 0, 0, 0];
+
     // Count up points and check if someone shot the moon.
     int shotMoon = null;
     for (int i = 0; i < 4; i++) {
       int delta = computeScore(i);
-      this.scores[i] += delta;
+      this.deltaScores[i] = delta;
       if (delta == 26) {
         // Shot the moon!
         shotMoon = i;
@@ -458,12 +462,17 @@
     if (shotMoon != null) {
       for (int i = 0; i < 4; i++) {
         if (shotMoon == i) {
-          this.scores[i] -= 26;
+          this.deltaScores[i] -= 26;
         } else {
-          this.scores[i] += 26;
+          this.deltaScores[i] += 26;
         }
       }
     }
+
+    // Finally, apply deltaScores to scores. Preserve deltaScores for the UI.
+    for (int i = 0; i < 4; i++) {
+      this.scores[i] += this.deltaScores[i];
+    }
   }
 
   int computeScore(int player) {
diff --git a/lib/styles/common.dart b/lib/styles/common.dart
index ccbef24..5dffee2 100644
--- a/lib/styles/common.dart
+++ b/lib/styles/common.dart
@@ -13,6 +13,12 @@
   static final TextStyle liveNow =
       new TextStyle(fontSize: 12.0, color: theme.accentColor);
   static final TextStyle error = new TextStyle(color: errorTextColor);
+  static final TextStyle hugeStyle = new TextStyle(fontSize: 32.0);
+  static final TextStyle hugeRedStyle =
+      new TextStyle(fontSize: 32.0, color: errorTextColor);
+  static final TextStyle largeStyle = new TextStyle(fontSize: 24.0);
+  static final TextStyle largeRedStyle =
+      new TextStyle(fontSize: 24.0, color: errorTextColor);
 }
 
 class Size {
diff --git a/pubspec.lock b/pubspec.lock
index 226e558..be2270f 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -120,23 +120,23 @@
   mojo:
     description: mojo
     source: hosted
-    version: "0.4.3"
+    version: "0.4.5"
   mojo_apptest:
     description: mojo_apptest
     source: hosted
-    version: "0.2.8"
+    version: "0.2.9"
   mojo_sdk:
     description: mojo_sdk
     source: hosted
-    version: "0.2.2"
+    version: "0.2.4"
   mojo_services:
     description: mojo_services
     source: hosted
-    version: "0.4.5"
+    version: "0.4.7"
   mojom:
     description: mojom
     source: hosted
-    version: "0.2.8"
+    version: "0.2.9"
   mustache4dart:
     description: mustache4dart
     source: hosted
@@ -186,11 +186,11 @@
   sky_engine:
     description: sky_engine
     source: hosted
-    version: "0.0.57"
+    version: "0.0.65"
   sky_services:
     description: sky_services
     source: hosted
-    version: "0.0.57"
+    version: "0.0.65"
   source_map_stack_trace:
     description: source_map_stack_trace
     source: hosted
@@ -218,7 +218,7 @@
   test:
     description: test
     source: hosted
-    version: "0.12.6"
+    version: "0.12.5+2"
   typed_data:
     description: typed_data
     source: hosted
diff --git a/test/hearts_test.dart b/test/hearts_test.dart
index 1b92fd4..09160db 100644
--- a/test/hearts_test.dart
+++ b/test/hearts_test.dart
@@ -144,10 +144,12 @@
       // Now, update the score, modifying game.scores.
       game.updateScore();
       expect(game.scores, equals([4, 8, 14, 0]));
+      expect(game.deltaScores, equals([4, 8, 14, 0]));
 
       // Do it again.
       game.updateScore();
       expect(game.scores, equals([8, 16, 28, 0]));
+      expect(game.deltaScores, equals([4, 8, 14, 0]));
 
       // Shoot the moon!
       game.cardCollections[HeartsGame.PLAYER_A_TRICK] = <Card>[];
@@ -156,6 +158,7 @@
       game.cardCollections[HeartsGame.PLAYER_D_TRICK] = Card.All;
       game.updateScore();
       expect(game.scores, equals([34, 42, 54, 0]));
+      expect(game.deltaScores, equals([26, 26, 26, 0]));
     });
   });
 
@@ -391,6 +394,7 @@
 
       // Check score to ensure it matches the expectation.
       expect(game.scores, equals([21, 3, 2, 0]));
+      expect(game.deltaScores, equals([21, 3, 2, 0]));
 
       // Score consists of 4 ready commands.
       runCommand();
@@ -435,6 +439,7 @@
             2 + 26 + 26 + 26,
             0 + 26 + 26 + 26
           ]));
+      expect(game.deltaScores, equals([0, 26, 26, 26]));
       expect(game.hasGameEnded, isFalse);
 
       // 5th round: 4 deal, 4 pass, 4 take, 52 play. Game is over, so no ready phase.
@@ -449,6 +454,7 @@
             2 + 26 + 26 + 26 + 26,
             0 + 26 + 26 + 26 + 26
           ]));
+      expect(game.deltaScores, equals([0, 26, 26, 26]));
       expect(game.hasGameEnded,
           isTrue); // assumes game ends after about 100 points.
     });