1 Github项目地址

https://github.com/wz111/sudoku_pair
命令行程序:https://github.com/wz111/sudoku_pair/tree/master/BIN
GUI:https://github.com/wz111/sudoku_pair/tree/master/GUIBIN


2 在开始实现程序之前,在下述PSP表格记录下你估计将在程序的各个模块的开发上耗费的时间

DGvzwJJ.png


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

除了所规定的三个接口实现求解数独和生成数独功能外,我们还定义了其他接口,并严格保证数据格式。比如generate的result数组是一个二维数组,和生成数独功能有关的接口就全部是二维的;solve接口的输入是一维的,和求解数独功能有关的接口就全部是一维的。尽可能的去缩小每一个模块,当实现其他功能时,去将多个小模块串联起来而不是重新写新的模块。每个接口所实现的功能相对固定和独立,减少接口之间彼此的依赖。


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

只有一个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的详细解释参看个人项目博客。
流程图如下:

1220932-20171014200246590-635267055.png

  • 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中判重,若重就重填,否则对这个数独进行求解,就可以得到终盘,然后对终盘进行行变换,这样的目的是尽可能的避免等价数独以及重复数独出现。

1220932-20171014210252637-1654355722.jpg

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

1220932-20171014212416340-24772908.png

  • 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,代表要判断进行两次求解判断是否单解。
流程图如下:

1220932-20171014221343809-990400240.png

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


5 阅读有关UML的内容,画出UML图显示计算模块部分各个实体之间的关系(画一个图即可)

AUbKfFS.png


6 计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2015/2017的性能分析工具自动生成),并展示你程序中消耗最大的函数。

改进性能模块花费了将近6个小时的时间。当时是由错误而发现的性能瓶颈。由于当时的create方法只是随机添加那9个空,自然在测试时发现了无解的bug,因此添加了最多出现3次的限制,实际操作后发现花费时间较长,经分析后发现会导致较深的回溯。因此限制只出现2次,这是初步性能改进之后运行-u -r 20~55 -n 10000的性能分析图:

1220932-20171015002610199-841226664.png

1220932-20171015002619684-852301565.png

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


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

契约式编程优点:

  • 规定了形式化的接口,分工明确,结构和逻辑清晰
  • 前置条件、后置条件和不变式违反任何一个都会抛出异常,出现错误可以有效定位
  • 参与契约式编程的人员编程更加规范化,可靠性更高

契约式编程缺点:

  • 不够灵活
  • 过于冷僻,普及度不高

作业博客规定了generate和solve等接口,我们在严格按照这个接口的基础上,自己设定了其他接口,并规范每个接口的输入输出数据格式。

参考:https://www.zhihu.com/question/19864652


8 计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中

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.单元测试截图

dhvmBrY.png

OgyiCTA.png


9 计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景

我们将可能会出现的异常概括整理为五类,下面一一进行说明:

  • 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";

10 界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程

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

1.主页面

主页面图:

sz7eo7p.png

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

2.游戏页面

游戏页面图:

rYUbTAG.png

点击开始游戏按钮后,首先会进入难度选择页面。用户可以选择简单(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.记录页面

记录页面图:

GMx5KOI.png

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

4.设置页面

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

背景1:

0AYlJmd.png

背景2:

NfVvMZT.png

背景3:

glJPCNd.jpg

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

5.介绍页面

介绍页面图:

RewcphK.png

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


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

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

  • 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个空格子

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

gdd4WCo.png

N87nb3Z.png

O5upRg7.png

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

2.数独提示

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

cekErhl.png

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

i2zjozb.png

3.数独检查

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

uDlkEhL.png

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

Xs1yby6.png

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

58eKdkB.png

4.UI模块设计

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

yLyNosY.png


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

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

RFSxjTa.jpg


13 看教科书和其它参考书,网站中关于结对编程的章节,例如:说明结对编程的优点和缺点。

结对编程优点:

  • 在结对双方有过一起结对编程的经验后,开发质量会很高
  • 结对编程会让结对的彼此互相熟悉,增强工作氛围
  • 技术会在双方之间互相分享,技术弱的一方可以快速的提高自己

结对编程缺点:

  • 在开始阶段需要两人相互磨合
  • 结对编程要求两人的空闲时间有交集
  • 性格不合的两个人往往只会事倍功半

partner优点:

  • 有耐心
  • 善于掌握新的知识
  • 很随和

partner缺点:

  • 不太说出自己的想法

14 在你实现完程序之后,在附录提供的PSP表格记录下你在程序的各个模块上实际花费的时间。

MkHkE7M.png


附加题

15 测试松耦合

我们是三个小组互相测试的,我们测试的是14011100赵奕,测试我们的是15061186安万贺。

测试者在github上提出的issue:https://github.com/wz111/sudoku_pair/issues

在互相测试的过程中,异常错误处理我们还有遗漏。对于输入数据的异常判断我们是在generate和solve之外处理的,然而,当测试者直接调用generate并输入异常的数据时,generate还会执行,所以我们在generate里面又加了异常判断。同时我们限制了窗口的大小,并且在load一栏加入了有待开发的提示。

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;
    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;
        }
    }
    }
}

16 用户反馈

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

1)有三个建议:

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

2)总体感觉还不错,包括数独来源,记录设置等等,但是有几个地方需要改进:

  • 1.最下面一行数字,菜单最后一行会被任务栏遮挡;
  • 2.填入当前行或当前列已有数字的时候应该有提示;
  • 3.希望计入当前已完成数独的个数。

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

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

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

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

7)优点和缺点:

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

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

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

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

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

改进:我们把做好的配好环境的GUI发给各个同学,发现他们看问题的角度和我们天天写这个GUI代码的角度不一样,提出了很多有针对性的意见,比如为什么软件那么大,为什么不支持低分辨率的显示器,以及一些界面设计等。低分辨率的问题,因为我们的软件最初设置的是1200*800,而且所有控件的位置和大小都是在这个基础上设定的,改起来几乎等于重置界面模块,所以就放弃了。在收集反馈的过程中,其实有一部分反馈的功能我们已经实现了,只是用户不知道而已,所以我们写了一个说明文档“开始游戏前请读我”。右下角小键盘上的清空按钮注明了clear。一些控件的位置和标签做了微调。

转载于:https://www.cnblogs.com/AlenDou-blog/p/7667662.html

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐