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
final class Player {
final class OldPlayer {
let name: PlayerName
@ -28,9 +28,12 @@ final class Player {
/// The players plays/played the first card for the current trick
var startedCurrentTrick = false
/// Indicate the currently highest bidder during bidding
var isHighestBidder = false
/// The action available to the player
var actions: [Action] = []
var actions: [PlayerAction] = []
/// Indicates if the player doubled ("legen")
var didDoubleAfterFourCards: Bool? = nil
@ -111,7 +114,7 @@ final class Player {
return true
}
func canPerform(_ action: Action) -> Bool {
func canPerform(_ action: PlayerAction) -> Bool {
actions.contains(action)
}
@ -124,6 +127,7 @@ final class Player {
didDoubleAfterFourCards = nil
isStillBidding = true
isGameLeader = false
isHighestBidder = false
handCards = []
playedCard = nil
wonTricks = []
@ -184,6 +188,7 @@ final class Player {
func didPerformBid() {
isNextActor = false
isHighestBidder = true
actions = []
}
@ -211,6 +216,7 @@ final class Player {
func auctionEnded() {
actions = []
isStillBidding = false
isHighestBidder = false
isNextActor = false
}
@ -447,13 +453,9 @@ final class Player {
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
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
}
}

View File

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

View File

@ -1,35 +1,32 @@
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 can request cards to be dealt
case deal = "deal"
/// The player doubles on the initial four cards
case initialDoubleCost = "double"
/// 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 does not double on the initial four cards
case noDoubleCost = "skip"
/// The player offers a wedding (one trump card)
case offerWedding = "wedding"
/// The player offers a wedding (one trump card)
case offerWedding = "wedding"
/// The player can choose to accept the wedding
case acceptWedding = "accept"
/// 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 matches or increases the game during auction
case increaseOrMatchGame = "bid"
/// The player does not want to play
case withdrawFromAuction = "out"
/// 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
}
/// 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
}
}

View File

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

View File

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

View File

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