目录
引言:本文写的项目为五子棋+扫雷属性的java控制台程序
项目简介
本项目与常规的五子棋的区别点在于增加了类似扫雷的元素, 在生成棋盘时会有隐藏的特殊位置(显示时与空棋盘相同), 当player在此处落子时将会产生不同的效果。
项目架构放于本文末尾。
例如:
死格,落子失败 炸弹,落子失败,并清空周围3*3的格子 反棋,落子变为对方颜色 己方反棋,落子后可以将一处对手棋子变为己方棋子 归零,将场上一个位置置0 幸运,还可以再下一次棋子
并增加ai对战选项, 利用算法计算ai落子位置, 不过ai并不会触发特殊效果
项目巧妙点
-
本项目为多人(7人)合作完成,自10月13日立项起至10月26日,共14天完成, 代码总行数1683行,若有考虑不周的地方还望见谅。采用写架构分方法的方式完成, 分包开发。代码编码时间3天,之前的时间完成可行性分析文档,项目需求文档,原型图绘制,框架编写。在写框架时将各个小的功能拆分出来降低程序耦合度, 也让组内每个人都不会出现完成的函数少但是十分难的情况出现。每个人均完成3-4个方法,最后每日会议进行整合代码,指出不足,和第二天完成事项。
-
编码中出现不同情况时,将其单拎出来作为一个新的方法去编写如:
switch (playerInput) { case 1: // 玩家对战 isAi = false; gameView(); break; case 2: // 人机对战 isAi = true; gameView(); break; case 3: // 游戏说明 gameDescription(); break; case 4: // 设置 gameSetting(); break; case 5: // 对局记录 showRecord(); break; case 6: // 退出游戏 System.out.println("正在退出游戏..."); scanner.close(); mv_flag = false; break; default: System.out.println("无效的输入,请重新输入。"); break; }
3. 编写纯文本方法时,将用到的字符串放到全局变量中,增加代码的可读性,例如:
/* 负责人: 功能:打印游戏主界面 例:" 五子棋 " " (1)玩家对战 " " (2)人机对战 " 详细请看原型图 使用的常量mainView_在 全局变量GlobalVariable类 下 参数:void 返回值:void */ public static void mainView(){ System.out.println(mainView_); }
4. 在控制台打印棋盘时,不只用一种颜色去打印,蓝方棋子用蓝色打印,红方棋子用红色打印,例如:
System.out.print("\u001B[34m1\u001B[0m\t"); System.out.print("\u001B[31m2\u001B[0m\t");
效果图:
5. 项目中使用音乐相关技术:
(1) 在程序运行过程中播放bgm
(2)在落子或触发特殊效果时播放落子音效(2种)和特殊音效(2种)
并提供了相应的方法可以在程序运行过程中更改音量大小
6. 在棋盘初始化时,利用random函数在随机生成的棋盘位置赋予特殊效果, 并用while循环, 保证每种效果的数量是给定的默认值。例如:
while(num3>0){ int a = r.nextInt(14); int b = r.nextInt(14); if (map[a][b] == 0) { map[a][b] = 3; num3--; } }
7. 落子后调用playerMove函数进行落子,根据不同的特殊效果去编写函数,每个特殊效果都单独写一个函数
public static void playerMove(int x,int y){ if (x == -2) { // 投降 if (isAi) {//ai对战 if (flag % 2 == 0) {//玩家投降 } } else {//玩家对战 if (flag % 2 == 0) {//蓝方投降 } else {//红方投降 } } } else if (x == -3) { // 申请平局 if (!isAi) { // 玩家对战 System.out.println("是否同意平局(Y/N)"); } else { // 人机对战,直接平局 } } else if (map[x][y] == 0) { // 落子位置为空,直接赋值 } else if (map[x][y] == 1 || map[x][y] == 2) { // 落子位置已有棋子 } else if(map[x][y] == -1){//死格 } else { switch (map[x][y]) { case 3: is3(x, y); break; case 4: is4(x, y); break; case 5: is5(x, y); break; case 6: is6(x, y); break; case 7: is7(x, y); break; case 8: is8(x, y); break; } } System.out.println();//使输出格式美观 }
8. 关于输入合法性的编码
(1)输入越界: 这种好解决,在调用函数之前先判断是否在数组范围之内, 如果越界则进入循环直到用户输入合法
(2)输入类型错误: 例如要接受int类型的数据, 使用scanner.nextInt(), 此时如果玩家输入字符例如&%等,会导致程序崩溃退出, 选择单独写一个inputChess函数用于完善此项功能, 当出现这类错误时将异常抛出并循环提示用户输入,例如:
public static void inputChess(){ boolean validInput = false; while (!validInput) { try { System.out.print("请输入坐标: "); user_x = scanner.nextInt(); user_y = scanner.nextInt(); /** 可能会抛出 InputMismatchException*/ validInput = true; // 输入成功,更新标志位 } catch (InputMismatchException e) { System.out.println("输入错误"); scanner.nextLine(); // 清除输入缓冲区的当前行 } } }
项目问题
框架写的不好,很乱。
显示提示信息也要放在view层 而且项目分包应该按照com.这种的来命名包名,没有遵循规范
项目结构
项目选择分包开发的方式
把所用到的非java文件放置在resource文件夹下来使用
分为mainProgram, ioStream, music, service, view五层
mainProgram: 项目的入口, 内有一个调用main函数的类 和保存全局变量的类
ioStream: 负责完成IO流读取存储文件, 本项目中用于保存对局记录到record.txt下
music: 负责完成所有播放音乐相关的功能
service: 实现修改全局变量的功能
view: 负责实现打印各种界面
mainProgram
GlobalVariable中保存所有要用到的全局变量,将全局变量用static修饰,以便调用
将程序中要用的长段字符串或相关设置抽离出来, 降低程序的耦合度
/** * 本类是程序的总入口 * @version */ public class Program { public static void main(String[] args) { menuView(); } } /** * 本类用于存放本项目所需的所有全局变量 */ public class GlobalVariable { //---------------------------数据设计----------------------------------- /** * 棋盘: * map[i][j]表示坐标(i,j)的值 * 0表示未落子 * 1表示蓝子 * 2表示红子 * -1表示已被翻出的死格,打印为 # * 3表示 未被翻出的死格,落子失败并提示 * 4表示炸弹,落子失败,并清空周围3*3的格子 * 5表示反棋,落子变为对方颜色 * 6己方反棋,先判断这次落子是否胜利,胜利则直接结束,未胜利则再次接收用户输入 * 将场上一个棋子变为自己颜色 * 7表示归零,将场上一个位置置0 * 8表示幸运,还可以再下一次棋子 */ public static int[][] map = new int[15][15]; /** * 表示当前回合数,偶数则蓝方(或玩家)落子,奇数则红方(或人机)落子 * 如:flag = 20 应该由蓝方(或玩家)落子 */ public static int flag; /** *Scanner对象 */ public static Scanner scanner = new Scanner(System.in); /** * 表示该游戏是否胜利 * 未胜利:true 胜利:false * bool值表示这局是否是人机对战 是人机对战则为true */ public static boolean isNotWin; public static boolean isAi; /** * 用户落子位置 * 人机落子位置 * 创建二维数组来表示棋盘位置的分数 */ public static int user_x,user_y; public static int ai_x,ai_y; public static int[][] score = new int[15][15]; /** * 用于管理特殊效果位置的个数 */ public final static int numOf3 = 6; public final static int numOf4 = 8; public final static int numOf5 = 8; public final static int numOf6 = 7; public final static int numOf7 = 7; public final static int numOf8 = 6; /** * IO流文件存取位置 * 音乐文件存放位置 */ public final static File IO_FILE= new File("resource/record.txt"); public final static File SPECIAL_GOOD_WAV = new File("resource/specialGoodSound.wav"); public final static File SPECIAL_BAD_WAV = new File("resource/specialBadSound.wav"); public final static File BGM_WAV = new File("resource/BGM.wav"); public final static File DROP01 = new File("resource/drop01.wav"); public final static File DROP02 = new File("resource/drop02.wav"); /** * 用于"对局记录"展示页面 */ public static int blueWinNum;//蓝方胜利次数 public static int redWinNum;//红方胜利次数 public static int playerWinNum;//玩家胜利次数 public static int aiWinNum;//ai胜利次数 /** * 音量大小 * soundVolume 音效声音大小 1: 大 * 2: 中 * 3: 小 * 4: 静音 * * bgmVolume * playerThread调用单线程播放bgm * playingBgm用于控制bgm的播放 */ public static int soundVolume; //public static Music music = new Music(); public static Bgm player = new Bgm(); public static Thread playerThread = new Thread(player); public static boolean playingBgm ; /** * mainView的显示字符串 */ public final static String mainView_ = "================================================\n" + " 五子棋 \n" + " (1)玩家对战 \n" + " (2)人机对战 \n" + " (3)游戏说明 \n" + " (4)设置 \n" + " (5)对局记录 \n" + " (6)退出游戏 \n" + "================================================" ; /** * Description的显示字符串 */ public final static String description_ = "#############################################################\n" + "##################### 游戏规则说明 ############################\n" + "#############################################################\n" + "# 1 胜利规则:轮流下子,先连五子者胜利\t\t\t\t\t\t\t#\n" + "# 1 表示玩家1的落子\t\t\t\t\t\t\t\t\t\t\t#\n" + "# 2 表示玩家2的落子\t\t\t\t\t\t\t\t\t\t\t#\n" + "# 0 未落子 ‘#’(死格)\t\t\t\t\t\t\t\t\t\t#\n" + "# 输入-2为投降 输入-3为求和\t\t\t\t\t\t\t\t\t#\n"+ "# 翻出事件\t\t\t\t\t\t\t\t\t\t\t#\n" + "# 3 (炸格)落子位置变为死格\t\t\t\t\t\t\t\t\t\t#\n" + "# 4 (炸弹落子)失败,周围3*3的格子中落子清空\t\t\t\t\t\t#\n" + "# 5 (反棋)落子变成对手棋子的颜色\t\t\t\t\t\t\t\t#\n" + "# 6 (己方反棋)可以将对手的一个棋子变成自己的棋子\t\t\t\t\t#\n" + "# 7 (归零)可以将某一个棋子失效(玩家输入xy值,将对应棋子变成0)\t\t#\n" + "# 8 (幸运)本轮可以落两子\t\t\t\t\t\t\t\t\t\t#\n" + "#############################################################\n" + "# 制作组:我的未来我做组\t\t\t\t\t\t\t#\n" + "# 组长:月霭 副组长:川\t\t\t\t\t\t\t\t\t#\n" + "# 产品经理:烈 技术官:隔了一道墙 Heavydrink\t\t\t\t#\n" + "# 监督官:wly\t\t\t\t\t\t\t\t\t\t\t\t\t#\n" + "#############################################################\n" + "# 输入0返回主界面\t\t\t\t\t\t\t\t\t\t\t\t#\n" + "#############################################################"; /** * gameSetting中展示的字符串 */ public final static String gameSetting_ = "################## 游戏音量大小调节 ####################\n" + "# 当前背景音乐大小:\t\t\t当前游戏音效大小:\t\t\t#\n" + "# 调节为大,输入1\t\t\t调节为大,输入a" +"\t\t\t#\n" + "# 调节为中,输入2\t\t\t调节为中,输入b\t\t\t#\n" + "# 调节为小,输入3\t\t\t" + "调节为小,输入c\t\t\t#\n" + "# 静音请输入4\t\t\t\t静音请输入d\t\t\t\t#\n" + "# 输入0返回主界面\t\t\t\t\t\t\t\t\t\t#\n" + "############################################$$#######\n"; /** * showRecord中展示的字符串 */ public static String showRecord_ = "";
ioStream
两个功能: 1.从txt中读取数据 2. 将数据保存在txt文件中
/** * 本类用于实现文件IO流相关操作 */ public class Io { //----------------------------IOSteam------------------------------- /* 负责人: 功能:从FILE中读取数据 格式为0_0_0_0 对应blueWinNum_redWinNum_playerWinNum_aiWinNum 读取完数据之后给对应变量赋值 参数:void 返回值:void */ public static void readFile(){ } /* 负责人: 功能:在对局胜利之后将结果写入文件FILE 对应blueWinNum_redWinNum_playerWinNum_aiWinNum 参数:void 返回值:void */ public static void writeFile(){ } //----------------------------IOSteam------------------------------- }
music
bgm部分单独做成类, 单独占用一个线程播放, 并提供更改音量的方法
/** * 负责人: * 功能:实现播放bgm的功能 * */ public class Bgm implements Runnable { }
Music类用于播放各种音效, 写一个通用的播放方法, 播放不同音乐直接更改传入的File播放文件即可, 降低程序耦合度, 提供更改音量的方法
/** * 本类用于实现音乐播放相关的操作 */ public class Music { //----------------------------music--------------------------------- /* 负责人: 功能: 播放音效 参数:file 要播放的音效 返回值:void */ public static void playMusic(File file){ } /* 负责人: 功能:播放drop01.wav,drop02.wav 调用playMusic(File)传入对应音频即可 根据flag值 判断 %2 == 0的时候放一种 %2 == 1的时候放另外一个 参数:void 返回值:void */ public static void playDropMusic(){ } /* 负责人: 功能: 播放specialGoodSound.wav 调用playMusic(File)传入对应音频即可 参数:void 返回值:void */ public static void playSpecialSound01(){ } /* 负责人: 功能: 播放specialBadSound.wav 调用playMusic(File)传入对应音频即可 参数:void 返回值:void */ public static void playSpecialSound02(){ } /* 负责人: 功能: 根据对应的音效等级,返回对应的分贝 1: 大 2: 中 3: 小 4: 静音 参数:void 返回值:float 播放的分贝大小 */ private static float getVolumeValue() { } /* 负责人: 功能: 根据传入的clip流 改变对应的分贝大小 调用 getVolumeValue() 得到相应的分贝大小 参数:clip 传入的音频流 返回值:void */ private static void setVolume(Clip clip) { } //----------------------------music--------------------------------- }
service
/** * 本类是项目中的service层,用于改变全局变量的值 */ public class Service { //---------------------------service----------------------------------- /* 负责人: 功能:初始化游戏数据 当前回合设为蓝棋(或player) (flag设为0) 将棋盘的值初始化 使用random函数,随机出0-8八个整数 空地:0-2时初始化为0 特殊效果:3-7则给对应个数加一,如果没超数量上限,则直接赋值3-8 3(6个) 4(8个) 5(8个) 6(7个) 7(7个) 8(6个) 参数: 返回值: */ public static void init(){ } /* 负责人: 功能:落子 参数: x:当前回合落子的x坐标 y:当前回合落子的y坐标 有几种情况: 1.落子的X坐标为-2(投降) (1)打印"某某方投降" (2)把对应的winNum+1 (3)调用writeFile()写入数据 (4)置 isNotWin = false 2.落子的X坐标为-3(申请平局) 判断是否为玩家对战 如果为玩家对战则 提示"是否同意平局(Y/N)" 输入Y: (1)提示:"本局平局,即将返回主菜单..." (2)置 isNotWin = false 输入N: 不做处理 如果为人机对战则平局 (1)提示:"本局平局,即将返回主菜单..." (2)置 isNotWin = false 3.落子位置为空 (1)直接对棋盘map赋值(1/2) (2)调用落子音效 playDropMusic() (3)将flag++ (4)调用isWin(x,y)函数判断是否胜利 4.落子位置已有1/2 打印"----------------------此处已有棋子,请重新输入:-----------------------" 5.落子位置为 -1 打印"------------------此处为死格不能放置棋子,请重新输入:------------------" 6.落子位置为3 调用函数 is3(x,y) 7.落子位置为4 调用函数 is4(x,y) 8.落子位置为5 调用函数 is5(x,y) 9.落子位置为6 调用函数 is6(x,y) 10.落子位置为7 调用函数 is7(x,y) 11.落子位置为8 调用函数 is8(x,y) */ public static void playerMove(int x,int y){ } /* 负责人: 功能:(死格) 提示 "死格,落子失败" 播放特殊音效playSpecialSound02() 打印落子失败,将对应格子的3改为-1 flag++ 参数: x 表示落子x坐标 y 表示落子y坐标 返回值:void */ public static void is3(int x,int y){ } /* 负责人: 功能:(炸弹) 提示 "遇到炸弹,清空周围3*3的格子..." 播放特殊音效playSpecialSound02() 打印"遇到炸弹,清空周围3*3的格子..." 将周围3*3的空间置0 flag++ 参数: x 表示落子x坐标 y 表示落子y坐标 返回值:void */ public static void is4(int x,int y){ } /*负责人: 功能:(反棋) 提示 "被反棋,落子变为对手颜色" 打印提示"被反棋,落子变为对手颜色" 播放特殊音效playSpecialSound02() 对map数组完成落子 调用落子声音函数playDropMusic(); flag++ 调用isWin(x,y)判断是否胜利 参数: x 表示落子x坐标 y 表示落子y坐标 返回值:void */ public static void is5(int x,int y){ } /* 负责人: 功能:(己方反棋) 先落子(改变map[x][y]) 调用落子音效playDropMusic() flag++; 调用isWin(x,y)函数 如果没赢: flag--;//还是这个人的回合 调用showMap() 提示 "己方反棋,可以将对手的一枚棋子变成己方棋子" "请输入x与y的值:" 播放特殊音效playSpecialSound01() 提示玩家输入xy值(参考原型图) while(输入的值非法){ 提示用户输入的是非法值 继续循环输入 } 改为己方落子 调用 playDropMusic() 播放落子音效 flag++ 参数: x 表示落子x坐标 y 表示落子y坐标 返回值:void */ public static void is6(int x,int y){ } /* 负责人: 功能:(己方反棋) 判断棋盘上是否有对方的棋子 如果有则返回true 没有则返回false 参数: 返回值:void */ public static boolean searchfor(int key){ } /* 负责人: 功能:(归零) 先落子(改变map[x][y]) 调用落子音效playDropMusic() flag++; 调用isWin(x,y)函数 如果没赢: flag--;//还是这个人的回合 调用showMap() 打印提示 "归零,可以将一处地方置0" "请输入x与y的值:" 播放特殊音效playSpecialSound01() do{ 提示玩家输入xy值(参考原型图) 接收xy值 如果xy值没有越界: 对应棋盘位置置0 退出循环 (只要棋盘中能用的地方都可以置0) 如果xy值非法: 继续循环 }while(); 调用playDropMusic();落子音效 flag++ 参数: x 表示落子x坐标 y 表示落子y坐标 返回值:void */ public static void is7(int x,int y){ } /* 负责人: 功能:(幸运) 先落子(改变map[x][y]) 调用落子音效playDropMusic() flag++ 调用isWin(x,y)函数 如果没赢: flag--;//还是这个人的回合 调用showMap() 打印提示 "幸运,可以再落一颗棋子" "请输入x与y的值:" 播放特殊音效playSpecialSound01() while(该位置越界了){ 提示用户重新输入 "非法值,请重新输入" 继续循环 } 调用playerMove() 参数: x 表示落子x坐标 y 表示落子y坐标 返回值:void */ public static void is8(int x,int y){ } /* 负责人: 功能:判断游戏是否胜利 判断是否胜利 如果胜利则 将isNotWin置为false 未胜利则不做处理 如果胜利了 if(flag): 置 isNotWin = false; 根据flag和isAi判断谁胜利 把对应winNum+1 调用对应的 winView() 展示胜利界面 (根据flag和isAi来判断) 1表示蓝方获胜 2表示红方获胜 3表示玩家获胜 4表示ai获胜 调用writeFile()把数据保存在文件中 参数: x:落子的x坐标 y:落子的y坐标 返回值:void */ public static void isWin(int x,int y){ } /* 负责人: 功能: 接收用户输入落子位置,如果输入的不是int类型则抛出异常 输入的是int类型则给全局变量user_x,user_y赋值 以保证不会因为scanner接收到非法字符输入后程序中断 参数:void 返回值:void */ public static void inputChess(){ } //---------------------------service----------------------------------- //----------------------------AI------------------------------------- /* 负责人: 功能: 算法得出ai落子位置 将落子位置返回ai_x,ai_y 参数:void 返回值:void */ public static void searchLocation(){ } /* 负责人: 功能:根据五元组和评分细则来得分 完成score数组 参数:void 返回值:void */ public static int tupleScore(int humanChessmanNum, int machineChessmanNum) { //----------------------------AI------------------------------------- }
view
/** * 本类为该项目的view层,用于与用户进行交互 */ public class View { //----------------------------view---------------------------------- /* 负责人: 功能:从此处进入游戏页面 1.调用文件初始化函数readFile()[参数void,返回值void]读取文件中的对局记录 音量大小设为中 并开始播放背景音乐//此行功能不用完成 由完成playBgm()的组员完成 2.do{ 3.调用打印主菜单函数mainView() 4.接收玩家输入 5.switch语句判断 case 1://玩家对战 1.令isAl = false 2.调用游戏函数gameView() case 2://人机对战 1.令isAl = true 2.调用游戏函数gameView() case 3://游戏说明 调用游戏说明函数gameDescription() case 4://设置 调用设置函数gameSetting() case 5://对局记录 调用函数showRecord() case 6://退出游戏 提示"正在退出游戏..." 关闭Scanner mv_flag = false }while(mv_flag); 参数:void 返回值:void */ public static void menuView(){ } /* 负责人: 功能:打印游戏主界面 例:" 五子棋 " " (1)玩家对战 " " (2)人机对战 " 详细请看原型图 使用的常量mainView_在 全局变量GlobalVariable类 下 参数:void 返回值:void */ public static void mainView(){ } /* 负责人: 功能:完成对局功能 调用init()初始化棋盘 do{ 1.打印棋盘 使用showMap() 2.先判断是否为人机落子( (flag+1)%2 == 0 && isAi ) 如果是则调用 人机落子函数searchLocation() 并把ai_x和ai_y传给落子函数调用playerMove() 如果不是则提示用户输入xy值,调用playerMove() }while(isNotWin); 参数:void 返回值:void */ public static void gameView(){ } /* 负责人: 功能:打印棋盘 把-1打印为# 把1打印为蓝色的1 把2打印为红色的2 其余全部打印为* ps:目前没有实现 "其余全部打印为*",是为了方便测试,等最后再改过来 打印时要把外面的框也打印出来,见原型图 注意:要把 该回合是第几回合 是谁落子 打印清楚 参数:void 返回值:void */ public static void showMap(){ } /* 负责人: 功能:打印胜利界面 打印完后 提示用户输入0返回主菜单 如果是0则跳出循环 如果不是0则一直输 参数: id表示是谁赢了 1表示蓝方获胜 2表示红方获胜 3表示玩家获胜 4表示ai获胜 返回值:void */ public static void winView(int id){ } /* 负责人: 功能: 打印游戏规则和制作组介绍(原型图) 使用的常量在 GlobalVariable类 下的description_ 提示用户输入0 如果是0则跳出循环 如果不是0则一直输 参数:void 返回值:void */ public static void gameDescription(){ } /* 负责人: 功能: 打印setting界面 提示用户输入 合法: 改变对应BGM音量 BgmVolume 1: 大 //player.setHighVolume(); 2: 中 //player.setMediumVolume(); 3: 小 //player.setLowVolume(); 4: 静音 //player.mute(); 改变对应音效大小 soundVolume 将 全局变量 soundVolume 改为对应数字即可 不合法: ...? 参数:void 返回值:void */ public static void gameSetting(){ } /* 负责人: 功能: 根据winNum打印界面(原型图) 提示用户输入0返回 参数:void 返回值:void */ public static void showRecord(){ } //----------------------------view---------------------------------- }