# Marelles_直棋对战游戏
### 背景描述
Marelles,中文名九子棋、直棋,是一款双人对战的益智棋盘类游戏,最早可追溯至罗马帝国时期。因其棋盘、棋子布置简单、可玩性高,是一款比较流行的游戏。
### 游戏规则
游戏棋盘由24个格点组成,棋盘如下图所示。两名玩家分别执黑、白,最初各有九个棋子,称为”士兵“。玩家通过移动”士兵“形成”军队“(即连续的三个棋子连成一条水平或竖直的直线)时,可以拿掉对方的一个棋子。当某一方玩家只剩两名“士兵”,或者玩家剩余的士兵不能移动时,则失败。
![img](README.assets/220px-Nine_Men's_Morris_board_with_coordinates,_modified.svg.png)
游戏共分为三个阶段:
1. 棋子放置阶段:棋盘起初为空。两名玩家决定谁先开始,轮流在空闲的格点处放置棋子。在放置过程中,如果有一方形成了连续的水平或竖直的三个棋子(称为“三连”),则可以从对方已放置的棋子中移除一个到游戏外。移除时,必须先移除没有形成一行或一列的棋子。双方轮流放置直到都放置过了九枚棋子。
2. 棋子移动阶段:玩家交替进行棋子移动。棋子移动时只能将自己一方的一枚棋子移动到相邻的空闲的格点处。如果移动棋子使得自己一方形成了一个“三连”,则该玩家需要立即选择对方玩家任意一枚棋子移出游戏。当某方玩家只剩三枚棋子时,进入第三阶段。
3. 棋子跳跃阶段:当某方玩家只剩三枚棋子时,他可以不受“每次移动只能选择相邻空闲位置”的限制,而可以将自己的任意一枚棋子移动到任意空闲格点。当某方玩家只剩两枚棋子时,该玩家失败。
### 程序设计原理
* 游戏界面设计:使用Java Swing组件进行GUI开发,游戏可以分为主页面、棋盘面板、状态面板、游戏进程面板等区域。当点击“玩家信息”、“游戏帮助”等按钮时,将会弹出提示窗口。
* 游戏进程进行:使用stepState模型来保存每个玩家每一步的信息,当落子、吃子时将修改模型内容,当游戏结束时会将模型内容保存至数据库中。
* 玩家信息管理:使用MySQL作为数据库管理玩家信息。当玩家进入棋盘所在页面时,需要输入昵称。如果昵称未被使用过则自动新建用户。
* 多人游戏模式:使用socket作为双人对战的基础。多人对战时MarellesServer默认预留20个连接槽,当玩家请求创建房间时,游戏服务器为其分配一个Handler。当玩家请求连接时,游戏服务器也为其分配一个Handler并将其与目标游戏房间建立关联,以实现数据通信。
### 程序设计目的
综合利用socket、GUI设计、数据库等所学知识,尝试使用设计模式,实现一款双人对战游戏。
### 程序算法说明
#### 游戏行为控制
##### 点击棋盘完成下棋
首先为鼠标注册回调函数。此处进行判断,当点击位置处于允许落子格点周围的圆形区域时,这一点击行为才会判定成为有效点击,并激发trigger函数。
```java
private void register(MouseEvent e, int eventCode){
int x = e.getX();
int y = e.getY();
boolean found = false;
for (int i = 1 ; i <= 7 && !found; i++){
if (Math.abs(i*edge-x) > edge) continue;
for (int j = 1; j <= 7 && !found; j++){
if (Math.abs(j*edge-y) > edge) continue;
if (((x-edge*i)*(x-edge*i)+(y-edge*j)*(y-edge*j)) < (edge*edge/4)){
found = true;
int validation = stepState.localClick(i,j);
if (validation < ConstantDataSet.chessPostionMap.length)
trigger(validation,eventCode);
}
}
}
}
```
trigger函数中首先判断当前是否允许落子,其次判断是否处于自己移除对方棋子的状态。最后根据游戏的不同阶段来判断应该执行何种操作。
```java
private void trigger(int index, int action){
if (wait) {
GameProcess.sendGameInfo("请等待对方落子!");
return;
}
int resultCode;
if (remove && action == CLICK && stepState.getPhase()!= StepState.PHASE6){
resultCode = playBoard.removeChess(index);
if (resultCode == ConstantDataSet.ERROR_EMPTY_CHESS){
GameProcess.sendGameInfo("该位置没有棋子!");
return;
} else if (resultCode == ConstantDataSet.ERROR_SELF_CHESS){
GameProcess.sendGameInfo("不能移除自己的棋子!");
return;
} else if (resultCode == ConstantDataSet.STATE_REMOVE_OK){
GameProcess.sendGameInfo(new int[]{index},"已移除对方一枚棋子<1>");
situation[index] = Chess.NONE;
hasRemoveNum++;
affect = index;
stepState.popStep();
stepState.addStep(from,to,index,Chess.BLACK);
updateState();
return;
}
}
int phase = stepState.getPhase();
if (action == CLICK) {
if (phase == StepState.PHASE1) {
placeBlackChess(index);
} else if (phase == StepState.PHASE2 || phase == StepState.PHASE3) {
moveBlackChess(index);
} else if (phase == StepState.PHASE4 || phase == StepState.PHASE5) {
jumpBlackChess(index);
} else if (phase == StepState.PHASE6) {
gameOver(index);
}
}
}
```
##### 放置阶段
当游戏处于放置阶段时,玩家可以在自己的回合内放置一枚棋子。
```java
private void placeBlackChess(int index){
int resultCode = playBoard.placeChess(index, Chess.BLACK);
if (resultCode == ConstantDataSet.ERROR_OVERLAP){
playBoard.selectChess(index);
} else if (resultCode == ConstantDataSet.STATE_PLACE_OK){
situation[index] = Chess.BLACK;
from = index;
to = index;
GameProcess.sendGameInfo(new int[]{index},"你在<1>放置了一枚棋子");
stepState.addStep(index,index,index,Chess.BLACK);
updateState();
}
}
```
##### 移动阶段
当游戏处于移动阶段时,玩家可以在自己的回合内将自己的一枚棋子移动到相邻的空格处
```java
private void moveBlackChess(int index){
int resultCode = playBoard.checkChessColor(index);
if (resultCode == Chess.BLACK){
playBoard.selectChess(index);
from = index;
} else if (resultCode == Chess.BLACK_SELECTED) {
playBoard.selectChess(index);
from = -1;
} else if (resultCode == Chess.NONE){
if (from != -1){
int ans = playBoard.moveChess(from,index);
switch (ans){
case ConstantDataSet.ERROR_OVERLAP:
GameProcess.sendGameInfo("该位置已有棋子!");
break;
case ConstantDataSet.ERROR_EMPTY_CHESS:
GameProcess.sendGameInfo("棋子已被移动!");
break;
case ConstantDataSet.ERROR_TOO_FAR:
GameProcess.sendGameInfo("只能相邻移动!");
break;
case ConstantDataSet.STATE_UNKOWN:
GameProcess.sendGameInfo("未知错误");
break;
case ConstantDataSet.STATE_MOVE_OK: