TBR Update Croupier
* Fixes a bug where Sky changed StatefulComponent's location.
* Forces checked mode.
* Includes pass and take phases to the UI.
* Uses the syncbase game log idea (in a simplistic way).
* For use in make test and make start, a mock implementation of the store is used.
This allows testing with pub, but also requires use of `make mock` and `make unmock`.
This is very hacky.
Bug found in sky with drag and drop, so I have disabled Transform for now.
Change-Id: I3f4c7dac3b66ba85f3e4c61c9334a75d66e6e252
diff --git a/Makefile b/Makefile
index cbd61ea..27f166e 100644
--- a/Makefile
+++ b/Makefile
@@ -49,16 +49,25 @@
.PHONY: lint
lint:
- dartanalyzer lib/main.dart
- dartanalyzer $(DART_TEST_FILES)
+ dartanalyzer lib/main.dart | grep -v "\[warning\] The imported libraries"
+ dartanalyzer $(DART_TEST_FILES) | grep -v "\[warning\] The imported libraries"
.PHONY: start
start:
- ./packages/sky/sky_tool start
+ ./packages/sky/sky_tool start --checked
+
+.PHONY: mock
+mock:
+ mv lib/src/syncbase/log_writer.dart lib/src/syncbase/log_writer.dart.backup
+ cp lib/src/mocks/log_writer.dart lib/src/syncbase/
+
+.PHONY: unmock
+unmock:
+ mv lib/src/syncbase/log_writer.dart.backup lib/src/syncbase/log_writer.dart
.PHONY: install
install: packages
- ./packages/sky/sky_tool start --install
+ ./packages/sky/sky_tool start --install --checked
.PHONY: env-check
env-check:
@@ -81,12 +90,15 @@
start-with-mojo: env-check packages
$(MOJO_DIR)/src/mojo/devtools/common/mojo_run --config-file $(PWD)/mojoconfig $(MOJO_SHELL_FLAGS) $(MOJO_ANDROID_FLAGS) 'mojo:window_manager https://croupier.v.io/lib/main.dart'
-# Could use `pub run test` too, but I like seeing every assertion print out.
# TODO(alexfandrianto): I split off the syncbase logic from game.dart because it
# would not run in a stand-alone VM. We will need to add mojo_test eventually.
.PHONY: test
test: packages
- dart --checked $(DART_TEST_FILES)
+ # Protect src/syncbase/log_writer.dart
+ mv lib/src/syncbase/log_writer.dart lib/src/syncbase/log_writer.dart.backup
+ cp lib/src/mocks/log_writer.dart lib/src/syncbase/
+ pub run test -r expanded $(DART_TEST_FILES) || (mv lib/src/syncbase/log_writer.dart.backup lib/src/syncbase/log_writer.dart && exit 1)
+ mv lib/src/syncbase/log_writer.dart.backup lib/src/syncbase/log_writer.dart
.PHONY: clean
clean:
diff --git a/lib/components/card_collection.dart b/lib/components/card_collection.dart
index 3ad62f8..f1c5d21 100644
--- a/lib/components/card_collection.dart
+++ b/lib/components/card_collection.dart
@@ -1,37 +1,51 @@
import '../logic/card.dart' as logic_card;
-import 'card.dart' show Card;
+import 'card.dart' as component_card;
import 'draggable.dart' show Draggable;
-import 'package:sky/widgets/basic.dart';
-import 'package:sky/widgets.dart' show DragTarget;
+import 'package:sky/widgets.dart';
import 'package:sky/theme/colors.dart' as colors;
enum Orientation { vert, horz, fan, show1 }
+enum DropType { none, card, card_collection } // I can see that both would be nice, but I'm not sure how to do that yet.
class CardCollectionComponent extends StatefulComponent {
List<logic_card.Card> cards;
Orientation orientation;
bool faceUp;
Function parentCallback;
+ bool dragChildren;
+ DropType acceptType;
String status = 'bar';
CardCollectionComponent(
- this.cards, this.faceUp, this.orientation, this.parentCallback);
+ this.cards, this.faceUp, this.orientation, this.parentCallback,
+ {this.dragChildren: false, this.acceptType: DropType.none});
void syncConstructorArguments(CardCollectionComponent other) {
cards = other.cards;
orientation = other.orientation;
faceUp = other.faceUp;
parentCallback = other.parentCallback;
+ dragChildren = other.dragChildren;
+ acceptType = other.acceptType;
}
- void _handleAccept(Card data) {
+ void _handleAccept(component_card.Card data) {
+ print('accept');
setState(() {
status = 'ACCEPT ${data.card.toString()}';
parentCallback(data.card, this.cards);
});
}
+ void _handleAcceptMultiple(CardCollectionComponent data) {
+ print('acceptMulti');
+ setState(() {
+ status = 'ACCEPT multi: ${data.cards.toString()}';
+ parentCallback(data.cards, this.cards);
+ });
+ }
+
List<Widget> flexCards(List<Widget> cardWidgets) {
List<Widget> flexWidgets = new List<Widget>();
cardWidgets.forEach(
@@ -58,31 +72,77 @@
}
Widget build() {
+ Widget w = new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: colors.Green[500], borderRadius: 5.0),
+ child:_buildHearts()
+ );
+ return w;
+ }
+
+ Widget _buildHearts() {
List<Widget> cardComponents = new List<Widget>();
cardComponents.add(new Text(status));
for (int i = 0; i < cards.length; i++) {
- cardComponents
- .add(new Draggable<Card>(new Card(cards[i], faceUp))); // flex
+ component_card.Card c = new component_card.Card(cards[i], faceUp);
+
+ if (dragChildren) {
+ cardComponents.add(new Draggable<component_card.Card>(c));
+ } else {
+ cardComponents.add(c);
+ }
}
// Let's draw a stack of cards with DragTargets.
// TODO(alexfandrianto): In many cases, card collections shouldn't have draggable cards.
// Additionally, it may be worthwhile to restrict it to 1 at a time.
- return new DragTarget<Card>(
- onAccept: _handleAccept, builder: (List<Card> data, _) {
- print(this.cards.length);
- print(data);
- return new Container(
- decoration: new BoxDecoration(
- border: new Border.all(
- width: 3.0,
- color: data.isEmpty ? colors.white : colors.Blue[500]),
- backgroundColor: data.isEmpty
- ? colors.Grey[500]
- : colors.Green[500]),
- height: 80.0,
- margin: new EdgeDims.all(10.0),
- child: wrapCards(cardComponents));
- });
+ switch(this.acceptType) {
+ case DropType.none:
+ return new Container(
+ decoration: new BoxDecoration(
+ border: new Border.all(
+ width: 3.0,
+ color: colors.white),
+ backgroundColor: colors.Grey[500]),
+ height: 80.0,
+ margin: new EdgeDims.all(10.0),
+ child: wrapCards(cardComponents)
+ );
+ case DropType.card:
+ return new DragTarget<component_card.Card>(
+ onAccept: _handleAccept, builder: (List<component_card.Card> data, _) {
+ print(this.cards.length);
+ print(data);
+ return new Container(
+ decoration: new BoxDecoration(
+ border: new Border.all(
+ width: 3.0,
+ color: data.isEmpty ? colors.white : colors.Blue[500]),
+ backgroundColor: data.isEmpty
+ ? colors.Grey[500]
+ : colors.Green[500]),
+ height: 80.0,
+ margin: new EdgeDims.all(10.0),
+ child: wrapCards(cardComponents));
+ });
+ case DropType.card_collection:
+ return new DragTarget<CardCollectionComponent>(
+ onAccept: _handleAcceptMultiple, builder: (List<CardCollectionComponent> data, _) {
+ print('CC ${this.cards.length}');
+ print(data);
+ return new Container(
+ decoration: new BoxDecoration(
+ border: new Border.all(
+ width: 3.0,
+ color: data.isEmpty ? colors.white : colors.Blue[500]),
+ backgroundColor: data.isEmpty
+ ? colors.Grey[500]
+ : colors.Green[500]),
+ height: 80.0,
+ margin: new EdgeDims.all(10.0),
+ child: wrapCards(cardComponents));
+ });
+ }
+
}
}
diff --git a/lib/components/croupier.dart b/lib/components/croupier.dart
index cf46d45..02c4f6d 100644
--- a/lib/components/croupier.dart
+++ b/lib/components/croupier.dart
@@ -1,9 +1,8 @@
import '../logic/croupier.dart' as logic_croupier;
import '../logic/game.dart' as logic_game;
-import 'game.dart' show GameComponent;
+import 'game.dart' show createGameComponent;
-import 'package:sky/widgets.dart' show FlatButton;
-import 'package:sky/widgets/basic.dart';
+import 'package:sky/widgets.dart';
import 'dart:sky' as sky;
@@ -69,8 +68,7 @@
case logic_croupier.CroupierState.PlayGame:
return new Container(
padding: new EdgeDims.only(top: sky.view.paddingTop),
- child: new GameComponent(
- croupier.game) // Asks the game UI to draw itself.
+ child: createGameComponent(croupier.game) // Asks the game UI to draw itself.
);
default:
assert(false);
diff --git a/lib/components/draggable.dart b/lib/components/draggable.dart
index 55406fb..a703620 100644
--- a/lib/components/draggable.dart
+++ b/lib/components/draggable.dart
@@ -22,11 +22,12 @@
onPointerUp: _drop,
child: new widgets.Transform(
transform: new vector_math.Matrix4.identity().translate(
- displacement.dx, displacement.dy),
+ 0.0,0.0),//displacement.dx, displacement.dy),
child: child));
}
widgets.EventDisposition _startDrag(sky.PointerEvent event) {
+ print("Drag Start");
setState(() {
dragController = new widgets.DragController(this.child);
dragController.update(new widgets.Point(event.x, event.y));
@@ -44,6 +45,7 @@
}
widgets.EventDisposition _cancelDrag(sky.PointerEvent event) {
+ print("Drag Cancel");
setState(() {
dragController.cancel();
dragController = null;
@@ -52,6 +54,7 @@
}
widgets.EventDisposition _drop(sky.PointerEvent event) {
+ print("Drag Drop");
setState(() {
dragController.update(new widgets.Point(event.x, event.y));
dragController.drop();
diff --git a/lib/components/game.dart b/lib/components/game.dart
index 94c5bca..4a36501 100644
--- a/lib/components/game.dart
+++ b/lib/components/game.dart
@@ -1,17 +1,16 @@
-import '../logic/card.dart' show Card;
+import '../logic/card.dart' as logic_card;
import '../logic/game.dart'
show Game, GameType, Viewer, HeartsGame, HeartsPhase;
-import '../logic/syncbase_echo_impl.dart' show SyncbaseEchoImpl;
-import 'board.dart' show Board;
-import 'card_collection.dart' show CardCollectionComponent, Orientation;
+import '../src/syncbase/syncbase_echo_impl.dart' show SyncbaseEchoImpl;
+//import 'board.dart' show Board;
+import 'card_collection.dart' show CardCollectionComponent, DropType, Orientation;
+import 'draggable.dart' show Draggable;
-import 'package:sky/widgets/basic.dart';
-import 'package:sky/widgets.dart' show FlatButton, RaisedButton;
+import 'package:sky/widgets.dart';
import 'package:sky/theme/colors.dart' as colors;
-class GameComponent extends StatefulComponent {
+abstract class GameComponent extends StatefulComponent {
Game game;
- SyncbaseEchoImpl s;
GameComponent(this.game) {
game.updateCallback = update;
@@ -25,33 +24,58 @@
this.game = other.game;
}
+ Widget _makeButton(String text, Function callback) {
+ return new FlatButton(child: new Text(text), onPressed: callback);
+ }
+
+ Widget build();
+}
+
+GameComponent createGameComponent(Game game) {
+ switch(game.gameType) {
+ case GameType.Proto:
+ return new ProtoGameComponent(game);
+ case GameType.Hearts:
+ return new HeartsGameComponent(game);
+ case GameType.SyncbaseEcho:
+ return new SyncbaseEchoGameComponent(game);
+ default:
+ // We're probably not ready to serve the other games yet.
+ assert(false);
+ return null;
+ }
+}
+
+
+class ProtoGameComponent extends GameComponent {
+ ProtoGameComponent(Game game) : super(game);
+
Widget build() {
- switch (game.gameType) {
- case GameType.Proto:
- return buildProto();
- case GameType.Hearts:
- return buildHearts();
- case GameType.SyncbaseEcho:
- if (s == null) {
- s = new SyncbaseEchoImpl(game);
- }
- return buildSyncbaseEcho();
- case GameType.Board:
- // Does NOT work in checked mode since it has a Stack of Positioned Stack with Positioned Widgets.
- // Issue and possible workaround? https://github.com/domokit/sky_engine/issues/732
- return new Board(1, [2, 3, 4], [1, 2, 3, 4]);
- default:
- return null; // unsupported
+ List<Widget> cardCollections = new List<Widget>();
+
+ cardCollections.add(new Text(game.debugString));
+
+ for (int i = 0; i < 4; i++) {
+ List<logic_card.Card> cards = game.cardCollections[i];
+ CardCollectionComponent c = new CardCollectionComponent(
+ cards, game.playerNumber == i, Orientation.horz, _makeGameMoveCallback, dragChildren: true, acceptType: DropType.card);
+ cardCollections.add(c); // flex
}
+
+ cardCollections.add(new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: colors.Green[500], borderRadius: 5.0),
+ child: new CardCollectionComponent(game.cardCollections[4], true,
+ Orientation.show1, _makeGameMoveCallback, dragChildren: true, acceptType: DropType.card)));
+
+ cardCollections.add(_makeSwitchViewButton());
+
+ return new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new Flex(cardCollections, direction: FlexDirection.vertical));
}
- _switchPlayersCallback() {
- setState(() {
- game.playerNumber = (game.playerNumber + 1) % 4;
- });
- }
-
- _updateGameCallback(Card card, List<Card> dest) {
+ void _makeGameMoveCallback(logic_card.Card card, List<logic_card.Card> dest) {
setState(() {
try {
game.move(card, dest);
@@ -62,30 +86,109 @@
});
}
- Widget buildProto() {
- List<Widget> cardCollections = new List<Widget>();
+ Widget _makeSwitchViewButton() =>
+ _makeButton('Switch View', _switchPlayersCallback);
- cardCollections.add(new Text(game.debugString));
+ void _switchPlayersCallback() {
+ setState(() {
+ game.playerNumber = (game.playerNumber + 1) % 4;
+ });
+ }
+}
- for (int i = 0; i < 4; i++) {
- List<Card> cards = game.cardCollections[i];
- CardCollectionComponent c = new CardCollectionComponent(
- cards, game.playerNumber == i, Orientation.horz, _updateGameCallback);
- cardCollections.add(c); // flex
+class SyncbaseEchoGameComponent extends GameComponent {
+ SyncbaseEchoImpl s;
+
+ SyncbaseEchoGameComponent(Game game) : super(game);
+
+ Widget build() {
+ if (s == null) {
+ s = new SyncbaseEchoImpl(game);
}
+ return buildSyncbaseEcho();
+ }
- cardCollections.add(new Container(
- decoration: new BoxDecoration(
- backgroundColor: colors.Green[500], borderRadius: 5.0),
- child: new CardCollectionComponent(game.cardCollections[4], true,
- Orientation.show1, _updateGameCallback)));
-
- cardCollections.add(new FlatButton(
- child: new Text('Switch View'), onPressed: _switchPlayersCallback));
-
+ Widget buildSyncbaseEcho() {
return new Container(
- decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
- child: new Flex(cardCollections, direction: FlexDirection.vertical));
+ decoration: const BoxDecoration(
+ backgroundColor: const Color(0xFF00ACC1)),
+ child: new Flex([
+ new RaisedButton(child: new Text('doEcho'), onPressed: s.doEcho),
+ new Text('sendMsg: ${s.sendMsg}'),
+ new Text('recvMsg: ${s.recvMsg}'),
+ new RaisedButton(child: new Text('doPutGet'), onPressed: s.doPutGet),
+ new Text('putStr: ${s.putStr}'),
+ new Text('getStr: ${s.getStr}')
+ ], direction: FlexDirection.vertical));
+ }
+}
+
+class HeartsGameComponent extends GameComponent {
+ List<logic_card.Card> passingCards = new List<logic_card.Card>();
+
+ HeartsGameComponent(Game game) : super(game);
+ Widget build() {
+ return buildHearts();
+ // Does NOT work in checked mode since it has a Stack of Positioned Stack with Positioned Widgets.
+ // Issue and possible workaround? https://github.com/domokit/sky_engine/issues/732
+ // return new Board(1, [2, 3, 4], [1, 2, 3, 4]);
+ // For GameType.Board
+ }
+
+ // 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 (dest == passingCards && !passingCards.contains(card) && passingCards.length < 3) {
+ passingCards.add(card);
+ } else if (dest != passingCards && passingCards.contains(card)) {
+ passingCards.remove(card);
+ }
+ });
+ }
+
+ // This shouldn't always be here, but for now, we have little choice.
+ void _switchPlayersCallback() {
+ setState(() {
+ game.playerNumber = (game.playerNumber + 1) % 4;
+ passingCards.clear(); // Just for sanity.
+ });
+ }
+
+ void _makeGamePassCallback(List<logic_card.Card> cards, List<logic_card.Card> dest) {
+ setState(() {
+ try {
+ HeartsGame game = this.game as HeartsGame;
+ game.passCards(cards);
+ passingCards.clear();
+ } catch (e) {
+ print("You can't do that! ${e.toString()}");
+ game.debugString = e.toString();
+ }
+ });
+ }
+
+ void _makeGameTakeCallback(List<logic_card.Card> cards, List<logic_card.Card> dest) {
+ setState(() {
+ try {
+ HeartsGame game = this.game as HeartsGame;
+ game.takeCards();
+ } catch (e) {
+ print("You can't do that! ${e.toString()}");
+ game.debugString = e.toString();
+ }
+ });
+ }
+
+ void _makeGameMoveCallback(logic_card.Card card, List<logic_card.Card> dest) {
+ setState(() {
+ try {
+ game.move(card, dest);
+ } catch (e) {
+ print("You can't do that! ${e.toString()}");
+ game.debugString = e.toString();
+ }
+ });
}
Widget _makeSwitchViewButton() =>
@@ -108,7 +211,9 @@
_makeSwitchViewButton()
], direction: FlexDirection.vertical));
case HeartsPhase.Pass:
+ return showPass();
case HeartsPhase.Take:
+ return showTake();
case HeartsPhase.Play:
case HeartsPhase.Score:
return showBoard();
@@ -118,20 +223,6 @@
}
}
- Widget buildSyncbaseEcho() {
- return new Container(
- decoration: const BoxDecoration(
- backgroundColor: const Color(0xFF00ACC1)),
- child: new Flex([
- new RaisedButton(child: new Text('doEcho'), onPressed: s.doEcho),
- new Text('sendMsg: ${s.sendMsg}'),
- new Text('recvMsg: ${s.recvMsg}'),
- new RaisedButton(child: new Text('doPutGet'), onPressed: s.doPutGet),
- new Text('putStr: ${s.putStr}'),
- new Text('getStr: ${s.getStr}')
- ], direction: FlexDirection.vertical));
- }
-
Widget showBoard() {
HeartsGame game = this.game as HeartsGame;
@@ -140,9 +231,9 @@
cardCollections.add(new Text(game.debugString));
for (int i = 0; i < 4; i++) {
- List<Card> cards = game.cardCollections[i];
+ List<logic_card.Card> cards = game.cardCollections[i];
CardCollectionComponent c = new CardCollectionComponent(
- cards, game.playerNumber == i, Orientation.horz, _updateGameCallback);
+ cards, game.playerNumber == i, Orientation.horz, _makeGameMoveCallback);
cardCollections.add(c); // flex
}
@@ -150,7 +241,7 @@
decoration: new BoxDecoration(
backgroundColor: colors.Green[500], borderRadius: 5.0),
child: new CardCollectionComponent(game.cardCollections[4], true,
- Orientation.show1, _updateGameCallback)));
+ Orientation.show1, _makeGameMoveCallback)));
cardCollections.add(new FlatButton(
child: new Text('Switch View'), onPressed: _switchPlayersCallback));
@@ -159,4 +250,64 @@
decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
child: new Flex(cardCollections, direction: FlexDirection.vertical));
}
+
+ Widget showPass() {
+ HeartsGame game = this.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 (!passingCards.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.
+
+ return new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new Flex(<Widget>[
+ new Text(game.debugString),
+ new CardCollectionComponent(passCards, true,
+ Orientation.horz, _makeGamePassCallback, acceptType: DropType.card_collection),
+ new Draggable<CardCollectionComponent>(new CardCollectionComponent(passingCards, true,
+ Orientation.horz, _uiPassCardCallback, dragChildren: !hasPassed, acceptType: DropType.card)),
+ new CardCollectionComponent(remainingCards, true,
+ Orientation.horz, _uiPassCardCallback, dragChildren: !hasPassed, acceptType: DropType.card),
+ new FlatButton(
+ child: new Text('Switch View'),
+ onPressed: _switchPlayersCallback)
+ ], direction: FlexDirection.vertical));
+ }
+
+ Widget showTake() {
+ HeartsGame game = this.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;
+
+ Widget take = new CardCollectionComponent(takeCards, true,
+ Orientation.horz, _makeGameTakeCallback);
+ if (!hasTaken) {
+ take = new Draggable<CardCollectionComponent>(take);
+ }
+
+ return new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new Flex(<Widget>[
+ new Text(game.debugString),
+ take,
+ new CardCollectionComponent(playerCards, true,
+ Orientation.horz, _makeGameTakeCallback, dragChildren: true, acceptType: DropType.card_collection),
+ new FlatButton(
+ child: new Text('Switch View'),
+ onPressed: _switchPlayersCallback)
+ ], direction: FlexDirection.vertical));
+ }
}
diff --git a/lib/logic/game.dart b/lib/logic/game.dart
index 142ec30..de9c5f3 100644
--- a/lib/logic/game.dart
+++ b/lib/logic/game.dart
@@ -1,6 +1,7 @@
import 'card.dart' show Card;
import 'dart:math' as math;
import 'syncbase_echo.dart' show SyncbaseEcho;
+import '../src/syncbase/log_writer.dart' show LogWriter;
// Note: Proto and Board are "fake" games intended to demonstrate what we can do.
// Proto is just a drag cards around "game".
@@ -15,7 +16,7 @@
final List<Card> deck = new List<Card>.from(Card.All);
final math.Random random = new math.Random();
- final GameLog gamelog = new GameLog();
+ final GameLog gamelog;
int playerNumber;
String debugString = 'hello?';
@@ -39,19 +40,19 @@
// TODO(alexfandrianto): The proper way to handle this would be to use 'parts'.
// That way, I can have all the game logic split up across multiple files and
// still access private constructors.
- Game.dummy(this.gameType) {}
+ Game.dummy(this.gameType, this.gamelog) {}
// A super constructor, don't call this unless you're a subclass.
- Game._create(this.gameType, this.playerNumber, int numCollections) {
+ Game._create(this.gameType, this.gamelog, this.playerNumber, int numCollections) {
gamelog.setGame(this);
for (int i = 0; i < numCollections; i++) {
cardCollections.add(new List<Card>());
}
}
- List<Card> deckPeek(int numCards) {
+ List<Card> deckPeek(int numCards, [int start = 0]) {
assert(deck.length >= numCards);
- List<Card> cards = new List<Card>.from(deck.take(numCards));
+ List<Card> cards = new List<Card>.from(deck.getRange(start, start + numCards));
return cards;
}
@@ -81,7 +82,7 @@
}
class ProtoGame extends Game {
- ProtoGame(int playerNumber) : super._create(GameType.Proto, playerNumber, 6) {
+ ProtoGame(int playerNumber) : super._create(GameType.Proto, new ProtoGameLog(), playerNumber, 6) {
// playerNumber would be used in a real game, but I have to ignore it for debugging.
// It would determine faceUp/faceDown status.faceDown
@@ -167,7 +168,7 @@
List<bool> ready;
HeartsGame(int playerNumber)
- : super._create(GameType.Hearts, playerNumber, 16) {
+ : super._create(GameType.Hearts, new HeartsGameLog(), playerNumber, 16) {
resetGame();
}
@@ -180,10 +181,12 @@
void dealCards() {
deck.shuffle();
- deal(PLAYER_A, 13);
- deal(PLAYER_B, 13);
- deal(PLAYER_C, 13);
- deal(PLAYER_D, 13);
+
+ // These things happen asynchronously, so we have to specify all cards now.
+ deal(PLAYER_A, this.deckPeek(13, 0));
+ deal(PLAYER_B, this.deckPeek(13, 13));
+ deal(PLAYER_C, this.deckPeek(13, 26));
+ deal(PLAYER_D, this.deckPeek(13, 39));
}
int get passTarget {
@@ -308,8 +311,8 @@
ready = <bool>[false, false, false, false];
}
- void deal(int playerId, int numCards) {
- gamelog.add(new HeartsCommand.deal(playerId, this.deckPeek(numCards)));
+ void deal(int playerId, List<Card> cards) {
+ gamelog.add(new HeartsCommand.deal(playerId, cards));
}
// Note that this will be called by the UI.
@@ -539,32 +542,139 @@
}
}
-class GameLog {
+abstract class GameLog {
Game game;
List<GameCommand> log = new List<GameCommand>();
- int position = 0;
+ List<GameCommand> pendingCommands = new List<GameCommand>(); // This list is normally empty, but may grow if multiple commands arrive.
+ bool hasFired = false;
+ //int position = 0;
void setGame(Game g) {
this.game = g;
}
- // This adds and executes the GameCommand.
void add(GameCommand gc) {
- log.add(gc);
+ pendingCommands.add(gc);
+ _tryPendingCommand();
+ }
- while (position < log.length) {
- log[position].execute(game);
- game.triggerEvents();
+ void _tryPendingCommand() {
+ if (pendingCommands.length > 0 && !hasFired) {
+ GameCommand gc = pendingCommands[0];
+ if (gc.canExecute(game)) {
+ hasFired = true;
+ addToLogCb(log, gc);
+ } else {
+ // What can we do if the first command isn't allowed to fire?
+ throw new StateError("Cannot run ${gc.data}");
+ }
+ }
+ }
+
+ void update(List<GameCommand> otherLog) {
+ int numMatches = 0;
+ while (numMatches < log.length && numMatches < otherLog.length && log[numMatches] == otherLog[numMatches]) {
+ numMatches++;
+ }
+
+ // At this point, i is at the farthest point of common-ness.
+ // If i matches the log length, then take the rest of the other log.
+ if (numMatches == log.length) {
+ for (int j = numMatches; j < otherLog.length; j++) {
+ log.add(otherLog[j]);
+ if (pendingCommands[0] == otherLog[j]) {
+ pendingCommands.removeAt(0);
+ hasFired = false;
+ }
+ log[j].execute(game);
+ game.triggerEvents();
+ }
if (game.updateCallback != null) {
game.updateCallback();
}
- position++;
+ } else if (numMatches == otherLog.length) {
+ // We seem to have done more valid moves, so we can just ignore the other side.
+ // TODO(alexfandrianto): If we play a game with actual 'undo' moves,
+ // do we want to record them or erase history?
+ print('Ignoring shorter log');
+ } else {
+ // This case is weird, we have some amount of common log and some mismatch.
+ // Ask the game itself what to do.
+ print('Oh no! A conflict!');
+ log = updateLogCb(log, otherLog, numMatches);
+ assert(false); // What we need to do here is to undo the moves that didn't match and then replay the new ones.
+ // TODO(alexfandrianto): At worst, we can also just reset the game and play through all of it. (No UI updates till the end).
}
+
+ // Now that we got an update, let's try our other pending commands.
+ _tryPendingCommand();
+ }
+
+ // UNIMPLEMENTED: Let subclasses override this.
+ void addToLogCb(List<GameCommand> log, GameCommand newCommand);
+ List<GameCommand> updateLogCb(List<GameCommand> current, List<GameCommand> other, int mismatchIndex);
+}
+
+class HeartsGameLog extends GameLog {
+ LogWriter logWriter;
+
+ HeartsGameLog() {
+ logWriter = new LogWriter(handleSyncUpdate);
+ }
+
+ Map<String, String> _toLogData(List<GameCommand> log, GameCommand newCommand) {
+ Map<String, String> data = new Map<String, String>();
+ for (int i = 0; i < log.length; i++) {
+ data["${i}"] = log[i].data;
+ }
+ data["${log.length}"] = newCommand.data;
+ return data;
+ }
+ List<HeartsCommand> _logFromData(Map<String, String> data) {
+ List<HeartsCommand> otherlog = new List<HeartsCommand>();
+ otherlog.length = data.length;
+ data.forEach((String k, String v) {
+ otherlog[int.parse(k)] = new HeartsCommand(v);
+ });
+ return otherlog;
+ }
+
+ void handleSyncUpdate(Map<String, String> data) {
+ this.update(_logFromData(data));
+ }
+
+ void addToLogCb(List<GameCommand> log, GameCommand newCommand) {
+ logWriter.write(_toLogData(log, newCommand));
+ }
+ List<GameCommand> updateLogCb(List<GameCommand> current, List<GameCommand> other, int mismatchIndex) {
+ assert(false); // TODO(alexfandrianto): How do you handle conflicts with Hearts?
+ return current;
+ }
+}
+
+class ProtoGameLog extends GameLog {
+ void addToLogCb(List<GameCommand> log, GameCommand newCommand) {
+ update(new List<GameCommand>.from(log)..add(newCommand));
+ }
+ List<GameCommand> updateLogCb(List<GameCommand> current, List<GameCommand> other, int mismatchIndex) {
+ assert(false); // This game can't have conflicts.
+ return current;
}
}
abstract class GameCommand {
+ bool canExecute(Game game);
void execute(Game game);
+ String get data;
+ bool operator ==(Object other) {
+ if (other is GameCommand) {
+ return this.data == other.data;
+ }
+ return false;
+ }
+ String toString() {
+ return data;
+ }
}
class HeartsCommand extends GameCommand {
@@ -611,6 +721,10 @@
return "Ready:${playerId}:END";
}
+ bool canExecute(Game g) {
+ return true; // TODO(alexfandrianto): not really. Should do validation too.
+ }
+
void execute(Game g) {
HeartsGame game = g as HeartsGame;
@@ -754,6 +868,10 @@
return "Play:${playerId}:${c.toString()}:END";
}
+ bool canExecute(Game game) {
+ return true;
+ }
+
void execute(Game game) {
print("ProtoCommand is executing: ${data}");
List<String> parts = data.split(":");
diff --git a/lib/logic/syncbase_echo.dart b/lib/logic/syncbase_echo.dart
index 2befa0b..5776b23 100644
--- a/lib/logic/syncbase_echo.dart
+++ b/lib/logic/syncbase_echo.dart
@@ -1,5 +1,15 @@
-import 'game.dart' show Game, GameType;
+import 'game.dart' show Game, GameType, GameLog, GameCommand;
class SyncbaseEcho extends Game {
- SyncbaseEcho() : super.dummy(GameType.SyncbaseEcho);
+ SyncbaseEcho() : super.dummy(GameType.SyncbaseEcho, new SyncbaseEchoLog());
+}
+
+class SyncbaseEchoLog extends GameLog {
+ void addToLogCb(List<GameCommand> log, GameCommand newCommand) {
+ update(new List<GameCommand>.from(log)..add(newCommand));
+ }
+ List<GameCommand> updateLogCb(List<GameCommand> current, List<GameCommand> other, int mismatchIndex) {
+ assert(false); // This game can't have conflicts.
+ return current;
+ }
}
\ No newline at end of file
diff --git a/lib/src/mocks/log_writer.dart b/lib/src/mocks/log_writer.dart
new file mode 100644
index 0000000..f76ba5b
--- /dev/null
+++ b/lib/src/mocks/log_writer.dart
@@ -0,0 +1,10 @@
+class LogWriter {
+ final Function updateCallback; // Takes in Map<String, String> data
+ LogWriter(this.updateCallback);
+
+ Map<String, String> _data;
+ void write(Map<String, String> data) {
+ _data = data;
+ updateCallback(_data);
+ }
+}
\ No newline at end of file
diff --git a/lib/src/syncbase/log_writer.dart b/lib/src/syncbase/log_writer.dart
new file mode 100644
index 0000000..71c9d64
--- /dev/null
+++ b/lib/src/syncbase/log_writer.dart
@@ -0,0 +1,97 @@
+/// The goal of log writer is to generically manage game logs.
+/// Syncbase will produce values that combine to form a List<GameCommand> while
+/// the in-memory GameLog will also hold such a list.
+///
+/// Updating the GameLog from the Store/Syncbase:
+/// GameLog will update to whatever Store data says.
+/// If it merges, the game log, then it will write that information off.
+/// Case A: Store is farther along than current state.
+/// Continue.
+/// Case B: Store is somehow behind the current state.
+/// Update with the current state of the GameLog (if not sent yet).
+/// Case C: Store's log branches off from the curernt GameLog.
+/// Depending on phase, resolve the conflict differently and write the resolution.
+///
+/// Updating the Store:
+/// When a new GameCommand is received (that doesn't contradict the existing log),
+/// it is added to a list of pending changes and written to the local store.
+
+/// Since this file includes Sky/Mojo, it will need to be mocked out for unit tests.
+/// Unfortunately, imports can't be replaced, so the best thing to do is to swap out the whole file.
+
+import 'dart:async';
+import 'dart:convert' show UTF8, JSON;
+
+import 'package:sky/mojo/embedder.dart' show embedder;
+
+import 'package:ether/syncbase_client.dart'
+ show Perms, SyncbaseClient, SyncbaseTable;
+
+log(String msg) {
+ DateTime now = new DateTime.now();
+ print('$now $msg');
+}
+
+Perms emptyPerms() => new Perms()..json = '{}';
+
+class LogWriter {
+ final Function updateCallback; // Takes in Map<String, String> data
+ final SyncbaseClient _syncbaseClient;
+
+ LogWriter(this.updateCallback) :
+ _syncbaseClient = new SyncbaseClient(embedder.connectToService,
+ 'https://mojo.v.io/syncbase_server.mojo');
+
+ int seq = 0;
+ SyncbaseTable tb;
+ String sendMsg, recvMsg, putStr, getStr;
+
+ Future _doSyncbaseInit() async {
+ log('LogWriter.doSyncbaseInit');
+ if (tb != null) {
+ log('syncbase already initialized');
+ return;
+ }
+ var app = _syncbaseClient.app('app');
+ if (!(await app.exists())) {
+ await app.create(emptyPerms());
+ }
+ var db = app.noSqlDatabase('db');
+ if (!(await db.exists())) {
+ await db.create(emptyPerms());
+ }
+ var table = db.table('table');
+ if (!(await table.exists())) {
+ await table.create(emptyPerms());
+ }
+ tb = table;
+ log('syncbase is now initialized');
+
+ // TODO(alexfandrianto): I'm not sure how we setup 'watch', but we would do so here.
+ }
+
+ Future write(Map<String, String> data) async {
+ log('LogWriter.write start');
+ await _doSyncbaseInit();
+
+ var row = tb.row('key');
+ await row.put(UTF8.encode(JSON.encode(data)));
+
+ // TODO(alexfandrianto): Normally, we would watch, but since I don't know how, I will just poll here.
+ await _poll();
+ log('LogWriter.start done');
+ }
+
+ Future _poll() async {
+ log('LogWriter.poll start');
+ await _doSyncbaseInit();
+
+ // Realistically, we wouldn't write it all to a single row, but I don't think it matters right now.
+ var row = tb.row('key');
+ var getBytes = await row.get();
+
+ Map<String, String> data = JSON.decode(UTF8.decode(getBytes));
+ this.updateCallback(data);
+ log('LogWriter.poll done');
+ }
+}
diff --git a/lib/logic/syncbase_echo_impl.dart b/lib/src/syncbase/syncbase_echo_impl.dart
similarity index 97%
rename from lib/logic/syncbase_echo_impl.dart
rename to lib/src/syncbase/syncbase_echo_impl.dart
index 4343d57..2eedd71 100644
--- a/lib/logic/syncbase_echo_impl.dart
+++ b/lib/src/syncbase/syncbase_echo_impl.dart
@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:convert' show UTF8;
-import 'game.dart' show Game;
+import '../../logic/game.dart' show Game;
import 'package:sky/mojo/embedder.dart' show embedder;