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

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" : [
{
"idiom" : "universal",
"platform" : "ios",
"filename" : "40.png",
"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"
}
],

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
struct SetGame {
private var deck: Array<Card>
private(set) var cardsOnTable: Array<Card>
private(set) var cards: 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() {
var deck = SetGame.createCards()
deck.shuffle()
let cardsOnTable = Array(deck[0..<12])
deck.removeAll { card in
cardsOnTable.contains { $0 == card }
for i in 0..<GameConstants.cardsOnTableInTheBeggining {
deck[i].isOnTheTable = true
}
self.deck = deck
self.cardsOnTable = cardsOnTable
self.cards = deck
}
private static func createCards() -> Array<SetGame.Card> {
@@ -30,7 +119,7 @@ struct SetGame {
var id = 0
for symbol in CardSymbol.allCases {
for shading in CardShading.allCases {
for number in 1..<4 {
for number in 1..<GameConstants.numberOfDifferences {
for symbolColor in colors.indices {
let newCard = SetGame.Card(id: id, shading: shading, symbol: symbol, color: colors[symbolColor], numberOnCard: number)
deck.append(newCard)
@@ -42,17 +131,12 @@ struct SetGame {
return deck
}
mutating func newGame() {
var deck = SetGame.createCards()
deck.shuffle()
let cardsOnTable = Array(deck[0..<12])
deck.removeAll { card in
cardsOnTable.contains { $0 == card }
private func randomColor() -> Color {
let red = Double.random(in: 0...1)
let blue = Double.random(in: 0...1)
let green = Double.random(in: 0...1)
return Color(red: red, green: green, blue: blue).opacity(0.5)
}
self.deck = deck
self.cardsOnTable = cardsOnTable
}
struct Card: Identifiable, Equatable {
@@ -62,7 +146,15 @@ struct SetGame {
var color: Color
var isMatched: Bool = false
var isSelected: Bool = false
var isOnTheTable: Bool = false
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 SwiftUI
class SetGameModelView: ObservableObject {
@@ -19,12 +20,32 @@ class SetGameModelView: ObservableObject {
}
var cardsOnTable: Array<Card> {
model.cardsOnTable
model.displayedCards.filter(showCard)
}
private func showCard(card: SetGameModelView.Card) -> Bool {
!card.isMatched
}
func newGame() {
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
var body: some View {
VStack {
AspectVGrid(items: game.cardsOnTable, aspectRatio: 2/3) { card in
CardView(card: card).foregroundColor(.red)
ScrollOrAspectVGrid()
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 {
var colors: [Int : Color]
var card: SetGameModelView.Card
var numberOfSymbols: Int
var color: Color
var symbol: CardSymbol
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.numberOfSymbols = card.numberOnCard
self.color = card.color
self.symbol = card.symbol
self.shading = card.shading
self.colors = colors;
}
var body: some View {
@@ -44,14 +76,16 @@ struct CardView: View {
ZStack {
let cardShape = RoundedRectangle(cornerRadius: 10)
cardShape.aspectRatio(2/3, contentMode: .fit)
if (card.isSelected) {
cardShape.foregroundColor(selectedGreen)
} else {
cardShape.foregroundColor(.white)
}
cardShape.strokeBorder(lineWidth: 5)
if (shading == CardShading.open) {
}
VStack {
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
private func createShape() -> some View {
private func createShape(height: CGFloat) -> some View {
switch symbol {
case .diamond:
colorShape(Diamond())
.frame(height: height)
case .squiggles:
colorShape(Rectangle())
.frame(height: height)
default:
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