iT邦幫忙

2024 iThome 鐵人賽

DAY 20
1

別的先不說,但一定要記住當天下的棋。只有做到這一點,才能反省並改正。

-- <突圍思考>,曹薰鉉著,盧鴻金譯。

昨天展示了與 ChatGPT 互動的內容,今天則展示如何調整成真正可用的樣子。

Rust 部份

新增命令列參數,使它可以指定欲載入的 sgf 檔案,

struct Args {
    /// SGF files to be loaded from
    #[arg(short, long)]
    load: String,
}

在主函數的開頭,使用 PathogenEngine 專案的函式庫,載入 sgf 檔,

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Load the record
    let args = Args::parse();
    let mut contents = String::new();
    print!("Loading ... ");
    let mut file = File::open(args.load)?;
    file.read_to_string(&mut contents)
        .expect("Failed to read file"); // 將 sgf 的內容讀到 contents 字串當中
    let mut iter = contents.trim().chars().peekable(); // 將 contents 字串轉換成迭代子,可以逐項掃過
    let t = TreeNode::new(&mut iter, None); // 初始化這個 sgf 代表的遊戲樹
    let g = Game::init(Some(t)); // 以這顆遊戲樹初始化這個棋局

    .... // 所有的 SGF 讀取過程,後述

昨日 ChatGPT 的模板中只有給靜態內容的 game_state函數,但實務上我們需要將載入的 sgf 檔案傳給該函數,再讓該函數呈現的資料能夠被前端取用。一樣詢問過 ChatGPT 意見,使用 Arc 方法以共享 GameState 物件,為此新增一些內容,

async fn game_state(data: web::Data<AppState>) -> impl Responder {
    let gs = data.amgs.lock().unwrap();
    web::Json(gs.clone())
}

struct AppState {
    amgs: Arc<Mutex<GameState>>,
}

完整的 GameState,有別於揭露給 ChatGPT 的資訊僅包含人界與冥界設置的部份,這裡設計了 Step 物件作為展示棋譜用。

#[derive(Serialize, Clone)]
struct GameState {
    white_positions: Vec<String>,
    black_positions: Vec<String>,
    steps: Vec<Step>,
}
#[derive(Serialize, Clone)]
struct Step {
    id: u32, // 流水號,這是第幾步
    pos: String, // 座標,直接以 sgf 內容呈現
    is_marker: bool, // 是否為標記。因為如果不是標記的話,就是座標,但有可能是羅盤或棋盤座標...
    char1: char, // 主要用來標記陣營或是角色在棋盤上的位置
    marker: i32, // 使用有號整數,讓醫療方為 1,疫病方為 -1
}

在主函數的最後,


    .... // 所有的 SGF 讀取過程,後述
    println!("... Done");

    let setup_state = web::Data::new(AppState {
        amgs: Arc::new(Mutex::new(gs)),
    }); // 將 sgf 載入到 gs 之後,封裝在這個可共享的 `Arc` 物件裡面,再打包成 `web::Data`

    // Start the service
    HttpServer::new(move || {
        App::new()
            .app_data(setup_state.clone()) // 這即是傳遞到 game_state 函數的方法
            .route("/", web::get().to(index))
            .route("/game_state", web::get().to(game_state)) // index 與 game_state 都是有 async 屬性的非同步函數
                                                             // route 方法可以將 URL 對應到這些函數去
            .service(fs::Files::new("/static", "static").show_files_listing())
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

SGF 讀取過程

接近 150 行,其實很平鋪直述的過程。舉一些設置階段的例子:

    let mut id = 0;
    let setup1_0 = setup0_u.borrow().children[0].clone();
    let setup1_1 = setup1_0.borrow().children[0].clone();
    let setup1_2 = setup1_1.borrow().children[0].clone();
    let setup1_3 = setup1_2.borrow().children[0].clone();
    gs.steps.push(Step {
        id: id,
        pos: setup1_0.borrow().properties[1].value[0].clone(),
        is_marker: true,
        char1: 'P',
        marker: -1,
    });
    id = id + 1;
    gs.steps.push(Step {
        id: id,
        pos: setup1_1.borrow().properties[1].value[0].clone(),
        is_marker: true,
        char1: 'P',
        marker: -1,
    });
...

setup1 階段,疫病方會放置四枚標記,這裡是其中兩枚。stepsGameState 當中的一個向量物件 Step,所以我們可以一直產生新的 Step 並加入(push)其中。properties 的索引都是 1,這風格的確不甚好,但在設置階段,properties[0] 都是 C,如隨意範例

...
C[Setup0]AW[dd][ee][fe][db][ea][cc][af][ce][ff][cf][cb][ba][ef][fc][ab][dc][bd][fa]
C[Setup0]AB[ed][ae][be][fd][bc][ca][aa][bb][cd][de][ac][ad][bf][ec][fb][eb][df][da]
C[Setup1]AB[cd]  <===
C[Setup1]AB[ee]  <=== 前兩個標記,相當於對應到這兩步。
C[Setup1]AB[aa]
C[Setup1]AB[fc]
C[Setup2]AW[df]
C[Setup2]AB[be]
C[Setup2]AW[ba]
C[Setup2]AB[cb]
C[Setup3]AW[jh]
B[ik][cb][cc][ce][cf][af][cb][ce][cc][cf]
...

sgf 核心的載入部份以一個迴圈包裝起來,仔細想想,這也許是少數真的由我自己完成的程式碼片段,

    let mut curr_node = setup3_0.borrow().children[0].clone(); // 剛完成 setup3 的內容,也就是醫療方的羅盤標記設定了
    loop {
        // opening
        // 標準回合階段,當前節點的屬性只有 "W" 或 "B",
        // 以 'D' 和 'P' 標記陣營。
        let side = if curr_node.borrow().properties[0].ident == "W" {
            'D'
        } else {
            'P'
        };

        // core
        // 想到就很頭痛,因為在一個標準回合中的座標序列,可能代表路徑,也可能代表標記階段的著點,
        // 還要從這裡逆向解析嗎?
        // 但幸好還是有遊戲邏輯可以遵循,總之先都當作是行進路徑,之後一旦有重複座標出現,就可以知道是標記的置放。
        // 當初 spec 那樣定實在是太好了...
        let mut route = vec![];
        for s in curr_node.borrow().properties[0].value.iter() {
            let is_marker = route.contains(s);
            gs.steps.push(Step {
                id: id,
                pos: s.clone(),
                is_marker: is_marker,
                char1: side,
                marker: if !is_marker {
                    0
                } else if side == 'D' {
                    1
                } else {
                    -1
                },
            });

            id = id + 1;
            if !is_marker {
                route.push(s.clone());
            }
        }

        // closing
        if curr_node.borrow().children.len() == 0 {
            break; // 遊戲樹就長到這裡為止
        } else {
            let temp = curr_node.borrow().children[0].clone(); // 繼續迴圈
            curr_node = temp;
        }
    }

這樣就算大功告成。但讀譜的時候有時需要往復檢索,這個部份相當於是 undo,但是是在棋譜呈現的面向上,而非遊戲狀態的保存上,所以做在 javascript 裡面。目前也還是有一些 bug,比方說倒帶角色經過殖民地物件或是敵方角色之後沒有辦法保留紀錄之類,也許是因為 Step 物件的描述力畢竟不夠?但由於也還堪用,也就沒有打算更新那個部份。

目前狀況

連續兩天都在蒐集自我對局內容,剛準備要訓練下一批,所以實在沒什麼好分享的進度,就以錄製這個打譜程式的動畫來結束今天的文章吧。

無法正常顯示... 怪了,格式和前年的 hyperskewb 一樣啊?

http://www.m101.nthu.edu.tw/~s101062801/ironman2024/20-1.gif


上一篇
導入網頁互動元素 1/2
下一篇
回顧訓練歷程 1/3
系列文
DeltaPathogen:國產雙人不對稱抽象棋「疫途」之桌遊 AI 實戰30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言