H5斗地主游戏应用后台教程(三)—— 业务模块的实现

TIM截图20190320114308.png

1. 消息机制

消息机制属于通知模块的部分,在这里我们来简单介绍一下,在游戏的客户端到底是如何实现游戏的运作的。我们都知道当三个玩家在一个房间内,如果有一个玩家准备游戏,那么必须在其他两个玩家的客户端(网页)更新该玩家的准备状态:

TIM截图20190228121401.png

因此不论是玩家的任何操作还是游戏的开局或者结束,都需要实时地更新其他客户端。我们首先定义Message的抽象类,所有的操作都定义一个继承的Message的类:

1
2
3
4
5
6
7
8
9
10
public abstract class Message {

public abstract WsMessageTypeEnum getType();

public String getDescription() {
WsMessageTypeEnum type = getType();
return type != null ? type.getValue() : "";
}

}

例如某个玩家准备的消息类ReadyGameMessage,这样前端可以根据type值判断出那个事件的发生,并作出相应的更新操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class ReadyGameMessage extends Message {

private String userId;

public ReadyGameMessage(String userId) {
this.userId = userId;
}

@Override
public WsMessageTypeEnum getType() {
return WsMessageTypeEnum.READY_GAME;
}
}

2. 房间管理

房间管理这部分业务逻辑比较简单,因为房间数据并不需要持久化,因此我将房间的数据全部存储到类的成员变量的内存当中:

1
2
3
4
5
6
7
8
9
10
@Component
public class RoomComponent {

// 用户玩家当前所在的房间号映射Map
private Map<String, String> userRoomMap = new ConcurrentHashMap<>();

// 房间号和与该房间所对应的Room对象映射Map
private Map<String, Room> roomMap = new ConcurrentHashMap<>();

}

存储了两个映射Map:roomMap用于建立roomIdRoom对象映射;userRoomMap用于建立用户userId与他目前所在的房间Room对象的映射,这样后台就可以知道当前用户所在的房间,而不需要前端再传roomId

具体的创建房间、加入房间、退出房间的逻辑可以看RoomComponent.java

3. 用户管理

因为该应用并没有复杂的用户业务,因此我使用了QQ互联的第三方登录,用户在前端通过QQ授权之后即在数据库生成了用户的记录。

用户授权成功之后回调地址的详细代码逻辑可以看AuthController.java

4. 游戏回合

4.1 叫牌

在叫牌阶段,一共要做以下的几件事:

  • 设置房间当前局的底分,即玩家所叫的分数;
  • 设置房间当前局的地主玩家,即当前叫牌的玩家;
  • 将三张地主牌添加到地主玩家手中的牌中;
  • 通知其他玩家叫牌结束,并通知地主玩家开始出牌。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void want(User user, int score) {
Room room = roomComponent.getUserRoom(user.getId());
room.setMultiple(score);
User landlordUser = null;
for (Player player : room.getPlayerList()) {
if (player.getUser().getId().equals(user.getId())) {
landlordUser = player.getUser();
room.setStepNum(player.getId());
player.setIdentity(IdentityEnum.LANDLORD);
// 将三张地主牌分配给地主
CardDistribution distribution = room.getDistribution();
player.addCards(distribution.getTopCards());
} else {
player.setIdentity(IdentityEnum.FARMER);
}
}
roomComponent.updateRoom(room);
notifyComponent.sendToAllUserOfRoom(room.getId(), new BidEndMessage()); // 叫牌结束
if (landlordUser != null) { // 通知地主玩家出牌
notifyComponent.sendToUser(landlordUser.getId(), new PleasePlayCardMessage());
}
}

4.2 出牌

出牌阶段逻辑较为复杂,具体的步骤有:

  • 判断牌序列的出牌类型,是否符合规范;
  • 如果上家有出牌,则与上家比较牌序列的大小;
  • 从当前玩家手中的牌移除打出的牌,并通知房间内的房间有玩家出牌;
  • 判断当前玩家手中的牌是否为空,如果为空,则游戏结束;
  • 通知下一玩家出牌。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Override
public RoundResult playCard(User user, List<Card> cardList) {
Room room = roomComponent.getUserRoom(user.getId());
Player player = room.getPlayerByUserId(user.getId());

/* 校验玩家出的牌是否符合斗地主规则规范 */
TypeEnum myType = CardUtil.getCardType(cardList);
if (myType == null) {
throw new ForbiddenException("玩家打出的牌不符合规则");
}
if (room.getPreCards() != null && room.getPrePlayerId() != player.getId()) {
/* 判断该玩家打出的牌是否能比上家出的牌大 */
TypeEnum preType = CardUtil.getCardType(room.getPreCards());
boolean canPlay = GradeComparison.canPlayCards(cardList, myType, room.getPreCards(), preType);
if (!canPlay) {
throw new ForbiddenException("该玩家出的牌管不了上家");
}
}
removeNextPlayerRecentCards(room, player); // 移除下一个玩家最近出的牌
player.setRecentCards(cardList);
// 移除玩家列表中打出的牌
player.removeCards(cardList);
Message message = new PlayCardMessage(user, cardList, myType); // 有玩家出牌通知
notifyComponent.sendToAllUserOfRoom(room.getId(), message);
// 判断出的牌是否是炸弹或者王炸,如果是,则底分加倍
if (myType == TypeEnum.BOMB || myType == TypeEnum.JOKER_BOMB) {
room.doubleMultiple();
}
RoundResult result = null;
if (player.getCards().size() == 0) { // 判断该玩家已经出完牌
result = getResult(room, player);
room.reset();
}
else {
room.setPreCards(cardList);
room.setPrePlayerId(player.getId());
room.incrStep();
// 通知下一个玩家出牌
User nextUser = room.getUserByPlayerId(player.getNextPlayerId());
notifyComponent.sendToUser(nextUser.getId(), new PleasePlayCardMessage());
}
roomComponent.updateRoom(room);
return result;
}

如果你记得,在前面我们创建房间个体POJO类时,有一个stepNum属性,它用来控制玩家的出牌回合。我们可以根据该属性值,来判断当前是否为该玩家出牌的回合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean isPlayerRound(User curUser) {
Room room = roomComponent.getUserRoom(curUser.getId());
if (room.getStatus() == RoomStatusEnum.PREPARING) {
throw new BadRequestException("游戏还未开始");
}
Player player = room.getPlayerByUserId(curUser.getId());
// 当step == -1时,代表叫牌未结束,直接返回false
if (room.getStepNum() == -1) return false;
int remainder = room.getStepNum() % 3;
if (remainder == 0) {
if (player.getId() != 3) return false;
} else {
if (player.getId() != remainder) return false;
}
return true;
}

4.3 不出

不出的逻辑很简单,我们只需要增加stepNum的值并通知下家玩家出牌即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void pass(User user) {
Room room = roomComponent.getUserRoom(user.getId());
Player player = room.getPlayerByUserId(user.getId());

removeNextPlayerRecentCards(room, player);
room.incrStep();
roomComponent.updateRoom(room);
// 通过下一个玩家出牌
User nextUser = room.getUserByPlayerId(player.getNextPlayerId());
notifyComponent.sendToUser(nextUser.getId(), new PleasePlayCardMessage());
notifyComponent.sendToAllUserOfRoom(room.getId(), new PassMessage(user));
}

该章节的业务代码讲解不是很清楚,如果感兴趣的同学可以阅读GameServiceImpl.java

Pushy wechat
欢迎订阅我的微信公众号