Sau một khoảng thời gian cũng lâu vừa đủ thì mình quyết định quay lại viết Blog, đi phân tích những cái trên trời dưới đất, chia sẽ kiến thức, kinh nghiệm và trải nghiệm của mình về Game và ngành Game.
Như tiêu đề bài viết thì hôm này mình sẽ đề cập đến việc vì sao trò Kéo - Búa - Bao huyền thoại, có
thể nói bài học vỡ lòng về thiết kế game (đặc biệt là cân bằng - Balancing) lại không cân bằng tuyệt đối. Và việc chứng minh này mình sẽ thông qua bài toán huyền thoại 0.(9) = 1 của toán học.
***Miễn trừ trừ trách nhiệm: bài viết này có thể làm thay đổi một tí "cảm nhận", có thể gây khó chịu và gây tranh cải dữ dội có thể dẫn đến gây war.
Bài toán 0.(9) = 1
Đầu tiên thì nói về bài toán 0.(9) = 1, hiện tại thì các nhà toán học đã chứng minh, khẳng định và được áp dụng vào giảng dạy (mọi người có thể kiếm thêm nguồn tại link này - Wikipedia), mình xin phép dẫn chứng lại như hình bên dưới:
Số 0.(3) hoặc 0.(9) ở đây là số thập phân vô hạn tuần hoàn, có vô số chữ số 3 và có vô số chữ số 9 ở phần thập phân. Như vậy có thể hiểu 1 = 0.(9) trên dãy "vô hạn", mình xin nhấn mạnh là "vô hạn", cái gì quan trọng nhắc lại 3 lần "vô hạn".
Kéo - Búa - Bao Tỉ Lệ Vàng:
Thật trùng hợp 1/3 cũng chính là tỉ lệ của trò chơi Kéo - Búa - Bao, dù lựa chọn nào người chơi luôn có 1/3 Thắng | 1/3 Hòa | 1/3 Thua
Nhưng buồn thay là 1/3 trong video game không phải là dãy vô hạn. Ở đây mình đang nói giữa 1/3 trong video game (trò chơi điện tử) chứ không phải 1/3 trong toán học.
Chúng ta đều biết video game là một dạng ứng dụng của khoa học máy tính, mà bộ nhớ của máy tính thì là "hữu hạn". Dành cho người đọc chưa biết thì "Về bản chất, mọi dữ liệu đều được chuyển đổi thành dạng nhị phân gồm hai ký tự cơ bản là 0 và 1. Đây là ngôn ngữ mà máy tính có thể hiểu và xử lý một cách chính xác" (trích bài viết của FPT). Chúng cần được lưu vào bộ nhớ, mà bộ nhớ thì lại có giới hạn, ví dụ: 1GB, 2GB,... ➡ một số thư viện cho chúng ta mở rộng số lượng chữ số sau số thập phân, nhưng con số tối đa mà máy tính có thể biểu diễn được luôn "bé hơn hoặc bằng" kích thước của bộ nhớ ➡ đây chính là sự "hữu hạn".
Vì là "hữu hạn" nên trong máy tính không thể nào biểu diễn được dãy 1/3 ở dưới dãy vô hạn. Lúc này để đáp ứng yêu cầu giới hạn của bộ nhớ, 1/3 trong máy tính là nột dãy số được biểu diễn dưới số thực 0.333...333 và vấn đề xuất hiện từ đây (thấy bắt đầu đau đầu rồi đó). Như đã biết Kéo - Búa - Bao có tỉ lệ bằng nhau nên 0.333...333 x 3 = 0.999...999 < 1 ➡ vì 0.999...999 là hữu hạn, vậy 0.000...001 nữa ở đâu????
Vì tổng tỉ lệ < 1 nên 0.000...001 là trường hợp bị mất hoặc còn được gọi là trường hợp không có gì, đây là vấn đề đến từ Floating-point (dấu phẩy động trong máy tính). Lúc này để đảm bảo không bị mất (lỗi) thì bắt buộc chúng ta phải chủ động cộng thêm 0.000...001 vào 1 trong 3 tỉ lệ của Kéo - Búa - Bao, ở đây mình ví dụ cộng vào Bao => 0.333...333 + 0.333...333 + 0.333...334 = 1 ➡ đã làm cho tỉ lệ Kéo - Búa - Bao lệch.
(Chú thích 0.33...33 ≈ 33.33...33% và 1 = 100% , mọi người có thể tham khảo thêm về số thực trong máy tính theo Link)
Vậy tại sao không giữ phân số luôn? Thật ra thắc mắc này là đúng, số thực trong máy tính luôn có rủi ro làm tròn, còn phân số thì lại ảnh hưởng performance. Vậy làm sao để khắc phục nó?
Lượng Hóa Giá Trị - Quantize Value:
Thay vì dùng Số Thực (Float/Double) thì sẽ dùng trên số Nguyên (Interger), phương pháp này sẽ xác định vùng [intMin : intMax] thay thể Số Thực để tiến hành random. Lúc này bài toán sẽ quay về dạng xác suất đơn giản như chúng ta đã học tại trường "Trong túi có X bi vàng, Y bi đỏ, Z bi xanh, số trường hợp thuận lợi để bốc ra được bi vàng/đỏ/xanh"
Lấy ví dụ trên bài toán Kéo - Búa - Bao, mỗi loại đều có tỉ lệ là 1/3, ta có thể biểu diễn dưới 2 trường hợp:
Từ 2 ví dụ trên chúng ta có thể thấy (thấy đau đầu) là Kéo - Búa - Bao vẫn đảm bảo tỉ lệ mặc dù là ở dãy số có giới hạn nếu như chúng ta xác định được vùng giới hạn, ở đây mình dùng thường vùng là [1 : intMax] lúc này chỉ cần xác định intMax là được. Hãy thử mở rộng bài toán, câu hỏi đặt ra là làm sao xác định intMax?
Đánh Trọng Số - Weight:
Lúc này vùng random sẽ từ [1 : Total Weight], cách này thì tự do hơn, intMax = Total Weight -> không cần biết trước intMax (một số bài viết gọi là scaler) nhưng cần chủ động kiểm soát Weight tại mỗi lựa chọn. Lấy ví dụ vẫn là Kéo - Búa - Bao
- Weight Kéo = 30
- Weight Búa = 30
- Weight Bao = 30
- => Total Weight = 90
#Weight Config
rock_weight = 30
paper_weight = 30
scissors_weight = 30
Xác định vùng kết quả của từng lựa chọn, chú ý là phải tạo thành một dãy số liên tục:
- Kéo = [1: Weight Kéo] = [1: 30]
- Búa = [Kéo Max + 1 : Kéo Max + Weight Búa] = [31 : 60]
- Bao = [Búa Max + 1 : Total Weight] = [61 : 90]
#Tính Toal Weight
total_weight = rock_weight + paper_weight + scissors_weight
#Tính Range
rock_range = [1,rock_weight]
paper_range = [rock_range[1] + 1, rock_range[1] + paper_weight]
scissors_range = [paper_range[1] +1, paper_range[1] + scissors_weight]
(Demo code Python, mình có thay đổi vị trí các biến một xíu cho lú)
Xác intMax từ đầu bằng cách xác định cần lấy bao nhiêu số sau phần thập phân (tạm gọi là n) sau đó lấy 10^n , lại lấy Kéo - Búa - Bao ra làm ví dụ: 1/3 -> 0.333...333 -> lấy 4 số thập phân -> 0.3333 -> 10^4 -> intMax =10000 -> sau đó lấy với con số được tính này.
Lúc này có thể hiểu 0.0001 trong số thực = 1 trong số nguyên, cách này giúp tránh trường hợp Floating-point nhưng trong một số trường hợp xấu vẫn phải tự điều chỉnh thêm bớt. Thông thường sẽ rất ít thấy vì luôn cố gắng đảm bảo đúng theo 10^n để đảm bảo 1 số nguyên = 0.000...1 số thực nên tỉ lệ cũng chỉ nằm trong khoảng được làm tròn.
- Có điều chỉnh tỉ lệ - xác định lựa chọn có tỉ lệ tăng thêm:
- Weight Kéo = 3333 => 3333/10000 = 33.33%
- Weight Búa = 3333 => 3333/10000 = 33.33%
- Weight Bao = intMax - (Weight Kéo + Weight Búa) = 10000 - (3333 + 3334) = 3334 => 3334 /10000 => 33.34%
- Không điều chỉnh tỉ lệ - ví dụ rate cho ban đầu là 30%, 35%, 35%
- Kéo = 3000
- Búa = 3500
- Bao = 10000 - (3000 + 3500) = 3500
Sau khi có Weight thì tính vùng kết quả như trên
#Rate đầu vào và số lượng chữ số muốn làm tròn
rate = [0.3, 0.35, 0.35]
round_num = 4
#Tính intMax
max_range = pow(10, 4)
#Tính Weight, ép kiểu vì rate đang là float nên gây ra chấm động 0.000...xxxx
rock_weight = int(rate[0] * max_range)
paper_weight = int(rate[1] * max_range)
scissors_weight = max_range - (rock_weight + paper_weight)
#Tính Toal Weight
total_weight = rock_weight + paper_weight + scissors_weight
#Tính vùng Random
rock_range = [1,rock_weight]
paper_range = [rock_range[1] + 1, rock_range[1] + paper_weight]
scissors_range = [paper_range[1] +1, paper_range[1] + scissors_weight]
(Demo code Python, mình có thay đổi vị trí các biến một xíu cho lú)
(chạy random 10 lần)
Kéo - Búa - Bao luôn là tỉ lệ vàng cho việc cân bằng, nhưng trong video game chúng ta lại quên đi cách máy tính thật sự hoạt động dẫn đến các sai lệch không mong muốn. Mình hy vọng bài viết này có thể giúp mọi người đang là Game Designer riêng và làm Game nói chung có thể hiểu hơn về một số vấn đề nhỏ. Mọi người có ý kiến góp ý có thể comment lại trong bài viết này hoặc tại post Facebook. Cảm ơn mọi người rất nhiều!
Phan Thanh Quốc Huy
Email: ptqhuy1612@gmail.com




Nhận xét
Đăng nhận xét