Featured image of post Game of Life 만들기

Game of Life 만들기

rust/wasm을 써서 game of life만들기

규칙

Game of Life은 간단한 2가지의 규칙으로 돌아간다.

  1. 죽은 칸에 접한 8칸 중 정확히 3칸에 세포가 살아 있다면 해당 칸의 세포는 그 다음 세대에 살아난다.
  2. 살아있는 칸과 접한 8칸 중 2칸미만 또는 3칸초과의 세포가 살아 있다면 해당 칸의 세포는 죽는다.

  • 1번 사진은 중항을 기준으로 근처에 3칸의 살아있는 세포가 있으니 살아난다.
  • 2번 사진은 중항을 기준으로 근처에 2칸 미만 또는 3칸 초과의 살아있는칸이 있으니 세포가 죽는다.

알고리즘 생각하기

  1. 죽어있는 세포와 살아있는 세포의 위치를 모두 배열에 넣는다.
  2. 살아있는 세포의 위치 인덱스만을 모아놓은 배열을 만들어 구현한다.

이런식으로 2가지방법이 있다.

  • 1번 방법의 경우 구현하기는 2번보다 쉽지만 맵을 크게 만들기에는좀 무리가 있다.
  • 2번 방법의 경우에는 구현하기가 좀 어렵지만 맵을 크게구현해도 세포의 살아있는칸의 위치만 기억하니 용량도 적고 최적화도 잘된다.(그리고 맵을 무한하게도 만들수 있다)

프로젝트 초기화하기

이제 프로젝트를 초기화해보자

명령어

1
2
3
cargo install wasm-pack
wasm-pack new game-of-life
cd life-of-game

src > lib.rs

1
2
mod utils;
use wasm_bindgen::prelude::*;

Cargo.toml

Cargo.toml은 이렇게바꿔준다

프로그래밍 시작

이제 Game of Life을 만들어보자

enum, struct만들기

enum을 통해서 세포의 상태를 만드는 이유는 세포의 상태를 직관적으로 상태를 볼 수 있기 때문에 사용한다

1
2
3
4
5
6
#[wasm_bindgen] // wasm으로 빌드하기위해 wasm_bindgen매크로를 써준다.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] // 나중에 비교, 복사, 출력 등을 할수있으니 추가해준다
pub enum Cell {
    Dead,
    Alive,
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#[wasm_bindgen]
pub struct LifeOfGame {
    alives: Vec<(isize, isize)>, // 살아있는 세포의 위치
    camera: (isize, isize), // 카메라의 위치(렌더링할떄 일정 부분만 보이게 하기위해 넣는다)
}

#[wasm_bindgen]
impl LifeOfGame {
    // 그냥 평범한 초기화 코드
    pub fn new() -> Self {
        Self {
            alives: vec![],
            camera: (0, 0),
        }
    } 
}

세포 삽입/삭제 구현

alives배열을 쉽게 관리하기위해 set메서드를 만든다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[wasm_bindgen]
impl LifeOfGame {
    // ..생략
    fn set(&mut self, i: isize, j: isize, state: Cell) {
        if state == Cell::Dead {
            // (i, j)에 살아있는 세포가 없으면 이미 죽어있는거니 굳이 안죽인다
            if self.alives.contains(&(i, j)) {
                self.alives.remove(
                    // (i, j)에 위치한 살아있는 세포의 인덱스를 가저옴
                    self.alives
                        .iter()
                        .enumerate()
                        .find(|x| x.1 == &(i, j))
                        .unwrap()
                        .0,
                );
            }
        } else {
            // (i, j)에 살아있는 세포가 있음면 이미 살아있는거니 추가하지 않는다
            if !self.alives.contains(&(i, j)) {
                self.alives.push((i, j));
            }
        }
    }
}

세포 값 변경하기

아까만든 set메서드를 활용해서 값을 반전시키는 메서드를 만든다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#[wasm_bindgen]
impl LifeOfGame {
    // ..생략
    pub fn toggle(&mut self, i: isize, j: isize) {
        if self.alives.contains(&(i, j)) {
            self.set(i, j, Cell::Dead)
        } else {
            self.set(i, j, Cell::Alive)
        }
    }
}

다음값 확인하기

#규칙에 있는걸 확인하여 1번 실행하는 step메서드를 만들어준다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#[wasm_bindgen]
impl LifeOfGame {
    // ..생략
    pub fn step(&mut self) {
        // 한번에 바꾸지 않고 조금씩 바꾸면 문제가 생길수있으니 백터를 만들어 해결행다
        let mut changes = vec![];

        // 규칙에서 살아있는 세포의 위치만 확인하니 반복문에 넣어서 돌린다.
        for alive in &self.alives {
            let mut count = 0;
            // 살아있는 세포에 이웃한 인덱스를 받아오기위해 -1부터 1까지 반복문에 넣는다
            for i in -1..=1 {
                for j in -1..=1 {
                    // 자신을 제외하고 카운트한다
                    if i == 0 && j == 0 {
                        continue;
                    }
                    let x = alive.0 as i32 + i;
                    let y = alive.1 as i32 + j;

                    {
                        let mut count = 0; // 변수를 가릴수 있는걸 이용하여 변수를 만들어준다

                        // 위 반복문가 같은이유로 이런식으로 돌린다
                        for k in -1..=1 {
                            for l in -1..=1 {
                                let xx = x + k;
                                let yy = y + l;

                                count += self.alives.contains(&(xx as isize, yy as isize)) as i32;
                            }
                        }
                        // 살아있는세포가 3개면 새로운 세포가 태어난다
                        if count == 3 {
                            changes.push((x as isize, y as isize, Cell::Alive));
                        }
                    }
                    count += self.alives.contains(&(x as isize, y as isize)) as i32;
                }
            }
            // 살아있는세포가 2보다 작거나 3보다 크면 죽는다
            if 2 > count || count > 3 {
                changes.push((alive.0, alive.1, Cell::Dead));
            }
        }

        // 위에서 변경한다고 해준값을 반복문을통해 변경해준다
        for data in &changes {
            self.set(data.0, data.1, data.2)
        }
    }
}

그리기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

#[wasm_bindgen]
impl LifeOfGame {
    // 자신이 보고있는부분을움직아는 메서드
    pub fn move_camera(&mut self, x: isize, y: isize) {
        self.camera.0 += x;
        self.camera.1 += y;
    }

    pub fn draw(&self, width: isize, height: isize) {
        let doc = web_sys::window().unwrap().document().unwrap(); // js의 document와 같은 객채를 가저옴
        let game = doc.get_element_by_id("game").unwrap(); // game이라는 id를 가진값을 가저옴

        let mut html = String::new();
        // id가 {i},{j}인 타일들을 그림
        for i in self.camera.1..(height + self.camera.1) {
            html.push_str("<div>");
            for j in self.camera.0..(width + self.camera.0) {
                html.push_str(&format!(
                    "<div type='button' id='{i},{j}' class='tile' {}></div>",
                    if self.alives.contains(&(i as isize, j as isize)) {
                        "alive" // 살아있을경우 alive attr을 추가해서 놔둠 
                    } else {
                        ""
                    }
                ));
            }
            html.push_str("</div>");
        }
        game.set_inner_html(&html);
    }
}

빌드하기

이제 wasm의 코드를 다 만들었으니 이 코드를 빌드해보자
아레 코드를쓰면 웹에서 쓸수있는 wasm이 pkgs폴더에 빌드될거다.

1
wasm-pack build --target web

html 코드 작성

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta notsus="i use nixos btw">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- <input type="button" value="START" id="puse"><br/> -->
    <main id="game">a</main>
</body>
</html>
<script type="module">
import init, { LifeOfGame } from './pkg/wasm_pack_pra.js'
await init() // 초기화
alert("move: arrow key\npuse: space");
let game = LifeOfGame.new(); // 아까만든 게임객채 가저오기

draw();
let PUSE = 1;
let SPEED = 100;

function toggle(i, j) {
    game.toggle(i, j);
}
function draw() {
    game.draw(window.innerWidth / 20, window.innerHeight / 20);
    document.querySelectorAll("div.tile").forEach((button) => {
        if (button.id.indexOf(",") != -1) {
            // 클릭했을때 세포 반전시키기
            button.addEventListener('click', () => {
                let [i, j] = button.id.split(",").map(Number);
                toggle(i, j);
                draw();
            });
        }
    })
}
// 카메라 움직이기
window.onkeydown = (e) => {
    if (e.key == "ArrowRight") {
        game.move_camera(1, 0);
        draw();
    } if (e.key == "ArrowLeft") {
        game.move_camera(-1, 0);
        draw();
    } if (e.key == "ArrowDown") {
        game.move_camera(0, 1);
        draw();
    }
    if (e.key == "ArrowUp") {
        game.move_camera(0, -1);
        draw();
    }
    if (e.key == " ") {
        PUSE = !PUSE;
    }
    console.log(e.key)
}

setInterval(() => {
    // 멈추지 않았을때 한번씩 실행시키기
    if (!PUSE) {
        game.step();
        draw();
    }
}, 100);
</script>

<style>
    * {
        background-color: #282828;
        margin: 0;
        padding: 0;
    }
    body {
        overflow: hidden;
    }
    main {
        display: flex;
        flex-direction: column;
    }
    div {
        display: flex;
    }
    div.tile {
        border: solid #ffffff28 0.5px;
        width: 20px !important;
        height: 20px;
        /* background-color: red; */
        width: fit-content;
        &[alive] {
            background-color: #fff;
        }
    }
</style>

결과

완성하면 다음과 같은 결과가 나온다
코드
결과
life.5-23.dev

Licensed under CC BY-NC-SA 4.0
마지막 수정: Apr 17, 2024 00:00 UTC
Hugo로 만듦
JimmyStack 테마 사용 중