Player actions, playable cards
This commit is contained in:
parent
50a35ece03
commit
20d1ce24da
@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
8
Sources/App/Infos/CardInfo.swift
Normal file
8
Sources/App/Infos/CardInfo.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CardInfo: Codable, Equatable {
|
||||||
|
|
||||||
|
let card: CardId
|
||||||
|
|
||||||
|
let playable: Bool
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
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
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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, +)
|
|
||||||
}
|
|
||||||
}
|
|
14
Sources/App/Results/PlayCardResult.swift
Normal file
14
Sources/App/Results/PlayCardResult.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum PlayCardResult {
|
||||||
|
|
||||||
|
case success
|
||||||
|
|
||||||
|
case invalidToken
|
||||||
|
|
||||||
|
case noTableJoined
|
||||||
|
|
||||||
|
case invalidTableState
|
||||||
|
|
||||||
|
case invalidCard
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user