Skip to content

Instantly share code, notes, and snippets.

@SLIB53
Last active January 13, 2024 21:31
Show Gist options
  • Save SLIB53/7e38e9735001263ac587a77cecc29457 to your computer and use it in GitHub Desktop.
Save SLIB53/7e38e9735001263ac587a77cecc29457 to your computer and use it in GitHub Desktop.
//
// MemorizeApp.swift
// Memorize
//
// Created by Akil Krishnan on 12/13/23.
//
import SwiftUI
@main
struct MemorizeApp: App {
var body: some Scene {
WindowGroup {
GameView(viewing: GamePresenter(presenting: Self.makeRandomGame()))
}
}
private static func makeRandomGame() -> Game {
[
{ Game(Pet.self, Pair.self) },
{ Game(Pet.self, Triplet.self) },
{ Game(Pet.self, Quadruplet.self) },
{ Game(Bakery.self, Pair.self) },
{ Game(Bakery.self, Triplet.self) },
{ Game(Bakery.self, Quadruplet.self) },
{ Game(Fantasy.self, Pair.self) },
{ Game(Fantasy.self, Triplet.self) },
{ Game(Fantasy.self, Quadruplet.self) },
{ Game(Ocean.self, Pair.self) },
{ Game(Ocean.self, Triplet.self) },
{ Game(Ocean.self, Quadruplet.self) }
].randomElement()!()
}
}
// MARK: - Views
struct GameView: View {
private let presenter: GamePresenter
var body: some View {
let cardPresentations = presenter.cardPresentations
return ScrollView {
LazyVGrid(columns: Array(repeating: GridItem(), count: 4)) {
ForEach(
cardPresentations,
content: makeCardView(from:)
)
}
.animation(.default, value: cardPresentations)
}
}
init(viewing presenter: GamePresenter) {
self.presenter = presenter
}
private func makeCardView(
from presentation: GamePresenter.CardPresentation
) -> CardView {
CardView(
viewing: presentation,
chooseIntention: { presenter.choose(cardPresentedBy: presentation) }
)
}
}
extension GameView {
struct CardView: View {
private static let cornerRadius = 10.0
private static let frameHeight = 120.0
private let presentation: GamePresenter.CardPresentation
private let chooseIntention: () -> Void
var body: some View {
if presentation.isFaceUp { faceUpView } else { faceDownView }
}
private var faceUpView: some View {
ZStack {
RoundedRectangle(cornerRadius: Self.cornerRadius)
.fill(.thickMaterial)
Text(presentation.icon)
.dynamicTypeSize(.xLarge)
}
.frame(height: Self.frameHeight)
}
private var faceDownView: some View {
ZStack {
RoundedRectangle(cornerRadius: Self.cornerRadius)
.fill(.tint)
}
.frame(height: Self.frameHeight)
.onTapGesture(perform: chooseIntention)
}
init(
viewing presentation: GamePresenter.CardPresentation,
chooseIntention: @escaping () -> Void
) {
self.presentation = presentation
self.chooseIntention = chooseIntention
}
}
}
// MARK: - View Models
@Observable
class GamePresenter {
private var game: Game
var cardPresentations: [CardPresentation] {
game.cards.map(CardPresentation.init(card:))
}
init(presenting game: Game) {
self.game = game
}
// MARK: - Intents
func choose(cardPresentedBy presentation: CardPresentation) {
do {
game.maintain()
defer { game.maintainMatches() }
try game.flipCardFaceUp(presentation.card)
} catch Game.GameError.CardNotFound {
print("warning: attempted to flip non-existent card")
} catch {
print("error: unknown error", error)
}
}
}
extension GamePresenter {
struct CardPresentation: Identifiable, Equatable {
let card: any Card
var id: Int { card.id }
var isFaceUp: Bool { card.isFaceUp }
var icon: String { Self.makeIcon(from: card) }
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.card.hashValue == rhs.card.hashValue
}
private static func makeIcon(from card: any Card) -> String {
func renderFace(_ face: any CardFace) -> String {
switch face {
case let face as Pet:
switch face {
case .dog:
"🐶"
case .cat:
"🐱"
case .bird:
"🐤"
case .fish:
"🐟"
}
case let face as Bakery:
switch face {
case .bread:
"🍞"
case .croissant:
"🥐"
case .cake:
"🍰"
case .cupcake:
"🧁"
}
case let face as Fantasy:
switch face {
case .fairy:
"🧚‍♀️"
case .elf:
"🧝‍♀️"
case .wizard:
"🧙‍♂️"
case .mermaid:
"🧜‍♀️"
}
case let face as Ocean:
switch face {
case .merman:
"🧜‍♂️"
case .whale:
"🐳"
case .waves:
"🌊"
case .sailboat:
"⛵️"
}
default:
"?"
}
}
return card.isMatched ? "🎉" : renderFace(card.face)
}
}
}
// MARK: - Models
struct Game {
private let occurrenceType: any CardOccurrence.Type
var cards: [any Card]
init<Occurrence, Face>(
_ faceType: Face.Type,
_ occurrenceType: Occurrence.Type
)
where Face: CardFace, Occurrence: CardOccurrence {
self.occurrenceType = occurrenceType
cards = BasicCard.makeOccurrences(faceType, occurrenceType)
.shuffled()
}
mutating func maintain() {
Self.groupDirtyCardIndexesByFace(cards, occurrenceType)
.forEach {
if $0.count == occurrenceType.allCases.count {
$0.forEach { index in
cards[index].isMatched = true
}
} else {
$0.forEach { index in
cards[index].isFaceUp = false
}
}
}
}
mutating func maintainMatches() {
Self.groupDirtyCardIndexesByFace(cards, occurrenceType)
.filter{ $0.count == occurrenceType.allCases.count }
.forEach {
$0.forEach { index in
cards[index].isMatched = true
}
}
}
mutating func flipCardFaceUp(_ card: any Card) throws {
guard let index = cards.firstIndex(where: { $0.id == card.id }) else {
throw GameError.CardNotFound
}
cards[index].isFaceUp = true
}
private static func groupDirtyCardIndexesByFace<Occurrence>(
_ cards: [any Card],
_ occurrenceType: Occurrence.Type
) -> [[Int]]
where Occurrence: CardOccurrence {
let maxOccurrences = occurrenceType.allCases.count
func isDirty(_ card: any Card) -> Bool {
card.isFaceUp && !(card.isMatched)
}
let candidate = Dictionary(
grouping: cards.enumerated().filter({ isDirty($0.1) }),
by: { $0.1.face.hashValue }
)
.values
.map {
$0.map { enumeratedCard in enumeratedCard.0 }
}
guard
candidate.count == 1 && candidate.first!.count >= maxOccurrences
|| candidate.count > 1
else {
return []
}
return candidate
}
enum GameError: Error {
case CardNotFound
}
}
protocol Card: Identifiable, Hashable {
var id: Int { get }
var face: any CardFace { get }
var occurrence: any CardOccurrence { get }
var isFaceUp: Bool { get set }
var isMatched: Bool { get set }
}
extension Card {
var id: Int {
var hasher = Hasher()
hasher.combine(face)
hasher.combine(occurrence)
return hasher.finalize()
}
}
struct BasicCard: Card {
let face: any CardFace
let occurrence: any CardOccurrence
var isFaceUp = false
var isMatched = false
static func makeOccurrences<Face, Occurrence>(
_ faceType: Face.Type,
_ occurrenceType: Occurrence.Type
) -> [BasicCard]
where Face: CardFace, Occurrence: CardOccurrence {
Face.allCases.flatMap { face in
Occurrence.allCases.map { occurrence in
BasicCard(face: face, occurrence: occurrence)
}
}
}
static func == (lhs: BasicCard, rhs: BasicCard) -> Bool {
lhs.face.hashValue == rhs.face.hashValue
&& lhs.occurrence.hashValue == rhs.occurrence.hashValue
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(isFaceUp)
hasher.combine(isMatched)
}
}
protocol CardFace: CaseIterable, Hashable, Equatable { }
enum Pet: CardFace {
case dog, cat, bird, fish
}
enum Bakery: CardFace {
case bread, croissant, cake, cupcake
}
enum Fantasy: CardFace {
case fairy, elf, wizard, mermaid
}
enum Ocean: CardFace {
case merman, whale, waves, sailboat
}
protocol CardOccurrence: CaseIterable, Hashable, Equatable { }
enum Pair: CardOccurrence {
case first, second
}
enum Triplet: CardOccurrence {
case first, second, third
}
enum Quadruplet: CardOccurrence {
case first, second, third, fourth
}
#Preview("Pet | Pair") {
GameView(viewing: GamePresenter(presenting: Game(Pet.self, Pair.self)))
}
#Preview("Pet | Triplet") {
GameView(viewing: GamePresenter(presenting: Game(Pet.self, Triplet.self)))
}
#Preview("Pet | Quadruplet") {
GameView(viewing: GamePresenter(presenting: Game(Pet.self, Quadruplet.self)))
}
#Preview("Bakery | Pair") {
GameView(viewing: GamePresenter(presenting: Game(Bakery.self, Pair.self)))
}
#Preview("Bakery | Triplet") {
GameView(viewing: GamePresenter(presenting: Game(Bakery.self, Triplet.self)))
}
#Preview("Bakery | Quadruplet") {
GameView(viewing: GamePresenter(presenting: Game(Bakery.self, Quadruplet.self)))
}
#Preview("Fantasy | Pair") {
GameView(viewing: GamePresenter(presenting: Game(Fantasy.self, Pair.self)))
}
#Preview("Fantasy | Triplet") {
GameView(viewing: GamePresenter(presenting: Game(Fantasy.self, Triplet.self)))
}
#Preview("Fantasy | Quadruplet") {
GameView(viewing: GamePresenter(presenting: Game(Fantasy.self, Quadruplet.self)))
}
#Preview("Ocean | Pair") {
GameView(viewing: GamePresenter(presenting: Game(Ocean.self, Pair.self)))
}
#Preview("Ocean | Triplet") {
GameView(viewing: GamePresenter(presenting: Game(Ocean.self, Triplet.self)))
}
#Preview("Ocean | Quadruplet") {
GameView(viewing: GamePresenter(presenting: Game(Ocean.self, Quadruplet.self)))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment