There are many CodeProject articles about other card games, but nothing with Schafkopf. So I started to create this document. I will not describe and explain every detail of the complete demo project, but how to use it and play the game.
Download Schafkopf.zip
Introduction
This article and the demo are about getting started using my SchafkopfStarterKit
VB.NET project.
Version 2.6 or higher includes source code for C# in CSchafkopfStarterKit
project.
Background
There are many CodeProject articles about other card games, but nothing with Schafkopf
. So I started to create this document.
I will not describe and explain every detail of the complete demo project but how to use it and play the game.
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
DockPanel
Panel0
on top for player0 = North
DockPanel
Panel2
on bottom for player2 = South
WrapPanel
Panel1
on right side for player1 = East
WrapPanel
Panel3
on left side for player3 = West
DockPanel
CenterPanel
Card Resources and Definitions
The files in folder Resources are taken from [2].
I have changed some things as follows:
- Jacks => Unter => Under (or Sergeant)
- Queens => Ober => Over (or Officer)
- Diamonds => Shells or Ring
- Clubs => Acorn
To keep things easy, I left Spades (which could become „Grass“) as it was.
Class Bridge
is taken from [1].
I have changed the cards deck from 52 to 32 and adjusted the card values and colors as described above.
Putting Things Together - WPF Concept and Code
Shuffle and Distribute Cards
Function ShuffleArray
and Function RandomNumber
are taken from Reference [4].
Classes PlayingCard
and CircularPanel
are taken from Reference [2].
I have added the following properties:
Public Property CardSymbol As String
Public Property CardShortName As String
Public Property IsCallAce As Boolean
Public Property IsAlreadyPlayed As Boolean
Private Shared CardOwnerProperty As DependencyProperty = DependencyProperty.Register("CardOwner", GetType(CardOwner), GetType(PlayingCard), New PropertyMetadata(CardOwner.North))
. . .
When you click on 1. New Game, the ShuffleArray
method is started.
After that, the shuffled cards are distributed to the four CardPanel
s.
Public Sub NewGame()
Me.Deck.Children.Clear()
handCards0.Clear()
handCards1.Clear()
handCards2.Clear()
handCards3.Clear()
Panel0.Children.Clear()
Panel2.Children.Clear()
Panel1.Children.Clear()
Panel3.Children.Clear()
CenterPanel.Children.Clear()
cards.Clear()
iTeamDeclarer = 0
iTeamOpponent = 0
HistoryTextBox.Text = "Trick History: " & Environment.NewLine
RufAs.CardOwner = -1
bh.iCoSpieler = -1
bh.iGeber = bh.iGeber + 1
If bh.iGeber = 4 Then bh.iGeber = 0
nextMove = bh.iGeber
bh.leadPlayer = bh.iGeber
GameStatus = 2
cbxDeclarer.SelectedIndex = 4
cbxContractSuit.SelectedIndex = 8
PlayCard.IsEnabled = False
labelTrumpLead.Content = " "
labelTrumpcard.Content = "Display for Game Type"
For Each type As CardType In [Enum].GetValues(GetType(CardType))
For Each value As CardValue In [Enum].GetValues(GetType(CardValue))
Dim card As PlayingCard = New PlayingCard()
card.CardType = type
card.CardValue = value
card.Height = 135
card.Width = 85
If card.CardType = 0 Then card.CardSymbol = "Ꚛ"
If card.CardType = 1 Then card.CardSymbol = "♥"
If card.CardType = 2 Then card.CardSymbol = "♠"
If card.CardType = 3 Then card.CardSymbol = "Ⴖ"
card.CardShortName = card.CardValue
If card.CardValue = 2 Then card.CardShortName = "U"
If card.CardValue = 3 Then card.CardShortName = "O"
If card.CardValue = 4 Then card.CardShortName = "K"
If card.CardValue = 11 Then card.CardShortName = "A"
cards.Add(card)
Next
Next
cards = ShuffleArray(cards)
Dim cardShuffled As PlayingCard = New PlayingCard()
Dim i As Integer = 0
For Each cardShuffled In cards
AddHandler cardShuffled.Click, AddressOf card_Click
If i = 0 Or i = 4 Or i = 8 Or i = 12 Or i = 16 Or i = 20 Or i = 24 Or
i = 28 Then
If handCards0.Count > 0 Then
For n = 0 To handCards0.Count - 1
If handCards0.Contains(cardShuffled) = False AndAlso
cardShuffled.CardType < handCards0.Item(n).CardType Then
cardShuffled.CardOwner = CardOwner.North
If handCards0.Contains(cardShuffled) = False Then
handCards0.Insert(n, cardShuffled)
Exit For
End If
Next
If handCards0.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.North
If handCards0.Contains(cardShuffled) = False Then
handCards0.Add(cardShuffled)
Else
If handCards0.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.North
If handCards0.Contains(cardShuffled) = False Then
handCards0.Add(cardShuffled)
End If
End If
If i = 1 Or i = 5 Or i = 9 Or i = 13 Or i = 17 Or i = 21 Or i = 25 Or
i = 29 Then
If handCards2.Count > 0 Then
For n = 0 To handCards2.Count - 1
If handCards2.Contains(cardShuffled) = False AndAlso
cardShuffled.CardType < handCards2.Item(n).CardType Then
If handCards2.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.South
handCards2.Insert(n, cardShuffled)
End If
Exit For
End If
Next
If handCards2.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.South
handCards2.Add(cardShuffled)
End If
Else
If handCards2.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.South
handCards2.Add(cardShuffled)
End If
End If
End If
If i = 2 Or i = 6 Or i = 10 Or i = 14 Or i = 18 Or i = 22 Or i = 26 Or
i = 30 Then
If handCards1.Count > 0 Then
For n = 0 To handCards1.Count - 1
If handCards1.Contains(cardShuffled) = False AndAlso
cardShuffled.CardType < handCards1.Item(n).CardType Then
If handCards1.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.East
If handCards1.Contains(cardShuffled) = False Then
handCards1.Insert(n, cardShuffled)
Exit For
End If
Next
If handCards1.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.East
If handCards1.Contains(cardShuffled) = False Then
handCards1.Add(cardShuffled)
Else
If handCards1.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.East
If handCards1.Contains(cardShuffled) = False Then
handCards1.Add(cardShuffled)
End If
End If
If i = 3 Or i = 7 Or i = 11 Or i = 15 Or i = 19 Or i = 23 Or i = 27 Or
i = 31 Then
If handCards3.Count > 0 Then
For n = 0 To handCards3.Count - 1
If handCards3.Contains(cardShuffled) = False AndAlso
cardShuffled.CardType < handCards3.Item(n).CardType Then
If handCards3.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.West
If handCards3.Contains(cardShuffled) = False Then
handCards3.Insert(n, cardShuffled)
Exit For
End If
Next
If handCards3.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.West
If handCards3.Contains(cardShuffled) = False Then
handCards3.Add(cardShuffled)
Else
If handCards3.Contains(cardShuffled) = False Then
cardShuffled.CardOwner = CardOwner.West
If handCards3.Contains(cardShuffled) = False Then
handCards3.Add(cardShuffled)
End If
End If
i += 1
Next
For Each cardShuffled0 In handCards0
Panel0.Children.Add(cardShuffled0)
Next
For Each cardShuffled2 In handCards2
Panel2.Children.Add(cardShuffled2)
Next
For Each cardShuffled1 In handCards1
Panel1.Children.Add(cardShuffled1)
Next
For Each cardShuffled3 In handCards3
Panel3.Children.Add(cardShuffled3)
Next
Panel0 = SortHandCards(Panel0)
Panel1 = SortHandCards(Panel1)
Panel2 = SortHandCards(Panel2)
Panel3 = SortHandCards(Panel3)
Me.Deck.AddHandler(ToggleButton.CheckedEvent, _
New RoutedEventHandler(AddressOf OnCardSelected))
End Sub
Selection of the Declarer and the Game Type
The selection of the declarer and the game type are also human controlled (by the user).
You have to follow the steps as shown in the menu on top.
1. New Game
A click on that menu item starts the ShuffleArray
method.
After that, the shuffled cards are distributed to the four CardPanel
s.
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.
Place a Card on the CenterPanel
If it is the human player's turn, he can play one of them by clicking on one of his cards.
The clicked card is then automatically moved to the DockPanel CenterPanel
.
Private Sub card_Click (ByVal sender As Object, ByVal e As System.EventArgs)
Dim ACP As New AutoCardPlay
If CenterPanel.Children.Count = 4 Then
CenterPanel.Children.Clear()
HistoryTextBox.AppendText(Environment.NewLine & _
"-----------------------" &
Environment.NewLine)
End If
sender.Parent.Children.Remove(sender)
Dim cardColor As String
Dim cardValue As String
Dim cardOwner As String
If sender.CardType = 0 Then cardColor = "Ꚛ"
If sender.CardType = 1 Then cardColor = "♥"
If sender.CardType = 2 Then cardColor = "♠"
If sender.CardType = 3 Then cardColor = "Ⴖ"
cardValue = sender.CardValue
If sender.CardValue = 2 Then cardValue = "U"
If sender.CardValue = 3 Then cardValue = "O"
If sender.CardValue = 4 Then cardValue = "K"
If sender.CardValue = 11 Then cardValue = "A"
If sender.CardOwner = 0 Then cardOwner = "North"
If sender.CardOwner = 1 Then cardOwner = "East "
If sender.CardOwner = 2 Then cardOwner = "South"
If sender.CardOwner = 3 Then cardOwner = "West "
If sender.CardType = RufAs.CardType AndAlso sender.CardShortName = _
"A " Then
RufAs.IsAlreadyPlayed = True
End If
HistoryTextBox.AppendText(Environment.NewLine & cardOwner & ": " _
& cardColor &
" " & cardValue)
CenterPanel.Children.Add(CType(sender, UIElement))
System.Threading.Thread.Sleep(250)
If totalMove Mod 4 = 0 Then Trick_Content.PrevCard1 = Trick_Content.Card1
If totalMove Mod 4 = 0 Then Trick_Content.PrevCard2 = Trick_Content.Card2
If totalMove Mod 4 = 0 Then Trick_Content.PrevCard3 = Trick_Content.Card3
If totalMove Mod 4 = 0 Then
If Trick_Content.Card4 IsNot Nothing Then Trick_Content.PrevCard4 =
Trick_Content.Card4
End If
If totalMove Mod 4 = 1 Then
Trick_Content.Card1 = cardValue.ToString & " " & cardColor
Trick_Content.Card2 = Nothing
Trick_Content.Card3 = Nothing
Trick_Content.Card4 = Nothing
Trick_Content.PlayerIdCard2 = -1
Trick_Content.PlayerIdCard3 = -1
Trick_Content.PlayerIdCard3 = -1
End If
If totalMove Mod 4 = 2 Then Trick_Content.Card2 = cardValue.ToString &
" " & cardColor
If totalMove Mod 4 = 3 Then Trick_Content.Card3 = cardValue.ToString &
" " & cardColor
If totalMove Mod 4 = 0 Then Trick_Content.Card4 = cardValue.ToString &
" " & cardColor
If totalMove Mod 4 = 1 Then Trick_Content.CountCardsInTrick = 1
If totalMove Mod 4 = 2 Then Trick_Content.CountCardsInTrick = 2
If totalMove Mod 4 = 3 Then Trick_Content.CountCardsInTrick = 3
If totalMove Mod 4 = 0 Then Trick_Content.CountCardsInTrick = 4
If totalMove Mod 4 = 1 Then Trick_Content.PlayerIdCard1 = nextMove
If totalMove Mod 4 = 2 Then Trick_Content.PlayerIdCard2 = nextMove
If totalMove Mod 4 = 3 Then Trick_Content.PlayerIdCard3 = nextMove
If totalMove Mod 4 = 0 Then Trick_Content.PlayerIdCard4 = nextMove
If totalMove Mod 4 = 1 Then
tc = Trick_Content.Card1
Trick_Content.CurrentTrickWinner = nextMove
End If
If totalMove Mod 4 = 2 Then
tc = Trick_Content.Card2
Trick_Content.CurrentTrickWinner = GetCurrentTrickWinner(Me,
totalMove Mod 4)
End If
If totalMove Mod 4 = 3 Then
tc = Trick_Content.Card3
Trick_Content.CurrentTrickWinner = GetCurrentTrickWinner(Me,
totalMove Mod 4)
End If
If totalMove Mod 4 = 0 Or totalMove Mod 4 = 4 Then
tc = Trick_Content.Card4
Trick_Content.CurrentTrickWinner = GetCurrentTrickWinner(Me,
totalMove Mod 4)
End If
If totalMove Mod 4 = 1 Then
sWenz = ""
sOber = ""
If sender.CardValue = 2 Or sender.CardValue = 3 Then
If totalMove Mod 4 = 1 Then bh.leadSuit = bh.trumpCard
Else
bh.leadSuit = sender.CardType
End If
labelTrumpLead.Content = "Trump or Lead: " & bh.suitRows(bh.leadSuit)
End If
nextMove = (nextMove + 1) Mod 4
updateTurnToMoveMessage(nextMove)
If totalMove Mod 4 = 0 Then
tally()
GameStatus = GameState.FirstCardInTrick
System.Threading.Thread.Sleep(500)
Else
GameStatus = GameState.AnotherCardInTrick
End If
totalMove += 1
Trick_Content.PossibleWinnerPlayerID =
TrickWinnerID(Trick_Content.CurrentTrickWinner)
Dim cardsPanel As Object = {Panel0, Panel1, Panel2, Panel3}
For i = 0 To 3
If GameStatus.ToString = "SpielAus" Then Exit Sub
If nextMove = 2 Then
If nextMove = 2 Then Exit Sub
Else
If nextMove = i Then ACP.SelectCard(Nothing, nextMove, bh.declarer,
GameStatus, cardsPanel(nextMove), _
bh.trumpCard, Nothing, Me, bh.leadSuit)
If nextMove = i Then If i > 0 Then GameStatus = _
GameState.AnotherCardInTrick
End If
Next
End Sub
If it is the auto player's turn, the Public Sub AutoPlaceCardOnTable
method does something similar like the Sub card_Click
.
At the end of this method:
ACP.SelectCard(Nothing, nextMove, bh.declarer, GameStatus, cardsPanel(nextMove),
bh.trumpCard, Nothing, Me, bh.leadSuit)
is called. ACP
is a New AutoCardPlay
object.
With nextMove
, we control whose turn it is to play a card.
In Sub
SelectCard
, we make a difference between the first card of a trick and the other cards of a trick.
For the first card of a trick, the methods SelectBestFirstCard
and WenzBestFirstCard
are relevant.
For the other cards of a trick, the method SelectBestReturnCard
is normally used.
All of these methods may call other Functions or Subs like:
OptimizeSelection
PlayTogether_WenzUsage
ContainsHandCardsStandardTrumps
GetLowOfHandCards
GetBestOfHandCards
Most used method is cardLessThan
which is very important for comparing values of cards.
The original version is taken from [1]. I extended it because of the special value of „Overs“ and „Unders“ in the Schafkopf game.
At the end of the SelectCard
method, we use the following code:
If CurrentCard IsNot Nothing Then
CardsPanel.Children.Remove(CurrentCard)
MyForm.AutoPlaceCardOnTable(CurrentCard, PlayerID)
End If
to remove the CurrentCard
from the players CardsPanel
and call the AutoPlaceCardOnTable
method again to paste the CurrentCard
to the CenterPanel
and go on with Auto Play.
TrickContent
is another important class which we need for using cardLessThan
or other features.
Public Class TrickContent
Public Property IdLeadSuit As Integer
Public Property Card1 As String
Public Property Card2 As String
Public Property Card3 As String
Public Property Card4 As String
Public Property CurrentTrickWinner As Integer
Public Property PossibleWinnerPlayerID As Integer
Public Property PossibleWinnerCardString As String
Public Property CountCardsInTrick As Integer
Public Property GetSumOfCardValuesInTrick As Integer
Public Property PlayerIdCard1 As Integer
Public Property PlayerIdCard2 As Integer
Public Property PlayerIdCard3 As Integer
Public Property PlayerIdCard4 As Integer
Public Property PrevCard1 As String
Public Property PrevCard2 As String
Public Property PrevCard3 As String
Public Property PrevCard4 As String
End Class
Auto Play Feature
At the end of:
Public Sub AutoPlaceCardOnTable
. . .
. . .
Dim cardsPanel As Object = {Panel0, Panel1, Panel2, Panel3}
For i = 0 To 3
If GameStatus.ToString = "SpielAus" Then Exit Sub
If nextMove = 2 Then
If nextMove = 2 Then Exit Sub
Else
If nextMove = i Then ACP.SelectCard(Nothing, nextMove, bh.declarer,
GameStatus, cardsPanel(nextMove), _
bh.trumpCard, Nothing, Me, bh.leadSuit)
If nextMove = i Then If i > 0 _
Then GameStatus = GameState.AnotherCardInTrick
End If
Next
End Sub
We take a break If nextMove = 2
(this means Human Player needs to place a card) or run ACP.SelectCard
again if nextMove
<> 2
.
We stop the game If GameStatus.ToString
= "SpielAus
".
Implementing Schafkopf Rules
After some time, my coding resulted in many, many loops when checking a players cards and endless "rules" which are done with "If
, Then
, ElseIf
..." statements. But it seems to work very well now.
The main problem is how to find possible issues within those code parts.
The new Version 2.6 or higher reaches an improved playing level, like medium playing level.
Private Function SelectBestFirstCard(ByVal CardsPanel As Object, PlayerID As Integer,
DeclarerID As Integer, GameStatus As Object, sHandCards As Object, _
TrumpCardID As Integer,
CorrectCards As Object, MyForm As MainWindow, LeadSuitID As Integer) As PlayingCard
Dim MyBridge As New Bridge
Dim CurrentCard As PlayingCard
Dim bTeamDeclarer As Boolean
Dim bTeamOpponent As Boolean
If MyForm.GameOver = True Then Exit Function
If GameStatus.ToString = "SpielAus" Then MyForm.GameOver = True
If GameStatus.ToString = "SpielAus" Then Exit Function
If PlayerID <> DeclarerID And PlayerID <> MyForm.iCoSpieler Then
bTeamOpponent = True
If PlayerID = DeclarerID Or PlayerID = MyForm.iCoSpieler Then
bTeamDeclarer = True
With CardsPanel.Children
If .Count > 0 AndAlso bTeamDeclarer = True Then
For n = 0 To .Count - 1
If .Item(n).CardType = TrumpCardID Then
If sHandCards.ToString.Contains("A") = True AndAlso
DeclarerID = PlayerID Then
If MyForm.iTricks = 5 Or MyForm.iTricks = 6 Or
MyForm.iTricks = 7 Then
If SearchInCardsPanel(11, CardsPanel).CardType <>
TrumpCardID Then
CurrentCard = SearchInCardsPanel(11, CardsPanel)
End If
End If
Else
Dim s As String = .Item(n).CardShortName
If s.Contains(GetBestOfHandCards_
(CardsPanel, PlayerID, DeclarerID,
GameStatus, sHandCards, TrumpCardID, "RufAs", MyForm,
LeadSuitID)) Then
CurrentCard = CardsPanel.Children.Item(n)
End If
End If
End If
Next
End If
If .Count > 0 AndAlso bTeamDeclarer = True Then
For n = 0 To .Count - 1
If .Item(n).CardType <> TrumpCardID Then
Dim sO As String
If TrumpCardID = 4 Then sO = "X"
If TrumpCardID <> 4 Then sO = "O"
If .Item(n).CardShortName.Contains(sO) Or
.Item(n).CardShortName.Contains("U") Then
CurrentCard = .Item(n)
LeadSuitID = TrumpCardID
MyBridge.leadSuit = TrumpCardID
End If
End If
Next
End If
If bTeamOpponent = True Then
If .Count > 0 Then
For n = 0 To .Count - 1
If .Item(n).CardType = LeadSuitID Then
Dim sO As String
If TrumpCardID = 4 Then sO = "X"
If TrumpCardID <> 4 Then sO = "O"
If .Item(n).CardShortName.Contains(sO) = False AndAlso
.Item(n).CardShortName.Contains("U") = False Then
CurrentCard = .Item(n)
End If
End If
Next
End If
If .Count > 0 AndAlso MyForm.RufAs.IsAlreadyPlayed = False AndAlso
MyForm.iCoSpieler > -1 Then
For n = 0 To .Count - 1
If .Item(n).CardType <> TrumpCardID AndAlso
.Item(n).CardType = MyForm.RufAs.CardType Then
Dim sO As String
If TrumpCardID = 4 Then sO = "X"
If TrumpCardID <> 4 Then sO = "O"
If .Item(n).CardShortName.Contains(sO) = False AndAlso
.Item(n).CardShortName.Contains("U") = False Then
CurrentCard = .Item(n)
MyForm.RufAs.IsAlreadyPlayed = True
End If
End If
Next
End If
End If
If .Count > 0 AndAlso CurrentCard Is Nothing Then
For n = 0 To .Count - 1
CurrentCard = .Item(n)
Next
End If
End With
Return CurrentCard
End Function
About Version 2.6 or higher
This version includes all improvements from v1.5 to 2.3
And it includes a converted version of the project to C# what caused some trouble but finally it seems to work now. For unknown reasons, the C# 2.0 version had a worse playing level than the VB.NET one.
With version 2.6 or higher, both - VB.NET and C# - projects are at a comparable level.
Conclusion
This is only a demo – it is not production ready.
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 / Reference
History
- 19th July, 2022 - VB and C# Version 2.7 include bug fixes and improved Team Playing.
- 11th July, 2022 - replaced dead link, VB and C# Version 2.6 with an improved playing level
- 7th July, 2022 - VB and C# Version 2.5 with an improved playing level
- 1st July, 2022 - VB and C# Version 2.3 come along with some improvements
- 25th June, 2022 - VB and C# Version 2.2 come along with some improvements
- 21st June, 2022 - C# Version 2.1 with an improved playing level
- 19th June, 2022 - Version 2.0 reaches almost medium playing level. NEW: C# Version of this project.
- 15th June, 2022 - Version 1.8 reaches an improved playing level, almost like medium playing level and fixes some bugs
- 12th June, 2022 - Version 1.7 reaches an improved playing level, almost like medium playing level and includes some bug fixes as well
- 10th June, 2022 - Version 1.6 improves Auto Playing Level and includes some bug fixes as well
- 7th June, 2022 - Version 1.5 allows an improved Auto Playing Level and includes some bug fixes as well
- 4th June, 2022 - Initial submission