Игра "Жизнь" на Rust

Игра "Жизнь" - клеточный автомат, придуманный английским математиком Джоном Конвеем. Несложная, но забавная штука. Его реализацию писал, пожалуй, каждый программист. И в этом посте мы напишем игру "Жизнь" на языке Rust.

Правила игры довольно просты. Здесь приводить я их не буду. Кто их не знает, можете прочитать в википедии (они не сложные).

Полный код получившейся программы можно найти на github.

Создание игрового мира

{{post_3_Создаем}} новый проект с помощью Cargo. Для большей гибкости сразу вынесем настройки нашего мира в константы - это количество живых соседей, необходимое для зарождения жизни и для смерти клетки. Оформим их в виде массивов, чтобы не ограничивать наши будущие эксперименты с игровым миром.

const NEIGHBORS_FOR_ALIVE: [usize; 1] = [3];
const NEIGHBORS_FOR_DIE: [usize; 7] = [0, 1, 4, 5, 6, 7, 8];

Так как игровой мир - это поле клеток, которые имеют всего два состояния, представим его в виде двумерного вектора, содержащего булевые значения (жива клетка или нет). Так же для удобства отдельно укажем ширину и высоту мира.

struct World {
    points: Vec<Vec<bool>>,
    width: usize,
    height: usize,
}

Добавим конструктор, принимающий размеры создаваемого мира и список клеток, которые будут живыми в первом поколении. Для удобства создадим метод set_alive(), который будет устанавливать состояние для указанной клетки.

impl World {
    // Constructor.
    fn new(
        width: usize,
        height: usize,
        alive_points: Vec<(usize, usize)>,
    ) -> World {
        let mut world = World {
            points: vec![vec![false; height]; width],
            width: width,
            height: height,
        };

        for (x, y) in alive_points {
            world.set_alive(x, y, true);
        }

        world
    }

    /// Sets state for point [x,y].
    fn set_alive(&mut self, x: usize, y: usize, value: bool) {
        self.points[x][y] = value;
    }
}

Добавим несколько вспомогательных методов. is_alive() проверяет наличие жизни в клетке. print() визуализирует текущее состояние мира. Для простоты будем просто выводить в терминал сетку символов размером с игровой мир: * - клетка жива, · - клетка мертва.

impl World {
    /// Returns state of point [x,y].
    fn is_alive(&self, x: usize, y: usize) -> bool {
        self.points[x][y]
    }

    /// Prints current state of points.
    fn print(&self) {
        for y in 0..self.height {
            for x in 0..self.width {
                print!("{}", if self.is_alive(x, y) {"*"} else {"·"});
            }
            println!("");
        }
        println!("");
    }
}

Добавление интерактивности

Для кого, чтобы рассчитать состояние клетки в следующем поколении необходимо получить количество живых соседей. Добавим для этого метод count_alive_neighbors(). Стоит отметить, что мы создаем вариант игры с ограниченным размером мира. Из-за этого необходимо добавить дополнительную проверку, чтобы при обсчете крайних клеток не вылезти за пределы вектора.

impl World {
    /// Returns count of alive neighbors of poin [x,y].
    fn count_alive_neighbors(&self, x: usize, y: usize) -> usize {
        let x = x as isize;
        let y = y as isize;
        let width = self.width as isize;
        let height = self.height as isize;
        let mut result = 0;

        for i in (x - 1)..(x + 2) {
            for j in (y - 1)..(y + 2) {
                if (i == x && j == y) || i < 0 || i >= width || j < 0 || j >= height {
                    continue;
                }
                if self.is_alive(i as usize, j as usize) {
                    result += 1;
                }
            }
        }
        result
    }
}

Теперь, зная количество живых соседей, мы можем рассчитать новое состояние клетки. Добавим метод tic(), который будет запускаться в цикле и сменять состояние клеток на следующее поколение. По условиям игры новое поколение должно рассчитываться только на основании текущего. Из-за этого мы не можем сразу же менять текущие значения клеток. Поэтому мы будем создавать новое поле, заполнять его данными, а потом заменять им текущее.

impl World {
    /// Update state of point.
    fn tic(&mut self) {
        let mut new_points = vec![vec![false; self.height]; self.width];
        for x in 0..self.width {
            for y in 0..self.height {
            let count_alive_neighbors = self.count_alive_neighbors(x, y);
                if self.is_alive(x, y) {
                    new_points[x][y] = !NEIGHBORS_FOR_DIE.contains(&count_alive_neighbors)
                } else{
                    new_points[x][y] = NEIGHBORS_FOR_ALIVE.contains(&count_alive_neighbors)
                }
            }
        }
        self.points = new_points;
    }
}

Игра должна продолжаться до тех пор, пока в мире существует хотя бы одна живая клетка. Добавим вспомогательный метод is_empty(), который будет проверять, есть ли на поле живые клетки.

impl World {
    /// Returns true is world contains any alive points.
    fn is_empty(&self) -> bool {
        for x in 0..self.width {
            for y in 0..self.height {
                if self.points[x][y] {
                    return false;
                }
            }
        }
        true
    }
}

Осталось добавить метод, содержащий главный игровой цикл. Он будет выводить на экран текущее состояние мира и затем сменять поколение до тех пор, пока будут существовать живые клетки. Так же для удобства наблюдения добавим задержку между итерациями.

use std::thread;

impl World {
    fn run(&mut self, pause: u32) {
        while !self.is_empty() {
            self.print();
            thread::sleep_ms(pause);
            self.tic();
        }
    }
}

Заключение

Наша программа готова. Для примера создадим и запустим мир, в котором будет существовать фигура планер.

fn main() {
    let points: Vec<(usize, usize)> = vec![
        (1,3),
        (2,1),
        (2,3),
        (3,2),
        (3,3),
    ];

    let mut world = World::new(25, 10, points);
    world.run(300);
}

В результате мы увидим это:

В полных исходниках, которые лежат на github, создается мир, который можно увидеть на изображении в начале статьи.

Ключевые слова: Rust

Коментарии

Используйте Markdown

Thank you for comment!
Ваше сообщение будет доступно после проверки.