Sync push

This commit is contained in:
Christoph Hagen 2021-12-03 18:03:29 +01:00
parent 4fe71136a2
commit 3db9652cad
27 changed files with 1540 additions and 898 deletions

View File

@ -32,26 +32,26 @@ async function resumeSessionRequest(token) {
async function performGetCurrentTableRequest(token) { async function performGetCurrentTableRequest(token) {
return fetch("player/table", { method: 'POST', body: token }) return fetch("player/table", { method: 'POST', body: token })
.then(convertServerResponse) .then(convertServerResponse)
.then(convertJsonResponse)
} }
async function performCreateTableRequest(token, name, visibility) { async function performCreateTableRequest(token, name, visibility) {
const vis = visibility ? "public" : "private"; const vis = visibility ? "public" : "private";
return fetch("/table/create/" + vis + "/" + name, { method: 'POST', body: token }) return fetch("/table/create/" + vis + "/" + name, { method: 'POST', body: token })
.then(convertServerResponse) .then(convertServerResponse)
.then(convertJsonResponse)
} }
async function performJoinTableRequest(tableId, token) { async function performJoinTableRequest(tableId, token) {
return fetch("/table/join/" + tableId, { method: 'POST', body: token }) return fetch("/table/join/" + tableId, { method: 'POST', body: token })
.then(convertServerResponse) .then(convertServerResponse)
.then(function(value) {}) .then(convertJsonResponse)
} }
async function performGetPublicTablesRequest(token) { async function performGetPublicTablesRequest(token) {
return fetch("/tables/public", { method: 'POST', body: token }) return fetch("/tables/public", { method: 'POST', body: token })
.then(convertServerResponse) .then(convertServerResponse)
.then(function(text) { .then(convertJsonResponse)
return JSON.parse(text);
})
} }
async function performLeaveTableRequest(token) { async function performLeaveTableRequest(token) {
@ -60,10 +60,9 @@ async function performLeaveTableRequest(token) {
.then(function(value) {}) .then(function(value) {})
} }
async function performDealCardsRequest(token) { async function performPlayerActionRequest(token, action) {
return fetch("/deal", { method: 'POST', body: token }) return fetch("/player/action/" + action, { method: 'POST', body: token })
.then(convertServerResponse) .then(convertServerResponse)
.then(function(value) {})
} }
function convertServerResponse(response) { function convertServerResponse(response) {
@ -90,3 +89,10 @@ function convertServerResponse(response) {
throw Error("Unexpected response: " + response.statusText) throw Error("Unexpected response: " + response.statusText)
} }
} }
function convertJsonResponse(text) {
if (text == "") {
return null;
}
return JSON.parse(text);
}

View File

@ -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 playerName = ""
var debugSessionToken = null
const debugMode = true // Does not load session token, to allow multiple players per browser
function setDisplayStyle(id, style) { function setDisplayStyle(id, style) {
document.getElementById(id).style.display = style document.getElementById(id).style.display = style
} }
function hide(elementId) {
setDisplayStyle(elementId, "none")
}
function showLoginElements() { function showLoginElements() {
setDisplayStyle("login-window", "table") setDisplayStyle("login-window", "table")
setDisplayStyle("top-bar", "none") hide("top-bar")
setDisplayStyle("table-list-bar", "none") hide("table-list-bar")
setDisplayStyle("table-list", "none") hide("table-list")
setDisplayStyle("game-bar", "none") hide("game-bar")
setDisplayStyle("table-players", "none") hide("table-players")
setDisplayStyle("player-cards", "none") hide("player-cards")
} }
function showTableListElements() { function showTableListElements() {
setDisplayStyle("login-window", "none") hide("login-window")
setDisplayStyle("top-bar", "inherit") setDisplayStyle("top-bar", "inherit")
setDisplayStyle("table-list-bar", "grid") setDisplayStyle("table-list-bar", "grid")
setDisplayStyle("table-list", "inherit") setDisplayStyle("table-list", "inherit")
setDisplayStyle("game-bar", "none") hide("game-bar")
setDisplayStyle("table-players", "none") hide("table-players")
setDisplayStyle("player-cards", "none") hide("player-cards")
} }
function showGameElements() { function showGameElements() {
setDisplayStyle("login-window", "none") hide("login-window")
setDisplayStyle("top-bar", "inherit") setDisplayStyle("top-bar", "inherit")
setDisplayStyle("table-list-bar", "none") hide("table-list-bar")
setDisplayStyle("table-list", "none") hide("table-list")
setDisplayStyle("game-bar", "grid") setDisplayStyle("game-bar", "grid")
setDisplayStyle("table-players", "grid") setDisplayStyle("table-players", "grid")
setDisplayStyle("player-cards", "grid") setDisplayStyle("player-cards", "grid")
} }
function showTableName(name) {
document.getElementById("table-connected-label").innerHTML = name
}
function showConnectedState() { function showConnectedState() {
const label = document.getElementById("table-connected-label") showConnectionState("bottom", true)
label.innerHTML = "Connected"
label.style.color = "var(--text-color)"
} }
function showDisconnectedState() { function showDisconnectedState() {
const label = document.getElementById("table-connected-label") showConnectionState("bottom", false)
label.innerHTML = "Disconnected"
label.style.color = "var(--alert-color)"
} }
function showDealButton() { function showDealButton() {
@ -55,7 +61,7 @@ function showDealButton() {
} }
function hideDealButton() { function hideDealButton() {
setDisplayStyle("deal-button", "none") hide("deal-button")
} }
function setPlayerName(name) { function setPlayerName(name) {
@ -80,10 +86,17 @@ function clearLoginPassword() {
} }
function getSessionToken() { function getSessionToken() {
if (debugMode) {
return debugSessionToken
}
return localStorage.getItem('token') return localStorage.getItem('token')
} }
function setSessionToken(token) { function setSessionToken(token) {
if (debugMode) {
debugSessionToken = token
return
}
localStorage.setItem('token', token) localStorage.setItem('token', token)
} }
@ -111,23 +124,25 @@ function setTableListContent(content) {
document.getElementById("table-list").innerHTML = content document.getElementById("table-list").innerHTML = content
} }
function setTablePlayerInfo(nr, name, connected, active) { function setTablePlayerInfo(position, name, connected, active) {
const infoElement = "table-player-name" + nr.toString() nameColor = active ? "var(--button-color)" : "var(--text-color)"
const connectionElement = "table-player-state" + nr setTablePlayerElements(position, name, nameColor, connected)
const nameElement = document.getElementById(infoElement) }
if (name == "") {
nameElement.innerHTML = "Empty" function setEmptyPlayerInfo(position) {
nameElement.style.color = "var(--secondary-text-color)" setTablePlayerElements(position, "Empty", "var(--secondary-text-color)", true)
setDisplayStyle(connectionElement, "none") }
return
} function setTablePlayerElements(position, name, nameColor, connected) {
const nameElement = document.getElementById("table-player-name-" + position)
nameElement.style.color = nameColor
nameElement.innerHTML = name nameElement.innerHTML = name
nameElement.style.color = active ? "var(--button-color)" : "var(--text-color)" showConnectionState(position, connected)
if (connected) { }
setDisplayStyle(connectionElement, "none")
} else { function showConnectionState(position, connected) {
setDisplayStyle(connectionElement, "inherit") const connectionElement = "table-player-state-" + position
} setDisplayStyle(connectionElement, connected ? "none" : "inherit")
} }
function setTableCard(index, card) { function setTableCard(index, card) {
@ -138,7 +153,7 @@ function setTableCard(index, card) {
function setHandCard(index, card, playable) { function setHandCard(index, card, playable) {
const id = "player-card" + index.toString() const id = "player-card" + index.toString()
setCard(id, card) setCard(id, card)
document.getElementById(id).disabled = true document.getElementById(id).disabled = !playable
} }
function setCard(id, card) { function setCard(id, card) {
@ -148,3 +163,124 @@ function setCard(id, card) {
document.getElementById(id).style.backgroundImage = "url('/cards/" + card + ".jpeg')" 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
}

View File

@ -1,7 +1,8 @@
// The web socket to connect to the server // The web socket to connect to the server
var socket = null; var socket = null;
var tableId = ""; var tableId = null;
var activePlayer = null;
function closeSocketIfNeeded() { function closeSocketIfNeeded() {
if (socket) { if (socket) {
@ -11,7 +12,7 @@ function closeSocketIfNeeded() {
} }
function didLeaveTable() { function didLeaveTable() {
tableId = "" tableId = null
} }
function didCloseSocket() { function didCloseSocket() {
@ -22,7 +23,15 @@ function setTableId(table) {
tableId = table tableId = table
} }
function showBlankLoginScreen(text) { function showLoginWithError(error) {
showLoginWithText(error.message)
}
function showBlankLogin() {
showLoginWithText("")
}
function showLoginWithText(text) {
closeSocketIfNeeded() closeSocketIfNeeded()
didLeaveTable() didLeaveTable()
clearLoginPassword() clearLoginPassword()
@ -32,125 +41,93 @@ function showBlankLoginScreen(text) {
setLoginError(text) setLoginError(text)
} }
function showGame(tableId) { function showTableOrList(table) {
setTableId(tableId) if (table == null) {
const token = getSessionToken() didLeaveTable()
if (token) { showTableListElements()
showGameElements() closeSocketIfNeeded()
openSocket(token) refreshTables()
// TODO: Show interface // TODO: Show table list, refresh
console.log("Show table " + tableId) return
} else {
showBlankLoginScreen("")
} }
const token = getSessionToken()
if (token == null) {
showBlankLogin()
return
}
clearTableName()
setTableId(table.id)
showGameElements()
updateTableInfo(table)
openSocket(token)
} }
function registerUser() { function registerUser() {
createSession(performRegisterPlayerRequest)
}
function loginUser() {
createSession(performLoginPlayerRequest)
}
function createSession(inputFunction) {
const username = getLoginName() const username = getLoginName()
const password = getLoginPassword() const password = getLoginPassword()
if (username == "") { if (username == "") {
setLoginError("Please enter your desired user name") showLoginWithText("Please enter your user name")
return return
} }
if (password == "") { if (password == "") {
setLoginError("Please enter a password") showLoginWithText("Please enter a password")
return return
} }
performRegisterPlayerRequest(username, password) inputFunction(username, password)
.then(function(token) { .then(function(token) {
setSessionToken(token) setSessionToken(token)
setPlayerName(username) setPlayerName(username)
showTableListElements()
loadCurrentTable(token) loadCurrentTable(token)
}).catch(function(error) { }).catch(showLoginWithError)
setLoginError(error.message) }
})
function logoutUser() {
const token = getSessionToken()
if (token == null) {
showBlankLogin()
return
}
performLogoutRequest(token)
.then(function() {
showBlankLogin()
}).catch(showLoginWithError)
} }
function deletePlayerAccount() { function deletePlayerAccount() {
const name = getPlayerName() const name = getPlayerName()
const password = getLoginPassword() const password = getLoginPassword()
performDeletePlayerRequest(name, password, function(error) { performDeletePlayerRequest(name, password)
if (error == "") { .then(showBlankLogin)
showBlankLoginScreen("") .catch(showLoginWithError)
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("")
}
} }
function loadExistingSession() { function loadExistingSession() {
const token = getSessionToken() const token = getSessionToken()
if (token) { if (token == null) {
resumeSessionRequest(token) showBlankLogin()
.then(function(name) { return
setPlayerName(name)
showTableListElements()
loadCurrentTable(token)
}).catch(function(error) {
showBlankLoginScreen(error.message)
})
} else {
showBlankLoginScreen("")
} }
resumeSessionRequest(token)
.then(function(name) {
setPlayerName(name)
loadCurrentTable(token)
}).catch(showLoginWithError)
} }
function loadCurrentTable(token) { function loadCurrentTable(token) {
performGetCurrentTableRequest(token) performGetCurrentTableRequest(token)
.then(function(tableId) { .then(showTableOrList)
if (tableId == "") { .catch(showLoginWithError)
didLeaveTable()
refreshTables()
return
}
console.log("Loaded table " + tableId)
showGame(tableId)
}).catch(function(error) {
showBlankLoginScreen(error.message)
})
} }
function createTable() { function createTable() {
@ -159,51 +136,38 @@ function createTable() {
return return
} }
const token = getSessionToken() const token = getSessionToken()
if (token) { if (token == null) {
const isVisible = getTableVisibility() showBlankLogin()
performCreateTableRequest(token, tableName, isVisible) return
.then(function(tableId) {
clearTableName()
showGame(tableId)
}).catch(function(error) {
showBlankLoginScreen(error.message)
})
} else {
showBlankLoginScreen("")
} }
const isVisible = getTableVisibility()
performCreateTableRequest(token, tableName, isVisible)
.then(showTableOrList)
.catch(showLoginWithError)
} }
function joinTable(tableId) { function joinTable(tableId) {
const token = getSessionToken() const token = getSessionToken()
if (token) { if (token == null) {
performJoinTableRequest(tableId, token) showBlankLogin()
.then(function() { return
showGame(tableId)
})
.catch(function(error) {
showBlankLoginScreen(error.message)
})
} else {
showBlankLoginScreen("")
} }
performJoinTableRequest(tableId, token)
.then(showTableOrList)
.catch(showLoginWithError)
} }
function leaveTable() { function leaveTable() {
const token = getSessionToken() const token = getSessionToken()
if (token) { if (token == null) {
performLeaveTableRequest(token) showBlankLogin()
.then(function() { return
showTableListElements()
closeSocketIfNeeded()
didLeaveTable()
refreshTables()
})
.catch(function(error) {
showBlankLoginScreen(error.message)
})
} else {
showBlankLoginScreen("")
} }
performLeaveTableRequest(token)
.then(function() {
showTableOrList(null)
})
.catch(showLoginWithError)
} }
function openSocket(token) { function openSocket(token) {
@ -215,9 +179,8 @@ function openSocket(token) {
}; };
socket.onmessage = function(event) { socket.onmessage = function(event) {
// TODO: Handle server data const table = convertJsonResponse(event.data)
//event.data updateTableInfo(table)
handleServerUpdates(event.data)
}; };
socket.onclose = function(event) { socket.onclose = function(event) {
@ -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() { function refreshTables() {
const token = getSessionToken() const token = getSessionToken()
if (token) { if (token == null) {
performGetPublicTablesRequest(token) showBlankLogin()
.then(function(json) { return
const html = processTableList(json)
setTableListContent(html)
}).catch(function(error) {
showBlankLoginScreen(error.message)
})
} else {
showBlankLoginScreen()
} }
performGetPublicTablesRequest(token)
.then(processTableList)
.catch(showLoginWithError)
} }
function processTableList(tables) { function processTableList(tables) {
@ -300,22 +226,20 @@ function processTableList(tables) {
if (tableInfo.players.length == 0) { if (tableInfo.players.length == 0) {
html += "<div class=\"table-subtitle\">No players</div>" html += "<div class=\"table-subtitle\">No players</div>"
} else { } else {
const names = tableInfo.players.map(function(player) { return player.name }).join(", ") const names = tableInfo.players.join(", ")
html += "<div class=\"table-subtitle\">Players: " + names + "</div>" html += "<div class=\"table-subtitle\">" + names + "</div>"
} }
html += "</div>" // table-row html += "</div>" // table-row
} }
return html setTableListContent(html)
} }
function dealCards() { function dealCards() {
const token = getSessionToken() const token = getSessionToken()
if (token) { if (token == null) {
performDealCardsRequest(token) showBlankLogin()
.catch(function(error) { return
showBlankLoginScreen(error.message)
})
} else {
showBlankLoginScreen()
} }
performDealCardsRequest(token)
.catch(showLoginWithError)
} }

View File

@ -33,28 +33,37 @@
<div id="table-list"> <div id="table-list">
</div> </div>
<div id="table-players"> <div id="table-players">
<button id="deal-button" class="standard-button" onclick="dealCards()">Deal cards</button> <div id="action-buttons">
<button id="bid-button" class="standard-button" onclick="increaseBid()">Bid</button> <button id="action-button-spacer"></button>
<button id="fold-button" class="standard-button" onclick="fold()">Fold</button> <div class="action-button-container">
<div id="table-player-card1" class="table-card"></div> <button id="action-button3" class="standard-button action-button"></button>
<div id="table-player-card2" class="table-card"></div> </div>
<div id="table-player-card3" class="table-card"></div> <div class="action-button-container">
<div id="table-player-card4" class="table-card"></div> <button id="action-button2" class="standard-button action-button"></button>
<div id="table-player-info1"> </div>
<div id="table-player-name1">Player 1</div> <div class="action-button-container">
<div id="table-player-state1" class="player-connection-state">Offline</div> <button id="action-button1" class="standard-button action-button"></button>
</div>
</div> </div>
<div id="table-player-info2"> <div id="table-player-card-top" class="table-card"></div>
<div id="table-player-name2">Player 2</div> <div id="table-player-card-right" class="table-card"></div>
<div id="table-player-state2" class="player-connection-state">Offline</div> <div id="table-player-card-bottom" class="table-card"></div>
<div id="table-player-card-left" class="table-card"></div>
<div id="table-player-info-top">
<div id="table-player-name-top">Player 1</div>
<div id="table-player-state-top" class="player-connection-state">Offline</div>
</div> </div>
<div id="table-player-info3"> <div id="table-player-info-right">
<div id="table-player-name3">Player 3</div> <div id="table-player-name-right">Player 2</div>
<div id="table-player-state3" class="player-connection-state">Offline</div> <div id="table-player-state-right" class="player-connection-state">Offline</div>
</div> </div>
<div id="table-player-info4"> <div id="table-player-info-bottom">
<div id="table-player-name4">Player 4</div> <div id="table-player-name-bottom">Player 3</div>
<div id="table-player-state4" class="player-connection-state">Offline</div> <div id="table-player-state-bottom" class="player-connection-state">Offline</div>
</div>
<div id="table-player-info-left">
<div id="table-player-name-left">Player 4</div>
<div id="table-player-state-left" class="player-connection-state">Offline</div>
</div> </div>
</div> </div>
<div id="player-cards"> <div id="player-cards">

View File

@ -275,31 +275,40 @@ body {
margin-top: 20px; margin-top: 20px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
width: 549px; width: 649px;
height: 437px; 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; grid-template-rows: 111px 76px 35px 76px 111px;
column-gap: 0px; column-gap: 0px;
row-gap: 0px; row-gap: 0px;
align-items: center; align-items: center;
} }
#deal-button { #action-buttons {
display: none;
grid-column: 7; 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; display: none;
grid-column: 1;
grid-row: 5;
} }
#bid-button { .action-button-container {
display: none; height: 40px;
grid-column: 7; width: 160px;
grid-row: 5; margin-top: 10px;
margin-left: 40px;
}
#action-button-spacer {
height: 37px;
width: 100%;
background-color: var(--standard-background);
border: none;
} }
.table-card { .table-card {
@ -308,31 +317,31 @@ body {
background-color: var(--element-background); background-color: var(--element-background);
} }
#table-player-card1 { /* bottom */ #table-player-card-bottom {
grid-column: 3 / span 3; grid-column: 3 / span 3;
grid-row: 4 / span 2; grid-row: 4 / span 2;
z-index: 2; z-index: 2;
} }
#table-player-card2 { /* left */ #table-player-card-left {
grid-column: 2 / span 2; grid-column: 2 / span 2;
grid-row: 2 / span 3; grid-row: 2 / span 3;
z-index: 1; z-index: 1;
} }
#table-player-card3 { /* top */ #table-player-card-top {
grid-column: 3 / span 3; grid-column: 3 / span 3;
grid-row: 1 / span 2; grid-row: 1 / span 2;
z-index: 4; z-index: 4;
} }
#table-player-card4 { /* right */ #table-player-card-right {
grid-column: 5 / span 2; grid-column: 5 / span 2;
grid-row: 2 / span 3; grid-row: 2 / span 3;
z-index: 3; z-index: 3;
} }
#table-player-info1 { /* bottom */ #table-player-info-bottom {
grid-column: 1 / span 2; grid-column: 1 / span 2;
grid-row: 5; grid-row: 5;
font-size: large; font-size: large;
@ -341,7 +350,7 @@ body {
margin-top: 50px; margin-top: 50px;
} }
#table-player-info2 { /* left */ #table-player-info-left {
grid-column: 1; grid-column: 1;
grid-row: 3; grid-row: 3;
font-size: large; font-size: large;
@ -349,7 +358,7 @@ body {
margin-right: 15px; margin-right: 15px;
} }
#table-player-info3 { /* top */ #table-player-info-top {
grid-column: 1 / span 2; grid-column: 1 / span 2;
grid-row: 1; grid-row: 1;
font-size: large; font-size: large;
@ -358,7 +367,7 @@ body {
margin-bottom: 50px; margin-bottom: 50px;
} }
#table-player-info4 { /* right */ #table-player-info-right {
grid-column: 7; grid-column: 7;
grid-row: 3; grid-row: 3;
font-size: large; font-size: large;
@ -370,14 +379,6 @@ body {
color: var(--alert-color); color: var(--alert-color);
} }
#table-player-state1 {
display: none;
}
#table-player-state2 {
display: none;
}
#player-cards { #player-cards {
display: none; display: none;
margin-left: auto; margin-left: auto;

View File

@ -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 # TODOs
## Make UI for table ## Make UI for table

View File

@ -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]
}

View File

@ -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 }
}
}
}

View File

@ -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
}
}

View File

@ -1,30 +1,34 @@
import Foundation import Foundation
struct TableInfo: ClientMessage { struct TableInfo: Codable {
static let type: ClientMessageType = .tableInfo
let id: String let id: String
let name: String let name: String
let players: [PlayerState] let player: PlayerInfo
let tableIsFull: Bool let playerLeft: PlayerInfo?
struct PlayerState: Codable, Equatable { let playerAcross: PlayerInfo?
let name: PlayerName let playerRight: PlayerInfo?
let connected: Bool init(_ table: Table, forPlayerAt playerIndex: Int) {
self.id = table.id
init(name: PlayerName, connected: Bool) { self.name = table.name
self.name = name self.player = table.player(at: playerIndex)!.info(masked: false)
self.connected = connected 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 { extension TableInfo: Comparable {
static func < (lhs: TableInfo, rhs: TableInfo) -> Bool { static func < (lhs: TableInfo, rhs: TableInfo) -> Bool {

View File

@ -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<T>(_ data: T) where T: ClientMessage {
let json = try! encoder.encode(data)
let string = String(data: json, encoding: .utf8)!
self.send(T.type.rawValue + string)
}
}

View File

@ -24,7 +24,7 @@ final class Database {
func deletePlayer(named name: PlayerName) { func deletePlayer(named name: PlayerName) {
_ = players.deletePlayer(named: name) _ = players.deletePlayer(named: name)
tables.remove(player: name) tables.leaveTable(player: name)
} }
func isValid(sessionToken token: SessionToken) -> Bool { func isValid(sessionToken token: SessionToken) -> Bool {
@ -63,8 +63,8 @@ final class Database {
players.registeredPlayerExists(withSessionToken: token) players.registeredPlayerExists(withSessionToken: token)
} }
func currentTableOfPlayer(named player: PlayerName) -> TableId { func currentTableOfPlayer(named player: PlayerName) -> TableInfo? {
tables.currentTableOfPlayer(named: player) ?? "" tables.tableInfo(player: player)
} }
// MARK: Tables // MARK: Tables
@ -73,20 +73,20 @@ final class Database {
Create a new table with optional players. Create a new table with optional players.
- Parameter name: The name of the table - Parameter name: The name of the table
- Parameter players: The player creating 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 - Returns: The table id
*/ */
func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId { func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId {
tables.createTable(named: name, player: player, visible: visible) tables.createTable(named: name, player: player, isPublic: isPublic)
} }
func getPublicTableInfos() -> [TableInfo] { func getPublicTableInfos() -> [PublicTableInfo] {
tables.getPublicTableInfos() tables.publicTableList
} }
func join(tableId: TableId, playerToken: SessionToken) -> JoinTableResult { func join(tableId: TableId, playerToken: SessionToken) -> Result<TableInfo,JoinTableResult> {
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
return .invalidToken return .failure(.invalidToken)
} }
return tables.join(tableId: tableId, player: player) return tables.join(tableId: tableId, player: player)
} }
@ -95,14 +95,14 @@ final class Database {
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
return false return false
} }
tables.remove(player: player) tables.leaveTable(player: player)
return true return true
} }
func dealCards(playerToken: SessionToken) -> DealCardResult { func performAction(playerToken: SessionToken, action: Player.Action) -> PlayerActionResult {
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
return .invalidToken return .invalidToken
} }
return tables.dealCards(player: player) return tables.performAction(player: player, action: action)
} }
} }

View File

@ -40,19 +40,23 @@ extension DiskWriter {
} }
} }
func readLinesFromDisk() throws -> [String] { func readDataFromDisk() throws -> Data {
if #available(macOS 10.15.4, *) { if #available(macOS 10.15.4, *) {
guard let data = try storageFile.readToEnd() else { guard let data = try storageFile.readToEnd() else {
try storageFile.seekToEnd() try storageFile.seekToEnd()
return [] return Data()
} }
return parseLines(data: data) return data
} else { } else {
let data = storageFile.readDataToEndOfFile() return storageFile.readDataToEndOfFile()
return parseLines(data: data)
} }
} }
func readLinesFromDisk() throws -> [String] {
let data = try readDataFromDisk()
return parseLines(data: data)
}
private func parseLines(data: Data) -> [String] { private func parseLines(data: Data) -> [String] {
String(data: data, encoding: .utf8)! String(data: data, encoding: .utf8)!
.components(separatedBy: "\n") .components(separatedBy: "\n")

View File

@ -9,34 +9,27 @@ typealias TableName = String
final class TableManagement: DiskWriter { final class TableManagement: DiskWriter {
/// A list of table ids for public games /// All tables indexed by their id
private var publicTables = Set<TableId>() private var tables = [TableId : Table]()
/// 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]()
/// The handle to the file where the tables are persisted
let storageFile: FileHandle let storageFile: FileHandle
/// The url to the file where the tables are persisted
let storageFileUrl: URL 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 { init(storageFolder: URL) throws {
let url = storageFolder.appendingPathComponent("tables.txt") let url = storageFolder.appendingPathComponent("tables.txt")
storageFileUrl = url storageFileUrl = url
storageFile = try Self.prepareFile(at: 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 try readLinesFromDisk().forEach { line in
// Each line has parts: ID | NAME | PLAYER, PLAYER, ... // Each line has parts: ID | NAME | PLAYER, PLAYER, ...
let parts = line.components(separatedBy: ":") let parts = line.components(separatedBy: ":")
@ -54,31 +47,38 @@ final class TableManagement: DiskWriter {
entries[id] = (name, isPublic, players) entries[id] = (name, isPublic, players)
} }
} }
entries.forEach { id, table in entries.forEach { id, tableData in
tableNames[id] = table.name let table = Table(id: id, name: tableData.name, isPublic: tableData.isPublic)
if table.public { tableData.players.forEach { _ = table.add(player: $0) }
publicTables.insert(id) tables[id] = table
}
tablePlayers[id] = table.players
tablePhase[id] = .waitingForPlayers
for player in table.players {
playerTables[player] = id
}
} }
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 @discardableResult
private func save(table tableId: TableId) -> Bool { private func writeTableToDisk(table: Table) -> Bool {
let name = tableNames[tableId]! let visible = table.isPublic ? "public" : "private"
let visible = publicTables.contains(tableId) ? "public" : "private" let players = table.playerNames.joined(separator: ",")
let players = tablePlayers[tableId]! let entry = [table.id, table.name, visible, players].joined(separator: ":")
let entry = [tableId, name, visible, players.joined(separator: ",")].joined(separator: ":")
return writeToDisk(line: entry) 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 @discardableResult
private func deleteTable(tableId: TableId) -> Bool { private func writeTableDeletionEntry(tableId: TableId) -> Bool {
let entry = [tableId, "", "", ""].joined(separator: ":") let entry = [tableId, "", "", ""].joined(separator: ":")
return writeToDisk(line: entry) return writeToDisk(line: entry)
} }
@ -87,166 +87,88 @@ final class TableManagement: DiskWriter {
Create a new table with optional players. Create a new table with optional players.
- Parameter name: The name of the table - Parameter name: The name of the table
- Parameter players: The player creating 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 - Returns: The table id
*/ */
func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId { func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId {
let tableId = TableId.newToken() let table = Table(newTable: name, isPublic: isPublic)
_ = table.add(player: name)
tableNames[tableId] = name tables[table.id] = table
tablePlayers[tableId] = [player] writeTableToDisk(table: table)
playerTables[player] = tableId return table.id
if visible {
publicTables.insert(tableId)
}
save(table: tableId)
return tableId
} }
func getPublicTableInfos() -> [TableInfo] { /// A list of all public tables
publicTables.map(tableInfo).sorted() var publicTableList: [PublicTableInfo] {
tables.values.filter { $0.isPublic }.map { $0.publicInfo }
} }
private func tableInfo(id tableId: TableId) -> TableInfo { /**
let players = tablePlayers[tableId]!.map(playerState) Get the table info for a player
return TableInfo( - Parameter player: The name of the player
id: tableId, - Returns: The table info, if the player has joined a table
name: tableNames[tableId]!, */
players: players, func tableInfo(player: PlayerName) -> TableInfo? {
tableIsFull: players.count == maximumPlayersPerTable) currentTable(for: player)?.compileInfo(for: player)
} }
private func playerState(_ player: PlayerName) -> TableInfo.PlayerState { private func currentTable(for player: PlayerName) -> Table? {
.init(name: player, connected: playerIsConnected(player)) tables.values.first(where: { $0.contains(player: player) })
}
private func playerIsConnected(_ player: PlayerName) -> Bool {
playerConnections[player] != nil
}
func currentTableOfPlayer(named player: PlayerName) -> TableId? {
playerTables[player]
} }
/** /**
Join a table. 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 - Returns: The result of the join operation
*/ */
func join(tableId: TableId, player: PlayerName) -> JoinTableResult { func join(tableId: TableId, player: PlayerName) -> Result<TableInfo, JoinTableResult> {
guard var players = tablePlayers[tableId] else { if let existing = currentTable(for: player) {
return .tableNotFound guard existing.id == tableId else {
return .failure(.alreadyJoinedOtherTable)
}
return .success(existing.compileInfo(for: player)!)
} }
guard !players.contains(player) else { guard let table = tables[tableId] else {
return .success return .failure(.tableNotFound)
} }
guard players.count < maximumPlayersPerTable else { guard table.add(player: player) else {
return .tableIsFull return .failure(.tableIsFull)
} }
players.append(player) writeTableToDisk(table: table)
if let oldTable = playerTables[tableId] { return .success(table.compileInfo(for: player)!)
remove(player: player, fromTable: oldTable)
}
tablePlayers[tableId] = players
playerTables[player] = tableId
save(table: tableId)
return .success
} }
func remove(player: PlayerName, fromTable tableId: TableId) { /**
tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player } A player leaves the table it previously joined
disconnect(player: player) - Parameter player: The name of the player
playerTables[player] = nil */
// TODO: End game if needed func leaveTable(player: PlayerName) {
// TODO: Remove table if empty guard let table = currentTable(for: player) else {
save(table: tableId)
}
func remove(player: PlayerName) {
guard let tableId = playerTables[player] else {
return return
} }
// Already saves table to disk table.remove(player: player)
remove(player: player, fromTable: tableId) writeTableToDisk(table: table)
} }
func connect(player: PlayerName, using socket: WebSocket) -> Bool { func connect(player: PlayerName, using socket: WebSocket) -> Bool {
guard let tableId = playerTables[player] else { guard let table = currentTable(for: player) else {
return false return false
} }
guard let players = tablePlayers[tableId] else { return table.connect(player: player, using: socket)
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
} }
func disconnect(player: PlayerName) { func disconnect(player: PlayerName) {
if let socket = playerConnections.removeValue(forKey: player) { guard let table = currentTable(for: player) else {
if !socket.isClosed {
_ = socket.close()
}
}
guard let tableId = playerTables[player] else {
return return
} }
sendTableInfo(toTable: tableId) table.disconnect(player: player)
// Change table phase to waiting
} }
private func sendTableInfo(toTable tableId: TableId) { func performAction(player: PlayerName, action: Player.Action) -> PlayerActionResult {
let name = tableNames[tableId]! guard let table = currentTable(for: player) else {
var players = tablePlayers[tableId]!
let isFull = players.count == maximumPlayersPerTable
for _ in players.count..<maximumPlayersPerTable {
players.append("")
}
let states = players.map(playerState)
players.enumerated().forEach { index, player in
guard let socket = playerConnections[player] else {
return
}
let info = TableInfo(
id: tableId,
name: name,
players: states.rotated(toStartAt: index),
tableIsFull: isFull)
socket.send(info)
}
}
func dealCards(player: PlayerName) -> DealCardResult {
guard let tableId = playerTables[player] else {
return .noTableJoined return .noTableJoined
} }
guard let players = tablePlayers[tableId] else { return table.perform(action: action, forPlayer: player)
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
} }
} }

View File

@ -73,6 +73,15 @@ struct Card: Codable {
var points: Int { var points: Int {
symbol.points symbol.points
} }
static let allCards: Set<Card> = {
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 { 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
}

View File

@ -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]! }
}
}

View File

@ -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<Card> = Set(trumpOrder)
static func trumpCount(_ cards: [Card]) -> Int {
cards.filter { trumps.contains(card) }.count
}
}

View File

@ -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<Card>.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..<i+4])
}
}
var canOfferWedding: Bool {
NormalCardOrder.trumpCount(self) == 1
}
}
struct Dealer {
/**
Creates a random assignment of 4 cards per 4 players for the initial round of doubling.
*/
static func dealFirstCards() -> [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)
}
}

View File

@ -32,9 +32,8 @@ struct Game: Codable {
self.numberOfDoubles = doubles self.numberOfDoubles = doubles
self.cards = cards self.cards = cards
self.leaders = leaders self.leaders = leaders
self.consecutiveTrumps = Dealer.consecutiveTrumps( self.consecutiveTrumps = Array(leaders.map { cards[$0] }.joined())
in: leaders.map { cards[$0] }.joined(), .consecutiveTrumps(for: type)
for: type)
self.currentActor = starter self.currentActor = starter
self.lastTrickWinner = starter self.lastTrickWinner = starter
self.completedTricks = [] self.completedTricks = []

View File

@ -2,6 +2,23 @@ import Foundation
enum GameType: Codable { 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 rufEichel
case rufBlatt case rufBlatt
case rufSchelln case rufSchelln
@ -14,18 +31,18 @@ enum GameType: Codable {
case soloHerz case soloHerz
case soloSchelln case soloSchelln
var gameClass: Int { var gameClass: GameClass {
switch self { switch self {
case .rufEichel, .rufBlatt, .rufSchelln: case .rufEichel, .rufBlatt, .rufSchelln:
return 1 return .ruf
case .hochzeit: case .hochzeit:
return 2 return .hochzeit
case .bettel: case .bettel:
return 3 return .bettel
case .wenz, .geier: case .wenz, .geier:
return 4 return .wenzGeier
case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln: case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln:
return 5 return .solo
} }
} }
@ -39,21 +56,10 @@ enum GameType: Codable {
} }
var basicCost: Int { var basicCost: Int {
switch self { gameClass.cost
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
}
} }
var sortingType: CardSortingStrategy { var sortingType: CardOrderType {
switch self { switch self {
case .wenz: case .wenz:
return .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
}

View File

@ -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
}
}
}

View File

@ -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..<maximumPlayersPerTable {
players[i].prepareForFirstGame(isFirstPlayer: i == index)
}
}
private func sendUpdateToAllPlayers() {
players.enumerated().forEach { playerIndex, player in
guard player.isConnected else {
return
}
let info = TableInfo(self, forPlayerAt: playerIndex)
player.send(info)
}
}
// MARK: Player actions
func perform(action: Player.Action, forPlayer player: PlayerName) -> 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)
}
}

View File

@ -5,7 +5,7 @@ typealias Trick = [Card]
extension Trick { extension Trick {
func winnerIndex(forGameType type: GameType) -> Int { 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)! return firstIndex(of: highCard)!
} }

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
enum DealCardResult { enum PlayerActionResult {
case success case success

View File

@ -1,9 +1,10 @@
import Foundation import Foundation
enum JoinTableResult { enum JoinTableResult: Error {
case invalidToken case invalidToken
case alreadyJoinedOtherTable
case tableNotFound case tableNotFound
case tableIsFull case tableIsFull
case success
} }

View File

@ -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<T>(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<Card>.SubSequence in
let start = part * 8
let end = start + 8
return random[start..<end]
}.map { sort(cards: $0) }
}
static func consecutiveTrumps<T>(in cards: T, for game: GameType) -> Int where T: Sequence, T.Element == Card {
var count = 0
let trumpsInOrder: Array<Card>.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
}
}

View File

@ -9,6 +9,11 @@ private let maximumPlayerNameLength = 40
/// The maximum length of a valid password /// The maximum length of a valid password
private let maximumPasswordLength = 40 private let maximumPasswordLength = 40
func encodeJSON<T>(_ response: T) throws -> String where T: Encodable {
let data = try encoder.encode(response)
return String(data: data, encoding: .utf8)!
}
func routes(_ app: Application) throws { func routes(_ app: Application) throws {
// MARK: Players & Sessions // MARK: Players & Sessions
@ -140,7 +145,7 @@ func routes(_ app: Application) throws {
- Throws: - Throws:
- 400: Missing token - 400: Missing token
- 401: Invalid 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 app.post("player", "table") { req -> String in
guard let token = req.body.string else { guard let token = req.body.string else {
@ -149,7 +154,10 @@ func routes(_ app: Application) throws {
guard let player = database.registeredPlayerExists(withSessionToken: token) else { guard let player = database.registeredPlayerExists(withSessionToken: token) else {
throw Abort(.unauthorized) // 401 throw Abort(.unauthorized) // 401
} }
return database.currentTableOfPlayer(named: player) guard let info = database.currentTableOfPlayer(named: player) else {
return ""
}
return try encodeJSON(info)
} }
/** /**
@ -183,11 +191,11 @@ func routes(_ app: Application) throws {
let token = req.body.string else { let token = req.body.string else {
throw Abort(.badRequest) // 400 throw Abort(.badRequest) // 400
} }
let isVisible: Bool let isPublic: Bool
if visibility == "private" { if visibility == "private" {
isVisible = false isPublic = false
} else if visibility == "public" { } else if visibility == "public" {
isVisible = true isPublic = true
} else { } else {
throw Abort(.badRequest) // 400 throw Abort(.badRequest) // 400
} }
@ -195,7 +203,7 @@ func routes(_ app: Application) throws {
guard let player = database.registeredPlayerExists(withSessionToken: token) else { guard let player = database.registeredPlayerExists(withSessionToken: token) else {
throw Abort(.unauthorized) // 401 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 return tableId
} }
@ -215,8 +223,7 @@ func routes(_ app: Application) throws {
throw Abort(.forbidden) // 403 throw Abort(.forbidden) // 403
} }
let list = database.getPublicTableInfos() let list = database.getPublicTableInfos()
let data = try encoder.encode(list) return try encodeJSON(list)
return String(data: data, encoding: .utf8)!
} }
/** /**
@ -226,6 +233,7 @@ func routes(_ app: Application) throws {
- Throws: - Throws:
- 400: Missing token - 400: Missing token
- 401: The session token is invalid - 401: The session token is invalid
- 403: The player already sits at another table
- 410: The table id doesn't exist - 410: The table id doesn't exist
- 417: The table is already full and can't be joined - 417: The table is already full and can't be joined
- Returns: Nothing - Returns: Nothing
@ -236,14 +244,19 @@ func routes(_ app: Application) throws {
throw Abort(.badRequest) throw Abort(.badRequest)
} }
switch database.join(tableId: table, playerToken: token) { switch database.join(tableId: table, playerToken: token) {
case .invalidToken: case .success(let table):
throw Abort(.unauthorized) // 401 return try encodeJSON(table)
case .tableNotFound: case .failure(let result):
throw Abort(.gone) // 410 switch result {
case .tableIsFull: case .invalidToken:
throw Abort(.expectationFailed) // 417 throw Abort(.unauthorized) // 401
case .success: case .alreadyJoinedOtherTable:
return "" 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 "" return ""
} }
app.post("deal") { req -> String in app.post("player", "action", ":action") { req -> String in
guard let token = req.body.string else { guard let token = req.body.string,
throw Abort(.badRequest) let actionString = req.parameters.get("action"),
} let action = Player.Action(rawValue: actionString) else {
switch database.dealCards(playerToken: token) { throw Abort(.badRequest)
}
switch database.performAction(playerToken: token, action: action) {
case .success: case .success:
return "" return ""
case .invalidToken: case .invalidToken: