程式設計師喜歡玩的life game是個什麼遊戲?

2021-12-22 樹屋編程

life game的全名叫Conway's Game of Life,是一個英國數學家70年代在一個雜誌上發表的遊戲。著名的C++大佬、C++ Weekly視頻專欄的作者Jason Turner有一次在他的「表弟教我學rust」的直播中用這個遊戲完成了rust的入門。

所以這個遊戲其實非常適合新手學習一門語言、或者嘗試熟悉某個gui庫以及遊戲庫。很多初學編程的人都把這個當做一個練習。

可以認為,life game就是有一個稍微複雜一點的hello world。

那麼,life game到底是什麼?根據wiki上的描述:

生命遊戲是一個零玩家遊戲(就是不需要玩家)。它包括一個二維矩形世界,這個世界中的每個方格居住著一個活著的或死了的細胞。一個細胞在下一個時刻生死取決於相鄰八個方格中活著的或死了的細胞的數量。如果相鄰方格活著的細胞數量過多,這個細胞會因為資源匱乏而在下一個時刻死去;相反,如果周圍活細胞過少,這個細胞會因太孤單而死去。

具體的規則如下:

每個細胞有兩種狀態 - 存活或死亡,每個細胞與以自身為中心的周圍八個細胞產生互動(黑色為存活,白色為死亡)當前細胞為存活狀態時,如果周圍的存活細胞低於2個(不包含2個),該細胞變成死亡狀態。(模擬生命數量稀少)當前細胞為存活狀態時,如果周圍有2個或3個存活細胞,該細胞保持原樣。當前細胞為存活狀態時,如果周圍有超過3個存活細胞,該細胞變成死亡狀態。(模擬生命數量過多)當前細胞為死亡狀態時,如果周圍有3個存活細胞,該細胞變成存活狀態。(模擬繁殖)

這個規則非常之簡單,它的有趣之處在於從看似無序的規則中包含一些穩定的狀態,充分體現了數學之美。廢話少說,馬上動手來實現一個看看。

最近在重新折騰rust,其實幾年前用它做過一些leetcode的題目,但是到現在已經忘了,所以相當於重新學,所以剛好可以來做這個life game的練習。但是我還是想用C++來先寫一遍,然後再「翻譯」成rust。

用Qt來做一個life game

C++的圖形庫非常之多,選什麼可以看心情。我這次選的是Qt,用Qt來做這種動畫非常簡單,表示每個細胞的矩形可以用QGraphicsRectItem對象,只要定時更新這些矩形的填充顏色即可。

用一個簡單的類來定義細胞所存在的二維矩形世界:

template<int WIDTH, int HEIGHT>
class World {
    World(World const&) = delete;
    World& operator=(World const&) = delete;
public:
    World() { // 構造函數,隨機初始化每個細胞的狀態
        // ...
    }

    // 更新全部細胞的下一個狀態
    void Next() {
        // ...
    }

    // 查詢某個格子的細胞是否活著
    bool Alive(int x, int y) const {
        return cells_[y][x];
    }
private:
    // 計算八個相鄰方向活著的細胞數量
    int LiveNeighbours(int x, int y) const {
        // ...
    }
private:
    // 每個細胞的狀態,只用兩種狀態,用bool類型
    std::array<std::array<bool, WIDTH>, HEIGHT> cells_ = {};
};

數據成員只有一個二維數組,每一幀調用一次Next()函數更新所有細胞的狀態,然後再調用Alive(x, y)查詢每個位置的狀態,用不同的顏色繪製出來即可。每個矩形的位置只要由數組下標就可以得到,其實在這個程序中,每個細胞的位置不需要改變。

上面每個函數的實現我都省略了,因為實現都很簡單,完整源碼我會上傳到github上。

用Qt creator創建一個工程,添加一個名字叫LifeCells的類,從QGraphicsView繼承:

class LifeCells : public QGraphicsView
{
    Q_OBJECT
    // 每個小矩形(其實是正方形)的寬度與高度
    static const int CELL_SIZE = 20;
    // 矩形的行數
    static const int CELL_ROWS = 30;
    // 矩形的列數
    static const int CELL_COLS = 40;
public:
    LifeCells(QWidget *parent = nullptr);
    ~LifeCells() override;
private slots:
    void timeout();
private:
    QGraphicsScene scene_;
    QTimer* timer_; // 定時器,用來調用world的Next函數
    // 表示每個細胞狀態的矩形
    QGraphicsRectItem* world_[CELL_ROWS][CELL_COLS];
    // World對象,保存所有細胞的數據
    World<CELL_COLS, CELL_ROWS> data_;
private:
    Ui::LifeCells *ui;
};

在LifeCells的構造函數中初始化表示World的data_成員以及創建所有的矩形:

LifeCells::LifeCells(QWidget *parent) :
    QGraphicsView(parent),
    ui(new Ui::LifeCells)
{
    ui->setupUi(this);
    this->setScene(&scene_);

    // 按照矩形的數量計算窗口的大小
    this->resize(CELL_SIZE * CELL_COLS, CELL_SIZE * CELL_ROWS);

    // 創建每個矩形對象
    for(int row = 0; row < CELL_ROWS; ++row) {
        for(int col = 0; col < CELL_COLS; ++col) {
            auto p = new QGraphicsRectItem(
                        CELL_SIZE * col + 1,
                        CELL_SIZE * row + 1,
                        CELL_SIZE - 2,
                        CELL_SIZE - 2);
            world_[row][col] = p;
            scene_.addItem(p);
            p->setBrush(data_.Alive(col, row) ? Qt::black : Qt::white);
        }
    }

    // 創建定時器對象,每隔一定時間更新一次數據
    timer_ = new QTimer(this);
    connect(timer_, SIGNAL(timeout()), this, SLOT(timeout()));
    timer_->start(200);
}

更新每個矩形的顏色,就是循環獲取每個細胞的當前狀態,然後設置成不同的填充色:

void LifeCells::timeout()
{
    data_.Next();
    for(int row = 0; row < CELL_ROWS; ++row) {
        for(int col = 0; col < CELL_COLS; ++col) {
            world_[row][col]->setBrush(data_.Alive(col, row) ? Qt::black : Qt::white);
        }
    }
}

至此,整個邏輯就完成了,當然new出來的矩形對象別忘了在析構函數中釋放,析構函數我這裡就省略了。

運行就可以看到漂亮的動畫。

因為C++非常熟悉,所以一下子就寫好了。現在可以開始用rust來寫一遍了。

用rust來做一個life game

rust中沒有成熟的ui庫,我這次選用了piston_window,它是一個簡單的遊戲框架的一部分。安裝rust非常簡單,一個命令然後出現提示的時候再按一個回車就裝好了,包括cargo。後面的其它步驟我儘量寫詳細一些,這樣即使從未用過rust的人也可以操作。

用cargo來創建一個程序,命令:

cargo new --bin rust_life

然後rust_life目錄如下:

rust_life
├── Cargo.toml
└── src
    └── main.rs

後面涉及到命令包括:cargo build或者cargo run都是在rust_life中運行。

編輯Cargo.toml,在[dependencies]下面加上需要依賴的庫,如下:

[dependencies]
piston_window = "*"
rand = "*"

之後運行cargo的時候會自動去下載所依賴的庫到本地,而且每個平臺的操作方法都一樣,這個對新手比C++要友好得多。

因為這個就是一個複雜一點的hello world而已,所以代碼量很少,所有代碼直接實現在一個文件中即可,即src/main.rs。

先看看piston_window怎麼用。因為我只要知道怎麼繪製矩形即可,它的教程上剛好就是用最簡單的代碼繪製了一個矩形,我只要照著這個改一下就行了。來看一下官方的這個例子:

extern crate piston_window;

use piston_window::*;

fn main() {
    // 創建一個640*480大小的窗口
    let mut window: PistonWindow =
        WindowSettings::new("Hello Piston!", [640, 480])
        .exit_on_esc(true).build().unwrap();
    
    // 處理窗口的所有事件
    while let Some(event) = window.next() {
        window.draw_2d(&event, |context, graphics, _device| {
            clear([1.0; 4], graphics); // 用背景色填充窗口
            rectangle([1.0, 0.0, 0.0, 1.0], // 繪製一個紅色矩形
                      [0.0, 0.0, 100.0, 100.0],
                      context.transform,
                      graphics);
        });
    }
}

我需要做的就是繪製N個矩形,每隔一段時間更新一下矩形的顏色信息。把之前的C++代碼改過來即可,所有代碼都寫在main.rs這一個文件中。定義數據結構及常量:

use rand::distributions::{Distribution, Uniform};
use piston_window::*;

// 代表細胞的矩形的長度和寬度
const CELL_SIZE: i32 = 20;
// 二維矩形的行數
const CELL_ROWS: i32 = 30;
// 二維矩形的列數
const CELL_COLS: i32 = 40;

// 活著的細胞的顏色,黑色 
const ALIVE_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
// 死亡細胞的顏色,我用了淺灰色,因為沒有畫邊框,白色的太不明顯
const DEAD_COLOR: [f32; 4] = [0.9, 0.9, 0.9, 1.0];

// 每個0.04秒更新一次所有細胞的狀態
const FRAME_LENGTH: f64 = 0.04;

// 表示二維矩形的World結構
struct World {
    // 二維bool數組
    cells : [[bool; CELL_COLS as usize]; CELL_ROWS as usize],
    // 距離上一次更新狀態的時間,如果超過上面設定的0.04,則觸發一次狀態更新
    time_elapsed: f64,
}

rust的數組大小及下標都需要是usize類型,如果不是就要用as來顯式強轉,看起來有點囉嗦。

給World添加幾個函數:

impl World {
    fn new() -> Self {
        // 類似構造函數的功能
    }

    fn next(&mut self) {
        // 更新每個細胞的狀態
    }

    fn draw_rects(&self,  context: Context, g: &mut G2d) {
        // 循環繪製全部矩形
    }

    fn update(&mut self, delta_time: f64) {
        // 更新time_elapsed,如果超過設定的0.04,則調用update
    }
}

然後在main函數中這樣調用:

fn main() {
    let mut window: PistonWindow =
        WindowSettings::new("Game of life", [(CELL_SIZE * CELL_COLS) as u32, (CELL_SIZE * CELL_ROWS) as u32])
        .exit_on_esc(true).build().unwrap();

    let mut world = World::new(); // 創建world

    while let Some(event) = window.next() {
        window.draw_2d(&event, |context, graphics, _device| {
            clear([1.0; 4], graphics);
            world.draw_rects(context, graphics); // 繪製world中的細胞
        });

        // 距離上一次update的時間,需要用UpdateEvent來獲取
        if let Some(ref args) = event.update_args() {
            world.update(args.dt); // dt就是距離上一次update的時間
        }
    }
}

前面提到,World有四個函數,先看new,用來創建一個新的World:

fn new() -> Self {
    // 隨機數庫的用法,照著文檔搬過來的
    let mut rng = rand::thread_rng();
    let die = Uniform::from(0..2);
    
    // 創建一個二維數組
    let mut cells: [[bool; CELL_COLS as usize]; CELL_ROWS as usize] = 
        [[false; CELL_COLS as usize]; CELL_ROWS as usize];
    
    // 給數組賦值
    for row in 0..CELL_ROWS {
        for col in 0..CELL_COLS {
            let v = die.sample(&mut rng); // 生成隨機數
            cells[row as usize][col as usize] = if v == 0 { false } else { true };
        }
    }

    // 返回一個World
    World {
        cells,
        time_elapsed: 0.0,
    }

update函數很簡單,就是根據一個時間增量判斷是否需要更新所有狀態:

fn update(&mut self, delta_time: f64) {
    self.time_elapsed += delta_time;
    if self.time_elapsed >= FRAME_LENGTH { // 時間間隔足夠了
        self.time_elapsed = 0.0; // 置零
        self.next(); // 更新狀態
    }
}

用於更新狀態的next可以按照之前的C++版本直接翻譯過來:

fn next(&mut self) {
    // rust中的閉包,相當於C++的lambda函數
    // 定義一個局部函數,用來計算每個細胞後者的鄰居數量
    let alive_neighbours = |x, y| {
        // 8個方向遍歷一下
        let deltax: [i32; 8] = [0,  1, 1, 1, 0, -1, -1, -1];
        let deltay: [i32; 8] = [-1, -1, 0, 1, 1,  1,  0, -1];
        let mut ret = 0;
        for i in 0..8usize {
            let x2 = x + deltax[i];
            let y2 = y + deltay[i];
            if x2 >= CELL_COLS || x2 < 0 || y2 >= CELL_ROWS || y2 < 0 {
                continue;
            }
            ret += if self.cells[y2 as usize][x2 as usize] { 1 } else { 0 };
        }
        ret
    };

    // 創建一個新的二維數組保存新的狀態
    let mut tmp: [[bool; CELL_COLS as usize]; CELL_ROWS as usize] = 
        [[false; CELL_COLS as usize]; CELL_ROWS as usize];
    for row in 0..CELL_ROWS {
        for col in 0..CELL_COLS {
            let nb = alive_neighbours(col, row);
            if nb > 3 || nb < 2 {
                tmp[row as usize][col as usize] = false;
            } else if nb == 2 {
                tmp[row as usize][col as usize] = self.cells[row as usize][col as usize];
            } else { // nb = 3
                tmp[row as usize][col as usize] = true;
            }
        }
    }

    // 用新的狀態替換掉老的狀態
    self.cells = tmp;
}

還有一個draw_rects函數,就是繪製所有矩形,在main函數中調用的,很簡單:

fn draw_rects(&self,  context: Context, g: &mut G2d) {
    for row in 0..CELL_ROWS {
        for col in 0..CELL_COLS {
            // 確定顏色
            let color: [f32; 4] = if self.cells[row as usize][col as usize] { ALIVE_COLOR } else { DEAD_COLOR };

            // 用rectange函數來繪製
            rectangle(color,
                [(col * CELL_SIZE) as f64 + 1.0,
                    (row * CELL_SIZE) as f64 + 1.0,
                    CELL_SIZE as f64 - 2.0,
                    CELL_SIZE as f64 - 2.0],
            context.transform,
            g);
        }
    }
}

運行cargo run,經過一番編譯以後,rust版本的life game就跑起來了,效果和之前的C++版本很像。所有代碼加起來也就百行左右(去掉空行和注釋就不足百行了),所以確實是一個hello world級別的東西,適合作為入門之用。

反覆運行,會發現一些有趣的圖形,這就是life game的魅力所在。

本文源碼我依然會放在github/franktea/treehouse與文章同名的目錄中。

註:Jason Turner學習用rust寫生命遊戲的直播視頻標題叫「Jonathan Teaches Jason Rust!」,youtube上有。

相關焦點

  • 生命遊戲 the Game of Life
    他喜歡在吵雜的環境裡工作,在辦公室裡放進行曲會吵到隔壁的愛因斯坦。普林斯頓研究拜佔庭歷史的教授說,馮諾依曼在拜佔庭史方面比自己更精通。馮諾依曼以什麼著名?一個顯示屏可能都放不下(+68More)(來源:維基百科)說到現代計算機科學的起源,除了馮·諾依曼,不得不提的是圖靈(Alan Turing).
  • 遊戲安利 | life is a game
    遊戲名:life is a game一款關於人生的遊戲,或許我們的人生,本身就是關於遊戲的人生。下載方式ios:apple store直接搜索下載安卓:taptap平臺目前可以預約,點擊閱讀全文獲得連結。
  • 程式設計師是這樣玩遊戲的
    附上地址:https://likexia.gitee.io/game/index.html這不是廣告,如果你認為是廣告,麻煩聯繫一下該站站長,拿到廣告費有分成~第一款遊戲:貓國建設者玩慣了畫風精美的遊戲,玩玩這種畫風的遊戲,別有一番感受。既然是從GitHub上漢化下來的遊戲,不出意外的,這個網站的玩家,絕大部分都是程式設計師,尷尬的事情就發生了。如果按照遊戲的設定玩,無疑需要大量的時間,一名程式設計師就想到了一個辦法,寫一串代碼,自動點擊「採集貓薄荷」獲取資源。
  • 經典雙語美文|Life Is a Game 生活是一場遊戲
    Image life as a game in which you are playing some five balls in the air.想像生活就是一場遊戲,在這個遊戲中,你向空中拋出五個球。你點著它們的名字:工作,家庭,健康,朋友和心境,而你正在讓這些球保持在空中。You understand that work is a rubber ball. If you drop it, it will bounce back.
  • 玩了3個程式設計師遊戲,第2個好難
    小編看著周圍忙碌程式設計師們,突發奇想的想知道程式設計師都玩什麼手機遊戲,於是就在蘋果商城搜索「程式設計師遊戲」。這就拉開了小編狂掉頭髮的序幕。第一個是app算法圖解,點進去才發現不是遊戲,是算法動畫圖解。第二個是TRYBIT LOGIC,這個遊戲難住了小編一上午。
  • 我很喜歡玩遊戲,那麼我就適合做遊戲程式設計師嗎?
    相信現在在看文章的你也玩遊戲,雖然愛玩的程度不同,但是至少都是感興趣的,當然你也知道,手遊行業利潤高,遊戲程式設計師自然也吃香,能一邊賺錢一邊玩遊戲,豈不是人生一大幸事呢?其實當年我也是這麼想的。為成為遊戲程式設計師而讀研大學的時候學的專業和計算機不太沾邊,對學的東西不太感興趣,每天的生活就是上課開黑打遊戲,在大學的男生宿舍裡,這樣的情況確實也比較普遍。
  • 程式設計師玩的幾款電腦遊戲,你玩過嗎?
    程式設計師在忙完一天的工作(編碼)以後,適當的放鬆一下自己,玩玩遊戲,鍛鍊身體等等。下面我給大家推薦一些程式設計師玩的遊戲。希望大家可以放鬆心情。
  • 什麼是Meta-Game? 元遊戲循環的四個階段
    在遊戲開發生涯中,我經常會遇到這樣一個問題:「什麼是meta-game(元遊戲)?」
  • 盤點程式設計師最喜歡的15個網站
    程式設計師作為一個經常和網際網路打交道的人群,他們喜歡瀏覽哪些網站呢?
  • 世上最傑出程式設計師,B 語言、Unix 之父為玩遊戲,寫了個作業系統
    )Unix之父——肯•湯普森(Ken Thompson)被稱作「世界上最傑出的程式設計師」,他自學編程,26歲創造Unix,改寫了計算機作業系統的歷史,並在古稀之年成為Go語言的共同開發者之一。有個教授為他申請了碩士,師從著名的資訊理論和博弈論專家埃爾溫•伯利坎普(Elwyn Berlekamp)。伯利坎普問他為什麼學編程時,湯普森說:「因為我從小喜歡邏輯學。」據湯普森回憶,他讀碩期間,大部分都是靠自學。從入學到碩士畢業,湯普森僅僅用了一年的時間。1966年,湯普森加入貝爾實驗室。
  • 為什麼程式設計師命中注定應該玩桌遊?
    簡單地說,不玩桌遊,是程式設計師的損失,玩了無愛,那一定是一個假程式設計師 :-)不信? 聽我細細道來。。。氣質匹配,相性相吸首先,讓我們來看看,一個有理想,有追求的程式設計師應該具備什麼樣的氣質,以及為什麼,它們和桌遊是辣麼的猩猩相吸。。。
  • NO GAME NO LIFE
    從小時候就地取材在地板上玩彈珠,到後來圍觀街頭的遊戲機,再長大之後有了手機和電腦上的遊戲能玩,而隨著時代變化,也許各種vr和主機遊戲也能開始慢慢普及了。自從閒下來以後,我每天都抱著我的switch在海拉魯大陸拯救世界,然後突然想著,要不要,試著寫寫遊戲呢。但我知道,對於沒有玩過某款遊戲的人,想要讓他真正理解這款遊戲的靈魂,是無法做到的。
  • 混沌:《Game of life》生命遊戲
    這個遊戲也叫康威生命遊戲、細胞自動機、元胞自動機,是一個二維矩陣世界的零玩家遊戲。
  • 又有撕逼遊戲玩啦!:《Stick Fight: The Game》
    )登陸了Steam平臺,意思就是,又有多人遊戲可以玩啦!類似的多人對抗小遊戲也玩過一些,比如《Move or Die》、《Speed Runners》、《Gang Beasts》,但是這些遊戲都不如現在這款遊戲帶給我的歡樂更多。
  • 為程式設計師們專門設計的代碼遊戲,你玩過幾個?
    忙完了一天的工作後,程式設計師都想休息一下。休息的時候,玩遊戲是最好的放鬆方式。
  • 開發獨立遊戲的程式設計師遊戲美術解決方案
    摘要:開發獨立遊戲的程式設計師遊戲美術解決方案
  • 15 個邊玩遊戲邊學編程的網站
    10、Ruby QuizRuby Quiz 是一個面向 Ruby 程式設計師的每周編程挑戰項目,目前有 156 個測驗項目。11、Git-GameGit-game 是一個基於終端的遊戲,它用來教授 git 中的那些非常酷的功能。
  • 沉迷遊戲自學編程,創建遊戲帝國,卻黯然退場的「鬼才程式設計師」
    )約翰·羅梅洛(John Romero)是著名的電子遊戲製作人,他靠著自學成才擁有了出色的編程能力,被稱作「鬼才程式設計師」。他開發的《德軍總部3D》遊戲開啟了FPS(First-person Shooting,第一人稱射擊遊戲)的新時代,被譽為「FPS之父」。同時,因為他設計的遊戲充滿血腥暴力的場景,而他本人也常以滿頭的長髮和張口就來的粗口形象示人,所以他一直是個充滿爭議的存在。
  • Linuxgame 站長說 Linux Game
    我個人不僅喜歡 Linux,也喜歡文學和哲學。Linux下的遊戲,你都通過什麼渠道獲取的呢?LinuxGame,了解下?我是 Linuxgame.cn 的現任站長 bart,今年22歲,大四剛畢業。我本人所學的專業是管理方面的。對於 Linux 的熱情完全是興趣使然,或者說我是一個技術宅。我個人不僅喜歡 Linux,也喜歡文學和哲學。
  • 【遊戲研究社 | 評測】《程式設計師升職記》
    看到遊戲名的那一刻,有的同學可能就要抗議了:「不是吧阿sir?在學校已經是天天掉頭髮了,在遊戲裡還要當程式設計師?」乍一看遊戲名,你可能會認為這是一款偏模擬經營類的遊戲,實際上這是一款以圖形化編程為核心的益智解密遊戲。程式設計師升職記是一家小眾的獨立遊戲公司——Tomorrow Corporatio旗下的遊戲。