Refactor tables and players for clarity

This commit is contained in:
Christoph Hagen 2021-12-09 11:11:17 +01:00
parent 33f72c43cf
commit 289458bfd8
17 changed files with 718 additions and 64 deletions

View File

@ -2,39 +2,35 @@ import Foundation
struct PlayerInfo: Codable, Equatable { struct PlayerInfo: Codable, Equatable {
/// The name of the player
let name: PlayerName let name: PlayerName
let connected: Bool /// Indicates that the player is active, i.e. a session is established
let isConnected: Bool
/// The player is the next one to perform an action /// The player is the next one to perform an action
let active: Bool let isNextActor: Bool
let selectsGame: Bool
/// The cards in the hand of the player
let cards: [CardInfo]
/// The action the player can perform
let actions: [String]
/// The card which the player added to the current trick
let playedCard: CardId? let playedCard: CardId?
/// The height of the player card on the table stack /// The height of the player card on the table stack
let position: Int let positionInTrick: Int
init(player: Player, isMasked: Bool, trickPosition: Int) { init(player: Player, isNextActor: Bool, position: Int) {
self.name = player.name self.name = player.name
self.connected = player.isConnected self.isConnected = player.isConnected
self.active = player.isNextActor self.isNextActor = isNextActor
self.selectsGame = player.selectsGame self.positionInTrick = position
self.playedCard = player.playedCard?.id self.playedCard = player.playedCard?.id
self.position = trickPosition }
if isMasked {
self.cards = [] /// Convert the property names into shorter strings for JSON encoding
self.actions = [] enum CodingKeys: String, CodingKey {
} else { case name = "name"
self.actions = player.actions.map { $0.path } case isConnected = "connected"
self.cards = player.handCards.map { $0.cardInfo } case isNextActor = "active"
} case playedCard = "card"
case positionInTrick = "position"
} }
} }

View File

@ -16,21 +16,29 @@ struct TableInfo: Codable {
let playableGames: [GameId] let playableGames: [GameId]
init(_ table: Table, forPlayerAt playerIndex: Int) { /// The cards in the hand of the player
let player = table.player(at: playerIndex)! let cards: [CardInfo]
self.id = table.id
self.name = table.name
self.player = table.playerInfo(at: playerIndex, masked: false)!
self.playerLeft = table.playerInfo(leftOf: playerIndex, masked: true)
self.playerAcross = table.playerInfo(acrossOf: playerIndex, masked: true)
self.playerRight = table.playerInfo(rightOf: playerIndex, masked: true)
if table.phase == .bidding || table.phase == .selectGame { /// The action the player can perform
let games = table.minimumPlayableGame?.availableGames ?? GameType.allCases let actions: [String]
self.playableGames = games.filter(player.canPlay).map { $0.id }
} else { let playerSelectsGame: Bool
self.playableGames = []
} init(id: String, name: String,
own: PlayerInfo, left: PlayerInfo?,
across: PlayerInfo?, right: PlayerInfo?,
games: [GameId] = [], actions: [PlayerAction],
cards: [PlayableCard], selectGame: Bool = false) {
self.id = id
self.name = name
self.player = own
self.playerLeft = left
self.playerAcross = across
self.playerRight = right
self.playableGames = games
self.actions = actions.map { $0.path }
self.cards = cards.map { $0.cardInfo }
self.playerSelectsGame = selectGame
} }
} }

View File

@ -99,7 +99,7 @@ final class Database {
return true return true
} }
func performAction(playerToken: SessionToken, action: Player.Action) -> PlayerActionResult { func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult {
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
return .invalidToken return .invalidToken
} }
@ -113,7 +113,7 @@ final class Database {
return tables.select(game: game, player: player) return tables.select(game: game, player: player)
} }
func play(card: Card, playerToken: SessionToken) -> PlayCardResult { func play(card: Card, playerToken: SessionToken) -> PlayerActionResult {
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
return .invalidToken return .invalidToken
} }

View File

@ -48,7 +48,7 @@ final class TableManagement: DiskWriter {
} }
} }
entries.forEach { id, tableData in entries.forEach { id, tableData in
let table = Table(id: id, name: tableData.name, isPublic: tableData.isPublic) let table = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic)
tableData.players.forEach { _ = table.add(player: $0) } tableData.players.forEach { _ = table.add(player: $0) }
tables[id] = table tables[id] = table
} }
@ -65,8 +65,10 @@ final class TableManagement: DiskWriter {
@discardableResult @discardableResult
private func writeTableToDisk(table: Table) -> Bool { private func writeTableToDisk(table: Table) -> Bool {
let visible = table.isPublic ? "public" : "private" let visible = table.isPublic ? "public" : "private"
let players = table.playerNames.joined(separator: ",") let players = table.playerNames
let entry = [table.id, table.name, visible, players].joined(separator: ":") .joined(separator: ",")
let entry = [table.id, table.name, visible, players]
.joined(separator: ":")
return writeToDisk(line: entry) return writeToDisk(line: entry)
} }
@ -79,7 +81,8 @@ final class TableManagement: DiskWriter {
*/ */
@discardableResult @discardableResult
private func writeTableDeletionEntry(tableId: TableId) -> Bool { private func writeTableDeletionEntry(tableId: TableId) -> Bool {
let entry = [tableId, "", "", ""].joined(separator: ":") let entry = [tableId, "", "", ""]
.joined(separator: ":")
return writeToDisk(line: entry) return writeToDisk(line: entry)
} }
@ -91,11 +94,11 @@ final class TableManagement: DiskWriter {
- Returns: The table id - Returns: The table id
*/ */
func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo { func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo {
let table = Table(newTable: name, isPublic: isPublic) let table = WaitingTable(newTable: name, isPublic: isPublic)
_ = table.add(player: player) _ = table.add(player: player)
tables[table.id] = table tables[table.id] = table
writeTableToDisk(table: table) writeTableToDisk(table: table)
return table.compileInfo(for: player)! return table.tableInfo(forPlayer: player)
} }
/// A list of all public tables /// A list of all public tables
@ -109,7 +112,7 @@ final class TableManagement: DiskWriter {
- Returns: The table info, if the player has joined a table - Returns: The table info, if the player has joined a table
*/ */
func tableInfo(player: PlayerName) -> TableInfo? { func tableInfo(player: PlayerName) -> TableInfo? {
currentTable(for: player)?.compileInfo(for: player) currentTable(for: player)?.tableInfo(forPlayer: player)
} }
private func currentTable(for player: PlayerName) -> Table? { private func currentTable(for player: PlayerName) -> Table? {
@ -127,16 +130,20 @@ final class TableManagement: DiskWriter {
guard existing.id == tableId else { guard existing.id == tableId else {
return .failure(.alreadyJoinedOtherTable) return .failure(.alreadyJoinedOtherTable)
} }
return .success(existing.compileInfo(for: player)!) return .success(existing.tableInfo(forPlayer: player))
} }
guard let table = tables[tableId] else { guard let table = tables[tableId] else {
return .failure(.tableNotFound) return .failure(.tableNotFound)
} }
guard table.add(player: player) else { guard let joinableTable = table as? WaitingTable else {
return .failure(.tableIsFull)
}
guard joinableTable.add(player: player) else {
return .failure(.tableIsFull) return .failure(.tableIsFull)
} }
writeTableToDisk(table: table) writeTableToDisk(table: table)
return .success(table.compileInfo(for: player)!) joinableTable.sendUpdateToAllPlayers()
return .success(joinableTable.tableInfo(forPlayer: player))
} }
/** /**
@ -144,10 +151,12 @@ final class TableManagement: DiskWriter {
- Parameter player: The name of the player - Parameter player: The name of the player
*/ */
func leaveTable(player: PlayerName) { func leaveTable(player: PlayerName) {
guard let table = currentTable(for: player) else { guard let oldTable = currentTable(for: player) else {
return return
} }
table.remove(player: player) let table = WaitingTable(oldTable: oldTable, removing: player)
tables[table.id] = table
table.sendUpdateToAllPlayers()
writeTableToDisk(table: table) writeTableToDisk(table: table)
} }
@ -165,26 +174,60 @@ final class TableManagement: DiskWriter {
table.disconnect(player: player) table.disconnect(player: player)
} }
func performAction(player: PlayerName, action: Player.Action) -> PlayerActionResult { func performAction(player: PlayerName, action: PlayerAction) -> 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") print("Player \(player) wants to \(action.path), but no table joined")
return .noTableJoined return .noTableJoined
} }
return table.perform(action: action, forPlayer: player) let (result, newTable) = table.perform(action: action, forPlayer: player)
guard result == .success else {
return result
}
guard let newTable = newTable else {
table.sendUpdateToAllPlayers()
return .success
}
tables[newTable.id] = newTable
newTable.sendUpdateToAllPlayers()
return .success
} }
func select(game: GameType, player: PlayerName) -> PlayerActionResult { func select(game: GameType, player: PlayerName) -> PlayerActionResult {
guard let table = currentTable(for: player) else { guard let aTable = currentTable(for: player) else {
print("Player \(player) wants to play \(game.rawValue), but no table joined") print("Player \(player) wants to play \(game.rawValue), but no table joined")
return .noTableJoined return .noTableJoined
} }
return table.select(game: game, player: player) guard let table = aTable as? BiddingTable else {
return .tableStateInvalid
}
let (result, newTable) = table.select(game: game, player: player)
guard result == .success else {
return result
}
guard let newTable = newTable else {
print("Game selected by \(player), but no playing table \(table.name) created")
table.sendUpdateToAllPlayers()
return result
}
tables[newTable.id] = newTable
newTable.sendUpdateToAllPlayers()
return .success
} }
func play(card: Card, player: PlayerName) -> PlayCardResult { func play(card: Card, player: PlayerName) -> PlayerActionResult {
guard let table = currentTable(for: player) else { guard let table = currentTable(for: player) else {
return .noTableJoined return .noTableJoined
} }
return table.play(card: card, player: player) let (result, newTable) = table.play(card: card, player: player)
guard result == .success else {
return result
}
guard let newTable = newTable else {
table.sendUpdateToAllPlayers()
return .success
}
tables[newTable.id] = newTable
newTable.sendUpdateToAllPlayers()
return .success
} }
} }

View File

@ -18,6 +18,7 @@ extension GameType {
} }
enum GameClass: Int { enum GameClass: Int {
case none = 0
case ruf = 1 case ruf = 1
case bettel = 2 case bettel = 2
case wenzGeier = 3 case wenzGeier = 3
@ -25,6 +26,7 @@ extension GameType {
var cost: Int { var cost: Int {
switch self { switch self {
case .none: return 0
case .ruf: return 5 case .ruf: return 5
case .bettel: return 15 case .bettel: return 15
case .wenzGeier, .solo: return 20 case .wenzGeier, .solo: return 20
@ -38,9 +40,18 @@ extension GameType {
self = .init(rawValue: rawValue + 1)! self = .init(rawValue: rawValue + 1)!
} }
var allowsWedding: Bool {
switch self {
case .none, .ruf:
return true
default:
return false
}
}
var availableGames: [GameType] { var availableGames: [GameType] {
switch self { switch self {
case .ruf: case .none, .ruf:
return GameType.allCases return GameType.allCases
case .bettel: case .bettel:
return [.bettel, .wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln] return [.bettel, .wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln]

View File

@ -0,0 +1,26 @@
import Foundation
import WebSocketKit
class AbstractPlayer {
let name: PlayerName
var socket: WebSocket?
init(name: PlayerName, socket: WebSocket? = nil) {
self.name = name
self.socket = socket
}
init(player: Player) {
self.name = player.name
self.socket = player.socket
}
}
extension AbstractPlayer: Equatable {
static func == (lhs: AbstractPlayer, rhs: AbstractPlayer) -> Bool {
lhs.name == rhs.name
}
}

View File

@ -0,0 +1,51 @@
import Foundation
import WebSocketKit
final class BiddingPlayer {
let name: String
var socket: WebSocket?
let cards: [PlayableCard]
var isStillBidding = true
var isAllowedToOfferWedding = true
var offersWedding = false
var wouldAcceptWedding = false
init(player: DealingPlayer, cards: [PlayableCard]) {
self.name = player.name
self.socket = player.socket
self.cards = cards
}
}
extension BiddingPlayer: Player {
var canOfferWedding: Bool {
rawCards.canOfferWedding
}
var rawCards: [Card] {
cards.map { $0.card }
}
var actions: [PlayerAction] {
guard isStillBidding else {
return []
}
guard canOfferWedding, isAllowedToOfferWedding, !offersWedding else {
return [.increaseOrMatchGame, .withdrawFromAuction]
}
return [.increaseOrMatchGame, .withdrawFromAuction, .offerWedding]
}
var playedCard: Card? {
nil
}
}

View File

@ -0,0 +1,24 @@
import Foundation
import WebSocketKit
final class DealingPlayer: AbstractPlayer {
var cards: [PlayableCard] = []
var didDouble: Bool? = nil
init(player: WaitingPlayer) {
super.init(player: player)
}
}
extension DealingPlayer: Player {
var actions: [PlayerAction] {
didDouble == nil ? [.initialDoubleCost, .noDoubleCost] : []
}
var playedCard: Card? {
nil
}
}

View File

@ -0,0 +1,63 @@
import Foundation
import WebSocketKit
protocol Player: AnyObject {
var name: String { get }
var socket: WebSocket? { get set }
var playedCard: Card? { get }
var actions: [PlayerAction] { get }
var cards: [PlayableCard] { get }
}
extension Player {
// MARK: Connection
/// Indicate that the player is connected when at a table
var isConnected: Bool {
guard let socket = socket else {
return false
}
guard !socket.isClosed else {
self.socket = nil
return false
}
return true
}
func connect(using socket: WebSocket) {
_ = self.socket?.close()
self.socket = socket
}
func disconnect() -> Bool {
guard let socket = socket else {
return false
}
do {
try socket.close().wait()
} catch {
print("Failed to close socket for player: \(name): \(error)")
}
self.socket = nil
return true
}
func send(_ info: TableInfo) {
try? socket?.send(encodeJSON(info))
}
// MARK: Actions
func canPerform(_ action: PlayerAction) -> Bool {
actions.contains(action)
}
}

View File

@ -0,0 +1,30 @@
import Foundation
import WebSocketKit
final class PlayingPlayer: AbstractPlayer {
var playedCard: Card? = nil
var cards: [PlayableCard]
var leadsGame = false
var canStillRaise = true
init(player: BiddingPlayer) {
self.cards = player.cards
super.init(player: player)
}
}
extension PlayingPlayer: Player {
var actions: [PlayerAction] {
guard canStillRaise, !leadsGame else {
return []
}
return [.doubleDuringGame]
}
}

View File

@ -0,0 +1,22 @@
import Foundation
import WebSocketKit
final class WaitingPlayer: AbstractPlayer {
var canStartGame: Bool = false
}
extension WaitingPlayer: Player {
var actions: [PlayerAction] {
canStartGame ? [.deal] : []
}
var cards: [PlayableCard] {
[]
}
var playedCard: Card? {
nil
}
}

View File

@ -0,0 +1,25 @@
import Foundation
class AbstractTable {
/// The unique id of the table
let id: TableId
/// The name of the table
let name: TableName
/// Indicates that the table is visible to all players, and can be joined by anyone
let isPublic: Bool
init(table: AbstractTable) {
self.id = table.id
self.name = table.name
self.isPublic = table.isPublic
}
init(id: TableId, name: TableName, isPublic: Bool) {
self.id = id
self.name = name
self.isPublic = isPublic
}
}

View File

@ -0,0 +1,51 @@
import Foundation
final class BiddingTable: AbstractTable {
var players: [BiddingPlayer]
var hasSelectedGame: Bool {
// TODO: Implement
false
}
init(table: DealingTable) {
self.players = table.players.map {
BiddingPlayer(player: $0, cards: [])
}
super.init(table: table)
}
func select(game: GameType, player: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// TODO: Implement
return (.tableStateInvalid, nil)
}
func makePlayingTable() -> PlayingTable {
// TODO: Implement
fatalError()
}
}
extension BiddingTable: Table {
var allPlayers: [Player] {
players
}
var indexOfNextActor: Int {
// TODO: Implement
return 0
}
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// TODO: Implement bidding actions
return (.tableStateInvalid, nil)
}
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// TODO: Implement for wedding
return (.tableStateInvalid, nil)
}
}

View File

@ -0,0 +1,39 @@
import Foundation
final class DealingTable: AbstractTable {
var players: [DealingPlayer]
init(table: WaitingTable) {
self.players = table.players.map(DealingPlayer.init)
super.init(table: table)
let cards = Dealer.dealFirstCards()
for (index, player) in players.enumerated() {
player.cards = cards[index].map { .init(card: $0, isPlayable: false) }
}
}
}
extension DealingTable: Table {
var allPlayers: [Player] {
players
}
var indexOfNextActor: Int {
// TODO: Implement
return 0
}
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// TODO: Implement doubling, additional cards
return (.tableStateInvalid, nil)
}
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// No cards playable while dealing
(.tableStateInvalid, nil)
}
}

View File

@ -0,0 +1,34 @@
import Foundation
final class PlayingTable: AbstractTable {
var players: [PlayingPlayer]
init(table: BiddingTable) {
self.players = table.players.map(PlayingPlayer.init)
super.init(table: table)
}
}
extension PlayingTable: Table {
var allPlayers: [Player] {
players
}
var indexOfNextActor: Int {
// TODO: Implement
return 0
}
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// TODO: Implement raises
return (.tableStateInvalid, nil)
}
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// TODO: Implement playing of cards
return (.tableStateInvalid, nil)
}
}

View File

@ -0,0 +1,121 @@
import Foundation
import WebSocketKit
protocol Table: AbstractTable {
/// The unique id of the table
var id: TableId { get }
/// The name of the table
var name: TableName { get }
/// The table is visible in the list of tables and can be joined by anyone
var isPublic: Bool { get }
/**
The players sitting at the table.
The players are ordered clockwise around the table, with the first player starting the game.
*/
var allPlayers: [Player] { get }
var indexOfNextActor: Int { get }
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?)
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?)
}
extension Table {
var playerNames: [String] {
allPlayers.map { $0.name }
}
func index(of player: PlayerName) -> Int {
allPlayers.firstIndex { $0.name == player }!
}
func player(named name: PlayerName) -> Player? {
allPlayers.first { $0.name == name }
}
func contains(player: PlayerName) -> Bool {
allPlayers.contains { $0.name == player }
}
// MARK: Connection
func sendUpdateToAllPlayers() {
allPlayers.enumerated().forEach { playerIndex, player in
guard player.isConnected else {
return
}
let info = self.tableInfo(forPlayerAt: playerIndex)
player.send(info)
}
}
func connect(player name: PlayerName, using socket: WebSocket) -> Bool {
guard let player = player(named: name) else {
return false
}
player.connect(using: socket)
sendUpdateToAllPlayers()
return true
}
func disconnect(player name: PlayerName) {
guard let player = player(named: name) else {
return
}
guard player.disconnect() else {
return
}
sendUpdateToAllPlayers()
return
}
// MARK: Client info
var publicInfo: PublicTableInfo {
.init(id: id, name: name, players: playerNames)
}
private func player(forIndex index: Int) -> Player? {
let players = allPlayers
guard index < players.count else {
return nil
}
return players[index]
}
private func playerInfo(forIndex index: Int) -> PlayerInfo? {
guard let player = player(forIndex: index) else {
return nil
}
let isNext = indexOfNextActor == index
return PlayerInfo(player: player, isNextActor: isNext, position: index)
}
func tableInfo(forPlayer player: PlayerName) -> TableInfo {
let index = index(of: player)
return tableInfo(forPlayerAt: index)
}
func tableInfo(forPlayerAt index: Int) -> TableInfo {
let player = player(forIndex: index)!
let own = playerInfo(forIndex: index)!
let left = playerInfo(forIndex: (index + 1) % 4)
let across = playerInfo(forIndex: (index + 2) % 4)
let right = playerInfo(forIndex: (index + 3) % 4)
return .init(
id: id, name: name,
own: own, left: left,
across: across, right: right,
actions: player.actions,
cards: player.cards)
}
}

View File

@ -0,0 +1,110 @@
import Foundation
/**
Represents a table where players are still joining and leaving.
*/
final class WaitingTable: AbstractTable {
/**
The players sitting at the table.
The players are ordered clockwise around the table, with the first player starting the game.
*/
var players: [WaitingPlayer] = []
/// The table contains enough players to start a game
var isFull: Bool {
players.count >= maximumPlayersPerTable
}
override init(id: TableId, name: TableName, isPublic: Bool) {
super.init(id: id, name: name, isPublic: isPublic)
}
/**
Create a new table.
- Parameter name: The name of the table
- Parameter isPublic: The table is visible and joinable by everyone
*/
init(newTable name: TableName, isPublic: Bool) {
super.init(id: .newToken(), name: name, isPublic: isPublic)
}
/**
Convert another table to a waiting table.
This is needed when a player leaves an active table.
- Parameter oldTable: The table to convert
- Parameter player: The player to remove from the table.
*/
init(oldTable: Table, removing player: PlayerName) {
self.players = oldTable.allPlayers
.filter { $0.name != player }
.map(WaitingPlayer.init)
super.init(table: oldTable)
}
/**
Add a player to the table.
- Parameter player: The name of the player to add
- Returns: `true`, if the player could be added, `false` if the table is full
*/
func add(player: PlayerName) -> Bool {
guard !isFull else {
return false
}
let player = WaitingPlayer(name: player)
players.append(player)
// Allow dealing of cards if table is full
if isFull {
players.forEach { $0.canStartGame = true }
}
return true
}
/**
Perform an action on the waiting table.
Only dealing is a valid action (if the table is full)
- Parameter action: The action to perform
- Parameter player: The name of the player
*/
func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// Only dealing is allowed...
guard action == .deal else {
return (.tableStateInvalid, nil)
}
// and only when table is full
guard isFull else {
return (.tableStateInvalid, nil)
}
guard let player = player(named: name) else {
print("Unexpected action \(action) for missing player \(name) at table \(self.name)")
return (.tableStateInvalid, nil)
}
guard player.canPerform(.deal) else {
print("Player \(name) cant perform deal, although table is full")
return (.tableStateInvalid, nil)
}
let table = DealingTable(table: self)
return (.success, table)
}
}
extension WaitingTable: Table {
var allPlayers: [Player] {
players as [Player]
}
var indexOfNextActor: Int {
// The first player at the table starts the game
0
}
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
// No cards playable while waiting
(.tableStateInvalid, nil)
}
}