Initial commit
This commit is contained in:
commit
f844d5466f
|
@ -0,0 +1,78 @@
|
|||
.DS_Store
|
||||
MyPlayground.playground/
|
||||
|
||||
### Swift ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData/
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
## Playgrounds
|
||||
timeline.xctimeline
|
||||
playground.xcworkspace
|
||||
|
||||
# Swift Package Manager
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
|
||||
# Packages/
|
||||
# Package.pins
|
||||
.build/
|
||||
|
||||
# CocoaPods - Refactored to standalone file
|
||||
|
||||
# Carthage - Refactored to standalone file
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/#source-control
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
|
||||
## Various settings
|
||||
|
||||
## Other
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "24x24",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "notificationCenter",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "27.5x27.5",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "notificationCenter",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "watch",
|
||||
"role" : "companionSettings",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "appLauncher",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "44x44",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "appLauncher",
|
||||
"subtype" : "40mm"
|
||||
},
|
||||
{
|
||||
"size" : "50x50",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "appLauncher",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"size" : "86x86",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "quickLook",
|
||||
"subtype" : "38mm"
|
||||
},
|
||||
{
|
||||
"size" : "98x98",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "quickLook",
|
||||
"subtype" : "42mm"
|
||||
},
|
||||
{
|
||||
"size" : "108x108",
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"role" : "quickLook",
|
||||
"subtype" : "44mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder.WatchKit.Storyboard" version="3.0" toolsVersion="11134" targetRuntime="watchKit" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="3mp-fW-waa">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBWatchKitPlugin" version="11055"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Interface Controller-->
|
||||
<scene sceneID="aou-V4-d1y">
|
||||
<objects>
|
||||
<hostingController id="3mp-fW-waa" customClass="HostingController"
|
||||
customModuleProvider="target"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Tetris WatchKit App</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>net.shadowfacts.Tetris</string>
|
||||
<key>WKWatchKitApp</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"assets" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"filename" : "Circular.imageset",
|
||||
"role" : "circular"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"filename" : "Extra Large.imageset",
|
||||
"role" : "extra-large"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"filename" : "Graphic Bezel.imageset",
|
||||
"role" : "graphic-bezel"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"filename" : "Graphic Circular.imageset",
|
||||
"role" : "graphic-circular"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"filename" : "Graphic Corner.imageset",
|
||||
"role" : "graphic-corner"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"filename" : "Graphic Large Rectangular.imageset",
|
||||
"role" : "graphic-large-rectangular"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"filename" : "Modular.imageset",
|
||||
"role" : "modular"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"filename" : "Utilitarian.imageset",
|
||||
"role" : "utilitarian"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : "<=145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">161"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">145"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"scale" : "2x",
|
||||
"screen-width" : ">183"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// Tetris WatchKit Extension
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Hello, World!")
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// ExtensionDelegate.swift
|
||||
// Tetris WatchKit Extension
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import WatchKit
|
||||
|
||||
class ExtensionDelegate: NSObject, WKExtensionDelegate {
|
||||
|
||||
func applicationDidFinishLaunching() {
|
||||
// Perform any final initialization of your application.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive() {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillResignActive() {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, etc.
|
||||
}
|
||||
|
||||
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
|
||||
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
|
||||
for task in backgroundTasks {
|
||||
// Use a switch statement to check the task type
|
||||
switch task {
|
||||
case let backgroundTask as WKApplicationRefreshBackgroundTask:
|
||||
// Be sure to complete the background task once you’re done.
|
||||
backgroundTask.setTaskCompletedWithSnapshot(false)
|
||||
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
|
||||
// Snapshot tasks have a unique completion call, make sure to set your expiration date
|
||||
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
|
||||
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
|
||||
// Be sure to complete the connectivity task once you’re done.
|
||||
connectivityTask.setTaskCompletedWithSnapshot(false)
|
||||
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
|
||||
// Be sure to complete the URL session task once you’re done.
|
||||
urlSessionTask.setTaskCompletedWithSnapshot(false)
|
||||
case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask:
|
||||
// Be sure to complete the relevant-shortcut task once you're done.
|
||||
relevantShortcutTask.setTaskCompletedWithSnapshot(false)
|
||||
case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask:
|
||||
// Be sure to complete the intent-did-run task once you're done.
|
||||
intentDidRunTask.setTaskCompletedWithSnapshot(false)
|
||||
default:
|
||||
// make sure to complete unhandled task types
|
||||
task.setTaskCompletedWithSnapshot(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// HostingController.swift
|
||||
// Tetris WatchKit Extension
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import WatchKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class HostingController: WKHostingController<ContentView> {
|
||||
override var body: ContentView {
|
||||
return ContentView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Tetris WatchKit Extension</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>WKAppBundleIdentifier</key>
|
||||
<string>net.shadowfacts.Tetris.test.watchkitapp</string>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.watchkit</string>
|
||||
</dict>
|
||||
<key>WKExtensionDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
|
||||
<key>WKRunsIndependentlyOfCompanionApp</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// AppDelegate.swift
|
||||
// Tetris
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "83.5x83.5",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -0,0 +1,129 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// Tetris
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import TetrisKit
|
||||
import TetrisUI
|
||||
|
||||
struct ContentView: View {
|
||||
@ObservedObject var controller: GameController = {
|
||||
let c = GameController()
|
||||
c.start()
|
||||
return c
|
||||
}()
|
||||
@State var timer: Timer?
|
||||
@State var prevHorizTranslation: CGFloat?
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { (geometry) in
|
||||
VStack {
|
||||
BoardView(board: self.$controller.board, currentPiece: self.$controller.currentPiece, droppedPiece: self.$controller.currentPieceAtDropPoint)
|
||||
.aspectRatio(CGSize(width: 10, height: 16), contentMode: .fit)
|
||||
.onAppear(perform: self.startTimer)
|
||||
.onDisappear(perform: self.stopTimer)
|
||||
.onTapGesture(perform: self.onTap)
|
||||
.gesture(self.horizDragGesture(geometry: geometry))
|
||||
// .gesture(ExclusiveGesture(horizDragGesture, verticalDragGesture))
|
||||
// .gesture(horizDragGesture.simultaneously(with: verticalDragGesture))
|
||||
|
||||
HStack {
|
||||
Button(action: self.onTap) {
|
||||
Image(systemName: "goforward").resizable().frame(width: 50, height: 50)
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Button(action: self.controller.left) {
|
||||
Image(systemName: "arrow.left.square.fill").resizable().frame(width: 50, height: 50)
|
||||
}
|
||||
Spacer()
|
||||
Button(action: self.controller.drop) {
|
||||
Image(systemName: "arrow.down.square.fill").resizable().frame(width: 50, height: 50)
|
||||
}
|
||||
Spacer()
|
||||
Button(action: self.controller.right) {
|
||||
Image(systemName: "arrow.right.square.fill").resizable().frame(width: 50, height: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func horizDragGesture(geometry: GeometryProxy) -> some Gesture {
|
||||
DragGesture(coordinateSpace: .global)
|
||||
.onChanged { (state) in
|
||||
guard let currentPiece = self.controller.currentPiece else { return }
|
||||
// let position = (state.location.x) / geometry.size.width * 10
|
||||
// let moved = currentPiece.moved(by: (Int(position.rounded()) - currentPiece.topLeft.0, 0))
|
||||
// if !self.controller.overlapsAny(moved) {
|
||||
// self.controller.currentPiece = moved
|
||||
// }
|
||||
if self.prevHorizTranslation == nil {
|
||||
self.prevHorizTranslation = state.translation.width
|
||||
}
|
||||
let delta = state.translation.width - self.prevHorizTranslation!
|
||||
let amount = Int((delta / 20).rounded())
|
||||
let moved = currentPiece.moved(by: (amount, 0))
|
||||
if !self.controller.overlapsAny(moved) {
|
||||
self.controller.currentPiece = moved
|
||||
self.prevHorizTranslation = state.translation.width
|
||||
}
|
||||
}.onEnded { (state) in
|
||||
self.prevHorizTranslation = nil
|
||||
}
|
||||
}
|
||||
|
||||
var verticalDragGesture: some Gesture {
|
||||
DragGesture()
|
||||
.onEnded { (state) in
|
||||
let deltaY = state.location.y - state.startLocation.y
|
||||
if abs(deltaY) > 40 {
|
||||
if deltaY > 0 {
|
||||
self.onSwipeDown()
|
||||
} else {
|
||||
self.onSwipeUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startTimer() {
|
||||
self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { (_) in
|
||||
self.controller.step()
|
||||
}
|
||||
}
|
||||
|
||||
func stopTimer() {
|
||||
timer?.invalidate()
|
||||
}
|
||||
|
||||
func onTap() {
|
||||
self.controller.rotate(direction: .clockwise)
|
||||
}
|
||||
|
||||
func onSwipeLeft() {
|
||||
self.controller.left()
|
||||
}
|
||||
|
||||
func onSwipeRight() {
|
||||
self.controller.right()
|
||||
}
|
||||
|
||||
func onSwipeUp() {
|
||||
// hold
|
||||
}
|
||||
|
||||
func onSwipeDown() {
|
||||
self.controller.drop()
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// SceneDelegate.swift
|
||||
// Tetris
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
|
||||
// Create the SwiftUI view that provides the window contents.
|
||||
let contentView = ContentView()
|
||||
|
||||
// Use a UIHostingController as window root view controller.
|
||||
if let windowScene = scene as? UIWindowScene {
|
||||
let window = UIWindow(windowScene: windowScene)
|
||||
window.rootViewController = UIHostingController(rootView: contentView)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
}
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
// Called as the scene is being released by the system.
|
||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
// Called when the scene has moved from an inactive state to an active state.
|
||||
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
// Called when the scene will move from an active state to an inactive state.
|
||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the background to the foreground.
|
||||
// Use this method to undo the changes made on entering the background.
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
// Array+Rotation.swift
|
||||
// TetrisKit
|
||||
//
|
||||
// Created by Shadowfacts on 10/13/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Array where Element: RandomAccessCollection, Element: MutableCollection, Element.Index == Int {
|
||||
mutating func rotateClockwise() {
|
||||
guard allSatisfy({ $0.count == self.count }) else { return }
|
||||
|
||||
let layerCount = self.count / 2
|
||||
|
||||
for layer in 0..<layerCount {
|
||||
let first = layer
|
||||
let last = self.count - first - 1
|
||||
|
||||
for element in first..<last {
|
||||
let offset = element - first
|
||||
|
||||
let top = self[first][element]
|
||||
let rightSide = self[element][last]
|
||||
let bottom = self[last][last - offset]
|
||||
let leftSide = self[last - offset][first]
|
||||
|
||||
self[first][element] = leftSide
|
||||
self[element][last] = top
|
||||
self[last][last - offset] = rightSide
|
||||
self[last - offset][first] = bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func rotateCounterclockwise() {
|
||||
guard allSatisfy({ $0.count == self.count }) else { return }
|
||||
|
||||
let layerCount = self.count / 2
|
||||
|
||||
for layer in 0..<layerCount {
|
||||
let first = layer
|
||||
let last = self.count - first - 1
|
||||
|
||||
for element in first..<last {
|
||||
let offset = element - first
|
||||
|
||||
let top = self[first][element]
|
||||
let rightSide = self[element][last]
|
||||
let bottom = self[last][last - offset]
|
||||
let leftSide = self[last - offset][first]
|
||||
|
||||
self[first][element] = rightSide
|
||||
self[element][last] = bottom
|
||||
self[last][last - offset] = leftSide
|
||||
self[last - offset][first] = top
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// GameBoard.swift
|
||||
// TetrisKit
|
||||
//
|
||||
// Created by Shadowfacts on 10/13/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct GameBoard {
|
||||
|
||||
public let width: Int
|
||||
public let height: Int
|
||||
public internal(set) var tiles: [[Bool]]
|
||||
|
||||
public init(width: Int, height: Int) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
tiles = (1...height).map { _ in
|
||||
Array(repeating: false, count: width)
|
||||
}
|
||||
}
|
||||
|
||||
public subscript(x: Int, y: Int) -> Bool {
|
||||
return self.tiles[y][x]
|
||||
}
|
||||
|
||||
mutating func set(piece: GamePiece) {
|
||||
let (left, top) = piece.topLeft
|
||||
|
||||
for y in 0..<piece.tiles.count where y + top < height {
|
||||
for x in 0..<piece.tiles.first!.count where x + left < width {
|
||||
if piece.tiles[y][x] {
|
||||
self.tiles[y + top][x + left] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public mutating func set(tiles: [(Int, Int)]) {
|
||||
for (x, y) in tiles {
|
||||
self.tiles[y][x] = true
|
||||
}
|
||||
}
|
||||
|
||||
mutating func unset(tiles: [(Int, Int)]) {
|
||||
for (x, y) in tiles {
|
||||
self.tiles[y][x] = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// GameController.swift
|
||||
// TetrisKit
|
||||
//
|
||||
// Created by Shadowfacts on 10/13/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public class GameController: ObservableObject {
|
||||
|
||||
public let width = 10
|
||||
public let height = 16
|
||||
|
||||
@Published public var board: GameBoard
|
||||
|
||||
@Published public var currentPiece: GamePiece? {
|
||||
didSet {
|
||||
updateCurrentPieceAtDropPoint()
|
||||
}
|
||||
}
|
||||
@Published public var currentPieceAtDropPoint: GamePiece?
|
||||
|
||||
public var ended: Bool {
|
||||
return (0..<width).first(where: { board[$0, 0] }) != nil
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.board = GameBoard(width: width, height: height)
|
||||
self.currentPiece = nil
|
||||
}
|
||||
|
||||
public func start() {
|
||||
nextPiece()
|
||||
}
|
||||
|
||||
func nextPiece() {
|
||||
currentPiece = GamePiece(tetromino: .random())
|
||||
currentPiece!.topLeft = ((width - currentPiece!.tiles.count) / 2, 0)
|
||||
}
|
||||
|
||||
func finalizePiece() {
|
||||
self.board.set(piece: currentPiece!)
|
||||
clearLines()
|
||||
nextPiece()
|
||||
}
|
||||
|
||||
func clearLines() {
|
||||
let fullRow = Array(repeating: true, count: width)
|
||||
var row = height - 1
|
||||
while row >= 0 {
|
||||
if board.tiles[row] == fullRow {
|
||||
board.tiles.remove(at: row)
|
||||
}
|
||||
row -= 1
|
||||
}
|
||||
for _ in 0..<height - board.tiles.count {
|
||||
board.tiles.insert(Array(repeating: false, count: width), at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
public func step() {
|
||||
guard let currentPiece = currentPiece else { return }
|
||||
let modifiedPiece = currentPiece.moved(by: (0, 1))
|
||||
if overlapsAny(modifiedPiece) {
|
||||
finalizePiece()
|
||||
} else {
|
||||
self.currentPiece = modifiedPiece
|
||||
}
|
||||
}
|
||||
|
||||
public func rotate(direction: RotationDirection) {
|
||||
guard let currentPiece = currentPiece else { return }
|
||||
let modifiedPiece = currentPiece.rotated(direction)
|
||||
if !overlapsAny(modifiedPiece) {
|
||||
self.currentPiece = modifiedPiece
|
||||
}
|
||||
}
|
||||
|
||||
public func left() {
|
||||
guard let currentPiece = currentPiece else { return }
|
||||
let modifiedPiece = currentPiece.moved(by: (-1, 0))
|
||||
if !overlapsAny(modifiedPiece) {
|
||||
self.currentPiece = modifiedPiece
|
||||
}
|
||||
}
|
||||
|
||||
public func right() {
|
||||
guard let currentPiece = currentPiece else { return }
|
||||
let modifiedPiece = currentPiece.moved(by: (1, 0))
|
||||
if !overlapsAny(modifiedPiece) {
|
||||
self.currentPiece = modifiedPiece
|
||||
}
|
||||
}
|
||||
|
||||
public func drop() {
|
||||
currentPiece = currentPieceAtDropPoint
|
||||
}
|
||||
|
||||
func updateCurrentPieceAtDropPoint() {
|
||||
guard let currentPiece = currentPiece else { return }
|
||||
var prev = currentPiece
|
||||
currentPieceAtDropPoint = currentPiece
|
||||
while !overlapsAny(currentPieceAtDropPoint!) {
|
||||
prev = currentPieceAtDropPoint!
|
||||
currentPieceAtDropPoint = currentPieceAtDropPoint!.moved(by: (0, 1))
|
||||
}
|
||||
currentPieceAtDropPoint = prev
|
||||
}
|
||||
|
||||
public func overlapsAny(_ piece: GamePiece) -> Bool {
|
||||
let (left, top) = piece.topLeft
|
||||
for y in 0..<piece.tiles.count {
|
||||
for x in 0..<piece.tiles.first!.count where piece.tiles[y][x] {
|
||||
if top + y >= height || left + x < 0 || left + x >= width || board[left + x, top + y] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
//
|
||||
// GamePiece.swift
|
||||
// TetrisKit
|
||||
//
|
||||
// Created by Shadowfacts on 10/13/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct GamePiece {
|
||||
public let tetromino: Tetromino
|
||||
public internal(set) var topLeft: (Int, Int)
|
||||
public internal(set) var tiles: [[Bool]]
|
||||
|
||||
public init(tetromino: Tetromino) {
|
||||
self.tetromino = tetromino
|
||||
self.tiles = tetromino.shape
|
||||
self.topLeft = (0, 0)
|
||||
}
|
||||
|
||||
mutating func rotate(direction: RotationDirection) {
|
||||
switch direction {
|
||||
case .clockwise:
|
||||
self.tiles.rotateClockwise()
|
||||
case .counterClockwise:
|
||||
self.tiles.rotateCounterclockwise()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum RotationDirection {
|
||||
case clockwise, counterClockwise
|
||||
}
|
||||
|
||||
extension GamePiece {
|
||||
public func moved(by: (Int, Int)) -> GamePiece {
|
||||
var modified = self
|
||||
modified.topLeft = (topLeft.0 + by.0, topLeft.1 + by.1)
|
||||
return modified
|
||||
}
|
||||
public func rotated(_ direction: RotationDirection) -> GamePiece {
|
||||
var modified = self
|
||||
modified.rotate(direction: direction)
|
||||
return modified
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// TetrisKit.h
|
||||
// TetrisKit iOS
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for TetrisKit.
|
||||
FOUNDATION_EXPORT double TetrisKitVersionNumber;
|
||||
|
||||
//! Project version string for TetrisKit.
|
||||
FOUNDATION_EXPORT const unsigned char TetrisKitVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <TetrisKit/PublicHeader.h>
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// Tetromino.swift
|
||||
// TetrisKit
|
||||
//
|
||||
// Created by Shadowfacts on 10/13/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Tetromino: CaseIterable {
|
||||
case i, o, t, j, l, s, z
|
||||
|
||||
public var shape: [[Bool]] {
|
||||
return Tetromino.shapes[self]!
|
||||
}
|
||||
}
|
||||
|
||||
extension Tetromino {
|
||||
static var shapes: [Tetromino: [[Bool]]] = [
|
||||
.i: parseShape("""
|
||||
XXXX
|
||||
----
|
||||
----
|
||||
----
|
||||
"""),
|
||||
.o: parseShape("""
|
||||
XX
|
||||
XX
|
||||
"""),
|
||||
.t: parseShape("""
|
||||
XXX
|
||||
-X-
|
||||
---
|
||||
"""),
|
||||
.j: parseShape("""
|
||||
XXX
|
||||
X--
|
||||
---
|
||||
"""),
|
||||
.l: parseShape("""
|
||||
XXX
|
||||
--X
|
||||
---
|
||||
"""),
|
||||
.s: parseShape("""
|
||||
XX-
|
||||
-XX
|
||||
---
|
||||
"""),
|
||||
.z: parseShape("""
|
||||
-XX
|
||||
XX-
|
||||
---
|
||||
""")
|
||||
]
|
||||
|
||||
static func parseShape(_ str: String) -> [[Bool]] {
|
||||
let lines = str.split(separator: "\n")
|
||||
return lines.map { line in line.map { char in char == "X" } }
|
||||
}
|
||||
}
|
||||
|
||||
extension Tetromino {
|
||||
static func random() -> Tetromino {
|
||||
return allCases[Int.random(in: 0..<allCases.count)]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// BoardView.swift
|
||||
// Tetris
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import TetrisKit
|
||||
|
||||
public struct BoardView: View {
|
||||
@Binding var board: GameBoard
|
||||
@Binding var currentPiece: GamePiece?
|
||||
@Binding var droppedPiece: GamePiece?
|
||||
|
||||
public init(board: Binding<GameBoard>, currentPiece: Binding<GamePiece?>, droppedPiece: Binding<GamePiece?>) {
|
||||
self._board = board
|
||||
self._currentPiece = currentPiece
|
||||
self._droppedPiece = droppedPiece
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
TilesView(board: $board)
|
||||
|
||||
CurrentPieceView(boardWidth: board.width, boardHeight: board.height, currentPiece: $currentPiece, droppedPiece: $droppedPiece)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BoardView_Previews: PreviewProvider {
|
||||
@ObservedObject static var controller: GameController = {
|
||||
var c = GameController()
|
||||
c.currentPiece = GamePiece(tetromino: .t)
|
||||
return c
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
BoardView(board: $controller.board, currentPiece: $controller.currentPiece, droppedPiece: $controller.currentPieceAtDropPoint)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// CurrentPieceView.swift
|
||||
// Tetris
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import TetrisKit
|
||||
|
||||
struct CurrentPieceView: View {
|
||||
let boardWidth: Int
|
||||
let boardHeight: Int
|
||||
@Binding var currentPiece: GamePiece?
|
||||
@Binding var droppedPiece: GamePiece?
|
||||
|
||||
var body: some View {
|
||||
GridView(rows: self.boardHeight, columns: self.boardWidth) { (col, row, size) in
|
||||
Rectangle()
|
||||
.foregroundColor(self.currentPieceAt(col, row) ? Color.blue : self.droppedPieceAt(col, row) ? Color.gray : Color.clear)
|
||||
.frame(width: size, height: size)
|
||||
}
|
||||
}
|
||||
|
||||
func currentPieceAt(_ col: Int, _ row: Int) -> Bool {
|
||||
guard let currentPiece = self.currentPiece else { return false }
|
||||
let (left, top) = currentPiece.topLeft
|
||||
let pieceHeight = currentPiece.tiles.count
|
||||
let pieceWidth = currentPiece.tiles.first!.count
|
||||
return col - left >= 0 && col - left < pieceWidth && row - top >= 0 && row - top < pieceHeight && currentPiece.tiles[row - top][col - left]
|
||||
}
|
||||
|
||||
func droppedPieceAt(_ col: Int, _ row: Int) -> Bool {
|
||||
guard let droppedPiece = self.droppedPiece else { return false }
|
||||
let (left, top) = droppedPiece.topLeft
|
||||
let pieceHeight = droppedPiece.tiles.count
|
||||
let pieceWidth = droppedPiece.tiles.first!.count
|
||||
return col - left >= 0 && col - left < pieceWidth && row - top >= 0 && row - top < pieceHeight && droppedPiece.tiles[row - top][col - left]
|
||||
}
|
||||
}
|
||||
|
||||
struct CurrentPieceView_Previews: PreviewProvider {
|
||||
@State static var currentPiece: GamePiece? = GamePiece(tetromino: .t)
|
||||
@State static var droppedPiece: GamePiece? = {
|
||||
var piece = GamePiece(tetromino: .t)
|
||||
return piece.moved(by: (0, 16 - piece.tiles.count))
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
CurrentPieceView(boardWidth: 10, boardHeight: 16, currentPiece: $currentPiece, droppedPiece: $droppedPiece)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// GridView.swift
|
||||
// Tetris
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct GridView<Cell>: View where Cell: View {
|
||||
|
||||
let rows: Int
|
||||
let columns: Int
|
||||
let cellProvider: (Int, Int, CGFloat) -> Cell
|
||||
|
||||
init(rows: Int, columns: Int, @ViewBuilder cellProvider: @escaping (Int, Int, CGFloat) -> Cell) {
|
||||
self.rows = rows
|
||||
self.columns = columns
|
||||
self.cellProvider = cellProvider
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { (geometry) in
|
||||
VStack(spacing: 0) {
|
||||
ForEach(0..<self.rows) { (row) in
|
||||
HStack(spacing: 0) {
|
||||
ForEach(0..<self.columns) { (col) in
|
||||
self.cellProvider(col, row, self.cellSize(for: geometry))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cellSize(for geometry: GeometryProxy) -> CGFloat {
|
||||
min(geometry.size.width / CGFloat(columns), geometry.size.height / CGFloat(rows))
|
||||
}
|
||||
}
|
||||
|
||||
struct GridView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GridView(rows: 3, columns: 3) { (col, row, size) in
|
||||
Rectangle().frame(width: size, height: size).foregroundColor(Color.red)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// TetrisUI_iOS.h
|
||||
// TetrisUI iOS
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for TetrisUI_iOS.
|
||||
FOUNDATION_EXPORT double TetrisUI_iOSVersionNumber;
|
||||
|
||||
//! Project version string for TetrisUI_iOS.
|
||||
FOUNDATION_EXPORT const unsigned char TetrisUI_iOSVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <TetrisUI_iOS/PublicHeader.h>
|
||||
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// TilesView.swift
|
||||
// Tetris
|
||||
//
|
||||
// Created by Shadowfacts on 10/14/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import TetrisKit
|
||||
|
||||
struct TilesView: View {
|
||||
@Binding var board: GameBoard
|
||||
|
||||
var body: some View {
|
||||
GridView(rows: board.height, columns: board.width) { (col, row, size) in
|
||||
Rectangle()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(self.board[col, row] ? Color.red : Color.black)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TilesView_Previews: PreviewProvider {
|
||||
@State static var board: GameBoard = {
|
||||
var b = GameBoard(width: 10, height: 16)
|
||||
b.set(tiles: [(0, 15), (1, 15), (1, 14), (2, 14)])
|
||||
return b
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
TilesView(board: $board)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// main.swift
|
||||
// tetriscli
|
||||
//
|
||||
// Created by Shadowfacts on 10/13/19.
|
||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TwoDimString: CustomStringConvertible {
|
||||
var width: Int
|
||||
var height: Int
|
||||
var rows: [String]
|
||||
|
||||
var description: String {
|
||||
return rows.joined(separator: "\n")
|
||||
}
|
||||
|
||||
init(width: Int, height: Int) {
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.rows = []
|
||||
|
||||
let empty = String(repeating: " ", count: width)
|
||||
for _ in 1...height {
|
||||
rows.append(empty)
|
||||
}
|
||||
}
|
||||
|
||||
subscript(column: Int, row: Int) -> Character {
|
||||
get {
|
||||
guard column >= 0 && column < width,
|
||||
row >= 0 && row < height else { fatalError("Out of bounds position \(column), \(row) for TwoDimString(width: \(width), height: \(height))") }
|
||||
let rowStr = rows[row]
|
||||
return rowStr[rowStr.index(rowStr.startIndex, offsetBy: column)]
|
||||
}
|
||||
set {
|
||||
guard column >= 0 && column < width,
|
||||
row >= 0 && row < height else { fatalError("Out of bounds position \(column), \(row) for TwoDimString(width: \(width), height: \(height))") }
|
||||
let rowStr = rows[row]
|
||||
|
||||
rows[row] = String(rowStr.prefix(column)) + String(newValue) + String(rowStr.dropFirst(column + 1))
|
||||
}
|
||||
}
|
||||
|
||||
func setRow(_ row: Int, to str: String) {
|
||||
guard row >= 0 && row < height else { fatalError("Can't set row \(row) for TwoDimString(width: \(width), height: \(height))") }
|
||||
guard str.count == width else { fatalError("Can't set row with count \(str.count) for TwoDimString(width: \(width), height: \(height))") }
|
||||
|
||||
rows[row] = str
|
||||
}
|
||||
|
||||
func setColumn(_ col: Int, to str: String) {
|
||||
guard col >= 0 && col < width else { fatalError("Can't set column \(col) for TwoDimString(width: \(width), height: \(height))") }
|
||||
guard str.count == height else { fatalError("Can't set row with count \(str.count) for TwoDimString(width: \(width), height: \(height))") }
|
||||
|
||||
for row in 0..<height {
|
||||
self[col, row] = str[str.index(str.startIndex, offsetBy: row)]
|
||||
}
|
||||
}
|
||||
|
||||
func setSubString(at column: Int, row: Int, string: TwoDimString) {
|
||||
guard column >= 0 && column + string.width <= width,
|
||||
row >= 0 && row + string.height <= height else { fatalError("Can't set TwoDimString(width: \(string.width), height: \(string.height)) at (\(column), \(row))") }
|
||||
|
||||
for subCol in 0..<string.width {
|
||||
for subRow in 0..<string.height {
|
||||
self[subCol + column, subRow + row] = string[subCol, subRow]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TwoDimString {
|
||||
convenience init(controller: GameController) {
|
||||
self.init(width: controller.width + 2, height: controller.height + 2)
|
||||
|
||||
setRow(0, to: "+" + String(repeating: "-", count: controller.width) + "+")
|
||||
setRow(controller.height + 1, to: "+" + String(repeating: "-", count: controller.width) + "+")
|
||||
setColumn(0, to: "+" + String(repeating: "|", count: controller.height) + "+")
|
||||
setColumn(controller.width + 1, to: "+" + String(repeating: "|", count: controller.height) + "+")
|
||||
|
||||
for y in 0..<controller.height {
|
||||
for x in 0..<controller.width where controller.board.get(tile: (x, y)) {
|
||||
self[x + 1, y + 1] = "X"
|
||||
}
|
||||
}
|
||||
|
||||
if let currentPiece = controller.currentPiece {
|
||||
let (left, top) = currentPiece.topLeft
|
||||
for y in 0..<currentPiece.tiles.count where y + top + 1 >= 0 && y + top + 1 < height {
|
||||
for x in 0..<currentPiece.tiles.first!.count where x + left + 1 >= 0 && x + left + 1 < width {
|
||||
self[x + left + 1, y + top + 1] = currentPiece.tiles[y][x] ? "X" : "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let controller = GameController()
|
||||
controller.currentPiece = GamePiece(tetromino: .l)
|
||||
|
||||
func readMove() {
|
||||
print(TwoDimString(controller: controller))
|
||||
print("Move: ", terminator: "")
|
||||
let input = readLine()!.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
switch input {
|
||||
case "", "s", "step":
|
||||
controller.step()
|
||||
case "cw":
|
||||
controller.rotate(direction: .clockwise)
|
||||
case "ccw":
|
||||
controller.rotate(direction: .counterClockwise)
|
||||
case "l", "left":
|
||||
controller.left()
|
||||
case "r", "right":
|
||||
controller.right()
|
||||
case "drop":
|
||||
controller.drop()
|
||||
case "hold":
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
while true {
|
||||
readMove()
|
||||
}
|
Loading…
Reference in New Issue