| // 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. |
| |
| import '../logic/card.dart' as logic_card; |
| import '../logic/game/game.dart' show Game, GameType; |
| import '../logic/hearts/hearts.dart' show HeartsGame, HeartsPhase; |
| //import 'board.dart' show Board; |
| import 'card_collection.dart' |
| show CardCollectionComponent, DropType, Orientation; |
| |
| import 'package:sky/widgets_next.dart'; |
| import 'package:sky/material.dart' as material; |
| |
| typedef void NoArgCb(); |
| |
| abstract class GameComponent extends StatefulComponent { |
| final NavigatorState navigator; |
| final Game game; |
| final NoArgCb gameEndCallback; |
| final double width; |
| final double height; |
| |
| GameComponent(this.navigator, this.game, this.gameEndCallback, {this.width, this.height}); |
| } |
| |
| abstract class GameComponentState<T extends GameComponent> extends State<T> { |
| void initState(_) { |
| super.initState(_); |
| |
| config.game.updateCallback = update; |
| } |
| |
| // 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() { |
| setState(() {}); |
| } |
| |
| // A helper that most subclasses use in order to quit their respective games. |
| void _quitGameCallback() { |
| setState(() { |
| config.gameEndCallback(); |
| }); |
| } |
| |
| // A helper that subclasses might override to create buttons. |
| Widget _makeButton(String text, NoArgCb callback) { |
| return new FlatButton(child: new Text(text), onPressed: callback); |
| } |
| |
| @override |
| Widget build(BuildContext context); // still UNIMPLEMENTED |
| } |
| |
| GameComponent createGameComponent(NavigatorState navigator, Game game, NoArgCb gameEndCallback, |
| {double width, double height}) { |
| switch (game.gameType) { |
| case GameType.Proto: |
| return new ProtoGameComponent(navigator, game, gameEndCallback, |
| width: width, height: height); |
| case GameType.Hearts: |
| return new HeartsGameComponent(navigator, game, gameEndCallback, |
| width: width, height: height); |
| default: |
| // We're probably not ready to serve the other games yet. |
| assert(false); |
| return null; |
| } |
| } |
| |
| class ProtoGameComponent extends GameComponent { |
| ProtoGameComponent(NavigatorState navigator, Game game, NoArgCb cb, {double width, double height}) |
| : super(navigator, game, cb, width: width, height: height); |
| |
| ProtoGameComponentState createState() => new ProtoGameComponentState(); |
| } |
| |
| class ProtoGameComponentState extends GameComponentState<ProtoGameComponent> { |
| @override |
| Widget build(BuildContext context) { |
| List<Widget> cardCollections = new List<Widget>(); |
| |
| cardCollections.add(new Text(config.game.debugString)); |
| |
| for (int i = 0; i < 4; i++) { |
| List<logic_card.Card> cards = config.game.cardCollections[i]; |
| CardCollectionComponent c = new CardCollectionComponent(config.navigator, cards, |
| config.game.playerNumber == i, Orientation.horz, |
| dragChildren: true, acceptType: DropType.card, acceptCallback: _makeGameMoveCallback, width: config.width); |
| cardCollections.add(c); // flex |
| } |
| |
| cardCollections.add(new Container( |
| decoration: new BoxDecoration( |
| backgroundColor: material.Colors.green[500], borderRadius: 5.0), |
| child: new CardCollectionComponent(config.navigator, config.game.cardCollections[4], true, |
| Orientation.show1, |
| dragChildren: true, acceptType: DropType.card, acceptCallback: _makeGameMoveCallback, width: config.width))); |
| |
| cardCollections.add(_makeDebugButtons()); |
| |
| return new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.Colors.pink[500]), |
| child: new Flex(cardCollections, direction: FlexDirection.vertical)); |
| } |
| |
| void _makeGameMoveCallback(logic_card.Card card, List<logic_card.Card> dest) { |
| setState(() { |
| try { |
| config.game.move(card, dest); |
| } catch (e) { |
| print("You can't do that! ${e.toString()}"); |
| config.game.debugString = e.toString(); |
| } |
| }); |
| } |
| |
| Widget _makeDebugButtons() => new Flex([ |
| new Text('P${config.game.playerNumber}'), |
| _makeButton('Switch View', _switchPlayersCallback), |
| _makeButton('Quit', _quitGameCallback) |
| ]); |
| |
| void _switchPlayersCallback() { |
| setState(() { |
| config.game.playerNumber = (config.game.playerNumber + 1) % 4; |
| }); |
| } |
| } |
| |
| class HeartsGameComponent extends GameComponent { |
| HeartsGameComponent(NavigatorState navigator, Game game, NoArgCb cb, {double width, double height}) |
| : super(navigator, game, 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>(); |
| |
| @override |
| Widget build(BuildContext context) { |
| return new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.Colors.grey[300]), |
| child: buildHearts()); |
| } |
| |
| // 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.length == 0) { |
| passingCards1.add(card); |
| } else if (dest == passingCards2 && passingCards2.length == 0) { |
| passingCards2.add(card); |
| } else if (dest == passingCards3 && passingCards3.length == 0) { |
| 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() => 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 View', _switchPlayersCallback)), |
| new Flexible( |
| flex: 5, |
| child: _makeButton('Dump Log', () => print(config.game.gamelog))), |
| 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 ? material.Colors.grey[500] : material.Colors.white; |
| var backgroundColor = inactive ? material.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)), |
| enabled: !inactive, |
| onPressed: inactive ? null : callback); |
| } |
| |
| Widget buildHearts() { |
| HeartsGame game = config.game as HeartsGame; |
| |
| switch (game.phase) { |
| 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 showPlay() { |
| HeartsGame game = config.game as HeartsGame; |
| |
| List<Widget> cardCollections = new List<Widget>(); |
| |
| List<Widget> plays = new List<Widget>(); |
| for (int i = 0; i < 4; i++) { |
| plays.add(new CardCollectionComponent( |
| config.navigator, |
| game.cardCollections[i + HeartsGame.OFFSET_PLAY], |
| true, |
| Orientation.show1, |
| width: config.width)); |
| } |
| cardCollections.add(new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.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: material.Colors.teal[500]), |
| width: config.width, |
| child: new Center( |
| child: new CardCollectionComponent( |
| config.navigator, |
| game.cardCollections[p + HeartsGame.OFFSET_PLAY], |
| true, |
| Orientation.show1, |
| acceptCallback: _makeGameMoveCallback, |
| acceptType: p == game.whoseTurn ? DropType.card : DropType.none, |
| width: config.width, |
| backgroundColor: p == game.whoseTurn |
| ? material.Colors.white |
| : material.Colors.grey[500], |
| altColor: p == game.whoseTurn |
| ? material.Colors.grey[200] |
| : material.Colors.grey[600]))); |
| cardCollections.add(playArea); |
| |
| List<logic_card.Card> cards = game.cardCollections[p]; |
| CardCollectionComponent c = new CardCollectionComponent( |
| config.navigator, |
| cards, game.playerNumber == p, Orientation.suit, |
| dragChildren: game.whoseTurn == p, |
| comparator: _compareCards, |
| width: config.width); |
| 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 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); |
| } |
| |
| return new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.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? |
| new Text('${game.scores}'), |
| w, |
| _makeButton("Return to Lobby", _quitGameCallback), |
| _makeDebugButtons() |
| ], direction: FlexDirection.vertical)); |
| } |
| |
| Widget showDeal() { |
| HeartsGame game = config.game as HeartsGame; |
| |
| return new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.Colors.pink[500]), |
| child: new Flex([ |
| new Text('Player ${game.playerNumber}'), |
| _makeButton('Deal', game.dealCards), |
| _makeDebugButtons() |
| ], |
| direction: FlexDirection.vertical, |
| justifyContent: FlexJustifyContent.spaceBetween)); |
| } |
| |
| // the pass phase screen consists of 2 parts: |
| // The cards being passed + Pass button. |
| // The cards in your hand. |
| 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; |
| // TODO(alexfandrianto): You can pass as many times as you want... which is silly. |
| // Luckily, later passes shouldn't do anything. |
| |
| List<Widget> passingCardWidgets = <Widget>[ |
| new Container( |
| margin: new EdgeDims.all(10.0), |
| child: new CardCollectionComponent( |
| config.navigator, |
| passingCards1, true, Orientation.show1, |
| acceptCallback: _uiPassCardCallback, |
| dragChildren: !hasPassed, |
| acceptType: DropType.card, |
| backgroundColor: material.Colors.white, |
| altColor: material.Colors.grey[200])), |
| new Container( |
| margin: new EdgeDims.all(10.0), |
| child: new CardCollectionComponent( |
| config.navigator, |
| passingCards2, true, Orientation.show1, |
| acceptCallback: _uiPassCardCallback, |
| dragChildren: !hasPassed, |
| acceptType: DropType.card, |
| backgroundColor: material.Colors.white, |
| altColor: material.Colors.grey[200])), |
| new Container( |
| margin: new EdgeDims.all(10.0), |
| child: new CardCollectionComponent( |
| config.navigator, |
| passingCards3, true, Orientation.show1, |
| acceptCallback: _uiPassCardCallback, |
| dragChildren: !hasPassed, |
| acceptType: DropType.card, |
| backgroundColor: material.Colors.white, |
| altColor: material.Colors.grey[200])) |
| ]; |
| Widget passArea; |
| if (hasPassed) { |
| passArea = new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.Colors.teal[600]), |
| width: config.width, |
| child: new Flex( |
| passingCardWidgets |
| ..add(_makeButton("Pass", null, inactive: true)), |
| justifyContent: FlexJustifyContent.spaceBetween)); |
| } else { |
| passArea = new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.Colors.teal[500]), |
| width: config.width, |
| child: new Flex( |
| passingCardWidgets |
| ..add(_makeButton("Pass", _makeGamePassCallback)), |
| justifyContent: FlexJustifyContent.spaceBetween)); |
| } |
| |
| // Return the pass cards and the player's remaining hand. |
| // (Also includes debug info) |
| return new Column(<Widget>[ |
| passArea, |
| new CardCollectionComponent( |
| config.navigator, |
| remainingCards, true, Orientation.suit, |
| acceptCallback: _uiPassCardCallback, |
| dragChildren: !hasPassed, |
| acceptType: DropType.card, |
| comparator: _compareCards, |
| width: config.width, |
| backgroundColor: material.Colors.grey[500], |
| altColor: material.Colors.grey[700]), |
| new Text(game.debugString), |
| _makeDebugButtons() |
| ], justifyContent: FlexJustifyContent.spaceBetween); |
| } |
| |
| 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) : []; |
| |
| List<Widget> takeCardWidgets = <Widget>[ |
| new Container( |
| margin: new EdgeDims.all(10.0), |
| child: new CardCollectionComponent( |
| config.navigator, |
| take1, true, Orientation.show1, |
| backgroundColor: material.Colors.white, |
| altColor: material.Colors.grey[200])), |
| new Container( |
| margin: new EdgeDims.all(10.0), |
| child: new CardCollectionComponent( |
| config.navigator, |
| take2, true, Orientation.show1, |
| backgroundColor: material.Colors.white, |
| altColor: material.Colors.grey[200])), |
| new Container( |
| margin: new EdgeDims.all(10.0), |
| child: new CardCollectionComponent( |
| config.navigator, |
| take3, true, Orientation.show1, |
| backgroundColor: material.Colors.white, |
| altColor: material.Colors.grey[200])) |
| ]; |
| Widget takeArea; |
| if (hasTaken) { |
| takeArea = new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.Colors.teal[600]), |
| width: config.width, |
| child: new Flex( |
| takeCardWidgets..add(_makeButton("Take", null, inactive: true)), |
| justifyContent: FlexJustifyContent.spaceBetween)); |
| } else { |
| takeArea = new Container( |
| decoration: |
| new BoxDecoration(backgroundColor: material.Colors.teal[500]), |
| width: config.width, |
| child: new Flex( |
| takeCardWidgets..add(_makeButton("Take", _makeGameTakeCallback)), |
| justifyContent: FlexJustifyContent.spaceBetween)); |
| } |
| |
| // Return the passsed cards and the player's hand. |
| // (Also includes debug info) |
| return new Column(<Widget>[ |
| takeArea, |
| new CardCollectionComponent( |
| config.navigator, playerCards, true, Orientation.suit, |
| comparator: _compareCards, |
| width: config.width, |
| backgroundColor: material.Colors.grey[500], |
| altColor: material.Colors.grey[700]), |
| new Text(game.debugString), |
| _makeDebugButtons() |
| ], justifyContent: FlexJustifyContent.spaceBetween); |
| } |
| } |