added cmd playing mode

This commit is contained in:
2024-07-22 00:48:10 +02:00
parent 4a167bf3b4
commit 05300c1153
15 changed files with 434 additions and 137 deletions

View File

@@ -0,0 +1,122 @@
import de.heiserer.CmdPlayer
import de.heiserer.SchafkopfMessager
import de.heiserer.cards.Card
import de.heiserer.cards.CardColor
import de.heiserer.cards.CardSymbol
import de.heiserer.player.Player
class CmdSchafkopfMessager: SchafkopfMessager {
override fun sendPlayerTurn(player: Player) {
if(player !is CmdPlayer){
clearConsole()
}
println("${player.getName()} ist am Zug")
Thread.sleep(1000)
}
override fun sendPlayerWonTrick(player: Player) {
println("${player.getName()} hat den Stich gewonnen.")
println()
}
override fun sendPlayerWonGame(player: Player) {
TODO("Not yet implemented")
}
override fun sendCardPlayed(player: Player, card: Card) {
println("${player.getName()} hat ${card.displayName} gespielt.")
}
override fun sendTableCards(cards: List<Card>) {
println("Tischkarten:")
CmdCard.printCards(cards)
Thread.sleep(3000)
}
class CmdCard(card: Card){
private val symbolMap = mapOf(
CardSymbol.SIEBEN to "7",
CardSymbol.ACHT to "8",
CardSymbol.NEUN to "9",
CardSymbol.ZEHN to "10",
CardSymbol.OBER to "O",
CardSymbol.UNTER to "U",
CardSymbol.KOENIG to "K",
CardSymbol.ASS to "A"
)
private val colorMap = mapOf(
CardColor.HERZ to "",
CardColor.SCHELL to "",
CardColor.EICHEL to "",
CardColor.BLATT to ""
)
private val symbol = symbolMap[card.symbol]?: "?"
private val color = colorMap[card.color]?: "?"
private val colorName = card.color.displayName
private val symbolName = card.symbol.displayName
private val cardWidth = 13
private val symbolLine = "${symbol.padEnd(cardWidth-5)}$symbol"
private val colorLine = "${color.padEnd(cardWidth-5)}$color"
private val cardLines = listOf(
"┌───────────┐",
symbolLine,
colorLine,
"${colorName.padCenter(cardWidth-2)}",
"${symbolName.padCenter(cardWidth-2)}",
colorLine,
symbolLine,
"└───────────┘"
)
fun print(){
cardLines.forEach {
println(it)
}
}
fun getCardLines(): List<String> {
return cardLines
}
private fun String.padCenter(totalWidth: Int, padChar: Char = ' '): String {
if (this.length >= totalWidth) return this
val padding = totalWidth - this.length
val padStart = this.length + padding / 2
return this.padStart(padStart, padChar).padEnd(totalWidth, padChar)
}
companion object {
fun printCards(cards: List<Card>) {
val lastCardLines = CmdCard(cards.last()).getCardLines()
val cardLines = cards.map { CmdCard(it).getCardLines() }
// Loop through the indices of the lines that are present in all cards
for (i in lastCardLines.indices) {
// For other lines, print up to the first 5 characters
var line = cardLines.joinToString(" ") { it[i].take(5) }
line += lastCardLines[i].drop(5)
println(line)
}
}
}
}
fun clearConsole() {
for (i in 1..100) {
println()
}
}
}

View File

@@ -0,0 +1,60 @@
package de.heiserer
import de.heiserer.cards.*
import de.heiserer.player.Player
import java.util.*
class CmdPlayer(name: String) : Player(name) {
private val scanner = Scanner(System.`in`)
override fun playCard(tableCards: UnsortedCardList, gameType: GameType): Card {
val cardsCopy = cards.getCopyOfCards()
println("Available cards:")
CmdSchafkopfMessager.CmdCard.printCards(cardsCopy)
// Print cards with their respective index
cardsCopy.forEachIndexed { index, card ->
println("(${index + 1}) ${card.name}") // Prints card with index starting from 1
}
// Prompt the user to enter a card number
println("Please enter the number of the card you want to play:")
val userInput = scanner.nextLine()
val cardToPlay = getCardByUserInput(userInput, tableCards, gameType)
return cards.remove(cardToPlay)
}
private fun getCardByUserInput(
userInput: String,
tableCards: UnsortedCardList,
gameType: GameType
): Card {
val cardsCopy = cards.getCopyOfCards()
return try {
// Convert user input to an integer and adjust for zero-based index
val cardIndex = userInput.toInt() - 1
// Ensure the index is within bounds
if (cardIndex in cardsCopy.indices) {
cardsCopy[cardIndex] // Return the selected card
if(validateCard(cardsCopy[cardIndex], tableCards, gameType)){
cardsCopy[cardIndex]
} else {
println("Invalid card. Please try again.")
playCard(tableCards, gameType) // Retry if the card is invalid
}
} else {
println("Invalid card number. Please try again.")
playCard(tableCards, gameType) // Retry if the input was invalid
}
} catch (e: NumberFormatException) {
println("Invalid input. Please enter a number.")
playCard(tableCards, gameType) // Retry on invalid input
}
}
}

View File

@@ -1,12 +1,14 @@
package de.heiserer
import CmdSchafkopfMessager
import de.heiserer.player.NPCPlayer
import de.heiserer.plugins.*
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
val test = SchafkopfGameController()
val test = SchafkopfGameController(listOf(NPCPlayer("NPC 1"), NPCPlayer("NPC 2"), CmdPlayer("Dev"), NPCPlayer("NPC 4")), CmdSchafkopfMessager())
test.playRound()

View File

@@ -1,7 +1,10 @@
package de.heiserer
class SchafkopfGameController {
private val players = listOf(NPCPlayer("NPC 1"), NPCPlayer("NPC 2"), NPCPlayer("NPC 3"), NPCPlayer("NPC 4"))
import de.heiserer.cards.*
import de.heiserer.player.Player
class SchafkopfGameController(palyers: List<Player>, private val messager: SchafkopfMessager) {
private val players = palyers
private lateinit var gameType: GameType
private val playedCards: CardList = UnsortedCardList()
@@ -16,6 +19,7 @@ class SchafkopfGameController {
for(i in 0 until 8){
startingPlayer = playTrick(startingPlayer)
messager.sendPlayerWonTrick(startingPlayer)
}
}
@@ -24,15 +28,17 @@ class SchafkopfGameController {
for(i in 0 until 4){
val currentPlayer = calculatePlayerOffset(startingPlayer, i)
currentPlayer.printName()
messager.sendPlayerTurn(currentPlayer)
val card = currentPlayer.playCard(tableCards, gameType)
messager.sendCardPlayed(currentPlayer, card)
tableCards.add(card)
messager.sendTableCards(tableCards.getCopyOfCards())
}
tableCards.print()
playedCards.add(tableCards)
return startingPlayer
val trickOffset = CardToolkit.whoTricks(gameType, tableCards)
return calculatePlayerOffset(startingPlayer, trickOffset)
}
private fun calculatePlayerOffset(startingPlayer: Player, i: Int): Player = players[(players.indexOf(startingPlayer) + i) % 4]
@@ -52,62 +58,3 @@ class SchafkopfGameController {
}
}
}
abstract class Player(private var name: String){
protected var cards: SortedCardList = SortedCardList(GameType.SAU_SPIEL)
fun serveCards(cards: UnsortedCardList){
this.cards = cards.asSortedCardList(GameType.SAU_SPIEL)
}
fun sortCards(gameType: GameType){
cards = cards.asSortedCardList(gameType)
println("Spieler $name hat folgende Karten sortiert:")
cards.print()
println()
println("Trumpf:")
cards.getTrumpf().print()
println()
println("Farbe:")
cards.getCardsWithoutTrumpf().print()
println()
}
abstract fun playCard(tableCards: UnsortedCardList, gameType: GameType): Card
fun printName(){
println(name)
}
}
class NPCPlayer(name: String) : Player(name){
override fun playCard(tableCards: UnsortedCardList, gameType: GameType): Card {
if(tableCards.size() == 0){
println("Erster Spieler")
val card = cards.removeLast()
println("Spielt ${card.displayName}")
println()
return card
}
val firstCard = tableCards.get(0)
if(cards.farbFreiOrTrumpf(firstCard, gameType)){
println("farbFrei Or Trumpf")
return playTrumpf()
}
}
private fun playTrumpf(): Card {
val trumpf = cards.getTrumpf()
if (trumpf.size() > 0) {
println("play Trumpf")
val card = trumpf.removeLast()
println("Spielt ${card.displayName}")
println()
return cards.remove(card)
} else {
println("abspatzen")
return cards.removeFirst()
}
}
}

View File

@@ -0,0 +1,12 @@
package de.heiserer
import de.heiserer.cards.Card
import de.heiserer.player.Player
interface SchafkopfMessager {
fun sendPlayerTurn(player: Player)
fun sendPlayerWonTrick(player: Player)
fun sendPlayerWonGame(player: Player)
fun sendCardPlayed(player: Player, card: Card)
fun sendTableCards(cards: List<Card>)
}

View File

@@ -0,0 +1,6 @@
package de.heiserer.cards
class CardAlreadyAddedException(message: String, val card: Card) : RuntimeException(message) {
// You can add additional constructors or methods if needed, but for now, this is sufficient to handle the scenario of adding a card that's already in the list.
}

View File

@@ -0,0 +1,21 @@
package de.heiserer.cards
interface CardList {
fun add(card: Card)
fun add(cards: CardList)
fun remove(card: Card): Card
fun remove(cards: CardList)
fun removeLast(): Card
fun removeFirst(): Card
fun get(index: Int): Card
fun getLast(): Card
fun getCopyOfCards(): List<Card>
fun indexOf(card: Card): Int
operator fun contains(card: Card): Boolean
fun size(): Int
fun print()
fun asSortedCardList(type: GameType): SortedCardList
}

View File

@@ -0,0 +1,36 @@
package de.heiserer.cards
class CardToolkit private constructor(private val gameType: GameType) {
private val sortedCardList = UnsortedCardList(true).asSortedCardList(gameType)
fun isTrumpf(card: Card): Boolean = card in sortedCardList.getTrumpf().getCopyOfCards()
fun whoTricks(cards: CardList): Int {
if(cards.size() != 4){
throw IllegalArgumentException("Es müssen 4 Karten auf dem Tisch liegen.")
}
val sortedCards = cards.asSortedCardList(gameType)
if(sortedCards.getTrumpf().size() > 0){
return cards.indexOf(sortedCards.getTrumpf().getLast())
} else {
val firstColor = cards.get(0).color
val colorCards = sortedCards.getCardsWithoutTrumpf(firstColor)
return cards.indexOf(colorCards.getLast())
}
}
companion object {
fun isTrumpf(gameType: GameType, card: Card): Boolean {
val toolkit = CardToolkit(gameType)
return toolkit.isTrumpf(card)
}
fun whoTricks(gameType: GameType, cards: CardList): Int {
val toolkit = CardToolkit(gameType)
return toolkit.whoTricks(cards)
}
}
}

View File

@@ -1,9 +1,9 @@
package de.heiserer
package de.heiserer.cards
enum class Card(val color: CardColor, val symbol: CardSymbol) {
SCHELL_7(CardColor.SCHELL, CardSymbol.SIEBEN),
SCHELL_8(CardColor.SCHELL, CardSymbol.ACHT),
SCHELL_9(CardColor.SCHELL, CardSymbol.NINE),
SCHELL_9(CardColor.SCHELL, CardSymbol.NEUN),
SCHELL_U(CardColor.SCHELL, CardSymbol.UNTER),
SCHELL_O(CardColor.SCHELL, CardSymbol.OBER),
SCHELL_K(CardColor.SCHELL, CardSymbol.KOENIG),
@@ -11,7 +11,7 @@ enum class Card(val color: CardColor, val symbol: CardSymbol) {
SCHELL_A(CardColor.SCHELL, CardSymbol.ASS),
HERZ_7(CardColor.HERZ, CardSymbol.SIEBEN),
HERZ_8(CardColor.HERZ, CardSymbol.ACHT),
HERZ_9(CardColor.HERZ, CardSymbol.NINE),
HERZ_9(CardColor.HERZ, CardSymbol.NEUN),
HERZ_U(CardColor.HERZ, CardSymbol.UNTER),
HERZ_O(CardColor.HERZ, CardSymbol.OBER),
HERZ_K(CardColor.HERZ, CardSymbol.KOENIG),
@@ -19,7 +19,7 @@ enum class Card(val color: CardColor, val symbol: CardSymbol) {
HERZ_A(CardColor.HERZ, CardSymbol.ASS),
BLATT_7(CardColor.BLATT, CardSymbol.SIEBEN),
BLATT_8(CardColor.BLATT, CardSymbol.ACHT),
BLATT_9(CardColor.BLATT, CardSymbol.NINE),
BLATT_9(CardColor.BLATT, CardSymbol.NEUN),
BLATT_U(CardColor.BLATT, CardSymbol.UNTER),
BLATT_O(CardColor.BLATT, CardSymbol.OBER),
BLATT_K(CardColor.BLATT, CardSymbol.KOENIG),
@@ -27,7 +27,7 @@ enum class Card(val color: CardColor, val symbol: CardSymbol) {
BLATT_A(CardColor.BLATT, CardSymbol.ASS),
EICHEL_7(CardColor.EICHEL, CardSymbol.SIEBEN),
EICHEL_8(CardColor.EICHEL, CardSymbol.ACHT),
EICHEL_9(CardColor.EICHEL, CardSymbol.NINE),
EICHEL_9(CardColor.EICHEL, CardSymbol.NEUN),
EICHEL_U(CardColor.EICHEL, CardSymbol.UNTER),
EICHEL_O(CardColor.EICHEL, CardSymbol.OBER),
EICHEL_K(CardColor.EICHEL, CardSymbol.KOENIG),
@@ -49,7 +49,7 @@ enum class CardColor(val order: Int, val displayName: String) {
enum class CardSymbol(val order: Int, val displayName: String, val value: Int) {
SIEBEN(0,"7", 0),
ACHT(1,"8", 0),
NINE(2,"9", 0),
NEUN(2,"9", 0),
UNTER(3,"Unter", 2),
OBER(4,"Ober", 3),
KOENIG(5,"König", 4),

View File

@@ -0,0 +1,41 @@
package de.heiserer.cards
class SortedCardList(private val gameType: GameType, withAllCards: Boolean = false) : UnsortedCardList(withAllCards) {
override fun add(card: Card) {
super.add(card)
sort()
}
fun getCardsWithoutTrumpf(color: CardColor? = null): SortedCardList {
val cardsWithoutTrumpf = SortedCardList(gameType)
color?.let { cardsWithoutTrumpf.add(get(it)) }?: cardsWithoutTrumpf.add(this)
try {
cardsWithoutTrumpf.remove(getTrumpf())
} catch (_: IllegalArgumentException) {
}
return cardsWithoutTrumpf
}
fun getTrumpf(): SortedCardList {
val trumpf = SortedCardList(gameType)
gameType.symbol?.let {
trumpf.add(get(it))
} ?: run {
trumpf.add(get(CardSymbol.OBER))
trumpf.add(get(CardSymbol.UNTER))
}
gameType.color?.let {
try{ trumpf.add(get(it)) } catch (_: CardAlreadyAddedException) {}
}
return trumpf
}
private fun sort() {
super.sortInternal(gameType.symbol, gameType.color)
}
}

View File

@@ -1,18 +1,4 @@
package de.heiserer
interface CardList {
fun add(card: Card)
fun add(cards: CardList)
fun remove(card: Card): Card
fun remove(cards: CardList)
fun removeLast(): Card
fun get(index: Int): Card
fun getCopyOfCards(): List<Card>
fun size(): Int
fun print()
fun asSortedCardList(type: GameType): SortedCardList
fun removeFirst(): Card
}
package de.heiserer.cards
open class UnsortedCardList(withAllCards: Boolean = false): CardList {
private val cards: MutableList<Card> = if(withAllCards){
@@ -21,6 +7,10 @@ open class UnsortedCardList(withAllCards: Boolean = false): CardList {
mutableListOf()
}
override operator fun contains(card: Card): Boolean {
return cards.contains(card)
}
override fun add(card: Card) {
if (card !in cards) {
cards.add(card)
@@ -60,6 +50,10 @@ open class UnsortedCardList(withAllCards: Boolean = false): CardList {
return cards.removeLast()
}
override fun indexOf(card: Card): Int {
return cards.indexOf(card)
}
override fun get(index: Int): Card {
if(index < 0 || index >= cards.size){
throw IllegalArgumentException("Index $index is out of bounds.")
@@ -67,6 +61,10 @@ open class UnsortedCardList(withAllCards: Boolean = false): CardList {
return cards[index]
}
override fun getLast(): Card {
return cards[cards.size - 1]
}
protected fun get(color: CardColor): CardList {
val list = UnsortedCardList()
cards.forEach { card ->
@@ -129,52 +127,3 @@ open class UnsortedCardList(withAllCards: Boolean = false): CardList {
}.thenComparing(compareBy({ it.color.order }, { it.symbol.order })))
}
}
class SortedCardList(private val gameType: GameType, withAllCards: Boolean = false) : UnsortedCardList(withAllCards) {
private fun sort() {
super.sortInternal(gameType.symbol, gameType.color)
}
fun getCardsWithoutTrumpf(color: CardColor? = null): SortedCardList {
val cardsWithoutTrumpf = SortedCardList(gameType)
color?.let { cardsWithoutTrumpf.add(get(it)) }?: cardsWithoutTrumpf.add(this)
try {
cardsWithoutTrumpf.remove(getTrumpf())
} catch (_: IllegalArgumentException) {
}
return cardsWithoutTrumpf
}
fun getTrumpf():SortedCardList {
val trumpf = SortedCardList(gameType)
gameType.symbol?.let {
trumpf.add(get(it))
} ?: run {
trumpf.add(get(CardSymbol.OBER))
trumpf.add(get(CardSymbol.UNTER))
}
gameType.color?.let { try{
trumpf.add(get(it))
} catch (_: CardAlreadyAddedException) {}
}
return trumpf
}
fun farbFreiOrTrumpf(firstCard: Card, gameType: GameType) =
firstCard.color == gameType.color || getCardsWithoutTrumpf(gameType.color).size() == 0
override fun add(card: Card) {
super.add(card)
sort()
}
}
class CardAlreadyAddedException(message: String, val card: Card) : RuntimeException(message) {
// You can add additional constructors or methods if needed, but for now, this is sufficient to handle the scenario of adding a card that's already in the list.
}

View File

@@ -0,0 +1,38 @@
package de.heiserer.player
import de.heiserer.cards.*
class NPCPlayer(name: String) : Player(name){
override fun playCard(tableCards: UnsortedCardList, gameType: GameType): Card {
if(tableCards.size() == 0){
return playTrumpf()
}
val firstCard = tableCards.get(0)
return if(CardToolkit.isTrumpf(gameType, firstCard)){
playTrumpf()
} else {
playColor(firstCard.color)
}
}
private fun playTrumpf(): Card {
val trumpfCards = cards.getTrumpf()
if (trumpfCards.size() > 0) {
val card = trumpfCards.removeLast()
return cards.remove(card)
} else {
return cards.removeFirst()
}
}
private fun playColor(color: CardColor): Card {
val colorCards = cards.getCardsWithoutTrumpf(color)
if (colorCards.size() > 0) {
val card = colorCards.removeLast()
return cards.remove(card)
} else {
return cards.removeFirst()
}
}
}

View File

@@ -0,0 +1,35 @@
package de.heiserer.player
import de.heiserer.cards.*
abstract class Player(private var name: String){
protected var cards: SortedCardList = SortedCardList(GameType.SAU_SPIEL)
fun serveCards(cards: UnsortedCardList){
this.cards = cards.asSortedCardList(GameType.SAU_SPIEL)
}
fun sortCards(gameType: GameType){
cards = cards.asSortedCardList(gameType)
}
abstract fun playCard(tableCards: UnsortedCardList, gameType: GameType): Card
fun getName(): String{
return name
}
protected fun validateCard(card: Card, tableCards: UnsortedCardList, gameType: GameType): Boolean{
if(tableCards.size() == 0){
return true
}
val firstCard = tableCards.get(0)
return if(CardToolkit.isTrumpf(gameType, firstCard)){
CardToolkit.isTrumpf(gameType, card) || cards.getTrumpf().size() == 0
} else {
card.color == firstCard.color || cards.getCardsWithoutTrumpf(firstCard.color).size() == 0
}
}
}

View File

@@ -1,4 +1,7 @@
import de.heiserer.*
import de.heiserer.cards.Card
import de.heiserer.cards.CardColor
import de.heiserer.cards.GameType
import de.heiserer.cards.UnsortedCardList
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest

View File

@@ -0,0 +1,25 @@
import de.heiserer.cards.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class CardToolkitTest {
@Test
fun `test isTrumpf`() {
// ASSERT AND ACT
assertEquals(true, CardToolkit.isTrumpf(GameType.SAU_SPIEL, Card.HERZ_O))
assertEquals(false, CardToolkit.isTrumpf(GameType.SAU_SPIEL, Card.SCHELL_7))
}
@Test
fun whoTricks() {
// ARRANGE
val cards = UnsortedCardList()
cards.add(Card.SCHELL_7)
cards.add(Card.SCHELL_8)
cards.add(Card.SCHELL_K)
cards.add(Card.SCHELL_O)
// ASSERT AND ACT
assertEquals(3, CardToolkit.whoTricks(GameType.SAU_SPIEL, cards))
}
}