Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF: P2P Chess

0.00/5 (No votes)
27 Jun 2011 4  
A WPF Peer-to-Peer chess application that utilizes WCF and PNM.

TheGame.png

Introduction

The .NET Framework provides developers with a rich set of class libraries that enable the creation of visually compelling applications that can effectively communicate with each other. The two technologies that afford us this luxury are WPF and WCF. WPF provides the eye-candy while WCF enables communication.

Some Windows [7 and Vista] features also enable discovery and ad-hoc collaboration, making it easier to find peers and send invitations for collaboration. People Near Me (PNM), Windows Contacts, and Invitations are the Windows features that enable discovery and collaboration.

It is with all this in mind that I decided to develop an application that would make use of the previously mentioned technologies, and what better application than a Peer-to-Peer (P2P) chess game. P2P Chess specifically makes use of WPF, WCF, and PNM.

Requirements

I will expect that you are conversant with WPF and WCF. If you're unfamiliar with Peer-to-Peer programming, check out the References section of this article.

To use the application, you require the following:

  • A PC running Win 7 or Vista
  • Connection to a local network
  • Suitable screen real-estate (15" + will do good)

Hopefully you have admin privileges on the machine you intend to run the application and there are other users on the same local network who also have the same app on their machines.

P2P Chess

So you want to challenge some worthy, or unworthy, fellow to a game of chess and you have launched P2P Chess. The first thing you should do is to check whether there are any worthy, or unworthy, fellows on the same local network. To do this, click on the expander button (the down facing arrow on the bottom left of the app) and click on the NewPeerButton (the button with an image of two fellows).

NewPeerBtn.png

You'll be presented with a request to enter a name to use when playing. After entering a name and clicking on the Enter button, or pressing the Enter key, the application will check if there are other people who have signed-into PNM on their machines and are on the same local subnet. If there are people who have satisfied those conditions, you'll see them listed and you can select a name and click on the Invite button.

Invite.png

If the application didn't find anyone on the local subnet, you'll be presented with a message box informing you of this unfortunate state of affairs and that you should try again later. But let's consider that people were found and you selected a name and clicked on the Invite button. An invitation will be sent to the invitee and he/she will see the following message box:

Invitation.png

In the best case scenario, the invitee is brave enough to accept the invitation and clicks on the Accept button. If the invitee, for reasons best known to him/her, declines the invitation, you'll be presented with a message box informing you of this unfortunate state of affairs. You can then proceed to try challenging someone else. But we shall consider that the invitation was accepted. Since that is the case, the application will assign playing colors and will inform you and your opponent which colors to use.

PieceAssignment.png

After clicking the OK button, you can proceed with the game. To select a piece, just click on it, and to move it, click on the square where you intend to move the piece. Selected pieces are slightly opaque.

GameInProgress.png

To castle, select your King and click on the square where you should move. The appropriate Rook will shift to the correct square.

Castling.png

You and your opponent can also engage in some interplay chat using the MessageTxtBox and the Send button.

GamingChatting.png

If you and your opponent want to start a new game, one of you can click on the NewGameButton. The application will be reset for a new game when you click Yes in the resulting dialog box.

NewGameBtn.png

NewGame.png

If you find it better to find another opponent, click the NewPeerButton (the one with an image of two fellows).

NewPeer.png

Issues to Note

P2P Chess will enforce chess rules, in regards to how you move a piece; for instance, only Knights can jump over other pieces. There are some rules though that aren't currently enforced so self-discipline and logic will have to prevail. The unenforced rules are:

  • Which piece moves first (obviously it is white)
  • How many times you can move even if the other player hasn't moved
  • Not moving a King to a position of check

En Passant is also currently not supported and you can't undo a move. Despite the fact that you can't undo a move, this doesn't prevent you from moving a piece back to its initial position if it isn't a pawn and you didn't take your opponent's piece.

A Pawn is automatically promoted to a Queen when it reaches the last rank.

Design and Layout

I designed some of the elements of P2P Chess in Expression Design and some in Expression Blend. The chess pieces though are made up of images I found on the net. The content elements/controls that are of importance are PiecesCanvas and LocationCanvas. Both are of equal dimensions and are stacked one after the other.

Objects_and_Timeline.png

Chess pieces will be added to the PiecesCanvas. The LocationCanvas contains 64 Rectangle objects that assist in piece selection and movement.

LocCanvas.png

NB: The colored squares are not the LocationCanvas rectangles.

Each of the chess pieces is a UserControl.

PieceControls.png

The Code

First off is the service contract:

<ServiceContract()> _
Public Interface IPlayChess
    <OperationContract(IsOneWay:=True)> _
    Sub SelectOrMovePiece(ByVal rctX As Double, ByVal rctY As Double, _
                          ByVal pcColor As String)

    <OperationContract(IsOneWay:=True)> _
    Sub AssignOpponentColor(ByVal myColor As String)

    <OperationContract(IsOneWay:=True)> _
    Sub AssignOpponentName(ByVal name As String)

    <OperationContract(IsOneWay:=True)> _
    Sub NewGame()

    <OperationContract(IsOneWay:=True)> _
    Sub Chat(ByVal message As String, ByVal player As String)
End Interface

The service and client configuration are defined in the application's configuration file.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
     <service name="p2pChess.MainWindow">
        <host>
          <baseAddresses>
            <add baseAddress="net.p2p://MeshackLabsCollab/P2PChess"/>
          </baseAddresses>
        </host>

        <endpoint name="p2pChessServiceEndpoint"
                  address=""
                  binding="netPeerTcpBinding"
                  bindingConfiguration="BindingUnsecure"
                  contract="p2pChess.IPlayChess"/>
      </service>
    </services>   
    
    <client>
      <endpoint name="p2pChessClientEndpoint"
                address="net.p2p://MeshackLabsCollab/P2PChess"
                binding="netPeerTcpBinding"
                bindingConfiguration="BindingUnsecure"
                contract="p2pChess.IPlayChess" />
    </client>
   
    <bindings>
      <netPeerTcpBinding>
        <binding name="BindingUnsecure">
          <security mode="None"/>
          <resolver mode="Pnrp"/>
        </binding>
      </netPeerTcpBinding>
    </bindings>

  </system.serviceModel>
  
  ...
</configuration>

The endpoint address is net.p2p://MeshackLabsCollab/P2PChess and the endpoint's binding is set to netPeerTcpBinding since this is a P2P application. From the address, you can gather that the mesh name is MeshackLabsCollab.

The MainWindow class is our service and it implements IPlayChess.

<ServiceBehavior(InstanceContextMode:=InstanceContextMode.Single)> _
Class MainWindow
    Implements IPlayChess
    ...

The class is decorated with the ServiceBehavior attribute with InstanceContextMode set to Single so that it acts as a singleton.

When the application is loaded, several methods are called:

Private Sub MainWindow_Loaded(ByVal sender As Object, _
                                  ByVal e As System.Windows.RoutedEventArgs) _
                                  Handles Me.Loaded
    StartService()
    RegisterApp()
    LoadPieces()
End Sub

The StartService method instantiates a ServiceHost object and call its Open method. It also creates a channel.

Private Sub StartService()
        host = New ServiceHost(Me)
        host.Open()
        channelFactory = New ChannelFactory(Of IPlayChess)("p2pChessClientEndpoint")
        channel = channelFactory.CreateChannel()
End Sub

The RegisterApp method registers P2P Chess with the peer infrastructure.

Private Sub RegisterApp()
    Try
        ' Sign-into peer infrastructure.
        PeerCollaboration.SignIn(PeerScope.NearMe)
        ' Sign-in will succeed but an exception will be
        ' thrown if IPv6 addresses are unavailable.
    Catch ex As PeerToPeerException
        MessageBox.Show("Make sure that you're connected to a local network", _
                        "P2P Chess", MessageBoxButton.OK, MessageBoxImage.Error)
        Me.Close()
    End Try

    Dim apps As PeerApplicationCollection = _
             PeerCollaboration.GetLocalRegisteredApplications()
    Dim appsCount As Integer = apps.Count

    p2pChessApp = New PeerApplication
    With p2pChessApp
        .Description = "P2P chess game"
        .Id = New Guid("faad461e-4d07-4ddb-99f2-94da5caa1124")
        .Path = Environment.CurrentDirectory & "\P2P Chess.exe"
        .PeerScope = PeerScope.NearMe
    End With

    If (appsCount > 0) Then
        For Each app As PeerApplication In apps
            If (app.Id = New Guid("faad461e-4d07-4ddb-99f2-94da5caa1124")) Then
                Exit Sub
            End If
        Next
        ' Register application to enable invitation.
        PeerCollaboration.RegisterApplication(p2pChessApp, _
                          PeerApplicationRegistrationType.CurrentUser)
    Else
        ' Register application to enable invitation.
        PeerCollaboration.RegisterApplication(p2pChessApp, _
                          PeerApplicationRegistrationType.CurrentUser)
        End If
End Sub

Registration of the application with the collaboration infrastructure is necessary if the user of the application intends to send invitations. Registration is typically done during installation of the app and the GUIDs of the application on the inviter's and invitee's machine should be similar. When you run P2P Chess for the first time, registration will be done once and ignored on the other occasions you run the application. Signing-in to PNM is also done in the RegisterApp method and is necessary to allow application registration or checking of applications already registered on the user's machine.

The LoadPieces method adds chess pieces to PiecesCanvas. You can take a look at this method, and other methods that load the chess pieces, in the MainWindow class.

Other methods to take note of in the MainWindow class are FindPeers and InvitePeer. FindPeers looks for peers who are currently on the same local network and are signed-into PNM on their machines.

Private Sub FindPeers()
    ' Find and add peers to listbox.
    Dim peers As PeerNearMeCollection = PeerCollaboration.GetPeersNearMe
    If (peers.Count > 0) Then
        For Each peer As PeerNearMe In peers
            If (peer.IsOnline = True) Then
                PeersListBox.Items.Add(peer.Nickname)
            End If
        Next
        InviteButton.Visibility = Windows.Visibility.Visible
    Else
        MessageBox.Show("No peers are online." & vbCrLf & "Try again later.", _
                        "P2P Chess", MessageBoxButton.OK, MessageBoxImage.Information)
        PeersGrid.Visibility = Windows.Visibility.Hidden
    End If
End Sub

InvitePeer sends an invitation for establishing a collaboration session.

Private Sub InvitePeer()
    If (PeersListBox.SelectedIndex <> -1) Then
        Dim peerToInvite As String = PeersListBox.SelectedValue.ToString
        Dim peers As PeerNearMeCollection = PeerCollaboration.GetPeersNearMe
        Dim nearPeer As PeerNearMe

        For Each pr As PeerNearMe In peers
            If (pr.Nickname = peerToInvite) Then
                nearPeer = pr
            End If
        Next

        otherPlayer = nearPeer.Nickname
        Dim response As PeerInvitationResponse = nearPeer.Invite(p2pChessApp, _
                                                                 "Fancy a game of chess?", _
                                                                 Nothing)
        WaitResponseGrid.Visibility = Windows.Visibility.Visible
        PeersGrid.Opacity = 0.5

        ' Check Peer's response
        If (response.PeerInvitationResponseType = PeerInvitationResponseType.Accepted) Then
            ' Assign colors to players.
            Dim colors() As String = {"White", "Black"}
            Dim rand As New Random
            Dim i As Integer = rand.Next(0, 2)
            pieceColor = colors(i)
            ' Give the other app some launch time.
            System.Threading.Thread.Sleep(4000)

            WaitResponseGrid.Visibility = Windows.Visibility.Hidden

            If (pieceColor = "White") Then
                channel.AssignOpponentColor("Black")
            Else
                channel.AssignOpponentColor("White")
            End If

            channel.AssignOpponentName(otherPlayer)
            LocationCanvas.IsEnabled = True

        ElseIf (response.PeerInvitationResponseType = _
                PeerInvitationResponseType.Declined) Then
            WaitResponseGrid.Visibility = Windows.Visibility.Hidden
            MessageBox.Show(otherPlayer & " declined your invitation.", "P2P Chess")
        ElseIf (response.PeerInvitationResponseType = _
                PeerInvitationResponseType.Expired) Then
            WaitResponseGrid.Visibility = Windows.Visibility.Hidden
            MessageBox.Show("Invitation expired.", "P2P Chess", _
                            MessageBoxButton.OK, MessageBoxImage.Exclamation)
        End If
        PeersGrid.Opacity = 1.0
    End If
End Sub

In InvitePeer, we also check on the response to the invitation from the invitee and respond accordingly. If the invitee accepts the invitation, playing colors will be assigned and a call to AssignOpponentColor() and AssignOpponentName() will be made on the service. AssignOpponentColor() mainly functions to assign a color to the invitee, but it also informs players which pieces to play with, among other things.

Public Sub AssignOpponentColor(ByVal color As String) _
           Implements IPlayChess.AssignOpponentColor
    ' Assign color to opposing player.
    If (pieceColor Is Nothing) Then
        pieceColor = color
    End If
    ' Hide some elements.
    If (PeersGrid.Visibility = Windows.Visibility.Visible) Then
        PeersGrid.Visibility = Windows.Visibility.Hidden
    End If
    If (InviteButton.Visibility = Windows.Visibility.Visible) Then
        InviteButton.Visibility = Windows.Visibility.Hidden
    End If
    If (NameGrid.Visibility = Windows.Visibility.Visible) Then
        NameGrid.Visibility = Windows.Visibility.Hidden
    End If
    ' Enable button for sending chat messages.
    SendButton.IsEnabled = True
    ' Enable button for starting a new game.
    NewGameButton.IsEnabled = True
    ' Keep an eye on changes to peer status.
    AddHandler PeerNearMe.PeerNearMeChanged, AddressOf PeerNearMeChangedCallback

    LocationCanvas.IsEnabled = True

    MessageBox.Show("Play with " & pieceColor & " pieces", "P2P Chess")
End Sub

Piece Selection and Movement

When the user clicks on the board to select or move a piece, the LocationCanvas MouseLeftButtonDown event handler is called.

Private Sub LocationCanvas_MouseLeftButtonDown(ByVal sender As Object, _
                                   ByVal e As System.Windows.Input.MouseButtonEventArgs) _
                                   Handles LocationCanvas.MouseLeftButtonDown
    Dim mouseX As Double = e.GetPosition(LocationCanvas).X
    Dim mouseY As Double = e.GetPosition(LocationCanvas).Y
    ' Check which rectangle is below the pointer and pass
    ' on its x and y coordinates.
    For Each rct As Rectangle In LocationCanvas.Children
        locRctX = Canvas.GetLeft(rct)
        locRctY = Canvas.GetTop(rct)
        If ((locRctX < mouseX) And ((locRctX + 44) > mouseX)) Then
            If ((locRctY < mouseY) And ((locRctY + 44) > mouseY)) Then
                channel.SelectOrMovePiece(locRctX, locRctY, pieceColor)
            End If
        End If
    Next
End Sub

SelectOrMovePiece() checks whether the player intends to select or move a piece. It also checks whether the player is handling his type of pieces (black or white).

Public Sub SelectOrMovePiece(ByVal rctX As Double, ByVal rctY As Double, _
                             ByVal pceColor As String) _
                             Implements IPlayChess.SelectOrMovePiece
    ' Check if a piece had been selected. Move a piece
    ' if it had been selected previously or decrease the
    ' opacity if otherwise.
    If (selectedPiece IsNot Nothing) Then
        ' Movement of piece.
        rules.MovePiece(selectedPiece, rctX, rctY, PiecesCanvas)
        selectedPiece.Opacity = 1
        selectedPiece = Nothing
    Else
        ' Selection of piece.
        For Each piece As UIElement In PiecesCanvas.Children
            Dim pieceX As Double = Canvas.GetLeft(piece)
            Dim pieceY As Double = Canvas.GetTop(piece)

            If (pieceX = rctX) And (pieceY = rctY) Then
                selectedPiece = piece
                ' If the color of selected piece isn't for current
                ' player then ignore.
                If (selectedPiece.ToString.Contains(pceColor)) Then
                    selectedPiece.Opacity = 0.4
                Else
                    selectedPiece = Nothing
                End If

            End If
        Next
    End If
End Sub

If the user intends to move a piece, a call is made to MovePiece() that resides in the class ChessRules. MovePiece() helps to ensure that chess pieces are moved according to the rules of chess.

Friend Sub MovePiece(ByRef selectedPiece As UIElement, _
                     ByVal rctX As Double, ByVal rctY As Double, _
                     ByRef piecesCanvas As Canvas)
    InitPieceX = Canvas.GetLeft(selectedPiece)
    InitPieceY = Canvas.GetTop(selectedPiece)
    ' Move King.
    If (TypeOf (selectedPiece) Is WhiteKing) Or (TypeOf (selectedPiece) Is BlackKing) Then
        If ((rctY - InitPieceY) = 44 Or (rctY - InitPieceY) = -44) Then
            Move(selectedPiece, rctX, rctY)
        ElseIf ((rctX - InitPieceX) = 44 Or (rctX - InitPieceX) = -44) Then
            Move(selectedPiece, rctX, rctY)
        End If
    End If
    ' Move Queen.
    If (TypeOf (selectedPiece) Is WhiteQueen) Or (TypeOf (selectedPiece) Is BlackQueen) Then
        If (((InitPieceY - rctY) - (rctX - InitPieceX) = 0) Or _
            ((InitPieceY - rctY) - (InitPieceX - rctX) = 0)) Then
            Move(selectedPiece, rctX, rctY)
        ElseIf (InitPieceX = rctX) Or (InitPieceY = rctY) Then
            Move(selectedPiece, rctX, rctY)
        End If
    End If
    ' Move Bishop.
    If (TypeOf (selectedPiece) Is WhiteBishop) Or (TypeOf (selectedPiece) Is BlackBishop) Then
        If (((InitPieceY - rctY) - (rctX - InitPieceX) = 0) Or _
            ((InitPieceY - rctY) - (InitPieceX - rctX) = 0)) Then
            Move(selectedPiece, rctX, rctY)
        End If
    End If
    ' Move Rook.
    If (TypeOf (selectedPiece) Is WhiteCastle) Or (TypeOf (selectedPiece) Is BlackCastle) Then
        If ((InitPieceX = rctX) Or (InitPieceY = rctY)) Then
            Move(selectedPiece, rctX, rctY)
        End If
    End If
    ' Move Knight.
    If (TypeOf (selectedPiece) Is WhiteKnight) Or (TypeOf (selectedPiece) Is BlackKnight) Then
        Dim xDiff As Integer = CInt(Math.Abs(rctX - InitPieceX))
        Dim yDiff As Integer = CInt(Math.Abs(rctY - InitPieceY))

        If ((xDiff = 44) And (yDiff = 88)) Or ((xDiff = 88) And (yDiff = 44)) Then
            Move(selectedPiece, rctX, rctY)
        End If
    End If
    ' Move White Pawn.
    MoveWhitePawn(selectedPiece, rctX, rctY, piecesCanvas)
    ' Move Black Pawn.
    MoveBlackPawn(selectedPiece, rctX, rctY, piecesCanvas)
    ' Prevent jumping of pieces.
    PreventJumpingOfPieces(selectedPiece, rctX, rctY, piecesCanvas)
    ' Prevent selected piece from landing on a piece
    ' with the same color.
    AvoidTeamPieces(selectedPiece, rctX, rctY, piecesCanvas)
    ' Castling
    Castle(selectedPiece, rctX, rctY, piecesCanvas)
    ' Take opponents piece.
    CapturePiece(selectedPiece, rctX, rctY, piecesCanvas)
    ' Reset so that only opponent pieces are captured
    ' if rules are followed.
    capture = False
    PromotePawn(selectedPiece, piecesCanvas)
End Sub

You can take a look at the ChessRules class to get an idea of how the rules are enforced. I've made a great effort to add comments where necessary and the logic is quite simple. Just to note, each chess piece has a size of 44 by 44 and each Rectangle in LocationCanvas has a similar size.

Conclusion

I hope that you have learnt something useful from this article and that P2P Chess has enhanced your social interaction with workmates or family. If there are any bugs, please inform me and I'll try my best to iron them out. If you aren't too familiar with Peer-to-Peer programming in .NET, then make sure to check out the links in the References section.

References

History

  • 17th Jun, 2011: Initial post.
  • 21st Jun, 2011: Updated code to enable castling.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here