Refactor player action

This commit is contained in:
Christoph Hagen 2021-12-09 11:10:20 +01:00
parent a2c2893499
commit 33f72c43cf
6 changed files with 93 additions and 111 deletions

View File

@ -13,7 +13,7 @@ private let numberOfCardsToProtectAce = 4
let numberOfCardsPerPlayer = 8 let numberOfCardsPerPlayer = 8
final class Player { final class OldPlayer {
let name: PlayerName let name: PlayerName
@ -29,8 +29,11 @@ final class Player {
/// The players plays/played the first card for the current trick /// The players plays/played the first card for the current trick
var startedCurrentTrick = false var startedCurrentTrick = false
/// Indicate the currently highest bidder during bidding
var isHighestBidder = false
/// The action available to the player /// The action available to the player
var actions: [Action] = [] var actions: [PlayerAction] = []
/// Indicates if the player doubled ("legen") /// Indicates if the player doubled ("legen")
var didDoubleAfterFourCards: Bool? = nil var didDoubleAfterFourCards: Bool? = nil
@ -111,7 +114,7 @@ final class Player {
return true return true
} }
func canPerform(_ action: Action) -> Bool { func canPerform(_ action: PlayerAction) -> Bool {
actions.contains(action) actions.contains(action)
} }
@ -124,6 +127,7 @@ final class Player {
didDoubleAfterFourCards = nil didDoubleAfterFourCards = nil
isStillBidding = true isStillBidding = true
isGameLeader = false isGameLeader = false
isHighestBidder = false
handCards = [] handCards = []
playedCard = nil playedCard = nil
wonTricks = [] wonTricks = []
@ -184,6 +188,7 @@ final class Player {
func didPerformBid() { func didPerformBid() {
isNextActor = false isNextActor = false
isHighestBidder = true
actions = [] actions = []
} }
@ -211,6 +216,7 @@ final class Player {
func auctionEnded() { func auctionEnded() {
actions = [] actions = []
isStillBidding = false isStillBidding = false
isHighestBidder = false
isNextActor = false isNextActor = false
} }
@ -447,13 +453,9 @@ final class Player {
handCards[i].isPlayable = playable handCards[i].isPlayable = playable
} }
} }
func info(masked: Bool, positionInTrick: Int) -> PlayerInfo {
.init(player: self, isMasked: masked, trickPosition: positionInTrick)
}
} }
extension Player { extension OldPlayer {
/// Indicate that the player is connected when at a table /// Indicate that the player is connected when at a table
var isConnected: Bool { var isConnected: Bool {
@ -468,9 +470,9 @@ extension Player {
} }
} }
extension Player: Equatable { extension OldPlayer: Equatable {
static func == (lhs: Player, rhs: Player) -> Bool { static func == (lhs: OldPlayer, rhs: OldPlayer) -> Bool {
lhs.name == rhs.name lhs.name == rhs.name
} }
} }

View File

@ -8,7 +8,7 @@ private extension Int {
} }
} }
final class Table { final class OldTable {
let id: TableId let id: TableId
@ -16,13 +16,13 @@ final class Table {
let isPublic: Bool let isPublic: Bool
var players: [Player] = [] var players: [OldPlayer] = []
var phase: GamePhase = .waitingForPlayers var phase: GamePhase = .waitingForPlayers
var gameType: GameType? = nil var gameType: GameType? = nil
var minimumPlayableGame: GameType.GameClass? var minimumPlayableGame: GameType.GameClass = .none
/// 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
@ -53,7 +53,7 @@ final class Table {
players.filter { $0.isStillBidding }.count players.filter { $0.isStillBidding }.count
} }
var auctionWinner: Player { var auctionWinner: OldPlayer {
players.first { $0.isStillBidding }! players.first { $0.isStillBidding }!
} }
@ -93,7 +93,7 @@ final class Table {
guard !isFull else { guard !isFull else {
return false return false
} }
let player = Player(name: player) let player = OldPlayer(name: player)
players.append(player) players.append(player)
if isFull { if isFull {
prepareTableForFirstGame() prepareTableForFirstGame()
@ -107,11 +107,11 @@ final class Table {
} }
/// The player to play the first card of the current game /// The player to play the first card of the current game
var firstPlayer: Player { var firstPlayer: OldPlayer {
players.first { $0.playsFirstCard }! players.first { $0.playsFirstCard }!
} }
func select(player: PlayerName) -> Player? { func select(player: PlayerName) -> OldPlayer? {
players.first { $0.name == player } players.first { $0.name == player }
} }
@ -119,42 +119,24 @@ final class Table {
players.firstIndex { $0.startedCurrentTrick }! players.firstIndex { $0.startedCurrentTrick }!
} }
func playerInfo(at index: Int, masked: Bool) -> PlayerInfo? { func player(at index: Int) -> OldPlayer? {
let starter = indexOfTrickStarter
let layer = (4 - starter + index) % 4
return player(at: index)?.info(masked: masked, positionInTrick: layer)
}
func playerInfo(leftOf index: Int, masked: Bool) -> PlayerInfo? {
playerInfo(at: (index + 1) % 4, masked: masked)
}
func playerInfo(acrossOf index: Int, masked: Bool) -> PlayerInfo? {
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? {
guard index < players.count else { guard index < players.count else {
return nil return nil
} }
return players[index] return players[index]
} }
func index(of player: Player) -> Int { func index(of player: OldPlayer) -> Int {
players.firstIndex(of: player)! players.firstIndex(of: player)!
} }
func nextPlayer(after player: Player) -> Player { func nextPlayer(after player: OldPlayer) -> OldPlayer {
let i = index(of: player) let i = index(of: player)
let newIndex = (i + 1) % maximumPlayersPerTable let newIndex = (i + 1) % maximumPlayersPerTable
return players[newIndex] return players[newIndex]
} }
func nextBidder(after player: Player) -> Player { func nextBidder(after player: OldPlayer) -> OldPlayer {
// Find next player to place bid // Find next player to place bid
let index = index(of: player) let index = index(of: player)
for i in 1..<4 { for i in 1..<4 {
@ -167,6 +149,11 @@ final class Table {
return player return player
} }
func availableGames(forPlayerAt index: Int) -> [GameType] {
return []
}
func remove(player: PlayerName) { func remove(player: PlayerName) {
guard let index = players.firstIndex(where: { $0.name == player }) else { guard let index = players.firstIndex(where: { $0.name == player }) else {
return return
@ -202,7 +189,7 @@ final class Table {
private func prepareTableForFirstGame() { private func prepareTableForFirstGame() {
phase = .waitingForPlayers phase = .waitingForPlayers
gameType = nil gameType = nil
minimumPlayableGame = nil // Not relevant in this phase minimumPlayableGame = .none // Not relevant in this phase
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 {
@ -211,13 +198,7 @@ final class Table {
} }
private func sendUpdateToAllPlayers() { 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 // MARK: Player actions
@ -285,7 +266,7 @@ final class Table {
didDoubleInCurrentRound = false didDoubleInCurrentRound = false
} }
func perform(action: Player.Action, forPlayer player: PlayerName) -> PlayerActionResult { func perform(action: PlayerAction, 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)") print("Player \(player) wants to \(action.path), but only allowed: \(player.actions)")
@ -332,7 +313,7 @@ final class Table {
return .success return .success
} }
func perform(double: Bool, forPlayer player: Player) -> PlayerActionResult { func perform(double: Bool, forPlayer player: OldPlayer) -> PlayerActionResult {
player.didDouble(double) player.didDouble(double)
guard allPlayersFinishedDoubling else { guard allPlayersFinishedDoubling else {
return .success return .success
@ -351,17 +332,17 @@ final class Table {
player.assignRemainingCards(cards[index]) player.assignRemainingCards(cards[index])
} }
players.forEach { $0.startAuction() } players.forEach { $0.startAuction() }
minimumPlayableGame = nil minimumPlayableGame = .none
phase = .bidding phase = .bidding
} }
private func performWeddingCall(forPlayer player: Player) -> PlayerActionResult { private func performWeddingCall(forPlayer player: OldPlayer) -> PlayerActionResult {
guard phase == .bidding else { guard phase == .bidding else {
print("Invalid phase \(phase) for wedding call") print("Invalid phase \(phase) for wedding call")
return .tableStateInvalid return .tableStateInvalid
} }
guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else { guard minimumPlayableGame.allowsWedding else {
print("Invalid minimum game \(minimumPlayableGame!) for wedding call") print("Invalid minimum game \(minimumPlayableGame) for wedding call")
return .tableStateInvalid return .tableStateInvalid
} }
guard player.canOfferWedding else { guard player.canOfferWedding else {
@ -381,7 +362,7 @@ final class Table {
return .success return .success
} }
private func performBidIncrease(forPlayer player: Player) -> PlayerActionResult { private func performBidIncrease(forPlayer player: OldPlayer) -> PlayerActionResult {
guard phase == .bidding else { guard phase == .bidding else {
return .tableStateInvalid return .tableStateInvalid
} }
@ -392,37 +373,41 @@ final class Table {
guard player.isNextActor else { guard player.isNextActor else {
return .tableStateInvalid return .tableStateInvalid
} }
if minimumPlayableGame == nil { // TODO: Check if new player sits before old player
minimumPlayableGame = .ruf // then don't increase game
} else { minimumPlayableGame.increase()
minimumPlayableGame!.increase() if !minimumPlayableGame.allowsWedding {
// Remove wedding offers // Remove wedding offers
players.forEach { $0.weddingOutbid() } players.forEach { $0.weddingOutbid() }
} }
#warning("Fix bidding")
// TODO: Remove highest bidder from old player
player.didPerformBid() player.didPerformBid()
if numberOfRemainingBidders == 1 { if numberOfRemainingBidders == 1 {
selectGame(player: player) selectGame(player: player)
return .success
} }
// Find next player to place bid // Find next player to place bid
nextBidder(after: player).requiresBid(hasWedding: false) nextBidder(after: player).requiresBid(hasWedding: false)
return .success return .success
} }
private func handleWeddingOutbid(forPlayer player: Player) -> PlayerActionResult { private func handleWeddingOutbid(forPlayer player: OldPlayer) -> PlayerActionResult {
if player.offersWedding { if player.offersWedding {
// A player offering a wedding can't outbid itself // A player offering a wedding can't outbid itself
return .tableStateInvalid return .tableStateInvalid
} }
players.forEach { $0.weddingOutbid() } players.forEach { $0.weddingOutbid() }
nextBidder(after: player).requiresBid(hasWedding: true) nextBidder(after: player).requiresBid(hasWedding: false)
return .success return .success
} }
private func handleWeddingAccept(forPlayer player: Player) -> PlayerActionResult { private func handleWeddingAccept(forPlayer player: OldPlayer) -> PlayerActionResult {
guard phase == .bidding else { guard phase == .bidding else {
return .tableStateInvalid return .tableStateInvalid
} }
guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else { guard minimumPlayableGame.allowsWedding else {
return .tableStateInvalid return .tableStateInvalid
} }
guard weddingOfferExists else { guard weddingOfferExists else {
@ -450,19 +435,19 @@ final class Table {
return .success return .success
} }
private func selectedWedding(player: Player) { private func selectedWedding(player: OldPlayer) {
minimumPlayableGame = nil minimumPlayableGame = .none
gameType = .hochzeit gameType = .hochzeit
phase = .selectWeddingCard phase = .selectWeddingCard
players.forEach { $0.auctionEnded() } players.forEach { $0.auctionEnded() }
player.mustSelectWeddingCard() player.mustSelectWeddingCard()
} }
private func selectedCardForWedding(card: Card, player: Player) -> PlayCardResult { private func selectedCardForWedding(card: Card, player: OldPlayer) -> PlayCardResult {
guard player.isNextActor, guard player.isNextActor,
player.wouldAcceptWedding, player.wouldAcceptWedding,
weddingOfferExists else { weddingOfferExists else {
return .invalidTableState return .tableStateInvalid
} }
guard !card.isTrump(in: .hochzeit), guard !card.isTrump(in: .hochzeit),
player.has(card: card) else { player.has(card: card) else {
@ -481,7 +466,7 @@ final class Table {
return .success return .success
} }
private func performWithdrawl(forPlayer player: Player) -> PlayerActionResult { private func performWithdrawl(forPlayer player: OldPlayer) -> PlayerActionResult {
guard phase == .bidding, guard phase == .bidding,
player.isNextActor, player.isNextActor,
player.isStillBidding else { player.isStillBidding else {
@ -490,7 +475,7 @@ final class Table {
player.withdrawFromBidding() player.withdrawFromBidding()
switch numberOfRemainingBidders { switch numberOfRemainingBidders {
case 1: case 1:
if minimumPlayableGame != nil { if minimumPlayableGame != .none {
// Will only be called when at least one player placed a bid // Will only be called when at least one player placed a bid
selectGame(player: auctionWinner) selectGame(player: auctionWinner)
return .success return .success
@ -515,7 +500,7 @@ final class Table {
prepareTableForFirstGame() prepareTableForFirstGame()
} }
private func selectGame(player: Player) { private func selectGame(player: OldPlayer) {
gameType = nil gameType = nil
phase = .selectGame phase = .selectGame
players.forEach { $0.auctionEnded() } players.forEach { $0.auctionEnded() }
@ -527,14 +512,14 @@ final class Table {
guard phase == .selectGame, player.selectsGame, game != .hochzeit else { guard phase == .selectGame, player.selectsGame, game != .hochzeit else {
return .tableStateInvalid return .tableStateInvalid
} }
guard minimumPlayableGame == nil || game.gameClass >= minimumPlayableGame! else { guard game.gameClass >= minimumPlayableGame else {
return .tableStateInvalid return .tableStateInvalid
} }
defer { sendUpdateToAllPlayers() } defer { sendUpdateToAllPlayers() }
guard let suit = game.calledSuit else { guard let suit = game.calledSuit else {
phase = .playing phase = .playing
gameType = game gameType = game
minimumPlayableGame = nil minimumPlayableGame = .none
players.forEach { $0.start(game: game) } players.forEach { $0.start(game: game) }
player.switchLeadership() player.switchLeadership()
@ -546,7 +531,7 @@ final class Table {
} }
phase = .playing phase = .playing
gameType = game gameType = game
minimumPlayableGame = nil minimumPlayableGame = .none
players.forEach { $0.start(game: game) } players.forEach { $0.start(game: game) }
player.switchLeadership() player.switchLeadership()
// Find called player // Find called player
@ -555,7 +540,7 @@ final class Table {
return .success return .success
} }
private func performDoubleDuringGame(forPlayer player: Player) -> PlayerActionResult { private func performDoubleDuringGame(forPlayer player: OldPlayer) -> PlayerActionResult {
guard phase == .playing, !player.isGameLeader else { guard phase == .playing, !player.isGameLeader else {
return .tableStateInvalid return .tableStateInvalid
} }
@ -568,14 +553,14 @@ final class Table {
private func reset() { private func reset() {
phase = .waitingForPlayers phase = .waitingForPlayers
gameType = nil gameType = nil
minimumPlayableGame = nil minimumPlayableGame = .none
for player in players { for player in players {
player.prepareForNewGame(isFirstPlayer: player.playsFirstCard) player.prepareForNewGame(isFirstPlayer: player.playsFirstCard)
} }
} }
} }
extension Table { extension OldTable {
var isFull: Bool { var isFull: Bool {
players.count == maximumPlayersPerTable players.count == maximumPlayersPerTable
@ -589,11 +574,4 @@ extension Table {
players.map { $0.name } 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)
}
} }

View File

@ -1,35 +1,32 @@
import Foundation import Foundation
extension Player { enum PlayerAction: String, Codable {
/// The player can request cards to be dealt
case deal = "deal"
enum Action: String, Codable { /// The player doubles on the initial four cards
/// The player can request cards to be dealt case initialDoubleCost = "double"
case deal = "deal"
/// The player doubles on the initial four cards /// The player does not double on the initial four cards
case initialDoubleCost = "double" case noDoubleCost = "skip"
/// The player does not double on the initial four cards /// The player offers a wedding (one trump card)
case noDoubleCost = "skip" case offerWedding = "wedding"
/// The player offers a wedding (one trump card) /// The player can choose to accept the wedding
case offerWedding = "wedding" case acceptWedding = "accept"
/// The player can choose to accept the wedding /// The player matches or increases the game during auction
case acceptWedding = "accept" case increaseOrMatchGame = "bid"
/// The player matches or increases the game during auction /// The player does not want to play
case increaseOrMatchGame = "bid" case withdrawFromAuction = "out"
/// The player does not want to play /// The player claims to win and doubles the game cost ("schießen")
case withdrawFromAuction = "out" case doubleDuringGame = "raise"
/// The player claims to win and doubles the game cost ("schießen") /// The url path for the client to call (e.g. /player/deal)
case doubleDuringGame = "raise" var path: String {
rawValue
/// The url path for the client to call (e.g. /player/deal)
var path: String {
rawValue
}
} }
} }

View File

@ -8,7 +8,7 @@ enum PlayCardResult {
case noTableJoined case noTableJoined
case invalidTableState case tableStateInvalid
case invalidCard case invalidCard
} }

View File

@ -12,4 +12,6 @@ enum PlayerActionResult {
case tableNotFull case tableNotFull
case tableStateInvalid case tableStateInvalid
case invalidCard
} }

View File

@ -284,7 +284,7 @@ func routes(_ app: Application) throws {
throw Abort(.badRequest) throw Abort(.badRequest)
} }
let result: PlayerActionResult let result: PlayerActionResult
if let action = Player.Action(rawValue: actionString) { if let action = PlayerAction(rawValue: actionString) {
result = database.performAction(playerToken: token, action: action) result = database.performAction(playerToken: token, action: action)
} else if let game = GameType(rawValue: actionString) { } else if let game = GameType(rawValue: actionString) {
result = database.select(game: game, playerToken: token) result = database.select(game: game, playerToken: token)
@ -302,6 +302,8 @@ func routes(_ app: Application) throws {
throw Abort(.preconditionFailed) // 412 throw Abort(.preconditionFailed) // 412
case .tableStateInvalid: case .tableStateInvalid:
throw Abort(.preconditionFailed) // 412 throw Abort(.preconditionFailed) // 412
case .invalidCard:
throw Abort(.preconditionFailed) // 412
} }
} }
@ -318,11 +320,12 @@ func routes(_ app: Application) throws {
throw Abort(.unauthorized) // 401 throw Abort(.unauthorized) // 401
case .noTableJoined: case .noTableJoined:
throw Abort(.preconditionFailed) // 412 throw Abort(.preconditionFailed) // 412
case .invalidTableState: case .tableStateInvalid:
throw Abort(.preconditionFailed) // 412 throw Abort(.preconditionFailed) // 412
case .invalidCard: case .invalidCard:
throw Abort(.preconditionFailed) // 412 throw Abort(.preconditionFailed) // 412
case .tableNotFull:
throw Abort(.preconditionFailed) // 412
} }
} }
} }