[2017BUAA软工]结对项目:数独扩展
結(jié)對項(xiàng)目:數(shù)獨(dú)擴(kuò)展
1. Github項(xiàng)目地址
https://github.com/Slontia/Sudoku2
2. PSP估計(jì)表格
3. 關(guān)于Information Hiding, Interface Design, Loose Coupling的設(shè)計(jì)
首先,在王辰昱同學(xué)的提醒下,我們將一開始的代碼按照功能分為若干個.cpp文件,每一個.cpp只處理一件事,如create_puzzle.cpp文件負(fù)責(zé)生成數(shù)獨(dú),而solve.cpp文件負(fù)責(zé)解決數(shù)獨(dú),在一定程度上保證了代碼的低耦合性。
我認(rèn)為在實(shí)際編碼中,最能體現(xiàn)這個思想的是dig函數(shù)和Rank類。首先來看一下dig函數(shù)在代碼中的調(diào)用情況:
這是create_puzzle函數(shù)的開頭部分,整個程序中只有這個地方用到了這個函數(shù),其功能在于盡可能地清除一個數(shù)獨(dú)中的數(shù)字,并保證其單解性。這個函數(shù)在清除數(shù)字的時候會依靠推理,因此挖起來非常快。這是保證代碼速度的一個很重要的部分。但是如果不看dig.cpp文件,我們并不清楚其具體的實(shí)現(xiàn)過程,或是用了什么高端的算法,只知道“這是一個挖數(shù)非常快的函數(shù)”,我認(rèn)為這是最能體現(xiàn)Information Hiding的部分。
除此之外,GUI項(xiàng)目中還有一個排行榜功能,這個功能位于Rank類中。我們在實(shí)現(xiàn)這個類之前,先考慮了一下可能的需要的功能,如清除記錄、插入到排行榜、寫入文件、讀取文件、加密等等。有了這些想法,我們便提供了相應(yīng)的接口,之后再實(shí)現(xiàn),我認(rèn)為遵循了Interface Design。下面是rank.h中定義的部分函數(shù):
4. 計(jì)算模塊接口的設(shè)計(jì)與實(shí)現(xiàn)過程。設(shè)計(jì)包括代碼如何組織,比如會有幾個類,幾個函數(shù),他們之間關(guān)系如何,關(guān)鍵函數(shù)是否需要畫出流程圖?說明你的算法的關(guān)鍵(不必列出源代碼),以及獨(dú)到之處。(7')
主要有三個模塊:
1. create 模塊
這次作業(yè)雖然指定不允許出現(xiàn)等價數(shù)獨(dú),但我們還是可以通過模板變換快速生成1000000個數(shù)獨(dú)。首先,我們需要保證數(shù)獨(dú)的等價性,我們的做法是不對第一個宮進(jìn)行任何操作(隨機(jī)性貌似在-c不做要求?)。我們沿用了劉暢同學(xué)在個人項(xiàng)目中使用的3-3-3位置輪換方法成功生成了一個數(shù)獨(dú)終盤。在個人項(xiàng)目中,模板的變化主要體現(xiàn)在R4~R6、R7~R9的行變換,但其實(shí),對于3-3-3位置輪換方法生成的終盤,部分行的變換也是可行的。
我們先來想一個問題,在一個3*3的宮中填入1~3三個數(shù)字各三次,保證行、列中沒有重復(fù)的數(shù)字,共有幾種填法呢?通過窮舉,我們可以發(fā)現(xiàn)有12種解,如下圖:
現(xiàn)在,我們將這個問題向數(shù)獨(dú)上靠攏,在生成的數(shù)獨(dú)上,我們標(biāo)出圖中所示的紅、綠、藍(lán)數(shù)字:
由上面的結(jié)論我們可以得出,通過變換紅、綠、藍(lán)共九個數(shù)字,可以得到12種不同排列。由于第一個宮我們不能動,只能動R4~R9,因此共可以找到6組類似的9個數(shù)字,而這6組的排列又相互獨(dú)立,因此共可以產(chǎn)生12^6=2985984種排列。
2. solve 模塊
solve模塊的實(shí)現(xiàn)和個人項(xiàng)目相同,只是做了一下封裝。
據(jù)之前的思路,我設(shè)計(jì)了三個類:數(shù)獨(dú)類(Subject_sudoku)、組類(Group)、方格類(Box)。
數(shù)獨(dú)類包含三個組數(shù)組,名字分別為rows[9]、columns[9]和blocks[9],分別代表思路中描述的三種組。每個組包括指向9個Box的指針和記錄以確定數(shù)字的二進(jìn)制數(shù)hasvalue。
在初始化數(shù)組之后,首先找到未確定值得Box中可能取值最少的那個,依次對它的值進(jìn)行猜測。在每次猜測之前,通過拷貝構(gòu)造將Subject_sudoku備份下來,在新的數(shù)獨(dú)中將該方格的值確定,再繼續(xù)尋找可能取值最少的Box,對它的值進(jìn)行猜測,直到所有的Box的值都被確定,或嘗試完某個Box的所有可能性(無解)。
基本流程描述如下:
對象之間的關(guān)系構(gòu)成網(wǎng)狀結(jié)構(gòu),便于Box和Group之間的信息傳遞
3. puzzle 模塊
獨(dú)到之處:
1. 選擇最有效的隨機(jī)方式。我們在隨機(jī)挖空的時候,發(fā)現(xiàn)純粹隨機(jī)的效果并不好,特別是要求生成55個空的獨(dú)解數(shù)獨(dú)時,會比較慢。但是如果在各個宮內(nèi)部進(jìn)行比較平均的挖空,得出來的空會分布得比較均勻,就容易產(chǎn)生單解數(shù)獨(dú)。
2. 結(jié)合邏輯推導(dǎo)。反向使用行列宮擯除法,可以挖掉當(dāng)前局面來看具有邏輯必然性的數(shù)字(比如說第一行是 1 2 3 4 5 6 7 8 9,那么 1 可以被挖掉,因?yàn)?1 的存在是被其他 8 個數(shù)決定的),這樣,這部分的挖空可以不用交給隨機(jī)挖空來解決,降低了算法的時間復(fù)雜度。
5. UML圖顯示計(jì)算模塊部分各個實(shí)體之間的關(guān)系
6. 計(jì)算模塊接口部分的性能改進(jìn)。記錄在改進(jìn)計(jì)算模塊性能上所花費(fèi)的時間,描述你改進(jìn)的思路,并展示一張性能分析圖(由VS 2015/2017的性能分析工具自動生成),并展示你程序中消耗最大的函數(shù)。(3')
1. puzzle 模塊
測試指令 -n 10000 -r 20~35 -u
測試指令 -n 10000 -r 36~50 -u
測試指令 -n 10000 -r 51~55 -u
可見,FgMap::outside_lock 是最耗時間的函數(shù),編寫之前已經(jīng)考慮到了這個問題,因此采用了位運(yùn)算等加速方法,實(shí)際上很難再優(yōu)化了。
2. create 模塊
create 采用的是行列交換的方法生成不等價的數(shù)獨(dú),因此很快,瓶頸在 io,但這里采用了緩存一次性輸出的方式,最大限度利用了 block 讀寫的功能,因此也無法再優(yōu)化了。
3. solve 模塊
7. 看Design by Contract, Code Contract的內(nèi)容:
http://en.wikipedia.org/wiki/Design_by_contract
http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx
描述這些做法的優(yōu)缺點(diǎn), 說明你是如何把它們?nèi)谌虢Y(jié)對作業(yè)中的。(5')
優(yōu)點(diǎn):
缺點(diǎn):
在本次結(jié)對作業(yè)中,我們通過定義了dig函數(shù)和rank類的需求,近似地實(shí)現(xiàn)了契約設(shè)計(jì)。dig函數(shù)負(fù)責(zé)將傳入的數(shù)獨(dú)盡可能地挖空,而rank類提供了各種對排行榜操作的接口。預(yù)先定義了這些需求再開始實(shí)際的代碼編寫,讓后期的代碼銜接更加簡單。
8. 計(jì)算模塊部分單元測試展示。展示出項(xiàng)目部分單元測試代碼,并說明測試的函數(shù),構(gòu)造測試數(shù)據(jù)的思路。并將單元測試得到的測試覆蓋率截圖,發(fā)表在博客中。要求總體覆蓋率到90%以上,否則單元測試部分視作無效。(6')
覆蓋率截圖:
單元測試
在編寫測試代碼之前,首先明確一下單元測試要實(shí)現(xiàn)的功能:
對于-c,需要檢測1、2;
對于-s,需要檢測1;
對于-n,需要檢測1、2,如果有-u,還需要檢測3。
合法性檢測
依然使用二進(jìn)制數(shù)存儲法判斷合法性,這一點(diǎn)和個人項(xiàng)目完全相同,優(yōu)點(diǎn)是速度快、代碼簡潔,代碼如下:
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;} }等價性檢測
沿用了個人項(xiàng)目中的字典樹,代碼變更如下:
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) = "";
這段代碼的情境是本身位于這個節(jié)點(diǎn)的字符串遇見了新加入的字符串,于是他們需要根據(jù)自己接下來的字符,從該節(jié)點(diǎn)引申出兩個屬于它們的新節(jié)點(diǎn),那里是他們的歸宿。可以看出之前的代碼是有問題的,sudoku是字典樹節(jié)點(diǎn)中儲存的字符串指針,舊代碼中為了清空節(jié)點(diǎn),清空的是指針指向的值,導(dǎo)致字典樹中很多字符串都被清空了,所幸被測試的代碼沒有問題,不然后果很嚴(yán)重……新的代碼修改的是指針本身,不影響存入的字符串,是正確的處理方式。
除此之外,我們沿用了個人項(xiàng)目中的字典樹重復(fù)性判斷,這次新添了檢測等價性的功能。我們選取第一個宮,對里面的數(shù)字分別和1~9進(jìn)行映射,之后將整個數(shù)獨(dú)根據(jù)映射刷新,再放入字典樹中進(jìn)行判斷。映射的代碼如下:
單解性測試
其實(shí)在生成數(shù)獨(dú)題目的時候已經(jīng)檢查過單解性了,這里引用的是那里的代碼:
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); }這個函數(shù)是對求解函數(shù)的修改,原來是找到解立刻輸出答案,現(xiàn)在是找到一個解繼續(xù)運(yùn)行,直到找到的解的個數(shù)超過SOLUTION_MAX為止。這里SOLUTION_MAX的值為1。
9. 計(jì)算模塊部分異常處理說明。在博客中詳細(xì)介紹每種異常的設(shè)計(jì)目標(biāo)。每種異常都要選擇一個單元測試樣例發(fā)布在博客中,并指明錯誤對應(yīng)的場景。(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 數(shù)獨(dú)謎題本身不符合規(guī)則(并非指全部無解謎題):
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. 界面模塊的詳細(xì)設(shè)計(jì)過程。在博客中詳細(xì)介紹界面模塊是如何設(shè)計(jì)的,并寫一些必要的代碼說明解釋實(shí)現(xiàn)過程。(5')
我們主要的GUI界面只有一個,其它的還包括排行榜界面和成績寫入界面。
GUI的布局使用代碼生成,沒有使用.ui文件,原因是覺得.ui文件自動生成的代碼很臃腫,而自己寫的話可以建立數(shù)組管理各個組件(大概是我們沒有找到正確的方法?)。舉例來說的話,下面是生成數(shù)獨(dú)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循環(huán)來初始化各個元件,他們的StyleSheet用預(yù)定義表示:
#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 ""下面是一些界面變化部分,首先是新游戲的開始,這里根據(jù)Core的generate接口處理數(shù)獨(dú)界面,將未被挖空的數(shù)獨(dú)對應(yīng)按鈕置為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;}} }這次我們實(shí)現(xiàn)的功能有四個,除了要求的check和tip外,我們還附加了filter和track功能。filter是在當(dāng)前格子內(nèi)切換所有滿足填入規(guī)則的值,而track是將某種數(shù)字標(biāo)紅便于查看。
check的設(shè)計(jì)思路是建立三個數(shù)組,分別對應(yīng)行、列、組,并將每一個格子的數(shù)字分別存儲于這三個數(shù)組中,假如某個數(shù)組儲存的某種數(shù)字的數(shù)量大于1,說明出現(xiàn)了數(shù)字的重復(fù),將重復(fù)的數(shù)字標(biāo)紅:
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的實(shí)現(xiàn)非常簡單,就是將終局?jǐn)?shù)獨(dú)對應(yīng)的數(shù)字填入就好,這里就不細(xì)說了。tracker的實(shí)現(xiàn)也很簡單,就是找到對應(yīng)的數(shù)字并涂紅就好,這里簡要說一下filter的實(shí)現(xiàn):
我采用了二進(jìn)制存儲的方法,將當(dāng)前選中格子所在行、列、宮中出現(xiàn)的所有數(shù)字進(jìn)行記錄,得到所有可取的值。但是由于filter所填入的數(shù)字是需要不斷輪換的,所以我要從當(dāng)前填入的數(shù)字開始進(jìn)行for循環(huán),保證下一個出現(xiàn)的數(shù)字是在當(dāng)前填入數(shù)字之后的。但是假如沒有可以填入的數(shù)字,我們就將這個格子清空(填入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);計(jì)時器采用QLCDNumber元件,利用槽函數(shù),每1ms調(diào)用一次timeout_handle函數(shù):
void Timer::timeout_handle() {*time = time->addMSecs(TIMEOUT_MILL);time_lcd->display(time->toString("hh:mm:ss.zzz")); }11. 界面模塊與計(jì)算模塊的對接。詳細(xì)地描述UI模塊的設(shè)計(jì)與兩個模塊的對接,并在博客中截圖實(shí)現(xiàn)的功能。(4')
代碼對接
對接很簡單,利用core的生成難度謎題功能和求解功能(用于提示功能),將謎題和解儲存在數(shù)獨(dú)中。
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);功能實(shí)現(xiàn)
主界面
點(diǎn)擊Game->New Game可以選擇難度,隨即開始游戲。操作流程為點(diǎn)擊某方格,并點(diǎn)擊右側(cè)鍵盤上的數(shù)字進(jìn)行填入,點(diǎn)擊Erase可以消除方格上填入的數(shù)字。
功能一:Check
點(diǎn)擊Check按鈕可以檢查填入的正確性,沖突部分會標(biāo)紅,如果全部方格都被填入且正確,游戲結(jié)束。
功能二:Tip
點(diǎn)擊某一可填入方格,此時可選擇Tip,Tip可以告訴你方格的正確答案,但代價是本次成績將不會被計(jì)入排名。
功能三:Track
點(diǎn)擊Track后,Track會變紅,此時為追蹤數(shù)字狀態(tài),點(diǎn)擊右側(cè)數(shù)字鍵盤,可以顯示數(shù)獨(dú)中該數(shù)字所有出現(xiàn)的位置,便于求解。(此功能只限于Normal和Hard難度)
功能四:Filter
點(diǎn)擊某一可填入方格,此時點(diǎn)擊Filter可以循環(huán)填入該方格的所有可行數(shù)值,便于求解。(此功能只限于Hard難度)
功能五:計(jì)時
游戲開始時,計(jì)時器會開始計(jì)時,精確到毫秒,游戲結(jié)束后計(jì)時器暫停,視成績決定是否計(jì)入排名。
功能六:排名
如果成績良好,在游戲結(jié)束后會彈出窗口供玩家輸入姓名,并存儲在排行榜中。排行榜位于Game->LeaderBoard中。
細(xì)節(jié)一:填入最后一個格子自動觸發(fā)Check
玩家填入最后一個格子即標(biāo)志著數(shù)獨(dú)的完成,此時應(yīng)當(dāng)立即結(jié)束游戲,或者告知玩家哪里填入有問題。
細(xì)節(jié)二:告知剩余方格數(shù)
玩家可以了解到當(dāng)前的游戲進(jìn)度。
細(xì)節(jié)三:功能隨難度進(jìn)行調(diào)整,保證游戲性
對于Easy模式,Filter太過強(qiáng)大,以至于無腦Filter就可以結(jié)束游戲,成了純粹比拼手速的游戲。我們認(rèn)為Easy象征著初學(xué)者難度,一些基礎(chǔ)的猜數(shù)方法要由玩家在此階段掌握,不能讓新玩家過于依賴Filter和Track功能。隨著難度的遞增,玩家的水平也逐漸遞增,此時給予玩家這些功能可以為高端玩家提供便利,這是我們設(shè)計(jì)的初衷。
細(xì)節(jié)四:Tip不可隨便使用
Tip的存在是把雙刃劍——它會讓玩家喪失思考的樂趣,破壞游戲性,但同時也能給予玩家?guī)椭?#xff0c;讓實(shí)在想不出答案的玩家可以睡個安穩(wěn)覺。為了權(quán)衡利弊,我們綜合了Rank功能,讓使用過Tip的玩家不能將成績計(jì)入Rank,保證了游戲的公平性,同時還鼓勵玩家盡可能減少Tip的次數(shù),專注于提高解題能力。
細(xì)節(jié)五:關(guān)閉主窗口,其它窗口隨即關(guān)閉
如果主窗口都關(guān)了,剩下的小窗口存在的意義是什么呢?還要勞煩用戶一個一個去關(guān)嗎?
12. 描述結(jié)對的過程,提供非擺拍的兩人在討論的結(jié)對照片。(1')
我們屬于是兩個落單的人,于是就結(jié)對了……結(jié)對的時候使用的是我的個人項(xiàng)目代碼,GUI部分由我編寫,-c生成算法也主要由我想出來的,而核心代碼中生成數(shù)獨(dú)謎題部分的算法則是由王辰昱提出的,實(shí)際運(yùn)行效率非常高。在本次結(jié)對編程過程中,我學(xué)到了很多東西,包括編程能力和溝通能力,收益匪淺。
13. 結(jié)對編程的優(yōu)點(diǎn)和缺點(diǎn)
結(jié)對編程
優(yōu)點(diǎn):
缺點(diǎn):
隊(duì)友
優(yōu)點(diǎn):
缺點(diǎn):
自己
優(yōu)點(diǎn):
缺點(diǎn):
14. 在你實(shí)現(xiàn)完程序之后,在附錄提供的PSP表格記錄下你在程序的各個模塊上實(shí)際花費(fèi)的時間。(0.5')
附加題4
交換小組:
王子銘 15231058
索一奇 15061180
合并情況
合并的時候出現(xiàn)了一些問題,主要原因是我們兩組的core接口不同,前者傳入的是一個二維指針,而我們傳入的是一個二維數(shù)組(助教博客中寫的是int[][] result,這是java中的用法,根據(jù)不同人理解不同,我認(rèn)為無論傳入指針還是數(shù)組都是正確的),導(dǎo)致最終通過修改代碼才能進(jìn)行合并。
要想使用交換小組的dll新建游戲,必須要調(diào)用set_play(bool)函數(shù),這個函數(shù)十分簡單:
其目的是為了調(diào)整生成puzzle的策略,如果play是false,說明是命令行模式下運(yùn)行,使用回溯法保證了數(shù)獨(dú)生成的不等價性;如果play是true,說明是在GUI中運(yùn)行,更加側(cè)重隨機(jī)性。這種做法的優(yōu)點(diǎn)在于生成數(shù)獨(dú)更加靈活,但同時為dll的交換造成了一定影響,因?yàn)閟et_play并不是約定的接口,導(dǎo)致只有通過修改代碼才能確保數(shù)獨(dú)游戲的正確生成。 因此我的個人觀點(diǎn)是,core中多余的函數(shù)會影響dll的靈活性,盡量不去添加沒有約定好的接口是比較好的設(shè)計(jì)方式。
除此之外的一個小缺陷是任意打開兩次游戲的第N局游戲都是相同的,我認(rèn)為可以通過srand(time(0))根據(jù)系統(tǒng)時間改變隨機(jī)數(shù)種子,從而解決問題~
合并后的游戲界面
我normal模式下挖45個空,對方是挖47個空:
Bug修復(fù)
經(jīng)交換小組提示,目前Exception類已提供dll接口,調(diào)用者可以自行調(diào)用所有的異常類。
附加題5
正在收集反饋,部分反饋結(jié)果如下:
某舍友A君
亮點(diǎn)在于:細(xì)節(jié)做得非常好,比如使用tip之后不算成績、難度的遞增會有伴隨功能的出現(xiàn)、排行榜的可以自行清除;check、filter和track的功能做得很新穎,也很實(shí)用,比如check可以隨時點(diǎn)擊隨時檢查,track和filter能夠使得游戲過程的真實(shí)性更強(qiáng)。
不足在于:未開放的按鈕可以不顯示;說明性的文字過少,影響用戶上手時的體驗(yàn)。
某舍友B君
優(yōu)點(diǎn):增加了filter功能,提升了游戲體驗(yàn),數(shù)獨(dú)填寫完成后自動觸發(fā)check功能,不必手動點(diǎn)擊check按鈕。排行榜信息豐富,用戶體驗(yàn)良好。
缺少必要的用戶提示。直接進(jìn)入游戲頁面感覺很突兀。按鈕太方方正正了。
某好友C君
感覺很方便啊,感覺可以改進(jìn)一下,不用點(diǎn)數(shù)字,鍵盤輸入數(shù)字更爽。
某舍友D君
優(yōu)點(diǎn):
缺點(diǎn):
某基友E君
這個成功提示顯示不全啊,話說這也太簡單了,hard8分多不用試數(shù)就做出來了= =
(不好意思,是你太強(qiáng)了)
相關(guān)改進(jìn)
B君說得很有道理,于是我們添加了help功能,在About->Help可以調(diào)出幫助:
解釋了相關(guān)的功能用法,同時該內(nèi)容也在README中寫入了。
E君提出的問題暫時無法解決,這和計(jì)算機(jī)自定義的文本大小有關(guān),經(jīng)測試,該同學(xué)的文本大小大約為1.5x,而在1.0x和1.25x上顯示正常。
感想
我是個強(qiáng)迫癥,覺得界面可以不精美,但用起來一定要舒服,因此在顏色搭配、布局等方面花了一點(diǎn)時間,包括使用七段數(shù)碼管提供毫秒級的計(jì)時功能,營造緊張感;所有窗口的一鍵關(guān)閉;創(chuàng)造新紀(jì)錄后立刻調(diào)出排行榜等等……感覺至少對用戶習(xí)慣有了一點(diǎn)思考,這是我結(jié)對編程主要的收獲之一,當(dāng)然這部分也離不開同伴的幫助和支持,后面關(guān)于tip的配色問題都是他進(jìn)行改良的hhh,總體來講最終效果還說得過去。其實(shí)我一開始是想向“掃雷”看齊的,系統(tǒng)自帶的掃雷沒有特別花哨的配色,界面十分簡潔樸素,但是用起來也很舒服,我感覺能做到這種程度就很不錯了。
但是內(nèi)部的代碼實(shí)現(xiàn)其實(shí)在個人復(fù)審的時候感覺還是挺糟糕的,至少應(yīng)該建幾個QPushButton的子類代表不同類型的按鈕,主要是一開始的界面還是在個人階段一晚上搞出來的,沒有考慮太多,導(dǎo)致GUI內(nèi)部元件的封裝性特別差。這一部分以后還是要注意的。
轉(zhuǎn)載于:https://www.cnblogs.com/slontia/p/7669797.html
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的[2017BUAA软工]结对项目:数独扩展的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BZOJ 1011: [HNOI2008
- 下一篇: Linux解压rar、zip、war、t