578 lines
18 KiB
Swift
578 lines
18 KiB
Swift
import Foundation
|
|
import WebSocketKit
|
|
|
|
private extension Int {
|
|
|
|
mutating func advanceInTable() {
|
|
self = (self + 1) % maximumPlayersPerTable
|
|
}
|
|
}
|
|
|
|
final class OldTable {
|
|
|
|
let id: TableId
|
|
|
|
let name: TableName
|
|
|
|
let isPublic: Bool
|
|
|
|
var players: [OldPlayer] = []
|
|
|
|
var phase: GamePhase = .waitingForPlayers
|
|
|
|
var gameType: GameType? = nil
|
|
|
|
var minimumPlayableGame: GameType.GameClass = .none
|
|
|
|
/// 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 }
|
|
}
|
|
|
|
/// At least one double exists after all players acted on their first cards
|
|
var initialDoubleExists: Bool {
|
|
players.contains { $0.didDoubleAfterFourCards == true }
|
|
}
|
|
|
|
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: OldPlayer {
|
|
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 }
|
|
}
|
|
|
|
var didFinishGame: Bool {
|
|
!players.contains { !$0.handCards.isEmpty }
|
|
}
|
|
|
|
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 = OldPlayer(name: player)
|
|
players.append(player)
|
|
if isFull {
|
|
prepareTableForFirstGame()
|
|
}
|
|
sendUpdateToAllPlayers()
|
|
return true
|
|
}
|
|
|
|
func contains(player: PlayerName) -> Bool {
|
|
players.contains { $0.name == player }
|
|
}
|
|
|
|
/// The player to play the first card of the current game
|
|
var firstPlayer: OldPlayer {
|
|
players.first { $0.playsFirstCard }!
|
|
}
|
|
|
|
func select(player: PlayerName) -> OldPlayer? {
|
|
players.first { $0.name == player }
|
|
}
|
|
|
|
var indexOfTrickStarter: Int {
|
|
players.firstIndex { $0.startedCurrentTrick }!
|
|
}
|
|
|
|
func player(at index: Int) -> OldPlayer? {
|
|
guard index < players.count else {
|
|
return nil
|
|
}
|
|
return players[index]
|
|
}
|
|
|
|
func index(of player: OldPlayer) -> Int {
|
|
players.firstIndex(of: player)!
|
|
}
|
|
|
|
func nextPlayer(after player: OldPlayer) -> OldPlayer {
|
|
let i = index(of: player)
|
|
let newIndex = (i + 1) % maximumPlayersPerTable
|
|
return players[newIndex]
|
|
}
|
|
|
|
func nextBidder(after player: OldPlayer) -> OldPlayer {
|
|
// 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 {
|
|
continue
|
|
}
|
|
return player
|
|
}
|
|
return player
|
|
}
|
|
|
|
func availableGames(forPlayerAt index: Int) -> [GameType] {
|
|
|
|
return []
|
|
}
|
|
|
|
func remove(player: PlayerName) {
|
|
guard let index = players.firstIndex(where: { $0.name == player }) else {
|
|
return
|
|
}
|
|
let removedPlayer = players[index]
|
|
if removedPlayer.playsFirstCard {
|
|
players[(index + 1) % players.count].playsFirstCard = true
|
|
}
|
|
players.remove(at: index)
|
|
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() {
|
|
phase = .waitingForPlayers
|
|
gameType = nil
|
|
minimumPlayableGame = .none // Not relevant in this phase
|
|
didDoubleInCurrentRound = false // Not relevant in this phase
|
|
let index = players.firstIndex { $0.playsFirstCard } ?? 0
|
|
for i in 0..<maximumPlayersPerTable {
|
|
players[i].prepareForNewGame(isFirstPlayer: i == index)
|
|
}
|
|
}
|
|
|
|
private func sendUpdateToAllPlayers() {
|
|
|
|
}
|
|
|
|
// 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 {
|
|
// Player only has playable cards when it is active
|
|
return .invalidCard
|
|
}
|
|
if hasCompletedTrick {
|
|
// Hide cards from last trick when next card is played
|
|
players.forEach { $0.clearLastTrick() }
|
|
}
|
|
player.play(card: card)
|
|
if let completedTrick = completedTrick {
|
|
didFinish(trick: completedTrick, in: game)
|
|
// Update cards for empty trick
|
|
players.forEach { $0.setPlayableCards(forCurrentTrick: [], in: game) }
|
|
} else {
|
|
let next = nextPlayer(after: player)
|
|
next.isNextActor = true
|
|
player.isNextActor = false
|
|
// Update cards for empty trick
|
|
players.forEach { $0.setPlayableCards(forCurrentTrick: currentTrick, in: game) }
|
|
}
|
|
if didFinishGame {
|
|
finishedGame()
|
|
}
|
|
sendUpdateToAllPlayers()
|
|
return .success
|
|
}
|
|
|
|
private func finishedGame() {
|
|
phase = .gameFinished
|
|
players.forEach { $0.didFinishGame() }
|
|
guard didFinishGame else {
|
|
// Either no doubles or bids
|
|
return
|
|
}
|
|
// TODO: Calculate winner, points, cost
|
|
|
|
}
|
|
|
|
func didFinish(trick: Trick, in game: GameType) {
|
|
// If trick is completed, calculate winner
|
|
let startIndex = indexOfTrickStarter
|
|
let rotated = trick.rotated(toStartAt: startIndex)
|
|
let index = rotated.highCardIndex(forGame: game)
|
|
print("Winner \(index) for \(rotated)")
|
|
let winner = players[(startIndex + index) % 4]
|
|
players.forEach {
|
|
$0.didFinish(trick: trick,
|
|
winner: winner == $0,
|
|
canDoubleInNextRound: didDoubleInCurrentRound)
|
|
}
|
|
if game == .bettel && winner.isGameLeader {
|
|
// A bettel is lost if a single trick is won by the leader
|
|
finishedGame()
|
|
return
|
|
}
|
|
didDoubleInCurrentRound = false
|
|
}
|
|
|
|
func perform(action: PlayerAction, forPlayer player: PlayerName) -> PlayerActionResult {
|
|
let player = select(player: player)!
|
|
guard player.canPerform(action) else {
|
|
print("Player \(player) wants to \(action.id), but only allowed: \(player.actions)")
|
|
return .tableStateInvalid
|
|
}
|
|
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:
|
|
return performWeddingCall(forPlayer: player)
|
|
case .acceptWedding:
|
|
return handleWeddingAccept(forPlayer: player)
|
|
case .increaseOrMatchGame:
|
|
return performBidIncrease(forPlayer: player)
|
|
case .withdrawFromAuction:
|
|
return performWithdrawl(forPlayer: player)
|
|
case .doubleDuringGame:
|
|
return performDoubleDuringGame(forPlayer: player)
|
|
}
|
|
}
|
|
|
|
private func dealInitialCards() -> PlayerActionResult {
|
|
guard isFull else {
|
|
return .tableNotFull
|
|
}
|
|
guard phase == .waitingForPlayers || phase == .gameFinished else {
|
|
return .tableStateInvalid
|
|
}
|
|
if phase == .gameFinished {
|
|
prepareForNextGame()
|
|
}
|
|
|
|
let cards = Dealer.dealFirstCards()
|
|
for (index, player) in players.enumerated() {
|
|
player.assignFirstCards(cards[index])
|
|
}
|
|
phase = .collectingDoubles
|
|
gameType = nil
|
|
return .success
|
|
}
|
|
|
|
func perform(double: Bool, forPlayer player: OldPlayer) -> PlayerActionResult {
|
|
player.didDouble(double)
|
|
guard allPlayersFinishedDoubling else {
|
|
return .success
|
|
}
|
|
if initialDoubleExists {
|
|
dealAdditionalCards()
|
|
} else {
|
|
finishedGame()
|
|
}
|
|
return .success
|
|
}
|
|
|
|
private func dealAdditionalCards() {
|
|
let cards = Dealer.dealRemainingCards(of: players.map { $0.rawCards })
|
|
for (index, player) in players.enumerated() {
|
|
player.assignRemainingCards(cards[index])
|
|
}
|
|
players.forEach { $0.startAuction() }
|
|
minimumPlayableGame = .none
|
|
phase = .bidding
|
|
}
|
|
|
|
private func performWeddingCall(forPlayer player: OldPlayer) -> PlayerActionResult {
|
|
guard phase == .bidding else {
|
|
print("Invalid phase \(phase) for wedding call")
|
|
return .tableStateInvalid
|
|
}
|
|
guard minimumPlayableGame.allowsWedding else {
|
|
print("Invalid minimum game \(minimumPlayableGame) for wedding call")
|
|
return .tableStateInvalid
|
|
}
|
|
guard player.canOfferWedding else {
|
|
print("Player does not offer wedding")
|
|
return .tableStateInvalid
|
|
}
|
|
guard !weddingOfferExists else {
|
|
// Only one wedding allowed at the table
|
|
print("Already one wedding at table")
|
|
return .tableStateInvalid
|
|
}
|
|
// Only allow wedding acceptance or outbidding
|
|
players.forEach { $0.weddingOfferExists() }
|
|
player.offerWedding()
|
|
firstPlayer.hasWeddingOffer()
|
|
minimumPlayableGame = .bettel
|
|
return .success
|
|
}
|
|
|
|
private func performBidIncrease(forPlayer player: OldPlayer) -> 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
|
|
}
|
|
// TODO: Check if new player sits before old player
|
|
// then don't increase game
|
|
minimumPlayableGame.increase()
|
|
if !minimumPlayableGame.allowsWedding {
|
|
// Remove wedding offers
|
|
players.forEach { $0.weddingOutbid() }
|
|
}
|
|
|
|
// TODO: Remove highest bidder from old player
|
|
player.didPerformBid()
|
|
|
|
if numberOfRemainingBidders == 1 {
|
|
selectGame(player: player)
|
|
return .success
|
|
}
|
|
// Find next player to place bid
|
|
nextBidder(after: player).requiresBid(hasWedding: false)
|
|
return .success
|
|
}
|
|
|
|
private func handleWeddingOutbid(forPlayer player: OldPlayer) -> PlayerActionResult {
|
|
if player.offersWedding {
|
|
// A player offering a wedding can't outbid itself
|
|
return .tableStateInvalid
|
|
}
|
|
players.forEach { $0.weddingOutbid() }
|
|
nextBidder(after: player).requiresBid(hasWedding: false)
|
|
return .success
|
|
}
|
|
|
|
private func handleWeddingAccept(forPlayer player: OldPlayer) -> PlayerActionResult {
|
|
guard phase == .bidding else {
|
|
return .tableStateInvalid
|
|
}
|
|
guard minimumPlayableGame.allowsWedding 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(hasWedding: false)
|
|
return .success
|
|
}
|
|
|
|
private func selectedWedding(player: OldPlayer) {
|
|
minimumPlayableGame = .none
|
|
gameType = .hochzeit
|
|
phase = .selectWeddingCard
|
|
players.forEach { $0.auctionEnded() }
|
|
player.mustSelectWeddingCard()
|
|
}
|
|
|
|
private func selectedCardForWedding(card: Card, player: OldPlayer) -> PlayCardResult {
|
|
guard player.isNextActor,
|
|
player.wouldAcceptWedding,
|
|
weddingOfferExists else {
|
|
return .tableStateInvalid
|
|
}
|
|
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) }
|
|
player.switchLeadership()
|
|
offerer.switchLeadership()
|
|
return .success
|
|
}
|
|
|
|
private func performWithdrawl(forPlayer player: OldPlayer) -> PlayerActionResult {
|
|
guard phase == .bidding,
|
|
player.isNextActor,
|
|
player.isStillBidding else {
|
|
return .tableStateInvalid
|
|
}
|
|
player.withdrawFromBidding()
|
|
switch numberOfRemainingBidders {
|
|
case 1:
|
|
if minimumPlayableGame != .none {
|
|
// Will only be called when at least one player placed a bid
|
|
selectGame(player: auctionWinner)
|
|
return .success
|
|
}
|
|
case 0:
|
|
// All players withdrawn, deal new cards
|
|
finishedGame()
|
|
return .success
|
|
default:
|
|
break
|
|
}
|
|
nextBidder(after: player).requiresBid(hasWedding: weddingOfferExists)
|
|
return .success
|
|
}
|
|
|
|
private func prepareForNextGame() {
|
|
let first = firstPlayer
|
|
let newPlayer = self.nextBidder(after: first)
|
|
first.playsFirstCard = false
|
|
newPlayer.playsFirstCard = true
|
|
print("Made \(newPlayer.name) to new starter")
|
|
prepareTableForFirstGame()
|
|
}
|
|
|
|
private func selectGame(player: OldPlayer) {
|
|
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 game.gameClass >= minimumPlayableGame else {
|
|
return .tableStateInvalid
|
|
}
|
|
defer { sendUpdateToAllPlayers() }
|
|
guard let suit = game.calledSuit else {
|
|
phase = .playing
|
|
gameType = game
|
|
minimumPlayableGame = .none
|
|
players.forEach { $0.start(game: game) }
|
|
player.switchLeadership()
|
|
|
|
return .success
|
|
}
|
|
|
|
guard player.canPlay(game: game) else {
|
|
return .tableStateInvalid
|
|
}
|
|
phase = .playing
|
|
gameType = game
|
|
minimumPlayableGame = .none
|
|
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: OldPlayer) -> PlayerActionResult {
|
|
guard phase == .playing, !player.isGameLeader else {
|
|
return .tableStateInvalid
|
|
}
|
|
player.numberOfRaises += 1
|
|
players.forEach { $0.switchLeadership() }
|
|
didDoubleInCurrentRound = true
|
|
return .success
|
|
}
|
|
|
|
private func reset() {
|
|
phase = .waitingForPlayers
|
|
gameType = nil
|
|
minimumPlayableGame = .none
|
|
for player in players {
|
|
player.prepareForNewGame(isFirstPlayer: player.playsFirstCard)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension OldTable {
|
|
|
|
var isFull: Bool {
|
|
players.count == maximumPlayersPerTable
|
|
}
|
|
|
|
var publicInfo: PublicTableInfo {
|
|
.init(id: id, name: name, players: playerNames)
|
|
}
|
|
|
|
var playerNames: [PlayerName] {
|
|
players.map { $0.name }
|
|
}
|
|
|
|
}
|