Merge branch 'master' of https://vanadium.googlesource.com/roadmap.projects.croupier
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..583a08c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+/.v23
+
+# Don’t commit the following directories created by pub.
+.buildlog
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..27f166e
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,105 @@
+# This beginning section is used to setup the environment for running with mojo_shell.
+ETHER_DIR := $(V23_ROOT)/roadmap/mojo/syncbase
+CROUPIER_DIR := $(shell pwd)
+SHELL := /bin/bash -euo pipefail
+
+ifdef ANDROID
+ MOJO_ANDROID_FLAGS := --android
+
+ MOJO_BUILD_DIR := $(MOJO_DIR)/src/out/android_Debug
+ SKY_BUILD_DIR := $(SKY_DIR)/src/out/android_Debug
+ ETHER_BUILD_DIR := $(ETHER_DIR)/gen/mojo/android
+
+ SYNCBASE_DATA_DIR := /data/data/org.chromium.mojo.shell/app_home/syncbase_data
+else
+ MOJO_BUILD_DIR := $(MOJO_DIR)/src/out/Debug
+ SKY_BUILD_DIR := $(SKY_DIR)/src/out/Debug
+ ETHER_BUILD_DIR := $(ETHER_DIR)/gen/mojo/linux_amd64
+
+ SYNCBASE_DATA_DIR := /tmp/syncbase_data
+endif
+
+# NOTE(nlacasse): Running Go Mojo services requires passing the
+# --enable-multiprocess flag to mojo_shell. This is because the Go runtime is
+# very large, and can interfere with C++ memory if they are in the same
+# process.
+MOJO_SHELL_FLAGS := -v --enable-multiprocess \
+ --config-alias MOJO_BUILD_DIR=$(MOJO_BUILD_DIR) \
+ --config-alias SKY_DIR=$(SKY_DIR) \
+ --config-alias SKY_BUILD_DIR=$(SKY_BUILD_DIR) \
+ --config-alias ETHER_DIR=$(ETHER_DIR) \
+ --config-alias ETHER_BUILD_DIR=$(ETHER_BUILD_DIR) \
+ --config-alias CROUPIER_DIR=$(CROUPIER_DIR)
+
+
+.DELETE_ON_ERROR:
+
+# Get the packages used by the dart project, according to pubspec.yaml
+# Can also use `pub get`, but Sublime occasionally reverts me to an ealier version.
+# Only `pub upgrade` can escape such a thing.
+packages: pubspec.yaml
+ pub upgrade
+
+DART_LIB_FILES := $(shell find lib -name *.dart ! -name *.part.dart)
+DART_TEST_FILES := $(shell find test -name *.dart ! -name *.part.dart)
+
+.PHONY: dartfmt
+dartfmt:
+ dartfmt -w $(DART_LIB_FILES) $(DART_TEST_FILES)
+
+.PHONY: lint
+lint:
+ 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 --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 --checked
+
+.PHONY: env-check
+env-check:
+ifndef MOJO_DIR
+ $(error MOJO_DIR is not set)
+endif
+ifndef SKY_DIR
+ $(error SKY_DIR is not set)
+endif
+ifndef V23_ROOT
+ $(error V23_ROOT is not set)
+endif
+ifeq ($(wildcard $(MOJO_BUILD_DIR)),)
+ $(error ERROR: $(MOJO_BUILD_DIR) does not exist. Please see README.md for instructions on compiling Mojo resources.)
+endif
+
+# Run the Sky program with mojo shell. This allows use of Syncbase and Mojo.
+# If syncbase doesn't load, it could be that port 4002 is still in use; try fuser 4002/tcp.
+.PHONY: start-with-mojo
+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'
+
+# 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
+ # 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:
+ rm -rf packages
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b01c72b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+This is the basic Getting Started with Sky + some Widgets.
+
+To run this, just do
+./packages/sky/sky_tool start
+
+when your phone is connected.
+Add --install to the end if this is the first run.
+
+You may also want to debug any problems. Use dartanalyzer.
+dartanalyzer lib/main.dart
\ No newline at end of file
diff --git a/lib/classic/down/c1.png b/lib/classic/down/c1.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c1.png
Binary files differ
diff --git a/lib/classic/down/c10.png b/lib/classic/down/c10.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c10.png
Binary files differ
diff --git a/lib/classic/down/c2.png b/lib/classic/down/c2.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c2.png
Binary files differ
diff --git a/lib/classic/down/c3.png b/lib/classic/down/c3.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c3.png
Binary files differ
diff --git a/lib/classic/down/c4.png b/lib/classic/down/c4.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c4.png
Binary files differ
diff --git a/lib/classic/down/c5.png b/lib/classic/down/c5.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c5.png
Binary files differ
diff --git a/lib/classic/down/c6.png b/lib/classic/down/c6.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c6.png
Binary files differ
diff --git a/lib/classic/down/c7.png b/lib/classic/down/c7.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c7.png
Binary files differ
diff --git a/lib/classic/down/c8.png b/lib/classic/down/c8.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c8.png
Binary files differ
diff --git a/lib/classic/down/c9.png b/lib/classic/down/c9.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/c9.png
Binary files differ
diff --git a/lib/classic/down/cj.png b/lib/classic/down/cj.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/cj.png
Binary files differ
diff --git a/lib/classic/down/ck.png b/lib/classic/down/ck.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/ck.png
Binary files differ
diff --git a/lib/classic/down/cq.png b/lib/classic/down/cq.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/cq.png
Binary files differ
diff --git a/lib/classic/down/d1.png b/lib/classic/down/d1.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d1.png
Binary files differ
diff --git a/lib/classic/down/d10.png b/lib/classic/down/d10.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d10.png
Binary files differ
diff --git a/lib/classic/down/d2.png b/lib/classic/down/d2.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d2.png
Binary files differ
diff --git a/lib/classic/down/d3.png b/lib/classic/down/d3.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d3.png
Binary files differ
diff --git a/lib/classic/down/d4.png b/lib/classic/down/d4.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d4.png
Binary files differ
diff --git a/lib/classic/down/d5.png b/lib/classic/down/d5.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d5.png
Binary files differ
diff --git a/lib/classic/down/d6.png b/lib/classic/down/d6.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d6.png
Binary files differ
diff --git a/lib/classic/down/d7.png b/lib/classic/down/d7.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d7.png
Binary files differ
diff --git a/lib/classic/down/d8.png b/lib/classic/down/d8.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d8.png
Binary files differ
diff --git a/lib/classic/down/d9.png b/lib/classic/down/d9.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/d9.png
Binary files differ
diff --git a/lib/classic/down/dj.png b/lib/classic/down/dj.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/dj.png
Binary files differ
diff --git a/lib/classic/down/dk.png b/lib/classic/down/dk.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/dk.png
Binary files differ
diff --git a/lib/classic/down/dq.png b/lib/classic/down/dq.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/dq.png
Binary files differ
diff --git a/lib/classic/down/h1.png b/lib/classic/down/h1.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h1.png
Binary files differ
diff --git a/lib/classic/down/h10.png b/lib/classic/down/h10.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h10.png
Binary files differ
diff --git a/lib/classic/down/h2.png b/lib/classic/down/h2.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h2.png
Binary files differ
diff --git a/lib/classic/down/h3.png b/lib/classic/down/h3.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h3.png
Binary files differ
diff --git a/lib/classic/down/h4.png b/lib/classic/down/h4.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h4.png
Binary files differ
diff --git a/lib/classic/down/h5.png b/lib/classic/down/h5.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h5.png
Binary files differ
diff --git a/lib/classic/down/h6.png b/lib/classic/down/h6.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h6.png
Binary files differ
diff --git a/lib/classic/down/h7.png b/lib/classic/down/h7.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h7.png
Binary files differ
diff --git a/lib/classic/down/h8.png b/lib/classic/down/h8.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h8.png
Binary files differ
diff --git a/lib/classic/down/h9.png b/lib/classic/down/h9.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/h9.png
Binary files differ
diff --git a/lib/classic/down/hj.png b/lib/classic/down/hj.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/hj.png
Binary files differ
diff --git a/lib/classic/down/hk.png b/lib/classic/down/hk.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/hk.png
Binary files differ
diff --git a/lib/classic/down/hq.png b/lib/classic/down/hq.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/hq.png
Binary files differ
diff --git a/lib/classic/down/s1.png b/lib/classic/down/s1.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s1.png
Binary files differ
diff --git a/lib/classic/down/s10.png b/lib/classic/down/s10.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s10.png
Binary files differ
diff --git a/lib/classic/down/s2.png b/lib/classic/down/s2.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s2.png
Binary files differ
diff --git a/lib/classic/down/s3.png b/lib/classic/down/s3.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s3.png
Binary files differ
diff --git a/lib/classic/down/s4.png b/lib/classic/down/s4.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s4.png
Binary files differ
diff --git a/lib/classic/down/s5.png b/lib/classic/down/s5.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s5.png
Binary files differ
diff --git a/lib/classic/down/s6.png b/lib/classic/down/s6.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s6.png
Binary files differ
diff --git a/lib/classic/down/s7.png b/lib/classic/down/s7.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s7.png
Binary files differ
diff --git a/lib/classic/down/s8.png b/lib/classic/down/s8.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s8.png
Binary files differ
diff --git a/lib/classic/down/s9.png b/lib/classic/down/s9.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/s9.png
Binary files differ
diff --git a/lib/classic/down/sj.png b/lib/classic/down/sj.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/sj.png
Binary files differ
diff --git a/lib/classic/down/sk.png b/lib/classic/down/sk.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/sk.png
Binary files differ
diff --git a/lib/classic/down/sq.png b/lib/classic/down/sq.png
new file mode 100644
index 0000000..f4ef2b9
--- /dev/null
+++ b/lib/classic/down/sq.png
Binary files differ
diff --git a/lib/classic/up/c1.png b/lib/classic/up/c1.png
new file mode 100644
index 0000000..5f90e5a
--- /dev/null
+++ b/lib/classic/up/c1.png
Binary files differ
diff --git a/lib/classic/up/c10.png b/lib/classic/up/c10.png
new file mode 100644
index 0000000..7768ba3
--- /dev/null
+++ b/lib/classic/up/c10.png
Binary files differ
diff --git a/lib/classic/up/c2.png b/lib/classic/up/c2.png
new file mode 100644
index 0000000..fd60560
--- /dev/null
+++ b/lib/classic/up/c2.png
Binary files differ
diff --git a/lib/classic/up/c3.png b/lib/classic/up/c3.png
new file mode 100644
index 0000000..853f0b8
--- /dev/null
+++ b/lib/classic/up/c3.png
Binary files differ
diff --git a/lib/classic/up/c4.png b/lib/classic/up/c4.png
new file mode 100644
index 0000000..ce8ea7a
--- /dev/null
+++ b/lib/classic/up/c4.png
Binary files differ
diff --git a/lib/classic/up/c5.png b/lib/classic/up/c5.png
new file mode 100644
index 0000000..328b4b3
--- /dev/null
+++ b/lib/classic/up/c5.png
Binary files differ
diff --git a/lib/classic/up/c6.png b/lib/classic/up/c6.png
new file mode 100644
index 0000000..3bc97f4
--- /dev/null
+++ b/lib/classic/up/c6.png
Binary files differ
diff --git a/lib/classic/up/c7.png b/lib/classic/up/c7.png
new file mode 100644
index 0000000..de94e1e
--- /dev/null
+++ b/lib/classic/up/c7.png
Binary files differ
diff --git a/lib/classic/up/c8.png b/lib/classic/up/c8.png
new file mode 100644
index 0000000..5caff1c
--- /dev/null
+++ b/lib/classic/up/c8.png
Binary files differ
diff --git a/lib/classic/up/c9.png b/lib/classic/up/c9.png
new file mode 100644
index 0000000..0419781
--- /dev/null
+++ b/lib/classic/up/c9.png
Binary files differ
diff --git a/lib/classic/up/cj.png b/lib/classic/up/cj.png
new file mode 100644
index 0000000..fe2bc6b
--- /dev/null
+++ b/lib/classic/up/cj.png
Binary files differ
diff --git a/lib/classic/up/ck.png b/lib/classic/up/ck.png
new file mode 100644
index 0000000..7e85b06
--- /dev/null
+++ b/lib/classic/up/ck.png
Binary files differ
diff --git a/lib/classic/up/cq.png b/lib/classic/up/cq.png
new file mode 100644
index 0000000..24e9efb
--- /dev/null
+++ b/lib/classic/up/cq.png
Binary files differ
diff --git a/lib/classic/up/d1.png b/lib/classic/up/d1.png
new file mode 100644
index 0000000..5169ec4
--- /dev/null
+++ b/lib/classic/up/d1.png
Binary files differ
diff --git a/lib/classic/up/d10.png b/lib/classic/up/d10.png
new file mode 100644
index 0000000..fe94ead
--- /dev/null
+++ b/lib/classic/up/d10.png
Binary files differ
diff --git a/lib/classic/up/d2.png b/lib/classic/up/d2.png
new file mode 100644
index 0000000..211a24a
--- /dev/null
+++ b/lib/classic/up/d2.png
Binary files differ
diff --git a/lib/classic/up/d3.png b/lib/classic/up/d3.png
new file mode 100644
index 0000000..9abcace
--- /dev/null
+++ b/lib/classic/up/d3.png
Binary files differ
diff --git a/lib/classic/up/d4.png b/lib/classic/up/d4.png
new file mode 100644
index 0000000..81ebd47
--- /dev/null
+++ b/lib/classic/up/d4.png
Binary files differ
diff --git a/lib/classic/up/d5.png b/lib/classic/up/d5.png
new file mode 100644
index 0000000..e9505ea
--- /dev/null
+++ b/lib/classic/up/d5.png
Binary files differ
diff --git a/lib/classic/up/d6.png b/lib/classic/up/d6.png
new file mode 100644
index 0000000..eaa1f6e
--- /dev/null
+++ b/lib/classic/up/d6.png
Binary files differ
diff --git a/lib/classic/up/d7.png b/lib/classic/up/d7.png
new file mode 100644
index 0000000..dc0b272
--- /dev/null
+++ b/lib/classic/up/d7.png
Binary files differ
diff --git a/lib/classic/up/d8.png b/lib/classic/up/d8.png
new file mode 100644
index 0000000..c736dc3
--- /dev/null
+++ b/lib/classic/up/d8.png
Binary files differ
diff --git a/lib/classic/up/d9.png b/lib/classic/up/d9.png
new file mode 100644
index 0000000..4f6d187
--- /dev/null
+++ b/lib/classic/up/d9.png
Binary files differ
diff --git a/lib/classic/up/dj.png b/lib/classic/up/dj.png
new file mode 100644
index 0000000..3fce1d7
--- /dev/null
+++ b/lib/classic/up/dj.png
Binary files differ
diff --git a/lib/classic/up/dk.png b/lib/classic/up/dk.png
new file mode 100644
index 0000000..307cd29
--- /dev/null
+++ b/lib/classic/up/dk.png
Binary files differ
diff --git a/lib/classic/up/dq.png b/lib/classic/up/dq.png
new file mode 100644
index 0000000..5e26a9b
--- /dev/null
+++ b/lib/classic/up/dq.png
Binary files differ
diff --git a/lib/classic/up/ec.png b/lib/classic/up/ec.png
new file mode 100644
index 0000000..d91730c
--- /dev/null
+++ b/lib/classic/up/ec.png
Binary files differ
diff --git a/lib/classic/up/h1.png b/lib/classic/up/h1.png
new file mode 100644
index 0000000..53681ca
--- /dev/null
+++ b/lib/classic/up/h1.png
Binary files differ
diff --git a/lib/classic/up/h10.png b/lib/classic/up/h10.png
new file mode 100644
index 0000000..b49eb67
--- /dev/null
+++ b/lib/classic/up/h10.png
Binary files differ
diff --git a/lib/classic/up/h2.png b/lib/classic/up/h2.png
new file mode 100644
index 0000000..ad333a0
--- /dev/null
+++ b/lib/classic/up/h2.png
Binary files differ
diff --git a/lib/classic/up/h3.png b/lib/classic/up/h3.png
new file mode 100644
index 0000000..9f5c26c
--- /dev/null
+++ b/lib/classic/up/h3.png
Binary files differ
diff --git a/lib/classic/up/h4.png b/lib/classic/up/h4.png
new file mode 100644
index 0000000..66f3e57
--- /dev/null
+++ b/lib/classic/up/h4.png
Binary files differ
diff --git a/lib/classic/up/h5.png b/lib/classic/up/h5.png
new file mode 100644
index 0000000..4f9408c
--- /dev/null
+++ b/lib/classic/up/h5.png
Binary files differ
diff --git a/lib/classic/up/h6.png b/lib/classic/up/h6.png
new file mode 100644
index 0000000..df43bc8
--- /dev/null
+++ b/lib/classic/up/h6.png
Binary files differ
diff --git a/lib/classic/up/h7.png b/lib/classic/up/h7.png
new file mode 100644
index 0000000..4d9026b
--- /dev/null
+++ b/lib/classic/up/h7.png
Binary files differ
diff --git a/lib/classic/up/h8.png b/lib/classic/up/h8.png
new file mode 100644
index 0000000..16cd43f
--- /dev/null
+++ b/lib/classic/up/h8.png
Binary files differ
diff --git a/lib/classic/up/h9.png b/lib/classic/up/h9.png
new file mode 100644
index 0000000..4a03d61
--- /dev/null
+++ b/lib/classic/up/h9.png
Binary files differ
diff --git a/lib/classic/up/hj.png b/lib/classic/up/hj.png
new file mode 100644
index 0000000..84a446f
--- /dev/null
+++ b/lib/classic/up/hj.png
Binary files differ
diff --git a/lib/classic/up/hk.png b/lib/classic/up/hk.png
new file mode 100644
index 0000000..2d47a44
--- /dev/null
+++ b/lib/classic/up/hk.png
Binary files differ
diff --git a/lib/classic/up/hq.png b/lib/classic/up/hq.png
new file mode 100644
index 0000000..02f7fef
--- /dev/null
+++ b/lib/classic/up/hq.png
Binary files differ
diff --git a/lib/classic/up/index.html b/lib/classic/up/index.html
new file mode 100644
index 0000000..5144375
--- /dev/null
+++ b/lib/classic/up/index.html
@@ -0,0 +1,108 @@
+<HTML>
+<HEAD>
+<META NAME="description" CONTENT="A full deck of Playing Card Icons">
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=ISO-8859-1">
+<META NAME="keywords" CONTENT="Playing Cards, deck of cards, deck, cards, icons, images">
+<TITLE>Playing Cards</TITLE>
+</HEAD>
+<BODY BGCOLOR=#006633 TEXT=#e1ffd7 LINK=#FFFFFF VLINK=#FFFFFF>
+<CENTER>
+<TABLE BORDER=0 CELLSPACING=10 CELLPADDING=10>
+<TR>
+<TD>
+<IMG SRC="b1pt.png"><BR>
+<IMG SRC="b1fh.png"><BR>
+<IMG SRC="b1pb.png"><BR>
+</TD>
+<TD>
+<IMG SRC="b1pl.png">
+<IMG SRC="b1fv.png">
+<IMG SRC="b1pr.png">
+</TD>
+<TD><IMG SRC="jb.png"></TD>
+<TD VALIGN=CENTER><STRONG>Playing Cards</STRONG></TD>
+<TD><IMG SRC="jr.png"></TD>
+<TD>
+<IMG SRC="b2pl.png">
+<IMG SRC="b2fv.png">
+<IMG SRC="b2pr.png">
+</TD>
+<TD>
+<IMG SRC="b2pt.png"><BR>
+<IMG SRC="b2fh.png"><BR>
+<IMG SRC="b2pb.png"><BR>
+</TD>
+</TR>
+<TR></TR>
+</TABLE>
+<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=0>
+<TR>
+<TD ALIGN=CENTER><IMG SRC="c1.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c2.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c3.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c4.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c5.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c6.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c7.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c8.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c9.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="c10.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="cj.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="cq.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="ck.png"></TD>
+</TR>
+<TR>
+<TD ALIGN=CENTER><IMG SRC="h1.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h2.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h3.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h4.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h5.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h6.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h7.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h8.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h9.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="h10.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="hj.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="hq.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="hk.png"></TD>
+</TR>
+<TR>
+<TD ALIGN=CENTER><IMG SRC="s1.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s2.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s3.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s4.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s5.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s6.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s7.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s8.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s9.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="s10.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="sj.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="sq.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="sk.png"></TD>
+</TR>
+<TR>
+<TD ALIGN=CENTER><IMG SRC="d1.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d2.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d3.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d4.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d5.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d6.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d7.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d8.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d9.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="d10.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="dj.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="dq.png"></TD>
+<TD ALIGN=CENTER><IMG SRC="dk.png"></TD>
+</TR>
+</TABLE>
+<TABLE BORDER=0 CELLSPACING=10>
+<TR>
+<TD ALIGN=CENTER><FONT SIZE=2><BR>
+These images were created using <A HREF="http://www.mindworkshop.com/alchemy/alchemy.html">GIFCon</A>.
+</FONT></TD>
+</TR>
+</TABLE>
+</BODY>
+</HTML>
diff --git a/lib/classic/up/jb.png b/lib/classic/up/jb.png
new file mode 100644
index 0000000..0d9f8bb
--- /dev/null
+++ b/lib/classic/up/jb.png
Binary files differ
diff --git a/lib/classic/up/jr.png b/lib/classic/up/jr.png
new file mode 100644
index 0000000..5555316
--- /dev/null
+++ b/lib/classic/up/jr.png
Binary files differ
diff --git a/lib/classic/up/s1.png b/lib/classic/up/s1.png
new file mode 100644
index 0000000..573838f
--- /dev/null
+++ b/lib/classic/up/s1.png
Binary files differ
diff --git a/lib/classic/up/s10.png b/lib/classic/up/s10.png
new file mode 100644
index 0000000..48169da
--- /dev/null
+++ b/lib/classic/up/s10.png
Binary files differ
diff --git a/lib/classic/up/s2.png b/lib/classic/up/s2.png
new file mode 100644
index 0000000..43447ca
--- /dev/null
+++ b/lib/classic/up/s2.png
Binary files differ
diff --git a/lib/classic/up/s3.png b/lib/classic/up/s3.png
new file mode 100644
index 0000000..15ec14f
--- /dev/null
+++ b/lib/classic/up/s3.png
Binary files differ
diff --git a/lib/classic/up/s4.png b/lib/classic/up/s4.png
new file mode 100644
index 0000000..859c85e
--- /dev/null
+++ b/lib/classic/up/s4.png
Binary files differ
diff --git a/lib/classic/up/s5.png b/lib/classic/up/s5.png
new file mode 100644
index 0000000..a9aed79
--- /dev/null
+++ b/lib/classic/up/s5.png
Binary files differ
diff --git a/lib/classic/up/s6.png b/lib/classic/up/s6.png
new file mode 100644
index 0000000..3e23667
--- /dev/null
+++ b/lib/classic/up/s6.png
Binary files differ
diff --git a/lib/classic/up/s7.png b/lib/classic/up/s7.png
new file mode 100644
index 0000000..4108d84
--- /dev/null
+++ b/lib/classic/up/s7.png
Binary files differ
diff --git a/lib/classic/up/s8.png b/lib/classic/up/s8.png
new file mode 100644
index 0000000..d2032f0
--- /dev/null
+++ b/lib/classic/up/s8.png
Binary files differ
diff --git a/lib/classic/up/s9.png b/lib/classic/up/s9.png
new file mode 100644
index 0000000..d8a4a33
--- /dev/null
+++ b/lib/classic/up/s9.png
Binary files differ
diff --git a/lib/classic/up/sj.png b/lib/classic/up/sj.png
new file mode 100644
index 0000000..a6cdec1
--- /dev/null
+++ b/lib/classic/up/sj.png
Binary files differ
diff --git a/lib/classic/up/sk.png b/lib/classic/up/sk.png
new file mode 100644
index 0000000..38287b5
--- /dev/null
+++ b/lib/classic/up/sk.png
Binary files differ
diff --git a/lib/classic/up/sq.png b/lib/classic/up/sq.png
new file mode 100644
index 0000000..27c08c0
--- /dev/null
+++ b/lib/classic/up/sq.png
Binary files differ
diff --git a/lib/components/board.dart b/lib/components/board.dart
new file mode 100644
index 0000000..2e1ea9a
--- /dev/null
+++ b/lib/components/board.dart
@@ -0,0 +1,114 @@
+import './card.dart' show Card;
+import '../logic/card.dart' as logic_card;
+
+import 'dart:math' as math;
+
+import 'package:sky/widgets.dart' as widgets;
+import 'package:vector_math/vector_math.dart' as vector_math;
+import 'package:sky/theme/colors.dart' as colors;
+
+const double cardHeight = 96.0;
+const double cardWidth = 71.0;
+
+class CardCluster extends widgets.Component {
+ List<int> cards; // the indicies of the cards in the center, in clockwise order
+ int startingPos;
+ CardCluster(this.startingPos, this.cards);
+
+ widgets.Widget build() {
+ var widgetsList = [];
+ for (int i = 0; i < cards.length; i++) {
+ var posMod = (startingPos + i) % 4;
+ switch (posMod) {
+ case 0:
+ widgetsList.add(new widgets.Transform(
+ transform: new vector_math.Matrix4.identity()
+ .rotateZ(math.PI)
+ .translate(0.0, -cardHeight / 2),
+ child: new Card(logic_card.Card.All[cards[i]], true)));
+ break;
+ case 1:
+ widgetsList.add(new widgets.Transform(
+ transform: new vector_math.Matrix4.identity()
+ .rotateZ(math.PI / 2.0)
+ .translate(0.0, cardWidth / 2),
+ child: new Card(logic_card.Card.All[cards[i]], true)));
+ break;
+ case 2:
+ widgetsList.add(new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().translate(
+ -cardWidth, cardWidth / 2),
+ child: new Card(logic_card.Card.All[cards[i]], true)));
+ break;
+ case 3:
+ widgetsList.add(new widgets.Transform(
+ transform: new vector_math.Matrix4.identity()
+ .rotateZ(math.PI / 2.0)
+ .translate(0.0, -cardHeight / 2),
+ child: new Card(logic_card.Card.All[cards[i]], true)));
+ break;
+ }
+ }
+ return new widgets.Container(child: new widgets.Stack(widgetsList));
+ }
+}
+
+class PlayerHand extends widgets.Component {
+ int count;
+ PlayerHand(this.count);
+
+ widgets.Widget build() {
+ List<widgets.Positioned> cards = [];
+ for (int i = 0; i < count; i++) {
+ cards.add(new widgets.Positioned(
+ child: new Card(logic_card.Card.All[0], false),
+ top: 0.0,
+ left: cardWidth * i / 2.0));
+ }
+ return new widgets.Stack(cards);
+ }
+}
+
+class Board extends widgets.Component {
+ CardCluster centerCluster;
+ List<PlayerHand> hands; // counts of cards in players hands, in clockwise order
+
+ Board(int firstCardPlayedPosition, List<int> cards, List<int> playerHandCount)
+ : centerCluster = new CardCluster(firstCardPlayedPosition, cards) {
+ assert(playerHandCount.length == 4);
+ hands = new List<PlayerHand>();
+ for (int count in playerHandCount) {
+ hands.add(new PlayerHand(count));
+ }
+ }
+
+ widgets.Widget build() {
+ return new widgets.Container(
+ decoration: new widgets.BoxDecoration(
+ backgroundColor: colors.Pink[500]),
+ child: new widgets.Stack([
+ new widgets.Positioned(child: hands[0], top: 0.0, left: 250.0),
+ new widgets.Positioned(
+ child: new widgets.Transform(
+ transform: new vector_math.Matrix4.identity()
+ .rotateZ(math.PI / 2.0),
+ child: hands[1]),
+ left: 100.0,
+ top: 400.0),
+ new widgets.Positioned(
+ child: new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().rotateZ(math.PI),
+ child: hands[2]),
+ top: 820.0,
+ left: 350.0),
+ new widgets.Positioned(
+ child: new widgets.Transform(
+ transform: new vector_math.Matrix4.identity()
+ .rotateZ(math.PI / 2.0),
+ child: hands[3]),
+ left: 500.0,
+ top: 400.0),
+ new widgets.Positioned(child: centerCluster, top: 400.0, left: 300.0),
+ ]));
+ }
+}
diff --git a/lib/components/card.dart b/lib/components/card.dart
new file mode 100644
index 0000000..5f7ee4b
--- /dev/null
+++ b/lib/components/card.dart
@@ -0,0 +1,22 @@
+import '../logic/card.dart' as logic_card;
+import 'package:sky/widgets.dart' as widgets;
+
+class Card extends widgets.Component {
+ logic_card.Card card;
+ bool faceUp;
+
+ Card(this.card, this.faceUp);
+
+ widgets.Widget build() {
+ return new widgets.Listener(child: imageFromCard(card, faceUp));
+ }
+
+ static widgets.Widget imageFromCard(logic_card.Card c, bool faceUp) {
+ // TODO(alexfandrianto): If we allow an optional prefix in front of this,
+ // we would be able to have multiple skins of the same deck.
+ // TODO(alexfandrianto): Better card organization?
+ String imageName =
+ "${c.deck}/${faceUp ? 'up' : 'down'}/${c.identifier}.png";
+ return new widgets.NetworkImage(src: imageName);
+ }
+}
diff --git a/lib/components/card_collection.dart b/lib/components/card_collection.dart
new file mode 100644
index 0000000..f1c5d21
--- /dev/null
+++ b/lib/components/card_collection.dart
@@ -0,0 +1,148 @@
+import '../logic/card.dart' as logic_card;
+import 'card.dart' as component_card;
+import 'draggable.dart' show Draggable;
+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.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(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(
+ (cardWidget) => flexWidgets.add(new Flexible(child: cardWidget)));
+ return flexWidgets;
+ }
+
+ Widget wrapCards(List<Widget> cardWidgets) {
+ switch (this.orientation) {
+ case Orientation.vert:
+ return new Flex(flexCards(cardWidgets),
+ direction: FlexDirection.vertical);
+ case Orientation.horz:
+ return new Flex(flexCards(cardWidgets));
+ case Orientation.fan:
+ // unimplemented, so we'll fall through to show1, for now.
+ // Probably a Stack + Positioned
+ case Orientation.show1:
+ return new Stack(cardWidgets);
+ default:
+ assert(false);
+ return null;
+ }
+ }
+
+ 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++) {
+ 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.
+ 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
new file mode 100644
index 0000000..02c4f6d
--- /dev/null
+++ b/lib/components/croupier.dart
@@ -0,0 +1,78 @@
+import '../logic/croupier.dart' as logic_croupier;
+import '../logic/game.dart' as logic_game;
+import 'game.dart' show createGameComponent;
+
+import 'package:sky/widgets.dart';
+
+import 'dart:sky' as sky;
+
+class CroupierComponent extends StatefulComponent {
+ logic_croupier.Croupier croupier;
+
+ CroupierComponent(this.croupier) : super();
+
+ void syncConstructorArguments(CroupierComponent other) {
+ croupier = other.croupier;
+ }
+
+ Function setStateCallbackFactory(logic_croupier.CroupierState s,
+ [var data = null]) {
+ return () => setState(() {
+ croupier.setState(s, data);
+ });
+ }
+
+ Widget build() {
+ switch (croupier.state) {
+ case logic_croupier.CroupierState.Welcome:
+ // in which we show them a UI to start a new game, join a game, or change some settings.
+ return new Container(
+ padding: new EdgeDims.only(top: sky.view.paddingTop),
+ child: new Flex([
+ new FlatButton(
+ child: new Text('Create Game'),
+ onPressed: setStateCallbackFactory(
+ logic_croupier.CroupierState.ChooseGame)),
+ new FlatButton(child: new Text('Join Game')),
+ new FlatButton(child: new Text('Settings'))
+ ], direction: FlexDirection.vertical));
+ case logic_croupier.CroupierState.Settings:
+ return null; // in which we let them pick an avatar, name, and color. And return to the previous screen after (NOT IMPLEMENTED YET)
+ case logic_croupier.CroupierState.ChooseGame:
+ // in which we let them pick a game out of the many possible games... There aren't that many.
+ return new Container(
+ padding: new EdgeDims.only(top: sky.view.paddingTop),
+ child: new Flex([
+ new FlatButton(
+ child: new Text('Proto'),
+ onPressed: setStateCallbackFactory(
+ logic_croupier.CroupierState.PlayGame,
+ logic_game.GameType.Proto)),
+ new FlatButton(
+ child: new Text('Hearts'),
+ onPressed: setStateCallbackFactory(
+ logic_croupier.CroupierState.PlayGame,
+ logic_game.GameType.Hearts)),
+ new FlatButton(child: new Text('Poker')),
+ new FlatButton(child: new Text('Solitaire')),
+ new FlatButton(
+ child: new Text('Syncbase Echo'),
+ onPressed: setStateCallbackFactory(
+ logic_croupier.CroupierState.PlayGame,
+ logic_game.GameType.SyncbaseEcho))
+ ], direction: FlexDirection.vertical));
+ case logic_croupier.CroupierState.AwaitGame:
+ return null; // in which players wait for game invitations to arrive.
+ case logic_croupier.CroupierState.ArrangePlayers:
+ return null; // If needed, lists the players around and what devices they'd like to use.
+ case logic_croupier.CroupierState.PlayGame:
+ return new Container(
+ padding: new EdgeDims.only(top: sky.view.paddingTop),
+ child: createGameComponent(croupier.game) // Asks the game UI to draw itself.
+ );
+ default:
+ assert(false);
+ return null;
+ }
+ }
+}
diff --git a/lib/components/draggable.dart b/lib/components/draggable.dart
new file mode 100644
index 0000000..a703620
--- /dev/null
+++ b/lib/components/draggable.dart
@@ -0,0 +1,67 @@
+import 'dart:sky' as sky;
+
+import 'package:sky/widgets.dart' as widgets;
+import 'package:vector_math/vector_math.dart' as vector_math;
+
+class Draggable<T extends widgets.Widget> extends widgets.StatefulComponent {
+ widgets.DragController dragController;
+ widgets.Offset displacement = widgets.Offset.zero;
+ T child;
+
+ Draggable(this.child);
+
+ void syncConstructorArguments(Draggable other) {
+ child = other.child;
+ }
+
+ widgets.Widget build() {
+ return new widgets.Listener(
+ onPointerDown: _startDrag,
+ onPointerMove: _updateDrag,
+ onPointerCancel: _cancelDrag,
+ onPointerUp: _drop,
+ child: new widgets.Transform(
+ transform: new vector_math.Matrix4.identity().translate(
+ 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));
+ displacement = widgets.Offset.zero;
+ });
+ return widgets.EventDisposition.consumed;
+ }
+
+ widgets.EventDisposition _updateDrag(sky.PointerEvent event) {
+ setState(() {
+ dragController.update(new widgets.Point(event.x, event.y));
+ displacement += new widgets.Offset(event.dx, event.dy);
+ });
+ return widgets.EventDisposition.consumed;
+ }
+
+ widgets.EventDisposition _cancelDrag(sky.PointerEvent event) {
+ print("Drag Cancel");
+ setState(() {
+ dragController.cancel();
+ dragController = null;
+ });
+ return widgets.EventDisposition.consumed;
+ }
+
+ widgets.EventDisposition _drop(sky.PointerEvent event) {
+ print("Drag Drop");
+ setState(() {
+ dragController.update(new widgets.Point(event.x, event.y));
+ dragController.drop();
+ dragController = null;
+
+ displacement = widgets.Offset.zero;
+ });
+ return widgets.EventDisposition.consumed;
+ }
+}
diff --git a/lib/components/game.dart b/lib/components/game.dart
new file mode 100644
index 0000000..4a36501
--- /dev/null
+++ b/lib/components/game.dart
@@ -0,0 +1,313 @@
+import '../logic/card.dart' as logic_card;
+import '../logic/game.dart'
+ show Game, GameType, Viewer, HeartsGame, HeartsPhase;
+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.dart';
+import 'package:sky/theme/colors.dart' as colors;
+
+abstract class GameComponent extends StatefulComponent {
+ Game game;
+
+ GameComponent(this.game) {
+ game.updateCallback = update;
+ }
+
+ void update() {
+ setState(() {});
+ }
+
+ void syncConstructorArguments(GameComponent other) {
+ 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() {
+ 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));
+ }
+
+ 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() =>
+ _makeButton('Switch View', _switchPlayersCallback);
+
+ void _switchPlayersCallback() {
+ setState(() {
+ game.playerNumber = (game.playerNumber + 1) % 4;
+ });
+ }
+}
+
+class SyncbaseEchoGameComponent extends GameComponent {
+ SyncbaseEchoImpl s;
+
+ SyncbaseEchoGameComponent(Game game) : super(game);
+
+ Widget build() {
+ if (s == null) {
+ s = new SyncbaseEchoImpl(game);
+ }
+ return buildSyncbaseEcho();
+ }
+
+ 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));
+ }
+}
+
+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() =>
+ _makeButton('Switch View', _switchPlayersCallback);
+
+ Widget _makeButton(String text, Function callback) {
+ return new FlatButton(child: new Text(text), onPressed: callback);
+ }
+
+ Widget buildHearts() {
+ HeartsGame game = this.game as HeartsGame;
+
+ switch (game.phase) {
+ case HeartsPhase.Deal:
+ return new Container(
+ decoration: new BoxDecoration(backgroundColor: colors.Pink[500]),
+ child: new Flex([
+ new Text('Player ${game.playerNumber}'),
+ _makeButton('Deal', game.dealCards),
+ _makeSwitchViewButton()
+ ], direction: FlexDirection.vertical));
+ case HeartsPhase.Pass:
+ return showPass();
+ case HeartsPhase.Take:
+ return showTake();
+ case HeartsPhase.Play:
+ case HeartsPhase.Score:
+ return showBoard();
+ default:
+ assert(false);
+ return null;
+ }
+ }
+
+ Widget showBoard() {
+ HeartsGame game = this.game as HeartsGame;
+
+ 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);
+ 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)));
+
+ cardCollections.add(new FlatButton(
+ child: new Text('Switch View'), onPressed: _switchPlayersCallback));
+
+ return new Container(
+ 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/card.dart b/lib/logic/card.dart
new file mode 100644
index 0000000..4dbba8a
--- /dev/null
+++ b/lib/logic/card.dart
@@ -0,0 +1,77 @@
+import 'dart:collection';
+
+class Card {
+ final String deck;
+ final String identifier;
+
+ Card(this.deck, this.identifier);
+ Card.fromString(String cardData)
+ : deck = cardData.split(" ")[0],
+ identifier = cardData.split(" ")[1];
+
+ bool operator ==(Object other) {
+ if (other is! Card) return false;
+ Card o = other as Card;
+ return deck == o.deck && identifier == o.identifier;
+ }
+ int get hashCode => 37 * (deck.hashCode + 41 * identifier.hashCode);
+
+ static final List<Card> All = new UnmodifiableListView<Card>([
+ new Card("classic", "c1"),
+ new Card("classic", "c2"),
+ new Card("classic", "c3"),
+ new Card("classic", "c4"),
+ new Card("classic", "c5"),
+ new Card("classic", "c6"),
+ new Card("classic", "c7"),
+ new Card("classic", "c8"),
+ new Card("classic", "c9"),
+ new Card("classic", "c10"),
+ new Card("classic", "cj"),
+ new Card("classic", "cq"),
+ new Card("classic", "ck"),
+ new Card("classic", "d1"),
+ new Card("classic", "d2"),
+ new Card("classic", "d3"),
+ new Card("classic", "d4"),
+ new Card("classic", "d5"),
+ new Card("classic", "d6"),
+ new Card("classic", "d7"),
+ new Card("classic", "d8"),
+ new Card("classic", "d9"),
+ new Card("classic", "d10"),
+ new Card("classic", "dj"),
+ new Card("classic", "dq"),
+ new Card("classic", "dk"),
+ new Card("classic", "h1"),
+ new Card("classic", "h2"),
+ new Card("classic", "h3"),
+ new Card("classic", "h4"),
+ new Card("classic", "h5"),
+ new Card("classic", "h6"),
+ new Card("classic", "h7"),
+ new Card("classic", "h8"),
+ new Card("classic", "h9"),
+ new Card("classic", "h10"),
+ new Card("classic", "hj"),
+ new Card("classic", "hq"),
+ new Card("classic", "hk"),
+ new Card("classic", "s1"),
+ new Card("classic", "s2"),
+ new Card("classic", "s3"),
+ new Card("classic", "s4"),
+ new Card("classic", "s5"),
+ new Card("classic", "s6"),
+ new Card("classic", "s7"),
+ new Card("classic", "s8"),
+ new Card("classic", "s9"),
+ new Card("classic", "s10"),
+ new Card("classic", "sj"),
+ new Card("classic", "sq"),
+ new Card("classic", "sk"),
+ ]);
+
+ toString() => "${deck} ${identifier}";
+
+ get string => toString();
+}
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
new file mode 100644
index 0000000..494e29e
--- /dev/null
+++ b/lib/logic/croupier.dart
@@ -0,0 +1,70 @@
+import 'game.dart' show Game, GameType;
+
+enum CroupierState {
+ Welcome,
+ Settings,
+ ChooseGame,
+ AwaitGame,
+ ArrangePlayers,
+ PlayGame
+}
+
+class Croupier {
+ CroupierState state;
+ Settings settings;
+ Game game; // null until chosen
+
+ Croupier() {
+ state = CroupierState.Welcome;
+ // settings = new Settings.load(?); // Give it in the croupier constructor. The app itself should load this info.
+ }
+
+ // Sets the next part of croupier state.
+ // Depending on the originating state, data can contain extra information that we need.
+ void setState(CroupierState nextState, var data) {
+ switch (state) {
+ case CroupierState.Welcome:
+ // data should be empty.
+ assert(data == null);
+ break;
+ case CroupierState.Settings:
+ // data should be empty.
+ // All settings changes affect the croupier settings directly without changing app state.
+ assert(data == null);
+ break;
+ case CroupierState.ChooseGame:
+ // data should be the game id here.
+ GameType gt = data as GameType;
+ game = new Game(gt, 0); // Start as player 0 of whatever game type.
+ break;
+ case CroupierState.AwaitGame:
+ // data would probably be the game id again.
+ GameType gt = data as GameType;
+ game = new Game(gt, 0); // Start as player 0 of whatever game type.
+ break;
+ case CroupierState.ArrangePlayers:
+ // data should be empty.
+ // All rearrangements affect the Game's player number without changing app state.
+ break;
+ case CroupierState.PlayGame:
+ // data should be empty.
+ // The signal to start really isn't anything special.
+ break;
+ default:
+ assert(false);
+ }
+
+ state = nextState;
+ }
+}
+
+class Settings {
+ String avatar;
+ String name;
+ String color; // in hex?
+
+ Settings(this.avatar, this.name, this.color);
+
+ // Settings.load(String data) {}
+ // String save() { return null; }
+}
diff --git a/lib/logic/game.dart b/lib/logic/game.dart
new file mode 100644
index 0000000..de9c5f3
--- /dev/null
+++ b/lib/logic/game.dart
@@ -0,0 +1,923 @@
+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".
+// Board is meant to show how one _could_ layout a game of Hearts. This one is not hooked up very well yet.
+enum GameType { Proto, Hearts, Poker, Solitaire, Board, SyncbaseEcho }
+
+/// A game consists of multiple decks and tracks a single deck of cards.
+/// It also handles events; when cards are dragged to and from decks.
+class Game {
+ final GameType gameType;
+ final List<List<Card>> cardCollections = new List<List<Card>>();
+ final List<Card> deck = new List<Card>.from(Card.All);
+
+ final math.Random random = new math.Random();
+ final GameLog gamelog;
+ int playerNumber;
+ String debugString = 'hello?';
+
+ Function updateCallback; // Used to inform components of when a change has occurred. This is especially important when something non-UI related changes what should be drawn.
+
+ factory Game(GameType gt, int pn) {
+ switch (gt) {
+ case GameType.Proto:
+ return new ProtoGame(pn);
+ case GameType.Hearts:
+ return new HeartsGame(pn);
+ case GameType.SyncbaseEcho:
+ return new SyncbaseEcho();
+ default:
+ assert(false);
+ return null;
+ }
+ }
+
+ // A public super constructor that doesn't really do anything.
+ // 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, this.gamelog) {}
+
+ // A super constructor, don't call this unless you're a subclass.
+ 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, [int start = 0]) {
+ assert(deck.length >= numCards);
+ List<Card> cards = new List<Card>.from(deck.getRange(start, start + numCards));
+ return cards;
+ }
+
+ // Which card collection has the card?
+ int findCard(Card card) {
+ for (int i = 0; i < cardCollections.length; i++) {
+ if (cardCollections[i].contains(card)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ void resetCards() {
+ for (int i = 0; i < cardCollections.length; i++) {
+ cardCollections[i].clear();
+ }
+ deck.addAll(Card.All);
+ }
+
+ // UNIMPLEMENTED: Let subclasses override this?
+ // Or is it improper to do so?
+ void move(Card card, List<Card> dest) {}
+
+ // UNIMPLEMENTED: Override this to implement game-specific logic after each event.
+ void triggerEvents() {}
+}
+
+class ProtoGame extends Game {
+ 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
+
+ // TODO: Set the number of piles created to either 9 (1x per player, 1 discard, 4 play piles) or 12 (2x per player, 4 play piles)
+ // But for now, we will deal with 6. 1x per player, 1 discard, and 1 undrawn pile.
+
+ // We do some arbitrary things here... Just for setup.
+ deck.shuffle();
+ deal(0, 8);
+ deal(1, 5);
+ deal(2, 4);
+ deal(3, 1);
+ }
+
+ void deal(int playerId, int numCards) {
+ gamelog.add(new ProtoCommand.deal(playerId, this.deckPeek(numCards)));
+ }
+
+ // Overrides Game's move method with the "move" logic for the card dragging prototype.
+ void move(Card card, List<Card> dest) {
+ // The first step is to find the card. Where is it?
+ // then we can remove it and add to the dest.
+ debugString = 'Moving... ${card.toString()}';
+ int i = findCard(card);
+ if (i == -1) {
+ debugString = 'NO... ${card.toString()}';
+ return;
+ }
+ int destId = cardCollections.indexOf(dest);
+
+ gamelog.add(new ProtoCommand.pass(i, destId, <Card>[card]));
+
+ debugString = 'Move ${i} ${card.toString()}';
+ print(debugString);
+ }
+}
+
+enum HeartsPhase { Deal, Pass, Take, Play, Score }
+
+class HeartsGame extends Game {
+ static const PLAYER_A = 0;
+ static const PLAYER_B = 1;
+ static const PLAYER_C = 2;
+ static const PLAYER_D = 3;
+ static const PLAYER_A_PLAY = 4;
+ static const PLAYER_B_PLAY = 5;
+ static const PLAYER_C_PLAY = 6;
+ static const PLAYER_D_PLAY = 7;
+ static const PLAYER_A_TRICK = 8;
+ static const PLAYER_B_TRICK = 9;
+ static const PLAYER_C_TRICK = 10;
+ static const PLAYER_D_TRICK = 11;
+ static const PLAYER_A_PASS = 12;
+ static const PLAYER_B_PASS = 13;
+ static const PLAYER_C_PASS = 14;
+ static const PLAYER_D_PASS = 15;
+
+ static const OFFSET_HAND = 0;
+ static const OFFSET_PLAY = 4;
+ static const OFFSET_TRICK = 8;
+ static const OFFSET_PASS = 12;
+
+ static const MAX_SCORE = 100; // Play until someone gets to 100.
+
+ // Note: These cards are final because the "classic" deck has 52 cards.
+ // It is up to the renderer to reskin those cards as needed.
+ final Card TWO_OF_CLUBS = new Card("classic", "c2");
+ final Card QUEEN_OF_SPADES = new Card("classic", "sq");
+
+ HeartsPhase _phase = HeartsPhase.Deal;
+ HeartsPhase get phase => _phase;
+ void set phase(HeartsPhase other) {
+ print('setting phase from ${_phase} to ${other}');
+ _phase = other;
+ }
+ int roundNumber = 0;
+ int lastTrickTaker;
+ bool heartsBroken;
+ int trickNumber;
+
+ // Used by the score screen to track scores and see which players are ready to continue to the next round.
+ List<int> scores = [0, 0, 0, 0];
+ List<bool> ready;
+
+ HeartsGame(int playerNumber)
+ : super._create(GameType.Hearts, new HeartsGameLog(), playerNumber, 16) {
+ resetGame();
+ }
+
+ void resetGame() {
+ this.resetCards();
+ heartsBroken = false;
+ lastTrickTaker = null;
+ trickNumber = 0;
+ }
+
+ void dealCards() {
+ deck.shuffle();
+
+ // 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 {
+ switch (roundNumber % 4) {
+ // is a 4-cycle
+ case 0:
+ return (playerNumber - 1) % 4; // passLeft
+ case 1:
+ return (playerNumber + 1) % 4; // passRight
+ case 2:
+ return (playerNumber + 2) % 4; // passAcross
+ case 3:
+ return null; // no player to pass to
+ default:
+ assert(false);
+ return null;
+ }
+ }
+ int get takeTarget => _getTakeTarget(playerNumber);
+ int _getTakeTarget(takerId) {
+ switch (roundNumber % 4) {
+ // is a 4-cycle
+ case 0:
+ return (takerId + 1) % 4; // takeRight
+ case 1:
+ return (takerId - 1) % 4; // takeLeft
+ case 2:
+ return (takerId + 2) % 4; // taleAcross
+ case 3:
+ return null; // no player to pass to
+ default:
+ assert(false);
+ return null;
+ }
+ }
+
+ // Please only call this in the Play phase. Otherwise, it's pretty useless.
+ int get whoseTurn {
+ if (phase != HeartsPhase.Play) {
+ return null;
+ }
+ return (lastTrickTaker + this.numPlayed) % 4;
+ }
+
+ int getCardValue(Card c) {
+ String remainder = c.identifier.substring(1);
+ switch (remainder) {
+ case "1": // ace
+ return 14;
+ case "k":
+ return 13;
+ case "q":
+ return 12;
+ case "j":
+ return 11;
+ default:
+ return int.parse(remainder);
+ }
+ }
+
+ String getCardSuit(Card c) {
+ return c.identifier[0];
+ }
+ bool isHeartsCard(Card c) {
+ return getCardSuit(c) == 'h' && c.deck == 'classic';
+ }
+ bool isQSCard(Card c) {
+ return c == QUEEN_OF_SPADES;
+ }
+ bool isFirstCard(Card c) {
+ return c == TWO_OF_CLUBS;
+ }
+
+ bool isPenaltyCard(Card c) {
+ return isQSCard(c) || isHeartsCard(c);
+ }
+
+ bool hasSuit(int player, String suit) {
+ Card matchesSuit = this.cardCollections[player + OFFSET_HAND].firstWhere(
+ (Card element) => (getCardSuit(element) == suit), orElse: () => null);
+ return matchesSuit != null;
+ }
+
+ Card get leadingCard {
+ if (this.numPlayed >= 1) {
+ return cardCollections[this.lastTrickTaker + OFFSET_PLAY][0];
+ }
+ return null;
+ }
+ int get numPlayed {
+ int count = 0;
+ for (int i = 0; i < 4; i++) {
+ if (cardCollections[i + OFFSET_PLAY].length == 1) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ bool get hasGameEnded => this.scores.reduce(math.max) >= HeartsGame.MAX_SCORE;
+
+ bool get allDealt => cardCollections[PLAYER_A].length == 13 &&
+ cardCollections[PLAYER_B].length == 13 &&
+ cardCollections[PLAYER_C].length == 13 &&
+ cardCollections[PLAYER_D].length == 13;
+
+ bool get allPassed => cardCollections[PLAYER_A_PASS].length == 3 &&
+ cardCollections[PLAYER_B_PASS].length == 3 &&
+ cardCollections[PLAYER_C_PASS].length == 3 &&
+ cardCollections[PLAYER_D_PASS].length == 3;
+ bool get allTaken => cardCollections[PLAYER_A_PASS].length == 0 &&
+ cardCollections[PLAYER_B_PASS].length == 0 &&
+ cardCollections[PLAYER_C_PASS].length == 0 &&
+ cardCollections[PLAYER_D_PASS].length == 0;
+ bool get allPlayed => this.numPlayed == 4;
+
+ bool get allReady => ready[0] && ready[1] && ready[2] && ready[3];
+ void setReady(int playerId) {
+ ready[playerId] = true;
+ }
+ void unsetReady() {
+ ready = <bool>[false, false, false, false];
+ }
+
+ void deal(int playerId, List<Card> cards) {
+ gamelog.add(new HeartsCommand.deal(playerId, cards));
+ }
+
+ // Note that this will be called by the UI.
+ // It won't be possible to pass for other players, except via the GameLog.
+ void passCards(List<Card> cards) {
+ assert(phase == HeartsPhase.Pass && this.passTarget != null);
+ if (cards.length != 3) {
+ throw new StateError('3 cards expected, but got: ${cards.toString()}');
+ }
+ gamelog.add(new HeartsCommand.pass(playerNumber, cards));
+ }
+
+ // Note that this will be called by the UI.
+ // It won't be possible to take cards for other players, except via the GameLog.
+ void takeCards() {
+ assert(phase == HeartsPhase.Take && this.takeTarget != null);
+ List<Card> cards = this.cardCollections[takeTarget + OFFSET_PASS];
+ assert(cards.length == 3);
+
+ gamelog.add(new HeartsCommand.take(playerNumber));
+ }
+
+ // Note that this will be called by the UI.
+ // It won't be possible to set the readiness for other players, except via the GameLog.
+ void setReadyUI() {
+ assert(phase == HeartsPhase.Score);
+ gamelog.add(new HeartsCommand.ready(playerNumber));
+ }
+
+ // Note that this will be called by the UI.
+ // TODO: Does this really need to be overridden? That seems like bad structure in GameComponent.
+ // Overrides Game's move method with the "move" logic for Hearts. Used for drag-drop.
+ // Note that this can only be called in the Play Phase of your turn.
+ // The UI will handle the drag-drop of the Pass Phase with its own state.
+ // The UI will initiate pass separately.
+ void move(Card card, List<Card> dest) {
+ assert(phase == HeartsPhase.Play && whoseTurn == playerNumber);
+
+ int i = findCard(card);
+ if (i == -1) {
+ throw new StateError(
+ 'card does not exist or was not dealt: ${card.toString()}');
+ }
+ int destId = cardCollections.indexOf(dest);
+ if (destId == -1) {
+ throw new StateError(
+ 'destination list does not exist: ${dest.toString()}');
+ }
+ if (destId != playerNumber + OFFSET_PLAY) {
+ throw new StateError(
+ 'player ${playerNumber} is not playing to the correct list: ${destId}');
+ }
+
+ gamelog.add(new HeartsCommand.play(playerNumber, card));
+
+ debugString = 'Play ${i} ${card.toString()}';
+ print(debugString);
+ }
+
+ // Overridden from Game for Hearts-specific logic:
+ // Switch from Pass to Take phase when all 4 players are passing.
+ // Switch from Take to Play phase when all 4 players have taken.
+ // During Play, if all 4 players play a card, move the tricks around.
+ // During Play, once all cards are gone and last trick is taken, go to Score phase (compute score and possibly end game).
+ // Switch from Score to Deal phase when all 4 players indicate they are ready.
+ void triggerEvents() {
+ switch (this.phase) {
+ case HeartsPhase.Deal:
+ if (this.allDealt) {
+ if (this.passTarget != null) {
+ phase = HeartsPhase.Pass;
+ } else {
+ // All cards are dealt. The person who "won" the last trick goes first.
+ // In this case, we'll just pretend it's the person with the 2 of clubs.
+ this.lastTrickTaker = this.findCard(TWO_OF_CLUBS);
+ phase = HeartsPhase.Play;
+ }
+ }
+ return;
+ case HeartsPhase.Pass:
+ if (this.allPassed) {
+ phase = HeartsPhase.Take;
+ }
+ return;
+ case HeartsPhase.Take:
+ if (this.allTaken) {
+ // All cards are dealt. The person who "won" the last trick goes first.
+ // In this case, we'll just pretend it's the person with the 2 of clubs.
+ this.lastTrickTaker = this.findCard(TWO_OF_CLUBS);
+ phase = HeartsPhase.Play;
+ }
+ return;
+ case HeartsPhase.Play:
+ if (this.allPlayed) {
+ // Determine who won this trick.
+ int winner = this.determineTrickWinner();
+
+ // Move the cards to their trick list. Also check if hearts was broken.
+ // Note: Some variants of Hearts allows the QUEEN_OF_SPADES to break hearts too.
+ for (int i = 0; i < 4; i++) {
+ List<Card> play = this.cardCollections[i + OFFSET_PLAY];
+ if (!heartsBroken && isHeartsCard(play[0])) {
+ heartsBroken = true;
+ }
+ this.cardCollections[winner + OFFSET_TRICK]
+ .addAll(play); // or add(play[0])
+ play.clear();
+ }
+
+ // Set them as the next person to go.
+ this.lastTrickTaker = winner;
+ this.trickNumber++;
+
+ // Additionally, if that was the last trick, move onto the score phase.
+ if (this.trickNumber == 13) {
+ phase = HeartsPhase.Score;
+ this.prepareScore();
+ }
+ }
+ return;
+ case HeartsPhase.Score:
+ if (!this.hasGameEnded && this.allReady) {
+ this.roundNumber++;
+ phase = HeartsPhase.Deal;
+ this.resetGame();
+ }
+ return;
+ default:
+ assert(false);
+ }
+ }
+
+ // Returns null or the reason that the player cannot play the card.
+ String canPlay(int player, Card c) {
+ if (phase != HeartsPhase.Play) {
+ return "It is not the Play phase of Hearts.";
+ }
+ if (!cardCollections[player].contains(c)) {
+ return "Player ${player} does not have the card (${c.toString()})";
+ }
+ if (this.whoseTurn != player) {
+ return "It is not Player ${player}'s turn.";
+ }
+ if (trickNumber == 0 && this.numPlayed == 0 && c != TWO_OF_CLUBS) {
+ return "Player ${player} must play the two of clubs.";
+ }
+ if (trickNumber == 0 && isPenaltyCard(c)) {
+ return "Cannot play a penalty card on the first round of Hearts.";
+ }
+ if (this.numPlayed == 0 && isHeartsCard(c) && !heartsBroken) {
+ return "Cannot lead with a heart when the suit has not been broken yet.";
+ }
+ if (this.leadingCard != null) {
+ String leadingSuit = getCardSuit(this.leadingCard);
+ String otherSuit = getCardSuit(c);
+ if (this.numPlayed >= 1 &&
+ leadingSuit != otherSuit &&
+ hasSuit(player, leadingSuit)) {
+ return "Must follow with a ${leadingSuit}.";
+ }
+ }
+ return null;
+ }
+
+ int determineTrickWinner() {
+ String leadingSuit = this.getCardSuit(this.leadingCard);
+ int highestIndex;
+ int highestValue; // oh no, aces are highest.
+ for (int i = 0; i < 4; i++) {
+ Card c = cardCollections[i + OFFSET_PLAY][0];
+ int value = this.getCardValue(c);
+ String suit = this.getCardSuit(c);
+ if (suit == leadingSuit &&
+ (highestIndex == null || highestValue < value)) {
+ highestIndex = i;
+ highestValue = value;
+ }
+ }
+
+ return highestIndex;
+ }
+ void prepareScore() {
+ this.unsetReady();
+ this.updateScore();
+
+ // At this point, it's up to the UI to determine what to do if the game is 'over'.
+ // Check this.hasGameEnded to determine if that is the case. Logically, there is nothing for this game to do.
+ }
+
+ void updateScore() {
+ // Count up points and check if someone shot the moon.
+ int shotMoon = null;
+ for (int i = 0; i < 4; i++) {
+ int delta = computeScore(i);
+ this.scores[i] += delta;
+ if (delta == 26) {
+ // Shot the moon!
+ shotMoon = i;
+ }
+ }
+
+ // If someone shot the moon, apply the proper score adjustments here.
+ if (shotMoon != null) {
+ for (int i = 0; i < 4; i++) {
+ if (shotMoon == i) {
+ this.scores[i] -= 26;
+ } else {
+ this.scores[i] += 26;
+ }
+ }
+ }
+ }
+
+ int computeScore(int player) {
+ int total = 0;
+ List<Card> trickCards = this.cardCollections[player + OFFSET_TRICK];
+ for (int i = 0; i < trickCards.length; i++) {
+ Card c = trickCards[i];
+ if (isHeartsCard(c)) {
+ total++;
+ }
+ if (isQSCard(c)) {
+ total += 13;
+ }
+ }
+ return total;
+ }
+}
+
+abstract class GameLog {
+ Game game;
+ List<GameCommand> log = new List<GameCommand>();
+ 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;
+ }
+
+ void add(GameCommand gc) {
+ pendingCommands.add(gc);
+ _tryPendingCommand();
+ }
+
+ 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();
+ }
+ } 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 {
+ final String data; // This will be parsed.
+
+ // Usually this constructor is used when reading from a log/syncbase.
+ HeartsCommand(this.data);
+
+ // The following constructors are used for the player generating the HeartsCommand.
+ HeartsCommand.deal(int playerId, List<Card> cards)
+ : this.data = computeDeal(playerId, cards);
+
+ HeartsCommand.pass(int senderId, List<Card> cards)
+ : this.data = computePass(senderId, cards);
+
+ HeartsCommand.take(int takerId) : this.data = computeTake(takerId);
+
+ HeartsCommand.play(int playerId, Card c)
+ : this.data = computePlay(playerId, c);
+
+ HeartsCommand.ready(int playerId) : this.data = computeReady(playerId);
+
+ static computeDeal(int playerId, List<Card> cards) {
+ StringBuffer buff = new StringBuffer();
+ buff.write("Deal:${playerId}:");
+ cards.forEach((card) => buff.write("${card.toString()}:"));
+ buff.write("END");
+ return buff.toString();
+ }
+ static computePass(int senderId, List<Card> cards) {
+ StringBuffer buff = new StringBuffer();
+ buff.write("Pass:${senderId}:");
+ cards.forEach((card) => buff.write("${card.toString()}:"));
+ buff.write("END");
+ return buff.toString();
+ }
+ static computeTake(int takerId) {
+ return "Take:${takerId}:END";
+ }
+ static computePlay(int playerId, Card c) {
+ return "Play:${playerId}:${c.toString()}:END";
+ }
+ static computeReady(int playerId) {
+ 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;
+
+ print("HeartsCommand is executing: ${data}");
+ List<String> parts = data.split(":");
+ switch (parts[0]) {
+ case "Deal":
+ if (game.phase != HeartsPhase.Deal) {
+ throw new StateError(
+ "Cannot process deal commands when not in Deal phase");
+ }
+ // Deal appends cards to playerId's hand.
+ int playerId = int.parse(parts[1]);
+ List<Card> hand = game.cardCollections[playerId];
+ if (hand.length + parts.length - 3 > 13) {
+ throw new StateError("Cannot deal more than 13 cards to a hand");
+ }
+
+ // The last part is 'END', but the rest are cards.
+ for (int i = 2; i < parts.length - 1; i++) {
+ Card c = new Card.fromString(parts[i]);
+ this.transfer(game.deck, hand, c);
+ }
+ return;
+ case "Pass":
+ if (game.phase != HeartsPhase.Pass) {
+ throw new StateError(
+ "Cannot process pass commands when not in Pass phase");
+ }
+ // Pass moves a set of cards from senderId to receiverId.
+ int senderId = int.parse(parts[1]);
+ int receiverId = senderId + HeartsGame.OFFSET_PASS;
+ List<Card> handS = game.cardCollections[senderId];
+ List<Card> handR = game.cardCollections[receiverId];
+
+ int numPassing = parts.length - 3;
+ if (numPassing != 3) {
+ throw new StateError("Must pass 3 cards, attempted ${numPassing}");
+ }
+
+ // The last part is 'END', but the rest are cards.
+ for (int i = 2; i < parts.length - 1; i++) {
+ Card c = new Card.fromString(parts[i]);
+ this.transfer(handS, handR, c);
+ }
+ return;
+ case "Take":
+ if (game.phase != HeartsPhase.Take) {
+ throw new StateError(
+ "Cannot process take commands when not in Take phase");
+ }
+ int takerId = int.parse(parts[1]);
+ int senderPile = game._getTakeTarget(takerId) + HeartsGame.OFFSET_PASS;
+ List<Card> handT = game.cardCollections[takerId];
+ List<Card> handS = game.cardCollections[senderPile];
+ handT.addAll(handS);
+ handS.clear();
+ return;
+ case "Play":
+ if (game.phase != HeartsPhase.Play) {
+ throw new StateError(
+ "Cannot process play commands when not in Play phase");
+ }
+
+ // Play the card from the player's hand to their play pile.
+ int playerId = int.parse(parts[1]);
+ int targetId = playerId + HeartsGame.OFFSET_PLAY;
+ List<Card> hand = game.cardCollections[playerId];
+ List<Card> discard = game.cardCollections[targetId];
+
+ Card c = new Card.fromString(parts[2]);
+
+ // If the card isn't valid, then we have an error.
+ String reason = game.canPlay(playerId, c);
+ if (reason != null) {
+ throw new StateError(
+ "Player ${playerId} cannot play ${c.toString()} because ${reason}");
+ }
+ this.transfer(hand, discard, c);
+ return;
+ case "Ready":
+ if (game.hasGameEnded) {
+ throw new StateError(
+ "Game has already ended. Start a new one to play again.");
+ }
+ if (game.phase != HeartsPhase.Score) {
+ throw new StateError(
+ "Cannot process ready commands when not in Score phase");
+ }
+ int playerId = int.parse(parts[1]);
+ game.setReady(playerId);
+ return;
+ default:
+ print(data);
+ assert(false); // How could this have happened?
+ }
+ }
+
+ void transfer(List<Card> sender, List<Card> receiver, Card c) {
+ if (!sender.contains(c)) {
+ throw new StateError(
+ "Sender ${sender.toString()} lacks Card ${c.toString()}");
+ }
+ sender.remove(c);
+ receiver.add(c);
+ }
+}
+
+class ProtoCommand extends GameCommand {
+ final String data; // This will be parsed.
+
+ // Usually this constructor is used when reading from a log/syncbase.
+ ProtoCommand(this.data);
+
+ // The following constructors are used for the player generating the ProtoCommand.
+ ProtoCommand.deal(int playerId, List<Card> cards)
+ : this.data = computeDeal(playerId, cards);
+
+ // TODO: receiverId is actually implied by the game round. So it may end up being removable.
+ ProtoCommand.pass(int senderId, int receiverId, List<Card> cards)
+ : this.data = computePass(senderId, receiverId, cards);
+
+ ProtoCommand.play(int playerId, Card c)
+ : this.data = computePlay(playerId, c);
+
+ static computeDeal(int playerId, List<Card> cards) {
+ StringBuffer buff = new StringBuffer();
+ buff.write("Deal:${playerId}:");
+ cards.forEach((card) => buff.write("${card.toString()}:"));
+ buff.write("END");
+ return buff.toString();
+ }
+ static computePass(int senderId, int receiverId, List<Card> cards) {
+ StringBuffer buff = new StringBuffer();
+ buff.write("Pass:${senderId}:${receiverId}:");
+ cards.forEach((card) => buff.write("${card.toString()}:"));
+ buff.write("END");
+ return buff.toString();
+ }
+ static computePlay(int playerId, Card c) {
+ 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(":");
+ switch (parts[0]) {
+ case "Deal":
+ // Deal appends cards to playerId's hand.
+ int playerId = int.parse(parts[1]);
+ List<Card> hand = game.cardCollections[playerId];
+
+ // The last part is 'END', but the rest are cards.
+ for (int i = 2; i < parts.length - 1; i++) {
+ Card c = new Card.fromString(parts[i]);
+ this.transfer(game.deck, hand, c);
+ }
+ return;
+ case "Pass":
+ // Pass moves a set of cards from senderId to receiverId.
+ int senderId = int.parse(parts[1]);
+ int receiverId = int.parse(parts[2]);
+ List<Card> handS = game.cardCollections[senderId];
+ List<Card> handR = game.cardCollections[receiverId];
+
+ // The last part is 'END', but the rest are cards.
+ for (int i = 3; i < parts.length - 1; i++) {
+ Card c = new Card.fromString(parts[i]);
+ this.transfer(handS, handR, c);
+ }
+ return;
+ case "Play":
+ // In this case, move it to the designated discard pile.
+ // For now, the discard pile is pile #4. This may change.
+ int playerId = int.parse(parts[1]);
+ List<Card> hand = game.cardCollections[playerId];
+
+ Card c = new Card.fromString(parts[2]);
+ this.transfer(hand, game.cardCollections[4], c);
+ return;
+ default:
+ print(data);
+ assert(false); // How could this have happened?
+ }
+ }
+
+ void transfer(List<Card> sender, List<Card> receiver, Card c) {
+ assert(sender.contains(c));
+ sender.remove(c);
+ receiver.add(c);
+ }
+}
diff --git a/lib/logic/syncbase_echo.dart b/lib/logic/syncbase_echo.dart
new file mode 100644
index 0000000..5776b23
--- /dev/null
+++ b/lib/logic/syncbase_echo.dart
@@ -0,0 +1,15 @@
+import 'game.dart' show Game, GameType, GameLog, GameCommand;
+
+class SyncbaseEcho extends Game {
+ 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/main.dart b/lib/main.dart
new file mode 100644
index 0000000..ae59599
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,23 @@
+import 'package:sky/widgets.dart';
+
+import 'logic/croupier.dart' show Croupier;
+import 'components/croupier.dart' show CroupierComponent;
+
+class CroupierApp extends App {
+ Croupier croupier;
+
+ CroupierApp() : super() {
+ this.croupier = new Croupier();
+ }
+
+ Widget build() {
+ return new Container(
+ decoration: new BoxDecoration(
+ backgroundColor: const Color(0xFF6666FF), borderRadius: 5.0),
+ child: new CroupierComponent(this.croupier));
+ }
+}
+
+void main() {
+ runApp(new CroupierApp());
+}
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/src/syncbase/syncbase_echo_impl.dart b/lib/src/syncbase/syncbase_echo_impl.dart
new file mode 100644
index 0000000..2eedd71
--- /dev/null
+++ b/lib/src/syncbase/syncbase_echo_impl.dart
@@ -0,0 +1,91 @@
+import 'dart:async';
+import 'dart:convert' show UTF8;
+
+import '../../logic/game.dart' show Game;
+
+import 'package:sky/mojo/embedder.dart' show embedder;
+
+import 'package:ether/echo_client.dart' show EchoClient;
+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 SyncbaseEchoImpl {
+ final EchoClient _echoClient;
+ final SyncbaseClient _syncbaseClient;
+ final Game game;
+
+ SyncbaseEchoImpl(this.game)
+ : _echoClient = new EchoClient(
+ embedder.connectToService, 'https://mojo.v.io/echo_server.mojo'),
+ _syncbaseClient = new SyncbaseClient(embedder.connectToService,
+ 'https://mojo.v.io/syncbase_server.mojo');
+
+ int seq = 0;
+ SyncbaseTable tb;
+ String sendMsg, recvMsg, putStr, getStr;
+
+ Future doEcho() async {
+ log('DemoApp.doEcho');
+
+ sendMsg = seq.toString();
+ recvMsg = '';
+ seq++;
+ log('setState sendMsg done');
+
+ String recvMsgAsync = await _echoClient.echo(sendMsg);
+
+ recvMsg = recvMsgAsync;
+ log('setState recvMsg done');
+
+ game.updateCallback(); // tell the UI to set/update state.
+ }
+
+ Future doSyncbaseInit() async {
+ log('DemoApp.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');
+ }
+
+ Future doPutGet() async {
+ log('DemoApp.doPutGet');
+ await doSyncbaseInit();
+
+ putStr = seq.toString();
+ getStr = '';
+ seq++;
+ log('setState putStr done');
+
+ // TODO(sadovsky): Switch to tb.put/get once they exist.
+ var row = tb.row('key');
+ await row.put(UTF8.encode(putStr));
+ var getBytes = await row.get();
+
+ getStr = UTF8.decode(getBytes);
+ log('setState getStr done');
+
+ game.updateCallback(); // tell the UI to set/update state.
+ }
+}
diff --git a/mojoconfig b/mojoconfig
new file mode 100644
index 0000000..23a16b5
--- /dev/null
+++ b/mojoconfig
@@ -0,0 +1,41 @@
+# This describes how we access our mojo services, including the sky_viewer.
+
+{
+ 'dev_servers': [
+ {
+ 'host': 'https://croupier.v.io/',
+ 'mappings': [
+ ('packages/', [
+ # For croupier packages.
+ '@{CROUPIER_DIR}/packages',
+ ]),
+ ('', [
+ # For croupier/lib/main.dart.
+ '@{CROUPIER_DIR}',
+ ]),
+ ]
+ },
+ {
+ 'host': 'https://mojo.v.io/',
+ 'mappings': [
+ ('', [
+ # For echo_server.mojo and syncbase_server.mojo.
+ '@{ETHER_BUILD_DIR}',
+ ]),
+ ],
+ },
+ {
+ 'host': 'https://sky/',
+ 'mappings': [
+ ('', [
+ # For sky_viewer.mojo.
+ '@{SKY_BUILD_DIR}'
+ ]),
+ ],
+ }
+ ],
+
+ 'content_handlers': {
+ 'application/dart': 'https://sky/sky_viewer.mojo',
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..d23cca5
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,9 @@
+name: your_app_name
+dependencies:
+ sky: any
+ sky_tools: any
+ test: any
+ ether: any
+dependency_overrides:
+ ether:
+ path: ../../mojo/syncbase/dart
\ No newline at end of file
diff --git a/test/game_log_hearts_test.txt b/test/game_log_hearts_test.txt
new file mode 100644
index 0000000..de1b3b2
--- /dev/null
+++ b/test/game_log_hearts_test.txt
@@ -0,0 +1,508 @@
+# Deal
+Deal:0:classic h1:classic h2:classic h3:classic h4:classic h5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:1:classic d1:classic d2:classic d3:classic d4:classic d5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+Deal:2:classic s1:classic s2:classic s3:classic s4:classic s5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:3:classic c1:classic c2:classic c3:classic c4:classic c5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+
+# Pass
+Pass:3:classic c1:classic c2:classic c3:END
+Pass:2:classic s1:classic s2:classic s3:END
+Pass:0:classic h1:classic h2:classic h3:END
+Pass:1:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# 0 has all diamonds except for h4 and h5
+# 1 has all spades except for d4 and d5
+# 2 has all clubs except for s4 and s5
+# 3 has all hearts except for c4 and c5
+
+# Trick 1 (2 leads with 2 of clubs)
+Play:2:classic c2:END
+Play:3:classic c4:END
+Play:0:classic d1:END
+Play:1:classic s1:END
+
+# Trick 2 (3 won last round with 4 of clubs)
+Play:3:classic c5:END
+Play:0:classic d2:END
+Play:1:classic s2:END
+Play:2:classic c1:END
+
+# Trick 3 (2 won with ace of clubs)
+Play:2:classic s4:END
+Play:3:classic h1:END
+Play:0:classic h5:END
+Play:1:classic s3:END
+
+# Trick 4 (2 won with s4)
+Play:2:classic s5:END
+Play:3:classic hk:END
+Play:0:classic h4:END
+Play:1:classic sk:END
+
+# Trick 5 (1 won with sk)
+Play:1:classic d5:END
+Play:2:classic ck:END
+Play:3:classic hq:END
+Play:0:classic d3:END
+
+# Trick 6 (1 won with d5)
+Play:1:classic d4:END
+Play:2:classic cq:END
+Play:3:classic hj:END
+Play:0:classic dk:END
+
+# Trick 7 (0 won with dk)
+Play:0:classic dq:END
+Play:1:classic sq:END
+Play:2:classic c3:END
+Play:3:classic h2:END
+
+# Trick 8 (0 won with dq)
+Play:0:classic dj:END
+Play:1:classic sj:END
+Play:2:classic cj:END
+Play:3:classic h3:END
+
+# Trick 9 (0 won with dj)
+Play:0:classic d10:END
+Play:1:classic s10:END
+Play:2:classic c10:END
+Play:3:classic h10:END
+
+# Trick 10 (0 won with d10)
+Play:0:classic d9:END
+Play:1:classic s9:END
+Play:2:classic c9:END
+Play:3:classic h9:END
+
+# Trick 11 (0 won with d9)
+Play:0:classic d8:END
+Play:1:classic s8:END
+Play:2:classic c8:END
+Play:3:classic h8:END
+
+# Trick 12 (0 won with d8)
+Play:0:classic d7:END
+Play:1:classic s7:END
+Play:2:classic c7:END
+Play:3:classic h7:END
+
+# Trick 13 (0 won with d7)
+Play:0:classic d6:END
+Play:1:classic s6:END
+Play:2:classic c6:END
+Play:3:classic h6:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# The score is [21, 3, 2, 0]
+
+# 2nd Round here
+
+# Deal
+Deal:0:classic d1:classic d2:classic d3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic h1:classic h2:classic h3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic s1:classic s2:classic s3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic c1:classic c2:classic c3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:1:classic h1:classic h2:classic h3:END
+Pass:2:classic s1:classic s2:classic s3:END
+Pass:3:classic c1:classic c2:classic c3:END
+Pass:0:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 3rd Round here
+
+# Deal
+Deal:0:classic h1:classic h2:classic h3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic s1:classic s2:classic s3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic c1:classic c2:classic c3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic d1:classic d2:classic d3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:0:classic h1:classic h2:classic h3:END
+Pass:1:classic s1:classic s2:classic s3:END
+Pass:2:classic c1:classic c2:classic c3:END
+Pass:3:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 4th round here
+
+# Deal
+Deal:0:classic c1:classic c2:classic c3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic d1:classic d2:classic d3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic h1:classic h2:classic h3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic s1:classic s2:classic s3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Score Phase (ready)
+Ready:2:END
+Ready:0:END
+Ready:3:END
+Ready:1:END
+
+# 5th round here
+
+# Deal
+Deal:0:classic s1:classic s2:classic s3:classic c4:classic c5:classic c6:classic c7:classic c8:classic c9:classic c10:classic cj:classic cq:classic ck:END
+Deal:1:classic c1:classic c2:classic c3:classic s4:classic d5:classic d6:classic d7:classic d8:classic d9:classic d10:classic dj:classic dq:classic dk:END
+Deal:2:classic d1:classic d2:classic d3:classic d4:classic h5:classic h6:classic h7:classic h8:classic h9:classic h10:classic hj:classic hq:classic hk:END
+Deal:3:classic h1:classic h2:classic h3:classic h4:classic s5:classic s6:classic s7:classic s8:classic s9:classic s10:classic sj:classic sq:classic sk:END
+
+# Pass
+Pass:3:classic h1:classic h2:classic h3:END
+Pass:0:classic s1:classic s2:classic s3:END
+Pass:1:classic c1:classic c2:classic c3:END
+Pass:2:classic d1:classic d2:classic d3:END
+
+# Take
+Take:1:END
+Take:2:END
+Take:0:END
+Take:3:END
+
+# Trick 1
+Play:0:classic c2:END
+Play:1:classic d2:END
+Play:2:classic d4:END
+Play:3:classic s2:END
+
+# Trick 2
+Play:0:classic c3:END
+Play:1:classic d3:END
+Play:2:classic h3:END
+Play:3:classic s3:END
+
+# Trick 3
+Play:0:classic c4:END
+Play:1:classic s4:END
+Play:2:classic h2:END
+Play:3:classic h4:END
+
+# Trick 4
+Play:0:classic c5:END
+Play:1:classic d5:END
+Play:2:classic h5:END
+Play:3:classic s5:END
+
+# Trick 5
+Play:0:classic c6:END
+Play:1:classic d6:END
+Play:2:classic h6:END
+Play:3:classic s6:END
+
+# Trick 6
+Play:0:classic c7:END
+Play:1:classic d7:END
+Play:2:classic h7:END
+Play:3:classic s7:END
+
+# Trick 7
+Play:0:classic c8:END
+Play:1:classic d8:END
+Play:2:classic h8:END
+Play:3:classic s8:END
+
+# Trick 8
+Play:0:classic c9:END
+Play:1:classic d9:END
+Play:2:classic h9:END
+Play:3:classic s9:END
+
+# Trick 9
+Play:0:classic c1:END
+Play:1:classic d1:END
+Play:2:classic h1:END
+Play:3:classic s1:END
+
+# Trick 10
+Play:0:classic c10:END
+Play:1:classic d10:END
+Play:2:classic h10:END
+Play:3:classic s10:END
+
+# Trick 11
+Play:0:classic cj:END
+Play:1:classic dj:END
+Play:2:classic hj:END
+Play:3:classic sj:END
+
+# Trick 12
+Play:0:classic cq:END
+Play:1:classic dq:END
+Play:2:classic hq:END
+Play:3:classic sq:END
+
+# Trick 13
+Play:0:classic ck:END
+Play:1:classic dk:END
+Play:2:classic hk:END
+Play:3:classic sk:END
+
+# Game is over!
\ No newline at end of file
diff --git a/test/hearts_test.dart b/test/hearts_test.dart
new file mode 100644
index 0000000..744245e
--- /dev/null
+++ b/test/hearts_test.dart
@@ -0,0 +1,575 @@
+import "package:test/test.dart";
+import "../lib/logic/game.dart";
+import "../lib/logic/card.dart";
+
+import "dart:io";
+
+void main() {
+ group("Initialization", () {
+ HeartsGame game = new HeartsGame(0);
+ test("Dealing", () {
+ game.dealCards(); // What the dealer actually runs to get cards to everybody.
+
+ // By virtue of creating the game, HeartsGame should have 4 collections with 13 cards and 8 collections with 0 cards each.
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_A + HeartsGame.OFFSET_HAND].length, equals(13),
+ reason: "Dealt 13 cards to A");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_B + HeartsGame.OFFSET_HAND].length, equals(13),
+ reason: "Dealt 13 cards to B");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_C + HeartsGame.OFFSET_HAND].length, equals(13),
+ reason: "Dealt 13 cards to C");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_D + HeartsGame.OFFSET_HAND].length, equals(13),
+ reason: "Dealt 13 cards to D");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_A + HeartsGame.OFFSET_PLAY].length, equals(0),
+ reason: "Not playing yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_B + HeartsGame.OFFSET_PLAY].length, equals(0),
+ reason: "Not playing yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_C + HeartsGame.OFFSET_PLAY].length, equals(0),
+ reason: "Not playing yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_D + HeartsGame.OFFSET_PLAY].length, equals(0),
+ reason: "Not playing yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_A + HeartsGame.OFFSET_PASS].length, equals(0),
+ reason: "Not passing yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_B + HeartsGame.OFFSET_PASS].length, equals(0),
+ reason: "Not passing yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_C + HeartsGame.OFFSET_PASS].length, equals(0),
+ reason: "Not passing yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_D + HeartsGame.OFFSET_PASS].length, equals(0),
+ reason: "Not passing yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_A + HeartsGame.OFFSET_TRICK].length, equals(0),
+ reason: "No tricks yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_B + HeartsGame.OFFSET_TRICK].length, equals(0),
+ reason: "No tricks yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_C + HeartsGame.OFFSET_TRICK].length, equals(0),
+ reason: "No tricks yet");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_D + HeartsGame.OFFSET_TRICK].length, equals(0),
+ reason: "No tricks yet");
+ });
+ });
+
+ // For this test, the cards may end up being duplicate or inconsistent.
+ group("Scoring", () {
+ HeartsGame game = new HeartsGame(0);
+ test("Compute/Prepare Score", () {
+ // In this situation, what's the score?
+ game.cardCollections[HeartsGame.PLAYER_A_TRICK] = <Card>[
+ new Card("classic", "dq"),
+ new Card("classic", "dk"),
+ new Card("classic", "h1"),
+ new Card("classic", "h2"),
+ new Card("classic", "h3"),
+ new Card("classic", "h4")
+ ];
+
+ expect(game.computeScore(HeartsGame.PLAYER_A), equals(4),
+ reason: "Player A has 4 hearts");
+
+ // In this alternative situation, what's the score?
+ game.cardCollections[HeartsGame.PLAYER_B_TRICK] = <Card>[
+ new Card("classic", "h6"),
+ new Card("classic", "h7"),
+ new Card("classic", "h8"),
+ new Card("classic", "h9"),
+ new Card("classic", "h10"),
+ new Card("classic", "hj"),
+ new Card("classic", "hq"),
+ new Card("classic", "hk"),
+ new Card("classic", "s1"),
+ new Card("classic", "s2")
+ ];
+
+ expect(game.computeScore(HeartsGame.PLAYER_B), equals(8),
+ reason: "Player B has 8 hearts.");
+
+ // Should prepare C as well.
+ game.cardCollections[HeartsGame.PLAYER_C_TRICK] = <Card>[
+ new Card("classic", "h5"),
+ new Card("classic", "sq")
+ ];
+ expect(game.computeScore(HeartsGame.PLAYER_C), equals(14),
+ reason: "Player C has 1 heart and the queen of spades.");
+
+ // Now, update the score, modifying game.scores.
+ game.updateScore();
+ expect(game.scores, equals([4, 8, 14, 0]));
+
+ // Do it again.
+ game.updateScore();
+ expect(game.scores, equals([8, 16, 28, 0]));
+
+ // Shoot the moon!
+ game.cardCollections[HeartsGame.PLAYER_A_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_B_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_C_TRICK] = <Card>[];
+ game.cardCollections[HeartsGame.PLAYER_D_TRICK] = Card.All;
+ game.updateScore();
+ expect(game.scores, equals([34, 42, 54, 0]));
+ });
+ });
+
+ group("Game Over", () {
+ HeartsGame game = new HeartsGame(0);
+
+ test("Has the game ended? Yes", () {
+ // Check if the game has ended. Should be yes.
+ game.scores = <int>[HeartsGame.MAX_SCORE + 5, 40, 35, 0];
+ expect(game.hasGameEnded, isTrue);
+ });
+ test("Has the game ended? No", () {
+ // Check if the game has ended. Should be no.
+ game.scores = <int>[HeartsGame.MAX_SCORE - 5, 40, 35, 0];
+ expect(game.hasGameEnded, isFalse);
+ });
+ });
+
+ // At this point, we should prepare the canonical game by setting up state and
+ // performing a single action or set of actions.
+ // Reads from a log, so we will go through logical game mechanics.
+ group("Card Manipulation", () {
+ HeartsGame game = new HeartsGame(0);
+
+ // Note: This could have been a non-file (in-memory), but it's fine to use a file too.
+ File file = new File("test/game_log_hearts_test.txt");
+ List<String> commands = file.readAsStringSync().split("\n");
+ int commandIndex = 0;
+
+ void runCommand() {
+ String c = commands[commandIndex];
+ commandIndex++;
+ if (c == "" || c[0] == "#") {
+ // Essentially, this case allows empty lines and comments.
+ runCommand();
+ } else {
+ game.gamelog.add(new HeartsCommand(c));
+ }
+ }
+
+ test("Deal Phase", () {
+ expect(game.phase, equals(HeartsPhase.Deal));
+
+ // Deal consists of 4 deal commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands.
+ List<Card> expectedAHand =
+ new List<Card>.from(Card.All.getRange(26, 26 + 5))
+ ..addAll(Card.All.getRange(13 + 5, 26));
+ List<Card> expectedBHand =
+ new List<Card>.from(Card.All.getRange(13, 13 + 5))
+ ..addAll(Card.All.getRange(39 + 5, 52));
+ List<Card> expectedCHand =
+ new List<Card>.from(Card.All.getRange(39, 39 + 5))
+ ..addAll(Card.All.getRange(0 + 5, 13));
+ List<Card> expectedDHand =
+ new List<Card>.from(Card.All.getRange(0, 0 + 5))
+ ..addAll(Card.All.getRange(26 + 5, 39));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+ });
+ test("Pass Phase", () {
+ expect(game.phase, equals(HeartsPhase.Pass));
+
+ // Pass consists of 4 pass commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands and passes.
+ List<Card> expectedAHand =
+ new List<Card>.from(Card.All.getRange(26 + 3, 26 + 5))
+ ..addAll(Card.All.getRange(13 + 5, 26));
+ List<Card> expectedBHand =
+ new List<Card>.from(Card.All.getRange(13 + 3, 13 + 5))
+ ..addAll(Card.All.getRange(39 + 5, 52));
+ List<Card> expectedCHand =
+ new List<Card>.from(Card.All.getRange(39 + 3, 39 + 5))
+ ..addAll(Card.All.getRange(0 + 5, 13));
+ List<Card> expectedDHand =
+ new List<Card>.from(Card.All.getRange(0 + 3, 0 + 5))
+ ..addAll(Card.All.getRange(26 + 5, 39));
+ List<Card> expectedAPass =
+ new List<Card>.from(Card.All.getRange(26, 26 + 3));
+ List<Card> expectedBPass =
+ new List<Card>.from(Card.All.getRange(13, 13 + 3));
+ List<Card> expectedCPass =
+ new List<Card>.from(Card.All.getRange(39, 39 + 3));
+ List<Card> expectedDPass =
+ new List<Card>.from(Card.All.getRange(0, 0 + 3));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_A_PASS],
+ equals(expectedAPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_B_PASS],
+ equals(expectedBPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_C_PASS],
+ equals(expectedCPass));
+ expect(game.cardCollections[HeartsGame.PLAYER_D_PASS],
+ equals(expectedDPass));
+ });
+ test("Take Phase", () {
+ expect(game.phase, equals(HeartsPhase.Take));
+
+ // Take consists of 4 take commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm cards in hands again.
+ // Note: I will eventually want to do a sorted comparison or set comparison instead.
+ List<Card> expectedAHand =
+ new List<Card>.from(Card.All.getRange(26 + 3, 26 + 5))
+ ..addAll(Card.All.getRange(13 + 5, 26))
+ ..addAll(Card.All.getRange(13, 13 + 3));
+ List<Card> expectedBHand =
+ new List<Card>.from(Card.All.getRange(13 + 3, 13 + 5))
+ ..addAll(Card.All.getRange(39 + 5, 52))
+ ..addAll(Card.All.getRange(39, 39 + 3));
+ List<Card> expectedCHand =
+ new List<Card>.from(Card.All.getRange(39 + 3, 39 + 5))
+ ..addAll(Card.All.getRange(0 + 5, 13))
+ ..addAll(Card.All.getRange(0, 0 + 3));
+ List<Card> expectedDHand =
+ new List<Card>.from(Card.All.getRange(0 + 3, 0 + 5))
+ ..addAll(Card.All.getRange(26 + 5, 39))
+ ..addAll(Card.All.getRange(26, 26 + 3));
+ expect(game.cardCollections[HeartsGame.PLAYER_A], equals(expectedAHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_B], equals(expectedBHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_C], equals(expectedCHand));
+ expect(game.cardCollections[HeartsGame.PLAYER_D], equals(expectedDHand));
+ });
+ test("Play Phase - Trick 1", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 1 consists of 4 play commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm the winner of the round.
+ expect(game.lastTrickTaker, equals(3),
+ reason: "Player 3 played 4 of Clubs");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4),
+ reason: "Player 3 won 1 trick.");
+ });
+ test("Play Phase - Trick 2", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 2 consists of 4 play commands.
+ runCommand();
+ runCommand();
+ runCommand();
+ runCommand();
+
+ // Confirm the winner of the round.
+ expect(game.lastTrickTaker, equals(2),
+ reason: "Player 2 played Ace of Clubs");
+ expect(game.cardCollections[HeartsGame.PLAYER_C_TRICK].length, equals(4),
+ reason: "Player 2 won 1 trick.");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4),
+ reason: "Player 3 won 1 trick.");
+ });
+ test("Play Phase - Trick 13", () {
+ expect(game.phase, equals(HeartsPhase.Play));
+
+ // Play Trick 13 consists of 44 play commands.
+ // Read line by line until the game is "over".
+ for (int i = 8; i < 52; i++) {
+ runCommand();
+ }
+
+ // Assert that hands/plays/passes are empty.
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_A + HeartsGame.OFFSET_HAND].length, equals(0),
+ reason: "Played all cards");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_B + HeartsGame.OFFSET_HAND].length, equals(0),
+ reason: "Played all cards");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_C + HeartsGame.OFFSET_HAND].length, equals(0),
+ reason: "Played all cards");
+ expect(game.cardCollections[
+ HeartsGame.PLAYER_D + HeartsGame.OFFSET_HAND].length, equals(0),
+ reason: "Played all cards");
+
+ // Check that all 52 cards are in tricks.
+ expect(game.lastTrickTaker, equals(0),
+ reason: "Player 0 won the last trick.");
+ expect(
+ game.cardCollections[HeartsGame.PLAYER_A_TRICK].length, equals(4 * 8),
+ reason: "Player 0 won 8 tricks.");
+ expect(
+ game.cardCollections[HeartsGame.PLAYER_B_TRICK].length, equals(4 * 2),
+ reason: "Player 1 won 2 tricks.");
+ expect(
+ game.cardCollections[HeartsGame.PLAYER_C_TRICK].length, equals(4 * 2),
+ reason: "Player 2 won 2 tricks.");
+ expect(game.cardCollections[HeartsGame.PLAYER_D_TRICK].length, equals(4),
+ reason: "Player 3 won 1 trick.");
+ });
+ test("Score Phase", () {
+ expect(game.phase, equals(HeartsPhase.Score));
+
+ // Check score to ensure it matches the expectation.
+ expect(game.scores, equals([21, 3, 2, 0]));
+
+ // Score consists of 4 ready commands.
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+ expect(game.allReady, isFalse);
+ runCommand();
+
+ // Back to the deal phase once everyone indicates that they are ready.
+ expect(game.phase, equals(HeartsPhase.Deal));
+ });
+ test("Score Phase - end of game", () {
+ expect(game.hasGameEnded, isFalse);
+
+ // 2nd Round: 4 deal, 4 pass, 4 take, 52 play, 4 ready
+ // Player A will shoot the moon for all remaining games (for simplicity).
+ for (int i = 0; i < 68; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([21 + 0, 3 + 26, 2 + 26, 0 + 26]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 3rd Round: 4 deal, 4 pass, 4 take, 52 play, 4 ready
+ for (int i = 0; i < 68; i++) {
+ runCommand();
+ }
+ expect(game.scores,
+ equals([21 + 0 + 0, 3 + 26 + 26, 2 + 26 + 26, 0 + 26 + 26]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 4th Round: 4 deal, 52 play, 4 ready
+ for (int i = 0; i < 60; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([
+ 21 + 0 + 0 + 0,
+ 3 + 26 + 26 + 26,
+ 2 + 26 + 26 + 26,
+ 0 + 26 + 26 + 26
+ ]));
+ expect(game.hasGameEnded, isFalse);
+
+ // 5th round: 4 deal, 4 pass, 4 take, 52 play. Game is over, so no ready phase.
+ for (int i = 0; i < 64; i++) {
+ runCommand();
+ }
+ expect(game.scores, equals([
+ 21 + 0 + 0 + 0 + 0,
+ 3 + 26 + 26 + 26 + 26,
+ 2 + 26 + 26 + 26 + 26,
+ 0 + 26 + 26 + 26 + 26
+ ]));
+ expect(game.hasGameEnded,
+ isTrue); // assumes game ends after about 100 points.
+ });
+ });
+
+ group("Card Manipulation - Error Cases", () {
+ test("Dealing - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.phase = HeartsPhase.Score;
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Dealing - missing card", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(
+ new HeartsCommand.deal(0, <Card>[new Card("fake", "not real")]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Dealing - too many cards dealt", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 15))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 5))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(5, 15))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Passing - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.pass(
+ 0, new List<Card>.from(Card.All.getRange(0, 4))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Passing - missing card", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(
+ 0, new List<Card>.from(Card.All.getRange(13, 16))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Passing - wrong number of cards", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(
+ 0, new List<Card>.from(Card.All.getRange(0, 2))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Pass;
+ game.gamelog.add(new HeartsCommand.pass(
+ 0, new List<Card>.from(Card.All.getRange(0, 4))));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Taking - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.take(3));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - wrong phase", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - missing card", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Play;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[13]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - invalid card (not 2 of clubs as first card)", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[0]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - invalid card (no penalty on first round)", () {
+ // NOTE: It is actually possible to be forced to play a penalty card on round 1.
+ // But the odds are miniscule, so this rule will be enforced.
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+ game.gamelog.add(new HeartsCommand.play(2, Card.All[26]));
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - wrong turn", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(
+ 0, new List<Card>.from(Card.All.getRange(0, 13))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(
+ 1, Card.All[13])); // player 0's turn, not player 1's.
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - invalid card (suit mismatch)", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0,
+ new List<Card>.from(Card.All.getRange(0, 12))..add(Card.All[25])));
+ game.gamelog.add(new HeartsCommand.deal(
+ 1, new List<Card>.from(Card.All.getRange(12, 25))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 2, new List<Card>.from(Card.All.getRange(26, 39))));
+ game.gamelog.add(new HeartsCommand.deal(
+ 3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog
+ .add(new HeartsCommand.play(0, Card.All[13])); // should play 12
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ test("Playing - invalid card (hearts not broken yet)", () {
+ expect(() {
+ HeartsGame game = new HeartsGame(0);
+ game.gamelog.add(new HeartsCommand.deal(0,
+ new List<Card>.from(Card.All.getRange(0, 12))..add(Card.All[38])));
+ game.gamelog.add(new HeartsCommand.deal(
+ 1, new List<Card>.from(Card.All.getRange(13, 26))));
+ game.gamelog.add(new HeartsCommand.deal(2,
+ new List<Card>.from(Card.All.getRange(26, 38))..add(Card.All[12])));
+ game.gamelog.add(new HeartsCommand.deal(
+ 3, new List<Card>.from(Card.All.getRange(39, 52))));
+ game.phase = HeartsPhase.Play;
+ game.lastTrickTaker = 0;
+ game.gamelog.add(new HeartsCommand.play(0, Card.All[1]));
+ game.gamelog.add(new HeartsCommand.play(1, Card.All[13]));
+ game.gamelog.add(new HeartsCommand.play(2, Card.All[12])); // 2 won!
+ game.gamelog.add(new HeartsCommand.play(3, Card.All[39]));
+ game.gamelog.add(new HeartsCommand.play(
+ 2, Card.All[26])); // But 2 can't lead with a hearts.
+ }, throwsA(new isInstanceOf<StateError>()));
+ });
+ });
+}