前言
两天做了两个很有意思的项目,一个Nim游戏,一个塔防游戏,对类与继承,比较器,异常处理和文件IO操作
的训练效果还是挺不错的,收获很多,写一篇笔记来记录一下学习效果。源代码有需要的可以微博联系我,
虽然我代码写的很乱哈。
从Nim游戏中理解比较器,异常处理和文件流操作
先来简述一下Nim游戏的规则吧,给出一堆石子的总数和每次允许取出的最大上限,两个玩家开始按
规则取石子,取走最后一个石子的玩家Lose,规则很简单。(关于这个游戏的继承我不详细说,有些简单,
塔防游戏要更复杂一些,用于对类与继承进行理解更加深刻。) 我们要做的任务就是:
- 制作一个类Linux的命令行面板用于接收输入,注意:命令可能会有数量不等的参数传入。
- 游戏运行有100个用户同时存在,但游戏同一时间内只能存在一场对局。
- 用户的数据统计和基本信息在游戏界面程序启动时从文件中读取并在游戏程序关闭时写入文件。(也就是一个数据库)
- 要求能够实现添加玩家,添加AI玩家,删除玩家,编辑玩家基本信息,重置玩家数据统计,展示玩家信息,按数据统计顺序逆序排名等功能。
- 没错,这个游戏需要实现人机对战和机机对战。
- 程序要能够处理异常,包括移动石子数,参数不足和指令不存在。
命令行面板如下图,下图展示的是commands功能:
类的设计
那对游戏本身有了一个了解之后我们就要去设计类了,显然玩家分为人类玩家和AI玩家,让这两个类继承玩家类就好了,
因为AI除了在移动石子上不需要从键盘输入以外和人类玩家的其他信息都是一样的,所以只需要重写移动方法。
接下来一个NimGame类用来实现游戏主体,一个NimSys类用来管理命令行面板和其他功能,在需要游戏的时候,创建
一个游戏实体就好了。
命令行面板实现
项目要求指令有不同的参数传入,指令和参数间用空格隔开,参数和参数间用英文逗号隔开。
这里记录第一个问题,就是java的Scanner缺陷问题。
1 Scanner的hasNext方法无法检测是否有下一个输入
java开发文档这样解释:
public boolean hasNext()
如果此扫描器的输入中有另一个标记,则返回 true。在等待要扫描的输入时,此方法可能阻塞。扫描器将不执行任何输入。
经过我各种实验发现这个方法在交互上没有任何作用,所以舍弃。于是改用另一种办法:在死循环中用next()与指令进行比对
配对成功进入对应函数,不成功抛出异常。 但问题在于,接受了next()后,如果同行有参数传入,如何处理?
关键在于nextLIne()函数,在同行读取了指令的情况下使用nextLIne()可以读取到指令后面的所有内容。
这样再用split处理就可以得到想要的参数数组了,同样,如果个数不够,抛出异常。
ps:去除字符串首尾空格用.trim()函数。
玩家信息用文件流存储
这个实现起来相对简单,方法也很多,我只提供一种读写方式作为参考。
2 Buffered包装文件流处理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
45
46写入方法:
String url = "./players.dat";//存储地址
File database = new File(url);
if (!database.exists()){
database.createNewFile();//不存在就创建,注意mkdir是创建文件夹,文件要用createNewFile
}else {
database.delete();
database.createNewFile();//因为启动都要读取数据到缓存,所以退出时删了重写就好
}
//读入
if(this.players.isEmpty()){
}else {
BufferedWriter writer = new BufferedWriter (new OutputStreamWriter(new FileOutputStream(database,false),"UTF-8"));
NimPlayer value = null;
Iterator iter = this.players.iterator();//迭代器遍历玩家数组
while (iter.hasNext()) {
value = (NimPlayer) iter.next();
String tmp = value.getINFO();//给玩家一个格式化信息的方法便于写入
writer.write(tmp);
}
writer.flush();//强制将缓冲区中数据发送出去,不必等到缓冲区满
writer.close();//关闭流,习惯要好。
}
读入方法:
String url = "./players.dat";
File database = new File(url);
if (database.exists()){
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(database),"UTF-8"));
while (true){
String stmp = reader.readLine();
if (stmp==null){
break;//读到末尾
}else {
String[] tmp = stmp.split(",");
if (tmp.length>5){
NimAIPlayer tmpPlayer = new NimAIPlayer(tmp[0],tmp[1],tmp[2],Integer.parseInt(tmp[3]),Integer.parseInt(tmp[4]));
this.players.add(tmpPlayer);
}else {
NimPlayer tmpPlayer = new NimPlayer(tmp[0],tmp[1],tmp[2],Integer.parseInt(tmp[3]),Integer.parseInt(tmp[4]));
this.players.add(tmpPlayer);
}
}
}
}
玩家数组
玩家数组这里我用ArrayList设置定长100处理(其实没必要),推荐个帖子:https://www.cnblogs.com/msymm/p/9872818.html
玩家信息按字母a-z顺序打印
这个功能实现起来用两个技巧,一个是compareTo方法对String类型的处理,一个是通过重写比较器Comparator的compara接口来
实现排序功能。话不多说直接上代码。
3 比较器1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Collections.sort(console.players, new Comparator<NimPlayer>() {
@Override
public int compare(NimPlayer s1, NimPlayer s2) {
String name1 = s1.getUsername().toLowerCase();
String name2 = s2.getUsername().toLowerCase();
int num = name1.compareTo(name2);
return num;
}
});
//display all
for (NimPlayer aplayer:console.players) {
String info = "";
info = info+aplayer.getUsername()+", "+aplayer.getFirstname()+", "+aplayer.getFamname()+", "+aplayer.getTurns()+" games, "+aplayer.getWins()+" wins";
System.out.println(info);
}
这里有一个重点要记住:形参s1的实参为数组中第二个对象。
举个例子:[Mike,Allen] 有一个这样的玩家数组,当用比较器排序时,传入参数s1为Allen,s2为Mike。然后String的compareTo方法
会比较字符串1的第一个字符跟字符串2的第一个字符不相等,则两个字符串都按照第一个字符的ASCII码顺序进行比较,其他字符都不用看
,并返回一个整型(ASCII码的差值)。这里a-m = 97-109 = -12 。 之后比较器会判断权重,因为返回了负数,所以后者会排在前面。
ranking按胜率排序
按胜率排序,胜率相同按字母顺序排序。类似,直接上代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//make ordered
Collections.sort(this.players, new Comparator<NimPlayer>() {
@Override
public int compare(NimPlayer s1, NimPlayer s2) {
int key = 0;
String name1 = s1.getUsername().toLowerCase();
String name2 = s2.getUsername().toLowerCase();
int num1 = name1.compareTo(name2);
//System.out.println(s1.getWinRate()+" "+s2.getWinRate());
double dnum2 = s1.getWinRate() - s2.getWinRate();
//System.out.println(dnum2);
if (dnum2>0){
key = -1;
}else if (dnum2==0){
key = num1;
}else if (dnum2<0){
key = 1;
}
return key;
}
});
人机对战
其他与人类玩家一样,只是在接受输入的使用通过判断是机器人玩家后,直接从机器人玩家中获取移动的石子数,
这里分享一个我写的Nim游戏必胜策略,不一定准确。可以通过简单测试。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//do a input
public boolean isVictory(int stoneremain, int upperbound,int move){
for (int k=0;k<100;k++){
if((stoneremain-move) == k*(upperbound+1)+1){
return true;
}
}
return false;
}
public int removeStone(int upperbound,int stoneremain){
int limit = 0;
if (upperbound>=stoneremain){
limit = stoneremain;
}else if (upperbound<stoneremain){
limit = upperbound;
}
int movedStones = 0;
for (int i=1;i<=limit;i++){
if (isVictory(stoneremain,upperbound,i)){
movedStones = i;
break;
}
}
if (movedStones==0){
movedStones = 1;
}
int after = stoneremain - movedStones;
return after;
}
从塔防游戏中理解抽象类,继承,抽象方法和重写方法。
同样,简单说下题目。
图中有两类对象,地砖Tile和昆虫Insect,昆虫分为蜜蜂和马蜂,蜜蜂有三种,分别是BusyBee、StingyBee和TankyBee。
这个游戏可以用植物大战僵尸来类比,上面三种分别对应太阳花,豌豆射手和地刺。
下面主要从蜜蜂类来学习多态的一些方法。
抽象类
如果一个类中没有包含足够的信息来描绘一个具体的对象,将这个类称为抽象类。
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。在Java中抽象类表示的是一种继承关系,一个类只能继承
一个抽象类,而一个类却可以实现多个接口。举个例子来说明为什么要写一个抽象类,在植物大战僵尸的游戏流程中,只需要
出现豌豆射手和冰冻豌豆射手就好了,对“射手”这个父类没有实例化需求,那么将这个类设置为抽象类。同理,蜜蜂和马蜂同属
于昆虫类,故将昆虫类设置为抽象类。下面给一个我写的抽象类例子: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
45
46
47
48
49
50
51
52
53
54
55
56
57
58public abstract class Insect {
private Tile myTile;
private int hp;
public Insect(Tile aTile,int hp){
this.myTile = aTile;
this.hp = hp;
if (!this.myTile.addInsect(this)){
throw new IllegalArgumentException();
}
}
public final Tile getPosition(){
return this.myTile;
}
public final int getHealth(){
return this.hp;
}
public void setPosition(Tile newTile){
this.myTile = newTile;
}
//other methods
public void takeDamage(int damage){
if (this instanceof HoneyBee && this.myTile.isHive()){
damage = (int) (damage*0.9);
this.hp = this.hp-damage;
if (this.hp<=0){
this.myTile.removeInsect(this);
}
}else {
this.hp = this.hp-damage;
if (this.hp<=0){
this.myTile.removeInsect(this);
}
}
}
public abstract boolean takeAction();
public boolean equals(Object o){
if (this==o){
return true;
}
if (!(o instanceof Insect)){
return false;
}
Insect io = (Insect) o;
if (this.myTile.equals(io.myTile) && this.hp==io.hp){
return true;
}else {
return false;
}
}
}
除了基本的成员变量和构造方法,该抽象类中还包含一个抽象方法takeAction()和重写的equals方法。
重写方法和继承
重写是子类对父类的允许访问的方法的实现过程进行重新编写!返回值和形参都不能改变。即外壳不变,核心重写!
重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要实现父类的方法。
下面给出蜜蜂类来再看一下重写方法,因为蜜蜂类也不需要实例化,所以设置为抽象类,继承昆虫这个抽象类。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
27public abstract class HoneyBee extends Insect{
private int cost4Food;
public HoneyBee(Tile aTile, int hp,int cost) {
super(aTile, hp); //我们在继承父类时,不需要将同样的代码再写一遍,直接使用super关键字调用父类构造方法。
this.cost4Food = cost;
}
public int getCost(){
return this.cost4Food;
}
public boolean equals(Object o){
if (!super.equals(o)){ //同样调用父类方法
return false;
}
if (!(o instanceof HoneyBee)){
return false;
}
HoneyBee io = (HoneyBee) o;
if (this.cost4Food == io.cost4Food){
return true;
}else {
return false;
}
}
}
4 重写方法的一些规则
- 参数列表必须完全与被重写方法的相同;
- 返回类型必须完全与被重写方法的返回类型相同;
- 访问权限不能比父类中被重写的方法的访问权限更高。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
- 父类的成员方法只能被它的子类重写。
- 声明为final的方法不能被重写。
- 声明为static的方法不能被重写,但是能够被再次声明。
- 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
- 子类和父类不在同一个包中,那么子类只能够重写父类的声明为public和protected的非final方法。
- 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
- 构造方法不能被重写。
- 如果不能继承一个方法,则不能重写这个方法。
另外,关于为什么要重写equals方法,这里有一篇博客写的比较好:https://blog.csdn.net/panchao888888/article/details/80888592
我这里从中摘录一些:
5 为什么要重写equals()方法?
1.Object类中equals方法比较的是两个对象的引用地址,只有对象的引用地址指向同一个地址时,才认为这两个地址是相等的,否则这两个对象就不想等。
2.如果有两个对象,他们的属性是相同的,但是地址不同,这样使用equals()比较得出的结果是不相等的,而我们需要的是这两个对象相等,因此默认的equals()方法是不符合我们的要求的,这个时候我们就需要对equals()方法进行重写以满足我们的预期结果。
3.在java的集合框架中需要用到equals()方法进行查找对象,如果集合中存放的是自定义类型,并且没有重写equals()方法,则会调用Object父类中的equals()方法按照地址比较,往往会出现错误的结果,此时我们应该根据业务需求重写equals()方法。
抽象方法
抽象方法和抽象类的使用原因十分相似,都是为了个性化定制每个实体类的方法,而将方法名统一。
下面用StingyBee类继承蜜蜂类来具体看一下如何重写抽象方法。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
45
46
47
48
49
50
51
52
53
54
55
56public class StingyBee extends HoneyBee{
private int attack;
public StingyBee(Tile aTile, int attack) {
super(aTile, 10, 1);
this.attack = attack;
}
public boolean takeAction() {
if (this.getPosition().isHive()||this.getPosition().isOnThePath()){
while (true){
if (this.getPosition().getHornet()!=null){
if (this.getPosition().isNest()){
return false;
}else {
this.getPosition().getHornet().takeDamage(this.attack);
return true;
}
}else {
Tile tmp = this.getPosition().towardTheNest();
if (tmp.isNest()){ return false; }
while (true){
if (tmp.isNest()){
return false;
}else {
if (tmp.getHornet()!=null){
tmp.getHornet().takeDamage(this.attack);
return true;
}else {
tmp = tmp.towardTheNest();
}
}
}
}
}
}else {
return false;
}
}
public boolean equals(Object o){
if (!super.equals(o)){
return false;
}
if (!(o instanceof StingyBee)){
return false;
}
StingyBee io = (StingyBee) o;
if (this.attack == io.attack){
return true;
}else {
return false;
}
}
}
后记
断断续续写了三天才完成这篇文章,写的还是不是很满意,但是想记录的东西都记录下来了,有什么问题欢迎来微博找我交流。