(2014年6月9日:高考结束了,尽了人事,剩下的基本上只能听天命了。)
温习了一下许久没有碰过的JAVA,说实话还真的有些生疏了。于是做了个挺简单的“猜数字”的小游戏,复习了下基本的语法。虽然这个“游戏”简单到基本学过编程就能做,但是自己亲自从思考程序逻辑,设计算法,编码调试,修改,然后再调试,最后差不多没有什么明显的问题,整个过程下来还是很有收获的。一些心得,分享一下, 如果有什么错误也希望大家能够不吝指正。
首先还是介绍一下这个游戏的玩法,因为很大程度上一个游戏的玩法(规则)就是我们设计程序总的算法,我们的编码总归是要围绕实现“游戏规则”这个目标的。这里的“猜数字”,并不是指的是“生成一个随机数,然后让游戏者猜,然后程序会告诉你高了还是低了,如此循环往复,直到猜中”,它的规则要复杂一些,同时游戏性也更强,它还要求游戏者有一定的推理判断力,当然还有运气。好了,规则如下:
1.程序随机生成一个四位的整数,要求是这个整数每一位都不相同,比如“1234”是符合要求的,而“3453”则不行
2.用户输入猜测的数字,当然这个数字也必须是四位的,而且每一位都不相同
3.程序会根据用户输入的数字,给出反馈,反馈的形式是nBmA,含义为用户输入的数字中有(m+n)个数字与生成的随机数(称为目标数)是一样的,其中有n个数字与目标数一样但所处的位置不同,有m个数与目标数一样而且所处的位置也相同。比如:目标数是:2456,输入:5236,那么程序反馈就是:2B1A。因为输入数与目标数中都有2和5,但位置不同,所以n=2;而输入数与目标数中都有6,且都处在相同的位置(都在个位),所以m=1。
4.五次还未猜出正确数字,那么游戏就失败。
看到上面的规则,是不是觉得很简单,急于编码。我认为急于下手是一种很不好的习惯,有时候头脑中的快速构想并不一定就是解决问题的理想模型,很可能实现到一半的时候才会发现有致命的缺陷,然后再来重构就会浪费更多的精力,这是经验之谈。就拿这个程序的实现来说,第一次我急于求成,写到一半才发现某些部分虽然可以实现但是非常繁琐,而且一定可以写得更精简一些,但是由于我的代码太过混乱,结构很不好,所以没办法实现。因此,我完全放弃了第一次的代码,先是仔细考虑了整个程序的目标是什么,大致的流程有哪些,有哪些关键的算法,能不能分成不同的模块来减少各部分间的相互影响(耦合)......然后想了一个非常粗略的框架,由宏观再到微观的实现,大方向有了,写起代码来基本上不会“东拉西扯”,写代码的感觉也比较流畅,除了写一些具体的算法会停下来思考,基本上都是一路没有“瓶颈”写下来的。
好了,上面是一些感受,讲点具体的东西。首先是对规则进行分析,搞清楚它要我们做什么,我们可以做什么,不可以做什么:通览所有规则,发现这个程序涉及到人机交互,也就是输入输出(有些废话了,这是所有游戏最基本的特征之一),所以我们在编码中应该考虑到采用哪种形式的交互,是字符界面还是GUI界面?如果你跟我想的一样,想要实现两种方式,那么势必要考虑任何让界面与业务逻辑尽量相互独立,毕竟无论采取何种表现形式,业务逻辑还是一样的,并不会随之转移。最好,业务逻辑根本不知道有界面的存在,这样界面的改动对业务逻辑来讲是没有任何影响的。所以我就想把业务逻辑抽取出来,并且封装起来,对外只提供输入输出的接口,于是我就创建了一个Logic类(在com.zyzz.Logic包下),来处理游戏的业务逻辑,所有的核心算法和游戏流程的实现都位于此,只提供了Logic.input(int[] in)来接受外部输入。它的构造器是:Logic(OutputHandler handler),而OutputHandler是一个接口(也位于com.zyzz.Logic下),里面有一个onOutput(Object out)的接口方法,每当游戏业务产生了输出都会回调这个方法,达到向外界传递输出信息的目的,总的来说,它相当于一个事件监听器,在发生“输出事件”时被触发。这样Logic的输入输出接口都有了,它与外界(特别是界面)的唯一交流就只能通过这些有限的接口,而内部的实现对外界来说是透明的(即:不可见的),它既不知道Logic内部发生了什么,也无法改变什么,它唯一能做的就是在适当的时候向Logic对象输入一些数据(通过调用Logic.input()),或者接受Logic对象的反馈输出并用适当的发生显示它们(通过实现OutputHandler,并重写onOutput()方法)。这样就达到了两相隔离,互不影响的目的。这样设计是基于“责任”的考虑,责任不同,分工不同,只有各行其道,方能条理清晰,层次分明。下面分别是字符界面的实现和GUI界面的实现,基本上没有什么差异:
//字符界面的游戏循环
Logic logic=new Logic(new OutputHandler(){
@Override
public void onOutput(Object val) {
System.out.println(val);//处理输出反馈
}
});
Scanner sc=new Scanner(System.in);
do{
System.out.print("Your Number:");
try{
final int[] input = RandomTool.stringToBitsArray(sc.next());
logic.input(input);//输入数据
}catch(Exception e){
System.out.println("DEADLY ERROR");
break;
}
}while(logic.getState()==STATE_ACTIVE);
System.out.println("Game Over!");
//-------------------------------//
//GUI界面的游戏循环
private final Logic logic=new Logic(new OutputHandler(){
@Override
public void onOutput(Object val) {
textArea.setText((String)val);//处理输出反馈
}
});
private final Action action = new SwingAction();
......此处省略界面初始化代码
private class SwingAction extends AbstractAction {
/**
*
*/
public SwingAction() {
putValue(NAME, "Guess");
putValue(SHORT_DESCRIPTION, "cilck to guess a number");
}
public void actionPerformed(ActionEvent e) {
if(logic.getState()==Logic.STATE_DEAD) {
logic.reset();
textArea.setText("");
}
logic.input(RandomTool.stringToBitsArray(textField.getText()));//输入数据
textField.setText("");
}
}
//---------------------------------------//
可以看到上面代码基本是神似的,唯一的区别就在于字符界面游戏循环要靠一个while循环来支撑,而因为GUI程序窗体有自己的生命周期,所以游戏循环是靠它本身的一些事件支持起来的。
我们只看到程序里出现一个logic对象,却不知道它到底做了什么,当然,对于上面的的代码来说更是不需要知道logic里发生了什么,但一些具体的业务逻辑,我觉得还是很有必要讲一下的。
分析规则1,我们会得出这样一个结论:我需要一个可以产生不重复的4位数字的算法,这是一个核心算法。网上流传了许多这方面的算法,其中有一些有缺陷(比如理论上会陷入死循环,不符合算法的确定性,有穷性),有一些实在没看懂,这里提供一个我自己想出来的,路子比较“野”的算法,讲一下,当初思考的过程:
当时考虑到这个算法的要求有两个:一是随机性,二是不重复。第一个要求比较简单,使用java提供的Random工具就行了,第二个要求实现起来要考虑的细节比较多,比如说如果采用“Step1.先随机产生一个数字——>Step2.判断有没有出现过,若出现过则——>Step1”这种思路严格说起来是不符合算法设计要求的,因为存在这样一种可能:每次随机产生的数字都是已经出现过的——虽然这是小概率事件,基本上不可能出现,但它仍然是不确定的。既然如此,不如换种思路:先保证数字是不重复的再保证数字序列是随机的。虽然这种思路只是把实现的顺序换了一下,但实现起来却要简单的多。想一想,怎样才能保证产生的数字序列一定是不重复的?换个问法,我们最多能够保证多少位的数字序列是不重复的?后一个问题,在十进制的条件下,我们能保证最多十位数字序列是不重复的,因为十进制中只有{0,1,2,3,4,5,6,7,8,9}这十个基数,所以超过十位的一个数字序列必定有重复的数字。弄清楚这个,要确保一个四位的数字序列不重复,只要保证这个四位的数字序列是{0,1,2,3,4,5,6,7,8,9}的一个子序列就行了。至于随机性,只需要让{0,1,2,3,4,5,6,7,8,9}这个序列中的每一个数字随机的交换位置就行了。这个比较另类的算法保证了在确定的步骤内一定能够产生结果,即所谓“确定性”。下面贴出具体的实现代码:
//-------------------------------------------------------------------------------------------------------------------------//
/**
*
* @param bits
* 要产生的不重复的随机数的位数,因为十进制只有{0,1,2,3,4,5,6,7,8,9}共十个数字,输入位数不能够超过10位
* @return 总共bits位,且各个位互不相同的随机数
*/
public static int[] randNonRepeated(int bits) {
if (bits > 10 || bits <= 0)
throw new RuntimeException(
"illegal arg:bits must range from 0 to 10!");
final int[] srcNum = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
for (int i = 0; i < 10; i++) {
int exchange_bit = rnd.nextInt(10);//随机产生将与第i位交换的位
int temp = srcNum[i];
srcNum[i] = srcNum[exchange_bit];
srcNum[exchange_bit] = temp;
}
if (srcNum[0] == 0) {
int exchange_bit = rnd.nextInt(9) + 1;
srcNum[0] = srcNum[exchange_bit];
srcNum[exchange_bit] = 0;
}
return Arrays.copyOf(srcNum, bits);
}
//-------------------------------------------------------------------------------------------------------------------------//
关于规则2,3,4的实现都比较简单这里不再赘述。
完整的源代码:http://download.csdn.net/detail/zyzzate/7472635
(PS:本人菜鸟,上面如有有偏颇之处望各位大侠指正,谢谢!)