65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF: P2P 国际象棋

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (61投票s)

2011年6月17日

CPOL

7分钟阅读

viewsIcon

108789

downloadIcon

8792

一个利用 WCF 和 PNM 的 WPF 点对点国际象棋应用程序。

TheGame.png

引言

.NET Framework 为开发人员提供了一套丰富的类库,使他们能够创建能够有效相互通信的视觉上引人注目的应用程序。使我们能够享受这种便利的两种技术是 WPF 和 WCF。WPF 提供华丽的界面,而 WCF 则实现通信。

某些 Windows [7 和 Vista] 功能还支持发现和即席协作,从而更容易找到对等方并发送协作邀请。 People Near Me (PNM)Windows Contacts 和 Invitations 是支持发现和协作的 Windows 功能。

正是基于这一切,我决定开发一个应用程序来利用前面提到的技术,而什么应用程序比点对点 (P2P) 国际象棋游戏更好呢?P2P Chess 特别利用了 WPF、WCF 和 PNM。

要求

我期望您熟悉 WPF 和 WCF。如果您不熟悉点对点编程,请查看本文的“参考文献”部分。

要使用该应用程序,您需要以下条件:

  • 运行 Win 7 或 Vista 的 PC
  • 连接到本地网络
  • 合适的屏幕空间(15 英寸及以上即可)

希望您拥有运行应用程序的机器的管理权限,并且同一本地网络上还有其他用户也在他们的机器上安装了相同的应用程序。

P2P 国际象棋

所以,您想挑战某位值得挑战(或不值得挑战)的朋友玩一局国际象棋,并且您已经启动了P2P Chess。您应该做的第一件事是检查同一本地网络上是否有值得挑战(或不值得挑战)的朋友。为此,请单击展开器按钮(应用程序左下角的向下箭头),然后单击 NewPeerButton(带有两人图像的按钮)。

NewPeerBtn.png

系统会提示您输入一个游戏名称。输入名称并单击“Enter”按钮或按 Enter 键后,应用程序将检查是否有其他人已登录其机器上的 PNM 并且在同一本地子网上。如果存在满足这些条件的人,您将看到他们的列表,您可以选择一个名称并单击“Invite”按钮。

Invite.png

如果应用程序未在本地子网上找到任何人,您将收到一个消息框,告知您这种情况很不理想,并建议您稍后重试。但我们假设找到了人,您选择了一个名称并单击了“Invite”按钮。将向被邀请者发送一个邀请,他/她将看到以下消息框:

Invitation.png

在最好的情况下,被邀请者足够勇敢,接受邀请并单击“Accept”按钮。如果被邀请者出于他/她自己的原因拒绝了邀请,您将收到一个消息框,告知您这种情况很不理想。然后您可以继续尝试挑战其他人。但我们将认为邀请已被接受。既然如此,应用程序将分配对弈颜色,并通知您和您的对手要使用什么颜色。

PieceAssignment.png

单击“OK”按钮后,您可以继续游戏。要选择一个棋子,只需单击它,要移动它,请单击您打算移动棋子的方格。选中的棋子会稍微半透明。

GameInProgress.png

要进行王车易位,请选择您的王并单击您应该移动的方格。相应的车将移动到正确方格。

Castling.png

您和您的对手还可以使用 MessageTxtBox 和“Send”按钮进行一些即时聊天。

GamingChatting.png

如果您和您的对手想开始新游戏,你们中的一个人可以单击 NewGameButton。当您在出现的对话框中单击“Yes”时,应用程序将重置以开始新游戏。

NewGameBtn.png

NewGame.png

如果您更愿意寻找另一位对手,请单击 NewPeerButton(带有两人图像的那个)。

NewPeer.png

注意事项

P2P Chess 将强制执行国际象棋规则,例如如何移动棋子;例如,只有马可以跳过其他棋子。但有一些规则目前未强制执行,因此需要靠自律和逻辑。未强制执行的规则是:

  • 哪个棋子先走(显然是白棋)
  • 即使对方未走,您还可以走多少步
  • 不将王移动到被将军的位置

吃过路兵目前也不支持,并且您无法撤销一步。尽管您无法撤销一步,但这并不能阻止您将棋子移回初始位置(如果它不是兵,并且您没有吃掉对方的棋子)。

当兵到达最后一行时,它会自动升变为后。

设计和布局

我使用 Expression Design 设计了 P2P Chess 的部分元素,使用 Expression Blend 设计了部分元素。国际象棋的棋子则是我在网上找到的图片。重要的内容元素/控件是 PiecesCanvasLocationCanvas。两者尺寸相同,并堆叠在一起。

Objects_and_Timeline.png

棋子将被添加到 PiecesCanvasLocationCanvas 包含 64 个 Rectangle 对象,用于辅助棋子的选择和移动。

LocCanvas.png

注意:彩色方格不是 LocationCanvas 的矩形。

每个国际象棋棋子都是一个 UserControl

PieceControls.png

代码

首先是服务契约。

<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

服务和客户端配置在应用程序的配置文件中定义。

<?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>

端点地址是 net.p2p://MeshackLabsCollab/P2PChess,端点的绑定设置为 netPeerTcpBinding,因为这是一个 P2P 应用程序。从地址可以看出,Mesh 名称是 MeshackLabsCollab

MainWindow 类是我们的服务,它实现了 IPlayChess

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

该类用 ServiceBehavior 属性装饰,其中 InstanceContextMode 设置为 Single,以便它充当单例。

应用程序加载时,将调用几个方法:

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

StartService 方法实例化一个 ServiceHost 对象并调用其 Open 方法。它还创建一个通道。

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

RegisterApp 方法将 P2P Chess 注册到对等基础结构。

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

如果应用程序用户打算发送邀请,则必须将应用程序注册到协作基础结构。注册通常在应用程序安装时完成,并且邀请者和被邀请者机器上的应用程序 GUID 应该相似。当您第一次运行 P2P Chess 时,注册将只执行一次,而您再次运行应用程序时则会被忽略。在 RegisterApp 方法中还会执行 PNM 登录,这对于允许应用程序注册或检查用户机器上已注册的应用程序是必需的。

LoadPieces 方法将国际象棋棋子添加到 PiecesCanvas。您可以查看此方法以及在 MainWindow 类中加载国际象棋棋子的其他方法。

MainWindow 类中还有其他值得注意的方法,如 FindPeersInvitePeerFindPeers 查找当前在同一本地网络上并且在其机器上登录了 PNM 的对等方。

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 发送建立协作会话的邀请。

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

InvitePeer 中,我们还检查被邀请者对邀请的响应并据此做出响应。如果被邀请者接受邀请,将分配对弈颜色,并会在服务上调用 AssignOpponentColor()AssignOpponentName()AssignOpponentColor() 主要功能是为被邀请者分配颜色,同时它还通知玩家要使用哪些棋子,等等。

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

棋子选择和移动

当用户单击棋盘以选择或移动棋子时,将调用 LocationCanvasMouseLeftButtonDown 事件处理程序。

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() 检查玩家是打算选择还是移动棋子。它还检查玩家是否正在处理自己的棋子(黑棋或白棋)。

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

如果用户打算移动棋子,将调用位于 ChessRules 类中的 MovePiece()MovePiece() 有助于确保棋子根据国际象棋规则移动。

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

您可以查看 ChessRules 类来了解规则是如何强制执行的。我已经尽力在必要的地方添加了注释,并且逻辑非常简单。请注意,每个国际象棋棋子的大小为 44x44,LocationCanvas 中的每个 Rectangle 具有类似的大小。

结论

我希望您从本文中学到了一些有用的知识,并且 P2P Chess 增强了您与同事或家人的社交互动。如果您发现任何错误,请告知我,我会尽力修复它们。如果您对 .NET 中的点对点编程不太熟悉,请务必查看“参考文献”部分中的链接。

参考文献

历史

  • 2011 年 6 月 17 日:初次发布。
  • 2011 年 6 月 21 日:更新代码以启用王车易位。
© . All rights reserved.