First working version
This commit is contained in:
parent
c9853dee28
commit
49787db1aa
@ -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.innerHTML = name
|
nameElement.style.color = "var(--secondary-text-color)"
|
||||||
showConnectionState(position, connected)
|
nameElement.innerHTML = missingPlayerText
|
||||||
setTableCard(position, card, layer)
|
} else {
|
||||||
|
nameElement.style.color = active ? "var(--button-color)" : "var(--text-color)"
|
||||||
|
nameElement.innerHTML = name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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":
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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, +)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
6
Sources/App/Model/GameConvertible.swift
Normal file
6
Sources/App/Model/GameConvertible.swift
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
protocol GameConvertible {
|
||||||
|
|
||||||
|
var id: GameId { get }
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -304,6 +304,7 @@ final class OldPlayer {
|
|||||||
actions = []
|
actions = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func didFinishGame() {
|
func didFinishGame() {
|
||||||
actions = [.deal]
|
actions = [.deal]
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
23
Sources/App/Model/PlayerState.swift
Normal file
23
Sources/App/Model/PlayerState.swift
Normal 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
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
21
Sources/App/Model/Players/FinishedPlayer.swift
Normal file
21
Sources/App/Model/Players/FinishedPlayer.swift
Normal 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]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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, +)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
89
Sources/App/Model/Players/WeddingPlayer.swift
Normal file
89
Sources/App/Model/Players/WeddingPlayer.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
return (.tableStateInvalid, nil)
|
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 makePlayingTable() -> PlayingTable {
|
func select(game: GameType, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
|
||||||
// TODO: Implement
|
guard let player = players.player(named: name) else {
|
||||||
fatalError()
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
extension BiddingTable: Table {
|
let table = PlayingTable(table: self, game: game, playedBy: player)
|
||||||
|
return (.success, 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 {
|
||||||
return (.tableStateInvalid, nil)
|
print("Player \(name) unexpectedly missing from bidding table \(self.name)")
|
||||||
|
return (.tableStateInvalid, nil)
|
||||||
|
}
|
||||||
|
guard player.canPerform(action) else {
|
||||||
|
return (.tableStateInvalid, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .offerWedding:
|
||||||
|
return performWeddingOffer(forPlayer: player)
|
||||||
|
case .increaseOrMatchGame:
|
||||||
|
return performBidIncrease(forPlayer: player)
|
||||||
|
case .withdrawFromAuction:
|
||||||
|
return performWithdrawl(forPlayer: player)
|
||||||
|
default:
|
||||||
|
return (.tableStateInvalid, nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
|
private func performWeddingOffer(forPlayer player: BiddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
|
||||||
// TODO: Implement for wedding
|
guard gameToOutbid.allowsWedding else {
|
||||||
return (.tableStateInvalid, nil)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All players either doubled or didn't double
|
||||||
|
var allPlayersActed: Bool {
|
||||||
|
!players.contains { $0.didDouble == nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// At least one player placed a bid
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case .initialDoubleCost:
|
||||||
|
return perform(double: true, forPlayer: player)
|
||||||
|
case .noDoubleCost:
|
||||||
|
return perform(double: false, forPlayer: player)
|
||||||
|
default:
|
||||||
|
return (.tableStateInvalid, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension DealingTable: Table {
|
private func perform(double: Bool, forPlayer player: DealingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
|
||||||
|
guard player.didDouble == nil else {
|
||||||
var allPlayers: [Player] {
|
return (.tableStateInvalid, nil)
|
||||||
players
|
}
|
||||||
}
|
player.didDouble = double
|
||||||
|
guard allPlayersActed else {
|
||||||
var indexOfNextActor: Int {
|
return (.success, nil)
|
||||||
// TODO: Implement
|
}
|
||||||
return 0
|
guard hasDouble else {
|
||||||
}
|
// Revert to a waiting table and switch to the next player
|
||||||
|
let table = WaitingTable(oldTableAdvancedByOne: self)
|
||||||
func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) {
|
return (.success, table)
|
||||||
// TODO: Implement doubling, additional cards
|
}
|
||||||
return (.tableStateInvalid, nil)
|
// Automatically adds remaining cards to the players
|
||||||
}
|
let table = BiddingTable(table: self)
|
||||||
|
return (.success, table)
|
||||||
func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
|
|
||||||
// No cards playable while dealing
|
|
||||||
(.tableStateInvalid, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
83
Sources/App/Model/Tables/FinishedTable.swift
Normal file
83
Sources/App/Model/Tables/FinishedTable.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
32
Sources/App/Model/Tables/ManageableTable.swift
Normal file
32
Sources/App/Model/Tables/ManageableTable.swift
Normal 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()
|
||||||
|
}
|
@ -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)
|
var didDoubleInCurrentRound = false
|
||||||
|
|
||||||
|
var hasCompletedTrick: Bool {
|
||||||
|
!players.contains { $0.playedCard == nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextTrick: [Card] {
|
||||||
|
hasCompletedTrick ? [] : currentTrick
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentTrick: [Card] {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
guard action == .doubleDuringGame else {
|
||||||
|
print("Player \(name) wants to perform action \(action) on playing table")
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
121
Sources/App/Model/Tables/WeddingTable.swift
Normal file
121
Sources/App/Model/Tables/WeddingTable.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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))
|
||||||
|
Loading…
Reference in New Issue
Block a user