思索的逍遥の記。

いろいろな考え事。

Rustでライフゲームを作ってみた

モチベーション

ニコニコ動画で面白いプログラミング動画を見つけましたが、その動画で C で実装されていたライフゲームを Rust で書き直すと練習になりそうだったので、ちょっと書いてみました。ちょうど ncurses の rust 向け wrapper があったので良かったです。

免責事項: 筆者は趣味でプログラミングをしている者なので、間違いが含まれている可能性があります。ご注意ください。

元ネタの動画様

業界のネタも散りばめられていて楽しめます。自分は業界人ではありませんが、プログラミングを勉強しているとついつい目にしてしまう情報の数々……。

ソースコード

所詮初心者が書いたコードなので、Rust っぽくなかったり、至らないところがあると思います。ご容赦ください。

Cargo.toml

[package]
name = "life_game-rs"
version = "0.1.0"
authors = ["SuitCase <foo@example.bar>"]

[dependencies]
ncurses = "5.0"

main.rs

2018/09/07 更新:条件分岐をいくつか match に置き換えました

extern crate ncurses;

use ncurses::*;
use std::char::from_u32;

const W: usize = 80;
const H: usize = 20;

fn show(buf: [[i32; W]; H]) {
    for y in 0..H {
        mv(y as i32, 0);
        for x in 0..W {
            match buf[y][x] {
                0 => { addch(' ' as u32); },
                _ => { addch('*' as u32); },
            }
        }
    }
}

fn wrap(x: i32, bound: usize) -> i32 {
    let mut ret_x = x;
    while ret_x < 0 {
        ret_x += bound as i32;
    }
    while ret_x >= bound as i32 {
        ret_x -= bound as i32;
    }
    ret_x
}

fn step(buf: &mut [[[i32; W]; H]; 2], cur_buf: usize) {
    for y in 0..H {
        for x in 0..W {
            let mut neighbors = 0;
            for dy in -1..2 {
                for dx in -1..2 {
                    if dy == 0 && dx == 0 {
                        continue;
                    }
                    let ny: usize = wrap(y as i32 + dy, H) as usize;
                    let nx: usize = wrap(x as i32 + dx, W) as usize;
                    if buf[cur_buf][ny][nx] != 0 {
                        neighbors += 1;
                    }
                }
            }
            match buf[cur_buf][y][x] {
                0 => { buf[cur_buf^1][y][x] = (neighbors == 3) as i32; },
                _ => { buf[cur_buf^1][y][x] = (neighbors == 2 || neighbors == 3) as i32; },
            }
        }
    }
}

fn main() {
    let mut buf = [[[0; W]; H]; 2];
    let mut cur_buf = 0;
    initscr();
    noecho();

    let mut cursor_y: usize = 0;
    let mut cursor_x: usize = 0;

    loop {
        show(buf[cur_buf]);
        refresh();
        mv(cursor_y as i32, cursor_x as i32);

        let ch: char = from_u32(getch() as u32).expect("Invalid char");

        match ch {
            'q' => { break; }
            'h' => { cursor_x -= 1; }
            'l' => { cursor_x += 1; }
            'j' => { cursor_y += 1; }
            'k' => { cursor_y -= 1; }
            's' => { buf[cur_buf][cursor_y][cursor_x] = 1; }
            'c' => { buf[cur_buf][cursor_y][cursor_x] = 0; }
            'n' => { step(&mut buf, cur_buf); cur_buf ^= 1; }
             _  => { continue; }
        }
        cursor_x = wrap(cursor_x as i32, W) as usize;
        cursor_y = wrap(cursor_y as i32, H) as usize;
    }
    endwin();
}

感想

前に C++ を勉強して挫折しました。Python でプログラミングに慣れた後に、またコンパイラ型言語を勉強しようと思って、一方で「また C++ はモチベが上がらない。何か新しい言語で代替物は……」という気持ちもあって、登場時にちょっと気になってた Rust を勉強することにしました。

Rust も書いてみると気をつけることは多くて、結構躓きます。このプログラムでは以下の点で立ち止まってちょっと考えました。

  • C言語ではif文の条件に int 型を使って良いが、Rustでは bool 型にしなければならない。
  • 配列のインデックスは usize 型にしようね、とコンパイラに言われたので、扱う数値型が i32 になったり usize になったりした。
  • ncurses の getch 関数の戻り値が u32 だとコンパイラに教えられた( ncurses-rs のドキュメントがない……)。
  • 今回のプログラム中の step 関数は、元ネタ様では引数に inbuf, outbuf( buf 配列内の2つの配列)を指定していたが、Rust でそれをやると、inbuf と outbuf を表現するのに cur_buf(元ネタ様では current_buf )を2回使うことになり、Rust コンパイラ「2回借用してるんだけど」と怒られたので、buf を丸ごとと、cur_buf を関数に直に渡して、関数内で2つの buf を表現することで回避した。C で悩まなくてもいいところで悩むポイントの1つだなと実感した。

しかし、今回 Rust プログラムを書いてみて、ちゃんと動いて楽しかったのも事実です( match がとても気持ちいい)。もっと練習を積み重ねて、作りたいツールを作れるようになったらなあ……と感じた次第です。