# 113-2 NCKU Program Design-II Homework 3 ## A Simple Online Judge System (Part 1. Main System and Account System) ## 目標 實作一個簡易版的 Online Judge System ## 本次實作功能 - 帳號系統 - 登入、註冊。 - 主頁面系統 - 列出功能清單、完成部分功能實作。 ## 架構 ``` hw3/ ├── data/ │ ├── problem/ | | └── problem.csv │ ├── user/ | | └── user.csv │ ├── msg/ │ │ └── login.txt ├── src/ │ ├── AccountSystem.cpp │ ├── AccountSystem.h │ ├── JudgeSystem.cpp │ ├── JudgeSystem.h │ ├── main.cpp │ ├── MainPage.cpp │ ├── MainPage.h │ ├── Problem.cpp │ ├── Problem.h │ ├── User.cpp │ └── User.h ``` ### class JudgeSystem 由 JudgeSystem 負責主控所有任務 #### JudgeSystem.h ```cpp= #include <iostream> #include "AccountSystem.h" #include "Problem.h" #include "MainPage.h" class JudgeSystem : private AccountSystem, private ProblemSystem, private MainPage { private: std::string USER_DATA_PATH; // 讀取 user 資料的路徑 std::string PROBLEM_DATA_PATH; // 讀取題目資料的路徑 std::string LOGIN_MSG_PATH; // 讀取訊息的路徑 std::string VERSION; // 版本 std::string status; // 記錄目前狀態 (初始化?未登入?已登入?) void loadData(); // 讀取使用者資料 (初始化) void effectLoading(std::string content); // 就是特效 :P public: JudgeSystem() = default; JudgeSystem(std::string userPath, std::string problemPath, std::string msgPath, std::string version); void initSystem(); // Function: 初始化系統 int mainPage(); // Function: 主頁面 (選單) std::string getUserpath() const { return USER_DATA_PATH; } std::string getProblempath() const { return PROBLEM_DATA_PATH; } std::string getMsgpath() const { return LOGIN_MSG_PATH; } std::string getVersion() const { return VERSION; } }; ``` ### class AccountSystem 由 AccountSystem 負責處理使用者資料相關事宜 #### AccountSystem.h ```cpp= #include <iostream> #include <vector> #include <utility> #include "User.h" class AccountSystem : private User { private: std::vector<User> user_list; // 所有使用者資料 std::string login_user; // 目前登入者 std::string USER_DATA_PATH; // 使用者資料儲存位置 void sign_up(); // Function: 註冊 void userdataUpdate(); // Function: 更新 USER_DATA_PATH 內的資料 public: void init(std::string USER_DATA_PATH); // Function: 初始化 (讀取資料) User* search(std::string name); // Function: 查詢 name 是否存在 std::pair<bool, std::string> login(); // Function: 登入 void adding_user(std::string name, std::string password); // Function:新增使用者 name,並將其密碼設定為 password std::string getuserLogin(); // Function: 獲取目前登入的使用者是誰 }; ``` ### class User 一位使用者的所需資料 #### User.h ```cpp= #include <iostream> #include <vector> class User { private: std::string username; std::string password; public: User() = default; User(std::string name, std::string passwd); std::string getUsername() const { return username; } std::string getPassword() const { return password; } }; ``` ### class ProblemSystem 負責處理題目相關事宜 (這次作業還不會使用到) #### Problem.h ```cpp= #include <iostream> class Problem { protected: std::string problem_title; // 題目名稱 std::string input_path; // 測資: 輸入路徑 std::string output_path; // 測資: 答案路徑 int magic_number; // 魔法數字 (下個作業會用到) public: Problem() = default; Problem(std::string title, std::string input, std::string output, int magic_num); std::string getTitle() const { return problem_title; } }; class ProblemSystem : public Problem { private: std::vector<Problem> problem_list; // 所有題目 void adding_problem(Problem new_problem); // Function: 將新題目資料更新至對應資料結構與變數 public: void init(std::string &PROBLEM_DATA_PATH); // Function: 初始化 (讀取題目資料) std::vector<Problem>* list_problem(); // Function: 列出所有題目 void newproblem_set(); // Function: 新增題目 }; ``` ### main.cpp 控制系統變數、啟動系統 ```cpp= #include <iostream> #include "JudgeSystem.h" #define blue(text) "\033[34m" text "\033[0m" #define USER_DATA_PATH "./data/user/user.csv" #define PROBLEM_DATA_PATH "./data/problem/problem.csv" #define LOGIN_MSG_PATH "./msg/login.txt" #define VERSION "1.0.0" int main() { #ifdef _WIN32 system("cls"); #else system("clear"); #endif JudgeSystem judge(USER_DATA_PATH, PROBLEM_DATA_PATH, LOGIN_MSG_PATH, VERSION); std::cout << blue("Simple Judge System start! Version: ") << VERSION << "\n"; int result = 0; do { result = judge.mainPage(); }while( result == 0 ); return 0; } ``` ### class MainPage 負責處理主選單相關訊息與過濾使用者輸入操作 #### MainPage.h ```cpp= #include <iostream> class MainPage { protected: void mainpagePrint(); // Function: 輸出主選單 int operationCheck(); // Function: 過濾輸入操作 }; ``` ## Show your work!! 讓我們一起來完成這一個 Judge 的部分功能吧! 首先,讓我們先從 `main.cpp` 開始看起 ```cpp= #include <iostream> #include "JudgeSystem.h" #define blue(text) "\033[34m" text "\033[0m" #define USER_DATA_PATH "./data/user/user.csv" #define PROBLEM_DATA_PATH "./data/problem/problem.csv" #define LOGIN_MSG_PATH "./msg/login.txt" #define VERSION "1.0.0" int main() { #ifdef _WIN32 system("cls"); #else system("clear"); #endif JudgeSystem judge(USER_DATA_PATH, PROBLEM_DATA_PATH, LOGIN_MSG_PATH, VERSION); std::cout << blue("Simple Judge System start! Version: ") << VERSION << "\n"; int result = 0; do { result = judge.mainPage(); }while( result == 0 ); return 0; } ``` 我們可以注意到在第 `18` 行宣告了 `JudgeSystem` 類別的變數,並透過 **Constructor (建構子)** 將 `USER_DATA_PATH`、`PROBLEM_DATA_PATH`、`LOGIN_MSG_PATH`、`VERSION` 變數作為參數傳入。 Constructor 會在變數被宣告時呼叫,並做對應的事情。 首先,讓我們先來完成 `JudgeSystem` 的 Constructor 吧! ### Subtask 1 - Constructor of JudgeSystem (5%, JudgeSystem.cpp) :::success JudgeSystem.cpp 的基礎模板已經寫好了,可以直接使用,並將對應的區域寫入正確的程式碼。 ::: ```cpp= JudgeSystem::JudgeSystem(std::string userPath, std::string problemPath, std::string msgPath, std::string version) { // do something } ``` 在此 Constructor 中,我們要將得到的參數資訊更新到 `JudgeSystem` 的成員 (member) 中,並且將 `status` 設為 `"NOT READY"` 表示系統尚未完成任何初始化。請你將此 Function 完成! --- 接下來我們繼續把 `main.cpp` 往下看會注意到有一個 `do-while` 迴圈。 ```cpp= int result = 0; do { result = judge.mainPage(); }while( result == 0 ); ``` ### Subtask 2 - Explain the code (5%) 請簡單解釋此 `do-while` 迴圈如何運作。 **解釋要點:** - `result` 變數會受誰影響? - `do-while` 迴圈的特性。 :::spoiler 提示 (Hint) 就是要送你們分數而已,不要想太多。 ::: --- 接下來我們把目光放到 `JudgeSystem::mainPage` 上,這一個 Function 回傳的變數型態為 `int`,且呼叫他的人為 `main.cpp`。 那為什麼 `main.cpp` 可以呼叫 `JudgeSystem` 的 Function 呢?這個問題,我們留給讀者自行練習。 ### Subtask 3 - Explain why (3%) 請簡單解釋為什麼 `main.cpp` 可以呼叫 `JudgeSystem` 的 `mainPage` Function。 **解釋要點:** - 請從 `public`、`private` 的角度去說明。 --- 接下來我們來處理 `JudgeSystem::mainPage` 的實作吧! 一開始,我們要先判斷當前系統的狀態,大致分成以下三種: - 未初始化 (NOT READY) - 呼叫 `JudgeSystem::loadData` 並將 `status` 更新為 `"USER LOGIN"` - 使用者未登入 (USER LOGIN) - 呼叫 `AccountSystem::login` 引導使用者登入,並接收其回傳值,確定使用者是否登入成功 - `AccountSystem::login` 會回傳一個 `pair<bool, std::string>` 型態的變數。 pair 的 `first` 表示的是使用者是否成功登入 (`true` 表示登入成功、`false` 表示未完成登入),`second` 的型態為 `string`,如果使用者有登入成功,則此字串為使用者名稱。 - 如果使用者成功登入則將 `status` 更新為 `"READY"`。 - 使用者已登入 (READY) - 主選單正常功能。 在不同狀態下 `mainPage` 必須能夠正確的將程式導向對應的 Function。 ### Subtask 4 - mainPage status (5%, JudgeSystem.cpp) ```cpp= int JudgeSystem::mainPage() { if( JudgeSystem::status == "NOT READY" ) { // call function loadData // update status return 0; } else if( JudgeSystem::status == "USER LOGIN" ) { ???(填入正確變數型態) user_info = AccountSystem::login(); if( user_info.first == false ) return 0; JudgeSystem::status = "READY"; return 0; } MainPage::mainpagePrint(); int opt = operationCheck(); clearWindow(); } ``` :::success 在 C++ 中,字串的比較可以直接使用比較運算子,相較於 C 語言來說這點方便了不少。 ::: :::spoiler 提示 1 (Hint 1) Function 的型態是什麼,回傳的型態就是什麼,知道答案放在哪裡了嗎? ::: :::spoiler 提示 2 (Hint 2) 使用 pair 這類的 STL (Standard Template Library) 會需要 include 對應的函式庫。 ::: --- 剛剛 `main.cpp` 可以直接呼叫 `JudgeSystem` 的 function,呼叫的方法為宣告一個型態為 `JudgeSystem` 的變數去呼叫。 但我們注意到 `JudgeSystem.cpp` 呼叫了一個 `AccoutSystem` 的 function,卻沒有像 `main.cpp` 一樣透過一個變數去呼叫,這邊有一些問題想要問你 >< ### Subtask 5 - Explain why (5%) 請解釋為什麼 `main.cpp` 一定要透過一個型態為 `JudgeSystem` 的變數才能呼叫 `mainPage`。 **解釋要點:** - 不用解釋得很嚴謹,簡單說明 `class` 的運作概念即可。 ### Subtask 6 - Explain why (5%) 承上題,那為什麼 `JudgeSystem.cpp` 可以不透過變數來直接呼叫 `AccountSystem` 的 `login` function 呢? **解釋要點:** - 繼承屬性 ### Subtask 7 - Explain Why (5%) 我們注意到 `login` 在 `AccountSystem` 裡為 `protected` 屬性的成員。 #### 7-1 如果今天 `login` 是 `public` 屬性的成員,那麼 `JudgeSystem.cpp` 可以呼叫嗎?為什麼? #### 7-2 如果今天 `login` 是 `private` 屬性的成員,那麼 `JudgeSystem.cpp` 可以呼叫嗎?為什麼? #### 7-3 那麼 `protected` 屬性的成員有誰可以存取? **解釋要點:** - 區分 `public`、`private`、`protected` 的不同即可。 - 如果怕自己解釋不好,可以用任意例子來舉例。 --- 接下來我們把目光放到 `MainPage` 的 `mainpagePrint` function。 這一個 function 的功能是負責輸出主選單。 ```cpp= #include <iostream> #include "MainPage.h" #define red(text) "\033[31m" text "\033[0m" #define green(text) "\033[32m" text "\033[0m" #define yellow(text) "\033[33m" text "\033[0m" #define blue(text) "\033[34m" text "\033[0m" #define magenta(text) "\033[35m" text "\033[0m" #define cyan(text) "\033[36m" text "\033[0m" #define white(text) "\033[37m" text "\033[0m" void MainPage::mainpagePrint() { std::cout << "+"; for (int i = 0; i < 45; ++i) { std::cout << "-"; } std::cout << "+\n"; std::cout << yellow("Please choose an operation:") << "\n"; std::cout << "+"; for (int i = 0; i < 45; ++i) { std::cout << "-"; } std::cout << "+\n"; std::cout << green("(1) Who am I") << "\n"; std::cout << green("(2) Query judge version") << "\n"; std::cout << green("(3) List all problem") << "\n"; std::cout << green("(4) Random some problem") << "\n"; std::cout << green("(5) Submit code") << "\n"; std::cout << red("(6) Add a new problem (admin only)") << "\n"; std::cout << yellow("(7) Exit the process") << "\n"; std::cout << "+"; for (int i = 0; i < 45; ++i) { std::cout << "-"; } std::cout << "+\n"; return; } ``` 輸出完後,我們要讓使用者選擇功能。 此部分會寫在 `MainPage` 的 `operationCheck` function 之中。 此 function 會回傳一個型態為 `int` 的變數,此變數代表的是使用選擇的操作。 我們使用 `std::getline` 讀取使用者輸入,並且設置防呆功能,如果發現輸入不是 1 ~ 7 要回傳 `-1` 給呼叫者知道使用者輸入了一個非指定操作,必須要求重新輸入。 否則,回傳一個 `int` 整數,代表使用者選擇的操作。 ### Subtask 8 - Operation Check (7%, MainPage.cpp) 請將此函數完成。 ```cpp= int MainPage::operationCheck() { // TODO: check input is a number in [1, 7] ( if yes, return what operation is. Otherwise return -1) std::string input; getline(std::cin, input); } ``` :::spoiler 提示 (Hint) 如果是合法操作,那字串長度一定只有 1,而且字元的位置位於 input[0] ::: --- 接下來我們回到 `JudgeSystem` 的 `loadData`。 這裡我們要做的事情是初始化 `AccountSystem` 與 `ProblemSystem` 因此在這個 function 中我們會呼叫這兩個 class 的 `init` function 並在呼叫完後透過 `try-catch` 語法來讀取 `./msg/login.txt` 檔案來輸出訊息。 ```cpp= void JudgeSystem::loadData() { // Step1: Call AccountSystem::init with user data path AccountSystem::init(JudgeSystem::USER_DATA_PATH); JudgeSystem::effectLoading("Status - Loading user data..."); std::cout << yellow("Status - Loading user data...") << green("OK!\n"); // Step2: Call ProblemSystem::init with problem data path ProblemSystem::init(JudgeSystem::PROBLEM_DATA_PATH); JudgeSystem::effectLoading("Status - Loading problem data..."); std::cout << yellow("Status - Loading problem data...") << green("OK!\n"); // Step3: welcome the user try { std::ifstream inputFile("./msg/login.txt"); if( !inputFile ) { throw std::runtime_error("Error: File does not exist - ./msg/login.tx"); } std::string line; std::cout << "\033[36m"; while (std::getline(inputFile, line)) { std::cout << line; std::cout << "\n"; } inputFile.close(); } catch (const std::exception& e) { std::cerr << "Exception caught: " << e.what() << "\n"; } } ``` `effectLoading` 只是一個讀取動畫,實際上沒什麼用,但可以讓你的程式看起來好像比較帥。 ```cpp= void JudgeSystem::effectLoading(std::string content) { static const char* spinner[] = {"|", "/", "-", "\\"}; for (int i = 0, j = 0; j < 20; j++) { std::cout << "\033[33m" << content << "\033[0m" << spinner[i] << "\r"; std::flush(std::cout); std::this_thread::sleep_for(std::chrono::milliseconds(150)); i = ( i + 1 ) % 4; } return; } ``` 基本上 `JudgeSystem` 的東西都完成的差不多了,我們接下來換處理 `AccountSystem` 的 `init`。 --- 在 `AccountSystem` 的 `init` 中,我們要去 `USER_DATA_PATH` 尋找檔案並讀入檔案中的資料,資料的格式如下: ``` 使用者1名稱,使用者1密碼 使用者2名稱,使用者2密碼 使用者3名稱,使用者3密碼 使用者4名稱,使用者4密碼 . . . ``` 我們可以透過 `std::ifstream` 來讀入資料,並想辦法將每一行的字串以 `,` 分隔出使用者名稱與密碼,讓我們一起來看看吧! ```cpp= void AccountSystem::init(std::string USER_DATA_PATH) { AccountSystem::USER_DATA_PATH = USER_DATA_PATH; try { std::ifstream file(USER_DATA_PATH); // 開啟檔案 if( !file ) { throw std::runtime_error("Error: File does not exist - " + USER_DATA_PATH); } // TODO: Loading user data from USER_DATA_PATH and call adding_user (from AccountSystem) function to insert data // Hints: stringstream std::string read_line; while( getline(file, read_line) ) { std::string username, password; // do something AccountSystem::adding_user(username, password); } file.close(); } catch (const std::exception& e) { std::cerr << "Exception caught: " << e.what() << "\n"; } } ``` ### Subtask 9 - AccountSystem Init (10%, AccountSystem.cpp) 現在每一行的資料已經會依序讀入 `read_line` 變數中,你的任務是分隔出帳號與密碼並存入變數 `username` 與 `password` 之中,請把此 function 完成。 --- 接下來我們要將每一次解析出來的 `username` 與 `password` 透過 `AccountSystem::adding_user` 將其放入 `AccountSystem` 的 member `user_list` 並且呼叫 `AccountSystem::userdataUpdate` 重新寫入資料進 `USER_DATA_PATH`。 ```cpp= void AccountSystem::adding_user(std::string username, std::string password) { User new_user(username, password); user_list.push_back(new_user); AccountSystem::userdataUpdate(); return; } ``` 在 `userdataUpdate` 中,我們要將所有存在 `user_list` 的資料重新覆蓋 (寫入) `USER_DATA_PATH`,請你將這一個 function 完成。 :::warning 寫入的格式如先前讀取時的格式一樣。 ``` 使用者1名稱,使用者1密碼 使用者2名稱,使用者2密碼 使用者3名稱,使用者3密碼 . . . ``` ::: ### Subtask 10 - AccountSystem userdataUpdate (5%, AcconutSystem.cpp) ```cpp= void AccountSystem::userdataUpdate() { try { std::ofstream info_out(AccountSystem::USER_DATA_PATH); if( !info_out ) { throw std::runtime_error("Error: File does not exist - " + USER_DATA_PATH); } // 把整個 vector 跑過一次,透過 info_out 寫入 // Hints: 你可能會需要 class User 的 getter 才能取得某些資訊 info_out.close(); } catch (const std::exception& e) { std::cerr << "Exception caught: " << e.what() << "\n"; } } ``` --- ### Subtask 11 - Explain Code (5%) 請解釋上方的 `try-catch` 語法如何運作。 --- `AccountSystem` 還提供了 `search` 的功能,呼叫時給予使用者名稱作為參數,如果 `user_list` 存在該使用者則回傳其指標。反之,如果找不到則回傳 `nullptr`。 請你將此 function 實作完成。 ### Subtask 12 - AccountSystem Search (5%, AccountSystem.cpp) ```cpp= User* AccountSystem::search(std::string name) { for(size_t i=0;i<user_list.size();i++) { // do something } return nullptr; } ``` --- `AccountSystem::getuserLogin` 回傳當前登入的使用者,其資訊儲存於 member `login_user` 當中。 ```cpp= std::string AccountSystem::getuserLogin() { return AccountSystem::login_user; } ``` 最後的大工程是登入與註冊功能,登入的 function 為 `AccountSystem::login`。 此 function 必須引導使用者輸入相關資訊完成登入動作。 注意到此 function 所回傳的型態為 `pair<bool,string>`,`first` 表示的為是否登入成功,而 `second` 則是該使用者名稱。 ### Subtask 13 - AccountSystem login (15%, AccountSystem.cpp) 請將登入功能完成。 ```cpp= std::pair<bool, std::string> AccountSystem::login() { std::pair<bool, std::string> result = std::make_pair(false, ""); return result; } ``` 要求: - 引導使用者輸入名稱 - 如果使用者輸入 `-1` 則呼叫 `AccountSystem::sign_up` 並回傳 `(false, "")` - 使用者輸入名稱後,查詢使用者是否存在,如果不存在必須提示使用者重新輸入 - 使用者名稱存在時,要求使用者輸入密碼 - 密碼輸入正確:提示登入成功,並將 `AccountSystem::login_user` 更新,直接回傳 `(true, username)` - 密碼輸入錯誤:提示使用者密碼錯誤需要重新輸入,如果錯超過三次則提示失敗太多次,回到輸入使用者名稱的狀態 :::warning 提示訊息可以自由發揮或參考照片。 ::: ![image](https://hackmd.io/_uploads/r1ksx2vakg.png) ![image](https://hackmd.io/_uploads/SyH1W2Dakl.png) --- ### Subtask 14 - AccountSystem Sign-up (15%, AccountSystem.cpp) 接下來我們將註冊的功能完成,註冊時系統必須提示使用者依序輸入對應資訊 (名稱與密碼),輸入密碼時必須輸入兩次並驗證兩次輸入的密碼是否一樣,如果不一樣必須提示使用者重新輸入密碼。 註冊成功後,直接回到登入功能即可。 :::warning 使用者註冊成功後,記得呼叫 `AccountSystem::adding_user` 來更新使用者資訊。 ::: ```cpp= void AccountSystem::sign_up() { // do something return; } ``` :::warning 不需要檢查使用者名稱是否已經註冊,這是下一次的作業。 ::: ![image](https://hackmd.io/_uploads/ByhkQnD6Je.png) --- 如此一來,註冊與登入相關的功能都完成啦!我們最後把已經可以完成的系統功能給完成吧! ### Subtask 15 - Operation 1 & 2 (5%, JudgeSystem.cpp) 請將 `JudgeSystem::mainPage` 當中操作 1 與操作 2 的功能完成。 ![image](https://hackmd.io/_uploads/ByKyH3wTkl.png) ![image](https://hackmd.io/_uploads/SJfgS3PpJl.png) ### Subtask 16 - Demo (5%, 必繳交) 請依照程式執行順序介紹程式碼的邏輯與運作方式,並實際 Demo 一次你的程式,錄成影片後上傳 moodle。 - 請在 Server 上 Demo - 一開始請先錄製你的 moodle 畫面 (需辨識得出名字) - Step 1. 打開 user.csv 確認當前資料 - Step 2. 輸入 `-1` 註冊使用者,並確認兩次密碼輸入錯誤惠要求使用者重新決定密碼 - Step 3. 登入時,輸入一個不存在的使用者 - Step 4. 輸入 `admin` 並輸入密碼錯誤 3 次 - Step 5. 登入 `admin` 後使用操作一與操作二 - Step 6. 輸入字串 `Hello World` 確認是否有過濾掉不合法操作。 - Step 7. 結束程式。 - Step 8. 打開 `user.csv` 確認剛剛被註冊的使用者資料有更新至檔案中 - Step 9. 解釋 code 的實作邏輯。 #### 編譯指令 ``` g++ -std=c++17 -o hw3 ./src/main.cpp ./src/AccountSystem.cpp ./src/User.cpp ./src/Problem.cpp ./src/JudgeSystem.cpp ./src/MainPage.cpp ``` 範例影片: ## Demo