Schafkopf-Server/Sources/App/Model/Table.swift

556 lines
17 KiB
Swift
Raw Normal View History

2021-12-03 18:03:29 +01:00
import Foundation
import WebSocketKit
private extension Int {
mutating func advanceInTable() {
self = (self + 1) % maximumPlayersPerTable
}
}
final class Table {
let id: TableId
let name: TableName
let isPublic: Bool
var players: [Player] = []
var phase: GamePhase = .waitingForPlayers
var gameType: GameType? = nil
2021-12-06 11:43:30 +01:00
var minimumPlayableGame: GameType.GameClass?
2021-12-03 18:03:29 +01:00
/// Indicates if any player doubled during the current round, extending it to the next round
var didDoubleInCurrentRound = false
/// Indicates that all players acted after the first four cards
var allPlayersFinishedDoubling: Bool {
!players.contains { $0.didDoubleAfterFourCards == nil }
}
2021-12-06 11:43:30 +01:00
/// At least one double exists after all players acted on their first cards
var initialDoubleExists: Bool {
players.contains { $0.didDoubleAfterFourCards == true }
}
2021-12-06 11:43:30 +01:00
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 }
}
2021-12-03 18:03:29 +01:00
init(id: TableId, name: TableName, isPublic: Bool) {
self.id = id
self.name = name
self.isPublic = isPublic
}
init(newTable name: TableName, isPublic: Bool) {
self.id = .newToken()
self.name = name
self.isPublic = isPublic
}
func add(player: PlayerName) -> Bool {
guard !isFull else {
return false
}
let player = Player(name: player)
players.append(player)
if isFull {
prepareTableForFirstGame()
}
sendUpdateToAllPlayers()
return true
}
func contains(player: PlayerName) -> Bool {
players.contains { $0.name == player }
}
2021-12-06 11:43:30 +01:00
/// The player to play the first card of the current game
var firstPlayer: Player {
players.first { $0.playsFirstCard }!
}
2021-12-03 18:03:29 +01:00
func select(player: PlayerName) -> Player? {
players.first { $0.name == player }
}
func playerInfo(at index: Int, masked: Bool) -> PlayerInfo? {
let position = players.firstIndex { $0.startedCurrentTrick }!
let layer = (index - position + 4) % 4
//
return player(at: index)?.info(masked: masked, positionInTrick: layer)
}
2021-12-03 18:03:29 +01:00
func playerInfo(leftOf index: Int, masked: Bool) -> PlayerInfo? {
playerInfo(at: (index + 1) % 4, masked: masked)
2021-12-03 18:03:29 +01:00
}
func playerInfo(acrossOf index: Int, masked: Bool) -> PlayerInfo? {
playerInfo(at: (index + 2) % 4, masked: masked)
2021-12-03 18:03:29 +01:00
}
func playerInfo(rightOf index: Int, masked: Bool) -> PlayerInfo? {
playerInfo(at: (index + 3) % 4, masked: masked)
2021-12-03 18:03:29 +01:00
}
func player(at index: Int) -> Player? {
guard index < players.count else {
return nil
}
return players[index]
}
2021-12-06 11:43:30 +01:00
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, !player.offersWedding else {
2021-12-06 11:43:30 +01:00
continue
}
return player
}
return player
}
2021-12-03 18:03:29 +01:00
func remove(player: PlayerName) {
guard let index = players.firstIndex(where: { $0.name == player }) else {
2021-12-03 18:03:29 +01:00
return
}
let removedPlayer = players[index]
if removedPlayer.playsFirstCard {
players[(index + 1) % players.count].playsFirstCard = true
}
players.remove(at: index)
2021-12-03 18:03:29 +01:00
reset()
}
func connect(player name: PlayerName, using socket: WebSocket) -> Bool {
guard let player = select(player: name) else {
return false
}
player.connect(using: socket)
sendUpdateToAllPlayers()
return true
}
func disconnect(player name: PlayerName) {
guard let player = select(player: name) else {
return
}
guard player.disconnect() else {
return
}
sendUpdateToAllPlayers()
return
}
private func prepareTableForFirstGame() {
2021-12-06 11:43:30 +01:00
phase = .waitingForPlayers
gameType = nil
minimumPlayableGame = nil // Not relevant in this phase
didDoubleInCurrentRound = false // Not relevant in this phase
2021-12-03 18:03:29 +01:00
let index = players.firstIndex { $0.playsFirstCard } ?? 0
for i in 0..<maximumPlayersPerTable {
players[i].prepareForNewGame(isFirstPlayer: i == index)
2021-12-03 18:03:29 +01:00
}
}
private func sendUpdateToAllPlayers() {
players.enumerated().forEach { playerIndex, player in
guard player.isConnected else {
return
}
let info = TableInfo(self, forPlayerAt: playerIndex)
player.send(info)
}
}
// MARK: Player actions
2021-12-06 11:43:30 +01:00
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()
sendUpdateToAllPlayers()
2021-12-06 11:43:30 +01:00
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
}
2021-12-03 18:03:29 +01:00
func perform(action: Player.Action, forPlayer player: PlayerName) -> PlayerActionResult {
2021-12-06 11:43:30 +01:00
let player = select(player: player)!
guard player.canPerform(action) else {
print("Player \(player) wants to \(action.path), but only allowed: \(player.actions)")
2021-12-06 11:43:30 +01:00
return .tableStateInvalid
}
2021-12-03 18:03:29 +01:00
defer { sendUpdateToAllPlayers() }
switch action {
case .deal:
return dealInitialCards()
case .initialDoubleCost:
return perform(double: true, forPlayer: player)
case .noDoubleCost:
return perform(double: false, forPlayer: player)
case .offerWedding:
2021-12-06 11:43:30 +01:00
return performWeddingCall(forPlayer: player)
2021-12-03 18:03:29 +01:00
case .acceptWedding:
2021-12-06 11:43:30 +01:00
return handleWeddingAccept(forPlayer: player)
2021-12-03 18:03:29 +01:00
case .increaseOrMatchGame:
2021-12-06 11:43:30 +01:00
return performBidIncrease(forPlayer: player)
2021-12-03 18:03:29 +01:00
case .withdrawFromAuction:
2021-12-06 11:43:30 +01:00
return performWithdrawl(forPlayer: player)
2021-12-03 18:03:29 +01:00
case .doubleDuringGame:
2021-12-06 11:43:30 +01:00
return performDoubleDuringGame(forPlayer: player)
2021-12-03 18:03:29 +01:00
}
}
private func dealInitialCards() -> PlayerActionResult {
guard isFull else {
return .tableNotFull
}
guard phase == .waitingForPlayers else {
return .tableStateInvalid
}
let cards = Dealer.dealFirstCards()
for (index, player) in players.enumerated() {
player.assignFirstCards(cards[index])
}
2021-12-06 11:43:30 +01:00
phase = .collectingDoubles
gameType = nil
2021-12-03 18:03:29 +01:00
return .success
}
2021-12-06 11:43:30 +01:00
func perform(double: Bool, forPlayer player: Player) -> PlayerActionResult {
2021-12-03 18:03:29 +01:00
player.didDouble(double)
guard allPlayersFinishedDoubling else {
return .success
}
guard initialDoubleExists else {
return dealNextGame()
2021-12-03 18:03:29 +01:00
}
dealAdditionalCards()
2021-12-03 18:03:29 +01:00
return .success
}
private func dealAdditionalCards() {
let cards = Dealer.dealRemainingCards(of: players.map { $0.rawCards })
for (index, player) in players.enumerated() {
player.assignRemainingCards(cards[index])
}
2021-12-06 11:43:30 +01:00
players.forEach { $0.startAuction() }
minimumPlayableGame = nil
phase = .bidding
2021-12-06 11:43:30 +01:00
}
private func performWeddingCall(forPlayer player: Player) -> PlayerActionResult {
guard phase == .bidding else {
print("Invalid phase \(phase) for wedding call")
2021-12-06 11:43:30 +01:00
return .tableStateInvalid
}
guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else {
print("Invalid minimum game \(minimumPlayableGame!) for wedding call")
2021-12-06 11:43:30 +01:00
return .tableStateInvalid
}
guard player.offersWedding else {
print("Player does not offer wedding")
2021-12-06 11:43:30 +01:00
return .tableStateInvalid
}
guard !weddingOfferExists else {
// Only one wedding allowed at the table
print("Already one wedding at table")
2021-12-06 11:43:30 +01:00
return .tableStateInvalid
}
// Only allow wedding acceptance or outbidding
players.forEach { $0.weddingOfferExists() }
player.offerWedding()
firstPlayer.hasWeddingOffer()
minimumPlayableGame = .bettel
2021-12-03 18:03:29 +01:00
return .success
}
2021-12-06 11:43:30 +01:00
private func performBidIncrease(forPlayer player: Player) -> PlayerActionResult {
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
} else {
minimumPlayableGame!.increase()
// Remove wedding offers
players.forEach { $0.weddingOutbid() }
2021-12-06 11:43:30 +01:00
}
player.didPerformBid()
// Find next player to place bid
nextBidder(after: player).requiresBid()
return .success
2021-12-03 18:03:29 +01:00
}
2021-12-06 11:43:30 +01:00
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.start(game: .hochzeit) }
2021-12-06 11:43:30 +01:00
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
return dealNextGame()
2021-12-06 11:43:30 +01:00
default:
nextBidder(after: player).requiresBid()
2021-12-06 11:43:30 +01:00
}
return .success
}
private func dealNextGame() -> PlayerActionResult {
let first = firstPlayer
let newPlayer = self.nextBidder(after: first)
first.playsFirstCard = false
newPlayer.playsFirstCard = true
prepareTableForFirstGame()
return dealInitialCards()
}
2021-12-06 11:43:30 +01:00
private func selectGame(player: Player) {
minimumPlayableGame = nil
gameType = nil
phase = .selectGame
players.forEach { $0.auctionEnded() }
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
}
2021-12-06 11:43:30 +01:00
private func performDoubleDuringGame(forPlayer player: Player) -> PlayerActionResult {
guard phase == .playing, !player.isGameLeader else {
return .tableStateInvalid
}
2021-12-06 11:43:30 +01:00
player.numberOfRaises += 1
players.forEach { $0.switchLeadership() }
return .success
}
2021-12-03 18:03:29 +01:00
private func reset() {
phase = .waitingForPlayers
gameType = nil
2021-12-06 11:43:30 +01:00
minimumPlayableGame = nil
for player in players {
player.prepareForNewGame(isFirstPlayer: player.playsFirstCard)
}
2021-12-03 18:03:29 +01:00
}
}
extension Table {
var isFull: Bool {
players.count == maximumPlayersPerTable
}
var publicInfo: PublicTableInfo {
.init(id: id, name: name, players: playerNames)
}
var playerNames: [PlayerName] {
players.map { $0.name }
}
func compileInfo(for player: PlayerName) -> TableInfo? {
guard let index = players.firstIndex(where: { $0.name == player }) else {
return nil
}
return TableInfo(self, forPlayerAt: index)
}
}