Player actions, playable cards

This commit is contained in:
Christoph Hagen 2021-12-06 11:43:30 +01:00
parent 50a35ece03
commit 20d1ce24da
13 changed files with 675 additions and 447 deletions

View File

@ -8,4 +8,8 @@ extension Array {
} }
return Array(self[index..<count] + self[0..<index]) return Array(self[index..<count] + self[0..<index])
} }
func sorted<T>(by converting: (Element) -> T) -> [Element] where T: Comparable {
sorted { converting($0) < converting($1) }
}
} }

View File

@ -0,0 +1,8 @@
import Foundation
struct CardInfo: Codable, Equatable {
let card: CardId
let playable: Bool
}

View File

@ -76,7 +76,7 @@ final class Database {
- Parameter isPublic: Indicates that this is a game joinable by everyone - Parameter isPublic: Indicates that this is a game joinable by everyone
- Returns: The table id - Returns: The table id
*/ */
func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId { func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo {
tables.createTable(named: name, player: player, isPublic: isPublic) tables.createTable(named: name, player: player, isPublic: isPublic)
} }
@ -105,4 +105,11 @@ final class Database {
} }
return tables.performAction(player: player, action: action) return tables.performAction(player: player, action: action)
} }
func play(card: Card, playerToken: SessionToken) -> PlayCardResult {
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
return .invalidToken
}
return tables.play(card: card, player: player)
}
} }

View File

@ -90,12 +90,12 @@ final class TableManagement: DiskWriter {
- Parameter isPublic: Indicates that this is a game joinable by everyone - Parameter isPublic: Indicates that this is a game joinable by everyone
- Returns: The table id - Returns: The table id
*/ */
func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId { func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo {
let table = Table(newTable: name, isPublic: isPublic) let table = Table(newTable: name, isPublic: isPublic)
_ = table.add(player: name) _ = table.add(player: player)
tables[table.id] = table tables[table.id] = table
writeTableToDisk(table: table) writeTableToDisk(table: table)
return table.id return table.compileInfo(for: player)!
} }
/// A list of all public tables /// A list of all public tables
@ -171,4 +171,11 @@ final class TableManagement: DiskWriter {
} }
return table.perform(action: action, forPlayer: player) return table.perform(action: action, forPlayer: player)
} }
func play(card: Card, player: PlayerName) -> PlayCardResult {
guard let table = currentTable(for: player) else {
return .noTableJoined
}
return table.play(card: card, player: player)
}
} }

View File

@ -1,294 +1,5 @@
import Foundation import Foundation
private let wenzCardOder = [
Card(.eichel, .unter),
Card(.blatt, .unter),
Card(.herz, .unter),
Card(.schelln, .unter),
Card(.herz, .ass),
Card(.herz, .zehn),
Card(.herz, .könig),
Card(.herz, .ober),
Card(.herz, .neun),
Card(.herz, .acht),
Card(.herz, .sieben),
Card(.eichel, .ass),
Card(.eichel, .zehn),
Card(.eichel, .könig),
Card(.eichel, .ober),
Card(.eichel, .neun),
Card(.eichel, .acht),
Card(.eichel, .sieben),
Card(.blatt, .ass),
Card(.blatt, .zehn),
Card(.blatt, .könig),
Card(.blatt, .ober),
Card(.blatt, .neun),
Card(.blatt, .acht),
Card(.blatt, .sieben),
Card(.schelln, .ass),
Card(.schelln, .zehn),
Card(.schelln, .könig),
Card(.schelln, .ober),
Card(.schelln, .neun),
Card(.schelln, .acht),
Card(.schelln, .sieben),
]
private let geierCardOrder = [
Card(.eichel, .ober),
Card(.blatt, .ober),
Card(.herz, .ober),
Card(.schelln, .ober),
Card(.herz, .ass),
Card(.herz, .zehn),
Card(.herz, .könig),
Card(.herz, .unter),
Card(.herz, .neun),
Card(.herz, .acht),
Card(.herz, .sieben),
Card(.eichel, .ass),
Card(.eichel, .zehn),
Card(.eichel, .könig),
Card(.eichel, .unter),
Card(.eichel, .neun),
Card(.eichel, .acht),
Card(.eichel, .sieben),
Card(.blatt, .ass),
Card(.blatt, .zehn),
Card(.blatt, .könig),
Card(.blatt, .unter),
Card(.blatt, .neun),
Card(.blatt, .acht),
Card(.blatt, .sieben),
Card(.schelln, .ass),
Card(.schelln, .zehn),
Card(.schelln, .könig),
Card(.schelln, .unter),
Card(.schelln, .neun),
Card(.schelln, .acht),
Card(.schelln, .sieben),
]
private let eichelCardOrder = [
Card(.eichel, .ober),
Card(.blatt, .ober),
Card(.herz, .ober),
Card(.schelln, .ober),
Card(.eichel, .unter),
Card(.blatt, .unter),
Card(.herz, .unter),
Card(.schelln, .unter),
Card(.eichel, .ass),
Card(.eichel, .zehn),
Card(.eichel, .könig),
Card(.eichel, .neun),
Card(.eichel, .acht),
Card(.eichel, .sieben),
Card(.blatt, .ass),
Card(.blatt, .zehn),
Card(.blatt, .könig),
Card(.blatt, .neun),
Card(.blatt, .acht),
Card(.blatt, .sieben),
Card(.herz, .ass),
Card(.herz, .zehn),
Card(.herz, .könig),
Card(.herz, .neun),
Card(.herz, .acht),
Card(.herz, .sieben),
Card(.schelln, .ass),
Card(.schelln, .zehn),
Card(.schelln, .könig),
Card(.schelln, .neun),
Card(.schelln, .acht),
Card(.schelln, .sieben),
]
private let blattCardOrder = [
Card(.eichel, .ober),
Card(.blatt, .ober),
Card(.herz, .ober),
Card(.schelln, .ober),
Card(.eichel, .unter),
Card(.blatt, .unter),
Card(.herz, .unter),
Card(.schelln, .unter),
Card(.blatt, .ass),
Card(.blatt, .zehn),
Card(.blatt, .könig),
Card(.blatt, .neun),
Card(.blatt, .acht),
Card(.blatt, .sieben),
Card(.eichel, .ass),
Card(.eichel, .zehn),
Card(.eichel, .könig),
Card(.eichel, .neun),
Card(.eichel, .acht),
Card(.eichel, .sieben),
Card(.herz, .ass),
Card(.herz, .zehn),
Card(.herz, .könig),
Card(.herz, .neun),
Card(.herz, .acht),
Card(.herz, .sieben),
Card(.schelln, .ass),
Card(.schelln, .zehn),
Card(.schelln, .könig),
Card(.schelln, .neun),
Card(.schelln, .acht),
Card(.schelln, .sieben),
]
private let schellnCardOrder = [
Card(.eichel, .ober),
Card(.blatt, .ober),
Card(.herz, .ober),
Card(.schelln, .ober),
Card(.eichel, .unter),
Card(.blatt, .unter),
Card(.herz, .unter),
Card(.schelln, .unter),
Card(.schelln, .ass),
Card(.schelln, .zehn),
Card(.schelln, .könig),
Card(.schelln, .neun),
Card(.schelln, .acht),
Card(.schelln, .sieben),
Card(.eichel, .ass),
Card(.eichel, .zehn),
Card(.eichel, .könig),
Card(.eichel, .neun),
Card(.eichel, .acht),
Card(.eichel, .sieben),
Card(.blatt, .ass),
Card(.blatt, .zehn),
Card(.blatt, .könig),
Card(.blatt, .neun),
Card(.blatt, .acht),
Card(.blatt, .sieben),
Card(.herz, .ass),
Card(.herz, .zehn),
Card(.herz, .könig),
Card(.herz, .neun),
Card(.herz, .acht),
Card(.herz, .sieben),
]
private let wenzSortIndex: [Card : Int] = {
wenzCardOder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
}()
private let geierSortIndex: [Card : Int] = {
geierCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
}()
private let eichelSortIndex: [Card : Int] = {
eichelCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
}()
private let blattSortIndex: [Card : Int] = {
blattCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
}()
private let schellnSortIndex: [Card : Int] = {
schellnCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
}()
private let wenzTrumps: [Card] = wenzCardOder[0..<4]
private let geierTrumps: [Card] = geierCardOrder[0..<4]
private let eichelTrumps: [Card] = eichelCardOrder[0..<8]
private let blattTrumps: [Card] = blattCardOrder[0..<8]
private let schellnTrumps: [Card] = schellnCardOrder[0..<8]
extension Card {
func isTrump(in game: GameType) -> Bool {
switch game.sortingType {
case .normal:
trumpsInOrder = normalCardOrder[0..<8]
case .wenz:
trumpsInOrder = wenzCardOder[0..<4]
case .geier:
trumpsInOrder = geierCardOrder[0..<4]
case .soloEichel:
trumpsInOrder = eichelCardOrder[0..<8]
case .soloBlatt:
trumpsInOrder = blattCardOrder[0..<8]
case .soloSchelln:
trumpsInOrder = schellnCardOrder[0..<8]
}
}
}
extension Array where Element == Card {
func sorted(cardOrder order: CardOrder) -> [Card] {
switch order {
case .normal:
return sorted { normalSortIndex[$0]! < normalSortIndex[$1]! }
case .wenz:
return sorted { wenzSortIndex[$0]! < wenzSortIndex[$1]! }
case .geier:
return sorted { geierSortIndex[$0]! < geierSortIndex[$1]! }
case .soloEichel:
return sorted { eichelSortIndex[$0]! < eichelSortIndex[$1]! }
case .soloBlatt:
return sorted { blattSortIndex[$0]! < blattSortIndex[$1]! }
case .soloSchelln:
return sorted { schellnSortIndex[$0]! < schellnSortIndex[$1]! }
}
}
func consecutiveTrumps(for game: GameType) -> Int {
var count = 0
let trumpsInOrder: Array<Card>.SubSequence
switch game.sortingType {
case .normal:
trumpsInOrder = normalCardOrder[0..<8]
case .wenz:
trumpsInOrder = wenzCardOder[0..<4]
case .geier:
trumpsInOrder = geierCardOrder[0..<4]
case .soloEichel:
trumpsInOrder = eichelCardOrder[0..<8]
case .soloBlatt:
trumpsInOrder = blattCardOrder[0..<8]
case .soloSchelln:
trumpsInOrder = schellnCardOrder[0..<8]
}
while contains(trumpsInOrder[count]) {
count += 1
}
guard count >= 3 else {
return 0
}
return count
}
func trumpCount(for game: GameType) -> Int {
}
/**
Split cards into chunks to assign them to players.
- Note: The array must contain a multiple of the `size` parameter
*/
func split(intoChunksOf size: Int) -> [Hand] {
stride(from: 0, to: count, by: size).map { i in
Array(self[i..<i+4])
}
}
var canOfferWedding: Bool {
NormalCardOrder.trumpCount(self) == 1
}
}
struct Dealer { struct Dealer {
/** /**
@ -298,7 +9,7 @@ struct Dealer {
// Select 16 random cards for the first hands // Select 16 random cards for the first hands
Array(Card.allCards.shuffled()[0..<16]) Array(Card.allCards.shuffled()[0..<16])
.split(intoChunksOf: 4) .split(intoChunksOf: 4)
.map { $0.sorted(cardOrder: .normal) } .map { $0.sortedCards(order: NormalCardOrder.self) }
} }

View File

@ -1,70 +0,0 @@
import Foundation
typealias Hand = [Card]
/// The number of tricks ("Stich") per game
let tricksPerGame = 8
struct Game: Codable {
let type: GameType
let numberOfDoubles: Int
/// The player(s) leading the game, i.e. they lose on 60-60
let leaders: [Int]
/// The remaining cards for all players
var cards: [Hand]
/// The total number of consecutive trump cards for one party (starts counting at 3)
let consecutiveTrumps: Int
var lastTrickWinner: Int
var currentActor: Int
/// The finished tricks, with each trick sorted according to the player order of the table
var completedTricks: [Trick]
init(type: GameType, doubles: Int, cards: [Hand], leaders: [Int], starter: Int) {
self.type = type
self.numberOfDoubles = doubles
self.cards = cards
self.leaders = leaders
self.consecutiveTrumps = Array(leaders.map { cards[$0] }.joined())
.consecutiveTrumps(for: type)
self.currentActor = starter
self.lastTrickWinner = starter
self.completedTricks = []
}
var isAtEnd: Bool {
completedTricks.count == tricksPerGame
}
var hasNotStarted: Bool {
!cards.contains { !$0.isEmpty }
}
var pointsOfLeaders: Int {
leaders.map(pointsOfPlayer).reduce(0, +)
}
func pointsOfPlayer(index: Int) -> Int {
completedTricks
.filter { $0.winnerIndex(forGameType: type) == index }
.map { $0.points }
.reduce(0, +)
}
/// The cost of the game, in cents
var cost: Int {
// TODO: Add läufer and schwarz, schneider
type.basicCost * costMultiplier
}
var costMultiplier: Int {
2 ^^ numberOfDoubles
}
}

View File

@ -14,9 +14,15 @@ enum GamePhase: String, Codable {
/// The game negotiation is ongoing /// The game negotiation is ongoing
case bidding = "bidding" case bidding = "bidding"
/// The game must be selected by the player
case selectGame = "select"
/// The game is in progress /// The game is in progress
case playing = "play" case playing = "play"
/// The player must select a card to give to the wedding offerer
case selectWeddingCard = "wedding"
/// The game is over /// The game is over
case gameFinished = "done" case gameFinished = "done"
} }

View File

@ -4,19 +4,24 @@ enum GameType: Codable {
enum GameClass: Int { enum GameClass: Int {
case ruf = 1 case ruf = 1
case hochzeit = 2 case bettel = 2
case bettel = 3 case wenzGeier = 3
case wenzGeier = 4 case solo = 4
case solo = 5
var cost: Int { var cost: Int {
switch self { switch self {
case .ruf: return 5 case .ruf: return 5
case .hochzeit: return 10
case .bettel: return 15 case .bettel: return 15
case .wenzGeier, .solo: return 20 case .wenzGeier, .solo: return 20
} }
} }
mutating func increase() {
guard self != .solo else {
return
}
self = .init(rawValue: rawValue + 1)!
}
} }
case rufEichel case rufEichel
@ -33,10 +38,8 @@ enum GameType: Codable {
var gameClass: GameClass { var gameClass: GameClass {
switch self { switch self {
case .rufEichel, .rufBlatt, .rufSchelln: case .rufEichel, .rufBlatt, .rufSchelln, .hochzeit:
return .ruf return .ruf
case .hochzeit:
return .hochzeit
case .bettel: case .bettel:
return .bettel return .bettel
case .wenz, .geier: case .wenz, .geier:
@ -46,6 +49,28 @@ enum GameType: Codable {
} }
} }
var isCall: Bool {
switch self {
case .rufEichel, .rufBlatt, .rufSchelln:
return true
default:
return false
}
}
var calledSuit: Card.Suit? {
switch self {
case .rufEichel:
return .eichel
case .rufBlatt:
return .blatt
case .rufSchelln:
return .schelln
default:
return nil
}
}
var isSingleGame: Bool { var isSingleGame: Bool {
switch self { switch self {
case .rufEichel, .rufBlatt, .rufSchelln, .hochzeit: case .rufEichel, .rufBlatt, .rufSchelln, .hochzeit:

View File

@ -4,6 +4,15 @@ import CloudKit
private let encoder = JSONEncoder() 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 { final class Player {
let name: PlayerName let name: PlayerName
@ -46,6 +55,14 @@ final class Player {
var socket: WebSocket? = nil var socket: WebSocket? = nil
var canOfferWedding: Bool {
rawCards.canOfferWedding
}
var offersWedding = false
var wouldAcceptWedding = false
init(name: PlayerName) { init(name: PlayerName) {
self.name = name self.name = name
} }
@ -54,6 +71,22 @@ final class Player {
handCards.map { $0.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
}
func connect(using socket: WebSocket) { func connect(using socket: WebSocket) {
_ = self.socket?.close() _ = self.socket?.close()
self.socket = socket self.socket = socket
@ -76,68 +109,285 @@ final class Player {
return true return true
} }
func canPerform(_ action: Action) -> Bool {
actions.contains(action)
}
func prepareForFirstGame(isFirstPlayer: Bool) { func prepareForFirstGame(isFirstPlayer: Bool) {
playsFirstCard = isFirstPlayer playsFirstCard = isFirstPlayer
isNextActor = isFirstPlayer isNextActor = isFirstPlayer
selectsGame = false // Not relevant in this phase
startedCurrentTrick = isFirstPlayer startedCurrentTrick = isFirstPlayer
actions = [.deal] actions = [.deal]
didDoubleAfterFourCards = nil // Not relevant in this phase
isStillBidding = false // Not relevant in this phase
isGameLeader = false // Not relevant in this phase
numberOfRaises = 0 // Not relevant in this phase
handCards = [] handCards = []
playedCard = nil playedCard = nil
wonTricks = [] wonTricks = []
} }
func assignFirstCards(_ cards: Hand) { func assignFirstCards(_ cards: Hand) {
selectsGame = false // Not relevant in this phase
actions = [.initialDoubleCost, .noDoubleCost] actions = [.initialDoubleCost, .noDoubleCost]
didDoubleAfterFourCards = nil
isStillBidding = false // Not relevant in this phase
isGameLeader = false // Not relevant in this phase
numberOfRaises = 0 // Not relevant in this phase
handCards = cards.map { .init(card: $0, isPlayable: false) } handCards = cards.map { .init(card: $0, isPlayable: false) }
playedCard = nil
wonTricks = []
} }
func didDouble(_ double: Bool) { func didDouble(_ double: Bool) {
selectsGame = false // Not relevant in this phase
actions = [] actions = []
didDoubleAfterFourCards = double didDoubleAfterFourCards = double
isStillBidding = false // Not relevant in this phase
isGameLeader = false // Not relevant in this phase
numberOfRaises = 0 // Not relevant in this phase
playedCard = nil
wonTricks = []
} }
func assignRemainingCards(_ cards: Hand) { func assignRemainingCards(_ cards: Hand) {
isStillBidding = true
isGameLeader = false
numberOfRaises = 0
handCards = (rawCards + cards) handCards = (rawCards + cards)
.sorted(CardOrderType: .normal) .sortedCards(order: NormalCardOrder.self)
.map { .init(card: $0, isPlayable: false) } .map { .init(card: $0, isPlayable: false) }
playedCard = nil
wonTricks = []
} }
func startAuction() { func startAuction() {
selectsGame = false // Not relevant in this phase
if playsFirstCard { if playsFirstCard {
actions = [.withdrawFromAuction, .increaseOrMatchGame] actions = [.withdrawFromAuction, .increaseOrMatchGame]
} else { } else {
actions = []
}
if canOfferWedding {
actions.append(.offerWedding)
}
}
func offerWedding() {
offersWedding = true
actions = []
}
func weddingOfferExists() {
guard isStillBidding else {
return
}
actions = [.increaseOrMatchGame, .withdrawFromAuction]
}
func hasWeddingOffer() {
guard isStillBidding else {
return
}
actions = [.acceptWedding, .increaseOrMatchGame, .withdrawFromAuction]
}
func weddingOutbid() {
isNextActor = false
guard isStillBidding else {
return
} }
actions = [] actions = []
isStillBidding = true if offersWedding {
isGameLeader = false // Not relevant in this phase offersWedding = false
numberOfRaises = 0 // Not relevant in this phase isStillBidding = false
}
}
func didPerformBid() {
isNextActor = false
actions = []
}
func requiresBid() {
isNextActor = true
actions = [.increaseOrMatchGame, .withdrawFromAuction]
}
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 mustSelectGame() {
isNextActor = 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 gameStarts() {
isNextActor = playsFirstCard
startedCurrentTrick = playsFirstCard
actions = [.doubleDuringGame]
isGameLeader = false
}
func switchLeadership() {
isGameLeader.toggle()
if isGameLeader {
actions = actions.filter { $0 != .doubleDuringGame }
} else if !actions.contains(.doubleDuringGame) {
actions.append(.doubleDuringGame)
}
}
func withdrawFromBidding() {
isStillBidding = false
actions = []
}
func didFinishTrick(canDoubleInNextRound: Bool) {
isNextActor = false
playedCard = nil playedCard = nil
wonTricks = [] if canDoubleInNextRound, !isGameLeader {
actions = [.doubleDuringGame]
}
}
func didWin(trick: Trick) {
self.wonTricks.append(trick)
self.isNextActor = true
}
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 first = trick.first else {
setPlayableCardsForStarter(game: game)
return
}
let sorter = game.sortingType
guard sorter.isTrump(first) else {
setPlayableCardsFollowing(suit: first.suit, game: game)
return
}
guard !sorter.hasTrump(in: cards) else {
// Must follow with trump
handCards = cards.map {
.init(card: $0, isPlayable: sorter.isTrump($0))
}
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)
}
}
private func setPlayableCardsFollowing(suit: Card.Suit, game: GameType) {
let cards = rawCards
let sorter = game.sortingType
// No calling game, allow all cards of same suit
let suitCards = sorter.cards(with: suit, in: cards)
func followSuit() {
handCards = cards.map {
.init(card: $0, isPlayable: !sorter.isTrump($0) && $0.suit == suit)
}
}
guard let called = game.calledSuit else {
if suitCards.isEmpty {
// Can play any card
setAllCards(playable: true)
} else {
// Must follow suit
followSuit()
}
return
}
let ace = Card(called, .ass)
guard called == suit else {
if suitCards.isEmpty {
// Exclude called ace, all others allowed
handCards = cards.map {
.init(card: $0, isPlayable: $0 != ace)
}
} else {
// Must follow suit (called ace automatically excluded)
followSuit()
}
return
}
// The called suit is player, 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) }
}
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) -> PlayerInfo { func info(masked: Bool) -> PlayerInfo {
@ -193,3 +443,10 @@ extension Player {
} }
} }
} }
extension Player: Equatable {
static func == (lhs: Player, rhs: Player) -> Bool {
lhs.name == rhs.name
}
}

View File

@ -22,10 +22,7 @@ final class Table {
var gameType: GameType? = nil var gameType: GameType? = nil
var minimumPlayableGame: GameType.GameClass = .ruf var minimumPlayableGame: GameType.GameClass?
/// Indicates if doubles are still allowed
var canDoubleDuringGame = false
/// Indicates if any player doubled during the current round, extending it to the next round /// Indicates if any player doubled during the current round, extending it to the next round
var didDoubleInCurrentRound = false var didDoubleInCurrentRound = false
@ -35,6 +32,42 @@ final class Table {
!players.contains { $0.didDoubleAfterFourCards == nil } !players.contains { $0.didDoubleAfterFourCards == nil }
} }
var weddingOfferExists: Bool {
players.contains { $0.offersWedding }
}
var weddingAcceptExists: Bool {
players.contains { $0.wouldAcceptWedding }
}
var hasAuctionWinner: Bool {
numberOfRemainingBidders == 1
}
var numberOfRemainingBidders: Int {
players.filter { $0.isStillBidding }.count
}
var auctionWinner: Player {
players.first { $0.isStillBidding }!
}
var hasCompletedTrick: Bool {
!players.contains { $0.playedCard == nil }
}
var completedTrick: Trick? {
let trick = players.compactMap { $0.playedCard }
guard trick.count == maximumPlayersPerTable else {
return nil
}
return trick
}
var currentTrick: [Card] {
players.compactMap { $0.playedCard }
}
init(id: TableId, name: TableName, isPublic: Bool) { init(id: TableId, name: TableName, isPublic: Bool) {
self.id = id self.id = id
self.name = name self.name = name
@ -64,6 +97,11 @@ final class Table {
players.contains { $0.name == player } players.contains { $0.name == player }
} }
/// The player to play the first card of the current game
var firstPlayer: Player {
players.first { $0.playsFirstCard }!
}
func select(player: PlayerName) -> Player? { func select(player: PlayerName) -> Player? {
players.first { $0.name == player } players.first { $0.name == player }
} }
@ -87,6 +125,29 @@ final class Table {
return players[index] return players[index]
} }
func index(of player: Player) -> Int {
players.firstIndex(of: player)!
}
func nextPlayer(after player: Player) -> Player {
let i = index(of: player)
let newIndex = (i + 1) % maximumPlayersPerTable
return players[newIndex]
}
func nextBidder(after player: Player) -> Player {
// Find next player to place bid
let index = index(of: player)
for i in 1..<4 {
let player = players[(index + i) % 4]
guard player.isStillBidding else {
continue
}
return player
}
return player
}
func remove(player: PlayerName) { func remove(player: PlayerName) {
guard contains(player: player) else { guard contains(player: player) else {
return return
@ -116,11 +177,10 @@ final class Table {
} }
private func prepareTableForFirstGame() { private func prepareTableForFirstGame() {
self.phase = .waitingForPlayers phase = .waitingForPlayers
self.gameType = nil gameType = nil
self.minimumPlayableGame = .ruf // Not relevant in this phase minimumPlayableGame = nil // Not relevant in this phase
self.canDoubleDuringGame = true // Not relevant in this phase didDoubleInCurrentRound = false // Not relevant in this phase
self.didDoubleInCurrentRound = false // Not relevant in this phase
let index = players.firstIndex { $0.playsFirstCard } ?? 0 let index = players.firstIndex { $0.playsFirstCard } ?? 0
for i in 0..<maximumPlayersPerTable { for i in 0..<maximumPlayersPerTable {
players[i].prepareForFirstGame(isFirstPlayer: i == index) players[i].prepareForFirstGame(isFirstPlayer: i == index)
@ -139,7 +199,49 @@ final class Table {
// MARK: Player actions // MARK: Player actions
func play(card: Card, player name: PlayerName) -> PlayCardResult {
let player = select(player: name)!
if phase == .selectWeddingCard {
return selectedCardForWedding(card: card, player: player)
}
guard let game = gameType,
player.hasPlayable(card: card) else {
return .invalidCard
}
player.play(card: card)
if let completedTrick = completedTrick {
didFinish(trick: completedTrick, in: game)
} else {
let next = nextPlayer(after: player)
next.isNextActor = true
player.isNextActor = false
}
updatePlayableCards()
return .success
}
private func updatePlayableCards() {
let playedCards = currentTrick
players.forEach {
$0.setPlayableCards(forCurrentTrick: playedCards, in: gameType)
}
}
func didFinish(trick: Trick, in game: GameType) {
// If trick is completed, calculate winner
let index = trick.highCardIndex(forGame: game)
players.forEach {
$0.didFinishTrick(canDoubleInNextRound: didDoubleInCurrentRound)
}
players[index].didWin(trick: trick)
didDoubleInCurrentRound = false
}
func perform(action: Player.Action, forPlayer player: PlayerName) -> PlayerActionResult { func perform(action: Player.Action, forPlayer player: PlayerName) -> PlayerActionResult {
let player = select(player: player)!
guard player.canPerform(action) else {
return .tableStateInvalid
}
defer { sendUpdateToAllPlayers() } defer { sendUpdateToAllPlayers() }
switch action { switch action {
case .deal: case .deal:
@ -149,15 +251,15 @@ final class Table {
case .noDoubleCost: case .noDoubleCost:
return perform(double: false, forPlayer: player) return perform(double: false, forPlayer: player)
case .offerWedding: case .offerWedding:
fatalError() return performWeddingCall(forPlayer: player)
case .acceptWedding: case .acceptWedding:
fatalError() return handleWeddingAccept(forPlayer: player)
case .increaseOrMatchGame: case .increaseOrMatchGame:
fatalError() return performBidIncrease(forPlayer: player)
case .withdrawFromAuction: case .withdrawFromAuction:
fatalError() return performWithdrawl(forPlayer: player)
case .doubleDuringGame: case .doubleDuringGame:
fatalError() return performDoubleDuringGame(forPlayer: player)
} }
} }
@ -169,19 +271,16 @@ final class Table {
return .tableStateInvalid return .tableStateInvalid
} }
phase = .collectingDoubles
gameType = nil
minimumPlayableGame = .ruf
let cards = Dealer.dealFirstCards() let cards = Dealer.dealFirstCards()
for (index, player) in players.enumerated() { for (index, player) in players.enumerated() {
player.assignFirstCards(cards[index]) player.assignFirstCards(cards[index])
} }
phase = .collectingDoubles
gameType = nil
return .success return .success
} }
func perform(double: Bool, forPlayer name: PlayerName) -> PlayerActionResult { func perform(double: Bool, forPlayer player: Player) -> PlayerActionResult {
let player = select(player: player)!
player.didDouble(double) player.didDouble(double)
if allPlayersFinishedDoubling { if allPlayersFinishedDoubling {
dealAdditionalCards() dealAdditionalCards()
@ -194,18 +293,173 @@ final class Table {
for (index, player) in players.enumerated() { for (index, player) in players.enumerated() {
player.assignRemainingCards(cards[index]) player.assignRemainingCards(cards[index])
} }
players.forEach { $0.startAuction() }
minimumPlayableGame = nil
}
private func performWeddingCall(forPlayer player: Player) -> PlayerActionResult {
guard phase == .bidding else {
return .tableStateInvalid
}
guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else {
return .tableStateInvalid
}
guard player.offersWedding else {
return .tableStateInvalid
}
guard !weddingOfferExists else {
// Only one wedding allowed at the table
return .tableStateInvalid
}
// Only allow wedding acceptance or outbidding
players.forEach { $0.weddingOfferExists() }
player.offerWedding()
firstPlayer.hasWeddingOffer()
minimumPlayableGame = .bettel
return .success return .success
} }
private func startAuction() { private func performBidIncrease(forPlayer player: Player) -> PlayerActionResult {
players.forEach { $0.startAuction() } guard phase == .bidding else {
minimumPlayableGame = .ruf return .tableStateInvalid
}
if weddingOfferExists {
// Anyone except the offerer can outbid a wedding
return handleWeddingOutbid(forPlayer: player)
}
guard player.isNextActor else {
return .tableStateInvalid
}
if minimumPlayableGame == nil {
minimumPlayableGame = .ruf
} else {
minimumPlayableGame!.increase()
}
player.didPerformBid()
// Find next player to place bid
nextBidder(after: player).requiresBid()
return .success
}
private func handleWeddingOutbid(forPlayer player: Player) -> PlayerActionResult {
if player.offersWedding {
// A player offering a wedding can't outbid itself
return .tableStateInvalid
}
players.forEach { $0.weddingOutbid() }
firstPlayer.requiresBid()
return .success
}
private func handleWeddingAccept(forPlayer player: Player) -> PlayerActionResult {
guard phase == .bidding else {
return .tableStateInvalid
}
guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else {
return .tableStateInvalid
}
guard weddingOfferExists else {
return .tableStateInvalid
}
guard player.isNextActor else {
return .tableStateInvalid
}
guard !player.offersWedding else {
return .tableStateInvalid
}
guard !weddingAcceptExists else {
return .tableStateInvalid
}
if hasAuctionWinner {
selectedWedding(player: player)
return .success
}
minimumPlayableGame = .bettel
players.forEach {
$0.weddingAccepted()
}
player.acceptWedding()
nextBidder(after: player).requiresBid()
return .success
}
private func selectedWedding(player: Player) {
minimumPlayableGame = nil
gameType = .hochzeit
phase = .selectWeddingCard
players.forEach { $0.auctionEnded() }
player.mustSelectWeddingCard()
}
private func selectedCardForWedding(card: Card, player: Player) -> PlayCardResult {
guard player.isNextActor,
player.wouldAcceptWedding,
weddingOfferExists else {
return .invalidTableState
}
guard !card.isTrump(in: .hochzeit),
player.has(card: card) else {
return .invalidCard
}
// Swap the cards
let offerer = players.first { $0.offersWedding }!
let offeredCard = offerer.replaceWeddingCard(with: card)
player.replace(card: card, with: offeredCard)
// Start the game
gameType = .hochzeit
players.forEach { $0.gameStarts() }
player.switchLeadership()
offerer.switchLeadership()
return .success
}
private func performWithdrawl(forPlayer player: Player) -> PlayerActionResult {
guard phase == .bidding,
player.isNextActor,
player.isStillBidding else {
return .tableStateInvalid
}
player.withdrawFromBidding()
switch numberOfRemainingBidders {
case 1:
selectGame(player: auctionWinner)
case 0:
// All players withdrawn, deal new cards
let first = firstPlayer
let newPlayer = self.nextBidder(after: first)
first.playsFirstCard = false
newPlayer.playsFirstCard = true
prepareTableForFirstGame()
return dealInitialCards()
default:
break
}
return .success
}
private func selectGame(player: Player) {
minimumPlayableGame = nil
gameType = nil
phase = .selectGame
players.forEach { $0.auctionEnded() }
player.mustSelectGame()
}
private func performDoubleDuringGame(forPlayer player: Player) -> PlayerActionResult {
guard phase == .playing,
!player.isGameLeader else {
return .tableStateInvalid
}
player.numberOfRaises += 1
players.forEach { $0.switchLeadership() }
return .success
} }
private func reset() { private func reset() {
phase = .waitingForPlayers phase = .waitingForPlayers
gameType = nil gameType = nil
minimumPlayableGame = .ruf minimumPlayableGame = nil
} }
} }

View File

@ -1,16 +0,0 @@
import Foundation
typealias Trick = [Card]
extension Trick {
func winnerIndex(forGameType type: GameType) -> Int {
let highCard = sorted(cardOrder: type.sortingType).first!
return firstIndex(of: highCard)!
}
var points: Int {
map { $0.points }
.reduce(0, +)
}
}

View File

@ -0,0 +1,14 @@
import Foundation
enum PlayCardResult {
case success
case invalidToken
case noTableJoined
case invalidTableState
case invalidCard
}

View File

@ -203,8 +203,8 @@ func routes(_ app: Application) throws {
guard let player = database.registeredPlayerExists(withSessionToken: token) else { guard let player = database.registeredPlayerExists(withSessionToken: token) else {
throw Abort(.unauthorized) // 401 throw Abort(.unauthorized) // 401
} }
let tableId = database.createTable(named: tableName, player: player, isPublic: isPublic) let table = database.createTable(named: tableName, player: player, isPublic: isPublic)
return tableId return try encodeJSON(table)
} }
/** /**
@ -293,4 +293,25 @@ func routes(_ app: Application) throws {
throw Abort(.preconditionFailed) // 412 throw Abort(.preconditionFailed) // 412
} }
} }
app.post("player", "card", ":card") { req -> String in
guard let token = req.body.string,
let cardId = req.parameters.get("card"),
let card = Card(id: cardId) else {
throw Abort(.badRequest)
}
switch database.play(card: card, playerToken: token) {
case .success:
return ""
case .invalidToken:
throw Abort(.unauthorized) // 401
case .noTableJoined:
throw Abort(.preconditionFailed) // 412
case .invalidTableState:
throw Abort(.preconditionFailed) // 412
case .invalidCard:
throw Abort(.preconditionFailed) // 412
}
}
} }