├── .gitignore ├── LICENSE ├── README.md ├── draughts ├── chessboard.cpp ├── chessboard.h ├── chesspiece.cpp ├── chesspiece.h ├── config.h ├── data.txt ├── doc │ ├── doc.pdf │ ├── doc.tex │ ├── img1.png │ ├── img10.png │ ├── img2.png │ ├── img3.png │ ├── img4.png │ ├── img5.png │ ├── img6.png │ ├── img7.png │ ├── img8.png │ └── img9.png ├── draught.pro ├── draughts.cpp ├── draughts.h ├── image │ ├── black-crown.png │ ├── black-piece.png │ ├── dark-bg.png │ ├── light-bg.png │ ├── white-crown.png │ └── white-piece.png ├── listen_dialog.cpp ├── listen_dialog.h ├── listen_dialog.ui ├── login_dialog.cpp ├── login_dialog.h ├── login_dialog.ui ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui ├── res.qrc └── sound │ └── sound.wav ├── sudoku ├── action_queue.cpp ├── action_queue.h ├── config.h ├── dancing_link.cpp ├── dancing_link.h ├── digit_button.cpp ├── digit_button.h ├── doc │ ├── doc.pdf │ ├── doc.tex │ ├── sudoku-3a.png │ ├── sudoku-3b.png │ ├── sudoku-3c.png │ ├── sudoku-4a.png │ ├── sudoku-4b.png │ ├── sudoku-4c.png │ └── sudoku-4d.png ├── icons │ ├── alarm-clock.png │ ├── back.png │ ├── eraser.png │ ├── information.png │ ├── license.html │ ├── new.png │ ├── next.png │ ├── pause.png │ ├── play.png │ ├── question.png │ └── restart.png ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui ├── res.qrc ├── sudoku.cpp ├── sudoku.h ├── sudoku.pro ├── sudoku_cell.cpp ├── sudoku_cell.h ├── sudoku_grid.cpp ├── sudoku_grid.h ├── timer.cpp ├── timer.h ├── tool_button.cpp ├── tool_button.h ├── utils.cpp └── utils.h └── wiki-index ├── crawler.py ├── db.py ├── doc ├── doc.pdf ├── doc.tex ├── img-1.png ├── img-2.png ├── page.png ├── title-born.png ├── turing-1.png ├── turing-2.png ├── turing-3.png ├── turing-4.png └── wiki.png ├── get_list.py └── server ├── db.sqlite3 ├── infobox ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ ├── bootstrap_header.html │ ├── highlight.js │ ├── index.html │ ├── person.html │ ├── person_item.html │ ├── search.html │ └── search_box.html ├── templatetags │ ├── __init__.py │ └── filter.py ├── tests.py ├── urls.py ├── utils.py └── views.py ├── manage.py └── server ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | object_script.*.Release 16 | object_script.*.Debug 17 | *_plugin_import.cpp 18 | /.qmake.cache 19 | /.qmake.stash 20 | *.pro.user 21 | *.pro.user.* 22 | *.qbs.user 23 | *.qbs.user.* 24 | *.moc 25 | moc_*.cpp 26 | moc_*.h 27 | qrc_*.cpp 28 | ui_*.h 29 | *.qmlc 30 | *.jsc 31 | Makefile* 32 | *build-* 33 | 34 | 35 | # Qt unit tests 36 | target_wrapper.* 37 | 38 | 39 | # QtCreator 40 | 41 | *.autosave 42 | 43 | # QtCtreator Qml 44 | *.qmlproject.user 45 | *.qmlproject.user.* 46 | 47 | # QtCtreator CMake 48 | CMakeLists.txt.user* 49 | 50 | # Vim swap file 51 | .*.swp 52 | 53 | # Latex 54 | *.aux 55 | *.log 56 | _minted-doc 57 | 58 | # Python 59 | *.py[cod] 60 | __pycache__/ 61 | *$py.class 62 | 63 | wiki-index/data 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Yuhao Zhou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 程序设计训练 2 | ================= 3 | 4 | 这些是THU2017年小学期程序设计训练的大作业。包含数独游戏、联网国际跳棋游戏以及人物信息检索系统。 5 | 6 | Table of Contents 7 | ================= 8 | 9 | * [Week 1: Sudoku Game](#week-1-sudoku-game) 10 | * [简介](#简介) 11 | * [基本功能](#基本功能) 12 | * [游戏生成算法](#游戏生成算法) 13 | * [参考资料](#参考资料) 14 | * [Week 2: Internet Draughts](#week-2-internet-draughts) 15 | * [简介](#简介-1) 16 | * [基本功能](#基本功能-1) 17 | * [Week 3: 人物信息检索](#week-3-人物信息检索) 18 | * [简介](#简介-2) 19 | 20 | ## Week 1: Sudoku Game 21 | 22 | ### 简介 23 | 24 | Sudoku 是一款利用 Qt 实现的数独游戏,提供了多达 10 个难度的关卡选择,同时还有丰富的功能来帮助玩家更加高效地求解数独问题,例如候选数、高亮相同数字、高亮选中的行列、撤销当前操作以及提示等功能。玩家还可以手动输入数独题目利用 Sudoku 帮助求解。 25 | 26 | 除了传统 9x9 的数独游戏以外,还提供了更高难度的 16x16 的数独游戏。 27 | 28 | ![](https://github.com/miskcoo/programming-training/blob/sudoku/sudoku/doc/sudoku-3c.png?raw=true) 29 | 30 | ### 基本功能 31 | 32 | Sudoku提供了多个方便的按钮: 33 | 34 | * **新游戏**:玩家可以开始一局新的游戏。 35 | * **重玩**:玩家可以重新开始本局游戏。 36 | * **暂停**:玩家可以暂停该局游戏(即暂停计时)。 37 | * **提示**:如果当前已经确定的数都是正确的,玩家将会得到一个未填空格的正确数字;如果当前已经确定的数和答案矛盾,导致整个数独无解,那么所有与答案矛盾的数字将会被粗体标出。 38 | * **清除**:清除当前选中格子的所有数字。 39 | * **撤销**:撤销前一步的操作,以及取消撤销(最多可支持 50 步撤销)。 40 | 41 | 同时可以通过菜单来实现多达 10 种难度的游戏选择,可以求解任意用户输入的数独问题。 42 | 43 | 玩家在空格中填入的数字分为确定数(采用正常大小的字体表示)以及候选数(采用小号字体表示)。确定数只能存在一个,候选数可以存在多个,并且确定数和候选数不能同时存在。 44 | 45 | 玩家可以通过多种方式进行格子的选择: 46 | 47 | * 鼠标直接点击格子进行选中。 48 | * 键盘方向键来进行当前选中格子的切换。 49 | * Tab键快速切换到下一个格子。 50 | 51 | 玩家可以用鼠标右键点击格子来对其进行标记。 52 | 53 | 玩家在选中一个格子之后,如果该格子的数字已经确定,那么所有与该数相同的数将会被加粗显示,以方便确认是否满足数独的条件。同时,所有与该格在相同行或列的格子都会被高亮显示。此外,无论该格子数字确定与否,右侧数字列表中该格子的所有数都会被加粗。 54 | 55 | 玩家可以通过多种方式在空格中填入数字: 56 | 57 | * 键盘直接输入数字将填入对应的确定数,如果 Ctrl 键被按下,那么填入的将会是候选数。 58 | * 鼠标点击右侧数字列表填入。使用鼠标左键点击将会填入确定数,右键点击则会填入候选数。 59 | 60 | 玩家也可以通过多种方式在空格中填入数字: 61 | 62 | * 键盘删除键删除格子中的一个数字。 63 | * 鼠标点击右侧数字列表已经选中的数进行删除。 64 | * “清除”功能键来清除该格子所有的数字。 65 | 66 | ### 游戏生成算法 67 | 我们认为一个数独游戏的难度可以根据行列空格数的最大值以及给定数字的数量来确定。行列空格数的最大值越小玩家拥有的信息量就越多,同样给定数字越多玩家也能获得更多的信息。 68 | 69 | 在我们的难度中,最简单的关卡给出的数字至少有 50 个,并且行列空格数不会超过 4 个。然而,在最困难的关卡中,最少只会给出 20 个数字,并且可能会有某些行或列全部是空格。在最简单和最困难中间这两个影响难度的因素平滑过渡。 70 | 71 | 为了生成一个满足条件的数独,可以按照以下步骤来实现: 72 | 73 | 1. 在空白棋盘随机填入数十个数。 74 | 2. 利用求解算法获得一个合法解,如果不存在合法解,转到 1。 75 | 3. 生成一个随机的格子排列。 76 | 4. 按照这个序列来尝试一个个删除所填入的数字,如果删除后能保证解唯一并且满足之前的条件,那么就删除,否则不删除。 77 | 5. 如果删除了足够的数字,则返回。否则跳到 1。 78 | 79 | 为了实现快速的数独问题求解,我们利用 Dancing Link 来优化搜索。 80 | 81 | ### 参考资料 82 | [1]: XUE, Y.H., JIANG, B.B., Li, Y., YAN, G.F. and SUN, H.F., 2009. Sudoku puzzles generating: From easy to evil. Mathematics in practice and theory, 21(000). 83 | 84 | [2]: Knuth, D.E., 2000. Dancing links. arXiv preprint cs/0011047. 85 | 86 | ## Week 2: Internet Draughts 87 | 88 | ### 简介 89 | Draughts 是一款利用 Qt 实现的国际跳棋游戏,支持双人在线对战。国际跳棋是十分古老的智力游戏之一,其规则是在 10x10 的棋盘内,黑白双方各执 20 子,通过斜向移动、跳吃等手段吃掉对方更多的棋子。最终吃掉对方所有棋子或者使对方无法移动的一方获得胜利。 90 | 91 | ![](https://github.com/miskcoo/programming-training/blob/draughts/draughts/doc/img5.png?raw=true) 92 | 93 | 国际跳棋的基本规则是,黑方先行,并且所有棋子均只能斜向移动,因此整个棋盘只有深色位置可以有棋子。它有几条基本的吃子以及前进规则: 94 | 95 | * **能吃子就必须吃**:如果有多个棋子以及多条路径都可以吃子,那么必须吃最多的棋子。如果多有条路径可以吃到相同个数的棋子,那么可以任意选择一条。 96 | * **土耳其打击**:如果吃多子,那么被吃棋子在整个吃子过程结束后才被撤出棋盘。 97 | * **普通棋子前进**:对于普通棋子,只能向前方对角线方向前进一格。 98 | * **普通棋子跳吃**:对于普通棋子,只要对角线方向最近的黑格有敌方棋子,并且该棋子后最近的一格有空位,那么就可以跳到后方空位并且吃掉对应敌方棋子。 99 | * **升王**:任何棋子在最后一步停在对方底线则称为王棋。 100 | * **王棋前进**:对于王棋,只要对应方向有空位,可以向任意对角线方向前进后退任意步数。 101 | * **王棋跳吃**:对于王棋,在跳吃的时候可以无时距离,并且停在被吃棋子后任意空格处。 102 | 103 | ### 基本功能 104 | 105 | 我们的游戏实现在当前是自己的回合时,所有可以移动的棋子会被以绿色标出。当选中某个可移动棋子时,其本身以及下一步能够移动到的位置会被以黄色标出。 106 | 107 | 由于有连跳以及能吃子就多吃的规则,在存在多条可选路径时,如果只标明最终可达位置,可能会造成困惑以及存在无法选择相同可达位置但不同走棋路线的困难。因此,我们允许玩家一步一步进行走棋,以便选择路径。 108 | 109 | ## Week 3: 人物信息检索 110 | 111 | ### 简介 112 | 这是一个利用 Django 搭建的一个人物信息检索系统,大约从 Wikipedia 爬取了 10000 个人物信息,并且提取了其中 Infobox 的对应信息。 113 | 114 | 对于爬取的信息,我们重新组织了其显示的格式并且提供了一个搜索页面,允许根据关键词对其进行搜索,并且还可以根据原先信息的特定字段(如Born,Name,Nationality等)进行关键字查询。搜索结果按照匹配的关键字个数从高到底排序后进行显示,同时匹配的关键字将会被高亮标出。另外,如果结果过多将会分页进行显示。 115 | 116 | ![](https://github.com/miskcoo/programming-training/blob/wiki-index/wiki-index/doc/turing-1.png?raw=true) 117 | 118 | ![](https://github.com/miskcoo/programming-training/blob/wiki-index/wiki-index/doc/turing-4.png?raw=true) 119 | -------------------------------------------------------------------------------- /draughts/chessboard.cpp: -------------------------------------------------------------------------------- 1 | #include "chessboard.h" 2 | #include 3 | 4 | #define CELL_MOVE_CANDIDATE 1 5 | #define CELL_SELECT_CANDIDATE 2 6 | #define CELL_MOVE_TRACE 4 7 | 8 | ChessBoard::ChessBoard(QWidget *parent) 9 | : QWidget(parent) 10 | { 11 | setFixedSize(501, 501); 12 | std::memset(cells, 0, sizeof(cells)); 13 | std::memset(cell_status, 0, sizeof(cell_status)); 14 | } 15 | 16 | void ChessBoard::initBoard(Draughts *game) 17 | { 18 | std::memset(cell_status, 0, sizeof(cell_status)); 19 | 20 | long_term_move = false; 21 | draughts = std::make_shared(); 22 | if(game) *draughts = *game; 23 | for(int i = 0; i != 10; ++i) 24 | { 25 | for(int j = 0; j != 10; ++j) 26 | { 27 | if(cells[i][j]) delete cells[i][j]; 28 | if(!draughts->is_empty(i, j)) 29 | { 30 | cells[i][j] = new ChessPiece(this); 31 | cells[i][j]->show(); 32 | cells[i][j]->setGeometry(getCellRect(i, j)); 33 | cells[i][j]->setPlayer(player); 34 | } else cells[i][j] = nullptr; 35 | } 36 | } 37 | 38 | updatePieces(); 39 | } 40 | 41 | void ChessBoard::startGame(DraughtsInfo::Types player, 42 | DraughtsInfo::Types first_player, 43 | Draughts *game) 44 | { 45 | this->player = player; 46 | this->cur_player = first_player; 47 | 48 | initBoard(game); 49 | 50 | if(cur_player == player) 51 | markMoveCandidates(player); 52 | } 53 | 54 | void ChessBoard::updatePieces() 55 | { 56 | for(int i = 0; i != 10; ++i) 57 | for(int j = 0; j != 10; ++j) 58 | if(!draughts->is_empty(i, j) && cells[i][j]) 59 | cells[i][j]->setPieceInfo(draughts->get_info(i, j)); 60 | } 61 | 62 | QRect ChessBoard::getCellRect(int row, int col) 63 | { 64 | if(player == DraughtsInfo::black) 65 | row = 9 - row, col = 9 - col; 66 | int cell_width = (width() - 1) / 10; 67 | int cell_height = (height() - 1) / 10; 68 | return QRect(cell_width * col, cell_height * row, cell_width, cell_height); 69 | } 70 | 71 | pair ChessBoard::mapMouseToCell(QPoint mouse) 72 | { 73 | for(int i = 0; i != 10; ++i) 74 | for(int j = 0; j != 10; ++j) 75 | if(getCellRect(i, j).contains(mouse, true)) 76 | return { i, j }; 77 | return { -1, -1 }; 78 | } 79 | 80 | void ChessBoard::markMoveCandidates(DraughtsInfo::Types player) 81 | { 82 | auto avail_chess = draughts->get_avail_chess(player); 83 | for(auto piece_info : avail_chess) 84 | cell_status[piece_info.x][piece_info.y] |= CELL_MOVE_CANDIDATE; 85 | update(); 86 | } 87 | 88 | void ChessBoard::markAvailTraces() 89 | { 90 | if(avail_traces.empty()) return; 91 | for(const DraughtsTrace& trace : avail_traces) 92 | { 93 | DraughtsInfo piece_info; 94 | if(trace.size() == 2) 95 | piece_info = trace.back(); 96 | else piece_info = trace[2]; 97 | cell_status[piece_info.x][piece_info.y] |= CELL_SELECT_CANDIDATE; 98 | } 99 | 100 | int x = avail_traces[0][0].x, y = avail_traces[0][0].y; 101 | cell_status[x][y] |= CELL_SELECT_CANDIDATE; 102 | update(); 103 | } 104 | 105 | void ChessBoard::clearMarks(int cleard_mask) 106 | { 107 | for(int i = 0; i != 10; ++i) 108 | for(int j = 0; j != 10; ++j) 109 | cell_status[i][j] &= ~cleard_mask; 110 | update(); 111 | } 112 | 113 | void ChessBoard::cellClicked(int x, int y) 114 | { 115 | if(cur_player != player) 116 | return; 117 | 118 | if(cell_status[x][y] == CELL_MOVE_CANDIDATE) 119 | { 120 | clearMarks(CELL_SELECT_CANDIDATE | CELL_MOVE_TRACE); 121 | avail_traces = draughts->get_avail_move(x, y).second; 122 | long_term_move = false; 123 | markAvailTraces(); 124 | cur_x = x, cur_y = y; 125 | } else if(cell_status[x][y] & CELL_SELECT_CANDIDATE) { 126 | if(draughts->is_empty(x, y)) 127 | { 128 | int trace_len = avail_traces.front().size(); 129 | clearMarks(); 130 | if(trace_len <= 3 && !long_term_move) 131 | { 132 | // one-step move 133 | vector> move_trace{{ cur_x, cur_y }, { x, y }}; 134 | if(moveChess(move_trace)) 135 | emit playerMove(move_trace); 136 | } else { 137 | // multi-step move 138 | long_term_move = true; 139 | DraughtsTrace sub_trace; 140 | for(auto it = avail_traces.begin(); it != avail_traces.end(); ) 141 | { 142 | DraughtsTrace& trace = *it; 143 | if(trace[2].x == x && trace[2].y == y) 144 | { 145 | sub_trace.assign(trace.begin(), trace.begin() + 3); 146 | trace.erase(trace.begin(), trace.begin() + 2); 147 | ++it; 148 | } else { 149 | it = avail_traces.erase(it); 150 | } 151 | } 152 | 153 | cur_move_trace.push_back({x, y}); 154 | 155 | applyTrace(sub_trace); 156 | if(trace_len <= 3) 157 | { 158 | // last move, apply to Draughts 159 | cur_move_trace.insert(cur_move_trace.begin(), { cur_x, cur_y }); 160 | if(moveChess(cur_move_trace)) 161 | emit playerMove(cur_move_trace); 162 | long_term_move = false; 163 | cur_move_trace.clear(); 164 | } else markAvailTraces(); 165 | } 166 | } 167 | } 168 | } 169 | 170 | void ChessBoard::applyTrace(const DraughtsTrace &trace) 171 | { 172 | if(trace.empty()) return; 173 | clearMarks(CELL_MOVE_TRACE); 174 | 175 | int src_x = trace.front().x, src_y = trace.front().y; 176 | int dest_x = trace.back().x, dest_y = trace.back().y; 177 | if(trace.size() == 2) 178 | { 179 | // non-eating move 180 | cells[dest_x][dest_y] = cells[src_x][src_y]; 181 | cells[src_x][src_y] = nullptr; 182 | 183 | cell_status[dest_x][dest_y] = CELL_MOVE_TRACE; 184 | cell_status[src_x][src_y] = CELL_MOVE_TRACE; 185 | 186 | cells[dest_x][dest_y]->moveAnimation( { getCellRect(dest_x, dest_y) }, ANIMATION_TIME); 187 | } else if(trace.size() > 2) { 188 | // eating move 189 | vector move_seq; 190 | cell_status[src_x][src_y] = CELL_MOVE_TRACE; 191 | for(size_t i = 2; i < trace.size(); i += 2) 192 | { 193 | move_seq.push_back(getCellRect(trace[i].x, trace[i].y)); 194 | cell_status[trace[i].x][trace[i].y] = CELL_MOVE_TRACE; 195 | } 196 | 197 | for(size_t i = 1; i < trace.size(); i += 2) 198 | { 199 | qreal start = 1.0 * (i / 2) / (trace.size() / 2); 200 | qreal end = 1.0 * (i / 2 + 0.5) / (trace.size() / 2); 201 | cells[trace[i].x][trace[i].y]->fadeOut(start, end, ANIMATION_TIME * (trace.size() / 2)); 202 | } 203 | 204 | cells[dest_x][dest_y] = cells[src_x][src_y]; 205 | cells[src_x][src_y] = nullptr; 206 | 207 | cells[dest_x][dest_y]->moveAnimation(move_seq, ANIMATION_TIME * (trace.size() / 2)); 208 | } 209 | } 210 | 211 | bool ChessBoard::moveChess(const vector>& move_trace) 212 | { 213 | if(move_trace.size() < 2) return false; 214 | auto trace = draughts->move(move_trace); 215 | if(trace.empty()) return false; 216 | 217 | if(!long_term_move) applyTrace(trace); 218 | 219 | if(cur_player == DraughtsInfo::black) 220 | cur_player = DraughtsInfo::white; 221 | else cur_player = DraughtsInfo::black; 222 | 223 | if(cur_player == player) 224 | markMoveCandidates(player); 225 | 226 | updatePieces(); 227 | 228 | if(draughts->get_avail_chess(cur_player).empty()) 229 | emit noAvailChess(); 230 | 231 | return true; 232 | } 233 | 234 | void ChessBoard::mouseReleaseEvent(QMouseEvent *ev) 235 | { 236 | if(ev->button() == Qt::LeftButton) 237 | { 238 | auto pos = mapMouseToCell(ev->pos()); 239 | if(pos.first >= 0) 240 | cellClicked(pos.first, pos.second); 241 | } 242 | 243 | QWidget::mouseReleaseEvent(ev); 244 | } 245 | 246 | void ChessBoard::paintEvent(QPaintEvent *ev) 247 | { 248 | QPainter p(this); 249 | 250 | // draw basic background 251 | static QBrush dark_cell_brush(QPixmap(":dark-bg")); 252 | static QBrush light_cell_brush(QPixmap(":light-bg")); 253 | 254 | for(int i = 0; i != 10; ++i) 255 | for(int j = 0; j != 10; ++j) 256 | { 257 | if((i ^ j) & 1) 258 | p.setBrush(dark_cell_brush); 259 | else p.setBrush(light_cell_brush); 260 | p.drawRect(getCellRect(i, j)); 261 | } 262 | 263 | // draw special cells 264 | auto drawRectWithGradient = [&](QRect rect, QColor c_begin, QColor c_end, double depth1, double depth2) 265 | { 266 | QLinearGradient gradient; 267 | gradient.setColorAt(0, c_begin); 268 | gradient.setColorAt(depth1, c_end); 269 | gradient.setColorAt(1, c_end); 270 | 271 | gradient.setStart(rect.topLeft()); 272 | gradient.setFinalStop(rect.topRight()); 273 | p.fillRect(rect, QBrush(gradient)); 274 | 275 | gradient.setStart(rect.topLeft()); 276 | gradient.setFinalStop(rect.bottomLeft()); 277 | p.fillRect(rect, QBrush(gradient)); 278 | 279 | gradient = QLinearGradient(); 280 | gradient.setColorAt(0, c_begin); 281 | gradient.setColorAt(depth2, c_end); 282 | gradient.setColorAt(1, c_end); 283 | 284 | gradient.setStart(rect.topRight()); 285 | gradient.setFinalStop(rect.topLeft()); 286 | p.fillRect(rect, QBrush(gradient)); 287 | 288 | gradient.setStart(rect.bottomLeft()); 289 | gradient.setFinalStop(rect.topLeft()); 290 | p.fillRect(rect, QBrush(gradient)); 291 | }; 292 | 293 | for(int i = 0; i != 10; ++i) 294 | for(int j = 0; j != 10; ++j) 295 | { 296 | QRect cell_rect = getCellRect(i, j); 297 | QRect gradient_cell_rect = cell_rect.adjusted(0, 0, 1, 1); 298 | p.setBrush(QBrush()); 299 | 300 | if(cell_status[i][j] & CELL_MOVE_TRACE) 301 | { 302 | drawRectWithGradient(gradient_cell_rect, 303 | QColor(0, 0, 0, 200), 304 | QColor(0, 0, 0, 0), 0.15, 0.1); 305 | } else if(cell_status[i][j] & CELL_SELECT_CANDIDATE) { 306 | drawRectWithGradient(gradient_cell_rect, 307 | QColor(255, 255, 255, 100), 308 | QColor(255, 255, 255, 0), 0.1, 0.1); 309 | p.setPen(QPen(Qt::yellow, 2)); 310 | p.drawRect(cell_rect); 311 | } else if(cell_status[i][j] & CELL_MOVE_CANDIDATE) { 312 | p.setPen(QPen(Qt::green, 2)); 313 | p.drawRect(cell_rect); 314 | } 315 | } 316 | 317 | QWidget::paintEvent(ev); 318 | } 319 | -------------------------------------------------------------------------------- /draughts/chessboard.h: -------------------------------------------------------------------------------- 1 | #ifndef CHESSBOARD_H 2 | #define CHESSBOARD_H 3 | 4 | #include 5 | #include 6 | #include "config.h" 7 | #include "draughts.h" 8 | #include "chesspiece.h" 9 | 10 | class ChessBoard : public QWidget 11 | { 12 | Q_OBJECT 13 | public: 14 | explicit ChessBoard(QWidget *parent = 0); 15 | 16 | void initBoard(Draughts *game); 17 | QRect getCellRect(int row, int col); 18 | pair mapMouseToCell(QPoint); 19 | 20 | void clearMarks(int cleard_mask = ~0u >> 1); 21 | void markMoveCandidates(DraughtsInfo::Types player); 22 | void markAvailTraces(); 23 | void cellClicked(int x, int y); 24 | bool moveChess(const vector>&); 25 | void applyTrace(const DraughtsTrace& trace); 26 | void updatePieces(); 27 | 28 | void startGame(DraughtsInfo::Types player, 29 | DraughtsInfo::Types first_player = DraughtsInfo::black, 30 | Draughts *game = nullptr); 31 | 32 | DraughtsInfo::Types getPlayer() { return player; } 33 | DraughtsInfo::Types getCurrentPlayer() { return cur_player; } 34 | signals: 35 | void playerMove(vector>); 36 | void noAvailChess(); 37 | 38 | public slots: 39 | 40 | protected: 41 | void paintEvent(QPaintEvent *); 42 | void mouseReleaseEvent(QMouseEvent *); 43 | private: 44 | bool long_term_move; 45 | int cur_x, cur_y; 46 | int cell_status[10][10]; 47 | ChessPiece *cells[10][10]; 48 | vector avail_traces; 49 | vector> cur_move_trace; 50 | DraughtsInfo::Types player, cur_player; 51 | std::shared_ptr draughts; 52 | }; 53 | 54 | #endif // CHESSBOARD_H 55 | -------------------------------------------------------------------------------- /draughts/chesspiece.cpp: -------------------------------------------------------------------------------- 1 | #include "chesspiece.h" 2 | #include 3 | 4 | ChessPiece::ChessPiece(QWidget *parent) : QWidget(parent) 5 | { 6 | sound = new QSound(":sound", this); 7 | opacity_effect = new QGraphicsOpacityEffect(this); 8 | opacity_effect->setOpacity(1.0); 9 | setGraphicsEffect(opacity_effect); 10 | } 11 | 12 | void ChessPiece::paintEvent(QPaintEvent *ev) 13 | { 14 | QPainter p(this); 15 | 16 | static QPixmap black_piece(":black-piece"); 17 | static QPixmap white_piece(":white-piece"); 18 | static QPixmap black_crown(":black-crown"); 19 | static QPixmap white_crown(":white-crown"); 20 | 21 | QPixmap *crown = nullptr; 22 | 23 | if(piece_info.type == DraughtsInfo::black) 24 | { 25 | p.drawPixmap(rect(), black_piece); 26 | crown = &white_crown; 27 | } else if(piece_info.type == DraughtsInfo::white) { 28 | p.drawPixmap(rect(), white_piece); 29 | crown = &black_crown; 30 | } 31 | 32 | if(piece_info.is_king && crown) 33 | { 34 | if(piece_info.type != player) 35 | { 36 | p.translate(rect().bottomRight() + QPoint(1, 1)); 37 | p.rotate(180); 38 | } 39 | p.drawPixmap(rect(), *crown); 40 | } 41 | 42 | QWidget::paintEvent(ev); 43 | } 44 | 45 | void ChessPiece::moveAnimation(vector dest_rects, int milliseconds) 46 | { 47 | QPropertyAnimation *move_ani = new QPropertyAnimation(this, "geometry"); 48 | move_ani->setDuration(milliseconds); 49 | move_ani->setStartValue(geometry()); 50 | for(int i = 0; i + 1 < (int)dest_rects.size(); ++i) 51 | move_ani->setKeyValueAt((i + 1.0) / dest_rects.size(), dest_rects[i]); 52 | move_ani->setEndValue(dest_rects.back()); 53 | move_ani->start(QPropertyAnimation::DeleteWhenStopped); 54 | connect(move_ani, SIGNAL(destroyed(QObject*)), this, SLOT(moveFinished())); 55 | } 56 | 57 | void ChessPiece::fadeOut(qreal start, qreal end, int milliseconds) 58 | { 59 | QPropertyAnimation *opacity_ani = new QPropertyAnimation(opacity_effect, "opacity"); 60 | opacity_ani->setDuration(milliseconds); 61 | opacity_ani->setStartValue(1); 62 | opacity_ani->setKeyValueAt(start, 1); 63 | opacity_ani->setKeyValueAt(end, 0); 64 | opacity_ani->setEndValue(0); 65 | opacity_ani->start(QPropertyAnimation::DeleteWhenStopped); 66 | } 67 | 68 | void ChessPiece::setPieceInfo(DraughtsInfo piece_info) 69 | { 70 | this->piece_info = piece_info; 71 | update(); 72 | } 73 | 74 | void ChessPiece::setPlayer(DraughtsInfo::Types player) 75 | { 76 | this->player = player; 77 | update(); 78 | } 79 | 80 | void ChessPiece::moveFinished() 81 | { 82 | sound->play(); 83 | } 84 | -------------------------------------------------------------------------------- /draughts/chesspiece.h: -------------------------------------------------------------------------------- 1 | #ifndef CHESSPIECE_H 2 | #define CHESSPIECE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "draughts.h" 8 | 9 | class ChessPiece : public QWidget 10 | { 11 | Q_OBJECT 12 | public: 13 | explicit ChessPiece(QWidget *parent = 0); 14 | 15 | void setPlayer(DraughtsInfo::Types player); 16 | void fadeOut(qreal start, qreal end, int milliseconds); 17 | void moveAnimation(vector dest_rects, int milliseconds); 18 | void setPieceInfo(DraughtsInfo piece_info); 19 | 20 | signals: 21 | 22 | public slots: 23 | void moveFinished(); 24 | 25 | protected: 26 | void paintEvent(QPaintEvent *); 27 | 28 | private: 29 | QSound *sound; 30 | DraughtsInfo piece_info; 31 | DraughtsInfo::Types player; 32 | QGraphicsOpacityEffect *opacity_effect; 33 | }; 34 | 35 | #endif // CHESSPIECE_H 36 | -------------------------------------------------------------------------------- /draughts/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #define CHESS_PIECE_SIZE 50 5 | #define ANIMATION_TIME 400 6 | 7 | // MOVE 8 | #define OPER_MOVE "MOVE" 9 | // GIVEUP 10 | #define OPER_GIVEUP "GIVEUP" 11 | // START 12 | #define OPER_START "START" 13 | // SETGAME 14 | #define OPER_SETGAME "SETGAME" 15 | // MAKEPEACE 16 | #define OPER_MAKEPEACE "MAKEPEACE" 17 | // CONFIRM_PEACE 18 | #define OPER_CONFIRMPEACE "CONFIRM_PEACE" 19 | // DENIED_PEACE 20 | #define OPER_DENIEDPEACE "DENIED_PEACE" 21 | 22 | #define OPER_SETGAME_BLACK 'b' 23 | #define OPER_SETGAME_BKING 'B' 24 | #define OPER_SETGAME_WHITE 'w' 25 | #define OPER_SETGAME_WKING 'W' 26 | 27 | #endif 28 | -------------------------------------------------------------------------------- /draughts/data.txt: -------------------------------------------------------------------------------- 1 | 000000000000b000w0w0000000000b00b0000000000w00000000b000000000000b000000b000000000000w000000000000w0 2 | 000000000000b000w0w0000000000b00b0000000000w00000000b000000000000b000000b000000000000w00000000000000 3 | 0b0000000000w0w0w000000000000000w000w000000000000000w0w000000000000000000000000000000000000000000000 4 | 000000000000b000w0w0000000000b00b0000000000w00000000b000000000000b000000b000000000000w00000000000000 5 | 0b0000000000b000w0w0000000000b00b0000000000w00000000b000000000000b000000b000000000000w00000000000000 6 | -------------------------------------------------------------------------------- /draughts/doc/doc.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/doc.pdf -------------------------------------------------------------------------------- /draughts/doc/doc.tex: -------------------------------------------------------------------------------- 1 | \XeTeXlinebreaklocale "zh" 2 | \XeTeXlinebreakskip = 0pt plus 1pt 3 | 4 | \documentclass[11pt,a4paper]{article} 5 | 6 | \usepackage{xltxtra,fontspec,xunicode} 7 | \usepackage{amsthm, amsmath, amssymb, amsfonts} 8 | \usepackage{abstract} 9 | \usepackage{subcaption} 10 | \usepackage{graphicx,float} 11 | \usepackage{minted} 12 | 13 | \defaultfontfeatures{Mapping=tex-text,Scale=MatchLowercase} 14 | \setmainfont{DejaVu Serif} 15 | \setsansfont{DejaVu Sans} 16 | \setmonofont{DejaVu Sans Mono} 17 | \usepackage[slantfont,boldfont]{xeCJK} % 允许斜体和粗体 18 | \setCJKmainfont{WenQuanYi Zen Hei} 19 | \setCJKsansfont{WenQuanYi Zen Hei} 20 | \setCJKmonofont{WenQuanYi Zen Hei Mono} 21 | 22 | \usepackage{titling} 23 | 24 | \renewcommand\refname{参考文献} 25 | 26 | \title{Draughts的功能及其实现} 27 | \author{周聿浩\\ \small{2016011347}} 28 | 29 | \begin{document} 30 | \maketitle 31 | \section{简介} 32 | Draughts是一款利用Qt实现的国际跳棋游戏,支持双人在线对战。国际跳棋是十分古老的智力游戏之一,其规则是在$10 \times 10$的棋盘内,黑白双方各执20子,通过斜向移动、跳吃等手段吃掉对方更多的棋子。最终吃掉对方所有棋子或者使对方无法移动的一方获得胜利。 33 | 34 | \begin{figure}[H] 35 | \centering 36 | \begin{subfigure}{.24\textwidth} 37 | \centering 38 | \includegraphics[width=\linewidth]{img1.png} 39 | \caption{} 40 | \end{subfigure} 41 | \hfill 42 | \begin{subfigure}{.24\textwidth} 43 | \centering 44 | \includegraphics[width=\linewidth]{img2.png} 45 | \caption{} 46 | \end{subfigure} 47 | \hfill 48 | \begin{subfigure}{.24\textwidth} 49 | \centering 50 | \includegraphics[width=\linewidth]{img3.png} 51 | \caption{} 52 | \end{subfigure} 53 | \hfill 54 | \begin{subfigure}{.24\textwidth} 55 | \centering 56 | \includegraphics[width=\linewidth]{img4.png} 57 | \caption{} 58 | \end{subfigure} 59 | \caption{图(a):游戏开始时,黑方先手,可以移动的棋子以绿色标出。图(b):选中某个棋子后,可以移动到的空位以及当前棋子以黄色标出。图(c):移动后,移动的轨迹以黑色标出。图(d):黑方移动后,白方视角所见。} 60 | \label{fig:1} 61 | \end{figure} 62 | 63 | 国际跳棋的基本规则是,黑方先行,并且所有棋子均只能斜向移动,因此整个棋盘只有深色位置可以有棋子。它有几条基本的吃子以及前进规则: 64 | 65 | \begin{enumerate} 66 | \item \textbf{能吃子就必须吃}:如果有多个棋子以及多条路径都可以吃子,那么必须吃最多的棋子。如果多有条路径可以吃到相同个数的棋子,那么可以任意选择一条。 67 | \item \textbf{土耳其打击}:如果吃多子,那么被吃棋子在整个吃子过程结束后才被撤出棋盘。 68 | \item \textbf{普通棋子前进}:对于普通棋子,只能向前方对角线方向前进一格。 69 | \item \textbf{普通棋子跳吃}:对于普通棋子,只要对角线方向最近的黑格有敌方棋子,并且该棋子后最近的一格有空位,那么就可以跳到后方空位并且吃掉对应敌方棋子。 70 | \item \textbf{升王}:任何棋子在最后一步停在对方底线则称为王棋。 71 | \item \textbf{王棋前进}:对于王棋,只要对应方向有空位,可以向任意对角线方向前进后退任意步数。 72 | \item \textbf{王棋跳吃}:对于王棋,在跳吃的时候可以无时距离,并且停在被吃棋子后任意空格处。 73 | \end{enumerate} 74 | 75 | 我们的游戏实现在当前是自己的回合时,所有可以移动的棋子会被以绿色标出。当选中某个可移动棋子时,其本身以及下一步能够移动到的位置会被以黄色标出。见图(\ref{fig:1})。 76 | 77 | 由于有连跳以及能吃子就多吃的规则,在存在多条可选路径时,如果只标明最终可达位置,可能会造成困惑以及存在无法选择相同可达位置但不同走棋路线的困难。因此,我们允许玩家一步一步进行走棋,以便选择路径,详见图(\ref{fig:2})。 78 | 79 | \begin{figure}[H] 80 | \centering 81 | \begin{subfigure}{.24\textwidth} 82 | \centering 83 | \includegraphics[width=\linewidth]{img7.png} 84 | \caption{} 85 | \end{subfigure} 86 | \hfill 87 | \begin{subfigure}{.24\textwidth} 88 | \centering 89 | \includegraphics[width=\linewidth]{img8.png} 90 | \caption{} 91 | \end{subfigure} 92 | \hfill 93 | \begin{subfigure}{.24\textwidth} 94 | \centering 95 | \includegraphics[width=\linewidth]{img9.png} 96 | \caption{} 97 | \end{subfigure} 98 | \hfill 99 | \begin{subfigure}{.24\textwidth} 100 | \centering 101 | \includegraphics[width=\linewidth]{img10.png} 102 | \caption{} 103 | \end{subfigure} 104 | \caption{图(a):当前最多可以吃两个棋子,仅有一个棋子可选。图(b):选中后,在吃子路径的下一步被标出(如果有多条路径,全都会被黄色标出)。图(c):选择相应路径的第一步后,随即标出继续可行路径。图(d):完成一步。} 105 | \label{fig:2} 106 | \end{figure} 107 | 108 | \begin{figure}[H] 109 | \centering 110 | \begin{subfigure}{.47\textwidth} 111 | \centering 112 | \includegraphics[width=\linewidth]{img5.png} 113 | \caption{} 114 | \end{subfigure} 115 | \hfill 116 | \begin{subfigure}{.47\textwidth} 117 | \centering 118 | \includegraphics[width=\linewidth]{img6.png} 119 | \caption{} 120 | \end{subfigure} 121 | \caption{王的可行路径,以及对手所见} 122 | \label{fig:3} 123 | \end{figure} 124 | 125 | 此外,在进行棋子移动时,我们提供动画效果以增加观赏性。以及增加了认输的功能(位于菜单)。 126 | 127 | 我们还支持在开局时自行定义开始局面以及先手,局面以 100 个字符的形式输入,以 0 表示空位,w 和 W 分别表示白色棋子和白色王棋,b 和 B 分别表示黑色棋子和黑色王棋。 128 | 129 | \section{部分实现细节} 130 | 关于动画效果,可以利用 {\it QPropertyAnimation} 来实现。对于被吃棋子的消失效果,可以利用 {\it QGraphicsOpacityEffect} 来调节透明度。 131 | 132 | 对于棋盘的内部黑色发光效果,可以针对矩形的四条边向内进行线性渐变的填充。 133 | 134 | 对于网络的协议,我们简单用可见字符来传输数据,以{\it 操作名+参数}的格式进行通信。各个操作的传输协议分别是: 135 | 136 | \begin{itemize} 137 | \item {\bf 开局}:START 138 | \item {\bf 放弃}:GIVEUP 139 | \item {\bf 设置开始局面}:SETGAME {\it } 140 | \item {\bf 移动}:MOVE {\it } 141 | \item {\bf 求和}:MAKEPEACE 142 | \item {\bf 同意求和}:CONFIRM\_PEACE 143 | \item {\bf 否决求和}:DENIED\_PEACE 144 | \end{itemize} 145 | \end{document} 146 | -------------------------------------------------------------------------------- /draughts/doc/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img1.png -------------------------------------------------------------------------------- /draughts/doc/img10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img10.png -------------------------------------------------------------------------------- /draughts/doc/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img2.png -------------------------------------------------------------------------------- /draughts/doc/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img3.png -------------------------------------------------------------------------------- /draughts/doc/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img4.png -------------------------------------------------------------------------------- /draughts/doc/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img5.png -------------------------------------------------------------------------------- /draughts/doc/img6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img6.png -------------------------------------------------------------------------------- /draughts/doc/img7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img7.png -------------------------------------------------------------------------------- /draughts/doc/img8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img8.png -------------------------------------------------------------------------------- /draughts/doc/img9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/doc/img9.png -------------------------------------------------------------------------------- /draughts/draught.pro: -------------------------------------------------------------------------------- 1 | #------------------------------------------------- 2 | # 3 | # Project created by QtCreator 2017-09-03T23:55:51 4 | # 5 | #------------------------------------------------- 6 | 7 | QT += core gui network multimedia 8 | 9 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 10 | 11 | TARGET = draughts 12 | TEMPLATE = app 13 | 14 | 15 | SOURCES += main.cpp\ 16 | mainwindow.cpp \ 17 | chessboard.cpp \ 18 | chesspiece.cpp \ 19 | draughts.cpp \ 20 | login_dialog.cpp \ 21 | listen_dialog.cpp 22 | 23 | HEADERS += mainwindow.h \ 24 | chessboard.h \ 25 | config.h \ 26 | chesspiece.h \ 27 | draughts.h \ 28 | login_dialog.h \ 29 | listen_dialog.h 30 | 31 | FORMS += mainwindow.ui \ 32 | login_dialog.ui \ 33 | listen_dialog.ui 34 | 35 | RESOURCES += \ 36 | res.qrc 37 | -------------------------------------------------------------------------------- /draughts/draughts.cpp: -------------------------------------------------------------------------------- 1 | #include "draughts.h" 2 | #include "config.h" 3 | #include 4 | 5 | Draughts::Draughts() 6 | { 7 | for(int i = 0; i != 10; ++i) 8 | for(int j = 0; j != 10; ++j) 9 | { 10 | int id = i * 10 + j + 1; 11 | if(((i ^ j) & 1) && id <= 40) 12 | status[i][j] = DraughtsInfo(i, j, DraughtsInfo::black); 13 | else if(((i ^ j) & 1) && id >= 60) 14 | status[i][j] = DraughtsInfo(i, j, DraughtsInfo::white); 15 | else status[i][j] = DraughtsInfo(i, j); 16 | } 17 | } 18 | 19 | Draughts::Draughts(const char *game) 20 | { 21 | for(int i = 0; i != 10; ++i) 22 | for(int j = 0; j != 10; ++j) 23 | { 24 | switch(game[i * 10 + j]) 25 | { 26 | case OPER_SETGAME_WHITE: 27 | status[i][j] = DraughtsInfo(i, j, DraughtsInfo::white, false); 28 | break; 29 | case OPER_SETGAME_WKING: 30 | status[i][j] = DraughtsInfo(i, j, DraughtsInfo::white, true); 31 | break; 32 | case OPER_SETGAME_BLACK: 33 | status[i][j] = DraughtsInfo(i, j, DraughtsInfo::black, false); 34 | break; 35 | case OPER_SETGAME_BKING: 36 | status[i][j] = DraughtsInfo(i, j, DraughtsInfo::black, true); 37 | break; 38 | default: 39 | status[i][j] = DraughtsInfo(i, j); 40 | } 41 | } 42 | } 43 | 44 | bool Draughts::is_empty(int x, int y) const 45 | { 46 | if(check_coord_avail(x, y)) 47 | return status[x][y].type == DraughtsInfo::empty; 48 | 49 | return true; 50 | } 51 | 52 | vector Draughts::get_avail_chess(DraughtsInfo::Types player) 53 | { 54 | int cur_step = 0; 55 | vector avail_chess; 56 | 57 | for(int i = 0; i != 10; ++i) 58 | for(int j = 0; j != 10; ++j) 59 | { 60 | if(status[i][j].type != player) 61 | continue; 62 | 63 | auto pair_info = get_avail_move(i, j); 64 | if(cur_step == pair_info.first) 65 | { 66 | avail_chess.push_back(get_info(i, j)); 67 | } else if(cur_step < pair_info.first) { 68 | cur_step = pair_info.first; 69 | avail_chess.clear(); 70 | avail_chess.push_back(get_info(i, j)); 71 | } 72 | } 73 | 74 | return avail_chess; 75 | } 76 | 77 | pair> Draughts::get_avail_move(int x, int y) 78 | { 79 | if(!check_coord_avail(x, y)) 80 | return {}; 81 | 82 | int cur_step = 0; 83 | auto player = status[x][y].type; 84 | vector avail_move; 85 | 86 | // non-eating move 87 | if(!status[x][y].is_king) 88 | { 89 | int dx = (player == DraughtsInfo::black) ? 1 : -1; 90 | int dys[] = { 1, -1 }; 91 | for(int i = 0; i != 2; ++i) 92 | { 93 | int nx = x + dx, ny = y + dys[i]; 94 | if(check_coord_avail(nx, ny) && is_empty(nx, ny)) 95 | { 96 | cur_step = 1; 97 | avail_move.push_back({ get_info(x, y), get_info(nx, ny) }); 98 | } 99 | } 100 | } else { 101 | int dxs[] = { 1, 1, -1, -1 }; 102 | int dys[] = { 1, -1, 1, -1 }; 103 | for(int i = 0; i != 4; ++i) 104 | { 105 | int nx = x + dxs[i], ny = y + dys[i]; 106 | while(check_coord_avail(nx, ny) && is_empty(nx, ny)) 107 | { 108 | cur_step = 1; 109 | avail_move.push_back({ get_info(x, y), get_info(nx, ny) }); 110 | nx += dxs[i], ny += dys[i]; 111 | } 112 | } 113 | } 114 | 115 | // eating move 116 | std::memset(mark, 0, sizeof(mark)); 117 | dfs_jump(0, x, y, status[x][y].is_king, player, 118 | [&](int step, DraughtsInfo* info, DraughtsInfo* eat) -> bool 119 | { 120 | int total_step = step * 2; // including eating steps 121 | if(total_step <= 1) return false; 122 | DraughtsTrace trace; 123 | for(int i = 0; i != step; ++i) 124 | { 125 | trace.push_back(info[i]); 126 | trace.push_back(eat[i]); 127 | } 128 | trace.push_back(info[step]); 129 | 130 | if(cur_step == total_step) 131 | { 132 | avail_move.push_back(trace); 133 | } else if(cur_step < total_step) { 134 | cur_step = total_step; 135 | avail_move.clear(); 136 | avail_move.push_back(trace); 137 | } 138 | 139 | return false; 140 | } ); 141 | 142 | return { cur_step, avail_move }; 143 | } 144 | 145 | DraughtsTrace Draughts::move(const vector>& move_trace) 146 | { 147 | if(move_trace.size() < 2) return {}; 148 | int src_x = move_trace.front().first, src_y = move_trace.front().second; 149 | int dest_x = move_trace.back().first, dest_y = move_trace.back().second; 150 | 151 | if(!check_coord_avail(src_x, src_y) 152 | && !check_coord_avail(dest_x, dest_y) 153 | && status[src_x][src_y].type != DraughtsInfo::empty) 154 | return {}; 155 | 156 | auto try_promote_king = [this] (DraughtsInfo& info) 157 | { 158 | int end_x = (info.type == DraughtsInfo::black) ? 9 : 0; 159 | if(info.x == end_x) 160 | info.is_king = true; 161 | }; 162 | 163 | auto move_chess = [&] () 164 | { 165 | status[dest_x][dest_y].type = status[src_x][src_y].type; 166 | status[dest_x][dest_y].is_king = status[src_x][src_y].is_king; 167 | status[src_x][src_y].set_empty(); 168 | }; 169 | 170 | // non-eating move 171 | auto player = status[src_x][src_y].type; 172 | if(move_trace.size() <= 2) 173 | { 174 | if(!status[src_x][src_y].is_king) 175 | { 176 | int dx = player == DraughtsInfo::black ? 1 : -1; 177 | int dys[] = { 1, -1 }; 178 | for(int i = 0; i != 2; ++i) 179 | { 180 | int nx = src_x + dx, ny = src_y + dys[i]; 181 | if(check_coord_avail(nx, ny) && is_empty(nx, ny) 182 | && nx == dest_x && ny == dest_y) 183 | { 184 | move_chess(); 185 | try_promote_king(status[nx][ny]); 186 | return { status[src_x][src_y], status[nx][ny] }; 187 | } 188 | } 189 | } else { 190 | int dxs[] = { 1, 1, -1, -1 }; 191 | int dys[] = { 1, -1, 1, -1 }; 192 | for(int i = 0; i != 4; ++i) 193 | { 194 | int nx = src_x + dxs[i], ny = src_y + dys[i]; 195 | while(check_coord_avail(nx, ny) && is_empty(nx, ny)) 196 | { 197 | if(nx == dest_x && ny == dest_y) 198 | { 199 | move_chess(); 200 | try_promote_king(status[nx][ny]); 201 | return { status[src_x][src_y], status[nx][ny] }; 202 | } 203 | 204 | nx += dxs[i], ny += dys[i]; 205 | } 206 | } 207 | } 208 | } 209 | 210 | // eating move 211 | DraughtsTrace trace; 212 | std::memset(mark, 0, sizeof(mark)); 213 | dfs_jump(0, src_x, src_y, status[src_x][src_y].is_king, player, 214 | [&](int step, DraughtsInfo* info, DraughtsInfo* eat) -> bool 215 | { 216 | if(step + 1 == (int)move_trace.size()) 217 | { 218 | for(int i = 0; i <= step; ++i) 219 | if(info[i].x != move_trace[i].first || info[i].y != move_trace[i].second) 220 | return false; 221 | for(int i = 0; i != step; ++i) 222 | { 223 | trace.push_back(info[i]); 224 | trace.push_back(eat[i]); 225 | status[eat[i].x][eat[i].y].set_empty(); 226 | } 227 | 228 | trace.push_back(info[step]); 229 | move_chess(); 230 | try_promote_king(status[dest_x][dest_y]); 231 | 232 | return true; 233 | } 234 | return false; 235 | } ); 236 | 237 | return trace; 238 | } 239 | 240 | bool Draughts::dfs_jump( 241 | int step, int x, int y, bool is_king, 242 | DraughtsInfo::Types type, 243 | std::function callback) 244 | { 245 | bool is_final = true; 246 | dfs_info[step] = DraughtsInfo(x, y); 247 | 248 | int dxs[] = { 1, 1, -1, -1 }; 249 | int dys[] = { 1, -1, 1, -1 }; 250 | for(int i = 0; i != 4; ++i) 251 | { 252 | if(!is_king) 253 | { 254 | int nx1 = x + dxs[i]; 255 | int ny1 = y + dys[i]; 256 | int nx2 = nx1 + dxs[i]; 257 | int ny2 = ny1 + dys[i]; 258 | if(!check_coord_avail(nx2, ny2) || mark[nx1][ny1]) 259 | continue; 260 | 261 | if(is_empty(nx2, ny2) && !is_empty(nx1, ny1) 262 | && status[nx1][ny1].type != type) 263 | { 264 | mark[nx1][ny1] = 1; 265 | eat_info[step] = get_info(nx1, ny1); 266 | if(dfs_jump(step + 1, nx2, ny2, is_king, type, callback)) 267 | return true; 268 | mark[nx1][ny1] = 0; 269 | is_final = false; 270 | } 271 | } else { 272 | // find first non-empty cell 273 | DraughtsInfo::Types first_meet = DraughtsInfo::empty; 274 | int nx = x, ny = y; 275 | do { 276 | nx += dxs[i], ny += dys[i]; 277 | if(!is_empty(nx, ny)) 278 | { 279 | first_meet = status[nx][ny].type; 280 | break; 281 | } 282 | } while(check_coord_avail(nx, ny)); 283 | 284 | // try to move 285 | if(first_meet != type && !mark[nx][ny]) 286 | { 287 | mark[nx][ny] = 1; 288 | eat_info[step] = get_info(nx, ny); 289 | 290 | int nx2 = nx + dxs[i], ny2 = ny + dys[i]; 291 | while(check_coord_avail(nx2, ny2) && is_empty(nx2, ny2)) 292 | { 293 | if(dfs_jump(step + 1, nx2, ny2, is_king, type, callback)) 294 | return true; 295 | nx2 += dxs[i], ny2 += dys[i]; 296 | is_final = false; 297 | } 298 | 299 | mark[nx][ny] = 0; 300 | } 301 | } 302 | } 303 | 304 | if(is_final) 305 | if(callback(step, dfs_info, eat_info)) 306 | return true; 307 | 308 | return false; 309 | } 310 | 311 | bool Draughts::check_coord_avail(int x, int y) const 312 | { 313 | return x >= 0 && y >= 0 && x < 10 && y < 10; 314 | } 315 | 316 | DraughtsInfo Draughts::get_info(int x, int y) const 317 | { 318 | if(check_coord_avail(x, y)) 319 | return status[x][y]; 320 | return {}; 321 | } 322 | -------------------------------------------------------------------------------- /draughts/draughts.h: -------------------------------------------------------------------------------- 1 | #ifndef DRAUGHTS_H 2 | #define DRAUGHTS_H 3 | 4 | #include 5 | #include 6 | #include 7 | using std::vector; 8 | using std::pair; 9 | 10 | struct DraughtsInfo 11 | { 12 | enum Types 13 | { 14 | empty, black, white 15 | }; 16 | 17 | bool is_king; 18 | int x, y; 19 | Types type; 20 | DraughtsInfo(int x = -1, int y = -1, Types type = empty, bool is_king = false) 21 | : is_king(is_king), x(x), y(y), type(type) {} 22 | 23 | void set_empty() { type = empty, is_king = false; } 24 | }; 25 | 26 | typedef vector DraughtsTrace; 27 | 28 | class Draughts 29 | { 30 | public: 31 | Draughts(); 32 | Draughts(const char* game); 33 | 34 | vector get_avail_chess(DraughtsInfo::Types player); 35 | pair> get_avail_move(int x, int y); 36 | DraughtsTrace move(const vector>&); 37 | DraughtsInfo get_info(int x, int y) const; 38 | bool is_empty(int x, int y) const; 39 | 40 | private: 41 | bool dfs_jump( 42 | int step, int x, int y, bool is_king, 43 | DraughtsInfo::Types type, 44 | std::function); 45 | bool check_coord_avail(int x, int y) const; 46 | 47 | private: 48 | DraughtsInfo status[10][10]; 49 | DraughtsInfo dfs_info[100], eat_info[100]; 50 | int mark[10][10]; 51 | }; 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /draughts/image/black-crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/image/black-crown.png -------------------------------------------------------------------------------- /draughts/image/black-piece.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/image/black-piece.png -------------------------------------------------------------------------------- /draughts/image/dark-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/image/dark-bg.png -------------------------------------------------------------------------------- /draughts/image/light-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/image/light-bg.png -------------------------------------------------------------------------------- /draughts/image/white-crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/image/white-crown.png -------------------------------------------------------------------------------- /draughts/image/white-piece.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/image/white-piece.png -------------------------------------------------------------------------------- /draughts/listen_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include "listen_dialog.h" 2 | #include "ui_listen_dialog.h" 3 | 4 | ListenDialog::ListenDialog(QWidget *parent) : 5 | QDialog(parent), 6 | ui(new Ui::ListenDialog) 7 | { 8 | ui->setupUi(this); 9 | ui->port_input->setValidator(new QIntValidator(1, 65536, this)); 10 | ui->ip_input->setFocus(); 11 | on_self_defined_game_toggled(false); 12 | } 13 | 14 | ListenDialog::~ListenDialog() 15 | { 16 | delete ui; 17 | } 18 | 19 | void ListenDialog::on_self_defined_game_toggled(bool checked) 20 | { 21 | ui->label_init_game->setVisible(checked); 22 | ui->init_game->setVisible(checked); 23 | ui->black_first->setVisible(checked); 24 | ui->white_first->setVisible(checked); 25 | } 26 | 27 | QString ListenDialog::getAddress() 28 | { 29 | return ui->ip_input->text(); 30 | } 31 | 32 | int ListenDialog::getPort() 33 | { 34 | return ui->port_input->text().toInt(); 35 | } 36 | 37 | bool ListenDialog::isUserDefined() 38 | { 39 | return ui->self_defined_game->isChecked(); 40 | } 41 | 42 | QString ListenDialog::getInitGameStatus() 43 | { 44 | return ui->init_game->text(); 45 | } 46 | 47 | bool ListenDialog::isBlackFirst() 48 | { 49 | return ui->black_first->isChecked(); 50 | } 51 | -------------------------------------------------------------------------------- /draughts/listen_dialog.h: -------------------------------------------------------------------------------- 1 | #ifndef LISTEN_DIALOG_H 2 | #define LISTEN_DIALOG_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class ListenDialog; 8 | } 9 | 10 | class ListenDialog : public QDialog 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit ListenDialog(QWidget *parent = 0); 16 | ~ListenDialog(); 17 | 18 | QString getAddress(); 19 | int getPort(); 20 | bool isUserDefined(); 21 | QString getInitGameStatus(); 22 | bool isBlackFirst(); 23 | 24 | private slots: 25 | void on_self_defined_game_toggled(bool checked); 26 | 27 | private: 28 | Ui::ListenDialog *ui; 29 | }; 30 | 31 | #endif // LISTEN_DIALOG_H 32 | -------------------------------------------------------------------------------- /draughts/listen_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ListenDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 30 20 | 240 21 | 341 22 | 32 23 | 24 | 25 | 26 | Qt::Horizontal 27 | 28 | 29 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 30 | 31 | 32 | 33 | 34 | 35 | 80 36 | 40 37 | 247 38 | 176 39 | 40 | 41 | 42 | 43 | 44 | 45 | IP Address 46 | 47 | 48 | 49 | 50 | 51 | 52 | 127.0.0.1 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 0 61 | 0 62 | 63 | 64 | 65 | Port 66 | 67 | 68 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 69 | 70 | 71 | 72 | 73 | 74 | 75 | 54808 76 | 77 | 78 | 79 | 80 | 81 | 82 | 000000000000b000w0w0000000000b00b0000000000w00000000b000000000000b000000b000000000000w00000000000000 83 | 84 | 85 | 86 | 87 | 88 | 89 | Init Game 90 | 91 | 92 | 93 | 94 | 95 | 96 | Self-Defined Game 97 | 98 | 99 | 100 | 101 | 102 | 103 | White First 104 | 105 | 106 | true 107 | 108 | 109 | 110 | 111 | 112 | 113 | Black First 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | buttonBox 124 | accepted() 125 | ListenDialog 126 | accept() 127 | 128 | 129 | 248 130 | 254 131 | 132 | 133 | 157 134 | 274 135 | 136 | 137 | 138 | 139 | buttonBox 140 | rejected() 141 | ListenDialog 142 | reject() 143 | 144 | 145 | 316 146 | 260 147 | 148 | 149 | 286 150 | 274 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /draughts/login_dialog.cpp: -------------------------------------------------------------------------------- 1 | #include "login_dialog.h" 2 | #include "ui_login_dialog.h" 3 | #include 4 | #include 5 | 6 | LoginDialog::LoginDialog(QWidget *parent) : 7 | QDialog(parent), 8 | ui(new Ui::LoginDialog) 9 | { 10 | ui->setupUi(this); 11 | ui->port_input->setValidator(new QIntValidator(1, 65536, this)); 12 | ui->ip_input->setFocus(); 13 | } 14 | 15 | QString LoginDialog::getAddress() 16 | { 17 | return ui->ip_input->text(); 18 | } 19 | 20 | int LoginDialog::getPort() 21 | { 22 | return ui->port_input->text().toInt(); 23 | } 24 | 25 | LoginDialog::~LoginDialog() 26 | { 27 | delete ui; 28 | } 29 | -------------------------------------------------------------------------------- /draughts/login_dialog.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGIN_DIALOG_H 2 | #define LOGIN_DIALOG_H 3 | 4 | #include 5 | 6 | namespace Ui { 7 | class LoginDialog; 8 | } 9 | 10 | class LoginDialog : public QDialog 11 | { 12 | Q_OBJECT 13 | 14 | public: 15 | explicit LoginDialog(QWidget *parent = 0); 16 | ~LoginDialog(); 17 | 18 | QString getAddress(); 19 | int getPort(); 20 | 21 | private: 22 | Ui::LoginDialog *ui; 23 | }; 24 | 25 | #endif // LOGIN_DIALOG_H 26 | -------------------------------------------------------------------------------- /draughts/login_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LoginDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 312 10 | 192 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Dialog 21 | 22 | 23 | 24 | 25 | 60 26 | 140 27 | 211 28 | 31 29 | 30 | 31 | 32 | Qt::Horizontal 33 | 34 | 35 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 36 | 37 | 38 | 39 | 40 | 41 | 50 42 | 50 43 | 196 44 | 60 45 | 46 | 47 | 48 | 49 | 50 | 51 | IP Address 52 | 53 | 54 | 55 | 56 | 57 | 58 | 127.0.0.1 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 0 67 | 0 68 | 69 | 70 | 71 | Port 72 | 73 | 74 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 75 | 76 | 77 | 78 | 79 | 80 | 81 | 54808 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | buttonBox 92 | accepted() 93 | LoginDialog 94 | accept() 95 | 96 | 97 | 248 98 | 254 99 | 100 | 101 | 157 102 | 274 103 | 104 | 105 | 106 | 107 | buttonBox 108 | rejected() 109 | LoginDialog 110 | reject() 111 | 112 | 113 | 316 114 | 260 115 | 116 | 117 | 286 118 | 274 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /draughts/main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include 3 | 4 | int main(int argc, char *argv[]) 5 | { 6 | QApplication a(argc, argv); 7 | MainWindow w; 8 | w.show(); 9 | 10 | return a.exec(); 11 | } 12 | -------------------------------------------------------------------------------- /draughts/mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include "ui_mainwindow.h" 3 | #include "login_dialog.h" 4 | #include "listen_dialog.h" 5 | 6 | MainWindow::MainWindow(QWidget *parent) : 7 | QMainWindow(parent), 8 | ui(new Ui::MainWindow) 9 | { 10 | ui->setupUi(this); 11 | 12 | rw_socket = nullptr; 13 | listen_socket = nullptr; 14 | 15 | initWidgets(); 16 | 17 | connect(ui->actionStart, SIGNAL(triggered(bool)), this, SLOT(startListening())); 18 | connect(ui->actionConnect, SIGNAL(triggered(bool)), this, SLOT(connectToServer())); 19 | } 20 | 21 | void MainWindow::initWidgets() 22 | { 23 | window = new QWidget; 24 | setCentralWidget(window); 25 | 26 | QVBoxLayout *layout = new QVBoxLayout(window); 27 | 28 | chess_board = new ChessBoard; 29 | layout->addWidget(chess_board); 30 | 31 | info_label = new QLabel; 32 | info_label->setMargin(0); 33 | ui->statusBar->addWidget(info_label); 34 | ui->statusBar->setSizeGripEnabled(false); 35 | 36 | connect(chess_board, SIGNAL(noAvailChess()), this, SLOT(gameEnd())); 37 | connect(chess_board, SIGNAL(playerMove(vector >)), 38 | this, SLOT(playerMove(vector >))); 39 | } 40 | 41 | void MainWindow::connectToServer() 42 | { 43 | LoginDialog dlg; 44 | if(dlg.exec() == QDialog::Accepted) 45 | { 46 | QHostAddress host_addr; 47 | if(host_addr.setAddress(dlg.getAddress())) 48 | { 49 | rw_socket = new QTcpSocket; 50 | rw_socket->connectToHost(host_addr, dlg.getPort()); 51 | info_label->setText("Connecting..."); 52 | connect(rw_socket, SIGNAL(connected()), this, SLOT(connectedToHost())); 53 | connect(rw_socket, SIGNAL(readyRead()), this, SLOT(recvMessage())); 54 | } else { 55 | QMessageBox::warning(this, "Error", "Error: Invalid address!"); 56 | } 57 | } 58 | } 59 | 60 | void MainWindow::playerMove(vector> move_trace) 61 | { 62 | QString text = QString("%1 %2") 63 | .arg(OPER_MOVE, 64 | QString::number(move_trace.size())); 65 | for(auto pos : move_trace) 66 | text += QString(" %1 %2") 67 | .arg(QString::number(pos.first), 68 | QString::number(pos.second)); 69 | rw_socket->write(text.toUtf8()); 70 | } 71 | 72 | void MainWindow::recvMessage() 73 | { 74 | QTextStream os(rw_socket); 75 | 76 | QString oper; 77 | os >> oper; 78 | if(oper == OPER_MOVE) 79 | { 80 | int trace_len; 81 | vector> move_trace; 82 | os >> trace_len; 83 | for(int i = 0; i != trace_len; ++i) 84 | { 85 | int x, y; 86 | os >> x >> y; 87 | move_trace.push_back({x, y}); 88 | } 89 | 90 | chess_board->moveChess(move_trace); 91 | } else if(oper == OPER_GIVEUP) { 92 | QMessageBox::information(this, "Game Over", "Congratulations! You win!"); 93 | chess_board->clearMarks(); 94 | } else if(oper == OPER_START) { 95 | chess_board->startGame(DraughtsInfo::white); 96 | } else if(oper == OPER_SETGAME) { 97 | QString first_player, game; 98 | os >> first_player >> game; 99 | Draughts draughts(game.toStdString().c_str()); 100 | chess_board->startGame(DraughtsInfo::white, 101 | first_player == "W" ? DraughtsInfo::white : DraughtsInfo::black, 102 | &draughts); 103 | } else if(oper == OPER_MAKEPEACE) { 104 | if(QMessageBox::question(this, "Make Peace?", 105 | "Your anamy wants to make peace, do you agree?", 106 | "No", "Yes") != QMessageBox::NoButton) 107 | { 108 | rw_socket->write(OPER_CONFIRMPEACE); 109 | peaceEnd(); 110 | } else rw_socket->write(OPER_DENIEDPEACE); 111 | } else if(oper == OPER_CONFIRMPEACE) { 112 | peaceEnd(); 113 | } else if(oper == OPER_DENIEDPEACE) { 114 | QMessageBox::information(this, "Make Peace", "Your anamy disagrees to make peace with you!"); 115 | } 116 | } 117 | 118 | void MainWindow::connectedToHost() 119 | { 120 | info_label->setText("Connected to " + rw_socket->peerAddress().toString() 121 | + ", port " + QString::number(rw_socket->peerPort())); 122 | // game starts when received OPER_START message 123 | } 124 | 125 | void MainWindow::acceptConnection() 126 | { 127 | rw_socket = listen_socket->nextPendingConnection(); 128 | connect(rw_socket, SIGNAL(readyRead()), this, SLOT(recvMessage())); 129 | 130 | info_label->setText("Connected with " + rw_socket->peerAddress().toString() 131 | + ", port " + QString::number(rw_socket->peerPort())); 132 | 133 | if(is_self_defined_game) 134 | { 135 | Draughts draughts(self_defined_game.toStdString().c_str()); 136 | chess_board->startGame(DraughtsInfo::black, 137 | is_black_first ? DraughtsInfo::black : DraughtsInfo::white, 138 | &draughts); 139 | QString text = OPER_SETGAME; 140 | text += is_black_first ? " B " : " W "; 141 | text += self_defined_game; 142 | rw_socket->write(text.toUtf8()); 143 | } else { 144 | chess_board->startGame(DraughtsInfo::black); 145 | rw_socket->write(OPER_START); 146 | } 147 | } 148 | 149 | void MainWindow::startListening() 150 | { 151 | ListenDialog dlg; 152 | if(dlg.exec() != QDialog::Accepted) 153 | return; 154 | 155 | QHostAddress listen_addr; 156 | if(!listen_addr.setAddress(dlg.getAddress())) 157 | { 158 | QMessageBox::warning(this, "Error", "Error: Invalid IP address."); 159 | return; 160 | } 161 | 162 | int listen_port = dlg.getPort(); 163 | is_self_defined_game = dlg.isUserDefined(); 164 | self_defined_game = dlg.getInitGameStatus(); 165 | is_black_first = dlg.isBlackFirst(); 166 | 167 | listen_socket = new QTcpServer; 168 | if(listen_socket->listen(listen_addr, listen_port)) 169 | { 170 | connect(listen_socket, SIGNAL(newConnection()), this, SLOT(acceptConnection())); 171 | 172 | info_label->setText("Listening on " + listen_addr.toString() 173 | + ", port " + QString::number(listen_socket->serverPort())); 174 | 175 | for(const QHostAddress& address : QNetworkInterface::allAddresses()) 176 | { 177 | if(address.protocol() == QAbstractSocket::IPv4Protocol 178 | && address != QHostAddress(QHostAddress::LocalHost)) 179 | info_label->setText("Listening on " + listen_addr.toString() 180 | + ", port " + QString::number(listen_socket->serverPort()) 181 | + ". Server: " + address.toString()); 182 | } 183 | } else { 184 | QMessageBox::warning(this, "Error", "Error: " + listen_socket->errorString()); 185 | delete listen_socket; 186 | } 187 | } 188 | 189 | void MainWindow::gameEnd() 190 | { 191 | QMessageBox *msg_box = new QMessageBox(this); 192 | msg_box->setModal(false); 193 | msg_box->setStandardButtons(QMessageBox::Ok); 194 | msg_box->setWindowTitle("Game Over"); 195 | if(chess_board->getCurrentPlayer() == chess_board->getPlayer()) 196 | msg_box->setText("You lost!"); 197 | else msg_box->setText("Congratulations! You win!"); 198 | msg_box->show(); 199 | } 200 | 201 | void MainWindow::peaceEnd() 202 | { 203 | chess_board->clearMarks(); 204 | QMessageBox::information(this, "Game Over", "Peace!"); 205 | } 206 | 207 | MainWindow::~MainWindow() 208 | { 209 | delete ui; 210 | } 211 | 212 | void MainWindow::on_actionGive_Up_triggered() 213 | { 214 | if(rw_socket) 215 | { 216 | rw_socket->write(OPER_GIVEUP); 217 | QMessageBox::information(this, "Game Over", "You lost!"); 218 | } 219 | } 220 | 221 | void MainWindow::on_actionMake_Peace_triggered() 222 | { 223 | if(rw_socket) 224 | { 225 | rw_socket->write(OPER_MAKEPEACE); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /draughts/mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "chessboard.h" 8 | #include "config.h" 9 | 10 | namespace Ui { 11 | class MainWindow; 12 | } 13 | 14 | class MainWindow : public QMainWindow 15 | { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit MainWindow(QWidget *parent = 0); 20 | ~MainWindow(); 21 | 22 | void initWidgets(); 23 | 24 | public slots: 25 | void connectToServer(); 26 | void startListening(); 27 | 28 | private slots: 29 | void connectedToHost(); 30 | void acceptConnection(); 31 | void recvMessage(); 32 | void playerMove(vector>); 33 | void gameEnd(); 34 | void peaceEnd(); 35 | 36 | void on_actionGive_Up_triggered(); 37 | 38 | void on_actionMake_Peace_triggered(); 39 | 40 | private: 41 | Ui::MainWindow *ui; 42 | QWidget *window; 43 | QLabel *info_label; 44 | ChessBoard *chess_board; 45 | 46 | // the following three variables are used for user-defined game 47 | bool is_self_defined_game; 48 | QString self_defined_game; 49 | bool is_black_first; 50 | 51 | QTcpServer *listen_socket; 52 | QTcpSocket *rw_socket; 53 | }; 54 | 55 | #endif // MAINWINDOW_H 56 | -------------------------------------------------------------------------------- /draughts/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Draughts 15 | 16 | 17 | 18 | 19 | 20 | 0 21 | 0 22 | 400 23 | 20 24 | 25 | 26 | 27 | 28 | Game 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Start 41 | 42 | 43 | 44 | 45 | Connect 46 | 47 | 48 | 49 | 50 | Give Up 51 | 52 | 53 | 54 | 55 | Make Peace 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /draughts/res.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | image/dark-bg.png 4 | image/light-bg.png 5 | image/white-piece.png 6 | image/black-piece.png 7 | image/white-crown.png 8 | image/black-crown.png 9 | sound/sound.wav 10 | 11 | 12 | -------------------------------------------------------------------------------- /draughts/sound/sound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/draughts/sound/sound.wav -------------------------------------------------------------------------------- /sudoku/action_queue.cpp: -------------------------------------------------------------------------------- 1 | #include "action_queue.h" 2 | 3 | ActionQueue::ActionQueue(int max_queue) 4 | : max_queue(max_queue), cur_pos(0) 5 | { 6 | } 7 | 8 | void ActionQueue::add_action( 9 | int row, int col, 10 | bool value_settled_old, IntList candidates_old, 11 | bool value_settled_new, IntList candidates_new) 12 | { 13 | ActionInfo action; 14 | action.row = row; 15 | action.col = col; 16 | action.candidates_new = candidates_new; 17 | action.candidates_old = candidates_old; 18 | action.value_settled_new = value_settled_new; 19 | action.value_settled_old = value_settled_old; 20 | 21 | actions.erase(actions.begin() + cur_pos, actions.end()); 22 | actions.push_back(action); 23 | if(++cur_pos > max_queue) 24 | { 25 | cur_pos = max_queue; 26 | actions.pop_front(); 27 | } 28 | } 29 | 30 | ActionInfo ActionQueue::forward() 31 | { 32 | if(is_forwardable()) 33 | return actions[cur_pos++]; 34 | return ActionInfo(); 35 | } 36 | 37 | ActionInfo ActionQueue::backward() 38 | { 39 | if(is_backwardable()) 40 | return actions[--cur_pos]; 41 | return ActionInfo(); 42 | } 43 | 44 | bool ActionQueue::is_forwardable() const 45 | { 46 | return (unsigned)cur_pos < actions.size(); 47 | } 48 | 49 | bool ActionQueue::is_backwardable() const 50 | { 51 | return cur_pos > 0; 52 | } 53 | 54 | void ActionQueue::reset() 55 | { 56 | cur_pos = 0; 57 | actions.clear(); 58 | } 59 | -------------------------------------------------------------------------------- /sudoku/action_queue.h: -------------------------------------------------------------------------------- 1 | #ifndef ACTION_QUEUE_H 2 | #define ACTION_QUEUE_H 3 | 4 | #include 5 | #include "utils.h" 6 | 7 | struct ActionInfo 8 | { 9 | int row, col; 10 | bool value_settled_old, value_settled_new; 11 | IntList candidates_old, candidates_new; 12 | 13 | ActionInfo() : row(-1), col(-1) {} 14 | }; 15 | 16 | class ActionQueue 17 | { 18 | public: 19 | ActionQueue(int max_queue); 20 | void add_action( 21 | int row, int col, 22 | bool value_settled_old, IntList candidates_old, 23 | bool value_settled_new, IntList candidates_new 24 | ); 25 | ActionInfo forward(); 26 | ActionInfo backward(); 27 | bool is_forwardable() const; 28 | bool is_backwardable() const; 29 | void reset(); 30 | private: 31 | int max_queue, cur_pos; 32 | std::deque actions; 33 | }; 34 | 35 | #endif // ACTION_QUEUE_H 36 | -------------------------------------------------------------------------------- /sudoku/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #define CELL_SIZE 4 5 | #define FIXED_SIZE 50 6 | #define TOOL_FIXED_SIZE 30 7 | 8 | #define DEFAULT_TEXT_COLOR "#222" 9 | #define DEFAULT_BG_COLOR "white" 10 | #define FILLED_TEXT_COLOR "red" 11 | #define SELECTED_BG_COLOR "#eee" 12 | #define GRID_BG_COLOR "#777" 13 | 14 | #define PRESSED_BG_COLOR "#eee" 15 | #define HOVER_BG_COLOR "#eee" 16 | 17 | #define CELL_SPACING 1 18 | #define GRID_SPACING 2 19 | 20 | #define ACTIONS_MAX_SIZE 50 21 | 22 | #endif // CONFIG_H 23 | -------------------------------------------------------------------------------- /sudoku/dancing_link.cpp: -------------------------------------------------------------------------------- 1 | #include "dancing_link.h" 2 | 3 | void DancingLink::init(int row_num, int col_num) 4 | { 5 | this->row_num = row_num; 6 | this->col_num = col_num; 7 | 8 | int node_num = (row_num + 1) * (col_num + 1) + col_num + 2; 9 | column_node.assign(col_num + 1, 0); 10 | belong_row.assign(node_num, 0); 11 | belong_col.assign(node_num, 0); 12 | L.assign(node_num, 0); R.assign(node_num, 0); 13 | U.assign(node_num, 0); D.assign(node_num, 0); 14 | 15 | row_now = 1; 16 | node_now = col_num + 1; 17 | for(int i = 0; i <= col_num; ++i) 18 | { 19 | L[i] = i - 1; 20 | R[i] = i + 1; 21 | U[i] = D[i] = i; 22 | } 23 | 24 | L[0] = col_num; 25 | R[col_num] = 0; 26 | } 27 | 28 | int DancingLink::append_row(const IntList& cols) 29 | { 30 | int first_node_now = node_now; 31 | for(int col : cols) 32 | { 33 | belong_row[node_now] = row_now; 34 | belong_col[node_now] = col; 35 | L[node_now] = node_now - 1; 36 | R[node_now] = node_now + 1; 37 | D[node_now] = col; 38 | U[node_now] = U[col]; 39 | D[U[col]] = node_now; 40 | U[col] = node_now; 41 | ++column_node[col]; 42 | ++node_now; 43 | } 44 | 45 | L[first_node_now] = node_now - 1; 46 | R[node_now - 1] = first_node_now; 47 | return row_now++; 48 | } 49 | 50 | void DancingLink::remove(int col) 51 | { 52 | L[R[col]] = L[col]; 53 | R[L[col]] = R[col]; 54 | for(int i = D[col]; i != col; i = D[i]) 55 | { 56 | for(int j = R[i]; j != i; j = R[j]) 57 | { 58 | U[D[j]] = U[j]; 59 | D[U[j]] = D[j]; 60 | --column_node[belong_col[j]]; 61 | } 62 | } 63 | } 64 | 65 | void DancingLink::restore(int col) 66 | { 67 | for(int i = U[col]; i != col; i = U[i]) 68 | { 69 | for(int j = L[i]; j != i; j = L[j]) 70 | { 71 | U[D[j]] = j; 72 | D[U[j]] = j; 73 | ++column_node[belong_col[j]]; 74 | } 75 | } 76 | 77 | L[R[col]] = col; 78 | R[L[col]] = col; 79 | } 80 | 81 | int DancingLink::dfs(int level, const std::function& callback) 82 | { 83 | if(R[0] == 0) 84 | { 85 | answer_num = level; 86 | return callback(); 87 | } 88 | 89 | int min_node_in_col = R[0]; 90 | for(int i = R[0]; i != 0; i = R[i]) 91 | { 92 | if(column_node[i] < column_node[min_node_in_col]) 93 | min_node_in_col = i; 94 | } 95 | 96 | remove(min_node_in_col); 97 | 98 | int m = min_node_in_col; 99 | for(int i = D[m]; i != m; i = D[i]) 100 | { 101 | answer[level] = belong_row[i]; 102 | for(int k = R[i]; k != i; k = R[k]) 103 | remove(belong_col[k]); 104 | if(dfs(level + 1, callback)) return 1; 105 | for(int k = L[i]; k != i; k = L[k]) 106 | restore(belong_col[k]); 107 | } 108 | 109 | restore(min_node_in_col); 110 | return 0; 111 | } 112 | 113 | IntList DancingLink::solve() 114 | { 115 | answer_num = 0; 116 | answer.assign(row_num + 1, 0); 117 | if(!dfs(0, []() { return true; })) return {}; 118 | return IntList(answer.begin(), answer.begin() + answer_num); 119 | } 120 | 121 | bool DancingLink::solve_unique(IntList& ans) 122 | { 123 | int count = 0; 124 | answer_num = 0; 125 | answer.assign(row_num + 1, 0); 126 | dfs(0, [&]() { return ++count > 1; }); 127 | if(count == 0) 128 | { 129 | ans = {}; 130 | return false; 131 | } else { 132 | ans = IntList(answer.begin(), answer.begin() + answer_num); 133 | return count == 1; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /sudoku/dancing_link.h: -------------------------------------------------------------------------------- 1 | #ifndef DANCING_LINK_H 2 | #define DANCING_LINK_H 3 | #include "utils.h" 4 | #include 5 | 6 | class DancingLink 7 | { 8 | int col_num, row_num; 9 | int node_now, row_now; 10 | IntList column_node; 11 | 12 | IntList L, R, U, D; 13 | IntList belong_row, belong_col; 14 | 15 | IntList answer; 16 | int answer_num; 17 | public: 18 | DancingLink(int row_num, int col_num) { init(row_num, col_num); } 19 | 20 | void init(int row_num, int col_num); 21 | // cols must be sorted ascending 22 | int append_row(const IntList& cols); 23 | IntList solve(); 24 | bool solve_unique(IntList&); 25 | private: 26 | void remove(int col); 27 | void restore(int col); 28 | int dfs(int level, const std::function& callback); 29 | }; 30 | 31 | #endif // DANCING_LINK_H 32 | -------------------------------------------------------------------------------- /sudoku/digit_button.cpp: -------------------------------------------------------------------------------- 1 | #include "digit_button.h" 2 | #include "utils.h" 3 | 4 | DigitButton::DigitButton(QWidget *parent) 5 | : QPushButton(parent) 6 | { 7 | setFocusPolicy(Qt::NoFocus); 8 | } 9 | 10 | void DigitButton::mouseReleaseEvent(QMouseEvent *e) 11 | { 12 | // e->button() == Qt::LeftButton is included in `else` 13 | if(e->button() == Qt::RightButton) { 14 | emit right_clicked(); 15 | } else QPushButton::mouseReleaseEvent(e); 16 | } 17 | 18 | void DigitButton::update_font() 19 | { 20 | QString text = this->text(); 21 | QRect rect_lbl = geometry().adjusted(5, 5, -5, -5); 22 | QFont font = this->font(); 23 | 24 | font = fit_font_with_text(font, text, rect_lbl); 25 | 26 | setFont(font); 27 | } 28 | 29 | void DigitButton::update_style() 30 | { 31 | if(is_checked()) 32 | { 33 | setStyleSheet("font-weight: 900; font-style: italic;"); 34 | } else { 35 | setStyleSheet("font-weight: normal; font-style: normal;"); 36 | } 37 | } 38 | 39 | void DigitButton::toggle() 40 | { 41 | set_checked(!checked); 42 | } 43 | 44 | bool DigitButton::is_checked() const 45 | { 46 | return checked; 47 | } 48 | 49 | void DigitButton::set_checked(bool checked) 50 | { 51 | this->checked = checked; 52 | update_style(); 53 | } 54 | -------------------------------------------------------------------------------- /sudoku/digit_button.h: -------------------------------------------------------------------------------- 1 | #ifndef DIGITBUTTON_H 2 | #define DIGITBUTTON_H 3 | 4 | #include 5 | 6 | class DigitButton : public QPushButton 7 | { 8 | Q_OBJECT 9 | public: 10 | explicit DigitButton(QWidget *parent = 0); 11 | 12 | void update_font(); 13 | void toggle(); 14 | bool is_checked() const; 15 | void set_checked(bool); 16 | 17 | signals: 18 | void right_clicked(); 19 | 20 | public slots: 21 | 22 | private: 23 | void update_style(); 24 | 25 | protected: 26 | void mouseReleaseEvent(QMouseEvent *); 27 | 28 | private: 29 | bool checked; 30 | }; 31 | 32 | #endif // DIGITBUTTON_H 33 | -------------------------------------------------------------------------------- /sudoku/doc/doc.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/doc/doc.pdf -------------------------------------------------------------------------------- /sudoku/doc/doc.tex: -------------------------------------------------------------------------------- 1 | \XeTeXlinebreaklocale "zh" 2 | \XeTeXlinebreakskip = 0pt plus 1pt 3 | 4 | \documentclass[11pt,a4paper]{article} 5 | 6 | \usepackage{xltxtra,fontspec,xunicode} 7 | \usepackage{amsthm, amsmath, amssymb, amsfonts} 8 | \usepackage{abstract} 9 | \usepackage{subcaption} 10 | \usepackage{graphicx,float} 11 | \usepackage{minted} 12 | 13 | \defaultfontfeatures{Mapping=tex-text,Scale=MatchLowercase} 14 | \setmainfont{DejaVu Serif} 15 | \setsansfont{DejaVu Sans} 16 | \setmonofont{DejaVu Sans Mono} 17 | \usepackage[slantfont,boldfont]{xeCJK} % 允许斜体和粗体 18 | \setCJKmainfont{WenQuanYi Zen Hei} 19 | \setCJKsansfont{WenQuanYi Zen Hei} 20 | \setCJKmonofont{WenQuanYi Zen Hei Mono} 21 | 22 | \usepackage{titling} 23 | 24 | \renewcommand\refname{参考文献} 25 | 26 | \title{Sudoku的功能及其实现} 27 | \author{miskcoo} 28 | 29 | \begin{document} 30 | \maketitle 31 | \section{简介} 32 | Sudoku是一款利用Qt实现的数独游戏,提供了多达10个难度的关卡选择,同时还有丰富的功能来帮助玩家更加高效地求解数独问题,例如候选数、高亮相同数字、高亮选中的行列、撤销当前操作以及提示等功能。玩家还可以手动输入数独题目利用Sudoku帮助求解。 33 | 34 | 除了传统$9 \times 9$的数独游戏以外,还提供了更高难度的$16 \times 16$的数独游戏。 35 | 36 | \begin{figure}[H] 37 | \centering 38 | \begin{subfigure}{.45\textwidth} 39 | \centering 40 | \includegraphics[width=\linewidth]{sudoku-3a.png} 41 | \end{subfigure} 42 | \hfill 43 | \begin{subfigure}{.45\textwidth} 44 | \centering 45 | \includegraphics[width=\linewidth]{sudoku-4a.png} 46 | \end{subfigure} 47 | \caption{传统的$9\times 9$数独以及高难度的$16\times 16$数独} 48 | \end{figure} 49 | 50 | \section{功能} 51 | Sudoku提供了多个方便的按钮: 52 | \begin{itemize} 53 | \item \textbf{新游戏}:玩家可以开始一局新的游戏。 54 | \item \textbf{重玩}:玩家可以重新开始本局游戏。 55 | \item \textbf{暂停}:玩家可以暂停该局游戏(即暂停计时)。 56 | \item \textbf{提示}:如果当前已经确定的数都是正确的,玩家将会得到一个未填空格的正确数字;如果当前已经确定的数和答案矛盾,导致整个数独无解,那么所有与答案矛盾的数字将会被粗体标出。 57 | \item \textbf{清除}:清除当前选中格子的所有数字。 58 | \item \textbf{撤销}:撤销前一步的操作,以及取消撤销(最多可支持50步撤销)。 59 | \end{itemize} 60 | 61 | 同时可以通过菜单来实现多达10种难度的游戏选择,可以求解任意用户输入的数独问题。 62 | 63 | \begin{figure}[H] 64 | \centering 65 | \begin{subfigure}{.45\textwidth} 66 | \centering 67 | \includegraphics[width=\linewidth]{sudoku-3b.png} 68 | \caption{最低难度的数独} \label{fig:sudoku-3b} 69 | \end{subfigure} 70 | \hfill 71 | \begin{subfigure}{.45\textwidth} 72 | \centering 73 | \includegraphics[width=\linewidth]{sudoku-3c.png} 74 | \caption{最高难度的数独} \label{fig:sudoku-3c} 75 | \end{subfigure} 76 | 77 | \vspace{0.5cm} 78 | \begin{subfigure}{.45\textwidth} 79 | \centering 80 | \includegraphics[width=\linewidth]{sudoku-4c.png} 81 | \caption{一个玩家任意输入的游戏} \label{fig:sudoku-4c} 82 | \end{subfigure} 83 | \hfill 84 | \begin{subfigure}{.45\textwidth} 85 | \centering 86 | \includegraphics[width=\linewidth]{sudoku-4d.png} 87 | \caption{利用算法计算出的答案} \label{fig:sudoku-4d} 88 | \end{subfigure} 89 | \caption{(\subref{fig:sudoku-3b}, \subref{fig:sudoku-3c})上方的按钮从左到右分别为:新游戏、重完、暂停/开始、提示、清除、撤销和取消撤销。黑色的数字表示初始时题目所给出的数字,大号红色数字表示用户已经确定的数字,小号红色数字表示候选的数字。当前选中的格子以及与其所在相同行或列的格子被高亮显示,同时右侧数字栏中已经选择的数字被加粗显示,可以方便进行操作。另外,对于已经确定的数字,其余与其相同并且也已经确定的数字会被加粗显示,如(\subref{fig:sudoku-3c})。玩家对于重要的数字可以进行标记,标记为在格子左上角打上一个三角(例如(\subref{fig:sudoku-3b})中的数字3)。} 90 | \end{figure} 91 | 92 | 玩家在空格中填入的数字分为确定数(采用正常大小的字体表示)以及候选数(采用小号字体表示)。确定数只能存在一个,候选数可以存在多个,并且确定数和候选数不能同时存在。 93 | 94 | 玩家可以通过多种方式进行格子的选择: 95 | \begin{itemize} 96 | \item 通过鼠标直接点击格子进行选中。 97 | \item 通过键盘方向键来进行当前选中格子的切换。 98 | \item 通过Tab键快速切换到下一个格子。 99 | \end{itemize} 100 | 101 | 玩家可以用鼠标右键点击格子来对其进行标记。 102 | 103 | 玩家在选中一个格子之后,如果该格子的数字已经确定,那么所有与该数相同的数将会被加粗显示,以方便确认是否满足数独的条件。同时,所有与该格在相同行或列的格子都会被高亮显示。此外,无论该格子数字确定与否,右侧数字列表中该格子的所有数都会被加粗。 104 | 105 | 玩家可以通过多种方式在空格中填入数字: 106 | \begin{itemize} 107 | \item 通过键盘直接输入数字将填入对应的确定数,如果Ctrl键被按下,那么填入的将会是候选数。 108 | \item 通过鼠标点击右侧数字列表填入。使用鼠标左键点击将会填入确定数,右键点击则会填入候选数。 109 | \end{itemize} 110 | 111 | 玩家也可以通过多种方式在空格中填入数字: 112 | \begin{itemize} 113 | \item 通过键盘删除键删除格子中的一个数字。 114 | \item 通过鼠标点击右侧数字列表已经选中的数进行删除。 115 | \item 通过“清除”功能键来清除该格子所有的数字。 116 | \end{itemize} 117 | \section{难度选择算法} 118 | 我们认为一个数独游戏的难度可以根据行列空格数的最大值以及给定数字的数量来确定。行列空格数的最大值越小玩家拥有的信息量就越多,同样给定数字越多玩家也能获得更多的信息。 119 | 120 | 在我们的难度中,最简单的关卡给出的数字至少有50个,并且行列空格数不会超过4个。然而,在最困难的关卡中,最少只会给出20个数字,并且可能会有某些行或列全部是空格。在最简单和最困难中间这两个影响难度的因素平滑过渡。 121 | 122 | 为了生成一个满足条件的数独,可以按照以下步骤来实现: 123 | 124 | \begin{enumerate} 125 | \item 在空白棋盘随机填入数十个数。 126 | \item 利用求解算法获得一个合法解,如果不存在合法解,转到1。 127 | \item 生成一个随机的格子排列。 128 | \item 按照这个序列来尝试一个个删除所填入的数字,如果删除后能保证解唯一并且满足之前的条件,那么就删除,否则不删除。 129 | \item 如果删除了足够的数字,则返回。否则跳到1。 130 | \end{enumerate} 131 | 132 | 为了实现快速的数独问题求解,我们利用Dancing Link来优化搜索。 133 | \section{GUI部分实现细节} 134 | 主要的游戏棋盘是通过$3\times 3$的QGridLayout,每个QGridLayout里再套一个$3\times 3$的QGridLayout来实现的。 135 | 136 | 功能按钮的鼠标移入以及点击的效果是通过QPainter::drawImage里图片的混合选项来实现的,通过多次混合以及不同的混合参数就可以实现颜色的深浅变化: 137 | 138 | \begin{minted}{c++} 139 | void ToolButton::paintEvent(QPaintEvent *) 140 | { 141 | QPainter p(this); 142 | QPixmap bg(image_path); 143 | p.drawPixmap(QPoint(0, 0), bg); 144 | if(is_mouse_pressed) 145 | { 146 | p.setCompositionMode(QPainter::CompositionMode_Multiply); 147 | p.drawPixmap(QPoint(0, 0), bg); 148 | } else if(is_mouse_over) { 149 | p.setCompositionMode(QPainter::CompositionMode_HardLight); 150 | p.drawPixmap(QPoint(0, 0), bg); 151 | } 152 | } 153 | \end{minted} 154 | 155 | 另外,关于窗口大小的固定,由于在切换数独棋盘大小时窗口大小也会随之改变,可以利用{\it minimumSizeHint()}来直接进行计算: 156 | 157 | \begin{minted}{c++} 158 | setFixedSize(window->minimumSizeHint() 159 | + ui->menuBar->minimumSizeHint()); 160 | \end{minted} 161 | \end{document} 162 | -------------------------------------------------------------------------------- /sudoku/doc/sudoku-3a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/doc/sudoku-3a.png -------------------------------------------------------------------------------- /sudoku/doc/sudoku-3b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/doc/sudoku-3b.png -------------------------------------------------------------------------------- /sudoku/doc/sudoku-3c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/doc/sudoku-3c.png -------------------------------------------------------------------------------- /sudoku/doc/sudoku-4a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/doc/sudoku-4a.png -------------------------------------------------------------------------------- /sudoku/doc/sudoku-4b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/doc/sudoku-4b.png -------------------------------------------------------------------------------- /sudoku/doc/sudoku-4c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/doc/sudoku-4c.png -------------------------------------------------------------------------------- /sudoku/doc/sudoku-4d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/doc/sudoku-4d.png -------------------------------------------------------------------------------- /sudoku/icons/alarm-clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/alarm-clock.png -------------------------------------------------------------------------------- /sudoku/icons/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/back.png -------------------------------------------------------------------------------- /sudoku/icons/eraser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/eraser.png -------------------------------------------------------------------------------- /sudoku/icons/information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/information.png -------------------------------------------------------------------------------- /sudoku/icons/license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | User License 6 | 7 | 14 | 15 | 71 | 72 | 73 |
74 |
75 | 76 |
77 |
78 |

Thank you!

79 |

Here you have your license!

80 | 81 |

82 | If you are not redirected automatically, please click this link to get your license 83 |

84 |
85 |
86 | 89 | 90 | -------------------------------------------------------------------------------- /sudoku/icons/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/new.png -------------------------------------------------------------------------------- /sudoku/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/next.png -------------------------------------------------------------------------------- /sudoku/icons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/pause.png -------------------------------------------------------------------------------- /sudoku/icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/play.png -------------------------------------------------------------------------------- /sudoku/icons/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/question.png -------------------------------------------------------------------------------- /sudoku/icons/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/sudoku/icons/restart.png -------------------------------------------------------------------------------- /sudoku/main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include 3 | #include 4 | #include 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | std::srand(std::time(0)); 9 | QApplication a(argc, argv); 10 | MainWindow w; 11 | w.show(); 12 | 13 | return a.exec(); 14 | } 15 | -------------------------------------------------------------------------------- /sudoku/mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include "ui_mainwindow.h" 3 | #include "sudoku_grid.h" 4 | #include "config.h" 5 | 6 | MainWindow::MainWindow(QMainWindow *parent) : 7 | QMainWindow(parent), 8 | ui(new Ui::MainWindow), 9 | is_paused(false) 10 | { 11 | ui->setupUi(this); 12 | init_widgets(3); 13 | init_menu(); 14 | } 15 | 16 | void MainWindow::init_menu() 17 | { 18 | // add menu actions 19 | QSignalMapper *m_level = new QSignalMapper(this); 20 | QActionGroup *level_group = new QActionGroup(this); 21 | for(int i = SUDOKU_LEVEL_MIN; i <= SUDOKU_LEVEL_MAX; ++i) 22 | { 23 | QAction *level_action = new QAction("Level " + QString::number(i)); 24 | level_action->setCheckable(true); 25 | if(i == SUDOKU_LEVEL_MIN) 26 | level_action->setChecked(true); 27 | 28 | m_level->setMapping(level_action, i); 29 | connect(level_action, SIGNAL(triggered()), m_level, SLOT(map())); 30 | 31 | level_group->addAction(level_action); 32 | ui->level_menu->addAction(level_action); 33 | } 34 | 35 | QAction *level_empty = new QAction("Level Empty"); 36 | m_level->setMapping(level_empty, 0); 37 | connect(level_empty, SIGNAL(triggered()), m_level, SLOT(map())); 38 | level_group->addAction(level_empty); 39 | ui->level_menu->addAction(level_empty); 40 | 41 | connect(m_level, SIGNAL(mapped(int)), this, SLOT(level_changed(int))); 42 | 43 | QSignalMapper *m_csize = new QSignalMapper(this); 44 | QActionGroup *csize_group = new QActionGroup(this); 45 | for(int i = 3; i <= 4; ++i) 46 | { 47 | QAction *csize_action = new QAction(QString::number(i)); 48 | csize_action->setCheckable(true); 49 | if(i == 3) 50 | csize_action->setChecked(true); 51 | 52 | m_csize->setMapping(csize_action, i); 53 | connect(csize_action, SIGNAL(triggered()), m_csize, SLOT(map())); 54 | 55 | csize_group->addAction(csize_action); 56 | ui->grid_size_menu->addAction(csize_action); 57 | } 58 | 59 | connect(m_csize, SIGNAL(mapped(int)), this, SLOT(change_cell_size(int))); 60 | } 61 | 62 | void MainWindow::init_widgets(int cell_size) 63 | { 64 | int fixed_size = FIXED_SIZE; 65 | int cell_span = cell_size * cell_size; 66 | 67 | window = new QWidget; 68 | setCentralWidget(window); 69 | 70 | layout = new QVBoxLayout(window); 71 | top_layout = new QHBoxLayout; 72 | bottom_layout = new QHBoxLayout; 73 | 74 | layout->addLayout(top_layout); 75 | layout->addLayout(bottom_layout); 76 | 77 | grid = new SudokuGrid(cell_size, fixed_size); 78 | bottom_layout->addWidget(grid); 79 | 80 | // init digit button layout 81 | QLabel *digit_layout_wrap = new QLabel; 82 | int size = (fixed_size + 1) * cell_span + 2 * (GRID_SPACING - 1) + 1; 83 | digit_layout_wrap->setFixedSize(fixed_size + GRID_SPACING * 2, size); 84 | digit_layout_wrap->setStyleSheet( 85 | "QLabel { background-color: " GRID_BG_COLOR ";}" 86 | "QPushButton{ " 87 | "border:none;" 88 | "background-color: " DEFAULT_BG_COLOR "; }" 89 | "QPushButton:pressed {" 90 | "border:none;" 91 | "background-color: " PRESSED_BG_COLOR "; }" 92 | "QPushButton:hover:!pressed {" 93 | "border:none;" 94 | "background-color: " HOVER_BG_COLOR "; }" 95 | ); 96 | 97 | digit_button_layout = new QGridLayout(digit_layout_wrap); 98 | digit_button_layout->setMargin(GRID_SPACING); 99 | digit_button_layout->setSpacing(CELL_SPACING); 100 | 101 | bottom_layout->setSpacing(15); 102 | bottom_layout->addWidget(digit_layout_wrap); 103 | bottom_layout->setContentsMargins(8, 0, 8, 8); 104 | 105 | QSignalMapper *m_r = new QSignalMapper(this); 106 | QSignalMapper *m_l = new QSignalMapper(this); 107 | for(int i = 1; i <= cell_span; ++i) 108 | { 109 | DigitButton *btn = new DigitButton; 110 | 111 | btn->setText(QString::number(i)); 112 | btn->setFixedSize(fixed_size, fixed_size); 113 | btn->update_font(); 114 | 115 | m_l->setMapping(btn, i); 116 | m_r->setMapping(btn, i); 117 | connect(btn, SIGNAL(clicked()), m_l, SLOT(map())); 118 | connect(btn, SIGNAL(right_clicked()), m_r, SLOT(map())); 119 | 120 | digit_button_layout->addWidget(btn, i - 1, 0); 121 | digit_btns.push_back(btn); 122 | } 123 | 124 | connect(m_r, SIGNAL(mapped(int)), this, SLOT(digit_add_value(int))); 125 | connect(m_l, SIGNAL(mapped(int)), this, SLOT(digit_set_value(int))); 126 | 127 | QHBoxLayout *timer_layout = new QHBoxLayout; 128 | timer_layout->setContentsMargins(15, 5, 15, 5); 129 | timer = new Timer; 130 | ToolButton *clock_img = new ToolButton; 131 | clock_img->set_image(":/icons/icons/alarm-clock.png"); 132 | clock_img->setFixedSize(18, 18); 133 | clock_img->setEnabled(false); 134 | 135 | start_btn = new ToolButton; 136 | start_btn->set_image(":/icons/icons/new.png"); 137 | start_btn->setToolTip("New Game"); 138 | connect(start_btn, SIGNAL(clicked()), this, SLOT(game_start())); 139 | 140 | reset_btn = new ToolButton; 141 | reset_btn->set_image(":/icons/icons/restart.png"); 142 | reset_btn->setToolTip("Restart"); 143 | connect(reset_btn, SIGNAL(clicked()), this, SLOT(game_reset())); 144 | 145 | pause_btn = new ToolButton; 146 | pause_btn->set_image(":/icons/icons/pause.png"); 147 | pause_btn->setToolTip("Pause"); 148 | connect(pause_btn, SIGNAL(clicked()), this, SLOT(toggle_button())); 149 | 150 | hint_btn = new ToolButton; 151 | hint_btn->set_image(":/icons/icons/information.png"); 152 | hint_btn->setToolTip("Hint"); 153 | connect(hint_btn, SIGNAL(clicked()), grid, SLOT(game_hint())); 154 | 155 | clear_btn = new ToolButton; 156 | clear_btn->set_image(":/icons/icons/eraser.png"); 157 | clear_btn->setToolTip("Clear Cell"); 158 | connect(clear_btn, SIGNAL(clicked()), grid, SLOT(clear_grid())); 159 | 160 | backward_btn = new ToolButton; 161 | backward_btn->set_image(":/icons/icons/back.png"); 162 | backward_btn->setToolTip("Undo"); 163 | connect(backward_btn, SIGNAL(clicked()), grid, SLOT(backward_step())); 164 | connect(grid, SIGNAL(set_backward_enable(bool)), this, SLOT(set_backward_enable(bool))); 165 | 166 | forward_btn = new ToolButton; 167 | forward_btn->set_image(":/icons/icons/next.png"); 168 | forward_btn->setToolTip("Rndo"); 169 | connect(forward_btn, SIGNAL(clicked()), grid, SLOT(forward_step())); 170 | connect(grid, SIGNAL(set_forward_enable(bool)), this, SLOT(set_forward_enable(bool))); 171 | 172 | level_label = new QLabel; 173 | level_label->setStyleSheet("font-size: 18pt;"); 174 | level_label->setContentsMargins(0, 0, 10, 0); 175 | 176 | // set top layout 177 | top_layout->addSpacerItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Minimum)); 178 | top_layout->addWidget(level_label); 179 | 180 | timer_layout->addWidget(clock_img); 181 | timer_layout->addWidget(timer); 182 | top_layout->addLayout(timer_layout); 183 | 184 | top_layout->addWidget(start_btn); 185 | top_layout->addWidget(reset_btn); 186 | top_layout->addWidget(pause_btn); 187 | top_layout->addWidget(hint_btn); 188 | top_layout->addWidget(clear_btn); 189 | top_layout->addWidget(backward_btn); 190 | top_layout->addWidget(forward_btn); 191 | 192 | top_layout->addSpacerItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Minimum)); 193 | 194 | connect(grid, SIGNAL(update_digit_signal(IntList)), 195 | this, SLOT(update_digit(IntList))); 196 | connect(grid, SIGNAL(game_over_signal()), 197 | this, SLOT(game_over())); 198 | connect(ui->actionNew_Game, SIGNAL(triggered()), this, SLOT(game_start())); 199 | connect(ui->actionHint_one, SIGNAL(triggered()), grid, SLOT(game_hint())); 200 | connect(ui->actionHint_All, SIGNAL(triggered()), grid, SLOT(game_solve())); 201 | connect(ui->actionRestart_Game, SIGNAL(triggered()), this, SLOT(game_reset())); 202 | 203 | setFixedSize(window->minimumSizeHint() + ui->menuBar->minimumSizeHint()); 204 | 205 | // run the game 206 | level_changed(1); 207 | } 208 | 209 | MainWindow::~MainWindow() 210 | { 211 | delete ui; 212 | } 213 | 214 | void MainWindow::toggle_button() 215 | { 216 | timer->toggle_timer(); 217 | is_paused = !is_paused; 218 | grid->setEnabled(!is_paused); 219 | for(DigitButton* btn : digit_btns) 220 | btn->setEnabled(!is_paused); 221 | set_tool_enable(!is_paused); 222 | pause_btn->setEnabled(true); 223 | if(is_paused) 224 | { 225 | grid->free_selection(); 226 | pause_btn->set_image(":/icons/icons/play.png"); 227 | pause_btn->setToolTip("Continue"); 228 | } else { 229 | pause_btn->set_image(":/icons/icons/pause.png"); 230 | pause_btn->setToolTip("Pause"); 231 | } 232 | } 233 | 234 | void MainWindow::set_backward_enable(bool enabled) 235 | { 236 | backward_btn->setEnabled(enabled); 237 | } 238 | 239 | void MainWindow::set_forward_enable(bool enabled) 240 | { 241 | forward_btn->setEnabled(enabled); 242 | } 243 | 244 | void MainWindow::game_start() 245 | { 246 | set_tool_enable(true); 247 | if(is_paused) toggle_button(); 248 | timer->restart_timer(); 249 | grid->game_start(); 250 | } 251 | 252 | void MainWindow::game_reset() 253 | { 254 | set_tool_enable(true); 255 | if(is_paused) toggle_button(); 256 | timer->restart_timer(); 257 | grid->game_reset(); 258 | } 259 | 260 | void MainWindow::level_changed(int level) 261 | { 262 | grid->level_changed(level); 263 | level_label->setText("Level " + QString::number(level)); 264 | game_start(); 265 | } 266 | 267 | void MainWindow::update_digit(IntList candidates) 268 | { 269 | for(int i = 1; i != (int)candidates.size(); ++i) 270 | digit_btns[i - 1]->set_checked(candidates[i]); 271 | } 272 | 273 | void MainWindow::digit_add_value(int pos) 274 | { 275 | SudokuCell *cell = grid->get_current_selected(); 276 | if(digit_btns[pos - 1]->is_checked() && cell) 277 | { 278 | if(cell->is_value_settled()) 279 | grid->add_value(pos); 280 | else grid->remove_value(pos); 281 | } else grid->add_value(pos); 282 | } 283 | 284 | void MainWindow::digit_set_value(int pos) 285 | { 286 | SudokuCell *cell = grid->get_current_selected(); 287 | if(digit_btns[pos - 1]->is_checked() && cell) 288 | { 289 | if(!cell->is_value_settled()) 290 | grid->set_value(pos); 291 | else grid->remove_value(pos); 292 | } else grid->set_value(pos); 293 | } 294 | 295 | void MainWindow::game_over() 296 | { 297 | if(!is_paused) toggle_button(); 298 | grid->free_selection(); 299 | for(DigitButton *digit_btn : digit_btns) 300 | digit_btn->set_checked(false); 301 | set_tool_enable(false); 302 | QMessageBox::information(this, "Problem Solved", "Congratulations! You solved this puzzle in " + timer->get_time() + "."); 303 | } 304 | 305 | void MainWindow::set_tool_enable(bool is_enabled) 306 | { 307 | pause_btn->setEnabled(is_enabled); 308 | forward_btn->setEnabled(is_enabled); 309 | backward_btn->setEnabled(is_enabled); 310 | clear_btn->setEnabled(is_enabled); 311 | hint_btn->setEnabled(is_enabled); 312 | timer->setEnabled(is_enabled); 313 | } 314 | 315 | void MainWindow::change_cell_size(int size) 316 | { 317 | digit_btns.clear(); 318 | QWidget *old_window = window; 319 | init_widgets(size); 320 | delete old_window; 321 | } 322 | -------------------------------------------------------------------------------- /sudoku/mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include "sudoku_grid.h" 7 | #include "digit_button.h" 8 | #include "tool_button.h" 9 | #include "timer.h" 10 | 11 | namespace Ui { 12 | class MainWindow; 13 | } 14 | 15 | class MainWindow : public QMainWindow 16 | { 17 | Q_OBJECT 18 | 19 | public: 20 | explicit MainWindow(QMainWindow *parent = 0); 21 | ~MainWindow(); 22 | 23 | public slots: 24 | void toggle_button(); 25 | void set_backward_enable(bool); 26 | void set_forward_enable(bool); 27 | void game_start(); 28 | void game_reset(); 29 | void game_over(); 30 | void level_changed(int); 31 | void update_digit(IntList); 32 | void digit_add_value(int); 33 | void digit_set_value(int); 34 | 35 | void change_cell_size(int); 36 | 37 | private: 38 | void init_widgets(int); 39 | void init_menu(); 40 | void set_tool_enable(bool); 41 | 42 | private: 43 | Ui::MainWindow *ui; 44 | QWidget *window; 45 | QVBoxLayout *layout; 46 | QHBoxLayout *top_layout, *bottom_layout; 47 | QGridLayout *digit_button_layout; 48 | std::vector digit_btns; 49 | ToolButton *start_btn, *reset_btn, *pause_btn, *hint_btn; 50 | ToolButton *clear_btn, *backward_btn, *forward_btn; 51 | QLabel *level_label; 52 | SudokuGrid *grid; 53 | Timer *timer; 54 | 55 | bool is_paused; 56 | }; 57 | 58 | #endif // MAINWINDOW_H 59 | -------------------------------------------------------------------------------- /sudoku/mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Sudoku Game 15 | 16 | 17 | false 18 | 19 | 20 | 21 | 22 | 23 | 0 24 | 0 25 | 400 26 | 20 27 | 28 | 29 | 30 | 31 | Level 32 | 33 | 34 | 35 | 36 | Game 37 | 38 | 39 | 40 | Grid Size 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | New Game 55 | 56 | 57 | 58 | 59 | Hint One 60 | 61 | 62 | 63 | 64 | Solve 65 | 66 | 67 | 68 | 69 | Restart Game 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /sudoku/res.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/back.png 4 | icons/eraser.png 5 | icons/information.png 6 | icons/next.png 7 | icons/pause.png 8 | icons/play.png 9 | icons/question.png 10 | icons/restart.png 11 | icons/alarm-clock.png 12 | icons/new.png 13 | 14 | 15 | -------------------------------------------------------------------------------- /sudoku/sudoku.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "sudoku.h" 5 | #include "dancing_link.h" 6 | 7 | Sudoku::Sudoku(int size) 8 | : size_(size), span_(size * size), grids_(span_ * span_, 0) 9 | { 10 | } 11 | 12 | bool Sudoku::_check_coord(int x, int y) const 13 | { 14 | if(x < 0 || y < 0 || x >= span_ || y >= span_) 15 | return false; 16 | return true; 17 | } 18 | 19 | bool Sudoku::is_empty() const 20 | { 21 | return count(grids_.begin(), grids_.end(), 0) == span_ * span_; 22 | } 23 | 24 | bool Sudoku::is_solved() const 25 | { 26 | return std::count(grids_.begin(), grids_.end(), 0) == 0 27 | && is_consistent(); 28 | } 29 | 30 | void Sudoku::clear() 31 | { 32 | fill(grids_.begin(), grids_.end(), 0); 33 | } 34 | 35 | int Sudoku::span() const 36 | { 37 | return span_; 38 | } 39 | 40 | bool Sudoku::reset(int x, int y) 41 | { 42 | return set(x, y, 0); 43 | } 44 | 45 | bool Sudoku::set(int x, int y, int v) 46 | { 47 | if(!_check_coord(x, y)) 48 | return false; 49 | 50 | grids_[x * span_ + y] = v; 51 | return true; 52 | } 53 | 54 | bool Sudoku::try_set(int x, int y, int v) 55 | { 56 | if(!_check_coord(x, y)) 57 | return false; 58 | 59 | IntList avail = get_available(x, y); 60 | if(!v || find(avail.begin(), avail.end(), v) != avail.end()) 61 | { 62 | grids_[x * span_ + y] = v; 63 | return true; 64 | } 65 | 66 | return false; 67 | } 68 | 69 | int Sudoku::get(int x, int y) const 70 | { 71 | if(!_check_coord(x, y)) 72 | return -1; 73 | return grids_[x * span_ + y]; 74 | } 75 | 76 | bool Sudoku::is_consistent() const 77 | { 78 | int now = 1; 79 | IntList mark(span_ + 1, 0); 80 | 81 | // check rows 82 | for(int i = 0; i != span_; ++i, ++now) 83 | { 84 | for(int j = 0; j != span_; ++j) 85 | { 86 | int g = get(i, j); 87 | if(g && mark[g] == now) 88 | return false; 89 | mark[g] = now; 90 | } 91 | } 92 | 93 | // check columns 94 | for(int i = 0; i != span_; ++i, ++now) 95 | { 96 | for(int j = 0; j != span_; ++j) 97 | { 98 | int g = get(j, i); 99 | if(g && mark[g] == now) 100 | return false; 101 | mark[g] = now; 102 | } 103 | } 104 | 105 | // check grids 106 | for(int i = 0; i != size_; ++i) 107 | { 108 | for(int j = 0; j != size_; ++j, ++now) 109 | { 110 | for(int k = 0; k != size_; ++k) 111 | { 112 | for(int w = 0; w != size_; ++w) 113 | { 114 | int x = i * size_ + k, y = j * size_ + w; 115 | int g = get(x, y); 116 | if(g && mark[g] == now) 117 | return false; 118 | mark[g] = now; 119 | } 120 | } 121 | } 122 | } 123 | 124 | return true; 125 | } 126 | 127 | IntList Sudoku::get_available(int x, int y) const 128 | { 129 | if(!_check_coord(x, y)) 130 | return {}; 131 | 132 | if(get(x, y)) 133 | return { get(x, y) }; 134 | 135 | IntList mark(span_ + 1, 0); 136 | 137 | // check row and column 138 | for(int j = 0; j != span_; ++j) 139 | { 140 | mark[get(x, j)] = 1; 141 | mark[get(j, y)] = 1; 142 | } 143 | 144 | // check grid 145 | int grid_x = x / size_, grid_y = y / size_; 146 | for(int i = 0; i != 3; ++i) 147 | for(int j = 0; j != 3; ++j) 148 | mark[get(grid_x * size_ + i, grid_y * size_ + j)] = 1; 149 | 150 | IntList avail; 151 | for(int i = 1; i <= span_; ++i) 152 | if(!mark[i]) avail.push_back(i); 153 | return avail; 154 | } 155 | 156 | Sudoku Sudoku::solve(bool check_unique, bool *is_unique) const 157 | { 158 | int span2 = span_ * span_; 159 | int row_num = span2 * span_; 160 | int col_num = span2 * 4; 161 | 162 | std::map> row_map; 163 | DancingLink dlx(row_num, col_num); 164 | for(int r = 0; r != span_; ++r) 165 | { 166 | for(int c = 0; c != span_; ++c) 167 | { 168 | int now_v = get(r, c); 169 | for(int v = 0; v != span_; ++v) 170 | { 171 | if(now_v != 0 && now_v != v + 1) 172 | continue; 173 | int g = r / size_ * size_ + c / size_; 174 | int row_id = dlx.append_row( { 175 | r * span_ + c + 1, // existance 176 | r * span_ + v + span2 + 1, // rows consistency 177 | c * span_ + v + span2 * 2 + 1, // columns consistency 178 | g * span_ + v + span2 * 3 + 1 // grids consistency 179 | } ); 180 | 181 | row_map[row_id] = std::make_pair(r * span_ + c, v + 1); 182 | } 183 | } 184 | } 185 | 186 | IntList dlx_rows; 187 | if(!check_unique) 188 | dlx_rows = dlx.solve(); 189 | else *is_unique = dlx.solve_unique(dlx_rows); 190 | Sudoku ans = *this; 191 | 192 | for(int row : dlx_rows) 193 | { 194 | auto info = row_map[row]; 195 | ans.grids_[info.first] = info.second; 196 | } 197 | 198 | if(dlx_rows.size() != (unsigned)span2) 199 | ans.clear(); 200 | 201 | return ans; 202 | } 203 | 204 | void Sudoku::exchange_column(int c1, int c2) 205 | { 206 | if(0 <= c1 && c1 < span_ && 0 <= c2 && c2 < span_) 207 | { 208 | for(int r = 0; r != span_; ++r) 209 | std::swap(grids_[r * span_ + c1], grids_[r * span_ + c2]); 210 | } 211 | } 212 | 213 | void Sudoku::exchange_row(int r1, int r2) 214 | { 215 | if(0 <= r1 && r1 < span_ && 0 <= r2 && r2 < span_) 216 | { 217 | for(int c = 0; c != span_; ++c) 218 | std::swap(grids_[r1 * span_ + c], grids_[r2 * span_ + c]); 219 | } 220 | } 221 | 222 | void Sudoku::exchange_number(int v1, int v2) 223 | { 224 | if(1 <= v1 && v1 <= span_ && 1 <= v2 && v2 <= span_) 225 | { 226 | for(int& cell : grids_) 227 | { 228 | if(cell == v1) cell = v2; 229 | else if(cell == v2) cell = v1; 230 | } 231 | } 232 | } 233 | 234 | void Sudoku::random_exchange(int times) 235 | { 236 | for(int i = 0; i != times; ++i) 237 | { 238 | int type = std::rand() % 5; 239 | if(type == 0) 240 | { 241 | int v1 = std::rand() % span_ + 1; 242 | int v2 = std::rand() % span_ + 1; 243 | exchange_number(v1, v2); 244 | } else if(type == 1 || type == 2) { 245 | int l = std::rand() % size_ * size_; 246 | int l1 = l + std::rand() % size_; 247 | int l2 = l + std::rand() % size_; 248 | if(type == 1) exchange_row(l1, l2); 249 | else exchange_column(l1, l2); 250 | } else { 251 | int l1 = std::rand() % size_ * size_; 252 | int l2 = std::rand() % size_ * size_; 253 | for(int i = 0; i != size_; ++i) 254 | if(type == 3) exchange_row(l1 + i, l2 + i); 255 | else exchange_column(l1 + i, l2 + i); 256 | } 257 | } 258 | } 259 | 260 | void Sudoku::random_sudoku( 261 | int init_cells, 262 | int empty_cells_lb, 263 | int line_lb, 264 | bool is_unique, 265 | int digging_seq_type) 266 | { 267 | int span2 = span_ * span_; 268 | auto try_random = [=] () -> int { 269 | clear(); 270 | 271 | // randomly fill `init_cells` cells 272 | auto try_random_one = [this] () -> bool { 273 | int r = std::rand() % span_, c = std::rand() % span_; 274 | if(get(r, c) != 0) return false; 275 | IntList avail = get_available(r, c); 276 | if(avail.empty()) return false; 277 | set(r, c, avail[std::rand() % avail.size()]); 278 | return true; 279 | }; 280 | 281 | for(int count = 0; count < init_cells; count += try_random_one()); 282 | 283 | // generate a valid solution 284 | *this = solve(); 285 | if(is_empty()) return 0; 286 | 287 | // random exchange row/column/value 288 | random_exchange(30); 289 | 290 | // generate digging sequence 291 | IntList digging_seq; 292 | 293 | switch(digging_seq_type) 294 | { 295 | case DIGGING_RANDOM: 296 | for(int i = 0; i != span2; ++i) 297 | digging_seq.push_back(i); 298 | std::random_shuffle(digging_seq.begin(), digging_seq.end()); 299 | break; 300 | case DIGGING_S: 301 | for(int r = 0; r != span_; ++r) 302 | for(int c = 0; c != span_; ++c) 303 | { 304 | int nc = (r & 1) ? span_ - c - 1 : c; 305 | digging_seq.push_back(r * span_ + nc); 306 | } 307 | break; 308 | case DIGGING_Z: 309 | for(int i = 0; i != span2; ++i) 310 | digging_seq.push_back(i); 311 | break; 312 | } 313 | 314 | // random peturb digging sequence 315 | for(int i = 0; i != span_; ++i) 316 | std::swap(digging_seq[std::rand() % span2], digging_seq[std::rand() % span2]); 317 | 318 | // digging empty cells 319 | auto try_dig_one = [=] (int pos) -> bool { 320 | int r = pos / span_, c = pos % span_; 321 | int cnt_r = 0, cnt_c = 0; 322 | for(int i = 0; i != span_; ++i) 323 | { 324 | if(get(r, i)) ++cnt_c; 325 | if(get(i, c)) ++cnt_r; 326 | } 327 | 328 | if(cnt_r <= line_lb || cnt_c <= line_lb) 329 | return false; 330 | 331 | if(is_unique) 332 | { 333 | Sudoku new_sudoku = *this; 334 | new_sudoku.reset(r, c); 335 | 336 | bool uniqueness; 337 | new_sudoku = new_sudoku.solve(true, &uniqueness); 338 | 339 | if(uniqueness) 340 | this->reset(r, c); 341 | return uniqueness; 342 | } else { 343 | this->reset(r, c); 344 | return true; 345 | } 346 | }; 347 | 348 | int dig_num = 0; 349 | for(int pos : digging_seq) 350 | { 351 | dig_num += try_dig_one(pos); 352 | if(dig_num >= empty_cells_lb) 353 | break; 354 | } 355 | 356 | return dig_num; 357 | }; 358 | 359 | while(try_random() < empty_cells_lb); 360 | random_exchange(50); 361 | } 362 | 363 | Sudoku Sudoku::generate(int size, int level) 364 | { 365 | bool is_unique = true; 366 | int given = 81, lb = 0, random_type = DIGGING_RANDOM; 367 | 368 | switch(level) 369 | { 370 | case 1: 371 | lb = 5; 372 | given = 50 + std::rand() % 8; 373 | break; 374 | case 2: 375 | lb = 4; 376 | given = 50 + std::rand() % 5; 377 | break; 378 | case 3: 379 | lb = 4; 380 | given = 40 + std::rand() % 8; 381 | break; 382 | case 4: 383 | lb = 4; 384 | given = 36 + std::rand() % 6; 385 | break; 386 | case 5: 387 | lb = 3; 388 | given = 36 + std::rand() % 6; 389 | break; 390 | case 6: 391 | lb = 3; 392 | given = 28 + std::rand() % 3; 393 | break; 394 | case 7: 395 | lb = 2; 396 | given = 28 + std::rand() % 3; 397 | break; 398 | case 8: 399 | lb = 2; 400 | given = 22 + std::rand() % 5; 401 | break; 402 | case 9: 403 | lb = 1; 404 | random_type = DIGGING_S; 405 | given = 22 + std::rand() % 3; 406 | break; 407 | case 10: 408 | lb = 0; 409 | random_type = DIGGING_Z; 410 | given = 22 + std::rand() % 2; 411 | break; 412 | } 413 | 414 | Sudoku ret(size); 415 | 416 | if(size > 3) 417 | { 418 | double p = ret.size_ / 3.0 + 1.0e-10; 419 | lb *= p; 420 | given *= p * p * p * p; 421 | is_unique = false; 422 | random_type = DIGGING_RANDOM; 423 | } 424 | 425 | ret.random_sudoku(11, ret.span() * ret.span() - given, lb, is_unique, random_type); 426 | return ret; 427 | } 428 | -------------------------------------------------------------------------------- /sudoku/sudoku.h: -------------------------------------------------------------------------------- 1 | #ifndef SUDOKU_H 2 | #define SUDOKU_H 3 | 4 | #include "utils.h" 5 | 6 | #define DIGGING_RANDOM 0 7 | #define DIGGING_S 1 8 | #define DIGGING_Z 2 9 | 10 | #define SUDOKU_LEVEL_MIN 1 11 | #define SUDOKU_LEVEL_MAX 10 12 | 13 | class Sudoku 14 | { 15 | int size_, span_; 16 | // 0 means not being filled yet 17 | IntList grids_; 18 | public: 19 | Sudoku(int size = 3); 20 | ~Sudoku() = default; 21 | 22 | // check whether all grids are unfilled 23 | bool is_empty() const; 24 | // check whether the Sudoku is solved 25 | bool is_solved() const; 26 | // set all grids to be unfilled, namely 0 27 | void clear(); 28 | // set (x, y) to be unfilled, namely 0 29 | bool reset(int x, int y); 30 | // set (x, y) to be of value v 31 | bool set(int x, int y, int v); 32 | // try to set (x, y) to be of value v 33 | bool try_set(int x, int y, int v); 34 | // get the value of (x, y) 35 | int get(int x, int y) const; 36 | // check whether the current status is consistent 37 | bool is_consistent() const; 38 | // get available candidate numbers of (x, y) 39 | IntList get_available(int x, int y) const; 40 | // solve this sudoku puzzle 41 | Sudoku solve(bool check_unique = false, bool* is_unique = nullptr) const; 42 | // get span 43 | int span() const; 44 | 45 | public: 46 | void exchange_number(int v1, int v2); 47 | void exchange_row(int r1, int r2); 48 | void exchange_column(int c1, int c2); 49 | void random_exchange(int times); 50 | void random_sudoku( 51 | int init_cells, 52 | int empty_cells_lb, 53 | int line_lb, 54 | bool is_unique = true, 55 | int digging_seq_type = DIGGING_Z 56 | ); 57 | 58 | static Sudoku generate(int size, int level); 59 | private: 60 | bool _check_coord(int x, int y) const; 61 | }; 62 | 63 | #endif // SUDOKU_H 64 | 65 | -------------------------------------------------------------------------------- /sudoku/sudoku.pro: -------------------------------------------------------------------------------- 1 | #------------------------------------------------- 2 | # 3 | # Project created by QtCreator 2017-08-28T10:56:53 4 | # 5 | #------------------------------------------------- 6 | 7 | QT += core gui 8 | QMAKE_CXXFLAGS += -std=c++11 9 | 10 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 11 | 12 | TARGET = sudoku 13 | TEMPLATE = app 14 | 15 | 16 | SOURCES += main.cpp\ 17 | mainwindow.cpp \ 18 | sudoku.cpp \ 19 | dancing_link.cpp \ 20 | sudoku_grid.cpp \ 21 | sudoku_cell.cpp \ 22 | timer.cpp \ 23 | action_queue.cpp \ 24 | digit_button.cpp \ 25 | utils.cpp \ 26 | tool_button.cpp 27 | 28 | HEADERS += mainwindow.h \ 29 | utils.h \ 30 | sudoku.h \ 31 | dancing_link.h \ 32 | sudoku_grid.h \ 33 | sudoku_cell.h \ 34 | timer.h \ 35 | action_queue.h \ 36 | config.h \ 37 | digit_button.h \ 38 | tool_button.h 39 | 40 | 41 | FORMS += mainwindow.ui 42 | 43 | RESOURCES += \ 44 | res.qrc 45 | -------------------------------------------------------------------------------- /sudoku/sudoku_cell.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "sudoku_cell.h" 4 | #include "utils.h" 5 | #include "config.h" 6 | 7 | SudokuCell::SudokuCell(int row, int col, std::shared_ptr sudoku, QWidget *parent) 8 | : QLabel(parent), 9 | row(row), col(col), 10 | initial_status(0), 11 | sudoku(sudoku), 12 | is_lighted(false), 13 | is_marked(false), 14 | is_vertical_selected(false), 15 | is_horizontal_selected(false), 16 | value_fixed(false), 17 | value_settled(false), 18 | candidates(sudoku->span() + 1, 0) 19 | { 20 | update_style(); 21 | } 22 | 23 | void SudokuCell::vertical_selected() 24 | { 25 | is_vertical_selected = true; 26 | update_style(); 27 | } 28 | 29 | void SudokuCell::horizontal_selected() 30 | { 31 | is_horizontal_selected = true; 32 | update_style(); 33 | } 34 | 35 | void SudokuCell::free_selection() 36 | { 37 | is_vertical_selected = false; 38 | is_horizontal_selected = false; 39 | update_style(); 40 | } 41 | 42 | void SudokuCell::add_value(int v, bool emit_signal) 43 | { 44 | int span = sudoku->span(); 45 | if(1 <= v && v <= span && !(!value_settled && candidates[v])) 46 | { 47 | IntList old_candidates = candidates; 48 | bool old_value_settled = value_settled; 49 | value_settled = false; 50 | candidates[v] = 1; 51 | is_lighted = false; 52 | update_style(); 53 | update_text(); 54 | if(emit_signal) 55 | emit value_changed(row, col, 56 | old_value_settled, old_candidates, 57 | value_settled, candidates); 58 | } 59 | } 60 | 61 | void SudokuCell::set_value(int v, bool emit_signal) 62 | { 63 | int span = sudoku->span(); 64 | if(1 <= v && v <= span && !(value_settled && candidates[v])) 65 | { 66 | bool old_value_settled = value_settled; 67 | value_settled = true; 68 | IntList old_candidates = candidates; 69 | std::fill(candidates.begin(), candidates.end(), 0); 70 | candidates[v] = 1; 71 | update_text(); 72 | if(emit_signal) 73 | emit value_changed(row, col, 74 | old_value_settled, old_candidates, 75 | value_settled, candidates); 76 | } 77 | } 78 | 79 | void SudokuCell::remove_value(int v, bool emit_signal) 80 | { 81 | IntList old_candidates = candidates; 82 | 83 | int span = sudoku->span(); 84 | if(1 <= v && v <= span && candidates[v]) 85 | { 86 | candidates[v] = 0; 87 | update_text(); 88 | if(emit_signal) 89 | emit value_changed(row, col, 90 | value_settled, old_candidates, 91 | value_settled, candidates); 92 | } else if(v == -1) { 93 | for(int i = span; i; --i) 94 | if(candidates[i]) 95 | { 96 | candidates[i] = 0; 97 | update_text(); 98 | if(emit_signal) 99 | emit value_changed(row, col, 100 | value_settled, old_candidates, 101 | value_settled, candidates); 102 | break; 103 | } 104 | } 105 | } 106 | 107 | void SudokuCell::update_style() 108 | { 109 | QString text_color = DEFAULT_TEXT_COLOR, 110 | bg_color = DEFAULT_BG_COLOR, 111 | font_style = "normal", 112 | font_weight = "normal"; 113 | 114 | if(!initial_status) text_color = FILLED_TEXT_COLOR; 115 | if(is_vertical_selected || is_horizontal_selected) 116 | bg_color = SELECTED_BG_COLOR; 117 | if(is_lighted) 118 | { 119 | font_style = "italic"; 120 | font_weight = "900"; 121 | } 122 | 123 | setStyleSheet("background-color: " + bg_color + ";" 124 | "color: " + text_color + ";" 125 | "font-style: " + font_style + ";" 126 | "font-weight: " + font_weight + ";"); 127 | } 128 | 129 | void SudokuCell::update_font() 130 | { 131 | QString text = this->text(); 132 | QRect rect_lbl = geometry().adjusted(5, 5, -5, -5); 133 | QFont font = this->font(); 134 | 135 | font = fit_font_with_text(font, text, rect_lbl); 136 | 137 | setFont(font); 138 | } 139 | 140 | void SudokuCell::update_text() 141 | { 142 | int last_v = 0; 143 | int digit_num = count(candidates.begin(), candidates.end(), 1); 144 | // int line_num = std::ceil(std::sqrt(1.0 * digit_num) - 1.0e-9); 145 | int line_num = std::round(std::sqrt(1.0 * sudoku->span())); 146 | 147 | QString str; 148 | for(int i = 1, cnt = 0; i <= sudoku->span(); ++i) 149 | { 150 | if(candidates[i]) 151 | { 152 | if(last_v) str += cnt % line_num ? ' ' : '\n'; 153 | str += QString::number(i); 154 | last_v = i; 155 | ++cnt; 156 | } 157 | } 158 | 159 | if(!value_settled) 160 | { 161 | // using space to fill 162 | for(int i = digit_num; i < sudoku->span(); ++i) 163 | if(i % line_num == 0) 164 | str += "\n "; 165 | else str += " "; 166 | } 167 | 168 | setText(str); 169 | update_font(); 170 | 171 | if(digit_num == 0) 172 | sudoku->reset(row, col); 173 | else if(digit_num == 1 && value_settled) 174 | sudoku->set(row, col, last_v); 175 | } 176 | 177 | void SudokuCell::mousePressEvent(QMouseEvent *ev) 178 | { 179 | // if(initial_status == 0) 180 | // { 181 | emit_selected_signal(); 182 | 183 | if(ev->button() == Qt::RightButton) 184 | set_mark(!is_marked); 185 | // } else emit free_signal(); 186 | } 187 | 188 | void SudokuCell::keyPressEvent(QKeyEvent *ev) 189 | { 190 | int key = ev->key(); 191 | if(!is_initial_status()) 192 | { 193 | if(Qt::Key_1 <= key && key <= Qt::Key_9) 194 | { 195 | int val = key - Qt::Key_0; 196 | if(QApplication::keyboardModifiers() & Qt::ControlModifier) 197 | add_value(val); 198 | else set_value(val); 199 | } else if(key == Qt::Key_Backspace) 200 | remove_value(-1); 201 | } 202 | 203 | if(key == Qt::Key_Left || key == Qt::Key_Right || key == Qt::Key_Down 204 | || key == Qt::Key_Up || key == Qt::Key_Tab) 205 | emit move_focus(key); 206 | } 207 | 208 | void SudokuCell::paintEvent(QPaintEvent *ev) 209 | { 210 | QLabel::paintEvent(ev); 211 | QPainter p(this); 212 | 213 | if(is_marked && !initial_status) 214 | { 215 | QPainterPath path; 216 | path.moveTo(0, 0); 217 | path.lineTo(0, 5); 218 | path.lineTo(5, 0); 219 | path.closeSubpath(); 220 | p.fillPath(path, QBrush(qRgb(44, 44, 44))); 221 | } 222 | } 223 | 224 | void SudokuCell::set_initial_status(int v) 225 | { 226 | initial_status = v; 227 | fill(candidates.begin(), candidates.end(), 0); 228 | value_settled = false; 229 | 230 | if(v) set_value(v); 231 | else update_text(); 232 | 233 | update_style(); 234 | set_mark(false); 235 | } 236 | 237 | int SudokuCell::get_initial_status() const 238 | { 239 | return initial_status; 240 | } 241 | 242 | void SudokuCell::clear_values(bool emit_signal) 243 | { 244 | IntList old_candidates = candidates; 245 | fill(candidates.begin(), candidates.end(), 0); 246 | update_text(); 247 | update_style(); 248 | 249 | if(emit_signal) 250 | emit value_changed(row, col, 251 | value_settled, old_candidates, 252 | value_settled, candidates); 253 | } 254 | 255 | IntList SudokuCell::get_candidates() const 256 | { 257 | return candidates; 258 | } 259 | 260 | void SudokuCell::emit_selected_signal() 261 | { 262 | emit selected_signal(this); 263 | } 264 | 265 | void SudokuCell::light_value(int v) 266 | { 267 | if(v && get_value() == v) 268 | { 269 | is_lighted = true; 270 | } else is_lighted = false; 271 | update_style(); 272 | update_text(); 273 | } 274 | 275 | void SudokuCell::set_mark(bool mark) 276 | { 277 | is_marked = mark; 278 | this->update(); 279 | } 280 | 281 | int SudokuCell::get_value() const 282 | { 283 | if(value_settled && count(candidates.begin(), candidates.end(), 1) == 1) 284 | return std::find(candidates.begin(), candidates.end(), 1) - candidates.begin(); 285 | return 0; 286 | } 287 | 288 | void SudokuCell::recover_status(bool value_settled, IntList candidates) 289 | { 290 | this->value_settled = value_settled; 291 | this->candidates = candidates; 292 | update_text(); 293 | update_style(); 294 | } 295 | 296 | bool SudokuCell::is_initial_status() const 297 | { 298 | return initial_status != 0; 299 | } 300 | 301 | bool SudokuCell::is_value_settled() const 302 | { 303 | return value_settled; 304 | } 305 | -------------------------------------------------------------------------------- /sudoku/sudoku_cell.h: -------------------------------------------------------------------------------- 1 | #ifndef SUDOKUCELL_H 2 | #define SUDOKUCELL_H 3 | 4 | #include 5 | #include 6 | #include "sudoku.h" 7 | #include "utils.h" 8 | 9 | class SudokuCell : public QLabel 10 | { 11 | Q_OBJECT 12 | public: 13 | explicit SudokuCell(int row, int col, std::shared_ptr sudoku, QWidget *parent = 0); 14 | 15 | void set_initial_status(int v); 16 | int get_initial_status() const; 17 | 18 | IntList get_candidates() const; 19 | bool is_initial_status() const; 20 | bool is_value_settled() const; 21 | 22 | void add_value(int v, bool emit_signal = true); 23 | void set_value(int v, bool emit_signal = true); 24 | void remove_value(int v, bool emit_signal = true); 25 | void clear_values(bool emit_signal = true); 26 | 27 | void emit_selected_signal(); 28 | void light_value(int); 29 | void set_mark(bool); 30 | 31 | int get_value() const; 32 | int get_row() const { return row; } 33 | int get_col() const { return col; } 34 | 35 | void recover_status(bool value_settled, IntList candidates); 36 | 37 | signals: 38 | void selected_signal(SudokuCell*); 39 | void free_signal(); 40 | void value_changed(int, int, bool, IntList, bool, IntList); 41 | void move_focus(int); 42 | 43 | public slots: 44 | void vertical_selected(); 45 | void horizontal_selected(); 46 | void free_selection(); 47 | 48 | protected: 49 | void mousePressEvent(QMouseEvent*); 50 | void keyPressEvent(QKeyEvent*); 51 | void paintEvent(QPaintEvent*); 52 | 53 | private: 54 | void update_text(); 55 | void update_font(); 56 | void update_style(); 57 | 58 | private: 59 | int row, col, initial_status; 60 | std::shared_ptr sudoku; 61 | 62 | bool is_lighted, is_marked; 63 | bool is_vertical_selected, is_horizontal_selected; 64 | 65 | int value_fixed, value_settled; 66 | IntList candidates; 67 | }; 68 | 69 | #endif // SUDOKUCELL_H 70 | -------------------------------------------------------------------------------- /sudoku/sudoku_grid.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "sudoku_grid.h" 3 | #include "config.h" 4 | 5 | SudokuGrid::SudokuGrid(int cell_size, int fixed_size, QWidget *parent) 6 | : QLabel(parent), 7 | current_level(1), 8 | cell_size(cell_size), 9 | cell_span(cell_size * cell_size), 10 | fixed_size(fixed_size), 11 | current_selected(nullptr), 12 | sudoku(std::make_shared(cell_size)), 13 | actions(ACTIONS_MAX_SIZE) 14 | { 15 | // init cells 16 | top_layer = new QGridLayout(this); 17 | 18 | cells.assign(cell_span * cell_span, nullptr); 19 | for(int r = 0; r != cell_size; ++r) 20 | for(int c = 0; c != cell_size; ++c) 21 | { 22 | QGridLayout *grid = new QGridLayout; 23 | top_layer->addLayout(grid, r, c); 24 | grid->setSpacing(CELL_SPACING); 25 | 26 | for(int x = 0; x != cell_size; ++x) 27 | for(int y = 0; y != cell_size; ++y) 28 | { 29 | int row = r * cell_size + x; 30 | int col = c * cell_size + y; 31 | SudokuCell *cell = new SudokuCell(row, col, sudoku); 32 | cell->setFocusPolicy(Qt::ClickFocus); 33 | cell->setFixedSize(fixed_size, fixed_size); 34 | cell->setTextFormat(Qt::PlainText); 35 | cell->setAlignment(Qt::AlignCenter); 36 | 37 | grid->addWidget(cell, x, y); 38 | 39 | cells[row * cell_span + col] = cell; 40 | } 41 | 42 | grids.push_back(grid); 43 | } 44 | 45 | // connect signals 46 | for(int r = 0; r != cell_span; ++r) 47 | for(int c = 0; c != cell_span; ++c) 48 | { 49 | SudokuCell *cell = cells[r * cell_span + c]; 50 | 51 | connect(cell, SIGNAL(selected_signal(SudokuCell*)), 52 | this, SLOT(cell_selected(SudokuCell*))); 53 | 54 | connect(cell, SIGNAL(selected_signal(SudokuCell*)), 55 | this, SLOT(light_value())); 56 | 57 | connect(cell, SIGNAL(value_changed(int,int,bool,IntList,bool,IntList)), 58 | this, SLOT(value_changed(int,int,bool,IntList,bool,IntList))); 59 | 60 | connect(cell, SIGNAL(value_changed(int,int,bool,IntList,bool,IntList)), 61 | this, SLOT(light_value())); 62 | 63 | connect(cell, SIGNAL(free_signal()), 64 | this, SLOT(free_selection())); 65 | 66 | connect(cell, SIGNAL(move_focus(int)), 67 | this, SLOT(move_focus(int))); 68 | 69 | for(int i = 0; i != cell_span; ++i) 70 | { 71 | connect(cell, SIGNAL(selected_signal(SudokuCell*)), 72 | cells[i * cell_span + c], SLOT(vertical_selected())); 73 | 74 | connect(cell, SIGNAL(selected_signal(SudokuCell*)), 75 | cells[r * cell_span + i], SLOT(horizontal_selected())); 76 | } 77 | } 78 | 79 | // set layer style 80 | top_layer->setSpacing(GRID_SPACING); 81 | top_layer->setMargin(GRID_SPACING); 82 | setStyleSheet(QString("background-color: ") + GRID_BG_COLOR + ";"); 83 | 84 | int size = (fixed_size + 1) * cell_span + cell_size * GRID_SPACING; 85 | setFixedSize(size, size); 86 | } 87 | 88 | SudokuGrid::~SudokuGrid() 89 | { 90 | } 91 | 92 | void SudokuGrid::cell_selected(SudokuCell *cell) 93 | { 94 | if(cell != current_selected) 95 | { 96 | free_selection(); 97 | current_selected = cell; 98 | cell->setFocus(); 99 | emit update_digit_signal(cell->get_candidates()); 100 | } 101 | } 102 | 103 | void SudokuGrid::add_value(int v) 104 | { 105 | if(current_selected && !current_selected->is_initial_status()) 106 | current_selected->add_value(v); 107 | } 108 | 109 | void SudokuGrid::set_value(int v) 110 | { 111 | if(current_selected && !current_selected->is_initial_status()) 112 | current_selected->set_value(v); 113 | } 114 | 115 | void SudokuGrid::remove_value(int v) 116 | { 117 | if(current_selected && !current_selected->is_initial_status()) 118 | current_selected->remove_value(v); 119 | } 120 | 121 | void SudokuGrid::game_start() 122 | { 123 | free_selection(); 124 | if(current_level) 125 | *sudoku = Sudoku::generate(cell_size, current_level); 126 | else sudoku->clear(); 127 | initial_sudoku = *sudoku; 128 | game_reset(); 129 | } 130 | 131 | void SudokuGrid::game_reset() 132 | { 133 | free_selection(); 134 | *sudoku = initial_sudoku; 135 | for(int r = 0; r != cell_span; ++r) 136 | for(int c = 0; c != cell_span; ++c) 137 | { 138 | int id = r * cell_span + c; 139 | cells[id]->set_initial_status(initial_sudoku.get(r, c)); 140 | } 141 | 142 | actions.reset(); 143 | emit set_forward_enable(false); 144 | emit set_backward_enable(false); 145 | emit update_digit_signal(IntList(cell_span + 1, 0)); 146 | } 147 | 148 | void SudokuGrid::game_solve() 149 | { 150 | Sudoku hint_sudoku = sudoku->solve(); 151 | if(hint_sudoku.is_empty()) 152 | { 153 | QMessageBox::warning(parentWidget(), "Error", "Your current status leads to no solution."); 154 | } else { 155 | for(int r = 0; r != cell_span; ++r) 156 | for(int c = 0; c != cell_span; ++c) 157 | if(sudoku->get(r, c) == 0) 158 | cells[r * cell_span + c]->set_value(hint_sudoku.get(r, c)); 159 | } 160 | } 161 | 162 | void SudokuGrid::game_hint() 163 | { 164 | Sudoku hint_sudoku = sudoku->solve(); 165 | if(hint_sudoku.is_empty()) 166 | { 167 | free_selection(); 168 | 169 | hint_sudoku = initial_sudoku.solve(); 170 | for(int r = 0; r != cell_span; ++r) 171 | for(int c = 0; c != cell_span; ++c) 172 | { 173 | int val = sudoku->get(r, c); 174 | if(val && val != hint_sudoku.get(r, c)) 175 | cells[r * cell_span + c]->light_value(val); 176 | } 177 | } else { 178 | IntList empty_cells; 179 | for(int r = 0; r != cell_span; ++r) 180 | for(int c = 0; c != cell_span; ++c) 181 | if(sudoku->get(r, c) == 0) 182 | empty_cells.push_back(r * cell_span + c); 183 | 184 | if(empty_cells.empty()) 185 | { 186 | // already solved 187 | return; 188 | } 189 | 190 | int id = empty_cells[std::rand() % empty_cells.size()]; 191 | int r = id / cell_span, c = id % cell_span; 192 | cells[id]->emit_selected_signal(); 193 | cells[id]->set_value(hint_sudoku.get(r, c)); 194 | } 195 | } 196 | 197 | void SudokuGrid::clear_grid() 198 | { 199 | if(current_selected && !current_selected->is_initial_status()) 200 | current_selected->clear_values(); 201 | } 202 | 203 | void SudokuGrid::value_changed( 204 | int r, int c, 205 | bool value_settled_old, IntList candidates_old, 206 | bool value_settled_new, IntList candidates_new) 207 | { 208 | actions.add_action(r, c, 209 | value_settled_old, candidates_old, 210 | value_settled_new, candidates_new); 211 | 212 | if(sudoku->is_solved()) 213 | { 214 | emit game_over_signal(); 215 | } else { 216 | emit set_backward_enable(actions.is_backwardable()); 217 | emit set_forward_enable(actions.is_forwardable()); 218 | emit update_digit_signal(candidates_new); 219 | } 220 | } 221 | 222 | void SudokuGrid::backward_step() 223 | { 224 | ActionInfo action = actions.backward(); 225 | 226 | if(action.row < 0) 227 | return; 228 | 229 | int id = action.row * cell_span + action.col; 230 | cells[id]->recover_status(action.value_settled_old, action.candidates_old); 231 | cells[id]->emit_selected_signal(); 232 | 233 | emit set_backward_enable(actions.is_backwardable()); 234 | emit set_forward_enable(actions.is_forwardable()); 235 | emit update_digit_signal(cells[id]->get_candidates()); 236 | } 237 | 238 | void SudokuGrid::forward_step() 239 | { 240 | ActionInfo action = actions.forward(); 241 | 242 | if(action.row < 0) 243 | return; 244 | 245 | int id = action.row * cell_span + action.col; 246 | cells[id]->recover_status(action.value_settled_new, action.candidates_new); 247 | cells[id]->emit_selected_signal(); 248 | 249 | emit set_backward_enable(actions.is_backwardable()); 250 | emit set_forward_enable(actions.is_forwardable()); 251 | emit update_digit_signal(cells[id]->get_candidates()); 252 | } 253 | 254 | void SudokuGrid::light_value() 255 | { 256 | if(current_selected) 257 | { 258 | int value = current_selected->get_value(); 259 | for(SudokuCell* cell : cells) 260 | cell->light_value(value); 261 | } 262 | } 263 | 264 | void SudokuGrid::free_selection() 265 | { 266 | if(current_selected) 267 | { 268 | int row = current_selected->get_row(), 269 | col = current_selected->get_col(); 270 | for(int i = 0; i != cell_span; ++i) 271 | { 272 | cells[row * cell_span + i]->free_selection(); 273 | cells[i * cell_span + col]->free_selection(); 274 | } 275 | 276 | for(SudokuCell* cell : cells) 277 | cell->light_value(0); 278 | 279 | emit update_digit_signal(IntList(cell_span + 1, 0)); 280 | } 281 | 282 | current_selected = nullptr; 283 | } 284 | 285 | void SudokuGrid::level_changed(int index) 286 | { 287 | current_level = index; 288 | } 289 | 290 | void SudokuGrid::move_focus(int key) 291 | { 292 | if(!current_selected) 293 | return; 294 | 295 | int row = current_selected->get_row(); 296 | int col = current_selected->get_col(); 297 | int dr = 0, dc = 0; 298 | 299 | if(key == Qt::Key_Left) dc = -1; 300 | else if(key == Qt::Key_Right) dc = 1; 301 | else if(key == Qt::Key_Up) dr = -1; 302 | else if(key == Qt::Key_Down) dr = 1; 303 | 304 | row += dr, col += dc; 305 | 306 | if(key == Qt::Key_Tab) 307 | { 308 | if(++col == cell_span) 309 | col = 0, ++row; 310 | if(row == cell_span) 311 | col = row = 0; 312 | } 313 | 314 | if(0 <= row && row < cell_span && 0 <= col && col < cell_span) 315 | { 316 | int id = row * cell_span + col; 317 | cells[id]->emit_selected_signal(); 318 | } 319 | } 320 | 321 | SudokuCell* SudokuGrid::get_current_selected() const 322 | { 323 | return current_selected; 324 | } 325 | -------------------------------------------------------------------------------- /sudoku/sudoku_grid.h: -------------------------------------------------------------------------------- 1 | #ifndef SUDOKU_GRID_H 2 | #define SUDOKU_GRID_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include "sudoku.h" 8 | #include "sudoku_cell.h" 9 | #include "action_queue.h" 10 | 11 | class SudokuGrid : public QLabel 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | explicit SudokuGrid(int cell_size, int fixed_size, QWidget *parent = 0); 17 | ~SudokuGrid(); 18 | 19 | SudokuCell *get_current_selected() const; 20 | 21 | signals: 22 | void set_forward_enable(bool); 23 | void set_backward_enable(bool); 24 | void update_digit_signal(IntList); 25 | void game_over_signal(); 26 | 27 | public slots: 28 | void cell_selected(SudokuCell*); 29 | void add_value(int); 30 | void set_value(int); 31 | void remove_value(int); 32 | void clear_grid(); 33 | 34 | void game_start(); 35 | void game_reset(); 36 | void game_hint(); 37 | void game_solve(); 38 | 39 | void move_focus(int); 40 | void backward_step(); 41 | void forward_step(); 42 | void value_changed(int, int, bool, IntList, bool, IntList); 43 | void level_changed(int); 44 | 45 | void light_value(); 46 | void free_selection(); 47 | 48 | private: 49 | int current_level; 50 | int cell_size, cell_span, fixed_size; 51 | QGridLayout *top_layer; 52 | SudokuCell *current_selected; 53 | std::vector grids; 54 | std::vector cells; 55 | 56 | Sudoku initial_sudoku; 57 | std::shared_ptr sudoku; 58 | ActionQueue actions; 59 | }; 60 | 61 | #endif // SUDOKU_GRID_H 62 | -------------------------------------------------------------------------------- /sudoku/timer.cpp: -------------------------------------------------------------------------------- 1 | #include "timer.h" 2 | #include "config.h" 3 | #include "utils.h" 4 | 5 | Timer::Timer(QWidget *parent) 6 | : QLabel(parent), 7 | clock_now(0) 8 | { 9 | timer = new QTimer(this); 10 | timer->setInterval(1000); 11 | 12 | connect(timer, SIGNAL(timeout()), this, SLOT(timeout())); 13 | 14 | set_time("00:00:00"); 15 | setStyleSheet("font-size: 18pt;"); 16 | } 17 | 18 | void Timer::restart_timer() 19 | { 20 | set_time("00:00:00"); 21 | clock_now = 0; 22 | timer->stop(); 23 | timer->start(); 24 | } 25 | 26 | void Timer::toggle_timer() 27 | { 28 | if(timer->isActive()) 29 | { 30 | timer->stop(); 31 | } else { 32 | timer->start(); 33 | } 34 | } 35 | 36 | QString Timer::get_time() const 37 | { 38 | return QDateTime::fromTime_t(clock_now).toUTC().toString("hh:mm:ss"); 39 | } 40 | 41 | void Timer::timeout() 42 | { 43 | ++clock_now; 44 | set_time(get_time()); 45 | } 46 | 47 | void Timer::set_time(QString time) 48 | { 49 | setText(time); 50 | } 51 | -------------------------------------------------------------------------------- /sudoku/timer.h: -------------------------------------------------------------------------------- 1 | #ifndef TIMER_H 2 | #define TIMER_H 3 | 4 | #include 5 | #include 6 | 7 | class Timer : public QLabel 8 | { 9 | Q_OBJECT 10 | public: 11 | explicit Timer(QWidget *parent = 0); 12 | 13 | void stop_timer(); 14 | QString get_time() const; 15 | 16 | signals: 17 | 18 | public slots: 19 | void restart_timer(); 20 | void toggle_timer(); 21 | void timeout(); 22 | 23 | private: 24 | void set_time(QString); 25 | 26 | private: 27 | QTimer *timer; 28 | int clock_now; 29 | }; 30 | 31 | #endif // TIMER_H 32 | -------------------------------------------------------------------------------- /sudoku/tool_button.cpp: -------------------------------------------------------------------------------- 1 | #include "tool_button.h" 2 | #include "config.h" 3 | 4 | ToolButton::ToolButton(QWidget *parent) 5 | : QPushButton(parent) 6 | { 7 | is_mouse_over = false; 8 | is_mouse_pressed = false; 9 | setFocusPolicy(Qt::NoFocus); 10 | setFixedSize(TOOL_FIXED_SIZE, TOOL_FIXED_SIZE); 11 | setContentsMargins(3, 0, 3, 0); 12 | setAttribute(Qt::WA_Hover, true); 13 | } 14 | 15 | void ToolButton::set_image(QString path) 16 | { 17 | image_path = path; 18 | update_style(); 19 | } 20 | 21 | void ToolButton::update_style() 22 | { 23 | update(); 24 | } 25 | 26 | void ToolButton::paintEvent(QPaintEvent *) 27 | { 28 | QRect rect = this->rect(); 29 | 30 | QPainter p(this); 31 | 32 | QPixmap bg(image_path); 33 | bg = bg.scaled(rect.width(), rect.height(), 34 | Qt::IgnoreAspectRatio, Qt::SmoothTransformation); 35 | if(!isEnabled()) 36 | { 37 | QImage img = bg.toImage(); 38 | p.drawImage(0, 0, img); 39 | } else { 40 | p.drawPixmap(QPoint(0, 0), bg); 41 | if(is_mouse_pressed) 42 | { 43 | p.setCompositionMode(QPainter::CompositionMode_Multiply); 44 | p.drawPixmap(QPoint(0, 0), bg); 45 | } else if(is_mouse_over) { 46 | p.setCompositionMode(QPainter::CompositionMode_HardLight); 47 | p.drawPixmap(QPoint(0, 0), bg); 48 | } 49 | } 50 | } 51 | 52 | void ToolButton::mousePressEvent(QMouseEvent *ev) 53 | { 54 | if(ev->button() == Qt::LeftButton) 55 | is_mouse_pressed = true; 56 | QPushButton::mousePressEvent(ev); 57 | } 58 | 59 | void ToolButton::mouseReleaseEvent(QMouseEvent *ev) 60 | { 61 | if(ev->button() == Qt::LeftButton) 62 | is_mouse_pressed = false; 63 | QPushButton::mouseReleaseEvent(ev); 64 | } 65 | 66 | void ToolButton::enterEvent(QEvent *ev) 67 | { 68 | is_mouse_over = true; 69 | QPushButton::enterEvent(ev); 70 | } 71 | 72 | void ToolButton::leaveEvent(QEvent *ev) 73 | { 74 | is_mouse_over = false; 75 | QPushButton::leaveEvent(ev); 76 | } 77 | -------------------------------------------------------------------------------- /sudoku/tool_button.h: -------------------------------------------------------------------------------- 1 | #ifndef TOOLBUTTON_H 2 | #define TOOLBUTTON_H 3 | 4 | #include 5 | 6 | class ToolButton : public QPushButton 7 | { 8 | Q_OBJECT 9 | public: 10 | ToolButton(QWidget* parent = 0); 11 | 12 | void set_image(QString); 13 | signals: 14 | 15 | public slots: 16 | 17 | protected: 18 | void paintEvent(QPaintEvent *); 19 | void mousePressEvent(QMouseEvent *); 20 | void mouseReleaseEvent(QMouseEvent *); 21 | void enterEvent(QEvent *); 22 | void leaveEvent(QEvent *); 23 | 24 | private: 25 | void update_style(); 26 | 27 | private: 28 | bool is_mouse_pressed, is_mouse_over; 29 | QString image_path; 30 | }; 31 | 32 | #endif // TOOLBUTTON_H 33 | -------------------------------------------------------------------------------- /sudoku/utils.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "utils.h" 3 | 4 | QFont fit_font_with_text(QFont font, QString text, QRect rect_lbl) 5 | { 6 | /* This function is modified from 7 | * https://stackoverflow.com/questions/42652738/how-to-automatically-increase-decrease-text-size-in-label-in-qt 8 | */ 9 | 10 | // get initial settings 11 | if(text.length() == 0) 12 | return font; 13 | 14 | int size = font.pointSize(); 15 | QFontMetrics fm(font); 16 | QRect rect = fm.boundingRect(rect_lbl, Qt::TextWordWrap, text); 17 | 18 | // decide whether to increase or decrease 19 | int step = rect.height() > rect_lbl.height() ? -1 : 1; 20 | 21 | // iterate until text fits best into rectangle of label 22 | for (;;) 23 | { 24 | font.setPointSize(size + step); 25 | QFontMetrics fm(font); 26 | rect = fm.boundingRect(rect_lbl, Qt::TextWordWrap, text); 27 | if (size <= 1) 28 | { 29 | // Font cannot be made smaller 30 | break; 31 | } 32 | if (step < 0) 33 | { 34 | size += step; 35 | if (rect.height() < rect_lbl.height()) break; 36 | } else { 37 | if (rect.height() > rect_lbl.height()) break; 38 | size += step; 39 | } 40 | } 41 | 42 | // apply result of iteration 43 | font.setPointSize(size); 44 | return font; 45 | } 46 | -------------------------------------------------------------------------------- /sudoku/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | typedef std::vector IntList; 10 | typedef std::vector IntMatrix; 11 | 12 | QFont fit_font_with_text(QFont font, QString text, QRect rect); 13 | 14 | #endif // UTILS_H 15 | -------------------------------------------------------------------------------- /wiki-index/crawler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import urllib.parse 5 | import urllib.request 6 | import re 7 | from bs4 import BeautifulSoup 8 | 9 | user_agent = r'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36' 10 | 11 | def get_wiki(title): 12 | headers = { 'User-Agent': user_agent } 13 | 14 | url = 'http://en.wikipedia.org/wiki/' + title.replace(' ', '_') 15 | request = urllib.request.Request(url, headers=headers) 16 | response = urllib.request.urlopen(request) 17 | return response.read().decode('utf-8') 18 | 19 | def get_sub_info(item_td): 20 | if item_td.find('div', class_='plainlist'): 21 | return [ li.get_text().strip() for li in item_td.find_all('li') ] 22 | elif item_td.find('a', class_='image'): 23 | return '!?image:' + item_td.img['src'] 24 | return item_td.get_text().strip() 25 | 26 | def get_infobox(html_page): 27 | html_page = re.sub(r'.*?', '', html_page) 28 | soup = BeautifulSoup(html_page, 'html.parser') 29 | 30 | for hidden in soup.find_all(style='display:none'): 31 | hidden.decompose() 32 | 33 | infobox = soup.find(class_='infobox') 34 | if not infobox: return None, None 35 | 36 | def get_info(infobox): 37 | info = {} 38 | now_info = {} 39 | 40 | caption = infobox.find('caption', recursive=False) 41 | if caption: 42 | pass 43 | info[caption.get_text()] = now_info 44 | caption.decompose() 45 | 46 | for item in infobox.find_all('tr'): 47 | if isinstance(item, str): 48 | continue 49 | 50 | th = item.find('th', recursive=False) 51 | td = item.find('td', recursive=False) 52 | 53 | if th and td: 54 | key = th.get_text() 55 | now_info[key] = get_sub_info(td) 56 | elif th: 57 | now_info = {} 58 | info[th.get_text()] = now_info 59 | elif td: 60 | image = td.find(class_='image') 61 | if image: 62 | now_info['image'] = image.img['src'] 63 | elif td.find('div', class_='NavContent'): 64 | pass 65 | # title = td.find(class_='NavHead').get_text().replace('[hide]', '') 66 | # info[title] = { 'sub_table': get_info(td.table) } 67 | elif td.find('table'): 68 | sub_table = td.find('table') 69 | sub_info = get_info(sub_table) 70 | now_info['sub_table'] = sub_info 71 | elif td.find('ul', recursive=False): 72 | now_info['sub_list'] = [ li.get_text() for li in td.find_all('li') ] 73 | else: 74 | now_info = {} 75 | info[td.get_text()] = now_info 76 | 77 | empty_keys = [ k for k, v in info.items() if not v ] 78 | for key in empty_keys: 79 | del info[key] 80 | return info 81 | 82 | title = soup.title.string.replace(' - Wikipedia', '') 83 | return title, get_info(infobox) 84 | 85 | def get_people_list(html_page): 86 | soup = BeautifulSoup(html_page, 'html.parser') 87 | 88 | peoples = {} 89 | for item in soup.find(id='mw-content-text').find_all('li'): 90 | links = item.find_all('a') 91 | if links and 'href' in links[0].attrs and links[0]['href'].startswith('/wiki'): 92 | peoples[links[0].get_text()] = links[0]['href'] 93 | return peoples 94 | -------------------------------------------------------------------------------- /wiki-index/db.py: -------------------------------------------------------------------------------- 1 | #!/use/bin/python3 2 | # -*- coding: utf8 -*- 3 | import sqlite3 4 | import crawler 5 | import json 6 | import fileinput 7 | 8 | conn = sqlite3.connect('server/db.sqlite3') 9 | 10 | def get_query_info(infobox): 11 | if isinstance(infobox, str): 12 | if infobox.startswith('!?image'): 13 | return "" 14 | return infobox 15 | elif isinstance(infobox, list): 16 | return ' '.join(infobox) 17 | 18 | s = "" 19 | for k, v in infobox.items(): 20 | if k == 'image': 21 | continue 22 | s += " " + get_query_info(v) 23 | return s 24 | 25 | 26 | def add_info(c, filename): 27 | data = open(filename, 'r').read() 28 | title, infobox = crawler.get_infobox(data) 29 | if not infobox: return 30 | 31 | print(infobox) 32 | 33 | s = c.execute('SELECT id FROM `infobox_infobox` WHERE title=?', [title]) 34 | if s.fetchone() is None: 35 | c.execute('INSERT INTO `infobox_infobox` (title, json_str, query_str) VALUES (?, ?, ?)', 36 | [title, json.dumps(infobox), title + " " + get_query_info(infobox)]) 37 | else: print("ERROR") 38 | 39 | filenames = [ f for f in fileinput.input() ] 40 | 41 | c = conn.cursor() 42 | cnt = 0 43 | for f in filenames: 44 | try: 45 | add_info(c, f.strip()) 46 | print('\r%d/%d, %s' % (cnt, len(filenames), f), end='') 47 | except: 48 | pass 49 | cnt += 1 50 | if cnt % 100 == 0: 51 | c.execute('COMMIT') 52 | #f = input() 53 | #add_info(f) 54 | -------------------------------------------------------------------------------- /wiki-index/doc/doc.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/doc.pdf -------------------------------------------------------------------------------- /wiki-index/doc/doc.tex: -------------------------------------------------------------------------------- 1 | \XeTeXlinebreaklocale "zh" 2 | \XeTeXlinebreakskip = 0pt plus 1pt 3 | 4 | \documentclass[11pt,a4paper]{article} 5 | 6 | \usepackage{xltxtra,fontspec,xunicode} 7 | \usepackage{amsthm, amsmath, amssymb, amsfonts} 8 | \usepackage{abstract} 9 | \usepackage{subcaption} 10 | \usepackage{graphicx,float} 11 | \usepackage{minted} 12 | 13 | \defaultfontfeatures{Mapping=tex-text,Scale=MatchLowercase} 14 | \setmainfont{DejaVu Serif} 15 | \setsansfont{DejaVu Sans} 16 | \setmonofont{DejaVu Sans Mono} 17 | \usepackage[slantfont,boldfont]{xeCJK} % 允许斜体和粗体 18 | \setCJKmainfont{WenQuanYi Zen Hei} 19 | \setCJKsansfont{WenQuanYi Zen Hei} 20 | \setCJKmonofont{WenQuanYi Zen Hei Mono} 21 | 22 | \usepackage{titling} 23 | 24 | \renewcommand\refname{参考文献} 25 | 26 | \title{人物信息检索系统的功能及其实现} 27 | \author{周聿浩\\ \small{2016011347}} 28 | 29 | \begin{document} 30 | \maketitle 31 | \section{简介} 32 | 这是一个利用Django搭建的一个人物信息检索系统,大约从Wikipedia爬取了10000个人物信息,并且提取了其中Infobox的对应信息。 33 | 34 | \begin{figure}[H] 35 | \centering 36 | \begin{subfigure}{.49\textwidth} 37 | \centering 38 | \includegraphics[width=\linewidth]{wiki.png} 39 | \end{subfigure} 40 | \hfill 41 | \begin{subfigure}{.49\textwidth} 42 | \centering 43 | \includegraphics[width=\linewidth]{turing-1.png} 44 | \end{subfigure} 45 | \caption{对于Wikipedia中爬取的信息,我们重新组织了其格式并且进行显示。} 46 | \end{figure} 47 | 48 | \begin{figure}[H] 49 | \centering 50 | \begin{subfigure}{.49\textwidth} 51 | \centering 52 | \includegraphics[width=\linewidth]{img-1.png} 53 | \end{subfigure} 54 | \hfill 55 | \begin{subfigure}{.49\textwidth} 56 | \centering 57 | \includegraphics[width=\linewidth]{img-2.png} 58 | \end{subfigure} 59 | \caption{左侧为搜索页面,右侧为搜索结果,匹配的字段被高亮显示。} 60 | \end{figure} 61 | 62 | 对于已经爬取的信息,我们提供了一个对其进行搜索的页面,可以根据关键词在其中搜索,并且还可以根据原先Infobox中的标题进行特定字段的查询(例如Born、Died、Name、Nationality等),同时还可以让用户自行添加可以查询的字段。 63 | 64 | 搜索的结果按照匹配的关键字个数从高到底排序后显示,如果结果过多将会分页显示。同时匹配的关键字会被高亮标出。 65 | 66 | \begin{figure}[H] 67 | \centering 68 | \begin{subfigure}{.49\textwidth} 69 | \centering 70 | \includegraphics[width=\linewidth]{page.png} 71 | \end{subfigure} 72 | \hfill 73 | \begin{subfigure}{.49\textwidth} 74 | \centering 75 | \includegraphics[width=\linewidth]{title-born.png} 76 | \end{subfigure} 77 | \caption{左侧为搜索结果过多时的分页显示效果,右侧为按照字段搜索Born中含1997的人物结果。} 78 | \end{figure} 79 | 80 | 81 | \begin{figure}[H] 82 | \centering 83 | \begin{subfigure}{.49\textwidth} 84 | \centering 85 | \includegraphics[width=\linewidth]{turing-1.png} 86 | \end{subfigure} 87 | \hfill 88 | \begin{subfigure}{.49\textwidth} 89 | \centering 90 | \includegraphics[width=\linewidth]{turing-2.png} 91 | \end{subfigure} 92 | \vspace{0.5cm} 93 | \begin{subfigure}{.49\textwidth} 94 | \centering 95 | \includegraphics[width=\linewidth]{turing-3.png} 96 | \end{subfigure} 97 | \hfill 98 | \begin{subfigure}{.49\textwidth} 99 | \centering 100 | \includegraphics[width=\linewidth]{turing-4.png} 101 | \end{subfigure} 102 | \caption{Alan Turing信息的展现。} 103 | \end{figure} 104 | 105 | \section{部分实现细节} 106 | 爬虫部分利用BeautifulSoup来处理获取的页面,提取Infobox中的信息。 107 | 108 | 具体来说,人物超链接的爬取是通过寻找ID为{\it mw-content-text}的元素下所有{\it li}标签的第一个超链接来实现的。在爬取完毕后检查是否存在infobox,如果存在则开始提取信息。由于其中信息具有一定规律(例如大部分信息是以标题、内容的形式来组织的),只需要用BeautifulSoup提取相应的{\it }以及{\it }部分即可。 109 | 110 | 前端界面利用Bootstrap来优化显示效果。 111 | 112 | 关于数据的存储,在提取出信息后利用JSON来保存在sqlite数据库中,并且额外提取出一个关键字字符串用于搜索。对于每个人物都会分配一个唯一的ID以方便索引。 113 | 114 | 分页功能利用了Django自带的Paginator类。查询关键词的高亮以及自定义字段搜索框的增加与删除使用Javascript在前端完成。 115 | \end{document} 116 | -------------------------------------------------------------------------------- /wiki-index/doc/img-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/img-1.png -------------------------------------------------------------------------------- /wiki-index/doc/img-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/img-2.png -------------------------------------------------------------------------------- /wiki-index/doc/page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/page.png -------------------------------------------------------------------------------- /wiki-index/doc/title-born.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/title-born.png -------------------------------------------------------------------------------- /wiki-index/doc/turing-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/turing-1.png -------------------------------------------------------------------------------- /wiki-index/doc/turing-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/turing-2.png -------------------------------------------------------------------------------- /wiki-index/doc/turing-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/turing-3.png -------------------------------------------------------------------------------- /wiki-index/doc/turing-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/turing-4.png -------------------------------------------------------------------------------- /wiki-index/doc/wiki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/doc/wiki.png -------------------------------------------------------------------------------- /wiki-index/get_list.py: -------------------------------------------------------------------------------- 1 | import crawler 2 | 3 | peoples = {} 4 | lists = crawler.get_people_list(crawler.get_wiki('Lists of people by occupation')) 5 | 6 | for k, v in lists.items(): 7 | if v.startswith('/wiki/List_of'): 8 | l = crawler.get_people_list(crawler.get_wiki(v[6:])) 9 | peoples = {**peoples, **l} 10 | print('\r%d' % len(peoples)) 11 | if len(peoples) > 30000: break 12 | 13 | cnt = 0 14 | 15 | for k, v in peoples.items(): 16 | if cnt > 8700: 17 | try: 18 | wiki_page = crawler.get_wiki(v[6:]) 19 | f = open('data/' + k + '.wiki', 'w') 20 | f.write(wiki_page) 21 | f.close() 22 | except: 23 | print("ERROR") 24 | cnt += 1 25 | print('\r%s %d/%d' % (k, cnt, len(peoples)), end='') 26 | -------------------------------------------------------------------------------- /wiki-index/server/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/server/db.sqlite3 -------------------------------------------------------------------------------- /wiki-index/server/infobox/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/server/infobox/__init__.py -------------------------------------------------------------------------------- /wiki-index/server/infobox/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class InfoboxConfig(AppConfig): 5 | name = 'infobox' 6 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.5 on 2017-09-09 14:15 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Infobox', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('query_str', models.TextField()), 21 | ('json_str', models.TextField()), 22 | ('title', models.TextField()), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/server/infobox/migrations/__init__.py -------------------------------------------------------------------------------- /wiki-index/server/infobox/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf8 -*- 3 | 4 | from django.db import models 5 | 6 | class Infobox(models.Model): 7 | query_str = models.TextField() 8 | json_str = models.TextField() 9 | title = models.TextField() 10 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/templates/bootstrap_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/templates/highlight.js: -------------------------------------------------------------------------------- 1 | function highlight_keywords(keywords) 2 | { 3 | var pattern = new RegExp('(' + keywords.join('|') + ')', 'gi'); 4 | var infos = $('.info'); 5 | for(var i = 0; i < infos.length; ++i) 6 | { 7 | var src_str = $(infos[i]).html(); 8 | src_str = src_str.replace(pattern, '$1') 9 | .replace(/([^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, '$1$2$4'); 10 | $(infos[i]).html(src_str); 11 | } 12 | } 13 | 14 | $('document').ready( function() { 15 | highlight_keywords(search_keywords); 16 | } ); 17 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include 'bootstrap_header.html' %} 5 | 8 | Index 9 | 10 | 11 |
12 |
 
13 | {% include 'search_box.html' %} 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/templates/person.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include 'bootstrap_header.html' %} 5 | {% if highlight %} 6 | 12 | {% endif %} 13 | {{ person_name }} 14 | 15 | 16 |
17 | {% include 'person_item.html' %} 18 |
19 | 33 |
34 | {% include 'search_box.html' %} 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/templates/person_item.html: -------------------------------------------------------------------------------- 1 | 2 | {% autoescape off %} 3 | {% for k, v0 in person.items %} 4 | {% if k != 'not_display' %} 5 | 6 | 7 | 8 | {% endif %} 9 | {% load filter %} 10 | {% for k, v in v0.items %} 11 | {% if forloop.first and v0.image %} 12 | 13 | 16 | 17 | {% endif %} 18 | {% if k != 'image' %} 19 | 20 | {% if v|get_type == 'dict' %} 21 | 26 | {% elif k == 'sub_list' %} 27 | 30 | {% else %} 31 | 32 | 41 | {% endif %} 42 | 43 | {% endif %} 44 | {% endfor %} 45 | {% endfor %} 46 | {% endautoescape %} 47 |
{{ k|title }}
14 | 15 |
22 | {% with v as person %} 23 | {% include 'person_item.html' %} 24 | {% endwith %} 25 | 28 |
    {% for item in v %}
  • {{ item|strip }}
  • {% endfor %}
29 |
{{ k|strip|title }} 33 | {% if v|get_type == 'list' %} 34 |
    {% for item in v %}
  • {{ item|strip }}
  • {% endfor %}
35 | {% elif v|is_image %} 36 | 37 | {% else %} 38 | {{ v|strip }} 39 | {% endif %} 40 |
48 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/templates/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include 'bootstrap_header.html' %} 5 | 37 | Search 38 | 39 | 40 |
41 | 42 | {% for info in results %} 43 | 44 | 50 | 51 | {% endfor %} 52 |
45 | {{ info.person_name }} 46 | {% with info.person as person %} 47 | {% include 'person_item.html' %} 48 | {% endwith %} 49 |
53 | 54 | {% if results.has_other_pages %} 55 |
56 |
    57 | {% if results.has_previous %} 58 |
  • 59 | 60 | 61 | Previous 62 | 63 |
  • 64 | {% endif %} 65 | {% for i in show_page_range %} 66 | {% if i == results.number %} 67 |
  • {{ i }}
  • 68 | {% else %} 69 |
  • {{ i }}
  • 70 | {% endif %} 71 | {% endfor %} 72 | {% if results.has_next %} 73 |
  • 74 | 75 | 76 | Next 77 | 78 |
  • 79 | {% endif %} 80 |
81 |
82 | {% endif %} 83 | {% include 'search_box.html' %} 84 |
85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/templates/search_box.html: -------------------------------------------------------------------------------- 1 | 34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 | 49 |
50 | 51 |
52 |
53 |
54 | 55 |
56 | 57 |
58 |
59 |
60 | 61 |
62 | 63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/server/infobox/templatetags/__init__.py -------------------------------------------------------------------------------- /wiki-index/server/infobox/templatetags/filter.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | @register.filter 6 | def get_type(value): 7 | return type(value).__name__ 8 | 9 | @register.filter 10 | def strip(value): 11 | if isinstance(value, str): 12 | return value.strip().replace('\n', '
') 13 | return value 14 | 15 | @register.filter 16 | def is_image(s): 17 | return s.startswith('!?image:') 18 | 19 | @register.filter 20 | def cut_image(s): 21 | return s.replace('!?image:', '') 22 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.contrib import admin 3 | 4 | from . import views 5 | 6 | urlpatterns = [ 7 | url(r'^$', views.index, name='index'), 8 | url(r'^person/(?P\d+)$', views.person, name='person'), 9 | url(r'^search$', views.search, name='search'), 10 | ] 11 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/utils.py: -------------------------------------------------------------------------------- 1 | from .models import Infobox 2 | import json 3 | 4 | def merge_list(list1, list2): 5 | for v in list2: 6 | if v not in list1: 7 | list1.append(v) 8 | return list1 9 | 10 | def split_keywords(keywords): 11 | keys = [] 12 | for k in keywords: 13 | keys = merge_list(keys, k.split(' ')) 14 | if '' in keys: keys.remove('') 15 | return keys 16 | 17 | def search_by_keywords_meta(keywords, search_str): 18 | matched = {} 19 | for index, keyword in enumerate(keywords): 20 | search_dict = { search_str: keyword } 21 | for info in Infobox.objects.filter(**search_dict): 22 | if info.id not in matched: 23 | matched[info.id] = { 'keywords': [index], 'object': info } 24 | else: 25 | matched[info.id]['keywords'].append(index) 26 | return matched 27 | 28 | def search_by_keywords(keywords): 29 | return search_by_keywords_meta(keywords, 'query_str__contains') 30 | 31 | def search_by_name(keywords): 32 | return search_by_keywords_meta(keywords, 'title__contains') 33 | 34 | def search_key(key, info): 35 | for k, v in info.items(): 36 | if k.lower() == key.lower(): 37 | return v 38 | elif isinstance(v, dict): 39 | ret = search_key(key, v) 40 | if ret is not None: 41 | return ret 42 | return None 43 | 44 | def search_value(keywords, info): 45 | def one_of(keywords, string): 46 | matched = [] 47 | for index, key in enumerate(keywords): 48 | if key.lower() in string.lower(): 49 | matched.append(index) 50 | return matched 51 | 52 | if not info: return [] 53 | 54 | matched = [] 55 | if isinstance(info, dict): 56 | for elem in info.values(): 57 | matched = merge_list(matched, search_value(keywords, elem)) 58 | elif isinstance(info, list): 59 | for elem in info: 60 | matched = merge_list(matched, search_value(keywords, elem)) 61 | else: matched = one_of(keywords, info) 62 | return matched 63 | 64 | def search_matched_value(keywords, info): 65 | def one_of(keywords, string): 66 | for index, key in enumerate(keywords): 67 | if key.lower() in string.lower(): 68 | return True 69 | return False 70 | 71 | if not info: return [] 72 | 73 | matched_info = {} 74 | if isinstance(info, dict): 75 | for key, val in info.items(): 76 | sub_info = search_matched_value(keywords, val) 77 | if sub_info: 78 | if isinstance(sub_info, bool): 79 | matched_info[key] = val 80 | else: 81 | for k, v in sub_info.items(): 82 | if k.lower() != 'image': 83 | if not isinstance(v, str) or not v.startswith('!?'): 84 | matched_info[k] = v 85 | return matched_info 86 | elif isinstance(info, list): 87 | for elem in info: 88 | if one_of(keywords, elem): return True 89 | return False 90 | else: return one_of(keywords, info) 91 | 92 | def search_by_field(field, keywords): 93 | mismatch = [] 94 | matched = search_by_keywords(keywords) 95 | for person_id, val in matched.items(): 96 | keys, info = val['keywords'], val['object'] 97 | person = json.loads(info.json_str) 98 | 99 | field_info = search_key(field, person) 100 | matched_value = search_value(keywords, field_info) 101 | if not field_info or not matched_value: 102 | mismatch.append(person_id) 103 | else: 104 | val['keywords'] = matched_value 105 | 106 | for person_id in mismatch: 107 | del matched[person_id] 108 | return matched 109 | -------------------------------------------------------------------------------- /wiki-index/server/infobox/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.shortcuts import get_object_or_404, render 3 | from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage 4 | from .models import Infobox 5 | import json 6 | from .utils import * 7 | 8 | default_fields = [ 'born', 'died', 'nationality', 'known for' ] 9 | 10 | def render_search_result(info, fields=None, keywords=None): 11 | person_id = info.id 12 | person_name = info.title 13 | person = json.loads(info.json_str) 14 | sub_person = {} 15 | if fields: 16 | for field in fields: 17 | val = search_key(field, person) 18 | if val: sub_person[field] = val 19 | if keywords: 20 | sub_key = search_matched_value(keywords, person) 21 | for k, v in sub_key.items(): 22 | if k not in sub_person: 23 | sub_person[k] = v 24 | 25 | return { 'person_id': int(person_id), 26 | 'person_name': person_name, 27 | 'person': { 'not_display': sub_person } 28 | } 29 | 30 | def person(request, person_id): 31 | person_info = get_object_or_404(Infobox, pk=person_id) 32 | person = json.loads(person_info.json_str) 33 | return render(request, 'person.html', 34 | { 'person_id': int(person_id), 35 | 'person': person, 36 | 'person_name': person_info.title, 37 | 'highlight': request.GET.getlist('highlight') }) 38 | 39 | def index(request): 40 | return render(request, 'index.html') 41 | 42 | def search(request): 43 | keywords = split_keywords(request.GET.getlist('q')) 44 | if keywords: 45 | matched = search_by_keywords(keywords) 46 | else: matched = None 47 | 48 | fields = request.GET.getlist('f') 49 | for key in request.GET: 50 | if not key.startswith('f_'): 51 | continue 52 | 53 | field = key[2:].replace('_', ' ') 54 | f_keys = split_keywords(request.GET.getlist(key)) 55 | if not f_keys: continue 56 | 57 | if field not in fields: 58 | fields.append(field) 59 | 60 | if field != 'name': 61 | matched_f = search_by_field(field, f_keys) 62 | else: matched_f = search_by_name(f_keys) 63 | 64 | mismatch = [] 65 | if matched is not None: 66 | for key, val in matched.items(): 67 | if key not in matched_f: 68 | mismatch.append(key) 69 | else: val['keywords'].extend(matched_f[key]['keywords']) 70 | for key in mismatch: 71 | del matched[key] 72 | else: matched = matched_f 73 | 74 | keywords = merge_list(keywords, f_keys) 75 | 76 | matched = [ (val['keywords'], val['object']) for val in matched.values() ] 77 | matched.sort(key=lambda x: len(x[0]), reverse=True) 78 | 79 | if fields: 80 | fields = merge_list(fields, default_fields) 81 | results = [ render_search_result(obj, fields=fields) for _, obj in matched ] 82 | else: 83 | results = [ render_search_result(obj, fields=default_fields, keywords=keywords) for _, obj in matched ] 84 | 85 | page = int(request.GET['page']) if 'page' in request.GET else 1 86 | page_size = int(request.GET['page_size']) if 'page_size' in request.GET else 10 87 | 88 | paginator = Paginator(results, page_size) 89 | try: 90 | contents = paginator.page(page) 91 | except PageNotAnInteger: 92 | # If page is not an integer, deliver first page. 93 | contents = paginator.page(1) 94 | except EmptyPage: 95 | # If page is out of range (e.g. 9999), deliver last page of results. 96 | contents = paginator.page(paginator.num_pages) 97 | 98 | show_page_range_min = contents.number - 5 99 | show_page_range_max = contents.number + 5 100 | if show_page_range_max > paginator.num_pages: 101 | show_page_range_max = paginator.num_pages 102 | show_page_range_min = show_page_range_max - 10 103 | elif show_page_range_min <= 1: 104 | show_page_range_min = 1 105 | show_page_range_max = show_page_range_min + 10 106 | 107 | show_page_range_max = min(show_page_range_max, paginator.num_pages) 108 | show_page_range_min = max(show_page_range_min, 1) 109 | show_page_range = range(show_page_range_min, show_page_range_max + 1) 110 | 111 | return render(request, 'search.html', 112 | { 'keywords': keywords, 113 | 'results': contents, 114 | 'show_page_range': show_page_range }) 115 | -------------------------------------------------------------------------------- /wiki-index/server/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /wiki-index/server/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miskcoo/programming-training/956d07eb998b7c2fa9c036e0d67b25f1d57ba6b3/wiki-index/server/server/__init__.py -------------------------------------------------------------------------------- /wiki-index/server/server/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for server project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'z42ur6f+)+pfy3941d-6n9soe4m#x8c6u5&scu14lq4*hq(m5z' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'infobox', 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'server.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'server.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'Asia/Shanghai' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | -------------------------------------------------------------------------------- /wiki-index/server/server/urls.py: -------------------------------------------------------------------------------- 1 | """server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url, include 17 | from django.contrib import admin 18 | 19 | urlpatterns = [ 20 | # url(r'^admin/', admin.site.urls), 21 | # url(r'^$', views.index, name='index'), 22 | url(r'^', include('infobox.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /wiki-index/server/server/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for server project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") 15 | 16 | application = get_wsgi_application() 17 | --------------------------------------------------------------------------------