First working version

This commit is contained in:
Christoph Hagen 2021-12-18 15:08:43 +01:00
parent c9853dee28
commit 49787db1aa
32 changed files with 1416 additions and 415 deletions

View File

@ -6,6 +6,11 @@ var playerName = ""
var debugSessionToken = null var debugSessionToken = null
const debugMode = true // Does not load session token, to allow multiple players per browser const debugMode = true // Does not load session token, to allow multiple players per browser
const playerCardsElement = "player-cards"
const offlineText = "Offline"
const missingPlayerText = "Leer"
function showDebugLogins() { function showDebugLogins() {
document.getElementById("login-window-inner").innerHTML += document.getElementById("login-window-inner").innerHTML +=
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('a')\">Player A</button>" + "<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('a')\">Player A</button>" +
@ -35,7 +40,7 @@ function showLoginElements() {
hide("table-list") hide("table-list")
hide("game-bar") hide("game-bar")
hide("table-players") hide("table-players")
hide("player-cards") hide(playerCardsElement)
} }
function showTableListElements() { function showTableListElements() {
@ -45,7 +50,7 @@ function showTableListElements() {
setDisplayStyle("table-list", "inherit") setDisplayStyle("table-list", "inherit")
hide("game-bar") hide("game-bar")
hide("table-players") hide("table-players")
hide("player-cards") hide(playerCardsElement)
} }
function showGameElements() { function showGameElements() {
@ -55,7 +60,7 @@ function showGameElements() {
hide("table-list") hide("table-list")
setDisplayStyle("game-bar", "grid") setDisplayStyle("game-bar", "grid")
setDisplayStyle("table-players", "grid") setDisplayStyle("table-players", "grid")
setDisplayStyle("player-cards", "grid") setDisplayStyle(playerCardsElement, "grid")
} }
function showTableName(name) { function showTableName(name) {
@ -63,11 +68,11 @@ function showTableName(name) {
} }
function showConnectedState() { function showConnectedState() {
showConnectionState("bottom", true) showPlayerState("bottom", "")
} }
function showDisconnectedState() { function showDisconnectedState() {
showConnectionState("bottom", false) showPlayerDisconnected("bottom")
} }
function showDealButton() { function showDealButton() {
@ -138,26 +143,36 @@ function setTableListContent(content) {
document.getElementById("table-list").innerHTML = content document.getElementById("table-list").innerHTML = content
} }
function setTablePlayerInfo(position, name, connected, active, card, layer) {
nameColor = active ? "var(--button-color)" : "var(--text-color)"
setTablePlayerElements(position, name, nameColor, connected, card, layer)
}
function setEmptyPlayerInfo(position) { function setEmptyPlayerInfo(position) {
setTablePlayerElements(position, "Empty", "var(--secondary-text-color)", true, "", 1) setTablePlayerName(position, null, false)
showPlayerState(position, "")
setTableCard(position, "", 1)
} }
function setTablePlayerElements(position, name, nameColor, connected, card, layer) { function setTablePlayerName(position, name, active) {
const nameElement = document.getElementById("table-player-name-" + position) const nameElement = document.getElementById("table-player-name-" + position)
nameElement.style.color = nameColor if (name == null) {
nameElement.style.color = "var(--secondary-text-color)"
nameElement.innerHTML = missingPlayerText
} else {
nameElement.style.color = active ? "var(--button-color)" : "var(--text-color)"
nameElement.innerHTML = name nameElement.innerHTML = name
showConnectionState(position, connected) }
setTableCard(position, card, layer)
} }
function showConnectionState(position, connected) { function showPlayerDisconnected(position) {
setPlayerState(position, "var(--alert-color)", offlineText)
}
function showPlayerState(position, state) {
setPlayerState(position, "var(--secondary-text-color)", state)
}
function setPlayerState(position, color, text) {
const connectionElement = "table-player-state-" + position const connectionElement = "table-player-state-" + position
setDisplayStyle(connectionElement, connected ? "none" : "inherit") const element = document.getElementById(connectionElement)
element.style.color = color
element.innerHTML = text
} }
function setTableCard(position, card, layer) { function setTableCard(position, card, layer) {
@ -206,9 +221,14 @@ function updateTableInfo(table) {
setHandCard(i+1, "", false) setHandCard(i+1, "", false)
} }
let playedGame = null
if (table.hasOwnProperty("game")) {
playedGame = textForAction(table.game)
}
// Show player info // Show player info
console.log(table) console.log(table)
setInfoForPlayer(table.player, "bottom") setInfoForPlayer(table.player, "bottom", playedGame)
if (table.playerSelectsGame) { if (table.playerSelectsGame) {
setActionsForOwnPlayer(table.playableGames) setActionsForOwnPlayer(table.playableGames)
showAvailableGames([]) showAvailableGames([])
@ -221,29 +241,61 @@ function updateTableInfo(table) {
setActionsForOwnPlayer(table.actions) setActionsForOwnPlayer(table.actions)
} }
if (table.hasOwnProperty("playerLeft")) { if (table.hasOwnProperty("playerLeft")) {
setInfoForPlayer(table.playerLeft, "left") setInfoForPlayer(table.playerLeft, "left", playedGame)
} else { } else {
setEmptyPlayerInfo("left") setEmptyPlayerInfo("left")
} }
if (table.hasOwnProperty("playerAcross")) { if (table.hasOwnProperty("playerAcross")) {
setInfoForPlayer(table.playerAcross, "top") setInfoForPlayer(table.playerAcross, "top", playedGame)
} else { } else {
setEmptyPlayerInfo("top") setEmptyPlayerInfo("top")
} }
if (table.hasOwnProperty("playerRight")) { if (table.hasOwnProperty("playerRight")) {
setInfoForPlayer(table.playerRight, "right") setInfoForPlayer(table.playerRight, "right", playedGame)
} else { } else {
setEmptyPlayerInfo("right") setEmptyPlayerInfo("right")
} }
} }
function setInfoForPlayer(player, position) { function setInfoForPlayer(player, position, game) {
var card = "" var card = ""
if (player.hasOwnProperty("playedCard")) { if (player.hasOwnProperty("card")) {
card = player.playedCard card = player.card
} }
const leadsGame = player.leads
const layer = player.position const layer = player.position
setTablePlayerInfo(position, player.name, player.connected, player.active, card, layer) setTableCard(position, card, layer)
setTablePlayerName(position, player.name, player.active)
if (!player.connected) {
showPlayerDisconnected(position)
return
}
var state = []
if (game != null && leadsGame) {
state.push(game)
}
const double = doubleText(player.doubles)
if (double) {
state.push(double)
}
if (game != null) {
state.push(player.points.toString() + " Punkte")
}
const text = state.join(", ")
showPlayerState(position, text)
}
function doubleText(doubles) {
if (doubles == 0) {
return null
}
if (doubles == 1) {
return "gedoppelt"
}
return doubles.toString() + "x gedoppelt"
} }
function clearInfoForPlayer(position) { function clearInfoForPlayer(position) {
@ -296,6 +348,8 @@ function textForAction(action) {
case "raise": case "raise":
return "Schießen" return "Schießen"
case "ruf":
return "Ruf"
case "ruf-eichel": case "ruf-eichel":
return "Ruf Eichel" return "Ruf Eichel"
case "ruf-blatt": case "ruf-blatt":
@ -308,6 +362,8 @@ function textForAction(action) {
return "Wenz" return "Wenz"
case "geier": case "geier":
return "Geier" return "Geier"
case "solo":
return "Solo"
case "solo-eichel": case "solo-eichel":
return "Eichel Solo" return "Eichel Solo"
case "solo-blatt": case "solo-blatt":

View File

@ -94,5 +94,7 @@ Version 3:
- Save data persistently - Save data persistently
- Table administrator can remove players - Table administrator can remove players
# Bugs
- Correctly show available games

View File

@ -2,6 +2,27 @@ import Foundation
extension Array { extension Array {
func at(_ index: Int) -> Element? {
guard index < count else {
return nil
}
return self[index]
}
func rotatedByOne() -> [Element] {
guard !isEmpty else {
return []
}
return self[1...] + [self[0]]
}
mutating func rotateByOne() {
guard !isEmpty else {
return
}
append(removeFirst())
}
func rotated(toStartAt index: Int) -> [Element] { func rotated(toStartAt index: Int) -> [Element] {
guard index != 0 else { guard index != 0 else {
return self return self
@ -13,3 +34,35 @@ extension Array {
sorted { converting($0) < converting($1) } sorted { converting($0) < converting($1) }
} }
} }
extension Array where Element: Equatable {
func index(of element: Element) -> Index {
firstIndex(of: element)!
}
}
extension Array where Element: Player {
var names: [PlayerName] {
map { $0.name }
}
func index(of player: PlayerName) -> Int {
firstIndex { $0.name == player }!
}
func player(named name: PlayerName) -> Element? {
first { $0.name == name }
}
func contains(player: PlayerName) -> Bool {
contains { $0.name == player }
}
func next(after player: Element) -> Element {
let i = index(of: player)
let newIndex = (i + 1) % maximumPlayersPerTable
return self[newIndex]
}
}

View File

@ -17,12 +17,22 @@ struct PlayerInfo: Codable, Equatable {
/// The height of the player card on the table stack /// The height of the player card on the table stack
let positionInTrick: Int let positionInTrick: Int
init(player: Player, isNextActor: Bool, position: Int) { /// The number of times the player doubled the game cost (initial double and raises)
let numberOfDoubles: Int
let leadsGame: Bool
let points: Int?
init(player: Player, position: Int) {
self.name = player.name self.name = player.name
self.isConnected = player.isConnected self.isConnected = player.isConnected
self.isNextActor = isNextActor self.isNextActor = player.isNextActor
self.positionInTrick = position self.positionInTrick = position
self.playedCard = player.playedCard?.id self.playedCard = player.playedCard?.id
self.numberOfDoubles = player.numberOfDoubles
self.leadsGame = player.leadsGame
self.points = player.points
} }
/// Convert the property names into shorter strings for JSON encoding /// Convert the property names into shorter strings for JSON encoding
@ -32,5 +42,8 @@ struct PlayerInfo: Codable, Equatable {
case isNextActor = "active" case isNextActor = "active"
case playedCard = "card" case playedCard = "card"
case positionInTrick = "position" case positionInTrick = "position"
case numberOfDoubles = "doubles"
case leadsGame = "leads"
case points = "points"
} }
} }

View File

@ -24,22 +24,21 @@ struct TableInfo: Codable {
let playerSelectsGame: Bool let playerSelectsGame: Bool
init(id: String, name: String, let game: GameId?
own: PlayerInfo, left: PlayerInfo?,
across: PlayerInfo?, right: PlayerInfo?, init<T>(table: AbstractTable<T>, index: Int) {
games: [GameType] = [], actions: [PlayerAction], self.id = table.id
cards: [PlayableCard], self.name = table.name
selectGame: Bool = false) { self.player = table.playerInfo(forIndex: index)!
self.id = id self.playerLeft = table.playerInfo(forIndex: (index + 1) % 4)
self.name = name self.playerAcross = table.playerInfo(forIndex: (index + 2) % 4)
self.player = own self.playerRight = table.playerInfo(forIndex: (index + 3) % 4)
self.playerLeft = left let data = table.playerData(at: index)
self.playerAcross = across self.playableGames = data.games.map { $0.id }
self.playerRight = right self.actions = data.actions.map { $0.id }
self.playableGames = games.map { $0.id } self.cards = data.cards.map { $0.cardInfo }
self.actions = actions.map { $0.id } self.playerSelectsGame = data.selectsGame
self.cards = cards.map { $0.cardInfo } self.game = table.playedGame?.id
self.playerSelectsGame = selectGame
} }
} }

View File

@ -10,7 +10,7 @@ typealias TableName = String
final class TableManagement: DiskWriter { final class TableManagement: DiskWriter {
/// All tables indexed by their id /// All tables indexed by their id
private var tables = [TableId : Table]() private var tables = [TableId : ManageableTable]()
/// The handle to the file where the tables are persisted /// The handle to the file where the tables are persisted
let storageFile: FileHandle let storageFile: FileHandle
@ -48,9 +48,7 @@ final class TableManagement: DiskWriter {
} }
} }
entries.forEach { id, tableData in entries.forEach { id, tableData in
let table = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic) tables[id] = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic, players: tableData.players)
tableData.players.forEach { _ = table.add(player: $0) }
tables[id] = table
} }
print("Loaded \(tables.count) tables") print("Loaded \(tables.count) tables")
} }
@ -63,7 +61,7 @@ final class TableManagement: DiskWriter {
- Returns: `true`, if the entry was written, `false` on error - Returns: `true`, if the entry was written, `false` on error
*/ */
@discardableResult @discardableResult
private func writeTableToDisk(table: Table) -> Bool { private func writeTableToDisk(table: ManageableTable) -> Bool {
let visible = table.isPublic ? "public" : "private" let visible = table.isPublic ? "public" : "private"
let players = table.playerNames let players = table.playerNames
.joined(separator: ",") .joined(separator: ",")
@ -94,8 +92,7 @@ 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 = WaitingTable(newTable: name, isPublic: isPublic) let table = WaitingTable(newTable: name, isPublic: isPublic, creator: player)
_ = table.add(player: player)
tables[table.id] = table tables[table.id] = table
writeTableToDisk(table: table) writeTableToDisk(table: table)
return table.tableInfo(forPlayer: player) return table.tableInfo(forPlayer: player)
@ -115,8 +112,8 @@ final class TableManagement: DiskWriter {
currentTable(for: player)?.tableInfo(forPlayer: player) currentTable(for: player)?.tableInfo(forPlayer: player)
} }
private func currentTable(for player: PlayerName) -> Table? { private func currentTable(for player: PlayerName) -> ManageableTable? {
tables.values.first(where: { $0.contains(player: player) }) tables.values.first(where: { $0.playerNames.contains(player) })
} }
/** /**
@ -154,6 +151,7 @@ final class TableManagement: DiskWriter {
guard let oldTable = currentTable(for: player) else { guard let oldTable = currentTable(for: player) else {
return return
} }
/// `player.canStartGame` is automatically set to false, because table is not full
let table = WaitingTable(oldTable: oldTable, removing: player) let table = WaitingTable(oldTable: oldTable, removing: player)
tables[table.id] = table tables[table.id] = table
table.sendUpdateToAllPlayers() table.sendUpdateToAllPlayers()

View File

@ -6,6 +6,14 @@ typealias Hand = [Card]
extension Array where Element == Card { extension Array where Element == Card {
var unplayable: [PlayableCard] {
map { $0.unplayable }
}
var playable: [PlayableCard] {
map { $0.playable }
}
var points: Int { var points: Int {
map { $0.points } map { $0.points }
.reduce(0, +) .reduce(0, +)

View File

@ -10,6 +10,10 @@ struct Card: Codable {
case blatt = "B" case blatt = "B"
case herz = "H" case herz = "H"
case schelln = "S" case schelln = "S"
var ace: Card {
.init(self, .ass)
}
} }
let symbol: Symbol let symbol: Symbol
@ -46,6 +50,18 @@ struct Card: Codable {
symbol.points symbol.points
} }
var playable: PlayableCard {
.init(card: self, isPlayable: true)
}
var unplayable: PlayableCard {
.init(card: self, isPlayable: false)
}
func playable(_ isPlayable: Bool) -> PlayableCard {
.init(card: self, isPlayable: isPlayable)
}
static let allCards: Set<Card> = { static let allCards: Set<Card> = {
let all = Card.Suit.allCases.map { suit in let all = Card.Suit.allCases.map { suit in
Card.Symbol.allCases.map { symbol in Card.Symbol.allCases.map { symbol in

View File

@ -9,33 +9,65 @@ extension GameType {
case .bettel: case .bettel:
return .bettel return .bettel
case .wenz, .geier: case .wenz, .geier:
return .wenzGeier return .wenz
case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln: case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln:
return .solo return .solo
} }
} }
enum GameClass: Int { enum GameClass: String {
case none = 0 case none = "none"
case ruf = 1 case ruf = "ruf"
case bettel = 2 case hochzeit = "hochzeit"
case wenzGeier = 3 case bettel = "bettel"
case solo = 4 case wenz = "wenz"
case geier = "geier"
case solo = "solo"
var cost: Int { var cost: Int {
switch self { switch self {
case .none: return 0 case .none: return 0
case .ruf: return 5 case .ruf: return 5
case .hochzeit: return 10
case .bettel: return 15 case .bettel: return 15
case .wenzGeier, .solo: return 20 case .wenz, .geier, .solo: return 20
} }
} }
mutating func increase() { @discardableResult
guard self != .solo else { mutating func increase() -> Bool {
return switch self {
case .none:
self = .ruf
case .ruf:
self = .bettel
case .hochzeit:
self = .bettel
case .bettel:
self = .wenz
case .wenz, .geier:
self = .solo
case .solo:
return false
}
return true
}
func increased() -> GameClass? {
switch self {
case .none:
return .ruf
case .ruf:
return .bettel
case .hochzeit:
return .bettel
case .bettel:
return .wenz
case .wenz, .geier:
return .solo
case .solo:
return nil
} }
self = .init(rawValue: rawValue + 1)!
} }
var allowsWedding: Bool { var allowsWedding: Bool {
@ -47,18 +79,39 @@ extension GameType {
} }
} }
func allows(game: GameType) -> Bool {
availableGames.contains(game)
}
var availableGames: [GameType] { var availableGames: [GameType] {
switch self { switch self {
case .none, .ruf: case .none, .ruf:
return GameType.allCases return GameType.allCases
case .bettel: case .hochzeit, .bettel:
return [.bettel, .wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln] return [.bettel, .wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
case .wenzGeier: case .wenz, .geier:
return [.wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln] return [.wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
case .solo: case .solo:
return [.soloEichel, .soloBlatt, .soloHerz, .soloSchelln] return [.soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
} }
} }
var availableClasses: [GameClass] {
switch self {
case .none, .ruf:
return [.ruf, .bettel, .wenz, .geier, .solo]
case .hochzeit, .bettel:
return [.bettel, .wenz, .geier, .solo]
case .wenz, .geier:
return [.wenz, .geier, .solo]
case .solo:
return [.solo]
}
}
var classesWhenOutbidding: [GameClass] {
increased()?.availableClasses ?? []
}
} }
} }
@ -68,3 +121,10 @@ extension GameType.GameClass: Comparable {
lhs.rawValue < rhs.rawValue lhs.rawValue < rhs.rawValue
} }
} }
extension GameType.GameClass: GameConvertible {
var id: GameId {
rawValue
}
}

View File

@ -0,0 +1,6 @@
import Foundation
protocol GameConvertible {
var id: GameId { get }
}

View File

@ -56,10 +56,6 @@ enum GameType: String, CaseIterable, Codable {
gameClass.cost gameClass.cost
} }
var id: GameId {
rawValue
}
var sortingType: CardOrder.Type { var sortingType: CardOrder.Type {
switch self { switch self {
case .wenz: case .wenz:
@ -77,3 +73,11 @@ enum GameType: String, CaseIterable, Codable {
} }
} }
} }
extension GameType: GameConvertible {
var id: GameId {
rawValue
}
}

View File

@ -304,6 +304,7 @@ final class OldPlayer {
actions = [] actions = []
} }
} }
func didFinishGame() { func didFinishGame() {
actions = [.deal] actions = [.deal]
} }

View File

@ -380,7 +380,7 @@ final class OldTable {
// Remove wedding offers // Remove wedding offers
players.forEach { $0.weddingOutbid() } players.forEach { $0.weddingOutbid() }
} }
#warning("Fix bidding")
// TODO: Remove highest bidder from old player // TODO: Remove highest bidder from old player
player.didPerformBid() player.didPerformBid()

View File

@ -0,0 +1,23 @@
import Foundation
enum PlayerState: String {
case canDouble
case didDouble
case isDisconnected
case mustBid
case didFold
case didBid
case mustPlaceBid
case isGameSelector
case isWeddingOfferer
case isCalled
case didRaise
case isWinner
case isLooser
}

View File

@ -1,26 +0,0 @@
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

@ -1,51 +1,61 @@
import Foundation import Foundation
import WebSocketKit import WebSocketKit
final class BiddingPlayer { final class BiddingPlayer: Player {
let name: String
var socket: WebSocket?
let cards: [PlayableCard]
var isStillBidding = true var isStillBidding = true
var isAllowedToOfferWedding = true var isAllowedToOfferWedding: Bool
var offersWedding = false var selectsGame = false
var wouldAcceptWedding = false init(player: DealingPlayer) {
isAllowedToOfferWedding = true
init(player: DealingPlayer, cards: [PlayableCard]) { super.init(player: player)
self.name = player.name
self.socket = player.socket
self.cards = cards
}
}
extension BiddingPlayer: Player {
var canOfferWedding: Bool {
rawCards.canOfferWedding
} }
var rawCards: [Card] { init(player: WeddingPlayer) {
cards.map { $0.card } isStillBidding = !player.offersWedding
isAllowedToOfferWedding = false
super.init(player: player)
} }
func canPlay(game: GameType) -> Bool {
guard let suit = game.calledSuit else {
if game == .hochzeit {
return canOfferWedding
}
return true
}
let sorter = game.sortingType
guard sorter.hasCardToCall(suit, in: cards) else {
// Player needs at least one card of the called suit
return false
}
let ace = Card(suit, .ass)
return !cards.contains(ace)
}
var actions: [PlayerAction] { override var actions: [PlayerAction] {
guard isStillBidding else { guard isStillBidding else {
return [] return []
} }
guard canOfferWedding, isAllowedToOfferWedding, !offersWedding else { var actions: [PlayerAction] = isNextActor ? [.increaseOrMatchGame, .withdrawFromAuction] : []
return [.increaseOrMatchGame, .withdrawFromAuction] if canOfferWedding {
actions.append(.offerWedding)
} }
return [.increaseOrMatchGame, .withdrawFromAuction, .offerWedding] return actions
} }
var playedCard: Card? { override var points: Int? {
nil get { nil }
set { }
}
}
extension BiddingPlayer {
var canOfferWedding: Bool {
cards.canOfferWedding
} }
} }

View File

@ -1,24 +1,36 @@
import Foundation import Foundation
import WebSocketKit import WebSocketKit
final class DealingPlayer: AbstractPlayer { final class DealingPlayer: Player {
var cards: [PlayableCard] = []
var didDouble: Bool? = nil var didDouble: Bool? = nil
override var isNextActor: Bool {
get { didDouble == nil }
set { }
}
override var actions: [PlayerAction] {
didDouble == nil ? [.initialDoubleCost, .noDoubleCost] : []
}
init(player: WaitingPlayer) { init(player: WaitingPlayer) {
super.init(player: player) super.init(player: player)
} }
}
extension DealingPlayer: Player { override var numberOfDoubles: Int {
get { didDouble == true ? 1 : 0 }
var actions: [PlayerAction] { set { }
didDouble == nil ? [.initialDoubleCost, .noDoubleCost] : []
} }
var playedCard: Card? { override var leadsGame: Bool {
nil get { false }
set { }
} }
override var points: Int? {
get { nil }
set { }
}
} }

View File

@ -0,0 +1,21 @@
import Foundation
final class FinishedPlayer: Player {
let tricks: [Trick]
init(player: PlayingPlayer) {
self.tricks = player.wonTricks
super.init(player: player)
}
override var points: Int? {
get { tricks.map { $0.points }.reduce(0, +) }
set { }
}
override var actions: [PlayerAction] {
[.deal]
}
}

View File

@ -1,18 +1,57 @@
import Foundation import Foundation
import WebSocketKit import WebSocketKit
protocol Player: AnyObject { class Player {
var name: String { get } let name: PlayerName
var socket: WebSocket? { get set } var socket: WebSocket?
var playedCard: Card? { get } var playedCard: Card?
var actions: [PlayerAction] { get } var isNextActor: Bool
var cards: [PlayableCard] { get } var cards: [Card]
var numberOfDoubles: Int
var leadsGame: Bool
var points: Int?
init(name: PlayerName, socket: WebSocket? = nil) {
self.name = name
self.socket = socket
self.cards = []
self.isNextActor = false
self.playedCard = nil
self.numberOfDoubles = 0
self.leadsGame = false
self.points = nil
}
init(player: Player) {
self.name = player.name
self.socket = player.socket
self.cards = player.cards
self.isNextActor = false
self.playedCard = player.playedCard
self.numberOfDoubles = player.numberOfDoubles
self.leadsGame = player.leadsGame
self.points = player.points
}
var actions: [PlayerAction] {
[]
}
}
extension Player: Equatable {
static func == (lhs: Player, rhs: Player) -> Bool {
lhs.name == rhs.name
}
} }
extension Player { extension Player {
@ -36,6 +75,7 @@ extension Player {
self.socket = socket self.socket = socket
} }
@discardableResult
func disconnect() -> Bool { func disconnect() -> Bool {
guard let socket = socket else { guard let socket = socket else {
return false return false
@ -50,8 +90,18 @@ extension Player {
} }
func send(_ info: TableInfo) { @discardableResult
try? socket?.send(encodeJSON(info)) func send(_ info: TableInfo) -> Bool {
guard let socket = socket else {
return false
}
do {
try socket.send(encodeJSON(info))
} catch {
print("Failed to send info: \(error)")
return false
}
return true
} }
// MARK: Actions // MARK: Actions
@ -61,3 +111,10 @@ extension Player {
} }
} }
extension Player: CustomStringConvertible {
var description: String {
name
}
}

View File

@ -1,30 +1,139 @@
import Foundation import Foundation
import WebSocketKit import WebSocketKit
final class PlayingPlayer: AbstractPlayer { /**
Specifies the number of cards of the called suit that a player must have
to be allowed to play any card of the suit instead of having to play the ace.
*/
private let numberOfCardsToProtectAce = 4
var playedCard: Card? = nil final class PlayingPlayer: Player {
var cards: [PlayableCard]
var leadsGame = false
var canStillRaise = true var canStillRaise = true
init(player: BiddingPlayer) { var isCalledWithAce: Card?
self.cards = player.cards
/// All tricks won by the player in this game
var wonTricks: [Trick] = []
init(player: Player, leads: Bool, calledAce ace: Card?) {
super.init(player: player) super.init(player: player)
leadsGame = leads
if let ace = ace, cards.contains(ace) {
isCalledWithAce = ace
} else {
isCalledWithAce = nil
}
} }
}
extension PlayingPlayer: Player { override var actions: [PlayerAction] {
guard canStillRaise, leadsGame == (isCalledWithAce != nil) else {
var actions: [PlayerAction] {
guard canStillRaise, !leadsGame else {
return [] return []
} }
return [.doubleDuringGame] return [.doubleDuringGame]
} }
func play(card: Card) {
playedCard = card
cards = cards.filter { $0 != card }
if card == isCalledWithAce {
leadsGame.toggle()
isCalledWithAce = nil
}
}
func switchLead() {
leadsGame.toggle()
}
func sortCards(for game: GameType) {
cards = cards.sortedCards(forGame: game)
}
func canPlay(card: Card, for trick: Trick, in game: GameType) -> Bool {
playableCards(for: trick, in: game).contains { $0.card == card && $0.isPlayable }
}
func playableCards(for trick: Trick, in game: GameType) -> [PlayableCard] {
guard isNextActor else {
return cards.unplayable
}
guard cards.count > 1 else {
// Last card can always be played
return cards.playable
}
guard let firstCard = trick.first else {
return playableCardsForStarter(game: game)
}
let sorter = game.sortingType
guard sorter.isTrump(firstCard) else {
return playableCardsFollowing(suit: firstCard.suit, game: game)
}
guard !sorter.hasTrump(in: cards) else {
// Must follow with trump
return cards.map { $0.playable(sorter.isTrump($0)) }
}
// Can play any card if not in calling game
guard let suit = game.calledSuit else {
return cards.playable
}
// Can play any card, except the called ace
let ace = Card(suit, .ass)
return cards.map { $0.playable($0 != ace) }
}
private func playableCardsFollowing(suit playedSuit: Card.Suit, game: GameType) -> [PlayableCard] {
let sorter = game.sortingType
let suitCards = sorter.cards(with: playedSuit, in: cards)
func followSuit() -> [PlayableCard] {
cards.map { $0.playable(!sorter.isTrump($0) && $0.suit == playedSuit) }
}
guard let calledSuit = game.calledSuit else {
return suitCards.isEmpty ? cards.playable : followSuit()
}
let ace = Card(calledSuit, .ass)
guard !suitCards.isEmpty else {
// Exclude called ace, all others allowed
return cards.map { $0.playable($0 != ace) }
}
guard calledSuit == playedSuit else {
// Must follow suit (called ace not present)
return followSuit()
}
// The called suit is played, must commit ace
guard cards.contains(ace) else {
// Must follow suit
return followSuit()
}
// Must play ace
return cards.map { $0.playable($0 == ace) }
}
private func playableCardsForStarter(game: GameType) -> [PlayableCard] {
guard let suit = game.calledSuit else {
return cards.playable
}
let ace = Card(suit, .ass)
// Check if called ace exists, to prohibit other cards of the same suit
guard cards.contains(ace) else {
return cards.playable
}
// Jodeln
if cards.count == numberOfCardsPerPlayer,
cards.suitCount(suit, in: game) >= numberOfCardsToProtectAce {
return cards.playable
}
// Only ace allowed for the called suit
return cards.map { $0.playable($0.suit != suit || $0.symbol.isTrumpOrAce) }
}
var currentPoints: Int {
wonTricks.map { $0.points }.reduce(0, +)
}
} }

View File

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

View File

@ -0,0 +1,89 @@
import Foundation
final class WeddingPlayer: Player {
enum State {
case requiresAction
case offersWedding
case wouldAcceptWedding
case withdrawnFromAuction
case selectsGame
}
var state: State
var requiresAction: Bool {
state == .requiresAction
}
var selectsGame: Bool {
get {
state == .selectsGame
}
set {
state = .selectsGame
}
}
var wouldAcceptWedding: Bool {
state == .wouldAcceptWedding
}
var offersWedding: Bool {
state == .offersWedding
}
init(player: BiddingPlayer, offersWedding: Bool) {
self.state = offersWedding ? .offersWedding : .requiresAction
super.init(player: player)
}
override var actions: [PlayerAction] {
guard state == .requiresAction else {
return []
}
return [.increaseOrMatchGame, .withdrawFromAuction, .acceptWedding]
}
override var isNextActor: Bool {
get {
switch state {
case .requiresAction, .selectsGame:
return true
default:
return false
}
}
set { }
}
override var points: Int? {
get { nil }
set { }
}
override var leadsGame: Bool {
get { offersWedding || selectsGame }
set { }
}
func canExchange(card: Card) -> Bool {
cards.filter { !$0.isTrump(in: .hochzeit) }.contains(card)
}
var exchangeableCards: [PlayableCard] {
cards.map { $0.playable(!$0.isTrump(in: .hochzeit)) }
}
func replaceWeddingCard(with card: Card) -> Card {
let ownCardIndex = cards.firstIndex { $0.isTrump(in: .hochzeit)}!
let ownCard = cards.remove(at: ownCardIndex)
cards.append(card)
cards = cards.sortedCards(forGame: .hochzeit)
return ownCard
}
func replace(_ card: Card, with trumpCard: Card) {
cards = (cards.filter { $0 != card } + [trumpCard]).sortedCards(forGame: .hochzeit)
}
}

View File

@ -1,6 +1,7 @@
import Foundation import Foundation
import WebSocketKit
class AbstractTable { class AbstractTable<TablePlayer> where TablePlayer: Player {
/// The unique id of the table /// The unique id of the table
let id: TableId let id: TableId
@ -11,15 +12,116 @@ class AbstractTable {
/// Indicates that the table is visible to all players, and can be joined by anyone /// Indicates that the table is visible to all players, and can be joined by anyone
let isPublic: Bool let isPublic: Bool
init(table: AbstractTable) { /**
The players sitting at the table.
The players are ordered clockwise around the table, with the first player starting the game.
*/
var players: [TablePlayer]
var playedGame: GameType? {
nil
}
init(table: ManageableTable, players: [TablePlayer]) {
self.id = table.id self.id = table.id
self.name = table.name self.name = table.name
self.isPublic = table.isPublic self.isPublic = table.isPublic
self.players = players
} }
init(id: TableId, name: TableName, isPublic: Bool) { init(id: TableId, name: TableName, isPublic: Bool, players: [TablePlayer]) {
self.id = id self.id = id
self.name = name self.name = name
self.isPublic = isPublic self.isPublic = isPublic
self.players = players
} }
func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
(.tableStateInvalid, nil)
}
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
(.tableStateInvalid, nil)
}
func playerData(at index: Int) -> (actions: [PlayerAction], games: [GameConvertible], cards: [PlayableCard], selectsGame: Bool) {
let player = players[index]
return (actions: player.actions, games: [], cards: player.cards.unplayable, selectsGame: false)
}
func cardStackPosition(ofPlayerAt index: Int) -> Int {
index
}
} }
extension AbstractTable: ManageableTable {
var publicInfo: PublicTableInfo {
.init(id: id, name: name, players: playerNames)
}
var playerNames: [PlayerName] {
players.map { $0.name }
}
var allPlayers: [Player] {
players
}
// MARK: Connection
func connect(player name: PlayerName, using socket: WebSocket) -> Bool {
guard let player = players.player(named: name) else {
return false
}
player.connect(using: socket)
sendUpdateToAllPlayers()
return true
}
func disconnect(player name: PlayerName) {
guard let player = players.player(named: name) else {
return
}
guard player.disconnect() else {
return
}
sendUpdateToAllPlayers()
return
}
func sendUpdateToAllPlayers() {
players.enumerated().forEach { playerIndex, player in
guard player.isConnected else {
return
}
let info = self.tableInfo(forPlayerAt: playerIndex)
player.send(info)
}
}
// MARK: Client info
func playerInfo(forIndex index: Int) -> PlayerInfo? {
guard let player = players.at(index) else {
return nil
}
let height = cardStackPosition(ofPlayerAt: index)
return PlayerInfo(player: player, position: height)
}
func tableInfo(forPlayer player: PlayerName) -> TableInfo {
let index = players.index(of: player)
return tableInfo(forPlayerAt: index)
}
func tableInfo(forPlayerAt index: Int) -> TableInfo {
.init(table: self, index: index)
}
}

View File

@ -1,51 +1,181 @@
import Foundation import Foundation
final class BiddingTable: AbstractTable { final class BiddingTable: AbstractTable<BiddingPlayer> {
var players: [BiddingPlayer] var gameToOutbid: GameType.GameClass = .none
var hasSelectedGame: Bool { var indexOfHighestBidder = 0
// TODO: Implement
false var remainingBidders: Int {
players.filter { $0.isStillBidding }.count
}
var isWaitingForGameSelection: Bool {
players.contains { $0.selectsGame }
} }
init(table: DealingTable) { init(table: DealingTable) {
self.players = table.players.map { // Add new cards to the players
BiddingPlayer(player: $0, cards: []) let newCards = Dealer.dealRemainingCards(of: table.players.map { $0.cards })
let players: [BiddingPlayer] = table.players.enumerated().map { index, player in
player.cards = (player.cards + newCards[index])
.sortedCards(order: NormalCardOrder.self)
player.isNextActor = false
return BiddingPlayer(player: player)
} }
super.init(table: table) players.first!.isNextActor = true
super.init(table: table, players: players)
} }
func select(game: GameType, player: PlayerName) -> (result: PlayerActionResult, table: Table?) { init(wedding table: WeddingTable, outbidBy player: WeddingPlayer) {
// TODO: Implement gameToOutbid = .hochzeit
indexOfHighestBidder = table.players.index(of: player)
// All players can bid again, except the wedding offerer
let players = table.players.map(BiddingPlayer.init)
players[indexOfHighestBidder].isNextActor = true
super.init(table: table, players: players)
// Choose the player after the one who discarded the wedding
selectNextBidder()
}
func select(game: GameType, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
guard let player = players.player(named: name) else {
print("Player \(name) unexpectedly missing from bidding table \(self.name)")
return (.tableStateInvalid, nil)
}
guard player.selectsGame else {
print("Player \(name) does not select the game")
return (.tableStateInvalid, nil)
}
guard gameToOutbid.allows(game: game) else {
print("Game \(game) not allowed for class \(gameToOutbid)")
return (.tableStateInvalid, nil)
}
guard player.canPlay(game: game) else {
print("Player \(game) can't play game \(game)")
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
} }
func makePlayingTable() -> PlayingTable { let table = PlayingTable(table: self, game: game, playedBy: player)
// TODO: Implement return (.success, table)
fatalError()
}
}
extension BiddingTable: Table {
var allPlayers: [Player] {
players
} }
var indexOfNextActor: Int { @discardableResult
// TODO: Implement private func selectNextBidder() -> Bool {
return 0 guard let index = players.firstIndex(where: { $0.isNextActor }) else {
print("Bidding: No current actor found to select next bidder")
return false
}
players[index].isNextActor = false
let newActor = players.rotated(toStartAt: (index + 1) % 4).first(where: { $0.isStillBidding })!
newActor.isNextActor = true
return true
} }
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) { override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
// TODO: Implement bidding actions guard let player = players.player(named: name) else {
print("Player \(name) unexpectedly missing from bidding table \(self.name)")
return (.tableStateInvalid, nil)
}
guard player.canPerform(action) else {
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
} }
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) { switch action {
// TODO: Implement for wedding case .offerWedding:
return performWeddingOffer(forPlayer: player)
case .increaseOrMatchGame:
return performBidIncrease(forPlayer: player)
case .withdrawFromAuction:
return performWithdrawl(forPlayer: player)
default:
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
} }
}
private func performWeddingOffer(forPlayer player: BiddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
guard gameToOutbid.allowsWedding else {
return (.tableStateInvalid, nil)
}
let newTable = WeddingTable(table: self, offerer: player)
return (.success, newTable)
}
private func performBidIncrease(forPlayer player: BiddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
guard player.isNextActor, player.isStillBidding else {
return (.tableStateInvalid, nil)
}
let index = players.index(of: player)
if index < indexOfHighestBidder {
// Player sits before the current highest bidder, so only needs to match the game
indexOfHighestBidder = index
if gameToOutbid == .solo {
// Can't be outbid, so player selects game
players.forEach { $0.isStillBidding = false }
player.selectsGame = true
return (.success, nil)
}
// TODO: Check that wedding can be offered at the correct times
// There may be a case where a player sitting before the highest bidder
// can't offer a wedding anymore although it should be able to
if !gameToOutbid.allowsWedding {
players.forEach { $0.isAllowedToOfferWedding = false }
}
} else {
// Player sits after the highest bidder, so must outbid the game
// Also the case when first starting bidding
gameToOutbid.increase()
indexOfHighestBidder = index
if !gameToOutbid.allowsWedding {
players.forEach { $0.isAllowedToOfferWedding = false }
}
}
selectNextBidder()
return (.success, nil)
}
private func performWithdrawl(forPlayer player: BiddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
guard player.isStillBidding else {
return (.tableStateInvalid, nil)
}
player.isStillBidding = false
switch remainingBidders {
case 0:
// Nobody wants to play something, so abort the game
// This case can only be reached when nobody has bid yet
let table = WaitingTable(oldTableAdvancedByOne: self)
return (.success, table)
case 1:
if gameToOutbid != .none {
// Last player must play
player.isNextActor = false
indexOfHighestBidder = players.firstIndex { $0.isStillBidding == true }!
let highestPlayer = players[indexOfHighestBidder]
highestPlayer.isStillBidding = false
highestPlayer.selectsGame = true
highestPlayer.isNextActor = true
return (.success, nil)
}
default:
break
}
selectNextBidder()
return (.success, nil)
}
override func playerData(at index: Int) -> (actions: [PlayerAction], games: [GameConvertible], cards: [PlayableCard], selectsGame: Bool) {
let player = players[index]
let games: [GameConvertible]
if isWaitingForGameSelection {
games = gameToOutbid.availableGames.filter(player.canPlay)
} else if index <= indexOfHighestBidder {
games = gameToOutbid.availableClasses
} else {
games = gameToOutbid.classesWhenOutbidding
}
return (player.actions, games, player.cards.unplayable, selectsGame: player.selectsGame)
}
} }

View File

@ -1,39 +1,61 @@
import Foundation import Foundation
final class DealingTable: AbstractTable { final class DealingTable: AbstractTable<DealingPlayer> {
var players: [DealingPlayer]
init(table: WaitingTable) { init(table: WaitingTable) {
self.players = table.players.map(DealingPlayer.init)
super.init(table: table)
let cards = Dealer.dealFirstCards() let cards = Dealer.dealFirstCards()
for (index, player) in players.enumerated() { for (index, player) in table.players.enumerated() {
player.cards = cards[index].map { .init(card: $0, isPlayable: false) } player.cards = cards[index]
} }
} let players = table.players.map(DealingPlayer.init)
} super.init(table: table, players: players)
extension DealingTable: Table {
var allPlayers: [Player] {
players
} }
var indexOfNextActor: Int { /// All players either doubled or didn't double
// TODO: Implement var allPlayersActed: Bool {
return 0 !players.contains { $0.didDouble == nil }
} }
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) { /// At least one player placed a bid
// TODO: Implement doubling, additional cards var hasDouble: Bool {
players.contains { $0.didDouble == true }
}
override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
guard let player = players.player(named: name) else {
print("Player \(name) unexpectedly missing from dealing table \(self.name)")
return (.tableStateInvalid, nil)
}
guard player.canPerform(action) else {
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
} }
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) { switch action {
// No cards playable while dealing case .initialDoubleCost:
(.tableStateInvalid, nil) return perform(double: true, forPlayer: player)
case .noDoubleCost:
return perform(double: false, forPlayer: player)
default:
return (.tableStateInvalid, nil)
}
}
private func perform(double: Bool, forPlayer player: DealingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
guard player.didDouble == nil else {
return (.tableStateInvalid, nil)
}
player.didDouble = double
guard allPlayersActed else {
return (.success, nil)
}
guard hasDouble else {
// Revert to a waiting table and switch to the next player
let table = WaitingTable(oldTableAdvancedByOne: self)
return (.success, table)
}
// Automatically adds remaining cards to the players
let table = BiddingTable(table: self)
return (.success, table)
} }
} }

View File

@ -0,0 +1,83 @@
import Foundation
final class FinishedTable: AbstractTable<FinishedPlayer> {
let game: GameType
var winners: [FinishedPlayer] {
leadersHaveWon ? leaders : opponents
}
var loosers: [FinishedPlayer] {
leadersHaveWon ? opponents : leaders
}
var leaders: [FinishedPlayer] {
players.filter { $0.leadsGame }
}
var opponents: [FinishedPlayer] {
players.filter { !$0.leadsGame }
}
var winningPoints: Int {
leadersHaveWon ? leadingPoints : 120 - leadingPoints
}
var loosingPoints: Int {
leadersHaveWon ? 120 - leadingPoints : leadingPoints
}
let leadingPoints: Int
var leadersHaveWon: Bool {
leadingPoints > 60
}
var isSchwarz: Bool {
loosingPoints == 0
}
var isSchneider: Bool {
loosingPoints < (leadersHaveWon ? 30 : 31)
}
override var playedGame: GameType? {
game
}
init(table: PlayingTable) {
let players = table.players.map(FinishedPlayer.init)
self.game = table.game
leadingPoints = players
.filter { $0.leadsGame }
.map { $0.points! }
.reduce(0, +)
super.init(table: table, players: players)
}
/**
Perform a deal action on the finished table.
- Parameter action: The action to perform
- Parameter player: The name of the player
*/
override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
// Only dealing is allowed...
guard action == .deal else {
return (.tableStateInvalid, nil)
}
guard let player = players.player(named: name) else {
print("Unexpectedly missing player \(name) for deal action at finished table \(self.name)")
return (.tableStateInvalid, nil)
}
guard player.canPerform(.deal) else {
print("Finished table: Player \(name) can't perform deal")
return (.tableStateInvalid, nil)
}
let waiting = WaitingTable(oldTableAdvancedByOne: self)
let table = DealingTable(table: waiting)
return (.success, table)
}
}

View File

@ -0,0 +1,32 @@
import Foundation
import WebSocketKit
protocol ManageableTable {
/// 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 }
var playerNames: [PlayerName] { get }
var allPlayers: [Player] { get }
var publicInfo: PublicTableInfo { get }
func tableInfo(forPlayer player: PlayerName) -> TableInfo
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?)
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?)
func connect(player name: PlayerName, using socket: WebSocket) -> Bool
func disconnect(player name: PlayerName)
func sendUpdateToAllPlayers()
}

View File

@ -1,34 +1,144 @@
import Foundation import Foundation
final class PlayingTable: AbstractTable { final class PlayingTable: AbstractTable<PlayingPlayer> {
var players: [PlayingPlayer] let game: GameType
init(table: BiddingTable) { var indexOfTrickStarter = 0
self.players = table.players.map(PlayingPlayer.init)
super.init(table: table)
}
}
extension PlayingTable: Table { var didDoubleInCurrentRound = false
var allPlayers: [Player] { var hasCompletedTrick: Bool {
players !players.contains { $0.playedCard == nil }
} }
var indexOfNextActor: Int { var nextTrick: [Card] {
// TODO: Implement hasCompletedTrick ? [] : currentTrick
return 0
} }
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) { var currentTrick: [Card] {
// TODO: Implement raises players.rotated(toStartAt: indexOfTrickStarter).compactMap { $0.playedCard }
}
var completedTrick: Trick? {
let trick = currentTrick
guard trick.count == maximumPlayersPerTable else {
return nil
}
return trick
}
var allCardsPlayed: Bool {
!players.contains { !$0.cards.isEmpty }
}
override var playedGame: GameType? {
game
}
convenience init(table: BiddingTable, game: GameType, playedBy player: BiddingPlayer) {
let calledAce = game.calledSuit?.ace
let players = table.players.map {
PlayingPlayer(player: $0, leads: $0 == player, calledAce: calledAce)
}
self.init(table: table, players: players, game: game)
}
convenience init(wedding table: WeddingTable, offeredBy offerer: WeddingPlayer, acceptedBy player: WeddingPlayer) {
let players = table.players.map {
PlayingPlayer(player: $0, leads: $0 == player || $0 == offerer, calledAce: nil)
}
self.init(table: table, players: players, game: .hochzeit)
}
private init(table: ManageableTable, players: [PlayingPlayer], game: GameType) {
self.game = game
super.init(table: table, players: players)
players.forEach { $0.sortCards(for: game) }
players.first!.isNextActor = true
}
override func cardStackPosition(ofPlayerAt index: Int) -> Int {
(4 + index - indexOfTrickStarter) % 4
}
override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
guard let player = players.player(named: name) else {
print("Player \(name) unexpectedly missing from playing table \(self.name)")
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
} }
guard action == .doubleDuringGame else {
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) { print("Player \(name) wants to perform action \(action) on playing table")
// TODO: Implement playing of cards
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
} }
guard player.canPerform(.doubleDuringGame) else {
print("Player \(name) is not allowed to raise")
return (.tableStateInvalid, nil)
}
player.numberOfDoubles += 1
players.forEach { $0.switchLead() }
self.didDoubleInCurrentRound = true
return (.success, nil)
}
override func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
guard let player = players.player(named: name) else {
print("Player \(name) unexpectedly missing from playing table \(self.name)")
return (.tableStateInvalid, nil)
}
guard player.isNextActor else {
print("Player \(name) wants to play card but is not active")
return (.tableStateInvalid, nil)
}
guard player.canPlay(card: card, for: nextTrick, in: game) else {
return (.tableStateInvalid, nil)
}
if hasCompletedTrick {
players.forEach { $0.playedCard = nil }
indexOfTrickStarter = players.index(of: player)
}
player.play(card: card)
if let completedTrick = completedTrick {
return didFinish(trick: completedTrick, in: game)
} else {
let next = players.next(after: player)
next.isNextActor = true
player.isNextActor = false
return (.success, nil)
}
}
override func playerData(at index: Int) -> (actions: [PlayerAction], games: [GameConvertible], cards: [PlayableCard], selectsGame: Bool) {
let player = players[index]
let cards = player.playableCards(for: nextTrick, in: game)
return (actions: player.actions, games: [], cards: cards, selectsGame: false)
}
private func didFinish(trick: Trick, in game: GameType) -> (result: PlayerActionResult, table: ManageableTable?) {
let index = trick.highCardIndex(forGame: game)
let winner = players[(indexOfTrickStarter + index) % 4]
players.forEach {
$0.isNextActor = false
$0.canStillRaise = didDoubleInCurrentRound
}
winner.wonTricks.append(trick)
winner.isNextActor = true
if game == .bettel && winner.leadsGame {
// A bettel is lost if a single trick is won by the leader
return finishedGame()
}
didDoubleInCurrentRound = false
if allCardsPlayed {
return finishedGame()
}
return (.success, nil)
}
private func finishedGame() -> (result: PlayerActionResult, table: ManageableTable?) {
let table = FinishedTable(table: self)
print("\(table.winners) have won with \(table.winningPoints) to \(table.loosingPoints) points")
return (.success, table)
}
} }

View File

@ -1,121 +0,0 @@
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

@ -3,22 +3,20 @@ import Foundation
/** /**
Represents a table where players are still joining and leaving. Represents a table where players are still joining and leaving.
*/ */
final class WaitingTable: AbstractTable { final class WaitingTable: AbstractTable<WaitingPlayer> {
/**
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 /// The table contains enough players to start a game
var isFull: Bool { var isFull: Bool {
players.count >= maximumPlayersPerTable players.count >= maximumPlayersPerTable
} }
override init(id: TableId, name: TableName, isPublic: Bool) { init(id: TableId, name: TableName, isPublic: Bool, players: [PlayerName]) {
super.init(id: id, name: name, isPublic: isPublic) let players = players.map { WaitingPlayer(name: $0) }
players.first!.isNextActor = true
super.init(id: id, name: name, isPublic: isPublic, players: players)
if isFull {
self.players.forEach { $0.canStartGame = true }
}
} }
/** /**
@ -26,8 +24,10 @@ final class WaitingTable: AbstractTable {
- Parameter name: The name of the table - Parameter name: The name of the table
- Parameter isPublic: The table is visible and joinable by everyone - Parameter isPublic: The table is visible and joinable by everyone
*/ */
init(newTable name: TableName, isPublic: Bool) { init(newTable name: TableName, isPublic: Bool, creator: PlayerName) {
super.init(id: .newToken(), name: name, isPublic: isPublic) let player = WaitingPlayer(name: creator)
player.isNextActor = true
super.init(id: .newToken(), name: name, isPublic: isPublic, players: [player])
} }
/** /**
@ -35,13 +35,35 @@ final class WaitingTable: AbstractTable {
This is needed when a player leaves an active table. This is needed when a player leaves an active table.
- Parameter oldTable: The table to convert - Parameter oldTable: The table to convert
- Parameter player: The player to remove from the table. - Parameter player: The name of the player to remove from the table.
*/ */
init(oldTable: Table, removing player: PlayerName) { init(oldTable: ManageableTable, removing player: PlayerName) {
self.players = oldTable.allPlayers let players = oldTable.allPlayers
.filter { $0.name != player } .filter {
.map(WaitingPlayer.init) guard $0.name == player else {
super.init(table: oldTable) return true
}
_ = $0.disconnect()
return false
}
.map { WaitingPlayer(name: $0.name, socket: $0.socket) }
players.first!.isNextActor = true
super.init(table: oldTable, players: players)
}
/**
Convert another table to a waiting table.
This is needed when a player leaves an active table.
- Parameter oldTable: The table to convert
*/
init(oldTableAdvancedByOne table: ManageableTable) {
let players = table.allPlayers
.rotatedByOne()
.map { WaitingPlayer(name: $0.name, socket: $0.socket) }
super.init(table: table, players: players)
players.forEach { $0.canStartGame = true }
players.first!.isNextActor = true
} }
/** /**
@ -69,7 +91,7 @@ final class WaitingTable: AbstractTable {
- Parameter action: The action to perform - Parameter action: The action to perform
- Parameter player: The name of the player - Parameter player: The name of the player
*/ */
func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: Table?) { override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
// Only dealing is allowed... // Only dealing is allowed...
guard action == .deal else { guard action == .deal else {
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
@ -78,7 +100,7 @@ final class WaitingTable: AbstractTable {
guard isFull else { guard isFull else {
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
} }
guard let player = player(named: name) else { guard let player = players.player(named: name) else {
print("Unexpected action \(action) for missing player \(name) at table \(self.name)") print("Unexpected action \(action) for missing player \(name) at table \(self.name)")
return (.tableStateInvalid, nil) return (.tableStateInvalid, nil)
} }
@ -90,20 +112,8 @@ final class WaitingTable: AbstractTable {
let table = DealingTable(table: self) let table = DealingTable(table: self)
return (.success, table) return (.success, table)
} }
}
extension WaitingTable: Table { override func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
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 // No cards playable while waiting
(.tableStateInvalid, nil) (.tableStateInvalid, nil)
} }

View File

@ -0,0 +1,121 @@
import Foundation
final class WeddingTable: AbstractTable<WeddingPlayer> {
var indexOfWeddingOffer: Int
init(table: BiddingTable, offerer: BiddingPlayer) {
let players = table.players.map { WeddingPlayer(player: $0, offersWedding: $0 == offerer) }
indexOfWeddingOffer = table.players.index(of: offerer)
super.init(table: table, players: players)
}
var hasRemainingActors: Bool {
players.contains { $0.requiresAction }
}
var requiresCardSelection: Bool {
players.contains { $0.selectsGame }
}
override var playedGame: GameType? {
.hochzeit
}
override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
guard let player = players.player(named: name) else {
print("Player \(name) unexpectedly missing from wedding table \(self.name)")
return (.tableStateInvalid, nil)
}
guard player.canPerform(action) else {
return (.tableStateInvalid, nil)
}
switch action {
case .acceptWedding:
return performWeddingAccept(forPlayer: player)
case .withdrawFromAuction:
return performWithdrawl(forPlayer: player)
case .increaseOrMatchGame:
fatalError()
default:
return (.tableStateInvalid, nil)
}
}
private func performWeddingAccept(forPlayer player: WeddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
guard player.requiresAction else {
return (.tableStateInvalid, nil)
}
player.state = .wouldAcceptWedding
guard !hasRemainingActors else {
return (.success, nil)
}
// Nobody wants to play a higher game, so let the first player accept the wedding
players.first { $0.wouldAcceptWedding }!.selectsGame = true
return (.success, nil)
}
private func performWithdrawl(forPlayer player: WeddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
guard player.requiresAction else {
return (.tableStateInvalid, nil)
}
player.state = .withdrawnFromAuction
guard !hasRemainingActors else {
return (.success, nil)
}
// Nobody wants to play a higher game, so let the first player accept the wedding
guard let player = players.first(where: { $0.wouldAcceptWedding }) else {
// Nobody wants to accept the wedding or play something higher, so abort the game
let table = WaitingTable(oldTableAdvancedByOne: self)
return (.success, table)
}
player.selectsGame = true
return (.success, nil)
}
private func performOutbid(forPlayer player: WeddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
guard player.requiresAction else {
return (.tableStateInvalid, nil)
}
let table = BiddingTable(wedding: self, outbidBy: player)
return (.success, table)
}
override func playerData(at index: Int) -> (actions: [PlayerAction], games: [GameConvertible], cards: [PlayableCard], selectsGame: Bool) {
guard requiresCardSelection else {
return super.playerData(at: index)
}
let player = players[index]
guard player.selectsGame else {
return super.playerData(at: index)
}
return (actions: player.actions, games: [], cards: player.exchangeableCards, selectsGame: false)
}
override func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
guard requiresCardSelection else {
print("Wedding is not in stage where card should be selected")
return (.tableStateInvalid, nil)
}
guard let player = players.player(named: name) else {
print("Player \(name) unexpectedly missing from wedding table \(self.name)")
return (.tableStateInvalid, nil)
}
guard player.selectsGame else {
print("Player \(name) is not the one selecting a wedding card")
return (.tableStateInvalid, nil)
}
guard player.canExchange(card: card) else {
print("Invalid card \(card) to exchange in wedding")
return (.tableStateInvalid, nil)
}
let offerer = players[indexOfWeddingOffer]
let trumpCard = offerer.replaceWeddingCard(with: card)
player.replace(card, with: trumpCard)
let table = PlayingTable(wedding: self, offeredBy: offerer, acceptedBy: player)
return (.success, table)
}
}

View File

@ -6,7 +6,8 @@ var database: Database!
public func configure(_ app: Application) throws { public func configure(_ app: Application) throws {
// Set target environment // Set target environment
app.environment = .development app.environment = .production
app.logger.logLevel = .info // .notice
// serve files from /Public folder // serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))