天天看點

結對項目-數獨程式擴充

1)Github傳送門

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

2)PSP表

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 30
· Estimate · 估計這個任務需要多少時間
Development 開發 990 2060
· Analysis · 需求分析 (包括學習新技術) 180 480
· Design Spec · 生成設計文檔
· Design Review · 設計複審 (和同僚稽核設計文檔)
· Coding Standard · 代碼規範 (為目前的開發制定合适的規範) 10
· Design · 具體設計 120 360
· Coding · 具體編碼 600
· Code Review · 代碼複審
· Test · 測試(自我測試,修改代碼,送出修改) 490
Reporting 報告 130 150
· Test Report · 測試報告 60
· Size Measurement · 計算工作量
· Postmortem & Process Improvement Plan · 事後總結, 并提出過程改進計劃
合計 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

-->右上角的九宮格不能放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 \"\351\273\221\344\275\223\";""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 \"\351\273\221\344\275\223\";""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 \"\351\273\221\344\275\223\";""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 \"\351\273\221\344\275\223\";"));
					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 \"\351\273\221\344\275\223\";""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.\n\n");
           

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

	if ((upper > EMPTY_UPPER))
	{
		throw NumberOutOfBoundException("The number of upper is bigger than maximum 50.\n\n");
	}
	if ((upper < EMPTY_LOWER))
	{
		throw NumberOutOfBoundException("The number of upper is smaller than minimum 20.\n\n");
	}

	if ((lower > EMPTY_UPPER))
	{
		throw NumberOutOfBoundException("The number of lower is bigger than maximum 50.\n\n");
	}
	if ((lower < EMPTY_LOWER))
	{
		throw NumberOutOfBoundException("The number of lower is smaller than minimum 20.\n\n");
	}
           

【第五部分】

下載下傳位址:

使用者回報

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的美化問題,做了一些修改,但是...讓所有人都滿意好難...

儲存功能和其他一些功能由于時間原因,未添加。