Sync push
This commit is contained in:
parent
4fe71136a2
commit
3db9652cad
@ -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);
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
286
Public/game.js
286
Public/game.js
@ -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)
|
||||||
}
|
}
|
@ -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">
|
||||||
|
@ -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;
|
||||||
|
50
Readme.md
50
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
|
# TODOs
|
||||||
|
|
||||||
## Make UI for table
|
## Make UI for table
|
||||||
|
@ -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]
|
|
||||||
}
|
|
30
Sources/App/Infos/PlayerInfo.swift
Normal file
30
Sources/App/Infos/PlayerInfo.swift
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
Sources/App/Infos/PublicTableInfo.swift
Normal file
27
Sources/App/Infos/PublicTableInfo.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
49
Sources/App/Model/CardOrders/CardOrder.swift
Normal file
49
Sources/App/Model/CardOrders/CardOrder.swift
Normal 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]! }
|
||||||
|
}
|
||||||
|
}
|
58
Sources/App/Model/CardOrders/NormalCardOrder.swift
Normal file
58
Sources/App/Model/CardOrders/NormalCardOrder.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
312
Sources/App/Model/Dealer.swift
Normal file
312
Sources/App/Model/Dealer.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 = []
|
||||||
|
@ -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
|
|
||||||
}
|
|
195
Sources/App/Model/Player.swift
Normal file
195
Sources/App/Model/Player.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
233
Sources/App/Model/Table.swift
Normal file
233
Sources/App/Model/Table.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
enum DealCardResult {
|
enum PlayerActionResult {
|
||||||
|
|
||||||
case success
|
case success
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user