blob: 60cef30903ecaafbd50e1288f6281683b1940bf2 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
part of game_component;
class HeartsGameComponent extends GameComponent {
HeartsGameComponent(Croupier croupier, NoArgCb cb,
{Key key, double width, double height})
: super(croupier, cb, key: key, width: width, height: height);
HeartsGameComponentState createState() => new HeartsGameComponentState();
}
class HeartsGameComponentState extends GameComponentState<HeartsGameComponent> {
List<logic_card.Card> passingCards1 = new List<logic_card.Card>();
List<logic_card.Card> passingCards2 = new List<logic_card.Card>();
List<logic_card.Card> passingCards3 = new List<logic_card.Card>();
HeartsType _lastViewType;
bool _showSplitView = false;
bool trickTaking = false;
List<List<logic_card.Card>> playedCards = new List<List<logic_card.Card>>(4);
static const int SHOW_TRICK_DURATION = 2000; // ms
@override
void initState() {
super.initState();
// If someone sat at the table, they would have the value 4.
// If nobody sat at the table, then we should show the split view.
if (!config.croupier.players_found.values.contains(4)) {
_showSplitView = true;
}
_reset();
_fillPlayedCards();
}
// Make copies of the played cards.
void _fillPlayedCards() {
for (int i = 0; i < 4; i++) {
playedCards[i] = new List<logic_card.Card>.from(
config.game.cardCollections[i + HeartsGame.OFFSET_PLAY]);
}
}
// If there were 3 played cards before and now there are 0...
bool _detectTrick() {
HeartsGame game = config.game;
int lastNumPlayed = playedCards.where((List<logic_card.Card> list) {
return list.length > 0;
}).length;
return lastNumPlayed == 3 && game.numPlayed == 0;
}
// Make a copy of the missing played card.
void _fillMissingPlayedCard() {
HeartsGame game = config.game;
List<logic_card.Card> trickPile =
game.cardCollections[game.lastTrickTaker + HeartsGame.OFFSET_TRICK];
// Find the index of the missing play card.
int missing;
for (int j = 0; j < 4; j++) {
if (playedCards[j].length == 0) {
missing = j;
break;
}
}
// Use the trickPile to get this card.
playedCards[missing] = <logic_card.Card>[
trickPile[trickPile.length - 4 + missing]
];
}
@override
void _reset() {
super._reset();
HeartsGame game = config.game as HeartsGame;
_lastViewType = game.viewType;
}
@override
Widget build(BuildContext context) {
HeartsGame game = config.game as HeartsGame;
// check if we need to swap out our 's map.
if (_lastViewType != game.viewType) {
_reset();
}
// Set the trickTaking flag on each build.
if (!trickTaking) {
if (_detectTrick()) {
trickTaking = true;
_fillMissingPlayedCard();
// Unfortunately, ZCards are drawn on the game layer,
// so instead of setState, we must use trueSetState.
new Future.delayed(const Duration(milliseconds: SHOW_TRICK_DURATION),
() {
setState(() {
trickTaking = false;
});
});
} else {
_fillPlayedCards();
}
}
// Hearts Widget
Widget heartsWidget = new Container(
decoration: 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));
List<int> visibleCardCollectionIndexes = new List<int>();
if (game.phase != HeartsPhase.StartGame &&
game.phase != HeartsPhase.Deal &&
game.phase != HeartsPhase.Score) {
int playerNum = game.playerNumber;
if (game.viewType == HeartsType.Player) {
switch (game.phase) {
case HeartsPhase.Pass:
visibleCardCollectionIndexes
.add(HeartsGame.OFFSET_PASS + playerNum);
visibleCardCollectionIndexes
.add(HeartsGame.OFFSET_HAND + playerNum);
break;
case HeartsPhase.Take:
visibleCardCollectionIndexes
.add(HeartsGame.OFFSET_PASS + game.takeTarget);
visibleCardCollectionIndexes
.add(HeartsGame.OFFSET_HAND + playerNum);
break;
case HeartsPhase.Play:
for (int i = 0; i < 4; i++) {
if (_showSplitView || i == playerNum) {
visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PLAY + i);
}
if (_showSplitView) {
visibleCardCollectionIndexes
.add(HeartsGame.OFFSET_HAND + playerNum);
visibleCardCollectionIndexes.add(HeartsGame.OFFSET_TRICK + i);
}
}
break;
default:
break;
}
} else {
// A board will need to see these things.
for (int i = 0; i < 4; i++) {
visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PLAY + i);
visibleCardCollectionIndexes.add(HeartsGame.OFFSET_PASS + i);
visibleCardCollectionIndexes.add(HeartsGame.OFFSET_HAND + i);
visibleCardCollectionIndexes.add(HeartsGame.OFFSET_TRICK + i);
}
}
}
children.add(this.buildCardAnimationLayer(visibleCardCollectionIndexes));
return new Container(
width: config.width, height: config.height, child: new Stack(children));
}
void _switchViewCallback() {
HeartsGame game = config.game;
setState(() {
if (game.viewType == HeartsType.Player) {
game.viewType = HeartsType.Board;
} else {
game.viewType = HeartsType.Player;
if (!game.isPlayer) {
game.playerNumber = 0; // avoid accidental red screen
}
}
});
}
// Passing between the temporary pass list and the player's hand.
// Does not actually move anything in game logic terms.
void _uiPassCardCallback(logic_card.Card card, List<logic_card.Card> dest) {
setState(() {
if (passingCards1.contains(card)) {
passingCards1.remove(card);
}
if (passingCards2.contains(card)) {
passingCards2.remove(card);
}
if (passingCards3.contains(card)) {
passingCards3.remove(card);
}
if (dest == passingCards1) {
passingCards1.clear();
passingCards1.add(card);
} else if (dest == passingCards2) {
passingCards2.clear();
passingCards2.add(card);
} else if (dest == passingCards3) {
passingCards3.clear();
passingCards3.add(card);
}
});
}
int _compareCards(logic_card.Card a, logic_card.Card b) {
if (a == b) return 0;
assert(a.deck == "classic" && b.deck == "classic");
HeartsGame game = config.game as HeartsGame;
int r = game.getCardSuit(a).compareTo(game.getCardSuit(b));
if (r != 0) return r;
return game.getCardValue(a) < game.getCardValue(b) ? -1 : 1;
}
void _clearPassing() {
passingCards1.clear();
passingCards2.clear();
passingCards3.clear();
}
List<logic_card.Card> _combinePassing() {
List<logic_card.Card> ls = new List<logic_card.Card>();
ls.addAll(passingCards1);
ls.addAll(passingCards2);
ls.addAll(passingCards3);
return ls;
}
// This shouldn't always be here, but for now, we have little choice.
void _switchPlayersCallback() {
setState(() {
config.game.playerNumber = (config.game.playerNumber + 1) % 4;
_clearPassing(); // Just for sanity.
});
}
void _makeGamePassCallback() {
setState(() {
try {
HeartsGame game = config.game as HeartsGame;
game.passCards(_combinePassing());
game.debugString = null;
} catch (e) {
print("You can't do that! ${e.toString()}");
config.game.debugString = e.toString();
}
});
}
void _makeGameTakeCallback() {
setState(() {
try {
// TODO(alexfandrianto): Another way to clear these passing cards is to
// do so upon the transition from the pass phase to the take phase.
// However, since they are never seen outside of the Pass phase, it is
// also valid to clear them upon taking any cards.
_clearPassing();
HeartsGame game = config.game as HeartsGame;
game.takeCards();
game.debugString = null;
} catch (e) {
print("You can't do that! ${e.toString()}");
config.game.debugString = e.toString();
}
});
}
void _makeGameMoveCallback(logic_card.Card card, List<logic_card.Card> dest) {
setState(() {
HeartsGame game = config.game;
String reason = game.canPlay(game.playerNumber, card);
if (reason == null) {
game.move(card, dest);
game.debugString = null;
} else {
print("You can't do that! ${reason}");
game.debugString = reason;
}
});
}
void _endRoundDebugCallback() {
setState(() {
HeartsGame game = config.game as HeartsGame;
game.jumpToScorePhaseDebug();
game.debugString = null;
});
}
Widget _makeDebugButtons() {
if (config.game.debugMode == false) {
return new Flex([]);
}
return new Container(
width: config.width,
child: new Flex([
new Flexible(
flex: 1, child: new Text('P${config.game.playerNumber}')),
new Flexible(
flex: 5,
child: _makeButton('Switch Player', _switchPlayersCallback)),
new Flexible(
flex: 5, child: _makeButton('Switch View', _switchViewCallback)),
new Flexible(
flex: 5, child: _makeButton('End Round', _endRoundDebugCallback)),
new Flexible(flex: 4, child: _makeButton('Quit', _quitGameCallback))
]));
}
@override
Widget _makeButton(String text, NoArgCb callback, {bool inactive: false}) {
var borderColor = inactive ? Colors.grey[500] : Colors.white;
var backgroundColor = inactive ? Colors.grey[500] : null;
return new FlatButton(
child: new Container(
decoration: new BoxDecoration(
border: new Border.all(width: 1.0, color: borderColor),
backgroundColor: backgroundColor),
padding: new EdgeDims.all(10.0),
child: new Text(text)),
onPressed: inactive ? null : callback);
}
Widget buildHearts() {
HeartsGame game = config.game as HeartsGame;
if (game.viewType == HeartsType.Board) {
return buildHeartsBoard();
}
switch (game.phase) {
case HeartsPhase.StartGame:
case HeartsPhase.Deal:
return showDeal();
case HeartsPhase.Pass:
return showPass();
case HeartsPhase.Take:
return showTake();
case HeartsPhase.Play:
return showPlay();
case HeartsPhase.Score:
return showScore();
default:
assert(false);
return null;
}
}
Widget buildHeartsBoard() {
HeartsGame game = config.game as HeartsGame;
List<Widget> kids = new List<Widget>();
switch (game.phase) {
case HeartsPhase.StartGame:
case HeartsPhase.Deal:
kids.add(new Text("Waiting for Deal..."));
break;
case HeartsPhase.Pass:
case HeartsPhase.Take:
case HeartsPhase.Play:
kids.add(showBoard());
break;
case HeartsPhase.Score:
return showScore();
default:
assert(false);
return null;
}
kids.add(_makeDebugButtons());
return new Column(kids, justifyContent: FlexJustifyContent.spaceBetween);
}
Widget showBoard() {
return new HeartsBoard(config.croupier,
width: config.width,
height: 0.80 * config.height,
trickTaking: trickTaking,
playedCards: playedCards);
}
String _getName(int playerNumber) {
return config.croupier.settingsFromPlayerNumber(playerNumber)?.name;
}
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";
// Override if someone is taking a trick.
if (this.trickTaking) {
String trickTaker =
_getName(game.lastTrickTaker) ?? "Player ${game.lastTrickTaker}";
status = "${trickTaker}'s trick";
}
// Override if there is a debug string.
if (config.game.debugString != null) {
status = config.game.debugString;
}
return status;
}
Widget _buildStatusBar() {
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));
}
Widget _buildFullMiniBoard() {
return new Container(
width: config.width * 0.5,
height: config.height * 0.25,
child: new HeartsBoard(config.croupier,
width: config.width * 0.5,
height: config.height * 0.25,
cardWidth: config.height * 0.1,
cardHeight: config.height * 0.1,
isMini: true,
gameAcceptCallback: _makeGameMoveCallback,
trickTaking: trickTaking,
playedCards: playedCards));
}
Widget showPlay() {
HeartsGame game = config.game as HeartsGame;
int p = game.playerNumber;
List<Widget> cardCollections = new List<Widget>();
if (_showSplitView) {
cardCollections.add(new Container(
decoration:
new BoxDecoration(backgroundColor: style.theme.primaryColor),
child: new Column([_buildFullMiniBoard(), _buildStatusBar()])));
} else {
Widget playArea = new Container(
decoration: 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,
animationType: component_card.CardAnimationType.NONE,
acceptCallback: _makeGameMoveCallback,
acceptType:
p == game.whoseTurn ? DropType.card : DropType.none,
backgroundColor:
p == game.whoseTurn ? Colors.white : Colors.grey[500],
altColor: p == game.whoseTurn
? Colors.grey[200]
: Colors.grey[600])));
cardCollections.add(new Container(
decoration:
new BoxDecoration(backgroundColor: style.theme.primaryColor),
child: new Column([_buildStatusBar(), playArea])));
}
List<logic_card.Card> cards = game.cardCollections[p];
CardCollectionComponent c = new CardCollectionComponent(
cards, game.playerNumber == p, CardCollectionOrientation.suit,
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());
return new Column(cardCollections,
justifyContent: FlexJustifyContent.spaceBetween);
}
Widget showScore() {
HeartsGame game = config.game as HeartsGame;
Widget w;
if (game.hasGameEnded) {
w = new Text("Game Over!");
} else if (!game.isPlayer || game.ready[game.playerNumber]) {
w = new Text("Waiting for other players...");
} else {
w = _makeButton('New Round', game.setReadyUI);
}
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 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: new CroupierProfileComponent(
settings: config.croupier.settingsFromPlayerNumber(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() {
HeartsGame game = config.game as HeartsGame;
return new Container(
decoration: new BoxDecoration(backgroundColor: Colors.pink[500]),
child: new Flex([
new Text('Player ${game.playerNumber}'),
new Text('Waiting for Deal...'),
_makeDebugButtons()
],
direction: FlexDirection.vertical,
justifyContent: FlexJustifyContent.spaceBetween));
}
Widget _helpPassTake(
String name,
List<logic_card.Card> c1,
List<logic_card.Card> c2,
List<logic_card.Card> c3,
List<logic_card.Card> hand,
AcceptCb cb,
NoArgCb buttoncb) {
bool draggable = (cb != null);
bool completed = (buttoncb == null);
List<Widget> topCardWidgets = new List<Widget>();
topCardWidgets.add(_topCardWidget(c1, cb));
topCardWidgets.add(_topCardWidget(c2, cb));
topCardWidgets.add(_topCardWidget(c3, cb));
topCardWidgets.add(_makeButton(name, buttoncb, inactive: completed));
Color bgColor = completed ? Colors.teal[600] : Colors.teal[500];
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 handArea = new CardCollectionComponent(
hand, true, CardCollectionOrientation.suit,
dragChildren: draggable,
comparator: _compareCards,
width: config.width,
backgroundColor: Colors.grey[500],
altColor: Colors.grey[700],
useKeys: true);
return new Column(<Widget>[topArea, handArea, _makeDebugButtons()],
justifyContent: FlexJustifyContent.spaceBetween);
}
Widget _topCardWidget(List<logic_card.Card> cards, AcceptCb cb) {
Widget ccc = new CardCollectionComponent(
cards, true, CardCollectionOrientation.show1,
acceptCallback: cb,
acceptType: cb != null ? DropType.card : null,
animationType: component_card.CardAnimationType.NONE,
backgroundColor: Colors.white,
altColor: Colors.grey[200],
useKeys: true);
if (cb == null) {
ccc = new Container(child: ccc);
}
return ccc;
}
// Pass Phase Screen: Show the cards being passed and the player's remaining cards.
Widget showPass() {
HeartsGame game = config.game as HeartsGame;
List<logic_card.Card> passCards =
game.cardCollections[game.playerNumber + HeartsGame.OFFSET_PASS];
List<logic_card.Card> playerCards = game.cardCollections[game.playerNumber];
List<logic_card.Card> remainingCards = new List<logic_card.Card>();
playerCards.forEach((logic_card.Card c) {
if (!passingCards1.contains(c) &&
!passingCards2.contains(c) &&
!passingCards3.contains(c)) {
remainingCards.add(c);
}
});
bool hasPassed = passCards.length != 0;
return _helpPassTake(
"Pass",
passingCards1,
passingCards2,
passingCards3,
remainingCards,
_uiPassCardCallback,
hasPassed ? null : _makeGamePassCallback);
}
// Take Phase Screen: Show the cards the player has received and the player's hand.
Widget showTake() {
HeartsGame game = config.game as HeartsGame;
List<logic_card.Card> playerCards = game.cardCollections[game.playerNumber];
List<logic_card.Card> takeCards =
game.cardCollections[game.takeTarget + HeartsGame.OFFSET_PASS];
bool hasTaken = takeCards.length == 0;
List<logic_card.Card> take1 =
takeCards.length != 0 ? takeCards.sublist(0, 1) : [];
List<logic_card.Card> take2 =
takeCards.length != 0 ? takeCards.sublist(1, 2) : [];
List<logic_card.Card> take3 =
takeCards.length != 0 ? takeCards.sublist(2, 3) : [];
return _helpPassTake("Take", take1, take2, take3, playerCards, null,
hasTaken ? null : _makeGameTakeCallback);
}
}
class HeartsArrangeComponent extends GameArrangeComponent {
HeartsArrangeComponent(Croupier croupier, {double width, double height})
: super(croupier, width: width, height: height);
bool get hasSat => croupier.game.playerNumber != null;
Widget build(BuildContext context) {
int numAtTable = croupier.players_found.values
.where((int playerNumber) => playerNumber == 4)
.length;
return new Container(
decoration: style.Box.liveNow,
height: height,
width: width,
child: new Column([
new Flexible(
flex: 1,
child: new Row(
[_buildEmptySlot(), _buildSlot("Player", 2), _buildEmptySlot()],
justifyContent: FlexJustifyContent.spaceAround,
alignItems: FlexAlignItems.stretch)),
new Flexible(
flex: 1,
child: new Row([
_buildSlot("Player", 1),
_buildSlot("Table: ${numAtTable}", 4),
_buildSlot("Player", 3)
],
justifyContent: FlexJustifyContent.spaceAround,
alignItems: FlexAlignItems.stretch)),
new Flexible(
flex: 1,
child: new Row(
[_buildEmptySlot(), _buildSlot("Player", 0), _buildEmptySlot()],
justifyContent: FlexJustifyContent.spaceAround,
alignItems: FlexAlignItems.stretch))
],
justifyContent: FlexJustifyContent.spaceAround,
alignItems: FlexAlignItems.stretch));
}
Widget _buildEmptySlot() {
return new Flexible(flex: 1, child: new Text(""));
}
Widget _buildSlot(String name, int index) {
NoArgCb onTap = () {
croupier.settings_manager.setPlayerNumber(croupier.game.gameID, index);
HeartsGame game = croupier.game;
game.playerNumber = index;
game.setReadyUI();
};
Widget slotWidget = new Text(name, style: style.Text.hugeStyle);
bool seatTaken =
index >= 0 && index < 4 && croupier.players_found.containsValue(index);
if (seatTaken) {
onTap = null;
slotWidget = new CroupierProfileComponent(
settings: croupier.settingsFromPlayerNumber(index));
} else if (hasSat) {
onTap = null;
}
return new Flexible(
flex: 1,
child: new GestureDetector(
child: new Card(
color: croupier.game.playerNumber == index
? style.theme.accentColor
: null,
child: slotWidget),
onTap: onTap));
}
}