[2017BUAA软工]结对项目:数独扩展
結對項目:數獨擴展
1. Github項目地址
https://github.com/Slontia/Sudoku2
2. PSP估計表格
3. 關于Information Hiding, Interface Design, Loose Coupling的設計
首先,在王辰昱同學的提醒下,我們將一開始的代碼按照功能分為若干個.cpp文件,每一個.cpp只處理一件事,如create_puzzle.cpp文件負責生成數獨,而solve.cpp文件負責解決數獨,在一定程度上保證了代碼的低耦合性。
我認為在實際編碼中,最能體現這個思想的是dig函數和Rank類。首先來看一下dig函數在代碼中的調用情況:
這是create_puzzle函數的開頭部分,整個程序中只有這個地方用到了這個函數,其功能在于盡可能地清除一個數獨中的數字,并保證其單解性。這個函數在清除數字的時候會依靠推理,因此挖起來非常快。這是保證代碼速度的一個很重要的部分。但是如果不看dig.cpp文件,我們并不清楚其具體的實現過程,或是用了什么高端的算法,只知道“這是一個挖數非常快的函數”,我認為這是最能體現Information Hiding的部分。
除此之外,GUI項目中還有一個排行榜功能,這個功能位于Rank類中。我們在實現這個類之前,先考慮了一下可能的需要的功能,如清除記錄、插入到排行榜、寫入文件、讀取文件、加密等等。有了這些想法,我們便提供了相應的接口,之后再實現,我認為遵循了Interface Design。下面是rank.h中定義的部分函數:
4. 計算模塊接口的設計與實現過程。設計包括代碼如何組織,比如會有幾個類,幾個函數,他們之間關系如何,關鍵函數是否需要畫出流程圖?說明你的算法的關鍵(不必列出源代碼),以及獨到之處。(7')
主要有三個模塊:
1. create 模塊
這次作業雖然指定不允許出現等價數獨,但我們還是可以通過模板變換快速生成1000000個數獨。首先,我們需要保證數獨的等價性,我們的做法是不對第一個宮進行任何操作(隨機性貌似在-c不做要求?)。我們沿用了劉暢同學在個人項目中使用的3-3-3位置輪換方法成功生成了一個數獨終盤。在個人項目中,模板的變化主要體現在R4~R6、R7~R9的行變換,但其實,對于3-3-3位置輪換方法生成的終盤,部分行的變換也是可行的。
我們先來想一個問題,在一個3*3的宮中填入1~3三個數字各三次,保證行、列中沒有重復的數字,共有幾種填法呢?通過窮舉,我們可以發現有12種解,如下圖:
現在,我們將這個問題向數獨上靠攏,在生成的數獨上,我們標出圖中所示的紅、綠、藍數字:
由上面的結論我們可以得出,通過變換紅、綠、藍共九個數字,可以得到12種不同排列。由于第一個宮我們不能動,只能動R4~R9,因此共可以找到6組類似的9個數字,而這6組的排列又相互獨立,因此共可以產生12^6=2985984種排列。
2. solve 模塊
solve模塊的實現和個人項目相同,只是做了一下封裝。
據之前的思路,我設計了三個類:數獨類(Subject_sudoku)、組類(Group)、方格類(Box)。
數獨類包含三個組數組,名字分別為rows[9]、columns[9]和blocks[9],分別代表思路中描述的三種組。每個組包括指向9個Box的指針和記錄以確定數字的二進制數hasvalue。
在初始化數組之后,首先找到未確定值得Box中可能取值最少的那個,依次對它的值進行猜測。在每次猜測之前,通過拷貝構造將Subject_sudoku備份下來,在新的數獨中將該方格的值確定,再繼續尋找可能取值最少的Box,對它的值進行猜測,直到所有的Box的值都被確定,或嘗試完某個Box的所有可能性(無解)。
基本流程描述如下:
對象之間的關系構成網狀結構,便于Box和Group之間的信息傳遞
3. puzzle 模塊
獨到之處:
1. 選擇最有效的隨機方式。我們在隨機挖空的時候,發現純粹隨機的效果并不好,特別是要求生成55個空的獨解數獨時,會比較慢。但是如果在各個宮內部進行比較平均的挖空,得出來的空會分布得比較均勻,就容易產生單解數獨。
2. 結合邏輯推導。反向使用行列宮擯除法,可以挖掉當前局面來看具有邏輯必然性的數字(比如說第一行是 1 2 3 4 5 6 7 8 9,那么 1 可以被挖掉,因為 1 的存在是被其他 8 個數決定的),這樣,這部分的挖空可以不用交給隨機挖空來解決,降低了算法的時間復雜度。
5. UML圖顯示計算模塊部分各個實體之間的關系
6. 計算模塊接口部分的性能改進。記錄在改進計算模塊性能上所花費的時間,描述你改進的思路,并展示一張性能分析圖(由VS 2015/2017的性能分析工具自動生成),并展示你程序中消耗最大的函數。(3')
1. puzzle 模塊
測試指令 -n 10000 -r 20~35 -u
測試指令 -n 10000 -r 36~50 -u
測試指令 -n 10000 -r 51~55 -u
可見,FgMap::outside_lock 是最耗時間的函數,編寫之前已經考慮到了這個問題,因此采用了位運算等加速方法,實際上很難再優化了。
2. create 模塊
create 采用的是行列交換的方法生成不等價的數獨,因此很快,瓶頸在 io,但這里采用了緩存一次性輸出的方式,最大限度利用了 block 讀寫的功能,因此也無法再優化了。
3. solve 模塊
7. 看Design by Contract, Code Contract的內容:
http://en.wikipedia.org/wiki/Design_by_contract
http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述這些做法的優缺點, 說明你是如何把它們融入結對作業中的。(5')
優點:
缺點:
在本次結對作業中,我們通過定義了dig函數和rank類的需求,近似地實現了契約設計。dig函數負責將傳入的數獨盡可能地挖空,而rank類提供了各種對排行榜操作的接口。預先定義了這些需求再開始實際的代碼編寫,讓后期的代碼銜接更加簡單。
8. 計算模塊部分單元測試展示。展示出項目部分單元測試代碼,并說明測試的函數,構造測試數據的思路。并將單元測試得到的測試覆蓋率截圖,發表在博客中。要求總體覆蓋率到90%以上,否則單元測試部分視作無效。(6')
覆蓋率截圖:
單元測試
在編寫測試代碼之前,首先明確一下單元測試要實現的功能:
對于-c,需要檢測1、2;
對于-s,需要檢測1;
對于-n,需要檢測1、2,如果有-u,還需要檢測3。
合法性檢測
依然使用二進制數存儲法判斷合法性,這一點和個人項目完全相同,優點是速度快、代碼簡潔,代碼如下:
for (int i = 0; i < number; i++) {sudoku = new string();int* sudoku_ptr = result[i];for (int j = 0; j < SIZE; j++) {for (int k = 0; k < SIZE; k++) {int digit;digit = sudoku_ptr[GET_POS(j, k)];(*sudoku) += digit + '0';bit = (1 << (digit - 1));row_record[j] |= bit;column_record[k] |= bit;block_record[(j / 3) * 3 + k / 3] |= bit;}}// judge & initialfor (int i = 0; i < 9; i++) {Assert::AreEqual(511, row_record[i]);Assert::AreEqual(511, column_record[i]);Assert::AreEqual(511, block_record[i]);row_record[i] = 0;column_record[i] = 0;block_record[i] = 0;} }等價性檢測
沿用了個人項目中的字典樹,代碼變更如下:
typedef struct node {bool isbottom;int depth;string* sudoku;struct node* ptrs[9];}Treenode;Treenode* create_treenode(int depth, string* sudoku) {Treenode* p = (Treenode*)malloc(sizeof(Treenode));p->isbottom = true;p->depth = depth;p->sudoku = sudoku;for (int i = 0; i < 9; i++) {p->ptrs[i] = NULL;}return p;}void add_sudoku_to_tree(int depth, Treenode** p, string* sudoku) {if ((*p) == NULL) {(*p) = create_treenode(depth, sudoku);}else {if ((*((*p)->sudoku)).length() > 0) {if ((*sudoku).compare(*((*p)->sudoku)) == 0) {fclose(fout);}Assert::AreNotEqual(*sudoku, *((*p)->sudoku));add_sudoku_to_tree(depth + 1, &((*p)->ptrs[(*((*p)->sudoku))[depth + 1] - '1']), ((*p)->sudoku));(*p)->sudoku = new string("");}add_sudoku_to_tree(depth + 1, &((*p)->ptrs[(*sudoku)[depth + 1] - '1']), sudoku);}}其中,這一行
(*p)->sudoku = new string("");是變動后的代碼,之前的代碼為
*((*p)->sudoku) = "";
這段代碼的情境是本身位于這個節點的字符串遇見了新加入的字符串,于是他們需要根據自己接下來的字符,從該節點引申出兩個屬于它們的新節點,那里是他們的歸宿。可以看出之前的代碼是有問題的,sudoku是字典樹節點中儲存的字符串指針,舊代碼中為了清空節點,清空的是指針指向的值,導致字典樹中很多字符串都被清空了,所幸被測試的代碼沒有問題,不然后果很嚴重……新的代碼修改的是指針本身,不影響存入的字符串,是正確的處理方式。
除此之外,我們沿用了個人項目中的字典樹重復性判斷,這次新添了檢測等價性的功能。我們選取第一個宮,對里面的數字分別和1~9進行映射,之后將整個數獨根據映射刷新,再放入字典樹中進行判斷。映射的代碼如下:
單解性測試
其實在生成數獨題目的時候已經檢查過單解性了,這里引用的是那里的代碼:
bool generator_fill_sudoku(Subject_sudoku* sudoku, int &solution_counter) { /* -- succeed(true) or failed(false) */Box* box;//cout << sudoku->to_string() << endl;box = sudoku->get_minpos_box();if (box == NULL) {solution_counter++;if (solution_counter > SOLUTION_MAX) {return false;}return true;}return generator_guess_value(box, sudoku, solution_counter); }這個函數是對求解函數的修改,原來是找到解立刻輸出答案,現在是找到一個解繼續運行,直到找到的解的個數超過SOLUTION_MAX為止。這里SOLUTION_MAX的值為1。
9. 計算模塊部分異常處理說明。在博客中詳細介紹每種異常的設計目標。每種異常都要選擇一個單元測試樣例發布在博客中,并指明錯誤對應的場景。(5')
InvalidCommandException 錯誤的指令;
TEST_METHOD(command_exception1) {bool test_result = false;int argc = 4;char* argv[10] = {"sudoku.exe","-s","100","-c"};try {read_command(argc, argv);}catch (InvalidCommandException*) {test_result = true;}Assert::IsTrue(test_result); }CannotOpenFileException 無法打開文件;
TEST_METHOD(cannot_open) {bool test_result = false;Core core;try {core.input_file("puz.txt", result_solve);}catch (CannotOpenFileException* e) {test_result = true;}Assert::IsTrue(test_result); }BadFileException 文件異常或損壞;
TEST_METHOD(incompleted_sudoku) {FILE* ftest;int erno = fopen_s(&ftest, "puzzle.txt", "w");if (ftest == NULL) {cout << erno << endl;Assert::Fail();}Core core;fputs("4 1 7 2 3 8 6 5 9\n\3 2 6 4 9 5 8 1 7\n\9 5 8 7 1 6 3 2 4\n\6 9 1 8 5 2 7 4 3\n\8 4 2 9 7 3 1 6 5\n\7 3 5 6 4 1 9 8 2\n\1 8 3 5 2 7 4 9 6\n\2 7 9 1 6 4 5 3 8\n\5 6 4 3 8 9 2 7 b", ftest);fclose(ftest);bool test_result = false;try {core.input_file("puzzle.txt", result_solve);}catch (BadFileException* e) {test_result = true;}Assert::IsTrue(test_result); }InvalidPuzzleException 數獨謎題本身不符合規則(并非指全部無解謎題):
int index = 0; int digit; QPushButton* btn; for (int i = 0; i < SIZE; i++) {for (int j = 0; j < SIZE; j++) {int digit = puzzle[index++];btn = buttons[i][j];if (digit == 0) { // free gridbtn->setText("");btn->setEnabled(true);btn->setStyleSheet(UNCERTAIN_GRID_STYLE);numbers[i][j] = 0;}else {char num[2] = { '0' + digit, '\0' };btn->setText(num);btn->setEnabled(false);btn->setStyleSheet(CERTAIN_GRID_STYLE);numbers[i][j] = digit;}} }以上樣例全部測試通過。
10. 界面模塊的詳細設計過程。在博客中詳細介紹界面模塊是如何設計的,并寫一些必要的代碼說明解釋實現過程。(5')
我們主要的GUI界面只有一個,其它的還包括排行榜界面和成績寫入界面。
GUI的布局使用代碼生成,沒有使用.ui文件,原因是覺得.ui文件自動生成的代碼很臃腫,而自己寫的話可以建立數組管理各個組件(大概是我們沒有找到正確的方法?)。舉例來說的話,下面是生成數獨9*9個方格的代碼:
void SudokuGUI::create_grids() {QSignalMapper* mapper = new QSignalMapper(this);for (int i = 0; i < SIZE; i++) {for (int j = 0; j < SIZE; j++) {numbers[i][j] = 0;buttons[i][j] = new QPushButton("", this);QPushButton* btn = buttons[i][j];btn->setGeometry((j + 1) * BOX_SIZE + (j / 3) * 10 - 20,(i + 1) * BOX_SIZE + (i / 3) * 10,BOX_SIZE,BOX_SIZE); // set positionbtn->setEnabled(false);btn->setFont(QFont("Times", 18, QFont::Bold)); // set fondbtn->setStyleSheet(CERTAIN_GRID_STYLE); // set colorbtn->setFont(GRID_FONT);QObject::connect(btn, SIGNAL(clicked()), mapper, SLOT(map()));mapper->setMapping(btn, GET_GRIDNO(i, j));}}QObject::connect(mapper, SIGNAL(mapped(int)), this, SLOT(record_button(int))); }利用類似的for循環來初始化各個元件,他們的StyleSheet用預定義表示:
#define FUNCTION_FONT QFont("Consolas", 16, QFont::Normal) #define REMAINING_FONT QFont("Consolas", 14, QFont::Normal) #define GRID_FONT QFont("Consolas", 18, QFont::Normal) #define UNCERTAIN_GRID_STYLE "QPushButton:hover{\background-color:#AFEEEE;\ }"\ "QPushButton{\background-color:#66CCFF;\ }" #define CERTAIN_GRID_STYLE "QPushButton{\color:#1C2460;\background-color:#99CCFF;\ }" #define TIP_GRID_STYLE "QPushButton{\color:#1E90FF;\background-color:#99CCFF;\ }" #define WRONG_GRID_STYLE "QPushButton{\background-color:#DC143C;\ }" #define MARK_GRID_STYLE "QPushButton{\background-color:#DC143C;\ }" #define CURRENT_GRID_STYLE "QPushButton{\background-color:#FFFF66;\ }" #define INPUT_BOTTON_STYLE "QPushButton{\background-color:#FF69B4;\ }" #define FUNCTION_BUTTON_STYLE "QPushButton{\color:#000000;\background-color:#FFFF66;\ }" #define ENABLE_BUTTON_STYLE "QPushButton{\background-color:#DC143C;\ }" #define DISABLE_INPUT_BUTTON_STYLE "QPushButton{\background-color:#FFE4E1;\ }" #define DISABLE_FUNCTION_STYLE "QPushButton{\background-color:#fcf8ab;\ }" #define WINDOW_SYTLE ""下面是一些界面變化部分,首先是新游戲的開始,這里根據Core的generate接口處理數獨界面,將未被挖空的數獨對應按鈕置為Disable:
int index = 0; int digit; QPushButton* btn; for (int i = 0; i < SIZE; i++) {for (int j = 0; j < SIZE; j++) {int digit = puzzle[index++];btn = buttons[i][j];if (digit == 0) { // free gridbtn->setText("");btn->setEnabled(true);btn->setStyleSheet(UNCERTAIN_GRID_STYLE);numbers[i][j] = 0;}else {char num[2] = { '0' + digit, '\0' };btn->setText(num);btn->setEnabled(false);btn->setStyleSheet(CERTAIN_GRID_STYLE);numbers[i][j] = digit;}} }這次我們實現的功能有四個,除了要求的check和tip外,我們還附加了filter和track功能。filter是在當前格子內切換所有滿足填入規則的值,而track是將某種數字標紅便于查看。
check的設計思路是建立三個數組,分別對應行、列、組,并將每一個格子的數字分別存儲于這三個數組中,假如某個數組儲存的某種數字的數量大于1,說明出現了數字的重復,將重復的數字標紅:
int row_digit_counter[SIZE][SIZE] = { 0 }; int column_digit_counter[SIZE][SIZE] = { 0 }; int block_digit_counter[SIZE][SIZE] = { 0 };bool pass = true;// store box for (int i = 0; i < SIZE; i++) {for (int j = 0; j < SIZE; j++) {int value = numbers[i][j];if (value != 0) {row_digit_counter[i][value - 1]++;column_digit_counter[j][value - 1]++;block_digit_counter[GET_BLOCKNO(i, j)][value - 1]++;}else {pass = false;}} }// judge & initial for (int i = 0; i < SIZE; i++) {for (int j = 0; j < SIZE; j++) {int value = numbers[i][j];if (value != 0 && (row_digit_counter[i][value - 1] > 1 ||column_digit_counter[j][value - 1] > 1 ||block_digit_counter[GET_BLOCKNO(i, j)][value - 1] > 1)) {buttons[i][j]->setStyleSheet(WRONG_GRID_STYLE);pass = false;}else {RESTORE_GRID_STYLE(buttons[i][j]);}} }tip的實現非常簡單,就是將終局數獨對應的數字填入就好,這里就不細說了。tracker的實現也很簡單,就是找到對應的數字并涂紅就好,這里簡要說一下filter的實現:
我采用了二進制存儲的方法,將當前選中格子所在行、列、宮中出現的所有數字進行記錄,得到所有可取的值。但是由于filter所填入的數字是需要不斷輪換的,所以我要從當前填入的數字開始進行for循環,保證下一個出現的數字是在當前填入數字之后的。但是假如沒有可以填入的數字,我們就將這個格子清空(填入CLEAN)。
if (curbtn != NULL) {GO_THROUGH_BLOCKS(GET_BLOCKNO(this->cur_rowno, this->cur_colno)) {int digit = numbers[i][j];if (digit != 0 && (i != this->cur_rowno || j != this->cur_colno)){binary_recorder |= (bit << (digit - 1));}}for (int i = 0; i < SIZE; i++) {int digit;digit = numbers[i][this->cur_colno];if (digit != 0 && i != this->cur_rowno) {binary_recorder |= (bit << (digit - 1));}digit = numbers[this->cur_rowno][i];if (digit != 0 && i != this->cur_colno) {binary_recorder |= (bit << (digit - 1));}}int cur_digit = numbers[this->cur_rowno][this->cur_colno];for (int digit = cur_digit + 1; digit <= SIZE; digit++) {if ((binary_recorder & (bit << (digit - 1))) == 0) {set_number(digit);return;}}for (int digit = 1; digit <= cur_digit; digit++) {if ((binary_recorder & (bit << (digit - 1))) == 0) {set_number(digit);return;}}set_number(CLEAN);計時器采用QLCDNumber元件,利用槽函數,每1ms調用一次timeout_handle函數:
void Timer::timeout_handle() {*time = time->addMSecs(TIMEOUT_MILL);time_lcd->display(time->toString("hh:mm:ss.zzz")); }11. 界面模塊與計算模塊的對接。詳細地描述UI模塊的設計與兩個模塊的對接,并在博客中截圖實現的功能。(4')
代碼對接
對接很簡單,利用core的生成難度謎題功能和求解功能(用于提示功能),將謎題和解儲存在數獨中。
FILE* fout; this->mode = difficulty - 1;this->unfilled_grid_count = 0; int puzzle_receiver[1][SIZE*SIZE]; core->generate(1, difficulty, puzzle_receiver);for (int i = 0; i < SIZE; i++) {for (int j = 0; j < SIZE; j++) {int gridno = GET_GRIDNO(i, j);this->puzzle[gridno] = puzzle_receiver[0][gridno];if (puzzle[gridno] == 0) {unfilled_grid_count++;}} }char unfilled_grid_count_str[3]; sprintf(unfilled_grid_count_str, "%d", unfilled_grid_count); grid_count->setText(REMAINING_TEXT + unfilled_grid_count_str);core->solve(puzzle_receiver[0], this->sudoku);功能實現
主界面
點擊Game->New Game可以選擇難度,隨即開始游戲。操作流程為點擊某方格,并點擊右側鍵盤上的數字進行填入,點擊Erase可以消除方格上填入的數字。
功能一:Check
點擊Check按鈕可以檢查填入的正確性,沖突部分會標紅,如果全部方格都被填入且正確,游戲結束。
功能二:Tip
點擊某一可填入方格,此時可選擇Tip,Tip可以告訴你方格的正確答案,但代價是本次成績將不會被計入排名。
功能三:Track
點擊Track后,Track會變紅,此時為追蹤數字狀態,點擊右側數字鍵盤,可以顯示數獨中該數字所有出現的位置,便于求解。(此功能只限于Normal和Hard難度)
功能四:Filter
點擊某一可填入方格,此時點擊Filter可以循環填入該方格的所有可行數值,便于求解。(此功能只限于Hard難度)
功能五:計時
游戲開始時,計時器會開始計時,精確到毫秒,游戲結束后計時器暫停,視成績決定是否計入排名。
功能六:排名
如果成績良好,在游戲結束后會彈出窗口供玩家輸入姓名,并存儲在排行榜中。排行榜位于Game->LeaderBoard中。
細節一:填入最后一個格子自動觸發Check
玩家填入最后一個格子即標志著數獨的完成,此時應當立即結束游戲,或者告知玩家哪里填入有問題。
細節二:告知剩余方格數
玩家可以了解到當前的游戲進度。
細節三:功能隨難度進行調整,保證游戲性
對于Easy模式,Filter太過強大,以至于無腦Filter就可以結束游戲,成了純粹比拼手速的游戲。我們認為Easy象征著初學者難度,一些基礎的猜數方法要由玩家在此階段掌握,不能讓新玩家過于依賴Filter和Track功能。隨著難度的遞增,玩家的水平也逐漸遞增,此時給予玩家這些功能可以為高端玩家提供便利,這是我們設計的初衷。
細節四:Tip不可隨便使用
Tip的存在是把雙刃劍——它會讓玩家喪失思考的樂趣,破壞游戲性,但同時也能給予玩家幫助,讓實在想不出答案的玩家可以睡個安穩覺。為了權衡利弊,我們綜合了Rank功能,讓使用過Tip的玩家不能將成績計入Rank,保證了游戲的公平性,同時還鼓勵玩家盡可能減少Tip的次數,專注于提高解題能力。
細節五:關閉主窗口,其它窗口隨即關閉
如果主窗口都關了,剩下的小窗口存在的意義是什么呢?還要勞煩用戶一個一個去關嗎?
12. 描述結對的過程,提供非擺拍的兩人在討論的結對照片。(1')
我們屬于是兩個落單的人,于是就結對了……結對的時候使用的是我的個人項目代碼,GUI部分由我編寫,-c生成算法也主要由我想出來的,而核心代碼中生成數獨謎題部分的算法則是由王辰昱提出的,實際運行效率非常高。在本次結對編程過程中,我學到了很多東西,包括編程能力和溝通能力,收益匪淺。
13. 結對編程的優點和缺點
結對編程
優點:
缺點:
隊友
優點:
缺點:
自己
優點:
缺點:
14. 在你實現完程序之后,在附錄提供的PSP表格記錄下你在程序的各個模塊上實際花費的時間。(0.5')
附加題4
交換小組:
王子銘 15231058
索一奇 15061180
合并情況
合并的時候出現了一些問題,主要原因是我們兩組的core接口不同,前者傳入的是一個二維指針,而我們傳入的是一個二維數組(助教博客中寫的是int[][] result,這是java中的用法,根據不同人理解不同,我認為無論傳入指針還是數組都是正確的),導致最終通過修改代碼才能進行合并。
要想使用交換小組的dll新建游戲,必須要調用set_play(bool)函數,這個函數十分簡單:
其目的是為了調整生成puzzle的策略,如果play是false,說明是命令行模式下運行,使用回溯法保證了數獨生成的不等價性;如果play是true,說明是在GUI中運行,更加側重隨機性。這種做法的優點在于生成數獨更加靈活,但同時為dll的交換造成了一定影響,因為set_play并不是約定的接口,導致只有通過修改代碼才能確保數獨游戲的正確生成。 因此我的個人觀點是,core中多余的函數會影響dll的靈活性,盡量不去添加沒有約定好的接口是比較好的設計方式。
除此之外的一個小缺陷是任意打開兩次游戲的第N局游戲都是相同的,我認為可以通過srand(time(0))根據系統時間改變隨機數種子,從而解決問題~
合并后的游戲界面
我normal模式下挖45個空,對方是挖47個空:
Bug修復
經交換小組提示,目前Exception類已提供dll接口,調用者可以自行調用所有的異常類。
附加題5
正在收集反饋,部分反饋結果如下:
某舍友A君
亮點在于:細節做得非常好,比如使用tip之后不算成績、難度的遞增會有伴隨功能的出現、排行榜的可以自行清除;check、filter和track的功能做得很新穎,也很實用,比如check可以隨時點擊隨時檢查,track和filter能夠使得游戲過程的真實性更強。
不足在于:未開放的按鈕可以不顯示;說明性的文字過少,影響用戶上手時的體驗。
某舍友B君
優點:增加了filter功能,提升了游戲體驗,數獨填寫完成后自動觸發check功能,不必手動點擊check按鈕。排行榜信息豐富,用戶體驗良好。
缺少必要的用戶提示。直接進入游戲頁面感覺很突兀。按鈕太方方正正了。
某好友C君
感覺很方便啊,感覺可以改進一下,不用點數字,鍵盤輸入數字更爽。
某舍友D君
優點:
缺點:
某基友E君
這個成功提示顯示不全啊,話說這也太簡單了,hard8分多不用試數就做出來了= =
(不好意思,是你太強了)
相關改進
B君說得很有道理,于是我們添加了help功能,在About->Help可以調出幫助:
解釋了相關的功能用法,同時該內容也在README中寫入了。
E君提出的問題暫時無法解決,這和計算機自定義的文本大小有關,經測試,該同學的文本大小大約為1.5x,而在1.0x和1.25x上顯示正常。
感想
我是個強迫癥,覺得界面可以不精美,但用起來一定要舒服,因此在顏色搭配、布局等方面花了一點時間,包括使用七段數碼管提供毫秒級的計時功能,營造緊張感;所有窗口的一鍵關閉;創造新紀錄后立刻調出排行榜等等……感覺至少對用戶習慣有了一點思考,這是我結對編程主要的收獲之一,當然這部分也離不開同伴的幫助和支持,后面關于tip的配色問題都是他進行改良的hhh,總體來講最終效果還說得過去。其實我一開始是想向“掃雷”看齊的,系統自帶的掃雷沒有特別花哨的配色,界面十分簡潔樸素,但是用起來也很舒服,我感覺能做到這種程度就很不錯了。
但是內部的代碼實現其實在個人復審的時候感覺還是挺糟糕的,至少應該建幾個QPushButton的子類代表不同類型的按鈕,主要是一開始的界面還是在個人階段一晚上搞出來的,沒有考慮太多,導致GUI內部元件的封裝性特別差。這一部分以后還是要注意的。
轉載于:https://www.cnblogs.com/slontia/p/7669797.html
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的[2017BUAA软工]结对项目:数独扩展的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BZOJ 1011: [HNOI2008
- 下一篇: Linux解压rar、zip、war、t