Initial commit

This commit is contained in:
Shadowfacts 2019-10-15 12:24:58 -04:00
commit f844d5466f
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
42 changed files with 1798 additions and 0 deletions

78
.gitignore vendored Normal file
View File

@ -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

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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()
}
}

View File

@ -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 youre 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 youre done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once youre 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)
}
}
}
}

View File

@ -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()
}
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

37
Tetris/AppDelegate.swift Normal file
View File

@ -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.
}
}

View File

@ -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"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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>

129
Tetris/ContentView.swift Normal file
View File

@ -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()
}
}

60
Tetris/Info.plist Normal file
View File

@ -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>

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -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.
}
}

View File

@ -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
}
}
}
}

52
TetrisKit/GameBoard.swift Normal file
View File

@ -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
}
}
}

View File

@ -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
}
}

47
TetrisKit/GamePiece.swift Normal file
View File

@ -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
}
}

22
TetrisKit/Info.plist Normal file
View File

@ -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>

19
TetrisKit/TetrisKit.h Normal file
View File

@ -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>

68
TetrisKit/Tetromino.swift Normal file
View File

@ -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)]
}
}

42
TetrisUI/BoardView.swift Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

48
TetrisUI/GridView.swift Normal file
View File

@ -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)
}
}
}

22
TetrisUI/Info.plist Normal file
View File

@ -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>

19
TetrisUI/TetrisUI_iOS.h Normal file
View File

@ -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>

34
TetrisUI/TilesView.swift Normal file
View File

@ -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)
}
}

131
tetriscli/main.swift Normal file
View File

@ -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()
}