Compare commits

...

10 Commits

41 changed files with 536 additions and 32 deletions

View File

@@ -1,3 +1,7 @@
![Set Game app Icon](https://github.com/olinpin/SetGame/blob/main/resources/AppIcon.png?raw=true)
### Set Game ### Set Game
This is an implementation of the Set card game in Swift using SwiftUI. The game is a matching game where players try to find sets of three cards that share the same properties (color, symbol, number, and shading) or differ in all of these properties. The game ends when there are no more sets on the table. This is an implementation of the Set card game in Swift using SwiftUI. The game is a matching game where players try to find sets of three cards that share the same properties (color, symbol, number, and shading) or differ in all of these properties. The game ends when there are no more sets on the table.
## Usage ## Usage

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,8 +1,342 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "40.png",
"platform" : "ios", "idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "60.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "80.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "57.png",
"idiom" : "iphone",
"scale" : "1x",
"size" : "57x57"
},
{
"filename" : "114.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "57x57"
},
{
"filename" : "120.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "180.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "58.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "80.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "50.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "50x50"
},
{
"filename" : "100.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "50x50"
},
{
"filename" : "72.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "72x72"
},
{
"filename" : "144.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "72x72"
},
{
"filename" : "76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "152.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "167.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "64.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "1024.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
},
{
"filename" : "48.png",
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "24x24",
"subtype" : "38mm"
},
{
"filename" : "55.png",
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "27.5x27.5",
"subtype" : "42mm"
},
{
"filename" : "58.png",
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "87.png",
"idiom" : "watch",
"role" : "companionSettings",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "66.png",
"idiom" : "watch",
"role" : "notificationCenter",
"scale" : "2x",
"size" : "33x33",
"subtype" : "45mm"
},
{
"filename" : "80.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "40x40",
"subtype" : "38mm"
},
{
"filename" : "88.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "44x44",
"subtype" : "40mm"
},
{
"filename" : "92.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "46x46",
"subtype" : "41mm"
},
{
"filename" : "100.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "50x50",
"subtype" : "44mm"
},
{
"filename" : "102.png",
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "51x51",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "appLauncher",
"scale" : "2x",
"size" : "54x54",
"subtype" : "49mm"
},
{
"filename" : "172.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "86x86",
"subtype" : "38mm"
},
{
"filename" : "196.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "98x98",
"subtype" : "42mm"
},
{
"filename" : "216.png",
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "108x108",
"subtype" : "44mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "117x117",
"subtype" : "45mm"
},
{
"idiom" : "watch",
"role" : "quickLook",
"scale" : "2x",
"size" : "129x129",
"subtype" : "49mm"
},
{
"filename" : "1024.png",
"idiom" : "watch-marketing",
"scale" : "1x",
"size" : "1024x1024" "size" : "1024x1024"
} }
], ],

View File

@@ -29,3 +29,19 @@ struct Diamond: Shape {
} }
} }
struct Diamond_Previews: PreviewProvider {
static var previews: some View {
GeometryReader {geometry in
VStack {
Diamond()
.size(CGSize(width: geometry.size.width, height: geometry.size.height / 3))
Diamond()
.size(CGSize(width: geometry.size.width, height: geometry.size.height / 3))
Diamond()
.size(CGSize(width: geometry.size.width, height: geometry.size.height / 3))
}
}
.padding()
}
}

View File

@@ -9,19 +9,108 @@ import Foundation
import SwiftUI import SwiftUI
struct SetGame { struct SetGame {
private var deck: Array<Card> private(set) var cards: Array<Card>
private(set) var cardsOnTable: Array<Card> private(set) var score: Int = 0
private(set) var colors : [Int : Color] = [:]
private var matches: Int = 0
mutating func choose(_ card: Card) {
if let chosenIndex = cards.firstIndex(where: {card.id == $0.id}),
!cards[chosenIndex].isMatched,
!cards[chosenIndex].isSelected,
cards[chosenIndex].isOnTheTable
{
cards[chosenIndex].isSelected = true
var chosenCards: Array<Card> = Array()
for c in cards {
if c.isSelected {
chosenCards.append(c)
}
}
if chosenCards.count > 2 {
let chosenCard1 = chosenCards[0]
let chosenCard2 = chosenCards[1]
let chosenCard3 = chosenCards[2]
let chosenCards = [chosenCard1, chosenCard2, chosenCard3]
let color = all3EqualOrAllDifferent(chosenCard1.color, chosenCard2.color, chosenCard3.color)
let shading = all3EqualOrAllDifferent(chosenCard1.shading, chosenCard2.shading, chosenCard3.shading)
let symbol = all3EqualOrAllDifferent(chosenCard1.symbol, chosenCard2.symbol, chosenCard3.symbol)
let numberOnCard = all3EqualOrAllDifferent(chosenCard1.numberOnCard, chosenCard2.numberOnCard, chosenCard3.numberOnCard)
if color && shading && symbol && numberOnCard {
for chosenCard in chosenCards {
cards[cards.firstIndex(where: {chosenCard.id == $0.id})!].isMatched = true
cards[cards.firstIndex(where: {chosenCard.id == $0.id})!].matchId = matches
}
colors.updateValue(randomColor(), forKey: self.matches)
score += 1
matches += 1
if (cardsOnTheTable < GameConstants.cardsOnTableInTheBeggining) {
addCardsToTable(GameConstants.numberOfCardsToDraw)
}
} else {
score -= 1
}
for chosenCard in chosenCards {
cards[cards.firstIndex(where: {chosenCard.id == $0.id})!].isSelected = false
}
}
}
else if let chosenIndex = cards.firstIndex(where: {card.id == $0.id}),
!cards[chosenIndex].isMatched,
cards[chosenIndex].isSelected,
cards[chosenIndex].isOnTheTable {
cards[chosenIndex].isSelected = false
}
}
func all3EqualOrAllDifferent<EquatableObject: Equatable>(_ first: EquatableObject, _ second: EquatableObject, _ third: EquatableObject) -> Bool {
(first == second && first == third) || ((first != second && first != third) && second != third)
}
mutating func addCardsToTable(_ numberOfCards: Int) {
var i = 0
var cardsAdded = 0
while cardsAdded < numberOfCards && i < cards.count {
let card = cards[i]
if (!card.isOnTheTable && !card.isMatched) {
let cardIndexToBeModified = cards.firstIndex(where: {$0.id == card.id})
if (cardIndexToBeModified != nil) {
cards[cardIndexToBeModified!].isOnTheTable = true
cardsAdded += 1
}
}
i += 1
}
}
var displayedCards: Array<Card> {
cards.filter { card in
card.isOnTheTable
// && !card.isMatched
}.sorted {
$0.matchId < $1.matchId
}
}
var cardsOnTheTable: Int {
cards.filter({$0.isOnTheTable && !$0.isMatched}).count
}
init() { init() {
var deck = SetGame.createCards() var deck = SetGame.createCards()
deck.shuffle() deck.shuffle()
let cardsOnTable = Array(deck[0..<12]) for i in 0..<GameConstants.cardsOnTableInTheBeggining {
deck.removeAll { card in deck[i].isOnTheTable = true
cardsOnTable.contains { $0 == card }
} }
self.deck = deck self.cards = deck
self.cardsOnTable = cardsOnTable
} }
private static func createCards() -> Array<SetGame.Card> { private static func createCards() -> Array<SetGame.Card> {
@@ -30,7 +119,7 @@ struct SetGame {
var id = 0 var id = 0
for symbol in CardSymbol.allCases { for symbol in CardSymbol.allCases {
for shading in CardShading.allCases { for shading in CardShading.allCases {
for number in 1..<4 { for number in 1..<GameConstants.numberOfDifferences {
for symbolColor in colors.indices { for symbolColor in colors.indices {
let newCard = SetGame.Card(id: id, shading: shading, symbol: symbol, color: colors[symbolColor], numberOnCard: number) let newCard = SetGame.Card(id: id, shading: shading, symbol: symbol, color: colors[symbolColor], numberOnCard: number)
deck.append(newCard) deck.append(newCard)
@@ -42,17 +131,12 @@ struct SetGame {
return deck return deck
} }
mutating func newGame() { private func randomColor() -> Color {
var deck = SetGame.createCards() let red = Double.random(in: 0...1)
deck.shuffle() let blue = Double.random(in: 0...1)
let cardsOnTable = Array(deck[0..<12]) let green = Double.random(in: 0...1)
deck.removeAll { card in return Color(red: red, green: green, blue: blue).opacity(0.5)
cardsOnTable.contains { $0 == card }
} }
self.deck = deck
self.cardsOnTable = cardsOnTable
}
struct Card: Identifiable, Equatable { struct Card: Identifiable, Equatable {
@@ -62,7 +146,15 @@ struct SetGame {
var color: Color var color: Color
var isMatched: Bool = false var isMatched: Bool = false
var isSelected: Bool = false var isSelected: Bool = false
var isOnTheTable: Bool = false
var numberOnCard: Int var numberOnCard: Int
var matchId: Int = -1
}
private struct GameConstants {
static let numberOfDifferences = 4
static let cardsOnTableInTheBeggining = 12
static let numberOfCardsToDraw = 3
} }
} }

View File

@@ -6,6 +6,7 @@
// //
import Foundation import Foundation
import SwiftUI
class SetGameModelView: ObservableObject { class SetGameModelView: ObservableObject {
@@ -19,12 +20,32 @@ class SetGameModelView: ObservableObject {
} }
var cardsOnTable: Array<Card> { var cardsOnTable: Array<Card> {
model.cardsOnTable model.displayedCards.filter(showCard)
}
private func showCard(card: SetGameModelView.Card) -> Bool {
!card.isMatched
} }
func newGame() { func newGame() {
self.model = SetGame() self.model = SetGame()
} }
func choose(_ card: Card) {
self.model.choose(card)
}
var score: Int {
model.score
}
func deal() {
self.model.addCardsToTable(3)
}
var colors: [Int : Color] {
self.model.colors
}
} }

View File

@@ -12,31 +12,63 @@ struct SetGameView: View {
@ObservedObject var game: SetGameModelView @ObservedObject var game: SetGameModelView
var body: some View { var body: some View {
VStack { VStack {
AspectVGrid(items: game.cardsOnTable, aspectRatio: 2/3) { card in ScrollOrAspectVGrid()
CardView(card: card).foregroundColor(.red) Spacer()
Spacer()
VStack {
Button(action: {game.deal()}, label: { Text("Deal 3 more cards").font(.largeTitle) })
HStack {
Button(action: {game.newGame()}, label: {Text("New game").font(.largeTitle)}).padding()
Spacer()
Text("\(game.score)").font(.largeTitle).bold()
}
} }
Spacer()
Spacer()
Button(action: {game.newGame()}, label: {Text("New game").font(.largeTitle)})
} }
} }
@ViewBuilder
private func ScrollOrAspectVGrid() -> some View {
if (game.cardsOnTable.count <= 21) {
AspectVGrid(items: game.cardsOnTable, aspectRatio: 2/3) { card in
CardView(card: card, colors: game.colors).foregroundColor(.red).onTapGesture {
game.choose(card)
}
}
}
else {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 70), spacing: 0)], spacing: 0) {
ForEach(game.cardsOnTable) { card in
CardView(card: card, colors: game.colors)
.aspectRatio(2/3, contentMode: .fit)
.foregroundColor(.red)
.onTapGesture {
game.choose(card)
}
}
}
}
}
}
} }
struct CardView: View { struct CardView: View {
var colors: [Int : Color]
var card: SetGameModelView.Card var card: SetGameModelView.Card
var numberOfSymbols: Int var numberOfSymbols: Int
var color: Color var color: Color
var symbol: CardSymbol var symbol: CardSymbol
var shading: CardShading var shading: CardShading
var selectedGreen: Color = Color(hue: 0.355, saturation: 1.0, brightness: 1.0)
init(card: SetGameModelView.Card) { init(card: SetGameModelView.Card, colors: [Int : Color]) {
self.card = card self.card = card
self.numberOfSymbols = card.numberOnCard self.numberOfSymbols = card.numberOnCard
self.color = card.color self.color = card.color
self.symbol = card.symbol self.symbol = card.symbol
self.shading = card.shading self.shading = card.shading
self.colors = colors;
} }
var body: some View { var body: some View {
@@ -44,14 +76,16 @@ struct CardView: View {
ZStack { ZStack {
let cardShape = RoundedRectangle(cornerRadius: 10) let cardShape = RoundedRectangle(cornerRadius: 10)
cardShape.aspectRatio(2/3, contentMode: .fit) cardShape.aspectRatio(2/3, contentMode: .fit)
if (card.isSelected) {
cardShape.foregroundColor(selectedGreen)
} else {
cardShape.foregroundColor(.white) cardShape.foregroundColor(.white)
}
cardShape.strokeBorder(lineWidth: 5) cardShape.strokeBorder(lineWidth: 5)
if (shading == CardShading.open) {
}
VStack { VStack {
ForEach (0..<numberOfSymbols, id: \.self) { _ in ForEach (0..<numberOfSymbols, id: \.self) { _ in
createShape().frame(height: geometry.size.height/4) createShape(height: geometry.size.height / 4)
} }
} }
} }
@@ -59,14 +93,17 @@ struct CardView: View {
} }
@ViewBuilder @ViewBuilder
private func createShape() -> some View { private func createShape(height: CGFloat) -> some View {
switch symbol { switch symbol {
case .diamond: case .diamond:
colorShape(Diamond()) colorShape(Diamond())
.frame(height: height)
case .squiggles: case .squiggles:
colorShape(Rectangle()) colorShape(Rectangle())
.frame(height: height)
default: default:
colorShape(RoundedRectangle(cornerRadius: 100)) colorShape(RoundedRectangle(cornerRadius: 100))
.frame(height: height)
} }
} }

BIN
resources/AppIcon.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB