Merge "Implemented soft reset functionality to debug mode. Hard reset to follow."
diff --git a/lib/components/board.dart b/lib/components/board.dart
index 84ca7fc..27fd7a5 100644
--- a/lib/components/board.dart
+++ b/lib/components/board.dart
@@ -2,13 +2,15 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+import 'dart:math' as math;
+
import 'package:flutter/material.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;
-import '../logic/croupier_settings.dart' show CroupierSettings;
import '../logic/game/game.dart' show Game, GameType, NoArgCb;
-import '../logic/hearts/hearts.dart' show HeartsGame;
+import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase;
import '../styles/common.dart' as style;
import 'card.dart' as component_card;
import 'card_collection.dart'
@@ -76,18 +78,21 @@
class HeartsBoardState extends State<HeartsBoard> {
Widget build(BuildContext context) {
- double offscreenDelta = config.isMini ? 5.0 : 2.0;
+ double offscreenDelta = config.isMini ? 5.0 : 1.5;
+
+ Widget boardChild;
+ if (config.game.phase == HeartsPhase.Play) {
+ boardChild =
+ config.isMini ? _buildMiniBoardLayout() : _buildBoardLayout();
+ } else {
+ boardChild = _buildPassLayout();
+ }
return new Container(
height: config.height,
width: config.width,
child: new Stack([
- new Positioned(
- top: 0.0,
- left: 0.0,
- child: config.isMini
- ? _buildMiniBoardLayout()
- : _buildBoardLayout()),
+ new Positioned(top: 0.0, left: 0.0, child: boardChild),
new Positioned(
top: config.height * (offscreenDelta + 0.5),
left: (config.width - config.cardWidth) / 2,
@@ -115,6 +120,109 @@
return (i + config.game.playerNumber) % 4;
}
+ static Map<int, String> passBackgrounds = const <int, String>{
+ 0: "images/games/hearts/pass_right.png",
+ 1: "images/games/hearts/pass_left.png",
+ 2: "images/games/hearts/pass_across.png",
+ 3: "",
+ };
+
+ Widget _buildPassLayout() {
+ String passBackground = ""; // It's possible to have no background.
+ if (config.game.phase == HeartsPhase.Pass ||
+ config.game.phase == HeartsPhase.Take) {
+ passBackground = passBackgrounds[config.game.roundNumber % 4];
+ }
+
+ return new Container(
+ height: config.height,
+ width: config.width,
+ child: new Stack([
+ new Positioned(
+ top: 0.0,
+ left: 0.0,
+ child: new AssetImage(
+ name: passBackground,
+ height: config.height,
+ width: config.width)),
+ new Positioned(top: 0.0, left: 0.0, child: _buildPassLayoutInternal())
+ ]));
+ }
+
+ double _rotationAngle(int pNum) {
+ return pNum * math.PI / 2;
+ }
+
+ Widget _rotate(Widget w, int pNum) {
+ return new Transform(
+ child: w,
+ transform:
+ new vector_math.Matrix4.identity().rotateZ(_rotationAngle(pNum)),
+ alignment: new FractionalOffset(0.5, 0.5));
+ }
+
+ Widget _getPass(int playerNumber) {
+ double sizeRatio = 0.10;
+ double cccSize = math.min(sizeRatio * config.width, config.cardWidth * 3.5);
+
+ HeartsGame game = config.game;
+ List<logic_card.Card> cardsToTake = [];
+ int takeTarget = game.getTakeTarget(playerNumber);
+ if (takeTarget != null) {
+ cardsToTake = game.cardCollections[
+ game.getTakeTarget(playerNumber) + HeartsGame.OFFSET_PASS];
+ }
+
+ bool isHorz = playerNumber % 2 == 0;
+ CardCollectionOrientation ori = isHorz
+ ? CardCollectionOrientation.horz
+ : CardCollectionOrientation.vert;
+ return new CardCollectionComponent(cardsToTake, false, ori,
+ backgroundColor: style.transparentColor,
+ width: isHorz ? cccSize : null,
+ height: isHorz ? null : cccSize,
+ widthCard: config.cardWidth,
+ heightCard: config.cardHeight,
+ rotation: playerNumber * math.PI / 2,
+ useKeys: true);
+ }
+
+ Widget _getProfile(int pNum, double sizeFactor) {
+ return new CroupierProfileComponent(
+ settings: config.croupier.settingsFromPlayerNumber(pNum),
+ height: config.height * sizeFactor,
+ width: config.height * sizeFactor * 1.5);
+ }
+
+ Widget _playerProfile(int pNum, double sizeFactor) {
+ return _rotate(_getProfile(pNum, sizeFactor), pNum);
+ }
+
+ Widget _buildPassLayoutInternal() {
+ return new Container(
+ height: config.height,
+ width: config.width,
+ child: new Column([
+ new Flexible(child: _playerProfile(2, 0.2), flex: 0),
+ new Flexible(child: _getPass(2), flex: 0),
+ new Flexible(
+ child: new Row([
+ new Flexible(child: _playerProfile(1, 0.2), flex: 0),
+ new Flexible(child: _getPass(1), flex: 0),
+ new Flexible(child: new Block([]), flex: 1),
+ new Flexible(child: _getPass(3), flex: 0),
+ new Flexible(child: _playerProfile(3, 0.2), flex: 0)
+ ],
+ alignItems: FlexAlignItems.center,
+ justifyContent: FlexJustifyContent.spaceAround),
+ flex: 1),
+ new Flexible(child: _getPass(0), flex: 0),
+ new Flexible(child: _playerProfile(0, 0.2), flex: 0)
+ ],
+ alignItems: FlexAlignItems.center,
+ justifyContent: FlexJustifyContent.spaceAround));
+ }
+
Widget _buildMiniBoardLayout() {
return new Container(
height: config.height,
@@ -190,91 +298,43 @@
child: new Stack(items));
}
+ Widget _showTrickText(int pNum) {
+ HeartsGame game = config.game;
+
+ int numTrickCards =
+ game.cardCollections[HeartsGame.OFFSET_TRICK + pNum].length;
+ int numTricks = numTrickCards ~/ 4;
+
+ String s = numTricks != 1 ? "s" : "";
+
+ return _rotate(new Text("${numTricks} trick${s}"), pNum);
+ }
+
Widget _buildBoardLayout() {
return new Container(
height: config.height,
width: config.width,
child: new Column([
- new Flexible(child: _buildPlayer(2), flex: 5),
+ new Flexible(child: _playerProfile(2, 0.2), flex: 0),
+ new Flexible(child: _showTrickText(2), flex: 0),
new Flexible(
child: new Row([
- new Flexible(child: _buildPlayer(1), flex: 3),
- new Flexible(child: _buildCenterCards(), flex: 4),
- new Flexible(child: _buildPlayer(3), flex: 3)
+ new Flexible(child: _playerProfile(1, 0.2), flex: 0),
+ new Flexible(child: _showTrickText(1), flex: 0),
+ new Flexible(child: _buildCenterCards(), flex: 1),
+ new Flexible(child: _showTrickText(3), flex: 0),
+ new Flexible(child: _playerProfile(3, 0.2), flex: 0)
],
alignItems: FlexAlignItems.center,
justifyContent: FlexJustifyContent.spaceAround),
- flex: 9),
- new Flexible(child: _buildPlayer(0), flex: 5)
+ flex: 1),
+ new Flexible(child: _showTrickText(0), flex: 0),
+ new Flexible(child: _playerProfile(0, 0.2), flex: 0)
],
alignItems: FlexAlignItems.center,
justifyContent: FlexJustifyContent.spaceAround));
}
- Widget _buildPlayer(int playerNumber) {
- bool wide = (config.width >= config.height);
-
- List<Widget> widgets = [
- _getProfile(playerNumber, wide),
- _getHand(playerNumber),
- _getPass(playerNumber)
- ];
-
- if (playerNumber % 2 == 0) {
- return new Row(widgets,
- alignItems: FlexAlignItems.center,
- justifyContent: FlexJustifyContent.center);
- } else {
- return new Column(widgets,
- alignItems: FlexAlignItems.center,
- justifyContent: FlexJustifyContent.center);
- }
- }
-
- Widget _getProfile(int playerNumber, bool isWide) {
- bool isMini = isWide && config.cardHeight * 2 > config.height * 0.25;
-
- // If cs is null, a placeholder is used instead.
- CroupierSettings cs =
- config.croupier.settingsFromPlayerNumber(playerNumber);
- return new CroupierProfileComponent(
- settings: cs, height: config.height * 0.15, isMini: isMini);
- }
-
- Widget _getHand(int playerNumber) {
- double sizeRatio = 0.30;
- double cccSize = sizeRatio * config.width;
-
- return new CardCollectionComponent(
- config.game.cardCollections[playerNumber + HeartsGame.OFFSET_HAND],
- false,
- CardCollectionOrientation.horz,
- width: cccSize,
- widthCard: config.cardWidth,
- heightCard: config.cardHeight,
- useKeys: true);
- }
-
- Widget _getPass(int playerNumber) {
- double sizeRatio = 0.10;
- double cccSize = sizeRatio * config.width;
-
- HeartsGame game = config.game;
- List<logic_card.Card> cardsToTake = [];
- int takeTarget = game.getTakeTarget(playerNumber);
- if (takeTarget != null) {
- cardsToTake = game.cardCollections[
- game.getTakeTarget(playerNumber) + HeartsGame.OFFSET_PASS];
- }
- return new CardCollectionComponent(
- cardsToTake, false, CardCollectionOrientation.horz,
- backgroundColor: Colors.grey[300],
- width: cccSize,
- widthCard: config.cardWidth / 2,
- heightCard: config.cardHeight / 2,
- useKeys: true);
- }
-
Widget _buildCenterCards() {
bool wide = (config.width >= config.height);
@@ -301,40 +361,62 @@
}
}
+ double get _centerScaleFactor {
+ return math.min(config.height * 0.6 / (config.cardHeight * 3),
+ config.width - config.height * 0.4 / (config.cardWidth * 3));
+ }
+
Widget _buildCenterCard(int playerNumber) {
HeartsGame game = config.game;
List<logic_card.Card> cards =
game.cardCollections[playerNumber + HeartsGame.OFFSET_PLAY];
- return new Container(
- decoration: game.whoseTurn == playerNumber ? style.Box.liveNow : null,
- child: new CardCollectionComponent(
- cards, true, CardCollectionOrientation.show1,
- widthCard: config.cardWidth * 2,
- heightCard: config.cardHeight * 2,
- useKeys: true));
+ bool hasPlayed = cards.length > 0;
+ bool isTurn = game.whoseTurn == playerNumber && !hasPlayed;
+
+ return new CardCollectionComponent(
+ cards, true, CardCollectionOrientation.show1,
+ widthCard: config.cardWidth * this._centerScaleFactor,
+ heightCard: config.cardHeight * this._centerScaleFactor,
+ rotation: _rotationAngle(playerNumber),
+ useKeys: true,
+ backgroundColor: isTurn ? style.theme.accentColor : null);
}
+ // The off-screen cards consist of trick cards and play cards.
+ // When the board is mini, the player's play cards are excluded.
Widget _buildOffScreenCards(int playerNumber) {
HeartsGame game = config.game;
List<logic_card.Card> cards = new List.from(
game.cardCollections[playerNumber + HeartsGame.OFFSET_TRICK]);
- double sizeFactor = 2.0;
+ bool isPlay = game.phase == HeartsPhase.Play;
+
+ // Prevent over-expansion of cards until a card has been played.
+ bool alreadyPlaying =
+ (isPlay && (game.numPlayed > 0 || game.trickNumber > 0));
+
+ double sizeFactor = 1.0;
if (config.isMini) {
- sizeFactor = 1.0;
if (playerNumber != game.playerNumber) {
cards.addAll(
game.cardCollections[playerNumber + HeartsGame.OFFSET_HAND]);
}
+ } else {
+ cards.addAll(game.cardCollections[playerNumber + HeartsGame.OFFSET_HAND]);
+
+ if (alreadyPlaying) {
+ sizeFactor = this._centerScaleFactor;
+ }
}
return new CardCollectionComponent(
- cards, true, CardCollectionOrientation.show1,
+ cards, isPlay, CardCollectionOrientation.show1,
widthCard: config.cardWidth * sizeFactor,
heightCard: config.cardHeight * sizeFactor,
useKeys: true,
+ rotation: config.isMini ? null : _rotationAngle(playerNumber),
animationType: component_card.CardAnimationType.LONG);
}
}
diff --git a/lib/components/card.dart b/lib/components/card.dart
index 628b40d..9a15f75 100644
--- a/lib/components/card.dart
+++ b/lib/components/card.dart
@@ -53,7 +53,7 @@
faceUp = dataComponent.faceUp,
width = dataComponent.width ?? 40.0,
height = dataComponent.height ?? 40.0,
- rotation = dataComponent.rotation,
+ rotation = dataComponent.rotation ?? 0.0,
animationType = dataComponent.animationType,
z = dataComponent.z;
@@ -75,7 +75,7 @@
Card(logic_card.Card card, this.faceUp,
{double width,
double height,
- this.rotation: 0.0,
+ double rotation,
bool useKey: false,
this.visible: true,
CardAnimationType animationType,
@@ -85,6 +85,7 @@
card = card,
width = width ?? 40.0,
height = height ?? 40.0,
+ rotation = rotation ?? 0.0,
useKey = useKey,
super(key: useKey ? new GlobalCardKey(card, CardUIType.CARD) : null);
diff --git a/lib/components/hearts/hearts.part.dart b/lib/components/hearts/hearts.part.dart
index 18a09bd..29fff21 100644
--- a/lib/components/hearts/hearts.part.dart
+++ b/lib/components/hearts/hearts.part.dart
@@ -336,8 +336,6 @@
List<Widget> kids = new List<Widget>();
switch (config.game.phase) {
case HeartsPhase.Deal:
- kids.add(new Text("Waiting for Deal..."));
- break;
case HeartsPhase.Pass:
case HeartsPhase.Take:
case HeartsPhase.Play:
@@ -450,6 +448,7 @@
child: new GestureDetector(onTap: () {
setState(() {
game.takeTrickUI();
+ game.debugString = null;
});
},
child: new Container(
@@ -678,13 +677,14 @@
List<logic_card.Card> hand,
AcceptCb cb,
NoArgCb buttoncb) {
- bool draggable = (cb != null);
bool completed = (buttoncb == null);
+ bool draggable = (cb != null) && !completed;
List<Widget> topCardWidgets = new List<Widget>();
- topCardWidgets.add(_topCardWidget(c1, cb));
- topCardWidgets.add(_topCardWidget(c2, cb));
- topCardWidgets.add(_topCardWidget(c3, cb));
+ AcceptCb topCb = completed ? null : cb;
+ topCardWidgets.add(_topCardWidget(c1, topCb));
+ topCardWidgets.add(_topCardWidget(c2, topCb));
+ topCardWidgets.add(_topCardWidget(c3, topCb));
topCardWidgets.add(_makeButton(name, buttoncb, inactive: completed));
Color bgColor = completed ? Colors.teal[600] : Colors.teal[500];
@@ -714,9 +714,9 @@
comparator: _compareCards,
width: config.width,
acceptCallback: cb,
- acceptType: cb != null ? DropType.card : null,
+ acceptType: draggable ? DropType.card : null,
cardTapCallback:
- cb != null ? (logic_card.Card c) => cb(c, emptyC) : null,
+ draggable ? (logic_card.Card c) => cb(c, emptyC) : null,
backgroundColor: Colors.grey[500],
altColor: Colors.grey[700],
useKeys: true);
diff --git a/lib/logic/hearts/hearts_game.part.dart b/lib/logic/hearts/hearts_game.part.dart
index 1da768c..e4759ed 100644
--- a/lib/logic/hearts/hearts_game.part.dart
+++ b/lib/logic/hearts/hearts_game.part.dart
@@ -101,9 +101,9 @@
switch (roundNumber % 4) {
// is a 4-cycle
case 0:
- return (playerNumber - 1) % 4; // passLeft
+ return (playerNumber - 1) % 4; // passRight
case 1:
- return (playerNumber + 1) % 4; // passRight
+ return (playerNumber + 1) % 4; // passLeft
case 2:
return (playerNumber + 2) % 4; // passAcross
case 3:
diff --git a/lib/src/syncbase/croupier_client.dart b/lib/src/syncbase/croupier_client.dart
index ca5feba..3746850 100644
--- a/lib/src/syncbase/croupier_client.dart
+++ b/lib/src/syncbase/croupier_client.dart
@@ -215,7 +215,7 @@
}
// 2. Then run through each value in order.
- watchSequence.forEach((sc.WatchChange _w) async {
+ await Future.forEach(watchSequence, (sc.WatchChange _w) async {
String key = _w.rowKey;
String value;
switch (_w.changeType) {
diff --git a/lib/styles/common.dart b/lib/styles/common.dart
index 5fec982..77caba5 100644
--- a/lib/styles/common.dart
+++ b/lib/styles/common.dart
@@ -52,5 +52,6 @@
Color secondaryTextColor = Colors.grey[500];
Color errorColor = Colors.red[500];
+Color transparentColor = const Color(0x00000000);
ThemeData theme = new ThemeData(
primarySwatch: Colors.blueGrey, accentColor: Colors.orangeAccent[700]);
diff --git a/manifest.yaml b/manifest.yaml
index 98c5ab0..cb3f666 100644
--- a/manifest.yaml
+++ b/manifest.yaml
@@ -249,6 +249,9 @@
- images/avatars/android.png
- images/avatars/man.png
- images/avatars/woman.png
+ - images/games/hearts/pass_across.png
+ - images/games/hearts/pass_left.png
+ - images/games/hearts/pass_right.png
- images/splash/background.png
- images/splash/flutter.png
- images/splash/vanadium.png