Play cards, select game, player actions
This commit is contained in:
parent
ca7fc858c2
commit
fa3aaadef8
@ -9,16 +9,26 @@ struct PlayerInfo: Codable, Equatable {
|
|||||||
/// The player is the next one to perform an action
|
/// The player is the next one to perform an action
|
||||||
let active: Bool
|
let active: Bool
|
||||||
|
|
||||||
|
let selectsGame: Bool
|
||||||
|
|
||||||
/// The cards in the hand of the player
|
/// The cards in the hand of the player
|
||||||
let cards: [CardInfo]
|
let cards: [CardInfo]
|
||||||
|
|
||||||
/// The action the player can perform
|
/// The action the player can perform
|
||||||
let actions: [String]
|
let actions: [String]
|
||||||
|
|
||||||
init(player: Player, isMasked: Bool) {
|
let playedCard: CardId?
|
||||||
|
|
||||||
|
/// The height of the player card on the table stack
|
||||||
|
let position: Int
|
||||||
|
|
||||||
|
init(player: Player, isMasked: Bool, trickPosition: Int) {
|
||||||
self.name = player.name
|
self.name = player.name
|
||||||
self.connected = player.isConnected
|
self.connected = player.isConnected
|
||||||
self.active = player.isNextActor
|
self.active = player.isNextActor
|
||||||
|
self.selectsGame = player.selectsGame
|
||||||
|
self.playedCard = player.playedCard?.id
|
||||||
|
self.position = trickPosition
|
||||||
if isMasked {
|
if isMasked {
|
||||||
self.cards = []
|
self.cards = []
|
||||||
self.actions = []
|
self.actions = []
|
||||||
|
@ -14,13 +14,23 @@ struct TableInfo: Codable {
|
|||||||
|
|
||||||
let playerRight: PlayerInfo?
|
let playerRight: PlayerInfo?
|
||||||
|
|
||||||
|
let playableGames: [GameId]
|
||||||
|
|
||||||
init(_ table: Table, forPlayerAt playerIndex: Int) {
|
init(_ table: Table, forPlayerAt playerIndex: Int) {
|
||||||
|
let player = table.player(at: playerIndex)!
|
||||||
self.id = table.id
|
self.id = table.id
|
||||||
self.name = table.name
|
self.name = table.name
|
||||||
self.player = table.player(at: playerIndex)!.info(masked: false)
|
self.player = table.playerInfo(at: playerIndex, masked: false)!
|
||||||
self.playerLeft = table.player(leftOf: playerIndex)?.info(masked: true)
|
self.playerLeft = table.playerInfo(leftOf: playerIndex, masked: true)
|
||||||
self.playerAcross = table.player(acrossOf: playerIndex)?.info(masked: true)
|
self.playerAcross = table.playerInfo(acrossOf: playerIndex, masked: true)
|
||||||
self.playerRight = table.player(rightOf: playerIndex)?.info(masked: true)
|
self.playerRight = table.playerInfo(rightOf: playerIndex, masked: true)
|
||||||
|
|
||||||
|
if table.phase == .selectGame, player.selectsGame {
|
||||||
|
let games = table.minimumPlayableGame?.availableGames ?? GameType.allCases
|
||||||
|
self.playableGames = games.filter(player.canPlay).map { $0.id }
|
||||||
|
} else {
|
||||||
|
self.playableGames = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +106,13 @@ final class Database {
|
|||||||
return tables.performAction(player: player, action: action)
|
return tables.performAction(player: player, action: action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func select(game: GameType, playerToken: SessionToken) -> PlayerActionResult {
|
||||||
|
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
||||||
|
return .invalidToken
|
||||||
|
}
|
||||||
|
return tables.select(game: game, player: player)
|
||||||
|
}
|
||||||
|
|
||||||
func play(card: Card, playerToken: SessionToken) -> PlayCardResult {
|
func play(card: Card, playerToken: SessionToken) -> PlayCardResult {
|
||||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
||||||
return .invalidToken
|
return .invalidToken
|
||||||
|
@ -167,11 +167,20 @@ final class TableManagement: DiskWriter {
|
|||||||
|
|
||||||
func performAction(player: PlayerName, action: Player.Action) -> PlayerActionResult {
|
func performAction(player: PlayerName, action: Player.Action) -> PlayerActionResult {
|
||||||
guard let table = currentTable(for: player) else {
|
guard let table = currentTable(for: player) else {
|
||||||
|
print("Player \(player) wants to \(action.path), but no table joined")
|
||||||
return .noTableJoined
|
return .noTableJoined
|
||||||
}
|
}
|
||||||
return table.perform(action: action, forPlayer: player)
|
return table.perform(action: action, forPlayer: player)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func select(game: GameType, player: PlayerName) -> PlayerActionResult {
|
||||||
|
guard let table = currentTable(for: player) else {
|
||||||
|
print("Player \(player) wants to play \(game.rawValue), but no table joined")
|
||||||
|
return .noTableJoined
|
||||||
|
}
|
||||||
|
return table.select(game: game, player: player)
|
||||||
|
}
|
||||||
|
|
||||||
func play(card: Card, player: PlayerName) -> PlayCardResult {
|
func play(card: Card, player: PlayerName) -> PlayCardResult {
|
||||||
guard let table = currentTable(for: player) else {
|
guard let table = currentTable(for: player) else {
|
||||||
return .noTableJoined
|
return .noTableJoined
|
||||||
|
@ -26,7 +26,7 @@ extension CardOrder {
|
|||||||
|
|
||||||
static func highCardIndex(cards: [Card]) -> Int {
|
static func highCardIndex(cards: [Card]) -> Int {
|
||||||
let high: Card
|
let high: Card
|
||||||
if isTrump(cards[0]) {
|
if hasTrump(in: cards) {
|
||||||
high = sort(cards).first!
|
high = sort(cards).first!
|
||||||
} else {
|
} else {
|
||||||
let suit = cards.first!.suit
|
let suit = cards.first!.suit
|
||||||
@ -66,4 +66,8 @@ extension CardOrder {
|
|||||||
static func cards(with suit: Card.Suit, in cards: [Card]) -> [Card] {
|
static func cards(with suit: Card.Suit, in cards: [Card]) -> [Card] {
|
||||||
cards.filter { !isTrump($0) && $0.suit == suit }
|
cards.filter { !isTrump($0) && $0.suit == suit }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func hasCardToCall(_ suit: Card.Suit, in cards: [Card]) -> Bool {
|
||||||
|
cards.contains { $0.symbol != .ass && $0.suit == suit && !isTrump($0) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
61
Sources/App/Model/GameClass.swift
Normal file
61
Sources/App/Model/GameClass.swift
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
typealias GameId = String
|
||||||
|
|
||||||
|
extension GameType {
|
||||||
|
|
||||||
|
var gameClass: GameClass {
|
||||||
|
switch self {
|
||||||
|
case .rufEichel, .rufBlatt, .rufSchelln, .hochzeit:
|
||||||
|
return .ruf
|
||||||
|
case .bettel:
|
||||||
|
return .bettel
|
||||||
|
case .wenz, .geier:
|
||||||
|
return .wenzGeier
|
||||||
|
case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln:
|
||||||
|
return .solo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GameClass: Int {
|
||||||
|
case ruf = 1
|
||||||
|
case bettel = 2
|
||||||
|
case wenzGeier = 3
|
||||||
|
case solo = 4
|
||||||
|
|
||||||
|
var cost: Int {
|
||||||
|
switch self {
|
||||||
|
case .ruf: return 5
|
||||||
|
case .bettel: return 15
|
||||||
|
case .wenzGeier, .solo: return 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func increase() {
|
||||||
|
guard self != .solo else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self = .init(rawValue: rawValue + 1)!
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableGames: [GameType] {
|
||||||
|
switch self {
|
||||||
|
case .ruf:
|
||||||
|
return GameType.allCases
|
||||||
|
case .bettel:
|
||||||
|
return [.bettel, .wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
|
||||||
|
case .wenzGeier:
|
||||||
|
return [.wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
|
||||||
|
case .solo:
|
||||||
|
return [.soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GameType.GameClass: Comparable {
|
||||||
|
|
||||||
|
static func < (lhs: GameType.GameClass, rhs: GameType.GameClass) -> Bool {
|
||||||
|
lhs.rawValue < rhs.rawValue
|
||||||
|
}
|
||||||
|
}
|
@ -1,53 +1,18 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum GameType: Codable {
|
enum GameType: String, CaseIterable, Codable {
|
||||||
|
|
||||||
enum GameClass: Int {
|
case rufEichel = "ruf-eichel"
|
||||||
case ruf = 1
|
case rufBlatt = "ruf-blatt"
|
||||||
case bettel = 2
|
case rufSchelln = "ruf-schelln"
|
||||||
case wenzGeier = 3
|
case hochzeit = "hochzeit"
|
||||||
case solo = 4
|
case bettel = "bettel"
|
||||||
|
case wenz = "wenz"
|
||||||
var cost: Int {
|
case geier = "geier"
|
||||||
switch self {
|
case soloEichel = "solo-eichel"
|
||||||
case .ruf: return 5
|
case soloBlatt = "solo-blatt"
|
||||||
case .bettel: return 15
|
case soloHerz = "solo-herz"
|
||||||
case .wenzGeier, .solo: return 20
|
case soloSchelln = "solo-schelln"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mutating func increase() {
|
|
||||||
guard self != .solo else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self = .init(rawValue: rawValue + 1)!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case rufEichel
|
|
||||||
case rufBlatt
|
|
||||||
case rufSchelln
|
|
||||||
case hochzeit
|
|
||||||
case bettel
|
|
||||||
case wenz
|
|
||||||
case geier
|
|
||||||
case soloEichel
|
|
||||||
case soloBlatt
|
|
||||||
case soloHerz
|
|
||||||
case soloSchelln
|
|
||||||
|
|
||||||
var gameClass: GameClass {
|
|
||||||
switch self {
|
|
||||||
case .rufEichel, .rufBlatt, .rufSchelln, .hochzeit:
|
|
||||||
return .ruf
|
|
||||||
case .bettel:
|
|
||||||
return .bettel
|
|
||||||
case .wenz, .geier:
|
|
||||||
return .wenzGeier
|
|
||||||
case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln:
|
|
||||||
return .solo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isCall: Bool {
|
var isCall: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
@ -84,6 +49,10 @@ enum GameType: Codable {
|
|||||||
gameClass.cost
|
gameClass.cost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var id: GameId {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
|
||||||
var sortingType: CardOrder.Type {
|
var sortingType: CardOrder.Type {
|
||||||
switch self {
|
switch self {
|
||||||
case .wenz:
|
case .wenz:
|
||||||
|
@ -86,7 +86,9 @@ final class Player {
|
|||||||
func play(card: Card) {
|
func play(card: Card) {
|
||||||
remove(card: card)
|
remove(card: card)
|
||||||
playedCard = card
|
playedCard = card
|
||||||
|
actions = actions.filter { $0 != .doubleDuringGame }
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect(using socket: WebSocket) {
|
func connect(using socket: WebSocket) {
|
||||||
_ = self.socket?.close()
|
_ = self.socket?.close()
|
||||||
self.socket = socket
|
self.socket = socket
|
||||||
@ -113,11 +115,15 @@ final class Player {
|
|||||||
actions.contains(action)
|
actions.contains(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareForFirstGame(isFirstPlayer: Bool) {
|
func prepareForNewGame(isFirstPlayer: Bool) {
|
||||||
playsFirstCard = isFirstPlayer
|
playsFirstCard = isFirstPlayer
|
||||||
isNextActor = isFirstPlayer
|
isNextActor = isFirstPlayer
|
||||||
|
selectsGame = false
|
||||||
startedCurrentTrick = isFirstPlayer
|
startedCurrentTrick = isFirstPlayer
|
||||||
actions = [.deal]
|
actions = [.deal]
|
||||||
|
didDoubleAfterFourCards = nil
|
||||||
|
isStillBidding = true
|
||||||
|
isGameLeader = false
|
||||||
handCards = []
|
handCards = []
|
||||||
playedCard = nil
|
playedCard = nil
|
||||||
wonTricks = []
|
wonTricks = []
|
||||||
@ -152,6 +158,7 @@ final class Player {
|
|||||||
|
|
||||||
func offerWedding() {
|
func offerWedding() {
|
||||||
offersWedding = true
|
offersWedding = true
|
||||||
|
isStillBidding = false
|
||||||
actions = []
|
actions = []
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,14 +178,8 @@ final class Player {
|
|||||||
|
|
||||||
func weddingOutbid() {
|
func weddingOutbid() {
|
||||||
isNextActor = false
|
isNextActor = false
|
||||||
guard isStillBidding else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
actions = []
|
actions = []
|
||||||
if offersWedding {
|
offersWedding = false
|
||||||
offersWedding = false
|
|
||||||
isStillBidding = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func didPerformBid() {
|
func didPerformBid() {
|
||||||
@ -221,8 +222,26 @@ final class Player {
|
|||||||
numberOfRaises += 1
|
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() {
|
func mustSelectGame() {
|
||||||
isNextActor = true
|
isNextActor = true
|
||||||
|
selectsGame = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func replace(card: Card, with other: Card) {
|
func replace(card: Card, with other: Card) {
|
||||||
@ -237,11 +256,15 @@ final class Player {
|
|||||||
return removed
|
return removed
|
||||||
}
|
}
|
||||||
|
|
||||||
func gameStarts() {
|
func start(game: GameType) {
|
||||||
isNextActor = playsFirstCard
|
isNextActor = playsFirstCard
|
||||||
startedCurrentTrick = playsFirstCard
|
startedCurrentTrick = playsFirstCard
|
||||||
|
selectsGame = false
|
||||||
actions = [.doubleDuringGame]
|
actions = [.doubleDuringGame]
|
||||||
isGameLeader = false
|
isGameLeader = false
|
||||||
|
if playsFirstCard {
|
||||||
|
setPlayableCardsForStarter(game: game)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchLeadership() {
|
func switchLeadership() {
|
||||||
@ -254,6 +277,7 @@ final class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func withdrawFromBidding() {
|
func withdrawFromBidding() {
|
||||||
|
isNextActor = false
|
||||||
isStillBidding = false
|
isStillBidding = false
|
||||||
actions = []
|
actions = []
|
||||||
}
|
}
|
||||||
@ -263,6 +287,8 @@ final class Player {
|
|||||||
playedCard = nil
|
playedCard = nil
|
||||||
if canDoubleInNextRound, !isGameLeader {
|
if canDoubleInNextRound, !isGameLeader {
|
||||||
actions = [.doubleDuringGame]
|
actions = [.doubleDuringGame]
|
||||||
|
} else {
|
||||||
|
actions = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,8 +416,8 @@ final class Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func info(masked: Bool) -> PlayerInfo {
|
func info(masked: Bool, positionInTrick: Int) -> PlayerInfo {
|
||||||
.init(player: self, isMasked: masked)
|
.init(player: self, isMasked: masked, trickPosition: positionInTrick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -410,40 +436,6 @@ extension Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Player {
|
|
||||||
|
|
||||||
enum Action: String, Codable {
|
|
||||||
/// The player can request cards to be dealt
|
|
||||||
case deal = "deal"
|
|
||||||
|
|
||||||
/// The player doubles on the initial four cards
|
|
||||||
case initialDoubleCost = "double"
|
|
||||||
|
|
||||||
/// The player does not double on the initial four cards
|
|
||||||
case noDoubleCost = "skip"
|
|
||||||
|
|
||||||
/// The player offers a wedding (one trump card)
|
|
||||||
case offerWedding = "wedding"
|
|
||||||
|
|
||||||
/// The player can choose to accept the wedding
|
|
||||||
case acceptWedding = "accept"
|
|
||||||
|
|
||||||
/// The player matches or increases the game during auction
|
|
||||||
case increaseOrMatchGame = "bid"
|
|
||||||
|
|
||||||
/// The player does not want to play
|
|
||||||
case withdrawFromAuction = "out"
|
|
||||||
|
|
||||||
/// The player claims to win and doubles the game cost ("schießen")
|
|
||||||
case doubleDuringGame = "raise"
|
|
||||||
|
|
||||||
/// The url path for the client to call (e.g. /player/deal)
|
|
||||||
var path: String {
|
|
||||||
rawValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Player: Equatable {
|
extension Player: Equatable {
|
||||||
|
|
||||||
static func == (lhs: Player, rhs: Player) -> Bool {
|
static func == (lhs: Player, rhs: Player) -> Bool {
|
||||||
|
35
Sources/App/Model/PlayerAction.swift
Normal file
35
Sources/App/Model/PlayerAction.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Player {
|
||||||
|
|
||||||
|
enum Action: String, Codable {
|
||||||
|
/// The player can request cards to be dealt
|
||||||
|
case deal = "deal"
|
||||||
|
|
||||||
|
/// The player doubles on the initial four cards
|
||||||
|
case initialDoubleCost = "double"
|
||||||
|
|
||||||
|
/// The player does not double on the initial four cards
|
||||||
|
case noDoubleCost = "skip"
|
||||||
|
|
||||||
|
/// The player offers a wedding (one trump card)
|
||||||
|
case offerWedding = "wedding"
|
||||||
|
|
||||||
|
/// The player can choose to accept the wedding
|
||||||
|
case acceptWedding = "accept"
|
||||||
|
|
||||||
|
/// The player matches or increases the game during auction
|
||||||
|
case increaseOrMatchGame = "bid"
|
||||||
|
|
||||||
|
/// The player does not want to play
|
||||||
|
case withdrawFromAuction = "out"
|
||||||
|
|
||||||
|
/// The player claims to win and doubles the game cost ("schießen")
|
||||||
|
case doubleDuringGame = "raise"
|
||||||
|
|
||||||
|
/// The url path for the client to call (e.g. /player/deal)
|
||||||
|
var path: String {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,11 @@ final class Table {
|
|||||||
!players.contains { $0.didDoubleAfterFourCards == nil }
|
!players.contains { $0.didDoubleAfterFourCards == nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// At least one double exists after all players acted on their first cards
|
||||||
|
var initialDoubleExists: Bool {
|
||||||
|
players.contains { $0.didDoubleAfterFourCards == true }
|
||||||
|
}
|
||||||
|
|
||||||
var weddingOfferExists: Bool {
|
var weddingOfferExists: Bool {
|
||||||
players.contains { $0.offersWedding }
|
players.contains { $0.offersWedding }
|
||||||
}
|
}
|
||||||
@ -106,16 +111,23 @@ final class Table {
|
|||||||
players.first { $0.name == player }
|
players.first { $0.name == player }
|
||||||
}
|
}
|
||||||
|
|
||||||
func player(leftOf index: Int) -> Player? {
|
func playerInfo(at index: Int, masked: Bool) -> PlayerInfo? {
|
||||||
player(at: (index + 1) % 4)
|
let position = players.firstIndex { $0.startedCurrentTrick }!
|
||||||
|
let layer = (index - position + 4) % 4
|
||||||
|
//
|
||||||
|
return player(at: index)?.info(masked: masked, positionInTrick: layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func player(acrossOf index: Int) -> Player? {
|
func playerInfo(leftOf index: Int, masked: Bool) -> PlayerInfo? {
|
||||||
player(at: (index + 2) % 4)
|
playerInfo(at: (index + 1) % 4, masked: masked)
|
||||||
}
|
}
|
||||||
|
|
||||||
func player(rightOf index: Int) -> Player? {
|
func playerInfo(acrossOf index: Int, masked: Bool) -> PlayerInfo? {
|
||||||
player(at: (index + 3) % 4)
|
playerInfo(at: (index + 2) % 4, masked: masked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func playerInfo(rightOf index: Int, masked: Bool) -> PlayerInfo? {
|
||||||
|
playerInfo(at: (index + 3) % 4, masked: masked)
|
||||||
}
|
}
|
||||||
|
|
||||||
func player(at index: Int) -> Player? {
|
func player(at index: Int) -> Player? {
|
||||||
@ -140,7 +152,7 @@ final class Table {
|
|||||||
let index = index(of: player)
|
let index = index(of: player)
|
||||||
for i in 1..<4 {
|
for i in 1..<4 {
|
||||||
let player = players[(index + i) % 4]
|
let player = players[(index + i) % 4]
|
||||||
guard player.isStillBidding else {
|
guard player.isStillBidding, !player.offersWedding else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return player
|
return player
|
||||||
@ -149,10 +161,14 @@ final class Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func remove(player: PlayerName) {
|
func remove(player: PlayerName) {
|
||||||
guard contains(player: player) else {
|
guard let index = players.firstIndex(where: { $0.name == player }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
players = players.filter { $0.name != player }
|
let removedPlayer = players[index]
|
||||||
|
if removedPlayer.playsFirstCard {
|
||||||
|
players[(index + 1) % players.count].playsFirstCard = true
|
||||||
|
}
|
||||||
|
players.remove(at: index)
|
||||||
reset()
|
reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +199,7 @@ final class Table {
|
|||||||
didDoubleInCurrentRound = false // Not relevant in this phase
|
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].prepareForNewGame(isFirstPlayer: i == index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,6 +233,7 @@ final class Table {
|
|||||||
player.isNextActor = false
|
player.isNextActor = false
|
||||||
}
|
}
|
||||||
updatePlayableCards()
|
updatePlayableCards()
|
||||||
|
sendUpdateToAllPlayers()
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,6 +257,7 @@ final class Table {
|
|||||||
func perform(action: Player.Action, forPlayer player: PlayerName) -> PlayerActionResult {
|
func perform(action: Player.Action, forPlayer player: PlayerName) -> PlayerActionResult {
|
||||||
let player = select(player: player)!
|
let player = select(player: player)!
|
||||||
guard player.canPerform(action) else {
|
guard player.canPerform(action) else {
|
||||||
|
print("Player \(player) wants to \(action.path), but only allowed: \(player.actions)")
|
||||||
return .tableStateInvalid
|
return .tableStateInvalid
|
||||||
}
|
}
|
||||||
defer { sendUpdateToAllPlayers() }
|
defer { sendUpdateToAllPlayers() }
|
||||||
@ -282,9 +300,13 @@ final class Table {
|
|||||||
|
|
||||||
func perform(double: Bool, forPlayer player: Player) -> PlayerActionResult {
|
func perform(double: Bool, forPlayer player: Player) -> PlayerActionResult {
|
||||||
player.didDouble(double)
|
player.didDouble(double)
|
||||||
if allPlayersFinishedDoubling {
|
guard allPlayersFinishedDoubling else {
|
||||||
dealAdditionalCards()
|
return .success
|
||||||
}
|
}
|
||||||
|
guard initialDoubleExists else {
|
||||||
|
return dealNextGame()
|
||||||
|
}
|
||||||
|
dealAdditionalCards()
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,20 +317,25 @@ final class Table {
|
|||||||
}
|
}
|
||||||
players.forEach { $0.startAuction() }
|
players.forEach { $0.startAuction() }
|
||||||
minimumPlayableGame = nil
|
minimumPlayableGame = nil
|
||||||
|
phase = .bidding
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performWeddingCall(forPlayer player: Player) -> PlayerActionResult {
|
private func performWeddingCall(forPlayer player: Player) -> PlayerActionResult {
|
||||||
guard phase == .bidding else {
|
guard phase == .bidding else {
|
||||||
|
print("Invalid phase \(phase) for wedding call")
|
||||||
return .tableStateInvalid
|
return .tableStateInvalid
|
||||||
}
|
}
|
||||||
guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else {
|
guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else {
|
||||||
|
print("Invalid minimum game \(minimumPlayableGame!) for wedding call")
|
||||||
return .tableStateInvalid
|
return .tableStateInvalid
|
||||||
}
|
}
|
||||||
guard player.offersWedding else {
|
guard player.offersWedding else {
|
||||||
|
print("Player does not offer wedding")
|
||||||
return .tableStateInvalid
|
return .tableStateInvalid
|
||||||
}
|
}
|
||||||
guard !weddingOfferExists else {
|
guard !weddingOfferExists else {
|
||||||
// Only one wedding allowed at the table
|
// Only one wedding allowed at the table
|
||||||
|
print("Already one wedding at table")
|
||||||
return .tableStateInvalid
|
return .tableStateInvalid
|
||||||
}
|
}
|
||||||
// Only allow wedding acceptance or outbidding
|
// Only allow wedding acceptance or outbidding
|
||||||
@ -334,6 +361,8 @@ final class Table {
|
|||||||
minimumPlayableGame = .ruf
|
minimumPlayableGame = .ruf
|
||||||
} else {
|
} else {
|
||||||
minimumPlayableGame!.increase()
|
minimumPlayableGame!.increase()
|
||||||
|
// Remove wedding offers
|
||||||
|
players.forEach { $0.weddingOutbid() }
|
||||||
}
|
}
|
||||||
player.didPerformBid()
|
player.didPerformBid()
|
||||||
// Find next player to place bid
|
// Find next player to place bid
|
||||||
@ -408,7 +437,7 @@ final class Table {
|
|||||||
|
|
||||||
// Start the game
|
// Start the game
|
||||||
gameType = .hochzeit
|
gameType = .hochzeit
|
||||||
players.forEach { $0.gameStarts() }
|
players.forEach { $0.start(game: .hochzeit) }
|
||||||
player.switchLeadership()
|
player.switchLeadership()
|
||||||
offerer.switchLeadership()
|
offerer.switchLeadership()
|
||||||
return .success
|
return .success
|
||||||
@ -426,18 +455,22 @@ final class Table {
|
|||||||
selectGame(player: auctionWinner)
|
selectGame(player: auctionWinner)
|
||||||
case 0:
|
case 0:
|
||||||
// All players withdrawn, deal new cards
|
// All players withdrawn, deal new cards
|
||||||
let first = firstPlayer
|
return dealNextGame()
|
||||||
let newPlayer = self.nextBidder(after: first)
|
|
||||||
first.playsFirstCard = false
|
|
||||||
newPlayer.playsFirstCard = true
|
|
||||||
prepareTableForFirstGame()
|
|
||||||
return dealInitialCards()
|
|
||||||
default:
|
default:
|
||||||
break
|
nextBidder(after: player).requiresBid()
|
||||||
}
|
}
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func dealNextGame() -> PlayerActionResult {
|
||||||
|
let first = firstPlayer
|
||||||
|
let newPlayer = self.nextBidder(after: first)
|
||||||
|
first.playsFirstCard = false
|
||||||
|
newPlayer.playsFirstCard = true
|
||||||
|
prepareTableForFirstGame()
|
||||||
|
return dealInitialCards()
|
||||||
|
}
|
||||||
|
|
||||||
private func selectGame(player: Player) {
|
private func selectGame(player: Player) {
|
||||||
minimumPlayableGame = nil
|
minimumPlayableGame = nil
|
||||||
gameType = nil
|
gameType = nil
|
||||||
@ -446,11 +479,43 @@ final class Table {
|
|||||||
player.mustSelectGame()
|
player.mustSelectGame()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func select(game: GameType, player: PlayerName) -> PlayerActionResult {
|
||||||
|
let player = select(player: player)!
|
||||||
|
guard phase == .selectGame, player.selectsGame, game != .hochzeit else {
|
||||||
|
return .tableStateInvalid
|
||||||
|
}
|
||||||
|
guard minimumPlayableGame == nil || game.gameClass >= minimumPlayableGame! else {
|
||||||
|
return .tableStateInvalid
|
||||||
|
}
|
||||||
|
defer { sendUpdateToAllPlayers() }
|
||||||
|
guard let suit = game.calledSuit else {
|
||||||
|
phase = .playing
|
||||||
|
gameType = game
|
||||||
|
minimumPlayableGame = nil
|
||||||
|
players.forEach { $0.start(game: game) }
|
||||||
|
player.switchLeadership()
|
||||||
|
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
|
guard player.canPlay(game: game) else {
|
||||||
|
return .tableStateInvalid
|
||||||
|
}
|
||||||
|
phase = .playing
|
||||||
|
gameType = game
|
||||||
|
minimumPlayableGame = nil
|
||||||
|
players.forEach { $0.start(game: game) }
|
||||||
|
player.switchLeadership()
|
||||||
|
// Find called player
|
||||||
|
let ace = Card(suit, .ass)
|
||||||
|
players.first { $0.rawCards.contains(ace) }!.switchLeadership()
|
||||||
|
return .success
|
||||||
|
}
|
||||||
|
|
||||||
private func performDoubleDuringGame(forPlayer player: Player) -> PlayerActionResult {
|
private func performDoubleDuringGame(forPlayer player: Player) -> PlayerActionResult {
|
||||||
guard phase == .playing,
|
guard phase == .playing, !player.isGameLeader else {
|
||||||
!player.isGameLeader else {
|
return .tableStateInvalid
|
||||||
return .tableStateInvalid
|
}
|
||||||
}
|
|
||||||
player.numberOfRaises += 1
|
player.numberOfRaises += 1
|
||||||
players.forEach { $0.switchLeadership() }
|
players.forEach { $0.switchLeadership() }
|
||||||
return .success
|
return .success
|
||||||
@ -460,6 +525,9 @@ final class Table {
|
|||||||
phase = .waitingForPlayers
|
phase = .waitingForPlayers
|
||||||
gameType = nil
|
gameType = nil
|
||||||
minimumPlayableGame = nil
|
minimumPlayableGame = nil
|
||||||
|
for player in players {
|
||||||
|
player.prepareForNewGame(isFirstPlayer: player.playsFirstCard)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,16 +280,27 @@ func routes(_ app: Application) throws {
|
|||||||
|
|
||||||
app.post("player", "action", ":action") { req -> String in
|
app.post("player", "action", ":action") { req -> String in
|
||||||
guard let token = req.body.string,
|
guard let token = req.body.string,
|
||||||
let actionString = req.parameters.get("action"),
|
let actionString = req.parameters.get("action") else {
|
||||||
let action = Player.Action(rawValue: actionString) else {
|
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest)
|
||||||
}
|
}
|
||||||
switch database.performAction(playerToken: token, action: action) {
|
let result: PlayerActionResult
|
||||||
|
if let action = Player.Action(rawValue: actionString) {
|
||||||
|
result = database.performAction(playerToken: token, action: action)
|
||||||
|
} else if let game = GameType(rawValue: actionString) {
|
||||||
|
result = database.select(game: game, playerToken: token)
|
||||||
|
} else {
|
||||||
|
throw Abort(.badRequest)
|
||||||
|
}
|
||||||
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
return ""
|
return ""
|
||||||
case .invalidToken:
|
case .invalidToken:
|
||||||
throw Abort(.unauthorized) // 401
|
throw Abort(.unauthorized) // 401
|
||||||
default:
|
case .noTableJoined:
|
||||||
|
throw Abort(.preconditionFailed) // 412
|
||||||
|
case .tableNotFull:
|
||||||
|
throw Abort(.preconditionFailed) // 412
|
||||||
|
case .tableStateInvalid:
|
||||||
throw Abort(.preconditionFailed) // 412
|
throw Abort(.preconditionFailed) // 412
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user