diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3820f58
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2020 BumMo Koo
+
+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.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f28758c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# WWDC20 PlaygroundBook Sumission
+
+
+
+
+
+
+
+## Description
+
+This PlaygroundBook is accepted as Apple's WWDC20 Student Swift Challenge award. In this PlaygroundBook, a Korean traditional game "Yut (윷)" is introduced. You can get a breif look at what Yut is, how the game works, and get to play it in a 3D environment. It can run on both macOS & iPadOS Swift Playground, but was submitted to run on iPadOS. (Because `PencilKit` does not work on macOS)
+
+
+
+It is made and tested with following tools: Xcode 11, Swift Playground (macOS & iPadOS version), Swift Playgrounds Author Template from Apple, Affinity Photo, Blender, and Reality Converter from Apple.
+
+
+
+It uses following frameworks: `Foundation`, `UIKit`, `SceneKit`, `AVFoundation`, `PencilKit`.
+
+
+
+Resources are from: Wikipedia, Wikimedia, textures.com.
+
+
+
+## Notice
+
+Just 20 minitue before submission, unknown part of the PlaygroundBook suddenly kept crashing, rendering everything unsuable. (Yes, I should have used git ¯\_(ツ)_/¯) I had to sumit a backup version that was unpolisehd and buggy. It missed some features (especially `ARKit` version of the game and the way game work), had typos, unmatching descriptions and dummy images, with some bugs.
+
+
+
+I was originally extremely disappointed that I couldn't submit a fully polished and feature-complete version originally imagined. Now I'm very surprised and happy I got accepted.
+
+
+
+This version of PlaygroundBook keeps everything as submitted, for anybody who wants future reference.
+
+
+
+## LICENSE
+
+See `LICENSE`.
\ No newline at end of file
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Manifest.plist
new file mode 100644
index 0000000..1d746c5
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Manifest.plist
@@ -0,0 +1,18 @@
+
+
+
+
+ Name
+ ChapterOneName
+ Pages
+
+ Introduction.playgroundpage
+ GameIntro.playgroundpage
+ Board.playgroundpage
+ Token.playgroundpage
+ Stick.playgroundpage
+ SceneKit.playgroundpage
+ Epilogue.playgroundpage
+
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/LiveView.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/LiveView.swift
new file mode 100644
index 0000000..da2dead
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/LiveView.swift
@@ -0,0 +1,13 @@
+//
+// See LICENSE folder for this template’s licensing information.
+//
+// Abstract:
+// Instantiates a live view and passes it to the PlaygroundSupport framework.
+//
+
+import UIKit
+import BookCore
+import PlaygroundSupport
+
+// Instantiate a new instance of the live view from BookCore and pass it to PlaygroundSupport.
+PlaygroundPage.current.liveView = BoardViewController.liveView()
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/Manifest.plist
new file mode 100644
index 0000000..497eb5a
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/Manifest.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ Name
+ BoardPageName
+ LiveViewEdgeToEdge
+
+ LiveViewMode
+ VisibleByDefault
+ PlaygroundLoggingMode
+ Off
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/main.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/main.swift
new file mode 100644
index 0000000..c3e3b56
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Board.playgroundpage/main.swift
@@ -0,0 +1,23 @@
+//#-hidden-code
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+//#-end-hidden-code
+
+/*:
+ # Board
+
+ Yut board is a rectangularly shaped, with four straight courses and two diagnoal courses. There is a total of 29 stations. The center stations is said to represent the Polar Star, and others eastern constellation. Historically, there was a round variation of the board.
+
+ 
+
+ When you arrive at stations with branches, you can take the shortcut. However, you can't take the shortcut unless you stop at the branching station.
+
+ One interesting way to win is advance one step, and go back one step go reach the end. It rarely happens, but when it does, it can turn the tide of the game!
+ */
+
+//#-hidden-code
+//let boardTheme = ""
+//let tokenTheme = ""
+//#-end-hidden-code
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/LiveView.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/LiveView.swift
new file mode 100644
index 0000000..51a876c
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/LiveView.swift
@@ -0,0 +1,14 @@
+//
+// See LICENSE folder for this template’s licensing information.
+//
+// Abstract:
+// Instantiates a live view and passes it to the PlaygroundSupport framework.
+//
+
+import UIKit
+import BookCore
+import PlaygroundSupport
+
+// Instantiate a new instance of the live view from BookCore and pass it to PlaygroundSupport.
+//PlaygroundPage.current.liveView = EpilogueViewController.liveView()
+PlaygroundPage.current.liveView = IntroductionViewController.liveView()
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/Manifest.plist
new file mode 100644
index 0000000..1920585
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/Manifest.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ Name
+ EpiloguePageName
+ LiveViewEdgeToEdge
+
+ LiveViewMode
+ VisibleByDefault
+ PlaygroundLoggingMode
+ Off
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/main.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/main.swift
new file mode 100644
index 0000000..49165b1
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Epilogue.playgroundpage/main.swift
@@ -0,0 +1,53 @@
+//#-hidden-code
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+//#-end-hidden-code
+
+/*:
+ # Thanks for playing!
+
+ Hope you enjoyed the Yut game. I hope this playground triggered some interest in Korean culture!
+
+ ---
+
+ Yut - a Tranditional Korean Game
+
+ Playground made with ❤️ by BumMo Koo
+
+ Built using Xcode 11 and Swift Playground
+
+ Using Swift Playgrounds Author Template from Apple
+
+ Foundation, UIKit, SceneKit, AVFoundation
+
+ Image from Wikipedia and Wikimedia
+
+ 2D assets created with Affinity Photo
+
+ 3D assets created with Blender
+
+ USDZ converted using Reality Converter
+
+ Textures from textures.com
+
+
+ WWDC 2020
+ Swift Student Challenge
+
+ BumMo Koo
+ May 2020
+
+ May 2020
+ */
+
+//#-hidden-code
+/*
+
+
+
+ */
+//#-end-hidden-code
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/LiveView.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/LiveView.swift
new file mode 100644
index 0000000..7690ce4
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/LiveView.swift
@@ -0,0 +1,13 @@
+//
+// See LICENSE folder for this template’s licensing information.
+//
+// Abstract:
+// Instantiates a live view and passes it to the PlaygroundSupport framework.
+//
+
+import UIKit
+import BookCore
+import PlaygroundSupport
+
+// Instantiate a new instance of the live view from BookCore and pass it to PlaygroundSupport.
+PlaygroundPage.current.liveView = GameIntroViewController.liveView()
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/Manifest.plist
new file mode 100644
index 0000000..da9d903
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/Manifest.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ Name
+ GameIntroductionPageName
+ LiveViewEdgeToEdge
+
+ LiveViewMode
+ VisibleByDefault
+ PlaygroundLoggingMode
+ Off
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/main.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/main.swift
new file mode 100644
index 0000000..8630aab
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/GameIntro.playgroundpage/main.swift
@@ -0,0 +1,20 @@
+//#-hidden-code
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+//#-end-hidden-code
+
+/*:
+ # Yut
+
+ Yut, pronounced "Yoot" (/juːt/), is a traditional board game in Korea. It is often played during Korean Lunar New Year's Day when family and relatives are gathered.
+
+ In fact, I got the idea of making this playground this year, while playing Yut with my family!
+
+ 
+
+ To here the pronunciation of "Yut", run the playground and hear the pronunciation.
+
+ Also, yut is written as "윷" in Korea. Try to write the word with an Apple Pencil or your finger.
+ */
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/LiveView.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/LiveView.swift
new file mode 100644
index 0000000..2957ead
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/LiveView.swift
@@ -0,0 +1,13 @@
+//
+// See LICENSE folder for this template’s licensing information.
+//
+// Abstract:
+// Instantiates a live view and passes it to the PlaygroundSupport framework.
+//
+
+import UIKit
+import BookCore
+import PlaygroundSupport
+
+// Instantiate a new instance of the live view from BookCore and pass it to PlaygroundSupport.
+PlaygroundPage.current.liveView = IntroductionViewController.liveView()
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/Manifest.plist
new file mode 100644
index 0000000..a7e8ca6
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/Manifest.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ Name
+ IntroductionPageName
+ LiveViewEdgeToEdge
+
+ LiveViewMode
+ VisibleByDefault
+ PlaygroundLoggingMode
+ Off
+ PosterReference
+ Cover.png
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/main.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/main.swift
new file mode 100644
index 0000000..352cb92
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Introduction.playgroundpage/main.swift
@@ -0,0 +1,23 @@
+//#-hidden-code
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+//#-end-hidden-code
+
+/*:
+ # Introduction
+
+
+ Welcome!
+
+ 
+
+ In this playground, I will introduce you a Korean traditional game - **Yut**.
+
+ You will get a breif introduction of the Yut game, will learn the rules and how to play, and finally play the yourself.
+
+ This playground mainly showcases use of `SceneKit` and `ARKit`. Other frameworks and tools used to create this playground are mentioned at the end.
+
+ **Let's begin!**
+ */
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/LiveView.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/LiveView.swift
new file mode 100644
index 0000000..d19c1f3
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/LiveView.swift
@@ -0,0 +1,13 @@
+//
+// See LICENSE folder for this template’s licensing information.
+//
+// Abstract:
+// Instantiates a live view and passes it to the PlaygroundSupport framework.
+//
+
+import UIKit
+import BookCore
+import PlaygroundSupport
+
+// Instantiate a new instance of the live view from BookCore and pass it to PlaygroundSupport.
+PlaygroundPage.current.liveView = SceneKitGameViewController.liveView()
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/Manifest.plist
new file mode 100644
index 0000000..b76cd20
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/Manifest.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ Name
+ SceneKitPageName
+ LiveViewEdgeToEdge
+
+ LiveViewMode
+ VisibleByDefault
+ PlaygroundLoggingMode
+ Off
+ MaximumSupportedExecutionSpeed
+ Fastest
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/main.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/main.swift
new file mode 100644
index 0000000..b99e4d6
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/SceneKit.playgroundpage/main.swift
@@ -0,0 +1,25 @@
+//#-hidden-code
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+//#-end-hidden-code
+
+/*:
+ # Let's Play!
+
+ Now that you learned the basics of the game, it's finally time to experience the Yut game!
+
+ * Tap "Throw" or shake device to throw sticks.
+ * Tap on tokens to see featsible steps.
+ * Stations you can move to are highlighted with a red sphere.
+
+ Have fun!
+ */
+
+//#-hidden-code
+
+//let player1Name = ""
+//let player2Name = ""
+
+//#-end-hidden-code
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/LiveView.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/LiveView.swift
new file mode 100644
index 0000000..8c89c3f
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/LiveView.swift
@@ -0,0 +1,13 @@
+//
+// See LICENSE folder for this template’s licensing information.
+//
+// Abstract:
+// Instantiates a live view and passes it to the PlaygroundSupport framework.
+//
+
+import UIKit
+import BookCore
+import PlaygroundSupport
+
+// Instantiate a new instance of the live view from BookCore and pass it to PlaygroundSupport.
+PlaygroundPage.current.liveView = StickViewController.liveView()
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/Manifest.plist
new file mode 100644
index 0000000..a1bcba4
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/Manifest.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ Name
+ StickPageName
+ LiveViewEdgeToEdge
+
+ LiveViewMode
+ VisibleByDefault
+ PlaygroundLoggingMode
+ Off
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/main.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/main.swift
new file mode 100644
index 0000000..5d913a7
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Stick.playgroundpage/main.swift
@@ -0,0 +1,34 @@
+//#-hidden-code
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+//#-end-hidden-code
+
+/*:
+ # Yut Sticks
+
+ 
+
+ To advance through stations on the board, you throw four Yut sticks just like a dice. Each stick has a front (flat side) and a back side (round side) and are made with wood.
+
+ Number of fronts and backs determine how many stations you can advance. Each combination even has a unique name!
+
+ * Do - One front. Advance one.
+ * Gae - Two front. Advance two.
+ * Geol - Three front. Advance three.
+ * Yut - Four front. Advance four. Throw one more time.
+ * Mo - Four back. Advance five. Throw one more time.
+
+ Interestingly, each represents an animal, too.
+
+ * Do - 🐖 Pig.
+ * Gae - 🐕 Dog.
+ * Geol - 🐑 Sheep.
+ * Yut - 🐂 Cow.
+ * Mo - 🐎 Horse.
+
+ (Wait, does that mean the name of this game is "Cow" game? Huh)
+
+ Try throwing the Yut sticks yourself! You can either shake your device or tap "Throw" button.
+ */
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/LiveView.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/LiveView.swift
new file mode 100644
index 0000000..db4f91c
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/LiveView.swift
@@ -0,0 +1,13 @@
+//
+// See LICENSE folder for this template’s licensing information.
+//
+// Abstract:
+// Instantiates a live view and passes it to the PlaygroundSupport framework.
+//
+
+import UIKit
+import BookCore
+import PlaygroundSupport
+
+// Instantiate a new instance of the live view from BookCore and pass it to PlaygroundSupport.
+PlaygroundPage.current.liveView = TokenViewController.liveView()
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/Manifest.plist
new file mode 100644
index 0000000..e04ad60
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/Manifest.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ Name
+ TokenPageName
+ LiveViewEdgeToEdge
+
+ LiveViewMode
+ VisibleByDefault
+ PlaygroundLoggingMode
+ Off
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/main.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/main.swift
new file mode 100644
index 0000000..e2d3268
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Chapters/Chapter1.playgroundchapter/Pages/Token.playgroundpage/main.swift
@@ -0,0 +1,21 @@
+//#-hidden-code
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+//#-end-hidden-code
+
+/*:
+ # Token
+
+ There is a total of 8 tokens, 4 per team. Goal of the game is to make all 4 tokens make a full round trip of the board and com back home.
+
+ There is no predeterimed token shape or material. Any objects that can easily differentiate teams will work, like coins, go, or even rocks.
+
+ If your token arrives at station with opponent's token, your opponent's token has to restart.
+
+ If multiple tokens of yours stop at same station, they can move together. It's faster way to win the game, but if you get caught by your opponent, there's a huge consequences!
+
+ Since it is easy to draw the board and get objects as tokens, you can play the game without specialied equipment!
+
+ */
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Manifest.plist b/Yut v2.1 Submitted.playgroundbook/Contents/Manifest.plist
new file mode 100644
index 0000000..4284c97
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Manifest.plist
@@ -0,0 +1,34 @@
+
+
+
+
+ Chapters
+
+ Chapter1.playgroundchapter
+
+ ContentIdentifier
+ com.example.PlaygroundBook
+ ContentVersion
+ 1.0
+ DeploymentTarget
+ ios13.1
+ DevelopmentRegion
+ en
+ ImageReference
+ Cover.png
+ Name
+ PlaygroundBookName
+ SupportsDarkMode
+
+ SwiftVersion
+ 5.1
+ UserAutoImportedAuxiliaryModules
+
+ BookAPI
+
+ UserModuleMode
+ Full
+ Version
+ 7.0
+
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookAPI.playgroundmodule/Sources/Dummy.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookAPI.playgroundmodule/Sources/Dummy.swift
new file mode 100644
index 0000000..d85213f
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookAPI.playgroundmodule/Sources/Dummy.swift
@@ -0,0 +1,8 @@
+//
+// Dummy.swift
+// BookAPI
+//
+// Created by BumMo Koo on 2020/05/18.
+//
+
+import Foundation
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/OSLog.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/OSLog.swift
new file mode 100644
index 0000000..622b3a6
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/OSLog.swift
@@ -0,0 +1,30 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+import os.log
+
+public extension OSLog {
+ // MARK: - Log
+ static var app: OSLog {
+ return log(category: "application")
+ }
+
+ static var sceneKit: OSLog {
+ return log(category: "sceneKit")
+ }
+
+ static var game: OSLog {
+ return log(category: "game")
+ }
+
+ // MARK: - Helper
+ private static func log(category: String) -> OSLog {
+ let identifier = Bundle.main.bundleIdentifier ?? "com.gbmksquare.WWDC2020"
+ return OSLog(subsystem: identifier, category: category)
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/SCNMaterial.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/SCNMaterial.swift
new file mode 100644
index 0000000..c1f3dd1
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/SCNMaterial.swift
@@ -0,0 +1,19 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import SceneKit
+
+extension SCNMaterial {
+ static var floorMaterial: SCNMaterial {
+ let material = SCNMaterial()
+ material.diffuse.contents = UIColor.systemGreen
+ material.metalness.contents = 0.05
+ material.specular.contents = 0.05
+ return material
+ }
+}
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/SCNNode.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/SCNNode.swift
new file mode 100644
index 0000000..d003158
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/SCNNode.swift
@@ -0,0 +1,28 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import SceneKit
+
+extension SCNNode {
+ var fittingBox: SCNBox {
+ let max = boundingBox.max
+ let min = boundingBox.min
+ let width = CGFloat(max.x - min.x)
+ let height = CGFloat(max.y - min.y)
+ let length = CGFloat(max.z - min.z)
+ return SCNBox(width: width, height: height, length: length, chamferRadius: 0)
+ }
+
+ func fittingBox(scaled scale: CGFloat) -> SCNBox {
+ let max = boundingBox.max
+ let min = boundingBox.min
+ let width = CGFloat(max.x - min.x) * scale
+ let height = CGFloat(max.y - min.y) * scale
+ let length = CGFloat(max.z - min.z) * scale
+ return SCNBox(width: width, height: height, length: length, chamferRadius: 0)
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/UIViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/UIViewController.swift
new file mode 100644
index 0000000..35adece
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Extension/UIViewController.swift
@@ -0,0 +1,23 @@
+//
+// UIViewController.swift
+// BookCore
+//
+// Created by BumMo Koo on 2020/05/18.
+//
+
+import UIKit
+import PlaygroundSupport
+
+extension UIViewController: PlaygroundLiveViewMessageHandler, PlaygroundLiveViewSafeAreaContainer {
+ public static func liveView() -> PlaygroundLiveViewable {
+ return Self.instantiateFromStoryboard()
+ }
+}
+
+public extension UIViewController {
+ static func instantiateFromStoryboard() -> UIViewController {
+ let identifier = String(describing: self)
+ let storyboard = UIStoryboard(name: "Main", bundle: nil)
+ return storyboard.instantiateViewController(identifier: identifier)
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/SceneSettings.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/SceneSettings.swift
new file mode 100644
index 0000000..36416d4
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/SceneSettings.swift
@@ -0,0 +1,35 @@
+//
+// SceneSettings.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import SceneKit
+
+enum SceneSettings {
+ static let pointLightNodePosition = SCNVector3(0, 10, 10)
+
+ static let cameraZNear: Double = 0
+ static let cameraZFar: Double = 50
+
+ static let cameraNodePosition = SCNVector3(0, 2.5, -2.5)
+ static let cameraNodeEulerAngles = SCNVector3(-Double.pi/3.5, Double.pi, 0)
+
+ static let cameraNodeBoardPosition = SCNVector3(0, 4, -2.5)
+ static let cameraNodeBoardEulerAngles = SCNVector3(-Double.pi/3, Double.pi, 0)
+
+ static let startNodePosition = SCNVector3(0, 0, -2)
+ static let endNodePosition = SCNVector3(0, 0, 2)
+
+ static let boardNodeName = "board"
+ static let stickNodeName = "stick_"
+ static let tokenNodeName = "token_"
+ static let helpStationNodeName = "help_"
+
+ static let stickThrowForce = SCNVector3(0, 1.25, 0)
+ static let stickThrowAt = SCNVector3(0, 0, 0.5)
+
+ static let animationDuration: CFTimeInterval = 1
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/TokenType.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/TokenType.swift
new file mode 100644
index 0000000..d0fabaf
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/TokenType.swift
@@ -0,0 +1,25 @@
+//
+// TokenType.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public enum TokenType {
+ case plastic
+
+ public var player1ResourceName: String {
+ switch self {
+ case .plastic: return "Token_Plastic_red"
+ }
+ }
+
+ public var player2ResourceName: String {
+ switch self {
+ case .plastic: return "Token_Plastic_blue"
+ }
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/URL.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/URL.swift
new file mode 100644
index 0000000..7d05b9f
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/Model/URL.swift
@@ -0,0 +1,19 @@
+//
+// URL.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public extension URL {
+ static let boardURL = Bundle.main.url(forResource: "Board", withExtension: "usdz")!
+ static let stickURL = Bundle.main.url(forResource: "Stick", withExtension: "usdz")!
+
+ static func usdzURL(for name: String) -> URL {
+ return Bundle.main.url(forResource: name, withExtension: "usdz")!
+ }
+}
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/ARKitGameViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/ARKitGameViewController.swift
new file mode 100644
index 0000000..155390c
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/ARKitGameViewController.swift
@@ -0,0 +1,19 @@
+//
+// ARKitGameViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+#if canImport(PlaygroundSupport)
+import PlaygroundSupport
+#endif
+
+public class ARKitGameViewController: UIViewController {
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/BoardViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/BoardViewController.swift
new file mode 100644
index 0000000..703056c
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/BoardViewController.swift
@@ -0,0 +1,37 @@
+//
+// BoardViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+import SceneKit
+#if canImport(PlaygroundSupport)
+import PlaygroundSupport
+#endif
+
+public class BoardViewController: SceneViewController {
+ private var boardNode: SCNNode!
+
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ addLight()
+ addCamera(position: SceneSettings.cameraNodeBoardPosition, eulerAngles: SceneSettings.cameraNodeBoardEulerAngles)
+ addFloor()
+ addBoard()
+ }
+
+ // MARK: - Setup
+ private func addBoard() {
+ let boardURL = URL.boardURL
+ let boardNode = SCNReferenceNode(url: boardURL)!
+ boardNode.name = SceneSettings.boardNodeName
+ boardNode.load()
+ sceneView.scene?.rootNode.addChildNode(boardNode)
+ boardNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: boardNode.fittingBox))
+ self.boardNode = boardNode
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/EpilogueViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/EpilogueViewController.swift
new file mode 100644
index 0000000..9ce047b
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/EpilogueViewController.swift
@@ -0,0 +1,19 @@
+//
+// EpilogueViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+#if canImport(PlaygroundSupport)
+import PlaygroundSupport
+#endif
+
+public class EpilogueViewController: UIViewController {
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Board.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Board.swift
new file mode 100644
index 0000000..bd9c71e
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Board.swift
@@ -0,0 +1,254 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+import os.log
+
+public class Board {
+ public let start: BoardPosition
+ public let end: BoardPosition
+
+ public let p1: BoardPosition
+ public let p2: BoardPosition
+ public let p3: BoardPosition
+ public let p4: BoardPosition
+ public let p5: BoardPosition
+ public let p6: BoardPosition
+ public let p7: BoardPosition
+ public let p8: BoardPosition
+ public let p9: BoardPosition
+ public let p10: BoardPosition
+ public let p11: BoardPosition
+ public let p12: BoardPosition
+ public let p13: BoardPosition
+ public let p14: BoardPosition
+ public let p15: BoardPosition
+ public let p16: BoardPosition
+ public let p17: BoardPosition
+ public let p18: BoardPosition
+ public let p19: BoardPosition
+ public let p20: BoardPosition
+
+ public let d1_1: BoardPosition
+ public let d1_2: BoardPosition
+ public let d1_3: BoardPosition
+ public let d1_4: BoardPosition
+ public let center: BoardPosition
+ public let d2_1: BoardPosition
+ public let d2_2: BoardPosition
+ public let d2_3: BoardPosition
+ public let d2_4: BoardPosition
+
+ private let allPositions: [BoardPosition]
+
+ public let player1Pieces: [Piece]
+ public let player2Pieces: [Piece]
+
+ public weak var delegate: BoardDelegate?
+
+ // MARK: - Initialization
+ public init(player1: Player, player2: Player) {
+ start = BoardPosition(name: "start", type: .start)
+ end = BoardPosition(name: "end", type: .end)
+
+ p1 = BoardPosition(name: "p1", type: .path)
+ p2 = BoardPosition(name: "p2", type: .path)
+ p3 = BoardPosition(name: "p3", type: .path)
+ p4 = BoardPosition(name: "p4", type: .path)
+ p5 = BoardPosition(name: "p5", type: .branch)
+ p6 = BoardPosition(name: "p6", type: .path)
+ p7 = BoardPosition(name: "p7", type: .path)
+ p8 = BoardPosition(name: "p8", type: .path)
+ p9 = BoardPosition(name: "p9", type: .path)
+ p10 = BoardPosition(name: "p10", type: .branch)
+ p11 = BoardPosition(name: "p11", type: .path)
+ p12 = BoardPosition(name: "p12", type: .path)
+ p13 = BoardPosition(name: "p13", type: .path)
+ p14 = BoardPosition(name: "p14", type: .path)
+ p15 = BoardPosition(name: "p15", type: .branch)
+ p16 = BoardPosition(name: "p16", type: .path)
+ p17 = BoardPosition(name: "p17", type: .path)
+ p18 = BoardPosition(name: "p18", type: .path)
+ p19 = BoardPosition(name: "p19", type: .path)
+ p20 = BoardPosition(name: "p20", type: .path)
+ d1_1 = BoardPosition(name: "d1_1", type: .path)
+ d1_2 = BoardPosition(name: "d1_2", type: .path)
+ d1_3 = BoardPosition(name: "d1_3", type: .path)
+ d1_4 = BoardPosition(name: "d1_4", type: .path)
+ center = BoardPosition(name: "center", type: .center)
+ d2_1 = BoardPosition(name: "d2_1", type: .path)
+ d2_2 = BoardPosition(name: "d2_2", type: .path)
+ d2_3 = BoardPosition(name: "d2_3", type: .path)
+ d2_4 = BoardPosition(name: "d2_4", type: .path)
+
+ allPositions = [start, end, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, d1_1, d1_2, d1_3, d1_4, center, d2_1, d2_2, d2_3, d2_4]
+
+ start.prev = [start]
+ end.prev = [end]
+ p1.prev = [p20]
+ p2.prev = [p1]
+ p3.prev = [p2]
+ p4.prev = [p3]
+ p5.prev = [p4]
+ p6.prev = [p5]
+ p7.prev = [p6]
+ p8.prev = [p7]
+ p9.prev = [p8]
+ p10.prev = [p9]
+ p11.prev = [p10]
+ p12.prev = [p11]
+ p13.prev = [p12]
+ p14.prev = [p13]
+ p15.prev = [p14, d1_4]
+ p16.prev = [p15]
+ p17.prev = [p16]
+ p18.prev = [p17]
+ p19.prev = [p18]
+ p20.prev = [p19, d2_4]
+
+ d1_1.prev = [p5]
+ d1_2.prev = [d1_1]
+ d1_3.prev = [center]
+ d1_4.prev = [d1_3]
+ center.prev = [d1_2, d2_2]
+ d2_1.prev = [p10]
+ d2_2.prev = [d2_1]
+ d2_3.prev = [center]
+ d2_4.prev = [d2_3]
+
+ start.next = [p1]
+ end.next = [end]
+ p1.next = [p2]
+ p2.next = [p3]
+ p3.next = [p4]
+ p4.next = [p5]
+ p5.next = [p6, d1_1]
+ p6.next = [p7]
+ p7.next = [p8]
+ p8.next = [p9]
+ p9.next = [p10]
+ p10.next = [p11, d2_1]
+ p11.next = [p12]
+ p12.next = [p13]
+ p13.next = [p14]
+ p14.next = [p15]
+ p15.next = [p16]
+ p16.next = [p17]
+ p17.next = [p18]
+ p18.next = [p19]
+ p19.next = [p20]
+ p20.next = [end]
+
+ d1_1.next = [d1_2]
+ d1_2.next = [center]
+ d1_3.next = [d1_4]
+ d1_4.next = [p15]
+ center.next = [d1_3, d2_3]
+ d2_1.next = [d2_2]
+ d2_2.next = [center]
+ d2_3.next = [d2_4]
+ d2_4.next = [p20]
+
+ player1Pieces = [
+ Piece(name: "1", player: player1, position: start),
+ Piece(name: "2", player: player1, position: start),
+ Piece(name: "3", player: player1, position: start),
+ Piece(name: "4", player: player1, position: start)
+ ]
+ player2Pieces = [
+ Piece(name: "1", player: player2, position: start),
+ Piece(name: "2", player: player2, position: start),
+ Piece(name: "3", player: player2, position: start),
+ Piece(name: "4", player: player2, position: start)
+ ]
+
+ player1.pieces = player1Pieces
+ player2.pieces = player2Pieces
+ player1.activePiece = player1Pieces.first
+ player2.activePiece = player2Pieces.first
+
+ start.pieces = player1Pieces + player2Pieces
+ }
+
+ // MARK: - Variable
+ public func pieces(for player: Player) -> [Piece] {
+ if player.name == "1" {
+ return player1Pieces
+ } else {
+ return player2Pieces
+ }
+ }
+
+ public func token(withIdentifier identifier: String) -> Piece? {
+ let allTokens = player1Pieces + player2Pieces
+ return allTokens.first { $0.identifier == identifier }
+ }
+
+ public func position(withIdentifier identifier: String) -> BoardPosition? {
+ return allPositions.first { $0.identifier == identifier }
+ }
+
+ public func position(withName name: String) -> BoardPosition? {
+ return allPositions.first { $0.name == name }
+ }
+
+ // MARK: - Action
+ public func move(piece: Piece, to position: BoardPosition, player: Player) -> Bool {
+ let pieces: [Piece]
+ let previousPosition = piece.currentPosition
+ if previousPosition != start {
+ pieces = previousPosition.pieces
+ previousPosition.pieces.removeAll(where: { $0 == piece })
+ } else {
+ pieces = [piece]
+ }
+ os_log(.debug, log: .game, "%{PUBLIC}@에서 이동합니다.", previousPosition.debugDescription)
+ previousPosition.pieces = []
+
+ if position.pieces.count == 0 {
+ os_log(.debug, log: .game, "%{PUBLIC}@이 비어있습니다.", position.debugDescription)
+ position.pieces = pieces
+ pieces.forEach {
+ $0.currentPosition = position
+ }
+ delegate?.board(self, moved: pieces, to: position)
+ } else if position.pieces.count > 0 {
+ os_log(.debug, log: .game, "%{PUBLIC}@에 %{PUBLIC}@ 말이 있습니다.", position.debugDescription, position.pieces.debugDescription)
+ if position != start {
+ // Same player - group
+ if piece.player == position.pieces.first!.player {
+ os_log(.debug, log: .game, "새로운 말이 기존 말을 업습니다.")
+ position.pieces.append(contentsOf: pieces)
+ pieces.forEach {
+ $0.currentPosition = position
+ }
+ delegate?.board(self, moved: pieces, to: position)
+ } else {
+ os_log(.debug, log: .game, "%{PUBLIC}@은 잡혀서 시작 지점으로 이동합니다.", position.pieces.debugDescription)
+ delegate?.board(self, pieceEaten: position.pieces)
+ position.pieces.forEach {
+ $0.currentPosition = start
+ }
+ start.pieces.append(contentsOf: position.pieces)
+ delegate?.board(self, moved: position.pieces, to: start)
+
+ position.pieces = pieces
+ pieces.forEach {
+ $0.currentPosition = position
+ }
+ delegate?.board(self, moved: pieces, to: position)
+ return true
+ }
+ } else {
+ piece.currentPosition = start
+ start.pieces.append(piece)
+ delegate?.board(self, moved: [piece], to: start)
+ }
+ }
+ return false
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/BoardDelegate.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/BoardDelegate.swift
new file mode 100644
index 0000000..0a6f37d
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/BoardDelegate.swift
@@ -0,0 +1,14 @@
+//
+// BoardDelegate.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public protocol BoardDelegate: class {
+ func board(_: Board, moved pieces: [Piece], to position: BoardPosition)
+ func board(_: Board, pieceEaten: [Piece])
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/BoardPosition.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/BoardPosition.swift
new file mode 100644
index 0000000..833bd35
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/BoardPosition.swift
@@ -0,0 +1,48 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public class BoardPosition: Equatable, CustomDebugStringConvertible {
+ public enum PositionType: String, CustomDebugStringConvertible {
+ case start
+ case end
+ case path
+ case branch
+ case center
+
+ // MARK: - Debug
+ public var debugDescription: String {
+ return rawValue.capitalized
+ }
+ }
+
+ public let identifier = UUID().uuidString
+ public let name: String
+ public let type: BoardPosition.PositionType
+
+ public var next = [BoardPosition]()
+ public var prev = [BoardPosition]()
+
+ public var pieces = [Piece]()
+
+ // MARK: - Initalization
+ public init(name: String, type: BoardPosition.PositionType) {
+ self.name = name
+ self.type = type
+ }
+
+ // MARK: - Debug
+ public var debugDescription: String {
+ return name
+ }
+
+ // MARK: - Equatable
+ public static func == (lhs: BoardPosition, rhs: BoardPosition) -> Bool {
+ return lhs.identifier == rhs.identifier
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Game.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Game.swift
new file mode 100644
index 0000000..34c9e41
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Game.swift
@@ -0,0 +1,163 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+import os.log
+
+class Game {
+ let board: Board
+ let player1: Player
+ let player2: Player
+
+ let sticks: StickSet
+
+ weak var delegate: GameDelegate?
+
+ var turnManager: TurnManager
+ private var rollManager: RollManager
+ var pieceMover: PieceMover
+
+ // MARK: - Initliazation
+ init() {
+ player1 = Player(name: "1")
+ player2 = Player(name: "2")
+ board = Board(player1: player1, player2: player2)
+
+ sticks = StickSet()
+
+ turnManager = TurnManager(player1: player1, player2: player2)
+ rollManager = RollManager()
+ pieceMover = PieceMover(board: board, player1: player1, player2: player2)
+ board.delegate = self
+ turnManager.delegate = self
+ rollManager.delegate = self
+ pieceMover.game = self
+ }
+
+ // MARK: - Game
+ func startGame() {
+ DispatchQueue.main.async {
+ self.delegate?.started(game: self)
+ self.delegate?.game(self, switchedTurnTo: self.player1)
+ self.delegate?.game(self, waitingRollFrom: self.player1)
+ }
+ }
+
+ func simulateGame() {
+ os_log(.debug, log: .game, "플레이어 1과 플레이어 2의 게임을 시작합니다.")
+
+ func run() {
+ let availableSteps = rollManager.rollForCurrentTurn()
+ let player = turnManager.currentPlayer
+ let (canRollOneMoreTime, gameEnded) = pieceMover.move(steps: availableSteps, for: player)
+ if gameEnded {
+ os_log(.debug, log: .game, "게임이 종료되었습니다. 플레이어 %{PUBLIC}@가 승리했습니다!", player.name)
+ } else {
+ if canRollOneMoreTime {
+
+ } else {
+ turnManager.switchTurn()
+ }
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ run()
+ }
+ }
+ os_log(.debug, log: .game, "\n", player1.name)
+ }
+
+ run()
+ }
+
+ private func endGame() {
+ DispatchQueue.main.async {
+ self.delegate?.game(self, wonBy: self.player1)
+ }
+ }
+
+ // MARK: - Roll
+ func receive(rollResult: StickSet.RollResult) {
+ let player = turnManager.currentPlayer
+ let canRollOneMoreTime = rollManager.receive(result: rollResult)
+ if canRollOneMoreTime {
+ DispatchQueue.main.async {
+ self.delegate?.game(self, waitingRollFrom: player)
+ }
+ return
+ }
+ let steps = rollManager.popAvailableSteps()
+ pieceMover.registerMovableSteps(steps: steps)
+ let destinations = pieceMover.possibleDestinations(for: player.activePiece!)
+ DispatchQueue.main.async {
+ self.delegate?.game(self, player: player, canMove: steps, to: destinations)
+ }
+ }
+
+ // MARK: - Move
+ func selectToken(_ token: Piece) {
+ token.player.activePiece = token
+ let player = turnManager.currentPlayer
+ let steps = pieceMover.remainingSteps
+ if player == token.player, steps.count > 0 {
+ let destinations = pieceMover.possibleDestinations(for: player.activePiece!)
+ DispatchQueue.main.async {
+ self.delegate?.game(self, player: player, canMove: steps, to: destinations)
+ }
+ }
+ }
+
+ func moveCurrentPlayerActiveToken(to position: BoardPosition) {
+ let player = turnManager.currentPlayer
+ let token = player.activePiece!
+
+ let didEnd = player.pieces.map {
+ $0.currentPosition.name == "end"
+ }
+ if !didEnd.contains(false) {
+ delegate?.game(self, wonBy: player)
+ return
+ }
+
+ let hasRemainingSteps = pieceMover.move(piece: token, to: position)
+ if hasRemainingSteps {
+ let destinations = pieceMover.possibleDestinations(for: token)
+ DispatchQueue.main.async {
+ self.delegate?.game(self, player: player, canMove: self.pieceMover.remainingSteps, to: destinations)
+ }
+ } else {
+ turnManager.switchTurn()
+ }
+ }
+}
+
+// MARK: - Delegate
+extension Game: BoardDelegate, TurnManagerDelegate, RollManagerDelegate {
+ // MARK: - Board delegate
+ func board(_: Board, moved pieces: [Piece], to position: BoardPosition) {
+ DispatchQueue.main.async {
+ self.delegate?.game(self, player: pieces.first!.player, pieces: pieces, movedTo: position)
+ }
+ }
+
+ func board(_: Board, pieceEaten: [Piece]) {
+ DispatchQueue.main.async {
+ self.delegate?.game(self, eatenPieces: pieceEaten)
+ }
+ }
+
+ // MARK: Turn manager delegate
+ func turnManager(turnManager: TurnManager, switchedTurnTo player: Player) {
+ DispatchQueue.main.async {
+ self.delegate?.game(self, switchedTurnTo: player)
+ }
+ }
+
+ // MARK: Roll manager delegate
+ func rollManager(rollManager: RollManager, rolledToGo steps: [Int]) {
+// delegate?.game(self, canGoForward: steps)
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/GameDelegate.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/GameDelegate.swift
new file mode 100644
index 0000000..c70cf36
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/GameDelegate.swift
@@ -0,0 +1,18 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+protocol GameDelegate: class {
+ func started(game: Game)
+ func game(_: Game, switchedTurnTo player: Player)
+ func game(_: Game, waitingRollFrom player: Player)
+ func game(_: Game, player: Player, canMove steps: [Int], to positions: [BoardPosition])
+ func game(_: Game, player: Player, pieces: [Piece], movedTo position: BoardPosition)
+ func game(_: Game, eatenPieces: [Piece])
+ func game(_: Game, wonBy player: Player)
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/PathFinder.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/PathFinder.swift
new file mode 100644
index 0000000..25c032c
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/PathFinder.swift
@@ -0,0 +1,42 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public final class PathFinder {
+ public static let shared = PathFinder()
+
+ // MARK: - Action
+ public func possibleDestination(from position: BoardPosition,
+ moving steps: Int,
+ isStartingPosition: Bool = false) -> [BoardPosition] {
+ // No more steps
+ if steps == 0 {
+ return [position]
+ }
+ // Already reached end
+ if position.type == .end {
+ return [position]
+ }
+ // Moving back
+ if steps == -1 {
+ if position.type == .start {
+ return [position]
+ }
+ return position.prev
+ }
+ if (position.type == .branch || position.type == .center) && isStartingPosition == true {
+ return position.next.compactMap {
+ possibleDestination(from: $0, moving: steps - 1).first
+ }
+ }
+ if let next = position.next.first {
+ return possibleDestination(from: next, moving: steps - 1)
+ }
+ fatalError()
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Piece.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Piece.swift
new file mode 100644
index 0000000..2875cf5
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Piece.swift
@@ -0,0 +1,36 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public class Piece: Equatable, CustomDebugStringConvertible {
+ public let identifier = UUID().uuidString
+ public let name: String
+ public let player: Player
+ public var currentPosition: BoardPosition
+ public var history: [BoardPosition]
+
+ // MARK: - Initalization
+ init(name: String,
+ player: Player,
+ position: BoardPosition) {
+ self.name = name
+ self.player = player
+ self.currentPosition = position
+ history = []
+ }
+
+ // MARK: - Debug
+ public var debugDescription: String {
+ return "Piece \(name) of Player \(player.name) at \(currentPosition)"
+ }
+
+ // MARK: - Equatable
+ public static func == (lhs: Piece, rhs: Piece) -> Bool {
+ return lhs.identifier == rhs.identifier
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/PieceMover.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/PieceMover.swift
new file mode 100644
index 0000000..b9f8e5e
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/PieceMover.swift
@@ -0,0 +1,83 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+import os.log
+
+class PieceMover {
+ private let board: Board
+ private let player1: Player
+ private let player2: Player
+
+ public private(set) var remainingSteps = [Int]()
+ private var register = [(Int, [BoardPosition])]()
+
+ public weak var game: Game?
+
+ // MARK: - Initialization
+ public init(board: Board, player1: Player, player2: Player) {
+ self.board = board
+ self.player1 = player1
+ self.player2 = player2
+ }
+
+ // MARK: - Action
+ func move(steps: [Int], for player: Player) -> (Bool, Bool) {
+ os_log(.debug, log: .game, "플레이어 %{PUBLIC}@는 %{PUBLIC}@만큼 전진할 수 있습니다.", player.name, steps.debugDescription)
+ var canRollOneMoreTime = false
+ for step in steps {
+ let piece = board.pieces(for: player).first!
+ let destination = PathFinder.shared.possibleDestination(from: piece.currentPosition, moving: step)
+ os_log(.debug, log: .game, "%{PUBLIC}@ 말은 %{PUBLIC}@으로 이동할 수 있습니다.", piece.debugDescription, destination.debugDescription)
+ if destination.count == 1 {
+ canRollOneMoreTime = board.move(piece: piece, to: destination.first!, player: player)
+ } else {
+ canRollOneMoreTime = board.move(piece: piece, to: destination.last!, player: player)
+ }
+ os_log(.debug, log: .game, "%{PUBLIC}@ 말은 %{PUBLIC}@으로 이동합니다.", piece.debugDescription, destination.debugDescription)
+ if canRollOneMoreTime {
+ os_log(.debug, log: .game, "상대방의 말을 잡아 한 번 더 기회가 주어집니다.")
+ }
+
+ if destination.first!.type == .end {
+ os_log(.debug, log: .game, "%{PUBLIC}@ 말이 종점에 도착했습니다!", piece.debugDescription)
+ return (canRollOneMoreTime, true)
+ }
+ }
+ return (canRollOneMoreTime, false)
+ }
+
+ // MARK: - Action
+ func registerMovableSteps(steps: [Int]) {
+ remainingSteps = steps
+ }
+
+ func possibleDestinations(for piece: Piece) -> [BoardPosition] {
+ register = remainingSteps.map {
+ ($0, PathFinder.shared.possibleDestination(from: piece.currentPosition, moving: $0, isStartingPosition: true))
+ }
+ return register.flatMap { $1 }
+ }
+
+ func move(piece: Piece, to position: BoardPosition) -> Bool {
+ let canRollOneMoreTime = board.move(piece: piece, to: position, player: piece.player)
+
+ if let index = register.firstIndex(where: { $0.1.contains(position) }) {
+ remainingSteps.remove(at: index)
+ register.removeAll()
+ }
+ if canRollOneMoreTime, let game = game {
+ game.delegate?.game(game, waitingRollFrom: game.turnManager.currentPlayer)
+ return true
+ }
+ if remainingSteps.count > 0 {
+ return true
+ } else {
+ return false
+ }
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Player.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Player.swift
new file mode 100644
index 0000000..f32eb6b
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Player.swift
@@ -0,0 +1,26 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public class Player: Equatable {
+ public let identifier = UUID().uuidString
+ public let name: String
+
+ public var pieces = [Piece]()
+ public var activePiece: Piece?
+
+ // MARK: - Initiazliation
+ public init(name: String) {
+ self.name = name
+ }
+
+ // MARK: - Equatable
+ public static func == (lhs: Player, rhs: Player) -> Bool {
+ return lhs.identifier == rhs.identifier
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/RollManager.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/RollManager.swift
new file mode 100644
index 0000000..a2319ff
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/RollManager.swift
@@ -0,0 +1,82 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+import os.log
+
+public class RollManager {
+ private let sticks: StickSet
+
+ private lazy var availableSteps = [Int]()
+
+ public weak var delegate: RollManagerDelegate?
+
+ // MARK: - Initalization
+ init() {
+ sticks = StickSet()
+ }
+
+ // MARK: - Action (Old)
+ @available(*, deprecated)
+ func rollForCurrentTurn(_ clearPreviousSteps: Bool = true) -> [Int] {
+ if clearPreviousSteps {
+ availableSteps = []
+ }
+
+ let (setResult, _) = sticks.roll()
+ availableSteps.append(setResult.steps)
+ if setResult.canRollOneMoreTime {
+ _ = rollForCurrentTurn(false)
+ }
+ os_log(.debug, log: .game, "획득한 칸수는 %{PUBLIC}@", availableSteps.debugDescription)
+ delegate?.rollManager(rollManager: self, rolledToGo: availableSteps)
+ return availableSteps
+ }
+
+ @available(*, deprecated)
+ func recieveRollResult(_ result: [Stick.RollResult], clearPreviousSteps: Bool = true) {
+ if clearPreviousSteps {
+ availableSteps = []
+ }
+
+ let setResult = StickSet.getSetResult(from: result)
+ availableSteps.append(setResult.steps)
+ if setResult.canRollOneMoreTime {
+ // ROll one more time
+ }
+ os_log(.debug, log: .game, "획득한 칸수는 %{PUBLIC}@", availableSteps.debugDescription)
+ delegate?.rollManager(rollManager: self, rolledToGo: availableSteps)
+// return availableSteps
+ }
+
+ // MARK: - Action
+ func roll() -> (StickSet.RollResult, [Stick.RollResult]) {
+ let results = sticks.roll()
+ _ = receive(result: results.0)
+ return results
+ }
+
+ func receive(result: StickSet.RollResult) -> Bool {
+ availableSteps.append(result.steps)
+ os_log(.debug, log: .game, "던진 결과는 \"%{PUBLIC}@\"", result.debugDescription)
+ if result.canRollOneMoreTime {
+ os_log(.debug, log: .game, "한 번 더 던질 수 있습니다.")
+ }
+ os_log(.debug, log: .game, "총 전진 가능한 칸수는 %{PUBLIC}@", availableSteps.debugDescription)
+ return result.canRollOneMoreTime
+ }
+
+ func popAvailableSteps() -> [Int] {
+ let steps = availableSteps
+ availableSteps = []
+ return steps
+ }
+
+ private func clearSteps() {
+ availableSteps = []
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/RollManagerDelegate.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/RollManagerDelegate.swift
new file mode 100644
index 0000000..07a4421
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/RollManagerDelegate.swift
@@ -0,0 +1,12 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public protocol RollManagerDelegate: class {
+ func rollManager(rollManager: RollManager, rolledToGo steps: [Int])
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Stick.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Stick.swift
new file mode 100644
index 0000000..d3b540e
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/Stick.swift
@@ -0,0 +1,59 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public struct Stick {
+ public enum RollResult: String, CaseIterable, CustomDebugStringConvertible {
+ case front, back, frontMarked
+
+ var name: String {
+ switch self {
+ case .front: return "Front"
+ case .back: return "Back"
+ case .frontMarked: return "Front (Marked)"
+ }
+ }
+
+ var localizedName: String {
+ switch self {
+ case .front: return "앞"
+ case .back: return "뒤"
+ case .frontMarked: return "앞 (백도)"
+ }
+ }
+
+ // MARK: - Debug
+ public var debugDescription: String {
+ #if canImport(PlaygroundSupport)
+ return name
+ #else
+ return localizedName
+ #endif
+ }
+ }
+
+ public var isMarked = false
+
+ // MARK: - Initialization
+ public init(isMarked: Bool = false) {
+ self.isMarked = isMarked
+ }
+
+ // MARK: - Action
+ public func roll() -> RollResult {
+ if isMarked == false {
+ var result: Stick.RollResult
+ repeat {
+ result = RollResult.allCases.randomElement()!
+ } while result == .frontMarked
+ return result
+ } else {
+ return RollResult.allCases.randomElement()!
+ }
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/StickSet.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/StickSet.swift
new file mode 100644
index 0000000..5f45ba5
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/StickSet.swift
@@ -0,0 +1,91 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+import os.log
+
+public struct StickSet {
+ public enum RollResult: String, CustomDebugStringConvertible {
+ case `do`, gae, gir, yut, mo, backDo
+
+ var name: String {
+ return rawValue.capitalized
+ }
+
+ var localizedName: String {
+ switch self {
+ case .backDo: return "빽도"
+ case .do: return "도"
+ case .gae: return "개"
+ case .gir: return "걸"
+ case .yut: return "윷"
+ case .mo: return "모"
+ }
+ }
+
+ public var steps: Int {
+ switch self {
+ case .backDo: return -1
+ case .do: return 1
+ case .gae: return 2
+ case .gir: return 3
+ case .yut: return 4
+ case .mo: return 5
+ }
+ }
+
+ public var canRollOneMoreTime: Bool {
+ switch self {
+ case .yut, .mo: return true
+ default: return false
+ }
+ }
+
+ // MARK: - Debug
+ public var debugDescription: String {
+ #if canImport(PlaygroundSupport)
+ return name
+ #else
+ return localizedName
+ #endif
+ }
+ }
+
+ public let sticks = [Stick(),
+ Stick(),
+ Stick(),
+ Stick(isMarked: true)]
+
+ // MARK: - Action
+ public func roll() -> (StickSet.RollResult, [Stick.RollResult]) {
+ let results = sticks.map { $0.roll() }.shuffled()
+ let setResult = StickSet.getSetResult(from: results)
+ os_log(.debug, log: .game, "%{PUBLIC}@!, %{PUBLIC}@", setResult.debugDescription, results.debugDescription)
+ if setResult.canRollOneMoreTime {
+ os_log(.debug, log: .game, "한 번 더!")
+ }
+ return (setResult, results)
+ }
+
+ static func getSetResult(from results: [Stick.RollResult]) -> StickSet.RollResult {
+ let backs = results.filter { $0 == .back }
+ let backCount = backs.count
+ if backCount == 0 {
+ return .yut
+ } else if backCount == 1{
+ return .gir
+ } else if backCount == 2 {
+ return .gae
+ } else if backCount == 3 && results.contains(.frontMarked) {
+ return .backDo
+ } else if backCount == 3 {
+ return .do
+ } else {
+ return .mo
+ }
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/TurnManager.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/TurnManager.swift
new file mode 100644
index 0000000..5728430
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/TurnManager.swift
@@ -0,0 +1,53 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+import os.log
+
+public class TurnManager {
+ private let player1: Player
+ private let player2: Player
+
+ public var isPlayer1Turn = true
+
+ public weak var delegate: TurnManagerDelegate?
+
+ public var currentPlayer: Player {
+ if isPlayer1Turn {
+ return player1
+ } else {
+ return player2
+ }
+ }
+
+ public var opponentPlayer: Player {
+ if isPlayer1Turn {
+ return player2
+ } else {
+ return player1
+ }
+ }
+
+ // MARK: - Initialization
+ public init(player1: Player, player2: Player) {
+ self.player1 = player1
+ self.player2 = player2
+ os_log(.debug, log: .game, "플레이어 %{PUBLIC}@부터 시작합니다.", player1.name)
+ }
+
+ // MARK: - Action
+ public func switchTurn() {
+ isPlayer1Turn = !isPlayer1Turn
+ if isPlayer1Turn {
+ os_log(.debug, log: .game, "플레이어 %{PUBLIC}@의 차례입니다.", player1.name)
+ delegate?.turnManager(turnManager: self, switchedTurnTo: player1)
+ } else {
+ os_log(.debug, log: .game, "플레이어 %{PUBLIC}@의 차례입니다.", player2.name)
+ delegate?.turnManager(turnManager: self, switchedTurnTo: player2)
+ }
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/TurnManagerDelegate.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/TurnManagerDelegate.swift
new file mode 100644
index 0000000..cc7cfa3
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/Game/TurnManagerDelegate.swift
@@ -0,0 +1,12 @@
+//
+// WWDC 2020
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import Foundation
+
+public protocol TurnManagerDelegate: class {
+ func turnManager(turnManager: TurnManager, switchedTurnTo player: Player)
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/GameIntroViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/GameIntroViewController.swift
new file mode 100644
index 0000000..80fb9d0
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/GameIntroViewController.swift
@@ -0,0 +1,53 @@
+//
+// GameIntroViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+import AVFoundation
+import PencilKit
+#if canImport(PlaygroundSupport)
+import PlaygroundSupport
+#endif
+
+public class GameIntroViewController: UIViewController {
+ @IBOutlet private var label: UILabel!
+ private lazy var canvas = PKCanvasView(frame: .zero)
+
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ setup()
+ }
+
+ // MARK: - Setup
+ private func setup() {
+ view.insertSubview(canvas, at: 0)
+ canvas.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ canvas.topAnchor.constraint(equalTo: label.topAnchor),
+ canvas.bottomAnchor.constraint(equalTo: label.bottomAnchor),
+ canvas.leadingAnchor.constraint(equalTo: label.leadingAnchor),
+ canvas.trailingAnchor.constraint(equalTo: label.trailingAnchor)
+ ])
+
+ #if (arch(i386) || arch(x86_64))
+ #else
+ canvas.tool = PKInkingTool(.pencil, color: UIColor.black.withAlphaComponent(0.85), width: 9)
+ #endif
+ }
+
+ // MARK: - User interfaction
+ @IBAction
+ private func tap(play button: UIButton) {
+ let utterance = AVSpeechUtterance(string: "윷")
+ utterance.voice = AVSpeechSynthesisVoice(language: "ko-KR")
+ utterance.rate = AVSpeechUtteranceMinimumSpeechRate
+
+ let synthesizer = AVSpeechSynthesizer()
+ synthesizer.speak(utterance)
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/IntroductionViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/IntroductionViewController.swift
new file mode 100644
index 0000000..ff8640d
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/IntroductionViewController.swift
@@ -0,0 +1,116 @@
+//
+// IntroductionViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+import SceneKit
+#if canImport(PlaygroundSupport)
+import PlaygroundSupport
+#endif
+
+public class IntroductionViewController: SceneViewController {
+ private var titleTextNode: SCNNode!
+ private var subtitleTextNode: SCNNode!
+ private var boardNode: SCNNode!
+ private var stickNodes = [SCNNode]()
+
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ SCNTransaction.animationDuration = 0
+ addLight()
+ addCamera(position: SCNVector3(0, 3, 5), eulerAngles: SCNVector3(0, 0, 0))
+ addFloor()
+ addText()
+ addBoard()
+ addSticks()
+ throwSticks()
+ }
+
+ // MARK: - Setup
+ private func addText() {
+ let titleMaterial = SCNMaterial()
+ titleMaterial.diffuse.contents = #colorLiteral(red: 0.3098039329, green: 0.2039215714, blue: 0.03921568766, alpha: 1)
+ titleMaterial.metalness.contents = 0.15
+ titleMaterial.reflective.contents = 0
+
+ let subtitleMaterial = SCNMaterial()
+ subtitleMaterial.diffuse.contents = #colorLiteral(red: 0.9529411793, green: 0.6862745285, blue: 0.1333333403, alpha: 1)
+ subtitleMaterial.metalness.contents = 0.15
+ subtitleMaterial.reflective.contents = 0
+
+ let titleText = SCNText(string: "Yut", extrusionDepth: 3)
+ titleText.firstMaterial = titleMaterial
+
+ let subtitleText = SCNText(string: "a Game", extrusionDepth: 1.5)
+ subtitleText.firstMaterial = subtitleMaterial
+
+ let titleTextNode = SCNNode(geometry: titleText)
+ sceneView.scene?.rootNode.addChildNode(titleTextNode)
+ titleTextNode.position = SCNVector3(-8, -2, -20)
+ titleTextNode.scale = SCNVector3(0.85, 0.85, 0.85)
+ titleTextNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
+ self.titleTextNode = titleTextNode
+
+ let subtitleTextNode = SCNNode(geometry: subtitleText)
+ sceneView.scene?.rootNode.addChildNode(subtitleTextNode)
+ subtitleTextNode.position = SCNVector3(-2, 0, -17)
+ subtitleTextNode.scale = SCNVector3(0.2, 0.2, 0.2)
+ subtitleTextNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
+ self.titleTextNode = titleTextNode
+ }
+
+ private func addBoard() {
+ let boardURL = URL.boardURL
+ let boardNode = SCNReferenceNode(url: boardURL)!
+ boardNode.name = SceneSettings.boardNodeName
+ boardNode.load()
+ boardNode.position = SCNVector3(-3, 0, -6)
+ boardNode.eulerAngles = SCNVector3(0, -Double.pi/4, 0)
+ boardNode.scale = SCNVector3(3, 3, 3)
+ sceneView.scene?.rootNode.addChildNode(boardNode)
+ boardNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: boardNode.fittingBox))
+ self.boardNode = boardNode
+ }
+
+ private func addSticks() {
+ let stickURL = URL.stickURL
+ stickNodes = (1...20).map { index in
+ let stickNode = SCNReferenceNode(url: stickURL)!
+ stickNode.name = SceneSettings.stickNodeName + "\(index)"
+
+ let randomX = Float.random(in: -2...2)
+ let randomZ = Float.random(in: -10...0)
+
+ stickNode.position = SCNVector3(randomX, 0, randomZ)
+ stickNode.load()
+ stickNode.scale = SCNVector3(3, 3, 3)
+ scene.rootNode.addChildNode(stickNode)
+ return stickNode
+ }
+
+ stickNodes.forEach { node in
+ let physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
+ node.physicsBody = physicsBody
+ }
+ }
+
+ // MARK: - Action
+ private func throwSticks() {
+ stickNodes.forEach { stick in
+ let randomX = Float.random(in: -2...2)
+ let randomY = Float.random(in: 3...10)
+ let force = SCNVector3(x: randomX, y: randomY , z: 0)
+ let position = SCNVector3(x: 0.05, y: 0.05, z: 0.05)
+ stick.physicsBody?.applyForce(force, at: position, asImpulse: true)
+ }
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
+ self.throwSticks()
+ }
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/SceneKitGameViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/SceneKitGameViewController.swift
new file mode 100644
index 0000000..3119185
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/SceneKitGameViewController.swift
@@ -0,0 +1,420 @@
+//
+// SceneKitGameViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+import SceneKit
+import os.log
+#if canImport(PlaygroundSupport)
+import PlaygroundSupport
+#endif
+
+public class SceneKitGameViewController: UIViewController {
+ @IBOutlet private weak var sceneView: SCNView!
+ private var scene: SCNScene {
+ return sceneView.scene!
+ }
+
+ @IBOutlet public var controlView: UIView!
+ @IBOutlet private weak var throwButton: UIButton!
+
+ @IBOutlet private weak var turnLabel: UILabel!
+ @IBOutlet private weak var descriptionLabel: UILabel!
+
+ // MARK: - Node
+ private var cameraNode: SCNNode!
+ private var boardNode: SCNNode!
+ private var stickNodes = [SCNNode]()
+ private var tokenNodes = [SCNNode]()
+
+ private var startNode: SCNNode!
+ private var endNode: SCNNode!
+
+ private var helpNodes = [SCNNode]()
+
+ private lazy var game = Game()
+ private var waitingForRollResult = false {
+ didSet {
+ if waitingForRollResult {
+ throwButton.isEnabled = false
+ } else {
+ throwButton.isEnabled = true
+ }
+ }
+ }
+
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ setup()
+ }
+
+ public override func viewDidAppear(_ animated: Bool) {
+ super.viewDidAppear(animated)
+ game.startGame()
+ }
+
+ // MARK: - Setup
+ private func setup() {
+ game.delegate = self
+
+ // MARK: Scene
+ controlView.layer.cornerRadius = 9
+
+ let scene = SCNScene(named: "Game.scnassets/Empty.scn", inDirectory: nil, options: [.convertUnitsToMeters: 1])!
+ sceneView.scene = scene
+ sceneView.backgroundColor = .black
+// sceneView.allowsCameraControl = true
+// sceneView.showsStatistics = true
+ sceneView.delegate = self
+ // sceneView.debugOptions = .showPhysicsShapes
+ SCNTransaction.animationDuration = SceneSettings.animationDuration
+
+ addLight()
+ addCamera()
+ addFloor()
+
+ addBoard()
+ addStartAndEnd()
+ addSticks()
+ addTokens()
+
+ // MARK: - Else initialization
+ turnLabel.text = "Player 1's"
+
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
+ sceneView.addGestureRecognizer(tapGesture)
+ }
+
+ private func addLight() {
+ let ambientLight = SCNLight()
+ ambientLight.type = .ambient
+ ambientLight.color = UIColor.darkGray
+ let ambientLightNode = SCNNode()
+ ambientLightNode.light = ambientLight
+ sceneView.scene?.rootNode.addChildNode(ambientLightNode)
+
+ let pointLight = SCNLight()
+ pointLight.type = .omni
+ let pointLightNode = SCNNode()
+ pointLightNode.light = pointLight
+ pointLightNode.position = SceneSettings.pointLightNodePosition
+ sceneView.scene?.rootNode.addChildNode(pointLightNode)
+ }
+
+ private func addCamera() {
+ let camera = SCNCamera()
+ camera.zNear = SceneSettings.cameraZNear
+ camera.zFar = SceneSettings.cameraZFar
+ let cameraNode = SCNNode()
+ cameraNode.camera = camera
+ cameraNode.position = SceneSettings.cameraNodeBoardPosition
+ cameraNode.eulerAngles = SceneSettings.cameraNodeBoardEulerAngles
+ sceneView.scene?.rootNode.addChildNode(cameraNode)
+
+ self.cameraNode = cameraNode
+ }
+
+ private func addFloor() {
+ let floor = SCNFloor()
+ floor.reflectivity = 0
+ floor.firstMaterial = SCNMaterial.floorMaterial
+ let floorNode = SCNNode(geometry: floor)
+ sceneView.scene?.rootNode.addChildNode(floorNode)
+ floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
+ }
+
+ private func addBoard() {
+ let boardURL = URL.boardURL
+ let boardNode = SCNReferenceNode(url: boardURL)!
+ boardNode.name = SceneSettings.boardNodeName
+ boardNode.load()
+ sceneView.scene?.rootNode.addChildNode(boardNode)
+ boardNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: boardNode.fittingBox))
+
+ self.boardNode = boardNode
+ }
+
+ private func addStartAndEnd() {
+ let startNode = SCNNode()
+ startNode.name = game.board.start.name
+ startNode.position = SceneSettings.startNodePosition
+ scene.rootNode.addChildNode(startNode)
+ self.startNode = startNode
+
+ let endNode = SCNNode()
+ endNode.name = game.board.end.name
+ endNode.position = SceneSettings.endNodePosition
+ scene.rootNode.addChildNode(endNode)
+ self.endNode = endNode
+ }
+
+ private func addSticks() {
+ let stickURL = URL.stickURL
+ stickNodes = (1...4).map { index in
+ let stickNode = SCNReferenceNode(url: stickURL)!
+ stickNode.name = SceneSettings.stickNodeName + "\(index)"
+ stickNode.position = SCNVector3(Double(index) - 1, 2.5, 0)
+ stickNode.load()
+ scene.rootNode.addChildNode(stickNode)
+ return stickNode
+ }
+
+ stickNodes.forEach { node in
+ // Custom Geometry
+ let radius: CGFloat = 0.157 / 2
+ let length: CGFloat = 1.34
+ let coordinate: CGFloat = sqrt(2) * radius / 2
+ let vertices: [SCNVector3] = [
+ // Front
+ SCNVector3(-radius, 0, length/2),
+ SCNVector3(-coordinate, coordinate, length/2),
+ SCNVector3(coordinate, coordinate, length/2),
+ SCNVector3(radius, 0, length/2),
+
+ // Back
+ SCNVector3(-radius, 0, -length/2),
+ SCNVector3(-coordinate, coordinate, -length/2),
+ SCNVector3(coordinate, coordinate, -length/2),
+ SCNVector3(radius, 0, -length/2),
+ ]
+ let indices: [UInt16] = [
+ // Front
+ 0,2,1,
+ 0,3,2,
+
+ // Back
+ 5,6,4,
+ 4,6,7,
+
+ // Bottom
+ 0,4,3,
+ 3,4,7,
+
+ // Top
+ 1,6,5,
+ 1,2,6,
+
+ // Side
+ 3,6,2,
+ 3,7,6,
+ 0,1,5,
+ 0,5,4
+ ]
+ // TODO: Move custom geometry
+
+ let source = SCNGeometrySource(vertices: vertices)
+ let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
+ let geometry = SCNGeometry(sources: [source], elements: [element])
+
+ let physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(geometry: geometry))
+ node.physicsBody = physicsBody
+ }
+ }
+
+ private func addTokens() {
+ let player1TokenNodes: [SCNNode] = game.board.player1Pieces.enumerated().map { (index, token) in
+ let url = URL.usdzURL(for: TokenType.plastic.player1ResourceName)
+ let node = SCNReferenceNode(url: url)!
+ node.name = SceneSettings.tokenNodeName + token.identifier
+ node.load()
+ let sp = startNode.position
+ let position = SCNVector3(sp.x + Float(index) * 0.25, sp.y, sp.z)
+ node.position = position
+ scene.rootNode.addChildNode(node)
+ return node
+ }
+ let player2TokenNodes: [SCNNode] = game.board.player2Pieces.enumerated().map { (index, token) in
+ let url = URL.usdzURL(for: TokenType.plastic.player2ResourceName)
+ let node = SCNReferenceNode(url: url)!
+ node.name = SceneSettings.tokenNodeName + token.identifier
+ node.load()
+ let sp = startNode.position
+ let position = SCNVector3(sp.x + Float(index) * 0.25, sp.y, sp.z + 0.25)
+ node.position = position
+ scene.rootNode.addChildNode(node)
+ return node
+ }
+ tokenNodes = player1TokenNodes + player2TokenNodes
+ }
+
+ // MARK: - User interaction
+ @IBAction
+ private func tap(roll button: UIButton) {
+ rollSticks()
+ }
+
+ @objc
+ func handleTap(_ gestureRecognize: UIGestureRecognizer) {
+ let location = gestureRecognize.location(in: sceneView)
+ let hits = sceneView.hitTest(location, options: nil)
+ guard let tappedNode = hits.first?.node else {
+ return
+ }
+ // Token
+ if tappedNode.name?.contains("Piece") == true {
+ let selectedNode = tappedNode.parent!.parent!.parent
+ let tokenIdentifier = selectedNode!.name!.replacingOccurrences(of: SceneSettings.tokenNodeName, with: "")
+ guard let token = game.board.token(withIdentifier: tokenIdentifier) else {
+ return
+ }
+ helpNodes.forEach {
+ $0.removeFromParentNode()
+ }
+ helpNodes.removeAll()
+ game.selectToken(token)
+ }
+ // Station
+ if tappedNode.name?.contains(SceneSettings.helpStationNodeName) == true {
+ let stationName = tappedNode.name!.replacingOccurrences(of: SceneSettings.helpStationNodeName, with: "")
+ let station = game.board.position(withName: stationName)
+ if let station = station {
+ game.moveCurrentPlayerActiveToken(to: station)
+ }
+ helpNodes.forEach {
+ $0.removeFromParentNode()
+ }
+ helpNodes.removeAll()
+ throwButton.isEnabled = true
+ }
+ }
+
+ // MARK: - Motion
+ public override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
+ guard motion == .motionShake else {
+ return
+ }
+ rollSticks()
+ }
+
+ // MARK: - Action
+ private func startGame() {
+ game.startGame()
+ }
+
+ private func rollSticks() {
+ waitingForRollResult = true
+ stickNodes.forEach { node in
+ let upward = SceneSettings.stickThrowForce
+ let at = SceneSettings.stickThrowAt
+ node.physicsBody?.applyForce(upward, at: at, asImpulse: true)
+ // node.physicsBody?.applyTorque(SCNVector4(0, 0, 0, 0.5), asImpulse: true)
+ }
+ }
+
+ private func checkRollResult() {
+ if waitingForRollResult == false {
+ return
+ }
+ let isResting = (1...4).compactMap { index in
+ return sceneView.scene?.rootNode.childNode(withName: "stick_\(index)", recursively: true)?.physicsBody?.isResting
+ }
+ guard isResting.contains(false) == false else {
+ return
+ }
+ let results: [Stick.RollResult] = (1...4).compactMap { index in
+ let stickNodes = sceneView.scene?.rootNode.childNode(withName: "stick_\(index)", recursively: true)
+ if stickNodes!.presentation.worldUp.y > 0 {
+ // Back (Ridge)
+ return Stick.RollResult.back
+ } else {
+ // Front (Flat)
+ return Stick.RollResult.front
+ }
+ }
+ let result = StickSet.getSetResult(from: results)
+ game.receive(rollResult: result)
+ DispatchQueue.main.async {
+ self.waitingForRollResult = false
+ }
+ }
+}
+
+
+// MARK: - Game
+extension SceneKitGameViewController: GameDelegate {
+ func started(game: Game) {
+ os_log(.debug, log: .app, "Game started.")
+ descriptionLabel.text = "Game started."
+ }
+
+ func game(_: Game, switchedTurnTo player: Player) {
+ os_log(.debug, log: .app, "Switched to player %{PUBLIC}@.", player.name)
+ turnLabel.text = "Player \(player.name)'s"
+ descriptionLabel.text = "Player \(player.name)'s turn!"
+ }
+
+ func game(_: Game, waitingRollFrom player: Player) {
+ os_log(.debug, log: .app, "Waiting roll from player %{PUBLIC}@.", player.name)
+ if game.pieceMover.remainingSteps.count > 1 {
+ descriptionLabel.text = "Extra roll chance! Wating for player \(player.name) to roll!"
+ } else {
+ descriptionLabel.text = "Wating for player \(player.name) to roll!"
+ }
+ }
+
+ func game(_: Game, player: Player, canMove steps: [Int], to positions: [BoardPosition]) {
+ os_log(.debug, log: .app, "Player %{PUBLIC}@ can go forward %{PUBLIC}@.", player.name, steps.debugDescription)
+ os_log(.debug, log: .app, "Player %{PUBLIC}@ can move to %{PUBLIC}@.", player.name, positions.debugDescription)
+
+ if steps.count > 0 {
+ var stepString = steps.reduce("") { (string, step) -> String in
+ return string + "\(step), "
+ }
+ stepString.removeLast()
+ stepString.removeLast()
+ descriptionLabel.text = "Player \(player.name) can go forward \(stepString) steps."
+ }
+
+ positions.forEach { position in
+ guard let node = scene.rootNode.childNode(withName: position.name, recursively: true) else {
+ return
+ }
+ let box = SCNSphere(radius: 0.1)
+ box.firstMaterial = SCNMaterial()
+ box.firstMaterial?.emission.contents = UIColor.red
+ box.firstMaterial?.transparency = 0.5
+ let boxNode = SCNNode(geometry: box)
+ boxNode.name = SceneSettings.helpStationNodeName + position.name
+ node.addChildNode(boxNode)
+
+ helpNodes.append(boxNode)
+ }
+ }
+
+ func game(_: Game, player: Player, pieces: [Piece], movedTo position: BoardPosition) {
+ os_log(.debug, log: .app, "Player %{PUBLIC}@ moved to to %{PUBLIC}@.", player.name, position.debugDescription)
+ let tokens = tokenNodes.filter { pieces.map { $0.identifier }.contains($0.name?.replacingOccurrences(of: SceneSettings.tokenNodeName, with: "")) }
+ if let station = scene.rootNode.childNode(withName: position.name, recursively: true)?.position {
+ tokens.forEach {
+ $0.position = station
+ }
+ }
+ }
+
+ func game(_: Game, eatenPieces: [Piece]) {
+ os_log(.debug, log: .app, "%{PUBLIC} pieces are eaten and are returned to home.", eatenPieces.debugDescription)
+ let pieces = eatenPieces.map { ($0.name, $0.player) }
+ if let player = eatenPieces.first?.player {
+ descriptionLabel.text = "\(pieces.count) pieces of player \(player.name) are caught. Extra roll chance!"
+ } else {
+ descriptionLabel.text = "\(pieces.count) pieces are caught. Extra roll chance!"
+ }
+ }
+
+ func game(_: Game, wonBy player: Player) {
+ os_log(.debug, log: .app, "Game ended with winner player %{PUBLIC}@.", player.name)
+ descriptionLabel.text = "Player \(player.name) won!"
+ }
+}
+
+// MARK - Scene
+extension SceneKitGameViewController: SCNSceneRendererDelegate {
+ public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
+ checkRollResult()
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/SceneViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/SceneViewController.swift
new file mode 100644
index 0000000..7e889c9
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/SceneViewController.swift
@@ -0,0 +1,71 @@
+//
+// SceneViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+import SceneKit
+
+public class SceneViewController: UIViewController {
+ @IBOutlet var sceneView: SCNView!
+ var scene: SCNScene {
+ return sceneView.scene!
+ }
+
+ var cameraNode: SCNNode!
+
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ setup()
+ }
+
+ // MARK: - Setup
+ private func setup() {
+ // MARK: Scene
+ let scene = SCNScene(named: "Game.scnassets/Empty.scn")!
+ sceneView.scene = scene
+ SCNTransaction.animationDuration = SceneSettings.animationDuration
+ }
+
+ func addLight() {
+ let ambientLight = SCNLight()
+ ambientLight.type = .ambient
+ ambientLight.color = UIColor.darkGray
+ let ambientLightNode = SCNNode()
+ ambientLightNode.light = ambientLight
+ sceneView.scene?.rootNode.addChildNode(ambientLightNode)
+
+ let pointLight = SCNLight()
+ pointLight.type = .omni
+ let pointLightNode = SCNNode()
+ pointLightNode.light = pointLight
+ pointLightNode.position = SceneSettings.pointLightNodePosition
+ sceneView.scene?.rootNode.addChildNode(pointLightNode)
+ }
+
+ func addCamera(position: SCNVector3 = SceneSettings.cameraNodePosition, eulerAngles: SCNVector3 = SceneSettings.cameraNodeEulerAngles) {
+ let camera = SCNCamera()
+ camera.zNear = SceneSettings.cameraZNear
+ camera.zFar = SceneSettings.cameraZFar
+ let cameraNode = SCNNode()
+ cameraNode.camera = camera
+ cameraNode.position = position
+ cameraNode.eulerAngles = eulerAngles
+ sceneView.scene?.rootNode.addChildNode(cameraNode)
+
+ self.cameraNode = cameraNode
+ }
+
+ func addFloor() {
+ let floor = SCNFloor()
+ floor.reflectivity = 0
+ floor.firstMaterial = SCNMaterial.floorMaterial
+ let floorNode = SCNNode(geometry: floor)
+ sceneView.scene?.rootNode.addChildNode(floorNode)
+ floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/StickViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/StickViewController.swift
new file mode 100644
index 0000000..aa327f2
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/StickViewController.swift
@@ -0,0 +1,192 @@
+//
+// StickViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+import SceneKit
+#if canImport(PlaygroundSupport)
+import PlaygroundSupport
+#endif
+
+public class StickViewController: SceneViewController {
+ @IBOutlet public var controlView: UIView!
+ @IBOutlet public var resultLabel: UILabel!
+ @IBOutlet public var descriptionLabel: UILabel!
+ @IBOutlet public var throwButton: UIButton!
+
+ private var waitingForRollResult = false {
+ didSet {
+ if waitingForRollResult {
+ throwButton.isEnabled = false
+ } else {
+ throwButton.isEnabled = true
+ }
+ }
+ }
+
+ private var stickNodes = [SCNNode]()
+
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ controlView.layer.cornerRadius = 9
+ sceneView.scene?.physicsWorld.gravity = SCNVector3(0, -0.98, 0)
+ sceneView.delegate = self
+ addLight()
+ addCamera(position: SceneSettings.cameraNodeBoardPosition, eulerAngles: SceneSettings.cameraNodeBoardEulerAngles)
+ addFloor()
+ addSticks()
+ }
+
+ // MARK: - Setup
+ private func addSticks() {
+ let stickURL = URL.stickURL
+ stickNodes = (1...4).map { index in
+ let stickNode = SCNReferenceNode(url: stickURL)!
+ stickNode.name = SceneSettings.stickNodeName + "\(index)"
+
+ let randomX = Float.random(in: -0.5...0.5)
+ let randomY = Float.random(in: 4...8)
+ let randomZ = Float.random(in: -0.5...0.5)
+
+ stickNode.position = SCNVector3(randomX, randomY, randomZ)
+ stickNode.load()
+ stickNode.physicsBody?.mass = 1
+ scene.rootNode.addChildNode(stickNode)
+ return stickNode
+ }
+
+ stickNodes.forEach { node in
+ // Custom Geometry
+ let radius: CGFloat = 0.157 / 2
+ let length: CGFloat = 1.34
+ let coordinate: CGFloat = sqrt(2) * radius / 2
+ let vertices: [SCNVector3] = [
+ // Front
+ SCNVector3(-radius, 0, length/2),
+ SCNVector3(-coordinate, coordinate, length/2),
+ SCNVector3(coordinate, coordinate, length/2),
+ SCNVector3(radius, 0, length/2),
+
+ // Back
+ SCNVector3(-radius, 0, -length/2),
+ SCNVector3(-coordinate, coordinate, -length/2),
+ SCNVector3(coordinate, coordinate, -length/2),
+ SCNVector3(radius, 0, -length/2),
+ ]
+ let indices: [UInt16] = [
+ // Front
+ 0,2,1,
+ 0,3,2,
+
+ // Back
+ 5,6,4,
+ 4,6,7,
+
+ // Bottom
+ 0,4,3,
+ 3,4,7,
+
+ // Top
+ 1,6,5,
+ 1,2,6,
+
+ // Side
+ 3,6,2,
+ 3,7,6,
+ 0,1,5,
+ 0,5,4
+ ]
+ // TODO: Move custom geometry
+
+ let source = SCNGeometrySource(vertices: vertices)
+ let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
+ let geometry = SCNGeometry(sources: [source], elements: [element])
+
+ let physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(geometry: geometry))
+ node.physicsBody = physicsBody
+ }
+ }
+
+ // MARK: - User interaction
+ @IBAction
+ private func tap(throw button: UIButton) {
+ throwSticks()
+ }
+
+ // MARK: - Motion
+ public override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
+ guard motion == .motionShake else {
+ return
+ }
+ throwSticks()
+ }
+
+ // MARK: - Action
+ private func throwSticks() {
+ waitingForRollResult = true
+ stickNodes.forEach { node in
+ let upward = SCNVector3(0, 1.25, 0)
+ let at = SceneSettings.stickThrowAt
+ node.physicsBody?.applyForce(upward, at: at, asImpulse: true)
+ }
+ }
+
+ private func checkRollResult() {
+ if waitingForRollResult == false {
+ return
+ }
+ let isResting = (1...4).compactMap { index in
+ return sceneView.scene?.rootNode.childNode(withName: "stick_\(index)", recursively: true)?.physicsBody?.isResting
+ }
+ guard isResting.contains(false) == false else {
+ return
+ }
+ let results: [Stick.RollResult] = (1...4).compactMap { index in
+ let stickNodes = sceneView.scene?.rootNode.childNode(withName: "stick_\(index)", recursively: true)
+ if stickNodes!.presentation.worldUp.y > 0 {
+ // Back (Ridge)
+ return Stick.RollResult.back
+ } else {
+ // Front (Flat)
+ return Stick.RollResult.front
+ }
+ }
+ let result = StickSet.getSetResult(from: results)
+ switch result {
+ case .do:
+ resultLabel.text = "\(result.name)! 🐖"
+ descriptionLabel.text = "You can advance \(result.steps) steps."
+ case .gae:
+ resultLabel.text = "\(result.name)! 🐕"
+ descriptionLabel.text = "You can advance \(result.steps) steps."
+ case .gir:
+ resultLabel.text = "\(result.name)! 🐑"
+ descriptionLabel.text = "You can advance \(result.steps) steps."
+ case .yut:
+ resultLabel.text = "\(result.name)! 🐂"
+ descriptionLabel.text = "You can advance \(result.steps) steps."
+ case .mo:
+ resultLabel.text = "\(result.name)! 🐎"
+ descriptionLabel.text = "You can advance \(result.steps) steps."
+ default:
+ resultLabel.text = "--"
+ descriptionLabel.text = "--"
+ }
+ waitingForRollResult = false
+ }
+}
+
+// MARK - Scene
+extension StickViewController: SCNSceneRendererDelegate {
+ public func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
+ DispatchQueue.main.async {
+ self.checkRollResult()
+ }
+ }
+}
+
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/TokenViewController.swift b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/TokenViewController.swift
new file mode 100644
index 0000000..7ff7bcc
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/Modules/BookCore.playgroundmodule/Sources/View Controller/TokenViewController.swift
@@ -0,0 +1,59 @@
+//
+// TokenViewController.swift
+// WWDC 2020
+//
+// Created by BumMo Koo on 2020/05/18.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
+
+import UIKit
+import SceneKit
+#if canImport(PlaygroundSupport)
+import PlaygroundSupport
+#endif
+
+public class TokenViewController: SceneViewController {
+ private lazy var game = Game()
+ private var tokenNodes = [SCNNode]()
+
+ // MARK: - View life cycle
+ public override func viewDidLoad() {
+ super.viewDidLoad()
+ sceneView.scene?.physicsWorld.gravity = SCNVector3(0, -0.098, 0)
+ addLight()
+ addCamera()
+ addFloor()
+ addTokens()
+ }
+
+ // MARK: - Setup
+ private func addTokens() {
+ var newNodes: [SCNNode]
+ newNodes = (1...20).map { _ in
+ let url = URL.usdzURL(for: TokenType.plastic.player1ResourceName)
+ let node = SCNReferenceNode(url: url)!
+ node.load()
+ return node
+ }
+ tokenNodes.append(contentsOf: newNodes)
+
+ newNodes = (1...20).map { _ in
+ let url = URL.usdzURL(for: TokenType.plastic.player2ResourceName)
+ let node = SCNReferenceNode(url: url)!
+ node.load()
+ return node
+ }
+ tokenNodes.append(contentsOf: newNodes)
+
+ tokenNodes.forEach { node in
+ let randomX = Float.random(in: -0.5...0.5)
+ let randomY = Float.random(in: 4...8)
+ let randomZ = Float.random(in: -0.5...0.5)
+
+ node.position = SCNVector3(randomX, randomY, randomZ)
+ node.scale = SCNVector3(4, 4, 4)
+ node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: SCNPhysicsShape(geometry: node.presentation.fittingBox(scaled: 4), options: nil))
+ scene.rootNode.addChildNode(node)
+ }
+ }
+}
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Board.usdz b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Board.usdz
new file mode 100644
index 0000000..28ec7f9
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Board.usdz differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Cover.png b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Cover.png
new file mode 100644
index 0000000..36cdade
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Cover.png differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Game.scnassets/Empty.scn b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Game.scnassets/Empty.scn
new file mode 100644
index 0000000..fd41863
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Game.scnassets/Empty.scn differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/ARKitGameViewController.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/ARKitGameViewController.nib
new file mode 100644
index 0000000..92d0232
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/ARKitGameViewController.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/BoardViewController.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/BoardViewController.nib
new file mode 100644
index 0000000..55d3b34
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/BoardViewController.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/EpilogueViewController.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/EpilogueViewController.nib
new file mode 100644
index 0000000..1303165
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/EpilogueViewController.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/GameIntroViewController.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/GameIntroViewController.nib
new file mode 100644
index 0000000..c33c359
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/GameIntroViewController.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/Info.plist b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/Info.plist
new file mode 100644
index 0000000..edb53d1
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/Info.plist differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/IntroductionViewController.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/IntroductionViewController.nib
new file mode 100644
index 0000000..108ae03
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/IntroductionViewController.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/KuM-xy-BHJ-view-oKB-z1-IYl.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/KuM-xy-BHJ-view-oKB-z1-IYl.nib
new file mode 100644
index 0000000..3022727
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/KuM-xy-BHJ-view-oKB-z1-IYl.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/Mo3-1S-cxS-view-ujN-Ti-x7k.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/Mo3-1S-cxS-view-ujN-Ti-x7k.nib
new file mode 100644
index 0000000..bcb286f
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/Mo3-1S-cxS-view-ujN-Ti-x7k.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/SceneKitGameViewController.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/SceneKitGameViewController.nib
new file mode 100644
index 0000000..38b4254
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/SceneKitGameViewController.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/StickViewController.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/StickViewController.nib
new file mode 100644
index 0000000..3fa413d
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/StickViewController.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/TokenViewController.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/TokenViewController.nib
new file mode 100644
index 0000000..901e6c5
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/TokenViewController.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/UwO-uz-mHV-view-UAp-qJ-Qd6.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/UwO-uz-mHV-view-UAp-qJ-Qd6.nib
new file mode 100644
index 0000000..decb208
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/UwO-uz-mHV-view-UAp-qJ-Qd6.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/WJv-TO-dph-view-YMB-jh-HcC.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/WJv-TO-dph-view-YMB-jh-HcC.nib
new file mode 100644
index 0000000..f5b8197
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/WJv-TO-dph-view-YMB-jh-HcC.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/WSq-f1-eAe-view-RF4-Uz-Hgc.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/WSq-f1-eAe-view-RF4-Uz-Hgc.nib
new file mode 100644
index 0000000..b7fc8b7
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/WSq-f1-eAe-view-RF4-Uz-Hgc.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/aGj-7V-pl1-view-EdD-Ib-hy9.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/aGj-7V-pl1-view-EdD-Ib-hy9.nib
new file mode 100644
index 0000000..59fd3c0
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/aGj-7V-pl1-view-EdD-Ib-hy9.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/eoC-tm-8WS-view-ObN-sA-OeL.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/eoC-tm-8WS-view-ObN-sA-OeL.nib
new file mode 100644
index 0000000..22b53e0
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/eoC-tm-8WS-view-ObN-sA-OeL.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/jIs-Oe-WxG-view-Imh-BA-enC.nib b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/jIs-Oe-WxG-view-Imh-BA-enC.nib
new file mode 100644
index 0000000..9312fcf
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Main.storyboardc/jIs-Oe-WxG-view-Imh-BA-enC.nib differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Piece.usdz b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Piece.usdz
new file mode 100644
index 0000000..d756dea
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Piece.usdz differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Stick.usdz b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Stick.usdz
new file mode 100644
index 0000000..164b2a5
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Stick.usdz differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Token_Plastic_blue.usdz b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Token_Plastic_blue.usdz
new file mode 100644
index 0000000..0032e3f
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Token_Plastic_blue.usdz differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Token_Plastic_red.usdz b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Token_Plastic_red.usdz
new file mode 100644
index 0000000..1558c7e
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/Token_Plastic_red.usdz differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/en.lproj/ManifestPlist.strings b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/en.lproj/ManifestPlist.strings
new file mode 100644
index 0000000..341adc5
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/en.lproj/ManifestPlist.strings differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut.jpg b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut.jpg
new file mode 100644
index 0000000..6f6cbd4
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut.jpg differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_board_round.png b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_board_round.png
new file mode 100644
index 0000000..fa1eaa1
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_board_round.png differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_combinations.jpg b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_combinations.jpg
new file mode 100644
index 0000000..7c50220
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_combinations.jpg differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_play.jpg b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_play.jpg
new file mode 100644
index 0000000..34960a7
Binary files /dev/null and b/Yut v2.1 Submitted.playgroundbook/Contents/PrivateResources/yut_play.jpg differ
diff --git a/Yut v2.1 Submitted.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/SharedCode.swift b/Yut v2.1 Submitted.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/SharedCode.swift
new file mode 100644
index 0000000..17edb62
--- /dev/null
+++ b/Yut v2.1 Submitted.playgroundbook/Contents/UserModules/UserModule.playgroundmodule/Sources/SharedCode.swift
@@ -0,0 +1,4 @@
+//
+// Created by BumMo Koo on May 2020.
+// Copyright © 2020 BumMo Koo. All rights reserved.
+//
diff --git a/images/image_1.png b/images/image_1.png
new file mode 100644
index 0000000..285d512
Binary files /dev/null and b/images/image_1.png differ
diff --git a/images/image_2.png b/images/image_2.png
new file mode 100644
index 0000000..2ec59f2
Binary files /dev/null and b/images/image_2.png differ