结对项目——数独扩展

  1. GitHub地址:https://github.com/Liu-SD/SudoCmd (这个地址是命令行模式数独的仓库,包含了用作测试的BIN。DLL核心计算模块地址是:https://github.com/Liu-SD/SudoCore ,UI界面项目地址是:https://github.com/Liu-SD/SudoUi 。)

  2. PSP表格放在文末。

  3. 看教科书和其它资料中关于Information Hiding, Interface Design, Loose Coupling的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。

    • 我们在编程中将程序分为了多个模块。算法部分就分为了十字链表模块和算法模块。他们之间是被调用和调用的关系。最后算法被封装为一个find方法。find的参数为生成数独的数量,是否加入随机性以及对结果做操作的函数指针。返回布尔值。当找到了要求数量的数独终局时为真否则为假。辅以addRestrict和clearRestrict方法就可实现数独生成时的限制以及清楚限制。再上层封装了三个接口分别为generate和solve。提供数独的生成功能。在UI模块只通过调用接口实现计算模块功能。这种代码结构使得项目的逻辑划分清晰。且为松耦合。
  4. 计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。

    • 在数独生成算法中加入随机性,提高可玩性:

if (withRandom)
shuffle(crossPtr, crossPtr + min_col_count,
std::default_random_engine(std::chrono::system_clock::now().time_since_epoch().count()));
```
- 在原始的dlx算法中加入这一部分,使得在选中最少的列后,不再按之前的那样从该列第一个元素开始压到栈中,而是将该列元素打乱,随机挑选一个压如栈中。此做法可以实现数独生成算法的随机性且几乎不会影响算法效率。但同样存在一定问题,即算法使用的是递归回溯,使得同一批生成的数独有很大的相似性。所以在上层生成多个随机数独时应该多次调用该算法,每次生成一个数独。
- 以下是程序结构图:

  1. 画出UML图显示计算模块部分各个实体之间的关系(画一个图即可)。

  2. 计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。

    • 在generate_r接口中的两个参数k_maxtry和k_maxback两个参数可以确定在每个数独终局上尝试挖空的次数。如果这两个数字设置的过大,那么可能会在一些很难挖出唯一解的数独上浪费时间。如果这两个数字设置过小,就可能会造成数独的生成次数过多。最终参数优化结果是k_maxtry = 7和k_maxback = 3组合的耗时最少。
  3. 看Design by Contract, Code Contract的内容:描述这些做法的优缺点, 说明你是如何把它们融入结对作业中的。

    • 契约式编程。即在编程实现之前,将接口的规格确定好。调用方有责任满足规格要求的输入条件,被调用方则必须在正确输入的条件下输出正确的结果。这使代码的结构清晰,并且在出现问题时能很快确定错误的位置和原因。
    • 在我们的结对作业中,接口做了输入的判断处理。如果出现问题可以很快确定错误是在调用方还是在被调用方。
  4. 计算模块部分单元测试展示。

    • 以下是单元测试部分代码:

TEST_METHOD(TestGenerate3)
{
int START[4] = { 0, 20, 30, 40 };
int **result;
result = new int[100];
for (int i = 0; i < 100; i++)
result[i] = new int[81];
for (int mode = 1; mode < 4; mode++) {
generate(100, mode, result);
int rstr[81][3] = { 0 };
int rstr_p = 0;
for (int i = 0; i < 100; i++) {
rstr_p = 0;
for (int j = 0; j < 81; j++) {
if (result[i][j]) {
rstr[rstr_p][0] = result[i][j];
rstr[rstr_p][1] = j / 9 + 1;
rstr[rstr_p][2] = j % 9 + 1;
rstr_p++;
}
}
Assert::AreEqual(81 - rstr_p >= START[mode], true);
DLX.addRestrict(rstr_p, rstr);
Assert::AreEqual(DLX.find(1, false, NULL), true);
DLX.clearRestrict();
}
}
for (int i = 0; i < 100; i++)delete[] result[i];
delete[] result;
}
TEST_METHOD(TestGenerate5)
{
int uppers[10] = { 20,55,55,55,55,55,20,20,46,50 };
int lowers[10] = { 20,55,20,20,46,46,20,20,40,40 };
for(int j=0;j<10;j++){
bool unique = j%2==0;
int upper = uppers[j];
int lower = lowers[j];
int number = 1000;
//这里可以把数字调小点,如果时间慢的话
int **result = new int
[number] {0};
for (int i = 0; i < number; i++)
{
result[i] = new int[81]{ 0 };
}

			generate(number,lower,upper,unique,result);
			for (int i = 0; i < number; i++) {
				int rstr[81][3] = { 0 };
				int rstr_p = 0;
				for (int j = 0; j < 81; j++) {
					if (result[i][j])
					{
						rstr[rstr_p][0] = result[i][j];
						//bug 2 下标都写错为了0
						rstr[rstr_p][1] = j / 9 + 1;
						rstr[rstr_p][2] = j % 9 + 1;
						++rstr_p;
					}
				}
				DLX.addRestrict(rstr_p, rstr);
				//1.挖空数量是否足够
                if(!unique)
				    Assert::AreEqual(rstr_p>=(81-upper)&&rstr_p<=(81-lower),true);
                else
				//2.是否满足唯一解要求
                    Assert::AreEqual(DLX.find(1, false, NULL), true);
				if (unique) {
					Assert::AreEqual(DLX.find(2,false, NULL), false);
				}
				DLX.clearRestrict();
			}
		}
	}
```
- 单元测试结果如图:
![](http://images2017.cnblogs.com/blog/1220200/201710/1220200-20171014223928965-162294694.jpg)
  1. 计算模块部分异常处理说明。
    • 在这次异常处理中,除了极少部分的异常,其他的异常我都专门写在了命令行参数的处理模块中,下面我详细的分析一下异常部分的处理。
      这次的外部输入,可能就是命令行参数的输入了,而我们这次根据命令行要实现的具体功能就是四个:

      1. -c 生成数独终局
      2. -s生解决数独文件中的数独游戏
      3. -m和-n根据难易程度生成数独游戏
      4. -r和-n(-u)根据挖空数生成数独游戏
    • 同时根据我之前所定义的错误的两种分类①功能性参数(-c -m -n等)的组合错误②内容型参数(-n后面的数字,-m后面的123等)的内容错误,我们可以从这两个角度很容易的总结出所有可能的异常,思路如下:

      1. 如果参数的数量不符合规定,那么会报出参数太少的错误。

      2. 参数的格式需要符合要求,即除了-u以外,其他的在功能性参数后必须要有内容型参数,如果不符合要求,那么抛出参数格式不对的错误

      3. 参数格式都对了以后,就看参数的内容是不是有问题,这里就要一个功能一个功能分析:

        • 对于-c,我们规定它后面的数字字符串转化为数字后必须要在1~100*10000,所以我们需要检验这个数字字符串是不是符合要求,注意这里不仅要保证数字范围不能过大,尤其需要注意的是需要保证这个数字范围不能超过int,这个在OO中已经很熟练了。所以这里可能抛出数字超出范围的错误
        • 对于-s,我们规定后面的字符串必须要是有效的文件名,即文件必须存在。这个我并没有在命令行参数的处理模块中规定,因为结对伙伴已经告诉我他在solve接口中判断了,所以这里没有检查文件名是否存在
        • 对于-r,我们规定后面必须是“x~y”的格式,所以不符合这个格式的会抛出参数格式不对的错误,而对于x和y不仅需要判断是不是有数字范围超出的错误,还要保证lower必须要小于等于upper,如果不满足需要抛出lower大于upper的错误
        • 对于-n,我们需要规定判断后面的数字字符串是不是满足1~10000的范围限制,具体的错误定义和之前的-c一致
          对于-m,我们同样需要规定后面的数字字符串是不是满足在1~3的范围内
        • 对于-u,其实查不出什么错
      4. 功能性参数都检测好后,接下来需要做的就是检查参数的组合问题,组合不正确需要抛出参数组合错误。

    • 综上,我们可以定义出5种错误:

      1. 参数太少:命令行输入: sudoku.exe -c

try{
paraHandle(argc,argv,req);
}catch(too_few_para &e){e.what();}
会捕获到对应的异常
```

2. 参数格式不对:命令行输入: sudoku.exe aaa
```

try{
paraHandle(argc,argv,req);
}catch(format_err &e){e.what();}
3. 数字超出范围:命令行输入: sudoku.exe -c 100000000000000
try{
paraHandle(argc,argv,req);
}catch(out_of_range &e){e.what();}
4. lower大于upper:命令行输入: sudoku.exe -r 55~54 -n 100
try{
paraHandle(argc,argv,req);
}catch(lower_biggerthan_upper &e){e.what();}
5. 参数组合错误:命令行输入: sudoku.exe -c 100 -s puzzle.txt
try{
paraHandle(argc,argv,req);
}catch(combination_err &e){e.what();}
```

  1. 界面模块的详细设计过程。
    • 界面模块的重点在于数独棋盘的设计。我们设计的棋盘由81个pushButton组成。每个按钮使用styleSheet做不同的变形。在相应函数中,使用正则匹配和修改styleSheet从而实现按钮式样的动态变化。这里贴几个按钮的stylesheet:

border-style:solid;
border-color:black;
border-width:1px;
border-top-width:2px;
border-top-left-radius:10px;
border-left-width:2px;
```
- 下面是一个动态修改stylesheet的代码示例:

const std::regex bg("(background-color:).+?;\\n");
QPushButton *pb = board_[i];
std::string styleSheet = pb->styleSheet().toStdString();
styleSheet = std::regex_replace(styleSheet, bg, "$1" + none_rstrColor+ ";\n");
pb->setStyleSheet(styleSheet.c_str());
  1. 界面模块与计算模块的对接。

    • 将计算模块封装到DLL中,然后在界面模块动态调用DLL。以下是调用DLL中函数的过程:
    typedef void(*GENERATE_M) (int, int, int**);
    typedef void(*GENERATE_R) (int, int, int, bool, int**);
    typedef bool(*SOLVE_S) (int *, int *);
    
    HMODULE coreDLL;
    GENERATE_M generate_m = NULL;
    GENERATE_R generate_r = NULL;
    SOLVE_S solve_s = NULL;
    
    coreDLL = LoadLibrary(TEXT("Core/SoduCore.dll"));
    generate_m = (GENERATE_M)GetProcAddress(coreDLL, "generate_m");
    generate_r = (GENERATE_R)GetProcAddress(coreDLL, "generate_r");
    solve_s = (SOLVE_S)GetProcAddress(coreDLL, "solve_s");
    
    FreeLibrary(CoreDLL);
    
    
    void MainWindow::initBoard(int mode, bool unique){
    ...
    if(unique){
        int difficultyDivide[4] = {20, 32, 44, 56};
        generate_r(1, difficultyDivide[mode - 1], difficultyDivide[mode] - 1, true, &originBoard);
        }
        else{
            generate_m(1, mode, &originBoard);
        }
    ...
    }
    void MainWindow::newGame(){
    ...
    if(currentindex<3){
        int difficultyDivide[4] = {20, 32, 44, 56};
            generate_r(1, difficultyDivide[currentindex ], difficultyDivide[currentindex+1] - 1, true, &originBoard);
        }
        else{
            generate_m(1, (currentindex%3)+1, &originBoard);
        }
    ...
    }
    
  2. 描述结对的过程,提供非摆拍的两人在讨论的结对照片。

    • 话不多说,直接贴照片:
  3. 说明结对编程的优点和缺点。结对的每一个人的优点和缺点在哪里 (要列出至少三个优点和一个缺点)。

    • 优点:
      • 编程的人就只是编程,只需要考虑当前问题,而不需要思考整个项目的结构。而在旁边指挥的人则要引导编程者不出现错误。这可以提高整个的编程效率。
    • 缺点:
      • 一个人的消极情绪可能会影响另一个人。从而导致项目开发进行不下去。
    • 解的优点:
      • 行动迅速,思维敏捷,代码结构清晰,注释完善。
    • 解的缺点:
      • 想得多做得少。
    • 刘的优点:
      • 积极主动,有较高的审美水平(哈哈哈哈哈),学习能力强。
    • 刘的缺点:
      • 代码没有注释。
  4. 完整PSP表格。

personal software process stages 预估耗时 实际耗时
计划
- 估计这个任务需要多少时间 30 min 40 min
开发
- 需求分析(包括学习新技术) 300 min 300 min
- 生成设计文档 0(没做设计文档) 0
- 设计复审(和同事审核设计文档) 0(没有同事复审) 0
- 代码规范(为目前的开发制定合适的规范) 50 min 60 min
- 具体设计 300 min 400 min
- 具体编码 1000 min 1500 min
- 代码复审 120 min 400 min
- 测试(自我测试,修改代码,提交修改) 120 min 400 min
报告
- 测试报告 300 min 400 min
- 计算工作量 0(没做这项工作) 0
- 事后总结,并提出过程改进计划 30 min 30 min
合计 2250 min 3530 min
  • 一些GUI截图:

第四阶段博客:

  • 合作小组两位同学学号:15061119 15061104

  • 代码合并的过程有些曲折...我们本想在qt creator上修改相应的代码引入他们的lib,但是因为至今仍然不明的原因,qt一直不能争取的引入这个lib,所以不得已我们把我们的代码转移到VS上,使用VS的qt插件,修改相应代码就可以正确的使用他们的lib了。

  • 经过测试,他们的模块没有大的问题,但是有两点不足:

    1.根据数独的生成情况可以看出他们的数独游戏的终局生成没有加入随机的因素,即生成数独的顺序是按照固定的顺序回溯得到的,这样导致可玩性有些下降。

    2.他们的生成唯一解的数独算法最后生成的数独空的分布不均匀,它们的挖空总是集中在上半部分。

  • 以下是唯一解模式下的截图:

第五阶段博客:

  • 我们把我们的数独程序介绍给周围人玩,收到了如下反馈:

  • “没有支持键盘输入很不爽”。关于这一点,因为截止时间快到了,所以在上交的版本还没有时间键盘输入,不过之后一定会实现键盘的输入的。

  • “提示错误时把一整行都变红了,感觉很不舒服”。这里我们把这部分改了,原来是把错误的行/列/九宫格变红,现在是仅仅把错误重复的两个数字变红,这样能舒服很多

  • “没有新手引导”。新手引导目前只能加上一个help按钮,给你说如何操作,更详细的引导之后再加。

  • “太难了,不玩了不玩了”。这位同学连easy模式都觉得难,我觉得这是他自己的问题:)

  • “remind me次数是不是需要加个限制”,这个限制之后会加。

  • “有个bug,gui刚打开就可以点击数独格子了”。这确实是个bug,在用户设定好模式之前,不能让填数独的格子Enable。

posted @ 2017-10-14 22:55  LiuSD  阅读(297)  评论(2编辑  收藏  举报