结对项目-数独程序扩展

1)Github传送门

https://github.com/Issac-Newton/Sudoku_extend

2)PSP表

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

3)结对编程中Information Hiding, Interface Design, Loose Coupling原则的使用

Information Hiding:Core类的所有数据成员都声明为private,所有访问都只能通过Core模块提供的四个接口实现;
Interface Design:针对-c,-s,-n -m,-n -r -u这几种参数组合我们分别设计了接口,每个接口负责完成一个类型的参数组合的功能,尽可能保证接口功能的单一性;
Loose Coupling:Core模块的接口对传入的参数除类型外没有要求,会自行对参数的合法性进行检查,减少了对调用时参数的要求;另外Core发生改变时只要接口不变不会对调用它的类产生影响;

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

我们共实现了10个类,分别为Core,Handler以及8个异常类;
计算模块为Core模块;在Core中设计和实现了4个接口,分别是:
void generate(int number, int result[][CELL]);用来生成最多100,0000个数独终局,对于该接口的实现,我们在判断传进来的参数的合法后,直接调用TraceBack()函数生成满足数量要求的数独终盘;
void generate(int number, int mode, int result[][CELL]);用来生成最多10,000个模式为mode的数独题目,对于该接口的实现,我们首先检查参数的合法性,然后生成满足数量要求的数独终盘,之后再根据模式来对数独进行修改;
void generate(int number, int lower, int upper, bool unique, int result[][CELL]);用来生成最多10,000个挖空数在lower到upper之间的数独题目,其中unique为true时表示数独题目只有唯一解;对于该接口的实现,我们首先检查参数的合法性,然后生成满足数量要求的数独终盘,之后再根据挖空数以及unique的值来对数独进行修改,若unique的值为true则每次修改完成后都调用IsSingleSolution()函数来检查数独题目是否为唯一解;
bool solve(int puzzle[CELL], int solution[CELL]);用来解sudoku.txt文件中的数独题目,若能解出数独题目则调用CopySudoku()将答案复制到solution中;
共10个函数,除上述的4个接口外,还有:
Core();构造函数
bool IsValid(int pos, bool isSolve);检查每次填的数是否满足数独的要求,若满足则返回true,否则返回false;
bool TraceBackSolve(int pos);用回溯法检查数独问题是否有解,若有解则返回true并求解,否则返回false;
int TraceBack(int pos, int number, int& count, int result[][CELL], bool isSolve);用回溯法生成最多100,0000个数独题目,每填一个格子都会调用IsValid()函数来检查正确性,每生成一个数独终盘都会调用CopySudoku()函数将终盘复制到result中;
void CopySudoku(int result[CELL], int temp[GRIDSIZE][GRIDSIZE]);将结果复制到result中;
bool IsSingleSolution(int tot, int& ans);用回溯法判断生成的数独题目是否为唯一解,若有唯一解则返回false,否则返回true;
算法的关键..就是回溯,没有多余的技巧;
以下是int TraceBack(int pos, int number, int& count, int result[][CELL], bool isSolve);的流程图:

以下是void generate(int number, int lower, int upper, bool unique, int result[][CELL]);的流程图:

Handler类用于处理命令行输入;
这个类的成员变量会有输入参数的信息,像是生成数独终盘的个数和生成数独游戏时要挖空的个数。
在对参数进行处理时,我们是按照参数个数对输入进行判断的。具体情况如下:
参数个数大于6或者是小于3,参数个数异常;
参数个数等于3,有效输入只可能是-c或者是-s;
参数个数等于4,有效输入只可能是-n和-u的搭配;
参数个数等于5,有效输入可能是-n和-m的搭配或者是-n和-r的搭配;
参数个数等于6,有效输入可能是-n -u -r的几种搭配;
首先对参数选项字符进行确认,然后对选项后面的参数进行提取,有错误则报异常。

各个异常类将在之后详细说明;

5)计算模块的UML图

(我们的计算模块只有一个Core,不太懂这个UML怎么画...)

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

花费时间约3小时;
改进思路:由于我们依旧采用回溯法,因此对之前的功能的性能没有更多改进;我们主要针对void generate(int number, int lower, int upper, bool unique, int result[][CELL]);这一函数进行性能改进,一开始我们的算法是针对某一个数独终盘,每随机挖一个空都立刻检查是否有唯一解,若唯一则随机挖下一个,否则还原这个空重新挖,若无法找到满足条件的挖空位置则回溯,但测试以后发现算法本身好像出了写问题,生成了多解数独;
于是我们采用了一次性随机产生所有挖空位置,挖好后再检查是否有唯一解的算法,我们的性能改进主要是减少产生的随机数的碰撞次数(实际上就是凑...),但是一直都最后也没能很好的提高产生挖空数为55的唯一解的数独题目的性能。

性能分析图是void generate(int number, int lower, int upper, bool unique, int result[][CELL]);在生成1个挖空数为55的唯一解的数独问题的性能分析图;消耗最大的函数是IsValid();

7)Design By Contract,Code Contract的优缺点以及在结对编程时的实际应用

Design By Contract:http://en.wikipedia.org/wiki/Design_by_contract
Code Contract:http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx

  • 优点:使用者和被调用者地位平等,双方必须彼此履行义务,才可以行驶权利。调用者必须提供正确的参数,被调用者必须保证正确的结果和调用者要求的不变性。双方都有必须履行的义务,也有使用的权利,这样就保证了双方代码的质量,提高了软件工程的效率和质量。
  • 缺点:会给接口的调用带来较大的风险,需要严格的验证参数的正确性;
  • 应用:一开始我们对Core模块的接口采用契约式设计...后来不知怎么的我们又在Core中对传入的部分参数做了正确性验证..但又没有验证传进来的数组size是否足够..现在看来设计的有点四不像,看来需要改进;

8)项目的单元测试展示:

单元测试结果:

项目的单元测试主要是回归测试,对generate和solve的测试以及对输入处理的测试。
回归测试:
增量修改工程之后要对之前的功能做一个覆盖性检查

//-c
TEST_METHOD(TestMethod4)
		{
			int result[100][CELL];
			int grid[GRIDSIZE][GRIDSIZE];

			set<string> container;
			string inserted;

			Core core;
			core.generate(100, result);
			for (int i = 0; i < 100; i++)
			{
				for (int j = 0; j < GRIDSIZE; j++)
				{
					for (int k = 0; k < GRIDSIZE; k++)
					{
						grid[j][k] = result[i][j * GRIDSIZE + k];
						assert(!(grid[j][k] <= 9 && grid[j][k] >= 1));
						inserted.push_back(grid[j][k] + '0');
					}
				}
				Assert::IsTrue(valid(grid));
				container.insert(inserted);
				inserted.clear();
			}
                        assert(container.size() == 100);
		}

每生成一个数独终盘就对数独的有效性进行检测,最后对数量进行检测,方法是将每个数独都转化为一个字符串,将字符串插入到一个集合中,可以找到生成的数独的数量。

//-s
TEST_METHOD(TestMethod7)
		{
			int puzzle[CELL];
			int solution[CELL];
			Core core;

			bool flag = true;

			FILE* file_in;
			freopen_s(&file_in, "C:\Users\dell\Source\sudoku\ModeTest\sudoku.txt", "r", stdin);
			assert(file_in != NULL);
			while (true)
			{
				if (fscanf(file_in, "%d", &puzzle[0]) == EOF)
				{
					break;
				}
				for (int i = 1; i < CELL; i++)
				{
					fscanf(file_in, "%d", &puzzle[i]);
				}
				assert(core.solve(puzzle,solution));
				int grid[GRIDSIZE][GRIDSIZE];
				for (int j = 0; j < CELL; j++)
				{
					grid[j / GRIDSIZE][j % GRIDSIZE] = solution[j];
				}
				assert(valid(grid));
			}
		}

每次从文件中读入一个数独就调用solve函数进行求解,求解之后对数独有效性进行判断,然后和solve函数的返回值进行比较

对新增功能的测试:
下面代码是对-u -r -n组合的测试

TEST_METHOD(TestMethod5)
		{
			int result[1000][CELL];
			Core core;
			core.generate(2, 55, 55, true, result);
			
			bool flag = true;

			for (int i = 0; i < 2; i++)
			{
				int grid[GRIDSIZE][GRIDSIZE];
				for (int j = 0; j < CELL; j++)
				{
					grid[j / GRIDSIZE][j % GRIDSIZE] = result[i][j];
				}
				int ans = 0;
				if (MultiSolution(0, ans, grid))
				{
					flag = false;
					Assert::IsTrue(flag);
				}
			}
		}

我们对生成好的数独游戏进行暴力求解(回溯法),如果有多解,那么断言失败。

//判断数独是不是有多解的函数
		bool MultiSolution(int tot, int& ans, int grid[GRIDSIZE][GRIDSIZE])
		{
			if (tot == GRIDSIZE * GRIDSIZE) {
				ans++;
				return true;
			}
			else {
				int x = tot / GRIDSIZE;
				int y = tot % GRIDSIZE;

				if (grid[x][y] == 0) {
					for (int i = 1; i <= 9; i++) {
						grid[x][y] = i;
						if (IsValid(tot, grid)) {
							if (MultiSolution(tot + 1, ans, grid)) {
								if (ans > 1)
								{
									return true;
								}
								continue;
							}
						}
					}
					grid[x][y] = 0;
				}
				else {
					return MultiSolution(tot + 1, ans, grid);
				}
			}
			return false;
		}

对异常的测试:
因为新增的有效输入只有几种,我们对每种都做出检查

以参数的数字范围异常为例,代码如下:

TEST_METHOD(TestMethod10)
		{
			//-c
			char* command[5] = { "sudoku.txt","-c","10000001"};
			try
			{
				main(3, command);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(NumberOutOfBoundException));
			}
			assert(hasException);
			hasException = false;

			//-n
			char* command1[5] = {"sudoku.txt","-n","10001","-m","1"};
			try
			{
				main(5,command1);
				
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(NumberOutOfBoundException));
			}
			assert(hasException);
			hasException = false;

			//-m(模式错误)
			char* command2[5] = { "sudoku.txt","-n","1000","-m","4" };
			try
			{
				main(5,command2);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(ModeException));
			}
			assert(hasException);
			hasException = false;

			//-r
			char* command3[5] = {"sudoku.exe","-n","10","-r","50~56"};
			try
			{
				main(5,command3);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(NumberOutOfBoundException));
			}
			assert(hasException);
			hasException = false;

我们首先将参数传入main函数,然后用assert将异常抛出的异常类型和应该抛出的异常类型做比较,但是如果没有抛出异常岂不是漏了bug。所以,在一个头文件里我定义了一个标记异常发生过的变量,main函数每次捕捉到异常之后就将该变量赋值为真,main函数之后断言这个变量为真。每个测试点跑过之后,将该值设置为假。

覆盖率如下:

9)计算模块部分异常处理说明

Ⅰ.参数个数异常(定义为ParametersNumberException)
设计目标:如果输入命令参数过多,那么程序抛出异常。
设计单元测试:

char* command[8] = {"sudoku.exe","-n","100","-n","-r","-s","-m","-d"};
			try {
				main(9,(char**)command);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(ParametersNumberException));
			}
			assert(hasException);
			hasException = false;

Ⅱ.文件不存在异常(定义为FileNotExistException)
设计目标:-s命令下,如果打开文件失败,那么抛出异常。
设计单元测试:

TEST_METHOD(TestMethod9)
		{
			char* command[3] = { "sudoku.exe","-s","NotExist.txt" };
			try
			{
				main(3,command);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(FileNotExistException));
			}
			assert(hasException);
			hasException = false;
		}

Ⅲ.命令中的各种数字溢出异常(定义为NumberOutOfBoundException)
设计目标:在各种参数下,如果数字不符合规范,抛出异常。
设计单元测试:
见上部分异常单元测试部分 ↑↑

Ⅳ.-r选项后面的数字异常(定义为RParametersException)
设计目标:在-r参数后面,如果后面跟的参数字符长度不是5或者第三个字符不是 ~ 或者存在不是1-9的字符,那么抛出异常。
设计单元测试:

char* command[5] = { "sudoku.exe","-n","10","-r","20-55"};
			try
			{
				main(5, command);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(RParametersException));
			}
			assert(hasException);
			hasException = false;

			char* command2[20] = { "sudoku.exe","-n","10","-r","3n~40" };
			try
			{
				main(5, command);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(RParametersException));
			}
			assert(hasException);
			hasException = false;

Ⅴ.命令中包含非法字符(定义为IllegalCharException)
设计目标:在-c这样的选项不能匹配时抛出异常。
设计单元测试

char* command[20] = { "sudoku.exe","-nn","10","-r","20~55" };
			try
			{
				main(5, (char**)command);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(IllegalCharException));
			}
			assert(hasException);
			hasException = false;

Ⅵ.-s参数中数独无解(定义为NoSolutionException)
设计目标:如果-s参数后面文件中的数独无解,抛出异常
设计单元测试:

char* command[20] = { "sudoku.exe","-s","puzzle.txt"};
			try
			{
				main(3,command);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(NoSolutionException));
			}
			assert(hasException);
			hasException = false;

文件中的数独:
000000123
009000000
000009000
000000000
000000000
000000000
000000000
000000000
000000000
-->右上角的九宫格不能放9

Ⅶ.数字错误异常(定义为IllegalNumberException)
设计目标:在求解数独的时候,如果从文件中读入的数字不在1-9,抛出异常
设计单元测试:
单元测试代码同上一个异常类型,但是文件中的数独中包含不在1-9的数字

Ⅷ. -m 后面的模式错误(定义为ModeException)
设计目标:检查generate参数中模式是不是1,2,3如果不是,抛出异常
设计单元测试:

char* command2[5] = { "sudoku.txt","-n","1000","-m","4" };
			try
			{
				main(5,command2);
			}
			catch (exception& e)
			{
				Assert::IsTrue(typeid(e) == typeid(ModeException));
			}
			assert(hasException);
			hasException = false;

10)界面模块的详细设计过程

以下将按照从上到下的顺序来对整个GUI进行描述
GUI菜单栏中有选择模式和查看每个模式下最佳记录的两个Action,每个里面都有三个选项-->easy,normal,hard
下面就是数独盘(左上角),右上角是计时器。
数独的实现使用的控件是textEdit,我们重写了这个控件的部分函数,改变鼠标focusIn和focusOut的行为,使之能够在鼠标定位到某个未填块的时候将边框标红;在鼠标离开的时候能够对输入的字符进行判断处理。代码如下:

void MyTextEdit::focusInEvent(QFocusEvent *e)
{
	if (!isReadOnly())
	{
		setStyleSheet(QString::fromUtf8("font: 20pt "351273221344275223";""border: 3px solid red"));
	}
	emit cursorPositionChanged();
}

void MyTextEdit::focusOutEvent(QFocusEvent *e)
{
	QString str;
	str = toPlainText();
	int position = textCursor().position();
	int length = str.count();
	if (!isReadOnly())
	{
		setStyleSheet(QString::fromUtf8("font: 23pt "351273221344275223";""border: 1px solid grey;color:blue"));
		if (!IsValidChar(str))
		{
			setPlainText("");
			setCursorWidth(0);
		}
		else
		{
			//setStyleSheet("color:blue");
			setAlignment(Qt::AlignCenter);
		}
        }
}

右上角的时钟支持暂停功能,暂停之后,数独盘上的所有模块都会清空,当继续之后又会恢复之前的数字。
清空和恢复的代码如下:

//清空
for (int i = 0; i < GRIDSIZE; i++)
		{
			for (int j = 0; j < GRIDSIZE; j++)
			{
				QString str = ui.textEdit[i][j]->toPlainText();
				
				if (IsValidChar(str))
				{
					int num = str.toInt();
					m_fill[i * GRIDSIZE + j] = num;
					if (ui.textEdit[i][j]->isReadOnly())
					{
						m_fillBlack[i * GRIDSIZE + j] = num;
					}
					ui.textEdit[i][j]->setReadOnly(false);
					QString strIn = "";
					ui.textEdit[i][j]->setText(strIn);
				}
				ui.textEdit[i][j]->setReadOnly(true);
			}
		}
//显示
	for (int i = 0; i < GRIDSIZE; i++)
	{
		for (int j = 0; j < GRIDSIZE; j++)
		{
			ui.textEdit[i][j]->setReadOnly(false);
			if (m_fill[i * GRIDSIZE + j] > 0)
			{
				QString str = QString::number(m_fill[i * GRIDSIZE + j], 10);
				ui.textEdit[i][j]->setText(str);
				if (m_fillBlack[i * GRIDSIZE + j] == 0)       //此时要用蓝色字体
				{
					ui.textEdit[i][j]->setStyleSheet(QString::fromUtf8("font: 23pt "351273221344275223";""border: 1px solid grey;color:blue"));
					ui.textEdit[i][j]->setAlignment(Qt::AlignCenter);
					ui.textEdit[i][j]->setReadOnly(false);
				}
				else
				{
					ui.textEdit[i][j]->setStyleSheet(QString::fromUtf8("font: 23pt "351273221344275223";"));
					ui.textEdit[i][j]->setAlignment(Qt::AlignCenter);
					ui.textEdit[i][j]->setReadOnly(true);
				}
				
			}
			else
			{
				QString str = "";
				ui.textEdit[i][j]->setText(str);
			}
		}
        }

清空的时候我们是先将数独中现有的数字拷贝下来,因为我们在生成的时候有一个挖空的未填的备份,所以可以知道之后哪个空是人为填的,所以显示的时候可以区分开人为填的空格(这两个显示是不一样的)。

数独盘下面有三个按钮,功能分别是“重新开始”,“检查答案”,“提示”,
重新开始就是将原来的textEdit控件上面的字符清空,然后将原来的那个重新填入。
检查答案就是将现在textEdit上的数字和答案数字相对比,如果有不同,那么会弹出一个弹窗。
提示功能是提示上一次鼠标定位到的未填的格子中的数字,并且将这个数字填入这个格子,之后这个格子的数字和最开始生成游戏时的字体一样。

if (ui.focusIn != NULL)
	{
		int col;
		int line;
		for (int i = 0; i < GRIDSIZE; i++)
		{
			for (int j = 0; j < GRIDSIZE; j++)
			{
				if (ui.focusIn == ui.textEdit[i][j])
				{
					line = i;
					col = j;
					break;
				}
			}
		}

		int num = m_result[line * GRIDSIZE + col];

		QString str = QString::number(num, 10);
		ui.textEdit[line][col]->setText(str);
		ui.textEdit[line][col]->setStyleSheet(QString::fromUtf8("font: 21pt "351273221344275223";""border: 1px solid grey"));    
		ui.textEdit[line][col]->setAlignment(Qt::AlignCenter);
		ui.textEdit[line][col]->setReadOnly(true);
		informSuccess = true;
	}

其中,我们在记录上一次鼠标定位的位置遇到了困难,因为不能够在类定义中对该对象进行赋值,所以没办法在鼠标focusIn的时候将指示对象指针赋值。所以我们只能在类定义外面对对象指针进行赋值。我们对每个textEdit块 connect 一个槽函数,在focusIn的时候 emit 一个信号。
focusIn的函数见上,其中,可以看到emit 一个 cursorPositionChanged() 信号,之后触发槽函数,槽函数获得调用者,对指针进行赋值

MyTextEdit* temp = qobject_cast<MyTextEdit*>(sender());
if (!temp->isReadOnly())
{
	ui.focusIn = temp;
}

另外还做得一些工作就是美工,这部分比较复杂,字体,背景,边框等等...每个控件用的方法也不一样,不过大体上使用 palette和setStyleSheet两种方法居多。
使用palette示例:

//整个窗口的背景
QPixmap pixmap = QPixmap("background.jpg").scaled(GUITestClass->size());
QPalette palette(GUITestClass->palette());
palette.setBrush(QPalette::Background, QBrush(pixmap));
GUITestClass->setPalette(palette);
//最下面三个按钮的样式
QString button_style = "QPushButton{font-family:Comic Sans MS;font-size:16pt;background-image:url(button1.jpg); color:white; border-radius:10px;border-style: outset;}"
			"QPushButton:pressed{background-image:url(pressed1.jpg);border-style:inset;}";
pushButton_3->setStyleSheet(button_style);
pushButton_4->setStyleSheet(button_style);
pushButton_5->setStyleSheet(button_style);

感受:一开始以为加个界面应该很快,后来我们才发现自己还是naive...以及在写界面的过程中两个直男由于审美不同还产生了一些分歧..

11)界面模块与计算模块的对接

界面的最终效果图如下:

界面设计和计算模块之间的联系主要是界面使用的数字是从Core模块中产生出来的,界面调用Core模块中函数的代码如下:

        int save_sudoku[1][CELL];
	memset(save_sudoku, 0, sizeof(save_sudoku));

	bool choosen[10];
	memset(choosen,0,sizeof(choosen));

	srand(time(0));

	for (int i = 0; i < 5; i++)
	{
	int posi = rand() % 9 + 1;
	while (choosen[posi])
	{
	posi = rand() % 9 + 1;
	}
	choosen[posi] = true;
	save_sudoku[0][i] = posi;
	}

	int reduce;
	int empty;
	switch (m_mode)
	{
	case EASY:
		reduce = 40 + rand() % 8;
		break;
	case MIDDLE:
		reduce = 32 + rand() % 8;
		break;
	case HARD:
		reduce = 31 + rand() % 8;
		break;
	default:
		break;
	}
	empty = CELL - reduce;

	Core temp;
	temp.generate(1, empty, empty, true, save_sudoku);

	memset(m_fillBlack, 0, sizeof(m_fillBlack));
	memset(m_fill,0,sizeof(m_fill));

	for (int i = 0; i < CELL; i++)
	{
		m_fill[i] = save_sudoku[0][i];
		m_fillBlack[i] = save_sudoku[0][i];
		m_backup[i] = save_sudoku[0][i];
	}

	m_hasStarted = true;
	temp.solve(save_sudoku[0], m_result);
	showNumber();

因为在Core模块中为了保证生成数独的速度,所以传入的result矩阵是空矩阵。但是,因为使用的是回溯法,这样就会造成每两个相邻的矩阵十分相似,可想而知,这样会严重影响用户的体验,所以,我们在GUI模块里添加了对result二维数组的初始化,随机填了五个数字。

12)描述结对的过程

13)结对编程的优点和缺点&结对的每一个人的优点和缺点

结对编程:

  • 优点:1.在设计时,两个人的思维更加开阔,设计的比一个人设计时更全面;2.不间断的交流与复审,可以在开发和写代码的过程中就减少大量bug;3.在遇到问题时,两人合作解决问题的能力更强;
  • 缺点:1.在遇到一些细节问题(比如界面用哪种字体哪张图片)时,两个人容易发生分歧,需要一方退让;2.两个人的思维有时可能不在同一个点上,导致讨论了半天发现讲的不是同一个问题,总之就是很难保证结对双方思维的一致性;3.一个人在写代码时,时间一长边上的另一个人可能注意力会越来越不集中;4.每个人对另一个人写的代码印象不深刻,导致在出现问题时大部分时间只能靠写那段代码的人分析;

本人:

  • 优点:考虑的比较多比较全面;在遇到问题时思维比较开阔;容易沟通;
  • 缺点:在一旁复审代码时注意力容易不集中;对于结对伙伴忽略我的建议而自己思考会有些不耐烦;对项目进度一直不够乐观;

结对伙伴:

  • 优点:脾气好,容易沟通;对工具的使用非常优秀;学习能力强;
  • 缺点:经常当前阶段的bug还没解决就直接想着后面的阶段了;

附加题部分

【第四部分】

我们测试的小组是15061187窦鑫泽 + 15061189李欣泽,测试我们的小组是15061199李奕君 + 14011100赵奕
我们找到的错误Issue到了对应小组的github项目地址
我们使用他们的Core模块发现不能捕捉到异常,也就是说他们的异常抛出是在他们项目的main函数里面。
我们被找的错误 Github
其中一个问题是我们的solve函数的问题,因为solve函数用的是回溯法来解,只会判断每个位置是不是满足数独对这个位置的要求,但是没有考虑到整体的要求。
最终导致那个错误的发生,所以,我们在求解完之后又对求解的数独进行了一次检验。

        for (int i = 0; i < GRIDSIZE; i++)
	{
		for (int j = 0; j < GRIDSIZE; j++)
		{
			m_grid[i][j] = puzzle[i*GRIDSIZE + j];
		}
	}

	if (TraceBackSolve(0))
	{
		CopySudoku(solution, m_grid);
		if (valid(m_grid))
		{
			return true;
		}
	}
	throw NoSolutionException("The sudoku has no solution.

");

valid就是对数独进行有效性检验的函数。

针对另外一个问题,因为我们用回溯法生成数独终盘之后挖空,而且传入的数组是空数组,所以就会从第一个位置开始回溯,这样导致每两个数独之间的相似性很大,
设计游戏的时候我们也考虑到了这个问题,所以在GUI工程里面调用generate函数之前先对矩阵进行一些初始化,所以,这就导致我们的模块不具备随机化的功能。
根据赵奕、李奕君小组提出的问题,我们把那个初始化放到了core模块里面。

        bool choosen[10];
	memset(choosen, 0, sizeof(choosen));
	srand(time(0));
	for (int i = 0; i < 5; i++)
	{
		int posi = rand() % 9 + 1;
		while (choosen[posi])
		{
			posi = rand() % 9 + 1;
		}
		choosen[posi] = true;
		m_grid[0][i] = posi;
	}

针对遇到异常时的反馈不明确,我们又对这一部分进行了细化。

        if ((number < 1))
	{
		throw NumberOutOfBoundException("The number after -n is smaller than minimum 1.

");
	}
	if ((number < 1) || (number > MAX_N))
	{
		throw NumberOutOfBoundException("The number after -n is bigger than maximum 1000000.

");
	}

	if ((upper > EMPTY_UPPER))
	{
		throw NumberOutOfBoundException("The number of upper is bigger than maximum 50.

");
	}
	if ((upper < EMPTY_LOWER))
	{
		throw NumberOutOfBoundException("The number of upper is smaller than minimum 20.

");
	}

	if ((lower > EMPTY_UPPER))
	{
		throw NumberOutOfBoundException("The number of lower is bigger than maximum 50.

");
	}
	if ((lower < EMPTY_LOWER))
	{
		throw NumberOutOfBoundException("The number of lower is smaller than minimum 20.

");
	}

【第五部分】

下载地址:

https://github.com/Issac-Newton/Sudoku_extend

用户反馈

User One:
和一般的软件认知不一样,不能将单独的exe文件拷贝到桌面上。
不同电脑上字符有差异。

User Two:
没有说明;
Hint的功能对新手不是特别容易使用;
界面过于单调,做对做错的弹窗差别不是特别明显。

User Three:
界面对新手不是很友好。
用户提出了新的需求(添加回退功能)。

User Four:
希望能够添加一个保存功能,保存上次未做完的游戏。

User Five:
希望可以有帮助菜单提供数独规则。

User Six:
不同电脑上显示的兼容性有差异。
希望提示功能做得更加智能一些,不要只是简单的显示答案。

User Seven:
亮点在于:游戏有暂停功能,方便用户使用;数独支持键盘填写,有一定便捷性。
不足在于:在未完成的时候,check应该显示未完成,而不是错误答案;界面的布局不够美观,如计时功能不够居中,右下方存在一定的蜜汁空白;对用户的提示过于简单,用户只能靠个人去摸索需要用键盘输入。

User Eight:
暂停功能是亮点,感觉打开gui直接进入到游戏页面有些突兀。界面右侧的说明引导步骤必要,但是有些过于简略。数独按钮的风格不知能不能在优美一点?

User Nine:

  1. exe标题的sudoku.exe多了个点.
  2. 上面的menu的“Personal Best”中间的空格不建议,给人一种2个menu的错觉。建议用一个单词或去掉空格
  3. 当暂停的时,再点Hint会出现一个格子的值。

User Ten:
我对这款软件有几点建议:
首先,我建议增加一个帮助菜单或帮助按钮。因为软件的界面虽然简单,但是对于那几个按钮都没有功能介绍,在询问开发者之前我都不知道Hint按钮是需要先选中一个输入框再点击Hint按钮的。
其次,我建议增加一个Clear按钮,改变Restart按钮的功能。界面中Restart按钮的功能是重新开始本局游戏,数独是不会改变的,每次改变数独需要在Mode菜单中重新选择难易度,我认为不如增加一个Clear按钮实现目前Restart按钮实现的清空已输入的功能,Restart按钮的功能改变为重新生成一个新的当前难易度下的数独。
第三,我建议增加一个保存功能,可以保存当前正在做的数独,下次打开软件可以继续上次的游戏。

改进:
关于发布的目录:现在发布时将所有的依赖项都放到了一个文件夹下,然后将快捷方式放到了和文件夹同目录下。
关于帮助:现在提供了help功能,如图:

添加了这个图片同时解决了关于右下角空白的问题。
关于不同电脑上各种图标大小显示比例的问题,经过更改界面,我们已经能够支持在 100%和125%上界面是没问题的,但是如果这个比例更大会有些问题。
其他关于GUI的美化问题,做了一些修改,但是...让所有人都满意好难...
保存功能和其他一些功能由于时间原因,未添加。

原文地址:https://www.cnblogs.com/Minstrel/p/7668919.html