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

@ -1,40 +1,36 @@
import Foundation
struct PlayerInfo: Codable, Equatable {
/// The name of the player
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
let active: Bool
let selectsGame: Bool
/// The cards in the hand of the player
let cards: [CardInfo]
/// The action the player can perform
let actions: [String]
let isNextActor: Bool
/// The card which the player added to the current trick
let playedCard: CardId?
/// The height of the player card on the table stack
let position: Int
init(player: Player, isMasked: Bool, trickPosition: Int) {
let positionInTrick: Int
init(player: Player, isNextActor: Bool, position: Int) {
self.name = player.name
self.connected = player.isConnected
self.active = player.isNextActor
self.selectsGame = player.selectsGame
self.isConnected = player.isConnected
self.isNextActor = isNextActor
self.positionInTrick = position
self.playedCard = player.playedCard?.id
self.position = trickPosition
if isMasked {
self.cards = []
self.actions = []
} else {
self.actions = player.actions.map { $0.path }
self.cards = player.handCards.map { $0.cardInfo }
}
}
/// Convert the property names into shorter strings for JSON encoding
enum CodingKeys: String, CodingKey {
case name = "name"
case isConnected = "connected"
case isNextActor = "active"
case playedCard = "card"
case positionInTrick = "position"
}
}

View File

@ -15,22 +15,30 @@ struct TableInfo: Codable {
let playerRight: PlayerInfo?
let playableGames: [GameId]
init(_ table: Table, forPlayerAt playerIndex: Int) {
let player = table.player(at: playerIndex)!
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 {
let games = table.minimumPlayableGame?.availableGames ?? GameType.allCases
self.playableGames = games.filter(player.canPlay).map { $0.id }
} else {
self.playableGames = []
}
/// The cards in the hand of the player
let cards: [CardInfo]
/// The action the player can perform
let actions: [String]
let playerSelectsGame: Bool
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
}
func performAction(playerToken: SessionToken, action: Player.Action) -> PlayerActionResult {
func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult {
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
return .invalidToken
}
@ -113,7 +113,7 @@ final class Database {
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 {
return .invalidToken
}

View File

@ -48,7 +48,7 @@ final class TableManagement: DiskWriter {
}
}
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) }
tables[id] = table
}
@ -65,8 +65,10 @@ final class TableManagement: DiskWriter {
@discardableResult
private func writeTableToDisk(table: Table) -> Bool {
let visible = table.isPublic ? "public" : "private"
let players = table.playerNames.joined(separator: ",")
let entry = [table.id, table.name, visible, players].joined(separator: ":")
let players = table.playerNames
.joined(separator: ",")
let entry = [table.id, table.name, visible, players]
.joined(separator: ":")
return writeToDisk(line: entry)
}
@ -79,7 +81,8 @@ final class TableManagement: DiskWriter {
*/
@discardableResult
private func writeTableDeletionEntry(tableId: TableId) -> Bool {
let entry = [tableId, "", "", ""].joined(separator: ":")
let entry = [tableId, "", "", ""]
.joined(separator: ":")
return writeToDisk(line: entry)
}
@ -91,27 +94,27 @@ final class TableManagement: DiskWriter {
- Returns: The table id
*/
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)
tables[table.id] = table
writeTableToDisk(table: table)
return table.compileInfo(for: player)!
return table.tableInfo(forPlayer: player)
}
/// A list of all public tables
var publicTableList: [PublicTableInfo] {
tables.values.filter { $0.isPublic }.map { $0.publicInfo }
}
/**
Get the table info for a player
- Parameter player: The name of the player
- Returns: The table info, if the player has joined a table
*/
func tableInfo(player: PlayerName) -> TableInfo? {
currentTable(for: player)?.compileInfo(for: player)
currentTable(for: player)?.tableInfo(forPlayer: player)
}
private func currentTable(for player: PlayerName) -> Table? {
tables.values.first(where: { $0.contains(player: player) })
}
@ -127,16 +130,20 @@ final class TableManagement: DiskWriter {
guard existing.id == tableId else {
return .failure(.alreadyJoinedOtherTable)
}
return .success(existing.compileInfo(for: player)!)
return .success(existing.tableInfo(forPlayer: player))
}
guard let table = tables[tableId] else {
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)
}
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
*/
func leaveTable(player: PlayerName) {
guard let table = currentTable(for: player) else {
guard let oldTable = currentTable(for: player) else {
return
}
table.remove(player: player)
let table = WaitingTable(oldTable: oldTable, removing: player)
tables[table.id] = table
table.sendUpdateToAllPlayers()
writeTableToDisk(table: table)
}
@ -165,26 +174,60 @@ final class TableManagement: DiskWriter {
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 {
print("Player \(player) wants to \(action.path), but no table joined")
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 {
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")
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 {
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 {
case none = 0
case ruf = 1
case bettel = 2
case wenzGeier = 3
@ -25,6 +26,7 @@ extension GameType {
var cost: Int {
switch self {
case .none: return 0
case .ruf: return 5
case .bettel: return 15
case .wenzGeier, .solo: return 20
@ -38,9 +40,18 @@ extension GameType {
self = .init(rawValue: rawValue + 1)!
}
var allowsWedding: Bool {
switch self {
case .none, .ruf:
return true
default:
return false
}
}
var availableGames: [GameType] {
switch self {
case .ruf:
case .none, .ruf:
return GameType.allCases
case .bettel:
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)
}
}