[WebAssembly 入門] 實現數獨遊戲 - 如何優雅的組織Rust程式碼

Yiniau發表於2018-04-25

title: [WebAssembly 入門] 實現數獨遊戲 - 如何優雅的組織Rust程式碼

date: 2018-4-23 22:55:00

categories: WebAssembly, 筆記

tags: WebAssembly, JavaScript, Rust, LLVM toolchain

auther: Yiniau


[WebAssembly 入門] 實現數獨遊戲 - 如何優雅的組織Rust程式碼


前言

最近探索了一下WebAssembly在不使用 imports.env.memory 的情況下如何分配記憶體,即在Rust端主導記憶體時如何組織程式碼

主要是WebAssembly學到了一個階段,需要一個專案來從整體結構設計上入門它,我之前學習Rust的時候用Piston遊戲引擎做過一個Sudoku遊戲,把它遷移到WebAssembly + JS上感覺不錯,於是有了這片文章。

專案初始結構

ll .
-rw-r--r--    1 yiniau  staff   357B  4 17 18:09 index.html
-rw-r--r--    1 yiniau  staff    81B  4 17 15:37 index.js
drwxr-xr-x  811 yiniau  staff    25K  4 17 16:50 node_modules
-rw-r--r--    1 yiniau  staff   844B  4 21 19:25 package.json
drwxr-xr-x    4 yiniau  staff   128B  4 23 22:23 src
drwxr-xr-x    9 yiniau  staff   288B  4 21 02:26 sudoku
-rw-r--r--    1 yiniau  staff   1.3K  4 17 16:50 webpack.config.js
-rw-r--r--    1 yiniau  staff   224K  4 23 19:39 yarn-error.log
-rw-r--r--    1 yiniau  staff   211K  4 17 16:13 yarn.lock
複製程式碼

rust模組放在sudoku資料夾下

ll sudoku
total 16
-rw-r--r--  1 yiniau  staff   3.3K  4 21 19:32 Cargo.lock
-rw-r--r--  1 yiniau  staff   168B  4 21 19:32 Cargo.toml
drwxr-xr-x  4 yiniau  staff   128B  4 23 22:21 src
drwxr-xr-x  6 yiniau  staff   192B  4 19 18:13 target
複製程式碼

基礎檔案內容

專案已上傳github

project address

數獨實現

結構設計

為互動,需要一個檢測當前矩陣是否合法的方法,可以是方法,也能獨立的函式,權衡之後方法的形式更好,這裡有考慮到Rust端的程式碼組織

對於矩陣的生成,我的思路是先生成一個9*9的數獨終盤矩陣,再通過一個挖洞函式生成題目

目前暫時不考慮唯一解,沒啥大意思

那麼大體上的設計就如下圖:

sudoku game struct

其中backend的結構主要就分兩層,Rust -> LLVM -> wasm 全域性可變(mutable)靜態結構體 SUDOKU 例項化 SudokuMatrix,初始化data屬性,這裡不能用Vec(靜態宣告的限制),通過ffi語法,extern fn 可以匯出函式到JS,需要注意的是要用 #[no_mangle] 避免編譯器打破函式命名。

LLVM會負責編譯程式碼,但是檔案體積巨大,可以通過 wasm-gc 將檔案大小從600多K減少到200多K

Rust主導記憶體的控制,這需要JS端在合適的時機從Memory物件中擷取資料,不過因為使用了靜態變數,我們可以直接使用方法語法,資料頭指標也不會變動,資料的修改會變得簡便,不過我看過其他人的實現,有人加了Mutux互斥,我倒是覺得JS本身就是單執行緒,並不是很需要,如果要互斥鎖,就不能直接使用靜態變數,而是需要使用 lazy-static crate ,用lazy-static 巨集包裹一個靜態指標,e.g.

lazy-static! {
  static ref SUDOKU = Mutux::new(SudokuMatrix {
    data: [u8; 81]
  });
}
複製程式碼

但是這樣就不能使用可變的方法了~ 權衡之下,我還是選擇了 static mut 宣告,主要是就這個專案而言,使用靜態變數帶來的便利遠超過擾亂靜態域的副作用來的大。

其實我並沒有實際體驗過濫用靜態變數的後果,有踩過坑的小夥伴評論一下唄

因為可以使用可變的方法,匯出函式和結構體函式可以分的很明白,介面的控制和補充,以及測試程式碼的組織都會十分明確,也不用手動alloc/dealloc記憶體。

邏輯實現

check函式

check函式檢查矩陣是否合法,由幾個迴圈組成

先迴圈判斷行是否合法

for y in 0..9 { // check row
    let mut checked_value: Vec<u8> = vec![];
    // 直接使用 序列操作符[] 獲得的是實際的物件而不是一個reference | pointer | copy
    let row = &m[y];
    row.into_iter()
        .for_each(|&x| {
            checked_value.push(x);
        });
    checked_value.sort();
    if !&SudokuMatrix::arr_repeat_check(&checked_value) {
        return false;
    }
}
複製程式碼

再判斷列和3*3塊

for x in 0..9 {
    { // check if there is any duplication of numbers in a column
        let mut checked_value: Vec<u8> = vec![];
        for y in 0..9 { // check column
            checked_value.push(m[y][x]);
        }
        checked_value.sort();
        if !&SudokuMatrix::arr_repeat_check(&checked_value) {
            return false;
        }
    }
    { // check 3 x 3 matrix
        // x use to point which matrix
        let mut mm_pos: Vec<(usize, usize)> = vec![];
        let y_range = match x / 3 {
            0 => 0..3,
            1 => 3..6,
            2 => 6..9,
            _ => panic!("index err"),
        };
        let x_range = match x % 3 {
            0 => 0..3,
            1 => 3..6,
            2 => 6..9,
            _ => panic!("index err"),
        };
        for y in y_range {
            for x_inm in x_range.clone() {
                mm_pos.push((y, x_inm));
            }
        }
        let mut checked_value: Vec<u8> = vec![];
        mm_pos.into_iter().for_each(|(y, x)| {
            checked_value.push(m[y][x]);
        });
        checked_value.sort();
        if !&SudokuMatrix::arr_repeat_check(&mut checked_value) {
            return false;
        }
    }
}
複製程式碼

就是把9個數字放到一個陣列裡測重就行了,但是要注意 0 也是一個合法的數字,即玩家沒有填數字的情況

終盤生產函式

使用隨機化和回朔

pub fn init(&mut self) -> &mut Self {
    let mut matrix: Vec<Vec<u8>> = vec![
        vec![0,0,0,0,0,0,0,0,0],
        vec![0,0,0,0,0,0,0,0,0],
        vec![0,0,0,0,0,0,0,0,0],
        vec![0,0,0,0,0,0,0,0,0],
        vec![0,0,0,0,0,0,0,0,0],
        vec![0,0,0,0,0,0,0,0,0],
        vec![0,0,0,0,0,0,0,0,0],
        vec![0,0,0,0,0,0,0,0,0],
        vec![0,0,0,0,0,0,0,0,0]
    ];

    let mut y = 0;
    let mut x = 0;
    let mut c_g = 0; // 回退次數計數
    loop {
        if y >= 9 {
            break;
        }
        if c_g >= 10 { // 回退次數過多時釋放整個 row
            {
                let m = &mut matrix[y];
                m.into_iter()
                    .for_each(|x| {
                        *x = 0;
                    });
                y -= 1;
                c_g = 0;
            }
            {
                let m = &mut matrix[y];
                m.into_iter()
                    .for_each(|x| {
                        *x = 0;
                    });
                x = 0;
            }
        }

        matrix[y][x] = random_num(1, 10) as u8;

        let mut c = 0; // 計數器

        while !Self::matrix_check(&matrix) {
            c += 1;
            matrix[y][x] = random_num(1, 10) as u8;
            if c >= 20 {
                c_g += 1;
                matrix[y][x] = 0;
                if x == 0 && y > 0 { // matrix 換行
                    y -= 1;
                    x = 8;
                } else if x > 0 {
                    x -= 1;
                }
                c = 0;
            }
        }
        x += 1;
        if x >= 9 { // matrix 換行
            y += 1;
            x = 0;
        }
    }

    for y in 0..9 {
        for x in 0..9 {
            self.data[x + (9 * y)] = matrix[y][x];
        }
    }

    // return self to enable link like invoke
    self
}
複製程式碼

這裡有個要注意的地方 ———— 隨機數的生成,rand crate目前不支援 wasm32-unknown-unknown 的編譯,只能在JS端把Math.random包裝一下傳過來用著先,不然就得自己寫。。挺麻煩的,而且rand在github上已經又一個bench用於支援 wasm32-unknown-unknown

測試

可以單元測試一起走

我這邊就直接放UA上的結果了

run result

相關文章