WPF: P2P 国际象棋






4.88/5 (61投票s)
一个利用 WCF 和 PNM 的 WPF 点对点国际象棋应用程序。
引言
.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
(带有两人图像的按钮)。
系统会提示您输入一个游戏名称。输入名称并单击“Enter”按钮或按 Enter 键后,应用程序将检查是否有其他人已登录其机器上的 PNM 并且在同一本地子网上。如果存在满足这些条件的人,您将看到他们的列表,您可以选择一个名称并单击“Invite”按钮。
如果应用程序未在本地子网上找到任何人,您将收到一个消息框,告知您这种情况很不理想,并建议您稍后重试。但我们假设找到了人,您选择了一个名称并单击了“Invite”按钮。将向被邀请者发送一个邀请,他/她将看到以下消息框:
在最好的情况下,被邀请者足够勇敢,接受邀请并单击“Accept”按钮。如果被邀请者出于他/她自己的原因拒绝了邀请,您将收到一个消息框,告知您这种情况很不理想。然后您可以继续尝试挑战其他人。但我们将认为邀请已被接受。既然如此,应用程序将分配对弈颜色,并通知您和您的对手要使用什么颜色。
单击“OK”按钮后,您可以继续游戏。要选择一个棋子,只需单击它,要移动它,请单击您打算移动棋子的方格。选中的棋子会稍微半透明。
要进行王车易位,请选择您的王并单击您应该移动的方格。相应的车将移动到正确方格。
您和您的对手还可以使用 MessageTxtBox
和“Send”按钮进行一些即时聊天。
如果您和您的对手想开始新游戏,你们中的一个人可以单击 NewGameButton
。当您在出现的对话框中单击“Yes”时,应用程序将重置以开始新游戏。
如果您更愿意寻找另一位对手,请单击 NewPeerButton
(带有两人图像的那个)。
注意事项
P2P Chess 将强制执行国际象棋规则,例如如何移动棋子;例如,只有马可以跳过其他棋子。但有一些规则目前未强制执行,因此需要靠自律和逻辑。未强制执行的规则是:
- 哪个棋子先走(显然是白棋)
- 即使对方未走,您还可以走多少步
- 不将王移动到被将军的位置
吃过路兵目前也不支持,并且您无法撤销一步。尽管您无法撤销一步,但这并不能阻止您将棋子移回初始位置(如果它不是兵,并且您没有吃掉对方的棋子)。
当兵到达最后一行时,它会自动升变为后。
设计和布局
我使用 Expression Design 设计了 P2P Chess 的部分元素,使用 Expression Blend 设计了部分元素。国际象棋的棋子则是我在网上找到的图片。重要的内容元素/控件是 PiecesCanvas
和 LocationCanvas
。两者尺寸相同,并堆叠在一起。
棋子将被添加到 PiecesCanvas
。LocationCanvas
包含 64 个 Rectangle
对象,用于辅助棋子的选择和移动。
注意:彩色方格不是 LocationCanvas
的矩形。
每个国际象棋棋子都是一个 UserControl
。
代码
首先是服务契约。
<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
类中还有其他值得注意的方法,如 FindPeers
和 InvitePeer
。FindPeers
查找当前在同一本地网络上并且在其机器上登录了 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
棋子选择和移动
当用户单击棋盘以选择或移动棋子时,将调用 LocationCanvas
的 MouseLeftButtonDown
事件处理程序。
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 中的点对点编程不太熟悉,请务必查看“参考文献”部分中的链接。
参考文献
- 使用 WCF 和 .NET Framework 3.5 进行点对点编程
- 使用 .NET Framework 3.5 连接
- Essential Windows Communication Foundation,第 12 章。
历史
- 2011 年 6 月 17 日:初次发布。
- 2011 年 6 月 21 日:更新代码以启用王车易位。