477 lines
13 KiB
Swift
477 lines
13 KiB
Swift
import Foundation
|
|
import WebSocketKit
|
|
import CloudKit
|
|
|
|
private let encoder = JSONEncoder()
|
|
|
|
/**
|
|
Specifies the number of cards of the called suit that a player must have
|
|
to be allowed to play any card of the suit instead of having to play the ace.
|
|
*/
|
|
private let numberOfCardsToProtectAce = 4
|
|
|
|
let numberOfCardsPerPlayer = 8
|
|
|
|
|
|
final class Player {
|
|
|
|
let name: PlayerName
|
|
|
|
/// The player is the first to play a card in a new game
|
|
var playsFirstCard = false
|
|
|
|
/// The player is the next to perform an action (e.g. play a card)
|
|
var isNextActor = false
|
|
|
|
/// The player must select the game to play after winning the auction
|
|
var selectsGame = false
|
|
|
|
/// The players plays/played the first card for the current trick
|
|
var startedCurrentTrick = false
|
|
|
|
/// The action available to the player
|
|
var actions: [Action] = []
|
|
|
|
/// Indicates if the player doubled ("legen")
|
|
var didDoubleAfterFourCards: Bool? = nil
|
|
|
|
/// Indicates if the player is still involved in the bidding process
|
|
var isStillBidding = true
|
|
|
|
/// Indicates that the player leads the game ("Spieler")
|
|
var isGameLeader = false
|
|
|
|
/// Indicates the number of raises ("Schuss") of the player
|
|
var numberOfRaises = 0
|
|
|
|
/// The remaining cards of the player
|
|
var handCards: [PlayableCard] = []
|
|
|
|
/// The card played for the current trick
|
|
var playedCard: Card? = nil
|
|
|
|
/// All tricks won by the player in this game
|
|
var wonTricks: [Trick] = []
|
|
|
|
var socket: WebSocket? = nil
|
|
|
|
var canOfferWedding: Bool {
|
|
rawCards.canOfferWedding
|
|
}
|
|
|
|
var offersWedding = false
|
|
|
|
var wouldAcceptWedding = false
|
|
|
|
init(name: PlayerName) {
|
|
self.name = name
|
|
}
|
|
|
|
var rawCards: [Card] {
|
|
handCards.map { $0.card }
|
|
}
|
|
|
|
func has(card: Card) -> Bool {
|
|
handCards.contains { $0.card == card }
|
|
}
|
|
|
|
func hasPlayable(card: Card) -> Bool {
|
|
handCards.contains { $0.card == card && $0.isPlayable }
|
|
}
|
|
|
|
func remove(card: Card) {
|
|
handCards = handCards.filter { $0.card != card }
|
|
}
|
|
|
|
func play(card: Card) {
|
|
remove(card: card)
|
|
playedCard = card
|
|
actions = actions.filter { $0 != .doubleDuringGame }
|
|
}
|
|
|
|
func connect(using socket: WebSocket) {
|
|
_ = self.socket?.close()
|
|
self.socket = socket
|
|
}
|
|
|
|
func send(_ info: TableInfo) {
|
|
try? socket?.send(encodeJSON(info))
|
|
}
|
|
|
|
func disconnect() -> Bool {
|
|
guard let socket = socket else {
|
|
return false
|
|
}
|
|
do {
|
|
try socket.close().wait()
|
|
} catch {
|
|
print("Failed to close socket for player: \(name): \(error)")
|
|
}
|
|
self.socket = nil
|
|
return true
|
|
}
|
|
|
|
func canPerform(_ action: Action) -> Bool {
|
|
actions.contains(action)
|
|
}
|
|
|
|
func prepareForNewGame(isFirstPlayer: Bool) {
|
|
playsFirstCard = isFirstPlayer
|
|
isNextActor = isFirstPlayer
|
|
selectsGame = false
|
|
startedCurrentTrick = isFirstPlayer
|
|
actions = [.deal]
|
|
didDoubleAfterFourCards = nil
|
|
isStillBidding = true
|
|
isGameLeader = false
|
|
handCards = []
|
|
playedCard = nil
|
|
wonTricks = []
|
|
}
|
|
|
|
func assignFirstCards(_ cards: Hand) {
|
|
actions = [.initialDoubleCost, .noDoubleCost]
|
|
handCards = cards.map { .init(card: $0, isPlayable: false) }
|
|
}
|
|
|
|
func didDouble(_ double: Bool) {
|
|
actions = []
|
|
didDoubleAfterFourCards = double
|
|
}
|
|
|
|
func assignRemainingCards(_ cards: Hand) {
|
|
handCards = (rawCards + cards)
|
|
.sortedCards(order: NormalCardOrder.self)
|
|
.map { .init(card: $0, isPlayable: false) }
|
|
}
|
|
|
|
func startAuction() {
|
|
if playsFirstCard {
|
|
actions = [.withdrawFromAuction, .increaseOrMatchGame]
|
|
} else {
|
|
actions = []
|
|
}
|
|
if canOfferWedding {
|
|
actions.append(.offerWedding)
|
|
}
|
|
}
|
|
|
|
func offerWedding() {
|
|
offersWedding = true
|
|
isStillBidding = false
|
|
actions = []
|
|
}
|
|
|
|
func weddingOfferExists() {
|
|
guard isStillBidding else {
|
|
return
|
|
}
|
|
actions = [.increaseOrMatchGame, .withdrawFromAuction]
|
|
}
|
|
|
|
func hasWeddingOffer() {
|
|
guard isStillBidding else {
|
|
return
|
|
}
|
|
actions = [.acceptWedding, .increaseOrMatchGame, .withdrawFromAuction]
|
|
}
|
|
|
|
func weddingOutbid() {
|
|
isNextActor = false
|
|
actions = []
|
|
offersWedding = false
|
|
}
|
|
|
|
func didPerformBid() {
|
|
isNextActor = false
|
|
actions = []
|
|
}
|
|
|
|
func requiresBid(hasWedding: Bool) {
|
|
isNextActor = true
|
|
actions = [.increaseOrMatchGame, .withdrawFromAuction]
|
|
if hasWedding {
|
|
actions.append(.acceptWedding)
|
|
}
|
|
}
|
|
|
|
func acceptWedding() {
|
|
wouldAcceptWedding = true
|
|
actions = []
|
|
}
|
|
|
|
func weddingAccepted() {
|
|
guard isStillBidding else {
|
|
actions = []
|
|
return
|
|
}
|
|
actions = [.increaseOrMatchGame, .withdrawFromAuction]
|
|
}
|
|
|
|
func auctionEnded() {
|
|
actions = []
|
|
isStillBidding = false
|
|
isNextActor = false
|
|
}
|
|
|
|
func mustSelectWeddingCard() {
|
|
isNextActor = true
|
|
// Only cards which are not trump can be given to the other player
|
|
handCards = handCards.map {
|
|
let card = $0.card
|
|
return .init(card: card, isPlayable: !card.isTrump(in: .hochzeit))
|
|
}
|
|
// Hochzeit costs double
|
|
numberOfRaises += 1
|
|
}
|
|
|
|
func canPlay(game: GameType) -> Bool {
|
|
guard let suit = game.calledSuit else {
|
|
if game == .hochzeit {
|
|
return canOfferWedding
|
|
}
|
|
return true
|
|
}
|
|
let sorter = game.sortingType
|
|
let cards = rawCards
|
|
guard sorter.hasCardToCall(suit, in: cards) else {
|
|
// Player needs at least one card of the called suit
|
|
return false
|
|
}
|
|
let ace = Card(suit, .ass)
|
|
return !cards.contains(ace)
|
|
}
|
|
|
|
func mustSelectGame() {
|
|
isNextActor = true
|
|
selectsGame = true
|
|
}
|
|
|
|
func replace(card: Card, with other: Card) {
|
|
remove(card: card)
|
|
handCards.append(.init(card: other, isPlayable: false))
|
|
}
|
|
|
|
func replaceWeddingCard(with card: Card) -> Card {
|
|
let index = handCards.firstIndex { $0.card.isTrump(in: .hochzeit) }!
|
|
let removed = handCards.remove(at: index).card
|
|
handCards.append(.init(card: card, isPlayable: false))
|
|
return removed
|
|
}
|
|
|
|
func start(game: GameType) {
|
|
isNextActor = playsFirstCard
|
|
startedCurrentTrick = playsFirstCard
|
|
selectsGame = false
|
|
actions = [.doubleDuringGame]
|
|
isGameLeader = false
|
|
handCards = game.sortingType.sort(rawCards).map { .init(card: $0, isPlayable: false) }
|
|
if playsFirstCard {
|
|
setPlayableCardsForStarter(game: game)
|
|
}
|
|
}
|
|
|
|
func switchLeadership() {
|
|
isGameLeader.toggle()
|
|
if isGameLeader {
|
|
actions = actions.filter { $0 != .doubleDuringGame }
|
|
} else if !actions.contains(.doubleDuringGame) {
|
|
actions.append(.doubleDuringGame)
|
|
}
|
|
}
|
|
|
|
func withdrawFromBidding() {
|
|
isNextActor = false
|
|
isStillBidding = false
|
|
actions = []
|
|
}
|
|
|
|
func didFinish(trick: Trick, winner: Bool, canDoubleInNextRound: Bool) {
|
|
isNextActor = winner
|
|
//startedCurrentTrick = winner
|
|
if winner {
|
|
wonTricks.append(trick)
|
|
}
|
|
if canDoubleInNextRound, !isGameLeader {
|
|
actions = [.doubleDuringGame]
|
|
} else {
|
|
actions = []
|
|
}
|
|
}
|
|
func didFinishGame() {
|
|
actions = [.deal]
|
|
}
|
|
|
|
func clearLastTrick() {
|
|
playedCard = nil
|
|
// This flag is not set until the last trick is cleared, because
|
|
// it would mess up the stacking of the cards on the table
|
|
// which relies on this property
|
|
startedCurrentTrick = isNextActor
|
|
}
|
|
|
|
|
|
func setPlayableCards(forCurrentTrick trick: Trick, in game: GameType?) {
|
|
guard let game = game, isNextActor else {
|
|
for i in 0..<handCards.count {
|
|
handCards[i].isPlayable = false
|
|
}
|
|
return
|
|
}
|
|
let cards = rawCards
|
|
guard cards.count > 1 else {
|
|
// Last card can always be played
|
|
setAllCards(playable: true)
|
|
return
|
|
}
|
|
guard let firstCard = trick.first else {
|
|
setPlayableCardsForStarter(game: game)
|
|
return
|
|
}
|
|
|
|
let sorter = game.sortingType
|
|
|
|
guard sorter.isTrump(firstCard) else {
|
|
setPlayableCardsFollowing(suit: firstCard.suit, game: game)
|
|
return
|
|
}
|
|
guard !sorter.hasTrump(in: cards) else {
|
|
// Must follow with trump
|
|
handCards = cards.map {
|
|
.init(card: $0, isPlayable: sorter.isTrump($0))
|
|
}
|
|
if !handCards.contains(where: { $0.isPlayable }) {
|
|
print("No cards to play when having to follow trump")
|
|
}
|
|
return
|
|
}
|
|
// Can play any card if not in calling game
|
|
guard let suit = game.calledSuit else {
|
|
setAllCards(playable: true)
|
|
return
|
|
}
|
|
// Can play any card, except the called ace
|
|
let ace = Card(suit, .ass)
|
|
handCards = cards.map {
|
|
.init(card: $0, isPlayable: $0 != ace)
|
|
}
|
|
if !handCards.contains(where: { $0.isPlayable }) {
|
|
print("No cards to play when not having to follow trump in a called game")
|
|
}
|
|
}
|
|
|
|
private func setPlayableCardsFollowing(suit playedSuit: Card.Suit, game: GameType) {
|
|
let cards = rawCards
|
|
let sorter = game.sortingType
|
|
let suitCards = sorter.cards(with: playedSuit, in: cards)
|
|
|
|
func followSuit() {
|
|
handCards = cards.map {
|
|
.init(card: $0, isPlayable: !sorter.isTrump($0) && $0.suit == playedSuit)
|
|
}
|
|
if !handCards.contains(where: { $0.isPlayable }) {
|
|
print("No cards to play when following suit")
|
|
}
|
|
}
|
|
|
|
guard let calledSuit = game.calledSuit else {
|
|
if suitCards.isEmpty {
|
|
// Can play any card
|
|
setAllCards(playable: true)
|
|
} else {
|
|
// Must follow suit
|
|
followSuit()
|
|
}
|
|
return
|
|
}
|
|
print("Has called suit \(calledSuit)")
|
|
let ace = Card(calledSuit, .ass)
|
|
guard !suitCards.isEmpty else {
|
|
// Exclude called ace, all others allowed
|
|
handCards = cards.map {
|
|
.init(card: $0, isPlayable: $0 != ace)
|
|
}
|
|
if !handCards.contains(where: { $0.isPlayable }) {
|
|
print("No cards to play when following called suit without suit cards")
|
|
}
|
|
return
|
|
}
|
|
guard calledSuit == playedSuit else {
|
|
print("Following uncalled suit since no suitable cards")
|
|
// Must follow suit (called ace not present)
|
|
followSuit()
|
|
return
|
|
}
|
|
|
|
// The called suit is played, must commit ace
|
|
guard cards.contains(ace) else {
|
|
// Must follow suit
|
|
followSuit()
|
|
return
|
|
}
|
|
// Must play ace
|
|
handCards = cards.map { .init(card: $0, isPlayable: $0 == ace) }
|
|
if !handCards.contains(where: { $0.isPlayable }) {
|
|
print("No cards to play when having to play ace of called suit")
|
|
}
|
|
}
|
|
|
|
private func setPlayableCardsForStarter(game: GameType) {
|
|
guard let suit = game.calledSuit else {
|
|
setAllCards(playable: true)
|
|
return
|
|
}
|
|
let cards = rawCards
|
|
let ace = Card(suit, .ass)
|
|
// Check if called ace exists, to prohibit other cards of the same suit
|
|
guard cards.contains(ace) else {
|
|
setAllCards(playable: true)
|
|
return
|
|
}
|
|
// Jodeln
|
|
if cards.count == numberOfCardsPerPlayer,
|
|
cards.suitCount(suit, in: game) >= numberOfCardsToProtectAce {
|
|
setAllCards(playable: true)
|
|
return
|
|
}
|
|
|
|
// Only ace allowed for the called suit
|
|
handCards = cards.map { card in
|
|
let notPlayable = card.suit == suit && !card.symbol.isTrumpOrAce
|
|
return PlayableCard(card: card, isPlayable: !notPlayable)
|
|
}
|
|
}
|
|
|
|
private func setAllCards(playable: Bool) {
|
|
for i in 0..<handCards.count {
|
|
handCards[i].isPlayable = playable
|
|
}
|
|
}
|
|
|
|
func info(masked: Bool, positionInTrick: Int) -> PlayerInfo {
|
|
.init(player: self, isMasked: masked, trickPosition: positionInTrick)
|
|
}
|
|
}
|
|
|
|
extension Player {
|
|
|
|
/// Indicate that the player is connected when at a table
|
|
var isConnected: Bool {
|
|
guard let socket = socket else {
|
|
return false
|
|
}
|
|
guard !socket.isClosed else {
|
|
self.socket = nil
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
extension Player: Equatable {
|
|
|
|
static func == (lhs: Player, rhs: Player) -> Bool {
|
|
lhs.name == rhs.name
|
|
}
|
|
}
|