mirror of
https://github.com/ivanvorobei/SwiftUI.git
synced 2026-04-14 04:49:52 +02:00
Add 2048 Game
This commit is contained in:
116
Examples/2048 Game/SwiftUI2048/Views/BlockGridView.swift
Executable file
116
Examples/2048 Game/SwiftUI2048/Views/BlockGridView.swift
Executable 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
|
||||
118
Examples/2048 Game/SwiftUI2048/Views/BlockView.swift
Executable file
118
Examples/2048 Game/SwiftUI2048/Views/BlockView.swift
Executable 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
|
||||
127
Examples/2048 Game/SwiftUI2048/Views/GameView.swift
Executable file
127
Examples/2048 Game/SwiftUI2048/Views/GameView.swift
Executable 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
|
||||
Reference in New Issue
Block a user