Phase 2: Validator + làm nổi xung đột
Thêm validator kiểm tra luật Sudoku (hàng / cột / hộp 3x3 không trùng) và nối làm nổi xung đột để ô UI chuyển đỏ khi người chơi nhập nước đi không hợp lệ.
Lesson 2 — Phase 2: Strategy Pattern với Validator
Commit:
2c97016(một phần). Phase 2 của ứng dụng sudoku — thêm phần kiểm tra hợp lệ.
1. Mục tiêu
Sau lesson này bạn sẽ có:
- Một lớp trừu tượng
Validatorvới destructor virtual và 2 phương thức pure virtual (name,conflicts) - 3 validator cụ thể:
RowValidator,ColValidator,BoxValidator— phát hiện trùng lặp trong nhóm tương ứng Appchứastd::vector<std::unique_ptr<Validator>>— sở hữu đa hình- Một UIPanel hiển thị các ô màu đỏ khi có xung đột (cả ô cố định lẫn ô có thể chỉnh sửa)
- Kiến trúc mở rộng được: thêm một validator mới (ví dụ
DiagonalValidatorcho X-Sudoku) không cần sửa đổi code hiện có
2. Cây thư mục
apps/sudoku/src/
├── app.h ← MODIFY: thêm field vector<unique_ptr<Validator>>
├── app.cpp ← MODIFY: push 3 validator trong init()
├── ui_panel.h ← MODIFY: signature draw() nhận validators
├── ui_panel.cpp ← MODIFY: helper cell_has_conflict, render đỏ
├── validator.h ← NEW: lớp trừu tượng + 3 lớp dẫn xuất
├── validator.cpp ← NEW: triển khai conflicts() cho từng lớp
├── grid.h, grid.cpp (đã có, không thay đổi)
└── main.cpp (đã có, không thay đổi)
3. Nội dung cần học
- Lớp trừu tượng — phương thức pure virtual (
= 0) - Destructor virtual — vì sao bắt buộc đối với lớp cơ sở đa hình
- Từ khoá
override— override được compiler kiểm tra = defaultcho destructor — dtor tầm thường, ý định rõ ràng- Kế thừa —
class Derived : public Base std::unique_ptr<Base>+ đa hình — sở hữu đơn với dispatch virtualstd::make_unique<Derived>()— factory an toàn với exception- Container chứa kiểu đa hình —
vector<unique_ptr<Base>> - Strategy pattern — họ thuật toán có thể hoán đổi cho nhau
- Hàm tự do
static— helper với internal linkage
4. Cách thực hiện
Bước 1 — Lớp cơ sở trừu tượng Validator (validator.h)
File: apps/sudoku/src/validator.h
#pragma once
#include "grid.h" // Phase 2; ở Phase 3.0 đổi thành "solver/grid.h"
class Validator {
public:
virtual ~Validator() = default;
virtual const char* name() const = 0;
virtual bool conflicts(const Grid& grid, int r, int c) const = 0;
};
class RowValidator : public Validator {
public:
const char* name() const override;
bool conflicts(const Grid& grid, int r, int c) const override;
};
class ColValidator : public Validator {
public:
const char* name() const override;
bool conflicts(const Grid& grid, int r, int c) const override;
};
class BoxValidator : public Validator {
public:
const char* name() const override;
bool conflicts(const Grid& grid, int r, int c) const override;
};
Giải thích:
virtual ~Validator() = default;: destructor virtual cho phép xoá thông qua con trỏ base mà dispatch đúng destructor của lớp dẫn xuất.= defaultyêu cầu compiler tự sinh ra (thân hàm rỗng tầm thường).- Specifier pure
= 0: phương thức không có thân ở base, và các lớp dẫn xuất bắt buộc phải override. Một lớp có pure virtual là lớp trừu tượng — không thể khởi tạo trực tiếp. overridesau signature: compiler kiểm tra ý định. Nếu signature không khớp với phương thức base, compiler báo lỗi. Điều này ngăn chặn lỗi gõ nhầm (nme()thay vìname()).const Grid& grid: truyền tham chiếu tránh việc copy 324 byte;constvì validator chỉ đọc.constở cuối phương thức: phương thức không thay đổi*this. Điều này cho phép gọi thông quaconst Validator&.
Bước 2 — Triển khai 3 validator (validator.cpp)
File: apps/sudoku/src/validator.cpp
#include "validator.h"
const char* RowValidator::name() const { return "Row"; }
bool RowValidator::conflicts(const Grid& grid, int r, int c) const {
int v = grid[r][c];
if (v == 0) return false; // empty cell never conflicts
for (int cc = 0; cc < N; ++cc) {
if (cc != c && grid[r][cc] == v) {
return true;
}
}
return false;
}
const char* ColValidator::name() const { return "Column"; }
bool ColValidator::conflicts(const Grid& grid, int r, int c) const {
int v = grid[r][c];
if (v == 0) return false;
for (int rr = 0; rr < N; ++rr) {
if (rr != r && grid[rr][c] == v) {
return true;
}
}
return false;
}
const char* BoxValidator::name() const { return "Box"; }
bool BoxValidator::conflicts(const Grid& grid, int r, int c) const {
int v = grid[r][c];
if (v == 0) return false;
int boxRow = r / 3;
int boxCol = c / 3;
for (int rr = boxRow * 3; rr < boxRow * 3 + 3; ++rr) {
for (int cc = boxCol * 3; cc < boxCol * 3 + 3; ++cc) {
if ((rr != r || cc != c) && grid[rr][cc] == v) {
return true;
}
}
}
return false;
}
Giải thích thuật toán:
Mỗi validator trả lời câu hỏi: "Giá trị tại (r, c) có bị trùng trong hàng/cột/box của nó không?"
- Kiểm tra
v == 0trước: một ô trống không gây xung đột - Lặp qua nhóm (9 ô trong hàng/cột/box)
- Bỏ qua ô hiện tại (
cc != c, hoặcrr != r, hoặc(rr != r || cc != c)) - So khớp giá trị: tìm thấy ô khác có cùng giá trị → xung đột
BoxValidator: tính bounding box 3x3:
int boxRow = r / 3; // 0, 1, hoặc 2
int boxCol = c / 3;
// phạm vi box: hàng [boxRow*3, boxRow*3+3), cột [boxCol*3, boxCol*3+3)
Ví dụ: r = 4, c = 7 → boxRow = 1, boxCol = 2 → lặp rr ∈ [3,6), cc ∈ [6,9).
Bước 3 — App sở hữu các validator (app.h)
File: apps/sudoku/src/app.h (sửa)
#pragma once
#include "grid.h"
#include "ui_panel.h"
#include "validator.h" // ← ADD
#include <memory> // ← ADD
#include <vector> // ← ADD
struct GLFWwindow;
class App {
public:
App();
~App();
App(const App&) = delete;
App& operator=(const App&) = delete;
int run();
private:
bool init();
bool shutdown();
void draw_frame();
GLFWwindow* window_ = nullptr;
Grid grid_;
FixedMap fixed_;
UIPanel panel_;
std::vector<std::unique_ptr<Validator>> validators_; // ← ADD
};
Giải thích kiểu của member:
std::vector<std::unique_ptr<Validator>> — một vector chứa smart pointer tới base.
So sánh các lựa chọn:
| Kiểu | Vấn đề |
|---|---|
| std::vector<Validator> | Lỗi biên dịch: Validator là trừu tượng. Cho dù có khả thi, object slicing — chỉ phần Validator được lưu, dữ liệu dẫn xuất bị mất |
| std::vector<Validator*> | Biên dịch được. Nhưng raw pointer — quyền sở hữu không rõ, dễ rò rỉ |
| std::vector<std::unique_ptr<Validator>> | ✓ Smart pointer sở hữu, đa hình qua con trỏ, dọn dẹp RAII |
Bước 4 — Push các validator trong init (app.cpp)
File: apps/sudoku/src/app.cpp — bên trong App::init(), sau fixed_ = make_fixed_map(grid_);:
validators_.push_back(std::make_unique<RowValidator>());
validators_.push_back(std::make_unique<ColValidator>());
validators_.push_back(std::make_unique<BoxValidator>());
Giải thích:
std::make_unique<T>()— một lần cấp phát, an toàn với exception. Luôn ưu tiên dùng nó thay vìstd::unique_ptr<T>(new T()).push_backcủa vector move-constructunique_ptrvào — không copy (unique_ptr không thể copy).
Bước 5 — Signature của UIPanel (ui_panel.h)
File: apps/sudoku/src/ui_panel.h (sửa)
#pragma once
#include "grid.h"
#include "validator.h" // ← ADD
#include <memory> // ← ADD
#include <vector> // ← ADD
class UIPanel {
public:
void draw(Grid& grid, const FixedMap& fixed,
const std::vector<std::unique_ptr<Validator>>& validators);
};
Bước 6 — UIPanel render với conflict (ui_panel.cpp)
File: apps/sudoku/src/ui_panel.cpp (sửa)
#include "ui_panel.h"
#include "imgui.h"
static bool cell_has_conflict(const Grid& grid, int r, int c,
const std::vector<std::unique_ptr<Validator>>& validators) {
for (const auto& v : validators) {
if (v->conflicts(grid, r, c)) {
return true;
}
}
return false;
}
void UIPanel::draw(Grid& grid, const FixedMap& fixed,
const std::vector<std::unique_ptr<Validator>>& validators) {
if (ImGui::BeginTable("grid", N,
ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) {
for (int i = 0; i < N; ++i) {
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 40.0f);
}
for (int r = 0; r < N; ++r) {
ImGui::TableNextRow();
for (int c = 0; c < N; ++c) {
ImGui::TableNextColumn();
bool conflict = cell_has_conflict(grid, r, c, validators);
int box_r = r / 3, box_c = c / 3;
if ((box_r + box_c) % 2 == 1) {
ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg,
IM_COL32(40, 40, 40, 255));
}
if (fixed[r][c]) {
if (conflict) {
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.5f, 1.0f),
"%d", grid[r][c]);
} else {
ImGui::Text("%d", grid[r][c]);
}
} else {
ImVec4 input_color = conflict
? ImVec4(1.0f, 0.5f, 0.5f, 1.0f)
: ImVec4(0.5f, 1.0f, 0.5f, 1.0f);
char buf[2] = "";
if (grid[r][c] != 0) buf[0] = '0' + grid[r][c];
buf[1] = '\0';
ImGui::PushID(r * N + c);
ImGui::SetNextItemWidth(34.0f);
ImGui::PushStyleColor(ImGuiCol_Text, input_color);
ImGui::InputText("##cell", buf, sizeof(buf),
ImGuiInputTextFlags_CharsDecimal);
ImGui::PopStyleColor();
ImGui::PopID();
if (buf[0] >= '1' && buf[0] <= '9')
grid[r][c] = buf[0] - '0';
else
grid[r][c] = 0;
}
}
}
ImGui::EndTable();
}
}
Giải thích các phần quan trọng:
static bool cell_has_conflict(...): một hàm helper tự do;staticcho nó internal linkage (chỉ TU này sử dụng). Nó lặp qua các validator và hỏi từng cái. Nếu bất kỳ validator nào báo xung đột, ô đó xung đột.for (const auto& v : validators): range-for;vđược suy ra làconst unique_ptr<Validator>&.v->conflicts(...):unique_ptr::operator->trả về rawValidator*, và lời gọi phương thức dispatch virtual qua vtable.- Ô cố định + xung đột:
ImGui::TextColoredvới màu đỏ nhạt.ImGui::Textmặc định là màu trắng. - Ô có thể chỉnh sửa + xung đột: dùng
PushStyleColor(ImGuiCol_Text, red)để text của InputText có màu đỏ. Đi cùng với Pop sau Input.
Bước 7 — Điểm gọi (app.cpp draw_frame)
void App::draw_frame() {
ImGui::Begin("Sudoku");
panel_.draw(grid_, fixed_, validators_); // ← thêm validators_
ImGui::End();
}
Bước 8 — Cập nhật CMakeLists
File: apps/sudoku/CMakeLists.txt — thêm src/validator.cpp vào sources:
add_executable(sudoku
src/main.cpp
src/grid.cpp
src/ui_panel.cpp
src/app.cpp
src/validator.cpp # ← ADD
)
5. Bài học chính — Lesson 2 so với Lesson 1
| Lesson 1 (Phase 1) | Lesson 2 (Phase 2) |
|---|---|
| App sở hữu Grid + FixedMap (kiểu POD) | App sở hữu một vector các smart pointer đa hình |
| UIPanel chỉ render | UIPanel + helper business-logic (cell_has_conflict) |
| Không có hệ phân cấp lớp | Lớp trừu tượng base + 3 lớp dẫn xuất = kế thừa + đa hình |
| Grid g{}; kiểu giá trị | std::make_unique<RowValidator>() cấp phát heap đa hình |
| Dispatch tại thời điểm biên dịch | Virtual dispatch tại thời điểm chạy (vtable) |
Pattern mới:
- Strategy pattern — họ thuật toán có thể hoán đổi đằng sau một interface base
- Lớp trừu tượng với pure virtual
- Destructor virtual + dispatch
- An toàn nhờ từ khoá
override - Container đa hình —
vector<unique_ptr<Base>> - Factory
std::make_unique<Derived>()
Dễ mở rộng:
Thêm class DiagonalValidator : public Validator trong một file mới cộng thêm một dòng push trong App::init(). UI tự động render đỏ cho xung đột với rule mới. Đây là Open-Closed Principle trong thực tế.
6. Cách kiểm tra hoạt động
Build
cd apps/sudoku
cmake --build build
Kỳ vọng:
[100%] Built target sudoku
Chạy
./build/sudoku # Mac
build\Debug\sudoku.exe # Windows
Checklist quan sát
- [ ] Cửa sổ "Sudoku" hiển thị lưới 9x9 với
SAMPLE_PUZZLE - [ ] Các ô cố định (số đã điền sẵn): trắng
- [ ] Ô trống có thể chỉnh sửa: con trỏ input màu xanh lá
- [ ] Test xung đột hàng: gõ
5vào ô (0, 1). Ô (0, 0) đã có5trong cùng hàng → cả hai ô chuyển đỏ. - [ ] Test xung đột cột: gõ
6vào ô (3, 0). Ô (1, 0) đã có6trong cùng cột → đỏ. - [ ] Test xung đột box: gõ
9vào ô (0, 1). Ô (1, 4) — hoặc một ô tương đương trong box 3x3 (0,0) — chứa9→ đỏ. - [ ] Xoá số đã gõ → màu đỏ biến mất ở frame tiếp theo.
- [ ] Nhiều xung đột trên cùng một ô: gõ một số vi phạm cả hàng lẫn cột → vẫn chỉ là một màu đỏ duy nhất (không có phiên bản "đỏ hơn") — đúng.
Verify bằng CLI (nâng cao)
Kiểm tra bảng ký hiệu:
nm build/sudoku 2>/dev/null | grep -i validator | head
# Hoặc trên macOS:
nm -gU build/sudoku | grep -i validator
Kỳ vọng: RowValidator::name, RowValidator::conflicts, các entry vtable.
7. Lỗi thường gặp / cạm bẫy
| Triệu chứng | Nguyên nhân | Cách khắc phục |
|---|---|---|
| Lỗi biên dịch cannot allocate an object of abstract type 'Validator' | Code có Validator v; đâu đó | Validator là trừu tượng — phải đi qua lớp dẫn xuất. Kiểm tra không có Validator{} |
| Lỗi biên dịch 'override' marked but doesn't override | Gõ sai tên phương thức hoặc signature không khớp | Kiểm tra cẩn thận name() const override — const quan trọng |
| Lỗi linker undefined reference to vtable for Validator | Quên triển khai destructor virtual | Thêm ~Validator() = default; hoặc khai báo-định nghĩa out-of-line |
| Ô không bao giờ chuyển đỏ | Vector validators rỗng (quên push) hoặc cell_has_conflict luôn trả về false | Debug: in validators_.size() lúc init; in kết quả conflict trong helper |
| Vòng lặp vô tận khi gõ số | for (int cc = 0; cc < N; ++c) gõ nhầm (++c thay vì ++cc) | Đổi tên biến lặp thành thứ gì đó khác hẳn với tham số (ví dụ col_iter) |
| App crash khi thoát | Thiếu destructor virtual → cascade delete sai | Luôn dùng dtor virtual cho lớp base |
| Push kiểu giá trị thay vì smart pointer | validators_.push_back(RowValidator{}); lỗi biên dịch | Dùng std::make_unique<RowValidator>() |
| Lỗi linker multiple-definition | Thân phương thức định nghĩa inline trong validator.h | Header chỉ khai báo, triển khai trong .cpp |
| Thiếu ; sau class Validator { ... } | Compiler báo lỗi khó hiểu ở dòng tiếp theo | Luôn kết thúc dấu ngoặc lớp bằng ; |
| Xung đột ở ô cố định không hiển thị đỏ | Quên kiểm tra conflict trong nhánh fixed | Nhánh fixed cũng phải kiểm tra và dùng TextColored |
8. Khoảng tự học
- DiagonalValidator cho X-Sudoku: 2 đường chéo mỗi cái phải chứa 9 số khác nhau. Thử triển khai (tương tự RowValidator nhưng index
i, ihoặci, N-1-i). - Vùng Killer Sudoku: định nghĩa một vùng tự do (một nhóm ô) mà tổng giá trị các ô bằng một mục tiêu. Triển khai
KillerValidator. - Bộ đếm xung đột: đếm tổng số ô xung đột trong lưới. Hiển thị "Errors: N" trên UI.
- Bật/tắt validator: thêm một checkbox cho mỗi validator (Row/Col/Box) — vô hiệu hoá lúc chạy. UI cập nhật xung đột dựa trên tập đã bật.
- Hiệu năng: mỗi frame lặp 81 ô * 3 validator * 9 = 2187 phép so sánh. Có cần cache không? Đo FPS, thử tắt một validator → so sánh.
- Mức độ nghiêm trọng theo màu: nếu một ô xung đột với cả 3 validator (cực kỳ hiếm), hiển thị màu khác (đỏ đậm) thay vì màu đỏ cũ.
9. Bài tập cuối lesson
Bài tập 1 — DiagonalValidator (X-Sudoku)
Tạo class DiagonalValidator : public Validator. Logic: nếu (r, c) nằm trên một trong 2 đường chéo, kiểm tra 9 ô trên đường chéo đó. Push nó vào App::init().
Bài tập 2 — UI đếm số lỗi
Trong App::draw_frame, trước panel_.draw(...), tính tổng số ô xung đột và hiển thị ImGui::Text("Errors: %d", count). Count = tổng cell_has_conflict trên 81 ô.
Bài tập 3 — Vô hiệu hoá validator
Thêm 3 member bool enable_row_, enable_col_, enable_box_; trong App, mặc định true. Thêm 3 checkbox trong UI. Sửa cell_has_conflict để bỏ qua các validator bị vô hiệu. Gợi ý: thay vì truyền vector, truyền vector + mảng enable_flags.
Bài tập 4 — Kiểm tra đã giải xong
Thêm phương thức App::is_solved() -> bool: trả về true khi (1) không ô nào bằng 0, và (2) không có xung đột. Hiển thị text "🎉 SOLVED" khi đã giải. (Không dùng emoji nếu khó gõ — dùng "SOLVED")
Bài tập 5 (thử thách) — Chế độ Hint
Thêm nút "Hint". Khi nhấn: tìm một ô trống mà có đúng 1 giá trị 1-9 không gây xung đột (tức là "naked single"). Tự động điền ô đó. Nếu không tìm thấy → log "No naked single available".
10. Đáp án
Bài tập 1 — DiagonalValidator
validator.h:
class DiagonalValidator : public Validator {
public:
const char* name() const override;
bool conflicts(const Grid& grid, int r, int c) const override;
};
validator.cpp:
const char* DiagonalValidator::name() const { return "Diagonal"; }
bool DiagonalValidator::conflicts(const Grid& grid, int r, int c) const {
int v = grid[r][c];
if (v == 0) return false;
bool on_main_diag = (r == c);
bool on_anti_diag = (r + c == N - 1);
if (on_main_diag) {
for (int i = 0; i < N; ++i) {
if (i != r && grid[i][i] == v) return true;
}
}
if (on_anti_diag) {
for (int i = 0; i < N; ++i) {
if (i != r && grid[i][N-1-i] == v) return true;
}
}
return false;
}
app.cpp:
validators_.push_back(std::make_unique<DiagonalValidator>());
CMakeLists giữ nguyên (đã có sẵn validator.cpp).
Bài tập 2 — Đếm số lỗi
app.cpp draw_frame:
void App::draw_frame() {
ImGui::Begin("Sudoku");
int errors = 0;
for (int r = 0; r < N; ++r) {
for (int c = 0; c < N; ++c) {
for (const auto& v : validators_) {
if (v->conflicts(grid_, r, c)) {
++errors;
break; // đếm mỗi ô chỉ một lần
}
}
}
}
ImGui::Text("Errors: %d", errors);
panel_.draw(grid_, fixed_, validators_);
ImGui::End();
}
Hoặc gọn hơn — một phương thức App::count_errors().
Bài tập 3 — Vô hiệu hoá validator
Field của App:
bool enable_row_ = true;
bool enable_col_ = true;
bool enable_box_ = true;
UI:
ImGui::Checkbox("Row", &enable_row_);
ImGui::SameLine();
ImGui::Checkbox("Column", &enable_col_);
ImGui::SameLine();
ImGui::Checkbox("Box", &enable_box_);
Cách 1: dựng lại validators_ mỗi frame (đơn giản, hơi phí cấp phát):
std::vector<std::unique_ptr<Validator>> active;
if (enable_row_) active.push_back(std::make_unique<RowValidator>());
if (enable_col_) active.push_back(std::make_unique<ColValidator>());
if (enable_box_) active.push_back(std::make_unique<BoxValidator>());
panel_.draw(grid_, fixed_, active);
Cách 2 (sạch hơn): giữ validators_ cố định và truyền enable_flags. Cần đổi signature của cell_has_conflict.
Bài tập 4 — Kiểm tra đã giải xong
app.h:
bool is_solved() const;
app.cpp:
bool App::is_solved() const {
for (int r = 0; r < N; ++r) {
for (int c = 0; c < N; ++c) {
if (grid_[r][c] == 0) return false;
for (const auto& v : validators_) {
if (v->conflicts(grid_, r, c)) return false;
}
}
}
return true;
}
draw_frame:
panel_.draw(grid_, fixed_, validators_);
if (is_solved()) {
ImGui::TextColored(ImVec4(0, 1, 0, 1), "SOLVED");
}
Bài tập 5 (thử thách) — Chế độ Hint
bool App::try_hint() {
for (int r = 0; r < N; ++r) {
for (int c = 0; c < N; ++c) {
if (grid_[r][c] != 0) continue;
int candidate_count = 0;
int candidate = 0;
for (int v = 1; v <= 9; ++v) {
grid_[r][c] = v;
bool any_conflict = false;
for (const auto& val : validators_) {
if (val->conflicts(grid_, r, c)) {
any_conflict = true;
break;
}
}
if (!any_conflict) {
++candidate_count;
candidate = v;
}
}
grid_[r][c] = 0; // restore
if (candidate_count == 1) {
grid_[r][c] = candidate;
return true;
}
}
}
return false;
}
UI:
if (ImGui::Button("Hint")) {
if (!try_hint()) {
std::fprintf(stderr, "No naked single available\n");
}
}
Gợi ý: thử mọi giá trị 1-9 cho mỗi ô trống và đếm số ứng cử viên không gây xung đột. Một ô có đúng 1 ứng cử viên là naked single → tự động điền.
→ Tiếp theo: 03-phase3-0-shared-lib.md — Phase 3.0: Tách libs/solver/ thành một thư viện static