您的位置:首页 > 其它

结对项目-数独扩展

2017-10-15 12:26 239 查看
Github地址:

Github 项目地址

时间估计

PSP2.1Personal Software Process Stages预估耗时(min)
Planning计划30
· Estimate估计这个任务需要多少时间30
Development开发1440
· Analysis需求分析 (包括学习新技术)360
· Design Spec生成设计文档120
· Design Review设计复审 (和同事审核设计文档)60
· Coding Standard代码规范 (为目前的开发制定合适的规范)0
· Design具体设计360
· Coding具体编码420
· Code Review代码复审120
Test测试(自我测试,修改代码,提交修改)740
· Reporting报告300
· Test Report测试报告400
· Size Measurement计算工作量10
· Postmortem & Process Improvement Plan事后总结, 并提出过程改进计划30
合计2010

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

UI分为两个模块,一个是进行数独游戏的主页面,另一个是负责获得输入的对话框。对话框模块向主模块传输的只有题目数组的二重指针,题目个数,题目难度(我们显示在游戏页面上),其他的诸如最大空格数,最小空格数,是否为唯一解等选项不会传给主模块,遵循Information Hiding的原则。
接口设计方面,UI主模块和UI输入模块的接口由一个自定义信号和一个自定义槽实现,输入模块响应请求,生成数独题题目后,发出自定义信号,同时传出上述三个参数,槽函数接收信号,同时接受输入模块传来的三个参数。除了UI主模块和UI输入模块之间的设计外,UI输入模块与计算模块之间通过generate接口交互(生成数独题目),UI主模块与计算模块之间通过solve接口交互,以支持提示功能。
UI主模块与UI输入模块仅通过自定义信号+自定义槽函数传递必需的三个参数而已,没有其他要求。UI模块与计算模块通过generate接口和solve接口实现交互,也是只传递必要的参数,符合松耦合的原则。

计算模块接口的设计与实现过程

类图



Sudoku9

对数独终局和问题建模,提供数独终局有效性检查,数独问题有效性检查,转化成字符串等操作。针对这次等价数独要求,又提供了归一化的函数。

DLXSolver

通用的求解精确覆盖问题的采用 DLX 算法的求解器。为了效率原因,在求解之外,实现了一个特化的 DLX 以检查是否具有唯一解的函数 。

DLX 求解过程中,采用每次选择候选项最少的列进行覆盖的启发式策略加速。

Sudoku9DLXSolver

使用 DLX 算法求解数独。其主要功能是将数独问题转化为精确覆盖问题,然后通过通用的
DLXSolver
进行求解。

Sudoku9Generator

调用
Sudoku9DLXSolver
来实现生成策略。由于生成固定难度的数独耗时较大,因此保存为文件,每次生成时从文件中读取。

生成有空数限制的数独问题策略是调用
Sudoku9DLXSolver
对第一行固定为
123456789
的数独求解生成终局,这样保证生成的每个终局一定是互补等价的。然后对总共 81 个空随机一个排列,逐一敲除后求解,确定是否为唯一解,如果是唯一解,则敲除该空,否则保留,直到尝试完所有空或者挖空数目符合要求。如果一个终局最后的挖空数目不合要求,舍弃后继续,否则算为一个合法的问题。

经观察发现,对于采用启发式策略的
DLXSolver
,大部分数独问题实际上都是可以每步通过必要条件唯一确定解的。而依靠必要条件这一过程实际上是数独游戏中的基本技巧。依据以所用技巧难度划分数独难度的分类法,无论数独的空数有多少,只用到低阶技巧的数独都是难度较低的。

根据这个分类法和实现难度,我们认为,依据空数和
DLXSolver
求解过程中经过的状态数来大致划分难度是合理的。因此难度策略如下:

简单难度:空数在 25 到 40 之间,只靠必要条件可唯一确定解。

中等难度:空数在 35 到 50 之间,
DLXSolver
经过状态数为 82 到 100 之间。

困难难度:空数在 45 以上,
DLXSolver
经过状态数在 120 以上。

实际测试,生成中等难度数独最为费时,需要 30 分钟才能生成 10000 个,因此对于
-m
参数,直接将已有的数独保存成文件,出题时直接从库里随机抽取。

由于库里的数独是固定的,虽然要求不能生成等价数独,但是等家属都的展现形式却是任意的,因此生成数独时会将数字进行重排列,避免记住终局以提高可玩性。

Core

作业要求的接口类,是对
Sudoku9DLXSolver
Sudoku9Generator
的简单封装。

计算模块接口部分的性能改进

最开始采用在 DLX 过程中根据深度产生空数在一定范围内的数独,但效果很糟糕。

后来和同学交流之后采用生成数独终局,然后随机挖空的方式,具体思路见上文
Sudoku9Generator
描述,效率有了很大提高。

由于之前在效率优化上已经做了很多,本次优化时间并不太长,大概 3h 左右。

性能分析图如下,测试命令为
-n 10000 -r 55~55 -u
,耗时最长的函数为 DLX 求解函数。



看Design by Contract, Code Contract的内容,描述这些做法的优缺点, 说明你是如何把它们融入结对作业中的。

优点:使用者和被调用者地位平等,调用者须保持参数正确,被调用者须保持结果正确和调用者要求的不变性。保证了双方代码的质量,提高了软件工程的效率和质量。[1]契约关系经常是相互的,权利和义务之间往往是互相捆绑在一起的,执行契约的义务在我,而核查契约的权力在人,我的义务保障的是你的利益,而你的义务保障的是我的利益。[2]代码契约有运行时检查,静态检查,文档生成的强大功能[3]
缺点:对程序语言有一定要求,须支持断言。
在UI模块与计算模块的连接中,只有值传递和指针传递,其余都是独立的,所以不涉及“保证调用者要求的不变性”的问题。UI模块向计算模块传递的参数只能有生成数量,难度,最小空格数,最大空格数,是否唯一解,而且输入模块以保证这些参数的值都在合法范围内,符合“调用者须保持参数正确”的要求。计算模块向UI模块传递的只是一个二维数组指针,计算模块保证了结果的正确。

计算模块单元测试

部分单元测试代码

以下代码测试的是
Core
类生成简单难度数独的功能。该单元测试依据简单数独的定义,对生成的最大数目的简单数独一次从空数,解是否唯一,是否有等价数独一次进行判断。

TEST_METHOD(CheckMode1Generate)
{
Sudoku9 puzzle, ans;
Sudoku9DLXSolver checkSolver;
Core core;
std::vector<Sudoku9Node> table;
int i, j, k, blank, N = 10000, r;
int(*result)[81] = new int[10000][81];
core.generate(N, 1, result);
char info[1000];
for (i = 0; i < N; i += 1)
{
for (j = 0; j < 9; j += 1)
{
for (k = 0; k < 9; k += 1)
puzzle.data[j][k] = result[i][j * 9 + k];
}
Assert::IsTrue(puzzle.isValidPuzzle());
for (j = blank = 0; j < 9; j += 1)
{
for (k = 0; k < 9; k += 1)
{
if (!puzzle.data[j][k])
blank += 1;
}
}
Assert::IsTrue(25 <= blank && blank <= 40, L"The number of blank is inconsistent.");
checkSolver.set(puzzle);
Assert::IsTrue(checkSolver.solve(), L"The puzzle has no solution.");
Assert::IsFalse(checkSolver.solve(), L"The puzzle has multiple solution.");
ans = checkSolver.solution();
Assert::IsTrue(ans.isValid());
table.push_back(ans.normNode());
}
std::sort(table.begin(), table.end());
table.erase(std::unique(table.begin(), table.end()), table.end());
Assert::IsTrue(table.size() == N, L"The number is inconsistent.");
delete[] result;
}

以下代码测试的是
Core
类的
solve
函数。随机挑选了一个 17 hint 唯一解的数独进行求解,对得到的解判断是否合法,以及是否确实是原来数独的一个解来验证求解功能的正确性。

TEST_METHOD(CheckCoreSolve1)
{
int i, j;
int origin[81], result[81];
Core core;
Sudoku9 puzzle(
"800000000"
"003600000"
"070090200"
"050007000"
"000045700"
"000100030"
"001000068"
"008500010"
"090000400"), ans;
for (i = 0; i < 9; i += 1)
{
for (j = 0; j < 9; j += 1)
origin[i * 9 + j] = puzzle.data[i][j];
}
Assert::IsTrue(core.solve(origin, result));
for (i = 0; i < 9; i += 1)
{
for (j = 0; j < 9; j += 1)
ans.data[i][j] = result[i * 9 + j];
}
Assert::IsTrue(ans.isValid());
for (i = 0; i < 9; i += 1)
{
for (j = 0; j < 9; j += 1)
{
if (puzzle.data[i][j] > 0)
Assert::IsTrue(puzzle.data[i][j] == ans.data[i][j]);
}
}
}

总体覆盖率



计算模块异常处理

传入了错误的数独

该异常发生的原因是传入
solve
的数独有格子不在取值范围内。该异常不负责处理明显无解的情况,比如一行有两个同样的数,该种情况属于
solve
返回
False
的无解情况。

其中一个单元测试如下:

TEST_METHOD(CheckSolveException)
{
int i, j;
int origin[81], result[81];
bool catchException = false;
Core core;
Sudoku9 puzzle(
"800000000"
"003600000"
"070090200"
"050007000"
"000045700"
"000100030"
"001000068"
"008500010"
"090000400"), ans;
for (i = 0; i < 9; i += 1)
{
for (j = 0; j < 9; j += 1)
origin[i * 9 + j] = puzzle.data[i][j];
}
origin[80] = -1;
try {
core.solve(origin, result);
}
catch (std::invalid_argument e)
{
catchException = true;
Logger::WriteMessage(e.what());
}
Assert::IsTrue(catchException);
}

传入了错误的困难模式

该异常发生的原因是在调用生成某一难度的数独时传入了错误的难度,即不是 1, 2, 3 的整数。

其中一个单元测试如下:

TEST_METHOD(CheckGenerateException1)
{
int result[10][81];
bool catchException = false;
Core core;
try {
core.generate(10, 4, result);
}
catch (std::invalid_argument e)
{
catchException = true;
Logger::WriteMessage(e.what());
}
Assert::IsTrue(catchException);
}

挖空数目不在范围内

该异常发生的原因是在调用生成挖空数目在一定范围内的数独时传入了不在范围内的上下界。

其中一个单元测试如下:

TEST_METHOD(CheckGenerateException3)
{
int result[10][81];
bool catchException = false;
Core core;
try {
core.generate(10, -1, 0, true, result);
}
catch (std::invalid_argument e)
{
catchException = true;
Logger::WriteMessage(e.what());
}
Assert::IsTrue(catchException);
}

挖空数目大小关系不正确

该异常发生的原因是在调用生成挖空数目在一定范围内的数独时传入的上下界大小关系错误。

其中一个单元测试如下:

TEST_METHOD(CheckGenerateException5)
{
int result[10][81];
bool catchException = false;
Core core;
try {
core.generate(10, 30, 0, true, result);
}
catch (std::invalid_argument e)
{
catchException = true;
Logger::WriteMessage(e.what());
}
Assert::IsTrue(catchException);
}

10.11.界面模块的详细设计过程。界面模块与计算模块的对接。(博客要求中对这两部分都有“详述UI设计的要求,所以两个问题一起回答”)

界面使用Qt插件,首先编辑.ui文件,设计好页面,再将.ui文件生成的.h文件include到主模块的头文件中,函数主要分为3类。
第一类,响应函数,响应点击按钮,键盘事件,鼠标事件,每个函数只响应一件事。下列代码是跳转按钮的响应函数:
第二类,状态更新函数,通过实时监控某位置的变化,更新另一位置的状态,比如任何填数的操作都会触发更新每个数独格子的颜色更新函数,如果此时存在同一行/列/宫内有相同的的数,则会被同时标为红色。下列代码是一个更新最佳纪录的函数:
第三类函数是具体实现功能的函数,这些函数在响应函数和状态更新函数中被调用,下列代码是检测所有格子的合法性(标红或标黑)的函数:


设计详述:

UI模块的类图如下所示:




启动程序,首先绘制输入窗口,提供各种选项,




点击帮助按钮,触发clicked信号,调用responseGetHelp函数,弹出来关于各个选项的帮助手册,




点击右上角的“x”,触发closeEvent函数响应,这个函数是重写的,为避免程序崩溃,closeEvent函数忽略关闭动作,并提示“点击ok进入游戏,点击cancel退出游戏”




点击cancel,调用reject函数响应,并退出程序。
点击ok,调用responseOK函数,从各控件上读取内容,若模式为自定义且最小空格数大于最大空格数,会提示重设。




之后调用计算模块提供的generate接口生成数独题目,为了之后传递方便,我改为了二重指针传递,生成结束后发出自己定义的generateSuccesssfully信号,传递参数给主模块,调用receiveQues函数接收参数,至此,输入模块任务结束。
responseOK为核心函数,代码如下:


void GenetateNumber::responseOK()
{

int i, j;
iGenerateNumber = ui.generationNumber->text().toInt();
if (ui.easy->isChecked())         //是简单模式?
{
iMode = 1;
goto ACCEPT;
}
else if (ui.medium->isChecked())        //是入门模式?
{
iMode = 2;
goto ACCEPT;
}
else if (ui.hard->isChecked())        //是困难模式?
{
iMode = 3;
goto ACCEPT;
}
else         //那就是自定义模式
{
iMode = 0;
iMinSpace = ui.hSliderMinSpace->value();        //读取最小空格数
iMaxSpace = ui.hSliderMaxSpace->value();        //读取最大空格数
bUnique = ui.uniqueSign->isChecked();        //读取是否要唯一解
if (iMinSpace > iMaxSpace)        //如果最小空格数大于最大空格数,提示玩家
{
QMessageBox::information(NULL, "\346\217\220\347\244\272", "", QMessageBox::Yes, QMessageBox::Yes);        //此处中文编码太长,略去
return;
}
}

ACCEPT:
int(*temp)[81] = new int[10000][81];
result = new int*[iGenerateNumber];
for (i = 0; i < iGenerateNumber; i++)
result[i] = new int[81];
if (iMode == 0)
c.generate(iGenerateNumber, iMinSpace, iMaxSpace, bUnique, temp);    //调用自定义模式的generate接口
else
c.generate(iGenerateNumber, iMode, temp);        //调用其他模式的generate接口

for (i = 0; i < iGenerateNumber; i++)
{
for (j = 0; j < 81; j++)
result[i][j] = temp[i][j];
}
delete[] temp;
emit generateSuccessfully();    //发射信号
this->accept();
}

之后进入主界面,




数独格子中的值每发生一次改变,都会调用refreshAboutSudokuBox函数更新状态,refreshAboutSudokuBox函数调用testValuechange函数为每个数字标色




鼠标放在数独格子上并右键单击,每个格子是包装过的QLineEdit控件,重写了contextMenuEvent函数,右键单击,原本是弹出右键菜单,现在是发出自定义的getTips信号同时传出该格子的行索引和列索引,进而调用responseGetTips函数响应“提示”请求,responseGetTips中调用计算模块的solve接口,若返回false,则提示之前某个格子填错了,




若返回true,则将发出信号的格子填上。重写contextMenuEvent函数代码如下:

void myQLineEdit::contextMenuEvent(QContextMenuEvent *event)
{
int rowId, colId;
if (this->isEnabled() && this->text().isEmpty())
{
colId = (geometry().left() - LEFT_MARGIN) / BOX_SIDE_LENGTH;
rowId = (geometry().top() - TOP_MARGIN) / BOX_SIDE_LENGTH;
emit getTips(rowId, colId);
}
}

内置的定时器每秒发送一个信号,调用refreshLCDCurTime函数,计时显示器加一。考虑到玩一盘数独玩24小时的情况很少见,为了表示对玩家坚持不懈解数独的尊重,无论玩家最后耗时多久,我们都将该次游戏时间记为23:59:59。
点击“上一关”,调用responsePreGame函数更新至上一关。点击“下一关”,调用responseNextGame函数更新至下一关。当前关卡序号每次发生变化,都会调用refreshPreAndNextButton,更新“上一关”和“下一关”两个按钮的状态。
编辑框内容每次发生变化,都调用refreshJump更新“跳转”按钮状态。点击“跳转”按钮,调用responseJump函数更新至指定关卡。
点击“暂停游戏”,调用responsePause函数,冻结时间,提示功能和完成按钮。点击“继续游戏”,调用responseContinue函数,解冻前述控件及功能。“暂停游戏”和“继续游戏”两个按钮不能同时有效,每次“暂停游戏”的状态发生变化,都会调用refreshContinueButton更新“继续游戏”按钮状态。
点击“再玩一组”,则调用responsePlayAgain函数,打开输入页面,重新开始。点击“帮助”按钮,调用responseGetHelp函数,显示帮助文档。点击“退出”,退出游戏。




点击“完成”,调用responseFinish函数,responseFinish函数调用testAnswer()函数检查对错,若不对,则提示玩家错误。




若正确,则提示玩家正确,调用refreshLCDMinTime更新最佳纪录,




再检查是否是最后一关,若是,提示玩家点击“再玩一局”,重新游戏,若不是,则直接更新至下一关。




键盘事件响应由keyPressEvent函数实现,支持“↑”(光标上移),“↓”(光标下移),tab(光标默认后移),ctrl(光标左移),alt(光标右移),F1(上一关快捷键),F2(下一关快捷键),F3(聚焦手动选关编辑框快捷键),F4(跳转快捷键),F6(完成快捷键),F7(再玩一局快捷键),F8(暂停快捷键),F9(继续快捷键),F10(寻求帮助快捷键)等。
最后,附上最为核心的主模块的信号槽连接代码:

bool flag = false;
flag = QObject::connect(generateDialog, SIGNAL(generateSuccessfully()), this, SLOT(receiveQues()));        //题目生成成功->接收参数
assert(flag);
flag = QObject::connect(ui._quit, SIGNAL(clicked()), qApp, SLOT(quit()));    //点击“退出”->退出应用
assert(flag);
flag = QObject::connect(ui.finish, SIGNAL(clicked()), this, SLOT(responseFinish()));    //点击“完成”->调用responseFinish函数
assert(flag);
flag = QObject::connect(timer, SIGNAL(timeout()), this, SLOT(refreshLCDCurTime()));    //计时显示器
assert(flag);
flag = QObject::connect(ui.curGameNumberContent, SIGNAL(textChanged(QString)), this, SLOT(refreshPreAndNextButton()));    //更新“上一关”“下一关”按钮状态
assert(flag);
flag = QObject::connect(ui.preGame, SIGNAL(clicked()), this, SLOT(responsePreGame()));    //点击“上一关”->调用responsePreGame函数
assert(flag);
flag = QObject::connect(ui.nextGame, SIGNAL(clicked()), this, SLOT(responseNextGame()));         //点击“下一关”->调用responseNextGame函数
assert(flag);
flag = QObject::connect(ui.chooseGameContent, SIGNAL(textChanged(QString)), this, SLOT(refreshJump()));        //更新跳转按钮状态
assert(flag);
flag = QObject::connect(ui.jump, SIGNAL(clicked()), this, SLOT(responseJump()));        //点击“跳转”->调用responseJump函数
assert(flag);
flag = QObject::connect(ui.playAgain, SIGNAL(clicked()), this, SLOT(responsePlayAgain()));        //点击“再玩一局”->调用responsePlayAgain函数
assert(flag);
flag = QObject::connect(ui.pauseButton, SIGNAL(clicked()), this, SLOT(responsePause()));         //点击“暂停游戏”->调用responsePause函数
assert(flag);
flag = QObject::connect(ui.continueButton, SIGNAL(clicked()), this, SLOT(responseContinue()));        //点击“继续游戏”->调用responseContinue函数
assert(flag);
flag = QObject::connect(ui.getHelp, SIGNAL(clicked()), this, SLOT(responseGetHelp()));         //点击“获取帮助”->调用responseGetHelp函数
assert(flag);

for (i = 0; i < NUMBER_OF_ROWS; i++)
{
for (j = 0; j < NUMBER_OF_COLUMNS; j++)
{
flag = QObject::connect(ui.sudokuBox[i][j], SIGNAL(textChanged(QString)), this, SLOT(refreshAboutSudokuBox()));        //更新数独格子颜色
assert(flag);
flag = QObject::connect(ui.sudokuBox[i][j], SIGNAL(getTips(int, int)), this, SLOT(responseGetTips(int, int)));        //右键单击->调用responseGetTips
assert(flag);
}
}

描述结对的过程,提供非摆拍的两人在讨论的结对照片。

由于上一次作业就是因为没接触过 GUI 因此没做,因此这次拿到题目后一脸懵逼。我们稍加讨论后认为前期并不适合结对编程,于是决定兵分两路。由于个人项目刘子渊同学的效率比较高,因此决定由他来研究如何生成数独,我负责调研 GUI 相关工具,各自按接口开发的差不多了再对接。
由于第一周接着国庆假期,各自都有安排,因此在时间上也比较适合各自钻研,事实也证明这样的效率是比较高的。
在大致有了架子之后,我们在放假后的一周通过结对编程完成了后续的对接、修改,单元测试和代码复审等工作。




看教科书和其它参考书,网站中关于结对编程的章节,说明结对编程的优点和缺点。结对的每一个人的优点和缺点在哪里 (要列出至少三个优点和一个缺点)。

结对编程优点:
1.在开发层次,结对编程能提供更好的设计质量和代码质量,两人合作能有更强的解决问题的能力。[4]
2.对开发人员自身来说,结对工作能带来更多的信心,高质量的产出能带来更高的满足感。[4]
3.在心理上,  当有另一个人在你身边和你紧密配合, 做同样一件事情的时候,  你不好意思开小差, 也不好意思糊弄。[4]
4.在企业管理层次上,结对能更有效地交流,相互学习和传递经验,能更好地处理人员流动。因为一个人的知识已经被其他人共享。[4]
结对编程缺点:
1.结对编程适合水平相近的人组队,如果水平相差过大,效率反而更低。

我的结对对象是刘子渊同学。
优点:
1.c++使用娴熟
2.算法能力强,使用高效的算法
3.善于沟通,遇到我不明白的地方很耐心的给我讲解
缺点:
1.思维比较活跃

用户反馈及改进

我们收到反馈的问题及相应的改进如下:
1.发布之后得到一些用户的反馈说保存记录的功能做的不彻底,只能在一次游戏中保持,关了之后就没有了。得到反馈之后花了一些时间加入了将记录存至文件后重新读取的功能。
2.还有反馈说输入页面没有提示用户输入数独数目的范围,不太方便,对此我们在输入页面提示框中增加“(1-10000)”提示用户。
3.数独游戏页面输入0是非法的,我们之前的程序虽然检查的时候会认定其非法,但并不会将其标红,对此,我们在实施检查时增加对填0情况标红。

14.实际花费时间

PSP2.1Personal Software Process Stages预估耗时(min)
Planning计划3030
· Estimate估计这个任务需要多少时间3030
Development开发14401635
· Analysis需求分析 (包括学习新技术)360430
· Design Spec生成设计文档12060
· Design Review设计复审 (和同事审核设计文档)6040
· Coding Standard代码规范 (为目前的开发制定合适的规范)00
· Design具体设计360405
· Coding具体编码420490
· Code Review代码复审120210
Test测试(自我测试,修改代码,提交修改)740410
· Reporting报告200180
· Test Report测试报告300220
· Size Measurement计算工作量1010
· Postmortem & Process Improvement Plan事后总结, 并提出过程改进计划3030
合计20102105

15.参考链接

[1]http://www.makaidong.com/%E5%8D%9A%E5%AE%A2%E5%9B%AD%E6%8E%92%E8%A1%8C/22570.shtml
[2]http://www.nowamagic.net/internet/internet_ContractProgramming.php
[3]http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
[4]http://www.cnblogs.com/xinz/archive/2011/08/07/2130332.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: