Chắc hẳn mọi người đều biết về trò chơi thú vị như cờ tướng. Tiếp theo loạt bài về trí tuệ nhân tạo, bài viết này mình sẽ nói về cụ thể giải thuật Minimax ứng dụng trong trò chơi trí tuệ cờ tướng như thế nào. OK! Let's go.
1. Ý tưởng
Cờ tướng là trò chơi đối kháng, trong đó hai người luôn phiên nhau đi nước đi của mình. Trạng thái bắt đầu là trạng thái khởi tạo bàn cờ, sau mỗi nước đi của một bên, trạng thái bàn cờ sẽ được thay đổi thành một trạng thái mới hiện hành. Cờ tướng có luật của nó, và trò chơi sẽ kết thúc khi một người có được trạng thái phản ánh sự thắng cuộc hoặc hai người rơi vào trạng thái hòa cờ. Ta tìm cách phân tích xem từ một trạng thái nào đó sẽ dẫn đến đấu thủ nào sẽ thắng với điều kiện cả hai có trình độ như nhau. Giải thuật Minimax sẽ được áp dụng vào trong trò chơi cờ tướng. Hai đấu thủ trong trò chơi sẽ được gọi là MIN và MAX và hai đấu thủ đều biết rõ thông tin trên bàn cờ như nhau.
- MAX đại diện cho đấu thủ quyết dành thắng lợi hoặc tối đa hóa ưu thế của mình.
- MIN ngược lại, cố gắng tối thiểu hóa điểm số của MAX.
Cờ tướng là trò chơi đối kháng, trong đó hai người luôn phiên nhau đi nước đi của mình. Trạng thái bắt đầu là trạng thái khởi tạo bàn cờ, sau mỗi nước đi của một bên, trạng thái bàn cờ sẽ được thay đổi thành một trạng thái mới hiện hành. Cờ tướng có luật của nó, và trò chơi sẽ kết thúc khi một người có được trạng thái phản ánh sự thắng cuộc hoặc hai người rơi vào trạng thái hòa cờ. Ta tìm cách phân tích xem từ một trạng thái nào đó sẽ dẫn đến đấu thủ nào sẽ thắng với điều kiện cả hai có trình độ như nhau. Giải thuật Minimax sẽ được áp dụng vào trong trò chơi cờ tướng. Hai đấu thủ trong trò chơi sẽ được gọi là MIN và MAX và hai đấu thủ đều biết rõ thông tin trên bàn cờ như nhau.
- MAX đại diện cho đấu thủ quyết dành thắng lợi hoặc tối đa hóa ưu thế của mình.
- MIN ngược lại, cố gắng tối thiểu hóa điểm số của MAX.
Bàn cờ tướng cũng
chính là một không gian trạng thái với các mức và được biểu diễn
bằng cây trò chơi. Mỗi nút của cây biểu diễn cho một trạng thái trên
bàn cờ. Nút gốc biểu diễn trạng thái bắt đầu ván cờ. Các nút lá
thể hiện trạng thái kết thúc của trò chơi (khi một đấu thủ giành phần
thắng, thua, hay hai đấu thủ hòa nhau).
Hai đấu thủ được gọi
là MAX và MIN và luân phiên đi nước cờ của mình nên mỗi mức trên cây
được biểu diễn luân phiên là MAX và MIN.
Các nút ứng với trạng thái mà từ đó đấu thủ MAX chọn nước đi ứng
với lớp MAX, các nút mà đấu thủ MIN chọn nước đi ứng với lớp MIN. Với
mỗi nước đi trong bàn cờ, tương ứng với các mức trên cây, giải thuật
minimax sẽ định giá trị cho các nút như sau:
-
Nếu
nút là nút lá gán cho nút đó một giá trị để phản ánh trạng thái
thắng, thua hay hòa của các đấu thủ
-
Sử
dụng giá trị các nút lá để xác định giá trị của các nút ở mức
trên nó:
+ Nút thuộc lớp MAX
gán cho nó giá trị lớn nhất trong các nút lá.
+ Nút thuộc lớp MIN
gán cho nó giá trị nhỏ nhất trong các nút lá.
Gán giá trị cho từng
thế cờ theo quy tắc trên chỉ rõ giá trị của trạng thái tốt nhất mà
mỗi đấu thủ hi vọng đạt được. Cũng giống như việc tính toán nước đi
và thực hiện một nước cờ thực sự của đấu thủ. Giải thuật Minimax
thể hiện việc tính toán nước đi tối ưu cho các đấu thủ thông quá
giá trị của các nút được gán. Trong một nước cờ, khi đấu thủ MAX đến
lượt đi, đấu thủ này sẽ chọn nước đi ứng với trạng thái có giá
trị cao nhất trong các trạng thái con, còn đấu thủ MIN sẽ chọn một
nước đi ứng với trạng thái có giá trị nhỏ nhất trong các trạng
thái con.
Tuy nhiên, cờ tướng là
một trò chơi có thể nói là phức tạp, việc mở rộng không gian trạng
thái khí áp dụng giải thuật Minimax có thể gặp khó khăn. Vì thế mà
chúng ta chỉ xét đến việc triển khai giải thuật Minimax với trò chơi cờ tướng có mức độ sâu được
định trước (khoảng 5 mức). Việc định trước mức hay độ sâu sẽ làm
giảm thời gian tính toán cho giải thuật và AI (máy) sẽ đưa ra được
một nước đi nhanh hơn và chính xác hơn. Phần sau đây mình sẽ trình
bày về quá trình xây dựng giải thuật Minimax.
2. Xây dựng giải thuật
2. Xây dựng giải thuật
Giả
sử chúng ta có một bộ phân tích thế cờ có thể áp dụng tất cả các luật, các
phương pháp đánh cờ khác nhau vào từng thế cờ và chuyển đổi chúng thành một con
số đại diện (cho điểm thế cờ). Mặt khác, ta giả sử con số đó là dương khi áp dụng
cho thế cờ của một đấu thủ (được gọi là đấu thủ MAX), và là âm khi áp dụng
cho đấu thủ bên kia (được gọi là đấu thủ MIN). Quá trình tính toán cho điểm
thế cờ được gọi là lượng giá tĩnh (static evaluation). Hàm thực hiện việc tính
toán được gọi là một bộ lượng giá tĩnh và giá trị nhận được gọi là điểm lượng
giá tĩnh. Cả hai đấu thủ đều cố gắng đi như thế nào đó để đạt được điểm tuyệt đối
lớn nhất. Đấu thủ MAX sẽ tìm những nước đi dẫn đến điểm của mình trở nên lớn
hơn (hay cao nhất có thể được) hay điểm của đối thủ bớt âm hơn (nhỏ hơn về giá
trị tuyệt đối). Còn đấu thủ MIN lại ra sức phản kháng lại, để dẫn tới điểm âm của
anh ta âm hơn hay điểm dương của đối thủ nhỏ đi.
Hình
2.4 Minh họa chiến lược chơi cờ của người lẫn máy
Ta có một ví dụ:
Hình 2.5 Cây trò chơi
thể hiện giải thuật Minimax
Trong ví dụ này: Người chơi cực đại hi vọng chọn
nước đi bên phải để đạt được điểm 8. Thế nhưng nếu đi như vậy thì khi đến lượt
đi của người chơi cực tiểu, anh ta sẽ cố gắng không cho người chơi cực đại đạt
được điểm này bằng cách chọn nước đi nhánh bên trái và như vậy, người chơi cực
đại chỉ được có 1 điểm thay vì 8. Ngược lại, nếu người chơi cực đại chọn nước
đi bên trái, thì trong tình huống xấu nhất anh ta vẫn còn được 2 điểm, lớn hơn
là chọn nước đi bên phải. Nói chung, người chơi cực đại sẽ phải tìm cách nhận
ra các nước đi của đối phương tiếp theo làm cho điểm giảm xuống. Và tương tự
như vậy, người chơi cực tiểu phải nhận biết được nước đi của người chơi cực đại
cố gắng làm tăng điểm lên. Thủ tục tìm nước đi tốt nhất trên cây trò chơi như
trên được gọi là thủ tục Minimax, do điểm ở mỗi nút có thể là điểm cực đại hoặc
có thể là điểm cực tiểu và có tư tưởng thuật toán như sau:
1. Nếu như đạt đến
giới hạn tìm kiếm (đến tầng dưới cùng của cây tìm kiếm), tính
giá trị tĩnh của thế cờ hiện tại ứng với người chơi ở
đó. Ghi nhớ kết quả.
2. Nếu như mức
đang xét là của người chơi cực tiểu, áp dụng thủ tục Minimax này cho các con của
nó. Ghi nhớ kết quả nhỏ nhất.
3. Nếu như mức đang xét là của người
chơi cực đại, áp dụng thủ tục Minimax này cho các con của nó. Ghi nhớ kết quả lớn
nhất
Hàm Minimax nhận vào một thế cờ pos
và trả về giá trị của thế cờ đó. Nếu thế cờ pos
tương ứng với nút lá trong cây trò chơi thì trả về giá trị đã được gán cho nút
lá. Ngược lại ta cho pos một giá trị
tạm value là - vô cùng hoặc + vô cùng tùy thuộc pos
là nút MAX hay MIN và xét các thế cờ con của pos. Sau khi một con của pos
có giá trị V thì đặt lại value = max(value, V) nếu n là nút MAX và value = min(
value, V) nếu n là nút MIN. Khi tất cả các con của n đã được xét thì giá trị tạm
value của pos trở thành giá trị của nó. (INFINITY thể hiện cho giá trị vô cùng).
Ta có giả mã cho giải thuật Minimax như sau:
function Minimax(pos): integer;
value, best: integer;
begin
if pos là nút lá then return
eval(pos)
else
begin
{Khởi tạo giá trị tạm cho
best}
if pos là nút MAX then
best:= -INFINITY
else best:=
INFINITY;
{hàm gen()
sinh ra mọi nước đi từ thế cờ pos}
gen(pos);
{Xét tất cả các con của pos, mỗi lần xác định
được giá trị của một nút con, ta phải đặt lại giá trị tạm value. Khi đã xét hết
tất cả các con thì value là giá trị của n}
while (còn lấy được một
nước đi m) do
begin
pos:= Tính
thế cờ mới nhờ đi m;
value =
Minimax (pos);
if pos là nút MAX then
if
(value > best) then best := value;
if pos là nút MIN then
if (value < best) then best:= value;
end;
Minimax :=best;
end;
end;
Xem xét đoạn chương trình
trên ta thấy:
Có hai hàm mới là hàm eval(pos) và hàm gen(pos). Hàm eval(pos) thực hiện việc
tính giá trị (lượng giá) của thế cờ pos. Hàm gen(pos) sinh ra tất cả các nước
đi có thể từ thế cờ pos hiện tại. Việc xây dựng hai hàm này sẽ phụ thuộc vào từng
trò chơi cụ thể.
Hàm đánh giá Eval ứng với mỗi trạng thái (thế cờ) pos của trò chơi với một
giá trị số Eval(pos). Giá trị này là sự đánh giá độ lợi thế của trạng thái pos.
Trạng thái pos càng thuận lợi cho MAX thì Eval(pos) là số dương càng lớn, pos
càng thuận lợi cho MIN thì eval(pos) là số âm càng nhỏ. Eval(pos) xấp xỉ 0 đối với trạng thái không lợi thế cho ai cả. Chất
lượng của chương trình chơi cờ phụ thuộc rất nhiều vào hàm đánh giá. Nếu hàm
đánh giá cho ta sự đánh giá không chính xác về các trạng thái, nó có thể hướng
ta đi tới trạng thái được xem là tốt, nhưng thực tế lại bất lợi cho ta. Thiết kế
một hàm đánh giá tốt là một việc khó. Đòi hỏi ta phải quan tâm đến nhiều nhân tố.
Ở đây có sự mâu thuẫn giữa độ chính xác của hàm đánh giá và thời gian tính của
nó. Hàm đánh giá chính xác sẽ đòi hỏi rất nhiều thời gian tính toán, mà người
chơi lại bị giới hạn bởi thời gian đưa ra nước đi.
Như chúng ta đã biết, không phải lúc nào cũng đến được tận các nút lá. Đặc
biệt trong các trò chơi cờ, bị giới hạn thời gian suy nghĩ của mỗi đối thủ đồng
thời số nút và độ sâu của cây trò chơi là rất lớn. Chúng ta khó có thể thực hiện
tìm kiếm đến độ sâu của các nút là. Vì vậy, chúng ta thực hiện tìm đến một độ
sâu depth cố định nào đó. Độ sâu depth càng lớn, hàm tìm kiếm càng gần
giá trị tối ưu, cũng có nghĩa là "trình độ suy nghĩ" của máy càng
cao! Như vậy, hàm Minimax bây giờ sẽ có lời gọi: Minimax(pos, depth) trong đó depth là độ sâu tìm kiếm.
Thông thường, để cho tiện (và cùng rất gần thực tế) ta coi cả hai người
chơi có cùng cách đánh giá về một thế cờ. Có điều thế cờ này là tốt với một người
thì phải được đánh giá là tồi với người kia và ngược lại. Trong máy tính cách
thể hiện tốt nhất là ta cho điểm một thế cờ có thêm dấu âm dương: dấu dương
dành cho người chơi cực đại và dấu âm cho người chơi cực tiểu. Với người chơi cực
đại sẽ mong muốn điểm này càng dương càng tốt, còn người chơi cực tiểu lại
mong muốn điểm này càng âm càng tốt. Do đó để dễ xử lí ta sẽ tùy theo mức người
chơi mà đổi dấu giá trị đánh giá thế cờ pos. Chú ý rằng, thay đổi độ sâu là
chuyển sang đối phương nên phải đổi dấu. Chương trình thực hiện đổi dấu như
sau:
value:= -Minimax
(pos, depth- 1); {Tính điểm của pos}
Do dùng cùng hàm lượng giá nên khi đến lượt, người chơi cực đại và cực tiểu
có cùng cái nhìn như nhau về một thế cờ. Điều này dẫn đến có thể dùng cùng cách
chọn nước đi tốt nhất cho họ. Vậy ta phát triển hàm Minimax thành dạng:
function Minimax (pos, depth): integer;
begin
if (depth = 0) or (pos là nút lá) then
Minimax:= Eval (pos) {Tính giá trị thế cờ pos}
else
begin
best := -INFINITY;
gen(pos); { Sinh ra mọi
nước đi từ thế cờ pos}
while còn lấy được một
nước đi m do
begin
pos := Tính thế cờ mới nhờ đi m;
value := -Minimax (pos, depth -1);
if value > best
then best := value;
end;
Minimax := best;
end;
end;
Trong cài đặt, bàn cờ được
biểu diễn bằng các biến toàn cục. Do đó thay cho truyền tham số là một bàn cờ mới
pos vào thủ tục Minimax thì người ta biến đổi luôn biến toàn cục này nhờ thược
hiện nước đi "thử" (nước đi dẫn đến bàn cờ mới pos). Sau khi Minimax
thực hiện việc tính toán dựa vào bàn cờ lưu ở biến toàn cục thì thuật toán sẽ
dùng một số thủ tục để loại bỏ nước đi này. Như vậy Minimax bỏ các tham số pos
như sau:
function Minimax(depth) : integer;
begin
if (depth = 0) then
Minimax := Eval { Tính giá trị thế
cờ pos }
else
begin
best := -INFINITY;
gen; {Sinh ra mọi nước đi từ thế cờ pos }
while (còn lấy được một
nước đi m) do
begin
thực hiện nước đi m;
value := -Minimax
(depth- 1);
bỏ thực hiện nước đi m;
if value > best then
best := value;
end;
Minimax := best;
end;
end;
3. Đánh giá
Thuật toán Minimax thăm
toàn bộ cây trò chơi bằng việc dùng chiến lược tìm kiếm theo chiều sâu. Nên độ
phức tạp của thuật toán này tương ứng trực tiếp với kích thước không gian tìm
kiếm bd, trong đó b là hệ
số phân nhánh của cây hay chính là nước đi hợp pháp tại mỗi điểm, d là độ sâu tối
đa của cây. Thuật toán sẽ thăm tất cả các nút không chỉ là các nút lá vì vậy số
lượng các nút được thăm sẽ là b(bd-1)(b-1).
Nhưng hàm lượng giá trị sẽ là phương thức chi phối hầu hết thời gian và chỉ làm
trên các nút lá, vì vậy việc thăm các nút không phải các nút lá có thể bỏ qua.
Do đó độ phức tạp thời gian là O(bd).
Bàn chất của thuật tóan là tìm kiếm theo chiều sâu, vì vậy việc đòi hỏi không
gian bộ nhớ của nó chỉ tuyến tính với d
và b. Vì thế độ phức tạp không gian
là O(bd).
Nếu hệ số nhánh trung
bình của cây là b=40, và tìm kiếm đến
độ sâu d=4 (các con số thường gặp
trong trò chơi cờ) thì số nút phải lượng giá là 404 =2560000 (trên 2
triệu rưỡi nút). Còn với b=40, d=5 thì
số nút phải lượng giá sẽ tăng 40 lần nữa thành 405=102400000 (trên
102 triệu nút) đây là con số tương đối lớn.
4. Ứng dụng vào AI cờ tướng
- Trong cả hai chế độ, có thể tuy
chỉnh “độ cao cờ”, cụ thể là độ sâu xét duyệt của thủ tục Minimax,
độ sâu càng lớn, độ khó càng tăng, thời gian tính toán của AI cũng
sẽ lâu hơn.
4.1 Lượng giá và
sinh nước đi
Như đã nói ở phần xây dựng giải
thuật Minimax trong chương hai, việc thiết kế hàm lượng giá eval() và
hàm sinh nước đi gen() đối với từng trò chơi là khác nhau, việc thiết
kế cũng khác nhau và khá phức tạp, trong phần này, mình sẽ trình
bày ngắn gọn về hai hàm này được xây dựng trong trò chơi Cờ tướng.
4.1.1 Lượng giá
Đánh giá một thế
cờ là một trong những nhiệm vụ quyết định chương trình chơi cờ của bạn có
là "cao thủ" hay không. Căn cứ vào một thế cờ máy sẽ gán cho nó một
điểm số (lượng giá tĩnh) để đánh giá độ tốt - xấu. Nhờ điểm này máy mới có thể
so sánh các thế cờ với nhau và biết chọn nước đi tốt nhất. Đây là một nhiệm vụ
rất khó khăn và phức tạp do không tồn tại một thuật toán tổng quát và thống nhất
nào để tính điểm cả. Điểm của một thế cờ dựa trên rất nhiều yếu tố mà khó có thể
số hoá hết được, như phụ thuộc vào số lượng và giá trị các quân cờ hiện tại, phụ
thuộc vào tính hãm, tính biến, thế công, thế thủ của từng quân cờ cũng như cả cục
diện trận đấu. Ví dụ, một cặp Mã giao chân, có thể sát cánh tiến quân và tựa
lưng phòng thủ thường có giá hơn hai Mã đứng rời. Nhưng cũng có lúc hai Mã đứng
rời lại tốt hơn hai Mã giao chân khi Mã này lại cản Mã kia trong một thế trận
nào đó. Ta cũng biết rằng, "lạc nước hai Xe đành bỏ phí, gặp thời một Tốt
cũng thành công", thế nhưng số hoá điều này qua hàm lượng giá quả là một
điều khó quá sức.
Trong phần này
chúng ta chỉ cài đặt phương pháp đơn giản nhưng cơ bản nhất: Lượng giá dựa trên
cơ sở giá trị của từng quân cờ. Cách tính này sẽ lấy tổng giá trị các quân cờ
hiện có của bên mình trừ đi tổng giá trị các quân cờ hiện có của đối phương. Do
đó, một thế cờ này hơn thế cờ kia ở chỗ nó còn nhiều quân bên mình hơn, nhiều
quân giá trị cao hơn cũng như có bắt được nhiều quân và quân giá trị cao của đối
phương hơn không.
Ta có đoạn
giả mã cho hàm lượng giá eval() như sau:
// Mảng 3 chiều
POINT_TABLE chứa điểm của các quân quân cờ ở các vị trí cụ thể trên bàn cờ.
private int eval() {
int sum = 0;
for (int i = 0; i < BOARD_SIZE; ++i)
{
if là bên MAX :
// Tổng điểm ở vị trí quân cờ i, màu i - 1, vị trí thứ
i trong bảng POINT_TABLE (tức điểm của nó ở vị trí cụ thể trên bàn cờ)
sum +=
POINT_TABLE[piece[i]][color[i] - 1][i];
else if là bên MIN :
sum -=
POINT_TABLE[piece[i]][color[i] - 1][i];
}
return sum + điểm thưởng khi quân cờ ở
các vị trí có lợi;
}
4.1.2 Sinh nước đi
Một trong những
việc quan trọng nhất để máy tính có thể chơi được cờ là phải chỉ cho nó biết mọi
nước đi có thể đi được từ một thế cờ. Máy sẽ tính toán để chọn nước đi có lợi
nhất cho nó. Các yêu cầu chính đối với một thủ tục sinh nước đi là:
·
Chính xác (rất quan trọng). Một nước đi
sai sẽ làm hỏng mọi tính toán. Đồng thời chương trình có thể bị trọng tài xử
thua luôn. Do số lượng nước đi sinh ra lớn, luật đi quân nhiều và phức tạp nên
việc kiểm tra tính đúng đắn tương đối khó.
·
Đầy đủ (rất quan trọng). Sinh được mọi
nước đi có thể có từ một thế cờ.
·
Nhanh. Do chức năng này phải sinh được
hàng triệu nước đi mỗi khi máy đến lượt nên giảm thời gian sinh nước đi có ý
nghĩa rất lớn.
Sinh nước đi là
một thuật toán vét cạn. Máy sẽ tìm mọi nước đi hợp lệ có thể có. Máy phải sinh
được từ những nước đi rất hay cho đến những nước đi rất ngớ ngẩn (như đẩy Tướng
vào vị trí khống chế của đối phương). Ta hình dung ngay thủ tục sinh nước đi
Gen sẽ có đầy những vòng lặp for, các câu lệnh kiểm tra if và rẽ nhánh case,
trong đó các phép tính kiểm tra giới hạn chiếm một phần đáng kể. Thủ tục này
luôn sinh nước cho bên đang tới lượt chơi căn cứ vào nội dung của biến side
(trong trương chình, biến side là một biến toàn cục chứa giá trị để
thể hiện một bên cờ, thêm một biến xside để thể hiện giá trị của
bên còn lại). Đây là một trong những thủ tục phức tạp và dễ sai nhất.
Dưới đây là
giả mã cho hàm sinh nước đi gen():
for Xét lần lượt từng quân cờ
bên hiện tại do
case quâncờ of
Xe: begin
while Xét lần lượt tất cả các ô từ bên phải Xe cho đến lề trái bàn cờ do
begin
if (ô đang xét là ô trống) or (ô đang xét chứa quân đối phương) then
gen_push(vị trí của Xe, vị trí ô đang xét)
if ô đang xét không trống then break;
end;
while Xét lần lượt tất cả các ô từ bên trái Xe cho đến lề phải bàn cờ do
...
while Xét lần lượt tất cả các ô từ bên trên Xe cho đến lề trên bàn cờ do
...
while Xét lần lượt tất cả các ô từ bên dưới Xe cho đến lề dưới bàn cờ do
...
end; { Xong quân Xe }
case quâncờ of
Xe: begin
while Xét lần lượt tất cả các ô từ bên phải Xe cho đến lề trái bàn cờ do
begin
if (ô đang xét là ô trống) or (ô đang xét chứa quân đối phương) then
gen_push(vị trí của Xe, vị trí ô đang xét)
if ô đang xét không trống then break;
end;
while Xét lần lượt tất cả các ô từ bên trái Xe cho đến lề phải bàn cờ do
...
while Xét lần lượt tất cả các ô từ bên trên Xe cho đến lề trên bàn cờ do
...
while Xét lần lượt tất cả các ô từ bên dưới Xe cho đến lề dưới bàn cờ do
...
end; { Xong quân Xe }
Pháo:
...
Trong đoạn mã
giả còn có thủ tục gen_push. Thủ tục gen_push có hai tham số là vị
trí xuất phát của quân cờ sẽ đi và đích đến của quân đó. Các nước
vừa sinh ra sẽ được đưa vào danh sách nước đi gen_dat nhờ thủ tục
gen_push này:
private void gen_push(int from, int dest) {
if
(moveSave(from, dest)) {
gen_dat[gen_end[ply]].m.from = from;
gen_dat[gen_end[ply]].m.dest = dest;
gen_end[ply]++;
}
Trình bày đến đây chắc các bạn cũng hiểu sơ sơ phải làm như thế nào để ứng dụng thuật toán Minimax vào trò chơi cờ tướng rồi đúng không. Hy vọng các bạn có thể tự làm cho mình một con AI cờ tướng để tự sướng nhé. Mình sẽ tiếp tục trở lại với các bạn vào loạt bài Tìm hiểu về trí tuệ nhân tạo trong các phần sau :)
Comments
Post a Comment