【软工】结对项目

一、项目的github地址

github地址:github.com/wz111/sudoku_pair

二、PSP表格

三、接口设计

看教科书和其它资料中关于Information Hiding, Interface Design, Loose Coupling的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。
我认为这三部分有相似之处,首先是隐藏一些信息,让那些只对一个模块进行开发的开发者不能误操作,接口设计也是为了考虑到通用性,可以在不同的平台上正常工作,松耦合度也是为了得到更好的可移植性。
在本次设计中,没有全局变量的存在,需要使用的常数也基本进行了宏定义,避免了强编码的问题;对generate以及solve等接口进行了设计,返回值均为基本数据类型,对传进来的数据也没有其他要求。生成的Core.dll经试验也可以适用于其他程序。

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

设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。

只有一个Core类,计算模块的接口和项目要求一致,有solve接口,和两个generate接口。下面分别来讲:

  • solve接口
    //声明
    //puzzle为题面,solution为puzzle题面的解
    bool Core::solve(int puzzle[], int solution[]); 

因为solve接口需要对puzzle进行求解,并将解答填入solution中,因此分为以下三步:首先,对传进来的puzzle进行检测,如果puzzle有不符合数独格式,即行列宫中若出现了相同元素就返回false,结束调用,则就将puzzle中非0数字标记数组的相应元素置为1,将puzzle中的0替换为511,即二进制数111111111,这里是为了简化表示候选数,详细解释请参看个人项目博客。最后调用Fill函数进行求解,这里我们对个人项目里的Fill进行了一些修改,加入了求解模式参数,即

//Pre:
    bool Fill(int index, int puzzle[], int flag[]);
//Now:
    bool Fill(int index, int puzzle[], int flag[], int solveMode);

这里的求解模式是为了-u参数而设计,solveMode为1时,只对puzzle进行求解;solveMode为2时,对puzzle进行两次求解,判断是否为单解。因为solve只进行求解,所以我们的solveMode置为1。关于Fill的详细解释参看个人项目博客。
流程图如下:

  • generate接口(3 params)
    //声明
    //number:要生成的数独个数
    //mode:模式(简单,中等,困难)
    //result:用来存储生成的数独
    //SUDOKU_SIZE:宏定义,81,表示数独格子数量
    void Core::generate(int number, int mode, int result[][SUDOKU_SIZE]);

generate方法要生成一些题面,因此很自然的想法是先生成一些终局,并对他们进行挖空。首先是生成终局,这里就要调用create方法,即:

    //参数意义同generate
    void Core::create(int number, int result[][SUDOKU_SIZE]);

create方法的大致思想是先将一个9 * 9棋盘的标红位置随机填入数字,然后将这些填入的数组成数组填入Set中判重,若重就重填,否则对这个数独进行求解,就可以得到终盘,然后对终盘进行行变换,这样的目的是尽可能的避免等价数独以及重复数独出现。

然而这样会产生无解数独,比如ABFI格子中填3,C格子中填一个不是3的数,这样就会导致第三宫无3可填,导致无解。为了消除这一问题,我们先是限制这9个数中同一数字最多出现3次。实践后发现这样会导致效率变低,因为这样可能导致某一位置只能填进一个数,这样就会导致多次回溯。因此综合正确性以及效能的考虑,我们最终限制9个数中同一数字最多出现2次。但这样是否真的严谨,我们觉得严格证明可能要用到图论的知识,因此在这里不进行详述,可能会单独开一个随笔来进行讨论QAQ。那么这样可以满足-c参数1,000,000的要求吗?是可以的,因为同一终盘行变换会产生3! * 3! * 3! = 216种不同终盘,同时对于这个添数数组,我们只考虑AB位置相同的情况,这样有9 * (8!)种排列,总个数为9! * 216 = 78382080种情况,远大于1,000,000,满足项目需求。
挖空的部分就以mode的不同来进行,easy难度挖[20, 31]个空,medium难度挖[32, 43]个空,hard难度挖[44, 55]个空,算法就是生成一个相应区间内的随机数,对随机位置进行挖空,挖够数目存入result中。调用结束。
流程图如下:

  • generate接口(5 params)
    //声明
    //与generate(3 params)相同的参数意义相同,lower,upper分别代表挖空个数的下限和上限,unique代表是否要求单解
    void Core::generate(int number, int lower, int upper, bool unique, int result[][SUDOKU_SIZE])

这个与generate(3 params)类似,只是在挖空后要判断是否为单解,不是则重新挖空,这里就要调用isUnique方法,来判断是否单解,声明如下:

    bool Core::isUnique(int puzzle[SUDOKU_SIZE]);

isUnique判断puzzle是否单解的方法是调用Fill,这里Fill的solveMode参数要设置为2,代表要判断进行两次求解判断是否单解。
流程图如下:

剩下的方法都是需要频繁使用的函数,并不是我们提供的接口。

  • 算法关键
    算法的关键就是调用的Fill方法和create方法,关于他们的特点以及和之前版本的差异我在前文都已描述。
  • 独到之处
    Fill并没有什么独到之处,就是很普通的回溯,对多解的处理也是简单粗暴的求解两次看返回值;create方法,在填初始数独的地方可以说有些独到之处。但由于我们匮乏的图论知识QAQ,目前还不能严谨的进行证明。

五、UML

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

记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。
改进性能模块花费了将近6个小时的时间。当时是由错误而发现的性能瓶颈。由于当时的create方法只是随机添加那9个空,自然在测试时发现了无解的bug,因此添加了最多出现3次的限制,实际操作后发现花费时间较长,经分析后发现会导致较深的回溯。因此限制只出现2次,这是初步性能改进之后运行-u -r 20~55 -n 10000的性能分析图:


可以看到消耗最大的是generate中的isUnique方法。因为每次判断都要直接解出来,所以暂时还有想到进一步优化的方法。

七、Design by Contract, Code Contract

优点:

  • 契约式设计会保证程序员在设计程序时明确地规定一个模块单元在调用某个操作前后应当属于何种状态。
  • 前置条件后置条件以及不变式极大程度上的对程序预期进行检查。很大程度上简化了调试。
  • 会生成很优质的文档,便于后续团队接手开发。
    缺点:
  • 契约的编写成本较大,需要权衡项目大小与契约成本的关系。
  • 对开发者的要求较高,需要时间学习良好的契约思想和技术。

这次作业中,对每个接口都进行了设计分析,近似地应用了契约式设计,但是没有生成优质的设计以及需求文档,同时针对相应的前置后置条件进行了单元测试,比如generate(5 params)的lower和upper的取值范围。

八、计算模块部分单元测试展示

1.部分单元测试代码及思路

1.generate

    TEST_METHOD(TestMethodGenerate3)
    {
        int test1_number = 1;
        int test1_mode = EASY;
        int test1_result[1][SUDOKU_SIZE] = { 0 };
        int test1_zeroCount = 0;
        bool test1_rightZero = false;
        core.generate(test1_number, test1_mode, test1_result);
        for (int i = 0; i < test1_number; i++)
        {
            for (int j = 0; j < SUDOKU_SIZE; j++)
            {
                if (test1_result[i][j] == 0)
                {
                    test1_zeroCount++;
                }
            }
        }
        test1_rightZero = (test1_zeroCount >= 20 && test1_zeroCount <= 31);
        //检查Test1生成的数独谜题是否符合规范:
        //0的个数是否在取值范围中
        Assert::IsTrue(test1_rightZero);
        int test2_number = 1000;
        int test2_mode = MEDIUM;
        int test2_result[1000][SUDOKU_SIZE] = { 0 };
        int test2_zeroCount[1000] = { 0 };
        bool test2_rightZero = true;
        bool test2_isSame = true;
        core.generate(test2_number, test2_mode, test2_result);
        for (int i = 0; i < test2_number; i++)
        {
            for (int j = 0; j < SUDOKU_SIZE; j++)
            {
                if (test2_result[i][j] == 0)
                {
                    test2_zeroCount[i]++;
                }
            }
            test2_rightZero = test2_rightZero && 
                (test2_zeroCount[i] >= 32 && test2_zeroCount[i] < 44);
        }
        set<string> test2_Set;
        for (int k = 0; k < test2_number; k++)
        {
            char test2_charSudoku[81];
            for (int l = 0; l < SUDOKU_SIZE; l++)
            {
                test2_charSudoku[l] = test2_result[k][l] + '0';
            }
            string test2_stringSudoku;
            test2_stringSudoku = test2_charSudoku;
            test2_Set.insert(test2_stringSudoku);
        }
        test2_isSame = haveSameSudoku(test2_Set, test2_number);
        //检查Test2生成的数独谜题是否符合规范:
        //0的个数是否在取值范围中和是否会产生相同的数独谜题
        Assert::IsTrue(test2_rightZero && !test2_isSame);
        int test3_number = 1000;
        int test3_mode = HARD;
        int test3_result[1000][SUDOKU_SIZE] = { 0 };
        int test3_zeroCount[1000] = { 0 };
        bool test3_rightZero = true;
        bool test3_isSame = true;
        core.generate(test3_number, test3_mode, test3_result);
        for (int i = 0; i < test3_number; i++)
        {
            for (int j = 0; j < SUDOKU_SIZE; j++)
            {
                if (test3_result[i][j] == 0)
                {
                    test3_zeroCount[i]++;
                }
            }
            test3_rightZero = test3_rightZero &&
                (test3_zeroCount[i] >= 44 && test3_zeroCount[i] < 56);
        }
        set<string> test3_Set;
        for (int k = 0; k < test3_number; k++)
        {
            char test3_charSudoku[81];
            for (int l = 0; l < SUDOKU_SIZE; l++)
            {
                test3_charSudoku[l] = test3_result[k][l] + '0';
            }
            string test3_stringSudoku;
            test3_stringSudoku = test3_charSudoku;
            test3_Set.insert(test3_stringSudoku);
        }
        test3_isSame = haveSameSudoku(test3_Set, test3_number);
        //检查Test3生成的数独谜题是否符合规范:
        //0的个数是否在取值范围中和是否会产生相同的数独谜题
        Assert::IsTrue(test3_rightZero && !test3_isSame);
    }

思路:TestMethodGenerate3测试主要对应了接口generate(int number, int mode, int result[][SUDOKU_SIZE]),可以生成三种难度不同的数独,上述代码即对着三种情况都进行了测试,分成了三个部分,分别测试0的个数是否合法以及是否会生成相同的数独谜题。

    TEST_METHOD(TestMethodGenerate5)
    {
        int test1_number = 1;
        int test1_lower = 20;
        int test1_upper = 30;
        bool test1_unique = true;
        int test1_result[1][SUDOKU_SIZE] = { 0 };
        int test1_flag[1][SUDOKU_SIZE] = { 0 };
        core.generate(test1_number, test1_lower, test1_upper, 
                        test1_unique, test1_result);
        int test1_zeroCount = 0;
        bool test1_rightZero = false;
        for (int i = 0; i < test1_number; i++)
        {
            for (int j = 0; j < SUDOKU_SIZE; j++)
            {
                if (test1_result[i][j] == 0)
                {
                    test1_zeroCount++;
                }
                else
                {
                    test1_flag[i][j] = 1;
                }
            }
        }
        test1_rightZero = (test1_zeroCount >= test1_lower && 
                            test1_zeroCount <= test1_upper);
        
        bool test1_expected = !core.Fill(0, test1_result[0], test1_flag[0], 2) && test1_unique;
        //生成单解数独并且0的个数在20到30之间
        Assert::IsTrue(test1_rightZero && test1_expected);
        int test2_number = 1000;
        int test2_lower = 30;
        int test2_upper = 55;
        bool test2_unique = true;
        int test2_result[1000][SUDOKU_SIZE] = { 0 };
        int test2_flag[1000][SUDOKU_SIZE] = { 0 };
        core.generate(test2_number, test2_lower, test2_upper,
            test2_unique, test2_result);
        int test2_zeroCount[1000] = { 0 };
        bool test2_rightZero = true;
        bool test2_isSame = true;
        bool test2_expected = true;
        set<string> test2_Set;
        for (int k = 0; k < test2_number; k++)
        {
            char test2_charSudoku[81];
            for (int l = 0; l < SUDOKU_SIZE; l++)
            {
                test2_charSudoku[l] = test2_result[k][l] + '0';
            }
            string test2_stringSudoku;
            test2_stringSudoku = test2_charSudoku;
            test2_Set.insert(test2_stringSudoku);
        }
        test2_isSame = haveSameSudoku(test2_Set, test2_number);
        //生成单解数独,0的个数在30到55之间,并验证重复性
        Assert::IsTrue(test2_rightZero && !test2_isSame);
        for (int i = 0; i < test2_number; i++)
        {
            for (int j = 0; j < SUDOKU_SIZE; j++)
            {
                if (test2_result[i][j] == 0)
                {
                    test2_zeroCount[i]++;
                }
                else
                {
                    test2_flag[i][j] = 1;
                }
            }
            test2_rightZero = (test2_zeroCount[i] >= test2_lower &&
                test2_zeroCount[i] <= test2_upper) && test2_rightZero;
            test2_expected = test2_expected &&
                !core.Fill(0, test2_result[i], test2_flag[i], 2) &&
                test2_unique;
        }
        //验证生成的数独是否是单解数独
        Assert::IsTrue(test2_rightZero && test2_expected && !test2_isSame);
    }

思路:单元测试TestMethodGenerate5对接口void generate(int number, int lower, int upper, bool unique, int result[][SUDOKU_SIZE])进行了测试,与上一个单元测试思路类似,主要对生成的数独的重复性、单解性和0的个数进行了判断。

2.solve

    TEST_METHOD(solve1)
	{
		int puzzle[81] = {
			9, 4, 1, 2, 8, 3, 6, 7, 5,
			3, 7, 2, 5, 6, 1, 8, 9, 4,
			8, 5, 6, 7, 4, 9, 1, 2, 3,
			2, 6, 4, 1, 5, 7, 3, 8, 9,
			1, 9, 5, 8, 3, 4, 7, 6, 2,
			7, 8, 3, 6, 9, 2, 5, 4, 1,
			5, 2, 7, 9, 1, 8, 4, 3, 6,
			4, 1, 9, 3, 7, 6, 2, 5, 8,
			6, 3, 8, 4, 2, 5, 9, 1, 7
		};
		int solution[81] = { 0 };
		Core c;
		Assert::IsTrue(c.solve(puzzle, solution));
	}
	TEST_METHOD(solve2)
	{
		int puzzle[81] = {
			9, 4, 1, 2, 8, 3, 6, 7, 5,
			3, 7, 2, 5, 6, 1, 8, 9, 4,
			8, 5, 6, 7, 4, 9, 1, 2, 3,
			2, 6, 4, 1, 5, 7, 3, 8, 9,
			1, 9, 5, 8, 3, 4, 7, 6, 2,
			7, 8, 3, 6, 9, 2, 5, 4, 1,
			5, 2, 7, 9, 1, 8, 4, 3, 6,
			4, 1, 9, 3, 7, 6, 2, 5, 8,
			0, 0, 0, 0, 0, 0, 0, 0, 0
		};
		int solution[81] = { 0 };
		Core c;
		Assert::IsTrue(c.solve(puzzle, solution));
	}
	TEST_METHOD(solve3)
	{
		int puzzle[81] = {
			9, 4, 1, 2, 8, 3, 6, 7, 5,
			3, 0, 2, 5, 6, 1, 8, 9, 4,
			8, 5, 6, 7, 4, 9, 1, 2, 3,
			2, 6, 4, 1, 5, 7, 3, 8, 9,
			1, 9, 5, 8, 3, 4, 7, 6, 2,
			7, 8, 3, 6, 9, 2, 5, 4, 1,
			5, 2, 7, 9, 1, 8, 4, 3, 6,
			4, 1, 9, 3, 7, 6, 2, 5, 8,
			6, 7, 8, 4, 2, 5, 9, 1, 0
		};
		int solution[81] = { 0 };
		Core c;
		Assert::IsFalse(c.solve(puzzle, solution));
	}
	int n = 100;
	int solve4puzzle[10000][81] = { 0 };
	TEST_METHOD(solve4)
	{
		int solution[81] = { 0 };
		Core c;

		c.generate(n, 1, solve4puzzle);
		for (int i = 0; i < n; i++)
		{
			Assert::IsTrue(c.solve(solve4puzzle[i], solution));
		}
		
		c.generate(n, 2, solve4puzzle);
		for (int i = 0; i < n; i++)
		{
			Assert::IsTrue(c.solve(solve4puzzle[i], solution));
		}

		c.generate(n, 3, solve4puzzle);
		for (int i = 0; i < n; i++)
		{
			Assert::IsTrue(c.solve(solve4puzzle[i], solution));
		}
	}
	int solve5puzzle[10000][81] = { 0 };
	TEST_METHOD(solve5)
	{
		int solution[81] = { 0 };
		Core c;

		c.generate(n, 20, 55, true, solve5puzzle);
		for (int i = 0; i <n; i++)
		{
			Assert::IsTrue(c.solve(solve5puzzle[i], solution));
		}

		c.generate(n, 20, 55, false, solve5puzzle);
		for (int i = 0; i < n; i++)
		{
			Assert::IsTrue(c.solve(solve5puzzle[i], solution));
		}
	}

思路:上面5个关于solve的单元测试分别在以下5种情况测试了solve接口:

  • 输入为一个已经填好的数独
  • 输入为一个正常的未填满数独
  • 输入为一个无解数独
  • 输入为generate接口(3个参数)生成的数独题目
  • 输入为generate接口(5个参数)生成的数独题目

构造单元测试的思路:从功能出发,按照一定的顺序,尽可能的去测程序可能遇到的各种情况。编写测试样例输入数据的时候,要思考这个输入数据还可能是什么类型、格式、取值。

2.单元测试覆盖率截图

九、计算模块部分异常处理说明

在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
我们将可能会出现的异常概括整理为五类,下面一一进行说明:

  • MyOrderException
    这个是为了防止用户输入除了规定参数之外的参数而导致程序crash,具体代码如下:
    //__declspec(dllexport)这部分是为了导出dll加上的
struct __declspec(dllexport)  MyOrderException : public exception
{
	const char * msg() const throw ()
	{
		return "Error : No Such Order\n\
        Correct Usage:\n\
            -c [number]                         (1<=number<=1,000,000)\n\
            -s [puzzle_file_path]               (absolute or relative)\n\
            -n [number]                         (1<=number<=10,000)\n\
            -n [number] -m [mode]               (1<=number<=10,000) (1<=mode<=3)\n\
            -n [number] -u                      (1<=number<=10,000)\n\
            -n [number] -r [lower]~[upper]      (1<=number<=10,000) (20<=lower<=upper<=55)\n\
            -n [number] -r [lower]~[upper] -u   (1<=number<=10,000) (20<=lower<=upper<=55)\n\
        Parameter order is arbitrary.";
	}
};

如代码所示会给出所有命令的使用方法以及参数的正确取值。
单元测试样例如下:

test_argc[18] = 1;

说明:因为我们把读取命令行的操作放在了read中,所以在测试read时就对这些异常进行了测试。

  • MyFileException
    这是为了处理有关于文件的异常处理,用于-s时,文件出现打不开,或者不存在这样的情况。具体代码如下:
struct __declspec(dllexport)  MyFileException : public exception
{
	const char * msg() const throw ()
	{
		return "Error : File Not Found\n\
        -s [puzzle_file_path]               (absolute or relative)\n\
        File must exist!";
	}
};

如代码所示会给出-s命令的使用方法以及参数的正确取值。
单元测试如下:

test_argc[14] = 3;
test_argv[14][1] = "-s";
test_argv[14][2] = "C:\Users\Administrator\Desktop\puzzle.txt";
//此时puzzle.txt并不存在	
  • MySudokuException
    这是为了处理有关于数独格式以及非法数独的异常。具体代码如下:
struct __declspec(dllexport)  MySudokuException : public exception
{
	const char * msg() const throw ()
	{
		return "Error : Unsupported Sudoku\n\
        File content must be legal!";
	}
};

会给出相应提示。
单元测试如下:

test_argc[14] = 3;
test_argv[14][1] = "-s";
test_argv[14][2] = "C:\Users\Administrator\Desktop\puzzle.txt";
//此时puzzle.txt中的数独格式不正确
  • MyParameterException
    这是为了处理有关参数取值的异常。具体代码如下:
struct __declspec(dllexport)  MyParameterException : public exception
{
	const char * msg() const throw ()
	{
        return "Error : No Such Order\n\
    Correct Usage:\n\
        -c [number]                         (1<=number<=1,000,000)\n\
        -s [puzzle_file_path]               (absolute or relative)\n\
        -n [number]                         (1<=number<=10,000)\n\
        -n [number] -m [mode]               (1<=number<=10,000) (1<=mode<=3)\n\
        -n [number] -u                      (1<=number<=10,000)\n\
        -n [number] -r [lower]~[upper]      (1<=number<=10,000) (20<=lower<=upper<=55)\n\
        -n [number] -r [lower]~[upper] -u   (1<=number<=10,000) (20<=lower<=upper<=55)\n\
    Parameter order is arbitrary.";
	}
};

单元测试如下:

test_argc[19] = 6;
test_argv[19][1] = "-r";
test_argv[19][2] = "20~100";
test_argv[19][3] = "-u";
test_argv[19][4] = "-n";
test_argv[19][5] = "-2";
  • MyFormatException
    这是为了处理不同参数组合的异常。
struct __declspec(dllexport) MyFormatException : public exception
{
	const char * msg() const throw ()
	{
        return "Error : No Such Order\n\
    Correct Usage:\n\
        -c [number]                         (1<=number<=1,000,000)\n\
        -s [puzzle_file_path]               (absolute or relative)\n\
        -n [number]                         (1<=number<=10,000)\n\
        -n [number] -m [mode]               (1<=number<=10,000) (1<=mode<=3)\n\
        -n [number] -u                      (1<=number<=10,000)\n\
        -n [number] -r [lower]~[upper]      (1<=number<=10,000) (20<=lower<=upper<=55)\n\
        -n [number] -r [lower]~[upper] -u   (1<=number<=10,000) (20<=lower<=upper<=55)\n\
    Parameter order is arbitrary.";
	}
};

单元测试如下:

test_argc[19] = 6;
test_argv[19][1] = "-r";
test_argv[19][2] = "20~100";
test_argv[19][3] = "-s";
test_argv[19][4] = "-c";
test_argv[19][5] = "-2";

十、界面模块的详细设计过程

我们一共设计了5个页面:主页面,游戏界面,记录界面,设置界面和介绍界面。多个界面被划分成主从关系,所有子页面都可以返回到主页面中。

1.主页面

主页面图:

参看以前玩过的网页小游戏和手机自带游戏,设置了5个按钮:开始游戏(start game),载入游戏(load game),最高分记录(record),设置(seeting)和游戏介绍(introduction)。其中载入按钮因为时间关系,功能被砍掉了。剩下的四个按钮都有自己的页面。
对于主页面的标题,因为Qt没有合适的字体,我们把自己生成的艺术字截图,之后贴到相应的位置(包括按钮上的文字)。

2.游戏页面

游戏页面图:

点击开始游戏按钮后,首先会进入难度选择页面。用户可以选择简单(easy)、中等(medium)和困难(hard)三种模式。必须选择一种难度后才可以进入到游戏页面。游戏页面选择了矩形横向。左半部分是81个数独按钮,每个3*3小的九宫格通过不同的颜色加以区分。游戏已有数字是普通字体,用户新填的数字是加大加粗字体。同时,如果用户点击了一个数字,全局的相同数字都会变成黄色,点击空白按钮则会恢复,用来提高游戏的用户体验。页面的右半部分从上至下依次是游戏计时器、生成新局按钮(generate)、提示按钮(hint),检查按钮(check),返回主页面按钮(back)和10个用来向数独按钮填数字的小键盘。游戏的计时器采用了时分秒的显示格式。生成新局按钮点击后首先会弹出一个窗口,提醒用户要确认当前局面已经完成或者想要放弃,点击弹窗的生成新局按钮(generate new)后会再次来到难度选择页面,之后重新生成数独题目。提示按钮点击后会在当前选定的数独各自里给出数独提示,如果当前局面是无解数独,则会弹出一个窗口,显示“Current Sudoku No Solution”。检查按钮可以让用户在填完数独后进行检查验证。如果当前局面还有没有数字的格子会弹窗提示用户“Current Sudoku is Still Blank”,如果答案错误会弹窗提示用户错误和具体的冲突位置,如果当前局面填写完整并且正确,会弹窗“Congratulations”。返回按钮点击后会返回到主页面,其中也有弹窗提示用户要确认当前局面。10个小键盘前9个分别对应数字里的1-9,最下面的矩形空按钮表示清空格子。填写数字需要用户鼠标点击格子选中之后点击小键盘上想要填取的数字。
游戏页面上的数独按钮和小键盘按钮都采用了贴图来提高游戏界面的美观性。

3.记录页面

记录页面图:

记录页面主要记录了三个游戏难度的最高分,将数据存储在外部文件里。每当用户成功完成某一难度的数独题目时,会暂停计时器并记录,将数据与记录的最优数据进行比较,如果优于之前的记录就进行更新。页面下方的按钮是返回主页面按钮。

4.设置页面

在设置页面里,用户可以选择自己喜欢喜欢的游戏背景图片。游戏提供了以下三种背景图片:

背景1:

背景2:

背景3:

页面下方是返回主页面按钮。

5.介绍页面

介绍页面图:

页面上半部分是数独游戏的英文介绍和简单玩法,最下面是返回主页面的按钮。

十一、界面模块与计算模块的对接

计算模块提供了三个接口:

  • void generate(int number, int mode, int result[][SUDOKU_SIZE]);
  • void generate(int number, int lower, int upper, bool unique, int result[][SUDOKU_SIZE]);
  • bool solve(int puzzle[], int solution[]);

界面模块通过调用这三个接口来实现软件的具体功能。

1.数独题目生成

数独题目有三种难度:简单、中等和困难,分别对应着第一个generate接口的mode参数的1、2、3。我们通过数独空格子的数目来对数独的难度进行区分:

  • 简单:20-31个空格子
  • 中等:32-43个空格子
  • 困难:44-55个空格子

这样的难度设定也得到了下面的游戏反馈:“简单的多长时间算正常啊”、“中等的好难”,“高级的真坑啊”

当确定用户选择的游戏难度后,向generate(number, mode, result[][81]),传入参数mode,number取值为1,返回的result数组就是一个难度为mode的数独题目。

2.数独提示

在用户数独填写遇到困难时,要提示答案给予帮助,提高游戏的体验性。在用户点击提示按钮后,首先要获取81个数独按钮上的数字,为空的格子数字为0。将得到的81个数字整合成一个81维的数组puzzle,并调用solve函数,传入puzzle数组。solve是一个布尔型函数,如果当前局面无解会返回false,这时界面模块会弹窗提示用户当前填写有误,数独无解:

如果solve返回为true,那么得到的数组solution就是当前局面的答案。之后再获取用户要得到提示的格子,向格子里填入solution对应的数字:

3.数独检查

用户填写完数独后,点击检查按钮,会获得当前局面的验证结果。与获得提示的步骤类似,首先判断当前局面是否有空格子,如果有空格子则直接结束验证过程,向用户弹窗提示:

如果没有空格子,将81个数独按钮整合成一个81维数组,判断每一行、每一列、每一个3*3九宫的数字是否都是由1-9九个数字组成,如果三个条件全都符合,则向用户弹窗提示答案正确:

如果三个条件没有全部符合,则向用户弹窗提示是哪里不符合条件:

4.UI模块设计

创建了一个Index类,里面是整个GUI的全部控件和槽函数。页面之间的进入与退出通过控件的show()和hide()来实现。用户点击按钮,触发槽函数,和数独计算有关的操作则会调用计算模块里的接口。结构如下图所示:

十二、结对的过程

我们主要利用国庆期间完成了结对编程的大部分工作。因为国庆假期我们都没回家,所以时间比较容易安排。在结对编程的过程中,基本上遵循了每隔一个小时变换角色的原则。也有许多之前没有想到的事情,比如最初我以为驾驶员使用自己的电脑效率会比较高,但其实在结对的过程中,在更换电脑的时候应该将代码push到github上,但很难保证更换的时候刚好完成了一个增量。而且连接github的网络不够稳定,pull和push的过程中会浪费一些时间,导致两人精力的分散,意外的降低效率。所以后来在结对编程的过程都使用一台电脑了。
结对图片:

十三、优点和缺点

结对编程的优点和缺点
优点:

  • 代码质量由水平高的一方决定,这样让双方都进行了学习。
  • 彼此监督,提高效率。
  • 交换岗位,劳逸结合。
  • 当面交流,思维碰撞可以产生更好的方法。

缺点:

  • 有时候很难找到两个人都有大块的空余时间。
  • 一旦驾驶员的思路比领航员快,两人的位置瞬间互换。

结对小伙伴的优缺点
和我结对的是同班的窦鑫泽(15061187),有很多优点值得我去学习:

  • 思路很开阔,常常能想到我想不到的高效解决办法。
  • 随时将下一步的工作记录在本子上,这样更有条理。
  • 动手能力很强,习惯有一点想法就实现看效果,而我总是有一些跟问题无关的顾虑。
  • 阅读代码的能力很强,可以很快了解别人的代码思路,而我往往要花更多的时间。

当然也有些小缺点:

  • 动手能力太快。。。有时候会写很多本来可以封装起来的功能函数。

当然瑕不掩瑜,跟豆豆的结对编程还是让我学到了很多东西。豆豆也教我学会基本的Qt编程,在这里给豆豆打call~2333

十四、PSP表格

附加部分1

我们是三个小组互相测试的,我们测试的是14011100赵奕,测试我们的是15061186安万贺。
测试者在github上提出的issue
在互相测试的过程中,异常错误处理我们还有遗漏。对于输入数据的异常判断我们是在generate和solve之外处理的,然而,当测试者直接调用generate并输入异常的数据时,generate还会执行,所以我们在generate里面又加了异常判断。
具体修改如下:

void Core::generate(int number, int lower, int upper, bool unique, int result[][SUDOKU_SIZE])
{
	create(number, result);
	int blankNum = 0;
	srand((unsigned)time(NULL));
        if (lower < 20 || lower>55)
        {
            throw(MyParameterException);
        }
        if (upper < 20 || upper>55)
        {
            throw(MyParameterException);
        }
        if (lower > upper)
        {
            throw(MyParameterException);
        }
	for (int i = 0; i < number; i++)
	{
		int temp[SUDOKU_SIZE] = { 0 };
		memcpy(temp, result[i], sizeof(int) * SUDOKU_SIZE);
		blankNum = rand() % (upper - lower + 1) + lower;
		//$todo: study set random
		for (int j = 0; j < blankNum; j++)
		{
			int t = rand() % SUDOKU_SIZE;
			while (result[i][t] == 0)
			{
				t = rand() % SUDOKU_SIZE;
			}
			result[i][t] = 0;
		}
		if (unique)
		{
			if (isUnique(result[i]))
			{
				continue;
			}
			else
			{
				memcpy(result[i], temp, sizeof(int) * SUDOKU_SIZE);
				i--;
				continue;
			}
		}
	}
}

我们对此进行了修改,在这里对这两位同学表示感谢[撒花][撒花]。他们的GUI加上我们的Core动态库可以正常运行,耦合度尚可,暂未发现其他严重的bug。
互测部分已经提交到dev-combine分支中。

附加部分2

下载地址:https://github.com/wz111/sudoku_pair/tree/dev-product/GUIBIN

用户体验

我们分别在自己的朋友中进行了小范围的发布,收到了一些用户反馈:

  • 有三个建议:
    第一个建议是:已有数字的字体和后填上去的数字的字体区分度不太明显,是否可以采用其他方式区分,例如不同颜色。
    第二个建议:是否可以考虑加上一个指导性的弹窗,告诉大家点一下空格再点数字这种填写方式。
    第三个建议:未完成时候的信息改成 the soduku is not completed怎么样。

  • 总体感觉还不错,包括数独来源,记录设置等等,但是有几个地方需要改进:
    1.最下面一行数字,菜单最后一行会被任务栏遮挡;
    2.填入当前行或当前列已有数字的时候应该有提示;
    3.希望计入当前已完成数独的个数。

  • 好处是不同背景色的九宫格可以缓解视觉疲劳,还有高亮显示相同数字很实用。缺点是这个程序要占用40M+的空间是不是可以优化?generate等4个按钮上的文字不够居中。鼠标悬浮在上面和点击之后要有明显的样式变化,界面上方有一个能拖走的小条条。

  • 程序对低分辨率显示屏很不友好,直到今天好多国行14寸电脑仍然是1366768的分辨率,你们的程序是1200800,就导致了在那种显示屏上软件界面显示不全。LoadGame是待开发功能吗?字体感觉还挺好,button的样式不够好看。还有为什么我的record被清除了?

  • 界面太土了,按钮方方正正的很不好看。有些选择按钮前面会有一个小圆圈,感觉很不好看。窗口大小调整后按钮位置会发生改变。

  • 亮点有:界面风格个性化,可以自行选择背景;高亮显示数字虽然降低了游戏难度,但是对于游戏向大众的推广有一定帮助,使得程序可以作为数独游戏的入门工具。不足在于:游戏中有load game但是在游戏过程中没有save,有些矛盾。另外建议可以在清除符上有中文注明,不然用户可能不太容易发现。

  • 优点和缺点:
    优点:
    1)界面设计非常好,可以自己修改背景图片,感觉很不错,如果可以用户自定义图片就更好了;
    2)按钮很有质感,字体很不错。
    缺点:
    1)窗口可以自己调整,会导致部分内容无法显示,建议设定成无法改变窗口大小,这一点提示框也是;
    2)Load功能没有实现的话最好点击之后给个提示,不然玩家会一脸懵逼
    3)hint功能不好……需要用户保证填入的一定是正确的,如果直接给提示会更好,而且提示最好可以给一个不一样的字体,告诉用户这个是被提示过的
    4)用户先点击可填入方块,再点击不可填入方块(此时黄色方格为不可填入的),这时候点击键盘会在上一个可填入的方格进行填入操作,如果忘了上一个点在哪里会导致没有注意哪里被改变
    5)个人感觉可填入和不可填入区分不明显

  • 界面按钮可以再做个好看点的,字体还不错,计时能不能加个暂停功能。

  • 应该用键盘输入用方向键改变输入位置,在游戏页面加一个刷新按钮,实在填不下去了就刷新一局,难度选择页面应该加上back。

  • 优点:1.可以更换背景图片;2.难度区分明显;3.可以找出数独表格中已经出现的相同数字,便于检查。
    缺点:1.找的图片有点儿low233333,建议可以设计不同风格不同系列的图片(这个工作量有点儿大)。2.无法更改页面大小,建议能对页面进行调整。

  • generate和back我觉得功能是一样的,hint没限制呀,load game是没编吗?

对此我们进行了讨论,对这些反馈做出了如下修改:

  • 针对已填空格和题面的区分问题,考虑到这个建议只出现了一次,而且我们进行了字号放大以及加粗的处理,所以不进行修改,至于变颜色的建议,我们认为在已经有相同数字变黄的功能的基础上再变颜色有些乱,暂不进行修改。
  • 针对指导玩法的系列建议,我们新编写了“开始游戏前请读我”这样一个指导文档。
  • 低分辨率的问题,因为我们的软件最初设置的是1200 * 800,而且所有控件的位置和大小都是在这个基础上设定的,改起来几乎等于重置界面模块,所以就放弃了。这也反应出使用布局的重要性。
  • 右下角小键盘上的清空按钮由原来的空白改为现在的clear。
  • 对灰色图标进行了修改,这样更加便于找到。
  • 针对可以无限hint的问题,我们在下一版中将加入hint一次加部分时间的功能。
  • 针对hint功能不友好的问题,我们觉得在9 * 9的棋盘里找到相同数字的行列宫不是困难的事情,毕竟为了游戏性考虑,有不会的就hint就失去了游戏的意义。所以暂不进行修改。

总结

我们把做好的配好环境的GUI发给各个同学,发现他们看问题的角度和我们天天写这个GUI代码的角度不一样,提出了很多有针对性的意见,比如为什么软件那么大,为什么不支持低分辨率的显示器,以及一些界面设计的问题。在收集反馈的过程中,其实有一部分反馈的功能我们已经实现了,只是用户不知道而已,所以我们写了一个说明文档“开始游戏前请读我”。针对一些控件的位置和标签做了微调。
在这个过程中我们发现,很多用户的反馈都是类似“挺好的”这种对软件修改没什么意义的反馈,我们难道就真的认为自己的软件很好了吗?我觉得不是,就比如我们的清空按钮一开始没有标记为“clear”,我们自己都觉得这样用户体验不好,但还是有人说“挺好的”,后来发现他其实都没有用到这个功能。因此怎样让用户深入使用软件,并且自己去发现并提出修改意见,这可能是个值得考虑的问题。

posted @ 2017-10-15 12:54  xinze  阅读(453)  评论(3编辑  收藏  举报