- Quy tắc chiến đấu của Pokémon gần giống một bộ máy quy tắc đan xen giữa tương khắc hệ, chiêu thức, chỉ số và đặc tính, nên có thể biểu diễn ngắn gọn bằng mô hình quan hệ và quy tắc của Prolog
- Prolog lưu các sự kiện bằng vị từ như
pokemon/1, type/2, rồi dùng biến viết hoa và phép hợp nhất để tìm Pokémon phù hợp với điều kiện về hệ và chiêu thức
- Việc tìm Pokémon học được Freeze-Dry, thuộc hệ Ice và có Special Attack lớn hơn 120 cho thấy truy vấn Prolog ngắn hơn nhiều so với nhiều
EXISTS trong SQL
- Đội draft có thể được biểu diễn bằng các vị từ như
alex/1, morry/1, còn quy tắc về chiêu ưu tiên có thể chồng thêm từng lớp điều kiện loại trừ và hiệu ứng Prankster
- Bảng tính như Techno's Prep Doc rất mạnh, nhưng cơ sở dữ liệu Prolog linh hoạt hơn cho các truy vấn tổ hợp tùy ý và được triển khai bằng prologdex cùng Scryer Prolog
Vì sao quy tắc chiến đấu của Pokémon phù hợp với lập trình logic
- Chiến đấu Pokémon gần với một bộ máy quy tắc nơi nhiều quy tắc móc nối phức tạp với nhau, và lập trình logic như Prolog rất phù hợp để biểu diễn ngắn gọn các quan hệ như vậy
- Pokémon là các nhân vật có tên loài, với hơn 1.000 loài từ Bulbasaur #1) đến Pecharunt #1025)
- Trong các trận chiến của dòng game chính, hai đội gồm 6 con sẽ đối đầu nhau; mỗi Pokémon thường chọn một trong 4 chiêu thức để gây sát thương cho đối thủ, và bên nào đưa HP của toàn bộ đội đối phương về 0 sẽ thắng
- Hiệu năng chiến đấu thay đổi theo chỉ số cơ bản, danh sách chiêu thức có thể học, đặc tính và hệ, nên số lượng tổ hợp rất lớn và đáng để theo dõi bằng phần mềm
- Hệ được gắn cho cả chiêu thức lẫn Pokémon; nếu hệ chiêu thức khắc hệ của đối thủ thì gây sát thương gấp 2, còn nếu bị khắc thì chỉ gây 1/2 sát thương
- Hệ số tương khắc của hệ được cộng dồn
- Scizor) thuộc hệ Bug/Steel, cả hai đều yếu trước Fire nên nhận sát thương gấp 4 từ chiêu Fire
- Nếu dùng chiêu Electric lên Swampert) hệ Water/Ground thì sát thương sẽ bằng 0 do khả năng miễn nhiễm của Ground
Mô hình cơ bản của Prolog
- Trong Prolog, các quan hệ được khai báo bằng vị từ (predicate)
pokemon(bulbasaur).
pokemon(ivysaur).
pokemon(venusaur).
pokemon(charmander).
pokemon(charmeleon).
pokemon(charizard).
pokemon(squirtle).
pokemon(wartortle).
pokemon(blastoise).
pokemon/1 là một vị từ tên pokemon với một đối số, và truy vấn như pokemon(squirtle). sẽ kiểm tra xem có thể làm cho mệnh đề đó đúng hay không
?- pokemon(squirtle).
true.
?- pokemon(alex).
false.
- Hệ của Pokémon có thể được biểu diễn như một quan hệ hai đối số kiểu
type/2, và Pokémon có hai hệ sẽ có hai sự kiện type cho cùng một Pokémon
type(bulbasaur, grass).
type(bulbasaur, poison).
type(charmander, fire).
type(charizard, fire).
type(charizard, flying).
type(squirtle, water).
- Tên bắt đầu bằng chữ hoa là biến, và Prolog sẽ cố gắng hợp nhất (unify) một truy vấn chứa biến với mọi giá trị khả dĩ
?- type(squirtle, Type).
Type = water.
?- type(venusaur, Type).
Type = grass
; Type = poison.
- Nếu đặt đối số đầu tiên là biến như
type(Pokemon, grass). thì có thể tìm toàn bộ Pokémon hệ Grass; trong dữ liệu thực tế sẽ ra 164 kết quả
- Dấu phẩy có nghĩa là tất cả các vị từ đều phải thỏa mãn, và cùng một tên biến phải nhận cùng một giá trị trong truy vấn
?- type(Pokemon, water), type(Pokemon, ice).
Pokemon = dewgong
; Pokemon = cloyster
; Pokemon = lapras
; Pokemon = laprasgmax
; Pokemon = spheal
; Pokemon = sealeo
; Pokemon = walrein
; Pokemon = arctovish
; Pokemon = ironbundle
; false.
- Giống như Iron Bundle), chỉ số và các chiêu thức có thể học cũng có thể được truy vấn dưới dạng quan hệ
?- pokemon_spa(ironbundle, SpA).
SpA = 124.
?- learns(ironbundle, Move), move_category(Move, special).
Move = aircutter
; Move = blizzard
; Move = chillingwater
; Move = freezedry
; Move = hydropump
; Move = hyperbeam
; Move = icebeam
; Move = icywind
; Move = powdersnow
; Move = swift
; Move = terablast
; Move = waterpulse
; Move = whirlpool.
- Nếu kết hợp thêm ràng buộc như
SpA #> 120, ta có thể lập tức tìm ra Pokémon có Special Attack lớn hơn 120, học được Freeze-Dry và thuộc hệ Ice
?- pokemon_spa(Pokemon, SpA), SpA #> 120, learns(Pokemon, freezedry), type(Pokemon, ice).
Pokemon = glaceon, SpA = 130
; Pokemon = kyurem, SpA = 130
; Pokemon = kyuremwhite, SpA = 170
; Pokemon = ironbundle, SpA = 124
; false.
- Quy tắc (rule) trong Prolog gồm phần đầu và phần thân; nếu phần thân đúng thì phần đầu cũng được hợp nhất
damaging_move(Move) :-
move_category(Move, physical)
; move_category(Move, special).
- Quy tắc này phân loại trực tiếp các chiêu Physical hoặc Special là chiêu gây sát thương
?- damaging_move(tackle).
true.
?- damaging_move(rest).
false.
Biểu đạt truy vấn so với SQL
- Các ví dụ từ nãy đến giờ về mặt logic chỉ là tổ hợp đơn giản của
and và or, nhưng trong Prolog, truy vấn quan hệ có thể ngắn hơn SQL và dễ chỉnh sửa hơn
- Nếu tổ chức cùng dữ liệu đó bằng SQL, có thể tách Pokémon, hệ và chiêu thức thành các bảng riêng
CREATE TABLE pokemon (pokemon_name TEXT, special_attack INTEGER);
CREATE TABLE pokemon_types(pokemon_name TEXT, type TEXT);
CREATE TABLE pokemon_moves(pokemon_name TEXT, move TEXT, category TEXT);
- Để tìm bằng SQL các Pokémon học được Freeze-Dry, thuộc hệ Ice và có Special Attack lớn hơn 120, cần dùng
EXISTS nhiều lần
SELECT DISTINCT pokmeon, special_attack
FROM pokemon as p
WHERE
p.special_attack > 120
AND EXISTS (
SELECT 1
FROM pokemon_moves as pm
WHERE p.pokemon_name = pm.pokemon_name AND move = 'freezedry'
)
AND EXISTS (
SELECT 1
FROM pokemon_types as pt
WHERE p.pokemon_name = pt.pokemon_name AND type = 'ice'
);
- Truy vấn Prolog tương ứng chỉ cần liệt kê trực tiếp các quan hệ cần thiết
?- pokemon_spa(Pokemon, SpA),
SpA #> 120,
learns(Pokemon, freezedry),
type(Pokemon, ice).
- Khi điều kiện tiếp tục được thêm vào, truy vấn SQL dễ trở nên phức tạp, còn truy vấn Prolog vẫn giữ được dạng dễ đọc và dễ sửa nếu đã quen với cách biến hoạt động
Cách xếp chồng các quy tắc chiến đấu
- Trong chiến đấu Pokémon có rất nhiều quy tắc tương tác như trượt đòn, tăng·giảm chỉ số, hiệu ứng vật phẩm, khoảng sát thương, trạng thái bất thường, hiệu ứng sân như thời tiết·địa hình·Trick Room, đặc tính, phân bổ chỉ số cơ bản từ trước
- Khi làm phần mềm cho Pokémon, cần xử lý độ phức tạp này mà vẫn giữ mô hình ở mức có thể kiểm soát được
- Prolog có thế mạnh ở mô hình truy vấn mô tả các tổ hợp tức thời và ở việc xếp lớp quy tắc một cách nhất quán
- Có thể tự kiểm chứng độ phức tạp này qua damage calculator
Draft League và truy vấn chiêu thức ưu tiên
- Trong Pokémon Draft, mỗi Pokémon có một giá trị riêng, và người chơi sẽ chọn Pokémon trong số điểm giới hạn để tạo thành một đội khoảng 8~11 con
- Vì trận đấu thực tế là 6v6, nên việc chuẩn bị để đối phó với mọi tổ hợp sáu con đối thủ có thể mang theo, rồi chọn ra sáu con để ứng chiến là rất quan trọng
- Các Pokémon mình đã chọn có thể được biểu diễn ngay bằng vị từ như
alex/1
alex(meowscarada).
alex(weezinggalar).
alex(swampertmega).
alex(latios).
alex(volcarona).
alex(tornadus).
alex(politoed).
alex(archaludon).
alex(beartic).
alex(dusclops).
- Truy vấn tìm Pokémon trong đội này học được Freeze-Dry thì đơn giản, nhưng không có kết quả
?- alex(Pokemon), learns(Pokemon, freezedry).
false.
- Thứ tự hành động trong chiến đấu về cơ bản do Speed quyết định, nhưng chiêu thức có độ ưu tiên (priority), và chiêu có độ ưu tiên cao hơn sẽ ra trước
- Độ ưu tiên của phần lớn chiêu thức là 0, nhưng chiêu có độ ưu tiên 1 như Accelerock sẽ đi trước chiêu độ ưu tiên 0 của một Pokémon nhanh hơn
- Các chiêu có độ ưu tiên dương mà một Pokémon cụ thể học được có thể được tìm bằng cách kết hợp
learns/2, move_priority/2 và điều kiện về độ ưu tiên
- Truy vấn đơn giản sẽ bao gồm cả những chiêu có ý nghĩa lớn trong Double Battles như Helping Hand, Ally Switch hoặc những chiêu ít ý nghĩa trong thi đấu thực chiến như Bide
\+/1 đúng khi mục tiêu thất bại, còn dif/2 có nghĩa là hai hạng tử khác nhau, nên có thể thêm quy tắc để loại trừ các chiêu dành cho Double Battles và Bide
learns_priority(Mon, Move, Priority) :-
learns(Mon, Move),
\+ doubles_move(Move),
dif(Move, bide),
move_priority(Move, Priority),
Priority #> 0.
- Nếu loại thêm cả các chiêu mang tính phòng thủ như Protect, Detect, Endure, Magic Coat, thì chỉ còn lại các chiêu ưu tiên thực sự có thể gây sát thương hoặc hiệu ứng bất lợi lên đối thủ
?- alex(Pokemon), learns_priority(Pokemon, Move, Priority).
Pokemon = meowscarada, Move = quickattack, Priority = 1
; Pokemon = meowscarada, Move = suckerpunch, Priority = 1
; Pokemon = beartic, Move = aquajet, Priority = 1
; Pokemon = dusclops, Move = shadowsneak, Priority = 1
; Pokemon = dusclops, Move = snatch, Priority = 4
; Pokemon = dusclops, Move = suckerpunch, Priority = 1
; false.
- Áp dụng cùng quy tắc đó cho vị từ của đội đối thủ thì cũng có thể tìm ngay các chiêu ưu tiên mà đối thủ đang có
?- morry(Pokemon), learns_priority(Pokemon, Move, Priority).
Pokemon = mawilemega, Move = snatch, Priority = 4
; Pokemon = mawilemega, Move = suckerpunch, Priority = 1
; Pokemon = walkingwake, Move = aquajet, Priority = 1
; Pokemon = ursaluna, Move = babydolleyes, Priority = 1
; Pokemon = lokix, Move = feint, Priority = 2
; Pokemon = lokix, Move = firstimpression, Priority = 2
; Pokemon = lokix, Move = suckerpunch, Priority = 1
; Pokemon = alakazam, Move = snatch, Priority = 4
; Pokemon = skarmory, Move = feint, Priority = 2
; Pokemon = froslass, Move = iceshard, Priority = 1
; Pokemon = froslass, Move = snatch, Priority = 4
; Pokemon = froslass, Move = suckerpunch, Priority = 1
; Pokemon = dipplin, Move = suckerpunch, Priority = 1.
Mở rộng đặc tính Prankster
- Pokémon có đặc tính Prankster sẽ được cộng thêm +1 độ ưu tiên cho chiêu trạng thái, và hiệu ứng này cũng có thể được cộng vào quy tắc
learns_priority/3 hiện có
- Trong đội, Tornadus có đặc tính Prankster
?- alex(Pokemon), pokemon_ability(Pokemon, prankster).
Pokemon = tornadus
; false.
- Có thể dùng cú pháp if/then
->/2 của Prolog để: nếu Pokémon có Prankster và phân loại chiêu là status thì cộng 1 vào độ ưu tiên cơ bản, còn không thì giữ nguyên độ ưu tiên cơ bản
learns_priority(Mon, Move, Priority) :-
learns(Mon, Move),
\+ doubles_move(Move),
\+ protection_move(Move),
Move \= bide,
move_priority(Move, BasePriority),
(
pokemon_ability(Mon, prankster), move_category(Move, status) ->
Priority #= BasePriority + 1
; Priority #= BasePriority
),
Priority #> 0.
- Sau quy tắc này, cùng một truy vấn sẽ bao gồm các chiêu trạng thái như Agility, Defog, Nasty Plot, Rain Dance, Tailwind, Taunt, Toxic của Tornadus với độ ưu tiên 1
- Chỉ cần mở rộng một quy tắc là đã có thể phản ánh cả hiệu ứng đặc tính, cho thấy ưu điểm phân lớp của Prolog
Đối chiếu với công cụ dựa trên bảng tính
- Trong cộng đồng Pokémon đã có sẵn các tài nguyên để tìm thông tin như chiêu ưu tiên của đội đối thủ; tiêu biểu là các Google Sheets nâng cao như “Techno’s Prep Doc”
- Bảng tính này khi nhập đội vào sẽ tạo ra nhiều thông tin về matchup, hỗ trợ nhiều định dạng, có tài liệu trực quan dễ lướt và tự động hoàn thành
- Công thức để tìm chiêu ưu tiên kết hợp
FILTER, VLOOKUP, INDIRECT, trong đó INDIRECT trả về tham chiếu ô
={IFERROR(ARRAYFORMULA(VLOOKUP(FILTER(INDIRECT(Matchup!$S$3&"!$AV$4:$AV"),INDIRECT(Matchup!$S$3&"!$AT$4:$AT")="X"),{Backend!$L$2:$L,Backend!$F$2:$F},2,FALSE))),IFERROR(FILTER(INDIRECT(Matchup!$S$3&"!$AW$4:$AW"),INDIRECT(Matchup!$S$3&"!$AT$4:$AT")="X"))}
- Sheet Backend liệt kê toàn bộ chiêu, và cấu trúc này khá giống với một phiên bản hardcode truy vấn Prolog
- Cơ sở dữ liệu Prolog có khả năng mở rộng tốt hơn cách hardcode danh sách các chiêu đáng chú ý, và có thể tra cứu bất kỳ chiêu nào
- Ngay cả những câu hỏi kết hợp không có trong công cụ sẵn có, như tìm các chiêu Special mà Tornadus học được và gây sát thương siêu hiệu quả lên thành viên trong đội của Justin, cũng có thể được biểu đạt ngắn gọn
?- justin(Target), learns(tornadus, Move), super_effective_move(Move, Target), move_category(Move, special).
Target = charizardmegay, Move = chillingwater
; Target = terapagosterastal, Move = focusblast
; Target = alomomola, Move = grassknot
; Target = scizor, Move = heatwave
; Target = scizor, Move = incinerate
; Target = runerigus, Move = chillingwater
; Target = runerigus, Move = darkpulse
; Target = runerigus, Move = grassknot
; Target = runerigus, Move = icywind
; Target = screamtail, Move = sludgebomb
; Target = screamtail, Move = sludgewave
; Target = trapinch, Move = chillingwater
; Target = trapinch, Move = grassknot
; Target = trapinch, Move = icywind
; false.
Ghi chú triển khai và giới hạn
1 bình luận
Ý kiến trên Lobste.rs
Mình khá tò mò không biết có ai thực sự dùng Prolog một cách hiệu quả không. Dù là cho công việc hay mục đích cá nhân đều được, vì từ trước đến giờ mình chỉ thấy những ví dụ kiểu đồ chơi như thế này
Nói chính xác thì tôi cũng đang vận hành ít nhất một đoạn mã Prolog, đó là dashboard phân tích nội bộ, và trước đây tôi còn viết backend server cho một ứng dụng iOS bằng Prolog. Trong quá trình đó, để gửi thông báo APNS mà không cần dịch vụ bên ngoài, tôi cũng đã tạo một thư viện HTTP/2 client cho Prolog
Trong cộng đồng Prolog chắc chắn có những người thích dùng nó cho các thứ như web server, nhưng với cá nhân tôi, nó giống như mở ra một nhánh kỹ năng khác. Chẳng hạn, tôi thấy mình nhận ra tốt hơn khi nào một parser tùy biến hoặc một DSL sẽ phù hợp với một vấn đề nào đó.
Với kiến thức có được sau khi viết bài này, tôi đã tái triển khai một tập con hữu ích của engine logic thuế trong IRS Fact Graph. Prolog hợp với công việc này một cách đáng ngạc nhiên, vì nó làm lộ ra và buộc phải xử lý những góc cạnh không được tài liệu hóa mà trong cách triển khai mệnh lệnh người ta rất dễ bỏ qua.
Phần “thực thi” thì tôi vẫn chưa hoàn thiện, nhưng phần parsing đã đủ xong để tôi có thể viết được một bản nháp tài liệu khá ổn. Hiện còn thiếu một tính năng lớn là phép toán ngày tháng, và khi thêm xong phần đó tôi định sẽ viết riêng về nó.
Khi dùng DCG, Prolog rất xuất sắc trong việc parse văn bản có cấu trúc phức tạp. Trước đây, nếu awk không kham nổi thì tôi sẽ nghĩ đến Python hay JS, nhưng giờ tôi thấy Prolog rất hợp ở những chỗ cần cấu trúc và tính kỷ luật. Cái cảm giác cổ điển khi viết một codebase phức tạp gói trong một file cũng khá thỏa mãn, mà nó lại không quá cô đọng như APL.
Bản thân ví dụ thì đơn giản, nhưng trường hợp Pokémon thì không phải vậy. Hầu hết những ví dụ trông có vẻ đơn giản đó đều khả thi vì đã có một codebase triển khai cực kỳ kỹ lưỡng cơ chế chiến đấu phức tạp đến mức buồn cười. Tôi có hứng thú với việc tạo một rule engine Prolog để thực hiện một phần công việc mà bộ công cụ hiện có đang làm, và cũng đã thử dần dần; ưu điểm là so với mã mệnh lệnh, depth-first search giúp bộc lộ các khả năng dễ hơn và thuận tiện hơn cho bảo trì
Tài liệu của Scryer Prolog cũng được tạo bằng một chương trình Prolog mà tôi gọi là DocLog. Tôi cũng đã tạo ra vài thư viện mà các chương trình này dùng. Bản thân SWI Prolog cũng trực tiếp dùng SWI cho website của họ, web IDE tên Swish, máy chủ ClioPatria, v.v.
Tôi từng thử dùng SQLite như một kiểu bảng tính an toàn kiểu dữ liệu để sắp xếp thông tin. Việc đó vẫn làm được ngay cả khi hoàn toàn không có giao diện CRUD ở phía trên.
Tuy vậy, nó không phải lúc nào cũng là ngôn ngữ dễ nhìn nhất. Tôi cũng đã xem qua Datalog một thời gian, nhưng phần lớn các implementation dường như phù hợp hơn để nhúng vào các chương trình lớn chứ không phải một công cụ để ghi chép thông tin dễ dàng như trong bài này.
Có lẽ Prolog mới thực sự là công cụ nên dùng
Prolog là siêu tập của Datalog nên mọi thứ Datalog làm được thì nó đều làm được, và còn hơn thế nữa. Tuy nhiên chính phần “hơn thế nữa” đôi khi lại là vấn đề, vì lúc này nó là một ngôn ngữ Turing-complete nên có thể không bao giờ kết thúc, việc suy luận có thể khó hơn một chút, và cũng có thể bị dùng theo những cách không an toàn.
Datalog nhờ có các ràng buộc nên có thể dùng những đường tắt, vì thế trong nhiều trường hợp hiệu năng cũng tốt hơn.
Cuối cùng, trong Prolog thì dữ liệu cũng chính là mã, tức có homoiconicity, nên thao tác tạo/sửa/xóa về bản chất là thay đổi mã nguồn. Có thể làm động được, nhưng khó mà mong quy trình sẽ thật trơn tru, và vẫn cần một mức đồng bộ thủ công nhất định giữa file và chương trình đã được nạp
Tôi muốn học Prolog sâu hơn để thử dùng nó cho truy vấn tập lệnh máy.
Nhưng việc mô hình hóa logic, bao gồm cả số lượng và các số nguyên ở mức bit, khá khó