一、学习目标
参考可以通过因特网对弈的“吃棋子”游戏程序,按以下要求进行改编,要求如下:
1)将游戏改为双方对弈,而不是系统自动下棋;
2)修改游戏规则,如五子棋的游戏规则;
3)同步改异步;
二、设计思路
1)放置棋子。先不考虑轮流下子的情况和输赢的问题,重新把服务器端和客户端的发送信息和处理收到的信息处理一次。
客户端:修改玩家对鼠标点击的事件处理,设置颜色color变量属性表示玩家下的棋子的颜色,和side变量相同,下的棋子的颜色会相同,在下棋的地方会出现玩家相应颜色的棋子。
private int color; //edit this.color = side;
/// <summary>在pictureBox1中按下鼠标触发的事件</summary>
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
int x = e.X / 20;
int y = e.Y / 20;
if (!(x < 1 || x > 15 || y < 1 || y > 15))
{
if (grid[x - 1, y - 1] == DotColor.None)//edit
{
//int color = (int)grid[x - 1, y - 1];
//发送格式:setDot,桌号,座位号,行,列,颜色
service.SendToServer(string.Format(
"SetDot,{0},{1},{2},{3},{4}", tableIndex, side, x - 1, y - 1, this.color));//edit
}
}
}
服务器端,去掉课本例子中根据事件间隔随机交替的发送两种颜色的棋子那部分功能。每当一方玩家下棋后,给同桌的玩家两人发送新的棋子的位置。
public int turn; //轮流玩,0为黑,1为白
/// <summary>发送产生的棋子信息</summary>
/// <param name="i">指定棋盘的第几行</param>
/// <param name="j">指定棋盘的第几列</param>
/// <param name="dotColor">棋子颜色</param>
public void SetDot(int i, int j, int dotColor)//edit private to public
{
if (dotColor == turn)//edit
{
//向两个用户发送产生的棋子信息,并判断是否有相邻棋子
//发送格式:SetDot,行,列,颜色
grid[i, j] = dotColor;
service.SendToBoth(this, string.Format("SetDot,{0},{1},{2}", i, j, dotColor));
if (win(dotColor))//edit
{
ShowWin(dotColor);
}
turn = (turn + 1)%2;
}
}
2)解决输赢判断的问题。五子棋游戏的规则为是先有相同颜色的五子相连的那方的玩家赢,棋子可以按照横竖斜三个方向,但是考虑边界的情况。
修改游戏胜利的条件。每当一个玩家下子完毕后就判断。GameTable类中更改胜利条件
//是否胜利。
private bool win(int dotColor)//edit
{
int num = 15;
int checkPoint = 0;
int i = 0;
int j = 0;
//横着检查。
for (int x = 0; x < num * num; x++)
{
int consecutive = 0;
checkPoint = x;
for (int y = 0; y < 5; y++)
{
i = (checkPoint + y) % num;
j = (int)checkPoint / num;
if (checkPoint > (num * num - 1) || i > num - 1)
break;
if (grid[i, j] == dotColor)
{
consecutive++;
}
//checkPoint++;
}
if (consecutive == 5)
return true;
}
//竖着检查
for (int x = 0; x < num * num; x++)
{
int consecutive = 0;
checkPoint = x;
for (int y = 0; y < 5; y++)
{
i = (checkPoint + y) % num;
j = (int)checkPoint / num;
if (checkPoint > (num * num - 1) || i > num - 1)
break;
if (grid[j, i] == dotColor)
{
consecutive++;
}
//checkPoint++;
}
if (consecutive == 5)
return true;
}
//正斜
for (int x = 0; x < num * num; x++)
{
int consecutive = 0;
checkPoint = x;
for (int y = 0; y < 5; y++)
{
i = checkPoint % num + y;
j = ((int)checkPoint / num) + y;
if (i > num - 1 || j > num - 1)
break;
if (grid[i, j] == dotColor)
{
consecutive++;
}
}
if (consecutive == 5)
return true;
}
//反斜
for (int x = 0; x < num * num; x++)
{
int consecutive = 0;
checkPoint = x;
for (int y = 0; y < 5; y++)
{
i = checkPoint % num - y;
j = (int)checkPoint / num + y;
if (i > num - 1 || i < 0 || j > num - 1 || j < 0)
break;
if (grid[i, j] == dotColor)
{
consecutive++;
}
}
if (consecutive == 5)
return true;
}
return false;
}
胜利后的提示信息改为
case "win":
//格式:Win,相邻棋子的颜色,黑方成绩,白方成绩
string winner = "";
if ((DotColor)int.Parse(splitString[1]) == DotColor.Black)
{
winner = "黑方出现五子相连,黑方胜利!";//edit
}
else
{
winner = "白方出现五子相连,白方胜利!";//edit
}
formPlaying.ShowMessage(winner);
formPlaying.Restart(winner);
break;
3)双方轮流的下棋子。在双方都开始后游戏正式开始。
在前面代码中已经添加了turn变量,控制下棋方玩家。初始化的时候是-1,表示如果双方没有开始游戏,都不能下子。FormServer类收到的message处理start时,更改条件,都开始后,把turn改为0,表示黑子先下,符合五子棋规则。关键代码如下:
if (gameTable[tableIndex].gamePlayer[anotherSide].started == true)
{
gameTable[tableIndex].ResetGrid();
//gameTable[tableIndex].StartTimer();
gameTable[tableIndex].Turn = 0;
}
GameTable中,只有当轮到自己时,即到自己的颜色时才能下子。
public int Turn{
get{return turn;}
set { turn = value; }
}
下子后,变换下子方。
turn = (turn + 1)%2;
4)同步改异步
程序在调用Begin…后,可以在调用线程上继续执行其下面的指令,同时异步操作在另一个线程上执行。Begin…方法开始异步操作,并返回一个实现 IAsyncResult接口的对象。IAsyncResult对象存储有关异步操作的状态信息。这些信息包括:IAsyncState:可选的特定的对象,包含异步操作需要的信息。syncWaitHandle:用于在异步操作完成前阻止程序执行。CompletedSynchronously:指示异步操作是否在用于调用Begin…的线程上完成,而不是在单独的ThreadPool线程上完成。IsCompleted:一个布尔值,指示异步操作是否已完成。
服务器端:监听客户端和接收客户端消息类似,使用异步方式调用同步方法,通过轮询方式检查异步调用是否完成,使用EndInvoke结束异步调用,EndInvoke方法用于检索异步调用的结果,并结束异步调用。调用BeginInvoke之后,随时可以调用该方法。如果异步调用尚未完成,则EndInvoke会一直阻止调用线程,直到异步调用完成。
private delegate void ListenClientDelegate(out TcpClient client);
/// <summary>接受挂起的客户端连接请求</summary>
private void ListenClient(out TcpClient newClient)
{
try
{
newClient = myListener.AcceptTcpClient();
}
catch
{
newClient = null;
}
}
/// <summary>监听客户端请求</summary>
private void ListenClientConnect()
{
TcpClient newClient = null;
while (true)
{
ListenClientDelegate d = new ListenClientDelegate(ListenClient);
IAsyncResult result = d.BeginInvoke(out newClient, null, null);
//使用轮询方式来判断异步操作是否完成
while (result.IsCompleted == false)
{
if (isExit)
{
break;
}
Thread.Sleep(250);
}
//获取Begin方法的返回值和所有输入/输出参数
d.EndInvoke(out newClient, result);
if (newClient != null)
{
//每接受一个客户端连接,就创建一个对应的线程循环接收该客户端发来的信息
User user = new User(newClient);
Thread threadReceive = new Thread(ReceiveData);
threadReceive.Start(user);
userList.Add(user);
service.AddItem(string.Format("[{0}]进入", newClient.Client.RemoteEndPoint));
service.AddItem(string.Format("当前连接用户数:{0}", userList.Count));
}
else
{
break;
}
}
}
服务器端发送消息也类似上面的方法:
/// <summary>异步发送message给user</summary>
public void AsyncSendToOne(User user, string message)
{
SendToOneDelegate d = new SendToOneDelegate(SendToOne);
IAsyncResult result = d.BeginInvoke(user, message, null, null);
while (result.IsCompleted == false)
{
Thread.Sleep(250);
}
d.EndInvoke(result);
}
public delegate void SendToOneDelegate(User user, string str);
/// <summary>向某个客户端发送信息</summary>
/// <param name="gameTable">指定客户</param>
/// <param name="gameTable">信息</param>
public void SendToOne(User user, string str)
{
try
{
user.sw.WriteLine(str);
user.sw.Flush();
AddItem(string.Format("向{0}发送{1}", user.userName, str));
}
catch
{
AddItem(string.Format("向{0}发送信息失败", user.userName));
}
}
客户端:用BackgroundWork组件实现异步编程功能
BackgroundWorker connectWork = new BackgroundWorker();
connectWork.DoWork += new DoWorkEventHandler(connectWork_DoWork);
connectWork.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(connectWork_RunWorkerCompleted);
/// <summary>异步方式与服务器进行连接</summary>
void connectWork_DoWork(object sender, DoWorkEventArgs e)
{
client = new TcpClient();
//此处为方便演示,实际使用时要将Dns.GetHostName()改为服务器域名
IAsyncResult result = client.BeginConnect(Dns.GetHostName(), 51888, null, null);
while (result.IsCompleted == false)
{
Thread.Sleep(100);
}
try
{
client.EndConnect(result);
e.Result = "success";
}
catch (Exception ex)
{
e.Result = ex.Message;
return;
}
}
/// <summary>异步方式与服务器完成连接操作后的处理</summary>
void connectWork_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Result.ToString() == "success")
{
groupBox1.Visible = true;
textBoxLocal.Text = client.Client.LocalEndPoint.ToString();
textBoxServer.Text = client.Client.RemoteEndPoint.ToString();
buttonConnect.Enabled = false;
//获取网络流
NetworkStream netStream = client.GetStream();
//将网络流作为二进制读写对象
sr = new StreamReader(netStream, System.Text.Encoding.UTF8);
sw = new StreamWriter(netStream, System.Text.Encoding.UTF8);
service = new Service(listBox1, sw);
service.SendToServer("Login," + textBoxName.Text.Trim());
Thread threadReceive = new Thread(new ThreadStart(ReceiveData));
threadReceive.IsBackground = true;
threadReceive.Start();
}
else
{
MessageBox.Show("与服务器连接失败 "+e.Result, "",
MessageBoxButtons.OK, MessageBoxIcon.Information);
buttonConnect.Enabled = true;
}
}
/// <summary>【登录】按钮的Click事件</summary>
private void buttonConnect_Click(object sender, EventArgs e)
{
buttonConnect.Enabled = false;
connectWork.RunWorkerAsync();
}
接受消息的方法跟服务器端相同,不重复描述。
三、个人总结
由于这个实验是参考书上已有的例子修改的,所以没有出现之前无从下手的情况。在实验的过程中,遇到了一些问题。首先,是轮流下子的问题,不知道怎么通知对方。后来的解决方案是当服务器端发送下子坐标的时候同时更新同一桌玩家双方的棋盘布局,只需要在特定棋盘上用turn变量断定下棋的玩家。第二是边界的判断比较复杂,我采取了最笨的方法,每次下子的时候都是从新判断整个棋盘的横竖正斜反斜,这样程序的运行效率降低了。第三是同步改异步的问题,。改成异步以后,一直在想,在Form中生成了一个Service类,那么终止条件是什么。请教了同学之后,发现其实在Form退出的时候,service就自动消失了。通过本次试验,我感觉受益匪浅。