blob: f9c3808210ad391f56f6985ad7f6e20a48d1bed4 [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,
{double width, double height})
: super(croupier, cb, 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;
@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) {
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: 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:
visibleCardCollectionIndexes
.add(HeartsGame.OFFSET_HAND + playerNum);
visibleCardCollectionIndexes
.add(HeartsGame.OFFSET_PLAY + playerNum);
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;
}
});
}
// 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());
} 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();
} 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);
} else {
print("You can't do that! ${reason}");
game.debugString = reason;
}
});
}
void _endRoundDebugCallback() {
setState(() {
HeartsGame game = config.game as HeartsGame;
game.jumpToScorePhaseDebug();
});
}
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:
kids.add(new Text("SCORE PHASE"));
break;
default:
assert(false);
return null;
}
kids.add(_makeDebugButtons());
return new Column(kids, justifyContent: FlexJustifyContent.spaceBetween);
}
Widget showBoard() {
return new HeartsBoard(config.croupier, this.update,
width: config.width, height: 0.80 * config.height);
}
Widget showPlay() {
HeartsGame game = config.game as HeartsGame;
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(
game.cardCollections[i + HeartsGame.OFFSET_PLAY],
true,
CardCollectionOrientation.show1,
width: config.width));
}
cardCollections.add(new Container(
decoration: new BoxDecoration(backgroundColor: Colors.teal[600]),
width: config.width,
child:
new Flex(plays, justifyContent: FlexJustifyContent.spaceAround)));
int p = game.playerNumber;
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,
acceptCallback: _makeGameMoveCallback,
acceptType: p == game.whoseTurn ? DropType.card : DropType.none,
width: config.width,
backgroundColor:
p == game.whoseTurn ? Colors.white : Colors.grey[500],
altColor: p == game.whoseTurn
? Colors.grey[200]
: Colors.grey[600])));
cardCollections.add(playArea);
List<logic_card.Card> cards = game.cardCollections[p];
CardCollectionComponent c = new CardCollectionComponent(
cards, game.playerNumber == p, CardCollectionOrientation.suit,
dragChildren: game.whoseTurn == p,
comparator: _compareCards,
width: config.width,
useKeys: true);
cardCollections.add(c); // flex
cardCollections.add(new Text("Player ${game.whoseTurn}'s turn"));
cardCollections.add(new Text(game.debugString));
cardCollections.add(_makeDebugButtons());
return new Column(cardCollections,
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(settings: cs);
}
Widget showScore() {
HeartsGame game = config.game as HeartsGame;
Widget w;
if (game.hasGameEnded) {
w = new Text("Game Over!");
} else if (game.ready[game.playerNumber]) {
w = new Text("Waiting for other players...");
} else {
w = _makeButton('Ready?', 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: _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() {
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) {
HeartsGame game = config.game as HeartsGame;
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,
acceptCallback: cb,
dragChildren: draggable,
acceptType: draggable ? DropType.card : null,
comparator: _compareCards,
width: config.width,
backgroundColor: Colors.grey[500],
altColor: Colors.grey[700],
useKeys: true);
return new Column(<Widget>[
topArea,
handArea,
new Text(game.debugString),
_makeDebugButtons()
], justifyContent: FlexJustifyContent.spaceBetween);
}
Widget _topCardWidget(List<logic_card.Card> cards, AcceptCb cb) {
Widget ccc = new CardCollectionComponent(
cards, true, CardCollectionOrientation.show1,
acceptCallback: cb,
dragChildren: cb != null,
acceptType: cb != null ? DropType.card : null,
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);
}
}