需求
三个人斗地主,54张牌(包括大王、小王)。每人按顺序摸牌,最后剩3张底牌,请打印输出三个玩家的手牌以及所剩的底牌。
设计
逻辑很清晰,首先是准备好54张牌,对于牌来讲除过特殊牌大王、小王,剩下的可以用四种花色和13张数字不一样的牌进行组合,可通过一个非固定长度的集合来存储牌,分别存入普通牌和特殊牌;
其次是洗牌,这点很重要,根据常识每次玩牌前都会有洗牌的动作,对应到程序逻辑中就是将集合中的元素随机重排列;
第三点是发牌,由于三名玩家都是按顺序进行摸牌,这里可以用到集合索引%3的运算实现三人顺序摸牌(0%3=0 1%3=1 2%3=2 3%3=0,三个数一循环),将集合中相应的值存储到玩家集合中,另外就是会在51张牌的时候停止摸牌,留三张底牌,在程序逻辑中就是当集合索引大于等于51时,将剩下的元素存储底牌集合中(51-53索引);
最后是打印输出,分别打印玩家集合和底牌集合即可。
实现
1. 准备牌,此处使用String类型的ArrayList实现,并准备两个String类型的数组分别存储四种花色和13种数字,并对这两个数组进行双重循环遍历,组合花色+数字并存储到ArrayList中表示一张牌,遍历完后在ArrayList中加入大王和小王即可实现54张牌。
private static ArrayList<String> perpareCard() {
//1. 准备牌 54张(大王 小王 4种花色 每种花色13张)
//1.1 用String类型的ArrayList集合存储54张牌
ArrayList<String> poker = new ArrayList<>();
//1.2 用String类型数组存储四种花色(数组定义使用省略的静态初始化方法,推荐使用)
String[] colors = {"♠","♣","♥","♦"};
//1.3 用String类型数组存储13张牌(数组定义使用静态初始化方法)
String[] numbers = new String[]{"2","A","K","Q","J","10","9","8","7","6","5","4","3"};
//1.4.1 双重循环遍历colors数组和numbers数组,得到52张牌并存入poker集合中
for(int i = 0; i < colors.length; i++){
for(int j = 0; j < numbers.length; j++){
//通过将两种String拼接在一起构成有花色有数字的一张牌
poker.add(colors[i]+numbers[j]);
}
}
//1.4.2 将大小王加入到poker中完成54张牌的准备工作
poker.add("大王");
poker.add("小王");
return poker;
}
此处需要注意的是花色和数字的双重循环同样可以使用加强的foreach循环完成,代码如下:(foreach遍历数组和集合元素)
for(String color : colors){
for(String number : numbers){
poker.add(color+number);
}
}
测试打印结果如下图,表名54张牌的准备工作已经完成。
2. 洗牌。洗牌的现实逻辑很简单就是打乱原本的牌的顺序,对应到程序逻辑中就是打乱存储牌的集合中元素的顺序,具体的实现从集合索引入手,新建一个集合存储打乱顺序之后的集合,对原有集合的索引取随机值,随机拿取原有集合的元素并顺序存入现有集合中(此处需要注意的是每取出一个元素,集合中的这个元素就需要删除,另外集合size--。由于集合索引从0开始计数,随机的Random.nextInt(n)中的n最小为1,当size=1的时候集合中只有一个元素,可以直接赋值取也可以继续使用随机,此时n=0)
private static ArrayList<String> shuffle(ArrayList<String> arrayList) {
//1. 洗牌(遍历集合并随机打乱顺序(随机取索引并转入新的集合)
//1.1 获取集合元素个数
int size = arrayList.size();
//1.2 创建新集合,用来存储打乱顺序后的元素
ArrayList<String> resortArrayList = new ArrayList<>();
//1.3 使用Random对象来随机取集合索引的值,随机范围为[0,size)
Random random = new Random();
for(int i = random.nextInt(size); size > 0;){
//1.3.1 将随机取出的元素存入新集合中并从原集合中取出该元素
resortArrayList.add(arrayList.remove(i));
//1.3.2 减小原集合的大小并重新对i赋值
if((--size)>=1) {//size最小为2,去掉一个元素后为1
i = random.nextInt(size);//Random.nextInt()方法取值随机范围>0,最小为1
}
}
return resortArrayList;
}
此处代码需要注意的是重新给索引i取随机值的时候,需要先size-1,if判断中size--是先判断size然后再进行size-1,如果使用size--,那么当size=1的时候,size--=0,i的随机值中的n=0会报bound must positive的错误。所以if判断中需要先size-1,然后再进行判断,故使用--size。另外此处使用的是集合的remove方法,将给定索引下的元素从集合中移除,方法返回被移除的元素,恰好可以将这个返回值作为参数调用add方法存入到另一个集合中。
洗牌后结果如下图所示,并且每次输出结果都不同,说明洗牌功能完成。
注意此处洗牌还可以直接使用Collections工具类的shuflle方法将ArrayList中元素打乱顺序,无返回值。
Collections.shuffle(orignArrayList);
结果如下图所示,第一行为未洗牌前的结果,第二行为调用Collections集合工具类的静态方法shuffle后输出的结果,洗牌成功。
3. 发牌。发牌中对于底牌实现很简单,就是判断集合索引值,当集合索引值>=51的时候将集合中元素加入到底牌集合中即可。如何实现三名玩家顺序摸牌,就需要使用到%3的概念,基数为3,0%3=0 1%3=1 2%3=2;3%3=0 4%3=1 5%3=2;6%3=0,可以明确看到三位数一循环,对应到游戏中就是顺序发三张牌后重回第一个人发然后再继续顺序发牌,那么可以在遍历集合元素的时候对索引值进行if-else if判断,符合%3=0的存入第一个玩家的集合,符合%3=1的存入第二个玩家集合,符合%3=2的存入第三个玩家集合(因为索引值%3只有三种可能的结果,所以每一种结果分别存储三个集合即可表示玩家顺序摸牌,此处判断的索引值需要小于51,保证留三张底牌)
ArrayList<String> player1 = new ArrayList<>();
ArrayList<String> player2 = new ArrayList<>();
ArrayList<String> player3 = new ArrayList<>();
ArrayList<String> dipai = new ArrayList<>();
//3.2 遍历洗完后的牌并给三个人发牌并留下底牌
for(int i = 0; i < resortArrayList.size(); i++){
// 0%3=0 1%3=1 2%3=2;3%3=0 4%3=1 5%3=2;6%3=0...
//3.2.1 底牌
if(i >= resortArrayList.size()-3){//51
dipai.add(resortArrayList.get(i));
}else if(i % 3 ==0){//3.2.2 给玩家player1发牌
player1.add(resortArrayList.get(i));
}else if(i % 3 == 1){//3.2.3 给玩家player2发牌
player2.add(resortArrayList.get(i));
}else if(i % 3 == 2){//3.2.4 给玩家player3发牌
player3.add(resortArrayList.get(i));
}
}
此处需要注意容易犯的一个错误是直接变化i的大小来%3,代码如下,结果就是只会不断将集合中前三个元素分别存入玩家集合。
//错误写法,这样集合永远取得都是索引值为0 1 2的值
// player1.add(resortArrayList.get(i%3)); //0
// player2.add(resortArrayList.get(((i++)%3))); //1
// player3.add(resortArrayList.get((i++)%3)); //2
4. 三名玩家及底牌打印输出。这个简单,就是直接输出集合即可。输出的方法可以直接输出集合名(类似toString()方法);也可以使用迭代器进行遍历输出。
//4.1 出牌,打印每个人的牌以及底牌
System.out.println("第一个人的手牌:");
scanArrayList(player1);
System.out.println("\n第二个人的手牌:");
scanArrayList(player2);
System.out.println("\n第三个人的手牌:");
scanArrayList(player3);
System.out.println("\n底牌:");
scanArrayList(dipai);
//遍历方法
private static void scanArrayList(ArrayList<String> arrayList) {
//1.1 foreach方法遍历
for (String str: arrayList) {
System.out.print(str + " ");
}
//1.2 迭代器遍历
Iterator<String> iterator = arrayList.iterator();
//1.2.1 使用while循环
while(iterator.hasNext(){
System.out.print(iterator.next());
}
//1.2.2 使用for循环
for(Iterator iterator1 = arrayList.iterator();iterator1.hasNext();){
System.out.print(iterator1.next());
}
}
//4.2 直接打印输出
System.out.println("第一个人的手牌:");
System.out.print(player1);
System.out.println("\n第二个人的手牌:");
System.out.print(player2);
System.out.println("\n第三个人的手牌:");
System.out.print(player3);
System.out.println("\n底牌:");
System.out.print(player4);
此处注意上述代码中给了多个实现方法,根据实际需求选择使用即可。
打印结果如下图所示,即所有功能实现。且每局的牌都不同(洗牌作用)
总结
知识点总结
1. 单列集合的定义及使用。此处使用ArrayList,且将集合类型定为String来替代原有的泛型E,使用ArrayList的构造方法来new初始化创建集合对象;使用arrayList.add("xx")将元素添加到集合中;使用arrayList.remove(i)将集合中某索引对应的集合元素移除,结果返回被移除的元素;使用arrayList.get(i)获取集合中某索引对应的元素,结果返回该元素
2. 遍历集合/数组,可以使用fori循环,可以使用foreach循环(推荐foreach),如果需要用到索引值那么使用fori循环;遍历集合使用迭代器
3. 顺序循环取值,需要想到%的使用
4. fori循环中,注意初始条件、判断条件和步进条件以及循环体的正确写法,同时注意边界值
for(初始条件; 判断条件; 步进条件){
//初始条件一旦赋值,不会改变,后续循环只会去找判断条件和步进条件,不会再看初始条件
//比如此处初始条件若为i=0,如果后续不对i进行修改,i在整个循环中就会一直为0
循环体(步进条件可写入到循环体中)
}
5. 使用Random对象的nextInt(n)方法可随机取[0,n)区间的值,注意是左闭右开(符合集合/数组索引特点),n最小为1,如果需要取到[0,n]左闭右闭区间的值,可使用random.nextInt(n+1)实现【[0,n)->[0,n+1)=[0,n]】;如果需要取到[1,n]区间的值,可使用random.nextInt(n)+1实现【[0,n)->[1,n+1)=[1,n]】。
其他总结
遇到一个题,需要首先分析清楚需求,然后列逻辑实现顺序点,然后一条一条的进行设计和实现,思路清晰事半功倍。