# 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
提示訊息可以自由發揮或參考照片。
:::


---
### Subtask 14 - AccountSystem Sign-up (15%, AccountSystem.cpp)
接下來我們將註冊的功能完成,註冊時系統必須提示使用者依序輸入對應資訊 (名稱與密碼),輸入密碼時必須輸入兩次並驗證兩次輸入的密碼是否一樣,如果不一樣必須提示使用者重新輸入密碼。
註冊成功後,直接回到登入功能即可。
:::warning
使用者註冊成功後,記得呼叫 `AccountSystem::adding_user` 來更新使用者資訊。
:::
```cpp=
void AccountSystem::sign_up() {
// do something
return;
}
```
:::warning
不需要檢查使用者名稱是否已經註冊,這是下一次的作業。
:::

---
如此一來,註冊與登入相關的功能都完成啦!我們最後把已經可以完成的系統功能給完成吧!
### Subtask 15 - Operation 1 & 2 (5%, JudgeSystem.cpp)
請將 `JudgeSystem::mainPage` 當中操作 1 與操作 2 的功能完成。


### 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