From 3db9652cadd990c57895e9e318c1f28ab85d8e9f Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Fri, 3 Dec 2021 18:03:29 +0100 Subject: [PATCH] Sync push --- Public/api.js | 20 +- Public/elements.js | 214 +++++++++--- Public/game.js | 294 ++++++----------- Public/schafkopf.html | 47 +-- Public/style.css | 57 ++-- Readme.md | 50 +++ Sources/App/Infos/CardInfo.swift | 19 -- Sources/App/Infos/PlayerInfo.swift | 30 ++ Sources/App/Infos/PublicTableInfo.swift | 27 ++ Sources/App/Infos/TableInfo.swift | 36 +- Sources/App/Management/ClientConnection.swift | 32 -- Sources/App/Management/Database.swift | 26 +- Sources/App/Management/DiskWriter.swift | 14 +- Sources/App/Management/TableManagement.swift | 242 +++++--------- Sources/App/Model/Card.swift | 26 ++ Sources/App/Model/CardOrders/CardOrder.swift | 49 +++ .../Model/CardOrders/NormalCardOrder.swift | 58 ++++ Sources/App/Model/Dealer.swift | 312 ++++++++++++++++++ Sources/App/Model/Game.swift | 5 +- Sources/App/{Infos => Model}/GameType.swift | 60 ++-- Sources/App/Model/Player.swift | 195 +++++++++++ Sources/App/Model/Table.swift | 233 +++++++++++++ Sources/App/Model/Trick.swift | 2 +- Sources/App/Results/DealCardsResult.swift | 2 +- Sources/App/Results/JoinTableResult.swift | 5 +- Sources/App/Sorting/Dealer.swift | 298 ----------------- Sources/App/routes.swift | 85 +++-- 27 files changed, 1540 insertions(+), 898 deletions(-) delete mode 100644 Sources/App/Infos/CardInfo.swift create mode 100644 Sources/App/Infos/PlayerInfo.swift create mode 100644 Sources/App/Infos/PublicTableInfo.swift delete mode 100644 Sources/App/Management/ClientConnection.swift create mode 100644 Sources/App/Model/CardOrders/CardOrder.swift create mode 100644 Sources/App/Model/CardOrders/NormalCardOrder.swift create mode 100644 Sources/App/Model/Dealer.swift rename Sources/App/{Infos => Model}/GameType.swift (60%) create mode 100644 Sources/App/Model/Player.swift create mode 100644 Sources/App/Model/Table.swift delete mode 100644 Sources/App/Sorting/Dealer.swift diff --git a/Public/api.js b/Public/api.js index fc18305..1aea014 100644 --- a/Public/api.js +++ b/Public/api.js @@ -32,26 +32,26 @@ async function resumeSessionRequest(token) { async function performGetCurrentTableRequest(token) { return fetch("player/table", { method: 'POST', body: token }) .then(convertServerResponse) + .then(convertJsonResponse) } async function performCreateTableRequest(token, name, visibility) { const vis = visibility ? "public" : "private"; return fetch("/table/create/" + vis + "/" + name, { method: 'POST', body: token }) .then(convertServerResponse) + .then(convertJsonResponse) } async function performJoinTableRequest(tableId, token) { return fetch("/table/join/" + tableId, { method: 'POST', body: token }) .then(convertServerResponse) - .then(function(value) {}) + .then(convertJsonResponse) } async function performGetPublicTablesRequest(token) { return fetch("/tables/public", { method: 'POST', body: token }) .then(convertServerResponse) - .then(function(text) { - return JSON.parse(text); - }) + .then(convertJsonResponse) } async function performLeaveTableRequest(token) { @@ -60,10 +60,9 @@ async function performLeaveTableRequest(token) { .then(function(value) {}) } -async function performDealCardsRequest(token) { - return fetch("/deal", { method: 'POST', body: token }) +async function performPlayerActionRequest(token, action) { + return fetch("/player/action/" + action, { method: 'POST', body: token }) .then(convertServerResponse) - .then(function(value) {}) } function convertServerResponse(response) { @@ -90,3 +89,10 @@ function convertServerResponse(response) { throw Error("Unexpected response: " + response.statusText) } } + +function convertJsonResponse(text) { + if (text == "") { + return null; + } + return JSON.parse(text); +} \ No newline at end of file diff --git a/Public/elements.js b/Public/elements.js index b6ae698..29b8d98 100644 --- a/Public/elements.js +++ b/Public/elements.js @@ -1,53 +1,59 @@ /** - * This file acts as an abstraction layer between HTML and JS. - */ +* This file acts as an abstraction layer between HTML and JS. +*/ var playerName = "" +var debugSessionToken = null +const debugMode = true // Does not load session token, to allow multiple players per browser function setDisplayStyle(id, style) { document.getElementById(id).style.display = style } +function hide(elementId) { + setDisplayStyle(elementId, "none") +} + function showLoginElements() { setDisplayStyle("login-window", "table") - setDisplayStyle("top-bar", "none") - setDisplayStyle("table-list-bar", "none") - setDisplayStyle("table-list", "none") - setDisplayStyle("game-bar", "none") - setDisplayStyle("table-players", "none") - setDisplayStyle("player-cards", "none") + hide("top-bar") + hide("table-list-bar") + hide("table-list") + hide("game-bar") + hide("table-players") + hide("player-cards") } function showTableListElements() { - setDisplayStyle("login-window", "none") + hide("login-window") setDisplayStyle("top-bar", "inherit") setDisplayStyle("table-list-bar", "grid") setDisplayStyle("table-list", "inherit") - setDisplayStyle("game-bar", "none") - setDisplayStyle("table-players", "none") - setDisplayStyle("player-cards", "none") + hide("game-bar") + hide("table-players") + hide("player-cards") } function showGameElements() { - setDisplayStyle("login-window", "none") + hide("login-window") setDisplayStyle("top-bar", "inherit") - setDisplayStyle("table-list-bar", "none") - setDisplayStyle("table-list", "none") + hide("table-list-bar") + hide("table-list") setDisplayStyle("game-bar", "grid") setDisplayStyle("table-players", "grid") setDisplayStyle("player-cards", "grid") } +function showTableName(name) { + document.getElementById("table-connected-label").innerHTML = name +} + function showConnectedState() { - const label = document.getElementById("table-connected-label") - label.innerHTML = "Connected" - label.style.color = "var(--text-color)" + showConnectionState("bottom", true) } function showDisconnectedState() { - const label = document.getElementById("table-connected-label") - label.innerHTML = "Disconnected" - label.style.color = "var(--alert-color)" + showConnectionState("bottom", false) } function showDealButton() { @@ -55,7 +61,7 @@ function showDealButton() { } function hideDealButton() { - setDisplayStyle("deal-button", "none") + hide("deal-button") } function setPlayerName(name) { @@ -80,10 +86,17 @@ function clearLoginPassword() { } function getSessionToken() { + if (debugMode) { + return debugSessionToken + } return localStorage.getItem('token') } function setSessionToken(token) { + if (debugMode) { + debugSessionToken = token + return + } localStorage.setItem('token', token) } @@ -111,23 +124,25 @@ function setTableListContent(content) { document.getElementById("table-list").innerHTML = content } -function setTablePlayerInfo(nr, name, connected, active) { - const infoElement = "table-player-name" + nr.toString() - const connectionElement = "table-player-state" + nr - const nameElement = document.getElementById(infoElement) - if (name == "") { - nameElement.innerHTML = "Empty" - nameElement.style.color = "var(--secondary-text-color)" - setDisplayStyle(connectionElement, "none") - return - } +function setTablePlayerInfo(position, name, connected, active) { + nameColor = active ? "var(--button-color)" : "var(--text-color)" + setTablePlayerElements(position, name, nameColor, connected) +} + +function setEmptyPlayerInfo(position) { + setTablePlayerElements(position, "Empty", "var(--secondary-text-color)", true) +} + +function setTablePlayerElements(position, name, nameColor, connected) { + const nameElement = document.getElementById("table-player-name-" + position) + nameElement.style.color = nameColor nameElement.innerHTML = name - nameElement.style.color = active ? "var(--button-color)" : "var(--text-color)" - if (connected) { - setDisplayStyle(connectionElement, "none") - } else { - setDisplayStyle(connectionElement, "inherit") - } + showConnectionState(position, connected) +} + +function showConnectionState(position, connected) { + const connectionElement = "table-player-state-" + position + setDisplayStyle(connectionElement, connected ? "none" : "inherit") } function setTableCard(index, card) { @@ -138,7 +153,7 @@ function setTableCard(index, card) { function setHandCard(index, card, playable) { const id = "player-card" + index.toString() setCard(id, card) - document.getElementById(id).disabled = true + document.getElementById(id).disabled = !playable } function setCard(id, card) { @@ -147,4 +162,125 @@ function setCard(id, card) { } else { document.getElementById(id).style.backgroundImage = "url('/cards/" + card + ".jpeg')" } +} + +function updateTableInfo(table) { + showTableName(table.name) + // Set own cards + const cardCount = table.player.cards.length + for (let i = 0; i < cardCount; i += 1) { + const card = table.player.cards[i] + setHandCard(i+1, card.card, card.playable) + } + for (let i = cardCount; i < 8; i += 1) { + setHandCard(i+1, "", false) + } + + // Show player info + console.log(table) + setInfoForPlayer(table.player, "bottom") + setActionsForOwnPlayer(table.player.actions) + if (table.hasOwnProperty("playerLeft")) { + setInfoForPlayer(table.playerLeft, "left") + } else { + setEmptyPlayerInfo("left") + } + if (table.hasOwnProperty("playerAcross")) { + setInfoForPlayer(table.playerAcross, "top") + } else { + setEmptyPlayerInfo("top") + } + if (table.hasOwnProperty("playerRight")) { + setInfoForPlayer(table.playerRight, "right") + } else { + setEmptyPlayerInfo("right") + } + + // if (table.tableIsFull) { + // showDealButton() + // } else { + // hideDealButton() + // } + + // TODO + // Use .hasOwnProperty() for optionals +} + +function setInfoForPlayer(player, position) { + setTablePlayerInfo(position, player.name, player.connected, player.active) +} + +function clearInfoForPlayer(position) { + setEmptyPlayerInfo(position) +} + +function setActionsForOwnPlayer(actions) { + const len = actions.length + for (let i = 0; i < len; i += 1) { + const action = actions[i] + setActionButton(i+1, textForAction(action), action) + } + for (let i = len; i < 3; i += 1) { + hideActionButton(i+1) + } +} + +function textForAction(action) { + switch (action) { + /// The player can request cards to be dealt + case "deal": + return "Austeilen" + + /// The player doubles on the initial four cards + case "double": + return "Legen" + + /// The player does not double on the initial four cards + case "skip": + return "Nicht legen" + + /// The player offers a wedding (one trump card) + case "wedding": + return "Hochzeit anbieten" + + /// The player can choose to accept the wedding + case "accept": + return "Hochzeit akzeptieren" + + /// The player matches or increases the game during auction + case "bid": + return "Spielen" + + /// The player does not want to play + case "out": + return "Weg" + + /// The player claims to win and doubles the game cost ("schießen") + case "raise": + return "Schießen" + } + return action +} + +function setActionButton(position, text, action) { + const button = document.getElementById("action-button" + position.toString()) + button.style.display = "inherit" + button.innerHTML = text + button.onclick = function() { + performAction(action) + }; +} + +function hideActionButton(position) { + hide("action-button" + position.toString()) +} + +function performAction(action) { + const token = getSessionToken() + if (token == null) { + showBlankLogin() + return + } + performPlayerActionRequest(token, action) + // TODO: Handle errors and success } \ No newline at end of file diff --git a/Public/game.js b/Public/game.js index bf408f1..9ed835b 100644 --- a/Public/game.js +++ b/Public/game.js @@ -1,7 +1,8 @@ // The web socket to connect to the server var socket = null; -var tableId = ""; +var tableId = null; +var activePlayer = null; function closeSocketIfNeeded() { if (socket) { @@ -11,7 +12,7 @@ function closeSocketIfNeeded() { } function didLeaveTable() { - tableId = "" + tableId = null } function didCloseSocket() { @@ -22,7 +23,15 @@ function setTableId(table) { tableId = table } -function showBlankLoginScreen(text) { +function showLoginWithError(error) { + showLoginWithText(error.message) +} + +function showBlankLogin() { + showLoginWithText("") +} + +function showLoginWithText(text) { closeSocketIfNeeded() didLeaveTable() clearLoginPassword() @@ -32,125 +41,93 @@ function showBlankLoginScreen(text) { setLoginError(text) } -function showGame(tableId) { - setTableId(tableId) - const token = getSessionToken() - if (token) { - showGameElements() - openSocket(token) - // TODO: Show interface - console.log("Show table " + tableId) - } else { - showBlankLoginScreen("") +function showTableOrList(table) { + if (table == null) { + didLeaveTable() + showTableListElements() + closeSocketIfNeeded() + refreshTables() + // TODO: Show table list, refresh + return } + + const token = getSessionToken() + if (token == null) { + showBlankLogin() + return + } + clearTableName() + setTableId(table.id) + showGameElements() + updateTableInfo(table) + openSocket(token) } function registerUser() { + createSession(performRegisterPlayerRequest) +} + +function loginUser() { + createSession(performLoginPlayerRequest) +} + +function createSession(inputFunction) { const username = getLoginName() const password = getLoginPassword() if (username == "") { - setLoginError("Please enter your desired user name") + showLoginWithText("Please enter your user name") return } if (password == "") { - setLoginError("Please enter a password") + showLoginWithText("Please enter a password") return } - performRegisterPlayerRequest(username, password) + inputFunction(username, password) .then(function(token) { setSessionToken(token) setPlayerName(username) - showTableListElements() loadCurrentTable(token) - }).catch(function(error) { - setLoginError(error.message) - }) + }).catch(showLoginWithError) +} + +function logoutUser() { + const token = getSessionToken() + if (token == null) { + showBlankLogin() + return + } + performLogoutRequest(token) + .then(function() { + showBlankLogin() + }).catch(showLoginWithError) } function deletePlayerAccount() { const name = getPlayerName() const password = getLoginPassword() - - performDeletePlayerRequest(name, password, function(error) { - if (error == "") { - showBlankLoginScreen("") - console.log("Player deleted") - } else { - closeSocketIfNeeded() - didLeaveTable() - deleteSessionToken() - alert(error) - console.log(error) - } - }) -} - -function loginUser() { - const username = getLoginName() - const password = getLoginPassword() - if (username == "") { - setLoginError("Please enter your user name") - return - } - if (password == "") { - setLoginError("Please enter your password") - return - } - performLoginPlayerRequest(username, password) - .then(function(token) { - setSessionToken(token) - setPlayerName(username) - showTableListElements() - loadCurrentTable(token) - }).catch(function(error) { - setLoginError(error.message) - }) -} - -function logoutUser() { - const token = getSessionToken() - if (token) { - performLogoutRequest(token) - .then(function() { - showBlankLoginScreen("") - }).catch(function(error) { - showBlankLoginScreen(error.message) - console.log(error) - }) - } else { - showBlankLoginScreen("") - } + + performDeletePlayerRequest(name, password) + .then(showBlankLogin) + .catch(showLoginWithError) } function loadExistingSession() { const token = getSessionToken() - if (token) { - resumeSessionRequest(token) - .then(function(name) { - setPlayerName(name) - showTableListElements() - loadCurrentTable(token) - }).catch(function(error) { - showBlankLoginScreen(error.message) - }) - } else { - showBlankLoginScreen("") + if (token == null) { + showBlankLogin() + return } + resumeSessionRequest(token) + .then(function(name) { + setPlayerName(name) + loadCurrentTable(token) + }).catch(showLoginWithError) } function loadCurrentTable(token) { performGetCurrentTableRequest(token) - .then(function(tableId) { - if (tableId == "") { - didLeaveTable() - refreshTables() - return - } - console.log("Loaded table " + tableId) - showGame(tableId) - }).catch(function(error) { - showBlankLoginScreen(error.message) - }) + .then(showTableOrList) + .catch(showLoginWithError) } function createTable() { @@ -159,70 +136,56 @@ function createTable() { return } const token = getSessionToken() - if (token) { - const isVisible = getTableVisibility() - performCreateTableRequest(token, tableName, isVisible) - .then(function(tableId) { - clearTableName() - showGame(tableId) - }).catch(function(error) { - showBlankLoginScreen(error.message) - }) - } else { - showBlankLoginScreen("") - } + if (token == null) { + showBlankLogin() + return + } + const isVisible = getTableVisibility() + performCreateTableRequest(token, tableName, isVisible) + .then(showTableOrList) + .catch(showLoginWithError) } function joinTable(tableId) { const token = getSessionToken() - if (token) { - performJoinTableRequest(tableId, token) - .then(function() { - showGame(tableId) - }) - .catch(function(error) { - showBlankLoginScreen(error.message) - }) - } else { - showBlankLoginScreen("") + if (token == null) { + showBlankLogin() + return } + performJoinTableRequest(tableId, token) + .then(showTableOrList) + .catch(showLoginWithError) } function leaveTable() { const token = getSessionToken() - if (token) { - performLeaveTableRequest(token) - .then(function() { - showTableListElements() - closeSocketIfNeeded() - didLeaveTable() - refreshTables() - }) - .catch(function(error) { - showBlankLoginScreen(error.message) - }) - } else { - showBlankLoginScreen("") + if (token == null) { + showBlankLogin() + return } + performLeaveTableRequest(token) + .then(function() { + showTableOrList(null) + }) + .catch(showLoginWithError) } function openSocket(token) { socket = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/session/start") - + socket.onopen = function(e) { socket.send(token); showConnectedState() }; socket.onmessage = function(event) { - // TODO: Handle server data - //event.data - handleServerUpdates(event.data) + const table = convertJsonResponse(event.data) + updateTableInfo(table) }; socket.onclose = function(event) { if (event.wasClean) { - + } else { // e.g. server process killed or network down // event.code is usually 1006 in this case @@ -238,52 +201,15 @@ function openSocket(token) { }; } -function handleServerUpdates(data) { - const info = JSON.parse(data.substring(1)) - if (data.startsWith("t")) { - handleTableInfoUpdate(info) - } else if (data.startsWith("c")) { - handleCardInfoUpdate(info) - } else { - console.log("Unhandled update: " + data) - } -} - -function handleTableInfoUpdate(info) { - for (let i = 0, len = info.players.length; i < len; i += 1) { - const player = info.players[i] - // TODO: Mark active player - setTablePlayerInfo(i + 1, player.name, player.connected, false) - } - if (info.tableIsFull) { - showDealButton() - } else { - hideDealButton() - } -} - -function handleCardInfoUpdate(info) { - for (let i = 0, len = info.cards.length; i < len; i += 1) { - setHandCard(i+1, info.cards[i].card, info.cards[i].playable) - } - for (let i = 0, len = info.tableCards.length; i < len; i += 1) { - setTableCard(i+1, info.tableCards[i]) - } -} - function refreshTables() { const token = getSessionToken() - if (token) { - performGetPublicTablesRequest(token) - .then(function(json) { - const html = processTableList(json) - setTableListContent(html) - }).catch(function(error) { - showBlankLoginScreen(error.message) - }) - } else { - showBlankLoginScreen() + if (token == null) { + showBlankLogin() + return } + performGetPublicTablesRequest(token) + .then(processTableList) + .catch(showLoginWithError) } function processTableList(tables) { @@ -300,22 +226,20 @@ function processTableList(tables) { if (tableInfo.players.length == 0) { html += "
No players
" } else { - const names = tableInfo.players.map(function(player) { return player.name }).join(", ") - html += "
Players: " + names + "
" + const names = tableInfo.players.join(", ") + html += "
" + names + "
" } html += "" // table-row } - return html + setTableListContent(html) } function dealCards() { const token = getSessionToken() - if (token) { - performDealCardsRequest(token) - .catch(function(error) { - showBlankLoginScreen(error.message) - }) - } else { - showBlankLoginScreen() + if (token == null) { + showBlankLogin() + return } + performDealCardsRequest(token) + .catch(showLoginWithError) } \ No newline at end of file diff --git a/Public/schafkopf.html b/Public/schafkopf.html index 6695e22..dcd07d5 100644 --- a/Public/schafkopf.html +++ b/Public/schafkopf.html @@ -33,28 +33,37 @@
- - - -
-
-
-
-
-
Player 1
-
Offline
+
+ +
+ +
+
+ +
+
+ +
-
-
Player 2
-
Offline
+
+
+
+
+
+
Player 1
+
Offline
-
-
Player 3
-
Offline
+
+
Player 2
+
Offline
-
-
Player 4
-
Offline
+
+
Player 3
+
Offline
+
+
+
Player 4
+
Offline
diff --git a/Public/style.css b/Public/style.css index dd4131e..564be98 100644 --- a/Public/style.css +++ b/Public/style.css @@ -275,31 +275,40 @@ body { margin-top: 20px; margin-left: auto; margin-right: auto; - width: 549px; + width: 649px; height: 437px; - grid-template-columns: 150px 71px 36px 35px 36px 71px 150px; + grid-template-columns: 200px 71px 36px 35px 36px 71px 200px; grid-template-rows: 111px 76px 35px 76px 111px; column-gap: 0px; row-gap: 0px; align-items: center; } -#deal-button { - display: none; +#action-buttons { grid-column: 7; - grid-row: 5; + grid-row: 4 / span 2; + width: 200px; + height: 187px; } -#fold-button { +.action-button { + height: 100%; + width: 100%; display: none; - grid-column: 1; - grid-row: 5; } -#bid-button { - display: none; - grid-column: 7; - grid-row: 5; +.action-button-container { + height: 40px; + width: 160px; + margin-top: 10px; + margin-left: 40px; +} + +#action-button-spacer { + height: 37px; + width: 100%; + background-color: var(--standard-background); + border: none; } .table-card { @@ -308,31 +317,31 @@ body { background-color: var(--element-background); } -#table-player-card1 { /* bottom */ +#table-player-card-bottom { grid-column: 3 / span 3; grid-row: 4 / span 2; z-index: 2; } -#table-player-card2 { /* left */ +#table-player-card-left { grid-column: 2 / span 2; grid-row: 2 / span 3; z-index: 1; } -#table-player-card3 { /* top */ +#table-player-card-top { grid-column: 3 / span 3; grid-row: 1 / span 2; z-index: 4; } -#table-player-card4 { /* right */ +#table-player-card-right { grid-column: 5 / span 2; grid-row: 2 / span 3; z-index: 3; } -#table-player-info1 { /* bottom */ +#table-player-info-bottom { grid-column: 1 / span 2; grid-row: 5; font-size: large; @@ -341,7 +350,7 @@ body { margin-top: 50px; } -#table-player-info2 { /* left */ +#table-player-info-left { grid-column: 1; grid-row: 3; font-size: large; @@ -349,7 +358,7 @@ body { margin-right: 15px; } -#table-player-info3 { /* top */ +#table-player-info-top { grid-column: 1 / span 2; grid-row: 1; font-size: large; @@ -358,7 +367,7 @@ body { margin-bottom: 50px; } -#table-player-info4 { /* right */ +#table-player-info-right { grid-column: 7; grid-row: 3; font-size: large; @@ -370,14 +379,6 @@ body { color: var(--alert-color); } -#table-player-state1 { - display: none; -} - -#table-player-state2 { - display: none; -} - #player-cards { display: none; margin-left: auto; diff --git a/Readme.md b/Readme.md index 4888b50..721a098 100644 --- a/Readme.md +++ b/Readme.md @@ -1,3 +1,53 @@ +# Game process + +1. Register user +- Store name, password +- Create session token +2. Create table +- Store name +- Create id +- Add player to table +3. Wait for all players +4. Deal first 4 cards +- Store all hand cards, show only 4 +5. Collect leger +- Consecutive choice for each player +6. Deal last 4 cards +- Start bidding +7. Bidding +- Choice for each player to match/increase bid +8. Select game +- Highest bidder selects game +- Move to playing +9. Play cards +- Allow shots until certain trick +- Collect completed tricks +- Determine winning card +- End game on last card +10. Finish game +- Count points +- Determine winner +- Back to 4 + +# Architecture + +## Messages to clients over the websocket + +### Table info + +Contains the general information about the table: +- Player names, Table name + +### ConnectionState + +Indicates which players are currently online + +### GameState + +- Current player (either to play a card, state game intention, ) +- First player in the round +- Game type (either game, or negotiation stage) + # TODOs ## Make UI for table diff --git a/Sources/App/Infos/CardInfo.swift b/Sources/App/Infos/CardInfo.swift deleted file mode 100644 index 44b36b4..0000000 --- a/Sources/App/Infos/CardInfo.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -struct CardInfo: ClientMessage { - - static let type: ClientMessageType = .cardInfo - - struct HandCard: Codable { - - let card: CardId - - let playable: Bool - } - - /// The cards for a player - let cards: [HandCard] - - // The cards on the table, as seen from the players perspective - let tableCards: [CardId] -} diff --git a/Sources/App/Infos/PlayerInfo.swift b/Sources/App/Infos/PlayerInfo.swift new file mode 100644 index 0000000..4dd0474 --- /dev/null +++ b/Sources/App/Infos/PlayerInfo.swift @@ -0,0 +1,30 @@ +import Foundation + +struct PlayerInfo: Codable, Equatable { + + let name: PlayerName + + let connected: Bool + + /// The player is the next one to perform an action + let active: Bool + + /// The cards in the hand of the player + let cards: [CardInfo] + + /// The action the player can perform + let actions: [String] + + init(player: Player, isMasked: Bool) { + self.name = player.name + self.connected = player.isConnected + self.active = player.isNextActor + if isMasked { + self.cards = [] + self.actions = [] + } else { + self.actions = player.actions.map { $0.path } + self.cards = player.handCards.map { $0.cardInfo } + } + } +} diff --git a/Sources/App/Infos/PublicTableInfo.swift b/Sources/App/Infos/PublicTableInfo.swift new file mode 100644 index 0000000..70cd504 --- /dev/null +++ b/Sources/App/Infos/PublicTableInfo.swift @@ -0,0 +1,27 @@ +import Foundation + +struct PublicTableInfo: Codable { + + let id: TableId + + let name: TableName + + let players: [PlayerName] + + let tableIsFull: Bool + + init(id: TableId, name: String, players: [PlayerName]) { + self.id = id + self.name = name + self.players = players + self.tableIsFull = players.count == maximumPlayersPerTable + } +} + +extension PublicTableInfo: Comparable { + + static func < (lhs: PublicTableInfo, rhs: PublicTableInfo) -> Bool { + lhs.name < rhs.name + } +} + diff --git a/Sources/App/Infos/TableInfo.swift b/Sources/App/Infos/TableInfo.swift index ab61c06..f2e7302 100644 --- a/Sources/App/Infos/TableInfo.swift +++ b/Sources/App/Infos/TableInfo.swift @@ -1,30 +1,34 @@ import Foundation -struct TableInfo: ClientMessage { +struct TableInfo: Codable { - static let type: ClientMessageType = .tableInfo - let id: String let name: String - - let players: [PlayerState] - let tableIsFull: Bool + let player: PlayerInfo - struct PlayerState: Codable, Equatable { - - let name: PlayerName - - let connected: Bool - - init(name: PlayerName, connected: Bool) { - self.name = name - self.connected = connected - } + let playerLeft: PlayerInfo? + + let playerAcross: PlayerInfo? + + let playerRight: PlayerInfo? + + init(_ table: Table, forPlayerAt playerIndex: Int) { + self.id = table.id + self.name = table.name + self.player = table.player(at: playerIndex)!.info(masked: false) + self.playerLeft = table.player(leftOf: playerIndex)?.info(masked: true) + self.playerAcross = table.player(acrossOf: playerIndex)?.info(masked: true) + self.playerRight = table.player(rightOf: playerIndex)?.info(masked: true) } } +extension TableInfo { + + +} + extension TableInfo: Comparable { static func < (lhs: TableInfo, rhs: TableInfo) -> Bool { diff --git a/Sources/App/Management/ClientConnection.swift b/Sources/App/Management/ClientConnection.swift deleted file mode 100644 index 16fb6f6..0000000 --- a/Sources/App/Management/ClientConnection.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import WebSocketKit - -private let encoder = JSONEncoder() - -enum ClientMessageType: String { - - /// The names and connection states of th player, plus table name and id - case tableInfo = "t" - - /// The hand cards of the player and the cards on the table - case cardInfo = "c" - - /// The game is in the bidding phase - case biddingInfo = "b" - - /// -} - -protocol ClientMessage: Encodable { - - static var type: ClientMessageType { get } -} - -extension WebSocket { - - func send(_ data: T) where T: ClientMessage { - let json = try! encoder.encode(data) - let string = String(data: json, encoding: .utf8)! - self.send(T.type.rawValue + string) - } -} diff --git a/Sources/App/Management/Database.swift b/Sources/App/Management/Database.swift index be9da6c..b476823 100644 --- a/Sources/App/Management/Database.swift +++ b/Sources/App/Management/Database.swift @@ -24,7 +24,7 @@ final class Database { func deletePlayer(named name: PlayerName) { _ = players.deletePlayer(named: name) - tables.remove(player: name) + tables.leaveTable(player: name) } func isValid(sessionToken token: SessionToken) -> Bool { @@ -63,8 +63,8 @@ final class Database { players.registeredPlayerExists(withSessionToken: token) } - func currentTableOfPlayer(named player: PlayerName) -> TableId { - tables.currentTableOfPlayer(named: player) ?? "" + func currentTableOfPlayer(named player: PlayerName) -> TableInfo? { + tables.tableInfo(player: player) } // MARK: Tables @@ -73,20 +73,20 @@ final class Database { Create a new table with optional players. - Parameter name: The name of the table - Parameter players: The player creating the table - - Parameter visible: Indicates that this is a game joinable by everyone + - Parameter isPublic: Indicates that this is a game joinable by everyone - Returns: The table id */ - func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId { - tables.createTable(named: name, player: player, visible: visible) + func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId { + tables.createTable(named: name, player: player, isPublic: isPublic) } - func getPublicTableInfos() -> [TableInfo] { - tables.getPublicTableInfos() + func getPublicTableInfos() -> [PublicTableInfo] { + tables.publicTableList } - func join(tableId: TableId, playerToken: SessionToken) -> JoinTableResult { + func join(tableId: TableId, playerToken: SessionToken) -> Result { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { - return .invalidToken + return .failure(.invalidToken) } return tables.join(tableId: tableId, player: player) } @@ -95,14 +95,14 @@ final class Database { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { return false } - tables.remove(player: player) + tables.leaveTable(player: player) return true } - func dealCards(playerToken: SessionToken) -> DealCardResult { + func performAction(playerToken: SessionToken, action: Player.Action) -> PlayerActionResult { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { return .invalidToken } - return tables.dealCards(player: player) + return tables.performAction(player: player, action: action) } } diff --git a/Sources/App/Management/DiskWriter.swift b/Sources/App/Management/DiskWriter.swift index 4a73676..8a96aa6 100644 --- a/Sources/App/Management/DiskWriter.swift +++ b/Sources/App/Management/DiskWriter.swift @@ -40,19 +40,23 @@ extension DiskWriter { } } - func readLinesFromDisk() throws -> [String] { + func readDataFromDisk() throws -> Data { if #available(macOS 10.15.4, *) { guard let data = try storageFile.readToEnd() else { try storageFile.seekToEnd() - return [] + return Data() } - return parseLines(data: data) + return data } else { - let data = storageFile.readDataToEndOfFile() - return parseLines(data: data) + return storageFile.readDataToEndOfFile() } } + func readLinesFromDisk() throws -> [String] { + let data = try readDataFromDisk() + return parseLines(data: data) + } + private func parseLines(data: Data) -> [String] { String(data: data, encoding: .utf8)! .components(separatedBy: "\n") diff --git a/Sources/App/Management/TableManagement.swift b/Sources/App/Management/TableManagement.swift index 83a5de7..54aeb67 100644 --- a/Sources/App/Management/TableManagement.swift +++ b/Sources/App/Management/TableManagement.swift @@ -9,34 +9,27 @@ typealias TableName = String final class TableManagement: DiskWriter { - /// A list of table ids for public games - private var publicTables = Set() - - /// A mapping from table id to table name (for all tables) - private var tableNames = [TableId: TableName]() - - /// A mapping from table id to participating players - private var tablePlayers = [TableId: [PlayerName]]() - - /// A reverse list of players and their table id - private var playerTables = [PlayerName: TableId]() - - private var playerConnections = [PlayerName : WebSocket]() - - private var tablePhase = [TableId: GamePhase]() - + /// All tables indexed by their id + private var tables = [TableId : Table]() + /// The handle to the file where the tables are persisted let storageFile: FileHandle + /// The url to the file where the tables are persisted let storageFileUrl: URL + /** + Load the tables from a file in the storage folder + - Parameter storageFolder: The url to the folder where the table file is stored + - Throws: Errors when the file could not be read + */ init(storageFolder: URL) throws { let url = storageFolder.appendingPathComponent("tables.txt") storageFileUrl = url storageFile = try Self.prepareFile(at: url) - var entries = [TableId : (name: TableName, public: Bool, players: [PlayerName])]() + var entries = [TableId : (name: TableName, isPublic: Bool, players: [PlayerName])]() try readLinesFromDisk().forEach { line in // Each line has parts: ID | NAME | PLAYER, PLAYER, ... let parts = line.components(separatedBy: ":") @@ -54,31 +47,38 @@ final class TableManagement: DiskWriter { entries[id] = (name, isPublic, players) } } - entries.forEach { id, table in - tableNames[id] = table.name - if table.public { - publicTables.insert(id) - } - tablePlayers[id] = table.players - tablePhase[id] = .waitingForPlayers - for player in table.players { - playerTables[player] = id - } + entries.forEach { id, tableData in + let table = Table(id: id, name: tableData.name, isPublic: tableData.isPublic) + tableData.players.forEach { _ = table.add(player: $0) } + tables[id] = table } - print("Loaded \(tableNames.count) tables") + print("Loaded \(tables.count) tables") } + /** + Writes the table info to disk. + + Currently only the id, name, visibility and players are stored, all other information is lost. + - Parameter table: The changed table information to persist + - Returns: `true`, if the entry was written, `false` on error + */ @discardableResult - private func save(table tableId: TableId) -> Bool { - let name = tableNames[tableId]! - let visible = publicTables.contains(tableId) ? "public" : "private" - let players = tablePlayers[tableId]! - let entry = [tableId, name, visible, players.joined(separator: ",")].joined(separator: ":") + private func writeTableToDisk(table: Table) -> Bool { + let visible = table.isPublic ? "public" : "private" + let players = table.playerNames.joined(separator: ",") + let entry = [table.id, table.name, visible, players].joined(separator: ":") return writeToDisk(line: entry) } + /** + Writes the deletion of a table to disk. + + The deletion is written as a separate entry and appended to the file, in order to reduce disk I/O. + - Parameter tableId: The id of the deleted table + - Returns: `true`, if the entry was written, `false` on error + */ @discardableResult - private func deleteTable(tableId: TableId) -> Bool { + private func writeTableDeletionEntry(tableId: TableId) -> Bool { let entry = [tableId, "", "", ""].joined(separator: ":") return writeToDisk(line: entry) } @@ -87,166 +87,88 @@ final class TableManagement: DiskWriter { Create a new table with optional players. - Parameter name: The name of the table - Parameter players: The player creating the table - - Parameter visible: Indicates that this is a game joinable by everyone + - Parameter isPublic: Indicates that this is a game joinable by everyone - Returns: The table id */ - func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId { - let tableId = TableId.newToken() - - tableNames[tableId] = name - tablePlayers[tableId] = [player] - playerTables[player] = tableId - - if visible { - publicTables.insert(tableId) - } - save(table: tableId) - return tableId + func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId { + let table = Table(newTable: name, isPublic: isPublic) + _ = table.add(player: name) + tables[table.id] = table + writeTableToDisk(table: table) + return table.id } - func getPublicTableInfos() -> [TableInfo] { - publicTables.map(tableInfo).sorted() + /// A list of all public tables + var publicTableList: [PublicTableInfo] { + tables.values.filter { $0.isPublic }.map { $0.publicInfo } } - private func tableInfo(id tableId: TableId) -> TableInfo { - let players = tablePlayers[tableId]!.map(playerState) - return TableInfo( - id: tableId, - name: tableNames[tableId]!, - players: players, - tableIsFull: players.count == maximumPlayersPerTable) + /** + Get the table info for a player + - Parameter player: The name of the player + - Returns: The table info, if the player has joined a table + */ + func tableInfo(player: PlayerName) -> TableInfo? { + currentTable(for: player)?.compileInfo(for: player) } - private func playerState(_ player: PlayerName) -> TableInfo.PlayerState { - .init(name: player, connected: playerIsConnected(player)) - } - - private func playerIsConnected(_ player: PlayerName) -> Bool { - playerConnections[player] != nil - } - - func currentTableOfPlayer(named player: PlayerName) -> TableId? { - playerTables[player] + private func currentTable(for player: PlayerName) -> Table? { + tables.values.first(where: { $0.contains(player: player) }) } /** Join a table. + - Parameter tableId: The table to join + - Parameter player: The name of the player who wants to join. - Returns: The result of the join operation */ - func join(tableId: TableId, player: PlayerName) -> JoinTableResult { - guard var players = tablePlayers[tableId] else { - return .tableNotFound + func join(tableId: TableId, player: PlayerName) -> Result { + if let existing = currentTable(for: player) { + guard existing.id == tableId else { + return .failure(.alreadyJoinedOtherTable) + } + return .success(existing.compileInfo(for: player)!) } - guard !players.contains(player) else { - return .success + guard let table = tables[tableId] else { + return .failure(.tableNotFound) } - guard players.count < maximumPlayersPerTable else { - return .tableIsFull + guard table.add(player: player) else { + return .failure(.tableIsFull) } - players.append(player) - if let oldTable = playerTables[tableId] { - remove(player: player, fromTable: oldTable) - } - tablePlayers[tableId] = players - playerTables[player] = tableId - save(table: tableId) - return .success + writeTableToDisk(table: table) + return .success(table.compileInfo(for: player)!) } - func remove(player: PlayerName, fromTable tableId: TableId) { - tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player } - disconnect(player: player) - playerTables[player] = nil - // TODO: End game if needed - // TODO: Remove table if empty - save(table: tableId) - } - - func remove(player: PlayerName) { - guard let tableId = playerTables[player] else { + /** + A player leaves the table it previously joined + - Parameter player: The name of the player + */ + func leaveTable(player: PlayerName) { + guard let table = currentTable(for: player) else { return } - // Already saves table to disk - remove(player: player, fromTable: tableId) + table.remove(player: player) + writeTableToDisk(table: table) } func connect(player: PlayerName, using socket: WebSocket) -> Bool { - guard let tableId = playerTables[player] else { + guard let table = currentTable(for: player) else { return false } - guard let players = tablePlayers[tableId] else { - print("Player \(player) was assigned to missing table \(tableId.prefix(5))") - playerTables[player] = nil - return false - } - guard players.contains(player) else { - print("Player \(player) wants updates for table \(tableId.prefix(5)) it didn't join") - return false - } - playerConnections[player] = socket - sendTableInfo(toTable: tableId) - // TODO: Send cards to player - return true + return table.connect(player: player, using: socket) } func disconnect(player: PlayerName) { - if let socket = playerConnections.removeValue(forKey: player) { - if !socket.isClosed { - _ = socket.close() - } - } - guard let tableId = playerTables[player] else { + guard let table = currentTable(for: player) else { return } - sendTableInfo(toTable: tableId) - // Change table phase to waiting + table.disconnect(player: player) } - private func sendTableInfo(toTable tableId: TableId) { - let name = tableNames[tableId]! - var players = tablePlayers[tableId]! - let isFull = players.count == maximumPlayersPerTable - for _ in players.count.. DealCardResult { - guard let tableId = playerTables[player] else { + func performAction(player: PlayerName, action: Player.Action) -> PlayerActionResult { + guard let table = currentTable(for: player) else { return .noTableJoined } - guard let players = tablePlayers[tableId] else { - playerTables[player] = nil - print("Player \(player) assigned to missing table \(tableId.prefix(5))") - return .noTableJoined - } - guard players.count == maximumPlayersPerTable else { - return .tableNotFull - } - - let cards = Dealer.deal() - let handCards = ["", "", "", ""] - players.enumerated().forEach { index, player in - guard let socket = playerConnections[player] else { - return - } - let info = CardInfo( - cards: cards[index].map { .init(card: $0.id, playable: false) }, - tableCards: handCards.rotated(toStartAt: index)) - socket.send(info) - } - return .success + return table.perform(action: action, forPlayer: player) } } diff --git a/Sources/App/Model/Card.swift b/Sources/App/Model/Card.swift index a5553ce..2b24b90 100644 --- a/Sources/App/Model/Card.swift +++ b/Sources/App/Model/Card.swift @@ -73,6 +73,15 @@ struct Card: Codable { var points: Int { symbol.points } + + static let allCards: Set = { + let all = Card.Suit.allCases.map { suit in + Card.Symbol.allCases.map { symbol in + Card(suit: suit, symbol: symbol) + } + }.joined() + return Set(all) + }() } extension Card: CustomStringConvertible { @@ -86,3 +95,20 @@ extension Card: Hashable { } +struct PlayableCard { + + let card: Card + + let isPlayable: Bool + + var cardInfo: CardInfo { + .init(card: card.id, playable: isPlayable) + } +} + +struct CardInfo: Codable, Equatable { + + let card: CardId + + let playable: Bool +} diff --git a/Sources/App/Model/CardOrders/CardOrder.swift b/Sources/App/Model/CardOrders/CardOrder.swift new file mode 100644 index 0000000..2c8adbe --- /dev/null +++ b/Sources/App/Model/CardOrders/CardOrder.swift @@ -0,0 +1,49 @@ +// +// File.swift +// +// +// Created by iMac on 03.12.21. +// + +import Foundation + +enum CardOrderType { + + /// The sorting for most games, where heart is trump + case normal + + case wenz + + case geier + + case soloEichel + + case soloBlatt + + case soloSchelln +} + +protocol CardOrder { + + static var trumpOrder: [Card] { get } + + static var sortIndex: [Card : Int] { get } +} + +extension CardOrder { + + static func consecutiveTrumps(_ cards: [Card]) -> Int { + var count = 0 + while cards.contains(trumpOrder[count]) { + count += 1 + } + guard count >= 3 else { + return 0 + } + return count + } + + static func sort(_ cards: [Card]) -> [Card] { + cards.sorted { sortIndex[$0]! < sortIndex[$1]! } + } +} diff --git a/Sources/App/Model/CardOrders/NormalCardOrder.swift b/Sources/App/Model/CardOrders/NormalCardOrder.swift new file mode 100644 index 0000000..509c0af --- /dev/null +++ b/Sources/App/Model/CardOrders/NormalCardOrder.swift @@ -0,0 +1,58 @@ +// +// File.swift +// +// +// Created by iMac on 03.12.21. +// + +import Foundation + +struct NormalCardOrder: CardOrder { + + private static let cardOrder = [ + Card(.eichel, .ober), + Card(.blatt, .ober), + Card(.herz, .ober), + Card(.schelln, .ober), + Card(.eichel, .unter), + Card(.blatt, .unter), + Card(.herz, .unter), + Card(.schelln, .unter), + Card(.herz, .ass), + Card(.herz, .zehn), + Card(.herz, .könig), + Card(.herz, .neun), + Card(.herz, .acht), + Card(.herz, .sieben), + Card(.eichel, .ass), + Card(.eichel, .zehn), + Card(.eichel, .könig), + Card(.eichel, .neun), + Card(.eichel, .acht), + Card(.eichel, .sieben), + Card(.blatt, .ass), + Card(.blatt, .zehn), + Card(.blatt, .könig), + Card(.blatt, .neun), + Card(.blatt, .acht), + Card(.blatt, .sieben), + Card(.schelln, .ass), + Card(.schelln, .zehn), + Card(.schelln, .könig), + Card(.schelln, .neun), + Card(.schelln, .acht), + Card(.schelln, .sieben), + ] + + private static let sortIndex: [Card : Int] = { + cardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + }() + + static let trumpOrder: [Card] = Array(cardOrder[0..<8]) + + private static let trumps: Set = Set(trumpOrder) + + static func trumpCount(_ cards: [Card]) -> Int { + cards.filter { trumps.contains(card) }.count + } +} diff --git a/Sources/App/Model/Dealer.swift b/Sources/App/Model/Dealer.swift new file mode 100644 index 0000000..ca09e5e --- /dev/null +++ b/Sources/App/Model/Dealer.swift @@ -0,0 +1,312 @@ +import Foundation + +private let wenzCardOder = [ + Card(.eichel, .unter), + Card(.blatt, .unter), + Card(.herz, .unter), + Card(.schelln, .unter), + Card(.herz, .ass), + Card(.herz, .zehn), + Card(.herz, .könig), + Card(.herz, .ober), + Card(.herz, .neun), + Card(.herz, .acht), + Card(.herz, .sieben), + Card(.eichel, .ass), + Card(.eichel, .zehn), + Card(.eichel, .könig), + Card(.eichel, .ober), + Card(.eichel, .neun), + Card(.eichel, .acht), + Card(.eichel, .sieben), + Card(.blatt, .ass), + Card(.blatt, .zehn), + Card(.blatt, .könig), + Card(.blatt, .ober), + Card(.blatt, .neun), + Card(.blatt, .acht), + Card(.blatt, .sieben), + Card(.schelln, .ass), + Card(.schelln, .zehn), + Card(.schelln, .könig), + Card(.schelln, .ober), + Card(.schelln, .neun), + Card(.schelln, .acht), + Card(.schelln, .sieben), +] + +private let geierCardOrder = [ + Card(.eichel, .ober), + Card(.blatt, .ober), + Card(.herz, .ober), + Card(.schelln, .ober), + Card(.herz, .ass), + Card(.herz, .zehn), + Card(.herz, .könig), + Card(.herz, .unter), + Card(.herz, .neun), + Card(.herz, .acht), + Card(.herz, .sieben), + Card(.eichel, .ass), + Card(.eichel, .zehn), + Card(.eichel, .könig), + Card(.eichel, .unter), + Card(.eichel, .neun), + Card(.eichel, .acht), + Card(.eichel, .sieben), + Card(.blatt, .ass), + Card(.blatt, .zehn), + Card(.blatt, .könig), + Card(.blatt, .unter), + Card(.blatt, .neun), + Card(.blatt, .acht), + Card(.blatt, .sieben), + Card(.schelln, .ass), + Card(.schelln, .zehn), + Card(.schelln, .könig), + Card(.schelln, .unter), + Card(.schelln, .neun), + Card(.schelln, .acht), + Card(.schelln, .sieben), +] + +private let eichelCardOrder = [ + Card(.eichel, .ober), + Card(.blatt, .ober), + Card(.herz, .ober), + Card(.schelln, .ober), + Card(.eichel, .unter), + Card(.blatt, .unter), + Card(.herz, .unter), + Card(.schelln, .unter), + Card(.eichel, .ass), + Card(.eichel, .zehn), + Card(.eichel, .könig), + Card(.eichel, .neun), + Card(.eichel, .acht), + Card(.eichel, .sieben), + Card(.blatt, .ass), + Card(.blatt, .zehn), + Card(.blatt, .könig), + Card(.blatt, .neun), + Card(.blatt, .acht), + Card(.blatt, .sieben), + Card(.herz, .ass), + Card(.herz, .zehn), + Card(.herz, .könig), + Card(.herz, .neun), + Card(.herz, .acht), + Card(.herz, .sieben), + Card(.schelln, .ass), + Card(.schelln, .zehn), + Card(.schelln, .könig), + Card(.schelln, .neun), + Card(.schelln, .acht), + Card(.schelln, .sieben), +] + +private let blattCardOrder = [ + Card(.eichel, .ober), + Card(.blatt, .ober), + Card(.herz, .ober), + Card(.schelln, .ober), + Card(.eichel, .unter), + Card(.blatt, .unter), + Card(.herz, .unter), + Card(.schelln, .unter), + Card(.blatt, .ass), + Card(.blatt, .zehn), + Card(.blatt, .könig), + Card(.blatt, .neun), + Card(.blatt, .acht), + Card(.blatt, .sieben), + Card(.eichel, .ass), + Card(.eichel, .zehn), + Card(.eichel, .könig), + Card(.eichel, .neun), + Card(.eichel, .acht), + Card(.eichel, .sieben), + Card(.herz, .ass), + Card(.herz, .zehn), + Card(.herz, .könig), + Card(.herz, .neun), + Card(.herz, .acht), + Card(.herz, .sieben), + Card(.schelln, .ass), + Card(.schelln, .zehn), + Card(.schelln, .könig), + Card(.schelln, .neun), + Card(.schelln, .acht), + Card(.schelln, .sieben), +] + +private let schellnCardOrder = [ + Card(.eichel, .ober), + Card(.blatt, .ober), + Card(.herz, .ober), + Card(.schelln, .ober), + Card(.eichel, .unter), + Card(.blatt, .unter), + Card(.herz, .unter), + Card(.schelln, .unter), + Card(.schelln, .ass), + Card(.schelln, .zehn), + Card(.schelln, .könig), + Card(.schelln, .neun), + Card(.schelln, .acht), + Card(.schelln, .sieben), + Card(.eichel, .ass), + Card(.eichel, .zehn), + Card(.eichel, .könig), + Card(.eichel, .neun), + Card(.eichel, .acht), + Card(.eichel, .sieben), + Card(.blatt, .ass), + Card(.blatt, .zehn), + Card(.blatt, .könig), + Card(.blatt, .neun), + Card(.blatt, .acht), + Card(.blatt, .sieben), + Card(.herz, .ass), + Card(.herz, .zehn), + Card(.herz, .könig), + Card(.herz, .neun), + Card(.herz, .acht), + Card(.herz, .sieben), +] + +private let wenzSortIndex: [Card : Int] = { + wenzCardOder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } +}() + +private let geierSortIndex: [Card : Int] = { + geierCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } +}() + +private let eichelSortIndex: [Card : Int] = { + eichelCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } +}() + +private let blattSortIndex: [Card : Int] = { + blattCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } +}() + +private let schellnSortIndex: [Card : Int] = { + schellnCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } +}() + +private let wenzTrumps: [Card] = wenzCardOder[0..<4] + +private let geierTrumps: [Card] = geierCardOrder[0..<4] + +private let eichelTrumps: [Card] = eichelCardOrder[0..<8] + +private let blattTrumps: [Card] = blattCardOrder[0..<8] + +private let schellnTrumps: [Card] = schellnCardOrder[0..<8] + +extension Card { + + func isTrump(in game: GameType) -> Bool { + switch game.sortingType { + case .normal: + trumpsInOrder = normalCardOrder[0..<8] + case .wenz: + trumpsInOrder = wenzCardOder[0..<4] + case .geier: + trumpsInOrder = geierCardOrder[0..<4] + case .soloEichel: + trumpsInOrder = eichelCardOrder[0..<8] + case .soloBlatt: + trumpsInOrder = blattCardOrder[0..<8] + case .soloSchelln: + trumpsInOrder = schellnCardOrder[0..<8] + } + } +} + +extension Array where Element == Card { + + func sorted(cardOrder order: CardOrder) -> [Card] { + switch order { + case .normal: + return sorted { normalSortIndex[$0]! < normalSortIndex[$1]! } + case .wenz: + return sorted { wenzSortIndex[$0]! < wenzSortIndex[$1]! } + case .geier: + return sorted { geierSortIndex[$0]! < geierSortIndex[$1]! } + case .soloEichel: + return sorted { eichelSortIndex[$0]! < eichelSortIndex[$1]! } + case .soloBlatt: + return sorted { blattSortIndex[$0]! < blattSortIndex[$1]! } + case .soloSchelln: + return sorted { schellnSortIndex[$0]! < schellnSortIndex[$1]! } + } + } + + func consecutiveTrumps(for game: GameType) -> Int { + var count = 0 + let trumpsInOrder: Array.SubSequence + switch game.sortingType { + case .normal: + trumpsInOrder = normalCardOrder[0..<8] + case .wenz: + trumpsInOrder = wenzCardOder[0..<4] + case .geier: + trumpsInOrder = geierCardOrder[0..<4] + case .soloEichel: + trumpsInOrder = eichelCardOrder[0..<8] + case .soloBlatt: + trumpsInOrder = blattCardOrder[0..<8] + case .soloSchelln: + trumpsInOrder = schellnCardOrder[0..<8] + } + while contains(trumpsInOrder[count]) { + count += 1 + } + guard count >= 3 else { + return 0 + } + return count + } + + func trumpCount(for game: GameType) -> Int { + + } + + /** + Split cards into chunks to assign them to players. + - Note: The array must contain a multiple of the `size` parameter + */ + func split(intoChunksOf size: Int) -> [Hand] { + stride(from: 0, to: count, by: size).map { i in + Array(self[i.. [Hand] { + // Select 16 random cards for the first hands + Array(Card.allCards.shuffled()[0..<16]) + .split(intoChunksOf: 4) + .map { $0.sorted(cardOrder: .normal) } + } + + + static func dealRemainingCards(of initial: [Hand]) -> [Hand] { + Card.allCards + .subtracting(initial.reduce([], +)) + .shuffled() + .split(intoChunksOf: 4) + } + +} diff --git a/Sources/App/Model/Game.swift b/Sources/App/Model/Game.swift index ba9e42e..0a5ed2b 100644 --- a/Sources/App/Model/Game.swift +++ b/Sources/App/Model/Game.swift @@ -32,9 +32,8 @@ struct Game: Codable { self.numberOfDoubles = doubles self.cards = cards self.leaders = leaders - self.consecutiveTrumps = Dealer.consecutiveTrumps( - in: leaders.map { cards[$0] }.joined(), - for: type) + self.consecutiveTrumps = Array(leaders.map { cards[$0] }.joined()) + .consecutiveTrumps(for: type) self.currentActor = starter self.lastTrickWinner = starter self.completedTricks = [] diff --git a/Sources/App/Infos/GameType.swift b/Sources/App/Model/GameType.swift similarity index 60% rename from Sources/App/Infos/GameType.swift rename to Sources/App/Model/GameType.swift index b3df7a6..dbd3427 100644 --- a/Sources/App/Infos/GameType.swift +++ b/Sources/App/Model/GameType.swift @@ -2,6 +2,23 @@ import Foundation enum GameType: Codable { + enum GameClass: Int { + case ruf = 1 + case hochzeit = 2 + case bettel = 3 + case wenzGeier = 4 + case solo = 5 + + var cost: Int { + switch self { + case .ruf: return 5 + case .hochzeit: return 10 + case .bettel: return 15 + case .wenzGeier, .solo: return 20 + } + } + } + case rufEichel case rufBlatt case rufSchelln @@ -14,18 +31,18 @@ enum GameType: Codable { case soloHerz case soloSchelln - var gameClass: Int { + var gameClass: GameClass { switch self { case .rufEichel, .rufBlatt, .rufSchelln: - return 1 + return .ruf case .hochzeit: - return 2 + return .hochzeit case .bettel: - return 3 + return .bettel case .wenz, .geier: - return 4 + return .wenzGeier case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln: - return 5 + return .solo } } @@ -39,21 +56,10 @@ enum GameType: Codable { } var basicCost: Int { - switch self { - case .rufEichel, .rufBlatt, .rufSchelln: - return 5 - case .hochzeit: - return 10 - case .bettel: - return 15 - case .wenz, .geier: - return 20 - case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln: - return 20 - } + gameClass.cost } - var sortingType: CardSortingStrategy { + var sortingType: CardOrderType { switch self { case .wenz: return .wenz @@ -70,19 +76,3 @@ enum GameType: Codable { } } } - -enum CardSortingStrategy { - - /// The sorting for most games, where heart is trump - case normal - - case wenz - - case geier - - case soloEichel - - case soloBlatt - - case soloSchelln -} diff --git a/Sources/App/Model/Player.swift b/Sources/App/Model/Player.swift new file mode 100644 index 0000000..2109afe --- /dev/null +++ b/Sources/App/Model/Player.swift @@ -0,0 +1,195 @@ +import Foundation +import WebSocketKit +import CloudKit + +private let encoder = JSONEncoder() + +final class Player { + + let name: PlayerName + + /// The player is the first to play a card in a new game + var playsFirstCard = false + + /// The player is the next to perform an action (e.g. play a card) + var isNextActor = false + + /// The player must select the game to play after winning the auction + var selectsGame = false + + /// The players plays/played the first card for the current trick + var startedCurrentTrick = false + + /// The action available to the player + var actions: [Action] = [] + + /// Indicates if the player doubled ("legen") + var didDoubleAfterFourCards: Bool? = nil + + /// Indicates if the player is still involved in the bidding process + var isStillBidding = true + + /// Indicates that the player leads the game ("Spieler") + var isGameLeader = false + + /// Indicates the number of raises ("Schuss") of the player + var numberOfRaises = 0 + + /// The remaining cards of the player + var handCards: [PlayableCard] = [] + + /// The card played for the current trick + var playedCard: Card? = nil + + /// All tricks won by the player in this game + var wonTricks: [Trick] = [] + + var socket: WebSocket? = nil + + init(name: PlayerName) { + self.name = name + } + + var rawCards: [Card] { + handCards.map { $0.card } + } + + func connect(using socket: WebSocket) { + _ = self.socket?.close() + self.socket = socket + } + + func send(_ info: TableInfo) { + try? socket?.send(encodeJSON(info)) + } + + func disconnect() -> Bool { + guard let socket = socket else { + return false + } + do { + try socket.close().wait() + } catch { + print("Failed to close socket for player: \(name): \(error)") + } + self.socket = nil + return true + } + + func prepareForFirstGame(isFirstPlayer: Bool) { + playsFirstCard = isFirstPlayer + isNextActor = isFirstPlayer + selectsGame = false // Not relevant in this phase + startedCurrentTrick = isFirstPlayer + actions = [.deal] + didDoubleAfterFourCards = nil // Not relevant in this phase + isStillBidding = false // Not relevant in this phase + isGameLeader = false // Not relevant in this phase + numberOfRaises = 0 // Not relevant in this phase + handCards = [] + playedCard = nil + wonTricks = [] + } + + func assignFirstCards(_ cards: Hand) { + selectsGame = false // Not relevant in this phase + actions = [.initialDoubleCost, .noDoubleCost] + didDoubleAfterFourCards = nil + isStillBidding = false // Not relevant in this phase + isGameLeader = false // Not relevant in this phase + numberOfRaises = 0 // Not relevant in this phase + handCards = cards.map { .init(card: $0, isPlayable: false) } + playedCard = nil + wonTricks = [] + } + + func didDouble(_ double: Bool) { + selectsGame = false // Not relevant in this phase + actions = [] + didDoubleAfterFourCards = double + isStillBidding = false // Not relevant in this phase + isGameLeader = false // Not relevant in this phase + numberOfRaises = 0 // Not relevant in this phase + playedCard = nil + wonTricks = [] + } + + func assignRemainingCards(_ cards: Hand) { + isStillBidding = true + isGameLeader = false + numberOfRaises = 0 + handCards = (rawCards + cards) + .sorted(CardOrderType: .normal) + .map { .init(card: $0, isPlayable: false) } + playedCard = nil + wonTricks = [] + } + + func startAuction() { + selectsGame = false // Not relevant in this phase + if playsFirstCard { + actions = [.withdrawFromAuction, .increaseOrMatchGame] + } else { + + } + actions = [] + isStillBidding = true + isGameLeader = false // Not relevant in this phase + numberOfRaises = 0 // Not relevant in this phase + playedCard = nil + wonTricks = [] + } + + func info(masked: Bool) -> PlayerInfo { + .init(player: self, isMasked: masked) + } +} + +extension Player { + + /// Indicate that the player is connected when at a table + var isConnected: Bool { + guard let socket = socket else { + return false + } + guard !socket.isClosed else { + self.socket = nil + return false + } + return true + } +} + +extension Player { + + enum Action: String, Codable { + /// The player can request cards to be dealt + case deal = "deal" + + /// The player doubles on the initial four cards + case initialDoubleCost = "double" + + /// The player does not double on the initial four cards + case noDoubleCost = "skip" + + /// The player offers a wedding (one trump card) + case offerWedding = "wedding" + + /// The player can choose to accept the wedding + case acceptWedding = "accept" + + /// The player matches or increases the game during auction + case increaseOrMatchGame = "bid" + + /// The player does not want to play + case withdrawFromAuction = "out" + + /// The player claims to win and doubles the game cost ("schießen") + case doubleDuringGame = "raise" + + /// The url path for the client to call (e.g. /player/deal) + var path: String { + rawValue + } + } +} diff --git a/Sources/App/Model/Table.swift b/Sources/App/Model/Table.swift new file mode 100644 index 0000000..631f00e --- /dev/null +++ b/Sources/App/Model/Table.swift @@ -0,0 +1,233 @@ +import Foundation +import WebSocketKit + +private extension Int { + + mutating func advanceInTable() { + self = (self + 1) % maximumPlayersPerTable + } +} + +final class Table { + + let id: TableId + + let name: TableName + + let isPublic: Bool + + var players: [Player] = [] + + var phase: GamePhase = .waitingForPlayers + + var gameType: GameType? = nil + + var minimumPlayableGame: GameType.GameClass = .ruf + + /// Indicates if doubles are still allowed + var canDoubleDuringGame = false + + /// Indicates if any player doubled during the current round, extending it to the next round + var didDoubleInCurrentRound = false + + /// Indicates that all players acted after the first four cards + var allPlayersFinishedDoubling: Bool { + !players.contains { $0.didDoubleAfterFourCards == nil } + } + + init(id: TableId, name: TableName, isPublic: Bool) { + self.id = id + self.name = name + self.isPublic = isPublic + } + + init(newTable name: TableName, isPublic: Bool) { + self.id = .newToken() + self.name = name + self.isPublic = isPublic + } + + func add(player: PlayerName) -> Bool { + guard !isFull else { + return false + } + let player = Player(name: player) + players.append(player) + if isFull { + prepareTableForFirstGame() + } + sendUpdateToAllPlayers() + return true + } + + func contains(player: PlayerName) -> Bool { + players.contains { $0.name == player } + } + + func select(player: PlayerName) -> Player? { + players.first { $0.name == player } + } + + func player(leftOf index: Int) -> Player? { + player(at: (index + 1) % 4) + } + + func player(acrossOf index: Int) -> Player? { + player(at: (index + 2) % 4) + } + + func player(rightOf index: Int) -> Player? { + player(at: (index + 3) % 4) + } + + func player(at index: Int) -> Player? { + guard index < players.count else { + return nil + } + return players[index] + } + + func remove(player: PlayerName) { + guard contains(player: player) else { + return + } + players = players.filter { $0.name != player } + reset() + } + + func connect(player name: PlayerName, using socket: WebSocket) -> Bool { + guard let player = select(player: name) else { + return false + } + player.connect(using: socket) + sendUpdateToAllPlayers() + return true + } + + func disconnect(player name: PlayerName) { + guard let player = select(player: name) else { + return + } + guard player.disconnect() else { + return + } + sendUpdateToAllPlayers() + return + } + + private func prepareTableForFirstGame() { + self.phase = .waitingForPlayers + self.gameType = nil + self.minimumPlayableGame = .ruf // Not relevant in this phase + self.canDoubleDuringGame = true // Not relevant in this phase + self.didDoubleInCurrentRound = false // Not relevant in this phase + let index = players.firstIndex { $0.playsFirstCard } ?? 0 + for i in 0.. PlayerActionResult { + defer { sendUpdateToAllPlayers() } + switch action { + case .deal: + return dealInitialCards() + case .initialDoubleCost: + return perform(double: true, forPlayer: player) + case .noDoubleCost: + return perform(double: false, forPlayer: player) + case .offerWedding: + fatalError() + case .acceptWedding: + fatalError() + case .increaseOrMatchGame: + fatalError() + case .withdrawFromAuction: + fatalError() + case .doubleDuringGame: + fatalError() + } + } + + private func dealInitialCards() -> PlayerActionResult { + guard isFull else { + return .tableNotFull + } + guard phase == .waitingForPlayers else { + return .tableStateInvalid + } + + phase = .collectingDoubles + gameType = nil + minimumPlayableGame = .ruf + + let cards = Dealer.dealFirstCards() + for (index, player) in players.enumerated() { + player.assignFirstCards(cards[index]) + } + return .success + } + + func perform(double: Bool, forPlayer name: PlayerName) -> PlayerActionResult { + let player = select(player: player)! + player.didDouble(double) + if allPlayersFinishedDoubling { + dealAdditionalCards() + } + return .success + } + + private func dealAdditionalCards() { + let cards = Dealer.dealRemainingCards(of: players.map { $0.rawCards }) + for (index, player) in players.enumerated() { + player.assignRemainingCards(cards[index]) + } + return .success + } + + private func startAuction() { + players.forEach { $0.startAuction() } + minimumPlayableGame = .ruf + } + + private func reset() { + phase = .waitingForPlayers + gameType = nil + minimumPlayableGame = .ruf + } +} + +extension Table { + + var isFull: Bool { + players.count == maximumPlayersPerTable + } + + var publicInfo: PublicTableInfo { + .init(id: id, name: name, players: playerNames) + } + + var playerNames: [PlayerName] { + players.map { $0.name } + } + + func compileInfo(for player: PlayerName) -> TableInfo? { + guard let index = players.firstIndex(where: { $0.name == player }) else { + return nil + } + + return TableInfo(self, forPlayerAt: index) + } +} diff --git a/Sources/App/Model/Trick.swift b/Sources/App/Model/Trick.swift index 07a5147..f14e0b6 100644 --- a/Sources/App/Model/Trick.swift +++ b/Sources/App/Model/Trick.swift @@ -5,7 +5,7 @@ typealias Trick = [Card] extension Trick { func winnerIndex(forGameType type: GameType) -> Int { - let highCard = Dealer.sort(cards: self, using: type.sortingType).first! + let highCard = sorted(cardOrder: type.sortingType).first! return firstIndex(of: highCard)! } diff --git a/Sources/App/Results/DealCardsResult.swift b/Sources/App/Results/DealCardsResult.swift index ca12d1f..937b98b 100644 --- a/Sources/App/Results/DealCardsResult.swift +++ b/Sources/App/Results/DealCardsResult.swift @@ -1,7 +1,7 @@ import Foundation -enum DealCardResult { +enum PlayerActionResult { case success diff --git a/Sources/App/Results/JoinTableResult.swift b/Sources/App/Results/JoinTableResult.swift index fd1c7b8..8545a8a 100644 --- a/Sources/App/Results/JoinTableResult.swift +++ b/Sources/App/Results/JoinTableResult.swift @@ -1,9 +1,10 @@ import Foundation -enum JoinTableResult { +enum JoinTableResult: Error { + case invalidToken + case alreadyJoinedOtherTable case tableNotFound case tableIsFull - case success } diff --git a/Sources/App/Sorting/Dealer.swift b/Sources/App/Sorting/Dealer.swift deleted file mode 100644 index e251519..0000000 --- a/Sources/App/Sorting/Dealer.swift +++ /dev/null @@ -1,298 +0,0 @@ -import Foundation - -struct Dealer { - - private static let normalCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), - ] - - private static let wenzCardOder = [ - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .ober), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .ober), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .ober), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .ober), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), - ] - - private static let geierCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .unter), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .unter), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .unter), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .unter), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), - ] - - private static let eichelCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), - ] - - private static let blattCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), - ] - - private static let schellnCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - ] - - private static let normalSortIndex: [Card : Int] = { - normalCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } - }() - - private static let wenzSortIndex: [Card : Int] = { - wenzCardOder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } - }() - - private static let geierSortIndex: [Card : Int] = { - geierCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } - }() - - private static let eichelSortIndex: [Card : Int] = { - eichelCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } - }() - - private static let blattSortIndex: [Card : Int] = { - blattCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } - }() - - private static let schellnSortIndex: [Card : Int] = { - schellnCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } - }() - - static func sort(cards: T, using strategy: CardSortingStrategy = .normal) -> [Card] where T: Sequence, T.Element == Card { - switch strategy { - case .normal: - return cards.sorted { normalSortIndex[$0]! < normalSortIndex[$1]! } - case .wenz: - return cards.sorted { wenzSortIndex[$0]! < wenzSortIndex[$1]! } - case .geier: - return cards.sorted { geierSortIndex[$0]! < geierSortIndex[$1]! } - case .soloEichel: - return cards.sorted { eichelSortIndex[$0]! < eichelSortIndex[$1]! } - case .soloBlatt: - return cards.sorted { blattSortIndex[$0]! < blattSortIndex[$1]! } - case .soloSchelln: - return cards.sorted { schellnSortIndex[$0]! < schellnSortIndex[$1]! } - } - } - - /** - Creates a random assignment of 8 cards per 4 players. - */ - static func deal() -> [[Card]] { - let deck = Card.Suit.allCases.map { suit in - Card.Symbol.allCases.map { symbol in - Card(suit: suit, symbol: symbol) - } - }.joined() - let random = Array(deck).shuffled() - return (0..<4).map { part -> Array.SubSequence in - let start = part * 8 - let end = start + 8 - return random[start..(in cards: T, for game: GameType) -> Int where T: Sequence, T.Element == Card { - var count = 0 - let trumpsInOrder: Array.SubSequence - switch game.sortingType { - case .normal: - trumpsInOrder = normalCardOrder[0..<8] - case .wenz: - trumpsInOrder = wenzCardOder[0..<4] - case .geier: - trumpsInOrder = geierCardOrder[0..<4] - case .soloEichel: - trumpsInOrder = eichelCardOrder[0..<8] - case .soloBlatt: - trumpsInOrder = blattCardOrder[0..<8] - case .soloSchelln: - trumpsInOrder = schellnCardOrder[0..<8] - } - while cards.contains(trumpsInOrder[count]) { - count += 1 - } - guard count >= 3 else { - return 0 - } - return count - } -} diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index eb0341a..b721380 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -9,10 +9,15 @@ private let maximumPlayerNameLength = 40 /// The maximum length of a valid password private let maximumPasswordLength = 40 +func encodeJSON(_ response: T) throws -> String where T: Encodable { + let data = try encoder.encode(response) + return String(data: data, encoding: .utf8)! +} + func routes(_ app: Application) throws { - + // MARK: Players & Sessions - + /** Create a new player - Parameter name: The name of the player, included in the url @@ -33,7 +38,7 @@ func routes(_ app: Application) throws { password.count < maximumPasswordLength else { throw Abort(.notAcceptable) // 406 } - + guard let hash = try? req.password.hash(password) else { throw Abort(.failedDependency) // 424 } @@ -42,7 +47,7 @@ func routes(_ app: Application) throws { } return token } - + /** Delete a player. - Parameter name: The name of the player, included in the url @@ -70,7 +75,7 @@ func routes(_ app: Application) throws { database.deletePlayer(named: name) return "" } - + /** Log in as an existing player. - Parameter name: The name of the player, included in the url @@ -98,7 +103,7 @@ func routes(_ app: Application) throws { let token = database.startNewSessionForRegisteredPlayer(named: name) return token } - + /** Log in using a session token. - Parameter token: The session token of the player, as a string in the request body @@ -116,7 +121,7 @@ func routes(_ app: Application) throws { } return player } - + /** Log out. - Parameter name: The name of the player, included in the url @@ -133,14 +138,14 @@ func routes(_ app: Application) throws { database.endSession(forSessionToken: token) return "" } - + /** Get the current table of the player, if one exists. - Parameter token: The session token of the player, as a string in the request body - Throws: - 400: Missing token - 401: Invalid token - - Returns: The table id, or an empty string + - Returns: The table info, or an empty string */ app.post("player", "table") { req -> String in guard let token = req.body.string else { @@ -149,9 +154,12 @@ func routes(_ app: Application) throws { guard let player = database.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } - return database.currentTableOfPlayer(named: player) + guard let info = database.currentTableOfPlayer(named: player) else { + return "" + } + return try encodeJSON(info) } - + /** Start a new websocket connection for the client to receive table updates from the server - Returns: Nothing @@ -165,9 +173,9 @@ func routes(_ app: Application) throws { } } } - + // MARK: Tables - + /** Create a new table. - Parameter visibility: Indicate a `"public"` or `"private"` table @@ -183,22 +191,22 @@ func routes(_ app: Application) throws { let token = req.body.string else { throw Abort(.badRequest) // 400 } - let isVisible: Bool + let isPublic: Bool if visibility == "private" { - isVisible = false + isPublic = false } else if visibility == "public" { - isVisible = true + isPublic = true } else { throw Abort(.badRequest) // 400 } - + guard let player = database.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } - let tableId = database.createTable(named: tableName, player: player, visible: isVisible) + let tableId = database.createTable(named: tableName, player: player, isPublic: isPublic) return tableId } - + /** List the public tables. - Parameter token: The session token of the player, as a string in the request body @@ -215,10 +223,9 @@ func routes(_ app: Application) throws { throw Abort(.forbidden) // 403 } let list = database.getPublicTableInfos() - let data = try encoder.encode(list) - return String(data: data, encoding: .utf8)! + return try encodeJSON(list) } - + /** Join a table. - Parameter table: The table id @@ -226,6 +233,7 @@ func routes(_ app: Application) throws { - Throws: - 400: Missing token - 401: The session token is invalid + - 403: The player already sits at another table - 410: The table id doesn't exist - 417: The table is already full and can't be joined - Returns: Nothing @@ -236,14 +244,19 @@ func routes(_ app: Application) throws { throw Abort(.badRequest) } switch database.join(tableId: table, playerToken: token) { - case .invalidToken: - throw Abort(.unauthorized) // 401 - case .tableNotFound: - throw Abort(.gone) // 410 - case .tableIsFull: - throw Abort(.expectationFailed) // 417 - case .success: - return "" + case .success(let table): + return try encodeJSON(table) + case .failure(let result): + switch result { + case .invalidToken: + throw Abort(.unauthorized) // 401 + case .alreadyJoinedOtherTable: + throw Abort(.forbidden) // 403 + case .tableNotFound: + throw Abort(.gone) // 410 + case .tableIsFull: + throw Abort(.expectationFailed) // 417 + } } } @@ -265,11 +278,13 @@ func routes(_ app: Application) throws { return "" } - app.post("deal") { req -> String in - guard let token = req.body.string else { - throw Abort(.badRequest) - } - switch database.dealCards(playerToken: token) { + app.post("player", "action", ":action") { req -> String in + guard let token = req.body.string, + let actionString = req.parameters.get("action"), + let action = Player.Action(rawValue: actionString) else { + throw Abort(.badRequest) + } + switch database.performAction(playerToken: token, action: action) { case .success: return "" case .invalidToken: