地主游戏的功能模块非常多,本小节将介绍各功能模块的实现思路及基本源代码。
23.2.1游戏服务器的启动
在server包下有一个Main类,这个类中包含main()方法,main()方法中包含启动游戏服务器的语句,游戏服务器的启动必须先于客户端的启动,否则游戏无法运行。Main类中main()方法的代码如下:
new MainServer();
可以看出:main()方法中仅是创建了一个MainServer类对象,而MainServer类对象会在其构造方法中启动一个ServerSocket,同时启动线程处理客户端与服务器的对话,实现这个过程的代码如下:
//1.创建服务器端socket
ServerSocket serverSocket=new ServerSocket(8888);
while(true)
{
//2.接收客户端的socket
Socket socket= serverSocket.accept();
//3.开启线程 处理客户端的socket
AcceptThread acceptThread=new AcceptThread(socket);
acceptThread.start();
}
23.2.2登录窗口和主界面窗口的启动
当启动了服务器之后,就可以启动用户登录窗口。在client.view包下有一个Main类,它包含main()方法,当main()方法执行后就会创建出玩家登录窗口对象。由于斗地主游戏必须够3个玩家才能开始运行,因此在本案例中,main()方法直接开启3个登录窗口以保证游戏能立刻进入运行状态,但读者必须清楚:在真实的游戏中一次只能开启一个登录窗口。Main类的main()方法代码如下:
new LoginFrame();//打开第一个玩家的登录窗口
new LoginFrame();//打开第二个玩家的登录窗口
new LoginFrame();//打开第三个玩家的登录窗口
从以上代码可以看出:main()方法只是打开了登录窗口,登录窗口的界面如图23-1所示。当玩家在登录窗口中任意输入一个昵称并单击“登录”按钮后,就会进入斗地主游戏主界面窗口。
之所以在单击“登录”按钮后会进入斗地主游戏主界面窗口,是因为“登录”按钮的监听器在监听到事件后会打开主界面窗口,并且用Socket与服务器建立连接。“登录”按钮监听器的事件响应程序如下:
//点击登录
//1.获得用户名
String uname= txtUserName.getText();
//2.创建一个socket链接服务器端
try {
Socket socket=new Socket("127.0.0.1",8888);
//3.跳转到主窗口
new MainFrame(uname,socket);
dispose();//关闭当前窗口
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
23.2.3接收客户端上线消息
每个玩家登录到斗地主游戏后,客户端要主动向服务器发送消息以通知服务器自己已经上线,这样服务器就能知道目前有哪些玩家上线,当有3个客户端连接到服务器后游戏参与者数量正好凑够,此时服务器将开启游戏。
在23.2.2小节曾讲到:玩家在登录之后会创建出一个游戏主界面窗体,也就是MainFrame类的对象,而MainFrame对象则会在自己的构造方法中启动一个线程,并用这个线程向服务器端发送消息,启动线程的代码如下:
// 启动发消息的线程
sendThread = new SendThread(socket, uname);
sendThread.start();
线程启动后发送登录消息的代码如下:
DataOutputStream dataOutputStream;
try {
dataOutputStream = new DataOutputStream(socket.getOutputStream());
while(true)
{
if(isRun==false){
break;
}
//如果消息不为null
if(msg!=null){
System.out.println("消息在发送中:"+msg);
//发送消息
dataOutputStream.writeUTF(msg);
//消息发送完毕 消息内容清空
msg=null;
}
Thread.sleep(50); //暂停 等待新消息进来
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
服务器端也要相应的接收客户端发来的消息,接收消息的任务也是由一个线程负责完成的,线程接收消息后就知道有一个玩家已经上线,此时它要创建一个玩家对象并进行计数,接收消息及创建玩家对象的代码如下:
Player player=new Player(index++,msg);
player.setSocket(socket);
//存入玩家列表
players.add(player);
System.out.println(msg+"上线了");
System.out.println("当前上线人数:"+players.size());
23.2.4发牌
当有3个玩家上线后,系统会自动启动发牌程序,发牌是由服务器完成的,因此在表示服务器的MainServer类中专门定义了一个发牌方法deal(),该方法的实现过程如下:
public void deal()
{
//发给三个玩家
for(int i=0;i<allPokers.size();i++)
{
//最后三张留给地主牌
if(i>=51)
{
lordPokers.add(allPokers.get(i));
}
else {
//依次分发给三个玩家
if(i%3==0)
players.get(0).getPokers().add(allPokers.get(i));
else if(i%3==1)
players.get(1).getPokers().add(allPokers.get(i));
else
players.get(2).getPokers().add(allPokers.get(i));
}
}
//将玩家的信息发送到客户端
for(int i=0;i<players.size();i++)
{
try {
DataOutputStream dataOutputStream= new DataOutputStream(players.get(i).getSocket().getOutputStream());
String jsonString=JSON.toJSONString(players);
System.out.println(jsonString);
dataOutputStream.writeUTF(jsonString);
} catch (IOException e) {
e.printStackTrace();
}
}
}
23.2.5解析消息
服务器向客户端发牌的操作过程中,所有的扑克牌的组合都是以JSON字符串的形式表示的,而客户端接收到所发牌之后必须把这个JSON字符串解析为一个JSON数组,而这个JSON数组则表示收到的牌,紧接着要把JSON数组传递给玩家对象,玩家对象在接收到这一组牌后要把这些牌在放到一个ArrayList对象中,表示玩家已经拿到了所发的牌,这个过程的实现代码写在客户端接收消息的线程ReceiveThread中,其实现过程如下:
String jsonString= dataInputStream.readUTF();
java.util.List<Player> players=new ArrayList<Player>();
//System.out.println(jsonString);
//解析json字符串
//将json字符串转换为json数组
JSONArray playerJsonArray = JSONArray.parseArray(jsonString);
for(int i=0;i<playerJsonArray.size();i++)
{
//获得当个json对象--> 玩家对象
JSONObject playerJson=(JSONObject) playerJsonArray.get(i);
int id=playerJson.getInteger("id");
String name=playerJson.getString("name");
//存放扑克列表
java.util.List<Poker> pokers=new ArrayList<Poker>();
JSONArray pokerJsonArray= playerJson.getJSONArray("pokers");
for(int j=0;j<pokerJsonArray.size();j++)
{
// 每循环一次 获得一个扑克对象
JSONObject pokerJSon= (JSONObject) pokerJsonArray.get(j);
int pid=pokerJSon.getInteger("id");
String pname=pokerJSon.getString("name");
int num=pokerJSon.getInteger("num");
Poker poker=new Poker(pid,pname,num);
pokers.add(poker);
}
Player player=new Player(id,name,pokers);
players.add(player);
23.2.6显示接收到的牌
玩家在收到所发的牌之后要把这些牌显示出来,这个操作是由定义在MainFrame类的showAllPlayersInfo()方法完成的,这个方法显示牌的核心操作是把表示牌的对象Poker与每张牌的图片对应起来并把这些牌的图片显示到窗体的上,showAllPlayersInfo()方法的实现过程如下:
public void showAllPlayersInfo(java.util.List<Player> players) {
// 1.显示三个玩家的名称
// 2.显示当前玩家的扑克列表
for (int i = 0; i < players.size(); i++) {
if (players.get(i).getName().equals(uname)) {
currentPlayer = players.get(i);
}
}
java.util.List<Poker> pokers = currentPlayer.getPokers();
for (int i = 0; i < pokers.size(); i++) {
// 创建扑克标签
Poker poker = pokers.get(i);
PokerLabel pokerLabel = new PokerLabel(poker.getId(),poker.getName(), poker.getNum());
pokerLabel.turnUp(); // 显示正面图
// 添加到面板中
this.myPanel.add(pokerLabel);
this.pokerLabels.add(pokerLabel);
// 动态的显示出来
this.myPanel.setComponentZOrder(pokerLabel, 0);
// 一张一张的显示出来
GameUtil.move(pokerLabel, 300 + 30 * i, 450);
}
// 对扑克列表排序
Collections.sort(pokerLabels);
// 重新移动位置
for (int i = 0; i < pokerLabels.size(); i++) {
this.myPanel.setComponentZOrder(pokerLabels.get(i), 0);
GameUtil.move(pokerLabels.get(i), 300 + 30 * i, 450);
}
if (currentPlayer.getId() == 0) {
getLord(); // 抢地主
}
}
23.2.7按大小排列牌
玩家在接收到发的牌时,这些牌都是随机排列的,为了方便玩家出牌,必须把这些牌按大小排列一遍。排列牌是调用Collections类的sort()方法完成的,为sort()方法所传递的参数是PokerLabel对象,根据第11章所学的知识可知,如果一个类的对象如果能够被排序,这个类必须实现Comparable接口并实现其compareTo()方法,以下是PokerLabel实现compareTo()方法的过程。
public int compareTo(Object arg0) {
PokerLabel pokerLabel=(PokerLabel)arg0;
if(this.num>pokerLabel.num)
return 1;
else if(this.num<pokerLabel.num)
return -1;
else
return 0;
}
从以上代码可以看出:两张牌进行比较时,所比较的依据就是PokerLabel对象的num属性,而这个num属性就代表了牌面的点数。
23.2.8抢地主
各玩家拿到牌后,根据牌的好坏可以选择是否抢地主。抢地主操作是由MainFrame类所定义的getLord()方法完成,其最核心代码是显示倒计时并箭头玩家的鼠标操作并根据玩家鼠标所单击是“抢地主”还是“不抢”。getLord()方法的实现过程如下:
public void getLord() {
// 显示抢地主的按钮 和 定时器按钮
lordLabel1 = new JLabel();
lordLabel1.setBounds(330, 400, 104, 46);
lordLabel1.setIcon(new ImageIcon("images/bg/jiaodizhu.png"));
lordLabel1.addMouseListener(new MyMouseEvent());//①
this.myPanel.add(lordLabel1);
lordLabel2 = new JLabel();
lordLabel2.setBounds(440, 400, 104, 46);
lordLabel2.setIcon(new ImageIcon("images/bg/bujiao.png"));
lordLabel2.addMouseListener(new MyMouseEvent());//②
this.myPanel.add(lordLabel2);
//显示定时器的图标
this.timeLabel.setVisible(true);
this.setVisible(true);
// 重绘
this.repaint();
// 启动计时器的线程
countThread = new CountThread(10, this);
countThread.start();
}
在以上代码中,lordLabel1表示“抢地主”的标签,而lordLabel2则表示“不抢”,这两个标签在被单击时也要做出响应,因此需要给这两个标签添加监听器。在MainFrame类中定义了一个鼠标事件监听器,它负责监听所有的鼠标单击操作,因此程序需要在进行处理时判断玩家单击的是哪一个标签,其中负责处理玩家单击“抢地主”标签的代码如下:
if (event.getSource().equals(lordLabel1)) {
// 停止计时器
countThread.setRun(false);
isLord = true;
// 设置抢地主的按钮不可见
lordLabel1.setVisible(false);
lordLabel2.setVisible(false);
timeLabel.setVisible(false);
}
而处理“不抢”标签被单击的代码如下:
// 点击的不抢
if (event.getSource().equals(lordLabel2)) {
// 停止计时器
countThread.setRun(false);
isLord = false;
lordLabel1.setVisible(false);
lordLabel2.setVisible(false);
timeLabel.setVisible(false);
}
23.2.9显示出牌和不出牌标签并开始倒计时
抢地主步骤结束后,需要开始出牌,因此游戏主界面上必须显示出“出牌”和“不出牌”标签以及倒计时标签。显示这些标签的操作由MainFrame类的showChuPaiJabel()方法完成,其实现过程如下:
//显示出牌的标签
public void showChuPaiJabel()
{
if(prevPlayerid==currentPlayer.getId())
{
//从窗口上移除之前的出牌的列表
for(int i=0;i<showOutPokerLabels.size();i++)
{
myPanel.remove(showOutPokerLabels.get(i));
}
//清空之前出牌的列表
showOutPokerLabels.clear();
}
// 显示出牌和不出牌的按钮 和 定时器按钮
chupaiJLabel.setVisible(true);
buchuJLabel.setVisible(true);
timeLabel.setVisible(true);
this.repaint();
chuPaiThread=new ChuPaiThread(30, this);//①倒计时
chuPaiThread.start();
}
从showChuPaiJabel()方法的语句①可以看出:出来要把各种标签显示出来外,还要开始倒计时操作,这个操作是由chuPaiThread线程完成的,其倒计时代码如下:
while(time>=0 && isRun){
mainFrame.timeLabel.setText(time+"");
time--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
23.2.10分发出牌消息
任意一个玩家完成出牌操作后,他所出的牌必须被自己以及另外两个玩家看到,因此必须向服务器发送出牌消息,并由服务器在把这些消息分发给其他两位玩家。出牌消息是由SendThread线程完成的,实际上整个游戏中所有消息都是由这个线程完成的。SendThread线程发送消息的代码写在它的run()方法中,其实现过程如下:
public void run() {
DataOutputStream dataOutputStream;
try {
dataOutputStream = new DataOutputStream(socket.getOutputStream());
while (true) {
if (isRun == false) {
break;
}
//如果消息不为null
if (msg != null) {
System.out.println("消息在发送中:" + msg);
//发送消息
dataOutputStream.writeUTF(msg);
//消息发送完毕 消息内容清空
msg = null;
}
Thread.sleep(50); //暂停 等待新消息进来
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
从这段代码可以看出:SendThread线程中有一个无限循环,这个循环一直在不断等待这发送消息的任务,一旦要发送的消息不为空则立刻发送消息,因此如果想让这个线程发送消息,只需要调用SendThread类的setMsg()方法把消息传递给SendThread即可,而ChuPaiThread线程的run()方法执行以下代码完成出牌操作以及把消息传递给SendThread:
message=new Message(4,mainFrame.currentPlayer.getId(),"出牌",
changePokerLableToPoker(mainFrame.selectedPokerLabels));
//转换为json 交给 sendThread发送到服务器去
String msg=JSON.toJSONString(message);
mainFrame.sendThread.setMsg(msg);
//将当前发送出去的扑克牌 从扑克牌列表中移除
mainFrame.removeOutPokerFromPokerList();
//如果扑克列表的数量为0 代表赢了
if(mainFrame.pokerLabels.size()==0)
{
message=new Message(5, mainFrame.currentPlayer.getId(), "游戏结束", null);
msg=JSON.toJSONString(message);
try {
Thread.sleep(100);
mainFrame.sendThread.setMsg(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
23.2.11判断牌型和出牌是否符合规则
如果轮到当前玩家出牌,那么玩家不能随意出,必须符合某种牌型。在斗地主游戏中,所有牌型都已经被定义到PokerType枚举中,而判断牌型的操作由PokerRule类所定义的checkPokerType()方法完成,其源代码如下:
//判断牌型
public static PokerType checkPokerType(List<PokerLabel> list){
Collections.sort(list);
int count=list.size();
if(count==1){
//单张
return PokerType.p_1;
}else if(count==2){
//对子
if(isSame(list, count)){
return PokerType.p_2;
}
//王炸
if(isWangZha(list)){
return PokerType.p_2w;
}
return PokerType.p_error;
}else if(count==3){
//三个头
if(isSame(list, count)){
return PokerType.p_3;
}
return PokerType.p_error;
}else if(count==4){
//炸弹
if(isSame(list, count)){
return PokerType.p_4;
}
//三带一
if(isSanDaiYi(list)){
return PokerType.p_31;
}
return PokerType.p_error;
}else if(count>=5){
//顺子
if(isShunZi(list)){
return PokerType.p_n;
}else if(isSanDaiYiDui(list)){
//三代一对
return PokerType.p_32;
}else if(isLianDui(list)){
//连对
return PokerType.p_1122;
}else if(isFeiJI(list)){
//双飞or双飞双带
return PokerType.p_111222;
}else if(isFeiJIDaiChiBang1(list)){
//双飞or双飞双带
return PokerType.p_11122234;
}
else if(isFeiJIDaiChiBang2(list)){
//双飞or双飞双带
return PokerType.p_1112223344;
}
}
return PokerType.p_error;
}
玩家所出的牌型如果正确,还必须看是否符合出牌规则,如果下家的牌不大于上家则认为不符合出牌规则,这个判断操作由PokerRule类的legal()静态方法完成,其实现过程如下:
//判断是否符合出牌规则,即根据上家牌面判断本次出牌是否合规
public static boolean legal(List<PokerLabel> prevList, List<PokerLabel> currentList){
// 首先判断牌型是不是一样
PokerType paiXing = checkPokerType(prevList);
if (paiXing.equals(checkPokerType(currentList))) {
// 根据牌型来判断大小
if (PokerType.p_1.equals(paiXing)) {
// 单张
if (compareLast(prevList, currentList)) {
return true;
}
return false;
} else if (PokerType.p_2w.equals(paiXing)) {
// 王炸
return false;
} else if (PokerType.p_2.equals(paiXing)) {
// 对子
if (compareLast(prevList, currentList)) {
return true;
}
return false;
} else if (PokerType.p_3.equals(paiXing)) {
// 三张
if (compareLast(prevList, currentList)) {
return true;
}
return false;
} else if (PokerType.p_31.equals(paiXing)) {
// 三带一
if (compareLast(prevList, currentList)) {
return true;
}
return false;
} else if (PokerType.p_32.equals(paiXing)) {
// 三带一对
if (compare(prevList, currentList)) {
return true;
}
return false;
} else if (PokerType.p_4.equals(paiXing)) {
// 炸弹
if (compareLast(prevList, currentList)) {
return true;
}
return false;
} else if (PokerType.p_n.equals(paiXing)) {
// 顺子
if (compareLast(prevList, currentList)) {
return true;
}
return false;
} else if (PokerType.p_1122.equals(paiXing)) {
// 连对
if (compareLast(prevList, currentList)) {
return true;
}
return false;
} else if (PokerType.p_111222.equals(paiXing)) {
// 双飞
if (compare(prevList, currentList)) {
return true;
}
return false;
}
else if (PokerType.p_11122234.equals(paiXing)) {
// 飞机带翅膀(单张)
if (compare(prevList, currentList)) {
return true;
}
return false;
}
else if (PokerType.p_1112223344.equals(paiXing)) {
// 飞机带翅膀(对子)
if (compare(prevList, currentList)) {
return true;
}
return false;
}
}else if(currentList.size()==2){
//判断是不是王炸
if(isWangZha(currentList)){
return true;
}
return false;
} else if(currentList.size()==4){
//判断是不是炸弹
if(isSame(currentList, 4)){
return true;
}
return false;
}
return false;
}
23.2.12判断玩家输赢
判断玩家输赢其实很简单,只要某个玩家手中的牌数量等于0就认定该玩家已经赢了,如果已经赢了,则还要向服务器发送游戏结束的消息。完成判断操和发送消息的操作由ChuPaiThread类的run()方法完成,由之前的介绍可知:ChuPaiThread类的run()方法会完成很多操作,其中判断玩家是否已经赢得牌局以及发送消息的代码如下:
if(mainFrame.pokerLabels.size()==0)
{
//产生游戏结束的消息
message=new Message(5, mainFrame.currentPlayer.getId(), "游戏结束", null);
msg=JSON.toJSONString(message);
try {
Thread.sleep(100);
//发送消息
mainFrame.sendThread.setMsg(msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。