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:
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.
using System;
using System.Windows;
using Schafkopf_Cs.Extensions;
using System.Diagnostics;
namespace Schafkopf_Cs.Models.Bidding
{
public class Auction
{
private int _PlayerID;
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
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'.");
}
}
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;
int[] iGameModeSoloTrumpCount = new int[4];
iGameModeSauspielTrumpCount = hand.GetHandTrumpCount(hand, 1);
iUnderCount = hand.GetHandUnderCount(hand, 1);
bidResult = iGameModeSauspielTrumpCount;
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; }
if (iUnderAndOverCount == iUnderCount)
{
bidSoloResult -= 1;
}
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;
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; }
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;
}
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;
if (hand.HasCard((CardValue)2, (CardType)3) == false)
iUnderCount -= 1;
if (iUnderCount + iAcesCount + iAcesWithTenDuoCount >= 6)
{
bidGameMode[player] = GameMode.Wenz;
iRatedBid[player] = 200;
iContractSuitIndex[player] = 4;
}
for (int i = 0; i < 4; i++)
{
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;
Trump_Color[player] = (Color)(i * 100 + 100);
iContractSuitIndex[player] = i;
bidGameMode[player] = GameMode.Solo;
iRatedBid[player] = 300 + (i * 10);
}
}
return iRatedBid[player];
}
#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
Public Function
IsToSchmearOK
Public Sub SetCards
in Modul 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
{
#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)
{
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)
{
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)
{
IsSchmearOK = true;
}
else
{
IsSchmearOK = false;
}
return IsSchmearOK;
}
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.