Fork me on GitHub

数独_结对项目

结对项目-数独

Github地址

https://github.com/ZhaoYi1031/Sudoku_Pair

第一阶段

难度(初级/中等/复杂)界定

关于难度的界定,我们小组没有只采取空格数界定难度的方式,因为数独的难度空格多不一定难的. 比如 欧泊颗数独这里列的全是17数的数独,但玩入门级与骨灰级,根本不是一个概念。

因此我阅读了一些相关资料,从这篇博文,又引入了自由度的概念。

数独的空格自由度,指除掉空格本身,空格所在行、列、九宫内的空格数总和。

因此,当只剩一个空格时,此时的自由度为0;当数独全为时,空格数为81,空间自由度为81*24=1944达到最大。

自由度越大代表数独越难,但自由度和空格数不完全程正比。因此那篇博主进行了自由度的阶段区分,并在后文中验证了模型的合理性。

Screen 2017-10-15 AM11.26.40.jpg

我们关于自由度的定义和他略有不同,主要在于他在算同一行同一列的自由度时,算入了自己。而我们没有。他最终分了10个等级,我们要求的是3个。因此我们最终的难度设定如下:

 简单中等复杂
空格数范围 [40,49] [50,56] [56,65]
自由度范围 [0,650] [651,999] [1000,1944]

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

首先主要的生成&求解仍然采用之前的回溯的方法,求解方面加了一点优化,关于这方面的具体思路见博客数独_个人项目。然后针对这次的新要求,在生成具有限制个数的数独时,我采用的方法就是先生成足够多的满数独,然后进行挖空。其中挖空的策略仍然采用回溯:如果要挖空number个格子,那么每个格子被挖空的几率就是number/81,我们每次
都按照这个几率去决定格子的去留,执行一遍dfs. 一点小优化就是如果发现后面的格子都填上才能刚好够number,就直接挖空;如果空的格子已经达到了number, 那么之后的就直接保留。

流程图如下:

illustrate.png

具体的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.jpg

单元测试

例如我测试求解一个数独是不是有唯一解这个函数,我就通过来填入一个有多个解的数独来进行求解其解数,来进行判断。之后我也对有唯一解的数独进行了测试。

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提供清除一个格子内容和显示一个格子的提示。"submit"QPushButton用于检查答案,"home"按钮用于返回coverWindow。一个计时器用于记录花费的时间。玩家也可以通过上面的菜单栏来进行新的游戏或查看游戏规则等。

coverWindow.png
单击"Record"按钮查看三个难度的最佳记录。
record.png
单击"Setting"来设置游戏难度。
setting.png
在gameWindow中用81个QPushButton来呈现数独,9个QPushButton来填数,"clear"和"hint"两个QPushButton分别对应清除一个格子内容和显示一个格子的提示。
gridBlack.png
使用"clear"和"hint"时先通过鼠标单击选中一个需要填数字的格子。
1.png
选中后该格子会变成绿色。
2.png
然后再点击"clear"按钮即可清空上面的数字。
3.png
或者点击"hint"按钮获得这个格子应该填的数字。
4.png
"submit"QPushButton用于检查答案。
submit.png
"home"按钮用于返回coverWindow。
home.png
一个计时器用于记录花费的时间。点击时钟图标可以暂停和开始计时。
timer.png
玩家也可以通过上面的菜单栏来进行新的游戏或查看游戏规则等。
menu.png

核心代码如下:

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

检查用户提交答案的正确性

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

为按钮添加缩放动画

界面模块与计算模块的对接。详细地描述UI模块的设计与两个模块的对接,并在博客中截图实现的功能。

将计算模块封装在Core类里,生成dll文件供UI模块调用,并将计算模块的头文件放入UI模块中。
core.png
header.png

实现对接的代码, 调用generate生成数独初局:

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

调用solve求解数独:

bool ok = core.solve(puzzle, solution);
if (ok) {
    btnTarget->setText(QString::number(solution[i*matrixLen + j], 10));
}
else {
    QMessageBox::critical(&gameWindow, "warning", "No solution!  ");
}

第四阶段

互测

我们是三个小组进行互相测试的,我测试的是15061186安万贺,测试我们的是由15061187窦鑫泽来完成的。

首先我们测试安万贺小组的情况如下,我们先把他们的Core.dll和Core.lib拷贝到了我们的sudokuGUI工程下进行测试,发现他们的生成有这样的问题,就是不管怎么开始,终盘的第一行总是1..9,即如下效果:

1_Fotor.jpg

2_Fotor.jpg

然后我就给他们提出了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如下,是几个比较中肯与实际的测试问题:

软件只能通过右上角的红叉来结束,在开始界面中没有退出软件的按钮。
noExit.png
于是我们增加了可以在开始界面中退出游戏的按钮。
addExit.png

软件在点击"home"按钮返回开始界面时没有"Setting"按钮,影响用户体验。
settingFade.png
于是我们修复了这个bug,时用户可以在游戏途中返回主界面对游戏难度进行设置,从而优化了用户体验
addSetting.png

然后第三点,关于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:不太明白record保存的是什么,如果可以让玩家自己输入文件名就好了。
用户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.中文有的字会变为乱码,还是英文比较保险。

性能分析及改进

分别对-n 10000 -r 55~55-n 10000 -r 55~55 -u进行了测试,然后性能分析图如下,可以看到当没有考虑-u时,init_gen这个生成50000的函数的时间占据了主导,但是一旦当测试进了-u,这个占据的时间就是非常小的一部分,此时solve_unique函数就占据了主导,因为我们得一直从一个确定的数独终盘里面去挖空来找到有唯一接的数独。

Screen 2017-10-15 AM1.41.23.jpg

Screen 2017-10-14 AM10.17.45.jpg

Screen 2017-10-14 AM10.55.15.jpg

考虑的优化如下:我们可以不是对一个确定的数独终盘进行一直来挖空,而是当一个数独终盘一直挖空不出唯一解时,跳到下一个进行执行。优化的代码如下:

if (times > 1000) { //执行了这个数独终盘1000次还没有解,就做后一个数独终盘来生成。
    id++;
    times = 0;
}

阅读作业

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

首先引用一段契约的概念:

契约作用于两方,每一方都会完成一些任务,从而促成契约的达成,但同时,每一方也会接受一些义务,作为制定契约的前提,有任意一方无视了必尽义的义务,则契约失败

契约式编程,我们在面向对象时曾经常用这个概念。我们当时在在设计程序的时候,需要明确地规定一个类的实例,在调用某个操作前后应当属于何种状态。我们当时需要在所有的类和方法前协商了前置条件,后置条件,不变式等等。

优点:代码规范,有利于实现者、设计者、使用者进行协作。
缺点:实现起来有困难,而是契约双方都需要履行责任。在一个大的项目里,一旦有人违反契约,后果不堪设想。

我在第二阶段处理异常时,就遇到了『遇到异常,是由我的Core程序catch住保证不crash还是throw给外面catch』的问题。当时弄的很头疼。最后我和我的partner约定了调用Core的要求,保证他调用时的参数是正确的,而另一方面,作为Core核心的编写者,对于预料范围内的参数错误,进行输出错误信息到控制台反馈给了调用者。

另外关于契约式编程,安利一下伯乐在线的这篇文章,契约式编程与防御式编程. 讲到了关于平衡的艺术,值得一读。

结对照片

结对编程.jpg

结对编程优缺点

说明结对编程的优点和缺点。
结对的每一个人的优点和缺点在哪里 (要列出至少三个优点和一个缺点)。(5')

  • 优点:提升团队协作的能力;有利于互相测试发现小错误;互相学习知识;学习氛围比较好,比一个人单干快。
  • 缺点:分工不好的时候,效率低下。另外需要互相了解,需要磨合。

partner优缺点

  • 优点:代码能力强;思路清晰;编码效率高;算法好;学习新知识快
  • 缺点:对于有些新事物有点排外;有些小问题有点缺乏主见。

我的优缺点:

  • 优点:具备一定的编程能力;接触较多;自认为有一点美工意识
  • 缺点:面向对象思维有待提升;写前端的速度捉鸡

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

Information Hiding
就类似面向对象的封装的概念,把一些不能共享或者需要考虑安全的信息封装到内部,不让其他的使用者来修改这部分的内容。最明显的例子就是类里的private成员变量和函数。

Interface Design
接口是在一定粒度视图上同类事物的抽象表示。设计一套共用的interface来供多个对象来使用。对象的交互只需要通过接口来完成。

Loose Coupling
上计组时高小鹏老师曾经多次强调的代码设计应该保持的风格就是高内聚低耦合,而低耦合的概念就是模块和模块间的联系要少。这样有利于代码移植和互相测试。

具体地在我们的程序中最明显的一点就是,我们用Core模块来包装核心的实现generate等功能,然后生成动态链接库给其它的地方直接调用就好,这样既保证了别人无法修改你的generate函数实现信息隐藏,又非常便于调用和移植。然后互测也只要交换dll就好了,很是方便。

结对编程总结

人生第一次这种体验,我们采用的分工的方式,总体上我负责后端Core接口的编写、测试,他负责前端。界面的设计我也参与了其中,提供了一些美工和游戏的玩法设置上的建议,主要是颜色还有Button的颜色啊图片啊等等。形成了现在这种以黑色和红色为主色调的界面(和我最近穿的一件格子衬衫颜色几乎一样.-.)如果你们觉得设计的很土我背锅

然后感觉最大的收获就在于Git操作吧。虽然我们是双线并发作战,但仍然存在着两人一起改的冲突这样的局面,因此也算积累了一些协作编程写仓库的经验吧。下图是我们的Git信息的部分截图。

Screen 2017-10-19 AM9.32.07.jpg

花费时间

PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning 计划    
·Estimate ·估计这个任务需要多少时间 5 5
Development 开发    
·Analysis ·需求分析(包括学习新技术) 60 80
·Design spec ·生成设计文档 0 0
·Design Review ·设计复审 0 0
·Coding Standard ·代码规范 60 90
·Design ·具体设计 120 30
·Coding ·具体编码 600 1800
·Code Review ·代码复审 100 140
·Test ·测试(自我测试,修改代码,提交修改) 120 240
Reporting 报告 150 150
·Test Report ·测试报告 15 25
·Size Measurement ·计算工作量 10 5
·Postmortem & Process Improvement Plan ·事后总结,并提出过程改进计划 15 10
  合计 1265 2575

 

posted @ 2017-10-15 12:01  ohazyi  阅读(699)  评论(5编辑  收藏  举报