TheGame.png

Introduction

The .NET Framework provides developers with a rich set class libraries that enable 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 [Win7 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 lets 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

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 especially 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.

Castling and En Passant are 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 opponents 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 is 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 endpoints binding is set to netPeerTcpBinding since this is a P2P application. From the address you can gather that the meshname for the service 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 the 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 both 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 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 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)
        ' 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 learnt something useful from this article and that P2P Chess 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

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"