gu 7 vuotta sitten
commit
8a95ae474b
47 muutettua tiedostoa jossa 3570 lisäystä ja 0 poistoa
  1. 21 0
      LICENSE
  2. 591 0
      NumberTileGame/NumberTileGame.xcodeproj/project.pbxproj
  3. 7 0
      NumberTileGame/NumberTileGame.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  4. BIN
      NumberTileGame/NumberTileGame.xcodeproj/project.xcworkspace/xcuserdata/gulizan.xcuserdatad/UserInterfaceState.xcuserstate
  5. 96 0
      NumberTileGame/NumberTileGame.xcodeproj/xcuserdata/austinzheng.xcuserdatad/xcschemes/NumberTileGame.xcscheme
  6. 27 0
      NumberTileGame/NumberTileGame.xcodeproj/xcuserdata/austinzheng.xcuserdatad/xcschemes/xcschememanagement.plist
  7. 101 0
      NumberTileGame/NumberTileGame.xcodeproj/xcuserdata/gulizan.xcuserdatad/xcschemes/NumberTileGame.xcscheme
  8. 27 0
      NumberTileGame/NumberTileGame.xcodeproj/xcuserdata/gulizan.xcuserdatad/xcschemes/xcschememanagement.plist
  9. 47 0
      NumberTileGame/NumberTileGame/Base.lproj/Main_iPhone.storyboard
  10. 15 0
      NumberTileGame/NumberTileGame/F3HAppDelegate.h
  11. 46 0
      NumberTileGame/NumberTileGame/F3HAppDelegate.m
  12. 42 0
      NumberTileGame/NumberTileGame/F3HNumberTileGameViewController.h
  13. 252 0
      NumberTileGame/NumberTileGame/F3HNumberTileGameViewController.m
  14. 21 0
      NumberTileGame/NumberTileGame/F3HTileAppearanceProvider.h
  15. 52 0
      NumberTileGame/NumberTileGame/F3HTileAppearanceProvider.m
  16. 13 0
      NumberTileGame/NumberTileGame/F3HViewController.h
  17. 28 0
      NumberTileGame/NumberTileGame/F3HViewController.m
  18. 93 0
      NumberTileGame/NumberTileGame/Images.xcassets/AppIcon.appiconset/Contents.json
  19. 51 0
      NumberTileGame/NumberTileGame/Images.xcassets/LaunchImage.launchimage/Contents.json
  20. 55 0
      NumberTileGame/NumberTileGame/Models/F3HGameModel.h
  21. 609 0
      NumberTileGame/NumberTileGame/Models/F3HGameModel.m
  22. 28 0
      NumberTileGame/NumberTileGame/Models/F3HMergeTile.h
  23. 42 0
      NumberTileGame/NumberTileGame/Models/F3HMergeTile.m
  24. 28 0
      NumberTileGame/NumberTileGame/Models/F3HMoveOrder.h
  25. 49 0
      NumberTileGame/NumberTileGame/Models/F3HMoveOrder.m
  26. 20 0
      NumberTileGame/NumberTileGame/Models/F3HQueueCommand.h
  27. 21 0
      NumberTileGame/NumberTileGame/Models/F3HQueueCommand.m
  28. 17 0
      NumberTileGame/NumberTileGame/Models/F3HTileModel.h
  29. 27 0
      NumberTileGame/NumberTileGame/Models/F3HTileModel.m
  30. 49 0
      NumberTileGame/NumberTileGame/NumberTileGame-Info.plist
  31. 16 0
      NumberTileGame/NumberTileGame/NumberTileGame-Prefix.pch
  32. 30 0
      NumberTileGame/NumberTileGame/Views/F3HControlView.h
  33. 151 0
      NumberTileGame/NumberTileGame/Views/F3HControlView.m
  34. 34 0
      NumberTileGame/NumberTileGame/Views/F3HGameboardView.h
  35. 279 0
      NumberTileGame/NumberTileGame/Views/F3HGameboardView.m
  36. 20 0
      NumberTileGame/NumberTileGame/Views/F3HScoreView.h
  37. 52 0
      NumberTileGame/NumberTileGame/Views/F3HScoreView.m
  38. 23 0
      NumberTileGame/NumberTileGame/Views/F3HTileView.h
  39. 83 0
      NumberTileGame/NumberTileGame/Views/F3HTileView.m
  40. 2 0
      NumberTileGame/NumberTileGame/en.lproj/InfoPlist.strings
  41. 18 0
      NumberTileGame/NumberTileGame/main.m
  42. 328 0
      NumberTileGame/NumberTileGameTests/F3HModelTests.m
  43. 22 0
      NumberTileGame/NumberTileGameTests/NumberTileGameTests-Info.plist
  44. 29 0
      NumberTileGame/NumberTileGameTests/NumberTileGameTests.m
  45. 2 0
      NumberTileGame/NumberTileGameTests/en.lproj/InfoPlist.strings
  46. 6 0
      README.md
  47. BIN
      screenshots/ss1.png

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Austin Zheng
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 591 - 0
NumberTileGame/NumberTileGame.xcodeproj/project.pbxproj

@@ -0,0 +1,591 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1A3487B118DEAD6E00D021C3 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3487B018DEAD6E00D021C3 /* Foundation.framework */; };
+		1A3487B318DEAD6E00D021C3 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3487B218DEAD6E00D021C3 /* CoreGraphics.framework */; };
+		1A3487B518DEAD6E00D021C3 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3487B418DEAD6E00D021C3 /* UIKit.framework */; };
+		1A3487BB18DEAD6E00D021C3 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1A3487B918DEAD6E00D021C3 /* InfoPlist.strings */; };
+		1A3487BD18DEAD6E00D021C3 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3487BC18DEAD6E00D021C3 /* main.m */; };
+		1A3487C118DEAD6E00D021C3 /* F3HAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3487C018DEAD6E00D021C3 /* F3HAppDelegate.m */; };
+		1A3487C418DEAD6E00D021C3 /* Main_iPhone.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1A3487C218DEAD6E00D021C3 /* Main_iPhone.storyboard */; };
+		1A3487CA18DEAD6E00D021C3 /* F3HViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3487C918DEAD6E00D021C3 /* F3HViewController.m */; };
+		1A3487CC18DEAD6E00D021C3 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A3487CB18DEAD6E00D021C3 /* Images.xcassets */; };
+		1A3487D318DEAD6E00D021C3 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3487D218DEAD6E00D021C3 /* XCTest.framework */; };
+		1A3487D418DEAD6E00D021C3 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3487B018DEAD6E00D021C3 /* Foundation.framework */; };
+		1A3487D518DEAD6E00D021C3 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3487B418DEAD6E00D021C3 /* UIKit.framework */; };
+		1A3487DD18DEAD6E00D021C3 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1A3487DB18DEAD6E00D021C3 /* InfoPlist.strings */; };
+		1A3487DF18DEAD6E00D021C3 /* NumberTileGameTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3487DE18DEAD6E00D021C3 /* NumberTileGameTests.m */; };
+		1A3487F318DEBADA00D021C3 /* F3HNumberTileGameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3487F218DEBADA00D021C3 /* F3HNumberTileGameViewController.m */; };
+		1A3487F618DEBB9C00D021C3 /* F3HTileAppearanceProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3487F518DEBB9C00D021C3 /* F3HTileAppearanceProvider.m */; };
+		1A3487FE18E028A100D021C3 /* F3HModelTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3487FD18E028A100D021C3 /* F3HModelTests.m */; };
+		1A34880318E1465D00D021C3 /* F3HMoveOrder.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34880218E1465D00D021C3 /* F3HMoveOrder.m */; };
+		1A34880D18E1468400D021C3 /* F3HGameModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34880C18E1468400D021C3 /* F3HGameModel.m */; };
+		1A34881018E146D800D021C3 /* F3HTileModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34880F18E146D700D021C3 /* F3HTileModel.m */; };
+		1A34881318E1477B00D021C3 /* F3HMergeTile.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34881218E1477B00D021C3 /* F3HMergeTile.m */; };
+		1A34881E18E148DB00D021C3 /* F3HGameboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34881B18E148DB00D021C3 /* F3HGameboardView.m */; };
+		1A34881F18E148DB00D021C3 /* F3HTileView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34881D18E148DB00D021C3 /* F3HTileView.m */; };
+		1A34882218E14BF200D021C3 /* F3HQueueCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34882118E14BF200D021C3 /* F3HQueueCommand.m */; };
+		1A34882518E169AA00D021C3 /* F3HScoreView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34882418E169AA00D021C3 /* F3HScoreView.m */; };
+		1A34882818E1713E00D021C3 /* F3HControlView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A34882718E1713E00D021C3 /* F3HControlView.m */; };
+		1A92D000191A016600E35C6C /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A92CFFF191A016600E35C6C /* QuartzCore.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		1A3487D618DEAD6E00D021C3 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 1A3487A518DEAD6E00D021C3 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 1A3487AC18DEAD6E00D021C3;
+			remoteInfo = NumberTileGame;
+		};
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+		1A3487AD18DEAD6E00D021C3 /* NumberTileGame.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NumberTileGame.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		1A3487B018DEAD6E00D021C3 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+		1A3487B218DEAD6E00D021C3 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
+		1A3487B418DEAD6E00D021C3 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
+		1A3487B818DEAD6E00D021C3 /* NumberTileGame-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "NumberTileGame-Info.plist"; sourceTree = "<group>"; };
+		1A3487BA18DEAD6E00D021C3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		1A3487BC18DEAD6E00D021C3 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		1A3487BE18DEAD6E00D021C3 /* NumberTileGame-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NumberTileGame-Prefix.pch"; sourceTree = "<group>"; };
+		1A3487BF18DEAD6E00D021C3 /* F3HAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = F3HAppDelegate.h; sourceTree = "<group>"; };
+		1A3487C018DEAD6E00D021C3 /* F3HAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = F3HAppDelegate.m; sourceTree = "<group>"; };
+		1A3487C318DEAD6E00D021C3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main_iPhone.storyboard; sourceTree = "<group>"; };
+		1A3487C818DEAD6E00D021C3 /* F3HViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = F3HViewController.h; sourceTree = "<group>"; };
+		1A3487C918DEAD6E00D021C3 /* F3HViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = F3HViewController.m; sourceTree = "<group>"; };
+		1A3487CB18DEAD6E00D021C3 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
+		1A3487D118DEAD6E00D021C3 /* NumberTileGameTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NumberTileGameTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+		1A3487D218DEAD6E00D021C3 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
+		1A3487DA18DEAD6E00D021C3 /* NumberTileGameTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "NumberTileGameTests-Info.plist"; sourceTree = "<group>"; };
+		1A3487DC18DEAD6E00D021C3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+		1A3487DE18DEAD6E00D021C3 /* NumberTileGameTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NumberTileGameTests.m; sourceTree = "<group>"; };
+		1A3487F118DEBADA00D021C3 /* F3HNumberTileGameViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = F3HNumberTileGameViewController.h; sourceTree = "<group>"; };
+		1A3487F218DEBADA00D021C3 /* F3HNumberTileGameViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = F3HNumberTileGameViewController.m; sourceTree = "<group>"; };
+		1A3487F418DEBB9C00D021C3 /* F3HTileAppearanceProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = F3HTileAppearanceProvider.h; sourceTree = "<group>"; };
+		1A3487F518DEBB9C00D021C3 /* F3HTileAppearanceProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = F3HTileAppearanceProvider.m; sourceTree = "<group>"; };
+		1A3487FD18E028A100D021C3 /* F3HModelTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = F3HModelTests.m; sourceTree = "<group>"; };
+		1A34880118E1465D00D021C3 /* F3HMoveOrder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HMoveOrder.h; path = Models/F3HMoveOrder.h; sourceTree = "<group>"; };
+		1A34880218E1465D00D021C3 /* F3HMoveOrder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HMoveOrder.m; path = Models/F3HMoveOrder.m; sourceTree = "<group>"; };
+		1A34880B18E1468400D021C3 /* F3HGameModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HGameModel.h; path = Models/F3HGameModel.h; sourceTree = "<group>"; };
+		1A34880C18E1468400D021C3 /* F3HGameModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HGameModel.m; path = Models/F3HGameModel.m; sourceTree = "<group>"; };
+		1A34880E18E146D700D021C3 /* F3HTileModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HTileModel.h; path = Models/F3HTileModel.h; sourceTree = "<group>"; };
+		1A34880F18E146D700D021C3 /* F3HTileModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HTileModel.m; path = Models/F3HTileModel.m; sourceTree = "<group>"; };
+		1A34881118E1477B00D021C3 /* F3HMergeTile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HMergeTile.h; path = Models/F3HMergeTile.h; sourceTree = "<group>"; };
+		1A34881218E1477B00D021C3 /* F3HMergeTile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HMergeTile.m; path = Models/F3HMergeTile.m; sourceTree = "<group>"; };
+		1A34881A18E148DB00D021C3 /* F3HGameboardView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HGameboardView.h; path = Views/F3HGameboardView.h; sourceTree = "<group>"; };
+		1A34881B18E148DB00D021C3 /* F3HGameboardView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HGameboardView.m; path = Views/F3HGameboardView.m; sourceTree = "<group>"; };
+		1A34881C18E148DB00D021C3 /* F3HTileView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HTileView.h; path = Views/F3HTileView.h; sourceTree = "<group>"; };
+		1A34881D18E148DB00D021C3 /* F3HTileView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HTileView.m; path = Views/F3HTileView.m; sourceTree = "<group>"; };
+		1A34882018E14BF200D021C3 /* F3HQueueCommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HQueueCommand.h; path = Models/F3HQueueCommand.h; sourceTree = "<group>"; };
+		1A34882118E14BF200D021C3 /* F3HQueueCommand.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HQueueCommand.m; path = Models/F3HQueueCommand.m; sourceTree = "<group>"; };
+		1A34882318E169AA00D021C3 /* F3HScoreView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HScoreView.h; path = Views/F3HScoreView.h; sourceTree = "<group>"; };
+		1A34882418E169AA00D021C3 /* F3HScoreView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HScoreView.m; path = Views/F3HScoreView.m; sourceTree = "<group>"; };
+		1A34882618E1713E00D021C3 /* F3HControlView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = F3HControlView.h; path = Views/F3HControlView.h; sourceTree = "<group>"; };
+		1A34882718E1713E00D021C3 /* F3HControlView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = F3HControlView.m; path = Views/F3HControlView.m; sourceTree = "<group>"; };
+		1A92CFFF191A016600E35C6C /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		1A3487AA18DEAD6E00D021C3 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1A92D000191A016600E35C6C /* QuartzCore.framework in Frameworks */,
+				1A3487B318DEAD6E00D021C3 /* CoreGraphics.framework in Frameworks */,
+				1A3487B518DEAD6E00D021C3 /* UIKit.framework in Frameworks */,
+				1A3487B118DEAD6E00D021C3 /* Foundation.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1A3487CE18DEAD6E00D021C3 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1A3487D318DEAD6E00D021C3 /* XCTest.framework in Frameworks */,
+				1A3487D518DEAD6E00D021C3 /* UIKit.framework in Frameworks */,
+				1A3487D418DEAD6E00D021C3 /* Foundation.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		1A3487A418DEAD6E00D021C3 = {
+			isa = PBXGroup;
+			children = (
+				1A3487B618DEAD6E00D021C3 /* NumberTileGame */,
+				1A3487D818DEAD6E00D021C3 /* NumberTileGameTests */,
+				1A3487AF18DEAD6E00D021C3 /* Frameworks */,
+				1A3487AE18DEAD6E00D021C3 /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		1A3487AE18DEAD6E00D021C3 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				1A3487AD18DEAD6E00D021C3 /* NumberTileGame.app */,
+				1A3487D118DEAD6E00D021C3 /* NumberTileGameTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		1A3487AF18DEAD6E00D021C3 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				1A92CFFF191A016600E35C6C /* QuartzCore.framework */,
+				1A3487B018DEAD6E00D021C3 /* Foundation.framework */,
+				1A3487B218DEAD6E00D021C3 /* CoreGraphics.framework */,
+				1A3487B418DEAD6E00D021C3 /* UIKit.framework */,
+				1A3487D218DEAD6E00D021C3 /* XCTest.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		1A3487B618DEAD6E00D021C3 /* NumberTileGame */ = {
+			isa = PBXGroup;
+			children = (
+				1A3487BF18DEAD6E00D021C3 /* F3HAppDelegate.h */,
+				1A3487C018DEAD6E00D021C3 /* F3HAppDelegate.m */,
+				1A3487C218DEAD6E00D021C3 /* Main_iPhone.storyboard */,
+				1A3487C818DEAD6E00D021C3 /* F3HViewController.h */,
+				1A3487C918DEAD6E00D021C3 /* F3HViewController.m */,
+				1A3487CB18DEAD6E00D021C3 /* Images.xcassets */,
+				1A3487B718DEAD6E00D021C3 /* Supporting Files */,
+				1A3487FF18E1461F00D021C3 /* Models */,
+				1A34880018E1463E00D021C3 /* Views */,
+				1A3487F118DEBADA00D021C3 /* F3HNumberTileGameViewController.h */,
+				1A3487F218DEBADA00D021C3 /* F3HNumberTileGameViewController.m */,
+				1A3487F418DEBB9C00D021C3 /* F3HTileAppearanceProvider.h */,
+				1A3487F518DEBB9C00D021C3 /* F3HTileAppearanceProvider.m */,
+			);
+			path = NumberTileGame;
+			sourceTree = "<group>";
+		};
+		1A3487B718DEAD6E00D021C3 /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				1A3487B818DEAD6E00D021C3 /* NumberTileGame-Info.plist */,
+				1A3487B918DEAD6E00D021C3 /* InfoPlist.strings */,
+				1A3487BC18DEAD6E00D021C3 /* main.m */,
+				1A3487BE18DEAD6E00D021C3 /* NumberTileGame-Prefix.pch */,
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+		1A3487D818DEAD6E00D021C3 /* NumberTileGameTests */ = {
+			isa = PBXGroup;
+			children = (
+				1A3487DE18DEAD6E00D021C3 /* NumberTileGameTests.m */,
+				1A3487D918DEAD6E00D021C3 /* Supporting Files */,
+				1A3487FD18E028A100D021C3 /* F3HModelTests.m */,
+			);
+			path = NumberTileGameTests;
+			sourceTree = "<group>";
+		};
+		1A3487D918DEAD6E00D021C3 /* Supporting Files */ = {
+			isa = PBXGroup;
+			children = (
+				1A3487DA18DEAD6E00D021C3 /* NumberTileGameTests-Info.plist */,
+				1A3487DB18DEAD6E00D021C3 /* InfoPlist.strings */,
+			);
+			name = "Supporting Files";
+			sourceTree = "<group>";
+		};
+		1A3487FF18E1461F00D021C3 /* Models */ = {
+			isa = PBXGroup;
+			children = (
+				1A34880B18E1468400D021C3 /* F3HGameModel.h */,
+				1A34880C18E1468400D021C3 /* F3HGameModel.m */,
+				1A34880118E1465D00D021C3 /* F3HMoveOrder.h */,
+				1A34880218E1465D00D021C3 /* F3HMoveOrder.m */,
+				1A34880E18E146D700D021C3 /* F3HTileModel.h */,
+				1A34880F18E146D700D021C3 /* F3HTileModel.m */,
+				1A34881118E1477B00D021C3 /* F3HMergeTile.h */,
+				1A34881218E1477B00D021C3 /* F3HMergeTile.m */,
+				1A34882018E14BF200D021C3 /* F3HQueueCommand.h */,
+				1A34882118E14BF200D021C3 /* F3HQueueCommand.m */,
+			);
+			name = Models;
+			sourceTree = "<group>";
+		};
+		1A34880018E1463E00D021C3 /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				1A34881A18E148DB00D021C3 /* F3HGameboardView.h */,
+				1A34881B18E148DB00D021C3 /* F3HGameboardView.m */,
+				1A34881C18E148DB00D021C3 /* F3HTileView.h */,
+				1A34881D18E148DB00D021C3 /* F3HTileView.m */,
+				1A34882318E169AA00D021C3 /* F3HScoreView.h */,
+				1A34882418E169AA00D021C3 /* F3HScoreView.m */,
+				1A34882618E1713E00D021C3 /* F3HControlView.h */,
+				1A34882718E1713E00D021C3 /* F3HControlView.m */,
+			);
+			name = Views;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		1A3487AC18DEAD6E00D021C3 /* NumberTileGame */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 1A3487E218DEAD6E00D021C3 /* Build configuration list for PBXNativeTarget "NumberTileGame" */;
+			buildPhases = (
+				1A3487A918DEAD6E00D021C3 /* Sources */,
+				1A3487AA18DEAD6E00D021C3 /* Frameworks */,
+				1A3487AB18DEAD6E00D021C3 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = NumberTileGame;
+			productName = NumberTileGame;
+			productReference = 1A3487AD18DEAD6E00D021C3 /* NumberTileGame.app */;
+			productType = "com.apple.product-type.application";
+		};
+		1A3487D018DEAD6E00D021C3 /* NumberTileGameTests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 1A3487E518DEAD6E00D021C3 /* Build configuration list for PBXNativeTarget "NumberTileGameTests" */;
+			buildPhases = (
+				1A3487CD18DEAD6E00D021C3 /* Sources */,
+				1A3487CE18DEAD6E00D021C3 /* Frameworks */,
+				1A3487CF18DEAD6E00D021C3 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				1A3487D718DEAD6E00D021C3 /* PBXTargetDependency */,
+			);
+			name = NumberTileGameTests;
+			productName = NumberTileGameTests;
+			productReference = 1A3487D118DEAD6E00D021C3 /* NumberTileGameTests.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		1A3487A518DEAD6E00D021C3 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				CLASSPREFIX = F3H;
+				LastUpgradeCheck = 0820;
+				TargetAttributes = {
+					1A3487AC18DEAD6E00D021C3 = {
+						DevelopmentTeam = GYV39WAN3G;
+					};
+					1A3487D018DEAD6E00D021C3 = {
+						TestTargetID = 1A3487AC18DEAD6E00D021C3;
+					};
+				};
+			};
+			buildConfigurationList = 1A3487A818DEAD6E00D021C3 /* Build configuration list for PBXProject "NumberTileGame" */;
+			compatibilityVersion = "Xcode 3.2";
+			developmentRegion = English;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 1A3487A418DEAD6E00D021C3;
+			productRefGroup = 1A3487AE18DEAD6E00D021C3 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				1A3487AC18DEAD6E00D021C3 /* NumberTileGame */,
+				1A3487D018DEAD6E00D021C3 /* NumberTileGameTests */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		1A3487AB18DEAD6E00D021C3 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1A3487CC18DEAD6E00D021C3 /* Images.xcassets in Resources */,
+				1A3487C418DEAD6E00D021C3 /* Main_iPhone.storyboard in Resources */,
+				1A3487BB18DEAD6E00D021C3 /* InfoPlist.strings in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1A3487CF18DEAD6E00D021C3 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1A3487DD18DEAD6E00D021C3 /* InfoPlist.strings in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		1A3487A918DEAD6E00D021C3 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1A3487C118DEAD6E00D021C3 /* F3HAppDelegate.m in Sources */,
+				1A34881018E146D800D021C3 /* F3HTileModel.m in Sources */,
+				1A3487BD18DEAD6E00D021C3 /* main.m in Sources */,
+				1A3487F618DEBB9C00D021C3 /* F3HTileAppearanceProvider.m in Sources */,
+				1A34880D18E1468400D021C3 /* F3HGameModel.m in Sources */,
+				1A3487CA18DEAD6E00D021C3 /* F3HViewController.m in Sources */,
+				1A34881F18E148DB00D021C3 /* F3HTileView.m in Sources */,
+				1A3487F318DEBADA00D021C3 /* F3HNumberTileGameViewController.m in Sources */,
+				1A34881318E1477B00D021C3 /* F3HMergeTile.m in Sources */,
+				1A34882218E14BF200D021C3 /* F3HQueueCommand.m in Sources */,
+				1A34881E18E148DB00D021C3 /* F3HGameboardView.m in Sources */,
+				1A34882518E169AA00D021C3 /* F3HScoreView.m in Sources */,
+				1A34882818E1713E00D021C3 /* F3HControlView.m in Sources */,
+				1A34880318E1465D00D021C3 /* F3HMoveOrder.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		1A3487CD18DEAD6E00D021C3 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				1A3487DF18DEAD6E00D021C3 /* NumberTileGameTests.m in Sources */,
+				1A3487FE18E028A100D021C3 /* F3HModelTests.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		1A3487D718DEAD6E00D021C3 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 1A3487AC18DEAD6E00D021C3 /* NumberTileGame */;
+			targetProxy = 1A3487D618DEAD6E00D021C3 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+		1A3487B918DEAD6E00D021C3 /* InfoPlist.strings */ = {
+			isa = PBXVariantGroup;
+			children = (
+				1A3487BA18DEAD6E00D021C3 /* en */,
+			);
+			name = InfoPlist.strings;
+			sourceTree = "<group>";
+		};
+		1A3487C218DEAD6E00D021C3 /* Main_iPhone.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				1A3487C318DEAD6E00D021C3 /* Base */,
+			);
+			name = Main_iPhone.storyboard;
+			sourceTree = "<group>";
+		};
+		1A3487DB18DEAD6E00D021C3 /* InfoPlist.strings */ = {
+			isa = PBXVariantGroup;
+			children = (
+				1A3487DC18DEAD6E00D021C3 /* en */,
+			);
+			name = InfoPlist.strings;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		1A3487E018DEAD6E00D021C3 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		1A3487E118DEAD6E00D021C3 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = YES;
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		1A3487E318DEAD6E00D021C3 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				GCC_PRECOMPILE_PREFIX_HEADER = YES;
+				GCC_PREFIX_HEADER = "NumberTileGame/NumberTileGame-Prefix.pch";
+				INFOPLIST_FILE = "NumberTileGame/NumberTileGame-Info.plist";
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "f3nghuang.${PRODUCT_NAME:rfc1034identifier}";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE = "";
+				WRAPPER_EXTENSION = app;
+			};
+			name = Debug;
+		};
+		1A3487E418DEAD6E00D021C3 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;
+				CODE_SIGN_IDENTITY = "iPhone Developer";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				GCC_PRECOMPILE_PREFIX_HEADER = YES;
+				GCC_PREFIX_HEADER = "NumberTileGame/NumberTileGame-Prefix.pch";
+				INFOPLIST_FILE = "NumberTileGame/NumberTileGame-Info.plist";
+				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "f3nghuang.${PRODUCT_NAME:rfc1034identifier}";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				PROVISIONING_PROFILE = "";
+				WRAPPER_EXTENSION = app;
+			};
+			name = Release;
+		};
+		1A3487E618DEAD6E00D021C3 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/NumberTileGame.app/NumberTileGame";
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(SDKROOT)/Developer/Library/Frameworks",
+					"$(inherited)",
+					"$(DEVELOPER_FRAMEWORKS_DIR)",
+				);
+				GCC_PRECOMPILE_PREFIX_HEADER = YES;
+				GCC_PREFIX_HEADER = "NumberTileGame/NumberTileGame-Prefix.pch";
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				INFOPLIST_FILE = "NumberTileGameTests/NumberTileGameTests-Info.plist";
+				PRODUCT_BUNDLE_IDENTIFIER = "f3nghuang.${PRODUCT_NAME:rfc1034identifier}";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TEST_HOST = "$(BUNDLE_LOADER)";
+				WRAPPER_EXTENSION = xctest;
+			};
+			name = Debug;
+		};
+		1A3487E718DEAD6E00D021C3 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/NumberTileGame.app/NumberTileGame";
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(SDKROOT)/Developer/Library/Frameworks",
+					"$(inherited)",
+					"$(DEVELOPER_FRAMEWORKS_DIR)",
+				);
+				GCC_PRECOMPILE_PREFIX_HEADER = YES;
+				GCC_PREFIX_HEADER = "NumberTileGame/NumberTileGame-Prefix.pch";
+				INFOPLIST_FILE = "NumberTileGameTests/NumberTileGameTests-Info.plist";
+				PRODUCT_BUNDLE_IDENTIFIER = "f3nghuang.${PRODUCT_NAME:rfc1034identifier}";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TEST_HOST = "$(BUNDLE_LOADER)";
+				WRAPPER_EXTENSION = xctest;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		1A3487A818DEAD6E00D021C3 /* Build configuration list for PBXProject "NumberTileGame" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1A3487E018DEAD6E00D021C3 /* Debug */,
+				1A3487E118DEAD6E00D021C3 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		1A3487E218DEAD6E00D021C3 /* Build configuration list for PBXNativeTarget "NumberTileGame" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1A3487E318DEAD6E00D021C3 /* Debug */,
+				1A3487E418DEAD6E00D021C3 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		1A3487E518DEAD6E00D021C3 /* Build configuration list for PBXNativeTarget "NumberTileGameTests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				1A3487E618DEAD6E00D021C3 /* Debug */,
+				1A3487E718DEAD6E00D021C3 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 1A3487A518DEAD6E00D021C3 /* Project object */;
+}

+ 7 - 0
NumberTileGame/NumberTileGame.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:">
+   </FileRef>
+</Workspace>

BIN
NumberTileGame/NumberTileGame.xcodeproj/project.xcworkspace/xcuserdata/gulizan.xcuserdatad/UserInterfaceState.xcuserstate


+ 96 - 0
NumberTileGame/NumberTileGame.xcodeproj/xcuserdata/austinzheng.xcuserdatad/xcschemes/NumberTileGame.xcscheme

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0510"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "1A3487AC18DEAD6E00D021C3"
+               BuildableName = "NumberTileGame.app"
+               BlueprintName = "NumberTileGame"
+               ReferencedContainer = "container:NumberTileGame.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      buildConfiguration = "Debug">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "1A3487D018DEAD6E00D021C3"
+               BuildableName = "NumberTileGameTests.xctest"
+               BlueprintName = "NumberTileGameTests"
+               ReferencedContainer = "container:NumberTileGame.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "1A3487AC18DEAD6E00D021C3"
+            BuildableName = "NumberTileGame.app"
+            BlueprintName = "NumberTileGame"
+            ReferencedContainer = "container:NumberTileGame.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </TestAction>
+   <LaunchAction
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Debug"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "1A3487AC18DEAD6E00D021C3"
+            BuildableName = "NumberTileGame.app"
+            BlueprintName = "NumberTileGame"
+            ReferencedContainer = "container:NumberTileGame.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      buildConfiguration = "Release"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "1A3487AC18DEAD6E00D021C3"
+            BuildableName = "NumberTileGame.app"
+            BlueprintName = "NumberTileGame"
+            ReferencedContainer = "container:NumberTileGame.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 27 - 0
NumberTileGame/NumberTileGame.xcodeproj/xcuserdata/austinzheng.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>SchemeUserState</key>
+	<dict>
+		<key>NumberTileGame.xcscheme</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>0</integer>
+		</dict>
+	</dict>
+	<key>SuppressBuildableAutocreation</key>
+	<dict>
+		<key>1A3487AC18DEAD6E00D021C3</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+		<key>1A3487D018DEAD6E00D021C3</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 101 - 0
NumberTileGame/NumberTileGame.xcodeproj/xcuserdata/gulizan.xcuserdatad/xcschemes/NumberTileGame.xcscheme

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "0820"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "1A3487AC18DEAD6E00D021C3"
+               BuildableName = "NumberTileGame.app"
+               BlueprintName = "NumberTileGame"
+               ReferencedContainer = "container:NumberTileGame.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "1A3487D018DEAD6E00D021C3"
+               BuildableName = "NumberTileGameTests.xctest"
+               BlueprintName = "NumberTileGameTests"
+               ReferencedContainer = "container:NumberTileGame.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "1A3487AC18DEAD6E00D021C3"
+            BuildableName = "NumberTileGame.app"
+            BlueprintName = "NumberTileGame"
+            ReferencedContainer = "container:NumberTileGame.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "1A3487AC18DEAD6E00D021C3"
+            BuildableName = "NumberTileGame.app"
+            BlueprintName = "NumberTileGame"
+            ReferencedContainer = "container:NumberTileGame.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "1A3487AC18DEAD6E00D021C3"
+            BuildableName = "NumberTileGame.app"
+            BlueprintName = "NumberTileGame"
+            ReferencedContainer = "container:NumberTileGame.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 27 - 0
NumberTileGame/NumberTileGame.xcodeproj/xcuserdata/gulizan.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>SchemeUserState</key>
+	<dict>
+		<key>NumberTileGame.xcscheme</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>0</integer>
+		</dict>
+	</dict>
+	<key>SuppressBuildableAutocreation</key>
+	<dict>
+		<key>1A3487AC18DEAD6E00D021C3</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+		<key>1A3487D018DEAD6E00D021C3</key>
+		<dict>
+			<key>primary</key>
+			<true/>
+		</dict>
+	</dict>
+</dict>
+</plist>

+ 47 - 0
NumberTileGame/NumberTileGame/Base.lproj/Main_iPhone.storyboard

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="vXZ-lx-hvc">
+    <device id="retina4_7" orientation="portrait">
+        <adaptation id="fullscreen"/>
+    </device>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="ufC-wZ-h7g">
+            <objects>
+                <viewController id="vXZ-lx-hvc" customClass="F3HViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="jyV-Pf-zRb"/>
+                        <viewControllerLayoutGuide type="bottom" id="2fi-mo-0CV"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="kh9-bI-dsS">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                        <subviews>
+                            <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="16F-gs-6Ou">
+                                <rect key="frame" x="79" y="269" width="163" height="30"/>
+                                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+                                <state key="normal" title="Play Game">
+                                    <color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                                </state>
+                                <connections>
+                                    <action selector="playGameButtonTapped:" destination="vXZ-lx-hvc" eventType="touchUpInside" id="PGL-Ig-Pfo"/>
+                                </connections>
+                            </button>
+                        </subviews>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="x5A-6p-PRh" sceneMemberID="firstResponder"/>
+            </objects>
+        </scene>
+    </scenes>
+    <simulatedMetricsContainer key="defaultSimulatedMetrics">
+        <simulatedStatusBarMetrics key="statusBar"/>
+        <simulatedOrientationMetrics key="orientation"/>
+        <simulatedScreenMetrics key="destination" type="retina4_7.fullscreen"/>
+    </simulatedMetricsContainer>
+</document>

+ 15 - 0
NumberTileGame/NumberTileGame/F3HAppDelegate.h

@@ -0,0 +1,15 @@
+//
+//  F3HAppDelegate.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+@interface F3HAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property (strong, nonatomic) UIWindow *window;
+
+@end

+ 46 - 0
NumberTileGame/NumberTileGame/F3HAppDelegate.m

@@ -0,0 +1,46 @@
+//
+//  F3HAppDelegate.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import "F3HAppDelegate.h"
+
+@implementation F3HAppDelegate
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+    // Override point for customization after application launch.
+    return YES;
+}
+							
+- (void)applicationWillResignActive:(UIApplication *)application
+{
+  // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
+  // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application
+{
+  // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
+  // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application
+{
+  // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application
+{
+  // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application
+{
+  // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+}
+
+@end

+ 42 - 0
NumberTileGame/NumberTileGame/F3HNumberTileGameViewController.h

@@ -0,0 +1,42 @@
+//
+//  F3HNumberTileGameViewController.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol F3HNumberTileGameProtocol <NSObject>
+/*!
+ Inform the delegate that the user completed a game.
+
+ \param didWin   YES if the player won, NO otherwise
+ \param score    the final score the player achieved
+ */
+- (void)gameFinishedWithVictory:(BOOL)didWin score:(NSInteger)score;
+@end
+
+@interface F3HNumberTileGameViewController : UIViewController
+
+@property (nonatomic, weak) id<F3HNumberTileGameProtocol>delegate;
+
+/*!
+ Return an instance of the number tile game view controller.
+
+ \param dimension                how many tiles wide and high the gameboard should be
+ \param threshold                the tile value the player must achieve to win the game (e.g. 2048)
+ \param backgroundColor          the background color of the gameboard
+ \param scoreModuleEnabled       if YES, the score module will be visible
+ \param buttonControlsEnabled    if YES, the directional touch controls will be visible
+ \param swipeControlsEnabled     if YES, performing swipe gestures will advance the game (not implemented yet)
+ */
++ (instancetype)numberTileGameWithDimension:(NSUInteger)dimension
+                               winThreshold:(NSUInteger)threshold
+                            backgroundColor:(UIColor *)backgroundColor
+                                scoreModule:(BOOL)scoreModuleEnabled
+                             buttonControls:(BOOL)buttonControlsEnabled
+                              swipeControls:(BOOL)swipeControlsEnabled;
+
+@end

+ 252 - 0
NumberTileGame/NumberTileGame/F3HNumberTileGameViewController.m

@@ -0,0 +1,252 @@
+//
+//  F3HNumberTileGameViewController.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import "F3HNumberTileGameViewController.h"
+
+#import "F3HGameboardView.h"
+#import "F3HControlView.h"
+#import "F3HScoreView.h"
+#import "F3HGameModel.h"
+
+#define ELEMENT_SPACING 10
+
+@interface F3HNumberTileGameViewController () <F3HGameModelProtocol, F3HControlViewProtocol>
+
+@property (nonatomic, strong) F3HGameboardView *gameboard;
+@property (nonatomic, strong) F3HGameModel *model;
+@property (nonatomic, strong) F3HScoreView *scoreView;
+@property (nonatomic, strong) F3HControlView *controlView;
+
+@property (nonatomic) BOOL useScoreView;
+@property (nonatomic) BOOL useControlView;
+
+@property (nonatomic) NSUInteger dimension;
+@property (nonatomic) NSUInteger threshold;
+@end
+
+@implementation F3HNumberTileGameViewController
+
++ (instancetype)numberTileGameWithDimension:(NSUInteger)dimension
+                               winThreshold:(NSUInteger)threshold
+                            backgroundColor:(UIColor *)backgroundColor
+                                scoreModule:(BOOL)scoreModuleEnabled
+                             buttonControls:(BOOL)buttonControlsEnabled
+                              swipeControls:(BOOL)swipeControlsEnabled {
+    F3HNumberTileGameViewController *c = [[self class] new];
+    c.dimension = dimension > 2 ? dimension : 2;
+    c.threshold = threshold > 8 ? threshold : 8;
+    c.useScoreView = scoreModuleEnabled;
+    c.useControlView = buttonControlsEnabled;
+    c.view.backgroundColor = backgroundColor ?: [UIColor whiteColor];
+    if (swipeControlsEnabled) {
+        [c setupSwipeControls];
+    }
+    return c;
+}
+
+
+#pragma mark - Controller Lifecycle
+
+- (void)setupSwipeControls {
+    UISwipeGestureRecognizer *upSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
+                                                                                  action:@selector(upButtonTapped)];
+    upSwipe.numberOfTouchesRequired = 1;
+    upSwipe.direction = UISwipeGestureRecognizerDirectionUp;
+    [self.view addGestureRecognizer:upSwipe];
+    
+    UISwipeGestureRecognizer *downSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
+                                                                                    action:@selector(downButtonTapped)];
+    downSwipe.numberOfTouchesRequired = 1;
+    downSwipe.direction = UISwipeGestureRecognizerDirectionDown;
+    [self.view addGestureRecognizer:downSwipe];
+    
+    UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
+                                                                                    action:@selector(leftButtonTapped)];
+    leftSwipe.numberOfTouchesRequired = 1;
+    leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
+    [self.view addGestureRecognizer:leftSwipe];
+    
+    UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
+                                                                                     action:@selector(rightButtonTapped)];
+    rightSwipe.numberOfTouchesRequired = 1;
+    rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
+    [self.view addGestureRecognizer:rightSwipe];
+}
+
+- (void)setupGame {
+    F3HScoreView *scoreView;
+    F3HControlView *controlView;
+    
+    CGFloat totalHeight = 0;
+    
+    // Set up score view
+    if (self.useScoreView) {
+        scoreView = [F3HScoreView scoreViewWithCornerRadius:6
+                                            backgroundColor:[UIColor darkGrayColor]
+                                                  textColor:[UIColor whiteColor]
+                                                   textFont:[UIFont fontWithName:@"HelveticaNeue-Bold" size:16]];
+        totalHeight += (ELEMENT_SPACING + scoreView.bounds.size.height);
+        self.scoreView = scoreView;
+    }
+    
+    // Set up control view
+    if (self.useControlView) {
+        controlView = [F3HControlView controlViewWithCornerRadius:6
+                                                  backgroundColor:[UIColor blackColor]
+                                                  movementButtons:YES
+                                                       exitButton:NO
+                                                         delegate:self];
+        totalHeight += (ELEMENT_SPACING + controlView.bounds.size.height);
+        self.controlView = controlView;
+    }
+    
+    // Create gameboard
+    CGFloat padding = (self.dimension > 5) ? 3.0 : 6.0;
+    CGFloat cellWidth = floorf((230 - padding*(self.dimension+1))/((float)self.dimension));
+    if (cellWidth < 30) {
+        cellWidth = 30;
+    }
+    F3HGameboardView *gameboard = [F3HGameboardView gameboardWithDimension:self.dimension
+                                                                 cellWidth:cellWidth
+                                                               cellPadding:padding
+                                                              cornerRadius:6
+                                                           backgroundColor:[UIColor blackColor]
+                                                           foregroundColor:[UIColor darkGrayColor]];
+    totalHeight += gameboard.bounds.size.height;
+    
+    // Calculate heights
+    CGFloat currentTop = 0.5*(self.view.bounds.size.height - totalHeight);
+    if (currentTop < 0) {
+        currentTop = 0;
+    }
+    
+    if (self.useScoreView) {
+        CGRect scoreFrame = scoreView.frame;
+        scoreFrame.origin.x = 0.5*(self.view.bounds.size.width - scoreFrame.size.width);
+        scoreFrame.origin.y = currentTop;
+        scoreView.frame = scoreFrame;
+        [self.view addSubview:scoreView];
+        currentTop += (scoreFrame.size.height + ELEMENT_SPACING);
+    }
+    
+    CGRect gameboardFrame = gameboard.frame;
+    gameboardFrame.origin.x = 0.5*(self.view.bounds.size.width - gameboardFrame.size.width);
+    gameboardFrame.origin.y = currentTop;
+    gameboard.frame = gameboardFrame;
+    [self.view addSubview:gameboard];
+    currentTop += (gameboardFrame.size.height + ELEMENT_SPACING);
+    
+    if (self.useControlView) {
+        CGRect controlFrame = controlView.frame;
+        controlFrame.origin.x = 0.5*(self.view.bounds.size.width - controlFrame.size.width);
+        controlFrame.origin.y = currentTop;
+        controlView.frame = controlFrame;
+        [self.view addSubview:controlView];
+    }
+    
+    self.gameboard = gameboard;
+    
+    // Create mode;
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:self.dimension
+                                                      winValue:self.threshold
+                                                      delegate:self];
+    [model insertAtRandomLocationTileWithValue:2];
+    [model insertAtRandomLocationTileWithValue:2];
+    self.model = model;
+}
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+    [self setupGame];
+}
+
+
+#pragma mark - Private API
+
+- (void)followUp {
+    // This is the earliest point the user can win
+    if ([self.model userHasWon]) {
+        [self.delegate gameFinishedWithVictory:YES score:self.model.score];
+        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Victory!" message:@"You won!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
+        [alert show];
+    }
+    else {
+        NSInteger rand = arc4random_uniform(10);
+        if (rand == 1) {
+            [self.model insertAtRandomLocationTileWithValue:4];
+        }
+        else {
+            [self.model insertAtRandomLocationTileWithValue:2];
+        }
+        // At this point, the user may lose
+        if ([self.model userHasLost]) {
+            [self.delegate gameFinishedWithVictory:NO score:self.model.score];
+            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Defeat!" message:@"You lost..." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
+            [alert show];
+        }
+    }
+}
+
+
+#pragma mark - Model Protocol
+
+- (void)moveTileFromIndexPath:(NSIndexPath *)fromPath toIndexPath:(NSIndexPath *)toPath newValue:(NSUInteger)value {
+    [self.gameboard moveTileAtIndexPath:fromPath toIndexPath:toPath withValue:value];
+}
+
+- (void)moveTileOne:(NSIndexPath *)startA tileTwo:(NSIndexPath *)startB toIndexPath:(NSIndexPath *)end newValue:(NSUInteger)value {
+    [self.gameboard moveTileOne:startA tileTwo:startB toIndexPath:end withValue:value];
+}
+
+- (void)insertTileAtIndexPath:(NSIndexPath *)path value:(NSUInteger)value {
+    [self.gameboard insertTileAtIndexPath:path withValue:value];
+}
+
+- (void)scoreChanged:(NSInteger)newScore {
+    self.scoreView.score = newScore;
+}
+
+
+#pragma mark - Control View Protocol
+
+- (void)upButtonTapped {
+    [self.model performMoveInDirection:F3HMoveDirectionUp completionBlock:^(BOOL changed) {
+        if (changed) [self followUp];
+    }];
+}
+
+- (void)downButtonTapped {
+    [self.model performMoveInDirection:F3HMoveDirectionDown completionBlock:^(BOOL changed) {
+        if (changed) [self followUp];
+    }];
+}
+
+- (void)leftButtonTapped {
+    [self.model performMoveInDirection:F3HMoveDirectionLeft completionBlock:^(BOOL changed) {
+        if (changed) [self followUp];
+    }];
+}
+
+- (void)rightButtonTapped {
+    [self.model performMoveInDirection:F3HMoveDirectionRight completionBlock:^(BOOL changed) {
+        if (changed) [self followUp];
+    }];
+}
+
+- (void)resetButtonTapped {
+    [self.gameboard reset];
+    [self.model reset];
+    [self.model insertAtRandomLocationTileWithValue:2];
+    [self.model insertAtRandomLocationTileWithValue:2];
+}
+
+- (void)exitButtonTapped {
+    [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+@end

+ 21 - 0
NumberTileGame/NumberTileGame/F3HTileAppearanceProvider.h

@@ -0,0 +1,21 @@
+//
+//  F3HTileAppearanceProvider.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import <Foundation/Foundation.h>
+
+@protocol F3HTileAppearanceProviderProtocol <NSObject>
+
+- (UIColor *)tileColorForValue:(NSUInteger)value;
+- (UIColor *)numberColorForValue:(NSUInteger)value;
+- (UIFont *)fontForNumbers;
+
+@end
+
+@interface F3HTileAppearanceProvider : NSObject <F3HTileAppearanceProviderProtocol>
+
+@end

+ 52 - 0
NumberTileGame/NumberTileGame/F3HTileAppearanceProvider.m

@@ -0,0 +1,52 @@
+//
+//  F3HTileAppearanceProvider.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import "F3HTileAppearanceProvider.h"
+
+@implementation F3HTileAppearanceProvider
+
+- (UIColor *)tileColorForValue:(NSUInteger)value {
+    switch (value) {
+        case 2:
+            return [UIColor colorWithRed:238./255. green:228./255. blue:218./255. alpha:1];
+        case 4:
+            return [UIColor colorWithRed:237./255. green:224./255. blue:200./255. alpha:1];
+        case 8:
+            return [UIColor colorWithRed:242./255. green:177./255. blue:121./255. alpha:1];
+        case 16:
+            return [UIColor colorWithRed:245./255. green:149./255. blue:99./255. alpha:1];
+        case 32:
+            return [UIColor colorWithRed:246./255. green:124./255. blue:95./255. alpha:1];
+        case 64:
+            return [UIColor colorWithRed:246./255. green:94./255. blue:59./255. alpha:1];
+        case 128:
+        case 256:
+        case 512:
+        case 1024:
+        case 2048:
+            return [UIColor colorWithRed:237./255. green:207./255. blue:114./255. alpha:1];
+        default:
+            return [UIColor whiteColor];
+    }
+}
+
+- (UIColor *)numberColorForValue:(NSUInteger)value {
+    switch (value) {
+        case 2:
+        case 4:
+            return [UIColor colorWithRed:119./255. green:110./255. blue:101./255. alpha:1];
+        default:
+            return [UIColor whiteColor];
+    }
+}
+
+- (UIFont *)fontForNumbers {
+    return [UIFont fontWithName:@"HelveticaNeue-Bold" size:20];
+}
+
+@end

+ 13 - 0
NumberTileGame/NumberTileGame/F3HViewController.h

@@ -0,0 +1,13 @@
+//
+//  F3HViewController.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+@interface F3HViewController : UIViewController
+
+@end

+ 28 - 0
NumberTileGame/NumberTileGame/F3HViewController.m

@@ -0,0 +1,28 @@
+//
+//  F3HViewController.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import "F3HViewController.h"
+
+#import "F3HNumberTileGameViewController.h"
+
+@interface F3HViewController ()
+@end
+
+@implementation F3HViewController
+
+- (IBAction)playGameButtonTapped:(id)sender {
+    F3HNumberTileGameViewController *c = [F3HNumberTileGameViewController numberTileGameWithDimension:4
+                                                                                         winThreshold:2048
+                                                                                      backgroundColor:[UIColor whiteColor]
+                                                                                          scoreModule:YES
+                                                                                       buttonControls:NO
+                                                                                        swipeControls:YES];
+    [self presentViewController:c animated:YES completion:nil];
+}
+
+@end

+ 93 - 0
NumberTileGame/NumberTileGame/Images.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,93 @@
+{
+  "images" : [
+    {
+      "idiom" : "iphone",
+      "size" : "20x20",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "20x20",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "29x29",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "40x40",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "iphone",
+      "size" : "60x60",
+      "scale" : "3x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "20x20",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "20x20",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "29x29",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "29x29",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "40x40",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "40x40",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "76x76",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "76x76",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "size" : "83.5x83.5",
+      "scale" : "2x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 51 - 0
NumberTileGame/NumberTileGame/Images.xcassets/LaunchImage.launchimage/Contents.json

@@ -0,0 +1,51 @@
+{
+  "images" : [
+    {
+      "orientation" : "portrait",
+      "idiom" : "iphone",
+      "extent" : "full-screen",
+      "minimum-system-version" : "7.0",
+      "scale" : "2x"
+    },
+    {
+      "orientation" : "portrait",
+      "idiom" : "iphone",
+      "extent" : "full-screen",
+      "minimum-system-version" : "7.0",
+      "subtype" : "retina4",
+      "scale" : "2x"
+    },
+    {
+      "orientation" : "portrait",
+      "idiom" : "ipad",
+      "extent" : "full-screen",
+      "minimum-system-version" : "7.0",
+      "scale" : "1x"
+    },
+    {
+      "orientation" : "landscape",
+      "idiom" : "ipad",
+      "extent" : "full-screen",
+      "minimum-system-version" : "7.0",
+      "scale" : "1x"
+    },
+    {
+      "orientation" : "portrait",
+      "idiom" : "ipad",
+      "extent" : "full-screen",
+      "minimum-system-version" : "7.0",
+      "scale" : "2x"
+    },
+    {
+      "orientation" : "landscape",
+      "idiom" : "ipad",
+      "extent" : "full-screen",
+      "minimum-system-version" : "7.0",
+      "scale" : "2x"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 55 - 0
NumberTileGame/NumberTileGame/Models/F3HGameModel.h

@@ -0,0 +1,55 @@
+//
+//  F3HGameModel.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/23/14.
+//
+//
+
+#import <Foundation/Foundation.h>
+
+typedef enum {
+    F3HMoveDirectionUp = 0,
+    F3HMoveDirectionDown,
+    F3HMoveDirectionLeft,
+    F3HMoveDirectionRight
+} F3HMoveDirection;
+
+@protocol F3HGameModelProtocol
+
+- (void)scoreChanged:(NSInteger)newScore;
+
+- (void)moveTileFromIndexPath:(NSIndexPath *)fromPath
+                  toIndexPath:(NSIndexPath *)toPath
+                     newValue:(NSUInteger)value;
+- (void)moveTileOne:(NSIndexPath *)startA
+            tileTwo:(NSIndexPath *)startB
+        toIndexPath:(NSIndexPath *)end
+           newValue:(NSUInteger)value;
+- (void)insertTileAtIndexPath:(NSIndexPath *)path
+                        value:(NSUInteger)value;
+
+@end
+
+@interface F3HGameModel : NSObject
+
+@property (nonatomic, readonly) NSInteger score;
+
++ (instancetype)gameModelWithDimension:(NSUInteger)dimension
+                              winValue:(NSUInteger)value
+                              delegate:(id<F3HGameModelProtocol>)delegate;
+
+- (void)reset;
+
+- (void)insertAtRandomLocationTileWithValue:(NSUInteger)value;
+
+- (void)insertTileWithValue:(NSUInteger)value
+                atIndexPath:(NSIndexPath *)path;
+
+- (void)performMoveInDirection:(F3HMoveDirection)direction
+               completionBlock:(void(^)(BOOL))completion;
+
+- (BOOL)userHasLost;
+- (NSIndexPath *)userHasWon;
+
+@end

+ 609 - 0
NumberTileGame/NumberTileGame/Models/F3HGameModel.m

@@ -0,0 +1,609 @@
+//
+//  F3HGameModel.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/23/14.
+//
+//
+
+#import "F3HGameModel.h"
+
+#import "F3HMoveOrder.h"
+#import "F3HTileModel.h"
+#import "F3HMergeTile.h"
+#import "F3HQueueCommand.h"
+
+// Command queue
+#define MAX_COMMANDS      100
+#define QUEUE_DELAY       0.3
+
+@interface F3HGameModel ()
+
+@property (nonatomic, weak) id<F3HGameModelProtocol> delegate;
+
+@property (nonatomic, strong) NSMutableArray *gameState;
+
+@property (nonatomic) NSUInteger dimension;
+@property (nonatomic) NSUInteger winValue;
+
+@property (nonatomic, strong) NSMutableArray *commandQueue;
+@property (nonatomic, strong) NSTimer *queueTimer;
+
+@property (nonatomic, readwrite) NSInteger score;
+
+@end
+
+@implementation F3HGameModel
+
++ (instancetype)gameModelWithDimension:(NSUInteger)dimension
+                              winValue:(NSUInteger)value
+                              delegate:(id<F3HGameModelProtocol>)delegate {
+    F3HGameModel *model = [F3HGameModel new];
+    model.dimension = dimension;
+    model.winValue = value;
+    model.delegate = delegate;
+    [model reset];
+    return model;
+}
+
+- (void)reset {
+    self.score = 0;
+    self.gameState = nil;
+    [self.commandQueue removeAllObjects];
+    [self.queueTimer invalidate];
+    self.queueTimer = nil;
+}
+
+#pragma mark - Insertion API
+
+- (void)insertAtRandomLocationTileWithValue:(NSUInteger)value {
+    // Check if gameboard is full
+    BOOL emptySpotFound = NO;
+    for (NSInteger i=0; i<[self.gameState count]; i++) {
+        if (((F3HTileModel *) self.gameState[i]).empty) {
+            emptySpotFound = YES;
+            break;
+        }
+    }
+    if (!emptySpotFound) {
+        // Board is full, we will never be able to insert a tile
+        return;
+    }
+    // Yes, this could run forever. Given the size of any practical gameboard, I don't think it's likely.
+    NSInteger row = 0;
+    BOOL shouldExit = NO;
+    while (YES) {
+        row = arc4random_uniform((uint32_t)self.dimension);
+        // Check if row has any empty spots in column
+        for (NSInteger i=0; i<self.dimension; i++) {
+            if ([self tileForIndexPath:[NSIndexPath indexPathForRow:row inSection:i]].empty) {
+                shouldExit = YES;
+                break;
+            }
+        }
+        if (shouldExit) {
+            break;
+        }
+    }
+    NSInteger column = 0;
+    shouldExit = NO;
+    while (YES) {
+        column = arc4random_uniform((uint32_t)self.dimension);
+        if ([self tileForIndexPath:[NSIndexPath indexPathForRow:row inSection:column]].empty) {
+            shouldExit = YES;
+            break;
+        }
+        if (shouldExit) {
+            break;
+        }
+    }
+    [self insertTileWithValue:value atIndexPath:[NSIndexPath indexPathForRow:row inSection:column]];
+}
+
+// Insert a tile (used by the game to add new tiles to the board)
+- (void)insertTileWithValue:(NSUInteger)value
+                atIndexPath:(NSIndexPath *)path {
+    if (![self tileForIndexPath:path].empty) {
+        return;
+    }
+    F3HTileModel *tile = [self tileForIndexPath:path];
+    tile.empty = NO;
+    tile.value = value;
+    [self.delegate insertTileAtIndexPath:path value:value];
+}
+
+
+#pragma mark - Movement API
+
+// Perform a user-initiated move in one of four directions
+- (void)performMoveInDirection:(F3HMoveDirection)direction
+               completionBlock:(void(^)(BOOL))completion {
+    [self queueCommand:[F3HQueueCommand commandWithDirection:direction completionBlock:completion]];
+}
+
+- (BOOL)performUpMove {
+    BOOL atLeastOneMove = NO;
+    
+    // Examine each column, left to right ([]-->[]-->[])
+    for (NSInteger column = 0; column<self.dimension; column++) {
+        NSMutableArray *thisColumnTiles = [NSMutableArray arrayWithCapacity:self.dimension];
+        for (NSInteger row = 0; row<self.dimension; row++) {
+            [thisColumnTiles addObject:[self tileForIndexPath:[NSIndexPath indexPathForRow:row inSection:column]]];
+        }
+        NSArray *ordersArray = [self mergeGroup:thisColumnTiles];
+        if ([ordersArray count] > 0) {
+            atLeastOneMove = YES;
+            for (NSInteger i=0; i<[ordersArray count]; i++) {
+                F3HMoveOrder *order = ordersArray[i];
+                if (order.doubleMove) {
+                    // Update internal model
+                    NSIndexPath *source1Path = [NSIndexPath indexPathForRow:order.source1 inSection:column];
+                    NSIndexPath *source2Path = [NSIndexPath indexPathForRow:order.source2 inSection:column];
+                    NSIndexPath *destinationPath = [NSIndexPath indexPathForRow:order.destination inSection:column];
+                    
+                    F3HTileModel *source1Tile = [self tileForIndexPath:source1Path];
+                    source1Tile.empty = YES;
+                    F3HTileModel *source2Tile = [self tileForIndexPath:source2Path];
+                    source2Tile.empty = YES;
+                    F3HTileModel *destinationTile = [self tileForIndexPath:destinationPath];
+                    destinationTile.empty = NO;
+                    destinationTile.value = order.value;
+                    
+                    // Update delegate
+                    [self.delegate moveTileOne:source1Path
+                                       tileTwo:source2Path
+                                   toIndexPath:destinationPath
+                                      newValue:order.value];
+                }
+                else {
+                    // Update internal model
+                    NSIndexPath *sourcePath = [NSIndexPath indexPathForRow:order.source1 inSection:column];
+                    NSIndexPath *destinationPath = [NSIndexPath indexPathForRow:order.destination inSection:column];
+                    
+                    F3HTileModel *sourceTile = [self tileForIndexPath:sourcePath];
+                    sourceTile.empty = YES;
+                    F3HTileModel *destinationTile = [self tileForIndexPath:destinationPath];
+                    destinationTile.empty = NO;
+                    destinationTile.value = order.value;
+                    
+                    // Update delegate
+                    [self.delegate moveTileFromIndexPath:sourcePath
+                                             toIndexPath:destinationPath
+                                                newValue:order.value];
+                }
+            }
+        }
+    }
+    return atLeastOneMove;
+}
+
+- (BOOL)performDownMove {
+    BOOL atLeastOneMove = NO;
+    
+    // Examine each column, left to right ([]-->[]-->[])
+    for (NSInteger column = 0; column<self.dimension; column++) {
+        NSMutableArray *thisColumnTiles = [NSMutableArray arrayWithCapacity:self.dimension];
+        for (NSInteger row = (self.dimension - 1); row >= 0; row--) {
+            [thisColumnTiles addObject:[self tileForIndexPath:[NSIndexPath indexPathForRow:row inSection:column]]];
+        }
+        NSArray *ordersArray = [self mergeGroup:thisColumnTiles];
+        if ([ordersArray count] > 0) {
+            atLeastOneMove = YES;
+            for (NSInteger i=0; i<[ordersArray count]; i++) {
+                F3HMoveOrder *order = ordersArray[i];
+                NSInteger dim = self.dimension - 1;
+                if (order.doubleMove) {
+                    // Update internal model
+                    NSIndexPath *source1Path = [NSIndexPath indexPathForRow:(dim - order.source1) inSection:column];
+                    NSIndexPath *source2Path = [NSIndexPath indexPathForRow:(dim - order.source2) inSection:column];
+                    NSIndexPath *destinationPath = [NSIndexPath indexPathForRow:(dim - order.destination) inSection:column];
+                    
+                    F3HTileModel *source1Tile = [self tileForIndexPath:source1Path];
+                    source1Tile.empty = YES;
+                    F3HTileModel *source2Tile = [self tileForIndexPath:source2Path];
+                    source2Tile.empty = YES;
+                    F3HTileModel *destinationTile = [self tileForIndexPath:destinationPath];
+                    destinationTile.empty = NO;
+                    destinationTile.value = order.value;
+                    
+                    // Update delegate
+                    [self.delegate moveTileOne:source1Path
+                                       tileTwo:source2Path
+                                   toIndexPath:destinationPath
+                                      newValue:order.value];
+                }
+                else {
+                    // Update internal model
+                    NSIndexPath *sourcePath = [NSIndexPath indexPathForRow:(dim - order.source1) inSection:column];
+                    NSIndexPath *destinationPath = [NSIndexPath indexPathForRow:(dim - order.destination) inSection:column];
+                    
+                    F3HTileModel *sourceTile = [self tileForIndexPath:sourcePath];
+                    sourceTile.empty = YES;
+                    F3HTileModel *destinationTile = [self tileForIndexPath:destinationPath];
+                    destinationTile.empty = NO;
+                    destinationTile.value = order.value;
+                    
+                    // Update delegate
+                    [self.delegate moveTileFromIndexPath:sourcePath
+                                             toIndexPath:destinationPath
+                                                newValue:order.value];
+                }
+            }
+        }
+    }
+    return atLeastOneMove;
+}
+
+- (BOOL)performLeftMove {
+    BOOL atLeastOneMove = NO;
+    
+    // Examine each row, up to down ([TTT] --> [---] --> [____])
+    for (NSInteger row = 0; row<self.dimension; row++) {
+        NSMutableArray *thisRowTiles = [NSMutableArray arrayWithCapacity:self.dimension];
+        for (NSInteger column = 0; column<self.dimension; column++) {
+            [thisRowTiles addObject:[self tileForIndexPath:[NSIndexPath indexPathForRow:row inSection:column]]];
+        }
+        NSArray *ordersArray = [self mergeGroup:thisRowTiles];
+        if ([ordersArray count] > 0) {
+            atLeastOneMove = YES;
+            for (NSInteger i=0; i<[ordersArray count]; i++) {
+                F3HMoveOrder *order = ordersArray[i];
+                if (order.doubleMove) {
+                    // Two tiles move and merge at the end of their moves.
+                    // Update internal model
+                    NSIndexPath *source1Path = [NSIndexPath indexPathForRow:row inSection:order.source1];
+                    NSIndexPath *source2Path = [NSIndexPath indexPathForRow:row inSection:order.source2];
+                    NSIndexPath *destinationPath = [NSIndexPath indexPathForRow:row inSection:order.destination];
+                    
+                    F3HTileModel *source1Tile = [self tileForIndexPath:source1Path];
+                    source1Tile.empty = YES;
+                    F3HTileModel *source2Tile = [self tileForIndexPath:source2Path];
+                    source2Tile.empty = YES;
+                    F3HTileModel *destinationTile = [self tileForIndexPath:destinationPath];
+                    destinationTile.empty = NO;
+                    destinationTile.value = order.value;
+                    
+                    // Update delegate
+                    [self.delegate moveTileOne:source1Path
+                                       tileTwo:source2Path
+                                   toIndexPath:destinationPath
+                                      newValue:order.value];
+                }
+                else {
+                    // One tile moves, either to an empty spot or to merge with another tile.
+                    // Update internal model
+                    NSIndexPath *sourcePath = [NSIndexPath indexPathForRow:row inSection:order.source1];
+                    NSIndexPath *destinationPath = [NSIndexPath indexPathForRow:row inSection:order.destination];
+                    
+                    F3HTileModel *sourceTile = [self tileForIndexPath:sourcePath];
+                    sourceTile.empty = YES;
+                    F3HTileModel *destinationTile = [self tileForIndexPath:destinationPath];
+                    destinationTile.empty = NO;
+                    destinationTile.value = order.value;
+                    
+                    // Update delegate
+                    [self.delegate moveTileFromIndexPath:sourcePath
+                                             toIndexPath:destinationPath
+                                                newValue:order.value];
+                }
+            }
+        }
+    }
+    return atLeastOneMove;
+}
+
+- (BOOL)performRightMove {
+    BOOL atLeastOneMove = NO;
+    
+    // Examine each row, up to down ([TTT] --> [---] --> [____])
+    for (NSInteger row = 0; row<self.dimension; row++) {
+        NSMutableArray *thisRowTiles = [NSMutableArray arrayWithCapacity:self.dimension];
+        for (NSInteger column = (self.dimension - 1); column >= 0; column--) {
+            [thisRowTiles addObject:[self tileForIndexPath:[NSIndexPath indexPathForRow:row inSection:column]]];
+        }
+        NSArray *ordersArray = [self mergeGroup:thisRowTiles];
+        if ([ordersArray count] > 0) {
+            NSInteger dim = self.dimension - 1;
+            atLeastOneMove = YES;
+            for (NSInteger i=0; i<[ordersArray count]; i++) {
+                F3HMoveOrder *order = ordersArray[i];
+                if (order.doubleMove) {
+                    // Update internal model
+                    NSIndexPath *source1Path = [NSIndexPath indexPathForRow:row inSection:(dim - order.source1)];
+                    NSIndexPath *source2Path = [NSIndexPath indexPathForRow:row inSection:(dim - order.source2)];
+                    NSIndexPath *destinationPath = [NSIndexPath indexPathForRow:row inSection:(dim - order.destination)];
+                    
+                    F3HTileModel *source1Tile = [self tileForIndexPath:source1Path];
+                    source1Tile.empty = YES;
+                    F3HTileModel *source2Tile = [self tileForIndexPath:source2Path];
+                    source2Tile.empty = YES;
+                    F3HTileModel *destinationTile = [self tileForIndexPath:destinationPath];
+                    destinationTile.empty = NO;
+                    destinationTile.value = order.value;
+                    
+                    // Update delegate
+                    [self.delegate moveTileOne:source1Path
+                                       tileTwo:source2Path
+                                   toIndexPath:destinationPath
+                                      newValue:order.value];
+                }
+                else {
+                    // Update internal model
+                    NSIndexPath *sourcePath = [NSIndexPath indexPathForRow:row inSection:(dim - order.source1)];
+                    NSIndexPath *destinationPath = [NSIndexPath indexPathForRow:row inSection:(dim - order.destination)];
+                    
+                    F3HTileModel *sourceTile = [self tileForIndexPath:sourcePath];
+                    sourceTile.empty = YES;
+                    F3HTileModel *destinationTile = [self tileForIndexPath:destinationPath];
+                    destinationTile.empty = NO;
+                    destinationTile.value = order.value;
+                    
+                    // Update delegate
+                    [self.delegate moveTileFromIndexPath:sourcePath
+                                             toIndexPath:destinationPath
+                                                newValue:order.value];
+                }
+            }
+        }
+    }
+    return atLeastOneMove;
+}
+
+
+#pragma mark - Game State API
+
+- (BOOL)userHasLost {
+    for (NSInteger i=0; i<[self.gameState count]; i++) {
+        if (((F3HTileModel *) self.gameState[i]).empty) {
+            // Gameboard must be full for the user to lose
+            return NO;
+        }
+    }
+    // This is a stupid algorithm, but given how small the game board is it should work just fine
+    // Every tile compares its value to that of the tiles to the right and below (if possible)
+    for (NSInteger i=0; i<self.dimension; i++) {
+        for (NSInteger j=0; j<self.dimension; j++) {
+            F3HTileModel *tile = [self tileForIndexPath:[NSIndexPath indexPathForRow:i inSection:j]];
+            if (j != (self.dimension - 1)
+                && tile.value == [self tileForIndexPath:[NSIndexPath indexPathForRow:i inSection:j+1]].value) {
+                return NO;
+            }
+            if (i != (self.dimension - 1)
+                && tile.value == [self tileForIndexPath:[NSIndexPath indexPathForRow:i+1 inSection:j]].value) {
+                return NO;
+            }
+        }
+    }
+    return YES;
+}
+
+- (NSIndexPath *)userHasWon {
+    for (NSInteger i=0; i<[self.gameState count]; i++) {
+        if (((F3HTileModel *) self.gameState[i]).value == self.winValue) {
+            return [NSIndexPath indexPathForRow:(i / self.dimension)
+                                      inSection:(i % self.dimension)];
+        }
+    }
+    return nil;
+}
+
+
+#pragma mark - Private Methods
+
+- (void)queueCommand:(F3HQueueCommand *)command {
+    if (!command || [self.commandQueue count] > MAX_COMMANDS) return;
+
+    [self.commandQueue addObject:command];
+    if (!self.queueTimer || ![self.queueTimer isValid]) {
+        // Timer isn't running, so fire the event immediately.
+        [self timerFired:nil];
+    }
+}
+
+- (void)timerFired:(NSTimer *)timer {
+    if ([self.commandQueue count] == 0) return;
+
+    BOOL changed = NO;
+    while ([self.commandQueue count] > 0) {
+        F3HQueueCommand *command = [self.commandQueue firstObject];
+        [self.commandQueue removeObjectAtIndex:0];
+        switch (command.direction) {
+            case F3HMoveDirectionUp:
+                changed = [self performUpMove];
+                break;
+            case F3HMoveDirectionDown:
+                changed = [self performDownMove];
+                break;
+            case F3HMoveDirectionLeft:
+                changed = [self performLeftMove];
+                break;
+            case F3HMoveDirectionRight:
+                changed = [self performRightMove];
+                break;
+        }
+        if (command.completion) {
+            command.completion(changed);
+        }
+        if (changed) {
+            // This allows us to immediately remove 'useless' commands without gumming up the queue
+            break;
+        }
+    }
+    
+    // Schedule the timer, so new moves aren't run immediately
+    self.queueTimer = [NSTimer scheduledTimerWithTimeInterval:QUEUE_DELAY
+                                                       target:self
+                                                     selector:@selector(timerFired:)
+                                                     userInfo:nil
+                                                      repeats:NO];
+}
+
+- (F3HTileModel *)tileForIndexPath:(NSIndexPath *)indexPath {
+    NSInteger idx = (indexPath.row*self.dimension + indexPath.section);
+    if (idx >= [self.gameState count]) {
+        return nil;
+    }
+    return self.gameState[idx];
+}
+
+- (void)setTile:(F3HTileModel *)tile forIndexPath:(NSIndexPath *)indexPath {
+    NSInteger idx = (indexPath.row*self.dimension + indexPath.section);
+    if (!tile || idx >= [self.gameState count]) {
+        return;
+    }
+    self.gameState[idx] = tile;
+}
+
+- (NSMutableArray *)commandQueue {
+    if (!_commandQueue) {
+        _commandQueue = [NSMutableArray array];
+    }
+    return _commandQueue;
+}
+
+- (NSMutableArray *)gameState {
+    if (!_gameState) {
+        _gameState = [NSMutableArray array];
+        for (NSInteger i=0; i<(self.dimension * self.dimension); i++) {
+            [_gameState addObject:[F3HTileModel emptyTile]];
+        }
+    }
+    return _gameState;
+}
+
+- (void)setScore:(NSInteger)score {
+    _score = score;
+    [self.delegate scoreChanged:score];
+}
+
+// Merge some items to the left
+// "Group" is an array of tile objects
+- (NSArray *)mergeGroup:(NSArray *)group {
+    NSInteger ctr = 0;
+    // STEP 1: collapse all tiles (remove any interstital space)
+    // e.g. |[2] [ ] [ ] [4]| becomes [[2] [4]|
+    // At this point, tiles either move or don't move, and their value remains the same
+    NSMutableArray *stack1 = [NSMutableArray array];
+    for (NSInteger i=0; i<self.dimension; i++) {
+        F3HTileModel *tile = group[i];
+        if (tile.empty) {
+            // Don't do anything with empty tiles
+            continue;
+        }
+        F3HMergeTile *mergeTile = [F3HMergeTile mergeTile];
+        mergeTile.originalIndexA = i;
+        mergeTile.value = tile.value;
+        if (i == ctr) {
+            mergeTile.mode = F3HMergeTileModeNoAction;
+        }
+        else {
+            mergeTile.mode = F3HMergeTileModeMove;
+        }
+        [stack1 addObject:mergeTile];
+        ctr++;
+    }
+    if ([stack1 count] == 0) {
+        // Nothing to do, no tiles in this group
+        return nil;
+    }
+    else if ([stack1 count] == 1) {
+        // Only one tile in this group. Either it moved, or it didn't.
+        if (((F3HMergeTile *)stack1[0]).mode == F3HMergeTileModeMove) {
+            // Tile moved. Add one move order.
+            F3HMergeTile *mTile = (F3HMergeTile *)stack1[0];
+            return @[[F3HMoveOrder singleMoveOrderWithSource:mTile.originalIndexA
+                                                 destination:0
+                                                    newValue:mTile.value]];
+        }
+        else {
+            return nil;
+        }
+    }
+    
+    // STEP 2: starting from the left, and moving to the right, collapse tiles
+    // e.g. |[8][8][4][2][2]| should become |[16][4][4]|
+    // e.g. |[2][2][2]| should become |[4][2]|
+    // At this point, tiles may become the subject of a single or double merge
+    ctr = 0;
+    BOOL priorMergeHasHappened = NO;
+    NSMutableArray *stack2 = [NSMutableArray array];
+    while (ctr < ([stack1 count] - 1)) {
+        F3HMergeTile *t1 = (F3HMergeTile *)stack1[ctr];
+        F3HMergeTile *t2 = (F3HMergeTile *)stack1[ctr+1];
+        if (t1.value == t2.value) {
+            // First: update t1 and t2's modes
+            NSAssert(t1.mode != F3HMergeTileModeSingleCombine && t2.mode != F3HMergeTileModeSingleCombine
+                     && t1.mode != F3HMergeTileModeDoubleCombine && t2.mode != F3HMergeTileModeDoubleCombine,
+                     @"Should not be able to get in a state where already-combined tiles are recombined");
+            
+            // Merge the two
+            if (t1.mode == F3HMergeTileModeNoAction && !priorMergeHasHappened) {
+                priorMergeHasHappened = YES;
+                // t1 didn't move, but t2 merged onto t1.
+                F3HMergeTile *newT = [F3HMergeTile mergeTile];
+                newT.mode = F3HMergeTileModeSingleCombine;
+                newT.originalIndexA = t2.originalIndexA;
+                newT.value = t1.value * 2;
+                self.score += newT.value;
+                [stack2 addObject:newT];
+            }
+            else {
+                // t1 moved earlier.
+                F3HMergeTile *newT = [F3HMergeTile mergeTile];
+                newT.mode = F3HMergeTileModeDoubleCombine;
+                newT.originalIndexA = t1.originalIndexA;
+                newT.originalIndexB = t2.originalIndexA;
+                newT.value = t1.value * 2;
+                self.score += newT.value;
+                [stack2 addObject:newT];
+            }
+            ctr += 2;
+        }
+        else {
+            // t1 is pushed onto stack2, as either a move or a no-op. The pointer is incremented
+            [stack2 addObject:t1];
+            if ([stack2 count] - 1 != ctr) {
+                t1.mode = F3HMergeTileModeMove;
+            }
+            ctr++;
+        }
+        // Addendum:
+        if (ctr == [stack1 count] - 1) {
+            // We're at the end of stack1, and need to add t2 as well as t1.
+            F3HMergeTile *item = stack1[ctr];
+            [stack2 addObject:item];
+            if ([stack2 count] - 1 != ctr) {
+                item.mode = F3HMergeTileModeMove;
+            }
+        }
+    }
+    
+    // STEP 3: create move orders for each mergeTile that did change this round
+    NSMutableArray *stack3 = [NSMutableArray new];
+    for (NSInteger i=0; i<[stack2 count]; i++) {
+        F3HMergeTile *mTile = stack2[i];
+        switch (mTile.mode) {
+            case F3HMergeTileModeEmpty:
+            case F3HMergeTileModeNoAction:
+                continue;
+            case F3HMergeTileModeMove:
+            case F3HMergeTileModeSingleCombine:
+                // Single combine
+                [stack3 addObject:[F3HMoveOrder singleMoveOrderWithSource:mTile.originalIndexA
+                                                              destination:i
+                                                                 newValue:mTile.value]];
+                break;
+            case F3HMergeTileModeDoubleCombine:
+                // Double combine
+                [stack3 addObject:[F3HMoveOrder doubleMoveOrderWithFirstSource:mTile.originalIndexA
+                                                                  secondSource:mTile.originalIndexB
+                                                                   destination:i
+                                                                      newValue:mTile.value]];
+                break;
+        }
+    }
+    // Return the finalized array
+    return [NSArray arrayWithArray:stack3];
+}
+
+@end

+ 28 - 0
NumberTileGame/NumberTileGame/Models/F3HMergeTile.h

@@ -0,0 +1,28 @@
+//
+//  F3HMergeTile.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import <Foundation/Foundation.h>
+
+typedef enum {
+    F3HMergeTileModeEmpty = 0,
+    F3HMergeTileModeNoAction,
+    F3HMergeTileModeMove,
+    F3HMergeTileModeSingleCombine,
+    F3HMergeTileModeDoubleCombine
+} F3HMergeTileMode;
+
+@interface F3HMergeTile : NSObject
+
+@property (nonatomic) F3HMergeTileMode mode;
+@property (nonatomic) NSInteger originalIndexA;
+@property (nonatomic) NSInteger originalIndexB;
+@property (nonatomic) NSInteger value;
+
++ (instancetype)mergeTile;
+
+@end

+ 42 - 0
NumberTileGame/NumberTileGame/Models/F3HMergeTile.m

@@ -0,0 +1,42 @@
+//
+//  F3HMergeTile.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import "F3HMergeTile.h"
+
+@implementation F3HMergeTile
+
++ (instancetype)mergeTile {
+    return [[self class] new];
+}
+
+- (NSString *)description {
+    NSString *modeStr;
+    switch (self.mode) {
+        case F3HMergeTileModeEmpty:
+            modeStr = @"Empty";
+            break;
+        case F3HMergeTileModeNoAction:
+            modeStr = @"NoAction";
+            break;
+        case F3HMergeTileModeMove:
+            modeStr = @"Move";
+            break;
+        case F3HMergeTileModeSingleCombine:
+            modeStr = @"SingleCombine";
+            break;
+        case F3HMergeTileModeDoubleCombine:
+            modeStr = @"DoubleCombine";
+    }
+    return [NSString stringWithFormat:@"MergeTile (mode: %@, source1: %ld, source2: %ld, value: %ld)",
+            modeStr,
+            (long)self.originalIndexA,
+            (long)self.originalIndexB,
+            (long)self.value];
+}
+
+@end

+ 28 - 0
NumberTileGame/NumberTileGame/Models/F3HMoveOrder.h

@@ -0,0 +1,28 @@
+//
+//  F3HMoveOrder.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import <Foundation/Foundation.h>
+
+@interface F3HMoveOrder : NSObject
+
+@property (nonatomic) NSInteger source1;
+@property (nonatomic) NSInteger source2;
+@property (nonatomic) NSInteger destination;
+@property (nonatomic) BOOL doubleMove;
+@property (nonatomic) NSInteger value;
+
++ (instancetype)singleMoveOrderWithSource:(NSInteger)source
+                              destination:(NSInteger)destination
+                                 newValue:(NSInteger)value;
+
++ (instancetype)doubleMoveOrderWithFirstSource:(NSInteger)source1
+                                  secondSource:(NSInteger)source2
+                                   destination:(NSInteger)destination
+                                      newValue:(NSInteger)value;
+
+@end

+ 49 - 0
NumberTileGame/NumberTileGame/Models/F3HMoveOrder.m

@@ -0,0 +1,49 @@
+//
+//  F3HMoveOrder.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import "F3HMoveOrder.h"
+
+@implementation F3HMoveOrder
+
++ (instancetype)singleMoveOrderWithSource:(NSInteger)source destination:(NSInteger)destination newValue:(NSInteger)value {
+    F3HMoveOrder *order = [[self class] new];
+    order.doubleMove = NO;
+    order.source1 = source;
+    order.destination = destination;
+    order.value = value;
+    return order;
+}
+
++ (instancetype)doubleMoveOrderWithFirstSource:(NSInteger)source1
+                                  secondSource:(NSInteger)source2
+                                   destination:(NSInteger)destination
+                                      newValue:(NSInteger)value {
+    F3HMoveOrder *order = [[self class] new];
+    order.doubleMove = YES;
+    order.source1 = source1;
+    order.source2 = source2;
+    order.destination = destination;
+    order.value = value;
+    return order;
+}
+
+- (NSString *)description {
+    if (self.doubleMove) {
+        return [NSString stringWithFormat:@"MoveOrder (double, source1: %ld, source2: %ld, destination: %ld, value: %ld)",
+                (long)self.source1,
+                (long)self.source2,
+                (long)self.destination,
+                (long)self.value];
+    }
+    return [NSString stringWithFormat:@"MoveOrder (single, source: %ld, destination: %ld, value: %ld)",
+            (long)self.source1,
+            (long)self.destination,
+            (long)self.value];
+}
+
+@end

+ 20 - 0
NumberTileGame/NumberTileGame/Models/F3HQueueCommand.h

@@ -0,0 +1,20 @@
+//
+//  F3HQueueCommand.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import <Foundation/Foundation.h>
+
+#import "F3HGameModel.h"
+
+@interface F3HQueueCommand : NSObject
+
+@property (nonatomic) F3HMoveDirection direction;
+@property (nonatomic, copy) void(^completion)(BOOL atLeastOneMove);
+
++ (instancetype)commandWithDirection:(F3HMoveDirection)direction completionBlock:(void(^)(BOOL))completion;
+
+@end

+ 21 - 0
NumberTileGame/NumberTileGame/Models/F3HQueueCommand.m

@@ -0,0 +1,21 @@
+//
+//  F3HQueueCommand.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import "F3HQueueCommand.h"
+
+@implementation F3HQueueCommand
+
++ (instancetype)commandWithDirection:(F3HMoveDirection)direction
+                     completionBlock:(void(^)(BOOL))completion {
+    F3HQueueCommand *command = [[self class] new];
+    command.direction = direction;
+    command.completion = completion;
+    return command;
+}
+
+@end

+ 17 - 0
NumberTileGame/NumberTileGame/Models/F3HTileModel.h

@@ -0,0 +1,17 @@
+//
+//  F3HTileModel.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import <Foundation/Foundation.h>
+
+@interface F3HTileModel : NSObject
+
+@property (nonatomic) BOOL empty;
+@property (nonatomic) NSUInteger value;
++ (instancetype)emptyTile;
+
+@end

+ 27 - 0
NumberTileGame/NumberTileGame/Models/F3HTileModel.m

@@ -0,0 +1,27 @@
+//
+//  F3HTileModel.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import "F3HTileModel.h"
+
+@implementation F3HTileModel
+
++ (instancetype)emptyTile {
+    F3HTileModel *tile = [[self class] new];
+    tile.empty = YES;
+    tile.value = 0;
+    return tile;
+}
+
+- (NSString *)description {
+    if (self.empty) {
+        return @"Tile (empty)";
+    }
+    return [NSString stringWithFormat:@"Tile (value: %lu)", (unsigned long)self.value];
+}
+
+@end

+ 49 - 0
NumberTileGame/NumberTileGame/NumberTileGame-Info.plist

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleDisplayName</key>
+	<string>${PRODUCT_NAME}</string>
+	<key>CFBundleExecutable</key>
+	<string>${EXECUTABLE_NAME}</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>${PRODUCT_NAME}</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1.0</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>UIMainStoryboardFile</key>
+	<string>Main_iPhone</string>
+	<key>UIMainStoryboardFile~ipad</key>
+	<string>Main_iPad</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+	</array>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+</dict>
+</plist>

+ 16 - 0
NumberTileGame/NumberTileGame/NumberTileGame-Prefix.pch

@@ -0,0 +1,16 @@
+//
+//  Prefix header
+//
+//  The contents of this file are implicitly included at the beginning of every source file.
+//
+
+#import <Availability.h>
+
+#ifndef __IPHONE_5_0
+#warning "This project uses features only available in iOS SDK 5.0 and later."
+#endif
+
+#ifdef __OBJC__
+  #import <UIKit/UIKit.h>
+  #import <Foundation/Foundation.h>
+#endif

+ 30 - 0
NumberTileGame/NumberTileGame/Views/F3HControlView.h

@@ -0,0 +1,30 @@
+//
+//  F3HControlView.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/25/14.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol F3HControlViewProtocol
+
+- (void)upButtonTapped;
+- (void)downButtonTapped;
+- (void)leftButtonTapped;
+- (void)rightButtonTapped;
+- (void)resetButtonTapped;
+- (void)exitButtonTapped;
+
+@end
+
+@interface F3HControlView : UIView
+
++ (instancetype)controlViewWithCornerRadius:(CGFloat)radius
+                            backgroundColor:(UIColor *)color
+                            movementButtons:(BOOL)moveButtonsEnabled
+                                 exitButton:(BOOL)exitButtonEnabled
+                                   delegate:(id<F3HControlViewProtocol>)delegate;
+
+@end

+ 151 - 0
NumberTileGame/NumberTileGame/Views/F3HControlView.m

@@ -0,0 +1,151 @@
+//
+//  F3HControlView.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/25/14.
+//
+//
+
+#import "F3HControlView.h"
+
+#define DEFAULT_FRAME_SMALL     CGRectMake(0, 0, 230, 30)
+#define DEFAULT_FRAME_LARGE     CGRectMake(0, 0, 230, 140)
+
+@interface F3HControlView ()
+@property (nonatomic) BOOL moveButtonsEnabled;
+@property (nonatomic) BOOL exitButtonEnabled;
+@property (nonatomic, weak) id<F3HControlViewProtocol> delegate;
+@end
+
+@implementation F3HControlView
+
++ (instancetype)controlViewWithCornerRadius:(CGFloat)radius
+                            backgroundColor:(UIColor *)color
+                            movementButtons:(BOOL)moveButtonsEnabled
+                                 exitButton:(BOOL)exitButtonEnabled
+                                   delegate:(id<F3HControlViewProtocol>)delegate {
+    F3HControlView *view = [[[self class] alloc] initWithFrame:(moveButtonsEnabled ?
+                                                                DEFAULT_FRAME_LARGE :
+                                                                DEFAULT_FRAME_SMALL)];
+    view.moveButtonsEnabled = moveButtonsEnabled;
+    view.exitButtonEnabled = exitButtonEnabled;
+    view.backgroundColor = color ?: [UIColor darkGrayColor];
+    view.layer.cornerRadius = radius;
+    view.delegate = delegate;
+    [view setupSubviews];
+    return view;
+}
+
+- (void)setupSubviews {
+    if (self.moveButtonsEnabled) {
+        // Large layout
+        UIButton *upButton = [[UIButton alloc] initWithFrame:CGRectMake(90, 6, 50, 60)];
+        upButton.layer.cornerRadius = 4;
+        upButton.backgroundColor = [UIColor grayColor];
+        upButton.showsTouchWhenHighlighted = YES;
+        [upButton addTarget:self
+                     action:@selector(upButtonTapped)
+           forControlEvents:UIControlEventTouchUpInside];
+        [self addSubview:upButton];
+        
+        UIButton *downButton = [[UIButton alloc] initWithFrame:CGRectMake(90, 74, 50, 60)];
+        downButton.layer.cornerRadius = 4;
+        downButton.backgroundColor = [UIColor grayColor];
+        downButton.showsTouchWhenHighlighted = YES;
+        [downButton addTarget:self
+                       action:@selector(downButtonTapped)
+             forControlEvents:UIControlEventTouchUpInside];
+        [self addSubview:downButton];
+        
+        UIButton *leftButton = [[UIButton alloc] initWithFrame:CGRectMake(20, 62, 60, 50)];
+        leftButton.layer.cornerRadius = 4;
+        leftButton.backgroundColor = [UIColor grayColor];
+        leftButton.showsTouchWhenHighlighted = YES;
+        [leftButton addTarget:self
+                       action:@selector(leftButtonTapped)
+             forControlEvents:UIControlEventTouchUpInside];
+        [self addSubview:leftButton];
+        
+        UIButton *rightButton = [[UIButton alloc] initWithFrame:CGRectMake(150, 62, 60, 50)];
+        rightButton.layer.cornerRadius = 4;
+        rightButton.backgroundColor = [UIColor grayColor];
+        rightButton.showsTouchWhenHighlighted = YES;
+        [rightButton addTarget:self
+                        action:@selector(rightButtonTapped)
+              forControlEvents:UIControlEventTouchUpInside];
+        [self addSubview:rightButton];
+        
+        UIButton *resetButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 70, 30)];
+        resetButton.titleLabel.textColor = [UIColor whiteColor];
+        resetButton.titleLabel.font = [UIFont fontWithName:@"HelveticaNeue-Bold" size:14];
+        resetButton.showsTouchWhenHighlighted = YES;
+        [resetButton addTarget:self
+                        action:@selector(resetButtonTapped)
+              forControlEvents:UIControlEventTouchUpInside];
+        [resetButton setTitle:@"RESET" forState:UIControlStateNormal];
+        [self addSubview:resetButton];
+        
+        if (self.exitButtonEnabled) {
+            UIButton *exitButton = [[UIButton alloc] initWithFrame:CGRectMake(160, 0, 70, 30)];
+            exitButton.titleLabel.textColor = [UIColor whiteColor];
+            exitButton.titleLabel.font = [UIFont fontWithName:@"HelveticaNeue-Bold" size:14];
+            exitButton.showsTouchWhenHighlighted = YES;
+            [exitButton setTitle:@"EXIT" forState:UIControlStateNormal];
+            [exitButton addTarget:self
+                           action:@selector(exitButtonTapped)
+                 forControlEvents:UIControlEventTouchUpInside];
+            [self addSubview:exitButton];
+        }
+    }
+    else {
+        // Small layout
+        UIButton *resetButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 70, 30)];
+        resetButton.titleLabel.textColor = [UIColor whiteColor];
+        resetButton.titleLabel.font = [UIFont fontWithName:@"HelveticaNeue-Bold" size:14];
+        resetButton.showsTouchWhenHighlighted = YES;
+        [resetButton setTitle:@"RESET" forState:UIControlStateNormal];
+        [resetButton addTarget:self
+                        action:@selector(resetButtonTapped)
+              forControlEvents:UIControlEventTouchUpInside];
+        [resetButton setTitle:@"RESET" forState:UIControlStateNormal];
+        [self addSubview:resetButton];
+        
+        if (self.exitButtonEnabled) {
+            UIButton *exitButton = [[UIButton alloc] initWithFrame:CGRectMake(160, 0, 70, 30)];
+            exitButton.titleLabel.textColor = [UIColor whiteColor];
+            exitButton.titleLabel.font = [UIFont fontWithName:@"HelveticaNeue-Bold" size:14];
+            exitButton.showsTouchWhenHighlighted = YES;
+            [exitButton setTitle:@"EXIT" forState:UIControlStateNormal];
+            [exitButton addTarget:self
+                           action:@selector(exitButtonTapped)
+                 forControlEvents:UIControlEventTouchUpInside];
+            [self addSubview:exitButton];
+        }
+    }
+}
+
+- (void)upButtonTapped {
+    [self.delegate upButtonTapped];
+}
+
+- (void)downButtonTapped {
+    [self.delegate downButtonTapped];
+}
+
+- (void)leftButtonTapped {
+    [self.delegate leftButtonTapped];
+}
+
+- (void)rightButtonTapped {
+    [self.delegate rightButtonTapped];
+}
+
+- (void)resetButtonTapped {
+    [self.delegate resetButtonTapped];
+}
+
+- (void)exitButtonTapped {
+    [self.delegate exitButtonTapped];
+}
+
+@end

+ 34 - 0
NumberTileGame/NumberTileGame/Views/F3HGameboardView.h

@@ -0,0 +1,34 @@
+//
+//  F3HGameboardView.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+@interface F3HGameboardView : UIView
+
++ (instancetype)gameboardWithDimension:(NSUInteger)dimension
+                             cellWidth:(CGFloat)width
+                           cellPadding:(CGFloat)padding
+                          cornerRadius:(CGFloat)cornerRadius
+                       backgroundColor:(UIColor *)backgroundColor
+                       foregroundColor:(UIColor *)foregroundColor;
+
+- (void)reset;
+
+- (void)insertTileAtIndexPath:(NSIndexPath *)path
+                    withValue:(NSUInteger)value;
+
+- (void)moveTileOne:(NSIndexPath *)startA
+            tileTwo:(NSIndexPath *)startB
+        toIndexPath:(NSIndexPath *)end
+          withValue:(NSUInteger)value;
+
+- (void)moveTileAtIndexPath:(NSIndexPath *)start
+                toIndexPath:(NSIndexPath *)end
+                  withValue:(NSUInteger)value;
+
+@end

+ 279 - 0
NumberTileGame/NumberTileGame/Views/F3HGameboardView.m

@@ -0,0 +1,279 @@
+//
+//  F3HGameboardView.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import "F3HGameboardView.h"
+
+#import <QuartzCore/QuartzCore.h>
+#import "F3HTileView.h"
+#import "F3HTileAppearanceProvider.h"
+
+#define PER_SQUARE_SLIDE_DURATION 0.08
+
+#if DEBUG
+#define F3HLOG(...) NSLog(__VA_ARGS__)
+#else
+#define F3HLOG(...)
+#endif
+
+// Animation parameters
+#define TILE_POP_START_SCALE    0.1
+#define TILE_POP_MAX_SCALE      1.1
+#define TILE_POP_DELAY          0.05
+#define TILE_EXPAND_TIME        0.18
+#define TILE_RETRACT_TIME       0.08
+
+#define TILE_MERGE_START_SCALE  1.0
+#define TILE_MERGE_EXPAND_TIME  0.08
+#define TILE_MERGE_RETRACT_TIME 0.08
+
+
+@interface F3HGameboardView ()
+
+@property (nonatomic, strong) NSMutableDictionary *boardTiles;
+
+@property (nonatomic) NSUInteger dimension;
+@property (nonatomic) CGFloat tileSideLength;
+
+@property (nonatomic) CGFloat padding;
+@property (nonatomic) CGFloat cornerRadius;
+
+@property (nonatomic, strong) F3HTileAppearanceProvider *provider;
+
+@end
+
+@implementation F3HGameboardView
+
++ (instancetype)gameboardWithDimension:(NSUInteger)dimension
+                             cellWidth:(CGFloat)width
+                           cellPadding:(CGFloat)padding
+                          cornerRadius:(CGFloat)cornerRadius
+                       backgroundColor:(UIColor *)backgroundColor
+                       foregroundColor:(UIColor *)foregroundColor {
+    
+    CGFloat sideLength = padding + dimension*(width + padding);
+    F3HGameboardView *view = [[[self class] alloc] initWithFrame:CGRectMake(0,
+                                                                            0,
+                                                                            sideLength,
+                                                                            sideLength)];
+    view.dimension = dimension;
+    view.padding = padding;
+    view.tileSideLength = width;
+    view.layer.cornerRadius = cornerRadius;
+    view.cornerRadius = cornerRadius;
+    [view setupBackgroundWithBackgroundColor:backgroundColor
+                             foregroundColor:foregroundColor];
+    return view;
+}
+
+- (void)reset {
+    for (NSString *key in self.boardTiles) {
+        F3HTileView *tile = self.boardTiles[key];
+        [tile removeFromSuperview];
+    }
+    [self.boardTiles removeAllObjects];
+}
+
+- (void)setupBackgroundWithBackgroundColor:(UIColor *)background
+                           foregroundColor:(UIColor *)foreground {
+    self.backgroundColor = background;
+    CGFloat xCursor = self.padding;
+    CGFloat yCursor;
+    CGFloat cornerRadius = self.cornerRadius - 2;
+    if (cornerRadius < 0) {
+        cornerRadius = 0;
+    }
+    for (NSInteger i=0; i<self.dimension; i++) {
+        yCursor = self.padding;
+        for (NSInteger j=0; j<self.dimension; j++) {
+            UIView *bkgndTile = [[UIView alloc] initWithFrame:CGRectMake(xCursor,
+                                                                         yCursor,
+                                                                         self.tileSideLength,
+                                                                         self.tileSideLength)];
+            bkgndTile.layer.cornerRadius = cornerRadius;
+            bkgndTile.backgroundColor = foreground;
+            [self addSubview:bkgndTile];
+            yCursor += self.padding + self.tileSideLength;
+        }
+        xCursor += self.padding + self.tileSideLength;
+    }
+}
+
+// Insert a tile, with the popping animation
+- (void)insertTileAtIndexPath:(NSIndexPath *)path
+                    withValue:(NSUInteger)value {
+    F3HLOG(@"Inserting tile at row %ld, column %ld", (long)path.row, (long)path.section);
+    if (!path
+        || path.row >= self.dimension
+        || path.section >= self.dimension
+        || self.boardTiles[path]) {
+        // Index path out of bounds, or there already is a tile
+        return;
+    }
+    
+    CGFloat x = self.padding + path.section*(self.tileSideLength + self.padding);
+    CGFloat y = self.padding + path.row*(self.tileSideLength + self.padding);
+    CGPoint position = CGPointMake(x, y);
+    CGFloat cornerRadius = self.cornerRadius - 2;
+    if (cornerRadius < 0) {
+        cornerRadius = 0;
+    }
+    F3HTileView *tile = [F3HTileView tileForPosition:position
+                                          sideLength:self.tileSideLength
+                                               value:value
+                                        cornerRadius:cornerRadius];
+    tile.delegate = self.provider;
+    tile.layer.affineTransform = CGAffineTransformMakeScale(TILE_POP_START_SCALE, TILE_POP_START_SCALE);
+    [self addSubview:tile];
+    self.boardTiles[path] = tile;
+
+    // Add the new tile to the board, with a pop animation
+    [UIView animateWithDuration:TILE_EXPAND_TIME
+                          delay:TILE_POP_DELAY
+                        options:0
+                     animations:^{
+                         tile.layer.affineTransform = CGAffineTransformMakeScale(TILE_POP_MAX_SCALE,
+                                                                                 TILE_POP_MAX_SCALE);
+    } completion:^(BOOL finished) {
+        // Run the 'shrink' animation
+        [UIView animateWithDuration:TILE_RETRACT_TIME animations:^{
+            tile.layer.affineTransform = CGAffineTransformIdentity;
+        } completion:^(BOOL finished) {
+            // Nothing right now
+        }];
+    }];
+}
+
+- (void)moveTileOne:(NSIndexPath *)startA
+            tileTwo:(NSIndexPath *)startB
+        toIndexPath:(NSIndexPath *)end
+          withValue:(NSUInteger)value {
+    F3HLOG(@"Moving tiles at row %ld, column %ld and row %ld, column %ld to destination row %ld, column %ld",
+           (long)startA.row, (long)startA.section,
+           (long)startB.row, (long)startB.section,
+           (long)end.row, (long)end.section);
+    if (!startA || !startB || !self.boardTiles[startA] || !self.boardTiles[startB]
+        || end.row >= self.dimension
+        || end.section >= self.dimension) {
+        NSAssert(NO, @"Invalid two-tile move and merge");
+        return;
+    }
+    F3HTileView *tileA = self.boardTiles[startA];
+    F3HTileView *tileB = self.boardTiles[startB];
+    
+    CGFloat x = self.padding + end.section*(self.tileSideLength + self.padding);
+    CGFloat y = self.padding + end.row*(self.tileSideLength + self.padding);
+    CGRect finalFrame = tileA.frame;
+    finalFrame.origin.x = x;
+    finalFrame.origin.y = y;
+    
+    // Don't perform update after animation
+    [self.boardTiles removeObjectForKey:startA];
+    [self.boardTiles removeObjectForKey:startB];
+    self.boardTiles[end] = tileA;
+
+    [UIView animateWithDuration:(PER_SQUARE_SLIDE_DURATION*1)
+                          delay:0
+                        options:UIViewAnimationOptionBeginFromCurrentState
+                     animations:^{
+                         tileA.frame = finalFrame;
+                         tileB.frame = finalFrame;
+                     }
+                     completion:^(BOOL finished) {
+                         tileA.tileValue = value;
+                         if (!finished) {
+                             [tileB removeFromSuperview];
+                             return;
+                         }
+                         tileA.layer.affineTransform = CGAffineTransformMakeScale(TILE_MERGE_START_SCALE,
+                                                                                  TILE_MERGE_START_SCALE);
+                         [tileB removeFromSuperview];
+                         [UIView animateWithDuration:TILE_MERGE_EXPAND_TIME
+                                          animations:^{
+                                              tileA.layer.affineTransform = CGAffineTransformMakeScale(TILE_POP_MAX_SCALE,
+                                                                                                       TILE_POP_MAX_SCALE);
+                                          } completion:^(BOOL finished) {
+                                              [UIView animateWithDuration:TILE_MERGE_RETRACT_TIME
+                                                               animations:^{
+                                                                   tileA.layer.affineTransform = CGAffineTransformIdentity;
+                                                               } completion:^(BOOL finished) {
+                                                                   // nothing yet
+                                                               }];
+                                          }];
+                     }];
+}
+
+// Move a single tile onto another tile (that stays stationary), merging the two
+- (void)moveTileAtIndexPath:(NSIndexPath *)start
+                toIndexPath:(NSIndexPath *)end
+                  withValue:(NSUInteger)value {
+    F3HLOG(@"Moving tile at row %ld, column %ld to destination row %ld, column %ld",
+           (long)start.row, (long)start.section, (long)end.row, (long)end.section);
+    if (!start || !end || !self.boardTiles[start]
+        || end.row >= self.dimension
+        || end.section >= self.dimension) {
+        NSAssert(NO, @"Invalid one-tile move and merge");
+        return;
+    }
+    F3HTileView *tile = self.boardTiles[start];
+    F3HTileView *endTile = self.boardTiles[end];
+    BOOL shouldPop = endTile != nil;
+    
+    CGFloat x = self.padding + end.section*(self.tileSideLength + self.padding);
+    CGFloat y = self.padding + end.row*(self.tileSideLength + self.padding);
+    CGRect finalFrame = tile.frame;
+    finalFrame.origin.x = x;
+    finalFrame.origin.y = y;
+    
+    // Update board state
+    [self.boardTiles removeObjectForKey:start];
+    self.boardTiles[end] = tile;
+    
+    [UIView animateWithDuration:(PER_SQUARE_SLIDE_DURATION)
+                          delay:0
+                        options:UIViewAnimationOptionBeginFromCurrentState
+                     animations:^{
+                         tile.frame = finalFrame;
+                     }
+                     completion:^(BOOL finished) {
+                         tile.tileValue = value;
+                         if (!shouldPop || !finished) {
+                             return;
+                         }
+                         tile.layer.affineTransform = CGAffineTransformMakeScale(TILE_MERGE_START_SCALE,
+                                                                                 TILE_MERGE_START_SCALE);
+                         [endTile removeFromSuperview];
+                         [UIView animateWithDuration:TILE_MERGE_EXPAND_TIME
+                                          animations:^{
+                                              tile.layer.affineTransform = CGAffineTransformMakeScale(TILE_POP_MAX_SCALE,
+                                                                                                      TILE_POP_MAX_SCALE);
+                                          } completion:^(BOOL finished) {
+                                              [UIView animateWithDuration:TILE_MERGE_RETRACT_TIME
+                                                               animations:^{
+                                                                   tile.layer.affineTransform = CGAffineTransformIdentity;
+                                                               } completion:^(BOOL finished) {
+                                                                   // nothing yet
+                                                               }];
+                                          }];
+                     }];
+}
+
+- (F3HTileAppearanceProvider *)provider {
+    if (!_provider) {
+        _provider = [F3HTileAppearanceProvider new];
+    }
+    return _provider;
+}
+
+- (NSMutableDictionary *)boardTiles {
+    if (!_boardTiles) {
+        _boardTiles = [NSMutableDictionary dictionary];
+    }
+    return _boardTiles;
+}
+
+@end

+ 20 - 0
NumberTileGame/NumberTileGame/Views/F3HScoreView.h

@@ -0,0 +1,20 @@
+//
+//  F3HScoreView.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/25/14.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+@interface F3HScoreView : UIView
+
+@property (nonatomic) NSInteger score;
+
++ (instancetype)scoreViewWithCornerRadius:(CGFloat)radius
+                          backgroundColor:(UIColor *)color
+                                textColor:(UIColor *)textColor
+                                 textFont:(UIFont *)textFont;
+
+@end

+ 52 - 0
NumberTileGame/NumberTileGame/Views/F3HScoreView.m

@@ -0,0 +1,52 @@
+//
+//  F3HScoreView.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/25/14.
+//
+//
+
+#import "F3HScoreView.h"
+
+#define DEFAULT_FRAME CGRectMake(0, 0, 140, 40)
+
+@interface F3HScoreView ()
+@property (nonatomic, strong) UILabel *scoreLabel;
+@end
+
+@implementation F3HScoreView
+
++ (instancetype)scoreViewWithCornerRadius:(CGFloat)radius
+                          backgroundColor:(UIColor *)color
+                                textColor:(UIColor *)textColor
+                                 textFont:(UIFont *)textFont {
+    F3HScoreView *view = [[[self class] alloc] initWithFrame:DEFAULT_FRAME];
+    view.score = 0;
+    view.layer.cornerRadius = radius;
+    view.backgroundColor = color ?: [UIColor whiteColor];
+    view.userInteractionEnabled = YES;
+    if (textColor) {
+        view.scoreLabel.textColor = textColor;
+    }
+    if (textFont) {
+        view.scoreLabel.font = textFont;
+    }
+    return view;
+}
+
+- (id)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    UILabel *scoreLabel = [[UILabel alloc] initWithFrame:frame];
+    scoreLabel.textAlignment = NSTextAlignmentCenter;
+    [self addSubview:scoreLabel];
+    self.scoreLabel = scoreLabel;
+    return self;
+}
+
+- (void)setScore:(NSInteger)score {
+    _score = score;
+    self.scoreLabel.text = [NSString stringWithFormat:@"SCORE: %ld", (long)score];
+}
+
+@end

+ 23 - 0
NumberTileGame/NumberTileGame/Views/F3HTileView.h

@@ -0,0 +1,23 @@
+//
+//  F3HTileView.h
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol F3HTileAppearanceProviderProtocol;
+@interface F3HTileView : UIView
+
+@property (nonatomic) NSInteger tileValue;
+
+@property (nonatomic, weak) id<F3HTileAppearanceProviderProtocol>delegate;
+
++ (instancetype)tileForPosition:(CGPoint)position
+                     sideLength:(CGFloat)side
+                          value:(NSUInteger)value
+                   cornerRadius:(CGFloat)cornerRadius;
+
+@end

+ 83 - 0
NumberTileGame/NumberTileGame/Views/F3HTileView.m

@@ -0,0 +1,83 @@
+//
+//  F3HTileView.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import "F3HTileView.h"
+
+#import <QuartzCore/QuartzCore.h>
+#import "F3HTileAppearanceProvider.h"
+
+@interface F3HTileView ()
+
+@property (nonatomic, readonly) UIColor *defaultBackgroundColor;
+@property (nonatomic, readonly) UIColor *defaultNumberColor;
+
+@property (nonatomic, strong) UILabel *numberLabel;
+@property (nonatomic) NSUInteger value;
+@end
+
+@implementation F3HTileView
+
++ (instancetype)tileForPosition:(CGPoint)position
+                     sideLength:(CGFloat)side
+                          value:(NSUInteger)value
+                   cornerRadius:(CGFloat)cornerRadius {
+    F3HTileView *tile = [[[self class] alloc] initWithFrame:CGRectMake(position.x,
+                                                                       position.y,
+                                                                       side,
+                                                                       side)];
+    tile.tileValue = value;
+    tile.backgroundColor = tile.defaultBackgroundColor;
+    tile.numberLabel.textColor = tile.defaultNumberColor;
+    tile.value = value;
+    tile.layer.cornerRadius = cornerRadius;
+    return tile;
+}
+
+- (id)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    
+    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0,
+                                                               0,
+                                                               frame.size.width,
+                                                               frame.size.height)];
+    label.textAlignment = NSTextAlignmentCenter;
+    label.minimumScaleFactor = 0.5;
+    [self addSubview:label];
+    self.numberLabel = label;
+    return self;
+}
+
+- (void)setDelegate:(id<F3HTileAppearanceProviderProtocol>)delegate {
+    _delegate = delegate;
+    if (delegate) {
+        self.backgroundColor = [delegate tileColorForValue:self.tileValue];
+        self.numberLabel.textColor = [delegate numberColorForValue:self.tileValue];
+        self.numberLabel.font = [delegate fontForNumbers];
+    }
+}
+
+- (void)setTileValue:(NSInteger)tileValue {
+    _tileValue = tileValue;
+    self.numberLabel.text = [@(tileValue) stringValue];
+    if (self.delegate) {
+        self.backgroundColor = [self.delegate tileColorForValue:tileValue];
+        self.numberLabel.textColor = [self.delegate numberColorForValue:tileValue];
+    }
+    self.value = tileValue;
+}
+
+- (UIColor *)defaultBackgroundColor {
+    return [UIColor lightGrayColor];
+}
+
+- (UIColor *)defaultNumberColor {
+    return [UIColor blackColor];
+}
+
+@end

+ 2 - 0
NumberTileGame/NumberTileGame/en.lproj/InfoPlist.strings

@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+

+ 18 - 0
NumberTileGame/NumberTileGame/main.m

@@ -0,0 +1,18 @@
+//
+//  main.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+#import "F3HAppDelegate.h"
+
+int main(int argc, char * argv[])
+{
+  @autoreleasepool {
+      return UIApplicationMain(argc, argv, nil, NSStringFromClass([F3HAppDelegate class]));
+  }
+}

+ 328 - 0
NumberTileGame/NumberTileGameTests/F3HModelTests.m

@@ -0,0 +1,328 @@
+//
+//  F3HModelTests.m
+//  NumberTileGame
+//
+//  Created by Austin Zheng on 3/24/14.
+//
+//
+
+#import <XCTest/XCTest.h>
+
+#import "F3HGameModel.h"
+#import "F3HMoveOrder.h"
+#import "F3HTileModel.h"
+#import "F3HMergeTile.h"
+
+@interface F3HGameModel ()
+- (NSArray *)mergeGroup:(NSArray *)group;
+@end
+
+@interface F3HModelTests : XCTestCase
+@end
+
+@implementation F3HModelTests
+
+- (void)setUp {
+    [super setUp];
+    // Put setup code here. This method is called before the invocation of each test method in the class.
+}
+
+- (void)tearDown {
+    // Put teardown code here. This method is called after the invocation of each test method in the class.
+    [super tearDown];
+}
+
+// Test basic scenario: no moves or merges
+- (void)testModelMerge1 {
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:5 winValue:2048 delegate:nil];
+    XCTAssertNotNil(model, @"model should not be nil, when created with test parameters");
+    
+    F3HTileModel *t1 = [F3HTileModel emptyTile];
+    t1.value = 1;
+    t1.empty = NO;
+    F3HTileModel *t2 = [F3HTileModel emptyTile];
+    t2.value = 2;
+    t2.empty = NO;
+    F3HTileModel *t3 = [F3HTileModel emptyTile];
+    t3.value = 4;
+    t3.empty = NO;
+    F3HTileModel *t4 = [F3HTileModel emptyTile];
+    t4.value = 8;
+    t4.empty = NO;
+    F3HTileModel *t5 = [F3HTileModel emptyTile];
+    t5.value = 1;
+    t5.empty = NO;
+    
+    NSArray *tiles = @[t1, t2, t3, t4, t5];
+    NSArray *moveOrders = [model mergeGroup:tiles];
+    XCTAssert([moveOrders count] == 0, @"No move orders should have happened");
+}
+
+// Test basic scenario: some moves
+- (void)testModelMerge2 {
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:5 winValue:2048 delegate:nil];
+    XCTAssertNotNil(model, @"model should not be nil, when created with test parameters");
+    
+    F3HTileModel *t1 = [F3HTileModel emptyTile];
+    t1.value = 1;
+    t1.empty = NO;
+    F3HTileModel *t2 = [F3HTileModel emptyTile];
+    // T2 is empty
+    F3HTileModel *t3 = [F3HTileModel emptyTile];
+    t3.value = 4;
+    t3.empty = NO;
+    F3HTileModel *t4 = [F3HTileModel emptyTile];
+    // T4 is empty
+    F3HTileModel *t5 = [F3HTileModel emptyTile];
+    t5.value = 1;
+    t5.empty = NO;
+    
+    NSArray *tiles = @[t1, t2, t3, t4, t5];
+    NSArray *moveOrders = [model mergeGroup:tiles];
+    XCTAssert([moveOrders count] == 2, @"Two move orders should have happened (got %d)", [moveOrders count]);
+    // Check the move orders
+    F3HMoveOrder *order = moveOrders[0];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 2, @"First move order should have source 2 (to destination 1)");
+    XCTAssertTrue(order.destination == 1, @"First move order should have destination 1 (from source 2)");
+    
+    order = moveOrders[1];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 4, @"Second move order should have source 4 (to destination 2)");
+    XCTAssertTrue(order.destination == 2, @"Second move order should have destination 2 (from source 4)");
+}
+
+// Test basic scenario: no moves, one merge at end
+- (void)testModelMerge3 {
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:5 winValue:2048 delegate:nil];
+    XCTAssertNotNil(model, @"model should not be nil, when created with test parameters");
+    
+    F3HTileModel *t1 = [F3HTileModel emptyTile];
+    t1.value = 1;
+    t1.empty = NO;
+    F3HTileModel *t2 = [F3HTileModel emptyTile];
+    t2.value = 2;
+    t2.empty = NO;
+    F3HTileModel *t3 = [F3HTileModel emptyTile];
+    t3.value = 4;
+    t3.empty = NO;
+    F3HTileModel *t4 = [F3HTileModel emptyTile];
+    t4.value = 1;
+    t4.empty = NO;
+    F3HTileModel *t5 = [F3HTileModel emptyTile];
+    t5.value = 1;
+    t5.empty = NO;
+    
+    NSArray *tiles = @[t1, t2, t3, t4, t5];
+    NSArray *moveOrders = [model mergeGroup:tiles];
+    XCTAssert([moveOrders count] == 1, @"One move order should have happened (got %d)", [moveOrders count]);
+    // Check the move orders
+    F3HMoveOrder *order = moveOrders[0];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 4, @"Move order should have source 4");
+    XCTAssertTrue(order.destination == 3, @"Move order should have destination 3");
+    XCTAssertTrue(order.value == 2, @"Move order should have new value of 2 (received value was %d)", order.value);
+}
+
+// Test advanced scenario: one move, one merge
+- (void)testModelMerge4 {
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:5 winValue:2048 delegate:nil];
+    XCTAssertNotNil(model, @"model should not be nil, when created with test parameters");
+    
+    F3HTileModel *t1 = [F3HTileModel emptyTile];
+    t1.value = 2;
+    t1.empty = NO;
+    F3HTileModel *t2 = [F3HTileModel emptyTile];
+    t2.value = 2;
+    t2.empty = NO;
+    F3HTileModel *t3 = [F3HTileModel emptyTile];
+    t3.value = 16;
+    t3.empty = NO;
+    F3HTileModel *t4 = [F3HTileModel emptyTile];
+    // T4 is empty
+    F3HTileModel *t5 = [F3HTileModel emptyTile];
+    t5.value = 1;
+    t5.empty = NO;
+    
+    NSArray *tiles = @[t1, t2, t3, t4, t5];
+    NSArray *moveOrders = [model mergeGroup:tiles];
+    XCTAssert([moveOrders count] == 3, @"Two move orders should have happened (got %d)", [moveOrders count]);
+    // Check the move orders
+    F3HMoveOrder *order = moveOrders[0];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 1, @"Move order should have source 1");
+    XCTAssertTrue(order.destination == 0, @"Move order should have destination 0");
+    XCTAssertTrue(order.value == 4, @"Move order should have new value of 4 (received value was %d)", order.value);
+    
+    order = moveOrders[1];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 2, @"Move order should have source 4");
+    XCTAssertTrue(order.destination == 1, @"Move order should have destination 2 (was %d)", order.destination);
+    XCTAssertTrue(order.value == 16, @"Move order should have new value of 1 (received value was %d)", order.value);
+    
+    order = moveOrders[2];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 4, @"Move order should have source 4");
+    XCTAssertTrue(order.destination == 2, @"Move order should have destination 2 (was %d)", order.destination);
+    XCTAssertTrue(order.value == 1, @"Move order should have new value of 1 (received value was %d)", order.value);
+}
+
+// Test advanced scenario: multi-merge of 3 equal values
+- (void)testModelMerge5 {
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:5 winValue:2048 delegate:nil];
+    XCTAssertNotNil(model, @"model should not be nil, when created with test parameters");
+    
+    F3HTileModel *t1 = [F3HTileModel emptyTile];
+    t1.value = 2;
+    t1.empty = NO;
+    F3HTileModel *t2 = [F3HTileModel emptyTile];
+    t2.value = 2;
+    t2.empty = NO;
+    F3HTileModel *t3 = [F3HTileModel emptyTile];
+    t3.value = 2;
+    t3.empty = NO;
+    F3HTileModel *t4 = [F3HTileModel emptyTile];
+    // T4 is empty
+    F3HTileModel *t5 = [F3HTileModel emptyTile];
+    // T4 is empty
+    
+    NSArray *tiles = @[t1, t2, t3, t4, t5];
+    NSArray *moveOrders = [model mergeGroup:tiles];
+    XCTAssert([moveOrders count] == 2, @"Two move orders should have happened (got %d)", [moveOrders count]);
+    // Check the move orders
+    F3HMoveOrder *order = moveOrders[0];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 1, @"Move order should have source 1");
+    XCTAssertTrue(order.destination == 0, @"Move order should have destination 0");
+    XCTAssertTrue(order.value == 4, @"Move order should have new value of 4 (received value was %d)", order.value);
+    
+    order = moveOrders[1];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 2, @"Move order should have source 2");
+    XCTAssertTrue(order.destination == 1, @"Move order should have destination 1");
+    XCTAssertTrue(order.value == 2, @"Move order should have new value of 2 (received value was %d)", order.value);
+}
+
+// Test advanced scenario: multiple merges
+- (void)testModelMerge6 {
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:5 winValue:2048 delegate:nil];
+    XCTAssertNotNil(model, @"model should not be nil, when created with test parameters");
+    
+    F3HTileModel *t1 = [F3HTileModel emptyTile];
+    t1.value = 2;
+    t1.empty = NO;
+    F3HTileModel *t2 = [F3HTileModel emptyTile];
+    t2.value = 2;
+    t2.empty = NO;
+    F3HTileModel *t3 = [F3HTileModel emptyTile];
+    t3.value = 2;
+    t3.empty = NO;
+    F3HTileModel *t4 = [F3HTileModel emptyTile];
+    t4.value = 16;
+    t4.empty = NO;
+    F3HTileModel *t5 = [F3HTileModel emptyTile];
+    t5.value = 16;
+    t5.empty = NO;
+    
+    NSArray *tiles = @[t1, t2, t3, t4, t5];
+    NSArray *moveOrders = [model mergeGroup:tiles];
+    XCTAssert([moveOrders count] == 3, @"3 move orders should have happened (got %d)", [moveOrders count]);
+    // Check the move orders
+    F3HMoveOrder *order = moveOrders[0];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 1, @"Move order should have source 1");
+    XCTAssertTrue(order.destination == 0, @"Move order should have destination 0");
+    XCTAssertTrue(order.value == 4, @"Move order should have new value of 4 (received value was %d)", order.value);
+    
+    order = moveOrders[1];
+    XCTAssertFalse(order.doubleMove, @"Should not be double move");
+    XCTAssertTrue(order.source1 == 2, @"Move order should have source 2");
+    XCTAssertTrue(order.destination == 1, @"Move order should have destination 1");
+    XCTAssertTrue(order.value == 2, @"Move order should have new value of 2 (received value was %d)", order.value);
+    
+    order = moveOrders[2];
+    XCTAssertTrue(order.doubleMove, @"Should be double move");
+    XCTAssertTrue(order.source1 == 3, @"Move order should have source 3");
+    XCTAssertTrue(order.source2 == 4, @"Move order should have source 4");
+    XCTAssertTrue(order.destination == 2, @"Move order should have destination 2");
+    XCTAssertTrue(order.value == 32, @"Move order should have new value of 32 (received value was %d)", order.value);
+}
+
+// Test advanced scenario: multiple spaces and merges A
+- (void)testModelMerge7 {
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:5 winValue:2048 delegate:nil];
+    XCTAssertNotNil(model, @"model should not be nil, when created with test parameters");
+    
+    F3HTileModel *t1 = [F3HTileModel emptyTile];
+    // T1 is empty
+    F3HTileModel *t2 = [F3HTileModel emptyTile];
+    t2.value = 2;
+    t2.empty = NO;
+    F3HTileModel *t3 = [F3HTileModel emptyTile];
+    t3.value = 2;
+    t3.empty = NO;
+    F3HTileModel *t4 = [F3HTileModel emptyTile];
+    t4.value = 16;
+    t4.empty = NO;
+    F3HTileModel *t5 = [F3HTileModel emptyTile];
+    t5.value = 16;
+    t5.empty = NO;
+
+    NSArray *tiles = @[t1, t2, t3, t4, t5];
+    NSArray *moveOrders = [model mergeGroup:tiles];
+    XCTAssert([moveOrders count] == 2, @"2 move orders should have happened (got %d)", [moveOrders count]);
+    // Check the move orders
+    F3HMoveOrder *order = moveOrders[0];
+    XCTAssertTrue(order.doubleMove, @"Should be double move");
+    XCTAssertTrue(order.source1 == 1, @"Move order should have source 1");
+    XCTAssertTrue(order.source2 == 2, @"Move order should have source 2");
+    XCTAssertTrue(order.destination == 0, @"Move order should have destination 0");
+    XCTAssertTrue(order.value == 4, @"Move order should have new value of 4 (received value was %d)", order.value);
+    
+    order = moveOrders[1];
+    XCTAssertTrue(order.doubleMove, @"Should be double move");
+    XCTAssertTrue(order.source1 == 3, @"Move order should have source 3");
+    XCTAssertTrue(order.source2 == 4, @"Move order should have source 4");
+    XCTAssertTrue(order.destination == 1, @"Move order should have destination 1");
+    XCTAssertTrue(order.value == 32, @"Move order should have new value of 32 (received value was %d)", order.value);
+}
+
+// Test advanced scenario: multiple spaces and merges B
+- (void)testModelMerge8 {
+    F3HGameModel *model = [F3HGameModel gameModelWithDimension:5 winValue:2048 delegate:nil];
+    XCTAssertNotNil(model, @"model should not be nil, when created with test parameters");
+    
+    F3HTileModel *t1 = [F3HTileModel emptyTile];
+    t1.value = 4;
+    t1.empty = NO;
+    F3HTileModel *t2 = [F3HTileModel emptyTile];
+    // T2 is empty
+    F3HTileModel *t3 = [F3HTileModel emptyTile];
+    t3.value = 4;
+    t3.empty = NO;
+    F3HTileModel *t4 = [F3HTileModel emptyTile];
+    t4.value = 32;
+    t4.empty = NO;
+    F3HTileModel *t5 = [F3HTileModel emptyTile];
+    t5.value = 32;
+    t5.empty = NO;
+    
+    NSArray *tiles = @[t1, t2, t3, t4, t5];
+    NSArray *moveOrders = [model mergeGroup:tiles];
+    XCTAssert([moveOrders count] == 2, @"2 move orders should have happened (got %d)", [moveOrders count]);
+    // Check the move orders
+    F3HMoveOrder *order = moveOrders[0];
+    XCTAssertFalse(order.doubleMove, @"Should be double move");
+    XCTAssertTrue(order.source1 == 2, @"Move order should have source 2");
+    XCTAssertTrue(order.destination == 0, @"Move order should have destination 0");
+    XCTAssertTrue(order.value == 8, @"Move order should have new value of 8 (received value was %d)", order.value);
+    
+    order = moveOrders[1];
+    XCTAssertTrue(order.doubleMove, @"Should be double move");
+    XCTAssertTrue(order.source1 == 3, @"Move order should have source 3");
+    XCTAssertTrue(order.source2 == 4, @"Move order should have source 4");
+    XCTAssertTrue(order.destination == 1, @"Move order should have destination 1");
+    XCTAssertTrue(order.value == 64, @"Move order should have new value of 64 (received value was %d)", order.value);
+}
+
+@end

+ 22 - 0
NumberTileGame/NumberTileGameTests/NumberTileGameTests-Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>${EXECUTABLE_NAME}</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleSignature</key>
+	<string>????</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 29 - 0
NumberTileGame/NumberTileGameTests/NumberTileGameTests.m

@@ -0,0 +1,29 @@
+//
+//  NumberTileGameTests.m
+//  NumberTileGameTests
+//
+//  Created by Austin Zheng on 3/22/14.
+//
+//
+
+#import <XCTest/XCTest.h>
+
+@interface NumberTileGameTests : XCTestCase
+
+@end
+
+@implementation NumberTileGameTests
+
+- (void)setUp
+{
+    [super setUp];
+    // Put setup code here. This method is called before the invocation of each test method in the class.
+}
+
+- (void)tearDown
+{
+    // Put teardown code here. This method is called after the invocation of each test method in the class.
+    [super tearDown];
+}
+
+@end

+ 2 - 0
NumberTileGame/NumberTileGameTests/en.lproj/InfoPlist.strings

@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+

+ 6 - 0
README.md

@@ -0,0 +1,6 @@
+iOS-2048
+================
+
+iOS drop-in library presenting a clean-room Objective-C/Cocoa implementation of the game 2048.
+
+开源

BIN
screenshots/ss1.png