结对项目-数独程序扩展
Runnable on x64 Only
sudoku17.txt 须放置在可执行文件同目录中,可移步以下链接进行下载
Core-Github项目地址
GUI-Github项目地址
Github说明中有详细运行规定,请运行前仔细查看。感谢留意。
时间预估
PSP2.1 | Personal Software Process Stages | 预估时间(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | |
· Estimate | · 估计这个任务需要多少时间 | 60 | |
Development | 开发 | 1590 | |
· Analysis | · 需求分析 (包括学习新技术) | 60 | |
· Design Spec | · 生成设计文档 | 0 | |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | |
· Design | · 具体设计 | 180 | |
· Coding | · 具体编码 | 900 | |
· Code Review | · 代码复审 | 300 | |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | |
Reporting | 报告 | 250 | |
· Test Report | · 测试报告 | 180 | |
· Size Measurement | · 计算工作量 | 10 | |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | |
合计 | 1670 |
接口设计
- Information Hiding:编译生成动态链接库之后,外部只可通过dll调用上述接口,具体接口的实现方式被封装起来;
- Interface Design: 考虑到跨语言、跨平台性,接口函数的返回值均为基本数据类型,避免了数据转换错误;接口函数名有意义,直接表达该接口意义;
- Loose Coupling:根据 Wikipedia-Loose Coupling
An example of tight coupling occurs when a dependent class contains a pointer directly to a concrete class which provides the required behavior. The dependency cannot be substituted, or its "signature" changed, without requiring a change to the dependent class. Loose coupling occurs when the dependent class contains a pointer only to an interface, which can then be implemented by one or many concrete classes.
本程序耦合度低,除solve
函数之外,其余接口对调用者传入参数无其它行为要求,即传入任何符合数据类型定义的参数皆可,降低了耦合度;而solve
接口第一个参数则有一定的行为要求:如挖空处须置0等。
计算模块接口的设计与实现过程
Core中的所有接口如下:
//对应-m -n参数
void generateM(int number, int mode, int result[][81]);
//对应-r -u -n参数
void generateR(int number, int lower, int upper, bool unique, int result[][81]);
//对应-c 参数
void generate(int number,int result[][81]);
//对应-s 参数
bool solve(int puzzle[], int solution[]);
//检查终局正确性
bool check(int board[81]);
//获取格子可行解
int getFeasible(int board[81], int x, int y)
各个接口通过SudokuSolver和SudokuBoard两个类来实现。SudokuBoard作为数独棋盘的表示,包括一些获取数独特性的方法。SudokuSolver的核心是回溯搜索+剪枝的求解方法。
生成数独终局
随机在数独棋盘上摆放一些合法的数字,使用求解算法求解,每当得到一个合法的数独终盘,就记录下来,然后继续回溯,直到生成的个数满足要求。这样生成的数独可以满足一定的初局要求。
生成特定难度
我们定义数独的难度是求解算法的回溯次数。三个难度等级分别对应三个不同的回溯次数范围。生成的时候,首先随机生成一个数独终局,然后随机挖空一定量的格子,求解数独获得回溯次数,如果满足当前难度规定的次数范围,就记录下来。不断生成直到满足数量要求。
但由于高难度的数独较难生成,当要求数量较多时,无法快速完成。这里我们提前存储了一些高难度数独,需要时直接读取文件输出。
生成给定挖空范围+唯一解
生成给定挖空数量范围的要求上节已经完成。
生成唯一解的思路是这样的,还是先生成一个数独终局,然后对所有格子进行尝试,每次挖空一个格子,使用求解算法判断是否有唯一解,如果有,那么继续挖,否则就回填这个格子,这时我们认为这个格子是对唯一解有贡献的。当尝试完所有格子以后,如果满足数量要求,则返回,否则重新从头开始。
UML图
我们的计算模块就这一丢丢,不允许雷同也得雷同了。。
计算模块接口部分的性能改进
以下是运行-r 55~55 -u -n 10000时的性能分析图
这里可以看到,最耗时的函数是getFeasible,这一点和之前的作业相同。。。暂时没有什么办法去优化了。
Design & Code by Contract
- Design by Contract(DbC) 优点在于通过如下信息规定了软件各个函数、类的规格;
- Acceptable and unacceptable input values or types, and their meanings
- Return values or types, and their meanings
- Error and exception condition values or types that can occur, and their meanings
- Side effects
- Preconditions
- Postconditions
- Invariants
- (more rarely) Performance guarantees, e.g. for time or space used
因此Dbc能够通过对上述条件的检查判断程序运行或模块的正确性,并且能够促进代码的重用。实现DbC的方式可通过Assertion来进行,然而因为一个程序不可能一个bug都没有,Assertion对程序的正确性则显得要求得过于严苛。
- Code Contract 是微软开发的一款代码契约检查器,可检查程序执行过程中是否符合规定好的代码七月。优点在于:将契约式编程规范化;可用在所有.NEt框架实现的软件或程序中;Code Contract大大节省了时间,其对变量旧值的访问非常简单,因此可以轻易地检查前置、后置条件及对象不变式;契约也能被文档生成器提取为文档信息。 缺点:会造成额外时间开销。
计算模块部分单元测试
void testArgM()
{
printf("test generate(10000, 1, result)
");
SudokuSolver::generate(10000, 1, re);
for (int i = 0; i < 10000; i++)
{
SudokuSolver::solve(re[i], sol);
if(!SudokuSolver::check(SudokuBoard(sol)))
printf("fail
");
}
printf("test generate(1000, 2, result)
");
SudokuSolver::generate(1000, 2, re);
for (int i = 0; i < 1000; i++)
{
SudokuSolver::solve(re[i], sol);
if(!SudokuSolver::check(SudokuBoard(sol)))
printf("fail
");
}
printf("test generate(100, 3, result)
");
SudokuSolver::generate(1000, 3, re);
for (int i = 0; i < 1000; i++)
{
SudokuSolver::solve(re[i], sol);
if(!SudokuSolver::check(SudokuBoard(sol)))
printf("fail
");
}
}
void testArgR()
{
printf("test generate(10000,20,55,false,result)
");
SudokuSolver::generate(10000, 20, 55, false, re);
for (int i = 0; i < 10000; i++)
{
SudokuSolver::solve(re[i], sol);
if(!SudokuSolver::check(SudokuBoard(sol)))
printf("fail
");
}
printf("test generate(10000,20,55,true,result)
");
SudokuSolver::generate(10000, 20, 55, true, re);
for (int i = 0; i < 10000; i++)
{
if (!SudokuSolver::isU(SudokuBoard(re[i])))
printf("fail
");
SudokuSolver::solve(re[i], sol);
if(!SudokuSolver::check(SudokuBoard(sol)))
printf("fail
");
}
}
check判断终局是否合法,isU判断是否有唯一解。
使用两种生成方式的各种参数组合,生成解,若要求唯一则使用isU判断,调用solve求解,最后用check判断。
计算模块部分异常处理
计算模块接口的异常有以下几种:
- generate函数number错误
number应在[1,10000]之间。
测试用例:0,-1,+100,100000
- solve函数传入数独不合法
数独每个数字应在1~9之间
测试用例:int input[81] = {10};
还有其他的异常在输入处理的时候抛出。
界面模块详细设计过程
界面模块由布局从左至右,利用C# WPF实现:9x9的数独板、其它信息或选项操作板及排行版信息板。
-
9x9数独板:用于显示当前数独局,81个方格由
TextBlock
和TextBox
组成,局中不可修改方格由TextBlock
显示,不可修改;局中需要用户填数的方格由TextBox
组成,可以修改,但仅可填写一个字符(用户可以填入任何字符,填入非数字字符程序不会提示,任何情况下该方格不会被判为正确)。之所以使用TextBox
和TextBlock
进行不同情况的显示,是因为这两个组件的样式在父组件Grid
中直接定义了,切换数独局时只须设置其显示值和可见性即可,还严格保证了用户不得修改其他数字;每一个用户可填处左上角均绑定了一个蓝色Button
,点击即可查看当前情况下(全局)该空格能填写的所有数字。每一个格子都须点击后由键盘进行输入。 -
排行榜信息:用于显示用户利用本程序玩数独的记录。排行榜利用
TabControl
组件进行三个难度榜单的切换显示,每一个Tab
下由ListView
组件绑定GridView
来显示表头(Rank Name Time)和记录(按排名由小到大,排名按时间由短及长)。排行榜信息来自于与可执行文件同目录下的Rank.txt
,文件中排行榜存储方式固定。若文件不存在,则程序初始化时会使用默认的排行榜单,并在可执行文件目录下新建Rank.txt
文件;若文件存在但内容不符合程序内部规定的存储方式,程序也会使用默认的排行榜单,并将默认榜单覆盖Rank.txt
文件。
-
信息查看及选项操作板:
-
计时器:用于记录用户解数独的耗时。计时开始:从程序展示一个新数独局开始计时;计时暂停:用户提交该盘数独;计时继续:用户提交该盘数独但不正确,则用户继续做本题,计时器继续;计时停止:用户提交该盘数独且合法。计时器实现:利用
DispatcherTimer
启动另一个线程来进行计时,设置count
整形变量记录经过的step
次数,来还算时间。本程序中step
设置为1000ms。
-
难易度显示板:利用
TextBlock
组件实现,显示当前难度:Easy, Medium, Hard。 -
提示板:利用3x3的
TextBlock
组件实现,当用户点击9x9数独板中须填数格左上角的蓝色Button
后,提示板中绿底数字即为可填数字,此时提示板显示的提示信息对应的须填格左上角按钮变为红色以提示用户。利用提示板左上方的ClearButton
可清除提示信息。
-
提交按钮:利用
Button
实现,并绑定相应函数进行事件响应提交操作。若提交的数独终局答案正确,则弹出子窗口(Window
组件实现)提示用户正确、难度、用时并输入玩家名称,若用户不输入则默认为“MyName”;若不正确,则弹出提示不正确,玩家继续修改数独。
-
难度选择:难度选择板块由
RadioButton
组件实现,提供单选Easy, Medium, Hard的下一局难度选择。利用Checked
属性与响应函数进行绑定,默认情况(初始)为Medium难度。 -
换题按钮:利用
Button
按钮实现,获取难度并生成下一个数独局并铺至9x9数独板上。
-
界面模块与计算模块的对接
界面模块由C#实现,核心计算模块由Cpp实现。C#通过调用Cpp生成的Core.dll
来进行交互对接。
Core.dll
定义见接口设计节,UI界面共使用三个接口:
void generateM(int number, int mode, int result[][81]);
用以生成number个mode难度的数独,存储在result中。
bool check(int board[81]);
用以检查board数独是否正确。
int getFeasible(int board[81], int x, int y);
获得x行y列的格子处可填的数字。
C#调用方法:
[DllImport(@"Core.dll", EntryPoint = "check")]
extern static bool check(int[] board);
[DllImport(@"Core.dll", EntryPoint = "generateM")]
extern static void generateM(int number, int mode, ref int board);
[DllImport(@"Core.dll", EntryPoint = "getFeasible")]
extern static int getFeasible(int[] board, int x, int y);
可以看到generateM
函数第三个参数在Cpp中定义时为二维整型数组,而在C#调用时却使用ref int
来进行转换。如下:
int[,] tmp = new int[1,81];
generateM(1, diff, ref tmp[0,0]);
其中tmp
是一个C#中对应的二维整型数组结构,其与int[][]
结构最大的区别在于每个元素的维度必须相同,刚好符合第三个参数int result[][81]
的定义。
其它调用:
public bool CheckValidation(int[] board)
{
return check(board);
}
UI响应:
结对编程总结
-
结对编程优点:两个人共同解决一个问题,两个人的能力在不同方面得以施展,能够提高开发效率;能考虑到平时考虑不到的问题,这与团队项目还是有一些不同的。领航员与驾驶员的角色使两人之间除了各司其职,更是互补与促进;一人coding另一人观察,除了能及时发现逻辑错误之外,对code style也能起到好的约束。好的习惯互相学习,坏的习惯就尽量改掉,两人平起平坐,能互相学到不少。
-
结对编程缺点:需要的时间会比两个人各做一个模块再合起来长一些;如果结对编程的双发有矛盾难以化解,难以停下来倾听并讨论对方的意见,那么效率更会大打折扣。
-
本次结对编程优点:队友实现速度快,代码经验丰富,为咱的
Core.dll
做出了巨大的贡献;任劳任怨,愉快沟通;言简意赅,做事认真专心专业。 -
本次结对编程不足:小有拖延症....
实际花费时间
PSP2.1 | Personal Software Process Stages | 预估时间(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 90 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 90 |
Development | 开发 | 1590 | 1460 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 180 |
· Design Spec | · 生成设计文档 | 0 | 0 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 120 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 180 | 180 |
· Coding | · 具体编码 | 900 | 500 |
· Code Review | · 代码复审 | 300 | 300 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 150 |
Reporting | 报告 | 250 | 220 |
· Test Report | · 测试报告 | 180 | 150 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 1900 | 1770 |