Part 2 of German Cards Game 'Schafkopf'
Part 2 of my article about 'Schafkopf' 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
- The basics for a manually controlled cards game with four players (code and concept for game control) are based on CodeProject article [1] A Bridge Card Game and Display Card Presentation
- How to model a playing card in WPF, and define templates for defining the visual aspect of the card as per the sign and the value of the card, are taken from CodeProject article [2] Power of Templates in Control Development Within Windows Presentation Foundation
- Schafkopf is different from the American Sheepshead – this demo uses the German cards deck and German rules.
- Game rules [3] Schafkopf - Rules and strategy of card games (gambiter.com)
- In my demo, three players (East, West and North) are controlled by the computer. Player "South" is human controlled (by the user). The selection of the declarer and the game type are also human controlled (by the user).
- The redesigned version has an extended framework, large parts of it are powered by Graeme_Grant's cards framework [4] Cards game - data binding issue[^] which also inspired me to use
extension module
s for some framework classes. - Parts of the new concept regarding the structure of the game logic or game rules are based on GitHub project [6] GitHub - llkippe/SchafkopfAI: Creating the fourth missing player in the bavarian card game: Schafkopf
- The
TrickContent
class
and theCardRatedValue
property/method are based on project [7] Schafkopf/Schafkopf/Models at master · mkre/Schafkopf · GitHub
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:
MainWindow
has the following controls:
- A
Grid
with- a
StackPanel
calledMyPanel
- a
WrapPanel
(on top) with the menu items HistoryTextBox
for Trick History- Panels for the cards within the
StackPanel
calledMyPanel
Panel0
withListBox0
on top forplayer0 = North
Panel2
withListBox2
on bottom forplayer2 = South
Panel1
withListBox1
on right side forplayer1 = East
Panel3
withListBox3
on left side forplayer3 = West
Panel4
withListBox4
is used asTrickHistory
(for already played cards)DockPanel
CenterPanel
- a
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.
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 ListBox
es / CardPanel
s.
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
- Check if it is ok to ´"
Schmear
" [https://en.wikipedia.org/wiki/Schmear]
Public Function
IsToSchmearOK
Public Sub SetCards
inModul Extensions_TrickMonitoring
, for example, is used to get:
PlayingCard
Property IsHighestPlayableTrumpCard
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
- [1] A Bridge Card Game and Display Card Presentation
- [2] Power of Templates in Control Development Within Windows Presentation Foundation
- [3] Schafkopf - Rules and strategy of card games (gambiter.com)
- [4] Graeme_Grant's cards framework Cards game - data binding issue[^]
- [5] https://stackoverflow.com/questions/390491/how-to-add-item-to-the-beginning-of-listt
- [6] GitHub - llkippe/SchafkopfAI: Creating the fourth missing player in the bavarian card game: Schafkopf
- [7] Schafkopf/Schafkopf at master · mkre/Schafkopf · GitHub
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.