首页 > 技术文章 > 第二次结对编程作业

stolf 2019-10-15 20:23 原文

Part0:UI演示视频

链接:https://pan.baidu.com/s/1GOcVyCjkjNZkg5xsV9Ikrw
提取码:hqp6

Part1:相关链接

鲍子涵的博客
吴宜航的博客
Github项目地址


Part2:具体分工

鲍子涵:AI设计与实现 + 网络接口
吴宜航:UI设计与实现


Part3:PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 120 100
Estimate 估计这个任务需要多少时间 60 60
Developm 开发 2800 3250
Analysis 需求分析(包括学习新技术) 50 60
Design Spec 生成设计文档 30 30
Design Review 设计复审 30 30
Coding Standard 代码规范(为目前的开发制定合适的规范) 30 30
Design 具体设计 160 200
Coding 具体编码 2400 2650
Code Review 代码复审 30 60
Test 测试(自我测试,修改代码,提交修改) 120 150
Reporting 报告 50 50
Test Report 测试报告 30 30
Size Measurement 计算工作量 10 10
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 20 20
合计 3080 3590

Part4:解题思路描述与设计实现说明

网络接口的使用

考虑到本次结对作业涉及网络接口的调用,使用Java的第三方库HTTPClient来访问永福设置的API,并构造封装了以下5个类来进行网络功能:

类名 功能
Until.HttpUntil HTTP请求处理工具类
Client.UserActivity 用户相关操作类,包括登录、注册、注销接口
Client.RankActivity 排名相关操作类,主要用来获取排名信息
Client.HistoryActivity 历史记录相关操作类,包括获取历史记录、查询详细战局信息
Client.GameActivity 游戏相关操作类,包括加入战局、出牌接口

代码组织与内部实现设计(类图)

网络功能相关

首先先实现一个处理请求的类,由于请求相关的代码除了部分Header和Body的内容以外几乎相同,我们将所有相关的代码全部实现在一个父类中。之后对于其他的网络功能操作类则全部继承自该父类,针对不同的请求方法尽可能的整合为一个函数,对于不能整合的则通过覆写实现(最后没有一个方法被覆写,接口做到了高耦合性)。

算法相关

在思考算法实现前,我和搭档一起进行了几局游戏,并从游戏过程中体会到了几个关键的信息:

  • 想要出牌合法且在某一墩获胜,后墩必须尽可能大

  • 确定其中两墩的牌型后,第三墩的牌一般不够优秀

  • 凑成同花以上的牌型非常困难

  • 没有任何一局比赛中出现了特殊牌型

  • 由于游戏中相同牌型的卡牌仅比较一张牌(即凑成牌型的牌),则按照贪心可以暂时不选择牌型中的非比较牌。

对于第一点,由于“相公”规则的存在,要求后墩一定要大于等于中墩,这使得后墩牌型的大小决定了中墩牌型的可能性。并且由于该游戏最大可凑出的牌是何种牌型运气要素占相当大的一部分,因此后墩不如孤注一掷,将最大的牌型打出,不仅可以凭借运气得分,还可以大大提高前中墩的牌型大小(事实上在和搭档的游戏过程中尝试过两种打法,拆散中墩的牌凑成更大的后墩牌更容易获得胜利)。

第二点很明显,第三墩的牌相当于集合中的差集,其牌型完全取决于其他两墩的取法。

三、四两点一开始我们认为是我们技术不够凑不出来,但稍微计算后发现确实难度较大:

  • 从一副牌中凑出葫芦牌的概率约为:

    \[\frac{C_{1}^{13}*C_{4}^{3}*C_{12}^{1}*C_{4}^{2}*C_{48}^{1}}{C_{52}^{5}} = 6.9*10^{-2} \]

  • 从一副牌中凑出炸弹牌的概率约为:

    \[\frac{C_{13}^{1}*C_{4}^{4}*C_{48}^{1}}{C_{52}^{5}} = 2.4*10^{-4} \]

  • 从一副牌中凑出同花顺的概率为:

    \[\frac{C_{4}^{1}*C_{8}^{1}}{C_{52}^{5}} = 1.2*10^{-5} \]

以上的算式仅计算一整副牌凑出大牌的概率,如果限定为随机的13张只会更难。连基本牌型都这么难凑,更别说特殊牌型了...

根据几天的游戏,一个“田忌赛马”风格的算法逐渐被提出(特别感谢一起打ACM的队友提供的思路):

第一步:对于发下来的牌,特判掉特殊牌型的可能性后,我们首先贪心凑出最大可能的后墩牌型,以保障前中墩的牌型尽量大,且可以赢下一水。

第二步:对于剩下的牌,如果我们能在中墩凑出葫芦以上的牌型(中墩葫芦以上的牌型得分效率高,且后墩一定大于等于葫芦),那么我们称之为好马,直接凑出最大的中墩并放弃前墩。如果不行则称之孬马,凑出最大可能的前墩牌并放弃中墩,由于前墩只有3张,对子以上的牌型便大概率可以打赢对手。

按此算法,后墩与前墩的获胜概率较大,当局期望为2水。

说明算法的关键与关键实现部分流程图

“田忌赛马”式算法:

1、首先先找出最大可能的后墩,保证前中墩的可能性,并凭借运气先拿一水

2、考虑中墩是否可以凑出葫芦以上的牌型

2.1、如果可以凑出(好马),直接中墩凑出大牌,前墩求差

2.2、如果不可以凑出(孬马),放弃考虑中墩,保证不相公的情况下凑出最大的前墩牌,尽可能在前墩获胜

UI 展示

登录与注册





大厅


菜单


牌谱(查询往期对战结果)


对战与出牌


显示当前对战状态


点击头像进入个人页面(显示往期对战结果)


显示排行榜


Part5:关键代码解释

网络功能处理代码

public class HttpUntil {
    private static HttpClient initHttpClient(){
        HttpClient httpClient = new DefaultHttpClient();
        // 设置超时时间
        httpClient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 6000);
        httpClient.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 6000);
        return httpClient;
    }

    private static HttpResponse Request(HttpUriRequest request) throws IOException{
        HttpClient httpClient = initHttpClient();
        HttpResponse response = httpClient.execute(request);
        Integer statusCode = response.getStatusLine().getStatusCode();
        if(statusCode != HttpStatus.SC_OK) {
            System.err.println("访问失败");
            throw new IOException(statusCode.toString());
        }
        return response;
    }

    protected static String postRequest(String url, ArrayList<Pair<String, String>> params)throws IOException{
        HttpPost post = new HttpPost(url);
        for(int i = 0;i < params.size();i++){
            Pair t = params.get(i);
            if(t.getKey() == "json"){
                post.setHeader("Content-type", "application/json");
                StringEntity entity = new StringEntity(t.getValue().toString(), Charset.forName("UTF-8"));
                entity.setContentEncoding("UTF-8");
                entity.setContentType("application/json");
                post.setEntity(entity);
            }else post.setHeader(t.getKey().toString(), t.getValue().toString());
        }
        return EntityUtils.toString(Request(post).getEntity());
    }

    protected static String getRequest(String url)throws IOException{
        HttpGet get = new HttpGet(url);
        return EntityUtils.toString(Request(get).getEntity());
    }

    protected static String getRequest(String url, String token)throws IOException{
        HttpGet get = new HttpGet(url);
        get.setHeader("X-Auth-Token", token);
        return EntityUtils.toString(Request(get).getEntity());
    }
}

所有的网络功能都经过上述接口完成

游戏逻辑代码(入口函数)

public class GameLogic{
	public GamePlay playing(){
        GamePlay ans = new GamePlay(gameData.getGID());
        //特判特殊牌型
        if(isDragon() || isAllBest() || isThreeBoom() || isTwoColor() ||
                isTwoThree(0,0,1) || isThreeStraight(card,1,0,3)) {
            Poker master = new Poker(Type.Tymaster);
            for (int i = 0; i < 4; i++)
                for (int j = 1; j <= 13; j++) {
                    if(card[i][j] == 0) continue;
                    master.add(i, j);
                }
            ans = new GamePlay(gameData.getGID());
            ans.add(master.toString());
            System.out.println("supper");
            return ans;
        }

        ArrayList<Poker> back = getBack();//获取最大可能的后墩牌型
        for(int i = 0;i < back.size();i++){
            Poker tback = back.get(i);
            ArrayList<Poker> middle = getMiddle(tback);//重载一:尝试获取葫芦以上的中墩牌
            if(middle != null)//如果中墩能凑出葫芦以上的牌型
                for(int j = 0;j < middle.size();j++){
                    Poker tmiddle = middle.get(i);
                    ArrayList<Poker> front = getFront(tmiddle);//获取后墩牌
                    for(int k = 0;k < front.size();k++) {
                        Poker tfront = front.get(j);
                        if (isValue(tfront, tmiddle, tback)) {//检查合法性
                            Poker ttfront = tfront.clone();
                            Poker ttmiddle = tmiddle.clone();
                            Poker ttback = tback.clone();
                            fixPoker(ttfront, ttmiddle, ttback);//补全杂牌
                            ans = max(1, new GamePlay(gameData.getGID(), ttfront, ttmiddle, ttback), ans);
                        }
                    }
                }
            else{//中墩为孬马
                ArrayList<Poker> front = getFront(tback);//获取前墩牌
                for(int j = 0;j < front.size();j++){
                    Poker tfront = front.get(j);
                    middle = getMiddle(tfront, tback);//重载二:求差中墩牌
                    if(middle == null) continue;
                    for(int k = 0;k < middle.size();k++) {
                        Poker tmiddle = middle.get(k);
                        if (isValue(tfront, tmiddle, tback)) {//检查合法性
                            Poker ttfront = tfront.clone();
                            Poker ttmiddle = tmiddle.clone();
                            Poker ttback = tback.clone();
                            fixPoker(ttfront, ttmiddle, ttback);
                            ans = max(0, new GamePlay(gameData.getGID(), ttfront, ttmiddle, ttback), ans);
                        }
                    }
                }
            }
        }
        for(int i = 0;i < 3;i++) ans.add(ans.getCard().get(i).toString());
        return ans;
    }
}

牌效计算入口


Part6:性能分析与改进

描述你改进的思路

按照一开始的思路我们采用一种贪心的算法去实现,但是实际表现并不理想,分析之后发现是算法太过保守,只能保证尽量不输掉,而不能追求分数最大化,然而十三水显然是个很看脸的赌博游戏,如果你不在抽到好牌的时候最大化收益,那么运气差的时候就会输掉好多局赢来的分数。

于是我们引入权值矩阵,对于贪心获得的所有等效牌,利用权值矩阵来比较大小获取最优的牌,在保证时空复杂度的同时确保得分最大化。

展示性能分析图和程序中消耗最大的函数

UI中字符串处理占用了大半消耗


Part7:单元测试

单元测试代码

import AI.GameLogic;
import Client.GameActivity;
import Client.HistoryActivity;
import Client.RankActivity;
import Client.UserActivity;
import Model.*;
import com.google.gson.Gson;
import com.google.gson.JsonElement;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Scanner;

public class Test {
    public static void main(String[] argc) throws IOException, InterruptedException {
        //*10 &9 &5 $6 &Q &10 &K #6 &3 &2 $K *4 $10
        //#9 &4 *Q &A *9 #8 #7 $J #A *8 &6 *5 *6
        //#2 *7 #10 #5 *J $Q #J $2 *A &J &8 #4 *2
        GameData gameData = new GameData();
        gameData.setStrCard("*2 #2 $2 $6 *J $K &4 #8 &9 $5 $7 #10 $Q");
        gameData.setGID(1);
        GameLogic ai = new GameLogic(gameData);
        GamePlay ans = ai.playing();
        new GameLogic(gameData).playing();
        System.out.println("ans");
        Gson gson = new Gson();
        JsonElement jsonElement = gson.toJsonTree(ans);
        System.out.println(jsonElement.toString());
        for (int i = 0; i < 3; i++)
            System.out.println(ans.getCard().get(i));
    }
}

单元测试代码覆盖率

利用Intellij Idea代码覆盖率工具对AI程序进行覆盖率测试,GameLogic类的覆盖率达到了93%以上,说明测试样例比较优秀,测试到了大部分逻辑代码。


Part8:Github代码签入记录


Part9:遇到的代码模块异常或结对困难及解决方法

网络功能部分

  • 困难:如何合理的设计客户端网络接口的框架,以简化实现难度
  • 解决方法:利用Java的特性,设计搭建了一个自认为比较优秀的框架

算法功能部分

  • 困难:如何最优化算法,降低算法时空复杂度的同时考虑兼顾算法的实现复杂度
  • 解决方法:用笔纸在草稿纸上搭建了一整天的算法框架,明确框架后才开始实现代码(中间还是出现了不少预期外的小问题)

结对方面

  • 鲍子涵:结对作业的困难就是在项目分工时不能使分工之间的耦合性太高,否则容易造成一方面依赖另一方面的进度导致不能并发开发,甚至出现死锁。

吴宜航:

  • 问题描述
    (1)初学Javax.swing,不太清楚UI的设计模式,在写代码的时候纠结于类的定义。
    (2)因为不喜欢多窗口,所以场景变换全部使用JPanel完成,发生各种不可描述的错误。
    (3)Java的String把中英文字符的长度都当作1,造成排行榜中的用户名长短不一,而且字符有宽有窄,对排版造成阻碍。
    (4)Live2D看板娘上线失败。

  • 做过哪些尝试
    (1)研究了一会MVC模式,决定把所有同类型的Panel继承JPanel作为一个类,而每个大场景也是独立的类。
    (2)谷歌百度B站,网络上的参考资料还是很多的。
    (3)发现了FontDesignMetrics.getMetrics.stringWidth这个方法可以获得字符串的实际(显示在屏幕上的)长度。
    (4)研究Live2D

  • 是否解决
    除了Live2D看板娘最终放弃并换上了静态抠图看板娘,其它都解决了。

  • 有何收获
    (1)了解了Javax.swing的基本设计方法。
    (2)对于Java这种不够底层的语言出现各种疑难杂症的debug思路

鲍子涵:

  • 问题描述
    (1)多种网络请求在代码中如何整合
    (2)算法如何优雅的设计与实现
    (3)GitHub多人协作的初次尝试
    (4)规范化Java工程项目
  • 做过哪些尝试
    (1)运用Java优秀的类特性将所有的网络请求整合于一个类中,所有和网络工程相关的类全部继承自这个类
    (2)设计算法流程图,拆分不同牌型的判断方法
    (3)建立了一个Organization,并在其中建立了一个repo,通过fork这个仓库来实现多人协作
    (4)将所有的类和文件根据功能整合在不同的文件夹中,变量和类的名称遵照通用的Java设计规范
  • 是否解决
    基本解决
  • 有何收获
    (1)了解Java语言的一些设计规范
    (2)初步掌握了GitHub优秀的多人协作功能

Part10:评价你的队友

吴宜航:

  • 值得学习的地方
      鲍子涵的代码能力很出色,特别是网络方面的功能,在我还没反应过来之前就把所有接口和设计文档处理好了,让我能比较舒服地摸鱼搞UI。而且我前面学得比较慢,一直在纠结一些不必要的东西,也不会来催促我,而是尽可能地提供帮助。真的非常感谢队友的辛勤劳动。

  • 需要改进的地方
      相对过于完美主义,比起先随便搞个半成品出来看看,鲍子涵更喜欢精益求精,每一步都做到较优的解,这样的坏处就是工期太赶,而且后面越来越急反而有些草率收场。

鲍子涵:

  • 值得学习的地方
      吴宜航的学习能力相当不错,短短几天的时间内不仅从对Java语言的一窍不通到熟练使用,甚至掌握了UI功能从无到精美的实现。我原本认为UI方面只要能动就行了,没想到吴宜航能将上次作业我们一起设计的原型实现了70%以上的功能,确实令人震惊。
  • 需要改进的地方
      由于两次作业的部分需求不同,上一次的原型并不能100%的与本次作业的要求相匹配,从这里可以看成吴宜航的一些缺点:虽然能比较完美的实现原型设计,但缺乏一些想象力,对于原型设计中没有提到的要求很少有自己的看法,经常询问我的观点。

Part11:学习进度条

第N周 新增代码(行) 累计代码(行) 本周学习耗时(小时) 累计学习耗时(小时) 重要成长
1 0 0 6 6 上手Axure RP,了解原型设计
2 480 480 20 26 学习java网络接口以及javax.swing
3 650 1130 35 61 实现算法框架以及基本UI界面
4 780 1910 35 96 基本完成

推荐阅读