Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added Hearts game #6

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AllTests.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@
"name" : "SevensTests"
}
},
{
"target" : {
"containerPath" : "container:Cards",
"identifier" : "HeartsTests",
"name" : "HeartsTests"
}
},
{
"target" : {
"containerPath" : "container:Card Play.xcodeproj",
Expand Down
7 changes: 7 additions & 0 deletions Card Play.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
7C5C324227B9778E004897BF /* SevensGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5C324127B9778E004897BF /* SevensGuide.swift */; };
7C72FDEC2A63EDFD001E6551 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C72FDEB2A63EDFD001E6551 /* Game.swift */; };
7C72FDEF2A63EE2E001E6551 /* Sevens in Frameworks */ = {isa = PBXBuildFile; productRef = 7C72FDEE2A63EE2E001E6551 /* Sevens */; };
7C72FDF12A640EFF001E6551 /* Hearts in Frameworks */ = {isa = PBXBuildFile; productRef = 7C72FDF02A640EFF001E6551 /* Hearts */; };
7C7560A4279F6927007E3DE1 /* SevensTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C7560A3279F6927007E3DE1 /* SevensTableView.swift */; };
7C8FED502A6F0E3C00C949A1 /* PlatformSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8FED4F2A6F0E3C00C949A1 /* PlatformSupport.swift */; };
7C8FED522A6F136600C949A1 /* GroupActivityView+AppKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C8FED512A6F136600C949A1 /* GroupActivityView+AppKit.swift */; };
Expand Down Expand Up @@ -96,6 +97,7 @@
files = (
7CA9E6902A6F292B009CFA58 /* SwiftUINavigation in Frameworks */,
7C72FDEF2A63EE2E001E6551 /* Sevens in Frameworks */,
7C72FDF12A640EFF001E6551 /* Hearts in Frameworks */,
7C91993227A59DD600BC5802 /* ConfettiSwiftUI in Frameworks */,
7C0AE719279D7E06005695FB /* Cards in Frameworks */,
);
Expand Down Expand Up @@ -283,6 +285,7 @@
7C91993127A59DD600BC5802 /* ConfettiSwiftUI */,
7C72FDEE2A63EE2E001E6551 /* Sevens */,
7CA9E68F2A6F292B009CFA58 /* SwiftUINavigation */,
7C72FDF02A640EFF001E6551 /* Hearts */,
);
productName = "Card Play (iOS)";
productReference = 7C0AE6D8279D674E005695FB /* Deal.app */;
Expand Down Expand Up @@ -792,6 +795,10 @@
isa = XCSwiftPackageProductDependency;
productName = Sevens;
};
7C72FDF02A640EFF001E6551 /* Hearts */ = {
isa = XCSwiftPackageProductDependency;
productName = Hearts;
};
7C91993127A59DD600BC5802 /* ConfettiSwiftUI */ = {
isa = XCSwiftPackageProductDependency;
package = 7C91993027A59DD600BC5802 /* XCRemoteSwiftPackageReference "ConfettiSwiftUI" */;
Expand Down
3 changes: 3 additions & 0 deletions Cards/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ let package = Package(
.target(name: "Hearts", dependencies: [
"CardsModel", "CardsScoring"
]),
.testTarget(name: "HeartsTests", dependencies: [
"CardsModel", "CardsScoring", "Hearts"
]),

.target(name: "Sevens", dependencies: [
"CardsModel", "CardsScoring"
Expand Down
30 changes: 30 additions & 0 deletions Cards/Sources/Hearts/Deck+Hearts.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import CardsModel

extension Deck {
static func hearts(playerCount: Int) -> [PlayingCard] {
Deck.noJokers.filter {
!cardsToRemove(playerCount: playerCount).contains($0)
}
}

static func cardsToRemove(playerCount: Int) -> [PlayingCard] {
switch playerCount {
case 3:
return [.suited(.two, .clubs)]
case 5:
return [
.suited(.two, .clubs),
.suited(.two, .diamonds)
]
case 6:
return [
.suited(.two, .clubs),
.suited(.three, .clubs),
.suited(.two, .diamonds),
.suited(.two, .spades)
]
default:
return []
}
}
}
34 changes: 33 additions & 1 deletion Cards/Sources/Hearts/Hearts.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
import CardsModel
import Combine

final class Hearts: ObservableObject { }
public final class Hearts: ObservableObject {
@Published private(set) var players: [Player]
var trick: [PlayingCard] {
players.compactMap(\.selectedCard)
}

public init(players: Int) {
self.players = Deck.hearts(playerCount: players)
.deal(playerCount: players)
.map(Player.init)
}

func isValid(card: PlayingCard) -> Bool {
false
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
players = try container.decode([Player].self, forKey: .players)
}
}

extension Hearts: Codable {
enum CodingKeys: CodingKey {
case players
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(players, forKey: .players)
}
}
18 changes: 8 additions & 10 deletions Cards/Sources/Hearts/HeartsScore.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import CardsModel
import CardsScoring

extension Hearts {
struct HeartsScore: Score {
func score(forCard card: PlayingCard) -> Int {
switch card {
case .suited(_, .hearts):
return 1
case .suited(.queen, .spades):
return 13
default: return 0
}
struct HeartsScore: Score {
func score(forCard card: PlayingCard) -> Int {
switch card {
case .suited(_, .hearts):
return 1
case .suited(.queen, .spades):
return 13
default: return 0
}
}
}
57 changes: 57 additions & 0 deletions Cards/Sources/Hearts/Player.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import CardsModel
import Combine

public final class Player: ObservableObject {
@Published
public private(set) var hand: [PlayingCard]

@Published
public private(set) var selectedCard: PlayingCard?

@Published
public private(set) var score: Int
private let scoringSystem = HeartsScore()

var shotForTheMoon: Bool {
score == 26
}

init(hand: [PlayingCard]) {
self.hand = hand
self.score = 0
}

func play(card: PlayingCard) {
guard let index = hand.firstIndex(of: card) else {
preconditionFailure("Cannot play a card that is not in your hand")
}
hand.remove(at: index)
selectedCard = card
}

func pickUp(cards: [PlayingCard]) {
selectedCard = nil
score = cards
.map(scoringSystem.score)
.reduce(score, +)
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
hand = try container.decode([PlayingCard].self, forKey: .hand)
score = try container.decode(Int.self, forKey: .score)
}
}

extension Player: Codable {
enum CodingKeys: CodingKey {
case hand
case score
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(hand, forKey: .hand)
try container.encode(score, forKey: .score)
}
}
10 changes: 10 additions & 0 deletions Cards/Sources/Hearts/PlayingCard+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import CardsModel

extension PlayingCard {
var isHeart: Bool {
switch self {
case .suited(_, .hearts): return true
default: return false
}
}
}
10 changes: 10 additions & 0 deletions Cards/Sources/Sevens/PlayingCard+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import CardsModel

extension PlayingCard {
var isSeven: Bool {
switch self {
case .suited(.seven, _): return true
default: return false
}
}
}
9 changes: 0 additions & 9 deletions Cards/Sources/Sevens/Sevens.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,3 @@ extension Sevens: Codable {
try container.encode(table, forKey: .table)
}
}

extension PlayingCard {
var isSeven: Bool {
switch self {
case .suited(.seven, _): return true
default: return false
}
}
}
92 changes: 92 additions & 0 deletions Cards/Tests/HeartsTests/Deck+HeartsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import CardsModel
@testable import Hearts
import XCTest

final class DeckHeartsTests: XCTestCase {
func testCardsToRemove() {
XCTAssertEqual(
Deck.cardsToRemove(playerCount: 3),
[.suited(.two, .clubs)]
)

XCTAssertEqual(
Deck.cardsToRemove(playerCount: 4),
[]
)

XCTAssertEqual(
Deck.cardsToRemove(playerCount: 5),
[
.suited(.two, .clubs),
.suited(.two, .diamonds),
]
)

XCTAssertEqual(
Deck.cardsToRemove(playerCount: 6),
[
.suited(.two, .clubs),
.suited(.three, .clubs),
.suited(.two, .diamonds),
.suited(.two, .spades)
]
)
}

func testDeck() {
XCTAssertEqual(Deck.hearts(playerCount: 4), Deck.noJokers)
XCTAssertEqual(Deck.hearts(playerCount: 6), [
.suited(.king, .spades),
.suited(.queen, .spades),
.suited(.jack, .spades),
.suited(.ten, .spades),
.suited(.nine, .spades),
.suited(.eight, .spades),
.suited(.seven, .spades),
.suited(.six, .spades),
.suited(.five, .spades),
.suited(.four, .spades),
.suited(.three, .spades),
.suited(.ace, .spades),

.suited(.king, .clubs),
.suited(.queen, .clubs),
.suited(.jack, .clubs),
.suited(.ten, .clubs),
.suited(.nine, .clubs),
.suited(.eight, .clubs),
.suited(.seven, .clubs),
.suited(.six, .clubs),
.suited(.five, .clubs),
.suited(.four, .clubs),
.suited(.ace, .clubs),

.suited(.king, .diamonds),
.suited(.queen, .diamonds),
.suited(.jack, .diamonds),
.suited(.ten, .diamonds),
.suited(.nine, .diamonds),
.suited(.eight, .diamonds),
.suited(.seven, .diamonds),
.suited(.six, .diamonds),
.suited(.five, .diamonds),
.suited(.four, .diamonds),
.suited(.three, .diamonds),
.suited(.ace, .diamonds),

.suited(.king, .hearts),
.suited(.queen, .hearts),
.suited(.jack, .hearts),
.suited(.ten, .hearts),
.suited(.nine, .hearts),
.suited(.eight, .hearts),
.suited(.seven, .hearts),
.suited(.six, .hearts),
.suited(.five, .hearts),
.suited(.four, .hearts),
.suited(.three, .hearts),
.suited(.two, .hearts),
.suited(.ace, .hearts),
])
}
}
36 changes: 36 additions & 0 deletions Cards/Tests/HeartsTests/HeartsInitialisationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@testable import Hearts
import XCTest

final class HeartsInitialisationTests: XCTestCase {
func testCreateHandsFourPlayers() {
let sut = Hearts(players: 4)
XCTAssertEqual(sut.players.count, 4)
sut.players.map(\.hand).forEach {
XCTAssertEqual($0.count, 13)
}
}

func testCreateHandsThreePlayers() {
let sut = Hearts(players: 3)
XCTAssertEqual(sut.players.count, 3)
sut.players.map(\.hand).forEach {
XCTAssertEqual($0.count, 17)
}
}

func testCreateHandsFivePlayers() {
let sut = Hearts(players: 5)
XCTAssertEqual(sut.players.count, 5)
sut.players.map(\.hand).forEach {
XCTAssertEqual($0.count, 10)
}
}

func testCreateHandsSixPlayers() {
let sut = Hearts(players: 6)
XCTAssertEqual(sut.players.count, 6)
sut.players.map(\.hand).forEach {
XCTAssertEqual($0.count, 8)
}
}
}
16 changes: 16 additions & 0 deletions Cards/Tests/HeartsTests/HeartsTrickTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import CardsModel
@testable import Hearts
import XCTest

final class HeartsTrickTests: XCTestCase {
func testTrick() {
let sut = Hearts(players: 4)
XCTAssertTrue(sut.trick.isEmpty)

let playerOne = sut.players[0]
let card = playerOne.hand[0]
playerOne.play(card: card)

XCTAssertEqual(sut.trick, [card])
}
}
Loading