结对项目-数独程序扩展
结对项目-数独程序扩展(要求细化更新)
Github项目地址
https://github.com/ZhaoYi1031/Sudoku_Pair
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 30 |
Development | 开发 | 2640 | 2280 |
· Analysis | · 需求分析 (包括学习新技术) | 480 | 600 |
· Design Spec | · 生成设计文档 | 120 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 60 |
· Design | · 具体设计 | 240 | 300 |
· Coding | · 具体编码 | 1200 | 900 |
· Code Review | · 代码复审 | 180 | 120 |
· Test | · 测试(自我测试,修改代码,提交修改) | 300 | 180 |
Reporting | 报告 | 240 | 150 |
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 60 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 120 | 60 |
合计 | 2940 | 2460 |
难度(初级/中等/复杂)界定
关于难度的界定,我们小组没有只采取空格数界定难度的方式,因为数独的难度空格多不一定难的. 比如 欧泊颗数独这里列的全是17数的数独,但玩入门级与骨灰级,根本不是一个概念。
因此我阅读了一些相关资料,从这篇博文,又引入了自由度的概念。
数独的空格自由度,指除掉空格本身,空格所在行、列、九宫内的空格数总和。
因此,当只剩一个空格时,此时的自由度为0;当数独全为时,空格数为81,空间自由度为81*24=1944达到最大。
自由度越大代表数独越难,但自由度和空格数不完全程正比。因此那篇博主进行了自由度的阶段区分,并在后文中验证了模型的合理性。
我们关于自由度的定义和他略有不同,主要在于他在算同一行同一列的自由度时,算入了自己。而我们没有。他最终分了10个等级,我们要求的是3个。因此我们最终的难度设定如下:
简单 | 中等 | 复杂 | |
---|---|---|---|
空格数范围 | [40,49] | [50,56] | [56,65] |
自由度范围 | [0,650] | [651,999] | [1000,1944] |
看教科书和其它资料中关于Information Hiding, Interface Design, Loose Coupling的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
Information Hiding:
信息隐藏使一个类把复杂的、敏感的、一旦被外界捕获可能会引起不良后果的内容封装在自身内部,这个类以外的代码得不到此类信息(通过反射等手段可能对得到),以提高程序的安全性与健壮性。
Interface Design:
接口设计是定义一套公共的接口,方便各个模块之间的通讯,极大的提高了团队的效率。
Loose Coupling:
松耦合使程序间以及程序内部都能够独立于其他程序,增加程序的可维护性和移植性。
在sudoku.h中对Core类的成员函数和成员变量进行申明,并在sudoku.cpp中完成这些成员函数的实体,再生成动态链接库dll文件从而实现信息的隐藏。核心模块中的Core类中封装了generate和solve接口给UI模块使用,方便了核心模块与UI模块的通讯,也使得程序相对独立,增强了程序的可维护性和移植性。
计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。
首先主要的生成&求解仍然采用之前的回溯的方法,求解方面加了一点优化,关于这方面的具体思路见博客数独_个人项目。然后针对这次的新要求,在生成具有限制个数的数独时,我采用的方法就是先生成足够多的满数独,然后进行挖空。其中挖空的策略仍然采用回溯:如果要挖空number个格子,那么每个格子被挖空的几率就是number/81,我们每次
都按照这个几率去决定格子的去留,执行一遍dfs. 一点小优化就是如果发现后面的格子都填上才能刚好够number,就直接挖空;如果空的格子已经达到了number, 那么之后的就直接保留。
流程图如下:
具体的Core类的所有函数即功能如下:
bool find(int x, int y, int z); //判断数独[x][y]的位置能否填z
void init_gen(int val, int type); //处理-c
void init_sol(); //处理-s
void trans(int a[81]); //把当前数独进行数字转换,转换成第一行为1..9的数独
bool isEquivalent(int a[81], int b[81]); //判断数独a与数独b是不是等效
bool deleteElement(int pos, int r); //求解数独中的操作_增加一个数独元素的限制
bool modifyElement(int pos, int r); //求解数独中的操作_填充一个数独元素
void out_file(int *s); //输出数独s到文件(sudoku.txt)中
void dfs1(int k, int type); //生成数独的回溯1
void dfs2(int k, int type); //求解数独的回溯2
int freedom(int a[M]); //求解数独a的自由度
double getRandData(int min, int max); //生成一个(min,max)的浮点数
void dfs3(int k, int tot); //从填满元素的数独中挖空的回溯3
int work3(int num_0_t, int id_t, double p_t); //生成确定0个数的数独,原型是第id号满数独
void generate(int number, int mode, int result[][M]); //对于输入的数独游戏的需求数量和难度等级,通过result返回生成的数独游戏的集合
void generate(int number, int lower, int upper, bool unique, int result[][M]); //生成number个空白数下界为lower,上界为upper的数独游戏,并且如果unique为真,则生成的数独游戏的解必须唯一,计算的结果存储在result中
bool check(int a[M]); //检查含0的数独a是不是合法的
bool solve(int puzzle[M], int solution[M]); //对于输入的数独题目puzzle(数独矩阵按行展开为一维数组),返回一个bool值表示是否有解。如果有解,则将一个可行解存储在solution中(同样表示为数独矩阵的一维数组展开)
void generate_single(int number, int from, int ran, int dow, int upd, bool unique, int result[][M]); //生成numer个数独,0的个数在[ran, ran+from-1]中,自由度在[dow, upd]中,unique代表要不要生成唯一解
int solve_unique(int tmp[M]); //求解一个单一的数独tmp并求出解的个数, 结果放在ans_first,返回hasAnswer的值代表有多少解
UI方面,当新开始一个不同难度的游戏时,generate(1, mode, res)即生成一个模式为mode的数独到数组中即可。然后在提示或者提交的时候,对当前的数独进行solve(puzzle, solution)判断有没有接以及求出解即可。
阅读有关UML的内容,画出UML图显示计算模块部分各个实体之间的关系(画一个图即可)。
计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。
分别对-n 10000 -r 55~55
和 -n 10000 -r 55~55 -u
进行了测试,然后性能分析图如下,可以看到当没有考虑-u时,init_gen这个生成50000的函数的时间占据了主导,但是一旦当测试进了-u,这个占据的时间就是非常小的一部分,此时solve_unique函数就占据了主导,因为我们得一直从一个确定的数独终盘里面去挖空来找到有唯一接的数独。
<img src="https://i.loli.net/2017/10/15/59e2f51a2ca4c.jpg
" alt="Screen 2017-10-15 AM1.41.23.jpg" title="Screen 2017-10-15 AM1.41.23.jpg" />
<img src="https://i.loli.net/2017/10/14/59e226f0e8eb9.jpg
" alt="Screen 2017-10-14 AM10.17.45.jpg" title="Screen 2017-10-14 AM10.17.45.jpg" width = "60%"/>
<img src="https://i.loli.net/2017/10/14/59e226f0ec735.jpg
" alt="Screen 2017-10-14 AM10.55.15.jpg" title="Screen 2017-10-14 AM10.55.15.jpg" />
考虑的优化如下:我们可以不是对一个确定的数独终盘进行一直来挖空,而是当一个数独终盘一直挖空不出唯一解时,跳到下一个进行执行。优化的代码如下:
if (times > 1000) { //执行了这个数独终盘1000次还没有解,就做后一个数独终盘来生成。
id++;
times = 0;
}
看Design by Contract, Code Contract的内容,描述这些做法的优缺点, 说明你是如何把它们融入结对作业中的。
契约式编程,在正式编程之前先约定一个契约,这个契约规定了类的规格包括前置条件、后置条件以及不变式。
Design by Contract
优点:使程序设计规范化,程序更加方便阅读。
缺点:对程序语言有一定的要求。
Code Contract
优点:保证调用者和被调用者地位平等。
缺点:没有标准化,给代码造成混乱。
在作业中
前置条件为输入满足作业要求的命令行参数,传入的数字在作业要求的范围内,否则会提示错误信息。
后置条件为输出正确的挖空数独或数独解,或提示该数独无解。
不变式为输出正确的挖空数独或数独解,或提示该数独无解。
计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。要求总体覆盖率到90%以上,否则单元测试部分视作无效。
例如我测试求解一个数独是不是有唯一解这个函数,我就通过来填入一个有多个解的数独来进行求解其解数,来进行判断。之后我也对有唯一解的数独进行了测试。
TEST_METHOD(TestMethod7)
{//test solve_unique
Core s;
int ppp[M] = {
6, 1, 0, 3, 0, 0, 0, 0, 9,
0, 4, 0, 7, 0, 0, 0, 2, 0,
0, 8, 9, 1, 2, 0, 0, 0, 0,
0, 2, 3, 0, 0, 7, 0, 9, 0,
4, 5, 6, 0, 9, 1, 2, 3, 7,
0, 0, 7, 0, 0, 3, 0, 5, 0,
0, 0, 0, 0, 0, 8, 0, 6, 0,
5, 6, 0, 0, 0, 0, 0, 0, 3,
9, 0, 0, 6, 3, 0, 0, 0, 0
};
int uni = s.solve_unique(ppp);
s.out = fopen("unit_out_7.txt", "w");
fprintf(s.out, "%d\n", uni);
Assert::AreEqual((uni>1), true);
}
测试的原函数代码如下:
int Core::solve_unique(int tmp[M]) {
hasAnswer = 0;//false
memcpy(x, tmp, sizeof(x));
memset(a, 0, sizeof(a));
rep(i, 0, 80) {
if (x[i] > 0)
modifyElement(i, x[i]);
}
dfs2(1, 0);
return hasAnswer;
}
再例如测试-n -r,并且r1 > r2的例子,我是通过在generate里捕获异常把一个布尔变量hasException置为true来判断的:
TEST_METHOD(TestMethod10)
{//Test -n with -r
Core s;
s.generate(1, 50,40, true, lll);
Assert::AreEqual(s.hasException, true);
}
而我的generate函数的代码如下:
void Core::generate(int number, int lower, int upper, bool unique, int result[][M]) {
try
{
if (number < 1 || number > 10000) {
throw exception("-n number should be in [1,10000]");
}
if (lower < 20 || upper > 55) {
throw exception("lower and upper should be in [20, 55]");
}
if (lower > upper)
{
throw exception("lower can not be bigger than upper");
}
init_gen(50000, 0);
generate_single(number, lower, upper - lower + 1, 0, 9999, unique, result);
}
catch (const exception& e)
{
hasException = true;
puts(e.what());
}
}
一共测试了20个例子,最终总的覆盖率为94%,主要的函数实现的cpp文件达到了98%,如下图:
计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
我主要采用了在函数里直接catch住异常并打印错误信息到控制台的方法。
常见的代码如下:
try {
if (situation ...) {
throw exception("...........");
}
catch (const exception& e)
{
hasException = true;
puts(e.what());
}
}
现选取sudoku.cpp中捕获异常的情形和单元测试的例子如下表:
异常情况 | 如何处理 | 单元测试 |
---|---|---|
generate的参数number<1或>10000 | throw exception(“-n number should be in [1,10000]"); | Core s; s.generate(-1, 2, lll); Assert::AreEqual(s.hasException, true); |
generate的参数mode<1或>3 | throw exception(“-m mode shoule be 1 or 2 or 3"); | s.generate(1, 4, lll); Assert::AreEqual(s.hasException, true); |
generate的参数lower<20或者upper>55 | throw exception(“lower and upper should be in [20,55]"); | s.generate(100, 30, 56, true, lll); Assert::AreEqual(s.hasException, true); |
generate的参数lower>upper | throw exception(“lower can not be bigger than upper"); | s.generate(100, 50, 40, true, lll); Assert::AreEqual(s.hasException, true); |
令选取主程序中捕获异常的情形和单元测试的例子如下表:
异常情况 | 如何处理 | 单元测试 |
---|---|---|
-m参数缺少-n参数 | throw exception(“-m must be with -n"); | char *command[] = {"sudoku.exe", "-m", "2"}; main(3, command); Assert::AreEqual(mainException, true); |
参数重复 | throw exception(“Repeated parameter!"); | char *command[] = {"sudoku.exe","-n", "100", "-n", "100", "-m", "2"}; main(5, command); Assert::AreEqual(mainException, true); |
不支持的参数 | throw exception(“Supported arguments: -c -s -n -m -r -u"); | char *command[] = {"sudoku.exe","-g","100"}; main(5, command); Assert::AreEqual(mainException, true); |
-c 后面没有数字 | throw exception(“Use a number after -c"); | char *command[] = {"sudoku.exe","-c" }; main(5, command); Assert::AreEqual(mainException, true); |
-n 后缺少-r和-m | throw exception(“-n Must be with -m or -r"); | char *command[] = { "sudoku.exe","-n 100 - u"}; main(5, command); Assert::AreEqual(mainException, true); |
界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
游戏一共有两个界面,一个是coverWindow(封面),另一个是gameWindow(游戏界面)。
在coverWindow中放置了一个QLabel呈现"SUDOKU"标题和三个QPushButton来对应"Start"、"Record"、和"Setting"。玩家单击"Start"按钮可以切换到游戏界面gameWindow。
单击"Record"按钮查看三个难度的最佳记录。
单击"Setting"来设置游戏难度。
在gameWindow中用81个QPushButton来呈现数独,9个QPushButton来填数,"clear"和"hint"两个QPushButton分别对应清除一个格子内容和显示一个格子的提示。
使用"clear"和"hint"时先通过鼠标单击选中一个需要填数字的格子。
选中后该格子会变成绿色。
然后再点击"clear"按钮即可清空上面的数字。
或者点击"hint"按钮获得这个格子应该填的数字。
"submit"QPushButton用于检查答案。
"home"按钮用于返回coverWindow。
一个计时器用于记录花费的时间。点击时钟图标可以暂停和开始计时。
玩家也可以通过上面的菜单栏来进行新的游戏或查看游戏规则等。
void sudokuGUI::sudokuButtonClicked() {
QPushButton *btn = qobject_cast<QPushButton*>(sender());
int temp = btn->objectName().toInt();
int j = temp % 10;
int i = temp / 10;
if (empty[i][j] == 0)
return;
if (btnTarget != NULL) {
int temp = btnTarget->objectName().toInt();
int j = temp % 10;
int i = temp / 10;
setRowStyleSheet(i, btnNotEmptyStyle, 0);
setColumnStyleSheet(j, btnNotEmptyStyle, 0);
setJiugongStyleSheet(i, j, btnNotEmptyStyle, 0);
setRowStyleSheet(i, btnEmptyStyle, 1);
setColumnStyleSheet(j, btnEmptyStyle, 1);
setJiugongStyleSheet(i, j, btnEmptyStyle, 1);
btnTarget->setStyleSheet(btnEmptyStyle);
}
btnTarget = btn;
setRowStyleSheet(i, btnNumberStyle, 0);
setColumnStyleSheet(j, btnNumberStyle, 0);
setJiugongStyleSheet(i, j, btnNumberStyle, 0);
setRowStyleSheet(i, btnNumberStyle, 1);
setColumnStyleSheet(j, btnNumberStyle, 1);
setJiugongStyleSheet(i, j, btnNumberStyle, 1);
btnTarget->setStyleSheet(btnTargetStyle);
}
数独矩阵上按钮对点击事件的相应。
void sudokuGUI::update() {
for (int i = 0; i < matrixLen; i++)
for (int j = 0; j < matrixLen; j++)
if (empty[i][j]) {
bool ok = false;
int x = btnFill[i][j].text().toInt(&ok, 10);
if (ok)
matrix[i][j] = x;
else
matrix[i][j] = -1;
}
if (solver.checkMatrix(matrix)) {
result.setText("Right ! ");
timer.stop();
int finishTime = timerCnt;
string res[3];
readRecordFile(res);
int temp = atoi(res[difficultyChosen].c_str());
if (temp == 0 || temp > finishTime)
res[difficultyChosen] = to_string(finishTime);
writeRecordFile(res);
}
else {
result.setText("Wrong ! ");
}
QMessageBox::about(&gameWindow, "Result", result.text());
}
点击submit按钮后检查用户提交答案的正确性。
void sudokuGUI::setBtnZoomAction(QPushButton &btn) {
QObject::connect(&btn, SIGNAL(pressed()), this, SLOT(setBtnZoomOut()));
QObject::connect(&btn, SIGNAL(released()), this, SLOT(setBtnZoomIn()));
}
void sudokuGUI::setBtnZoomOut() {
QPushButton *btn = qobject_cast<QPushButton*>(sender());
int cx = btn->x() + btn->width() / 2;
int cy = btn->y() + btn->height() / 2;
int nw = btn->width()*ZOOM_OUT_RATIO;
int nh = btn->height()*ZOOM_OUT_RATIO;
btn->setGeometry(cx - nw / 2, cy - nh / 2, nw, nh);
QFont f = btn->font();
f.setPixelSize(f.pixelSize() + 2);
btn->setFont(f);
}
void sudokuGUI::setBtnZoomIn() {
QPushButton *btn = qobject_cast<QPushButton*>(sender());
int cx = btn->x() + btn->width() / 2;
int cy = btn->y() + btn->height() / 2;
int nw = btn->width() / ZOOM_OUT_RATIO;
int nh = btn->height() / ZOOM_OUT_RATIO;
btn->setGeometry(cx - nw / 2, cy - nh / 2, nw, nh);
QFont f = btn->font();
f.setPixelSize(f.pixelSize() - 2);
btn->setFont(f);
}
为按钮添加缩放动画。
if (btn->text() == HINT) {
int puzzle[M] = { 0 };
int solution[M] = { 0 };
for (int i = 0; i < matrixLen; i++)
for (int j = 0; j < matrixLen; j++) {
bool ok = false;
int x = btnFill[i][j].text().toInt(&ok, 10);
puzzle[i*matrixLen + j] = ok ? x : 0;
}
bool ok = core.solve(puzzle, solution);
if (ok) {
btnTarget->setText(QString::number(solution[i*matrixLen + j], 10));
}
else {
QMessageBox::critical(&gameWindow, "warning", "No solution! ");
}
}
hint功能的实现。
界面模块与计算模块的对接。详细地描述UI模块的设计与两个模块的对接,并在博客中截图实现的功能。
将计算模块封装在Core类里,生成dll文件供UI模块调用,并将计算模块的头文件放入UI模块中。
实现对接的代码:
void sudokuGUI::initMatrix() {
int res[1][M] = { 0 };
core.generate(1, difficultyChosen + 1, res);
for (int i = 0; i < matrixLen; i++)
for (int j = 0; j < matrixLen; j++)
matrix[i][j] = res[0][i*matrixLen + j];
}
调用generate生成数独初局。
bool ok = core.solve(puzzle, solution);
if (ok) {
btnTarget->setText(QString::number(solution[i*matrixLen + j], 10));
}
else {
QMessageBox::critical(&gameWindow, "warning", "No solution! ");
}
调用solve求解数独。
描述结对的过程,提供非摆拍的两人在讨论的结对照片。
看教科书和其它参考书,网站中关于结对编程的章节,说明结对编程的优点和缺点。结对的每一个人的优点和缺点在哪里 (要列出至少三个优点和一个缺点)。
结对编程
优点:
(1)不间断地复审,更容易发现错误,提高代码的质量。
(2)编程中驾驶员和领航员的互换可以让程序员轮流工作,从而避免出现过度思考而导致观察力和判断力下降。
(3)两个人之间可以相互交流学习,从而互相取长补短。
缺点:
(1)可能会出现一人编程,另一人偷懒的情况。
(2)可能会降低项目开发效率。
我的优点:
勇于探索,善于利用搜索工具,舍得花时间来不断完善程序。
缺点:
遇到bug会慌张。
赵奕的优点:
手速快,技术高,调理清晰,代码风格优秀。
缺点:
有拖延症。
附加题
界面模块,测试模块和核心模块的松耦合
我们是三个小组进行互相测试的,我测试的是15061186安万贺,测试我们的是由15061187窦鑫泽来完成的。
首先我们测试安万贺小组的情况如下,我们先把他们的Core.dll和Core.lib拷贝到了我们的sudokuGUI工程下进行测试,发现他们的生成有这样的问题,就是不管怎么开始,终盘的第一行总是1..9,即如下效果:
然后我就给他们提出了issue,后来通过交流发现他们的随机化放在了GUI里而不是generate里进行随机,因此导致我们用的时候总是1..9。
另外是关于命令行的调用。我发现不管是调用generate(1, 1, 53, false, a); 还是c.generate(1, 54, 53, false, a); 或者c.generate(1, 20, 500000, false, a);
反馈的错误都是:
The number after -r is not in the range.
然后我希望能够他们区分这些错误,就提了issue.
一个可能的测试代码如下:
try {
Core c;
c.generate(1, 20, 5000, false, a);
}
catch (NumberOutOfBoundException& e){
cout << e.m_msg << endl;
}
还有一个问题是关于solve的异常捕捉的。
如果solve函数的puzzle函数是一个本身就非法的数独,例如一行有两个数是一样的情况,没有捕捉到异常或者返回false,而是也求了一个解。
一个可能的测试代码如下. 这个矩阵的第一行有两个9,显然是无解的:
int puzzle[81] = {
0, 0, 0, 0, 0, 5, 9, 0, 9,
3, 0, 5, 0, 0, 9, 0, 0, 6,
7, 8, 0, 0, 2, 0, 0, 0, 0,
1, 0, 0, 4, 0, 7, 6, 0, 0,
4, 5, 0, 0, 0, 1, 0, 0, 7,
8, 0, 0, 0, 0, 3, 0, 0, 0,
0, 0, 1, 0, 7, 0, 5, 0, 4,
0, 6, 4, 0, 0, 2, 8, 7, 3,
5, 7, 8, 6, 3, 4, 9, 1, 2
};
int solution[81];
Core c;
c.solve(puzzle, solution);
rep(j, 0, 80) {
cout << solution[j] << " ";
if ((j + 1) % 9 == 0)
puts("");
}
而最终却给出了一个解如下:
6 1 2 3 4 5 9 8 9
3 4 5 7 8 9 1 2 6
7 8 9 1 2 6 3 4 5
1 2 3 4 5 7 6 9 8
4 5 6 8 9 1 2 3 7
8 9 7 2 6 3 4 5 1
2 3 1 9 7 8 5 6 4
9 6 4 5 1 2 8 7 3
5 7 8 6 3 4 9 1 2
然后是别人给我们的测试进行的修正。最初始的改正就是把generate的数组的参数从83改成了81,即宏定义的M值。(之前写成83是害怕溢出)因为发现如果参数不同是无法调用别人的函数的。
根据窦哥给我们组的issue如下,是几个比较中肯与实际的测试问题:
软件只能通过右上角的红叉来结束,在开始界面中没有退出软件的按钮。
于是我们增加了可以在开始界面中退出游戏的按钮。
软件在点击"home"按钮返回开始界面时没有"Setting"按钮,影响用户体验。
于是我们修复了这个bug,时用户可以在游戏途中返回主界面对游戏难度进行设置,从而优化了用户体验。
然后第三点,关于sudoku.exe -c abc, 其中abc是个不存在的文件,此时我们的是不能识别这个异常的。相当于直接闷声过去了。然后增加的修改如下:
FILE *fp = NULL;
fp = fopen(argv[2], "r");
if (fp == NULL) {
throw std::exception("-s file doesn't exist");
}
用fopen判断是不是NULL就可以增加捕捉了这个异常。
通过增量修改的方式,改进程序,发布一个真正的软件
用户的反馈:
用户1:按钮有动态效果,界面很好看。
用户2:支持键盘输入数字,很方便。
用户3:No Solution的意义不是很明确。
用户4:成功求解数独弹出right之后点击ok能返回主界面就好了。
用户5:没有音效。
用户6:没有暂停功能。
用户7:点击黑色按钮最好不要有动画,因为不能填数字。
用户8:挺好的。
用户9:可以提示哪些数字是填错的就好了。
用户10:游戏介绍是中文的就好了。
改进:
3.No Solution就是代表当前数独无解,意思应该还是比较明确吧。
4.成功求解数独弹出right之后点击ok停留在当前界面也挺好的吧,如果想重新玩的话通过左上角的New菜单进行难度选择即可。
5.添加音效的话确实能够优化用户体验,不过时间不够了就不加了。
6.
void sudokuGUI::timerSwitch() {
if (timer.isActive())
timer.stop();
else {
timer.start();
}
}
增加了该功能,现在点击时钟图标可以暂停计时和开始计时了。
7.有动画也挺好看的啊,而且改起来没想象中的简单,就不改了。
9.填一个数字按一下hint就可以直到当前填的这个数字是不是错的了
10.中文有的字会变为乱码,还是英文比较保险。