Refactor tables and players for clarity
This commit is contained in:
parent
33f72c43cf
commit
289458bfd8
@ -1,40 +1,36 @@
|
|||||||
import Foundation
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,22 +15,30 @@ struct TableInfo: Codable {
|
|||||||
let playerRight: PlayerInfo?
|
let playerRight: PlayerInfo?
|
||||||
|
|
||||||
let playableGames: [GameId]
|
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 {
|
/// The cards in the hand of the player
|
||||||
let games = table.minimumPlayableGame?.availableGames ?? GameType.allCases
|
let cards: [CardInfo]
|
||||||
self.playableGames = games.filter(player.canPlay).map { $0.id }
|
|
||||||
} else {
|
/// The action the player can perform
|
||||||
self.playableGames = []
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,27 +94,27 @@ 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
|
||||||
var publicTableList: [PublicTableInfo] {
|
var publicTableList: [PublicTableInfo] {
|
||||||
tables.values.filter { $0.isPublic }.map { $0.publicInfo }
|
tables.values.filter { $0.isPublic }.map { $0.publicInfo }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Get the table info for a player
|
Get the table info for a player
|
||||||
- Parameter player: The name of the player
|
- Parameter player: The name of the player
|
||||||
- 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? {
|
||||||
tables.values.first(where: { $0.contains(player: player) })
|
tables.values.first(where: { $0.contains(player: player) })
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
26
Sources/App/Model/Players/AbstractPlayer.swift
Normal file
26
Sources/App/Model/Players/AbstractPlayer.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
51
Sources/App/Model/Players/BiddingPlayer.swift
Normal file
51
Sources/App/Model/Players/BiddingPlayer.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
24
Sources/App/Model/Players/DealingPlayer.swift
Normal file
24
Sources/App/Model/Players/DealingPlayer.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
63
Sources/App/Model/Players/Player.swift
Normal file
63
Sources/App/Model/Players/Player.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
Sources/App/Model/Players/PlayingPlayer.swift
Normal file
30
Sources/App/Model/Players/PlayingPlayer.swift
Normal 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]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
22
Sources/App/Model/Players/WaitingPlayer.swift
Normal file
22
Sources/App/Model/Players/WaitingPlayer.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
25
Sources/App/Model/Tables/AbstractTable.swift
Normal file
25
Sources/App/Model/Tables/AbstractTable.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
51
Sources/App/Model/Tables/BiddingTable.swift
Normal file
51
Sources/App/Model/Tables/BiddingTable.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
39
Sources/App/Model/Tables/DealingTable.swift
Normal file
39
Sources/App/Model/Tables/DealingTable.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
34
Sources/App/Model/Tables/PlayingTable.swift
Normal file
34
Sources/App/Model/Tables/PlayingTable.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
121
Sources/App/Model/Tables/Table.swift
Normal file
121
Sources/App/Model/Tables/Table.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
110
Sources/App/Model/Tables/WaitingTable.swift
Normal file
110
Sources/App/Model/Tables/WaitingTable.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user