日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

[2017BUAA软工]结对项目:数独扩展

發(fā)布時(shí)間:2024/4/17 编程问答 48 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [2017BUAA软工]结对项目:数独扩展 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

結(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é)的提醒下,我們將一開始的代碼按照功能分為若干個(gè).cpp文件,每一個(gè).cpp只處理一件事,如create_puzzle.cpp文件負(fù)責(zé)生成數(shù)獨(dú),而solve.cpp文件負(fù)責(zé)解決數(shù)獨(dú),在一定程度上保證了代碼的低耦合性。
  我認(rèn)為在實(shí)際編碼中,最能體現(xiàn)這個(gè)思想的是dig函數(shù)和Rank類。首先來看一下dig函數(shù)在代碼中的調(diào)用情況:

int lower, int upper) { if (lower > upper || lower < 0 || upper > SIZE * SIZE) {return 0; } int all_freebox_num = (rand() % (upper - lower + 1)) + lower; int cleaned_num = dig(sudoku, puzzle, all_freebox_num);int freebox_num = all_freebox_num - cleaned_num; if (freebox_num == 0) {return all_freebox_num; }

  這是create_puzzle函數(shù)的開頭部分,整個(gè)程序中只有這個(gè)地方用到了這個(gè)函數(shù),其功能在于盡可能地清除一個(gè)數(shù)獨(dú)中的數(shù)字,并保證其單解性。這個(gè)函數(shù)在清除數(shù)字的時(shí)候會(huì)依靠推理,因此挖起來非常快。這是保證代碼速度的一個(gè)很重要的部分。但是如果不看dig.cpp文件,我們并不清楚其具體的實(shí)現(xiàn)過程,或是用了什么高端的算法,只知道“這是一個(gè)挖數(shù)非常快的函數(shù)”,我認(rèn)為這是最能體現(xiàn)Information Hiding的部分。
  
  除此之外,GUI項(xiàng)目中還有一個(gè)排行榜功能,這個(gè)功能位于Rank類中。我們在實(shí)現(xiàn)這個(gè)類之前,先考慮了一下可能的需要的功能,如清除記錄、插入到排行榜、寫入文件、讀取文件、加密等等。有了這些想法,我們便提供了相應(yīng)的接口,之后再實(shí)現(xiàn),我認(rèn)為遵循了Interface Design。下面是rank.h中定義的部分函數(shù):

bool record(int mode, double time, char * name); // 保存記錄(難度,用時(shí),姓名) bool fetch_rank(int mode, int r, char * name, double & time); // 獲取 mode 難度排名為 r 的姓名和時(shí)間, 1-indexed bool clear(); // 清空排行榜 bool fetch_last_record(char * name, double & time); // 獲得上次錄入的姓名和時(shí)間 bool encrypt_flush(bool encrypt); // 加密并保存 void load_db(bool encrypt); // 從文件中加載 void init_db(); // 初始化數(shù)據(jù)庫 void init_board(); // 初始化 board bool fetch_free(int mode, int & index); // 拿走一個(gè)空的 entry bool insert_after(int mode, int tg_id, int new_id); // 在空間中id為tg_entry的位置后面插入new_entry bool show(int mode);

4. 計(jì)算模塊接口的設(shè)計(jì)與實(shí)現(xiàn)過程。設(shè)計(jì)包括代碼如何組織,比如會(huì)有幾個(gè)類,幾個(gè)函數(shù),他們之間關(guān)系如何,關(guān)鍵函數(shù)是否需要畫出流程圖?說明你的算法的關(guān)鍵(不必列出源代碼),以及獨(dú)到之處。(7')

主要有三個(gè)模塊:

1. create 模塊

  這次作業(yè)雖然指定不允許出現(xiàn)等價(jià)數(shù)獨(dú),但我們還是可以通過模板變換快速生成1000000個(gè)數(shù)獨(dú)。首先,我們需要保證數(shù)獨(dú)的等價(jià)性,我們的做法是不對第一個(gè)宮進(jìn)行任何操作(隨機(jī)性貌似在-c不做要求?)。我們沿用了劉暢同學(xué)在個(gè)人項(xiàng)目中使用的3-3-3位置輪換方法成功生成了一個(gè)數(shù)獨(dú)終盤。在個(gè)人項(xiàng)目中,模板的變化主要體現(xiàn)在R4~R6、R7~R9的行變換,但其實(shí),對于3-3-3位置輪換方法生成的終盤,部分行的變換也是可行的。
  我們先來想一個(gè)問題,在一個(gè)3*3的宮中填入1~3三個(gè)數(shù)字各三次,保證行、列中沒有重復(fù)的數(shù)字,共有幾種填法呢?通過窮舉,我們可以發(fā)現(xiàn)有12種解,如下圖:

  

  現(xiàn)在,我們將這個(gè)問題向數(shù)獨(dú)上靠攏,在生成的數(shù)獨(dú)上,我們標(biāo)出圖中所示的紅、綠、藍(lán)數(shù)字:
  

  由上面的結(jié)論我們可以得出,通過變換紅、綠、藍(lán)共九個(gè)數(shù)字,可以得到12種不同排列。由于第一個(gè)宮我們不能動(dòng),只能動(dòng)R4~R9,因此共可以找到6組類似的9個(gè)數(shù)字,而這6組的排列又相互獨(dú)立,因此共可以產(chǎn)生12^6=2985984種排列。

2. solve 模塊

  solve模塊的實(shí)現(xiàn)和個(gè)人項(xiàng)目相同,只是做了一下封裝。

據(jù)之前的思路,我設(shè)計(jì)了三個(gè)類:數(shù)獨(dú)類(Subject_sudoku)、組類(Group)、方格類(Box)。
數(shù)獨(dú)類包含三個(gè)組數(shù)組,名字分別為rows[9]、columns[9]和blocks[9],分別代表思路中描述的三種組。每個(gè)組包括指向9個(gè)Box的指針和記錄以確定數(shù)字的二進(jìn)制數(shù)hasvalue。
在初始化數(shù)組之后,首先找到未確定值得Box中可能取值最少的那個(gè),依次對它的值進(jìn)行猜測。在每次猜測之前,通過拷貝構(gòu)造將Subject_sudoku備份下來,在新的數(shù)獨(dú)中將該方格的值確定,再繼續(xù)尋找可能取值最少的Box,對它的值進(jìn)行猜測,直到所有的Box的值都被確定,或嘗試完某個(gè)Box的所有可能性(無解)。
基本流程描述如下:

  • 初始化數(shù)獨(dú);
  • 找到可能取值最少的Box;
  • 依次假定它所有的可能取值;
  • 重復(fù)2、3,直到所有Box都被確定,或嘗試完某Box所有可能性。
    對象之間的關(guān)系構(gòu)成網(wǎng)狀結(jié)構(gòu),便于Box和Group之間的信息傳遞
  • 3. puzzle 模塊

      

    獨(dú)到之處:

      1. 選擇最有效的隨機(jī)方式。我們在隨機(jī)挖空的時(shí)候,發(fā)現(xiàn)純粹隨機(jī)的效果并不好,特別是要求生成55個(gè)空的獨(dú)解數(shù)獨(dú)時(shí),會(huì)比較慢。但是如果在各個(gè)宮內(nèi)部進(jìn)行比較平均的挖空,得出來的空會(huì)分布得比較均勻,就容易產(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 個(gè)數(shù)決定的),這樣,這部分的挖空可以不用交給隨機(jī)挖空來解決,降低了算法的時(shí)間復(fù)雜度。

    5. UML圖顯示計(jì)算模塊部分各個(gè)實(shí)體之間的關(guān)系

    6. 計(jì)算模塊接口部分的性能改進(jìn)。記錄在改進(jìn)計(jì)算模塊性能上所花費(fèi)的時(shí)間,描述你改進(jìn)的思路,并展示一張性能分析圖(由VS 2015/2017的性能分析工具自動(dòng)生成),并展示你程序中消耗最大的函數(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í)間的函數(shù),編寫之前已經(jīng)考慮到了這個(gè)問題,因此采用了位運(yùn)算等加速方法,實(shí)際上很難再優(yōu)化了。

    2. create 模塊

      
        

      create 采用的是行列交換的方法生成不等價(jià)的數(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):

  • 在開發(fā)過程中可能會(huì)忘記一個(gè)函數(shù)本身的含義,而契約設(shè)計(jì)可以時(shí)刻提醒設(shè)計(jì)者這個(gè)函數(shù)本身的功能,不至于偏離原本的需求;
  • 在調(diào)用函數(shù)時(shí)可以了解到需要保證的條件和可能帶來的后果,增加了開發(fā)的安全性。
  • 缺點(diǎn):

  • 契約設(shè)計(jì)的前置條件和后置條件可能有很多,如果一定嚴(yán)格按照語法規(guī)則描述,則閱讀起來比較困難;而如果改為用自然語言描述,又會(huì)出現(xiàn)描述模糊的情況,這一點(diǎn)在上學(xué)期《面向?qū)ο笳n程設(shè)計(jì)》中有所體會(huì);
  • 保證契約的正確性會(huì)帶來更多的開發(fā)成本。
  •   在本次結(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)的功能:

  • 合法性檢測
  • 等價(jià)性檢測(是更嚴(yán)格的重復(fù)性檢測)
  • 單解性檢測
  • 對于-c,需要檢測1、2;
    對于-s,需要檢測1;
    對于-n,需要檢測1、2,如果有-u,還需要檢測3。

    合法性檢測

    依然使用二進(jìn)制數(shù)存儲(chǔ)法判斷合法性,這一點(diǎn)和個(gè)人項(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;} }

    等價(jià)性檢測

    沿用了個(gè)人項(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("");

    是變動(dòng)后的代碼,之前的代碼為

    *((*p)->sudoku) = "";

      
    這段代碼的情境是本身位于這個(gè)節(jié)點(diǎn)的字符串遇見了新加入的字符串,于是他們需要根據(jù)自己接下來的字符,從該節(jié)點(diǎn)引申出兩個(gè)屬于它們的新節(jié)點(diǎn),那里是他們的歸宿。可以看出之前的代碼是有問題的,sudoku是字典樹節(jié)點(diǎn)中儲(chǔ)存的字符串指針,舊代碼中為了清空節(jié)點(diǎn),清空的是指針指向的值,導(dǎo)致字典樹中很多字符串都被清空了,所幸被測試的代碼沒有問題,不然后果很嚴(yán)重……新的代碼修改的是指針本身,不影響存入的字符串,是正確的處理方式。
      
    除此之外,我們沿用了個(gè)人項(xiàng)目中的字典樹重復(fù)性判斷,這次新添了檢測等價(jià)性的功能。我們選取第一個(gè)宮,對里面的數(shù)字分別和1~9進(jìn)行映射,之后將整個(gè)數(shù)獨(dú)根據(jù)映射刷新,再放入字典樹中進(jìn)行判斷。映射的代碼如下:

    char digit_map[SIZE]; for (int i = 0; i < SIZE; i++) {digit_map[i] = (*sudoku)[i]; // build map } /* change to equivalence */ for (int i = 0; i < SIZE * SIZE; i++) {(*sudoku)[i] = digit_map[(*sudoku)[i] - '1']; }

    單解性測試

    其實(shí)在生成數(shù)獨(dú)題目的時(shí)候已經(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); }

    這個(gè)函數(shù)是對求解函數(shù)的修改,原來是找到解立刻輸出答案,現(xiàn)在是找到一個(gè)解繼續(xù)運(yùn)行,直到找到的解的個(gè)數(shù)超過SOLUTION_MAX為止。這里SOLUTION_MAX的值為1。

    9. 計(jì)算模塊部分異常處理說明。在博客中詳細(xì)介紹每種異常的設(shè)計(jì)目標(biāo)。每種異常都要選擇一個(gè)單元測試樣例發(fā)布在博客中,并指明錯(cuò)誤對應(yīng)的場景。(5')

      InvalidCommandException 錯(cuò)誤的指令;

    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界面只有一個(gè),其它的還包括排行榜界面和成績寫入界面。

      GUI的布局使用代碼生成,沒有使用.ui文件,原因是覺得.ui文件自動(dòng)生成的代碼很臃腫,而自己寫的話可以建立數(shù)組管理各個(gè)組件(大概是我們沒有找到正確的方法?)。舉例來說的話,下面是生成數(shù)獨(dú)9*9個(gè)方格的代碼:

    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)來初始化各個(gè)元件,他們的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)的功能有四個(gè),除了要求的check和tip外,我們還附加了filter和track功能。filter是在當(dāng)前格子內(nèi)切換所有滿足填入規(guī)則的值,而track是將某種數(shù)字標(biāo)紅便于查看。

      check的設(shè)計(jì)思路是建立三個(gè)數(shù)組,分別對應(yīng)行、列、組,并將每一個(gè)格子的數(shù)字分別存儲(chǔ)于這三個(gè)數(shù)組中,假如某個(gè)數(shù)組儲(chǔ)存的某種數(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)制存儲(chǔ)的方法,將當(dāng)前選中格子所在行、列、宮中出現(xiàn)的所有數(shù)字進(jìn)行記錄,得到所有可取的值。但是由于filter所填入的數(shù)字是需要不斷輪換的,所以我要從當(dāng)前填入的數(shù)字開始進(jìn)行for循環(huán),保證下一個(gè)出現(xiàn)的數(shù)字是在當(dāng)前填入數(shù)字之后的。但是假如沒有可以填入的數(shù)字,我們就將這個(gè)格子清空(填入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ì)時(shí)器采用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ì)與兩個(gè)模塊的對接,并在博客中截圖實(shí)現(xiàn)的功能。(4')

    代碼對接

      對接很簡單,利用core的生成難度謎題功能和求解功能(用于提示功能),將謎題和解儲(chǔ)存在數(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按鈕可以檢查填入的正確性,沖突部分會(huì)標(biāo)紅,如果全部方格都被填入且正確,游戲結(jié)束。

    功能二:Tip

    點(diǎn)擊某一可填入方格,此時(shí)可選擇Tip,Tip可以告訴你方格的正確答案,但代價(jià)是本次成績將不會(huì)被計(jì)入排名。

    功能三:Track

    點(diǎn)擊Track后,Track會(huì)變紅,此時(shí)為追蹤數(shù)字狀態(tài),點(diǎn)擊右側(cè)數(shù)字鍵盤,可以顯示數(shù)獨(dú)中該數(shù)字所有出現(xiàn)的位置,便于求解。(此功能只限于Normal和Hard難度)

    功能四:Filter

    點(diǎn)擊某一可填入方格,此時(shí)點(diǎn)擊Filter可以循環(huán)填入該方格的所有可行數(shù)值,便于求解。(此功能只限于Hard難度)

    功能五:計(jì)時(shí)

    游戲開始時(shí),計(jì)時(shí)器會(huì)開始計(jì)時(shí),精確到毫秒,游戲結(jié)束后計(jì)時(shí)器暫停,視成績決定是否計(jì)入排名。

    功能六:排名

    如果成績良好,在游戲結(jié)束后會(huì)彈出窗口供玩家輸入姓名,并存儲(chǔ)在排行榜中。排行榜位于Game->LeaderBoard中。

    細(xì)節(jié)一:填入最后一個(gè)格子自動(dòng)觸發(fā)Check

    玩家填入最后一個(gè)格子即標(biāo)志著數(shù)獨(dú)的完成,此時(shí)應(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í)給予玩家這些功能可以為高端玩家提供便利,這是我們設(shè)計(jì)的初衷。

    細(xì)節(jié)四:Tip不可隨便使用

    Tip的存在是把雙刃劍——它會(huì)讓玩家喪失思考的樂趣,破壞游戲性,但同時(shí)也能給予玩家?guī)椭?#xff0c;讓實(shí)在想不出答案的玩家可以睡個(gè)安穩(wěn)覺。為了權(quán)衡利弊,我們綜合了Rank功能,讓使用過Tip的玩家不能將成績計(jì)入Rank,保證了游戲的公平性,同時(shí)還鼓勵(lì)玩家盡可能減少Tip的次數(shù),專注于提高解題能力。

    細(xì)節(jié)五:關(guān)閉主窗口,其它窗口隨即關(guān)閉

    如果主窗口都關(guān)了,剩下的小窗口存在的意義是什么呢?還要?jiǎng)跓┯脩粢粋€(gè)一個(gè)去關(guān)嗎?

    12. 描述結(jié)對的過程,提供非擺拍的兩人在討論的結(jié)對照片。(1')

    我們屬于是兩個(gè)落單的人,于是就結(jié)對了……結(jié)對的時(shí)候使用的是我的個(gè)人項(xiàng)目代碼,GUI部分由我編寫,-c生成算法也主要由我想出來的,而核心代碼中生成數(shù)獨(dú)謎題部分的算法則是由王辰昱提出的,實(shí)際運(yùn)行效率非常高。在本次結(jié)對編程過程中,我學(xué)到了很多東西,包括編程能力和溝通能力,收益匪淺。

    13. 結(jié)對編程的優(yōu)點(diǎn)和缺點(diǎn)

    結(jié)對編程

    優(yōu)點(diǎn):

  • 減少代碼編寫過程中出現(xiàn)問題的幾率
  • 在出現(xiàn)問題的時(shí)候,兩個(gè)人的思路更加廣闊,更利于解決問題
  • 在代碼設(shè)計(jì)過程中,兩個(gè)人經(jīng)過討論可以使代碼設(shè)計(jì)更加完美
  • 兩個(gè)人取長補(bǔ)短,結(jié)對過程也是相互學(xué)習(xí)的過程
  • 缺點(diǎn):

  • 對空閑時(shí)間要求較大,兩個(gè)人的空閑時(shí)間必須有很多交集
  • 隊(duì)友

    優(yōu)點(diǎn):

  • 代碼能力超強(qiáng),封裝性很好,代碼風(fēng)格很贊
  • 學(xué)習(xí)能力很強(qiáng),可以通過學(xué)習(xí)掌握一些算法,提高代碼運(yùn)行效率
  • 用戶體驗(yàn)部分理解比我要深刻,GUI的優(yōu)化部分幫了不少忙
  • 肯下工夫,愿意花費(fèi)時(shí)間對代碼進(jìn)行雕琢(很肝)
  • 缺點(diǎn):

  • 感覺有些缺乏計(jì)劃性,導(dǎo)致最后時(shí)間很緊張……
  • 自己

    優(yōu)點(diǎn):

  • debug能力感覺還可以,小技巧有很多,de掉了一些比較玄的bug
  • 肯下工夫,愿意花費(fèi)時(shí)間對代碼進(jìn)行雕琢(也很肝)
  • 感覺溝通能力較差,很多時(shí)候不能表達(dá)出自己的觀點(diǎn)
  • 缺點(diǎn):

  • 代碼風(fēng)格不好,比較粗心大意,造成很多bug
  • 14. 在你實(shí)現(xiàn)完程序之后,在附錄提供的PSP表格記錄下你在程序的各個(gè)模塊上實(shí)際花費(fèi)的時(shí)間。(0.5')

    附加題4

    交換小組:

    王子銘 15231058
    索一奇 15061180

    合并情況

      合并的時(shí)候出現(xiàn)了一些問題,主要原因是我們兩組的core接口不同,前者傳入的是一個(gè)二維指針,而我們傳入的是一個(gè)二維數(shù)組(助教博客中寫的是int[][] result,這是java中的用法,根據(jù)不同人理解不同,我認(rèn)為無論傳入指針還是數(shù)組都是正確的),導(dǎo)致最終通過修改代碼才能進(jìn)行合并。
      要想使用交換小組的dll新建游戲,必須要調(diào)用set_play(bool)函數(shù),這個(gè)函數(shù)十分簡單:

    void Core::set_play(bool a) {play = a; }

      其目的是為了調(diào)整生成puzzle的策略,如果play是false,說明是命令行模式下運(yùn)行,使用回溯法保證了數(shù)獨(dú)生成的不等價(jià)性;如果play是true,說明是在GUI中運(yùn)行,更加側(cè)重隨機(jī)性。這種做法的優(yōu)點(diǎn)在于生成數(shù)獨(dú)更加靈活,但同時(shí)為dll的交換造成了一定影響,因?yàn)閟et_play并不是約定的接口,導(dǎo)致只有通過修改代碼才能確保數(shù)獨(dú)游戲的正確生成。 因此我的個(gè)人觀點(diǎn)是,core中多余的函數(shù)會(huì)影響dll的靈活性,盡量不去添加沒有約定好的接口是比較好的設(shè)計(jì)方式。
      除此之外的一個(gè)小缺陷是任意打開兩次游戲的第N局游戲都是相同的,我認(rèn)為可以通過srand(time(0))根據(jù)系統(tǒng)時(shí)間改變隨機(jī)數(shù)種子,從而解決問題~

    合并后的游戲界面

      我normal模式下挖45個(gè)空,對方是挖47個(gè)空:

    Bug修復(fù)

    經(jīng)交換小組提示,目前Exception類已提供dll接口,調(diào)用者可以自行調(diào)用所有的異常類。

    附加題5

    正在收集反饋,部分反饋結(jié)果如下:

    某舍友A君
    亮點(diǎn)在于:細(xì)節(jié)做得非常好,比如使用tip之后不算成績、難度的遞增會(huì)有伴隨功能的出現(xiàn)、排行榜的可以自行清除;check、filter和track的功能做得很新穎,也很實(shí)用,比如check可以隨時(shí)點(diǎn)擊隨時(shí)檢查,track和filter能夠使得游戲過程的真實(shí)性更強(qiáng)。
    不足在于:未開放的按鈕可以不顯示;說明性的文字過少,影響用戶上手時(shí)的體驗(yàn)。

    某舍友B君
    優(yōu)點(diǎn):增加了filter功能,提升了游戲體驗(yàn),數(shù)獨(dú)填寫完成后自動(dòng)觸發(fā)check功能,不必手動(dòng)點(diǎn)擊check按鈕。排行榜信息豐富,用戶體驗(yàn)良好。
    缺少必要的用戶提示。直接進(jìn)入游戲頁面感覺很突兀。按鈕太方方正正了。

    某好友C君
    感覺很方便啊,感覺可以改進(jìn)一下,不用點(diǎn)數(shù)字,鍵盤輸入數(shù)字更爽。

    某舍友D君
    優(yōu)點(diǎn):

  • check功能標(biāo)注了同行或同列相同數(shù)字。
  • 增加filter功能,可提示當(dāng)前數(shù)字框接受數(shù)字。
  • 有track功能,可以顯示當(dāng)前數(shù)獨(dú)游戲中某數(shù)字所在位置。
  • help功能是一個(gè)亮點(diǎn),包含各個(gè)按鈕使用規(guī)則,顯得逼格很高的樣子。
  • 缺點(diǎn):

  • 被提示之后不能消除,不利于小伙伴之間的互動(dòng)。
  • 使用界面不友善。建議可以將按鈕變換形狀增添界面趣味性
  • 某基友E君

    這個(gè)成功提示顯示不全啊,話說這也太簡單了,hard8分多不用試數(shù)就做出來了= =
    (不好意思,是你太強(qiáng)了)

    相關(guān)改進(jìn)

    B君說得很有道理,于是我們添加了help功能,在About->Help可以調(diào)出幫助:

    解釋了相關(guān)的功能用法,同時(shí)該內(nèi)容也在README中寫入了。
    E君提出的問題暫時(shí)無法解決,這和計(jì)算機(jī)自定義的文本大小有關(guān),經(jīng)測試,該同學(xué)的文本大小大約為1.5x,而在1.0x和1.25x上顯示正常。

    感想

    我是個(gè)強(qiáng)迫癥,覺得界面可以不精美,但用起來一定要舒服,因此在顏色搭配、布局等方面花了一點(diǎn)時(shí)間,包括使用七段數(shù)碼管提供毫秒級的計(jì)時(shí)功能,營造緊張感;所有窗口的一鍵關(guān)閉;創(chuàng)造新紀(jì)錄后立刻調(diào)出排行榜等等……感覺至少對用戶習(xí)慣有了一點(diǎn)思考,這是我結(jié)對編程主要的收獲之一,當(dāng)然這部分也離不開同伴的幫助和支持,后面關(guān)于tip的配色問題都是他進(jìn)行改良的hhh,總體來講最終效果還說得過去。其實(shí)我一開始是想向“掃雷”看齊的,系統(tǒng)自帶的掃雷沒有特別花哨的配色,界面十分簡潔樸素,但是用起來也很舒服,我感覺能做到這種程度就很不錯(cuò)了。
    但是內(nèi)部的代碼實(shí)現(xiàn)其實(shí)在個(gè)人復(fù)審的時(shí)候感覺還是挺糟糕的,至少應(yīng)該建幾個(gè)QPushButton的子類代表不同類型的按鈕,主要是一開始的界面還是在個(gè)人階段一晚上搞出來的,沒有考慮太多,導(dǎo)致GUI內(nèi)部元件的封裝性特別差。這一部分以后還是要注意的。

    轉(zhuǎn)載于:https://www.cnblogs.com/slontia/p/7669797.html

    與50位技術(shù)專家面對面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖

    總結(jié)

    以上是生活随笔為你收集整理的[2017BUAA软工]结对项目:数独扩展的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。