Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Part 2 of German Cards Game 'Schafkopf'

5.00/5 (1 vote)
21 Feb 2024CPOL7 min read 2.6K   25  
Part 2 of my article about 'Schafkopf' introduces Automated Bidding
In Southern Germany, 'Schafkopf' is a well-known cards game. Part 1 of my article shows how to get a computer playing 'Schafkopf'. Part 2 introduces Automated Bidding.

Download Schafkopf_Cs.zip (C# version)

Introduction

This article and the demo are about adding Automated Bidding to my Schafkopf_Cs C# project.

Background

In Southern Germany, 'Schafkopf' is a well-known cards game. There are many CodeProject articles about other card games, but nothing with 'Schafkopf'. So I started to create this article series.

Using the Code

Here is a Quick Overview

MainWindow Concept and Code

When you start the program, the main window shows a complete deck of cards as a card fan and renders it in a circular panel (based on the above mentioned CodeProject article, Power of Templates in Control Development Within Windows Presentation Foundation including this credit: "I took this panel from the color swatch sample that ships with Microsoft Expression Blend“).

On top of the window, there are some menu items – click on 1. New Game please, then you should see something like that:

Image 1

MainWindow has the following controls:

  • A Grid with
    • a StackPanel called MyPanel
    • a WrapPanel (on top) with the menu items
    • HistoryTextBox for Trick History
    • Panels for the cards within the StackPanel called MyPanel
    • Panel0 with ListBox0 on top for player0 = North
    • Panel2 with ListBox2 on bottom for player2 = South
    • Panel1 with ListBox1 on right side for player1 = East
    • Panel3 with ListBox3 on left side for player3 = West
    • Panel4 with ListBox4 is used as TrickHistory (for already played cards)
    • DockPanel CenterPanel

Part 2 Automated Bidding

In the older versions of this project, the human player had to look over all 4 players hand cards and decide which one should take the role of the declarer and about the game mode.

Well, this was not perfect and now the computer does this work for us.

In the bidding or auction round, each player has to decide if a bid makes sense dependent on the cards they have on hand.

GameMode.Solo has the highest level, followed by GameMode.Wenz.

We count how many trumps and how many aces a player has on hand.

For a Solo or a Call Ace game, we also reduce the given points (as described above) when the trumps are mostly of small value (small number of Unders and Overs cards).

And we add points if the number of Unders and Overs cards is high.

For a Call Ace game, we also have to make sure that the declarer does not have the Call Ace in his own cards!

For a Wenz, we add points for a duo of Ace and Ten of the same suit.

Regarding the source code, there were only few changes needed within the existing cards framework.

So most of the new code is in the new class which we show below.

Because we have four players who all do the same steps, it makes sense to use arrays for variables.

And for the results, we use the existing UI.

Declarer and game mode are shown in the existing combo boxes and the HistoryTextBox is used for bidding details.

C#
using System;
using System.Windows;
using Schafkopf_Cs.Extensions;
using System.Diagnostics;


namespace Schafkopf_Cs.Models.Bidding
{
    public class Auction
    {
        private int _PlayerID; // player
        private MainWindow _Wnd;
        private int bidValue = 0;
        GameMode[] bidGameMode = new GameMode[4];
        int[] iContractSuitIndex = new int[4];
        Color[] Trump_Color = new Color[4];
        Color[] CallAce_Color = new Color[4];
        int[] iRatedBid = new int[4];

        public Auction(int iPlayerID, MainWindow Wnd)
        {
            _Wnd= Wnd;
            _PlayerID = iPlayerID;
        }

        # region Bid

        /// <summary>
        /// Prepare bidding.
        /// </summary>
        public void StartBid()
        {
            HandCards[] _Hand = new[] { _Wnd.Hand0, _Wnd.Hand1, _Wnd.Hand2, _Wnd.Hand3 };
            int maxBid = 0;

            _Wnd.HistoryTextBox.AppendText(Environment.NewLine + "----------------------"
                + Environment.NewLine +
                "StartBid() " + Environment.NewLine + "_PlayerID = " + _PlayerID);
            _Wnd.HistoryTextBox.ScrollToEnd();

            int[] iResult = new int[4];
            int i = 0;
            for (i = 0; i < 4; i++)
            {
                bidValue = (_PlayerID + i) % 4;
                iRatedBid[bidValue] = CalcBid(_Hand[bidValue], bidValue);

                if (iRatedBid[bidValue] > maxBid)
                {
                    _Wnd.cbxContractSuit.SelectedIndex = iContractSuitIndex[bidValue];
                    _Wnd.cbxDeclarer.SelectedIndex = bidValue;
                    maxBid += iRatedBid[bidValue];
                }

                _Wnd.HistoryTextBox.AppendText(Environment.NewLine + "-------------------" +
                    Environment.NewLine + "Player = " + bidValue.ToString() +
                    Environment.NewLine + "Trump_Color: " + 
                    Trump_Color[bidValue].ToString() +
                    Environment.NewLine + "CallAce_Color: " + 
                    CallAce_Color[bidValue].ToString() +
                    Environment.NewLine + "bidGameMode: " + 
                    bidGameMode[bidValue].ToString() +
                    Environment.NewLine + "iRatedBid: " + 
                    iRatedBid[bidValue].ToString());
                _Wnd.HistoryTextBox.ScrollToEnd();
            }
            if (maxBid == 0)
                MessageBox.Show("All players passed without bidding");
            else if (maxBid > 0) {
                MessageBox.Show("Bidding done... " + Environment.NewLine +
                Environment.NewLine + "You can accept this result or change your bidding." +
                Environment.NewLine + "Then click on '4. Ready to Play'.");
            }
        }

        /// <summary>
        /// Called to calc a bid
        /// </summary>
        /// <param name="hand"></param>
        /// <param name="player"></param>
        /// <returns>iRatedBid[player]</returns>
        private int CalcBid(HandCards hand, int player)
        {
            int bidResult;
            int bidSoloResult=0;

            int iGameModeSauspielTrumpCount= hand.GetHandTrumpCount(hand, 1);
            int iUnderCount = hand.GetHandUnderCount(hand, 1);
            int iUnderAndOverCount = hand.GetHandUnderAndOverCount(hand, 1);
            int iAcesCount = hand.GetHandAceCount(hand, 1);
            int iAcesWithTenDuoCount =0;

            bidGameMode[player] = GameMode.None;

            //https://stackoverflow.com/questions/1241165/how-do-you-initialize-an-array-in-c
            int[] iGameModeSoloTrumpCount = new int[4];

            iGameModeSauspielTrumpCount = hand.GetHandTrumpCount(hand, 1);
            iUnderCount = hand.GetHandUnderCount(hand, 1);

            bidResult = iGameModeSauspielTrumpCount;

            // check for aces
            if (iUnderAndOverCount >= 4 && bidResult == 5) { bidResult += 1; }
            if (iAcesCount > 1) {
                bidSoloResult += 1;
                bidResult += 1; }
            if (iAcesCount > 0 && hand.HasCard((CardValue)11, (CardType)1) == false)
            {
                if (bidResult == 5) bidResult += 1;
            }
            if (iUnderAndOverCount < 3) {
                bidSoloResult -= 1; }
            if (iUnderAndOverCount < 2) {
                bidResult -= 1; }
            // Solo without any 'Over' is a challenge
            if (iUnderAndOverCount == iUnderCount)
            {
                bidSoloResult -= 1;
            }

            // Check if playing cards for each suit are there
            if (AllPlayers.GetLowestCard(hand, 0, 1) is null ||
                AllPlayers.GetLowestCard(hand, 2, 1) is null ||
                AllPlayers.GetLowestCard(hand, 3, 1) is null)
                if(bidResult == 5 && iUnderAndOverCount - iUnderCount >0)
                    bidResult += 1;

            if (bidResult > 5 && iGameModeSauspielTrumpCount < 5)
                bidResult -= 1;

            // statements for GameMode.AssenSpiel
            switch (bidResult)
            {
                case >= 6:

                    Trump_Color[player] = (Color)200;
                    if (AllPlayers.GetLowestCard(hand, 0, 1) is not null && 
                        bidSoloResult < 7 &&
                        hand.HasCard((CardValue)11, (CardType)0) == false)
                    { CallAce_Color[player] = (Color)100; iContractSuitIndex[player] = 7; }

                        if (AllPlayers.GetLowestCard(hand, 2, 1) is not null && 
                        bidSoloResult < 7 &&
                        hand.HasCard((CardValue)11, (CardType)2) == false)
                    { CallAce_Color[player] = (Color)300; iContractSuitIndex[player] = 6; }

                    if (AllPlayers.GetLowestCard(hand, 3, 1) is not null && 
                        bidSoloResult < 7 &&
                        hand.HasCard((CardValue)11, (CardType)3) == false)
                    { CallAce_Color[player] = (Color)400; iContractSuitIndex[player] = 5; }

                    // prefer suit where a 'Ten' is on hand
                    Trump_Color[player] = (Color)200;
                    if (AllPlayers.GetLowestCard(hand, 0, 1) is not null &&
                        bidSoloResult < 7 && hand.HasCard((CardValue)10, (CardType)0) &&
                        hand.HasCard((CardValue)11, (CardType)0) == false)
                    { CallAce_Color[player] = (Color)100; iContractSuitIndex[player] = 7; }

                    if (AllPlayers.GetLowestCard(hand, 2, 1) is not null &&
                    bidSoloResult < 7 && hand.HasCard((CardValue)10, (CardType)2) &&
                    hand.HasCard((CardValue)11, (CardType)2) == false)
                    { CallAce_Color[player] = (Color)300; iContractSuitIndex[player] = 6; }

                    if (AllPlayers.GetLowestCard(hand, 3, 1) is not null &&
                        bidSoloResult < 7 && hand.HasCard((CardValue)10, (CardType)3) &&
                        hand.HasCard((CardValue)11, (CardType)3) == false)
                    { CallAce_Color[player] = (Color)400; iContractSuitIndex[player] = 5; }

                    if (CallAce_Color[player] > 0)
                        { 
                        bidGameMode[player] = GameMode.AssenSpiel; iRatedBid[player] = 100; 
                        }
                    break;
                default:
                    break;
            }
            // statements for GameMode.Wenz
            if (hand.HasCard((CardValue)11, (CardType)3) && 
                hand.HasCard((CardValue)10, (CardType)3) && 
                hand.HasCard((CardValue)4, (CardType)3) ||
                hand.HasCard((CardValue)11, (CardType)2) && 
                hand.HasCard((CardValue)10, (CardType)2) && 
                hand.HasCard((CardValue)4,  (CardType)2) ||
                hand.HasCard((CardValue)11, (CardType)1) && 
                hand.HasCard((CardValue)10, (CardType)1) && 
                hand.HasCard((CardValue)4, (CardType)1) ||
                hand.HasCard((CardValue)11, (CardType)0) && 
                hand.HasCard((CardValue)10, (CardType)0) && 
                hand.HasCard((CardValue)4, (CardType)0))
                iAcesWithTenDuoCount += 1;
            if (hand.HasCard((CardValue)11, (CardType)3) && 
                hand.HasCard((CardValue)10, (CardType)3) ||
                hand.HasCard((CardValue)11, (CardType)2) && 
                hand.HasCard((CardValue)10, (CardType)2) ||
                hand.HasCard((CardValue)11, (CardType)1) && 
                hand.HasCard((CardValue)10, (CardType)1) ||
                hand.HasCard((CardValue)11, (CardType)0) && 
                hand.HasCard((CardValue)10, (CardType)0))
                iAcesWithTenDuoCount += 1;

            // reduce UnderCount points if highest Wenz card is missing
            if (hand.HasCard((CardValue)2, (CardType)3) == false)
                iUnderCount -= 1;
            if (iUnderCount + iAcesCount + iAcesWithTenDuoCount >= 6)
            {
                bidGameMode[player] = GameMode.Wenz;
                //bidResult = iUnderCount + iAcesCount;
                iRatedBid[player] = 200;
                iContractSuitIndex[player] =  4;
            }
            // statements for GameMode.Solo
            for (int i = 0; i < 4; i++)
            {
                // reduce bidSoloResult points if highest Over card is missing
                if (hand.HasCard((CardValue)3, (CardType)3) == false && 
                    hand.GetHandTrumpCount(hand, i) < 7)
                    bidSoloResult -= 1;
                iGameModeSoloTrumpCount[i] = hand.GetHandTrumpCount(hand, i);
                iGameModeSoloTrumpCount[i] += bidSoloResult;
                if (iGameModeSoloTrumpCount[i] >= 7)
                {
                    Debug.Print("Auction 210 iGameModeSoloTrumpCount[i]: " + 
                        iGameModeSoloTrumpCount[i].ToString());
                    bidGameMode[player] = GameMode.Solo;
                    //bidSoloResult = iGameModeSoloTrumpCount[i];
                    Trump_Color[player] = (Color)(i * 100 + 100); // Solo
                    iContractSuitIndex[player] = i;
                    bidGameMode[player] = GameMode.Solo;
                    iRatedBid[player] = 300 + (i * 10);
                }
            }

            return iRatedBid[player];  //bidResult;
        }
               
        #endregion

    }
}

Selection of the Declarer and the Game Type

When the Automatic Bidding is done, the selection of the declarer and the game type are also human controlled (by the user).

Many games will see no bids because no player has good enough cards.

In this case, you can start a new game.

1. New Game

A click on that menu item starts the CardsDeck.Shuffle method.

After that, the shuffled cards are distributed to the four ListBoxes / CardPanels.

or change your bid (which was done by the pc in Automatic Bidding). In this case, you have to follow the steps as shown in the menu on top.

2. Select Declarer

From the combobox on the right of this label, you can select the declarer of the game (who plays a solo or calls an ace).

3. Select GameType

From the combobox on the right of this label, you can select which game type the declarer of the game wants to play – select which solo he wants to play or which ace he wants to call.

Menu item 4. Ready to Play is only active after steps 2. And 3. are completed.

After you clicked it, the Auto Play feature moves a card to the CenterPanel or - if it is the human player's turn – nothing happens until the human player clicked on one of his cards.

The label "Waiting for Card from Player:“ shows whose turn is next.

Cards Tracking and other Details

The related class(es) handle(s) some special cases like a human player would do.

One of them is called AIBase, however technically, it is not an AI.
But the results seem to be comparable to an AI which was trained or has learned to play Schafkopf.

For Cards Tracking, we are also using class TrickContent with extension Module Extensions_TrickMonitoring

What we are doing like a human player would do is for example:

  • Check if a color [suit] was already played in the current game:

Public Function IsLeadSuitPlayedTwice

  • Check if the "CallAce" was already played because we want to know if we should take a higher or lower trump:

Public Function IsGetMediumHigherTrumpOk

Public Function IsToSchmearOK

  • Public Sub SetCards in Modul Extensions_TrickMonitoring, for example, is used to get:

PlayingCard Property IsHighestPlayableTrumpCard

C#
using System.Diagnostics;
using System.Linq;
using Schafkopf_Cs.Extensions;
using Schafkopf_Cs.Models;

namespace Schafkopf_Cs.aiLogic
{
    public class AIBase
    {
        // This class handles some special cases like a human player would do

        #region Fields And Properties

        private int iRufAsOwner;
        private TrickContent tc;

        #endregion

        #region Initializations

        public AIBase(int TrumpID, MainWindow MyWnd)
        {
            tc = MyWnd.TrickState;
            iRufAsOwner = (int)MyWnd.RufAs.CardOwner;
            if (MyWnd.GameModus == GameMode.Solo)
                iRufAsOwner = -1;
            if (MyWnd.GameModus == GameMode.Wenz)
                iRufAsOwner = -1;
        }

        private void InitHand(object CardsPanel, int PlayerID, 
                              int DeclarerID, object GameStatus, 
            object sHandCards, int TrumpID, HandCards hc, MainWindow Wnd, int LeadSuitID)
        {

        }

        #endregion

        #region AI

        public PlayingCard CallAceDownBy(HandCards hand, int suit, int TrumpCardID)
        {
            // case the call ace owner has 4 cards with call ace cardType / Color

            if (TrumpCardID != 4 && suit != TrumpCardID)
            {
                if (hand.Cards.OrderBy(card => card.CardValue).Where
                   (card => (int)card.CardValue > 3)
                    .Where(card => (int)card.CardType == suit).FirstOrDefault() 
                                    is not null)

                {
                    if (hand.Cards.OrderBy(card => card.CardValue).Where
                                          (card => (int)card.CardValue > 3)
                        .Where(card => (int)card.CardType == suit).Count() > 3)

                    {
                        return hand.Cards.OrderBy(card => card.CardValue).Where
                               (card => (int)card.CardValue > 3)
                            .Where(card => (int)card.CardType == suit).FirstOrDefault();
                    }
                }
            }

            return default;
        }

        public bool WenzPlayingIsOK(MainWindow Wnd)
        {
            bool WenzPlayingIsOKRet = default;
            if (Wnd.TrickHistory.OnePrevCardContainsU() == false && 
                Wnd.TrickHistory.Cards[0].ToString().Contains("U") == false)
            {
                WenzPlayingIsOKRet = true;
            }
            else
            {
                WenzPlayingIsOKRet = false;
            }
            return WenzPlayingIsOKRet;
        }

        public bool IsLeadSuitPlayedTwice(MainWindow Wnd, int LeadSuitID)
        {
            // IsLeadSuitPlayedTwice = False
            if (LeadSuitID == 0 && Wnd.ShellsColor.IsPlayedTwice == true)
                return true;
            if (LeadSuitID == 1 && Wnd.HeartsColor.IsPlayedTwice == true)
                return true;
            if (LeadSuitID == 2 && Wnd.SpadesColor.IsPlayedTwice == true)
                return true;
            if (LeadSuitID == 3 && Wnd.AcornColor.IsPlayedTwice == true)
                return true;
            return false;
        }

        public bool IsToSchmearOK(MainWindow Wnd, HandCards hand, 
                                  int suit, int TrumpCardID)
        {
            bool IsSchmearOK;

            if (tc.CountCardsInTrick == 1 
                && Wnd.GameModus == GameMode.Solo
                && tc.CurrentTrickWinner == Wnd.sk.declarer
                && tc.GetWinnerCard.CardRatedValue < 555
                && Wnd.iTricks < 4)
            {
                IsSchmearOK = true;
            }
            else if (tc.CountCardsInTrick > 0
                && Wnd.GameModus == GameMode.AssenSpiel
                && tc.CurrentTrickWinner == Wnd.sk.declarer
                && tc.GetWinnerCard.IsHighestPlayableTrumpCard == true
                && hand.TrickIsOur)
            {
                IsSchmearOK = true;
            }
            else if (tc.CountCardsInTrick == 2
                && Wnd.GameModus == GameMode.Solo
                && (int)tc.Cards.First().CardOwner == Wnd.sk.declarer
                && hand.TrickIsOur)
            {
                IsSchmearOK = true;
            }
            else if (tc.CountCardsInTrick == 2
                && Wnd.GameModus == GameMode.Solo
                && (int)tc.Cards.First().CardOwner == Wnd.sk.declarer
                && tc.GetWinnerCard.CardRatedValue < 550) // hand.TrickIsOur)
            {
                IsSchmearOK = true;
            }
            else
            {
                IsSchmearOK = false;
            }
            return IsSchmearOK;
        }

        // NOT used in C# version! Only converted from VB
        public bool IsGetMediumHigherTrumpOk(MainWindow Wnd, TrickContent tc, 
            int PlayerID, int DeclarerID, HandCards hand)
        {
            if (Wnd.RufAs.IsAlreadyPlayed || 
                tc.GetCurrentTrickWinnerCard(Wnd) == tc.Card1 || 
                tc.GetWinnerCard.CardRatedValue < 1000 || 
                   Wnd.GameModus == GameMode.Solo || 
                Wnd.GameModus == GameMode.Wenz || 
                    (int)tc.GetWinnerCard.CardOwner == DeclarerID | 
                tc.CountCardsInTrick > 2)
            {
                if (PlayerID != DeclarerID | 
                   (int)tc.GetWinnerCard.CardOwner == DeclarerID)
                    return true;
                else if (PlayerID == DeclarerID & tc.GetWinnerCard.CardRatedValue < 1000)
                    return true;
                else if (tc.CountCardsInTrick > 2 & 
                         tc.GetCurrentTrickWinnerCard(Wnd) != tc.Card1)
                    return true;
                else if (Wnd.RufAs.IsAlreadyPlayed & hand.TrickIsOur == false ||
                    Wnd.GameModus == GameMode.Solo & hand.TrickIsOur == false ||
                    Wnd.GameModus == GameMode.Wenz & hand.TrickIsOur == false ||
                    Wnd.RufAs.IsAlreadyPlayed & tc.GetWinnerCard.CardRatedValue < 1000)
                    return true;
            }

            return false;
        }

        #endregion
    }
}

There are many more things which are related to Cards Tracking - explore the source code and you will find it.

I made sure that the computer player doesn't have more information than a human player.
In the current version, the Computer Player is a first-for-all opponent.
It is more important who gets good cards and who gets bad cards.
To get a meaningful result, about 100 games are necessary.

Conclusion

The new Version 4.6 or higher reaches a level like a human player with medium playing level.

This is only a demo – but I think it will allow you to play Schafkopf with / against your computer and have a lot of fun.

Final Note

I am very interested in feedback of any kind - problems, suggestions and other.

Credits / References

History

  • 20th February, 2024 - Part 2 of my article about 'Schafkopf' introduces Automated Bidding. Source code version 4.5 is only available in C#.
  • 22th February, 2024 - Source code version 4.6 with improved Automated Bidding.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)