Thực nghiệm xác suất đối với súc sắc bằng Canvas và Javascript

Nhằm thuyết phục học sinh rằng xác suất xuất hiện các mặt của viên súc sắc là như nhau và đều bằng $\dfrac{1}{6}\approx0,1666\ldots$, thay vì trải nghiệm thực với các viên súc sắc và hàng trăm, hàng nghìn lượt ném, tôi quyết định sử dụng máy tính để giải quyết giúp nhiệm vụ này.

Có rất nhiều nền tảng và thuật toán khả dĩ thực hiện được việc thực nghiệm xác suất, tính toán và vẽ hình như mô tả ở đầu bài viết. Tuy nhiên, tôi nhận thấy việc chạy một chương trình javascript trên nền web là thuận lợi nhất. Một là, web có thể được truy cập bằng hầu hết các thiết bị có kết nối internet, thậm chí là với các TV thông minh. Hai là, khả năng thể hiện các hoạt cảnh, tính toán của Javascript rất mượt mà và vô cùng bá đạo, lại không chiếm nhiều tài nguyên của thiết bị, thuật toán cũng không quá phức tạp nữa. PR vậy đủ rồi, triển thôi!

Trước tiên, ta cần tạo một file index.html để hiển thị chương trình, file này có thể mở trực tiếp trên máy mà không cần đến máy chủ ảo.

<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            margin: 0;
        }
    </style>
</head>
<body>
    <canvas></canvas>
    <script src="canvas.js"></script>
</body>
</html>

Trong thẻ body, có hai thẻ cần khai báo là canvas và file canvas.js để thực thi chương trình. Tiếp theo đây, ta sẽ viết code Javascript để thể hiện thông tin trong vùng canvas. File canvas.js được đặt chung thư mục với file index.html, nếu đặt trong thư mục khác thì sửa lại src trong index.html.

Trước tiên, ta cần khai báo môi trường làm việc với canvas:

const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');

canvas.width = innerWidth;
canvas.height= innerHeight;

Tiếp theo, ta cần tạo một mảng chứa các màu sắc để vẽ biểu đồ cột, điều này là không bắt buộc, chỉ nhằm tăng thêm tác động thị giác thôi. Ngoài ra, ta cũng cần chèn thêm hình ảnh các mặt của con súc sắc, ở đây tôi chèn ảnh có sẳn để code đơn giản và nhẹ nhàng hơn.

const colors = ['red','blue','magenta','green','purple','brown'];
const image = new Image();
image.src = "dice.jpg";

let rdDice = [0,0,0,0,0,0];
let rdSum  = 0;

Mảng rdDice dùng để chứa số kết quả số lần xuất hiện của 6 mặt, biến rdSum để ghi lại tổng số lần gieo, cũng chính là số phần tử của không gian mẫu.

Ta cũng cần tạo một function để tạo một số nguyên ngẫu nhiên trong khoảng từ 1 đến 6, tương tự như chức năng RanInt trên máy tính cầm tay. Khác với PHP, Javascript chỉ cung cấp hàm  Math.random()  để tạo số thực ngẫu nhiên từ 0 đến 1 nên ta buộc phải viết hàm riêng.

function RandInt(min,max){
    return Math.floor(min+Math.random()*(max-min+1));
}

Sau đây là phần quan trọng nhất, ta phải tạo một chương trình tạo hoạt cảnh chuyển động và cập nhật liên tục số lần gieo cũng như xác suất của từng mặt súc sắc.

function animate(){
    requestAnimationFrame(animate);
    c.clearRect(0,0,canvas.width,canvas.height);

    rdSum += 1;
    let rdNum = RandInt(1,6);
    if(rdNum==1){rdDice[0]=rdDice[0]+1;}
    if(rdNum==2){rdDice[1]=rdDice[1]+1;}
    if(rdNum==3){rdDice[2]=rdDice[2]+1;}
    if(rdNum==4){rdDice[3]=rdDice[3]+1;}
    if(rdNum==5){rdDice[4]=rdDice[4]+1;}
    if(rdNum==6){rdDice[5]=rdDice[5]+1;}
}

animate();

Hàm  animate()  này sẽ thực thi nhiệm vụ liên tục “gieo súc sắc”, mỗi lần ra kết quả mới thì xóa màn hình cũ và vẽ lại các hình mới bằng những giá trị mới. Mỗi lần chuyển cảnh, giá trị rdSum sẽ tăng 1, đồng thời tùy kết quả random mà giá trị rdDice[i] tương ứng sẽ tăng 1. Bây giờ ta sẽ thực hiện công việc cuối cùng là vẽ hình, vì có 6 mặt súc sắc nên ta sẽ dùng vòng lặp for.

for (var j=1;j<=6;j++){
        c.beginPath();

        if(j==1){
            c.drawImage(image,4,0,170,170,5,50*j-25,40,40);
        }
        if(j==2){
            c.drawImage(image,164,0,170,170,5,50*j-25,40,40);
        }
        if(j==3){
            c.drawImage(image,326,0,170,170,5,50*j-25,40,40);
        }
        if(j==4){
            c.drawImage(image,4,170,170,170,5,50*j-25,40,40);
        }
        if(j==5){
            c.drawImage(image,164,170,170,170,5,50*j-25,40,40);
        }
        if(j==6){
            c.drawImage(image,326,170,170,170,5,50*j-25,40,40);
        }

        c.fillStyle = colors[j-1];
        c.font = "20px Arial";
        c.fillText(rdDice[j-1]+" ("+(rdDice[j-1]/rdSum).toFixed(5)+")", 55, 50*j+6);

        c.fillRect(210,50*j-10,canvas.width*2*rdDice[j-1]/rdSum,20);

        c.closePath();
    }

Các dòng lệnh c.drawImage()  dùng để chèn ảnh các mặt của viên súc sắc, cái hay của lệnh này là có thể hiển thị một phần của hình, chức năng này tương tự như lệnh background trong CSS. Quý bạn đọc có thể lưu ảnh các mặt súc sắc dưới đây về máy để dùng.

Lệnh c.fillText(rdDice[j-1]+" ("+(rdDice[j-1]/rdSum).toFixed(5)+")", 55, 50*j+6); dùng để hiển thị dòng chữ, bao gồm số lần xuất hiện của từng mặt và tỉ lệ so với tổng số lần thử, khi số lần thử càng lớn thì tỉ lệ này càng tiệm cận với xác suất $0,1666\ldots$. Còn lệnh c.fillRect(210,50*j-10,canvas.width*2*rdDice[j-1]/rdSum,20); để vẽ hình chữ nhật, kiểu như biểu đồ cột nằm ngang, độ rộng của mỗi hình chữ nhật liên tục thay đổi là do hàm  animate()  liên tục xóa màng hình và vẽ mới.

Sau đây là full code của file canvas.js:

const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');

canvas.width = innerWidth;
canvas.height= innerHeight;

const colors = ['red','blue','magenta','green','purple','brown'];
const image = new Image();
image.src = "dice.jpg";

let rdDice = [0,0,0,0,0,0];
let rdSum  = 0;

function RandInt(min,max){
    return Math.floor(min+Math.random()*(max-min+1));
}

function animate(){
    requestAnimationFrame(animate);
    c.clearRect(0,0,canvas.width,canvas.height);
    rdSum += 1;
    let rdNum = RandInt(1,6);
    if(rdNum==1){rdDice[0]=rdDice[0]+1;}
    if(rdNum==2){rdDice[1]=rdDice[1]+1;}
    if(rdNum==3){rdDice[2]=rdDice[2]+1;}
    if(rdNum==4){rdDice[3]=rdDice[3]+1;}
    if(rdNum==5){rdDice[4]=rdDice[4]+1;}
    if(rdNum==6){rdDice[5]=rdDice[5]+1;}

    for (var j=1;j<=6;j++){
        c.beginPath();
        if(j==1){
            c.drawImage(image,4,0,170,170,5,50*j-25,40,40);
        }
        if(j==2){
            c.drawImage(image,164,0,170,170,5,50*j-25,40,40);
        }
        if(j==3){
            c.drawImage(image,326,0,170,170,5,50*j-25,40,40);
        }
        if(j==4){
            c.drawImage(image,4,170,170,170,5,50*j-25,40,40);
        }
        if(j==5){
            c.drawImage(image,164,170,170,170,5,50*j-25,40,40);
        }
        if(j==6){
            c.drawImage(image,326,170,170,170,5,50*j-25,40,40);
        }
        c.fillStyle = colors[j-1];
        c.font = "20px Arial";
        c.fillText(rdDice[j-1]+" ("+(rdDice[j-1]/rdSum).toFixed(5)+")", 55, 50*j+6);
        c.fillRect(210,50*j-10,canvas.width*2*rdDice[j-1]/rdSum,20);
        c.closePath();
    }
}

animate();

Chúc quý bạn đọc thành công và hài lòng với ý tưởng này, rất mong nhận được ý kiến đóng góp để chương trình hoàn thiện và phát triển thêm. Và đây là thành phẩm:

https://huynhphusi.com/lab/dice/

Bình luận

Chia sẻ