Add 2048 Game

This commit is contained in:
Ivan Vorobei
2019-06-06 22:28:01 +03:00
parent bcedea412a
commit 5eaf1fbc68
20 changed files with 1390 additions and 1 deletions

View File

@@ -0,0 +1,116 @@
//
// BlockGridView.swift
// SwiftUI2048
//
// Created by Hongyu on 6/5/19.
// Copyright © 2019 Cyandev. All rights reserved.
//
import SwiftUI
fileprivate struct IdentifiableIndexedBlock : Identifiable {
typealias ID = String
typealias IdentifiedValue = IndexedBlock<IdentifiedBlock>
static var uniqueBlankId = 0
let indexedBlock: IndexedBlock<IdentifiedBlock>
var id: Self.ID {
if let id = indexedBlock.item?.id {
return "\(id)"
}
// TODO: (Refactor) Don't mix two types of block views.
IdentifiableIndexedBlock.uniqueBlankId += 1
return "Blank_\(IdentifiableIndexedBlock.uniqueBlankId)"
}
var identifiedValue: Self.IdentifiedValue {
return indexedBlock
}
}
extension AnyTransition {
static func blockAppear(from: Edge) -> AnyTransition {
return .asymmetric(
insertion: AnyTransition.opacity
.combined(with: .move(edge: from)),
removal: .identity)
}
}
struct BlockGridView : View {
typealias SupportingMatrix = BlockMatrix<IdentifiedBlock>
let matrix: Self.SupportingMatrix
let blockEnterEdge: Edge
func createBlock(_ block: IdentifiedBlock?) -> some View {
if let block = block {
return BlockView(number: block.number)
}
return BlockView.blank()
}
// FIXME: This is existed as a workaround for a Swift compiler bug.
func zIndex(_ block: IdentifiedBlock?) -> Double {
if block == nil {
return 1
}
return 1000
}
var body: some View {
ZStack {
ForEach(
self.matrix.flatten.map { IdentifiableIndexedBlock(indexedBlock: $0) }
) { block in
self.createBlock(block.item)
.frame(width: 65, height: 65, alignment: .center)
.position(x: CGFloat(block.index.0) * (65 + 12) + 32.5 + 12,
y: CGFloat(block.index.1) * (65 + 12) + 32.5 + 12)
.zIndex(self.zIndex(block.item))
.transition(.blockAppear(from: self.blockEnterEdge))
.animation(block.item == nil ? nil : .spring(mass: 1, stiffness: 400, damping: 56, initialVelocity: 0))
}
}
.frame(width: 320, height: 320, alignment: .center)
.background(
Rectangle()
.fill(Color(red:0.72, green:0.66, blue:0.63, opacity:1.00))
)
.clipped()
.cornerRadius(6)
}
}
#if DEBUG
struct BlockGridView_Previews : PreviewProvider {
static var matrix: BlockGridView.SupportingMatrix {
var _matrix = BlockGridView.SupportingMatrix()
_matrix.place(IdentifiedBlock(id: 1, number: 2), to: (2, 0))
_matrix.place(IdentifiedBlock(id: 2, number: 2), to: (3, 0))
_matrix.place(IdentifiedBlock(id: 3, number: 8), to: (1, 1))
_matrix.place(IdentifiedBlock(id: 4, number: 4), to: (2, 1))
_matrix.place(IdentifiedBlock(id: 5, number: 512), to: (3, 3))
_matrix.place(IdentifiedBlock(id: 6, number: 1024), to: (2, 3))
_matrix.place(IdentifiedBlock(id: 7, number: 16), to: (0, 3))
_matrix.place(IdentifiedBlock(id: 8, number: 8), to: (1, 3))
return _matrix
}
static var previews: some View {
BlockGridView(matrix: matrix, blockEnterEdge: .top)
.previewLayout(.sizeThatFits)
}
}
#endif

View File

@@ -0,0 +1,118 @@
//
// BlockView.swift
// SwiftUI2048
//
// Created by Hongyu on 6/5/19.
// Copyright © 2019 Cyandev. All rights reserved.
//
import SwiftUI
struct BlockView : View {
fileprivate let colorScheme: [(Color, Color)] = [
// 2
(Color(red:0.91, green:0.87, blue:0.83, opacity:1.00), Color(red:0.42, green:0.39, blue:0.35, opacity:1.00)),
// 4
(Color(red:0.90, green:0.86, blue:0.76, opacity:1.00), Color(red:0.42, green:0.39, blue:0.35, opacity:1.00)),
// 8
(Color(red:0.93, green:0.67, blue:0.46, opacity:1.00), Color.white),
// 16
(Color(red:0.94, green:0.57, blue:0.38, opacity:1.00), Color.white),
// 32
(Color(red:0.95, green:0.46, blue:0.33, opacity:1.00), Color.white),
// 64
(Color(red:0.94, green:0.35, blue:0.23, opacity:1.00), Color.white),
// 128
(Color(red:0.91, green:0.78, blue:0.43, opacity:1.00), Color.white),
// 256
(Color(red:0.91, green:0.78, blue:0.37, opacity:1.00), Color.white),
// 512
(Color(red:0.90, green:0.77, blue:0.31, opacity:1.00), Color.white),
// 1024
(Color(red:0.91, green:0.75, blue:0.24, opacity:1.00), Color.white),
// 2048
(Color(red:0.91, green:0.74, blue:0.18, opacity:1.00), Color.white),
]
fileprivate let number: Int?
init(number: Int) {
self.number = number
}
fileprivate init() {
self.number = nil
}
static func blank() -> Self {
return self.init()
}
fileprivate var numberText: String {
guard let number = number else {
return ""
}
return String(number)
}
fileprivate var fontSize: CGFloat {
let textLength = numberText.count
if textLength < 3 {
return 32
} else if textLength < 4 {
return 18
} else {
return 12
}
}
fileprivate var colorPair: (Color, Color) {
guard let number = number else {
return (Color(red:0.78, green:0.73, blue:0.68, opacity:1.00), Color.black)
}
let index = Int(log2(Double(number))) - 1
if index < 0 || index >= colorScheme.count {
fatalError("No color for such number")
}
return colorScheme[index]
}
// MARK: Body
var body: some View {
ZStack {
Rectangle().fill(colorPair.0)
Text(numberText)
.font(Font.system(size: fontSize).bold())
.color(colorPair.1)
.id(numberText)
.transition(AnyTransition.scale(scale: 0.5, anchor: .center).combined(with: .opacity))
.animation(.fluidSpring())
}
.clipped()
.cornerRadius(6)
}
}
// MARK: - Previews
#if DEBUG
struct BlockView_Previews : PreviewProvider {
static var previews: some View {
Group {
ForEach((1...11).map { Int(pow(2, Double($0))) }) { i in
BlockView(number: i)
.previewLayout(.sizeThatFits)
}
BlockView.blank()
.previewLayout(.sizeThatFits)
}
}
}
#endif

View File

@@ -0,0 +1,127 @@
//
// GameView.swift
// SwiftUI2048
//
// Created by Hongyu on 6/5/19.
// Copyright © 2019 Cyandev. All rights reserved.
//
import SwiftUI
extension Edge {
static func from(_ from: GameLogic.Direction) -> Self {
switch from {
case .down:
return .top
case .up:
return .bottom
case .left:
return .trailing
case .right:
return .leading
}
}
}
struct GameView : View {
@State var gestureStartLocation: CGPoint = .zero
@State var lastGestureDirection: GameLogic.Direction = .up
@EnvironmentObject var gameLogic: GameLogic
fileprivate struct LayoutTraits {
let bannerOffset: CGSize
let containerAlignment: Alignment
}
fileprivate func layoutTraits(`for` proxy: GeometryProxy) -> LayoutTraits {
let landscape = proxy.size.width > proxy.size.height
return LayoutTraits(
bannerOffset: landscape
? .init(width: proxy.safeAreaInsets.leading + 32, height: 0)
: .init(width: 0, height: proxy.safeAreaInsets.top + 32),
containerAlignment: landscape ? .leading : .top
)
}
var gesture: some Gesture {
let threshold: CGFloat = 44
let drag = DragGesture()
.onChanged { v in
guard self.gestureStartLocation != v.startLocation else { return }
withTransaction(Transaction()) {
self.gestureStartLocation = v.startLocation
if v.translation.width > threshold {
// Move right
self.gameLogic.move(.right)
self.lastGestureDirection = .right
} else if v.translation.width < -threshold {
// Move left
self.gameLogic.move(.left)
self.lastGestureDirection = .left
} else if v.translation.height > threshold {
// Move down
self.gameLogic.move(.down)
self.lastGestureDirection = .down
} else if v.translation.height < -threshold {
// Move up
self.gameLogic.move(.up)
self.lastGestureDirection = .up
} else {
// Direction cannot be deduced, reset gesture state.
self.gestureStartLocation = .zero
}
}
// After the scene is updated, reset the last gesture direction
// to make sure the animation is right when user starts a new
// game.
OperationQueue.main.addOperation {
self.lastGestureDirection = .up
}
}
return drag
}
var body: some View {
GeometryReader { proxy in
bind(self.layoutTraits(for: proxy)) { layoutTraits in
ZStack(alignment: layoutTraits.containerAlignment) {
Text("2048")
.font(Font.system(size: 48).weight(.black))
.color(Color(red:0.47, green:0.43, blue:0.40, opacity:1.00))
.offset(layoutTraits.bannerOffset)
ZStack(alignment: .top) {
BlockGridView(matrix: self.gameLogic.blockMatrix,
blockEnterEdge: .from(self.lastGestureDirection))
.gesture(self.gesture)
}
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
}
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
.background(
Rectangle().fill(Color(red:0.96, green:0.94, blue:0.90, opacity:1.00))
)
}
}
.edgesIgnoringSafeArea(.all)
}
}
#if DEBUG
struct GameView_Previews : PreviewProvider {
static var previews: some View {
GameView()
.environmentObject(GameLogic())
}
}
#endif