- GitHub位址:https://github.com/Liu-SD/SudoCmd (這個位址是指令行模式數獨的倉庫,包含了用作測試的BIN。DLL核心計算子產品位址是:https://github.com/Liu-SD/SudoCore ,UI界面項目位址是:https://github.com/Liu-SD/SudoUi 。)
- PSP表格放在文末。
- 看教科書和其它資料中關于Information Hiding, Interface Design, Loose Coupling的章節,說明你們在結對程式設計中是如何利用這些方法對接口進行設計的。
- 我們在程式設計中将程式分為了多個子產品。算法部分就分為了十字連結清單子產品和算法子產品。他們之間是被調用和調用的關系。最後算法被封裝為一個find方法。find的參數為生成數獨的數量,是否加入随機性以及對結果做操作的函數指針。傳回布爾值。當找到了要求數量的數獨終局時為真否則為假。輔以addRestrict和clearRestrict方法就可實作數獨生成時的限制以及清楚限制。再上層封裝了三個接口分别為generate和solve。提供數獨的生成功能。在UI子產品隻通過調用接口實作計算子產品功能。這種代碼結構使得項目的邏輯劃厘清晰。且為松耦合。
- 計算子產品接口的設計與實作過程。設計包括代碼如何組織,比如會有幾個類,幾個函數,他們之間關系如何,關鍵函數是否需要畫出流程圖?說明你的算法的關鍵(不必列出源代碼),以及獨到之處。
- 在數獨生成算法中加入随機性,提高可玩性:
if (withRandom)
shuffle(crossPtr, crossPtr + min_col_count,
std::default_random_engine(std::chrono::system_clock::now().time_since_epoch().count()));
```
- 在原始的dlx算法中加入這一部分,使得在選中最少的列後,不再按之前的那樣從該列第一個元素開始壓到棧中,而是将該列元素打亂,随機挑選一個壓如棧中。此做法可以實作數獨生成算法的随機性且幾乎不會影響算法效率。但同樣存在一定問題,即算法使用的是遞歸回溯,使得同一批生成的數獨有很大的相似性。是以在上層生成多個随機數獨時應該多次調用該算法,每次生成一個數獨。
- 以下是程式結構圖:

- 畫出UML圖顯示計算子產品部分各個實體之間的關系(畫一個圖即可)。
結對項目——數獨擴充 - 計算子產品接口部分的性能改進。記錄在改進計算子產品性能上所花費的時間,描述你改進的思路,并展示一張性能分析圖(由VS 2015/2017的性能分析工具自動生成),并展示你程式中消耗最大的函數。
- 在generate_r接口中的兩個參數k_maxtry和k_maxback兩個參數可以确定在每個數獨終局上嘗試挖空的次數。如果這兩個數字設定的過大,那麼可能會在一些很難挖出唯一解的數獨上浪費時間。如果這兩個數字設定過小,就可能會造成數獨的生成次數過多。最終參數優化結果是k_maxtry = 7和k_maxback = 3組合的耗時最少。
結對項目——數獨擴充
- 在generate_r接口中的兩個參數k_maxtry和k_maxback兩個參數可以确定在每個數獨終局上嘗試挖空的次數。如果這兩個數字設定的過大,那麼可能會在一些很難挖出唯一解的數獨上浪費時間。如果這兩個數字設定過小,就可能會造成數獨的生成次數過多。最終參數優化結果是k_maxtry = 7和k_maxback = 3組合的耗時最少。
- 看Design by Contract, Code Contract的内容:描述這些做法的優缺點, 說明你是如何把它們融入結對作業中的。
- 契約式程式設計。即在程式設計實作之前,将接口的規格确定好。調用方有責任滿足規格要求的輸入條件,被調用方則必須在正确輸入的條件下輸出正确的結果。這使代碼的結構清晰,并且在出現問題時能很快确定錯誤的位置和原因。
- 在我們的結對作業中,接口做了輸入的判斷處理。如果出現問題可以很快确定錯誤是在調用方還是在被調用方。
- 計算子產品部分單元測試展示。
- 以下是單元測試部分代碼:
TEST_METHOD(TestGenerate3)
{
int START[4] = { 0, 20, 30, 40 };
int **result;
result = new int[100];
for (int i = 0; i < 100; i++)
result[i] = new int[81];
for (int mode = 1; mode < 4; mode++) {
generate(100, mode, result);
int rstr[81][3] = { 0 };
int rstr_p = 0;
for (int i = 0; i < 100; i++) {
rstr_p = 0;
for (int j = 0; j < 81; j++) {
if (result[i][j]) {
rstr[rstr_p][0] = result[i][j];
rstr[rstr_p][1] = j / 9 + 1;
rstr[rstr_p][2] = j % 9 + 1;
rstr_p++;
}
Assert::AreEqual(81 - rstr_p >= START[mode], true);
DLX.addRestrict(rstr_p, rstr);
Assert::AreEqual(DLX.find(1, false, NULL), true);
DLX.clearRestrict();
for (int i = 0; i < 100; i++)delete[] result[i];
delete[] result;
TEST_METHOD(TestGenerate5)
int uppers[10] = { 20,55,55,55,55,55,20,20,46,50 };
int lowers[10] = { 20,55,20,20,46,46,20,20,40,40 };
for(int j=0;j<10;j++){
bool unique = j%2==0;
int upper = uppers[j];
int lower = lowers[j];
int number = 1000;
//這裡可以把數字調小點,如果時間慢的話
int **result = new int[number] {0};
for (int i = 0; i < number; i++)
result[i] = new int[81]{ 0 };
generate(number,lower,upper,unique,result);
for (int i = 0; i < number; i++) {
int rstr[81][3] = { 0 };
int rstr_p = 0;
for (int j = 0; j < 81; j++) {
if (result[i][j])
{
rstr[rstr_p][0] = result[i][j];
//bug 2 下标都寫錯為了0
rstr[rstr_p][1] = j / 9 + 1;
rstr[rstr_p][2] = j % 9 + 1;
++rstr_p;
}
}
DLX.addRestrict(rstr_p, rstr);
//1.挖空數量是否足夠
if(!unique)
Assert::AreEqual(rstr_p>=(81-upper)&&rstr_p<=(81-lower),true);
else
//2.是否滿足唯一解要求
Assert::AreEqual(DLX.find(1, false, NULL), true);
if (unique) {
Assert::AreEqual(DLX.find(2,false, NULL), false);
}
DLX.clearRestrict();
}
}
}
```
- 單元測試結果如圖:

- 計算子產品部分異常處理說明。
-
在這次異常進行中,除了極少部分的異常,其他的異常我都專門寫在了指令行參數的處理子產品中,下面我詳細的分析一下異常部分的處理。
這次的外部輸入,可能就是指令行參數的輸入了,而我們這次根據指令行要實作的具體功能就是四個:
- -c 生成數獨終局
- -s生解決數獨檔案中的數獨遊戲
- -m和-n根據難易程度生成數獨遊戲
- -r和-n(-u)根據挖空數生成數獨遊戲
- 同時根據我之前所定義的錯誤的兩種分類①功能性參數(-c -m -n等)的組合錯誤②内容型參數(-n後面的數字,-m後面的123等)的内容錯誤,我們可以從這兩個角度很容易的總結出所有可能的異常,思路如下:
- 如果參數的數量不符合規定,那麼會報出參數太少的錯誤。
- 參數的格式需要符合要求,即除了-u以外,其他的在功能性參數後必須要有内容型參數,如果不符合要求,那麼抛出參數格式不對的錯誤
- 參數格式都對了以後,就看參數的内容是不是有問題,這裡就要一個功能一個功能分析:
- 對于-c,我們規定它後面的數字字元串轉化為數字後必須要在1~100*10000,是以我們需要檢驗這個數字字元串是不是符合要求,注意這裡不僅要保證數字範圍不能過大,尤其需要注意的是需要保證這個數字範圍不能超過int,這個在OO中已經很熟練了。是以這裡可能抛出數字超出範圍的錯誤
- 對于-s,我們規定後面的字元串必須要是有效的檔案名,即檔案必須存在。這個我并沒有在指令行參數的處理子產品中規定,因為結對夥伴已經告訴我他在solve接口中判斷了,是以這裡沒有檢查檔案名是否存在
- 對于-r,我們規定後面必須是“x~y”的格式,是以不符合這個格式的會抛出參數格式不對的錯誤,而對于x和y不僅需要判斷是不是有數字範圍超出的錯誤,還要保證lower必須要小于等于upper,如果不滿足需要抛出lower大于upper的錯誤
-
對于-n,我們需要規定判斷後面的數字字元串是不是滿足1~10000的範圍限制,具體的錯誤定義和之前的-c一緻
對于-m,我們同樣需要規定後面的數字字元串是不是滿足在1~3的範圍内
- 對于-u,其實查不出什麼錯
- 功能性參數都檢測好後,接下來需要做的就是檢查參數的組合問題,組合不正确需要抛出參數組合錯誤。
- 綜上,我們可以定義出5種錯誤:
- 參數太少:指令行輸入: sudoku.exe -c
-
try{
paraHandle(argc,argv,req);
}catch(too_few_para &e){e.what();}
會捕獲到對應的異常
2. 參數格式不對:指令行輸入: sudoku.exe aaa
```
}catch(format_err &e){e.what();}
3. 數字超出範圍:指令行輸入: sudoku.exe -c 100000000000000
}catch(out_of_range &e){e.what();}
4. lower大于upper:指令行輸入: sudoku.exe -r 55~54 -n 100
}catch(lower_biggerthan_upper &e){e.what();}
5. 參數組合錯誤:指令行輸入: sudoku.exe -c 100 -s puzzle.txt
}catch(combination_err &e){e.what();}
- 界面子產品的詳細設計過程。
- 界面子產品的重點在于數獨棋盤的設計。我們設計的棋盤由81個pushButton組成。每個按鈕使用styleSheet做不同的變形。在相應函數中,使用正則比對和修改styleSheet進而實作按鈕式樣的動态變化。這裡貼幾個按鈕的stylesheet:
border-style:solid;
border-color:black;
border-width:1px;
border-top-width:2px;
border-top-left-radius:10px;
border-left-width:2px;
- 下面是一個動态修改stylesheet的代碼示例:
const std::regex bg("(background-color:).+?;\\n");
QPushButton *pb = board_[i];
std::string styleSheet = pb->styleSheet().toStdString();
styleSheet = std::regex_replace(styleSheet, bg, "$1" + none_rstrColor+ ";\n");
pb->setStyleSheet(styleSheet.c_str());
- 界面子產品與計算子產品的對接。
- 将計算子產品封裝到DLL中,然後在界面子產品動态調用DLL。以下是調用DLL中函數的過程:
typedef void(*GENERATE_M) (int, int, int**); typedef void(*GENERATE_R) (int, int, int, bool, int**); typedef bool(*SOLVE_S) (int *, int *); HMODULE coreDLL; GENERATE_M generate_m = NULL; GENERATE_R generate_r = NULL; SOLVE_S solve_s = NULL; coreDLL = LoadLibrary(TEXT("Core/SoduCore.dll")); generate_m = (GENERATE_M)GetProcAddress(coreDLL, "generate_m"); generate_r = (GENERATE_R)GetProcAddress(coreDLL, "generate_r"); solve_s = (SOLVE_S)GetProcAddress(coreDLL, "solve_s"); FreeLibrary(CoreDLL); void MainWindow::initBoard(int mode, bool unique){ ... if(unique){ int difficultyDivide[4] = {20, 32, 44, 56}; generate_r(1, difficultyDivide[mode - 1], difficultyDivide[mode] - 1, true, &originBoard); } else{ generate_m(1, mode, &originBoard); } ... } void MainWindow::newGame(){ ... if(currentindex<3){ int difficultyDivide[4] = {20, 32, 44, 56}; generate_r(1, difficultyDivide[currentindex ], difficultyDivide[currentindex+1] - 1, true, &originBoard); } else{ generate_m(1, (currentindex%3)+1, &originBoard); } ... }
- 描述結對的過程,提供非擺拍的兩人在讨論的結對照片。
- 話不多說,直接貼照片:
結對項目——數獨擴充
- 話不多說,直接貼照片:
- 說明結對程式設計的優點和缺點。結對的每一個人的優點和缺點在哪裡 (要列出至少三個優點和一個缺點)。
- 優點:
- 程式設計的人就隻是程式設計,隻需要考慮目前問題,而不需要思考整個項目的結構。而在旁邊指揮的人則要引導程式設計者不出現錯誤。這可以提高整個的程式設計效率。
- 缺點:
- 一個人的消極情緒可能會影響另一個人。進而導緻項目開發進行不下去。
- 解的優點:
- 行動迅速,思維靈活,代碼結構清晰,注釋完善。
- 解的缺點:
- 想得多做得少。
- 劉的優點:
- 積極主動,有較高的審美水準(哈哈哈哈哈),學習能力強。
- 劉的缺點:
- 代碼沒有注釋。
- 優點:
- 完整PSP表格。
personal software process stages | 預估耗時 | 實際耗時 |
---|---|---|
計劃 | ||
- 估計這個任務需要多少時間 | 30 min | 40 min |
開發 | ||
- 需求分析(包括學習新技術) | 300 min | |
- 生成設計文檔 | 0(沒做設計文檔) | |
- 設計複審(和同僚稽核設計文檔) | 0(沒有同僚複審) | |
- 代碼規範(為目前的開發制定合适的規範) | 50 min | 60 min |
- 具體設計 | 400 min | |
- 具體編碼 | 1000 min | 1500 min |
- 代碼複審 | 120 min | |
- 測試(自我測試,修改代碼,送出修改) | ||
報告 | ||
- 測試報告 | ||
- 計算工作量 | 0(沒做這項工作) | |
- 事後總結,并提出過程改進計劃 | ||
合計 | 2250 min | 3530 min |
- 一些GUI截圖:
結對項目——數獨擴充
第四階段部落格:
- 合作小組兩位同學學号:15061119 15061104
- 代碼合并的過程有些曲折...我們本想在qt creator上修改相應的代碼引入他們的lib,但是因為至今仍然不明的原因,qt一直不能争取的引入這個lib,是以不得已我們把我們的代碼轉移到VS上,使用VS的qt插件,修改相應代碼就可以正确的使用他們的lib了。
-
經過測試,他們的子產品沒有大的問題,但是有兩點不足:
1.根據數獨的生成情況可以看出他們的數獨遊戲的終局生成沒有加入随機的因素,即生成數獨的順序是按照固定的順序回溯得到的,這樣導緻可玩性有些下降。
2.他們的生成唯一解的數獨算法最後生成的數獨空的分布不均勻,它們的挖空總是集中在上半部分。
- 以下是唯一解模式下的截圖:
結對項目——數獨擴充
第五階段部落格:
- 我們把我們的數獨程式介紹給周圍人玩,收到了如下回報:
- “沒有支援鍵盤輸入很不爽”。關于這一點,因為截止時間快到了,是以在上交的版本還沒有時間鍵盤輸入,不過之後一定會實作鍵盤的輸入的。
- “提示錯誤時把一整行都變紅了,感覺很不舒服”。這裡我們把這部分改了,原來是把錯誤的行/列/九宮格變紅,現在是僅僅把錯誤重複的兩個數字變紅,這樣能舒服很多
- “沒有新手引導”。新手引導目前隻能加上一個help按鈕,給你說如何操作,更詳細的引導之後再加。
- “太難了,不玩了不玩了”。這位同學連easy模式都覺得難,我覺得這是他自己的問題:)
- “remind me次數是不是需要加個限制”,這個限制之後會加。
- “有個bug,gui剛打開就可以點選數獨格子了”。這确實是個bug,在使用者設定好模式之前,不能讓填數獨的格子Enable。